Uh oh!
There was an error while loading. Please reload this page.
fix(spec): materialize deleteBehavior only on reference field types - #11903
Conversation
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
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9784
Clause-②: yes (parse output changes for accepted inputs on a published face).
Scope — the re-priced remainder
The
master_detailhalf of #9784 was delivered by merged PR #11406. This PR delivers what remained: bare NON-REFERENCE fields (text/datetime/number/…) no longer materialize the inertdeleteBehavior: '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 inpackages/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
deleteBehaviorin the tree was enumerated (grep -rn deleteBehaviorover packages/, examples/, scripts/, plus the objectui sibling checkout):packages/objectql/src/engine.tscascadeDeleteRelations— ALL reads (behavior resolution at :10830-10831, the FieldSchema acceptsdeleteBehavior: 'set_null'on amaster_detail, and the engine silently resolves it tocascade#9689 diagnostic at :10851, the restrict-messagerequiredread at :11036) sit inside the loop guarded byfdef.type !== 'master_detail' && fdef.type !== 'lookup' → continue(:10811) plusif (!ref) continueonfdef.reference(:10812-10813).packages/lint/src/data-model-rules.ts:581— insideif (type === 'master_detail'), itself inside aRELATIONSHIP_TYPES(= lookup, master_detail) filter.packages/spec/src/data/field.form.ts/object.form.ts— form visibility predicates gated ondata.type == 'lookup'/'master_detail'.packages/app-shell/.../predicate.ts) — the same form predicate, gated ontype in ['lookup','master_detail'].'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):lookup/treekeep materializingset_nullbyte-identically, at shape position (betweenreferenceandhidden).master_detailkeeps omitting it (FieldSchema acceptsdeleteBehavior: 'set_null'on amaster_detail, and the engine silently resolves it tocascade#9689 idempotent-materialization ruling — untouched).deleteBehavioron any type round-trips verbatim, so installed-base artifacts (which carry the materialized key on every field) remain fully legal inputs.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 islookup/master_detail/tree.tree(hierarchical reference) keeps materializing withlookup— 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 liketext, 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:deleteBehavioruser), every one inert by the Step-0 measurementThe 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.tsandprotocol.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' authoreddeleteBehavior: '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/specminor 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:fdef.deleteBehavior || 'set_null'fallback.objectstack migrate metato 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.Reverse verification (committed-state, both legs; spec tests import
./field.zodrelatively — src-to-src, no dist in the resolution path)git restore --source=387e23138(tree-only, no staging); mutation PROVEN on disk before the run: porcelain loneM, andgrep -cof the fix's gate line = 0.omits deleteBehavior from bare non-reference fieldsred withAssertionError: type=text: expected 'set_null' to be undefined, and the bare-userpin red. Every preservation pin (lookup/tree byte-identity, master_detail omission, authored round-trip, idempotency) stayed green against unfixed source, as predicted.Verification (all under the shared verify lock or declared-light foreground; exit codes captured before any pipe)
At head e7072f5 unless noted:
@objectstack/spectest: 424 files / 11259 passed; typecheck green;check:generated: "All 14 generated artifacts are up to date".@objectstack/objectql4109 passed;@objectstack/metadata-protocol1914 passed / 10 pre-existing skips;@objectstack/lint2294 passed;@objectstack/example-showcasetsc green + 362 passed; CLImigrate-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 inpackages/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) andcheck: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).H17: the currencyConfig region of
field.zod.tsis 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