Backup classification: schema + parser + pure classifier (INERT, v0.132.0)
Task 2 of the backup-classification-redesign arc. Ships the referential-
coupling classification as DATA + PARSER + PURE CLASSIFIER, deliberately
inert — no backup tier changes behavior. Task 3 (tier policy engine) and
Task 4 (manual .fab UI) consume it.
- appbackup/classify.go: BackupSpec/BindSpec/ComposeBind/ClassifiedBind;
ClassifyBinds (SQ5 two-level default — explicit beats :ro; unlisted
writable→mandatory, unlisted :ro→excluded; nil spec→legacy/false);
ValidateBackupSpec (whole-block-reject on any defect, first defect named).
- stacks/classify_binds.go: ParseComposeClassifiableBinds — ${VAR}-relative
binds + :ro flag (NOT ParseComposeHDDMounts/ExportDataMounts, the traps).
- Metadata.Backup + LoadMetadata as the single validation choke point (bad
catalog block → nil + one ERROR → legacy, within one sync cycle).
- Manager.ClassifiedBinds + StackDataProvider.GetStackClassifiedBinds seam
(delegated by stackAdapter, nil-stubbed in every fake) — wired + tested
now so Task 3 consumes a tested seam.
INERT: full pre-existing suite green with zero test-logic edits. +14 tests;
red-proofs RP-1..RP-4 confirmed. The 13 catalog backup: blocks ship in the
same app-catalog change (this controller deploys first).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
This commit is contained in:
@@ -1,5 +1,49 @@
|
||||
## Changelog
|
||||
|
||||
### v0.132.0 — Backup classification: schema + parser + pure classifier (INERT; Task 2) (2026-07-14)
|
||||
|
||||
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. Task 3 (tier policy engine) and Task 4 (manual `.fab` UI) are the
|
||||
consumers; today only a validation log pass touches it.
|
||||
|
||||
- **Schema (`internal/appbackup/classify.go`, new):** `BackupSpec`/`BindSpec` model the `.felhom.yml`
|
||||
`backup:` block (`userdata:`/`hdd:` lists of `{path, class}`); `ComposeBind` is a `${VAR}`-relative
|
||||
host bind carrying the `:ro` flag. Classes: **mandatory** (COUPLED — restore-without is
|
||||
broken-not-empty, SQ3), **optional** (DECOUPLED-precious), **excluded** (DECOUPLED-bulk/transient).
|
||||
- **Pure classifier `ClassifyBinds`:** the SQ5 **two-level default** — an explicit block entry ALWAYS
|
||||
wins (an explicit `optional` on immich's `:ro` external library beats the reader default); an
|
||||
unlisted **writable** bind defaults **mandatory** (the C6B-F1 capture direction, never silent-drop);
|
||||
an unlisted **`:ro`** bind defaults **excluded** (reader rule). Returns `hasClassification` — **a
|
||||
nil spec (no block) → every bind is `legacy` with NO class semantics**, so an unmigrated app's
|
||||
behavior is byte-identical.
|
||||
- **Validation `ValidateBackupSpec` (whole-block-reject):** ANY defect — unknown/empty class (a typoed
|
||||
`clas:` key leaves `""`), empty/absolute/`..`/backslash/non-clean path, duplicate `(root, path)`, or
|
||||
an entry matching NO compose bind (a stale/typoed path must not silently shift the real bind onto
|
||||
the mandatory default) — rejects the ENTIRE block with the first defect named. Never partial.
|
||||
- **Parser `ParseComposeClassifiableBinds` (`internal/stacks`):** copies the
|
||||
`ParseComposeUserdataMounts` scanner shape but stays in `${VAR}`-relative space and preserves `:ro`
|
||||
(why it does NOT reuse `ParseComposeHDDMounts`, which resolves absolutes and drops the mode). Deduped
|
||||
on `(root, relpath)`, first-occurrence `:ro` wins; short-syntax only.
|
||||
- **Integration:** `Metadata` gains `Backup *appbackup.BackupSpec`; `LoadMetadata` is the SINGLE
|
||||
validation choke point (catalog listing, deployed-stack scan, and git-sync all flow through it, so a
|
||||
bad catalog push logs `[ERROR] ... backup block rejected in <dir>: <reason>` within one sync cycle
|
||||
and the app degrades to legacy). `stacks.Manager.ClassifiedBinds` + a new
|
||||
`StackDataProvider.GetStackClassifiedBinds` seam (delegated by `stackAdapter`, nil-stubbed in every
|
||||
fake) exist so Task 3 consumes a **wired, end-to-end-tested** seam — the F-S3 lesson that wiring is
|
||||
where typos hide.
|
||||
|
||||
INERT by design: offsite/tier-2/`.fab`/deploy/sync are byte-identical — proven by the entire
|
||||
pre-existing test suite staying green with **zero test-logic edits** (only mandated nil-stub methods
|
||||
added to fakes). Recovery units already carry `.felhom.yml` and git-sync already whitelists it, so the
|
||||
block propagates to deployed stacks + units with zero plumbing changes; no recovery-unit SchemaVersion
|
||||
bump. The 13 catalog `backup:` blocks ship in the same `app-catalog-felhom.eu` change (this controller
|
||||
must be live first so the parser validates them on first sync). +14 test functions (Groups A–E,
|
||||
incl. a 9-case validation table); red-proofs RP-1..RP-4 all
|
||||
confirmed (validation, explicit-beats-ro precedence, capture-default direction, LoadMetadata→validate
|
||||
wiring). Controller-only; no MinAgent/hub coupling.
|
||||
|
||||
### v0.131.0 — F-S2 + F-S3: compose-derived appdata dir resolution (paperless-ngx → appdata/paperless) (2026-07-14)
|
||||
|
||||
The controller assumed an app's HDD appdata dir is always `appdata/<stackName>`. paperless-ngx binds
|
||||
|
||||
Reference in New Issue
Block a user