CAMPAIGN 12 — the class sweep: golden 0.208.0 baked (awaiting vouch), R-256..R-263 filed, gating ranked
gates / gates (push) Successful in 20s

Part 1. Golden 0.208.0 baked on the drill VM, published and ROUND-TRIP VERIFIED — 656 150 362 B,
sha256 ba668f59..5ffb82, and ./etc/felhom-controller-image read OUT of the downloaded archive says
felhom-controller:0.208.0. Acceptance markers all green (overlay2 x1, mount points x2 rootfs+mp0,
upload HTTP 201 x1, excluding/FATAL/mp1 x0), Result=success. Token file->file, read inside the VM;
systemctl show grep = 0; committed-log grep = 0 WITH a control proving the grep works. Bake VM
destroyed, drill disk restored to virgin. NOT VOUCHED — the campaign halts there deliberately.

golden_currency_gate.py was correctly RED on arrival and is green after the bake. No --no-verify
was needed anywhere in this session.

Parts 2-4. Seven defect classes swept for siblings by class rather than by feature. Analysis only:
no product code, nothing deployed, no machine touched beyond the bake VM.

Eight new rows R-256..R-263 (ceiling moved from R-255), grouped by class in OPEN-ITEMS.md. C1
produced no new instance and has no row. The sharpest is R-260: the agent reports
operator_key_configured every heartbeat, the hub has no field for it, so the check that answers
"can the operator get into this box" returns ok for a box with no operator key installed.

Every class states whether its method re-found the known instances, because a method that cannot
re-find them has not been shown to work: C1 2/3 (verified by replaying the pre-fix templates),
C2 2/2, C3 2/3 + 1 as fixed, C4 fix-pattern re-found, C5 re-found, C6 deadcode 0/2 and bespoke 1/2,
C7 weakest and said so. Blind spots stated per class; seven suspicions investigated and DISPROVED,
including two of my own methods.

Part 4's ranking is in ROADMAP.md as G-1..G-8. Gate C5 (cross-repo tag reachability — cheap,
--fast-eligible, would have caught every R-260 instance on the introducing commit). Do NOT gate C6:
golang.org/x/tools/cmd/deadcode was measured against a PLANTED probe and is blind to unreachable
METHODS on widely-used types, which is exactly the shape both known instances have.

R-242's untouched half is recorded, not built: this bake demonstrated it, the currency gate flipping
green the moment the evidence DIRECTORY existed, before the round trip finished and with no vouch
near it.

Correction the campaign owed its own brief: escrow_stale was described as closed; it is R-247 and
READY. The live repo is the source.

Sampled rather than swept, exactly: C7 60 of 2652 production invariant comments and NONE of the 1440
test comments (that half is owed); C2 19 of 221 refusals; C3/C4 controller only. No finding was
reproduced live. STATUS.md is 100 lines against its 93-line one screen.
This commit is contained in:
2026-08-07 23:07:42 +02:00
parent 59527d00f9
commit b7fb2117ae
7 changed files with 1038 additions and 35 deletions
+18
View File
@@ -128,6 +128,24 @@
| R-109 | ~~The DR recipe records no backup target~~ | XS | **SHIPPED** — agent v0.118.1 + hub v0.83.0, 2026-07-30 | `backup_target` resolves from the PRIMARY tier of `cfg.Backup.BackupTiers()` (the scheduler's own function) + the mountpoint that separates `/mnt/hdd_1` from `/var/lib/vz`; unresolvable is recorded as unresolvable, never defaulted. Required an unscoped HUB half: `AssembleDRRecipe` allow-lists top-level keys → **R-122**, a fourth recipe-completeness defect that had already shipped (`offsite_restic` dropped for the feature's whole life). `audits/R106-R109-recipe-completeness-2026-07-30.md`; flipped `07` §4 |
| E-2 | **Drive-role machinery around the moved vzdump target.** The 2026-07-28 runbook proved the architecture change by hand on both demo boxes; this is the machinery: a **backup-target role** on `StoragePath` beside `Schedulable`/`IsDefault`/`Kind`; **assignment in the storage wizard** (suggest by attribute, refuse the absurd, never decide by transport or `removable` — on the reference hardware demo-felhom's target IS a USB HDD and BOTH drives report `removable=0`); **unassigned drives do nothing automatically**; **stickiness** (never silently retarget); `felhom-host-install.sh` creating the target with `--is_mountpoint 1` **and** issuing the `FelhomAgentStore` ACL; **absent-target policy**; **retention/space accounting** on a drive the customer shares; the honest **single-drive label**; remaining fleet migration | M | READY — 2026-07-28 | Full scope + rationale in `runbooks/RUNBOOK-vzdump-target-move-2026-07-29.md` §7. Two traps already paid for live: the storage `path` must BE the mountpoint or the agent reports the target `disconnected` forever (`internal/storage/observe.go:321`), and the per-storage `FelhomAgentStore` grant is mandatory or every backup 403s. Absent-drive behaviour today is **fail-loudly, no silent retarget** (`is_mountpoint 1` proven live) — which is NOT the intended fall-back-and-alarm design. Flips: matrix row 4 |
## Gating candidates — Campaign 12, Part 4 (2026-08-08)
**Ranked by what a gate would be worth, using Campaign 12's own instance counts as the evidence.**
Nothing here was built; §4 of `audits/CAMPAIGN-12-class-sweep-2026-08-08.md` carries the reasoning and
the measurements. The recurring lesson these rank against: **a pattern found three times is not closed
by looking a fourth time.**
| Rank | Item | Size | Status | Notes |
|----|------|------|--------|-------|
| **G-1** | **Gate C5 — the cross-repo tag-reachability check.** For every json tag emitted on a real wire (agent→hub, controller→hub, agent→controller, hub→both), assert that the literal tag string occurs somewhere in the receiving repo. A tag occurring nowhere cannot be decoded by any struct, named **or anonymous** — which is what makes this definitive where struct-pair diffing is not. | S (~150 lines Python) | **candidate — recommended first** | **The cheapest high-value gate available to this project.** No network, no container runtime → `--fast`-eligible, so it runs in BOTH the pre-push hook and CI (the constraint that forced `golden_currency_gate.py` to check the bake rather than the vouch). It would have caught `escrow_stale` (R-247), `operator_key_configured` and every other row of R-260's census **on the commit that introduced them**. **Design requirement, not optional:** an allowlist with stated reasons, in the shape `secret_in_markup_gate.py` already uses — the hub deliberately ignores some collections and says so. **Known miss:** generically-named tags (`name`, `state`, `status`, …), where a repo-wide string test is meaningless; 29 were excluded by name in the sweep. → R-260 |
| **G-2** | **Gate C3 — a success verdict may not be set where an incompleteness signal is in scope.** Assert that every literal success-status assignment either has no gap/skip/missing signal available at that point, or consults it. | S | candidate | The whole population in the controller is **9 sites** — Campaign 12 read all of them, which is why this class is the one where "no others exist" is supportable. Small enough to gate by enumeration rather than by inference. **Known miss:** verdicts expressed as booleans, enum constants, or the absence of an error — and the controller does use those elsewhere. Instances: R-240 (open), R-258 (new). |
| **G-3** | **Gate C4 — every rendered count/size/percentage needs a `*Known` companion.** | M | candidate — **needs a convention decision first** | R-225's `StatsKnown` is the house pattern and it is exemplary; a gate can only enforce it once it is *the* house style. Today three-state is also encoded with pointers and with separate error fields, both legitimate, and a name-based gate reads those as unguarded. **The decision owed is "how does this codebase say 'we could not look'", not "should we gate it".** Instance: R-259. |
| **G-4** | **Complete C1's runtime body assertion — 4 of 27 pages today.** | L | candidate — the expensive one, and honestly so | `secret_in_markup_gate.py` covers all 36 templates on the NAME-based check and is **blind to a secret under a neutral page-data key** — verified 2026-08-08 by replaying the three pre-fix templates through it: it convicts 2 of 3 and not the third. The runtime assertion catches all three; extending it means constructing each remaining page's data in a test, which is a per-page cost and is the real reason it has not been done. **Do NOT adopt the Go-side mirror Campaign 12 wrote as a gate on its own** — 27 candidates, 0 findings is bad signal-to-noise in front of every push. → R-255 |
| **G-5** | **Gate C7, narrowly — uniqueness claims only.** A comment saying "X is the ONLY writer/place/caller of Y" is mechanically falsifiable; assert it. | S | candidate — narrow by construction | Covers ~60 of the 2652 production invariant comments. The other ~97% of the vocabulary (`never`, `always`, `must not`, `guarantees`) is not mechanical and a gate must not pretend otherwise. Instance: R-263. |
| **G-6** | **C2 — NOT mechanically gateable.** | — | **recorded as a no** | "Names a route" is a judgement, not a predicate. The most a check could do is enforce a *convention* (e.g. every customer-visible refusal string ends in an imperative clause), which would be gamed rather than followed. Better served by the UI-copy review the `felhom-ui-design` skill already governs. Instances: R-256, R-257. |
| **G-7** | **C6 — NOT gateable, and the measurement is the finding.** | — | **recorded as a no, with evidence** | The class *looks* the most mechanical of the seven and is the one where the off-the-shelf tool measurably fails: `golang.org/x/tools/cmd/deadcode` re-found **neither** known instance, and a **planted probe** showed why — it reports an unreachable exported FUNCTION but not an unreachable exported METHOD on a widely-used type, and both known instances are methods. A bespoke gate would be conservative by construction and would spend most of its output on **inert dead accessors** (4 of the 5 residual candidates were inert). **Recommendation: gate C5 instead** — it catches a strict subset of the same "the answer was available and discarded" family with none of the ambiguity. → R-261 |
| **G-8** | **R-242's untouched half — catch a SKIPPED VOUCH.** | S then M | candidate — **the one with a live recurrence** | Measured during Campaign 12's own bake: `golden_currency_gate.py` **flipped red→green the moment the evidence DIRECTORY existed**, before the round-trip download finished and with no vouch near it. **(a) Cheapest, now:** have the bake session re-read `/configuration` after the operator's Save and write the observed `golden_version` into the evidence README as a machine-readable line; the gate then requires that line rather than the directory. Detects the FORGETTING, which is the actual failure mode. **(b) Loudest, when the hub is next touched:** a hub-side daily check comparing the vouched golden against the newest controller the fleet reports — it fires within a day and catches a silent ROLLBACK too, which nothing in git can ever see. **(c) A checklist item is not a fix** — R-242 already was a rule without a mechanism and it recurred the next day. → R-242 |
## P3 — post-alpha
| ID | Item | Size | Status | Notes |