R-116 (v0.116.0): give the backup-target flag and the gate's key the same row

The absent-drive alarm was generic while its recovery was specific -- a pair an
operator cannot match. Mechanism now measured, not reasoned (felhom.eu
audits/DIAG-r116-disks-payload-2026-07-30.md): with the device gone /disks returns
4 rows, not 3. The drive appears TWICE and the two facts the controller needs are
on different rows -- the Observe row has backup_target:true but mount_path:"" and
guest_path:"" (so driveTargetByPath registers NO key from it), while the registry
row owns /mnt/felhom-drives/<name>, the key the gate looks up, with BackupTarget
absent from its struct literal => false.

WHY v0.115.0 WAS INERT: its fallback computed StablePathForRaw(t.MountPath), and in
the absent state MountPath is ALSO "" -- emptied by the same exactMount failure
that empties BackingDevice. It assigned nothing. Its test passed because the fixture
supplied a MountPath production never supplies, and the harness left DriveTargets
nil so the union loop never ran. Both corrected here; red-proof 1 replays v0.115.0's
exact code against the real shape and it fails.

THE JOIN, which was the hard part: with the device gone the two records share no
runtime field -- no mount, no backing device, and the Observe row's DurableID has
degraded off the fs-UUID. They share CONFIGURATION: storage.cfg's path on one side,
the .mount unit's Where on the other, both yielding the same stable guest path. New
hub.StorageTarget.ConfigPath (json:"-" -- that struct is a cross-repo contract
pinned by the golden + contract_test key-set comparison, and nothing off-box needs
the value), set from s.Path in observe.go, consulted in disks.go only after MountPath
so the present-state path is byte-identical, plus a guest-path arm on the union dedup
so exactly one row carries the drive.

WHY NEITHER OBVIOUS OPTION WAS TAKEN -- both regress R-114, which shipped yesterday.
backup_target_offer.go:79 reads (BackupTarget && MountPath != "") as "a real
drive with its own mountpoint -- healthy" and returns before its TargetAbsent
branch. Back-filling MountPath onto the Observe row (the smallest change, and the
spec's lean) and teaching the registry row the flag (its MountPath is non-empty, read
from the stale unit file) BOTH manufacture that row while the drive is missing, which
would have told the customer the backup target is fine while its drive is gone.
R-114's correctness rests on the absent-state rows not combining the flag with a
mount path; that coupling was invisible until the payload existed. Pinned by
TestAbsentTargetKeepsR114DegradedSignal.

Role unchanged, BoundUnderParent conjunction not widened, no wire field changed.
Suppressing the registry row in the absent state also removes its false
state:"attached" and its root-filesystem-derived total_bytes -- R-118's symptom
goes incidentally; R-118 is NOT fixed and stays open.

Tests 845 -> 849, suite rc=0 read separately from this commit. Four red-proofs, each
mutation asserted to have landed first.

NOT live-validated at this commit: publish+vouch, C5, discrimination, over-correction.
This commit is contained in:
2026-07-30 08:48:05 +02:00
parent 2f4ccab166
commit 21b0164fad
6 changed files with 389 additions and 92 deletions
+69
View File
@@ -1,3 +1,72 @@
## v0.116.0 — R-116: the flag and the key finally share a row (2026-07-30)
**Closes the mechanism `v0.115.0` guessed at.** The absent-drive alarm was the generic
`storage_disconnected` while its recovery was the specific `backup_target_restored` — a pair an
operator cannot match. The cause is now measured, not reasoned: `felhom.eu`
`audits/DIAG-r116-disks-payload-2026-07-30.md`.
**What the captured payload showed.** With the device gone `/disks` returns **4 rows, not 3** — the
drive appears **twice**, and the two facts the controller needs are on **different rows**:
| row | source | `mount_path` | `guest_path` | `backup_target` |
|---|---|---|---|---|
| `felhom-backup` | Observe | `""` | `""` | **`true`** |
| `694034cc-…` | registry union | `/mnt/cel` | `/mnt/felhom-drives/cel` | field absent ⇒ **`false`** |
`driveTargetByPath` (controller `intermediary.go:602-618`) registers a key only from a **non-empty**
field, so the flag-bearing row contributes **no key at all**, while the registry row owns
`/mnt/felhom-drives/cel` — the key the gate looks up — and says `false`.
**Why v0.115.0 was inert, and it was not subtle.** Its fallback computed
`StablePathForRaw(t.MountPath)` on the Observe row, and in the absent state `t.MountPath` is `""`
emptied by the same `exactMount` failure that empties `BackingDevice` (`observe.go:184-190`). So it was
`StablePathForRaw("")` == `""` and it assigned nothing. **Its test passed because the fixture supplied a
`MountPath` the real absent state does not have.** That fixture is corrected in this release, and
red-proof 1 replays v0.115.0's exact code against the corrected shape: it fails.
**THE JOIN — the actual hard part.** With the device gone the two records of one drive share **no
runtime field**: no mount, no backing device, and the Observe row's `DurableID` has already degraded off
the fs-UUID (`path:/mnt/cel`). What they do share is **configuration** — the storage's `path` from
`storage.cfg` on one side, the `.mount` unit's `Where` on the other. Both yield the same stable guest
path, so that is the key the dedup now uses.
- `hub.StorageTarget.ConfigPath` — the configured path, carried verbatim, **`json:"-"`**. That struct is
a cross-repo contract pinned by `testdata/host-report.golden.json` + `contract_test.go`'s key-set
comparison; a wire-visible field would have required a matching `felhom.eu/hub` change to stay
non-drifting, and nothing off-box needs the value.
- `observe.go` sets it from `s.Path`. **This is NOT the fallthrough the comment at `:176-183` forbids**
that prohibition is about resolving a *device or UUID* from the **containing** filesystem, which would
hand back root's identity and mis-target a DR re-attach. `s.Path` is the storage's own declaration,
identifies nothing but itself, and is used for no resolution. `MountPath` stays empty, which is true.
- `disks.go` tries `MountPath` first (so the present-state path and v0.115.0's behaviour are
byte-identical) and falls back to `ConfigPath` only when the mount is genuinely gone.
- The union loop now dedups on guest path as well as mount path, so exactly **one** row carries the drive.
**THE REGRESSION THIS AVOIDED, and it is why the two obvious fixes were both rejected.** The controller
reads `d.BackupTarget && d.MountPath != ""` as *"a real drive with its own mountpoint — healthy"* and
returns immediately (`backup_target_offer.go:79`). So **back-filling `MountPath` onto the Observe row**
(the smallest change) and **teaching the registry row the flag** (its `MountPath` is non-empty, read from
the by-then-stale unit file) both produce a row satisfying that predicate while the drive is missing —
either would have silently flipped **R-114**, which shipped 2026-07-29, back to a false *healthy*.
**R-114's correctness currently rests on the absent-state rows not combining the flag with a mount
path**; that coupling was invisible until the payload existed. Pinned by
`TestAbsentTargetKeepsR114DegradedSignal`, red-proofed by feeding the handler exactly what the rejected
option would have produced.
**Deliberately unchanged:** `Role` (the absent target still reads `system`), the `BoundUnderParent`
conjunction, and every wire field. Suppressing the registry row in the absent state removes its
`uuid:` `durable_id`, its hardcoded — and false — `state: "attached"`, and its `total_bytes` copied from
the **root** filesystem (**R-118's symptom, incidentally gone; R-118 itself is NOT fixed and stays
open**). No consumer of those was found: wipe/decommission need the drive present, `firstOfferableDrive`
already excludes it, and the disk-health baseline excludes UNKNOWN verdicts.
**Tests 845 → 849.** Four red-proofs, each mutation verified to have landed before the run:
(1) v0.115.0's MountPath-only fallback → `isTarget[guestPath] = FALSE, rows=2`; (2) drop the guest-path
dedup → `carried by 2 rows`; (3) give the absent row a MountPath → the R-114 guard fires; (4) over-broad
dedup → the non-target drive loses its own row (and two pre-existing R-113 tests fail too).
`TestPresentTargetPayloadUnchanged` pins the healthy payload field-for-field — the state the whole fleet
is in.
## v0.115.0 — R-116: the backup-target flag reaches the row the controller keys on (2026-07-29)
**The defect, measured live in Session C.** A drive whose device vanished raised the **generic**