Skip to content

Commit 3f45344

Browse files
committed
docs(adr-0104): backfill must precede the write cutover, not follow it
The 2026-07-27 addendum sequenced the write cutover before the backfill. That is wrong for the same reason the backfill must precede collection, one step further back: narrowing the accepted stored form while records still hold legacy values would reject any update that rewrites such a field, breaking working apps until the backfill catches up. Backfilling first leaves the cutover nothing legacy to reject. Also states plainly why the last two steps are held to different standards. The cutover breaks loudly and recoverably (a rejected write); enabling collection breaks silently and permanently (deleted bytes). Only the second earns the reconciliation evidence requirement, and neither it nor the migration run should happen without an explicit human decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
1 parent d0d28ff commit 3f45344

1 file changed

Lines changed: 27 additions & 9 deletions

File tree

docs/adr/0104-field-runtime-value-shape-contract.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -567,24 +567,42 @@ sequences as:
567567
actually holds an id, so it lands safely ahead of the cutover.
568568
2. **Governed download** — read authorisation derived from the one owning
569569
record; anonymous capability URL demoted to opt-in `acl: 'public_read'`.
570-
3. **Write cutover** (protocol major) — stored form narrows to an id;
570+
3. **Backfill** — converts legacy inline blobs and own-resolver URLs to
571+
`sys_file` references, which the claim hooks then own; external URLs are
572+
reported, never re-hosted. Dry-run by default, idempotent.
573+
4. **Write cutover** (protocol major) — stored form narrows to an id;
571574
`accept` / `maxSize` enforced. Still deletes nothing.
572-
4. **Backfill**`os migrate` converts legacy inline blobs to `sys_file` rows
573-
and claims them; external URLs reported, not converted.
574-
5. **Reconciliation soak**`os storage verify-references` compares what
575-
records actually hold against recorded ownership, reporting *over-claim*
576-
(safe: file retained longer than needed), *under-claim* (**blocking**: a held
577-
file with no owner would be treated as free), and unclaimed orphans.
575+
5. **Reconciliation soak**`verify-references` compares what records actually
576+
hold against recorded ownership, and splits disagreements by whether they can
577+
cause data loss: *blocking* (a held file nothing owns; a file held by a slot
578+
that does not own it; one file held by two slots) versus *advisory* (owned
579+
but no longer held — fails toward retention; unreferenced committed files —
580+
storage cost only).
578581
6. **GC enable***gated, irreversible*. Relaxing the `scope === 'attachments'`
579582
tombstone guardrail and extending the `sys_file` reap guard's sweep-time
580583
re-verify to the ownership columns **must ship in the same change**. Half of
581584
it is worse than none: tombstoning released files while the guard still
582585
re-verifies only `sys_attachment` — always empty for a field file — turns
583586
every release into a guaranteed byte delete rather than a risky one.
584587

588+
Backfill precedes the cutover for the same reason it precedes collection, one
589+
step further back: narrowing the accepted stored form while records still hold
590+
legacy values would reject any update that rewrites such a field, breaking
591+
working apps until the backfill catches up. Backfilling first leaves the cutover
592+
nothing legacy to reject.
593+
585594
R4's acceptance gate is now executable rather than aspirational: step 6 may not
586-
merge until step 5 reports **zero under-claims for ≥7 consecutive days** on real
587-
tenant data. R5 (public-posture inventory) and R6 (sub-key read scan) stand.
595+
merge until step 5 reports **zero blocking discrepancies for ≥7 consecutive
596+
days** on real tenant data. R5 (public-posture inventory) and R6 (sub-key read
597+
scan) stand.
598+
599+
Steps 4 and 6 are the two that can break something. Step 4 is a declared
600+
protocol major and breaks *loudly* — a rejected write, recoverable. Step 6
601+
breaks *silently and permanently* — deleted bytes. They are therefore held to
602+
different standards: step 4 rides the planned v17 window paired with the client
603+
adoption that consumes the new form, while step 6 additionally requires the
604+
reconciliation evidence above, and neither the enable nor the migration run
605+
should be performed without an explicit human decision.
588606

589607
### Why this stays inside ADR-0104 rather than a new ADR
590608

0 commit comments

Comments
 (0)