# 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: ```json "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 ` 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 ```json "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, apps` — **no `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.