Files
felhom.eu/REPORT-r106-r109-recipe-completeness.md
T
admin 70f84941d4 R-106/R-109 audit + registers: shipped at agent 0.118.1, plus R-125
Adds the full audit: Part 0's three answers, the pre/post recipe for both boxes,
the on-disk proof that `local` froze at the 2026-07-28 target move while
felhom-backup kept running, all seven red-proofs, and the three publish
observables.

R-125 filed: v0.118.0's R-106 half shipped INERT. Two tests ran the real
Collector.Collect() but both injected a fakeObserver, and the break was one layer
below in mergeConfig, which dropped the pbs namespace. The recipe still said
"root" — now with namespace_state "resolved" beside it, confident and wrong.
Caught by live validation, not by the green suite. Fixed in 0.118.1; filed for
the doctrine point that a production-path claim must name the seam it injects at.
2026-07-30 13:28:10 +02:00

8.0 KiB

REPORT — R-106 + R-109 (+ R-122): closing the recipe-completeness set (2026-07-30)

Non-overwritten sibling per CLAUDE.md:82-87 — the shared REPORT.md holds R-117 and is not touched.

Shipped: agent v0.118.0 → v0.118.1 (felhom-agent 1c8a67e, 6b5dade) + hub v0.83.0 (felhom.eu acfc2b7). Neither half is useful alone.

Read §3 first if you read nothing else: v0.118.0's R-106 half shipped INERT and live validation is what caught it — the recipe still said "root", now with namespace_state: resolved beside it. Full account in the audit §6, filed as R-125.

Part 0 — the answers, before the fix

0.1 Which items are actually open, and R-105/R-106's registration

OPEN-ITEMS.md calls itself "the single source of truth for open work" (:1), with ROADMAP.md keeping "the full history and reasoning" (:3-4).

item ROADMAP.md OPEN-ITEMS.md verdict
R-105 row, READY — 2026-07-28 (:108) absent open but UNREGISTERED
R-106 row, READY — 2026-07-28 (:109) absent open but UNREGISTERED
R-108 row (:111) row (:50) registered
R-109 row (:112) row (:61) registered

So R-109's own cell — "third recipe-completeness defect beside R-105/R-106" — was the only place in the register naming two open items. That is exactly the thread-loss the register exists to prevent, and it is itself a finding (filed R-123). Both now have rows.

The set this task closes is R-106 + R-109, matching the arc's stated definition of done (OPEN-ITEMS.md:14). R-105 is NOT in it and was not worked: it is M-sized and is about three hub-held DR records being {} (hosts.dr_record_json, host_escrow.directive_json, and the drives third — already traced and populated by the 2026-07-28 target move). Different fields, different owner, different size.

0.2 Where the recipe is generated — three producers, not two

half repo function
host (guests/pbs/drives/pve_storage) felhom-agent BuildDRRecipeHostHalf, internal/hub/dr_recipe.go:86
app (customer/apps/offsite_restic) felhom-controller controller/internal/report/dr_recipe.go
assembly + delivery felhom.eu/hub AssembleDRRecipe, internal/store/dr_recipe.go:104; served by handleDRRecipeDownload, internal/web/dr_recipe.go:14, route internal/web/server.go:439

R-109's "host-half" is therefore the agent, and the field must also pass the hub's allow-list — see §2.

0.3 What the namespace field actually contained — verified, and the brief was RIGHT

The eleven-session-old brief held up. Live, pre-fix, from the hub for both boxes:

"pbs": { "repo_id": "felhom-pbs", "namespace": "root", "latest_snapshot_id": "9201" }

against /etc/pve/storage.cfg on the same boxes:

pbs: felhom-pbs
        datastore felhom-offsite
        namespace demo-felhom          # demo-hp reads: namespace demo-hp

Traced to source: Snapshot.Namespace decodes ns (internal/pbs/client.go:97), which PBS does not echo per item once the list is namespace-scoped via ?ns= (:118-120) → always empty → ToHub normalises empty to "root" (internal/pbs/report.go:22-25) → latestPBSCoord writes it in.

The authority taken, and why: storage.cfg's namespace on the pbs storage. It is the same field vzdump --storage <pbs> makes PVE read, and the agent's own verify client is built from it (cmd/felhom-agent/main.go:1164). Deriving the recipe from anything else is how it drifts again.

1. R-109's ambiguity is real, in the boxes' own pre-fix recipe

"pve_storage": [
  { "name": "local-lvm",     "type": "lvmthin",   "content": "images,rootdir" },
  { "name": "felhom-backup", "type": "local-dir", "content": "backup" },
  { "name": "felhom-pbs",    "type": "pbs",       "content": "backup" },
  { "name": "local",         "type": "local",     "content": "backup,import,vztmpl,iso" }
]

No backup_target key anywhere. felhom-backup (live, /mnt/hdd_1) and local (/var/lib/vz, archives frozen 2026-07-28) are both content=backup dir storages; local is also the historically correct answer, which is what makes guessing it so easy.

2. R-122 — a fourth defect, found here, and it had already shipped

AssembleDRRecipe's hostHalfShape/appHalfShape are allow-lists dressed as forward-compat. The controller has emitted offsite_restic since fork-4 (controller/internal/report/dr_recipe.go:39-41, "so DR knows WHERE to recover from"); appHalfShape never listed the key. Verified both ways:

  • stored: dr_recipe.app_half_json carries it for all three real customers — peti-felhom, demo-felhom (u629488-sub1.your-storagebox.de:23/home/felhom-repo), demo-hp.
  • delivered: the downloaded recipe's top-level keys were recipe_version, customer, guests, pbs, drives, pve_storage, appsno offsite_restic.

So a restorer reading the recipe had no offsite location at all, for the whole life of the feature, with a green suite throughout — because the test fixture drAppHalf is hand-written and omits the field.

Deviation from the task's §7.10 ("Findings — filed as R-n, none fixed"), stated rather than absorbed: I fixed it. Reasons — (a) Part 0 authorises working the real set if it differs; (b) it is the same symptom the task is named for (the recipe is incomplete), and the worst instance, a whole section missing; (c) it is in the same two structs R-109 forced me to edit, and leaving one of three known keys off a drop-list I was already correcting would be indefensible. It is filed as R-122 with a SHIPPED disposition.

3. The before/after recipe — both boxes, quoted

demo-felhom  BEFORE  "namespace":"root"           backup_target absent   offsite_restic absent
             AFTER   "namespace":"demo-felhom"    backup_target {resolved, felhom-backup, /mnt/hdd_1}
                                                  offsite_restic {u629488-sub1…}
demo-hp      BEFORE  "namespace":"root"           backup_target absent   offsite_restic absent
             AFTER   "namespace":"demo-hp"        backup_target {resolved, felhom-backup, /mnt/nvme-1tb}
                                                  offsite_restic {u629488-sub3…}

The two boxes DISAGREEING is the point — nothing is hardcoded. And the ambiguity was not theoretical: on both boxes felhom-backup holds an archive from 07-30 04:36 while local stops at 07-28 17:5x, frozen at the target-move date. The recipe now names the live one.

Full evidence, all seven red-proofs and the publish observables: documentation/audits/R106-R109-recipe-completeness-2026-07-30.md.

4. Findings filed (none of them fixed except R-122, see §2)

id finding
R-122 AssembleDRRecipe allow-list dropped offsite_restic for the feature's whole life — FIXED here, hub v0.83.0
R-123 R-105 and R-106 were READY in ROADMAP.md with no OPEN-ITEMS.md row — referenced only inside R-109's prose. Registered here
R-125 v0.118.0 shipped an inert R-106 because the "production path" test injected fakeObserver one layer below the break — FIXED in v0.118.1; filed for the doctrine point (name the seam you inject at)
R-124 The recipe spells PBS's root namespace "root", but the PBS API spells it "" and there is no namespace literally named root — a restorer pasting it into pct restore --ns root would fail. Pre-existing wire convention, deliberately unchanged; documented at PBSRootNamespace

5. Not done, and why

  • R-105, R-108, D5 — out of scope by the task's §6. R-108 blocks D5; starting either would leave both half-done.
  • The backup machinery — untouched. This corrects the record, not the doing.
  • sess-f (0.116.0) and drill-r50 (0.113.0) were not upgraded — neither was named as a venue, and drill-r50 is fenced by the task's §6.
  • R-124 not fixed — changing the wire's spelling of the root namespace mid-R-106 would shift the field's meaning during the fix meant to make it trustworthy.