Files
felhom-controller/REPORT.md
T
admin 2668ac4da3 Capture-set computation (INERT; Task 3-core, v0.133.0)
Pure appbackup.ComputeCaptureSet(binds, hasClassification, tier, hddPath) → CaptureSet
{HasClassification, Paths, Skipped}: legacy short-circuit → tier filter (§2) → structural
guards → equal-Abs collapse (mandatory>optional) → containment dedup → sort. Slash algebra,
no filepath/FS/log. Structural guards (traversal / bare HDD drive-root / reserved backups/)
are load-bearing (the compose parser does not reject ..). Pure CrossAppOverlaps advisory
(WARN wiring deferred to 3a/3b). INERT — no engine consumes it yet.

Tests: Groups A-F (appbackup) + F-S3 no-seam wiring (stacks); all 6 §10 red-proofs verified.
Docs: architecture §3 aligned (felhom.eu 8d85da7).
2026-07-14 21:53:05 +02:00

97 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — Capture-set computation (INERT; Task 3-core) — controller v0.133.0
## Summary
Task 3-core of the backup-classification-redesign arc
(`felhom.eu/documentation/architecture/07-backup-architecture.md` §3; tier×class matrix §2; spike
verdicts in `SPIKE-restic-snapshot-shape-2026-07-14.md`). Ships **one pure function**,
`appbackup.ComputeCaptureSet`, that turns an app's classified binds + a tier + the app's live
`hddPath` into the tier-filtered, structurally-guarded, containment-deduped absolute capture set that
the 3a (offsite) and 3b (tier-2) engines will consume — plus a `Skipped` list for structurally unsafe
would-be captures and a pure `CrossAppOverlaps` advisory. **Deliberately INERT** like Task 2: no
backup tier changes behavior; nothing consumes any of it yet.
## Baselines (live-verified at session start)
| Repo | `main` @ start | Version | This task |
|---|---|---|---|
| felhom-controller | `95f3180` | v0.132.0 → **v0.133.0** | `appbackup` engine + `stacks` wiring test |
| felhom.eu | `b279312` | — | §3 docs alignment only (commit `8d85da7`) |
## Files created / modified
- **new** `controller/internal/appbackup/captureset.go``ComputeCaptureSet`, `CaptureTier`,
`CapturePath`, `SkippedPath`, `CaptureSet`, `CrossAppOverlaps`, `Overlap`; the §8 pipeline +
structural guards.
- **new** `controller/internal/appbackup/captureset_test.go` — Groups AF (7 tests).
- **new** `controller/internal/stacks/captureset_wiring_test.go` — Group G, F-S3 no-seam end-to-end.
- **mod** `controller/CHANGELOG.md` (v0.133.0, newest-on-top), `controller/REPORT.md` (this),
`controller/CONTEXT.md`, `controller/README.md` (appbackup surface, one block).
- **mod** `felhom.eu/documentation/architecture/07-backup-architecture.md` §3 (as-built API sketch;
separate commit `8d85da7`).
No engine edits (`RunTier2`/`RunOffboxBackup`/`RestoreOffbox` untouched), no web/scheduler wiring, no
`ClassifyBinds`/`ValidateBackupSpec`/`ParseComposeClassifiableBinds` change, no `AppDataDirNames` call.
## Design (as-built)
Fixed pipeline (§8): **legacy short-circuit → tier filter (§2) → structural guards → equal-Abs
collapse (mandatory > optional) → containment dedup (keep ancestor) → sort by Abs.** Pure: no
`os`/`exec`/`filepath`/logging; slash algebra throughout (`RelPath` is forward-slash, resolved Abs is
an in-container Linux path — `filepath` on the Windows test host would flip separators and break the
containment prefix checks). Resolution: `RootHDD → path.Join(hddPath, rel)`,
`RootUserdata → path.Join(hddPath, "userdata", rel)`. Structural guards are load-bearing security: the
compose parser path.Cleans but does not reject `..`, so an unlisted writable `${HDD_PATH}/../x` bind
reaches the function classed mandatory; the guard moves it to `Skipped` (with the bare-drive-root and
reserved-`backups/` guards) instead of into a captured path.
## Tests — results
`go build ./... && go vet ./... && go test ./...`**all green, both repos** (felhom.eu has no Go).
New tests (8 total): `internal/appbackup` +7 (`PerTierSplit`, `LegacyInert`, `ExcludedInvisible`,
`StructuralGuards`, `LegitDotDotName`, `ContainmentAndCollision`, `CrossAppOverlaps`);
`internal/stacks` +1 (`CaptureSet_Wiring`). Full-suite package count unchanged, all `ok`.
### §10 red-proofs (mutation → FAIL → revert), every one verified
| ID | Mutation | Test that must fail | Observed failure |
|---|---|---|---|
| **B** (SQ5) | legacy short-circuit resolves binds as mandatory | `LegacyInert` | legacy app resolved `[appdata/sonarr, media/tv]` into Paths (+HasClassification=true) |
| **A** (tier) | `TierOffsite` includes optional | `PerTierSplit` | offsite Paths gained `/userdata/media/photos` |
| **D** (guard) | traversal guard deleted | `StructuralGuards` | `/mnt/evil` (escaped root) present in Paths; 2 Skipped not 3 |
| **E1** (contain) | containment dedup disabled | `ContainmentAndCollision` | descendant `appdata/paperless/media` not dropped |
| **E2** (mand-wins) | mandatory strength = optional | `ContainmentAndCollision` | collapsed `/userdata/media` class degraded to optional |
| **G** (wiring) | tier constants swapped in the filter | `CaptureSet_Wiring` (end-to-end) | real-Manager secondary lost photos / offsite gained it — no fake absorbed the typo |
All mutations reverted; post-revert full suite green; no `RED-PROOF` residue in the new files.
## Deploy / verify
Built + pushed `felhom-controller:0.133.0` on 180; deployed to demo guest 9201 (bootstrap-managed).
**INERT-silence check:** startup clean, no new feature log lines at runtime (the package has zero call
sites). Per-guest verification recorded below.
_Deploy verification (docker ps + logs) appended after the live deploy step — see the deploy commit._
## Live-validation scope
Live validation beyond deploy-health is **inherently N/A** for an inert pure package: it has no
runtime surface, no UI, no behavior change. The real live legs belong to 3a (offsite) and 3b (tier-2)
acceptance, which consume this function.
## Observations (documented, NOT acted on)
- **Parser-side traversal:** `ParseComposeClassifiableBinds` / `classifyRoot` path.Clean the compose
host token but do not reject a `..` that survives cleaning (`${HDD_PATH}/../x` → RelPath `../x`).
This is by design per the task (the parser stays a faithful extractor; the guard lives in 3-core),
and the structural guard here is what makes it safe. If a future task ever wants defence-in-depth,
the parser is the second place it could live — noted, not changed.
- `ClassifyBinds` emits legacy binds with an **empty** `Class`; `ComputeCaptureSet`'s legacy
short-circuit means those are never resolved, but `tierKeeps` also treats an empty class as
not-captured (defensive) — so even a future caller that skipped the short-circuit could not resolve
a classless bind. Belt-and-suspenders, intentional.
- The equal-Abs collapse can arise from two *spellings* of one path (`hdd:userdata/media` vs
`userdata:media`); today no catalog app does this, but the collapse + mandatory-wins rule makes it
safe if one ever does.