# 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 <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

```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.
