docs: R-166 SHIPPED — the desired/in-flight/observed split (D-b)
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
Controller v0.189.0 implements operator decision D-b. Docs only here; no hub change and no hub version bump. - architecture/02-controller-module-map.md §0a — NEW, and it is the S-1 contract: desired (app.yaml) / in-flight (own marker file) / observed (not persisted), with the rule that ties them — never derive one from another. Absent desired state means UNKNOWN, never "running". One file, one writer. D-b's binding safety rule quoted verbatim. - 00-capability-map.md — the boot-recovery row now rests on a recorded signal, with the three live flows from 9201. The interrupted-operation half is marked IMPLEMENTED, not PROVEN-LIVE: nobody killed the controller mid-backup on metal. - OPEN-ITEMS/ROADMAP — R-166 SHIPPED with both blocking facts and their answers; R-157 mechanism B CLOSED and A restated as the whole item; R-170 NEW (the drive-backed boot gate still infers a Stop from a container count). - STATUS.md — the "an app can stay switched off and nothing says so" line rewritten to what is actually left: timing. - CLAUDE.md — end-of-session checklist gains: confirm your own last push's CI run went green, BY RUN ID. The failure email is a push signal; this is the pull check. - CONTEXT.md S-12 — the rulings, and the two lessons worth carrying: a test that constructs the thing it should prove the caller constructs is hollow (its red-proof will say so), and a field-by-field struct rebuild in a save path is a defect on sight.
This commit is contained in:
+43
@@ -164,6 +164,49 @@ Five decisions were deliberately **left open for the operator** and are recorded
|
||||
stated**) · Hetzner as a single failure domain · and `local` vzdump sharing a physical device with
|
||||
the guest it backs up. Gaps minted the same session: **R-102 … R-108**.
|
||||
|
||||
**S-12 — D-b is BUILT (controller v0.189.0, 2026-08-02, R-166).** The desired/in-flight/observed
|
||||
split now exists; the S-1 contract lives in `architecture/02-controller-module-map.md` §0a.
|
||||
|
||||
**Both facts D-b said to establish first were established at source, and both changed the shape.**
|
||||
(a) *Does the crash-safe journal in the backup code already cover the in-flight case?* The pattern
|
||||
DID already exist — twice (`quiesce` marker+`Recover`, `migrate` journal+`RecoverMigration`) — and
|
||||
covered **none** of the app-data path: `DumpAppVolumesSafe` stopped and restarted an app with no
|
||||
marker, no journal and **not even a `defer`**. So the answer was neither "it exists, wire it" nor
|
||||
"build it": copy the proven shape into its own file. It was the fifth time the question was worth
|
||||
asking and the first time the answer was "the pattern, not the coverage". (b) *Is the SQLite store
|
||||
reachable?* Reachable and **deliberately not used** — `metrics.db` is optional by design (the
|
||||
controller runs with it absent), and operational state must not live in a store built to be dropped.
|
||||
|
||||
**The ruling that carried the design: absent means UNKNOWN, never "running".** Every `app.yaml` on
|
||||
every box predates the field, so absent is what the whole fleet reads on upgrade; reading it as
|
||||
running would have started every deliberately-stopped app on the first boot after the upgrade —
|
||||
fleet-wide, silently. Where intent is unknown the box keeps the OLD inference rather than inventing
|
||||
an answer. That is also why the backfill is **running-only**: "zero containers ⇒ stopped" is the
|
||||
defect itself, so an ambiguous app stays ambiguous until a customer presses a button.
|
||||
|
||||
**The other load-bearing ruling: `StartStack`/`StopStack` are NOT writers of intent.** A census found
|
||||
14 callers, of which exactly 2 are the customer. Recording intent in the primitive would make a
|
||||
nightly backup indistinguishable from the customer pressing Stop — the confusion being removed.
|
||||
|
||||
**Found on the way, and it would have silently eaten the feature: `SaveAppConfig` rebuilt `AppConfig`
|
||||
field-by-field.** That is the R-100 shape, which v0.181.0 shipped two live instances of. The literal
|
||||
named five fields, so the sixth would have been dropped on every save across nine call sites — a
|
||||
customer's Stop erased by the next unrelated `app.yaml` write. Copy-and-overlay is safe by
|
||||
construction; the failure mode is generic, so **treat any field-by-field struct rebuild in a save
|
||||
path as a defect on sight.** Measured and documented rather than assumed: `app.yaml` does NOT
|
||||
round-trip YAML keys the struct does not model.
|
||||
|
||||
**Closes R-157 mechanism B; mechanism A (the sweep observes ~5 s after start and never re-checks) is
|
||||
untouched and is now the whole of R-157** — and B's fix makes A cost more, since the sweep now has
|
||||
more it could legitimately recover. **New: R-170** — `shouldRecreateOnBoot`
|
||||
(`internal/web/intermediary.go:131`) still infers a Stop from `hasContainers`, i.e. the same defect
|
||||
one gate over for drive-backed apps. Left deliberately: the task scoped `bootrecon`, and two boot
|
||||
behaviour changes under one live validation is one too many.
|
||||
|
||||
**Live on 9201, three flows, each with a positive observable.** The interrupted-operation half is
|
||||
**IMPLEMENTED, not PROVEN-LIVE** — unit-proven and red-proofed, but nobody killed the controller
|
||||
mid-backup on real hardware; the capability map says so rather than rounding it up.
|
||||
|
||||
**S-5 — four operator decisions taken in discussion on 2026-08-02, recorded before anything is
|
||||
built.** They existed only in conversation, which is the condition the standing rules were written
|
||||
against. Labels are the ones used in the discussion (**D-a … D-d**) and are deliberately kept
|
||||
|
||||
Reference in New Issue
Block a user