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).
6.0 KiB
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 A–F (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 commit8d85da7).
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/classifyRootpath.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. ClassifyBindsemits legacy binds with an emptyClass;ComputeCaptureSet's legacy short-circuit means those are never resolved, buttierKeepsalso 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/mediavsuserdata:media); today no catalog app does this, but the collapse + mandatory-wins rule makes it safe if one ever does.