# 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` — extracted `resolveGuardCollapse` (shared pipeline); new `ComputeFabBuckets` + `FabBuckets`. - **new** `internal/appexport/fabplan.go` — `fabPlan`, `computeFabPlan`, `fabUserdataExcludes` + `classifyFabRel`, `tarDirectoryExcluding`, `fabEstimateSplit`. - **mod** `internal/appexport/export.go` — `ExportRequest` += `DeselectOptional`/`OptInExcluded`; `dirLister` seam; `exportHDDData` consumes the plan; `tarDirectory` → thin wrapper. - **mod** `internal/appexport/estimate.go` — `ExportEstimate` class-split fields (additive); `FabItem`. - **mod** `internal/appexport/provider.go` + `cmd/controller/main.go` — `GetStackClassifiedBinds` on 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.txt` under calibre-web's `media/books/` and `SPIKE-marker-sibling.txt` under the sibling `media/movies/`; exported calibre-web via the real UI (download pipeline, 356 MB). `tar tf` of the bundle's `data/hdd/userdata.tar`: **BOTH markers present** (own=1, sibling=1, 15 `media/movies` entries) — 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/movies` entries=0, `media/books` entries=39** — the sibling no longer rides along (Scenario C live). radarr default export → the bundle has **no `hdd/` contents at all** (no userdata tar, Scenario B live — radarr is classified all-excluded). - **Leg 3 — opt-in + estimate split:** radarr's `download/estimate` returns `has_classification:true` + `excluded_items:[{downloads, 308 MB}, {media/movies, 2.7 GB}]` (the two-number-warning data, live, both pipelines). Re-exported radarr with `opt_in_excluded:["userdata/downloads"]` → the userdata tar now carries `downloads` (2 entries) while `media/movies` stays 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 the `userdata` basename → `/userdata` via 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 `.fab` bundles; cleaned the 180 session temp. Listed removals above. ## 6D-pending - **CAMPAIGN-6D Accept #1** (the ≥1 GiB `.fab` full 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: `PlaceOffsiteRestore` against live data; the SQ3 immich offsite-only full circle; organic enlarge-block firing. ## Observations - `ComputeCaptureSet` was refactored to share `resolveGuardCollapse` with `ComputeFabBuckets` (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.