Files
felhom-controller/REPORT.md
T

6.4 KiB
Raw Blame History

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.goComputeCaptureSet, 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 (digest sha256:0832106…c772d8); deployed to demo guest 9201 (bootstrap-managed). Code commit 2668ac4 (controller main); docs 8d85da7 (felhom.eu main).

  • docker ps (guest 9201): gitea.dooplex.hu/admin/felhom-controller:0.133.0 Up (healthy).
  • Startup log: [INFO] Event pushed: controller_started (info) — Controller elindult (0.133.0); recovery-unit capture + hub report + health probes all normal (the seerr/radarr/calibre-web no such host warns are pre-existing demo-DNS noise, unrelated to this change).
  • INERT-silence check PASS: grep -iE 'capturese|computecapture|crossapp' over the container logs returns nothing — the package has zero call sites, so no feature log line fires at runtime.

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.