architecture §3: align capture-set API sketch to Task 3-core as-built
This commit is contained in:
@@ -103,25 +103,45 @@ Row-level decisions folded in:
|
||||
|
||||
## 3. Capture-set computation (Task 3-core)
|
||||
|
||||
One pure function, leaf package `appbackup`, consumed by all three engines:
|
||||
One pure function, leaf package `appbackup`, consumed by all three engines (as-built, v0.133.0):
|
||||
|
||||
```
|
||||
ComputeCaptureSet(binds []ClassifiedBind, hasClassification bool, tier Tier,
|
||||
hddPath string) CaptureSet
|
||||
ComputeCaptureSet(binds []ClassifiedBind, hasClassification bool,
|
||||
tier CaptureTier, hddPath string) CaptureSet
|
||||
→ CaptureSet{HasClassification bool; Paths []CapturePath; Skipped []SkippedPath}
|
||||
```
|
||||
|
||||
- Resolves each bind's `(Root, RelPath)` against `hddPath` (HDD) / `hddPath+"/userdata"`
|
||||
(USERDATA) into absolute paths; filters by the tier column of §2; returns
|
||||
`{UnitOnly bool, Paths []CapturePath}` where each `CapturePath` carries
|
||||
`{Abs, Root, RelPath, Class}` (the relpath is what the tier-2 layout and restore need).
|
||||
- `hasClassification == false` → `UnitOnly` for offsite, resolver-legacy marker for tier-2 —
|
||||
engines branch on it explicitly, so the legacy path stays byte-identical and testable.
|
||||
- Pure: no FS, no docker (the Tasks 1/2 leaf-package discipline). The stat-filter (decision 2.5)
|
||||
lives in the engines, not here.
|
||||
- Companion red-proofs: (a) sonarr with no block must produce UnitOnly for offsite (the SQ5
|
||||
cost-regression guard — this is the test that fails if the two-level default is miswired);
|
||||
(b) immich's explicit-optional `:ro` external library must appear in tier-2's set and NOT in
|
||||
offsite's; (c) paperless's `export` must appear in no automatic tier.
|
||||
(USERDATA) into absolute paths (slash algebra — `path.Join`, never `filepath`); filters by the
|
||||
tier column of §2 (`TierOffsite` = mandatory only; `TierSecondary` = mandatory + optional;
|
||||
excluded is silently dropped). Each `CapturePath` carries `{Abs, Root, RelPath, Class}` (the
|
||||
relpath is what the tier-2 layout and restore need).
|
||||
- The former `UnitOnly` field is replaced by **`HasClassification`** (it mirrors the classifier's
|
||||
existing bool); engines derive unit-only as `!HasClassification || len(Paths)==0`. When
|
||||
`hasClassification == false` the function short-circuits to `{HasClassification:false}` — nothing
|
||||
resolved, `Paths`/`Skipped` nil — so the legacy path stays byte-identical and testable (the SQ5
|
||||
cost-regression guard: an unmigrated app never resolves a bind into an automatic tier).
|
||||
- **`Skipped`** carries would-be captures the tier filter selected but that are structurally unsafe,
|
||||
each with an English `Reason`, so the pure function stays log-free (engines log a skipped
|
||||
**mandatory** as a loud capture GAP). Three structural guards, evaluated after the tier filter:
|
||||
traversal (a `..` path segment or an absolute RelPath — **load-bearing because the compose parser
|
||||
path.Cleans but does NOT reject `..`, and `ValidateBackupSpec` only vets *spec* entries, so an
|
||||
unlisted writable `${HDD_PATH}/../x` bind arrives classed mandatory**), bare HDD drive-root
|
||||
(RelPath `""` — would nest `<hddPath>/backups` into the capture), and the reserved `backups/`
|
||||
zone; a bare **userdata** root (`<hddPath>/userdata`) is allowed (it does not nest `backups/`).
|
||||
- After filtering+guards: an equal-`Abs` collision from two binds collapses to one entry with
|
||||
**mandatory winning** over optional (mandatory semantics never degrade); then **containment
|
||||
dedup** drops any path whose ancestor is already in the set (keep the ancestor); output is sorted
|
||||
by `Abs` (deterministic despite map/slice input).
|
||||
- `CrossAppOverlaps(map[app]CaptureSet) []Overlap` is shipped here as a **pure** function (exact-Abs
|
||||
match across ≥2 apps' `Paths`; cross-app *containment* is legitimate per §4 and is NOT an overlap).
|
||||
Its WARN **wiring** lands in 3a/3b, not in 3-core — no log call sites exist yet.
|
||||
- Pure: no FS, no docker, no logging (the Tasks 1/2 leaf-package discipline). The stat-filter
|
||||
(decision 2.5) lives in the engines, not here.
|
||||
- Companion red-proofs: (a) sonarr with no block must produce unit-only (`HasClassification:false`,
|
||||
empty `Paths`) for offsite (the SQ5 cost-regression guard — the test that fails if the two-level
|
||||
default is miswired); (b) immich's explicit-optional `:ro` external library must appear in
|
||||
tier-2's set and NOT in offsite's; (c) paperless's `export` must appear in no automatic tier.
|
||||
|
||||
The engines consume the existing wired seam `GetStackClassifiedBinds` (main.go ~L1106) — no new
|
||||
seam. Per the F-S3 lesson, 3-core still ships an end-to-end wiring test through a real Manager.
|
||||
|
||||
Reference in New Issue
Block a user