Skip to content

fix(spec): materialize deleteBehavior only on reference field types - #11903

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-9784-inert-deletebehavior-nonreference
Aug 25, 2026
Merged

fix(spec): materialize deleteBehavior only on reference field types#11903
os-warren merged 2 commits into
mainfrom
claude/issue-9784-inert-deletebehavior-nonreference

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#9784

Clause-②: yes (parse output changes for accepted inputs on a published face).

Scope — the re-priced remainder

The master_detail half of #9784 was delivered by merged PR #11406. This PR delivers what remained: bare NON-REFERENCE fields (text/datetime/number/…) no longer materialize the inert deleteBehavior: 'set_null' into parse output and built artifacts. The fix is the card's shape 1, seat-determined in the claim comment: a per-type conditional at the relocated .overwrite() site in packages/spec/src/data/field.zod.ts, beside the #9689 master_detail conditional and following the #11423 currency-twin idiom.

Step 0 — zero-readers premise re-verified on current main (base 387e231)

Every reader of deleteBehavior in the tree was enumerated (grep -rn deleteBehavior over packages/, examples/, scripts/, plus the objectui sibling checkout):

  • packages/objectql/src/engine.tscascadeDeleteRelations — ALL reads (behavior resolution at :10830-10831, the FieldSchema accepts deleteBehavior: 'set_null' on a master_detail, and the engine silently resolves it to cascade #9689 diagnostic at :10851, the restrict-message required read at :11036) sit inside the loop guarded by fdef.type !== 'master_detail' && fdef.type !== 'lookup' → continue (:10811) plus if (!ref) continue on fdef.reference (:10812-10813).
  • packages/lint/src/data-model-rules.ts:581 — inside if (type === 'master_detail'), itself inside a RELATIONSHIP_TYPES (= lookup, master_detail) filter.
  • packages/spec/src/data/field.form.ts / object.form.ts — form visibility predicates gated on data.type == 'lookup' / 'master_detail'.
  • objectui (packages/app-shell/.../predicate.ts) — the same form predicate, gated on type in ['lookup','master_detail'].
  • Everything else is comments, alias-lint key maps, migration-ledger prose, or authored 'cascade' declarations on master_detail/lookup fields (writers, not readers).

No code path reads the key off a non-reference field. Premise holds; no fork.

The change

At the .overwrite() site (after the authored-value early return and the #9689 master_detail short-circuit, both unchanged):

Reference-type list, verified against the code rather than the dispatch: the engine guard names master_detail/lookup + reference; the field-type family's Relational group is lookup/master_detail/tree. tree (hierarchical reference) keeps materializing with lookup — conservative byte-identity for the relational family, per the seat determination. One boundary observation the dispatch list implies but does not spell out: user (a lookup specialization, stored identically to lookup) sits OUTSIDE the engine's cascade guard exactly like text, so it takes the non-reference side and stops materializing — the same zero-readers argument applies (measured: the guard admits only master_detail/lookup), and an authored value there still round-trips. Flagged for the PM review; pinned in the tests.

Population measurement (showcase artifact, freshly built with the fixed spec)

examples/app-showcasedist/objectstack.json — 24 objects, 210 fields:

fields carrying deleteBehavior
before (materializing era)206 of 210 (every field except the 4 bare master_detail, per #11406)
after this PR16 of 210 — lookup 13/13, tree 1/1, authored-cascade master_detail 2/6
thinned190 non-reference fields (incl. 4 user), every one inert by the Step-0 measurement

The showcase dist is NOT checked in (build product) — no generated file in the repo changes shape. pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date" (the schema declaration itself — optional enum + meta default — is unchanged, so authorable-surface/json-schema artifacts do not move).

Fixture disposition (declared, none regenerated by hand): the two installed-base fixtures the card names (engine-audit-anchor-write.test.ts and protocol.audit-field-governance.test.ts, the #4447 audit-governance pins) deliberately KEEP their bytes — they pin artifacts of the materializing era, which remain in the wild and must keep loading; each now carries an era comment so nobody "modernizes" them and loses installed-base coverage. field.test.ts's showcaseVerbatim pin likewise keeps its bytes as an accept-set pin. The examples' authored deleteBehavior: 'cascade' declarations (invoice, expense-report, opportunity-line-item) are authored master_detail values — meaningful, untouched.

Migration disposition — PROPOSAL for PM adjudication (not silently decided)

The changeset is @objectstack/spec minor with NO breaking declaration and NO ADR-0087 entry, on the #11423 rec-A no-entry template, extended to this card's non-empty-but-inert impact:

  • Nothing an author wrote changes meaning or validity: no key removed/renamed/re-shaped, accept-set byte-identical, and zero non-artifact producers of the key on non-reference types exist in any measured corpus (the key only ever appeared there BY materialization).
  • The artifact delta is real but inert end to end: 190 keys leave the showcase artifact, and the Step-0 measurement shows no reader anywhere (engine, lint, forms, objectui) can observe the absence — the engine's lookup branch even carries its own fdef.deleteBehavior || 'set_null' fallback.
  • There is nothing for objectstack migrate meta to rewrite: stored rows carrying the materialized key stay legal (accept-set unchanged), and rewriting them to drop it would be cosmetic churn on data at rest.
  • The counter-argument for a ledger entry is visibility alone: artifact diffs after upgrade will show mass key removal, and an entry would give that diff a citable name. If the PM rules ledger visibility is owed, the entry is additive (an informational semantic entry, no rewrite prescription) and can land in this PR pre-merge — the changeset body already carries the FROM/TO story.

Reverse verification (committed-state, both legs; spec tests import ./field.zod relatively — src-to-src, no dist in the resolution path)

  • Fix committed first (f742c7e). Pre-fix source stood up via git restore --source=387e23138 (tree-only, no staging); mutation PROVEN on disk before the run: porcelain lone M, and grep -c of the fix's gate line = 0.
  • RED leg (expected direction: the omission pins fail by FINDING the key): exactly 2 failed / 168 passed — omits deleteBehavior from bare non-reference fields red with AssertionError: type=text: expected 'set_null' to be undefined, and the bare-user pin red. Every preservation pin (lookup/tree byte-identity, master_detail omission, authored round-trip, idempotency) stayed green against unfixed source, as predicted.
  • Restore proven on disk (porcelain clean, gate-line grep = 1); GREEN leg: 170/170.

Verification (all under the shared verify lock or declared-light foreground; exit codes captured before any pipe)

At head e7072f5 unless noted:

  • @objectstack/spec test: 424 files / 11259 passed; typecheck green; check:generated: "All 14 generated artifacts are up to date".
  • Consumer sweep (downstream direction): @objectstack/objectql4109 passed; @objectstack/metadata-protocol1914 passed / 10 pre-existing skips; @objectstack/lint2294 passed; @objectstack/example-showcase tsc green + 362 passed; CLI migrate-meta.e2e14 passed; @objectstack/dogfood926 passed / 3 pre-existing skips.
  • node scripts/pm/dispatch-gates.mjs (no paths — script derives the change set): derivation line "gate list derived from the tree of 'objectstack-ai/objectstack' at commit f742c7e … change set derived from git — 5 path(s) vs merge base 387e231". All 25 path-matched families + the 5 test-file convention families ran green locally, EXCEPT: check:type-check-debt --re-measure (needs the FULL workspace built — farm-scale, CI-owned; narrowed with evidence: the only TS-code diff is in packages/spec, whose own typecheck program — test tsconfig included — ran green; the objectql/metadata-protocol diffs are comment-only lines, which cannot move a tsc error count, and both packages' full suites ran green) and check:dev-prereqs (red locally for missing dist in 7 packages OUTSIDE this change's closure — an unbuilt-worktree fact, not a diff finding; CI builds fresh).
  • Final-head union re-run at e7072f5 (ratchet + changeset families): spec-liveness, strictness-ledger, empty-state, variant-docs, adr-0087-registration, changeset-no-major, empty-changeset, changeset-gate-self-tests, nul-bytes, engine-double-contract, where-matcher, merge-driver, cross-package-test-inputs, type-check-coverage — all exit 0.

H17: the currencyConfig region of field.zod.ts is untouched (its prose pointer to "the #9689 master_detail conditional" stays true — that conditional is kept verbatim).

Generated by Claude Code


Generated by Claude Code

Gate the relocated .overwrite() default so bare non-reference fields
(text/datetime/number/...) no longer materialize the inert
deleteBehavior: 'set_null' into parse output and built artifacts.
lookup/tree keep byte-identical materialization; master_detail keeps
omitting it (#9689 idempotent-materialization ruling); authored values
on any type round-trip verbatim (accept-set unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
…#9784 era; add changeset
The two #4447 audit-governance fixtures pin the PRE-#9784 artifact shape
(materialized deleteBehavior on a datetime) — artifacts of that shape
remain in the installed base and must keep loading, so the bytes stay;
the comments now say which era they are. Changeset: @objectstack/spec
minor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/external-datasources.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/field-types.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/validation-rules.mdx(via FieldSchema (symbol))
  • content/docs/deployment/troubleshooting.mdx(via FieldSchema (symbol))
  • content/docs/deployment/validating-metadata.mdx(via FieldSchema (symbol))
  • content/docs/getting-started/quick-reference.mdx(via FieldSchema (symbol))
  • content/docs/kernel/contracts/data-engine.mdx(via FieldSchema (symbol))
  • content/docs/protocol/backward-compatibility.mdx(via FieldSchema (symbol))
  • content/docs/protocol/objectql/types.mdx(via FieldSchema (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via FieldSchema (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 50369dc07d7948793c5d3680bd2242610ae2cccdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 8addd3c33eb804b3c38ae95eaba91eea49c14f71 — the merge of head e7072f546b6e024118d0201c1b889f0f3236e6cd into base 50369dc07d7948793c5d3680bd2242610ae2cccd, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8addd3c33eb804b3c38ae95eaba91eea49c14f71 && git checkout 8addd3c33eb804b3c38ae95eaba91eea49c14f71
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 50369dc07d7948793c5d3680bd2242610ae2cccd e7072f546b6e024118d0201c1b889f0f3236e6cd && git checkout -B drift-repro 50369dc07d7948793c5d3680bd2242610ae2cccd && git merge --no-ff e7072f546b6e024118d0201c1b889f0f3236e6cd
node scripts/docs-audit/affected-docs.mjs --json 50369dc07d7948793c5d3680bd2242610ae2cccd

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 50369dc07d7948793c5d3680bd2242610ae2cccd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FieldSchema.deleteBehavior defaults set_null onto EVERY field type, so built artifacts ship the key on text/datetime/number fields

2 participants

@os-warren@claude