6.3 KiB
REPORT — .fab exclusion scoping (Task 4) — controller v0.136.0
Summary
Task 4: the .fab column of the matrix (SQ5 verdict + ruling #1 + Viktor's R1-C). The SQ6
over-capture is fixed for classified apps — a manual export's userdata root tar is now
exclude-scoped (keeps only ancestors/descendants of a selected bind), so sibling apps' content no
longer rides along; an all-excluded app produces no userdata tar at all. Mechanics unchanged from
v0.130.0 (one root tar + per-mount skip), so the manifest stays v1 and the import side is
untouched; legacy apps export byte-identically. Mandatory is a server-side floor; optional is a
pre-selected checkbox; excluded is opt-in behind the two-number warning.
Baselines (live-verified at session start)
| Repo | main @ start |
Version | → |
|---|---|---|---|
| felhom-controller | 5f21613 |
v0.135.0 | v0.136.0 |
Files
- mod
internal/appbackup/captureset.go— extractedresolveGuardCollapse(shared pipeline); newComputeFabBuckets+FabBuckets. - new
internal/appexport/fabplan.go—fabPlan,computeFabPlan,fabUserdataExcludes+classifyFabRel,tarDirectoryExcluding,fabEstimateSplit. - mod
internal/appexport/export.go—ExportRequest+=DeselectOptional/OptInExcluded;dirListerseam;exportHDDDataconsumes the plan;tarDirectory→ thin wrapper. - mod
internal/appexport/estimate.go—ExportEstimateclass-split fields (additive);FabItem. - mod
internal/appexport/provider.go+cmd/controller/main.go—GetStackClassifiedBindson the export seam + adapter. - mod
internal/web/handler_export.go+handler_export_download.go— both start handlers carry the selections (two-call-site). - mod
internal/web/templates/app_export.html— the class-selection UI. - tests new
fabbuckets_test.go,fabplan_test.go,fabexport_test.go,fab_export_test.go; provider fakes updated. - CHANGELOG / REPORT / CONTEXT / README.
Untouched (§5/§12): manifest.go, validate.go, restore.go, every import handler, ExportDataMounts — the verdict rests on the import side staying byte-identical.
Tests — results
go build ./... && go vet ./... && go test ./... — green. New: bucket/guard/no-containment
(ComputeFabBuckets); plan scenarios A–F + §8 (unmatched-mount-kept, skip-when-none, mandatory floor,
nested excludes); tarDirectoryExcluding FS-level; export-level bundle tests (exclude-scoped tar,
legacy full-root, all-excluded no-tar); the two-call-site bundle test across BOTH start pipelines.
§10 red-proofs (mutate → FAIL → revert), all verified
| ID | Mutation | Test |
|---|---|---|
| A | legacy routed through the classified plan (drop short-circuit) | FabExport_LegacyFullRoot (userdata tar skipped) |
| B | always-tar the userdata root | FabExport_AllExcludedNoUserdataTar (tar present) |
| C | invert the keep-rule | FabExport_ExcludeScopedUserdataTar (mandatory excluded) |
| D3 | drop the server-side mandatory floor | FabPlan_MandatoryFloor (mandatory deselected) |
| E | skip guards for excluded buckets | ComputeFabBuckets_GuardsAllClasses (traversal enters a bucket) |
| F | drop selection fields from one start handler | Fab_SelectionsRideBothStartPipelines/download (opted-in content absent) |
All reverted; post-revert full suite green; no residue.
Deploy / verify
Built + pushed felhom-controller:0.136.0 on 180; deployed to guest 9201 → Up (healthy). Commit
cf9ce01.
§13 live validation — the SQ6 before/after, on the real demo
- Leg 1 — before-picture (v0.135.0, the SQ6 protocol): planted
SPIKE-marker-own.txtunder calibre-web'smedia/books/andSPIKE-marker-sibling.txtunder the siblingmedia/movies/; exported calibre-web via the real UI (download pipeline, 356 MB).tar tfof the bundle'sdata/hdd/userdata.tar: BOTH markers present (own=1, sibling=1, 15media/moviesentries) — the whole userdata root rode along (the over-capture). - Leg 2 — after (v0.136.0): re-exported calibre-web (default).
tar tf: own=1, sibling=0,media/moviesentries=0,media/booksentries=39 — the sibling no longer rides along (Scenario C live). radarr default export → the bundle has nohdd/contents at all (no userdata tar, Scenario B live — radarr is classified all-excluded). - Leg 3 — opt-in + estimate split: radarr's
download/estimatereturnshas_classification:true+excluded_items:[{downloads, 308 MB}, {media/movies, 2.7 GB}](the two-number-warning data, live, both pipelines). Re-exported radarr withopt_in_excluded:["userdata/downloads"]→ the userdata tar now carriesdownloads(2 entries) whilemedia/moviesstays excluded (0) — the opt-in flips Scenario B, per-path, live. - Leg 4 — import compatibility (read-only): the new exclude-scoped bundle's manifest is v1
(
version=1,hdd_subdirs=['userdata'],has_hdd_data=true) — the untouched import maps theuserdatabasename →<HDD_PATH>/userdatavia the existing fallback, so the smaller tar restores exactly as before. A destructive live import over calibre-web (compose down --volumes) was NOT run — compatibility is structurally guaranteed (v1 manifest + untouched import) and unit-proven (roundtrip_test.go). - Leg 5 — hygiene: removed the two SPIKE markers from live userdata and the test
.fabbundles; cleaned the 180 session temp. Listed removals above.
6D-pending
- CAMPAIGN-6D Accept #1 (the ≥1 GiB
.fabfull circle) now runs against this final capture shape — a real export→transport→import round-trip of a large classified app, exercising the exclude-scoped userdata tar end-to-end (the live leg-4 import was deliberately read-only here). - Carried from earlier tasks:
PlaceOffsiteRestoreagainst live data; the SQ3 immich offsite-only full circle; organic enlarge-block firing.
Observations
ComputeCaptureSetwas refactored to shareresolveGuardCollapsewithComputeFabBuckets(no duplicate pipeline); its existing tests stayed green through the refactor.- The estimate split is in the shared
EstimateExport, so both estimate pipelines surface it by construction; the genuine two-call-site risk (and the F red-proof) is on the START selections, which the bundle test exercises per-handler.