Skip to content

ADR-0087's field-required-notnull-explicit conversion asserts an implication ADR-0113 abolished — the boot calls it a forward conversion, but its output and the source it prescribes disagree at the storage layer #16693

Description

@hotlong

Filed from an app repo (objectstack-ai/hotclm, a CLM sample app on @objectstack/spec ^17.0.0, runtime 17.3.0, better-sqlite3 driver). Measured, not reasoned — the measurements are below and each negative has its positive control. ⛔ Unassigned, ungraded; routing is triage's.

Dedup: field-required-notnull-explicit returns 0 issues in this repo, open or closed. Related but not this: #14193 (ADR-0113's Context row dated — closed, PR #15160; it is the record that makes this card legible) · #13743 (the notice's wording duplicated across three commands — closed; about the wording, not its truth).

What the boot says

An artifact authored at protocol floor 17.0.0, loaded on runtime 17.3.0:

⚠ Boot diagnostics — 1 warning logged during startup:
  WARN [MetadataPlugin] artifact 'dist/objectstack.json' predates this runtime's spec
  (authored engines.protocol floor 17.0.0, runtime spec 17.3.0) — converted 40 site(s)
  forward via ADR-0087 conversion 'field-required-notnull-explicit'
  (first at objects[0].fields.name.storage.notNull). … Update the source to 'storage.notNull: true'.

Two claims are made there: the metadata has been converted forward (i.e. its original meaning preserved under the new spelling), and the author should write that spelling into the source to keep it once protocol 18 retires the conversion.

The measurement: those two are not the same metadata

Same repo, same runtime, same driver, same database file. required: true on 40 fields across 11 objects; the only variable is whether storage: { notNull: true } is written in the source or supplied by the conversion.

source has required: true only (conversion runs) source has both (conversion silent)
ADR-0087 warning 1 — "converted 40 site(s) forward" 0
NOT NULL columns created in a fresh database 0 40
[schema-drift] lines on an existing database 0 40 (tighten_not_null, severity: error, category: destructive)
INSERT INTO clm_party (id) VALUES (?) ACCEPTED REFUSEDNOT NULL constraint failed: clm_party.name
INSERT INTO clm_clause (id) VALUES (?) ACCEPTED REFUSEDNOT NULL constraint failed: clm_clause.title

Both inserts rolled back. The 40 NOT NULL columns in the right-hand column map 1:1 onto the 40 declared sites, none missing and none extra. The two rows of the table are the same 40 fields with the same declared required: true; only the spelling differs.

A conversion described as forward-preserving produces metadata that behaves differently from the source it tells you to write. The conversion's output leaves the columns nullable; the prescribed source makes them NOT NULL. Whichever is right, they cannot both be what protocol 18 will mean.

The second leg was re-measured on a container where the auth plugin failed to load entirely (#16186) — the DDL result is identical, so it does not depend on auth, seeding, or plugin order.

Why this is a contradiction and not just a gap

ADR-0113 (0113-required-write-contract-vs-column-constraint.md, Accepted 2026-07-30) split the two axes on purpose: required is the write-time contract and "NOT a column constraint"; storage.notNull is the physical column, "absent = the column stays nullable even under required: true". Its P0 shipped, and sql-driver.ts#createColumn keys off storage.notNull alone — the triage ruling on #14193 confirms exactly this and says the pre-P0 wording had merely outlived its decision.

The ADR-0087 conversion adds storage: { notNull: true } wherever it finds required: true. That is precisely the implication ADR-0113 abolished. So one of these must be true, and the platform should say which:

Reading A — the conversion is over-reaching. If required never implied a column constraint under 17.x, the conversion is inventing one. Then it should not add storage.notNull at all, and the warning should not tell authors to write it: today it instructs every app on a ^17.0.0 range to tighten its columns as the remedy for a deprecation notice.

Reading B — the conversion is right about old artifacts, and the runtime is not applying it. If the implication genuinely held at floor 17.0.0, converting forward is correct — but then the converted metadata must reach the SQL driver, and it does not: the boot that logged "converted 40 site(s)" created 40 nullable columns and reported no drift. Under this reading the conversion is a read-path shim that announces a storage guarantee it does not deliver, and protocol 18 will silently relax those columns for real.

I cannot settle A vs B from an app repo — it turns on whether required bound the column in shipped 17.0.0, which is your record, not mine.

The consequence that made this worth filing

The instruction in the warning is not a no-op, and it does not look like a schema change to the person following it:

  • On a fresh database it creates 40 NOT NULL columns.
  • On an existing database it produces 40 [schema-drift] findings, tighten_not_null, severity: error, category: destructive — the class the platform deliberately never applies unattended, requiring a backfill ceremony.
  • Boot diagnostics still reports 0 warnings in that state, so the drift lines are the only signal.

In our case the tightening was wanted, and we took it deliberately (objectstack-ai/hotclm#8, PR objectstack-ai/hotclm#22). But we took it because we measured the columns, not because the warning told us what it would do — the warning presents it as clearing a deprecation. An app that follows the same instruction against a populated production database gets a destructive migration it did not know it was asking for.

Note the trigger is a declared floor, not an artifact's actual age: this app was authored in September 2026, well after ADR-0113, and its required: true was written with 17.3.0 semantics. It gets the conversion only because its dependency range says ^17.0.0. Any app on a permissive range is treated as pre-ADR-0113 metadata regardless of when it was written — which is a reasonable default for a conversion, and a bad one for a destructive inference.

Reproduce

git clone https://github.com/objectstack-ai/hotclm && cd hotclm && pnpm install
git checkout 9025992                 # required: true only
rm -rf .objectstack/data && pnpm dev --seed-admin    # logs the ADR-0087 warning
#   -> PRAGMA table_info: 0 NOT NULL columns; INSERT INTO clm_party (id) succeeds
git checkout baf5cc3                 # + storage: { notNull: true }, 40 insertions, 0 deletions
rm -rf .objectstack/data && pnpm dev --seed-admin    # no warning
#   -> 40 NOT NULL columns; the same INSERT fails with NOT NULL constraint failed: clm_party.name

Reverse-control for the zero: on the same database, count NOT NULL columns across all tables — the before database returns 0 across all 37, which is the platform's own posture and not an artefact of reading the wrong table.

Boundaries

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions