Files
felhom-controller/REPORT.md
T
admin 95f3180ab4 docs: REPORT — v0.132.0 backup classification (INERT) deploy + live sync evidence
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
2026-07-14 18:54:34 +02:00

128 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.