Skip to content

[finding] FieldSchema refuses notNull / not_null by prescribing required — the one key ADR-0113 says is NOT a column constraint — while the correct sentence is reachable only from storageNotNull, a spelling nobody writes #16867

Description

@os-musk

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

  1. 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.
  2. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions