register: file R-156..R-164 in one pass, ranked; and record what mp1 is actually for

Nine rows into OPEN-ITEMS.md and ROADMAP.md, matching each file's column shape.
R-156 and R-157 had lived only in audit documents - the identical "minted in a
spike doc and never carried across" failure the register already records for
R-153/R-154/R-155, caught by the catalog sweep's own section 8.0 while it was
happening. R-158 was minted by a second session the same day for an unrelated
finding, which is why the sweep's proposals were renumbered R-159..R-162 at filing
time. All nine IDs verified free in BOTH backlog files before use.

Part 0 settled the question the sizing item depended on, by reading:

mp1 is RETENTION, not staging, and neither of the two framings was right. A unit
is the KEPT copy on the app's OWN drive (backup.go:245-255); for an app with no
HDD_PATH the namespace falls back to the system SSD - "the SSD-only system-data
fallback" (appbackup/paths.go:26-27). There is no post-copy deletion: the only
prune is F5 residue-on-old-drives when an app MOVES (backup.go:1053-1112). So mp1
retains the units of driveless apps only - not every app, but not transient
either. Confirmed against the spike: sys_drive held exactly the four driveless
apps and not calibre-web, which had a drive and was still backed up.

A unit is volume tars + DB dumps only, never mp8 userdata
(recovery_unit.go:20-25), so a 1 TB photo library can never overflow one. And mp1
gates the WHOLE chain, not just Tier 1: Tier-2 mirrors the unit "(always)" from
RecoveryUnitPath (tier2.go:302,368) and Tier-3 carries it, so a unit that cannot
be written leaves both with nothing to copy.

Part 2 fired on both triggers - retention, and the fallback undocumented - so
07-backup-architecture.md gains section 7.5. Section 6.1 said a unit lives "on the
app's own drive", which is true and was the whole story only for drive-resident
apps; the no-drive case was undocumented, as was the sizing constraint. 7.5
records the mp0-50G-vs-mp1-20G mismatch, the measured ratios (DB app up to ~2x,
21.1GB -> 40.2GB; file-only 1.00x), and the bound this puts on D5's Lane-1
independence: restorable from the drive alone only while the unit still fits -
about 19 GB file-only, about 10 GB DB-backed. No number proposed; the ratio is the
operator's ruling (R-163).

R-159/R-160 marked SHIPPED only after verifying the template changes are in
app-catalog origin/main, and R-156's gate likewise (check-volume-persistence.py
present). papra is NOT fixed - referred - so R-156 stays open on that one app.

Ranked, with one line of reasoning each: R-157 first (an app can stay down
indefinitely with mechanism B silent on every channel), then R-161 (the gate
exists and nothing runs it, which is why R-156's class recurs - R-29's record is
three orphaned gates and one enforced), R-156, R-163, R-158, R-164, R-162.
This commit is contained in:
2026-08-02 12:33:12 +02:00
parent 482af37b7d
commit b06ea9c877
3 changed files with 99 additions and 0 deletions
@@ -484,6 +484,58 @@ Until D5 is actually implemented, §7.1's chain stands as the model.
---
### 7.5 Where a recovery unit is RETAINED — and the size bound it puts on Lane 1
Added 2026-08-02 from `audits/SPIKE-recovery-unit-space-2026-08-02.md` and
`audits/CAMPAIGN-10-closeout-2026-08-02.md`. §6.1 says a Tier-1 unit lives *"on the app's own drive"*,
which is true and is the whole story for a drive-resident app. **It is not the whole story for an app
with no data drive, and that case was undocumented until now.**
**The unit is RETAINED, not staged.** `GetAppDrivePath` (`felhom-controller/internal/backup/backup.go:245-255`)
returns the app's `HDD_PATH` if it has one and **`systemDataPath` otherwise** — what
`internal/appbackup/paths.go:26-27` calls *"the SSD-only system-data fallback"*. Nothing deletes a unit
after it is copied onward: the only prune is F5 (`backup.go:1053-1112`), which removes residue on an
**old** drive when an app **moves**. So for every app without a data drive, `mp1` holds the kept copy
indefinitely.
**What a unit contains, which bounds the problem** (`internal/backup/recovery_unit.go:20-25`):
`compose/` + `db-dumps/` + `volume-dumps/` (named-volume tars) + `manifest.json`. **It never contains
`mp8` / `HDD_PATH` userdata** — a photo library on a 1 TB drive is not in a unit, and cannot make one
overflow.
**The resulting mismatch, on a default appliance:**
| | size | holds |
|---|---|---|
| `mp0` `/var/lib/docker` | **50 G** | every app's live volumes — the thing a unit copies |
| `mp1` `/mnt/sys_drive` | **20 G** | the retained units of every **driveless** app |
A DB-backed app's unit is up to **~2×** its data, because it carries the volume tar **and** the SQL
dump (measured: 21.1 GB of app data → a **40.2 GB** unit). A file-only app's is **1.00×** (measured:
homebox 2305 MB → 2305 MB). `--sysdata-grow` (`felhom-agent/cmd/felhom-agent/main.go:178`) defaults to
**0**, is not derived from the physical drive, and no observed box passes it — demo-hp's production
guest 9201 ships `mp0 50G / mp1 20G`.
**`mp1` gates the entire app-data chain, not just Tier 1.** Tier-2 mirrors the unit *"(always)"* from
`RecoveryUnitPath` (`internal/backup/tier2.go:302,368`) and Tier-3 carries it too, so a unit that
cannot be written to `mp1` leaves Tier-2 and Tier-3 with nothing to copy.
**The bound this places on Lane 1 (§3).** Lane 1's promise is that a customer can restore an app from
the drive alone, without the operator and without the guest — D5 (§7.4) made that true by putting the
portable secrets in the unit. **That independence is bounded by app size**, and the bound is:
> On a default appliance, an app can be restored by Lane 1 only while its recovery unit still fits the
> retained space — **≈ 19 GB of app data for a file-only app, ≈ 10 GB for a DB-backed one.**
> Past that the unit cannot be captured, and the app falls back to Lane 2's operator-driven
> whole-guest route.
Two things are deliberately **not** recorded here. **The sizing ratio is the operator's ruling**
(**R-163**) — this section states the constraint, not a number. And **the same-device placement is
intended, not a defect**: a driveless app's unit sits on the same SSD as its volumes, but Tier-2's
cross-drive mirror, the whole-guest tier (which covers `rootfs + mp0 + mp1`, §6.1) and Tier-3 all cover
device loss. What was missing was that this case existed at all, and that nothing warns when an app
crosses the line — **R-158**.
## 8. The failure → recovery matrix
**This is the core artifact.** One row per failure. It is authoritative for recovery routes;