Skip to content

Commit d7851cf

Browse files
authored
docs(adr): ADR-0104 addendum — evidence for the remaining #3438 strict flips (#3438) (#4188)
D1 references + structured JSON gate on a per-deployment `os migrate value-shapes` scan (evidence CAN exist — data at rest is enumerable); fresh datastores attest both flags at creation (emptiness is observed, not version-inferred); the D2 action-param flip rides the 18.0 major with a served warn window and an `OS_ALLOW_LAX_ACTION_PARAMS` escape hatch (evidence CANNOT exist — callers are not enumerable). Design doc only — releases nothing.
1 parent f6472d7 commit d7851cf

2 files changed

Lines changed: 159 additions & 1 deletion

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(adr): ADR-0104 addendum — evidence for the remaining #3438 strict flips (D1 references/structured JSON gate on a per-deployment `os migrate value-shapes` scan; fresh datastores attest both flags at creation; the D2 action-param flip rides the 18.0 major with an escape hatch) — releases nothing.

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

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
- **Status**: Accepted (2026-07-22) — staged per D4. D1 landed (#3429), D2
44
landed (#3432); D3 refined into two waves by the 2026-07-24 addendum below,
55
and wave 2's ownership model settled by the 2026-07-27 addendum.
6-
Strict-default flip of D1/D2 tracked in #3438; wave 2 sequence in #3459.
6+
Strict-default flip of D1/D2 tracked in #3438 — the media half flips per
7+
deployment (#3681); evidence for the remaining halves decided by the
8+
2026-07-30 addendum. Wave 2 sequence in #3459.
79
- **Date**: 2026-07-22
810
- **Issue**: design follow-up generalizing #3405 / #3406 (inline lookup param
911
silently stripped); relates #3407 (silently dropped writes), #1878 / #1891
@@ -716,3 +718,155 @@ separate ADRs, because each settles *how* D3's already-accepted "field values
716718
point into `sys_file`" reaches production rather than revisiting whether to do
717719
it. A future choice that stands on its own (say, a chunked-migration protocol,
718720
or byte-layer content dedup) would earn its own ADR.
721+
722+
## Addendum (2026-07-30) — evidence for the remaining flips: a scan gate for references / structured JSON; the action-param flip rides a declared major
723+
724+
The 2026-07-27 amendment split #3438 into three and settled only the first:
725+
media value shapes flip per deployment, on the `adr-0104-file-references` flag
726+
(#3681). The other two were left "blocked on someone deciding what would
727+
constitute evidence." This addendum decides — differently for the two, because
728+
their facts have different epistemic reach — and fixes the release vehicle for
729+
each around the v17 major.
730+
731+
One timeline fact frames everything: **warn-first itself first reaches stable
732+
deployments in 17.0.** D1 and D2 ride the v17 train (their changesets sit in
733+
the same pre-mode window as this addendum), so no stable deployment has served
734+
a single day of the warn window yet. Whatever the right gate is, 17.0 cannot
735+
flip any default version-wide without deleting the warn window R2/R3 promised
736+
— independent of the per-deployment argument.
737+
738+
### D1 references + structured JSON: the evidence can exist, so build it
739+
740+
The fact strict enforcement needs is: *no stored value of the covered classes
741+
fails `valueSchemaFor(field, 'stored')`.* The covered classes are exactly the
742+
validator's own non-media branch — `REFERENCE_VALUE_TYPES` (single-value
743+
`lookup` / `master_detail` / `user` / `tree`) and `STRUCTURED_JSON_TYPES`
744+
(`location` / `address` / `composite` / `repeater` / `record` / `vector`;
745+
`json`'s derived schema is deliberately `z.unknown()`, so it can yield no
746+
findings). Unlike caller behaviour, data at rest is enumerable: a scan that
747+
walks every row is **complete** evidence, with the same epistemic standing the
748+
file reconciliation has. So the gate takes the #3617 shape, minus the
749+
backfill:
750+
751+
```
752+
os migrate value-shapes
753+
1. scan every object's covered fields against valueSchemaFor(field, 'stored')
754+
2. report violations per (object, field, type): count, sample record ids,
755+
first parse issue — the per-deployment form of R1's stored-data audit
756+
3. zero blocking findings + --apply → record sys_migration
757+
{ id: 'adr-0104-value-shapes', verified_at, blocking: 0 }
758+
4. strict enforcement of the covered classes reads THAT row, never the
759+
version number
760+
```
761+
762+
- **There is nothing to convert.** A malformed `location` payload, or an
763+
expanded object stored where an id belongs, is an application-data fix only
764+
its owner can make: the command reports and prescribes; the operator fixes
765+
and re-runs until green. (A mechanical normaliser — say, collapsing a stored
766+
expanded-lookup object to its `id` — is deliberately *not* built until real
767+
reports show the shape common enough to justify one; a normaliser that
768+
guesses is how silently-wrong values got here.) With no conversions,
769+
`--apply`'s only write is the flag row — which keeps the #3617 invariant
770+
intact: a dry run changes nothing, and whether a run changed the
771+
deployment's posture never depends on what the run found.
772+
- **The scanner and the validator share one predicate.** Same class sets,
773+
same cached `valueSchemaFor`, same skip rules (`system` / `readonly` /
774+
lifecycle columns are never validated, so they are never scanned): the scan
775+
must count exactly what strict mode would reject on that record's next
776+
rewrite, and nothing else. Imported, not re-derived — hand-copied value
777+
knowledge is the disease this ADR exists to cure.
778+
- **Gate consumption mirrors the media half verbatim**: the engine reads the
779+
flag once per process (memoized), an object declaring no covered field never
780+
consults it, `ValidateRecordOptions` gains the parallel option beside
781+
`mediaValueShapeStrict`, and a later failing run clears `verified_at` — a
782+
regression closes its own gate.
783+
- **Precedence, and the one new env var**: `OS_ALLOW_LAX_VALUE_SHAPES` (an
784+
`OS_ALLOW_*` escape hatch per Prime Directive #9, scoped to these classes —
785+
media keeps its own `OS_ALLOW_LAX_MEDIA_VALUES`) beats
786+
`OS_DATA_VALUE_SHAPE_STRICT_ENABLED` (unchanged: still the all-class
787+
opt-in) beats the flag. Same contradictory-config rule as
788+
`mediaStrictEffective`, for the same reason: wrongly lax costs a warning,
789+
wrongly strict stops a working app.
790+
791+
### Fresh datastores attest at creation — both flags
792+
793+
Today nothing records a flag except a migration run, which leaves a gap at
794+
the other end of a deployment's life: a deployment born on ≥17 starts **lax**
795+
and stays lax until its operator thinks to run a migration that is, for them,
796+
a no-op. The warn regime would never die out — every new deployment re-enters
797+
it.
798+
799+
So the bootstrap that creates an empty datastore records both rows
800+
(`adr-0104-file-references`, `adr-0104-value-shapes`) at creation time. This
801+
is not version-gating in disguise: the fact recorded — *no legacy value
802+
exists here* — is **observed** (the store is empty by construction at the
803+
moment of creation), the same observed-transition discipline the GC gate runs
804+
on. An existing database upgrading to 17 is non-empty at boot, gets no
805+
attestation, and keeps producing its evidence by scan. A later legacy import
806+
into a born-strict deployment is rejected loudly at the write path — the
807+
correct outcome; an operator who must temporarily admit such data has the
808+
escape hatch, and re-verifies by re-running the scan. The exact seam is the
809+
implementing PR's decision (candidates: the system seed that first creates
810+
`sys_migration`, or the engine's schema bootstrap), with one hard
811+
requirement: it fires only for a store it is itself creating from empty,
812+
never for one it found.
813+
814+
Born-strict deployments also make the platform's own dogfood the standing
815+
canary for R2 (a codified shape stricter than some legitimate client's
816+
writes): showcase/CRM boots are fresh datastores, so they enforce from birth,
817+
and a false rejection fails our suites before any customer sees it.
818+
819+
### D2 action params: the evidence cannot exist, so the honest gate is a declared major
820+
821+
What per-deployment evidence would have to prove is: *no caller of this
822+
deployment still depends on the lax contract.* Callers are not enumerable —
823+
an integration that fires monthly, a script in a cron nobody remembers, an AI
824+
agent constructing each call fresh. Data at rest can be scanned to
825+
completion; future calls cannot. A "quiet for N days" flag would assert a
826+
completeness it cannot have — the unlocatable-gate error of the 2026-07-27
827+
addendum, reproduced one layer down. A gate must not claim evidence that
828+
cannot exist, and we decline to build the theatre of one.
829+
830+
What remains is the failure-mode calculus that already sequenced wave 2: a
831+
strict rejection here is **loud** (a 400 naming the offending param and the
832+
declared list), **caller-facing** (a developer or an AI mid-integration — not
833+
an end user stranded in a form above data they cannot fix), **recoverable**
834+
(fix the call), and **reversible** (the escape hatch below). No data is
835+
stranded, no byte is deleted. That is precisely the class of break wave 2's
836+
step 4 was allowed to ride a declared major for, while step 6 waits on
837+
evidence.
838+
839+
So, owned explicitly rather than smuggled: **the D2 flip is a version flip —
840+
on the major *after* the warn window, not this one.** 17.0 ships warn-first
841+
(first exposure) and announces the coming default in its release notes; 18.0
842+
flips `actionParamsStrict()` to default-true. The v17→v18 gap is every
843+
deployment's own warn window, served on its own upgrade schedule; the warn
844+
line already names `OS_ACTION_PARAMS_STRICT_ENABLED=1`, which is the
845+
per-deployment act of opting into tomorrow's default today. Version-wide
846+
flips remain wrong where per-deployment evidence is *possible* (D1); where it
847+
is impossible in principle, a declared major with a served warn window and an
848+
escape hatch is what honest looks like. The alternative — warn forever —
849+
permanently un-enforces the declared contract at exactly the surface (AI/MCP
850+
callers) D2 was built for, where a 400 is corrective feedback the caller
851+
consumes and a server-side warn is feedback nobody sees.
852+
853+
Mechanics at 18.0: `OS_ACTION_PARAMS_STRICT_ENABLED` stays accepted — it
854+
becomes a redundant opt-in to the default; its semantics never change, so no
855+
`readEnvWithDeprecation` rename, the same conclusion the media half reached.
856+
A new `OS_ALLOW_LAX_ACTION_PARAMS` opt-out wins over everything (the
857+
contradictory-config rule again), and the dogfood contract suite
858+
(`action-params-contract.dogfood.test.ts`) flips its duals so the *default*
859+
path is the strict one and the escape hatch is the explicitly-set case.
860+
861+
### What rides where
862+
863+
| vehicle | change |
864+
|---|---|
865+
| 17.0 | warn-first first ships (D1 non-media + D2). `os migrate value-shapes` + the `adr-0104-value-shapes` gate wiring. Fresh-datastore attestation of both flags. Release notes announce the 18.0 D2 default. **No version-wide default changes.** |
866+
| each deployment, ≥17.0 | runs `os migrate value-shapes --apply` (or is born attested) → its own D1 non-media classes flip to strict. |
867+
| 18.0 | D2 strict by default + `OS_ALLOW_LAX_ACTION_PARAMS`; dogfood duals flip. |
868+
869+
#3438 then tracks two work items instead of an open question: the scan
870+
migration (command, engine wiring, creation-time attestation) targeting the
871+
17.0 train, and the D2 flip parked for the 18.0 train with its release-note
872+
announcement landing now.

0 commit comments

Comments
 (0)