Found while executing the ruling on #16693 (which withdrew the ADR-0087 conversion that made the same conflation). ⛔ Not fixed there: the repair edits an accept/reject message on a contract schema, which is a different review class from that card's declaration, and the card's dispatch fences it.
⛔ Not proposing a wording. Which target notNull should name is a contract question — see the two readings at the end.
The measurement
ObjectStackDefinitionSchema.safeParse over one object with one field, on built dist/, origin/main c5ea982d97 + the #16693 branch (the diff there does not touch field.zod.ts).
SUBJECT — the spelling an author actually writes:
field: { type: 'text', label: 'F', notNull: true }
-> parse = false
Unrecognized key(s) on this field: `notNull`. Did you mean `notNull` -> `required`?
Until this shape was closed these were dropped silently — the field was still created,
minus whatever the key was meant to constrain, protect or compute.
SAME BRANCH, second spelling (so this is a branch, not a one-key typo table):
field: { type: 'text', label: 'F', not_null: true }
-> Unrecognized key(s) on this field: `not_null`. Did you mean `not_null` -> `required`?
POSITIVE CONTROL — the correct sentence exists, and is reachable, from a different key:
field: { type: 'text', label: 'F', storageNotNull: true }
-> Unrecognized key(s) on this field: `storageNotNull`.
• physical column constraints live under `storage` — write `storage: { notNull: true }`
(ADR-0113). `required` is the WRITE contract and deliberately does not imply the
column constraint.
NEGATIVE CONTROLS — the instrument can say "fine":
field: { type: 'text', label: 'F' } -> parse = true, required=false, storage=undefined
field: { type: 'text', label: 'F', storage: { notNull: true } } -> parse = true, storage={"notNull":true}
Why the prescription is the wrong one
packages/spec/src/data/field.zod.ts:860 puts notNull in the RENAME table beside genuine synonyms of the write contract:
isRequired: 'required', mandatory: 'required', notNull: 'required',
and :914 puts the ADR-0113 sentence in the prose-guidance table under the key storageNotNull. The rename branch is evaluated first, so for the two spellings anyone actually types, the prose never runs.
The file already knows this is the interesting case — :911-913, verbatim:
notNull is aliased to required above for the common case, but ADR-0113 makes the two deliberately distinct and the distinction IS the point, so the flattened spelling gets its own sentence rather than a rename.
⚠️ That comment describes the intended arrangement. The measurement above says the arrangement did not land on the spellings it was written for: the sentence sits behind storageNotNull, and notNull — the "flattened spelling" the comment is about — still gets the rename.
The consequence is exactly the confusion #16693 was filed over, delivered by the platform's own refusal:
- ADR-0113:
required is the write-time contract and "NOT a column constraint"; storage.notNull alone binds the column, and "absent = the column stays nullable even under required: true".
- An author (or an AI author) writes
notNull: true because they want a NOT NULL column. The refusal is loud — good — and then tells them to write required.
- They comply. They get a nullable column plus a write gate, and no further diagnostic. The refusal reads as if it had been satisfied.
⭐ For the ③ 防 AI 犯错 axis this is the bad shape twice over: the loud failure is present and does its job, but its remedy is what produces the wrong end state, and the wrong end state is SILENT. Nothing downstream refuses; the column is simply not what was asked for. The same conflation reached apps through the ADR-0087 conversion (#16693, ruled and removed) and reaches generated migrations through both generators (#16294) — this is its third carrier, and the only one that speaks directly to the author at the moment they are typing.
Two readings, ⛔ not settled here
notNull means the column — the plain reading of the word. Then it belongs in the guidance table with the storageNotNull sentence, and the rename entry is removed. Cost: an author who wrote notNull meaning "required" (the pre-17 conflation, when it meant both) now gets prose instead of a one-word rename.
notNull is a legacy synonym of required — which it was before ADR-0113 split the axes. Then the rename is right and the sentence needs to say both halves out loud, because the author cannot tell which one they are getting.
Either way the current state is the one thing that cannot be right: the same word gets a confident rename to the key whose whole documented purpose is that it is NOT that word.
Scope of this card
Measured on FieldSchema only. ⛔ Not audited: whether other rename tables in the spec point at keys an ADR has since split off, and whether isRequired / mandatory (genuine write-contract synonyms) are affected — read alone, both look correct.
Dedup
notNull + rename/alias returns no issue in this repo, open or closed. Semantically nearest and NOT this: #16693 (the ADR-0087 conversion making the same conflation — ruled, removed) · #16294 (both migration generators binding column NOT NULL to required) · #14193 (ADR-0113's own Context row stating the inverse — closed) · #16077 (a different key whose gate prescribes the wrong remedy) · #11668 (a lint suggestion naming a spelling the schema rejects).
Found while executing the ruling on #16693 (which withdrew the ADR-0087 conversion that made the same conflation). ⛔ Not fixed there: the repair edits an accept/reject message on a contract schema, which is a different review class from that card's declaration, and the card's dispatch fences it.
⛔ Not proposing a wording. Which target
notNullshould name is a contract question — see the two readings at the end.The measurement
ObjectStackDefinitionSchema.safeParseover one object with one field, on builtdist/,origin/mainc5ea982d97+ the #16693 branch (the diff there does not touchfield.zod.ts).SUBJECT — the spelling an author actually writes:
SAME BRANCH, second spelling (so this is a branch, not a one-key typo table):
POSITIVE CONTROL — the correct sentence exists, and is reachable, from a different key:
NEGATIVE CONTROLS — the instrument can say "fine":
Why the prescription is the wrong one
packages/spec/src/data/field.zod.ts:860putsnotNullin the RENAME table beside genuine synonyms of the write contract:and
:914puts the ADR-0113 sentence in the prose-guidance table under the keystorageNotNull. The rename branch is evaluated first, so for the two spellings anyone actually types, the prose never runs.The file already knows this is the interesting case —
:911-913, verbatim:storageNotNull, andnotNull— the "flattened spelling" the comment is about — still gets the rename.The consequence is exactly the confusion #16693 was filed over, delivered by the platform's own refusal:
requiredis the write-time contract and "NOT a column constraint";storage.notNullalone binds the column, and "absent = the column stays nullable even underrequired: true".notNull: truebecause they want a NOT NULL column. The refusal is loud — good — and then tells them to writerequired.⭐ For the ③ 防 AI 犯错 axis this is the bad shape twice over: the loud failure is present and does its job, but its remedy is what produces the wrong end state, and the wrong end state is SILENT. Nothing downstream refuses; the column is simply not what was asked for. The same conflation reached apps through the ADR-0087 conversion (#16693, ruled and removed) and reaches generated migrations through both generators (#16294) — this is its third carrier, and the only one that speaks directly to the author at the moment they are typing.
Two readings, ⛔ not settled here
notNullmeans the column — the plain reading of the word. Then it belongs in the guidance table with thestorageNotNullsentence, and the rename entry is removed. Cost: an author who wrotenotNullmeaning "required" (the pre-17 conflation, when it meant both) now gets prose instead of a one-word rename.notNullis a legacy synonym ofrequired— which it was before ADR-0113 split the axes. Then the rename is right and the sentence needs to say both halves out loud, because the author cannot tell which one they are getting.Either way the current state is the one thing that cannot be right: the same word gets a confident rename to the key whose whole documented purpose is that it is NOT that word.
Scope of this card
Measured on
FieldSchemaonly. ⛔ Not audited: whether other rename tables in the spec point at keys an ADR has since split off, and whetherisRequired/mandatory(genuine write-contract synonyms) are affected — read alone, both look correct.Dedup
notNull+ rename/alias returns no issue in this repo, open or closed. Semantically nearest and NOT this: #16693 (the ADR-0087 conversion making the same conflation — ruled, removed) · #16294 (both migration generators binding column NOT NULL torequired) · #14193 (ADR-0113's own Context row stating the inverse — closed) · #16077 (a different key whose gate prescribes the wrong remedy) · #11668 (a lint suggestion naming a spelling the schema rejects).