# CAMPAIGN 12 — the class sweep (2026-08-08, unattended)

**Look for the siblings of everything this arc found, by class rather than by feature.** Nothing was
fixed, nothing was deployed beyond Part 1's golden, and no machine was touched beyond the bake VM,
which was torn down.

Baselines re-read on arrival, all three clean and equal to `origin/main`:

| Component | Version | commit |
|---|---|---|
| `felhom-controller` | released **v0.208.0** | `37b5ba08a704` |
| `felhom-agent` | v0.127.0 | `703db166e762` |
| hub | v0.98.0 deployed | `59527d00f9c4` |
| `app-catalog-felhom.eu` | — | `459766c` |

Highest register ID on arrival **R-255**; re-checked before minting, unmoved. This session opens
**R-256 … R-263**.

---

## 1. The bake — and the one approval awaiting the operator

**Golden 0.208.0 is baked, published and round-trip verified. It is NOT vouched.** Full evidence:
`documentation/tests/golden-0.208.0-2026-08-08/` (README + the 324-line `bake.log`).

| | |
|---|---|
| version | **0.208.0** (derived from the controller image, which is a required argument) |
| size | **656 150 362 B** |
| sha256 | **`ba668f59f063511dda122a5b05900f1350573cdc9619980872f87a58755ffb82`** |
| round trip | downloaded back from Gitea, hashed independently on DooPlex — **size and sha identical** |
| **the delivered artifact's own claim** | `./etc/felhom-controller-image` read **out of the downloaded archive** → `gitea.dooplex.hu/admin/felhom-controller:0.208.0` |
| fourth confirmation | the sha the hub's `/configuration` dropdown offers for 0.208.0 matches the bake exactly |

**Acceptance markers, each grepped verbatim against the real log:** `docker OK (overlay2; data-root
/var/lib/docker)` ×1 · `including mount point` ×2 (rootfs **and mp0**, no mp1) · `upload OK (HTTP 201)`
×1 · `excluding` ×0 · `FATAL` ×0 · `mp1` ×0. `Result=success`, `ExecMainStatus=0`.

**Hygiene.** The token was copied **file → file** and read from a `0600` file **inside** the VM by a
runner script; `systemctl show golden-bake -p Environment -p ExecStart | grep -c -F <token>` → **0**.
The committed log greps **0** for the literal token, **and the grep was shown to work first** — the
same grep on a copy with the token appended returned **1**, then the copy was shredded.

**404 pre-gate with a control:** `…/0.207.0/golden.tar.zst` → **200** (the URL shape is right),
`…/0.208.0/golden.tar.zst` → **404** (nothing to overwrite).

**Teardown:** `pct destroy 9100 --purge`, `pct list` empty; token/runner/script/log `shred -u`'d and
`/root` residue clean; `poweroff`; qemu confirmed gone via `ps -eo comm | grep -c qemu-system-x86` → 0
(never `pgrep -f`, which self-matches); **`qemu-img snapshot -a virgin` restored** and the snapshot
list re-read.

### ⚠ THE ONE THING ASKED OF THE OPERATOR

**Vouch golden 0.208.0** — hub → Configuration → Day-0 artifacts → Golden `0.208.0` → Save.

| field | now | after | why |
|---|---|---|---|
| `golden_version` | 0.207.0 | **0.208.0** | **the only field that moves** |
| `agent_version` | 0.127.0 | 0.127.0 | already ≥ `MinAgent 0.127.0` — read from the controller CHANGELOG header, checked not skipped |
| `min_agent` | 0.127.0 | 0.127.0 | equals the declared `MinAgent`; **not above `agent_version`** (the R-216 shape hub v0.97.0 holds) |

**Vouching is reversible** — re-select 0.207.0 and Save. The 0.207.0 package was not deleted (the
publish step's pre-delete targets only its own version), so a rollback is a form submission.

**Until that Save, a machine installed tonight still receives controller 0.207.0.**

---

## 2. What golden is vouched, and the currency gate's state

- **Vouched at the time of writing: golden 0.207.0**, agent 0.127.0, `min_agent` 0.127.0,
  `min_controller_version` 0.156.0 — read live from the hub, not from a document.
- `golden_currency_gate.py` was **RED on arrival** (`controller v0.208.0 is released and NO golden
  carries it`) exactly as §1 of the task predicted, and is **GREEN after the bake**.
- **No `--no-verify` was needed anywhere in this session.**

---

## 3. Per class — new instances, the control, the method, and its blind spots

### C1 — a secret in a response body

**New instances: 0** on the surface the class names (a response body a customer can fetch).

**Control — the shipped gate re-finds 2 of the 3 known instances, measured not assumed.** The three
pre-fix templates were restored from history (`8dbbc98^`) into a scratch copy of the controller and
`scripts/secret_in_markup_gate.py` was run against them. It convicted
`settings_security.html:408`, `:430` and `app_info.html:185`, and **did not convict**
`deploy.html:482` (`value="{{$val}}"`) — R-255's stated blind spot, now verified by replay rather
than by reading its docstring. The gate passes clean over today's 36 templates.

**Method (the new half).** The shipped gate is name-based on the TEMPLATE side. This sweep is its
mirror on the GO side: every assignment into template data, a JSON response map or a render struct
whose **right-hand side** names a secret, regardless of what the **key** is called — the shape the
gate cannot see. 27 sites matched; 5 under a key that does not itself announce a secret.

All 5 resolve to non-findings, and the reasoning is recorded in §6.

**Blind spots.** One hop only — a secret laundered through a neutrally-named local two or more hops
from its source is invisible. Struct-literal page data is covered only in the field-assignment form.
A secret fetched inside a helper the handler calls is invisible. And the whole sweep reasons about
**source**, never about rendered bytes — the runtime body assertion remains the only check that
catches all three known shapes, and it still covers 4 of 27 pages.

### C2 — a refusal that names no route

**New instances: 2 (R-256, R-257).**

**Control.** The known instances (R-252, R-253) were re-read in their pre-fix form at `8dbbc98^` and
in today's tree. The method distinguishes them correctly: the pre-fix strings state a reason and stop
(*"nincs mentés felhasználói adattal"*), and today's `backups_restore.html:79` names the route —
*„Meghajtók", „Meglévő meghajtó csatolása". Utána gyere vissza ide.* **2 of 2 re-found, as fixed.**

**Method + what was SAMPLED.** 221 distinct customer-visible refusal strings exist across the
controller's production code. **This was NOT swept exhaustively.** The sample is the three files on
the surface where both known instances lived — `recovery_handlers.go`, `offbox_handlers.go`,
`restore_wizard.go` — yielding 19 distinct refusals, all read by hand. **The other ~202 strings were
not examined.**

**Blind spots.** Refusals composed at render time from template fragments are invisible to a
string-literal grep; so are refusals whose text lives in the HTML rather than in Go. "Names a route"
is a judgement, not a predicate — two readers could disagree on the borderline cases, and three of
the 19 were called borderline and left unfiled.

### C3 — a success verdict over an incomplete state

**New instances: 1 (R-258). This class was swept COMPLETELY, not sampled.**

**Method.** Every assignment of a success-valued status in the controller's production code:
**9 sites**, all read. That is the whole population, which is why this is the one class where
"no others exist" is a claim the method can support.

**Control — 2 of 3 known instances re-found, and the third re-found as fixed.** `offbox.go:1040`
still carries R-240 verbatim: a configured box with nothing selected reports `LastStatus = "ok"` with
the warning *„Sikeres — nincs mentésre jelölt alkalmazás"*. R-234's fix is visible in the same block
(`missingUnprotected` → `incomplete`). R-203's mandatory-gap fix likewise.

**Blind spots.** It sees only *literal* success values assigned to a name matching
`Status|State|Verdict|Result`. A verdict expressed as a boolean, an enum constant, or the absence of
an error is invisible — and the controller does use booleans for verdicts elsewhere. It covers the
controller only; the hub and agent were not swept for this class.

### C4 — an absence reported as a fact

**New instances: 1 (R-259).**

**Control — the fix pattern re-found.** R-225's `StatsKnown` is exemplary and the sweep re-finds it
intact: on `backups_remote.html` every derived figure — size, snapshot count, quota text and the
**bar itself** — is behind `{{if .Offbox.StatsKnown}}`, with the honest alternative text
*„a pillanatképek száma még ismeretlen"*. Its own comment states the principle this class is about:
*"a 0%-wide bar over an unread store is a picture of emptiness, and a picture is a claim."*

**Method.** Enumerate the rendered counts/sizes/percentages in the 36 templates, then ask of each
whether a `*Known`-style companion exists and whether the producing Go call can fail into a zero.

**Blind spots.** Name-based on the companion flag — a three-state encoding done some other way (a
pointer, a sentinel, a separate error field) reads as "unguarded" and needs a human look; two
candidates were dismissed that way. Only the controller's templates were covered; the hub's operator
UI was not.

### C5 — a field one side sends and the other drops

**New instances: 1 register row (R-260) carrying a measured census of 8+ dropped fields.**
**This class was the highest-yield, as predicted.**

**Control — re-found.** The known instance is live: the hub's `store.EscrowStatus` carries
`Stale bool json:"escrow_stale,omitempty"` and the controller's `report.EscrowStatus` has no matching
field. **Correction to the task's framing, recorded rather than dropped: this is R-247 and it is
`READY`, not closed.** The live repo is the source (rule 7).

**Method — and the first one was wrong.** Attempt 1 paired structs across repos by json-tag-set
overlap. It false-positived badly, because the hub decodes the same controller report through
**several ad-hoc anonymous structs** — `offsite.state` and `offsite.abandon_purge_requested` looked
dropped and are in fact consumed at `api/handler.go:530` and `store/store.go:1490`. Attempt 2 is the
**tag-reachability test**: for every json tag emitted on a real wire, does that literal string occur
**anywhere** in the receiving repo? A tag that occurs nowhere cannot be decoded by any struct, named
or anonymous. That is definitive, and it is the version that generalises into a gate.

**Blind spots, several of them earned during the sweep.** (a) The emit sets initially included
*decode* structs — `hostReportPayload`, `applianceRegisterReq`, `WireDesiredState` — producing
nonsense in the reverse direction; those rows were struck. (b) The struct extractor **flattens
anonymous nested structs**, which produced a phantom asymmetry on `hostStorageTarget` (§6). (c) 29
generic tag names (`name`, `type`, `state`, `status`, …) were excluded by name because a repo-wide
string test is meaningless for them — **a genuine drop of a generically-named field would be missed.**
(d) It cannot tell a deliberate ignore from an accidental one; the hub's `hostReportPayload` documents
that it ignores several collections on purpose, and those were not counted.

### C6 — built and never wired

**New instances: 1 (R-261). The more valuable output is the method result, not the list.**

**Control — and the standard tool FAILS it.** `golang.org/x/tools/cmd/deadcode` was run against all
three binaries and **re-found neither** known instance. A **planted probe** measured why: an
unreachable exported *function* added to the hub's store package is reported; an unreachable exported
*method* on `*Store`, in the same file, is **not**. RTA treats the method set of a dynamically-used
type as potentially reachable. **Both known instances are methods** — `(*Store).MarkEscrowStale` and
`(*RestoreTestState).Snapshot`, both confirmed still callerless today — which is exactly the shape it
cannot see.

A method-aware textual sweep was then written. It re-finds **1 of 2** (`MarkEscrowStale`); it misses
`RestoreTestState.Snapshot` because a `.Snapshot()` call on a *different* type in
`guestnet/report.go` satisfies its name-based test. **So: 1 of 2, and stated as 1 of 2.**

**Findings after triage: small, and mostly inert — which is itself the result.** 137 raw candidates →
31 whose doc comment claims a consumer → after removing method-values-as-callbacks
(`PBSDRAutoProvision`), cross-repo name reuse (`DebugLogs`, `AppStopGuard`) and template dispatch,
five genuinely have only their own declaration. Four of those five are **inert**: the data reaches its
stated consumer by another route. `OffboxOrphanedRenamedTo` says *"for the card copy"* and has no
caller — **but R-228 is genuinely closed**, because the card reads the settings field
`.Offbox.OrphanedRenamedTo` directly in `backups_remote.html:80`. Same for `OffboxEscrowState`.
**An unwired accessor whose data reaches its consumer another way is dead code, not a defect**, and
ranking it as one would inflate this class.

The one that is not inert is R-261.

**Blind spots.** Go **templates** call methods with no `.Name(` syntax; method **values** passed as
callbacks likewise; reflection and struct-literal function fields likewise. Same-name methods on
different types are conflated, which makes the sweep conservative — it under-reports and never
over-reports. A method used only inside its own file counts as used, so whole-subsystem orphans are
invisible.

### C7 — a comment asserting an invariant the code does not hold

**New instances: 2 (R-262, R-263), both found while sweeping other classes.**

**Method + what was SAMPLED — this is the class where the honesty matters most.** There are **4092**
comments across the three repos matching the invariant vocabulary (`never` / `always` / `must not` /
`cannot` / `is the only` / `guarantees` / `impossible`) — 2652 in production code and 1440 in tests.
**Exhaustive is not possible in one session and was not attempted.** The sample is the **60 production
comments containing "is the only"**, chosen because a uniqueness claim is the one form that is
*mechanically falsifiable* by a grep. Of those 60, the safety-bearing ones were tested by hand.
**The other ~2592 production comments and all 1440 test comments were not examined** — and the task's
own instruction to check the tests' claims is therefore **only partly discharged**: the two instances
below are production comments.

**Control.** The nine known instances are the repo's own table (in the `felhom-testing` skill) and are
closed; the sample re-finds the *pattern* rather than the instances, so **C7 has the weakest control
of the seven and should be read that way.**

Both new instances are stated in the register. The sharper one, R-262, is the campaign in miniature:
a comment claiming a contract is mirrored *field-for-field* and *guarded by a key-set test*, where the
contract is two fields short **and the fixture the test reads omits the same two fields** — the
instrument is blind in exactly the place the defect is.

---

## 4. Which classes can be gated — the ranking

Ranked by what a gate would be worth, using this campaign's own instance counts as the evidence.

| rank | class | gateable? | cost | what it would MISS |
|---|---|---|---|---|
| **1** | **C5** | **Yes — cheaply and definitively** | ~150 lines of Python, no network, `--fast`-eligible | generically-named tags; a deliberate ignore looks like an accident, so it needs an allowlist-with-reasons like the C1 gate has |
| **2** | **C3** | **Yes** | small — the whole population is 9 sites | verdicts expressed as booleans or as the absence of an error |
| **3** | **C4** | **Partly** | medium | the companion-flag convention must be adopted deliberately; a gate can enforce "every rendered figure has a `*Known`" only once that is the house style |
| **4** | **C1** | **Already gated, partly** | completing it is the expensive one | see below |
| **5** | **C7** | **Narrowly** | small for the narrow form | only uniqueness claims are mechanical; the other ~97% of the vocabulary is not |
| **6** | **C2** | **No, not mechanically** | — | "names a route" is a judgement; a gate could only enforce a *convention* |
| **7** | **C6** | **No — and this is the finding** | — | the off-the-shelf tool is blind to the project's own shape, measured |

**C5 is the clear first.** The tag-reachability test is a string comparison over two repos' source: it
convicts definitively (a tag occurring nowhere in the receiver cannot be decoded), needs no network,
and would have caught the escrow_stale instance, the `operator_key_configured` instance and every
other row in R-260's census on the commit that introduced them. **It is the cheapest high-value gate
available to this project.** Its one design requirement is an allowlist with stated reasons, because
the hub deliberately ignores some collections.

**C6 does not hold, and saying so is the point.** The class *looks* the most mechanical of the seven
and it is the one where the standard tool measurably fails. Any gate here would have to be written
from scratch, would be conservative by construction, and — on this campaign's evidence — would spend
most of its output on inert dead accessors. **Recommendation: do not gate C6. Gate C5 instead, which
catches a strict subset of the same "the answer was available and discarded" family with none of the
ambiguity.**

**C1's existing gate, stated honestly.** `secret_in_markup_gate.py` covers all 36 templates on the
name-based check and is **blind to the neutral-page-data-key shape** — measured this session by replay,
not merely quoted: it convicts 2 of the 3 known instances and not the third. The runtime body
assertion, which catches all three, covers **4 of 27 pages**. Completing it means constructing each
remaining page's data in a test — the cost is per-page and real, and it is the honest reason it has
not been done. The Go-side mirror written for this campaign is a cheap partial third net and is **not
recommended as a gate on its own**: it produced 27 candidates and 0 findings, which is a bad
signal-to-noise ratio to put in front of every push.

**Nothing was built.** These are candidates recorded in `backlog/ROADMAP.md`.

---

## 5. Harness faults, separated from product faults

**Harness, not product:**

1. **My first C5 method was wrong** and would have produced ~10 false findings had it been believed —
   struct-pair diffing cannot see a receiver that decodes through ad-hoc anonymous structs. Replaced,
   not patched over.
2. **My struct extractor flattens anonymous nested structs**, which manufactured a phantom
   `hostStorageTarget` asymmetry (§6).
3. **A `grep -v host_oob.go` excluded `monitor/host_oob.go` too**, and I briefly believed
   `GetHostOOBStates` had no caller. Caught by checking before writing it down. Excluding by basename
   is a trap worth naming.
4. **`deadcode` failing its control** is a harness fault in the sense that matters here — but it is
   reported as a *result* in §3/C6, because "the standard tool cannot see this project's shape" is
   exactly what Part 4 was asked to establish.

**Session hygiene, recorded:** this session did **not** run inside `tmux`, contrary to the workspace
`CLAUDE.md`. Nothing was lost, but the whole unattended run was one dropped connection from dying.

---

## 6. Suspicions investigated and DISPROVED

A campaign reporting only what it found is half a campaign.

1. **`offsite.state` / `offsite.abandon_purge_requested` are dropped by the hub.** **DISPROVED.**
   The hub's `monitor.offsiteReport` has neither, but both are consumed through separate ad-hoc
   structs — `api/handler.go:530` and `store/store.go:1490`. This is what killed method 1.
2. **A restore-test mount-parity mismatch is invisible to the hub.** **DISPROVED, and it mattered.**
   `mount_parity` and `mount_inventory` genuinely are dropped, but a mismatch sets `res.Err` and
   returns **before** `res.Pass = true` (`reconcile/restoretest.go:277-283`), so the *verdict* travels
   as `pass:false` with the delta in the error string. What is lost is the *depth* of a pass, not the
   failure. R-260 states it that way rather than the way it first looked.
3. **`hostStorageTarget` does not mirror the agent field-for-field.** **DISPROVED.** The 11 "missing"
   SMART fields are nested under `smart` on both sides; my extractor flattened the hub's anonymous
   inner struct. That half of the comment is TRUE — which is what makes the `hostRestoreTest` half
   (R-262) a real finding rather than a sweeping accusation.
4. **`GetHostOOBStates` has no caller (a C6 instance).** **DISPROVED** — `monitor/host_oob.go:40,77`.
   My own grep excluded it.
5. **`Validation.TableCount` reports 0 tables over a validation that could not run (a C4 instance).**
   **DISPROVED.** `backups_apps.html:74-82` is correctly three-stated: `Valid` → the count,
   `Error` → *„Hiba"*, neither → *„Az érvényesítés nem futott le"*. A good example of the class handled
   well.
6. **The controller leaks a NAS password into a response body (a C1 instance).** **DISPROVED.**
   `netstorage_handlers.go:163` puts it in a **request to the agent**, documented in place as never
   persisted and never in the job status.
7. **`OffboxOrphanedRenamedTo` being callerless means R-228 regressed.** **DISPROVED** — the card reads
   the settings field directly (`backups_remote.html:80`). R-228 is genuinely closed.

---

## 7. What was SAMPLED rather than swept, exactly

| class | swept completely | sampled | NOT examined |
|---|---|---|---|
| C1 | all 36 templates (shipped gate); 27 Go-side candidate sites | — | the 23 pages with no runtime body assertion |
| C2 | — | 3 files / 19 refusals on the recovery-restore-offbox surface | **~202 of 221 refusal strings**; all refusals composed in HTML |
| C3 | **yes — all 9 success-status sites in the controller** | — | the hub and the agent entirely; boolean/enum verdicts |
| C4 | the 36 controller templates' rendered figures | — | the hub operator UI |
| C5 | **yes — all 4 wire directions, 465 emitted tags** | — | 29 generically-named tags, excluded by name |
| C6 | all 3 binaries via `deadcode`; 1323 exported methods textually | 31 of 137 candidates triaged by hand | the 106 candidates whose doc comment claims no consumer |
| C7 | — | **60 of 2652** production invariant comments ("is the only" only) | **~2592 production + all 1440 test comments** |

---

## 8. R-242's untouched half — what would catch a skipped vouch earliest

**Recorded, not built.** This bake demonstrated the gap rather than arguing it:
`golden_currency_gate.py` **flipped from red to green the moment the evidence DIRECTORY existed** —
before the round-trip download had finished, and with no vouch anywhere near it. Its positive signal
is "a directory named after the version is present in the repo".

**Earliest catch, in order of how early it fires:**

1. **Earliest and cheapest — make the vouch the thing that writes the evidence.** The gate cannot read
   the hub (its docstring records why: a network-reading gate is not `--fast`, so it would run in
   neither the pre-push hook nor CI). But the *bake session* can: after the operator saves, re-read
   `/configuration` and write the observed `golden_version` into the evidence README as a machine-
   readable line. The gate then requires that line, not merely the directory. It still cannot detect a
   lie, but it detects the **forgetting**, which is the actual failure mode the gate was built for.
2. **Loudest — a hub-side check, not a repo-side one.** The hub already knows both numbers: the
   vouched `golden_version` and the newest controller the fleet reports. A daily monitor raising
   `golden_behind_release` would catch a skipped vouch **and** a vouch that silently rolled back, which
   nothing in git can ever see. It fires within a day rather than at the next push.
3. **Weakest — a checklist item.** R-242 already was a rule without a mechanism, and it recurred the
   next day. Recording it again is not a fix.

**Recommendation: (1) now, (2) when the hub is next touched.** (1) closes the recurrence that has cost
two releases in two days; (2) closes the class.

---

## 9. Register rows opened, grouped by class

Highest ID **moved: R-255 → R-263.**

| class | rows |
|---|---|
| C1 | *none — no new instance* |
| C2 | **R-256**, **R-257** |
| C3 | **R-258** |
| C4 | **R-259** |
| C5 | **R-260** |
| C6 | **R-261** |
| C7 | **R-262**, **R-263** |

Gating candidates from Part 4 are items in `backlog/ROADMAP.md`, not register rows.

---

## 10. What did not run, and why

- **The vouch.** Deliberate — §3 of the task halts for it, and the campaign does not depend on it.
- **Any fix.** Deliberate — §0 forbids it; a campaign that fixes as it goes measures a moving target.
- **C7 exhaustively.** Not possible in one session: 4092 comments. Sampled and declared.
- **C2 exhaustively.** 221 refusals; 19 read. Sampled and declared.
- **C3/C4 on the hub and agent.** Only the controller was swept. Not attempted, not a claim.
- **The tests' invariant claims (C7's second half).** 1440 test comments; none examined. The task asked
  for this explicitly and it is **owed**, not done.
- **Live validation of any finding.** Every finding in this report is from source reading. **None was
  reproduced on a live box** — deliberately, since §6 rule 2 permits no machine beyond the bake VM.
  R-258 and R-259 are the two most worth confirming live before they are fixed.

---

## 11. Observations — noticed and NOT acted on

1. **The `felhom-golden` package list holds 14 goldens** back to 0.136.0, none ever pruned. Not a
   problem today (a rollback needs the old package to exist), but nothing decides when one may go.
2. **The hub's `host-report.golden.json` fixture contains `cpu_temp_c` and `loadavg`**, which no hub
   struct decodes. A fixture carrying keys the receiver cannot read is a quiet way for a "contract
   test" to look broader than it is — the same shape as R-262, one level down.
3. **`readDiskUsage` (R-259's site) is one of several collectors that return silently on error**,
   leaving the caller's zero value. The class may be wider than the one rendered figure this campaign
   measured; only the dashboard's system-disk meter was traced to a customer-visible surface.
4. **The re-enroll response echoes `new_api_key` back to the caller that sent it**
   (`api/dr.go:148`). Not a leak — the box already holds it — but it puts a live credential in any
   transcript of that response, and the endpoint is one an operator drives by hand.
5. **The hub operator UI renders `RetrievalPassword` into page data** (`web/configs.go:672`, `:1262`).
   Outside C1 as defined (operator-only, not a customer-fetchable page), so not filed — but it is the
   R-249 shape on the other surface, and the operator UI has no equivalent of the controller's gate.
6. **This session did not run inside `tmux`**, contrary to the workspace `CLAUDE.md`.
