95f3180ab4
Both guests healthy on 0.132.0; catalog 21e8df1 (13 blocks) synced live with ZERO backup-block rejections. 13-app cross-check table (every bind resolves explicit). RP-1..RP-4 confirmed. audiobookshelf PENDING-VETO = optional pending Viktor's ruling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
128 lines
8.0 KiB
Markdown
128 lines
8.0 KiB
Markdown
# REPORT — Backup classification: schema + parser + classifier + catalog migration (controller v0.132.0)
|
||
|
||
## Summary
|
||
|
||
Task 2 of the backup-classification-redesign arc
|
||
(`felhom.eu/documentation/audits/SPIKE-backup-classification-2026-07-14.md`). Ships the
|
||
referential-coupling classification as **DATA + PARSER + PURE CLASSIFIER — deliberately INERT**: no
|
||
backup tier changes behavior. The `backup:` block now lands in `.felhom.yml` for all 13 bind-bearing
|
||
catalog apps; the controller parses + validates it loudly and a pure classifier resolves per-bind
|
||
classes with the SQ5 two-level default. Task 3 (tier policy engine) and Task 4 (manual `.fab` UI)
|
||
consume this — nothing does yet except a validation log pass.
|
||
|
||
## Confirmed baselines
|
||
|
||
| Repo | `main` @ start | → | Commit(s) on `main` |
|
||
|------|----------------|---|---------------------|
|
||
| felhom-controller | `af98c53` (v0.131.0) | **v0.132.0** | `0649f9a` (code+tests+docs) |
|
||
| app-catalog-felhom.eu | `365a017` | (no version scheme) | `21e8df1` (13 blocks + CHANGELOG) |
|
||
|
||
Trunk-based, direct to `main`. Controller committed + deployed FIRST, then the catalog blocks (so the
|
||
parser validates them on first sync).
|
||
|
||
## Files created / modified
|
||
|
||
**Controller (`0649f9a`)**
|
||
- `internal/appbackup/classify.go` (NEW) — schema (`BindClass`, `BindRoot`, `BackupSpec`, `BindSpec`,
|
||
`ComposeBind`, `ClassOrigin`, `ClassifiedBind`) + `ValidateBackupSpec` (whole-block-reject) +
|
||
`ClassifyBinds` (two-level default; explicit beats `:ro`; nil spec → legacy/false).
|
||
- `internal/stacks/classify_binds.go` (NEW) — `ParseComposeClassifiableBinds`: `${VAR}`-relative binds
|
||
+ `:ro` flag, `ParseComposeUserdataMounts` scanner shape, dedupe (first-`:ro`-wins).
|
||
- `internal/stacks/metadata.go` — `Metadata.Backup` field + the LoadMetadata validation choke point
|
||
(bad block → nil + one `[ERROR]` → legacy) + `Manager.ClassifiedBinds`.
|
||
- `internal/appbackup/appdata.go` — `StackDataProvider.GetStackClassifiedBinds`.
|
||
- `internal/backup/appbackup_bridge.go` — `ClassifiedBind` alias.
|
||
- `cmd/controller/main.go` — `stackAdapter.GetStackClassifiedBinds` delegation.
|
||
- 5 test fakes (`recovery_unit_test.go`, `tier2_restore_test.go`, `volume_dumps_test.go`,
|
||
`api/backup_snapshots_test.go`, `web/async_restore_test.go`) — nil-stub method ONLY (no logic edits).
|
||
- Tests: `appbackup/classify_test.go`, `stacks/classify_binds_test.go`, `stacks/classify_wiring_test.go`.
|
||
- Docs: `CHANGELOG.md`, `CONTEXT.md`, `REUSE.md`, `controller/README.md`.
|
||
|
||
**Catalog (`21e8df1`)** — `backup:` block added after `resources:` in 13 `.felhom.yml` files (immich,
|
||
paperless-ngx, nextcloud, calibre-web, audiobookshelf, komga, navidrome, radarr, sonarr, emby,
|
||
jellyfin, plex, romm) + `CHANGELOG.md`.
|
||
|
||
## Tests
|
||
|
||
`go build ./... && go vet ./... && go test ./...` — **all green**, both repos' content parses clean.
|
||
**+14 test functions** (before → after in the touched packages; a 9-case validation subtable inside):
|
||
|
||
- **Group A (classifier):** `TestClassify_ImmichShape` (explicit beats `:ro`), `_TwoLevelDefault`
|
||
(unlisted writable→mandatory, `:ro`→excluded), `_NilSpecLegacy`, `_BareRootFallsToDefault`,
|
||
`_SameRelPathBothRoots`.
|
||
- **Group B (validation):** `TestValidateBackupSpec_Defects` (9 cases: unknown/empty class, empty/
|
||
absolute/`..`/backslash/non-clean path, duplicate, no-matching-bind — each names the entry) + `_ValidAndNil`.
|
||
- **Group C (parser):** `TestParseClassifiableBinds_Variants` (ro/rw/ro,z/z, dedupe, bare root, both
|
||
roots, non-volume sections), `_CleanAndSubpaths` (false-prefix rejected), `_Missing`.
|
||
- **Group D (end-to-end, NO seams):** `TestClassifiedBinds_ValidFixture`, `_RejectedFixture` (RP-4),
|
||
`_NoBlockLegacy` — real files through the REAL LoadMetadata path.
|
||
- **Group E (catalog fidelity):** `TestCatalogBlocks_Fidelity` (immich, paperless-ngx, sonarr exact class sets).
|
||
|
||
**Inertness proof (Scenario C):** the entire pre-existing suite is green with **zero test-logic
|
||
edits** — only the mandated nil-stub method was added to each fake (verified: `+14 −1` across the 5
|
||
fake files, the `−1` being a gofmt re-alignment).
|
||
|
||
### Red-proofs (run → fail → revert → green) — all confirmed
|
||
|
||
| RP | Mutation | Failing assertion | Reverted |
|
||
|----|----------|-------------------|----------|
|
||
| RP-1 | disable the class-value check | `TestValidateBackupSpec_Defects/unknown_class` + `/empty_class` accept silently | green |
|
||
| RP-2 | `:ro` default overrides explicit | `TestClassify_ImmichShape`: media/photos forced excluded | green |
|
||
| RP-3 | flip unlisted-writable default to excluded | `TestClassify_TwoLevelDefault`: unlisted writable ≠ mandatory | green |
|
||
| RP-4 | LoadMetadata accepts `Backup` without validating | `TestClassifiedBinds_RejectedFixture`: bad block not degraded to legacy | green |
|
||
|
||
## Deploy + verify
|
||
|
||
- Built `v0.132.0` on 180 (`build.sh 0.132.0 --push`, image 145M, digest `sha256:00c6f83e…`).
|
||
- Deployed via the bootstrap mechanism to **BOTH guests** — `felhom-controller:0.132.0 … Up (healthy)`
|
||
on demo 9201 (felhom-pve) and the drill guest 9201 (nested PVE `drill-day0`, 192.168.0.152).
|
||
|
||
## Live sync check (demo guest — the real server-side pipeline)
|
||
|
||
Catalog `21e8df1` pushed AFTER the controller was live, then the controller's `doSync` pipeline
|
||
(same code path as the "Sablonok frissítése" button) was triggered by a bootstrap restart:
|
||
|
||
- `[sync] Updated <app>/.felhom.yml` for **exactly the 13 apps**, then `[sync] Catalog sync complete`.
|
||
- **`backup block rejected` count: 0.** Zero `[ERROR] [stacks]` lines anywhere in the log. Controller
|
||
healthy. → **live proof that all 13 committed blocks parse, validate, and match their real composes.**
|
||
|
||
## 13-app cross-check (shipped parser against the real catalog clone — every bind resolved `explicit`)
|
||
|
||
| App | Resolved class set (root/relpath = class) |
|
||
|-----|-------------------------------------------|
|
||
| immich | hdd/appdata/immich=mandatory · userdata/media/photos=optional |
|
||
| paperless-ngx | hdd/appdata/paperless/media=mandatory · hdd/appdata/paperless/export=excluded · userdata/import/paperless=excluded |
|
||
| nextcloud | hdd/appdata/nextcloud=mandatory |
|
||
| calibre-web | userdata/media/books=mandatory · userdata/import/calibre=excluded |
|
||
| audiobookshelf | userdata/media/audiobooks=optional · userdata/media/podcasts=excluded |
|
||
| komga | userdata/media/comics=optional |
|
||
| navidrome | userdata/media/music=excluded |
|
||
| radarr | userdata/media/movies=excluded · userdata/downloads=excluded |
|
||
| sonarr | userdata/media/tv=excluded · userdata/downloads=excluded |
|
||
| emby | userdata/media=excluded |
|
||
| jellyfin | userdata/media=excluded |
|
||
| plex | userdata/media=excluded |
|
||
| romm | userdata/roms=optional · hdd/appdata/romm/resources=excluded |
|
||
|
||
Every bind resolves with origin `explicit` (full coverage — no writable bind left to the mandatory
|
||
default). Verified via a throwaway `go run` harness over the shipped `LoadMetadata →
|
||
ParseComposeClassifiableBinds → ClassifyBinds`, then removed (not committed).
|
||
|
||
## PENDING-VETO resolution
|
||
|
||
**audiobookshelf `media/audiobooks` = `optional`** (spike SQ2 proposed `excluded`). Committed optional
|
||
for consistency with komga/romm (curated, often personally-ripped → precious-decoupled). If Viktor
|
||
rules `excluded`, flip that ONE line in `templates/audiobookshelf/.felhom.yml`.
|
||
|
||
## Observations (noticed, NOT acted on)
|
||
|
||
- The classification is INERT by contract — offsite/tier-2/`.fab` still capture per today's legacy
|
||
behavior. **Task 3** (tier policy engine) is the next step: consume `Manager.ClassifiedBinds` /
|
||
`GetStackClassifiedBinds` to scope offsite/tier-2/`.fab` capture by class (mandatory→offsite,
|
||
excluded→never, optional→customer-selectable). The seam is wired + tested for it.
|
||
- Recovery units already carry `.felhom.yml` and git-sync already whitelists it, so the block reaches
|
||
deployed stacks + units with zero plumbing changes; no recovery-unit SchemaVersion bump (Task 3 owns
|
||
any content-semantics bump).
|
||
- All 13 catalog apps list every classifiable bind explicitly — the two-level *default* branch
|
||
(unlisted writable→mandatory) is exercised only by tests today, never by a real catalog app.
|