Uh oh!
There was an error while loading. Please reload this page.
fix(spec): reject an authored deleteBehavior 'set_null' on a master_detail at parse time; log the engine coercion loudly - #11406
Conversation
…l at parse time
Relocate the property-level .default('set_null') off deleteBehavior into a
post-check .overwrite() (the currency-precision Option A shape) so the
field-level superRefine can tell authored from defaulted; an authored
set_null on a master_detail is now a named parse-time rejection, while parse
output stays byte-identical for every accepted input (shape-position
re-materialization, measured 13/13 on a before/after battery). Ship the
SemanticMigration entry (no automatic conversion - only the author knows
whether they meant restrict or cascade), and log loudly at the engine's
coercion site when the combination still arrives via a raw registration or a
pre-tightening stored row (error, falling back to warn). Engine behavior is
unchanged; the existing cascade pin stays true. Docs rows that described the
silent acceptance now state the rejection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2The coercion-site log fires whenever a parent delete computes a misdeclared child field's behavior - with or without child rows - so the negative control must not register the misdeclared object at all. Message wording adjusted to not assert rows exist (CASCADES its referencing child rows). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2
📓 Docs Drift CheckThis PR changes 2 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 127 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 01cd6da514d259adfc3dffbbdcb1aa58b2369c1b && git checkout 01cd6da514d259adfc3dffbbdcb1aa58b2369c1b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bb006446adb558e5a86c009df9c0b08e98833011 6c54f1650bb276431f91c926808ba5f63be074a2 && git checkout -B drift-repro bb006446adb558e5a86c009df9c0b08e98833011 && git merge --no-ff 6c54f1650bb276431f91c926808ba5f63be074a2
node scripts/docs-audit/affected-docs.mjs --json bb006446adb558e5a86c009df9c0b08e98833011
|
Fixes#9689
Implements the maintainer ruling of 2026-08-19 (「接受你的所有建议。」, recorded on the card): Q1 = A (the #7918 Option A relocation, exactly as spiked), Q2 = C (no automatic conversion —
SemanticMigrationentry only), Q3 = B (spec rejection plus a loud engine log at the coercion site), Q4 = B (maintainer authors the ADR — drafted below,docs/adr/**untouched).What changed
packages/spec/src/data/field.zod.ts—.default('set_null')moved offdeleteBehaviorto.optional()+.meta({ description, default: 'set_null' }); a per-type check in the field-levelsuperRefinemakes an AUTHOREDset_nullon amaster_detaila named parse-time rejection (pathdeleteBehavior, message states the actual outcome — children DELETED, not kept — and names both legal re-declarations plus thelookupalternative); a post-check.overwrite()re-materializes'set_null'at its shape position, so parse output is byte-identical to the.default()era. The schema stays aZodObject(shape enumerable, 71 keys — no pipe degradation). Accepted cost, same as the currencyprecisionprecedent: the inferredFieldoutput type now declaresdeleteBehavior?(ADR-0122 forbids hand-narrowing); a parsed field always carries it at runtime.packages/spec/src/migrations/entries/semantic/18.field-master-detail-set-null-refused.ts(+ regeneratedregistry.ts) — the Q2 = CSemanticMigrationentry,field-scale-precision-integer-refusedprecedent, with option B's reasoning stated in thereplacementtext ("restrictis the only rewrite that cannot lose data").packages/objectql/src/engine.ts(cascadeDeleteRelationsonly) — the coercion stays (Q3 = B; engine behavior unchanged). It now logs loudly when an authoredset_nullon amaster_detailreaches the coercion site — the populations parse-time rejection measurably cannot catch (raw registrations; rows stored before the tightening — the engine registers raw objects and never re-parses, measured: the pin suite is green with the spec change alone). Sanctioned #9750 shape: reach forerror, fall back towarn, never an optional call.Tests — spec: rejection pinned (path + message head + both remedies named); bare
master_detailstill parses and still materializesset_nullat shape position (byte-identity pin); authoredcascade/restrictuntouched;set_nullonlookup(required and not) stays legal; the #4447 showcase-verbatim datetime shape stays legal; shape enumerability pinned. objectql: the #9625 pina master_detail declaring an explicit deleteBehavior:set_null still cascadesstays true and untouched; a new describe pins the log firing exactly once on the authored case (error path and warn-fallback path), and NOT firing on bare/cascade(same resolved behavior, no divergence).Docs (declared surface addition — two sentences the diff falsifies):
content/docs/protocol/objectql/types.mdxand themaster_detailrow ofcontent/docs/data-modeling/field-types.mdxsaid an explicitset_nullis accepted-but-not-honored; both now state the parse-time rejection and the raw/stored residual. These are the exact sentences #9625 wrote to document the behavior this card changes; leaving them would make the docs affirmatively wrong about the authorable surface. Not in the claim's file list — flagged as a deviation, see below.Changeset:
minorfor@objectstack/specand@objectstack/objectql(launch window forbidsmajor;check-changeset-no-majorrespected).Measurements
origin/main@ 735f5c7): exactly 1 authoredmaster_detail+set_nullrepo-wide — the Docs and engine disagree ondeleteBehavior: 'set_null'written EXPLICITLY on a required lookup — the escalation torestrictcannot see the difference #9625 pin fixture itself (engine-cascade-delete.test.ts:181, drifted from :133 as predicted). Zero inexamples/,create-objectstacktemplates, or any app object. The newset_nullhits landed since 08-18 (protocol.audit-field-governance.test.ts:144,engine-audit-anchor-write.test.ts:251,engine-cascade-delete-multivalue-probe.test.ts:99) are datetime/datetime/lookup — nonemaster_detail, none affected. Q2 = C's basis unchanged.md_set_nullflipped PARSED → REJECTED as designed; shape keys 71 → 71.sys_metadatarehydration replays conversions and never re-parses (database-loader.ts) — unaffected.registry.registerItemvalidation is a diagnostic, not a gate ([P2] The spec contract stops at authored source — stored metadata is rehydrated unparsed, unconverted, and ungated #3903) — a stored/artifact row carrying the refused combination keeps loading and serving, flagsmetadata_spec_invalid, and is refused on its next authoring-path save (the Q2 = C re-declare flow). The publish path collects it as a validation error — the intended authoring seam.ObjectSchema.parseoutput for a baremaster_detailre-parsed throughObjectSchemanow rejects atfields.parent.deleteBehavior— inherent to the ruled byte-identical design (the built-artifact materialization mechanism of data: created_at is client-writable on an ordinary PATCH — the audit anchor can be forged silently #4447). Consequences are the diagnostic/log residuals above, and this is deliberatelyFieldSchema.deleteBehaviordefaultsset_nullonto EVERY field type, so built artifacts ship the key ontext/datetime/numberfields #9784's territory (see below), not softened here.Q4 = B — draft ADR entry (for the maintainer; NOT written to
docs/adr/**)Required answer — effect on #9784
This change makes #9784more urgent and its edit easier, its decision slightly harder:
FieldSchema.deleteBehaviordefaultsset_nullonto EVERY field type, so built artifacts ship the key ontext/datetime/numberfields #9784's enabling step. The default now materializes in exactly one place — the.overwrite()— so gating it per type (option C's shape) is a one-conditional edit instead of re-plumbing a property default.master_detail, the materialized default is now a value the schema itself rejects on re-parse (measured round-trip above). SoFieldSchema.deleteBehaviordefaultsset_nullonto EVERY field type, so built artifacts ship the key ontext/datetime/numberfields #9784's fix must decide aboutmaster_detailtoo, not only the inert non-reference types: stop stamping the default there (artifact-moving, as its card already prices) or accept the diagnostic/log residual. UntilFieldSchema.deleteBehaviordefaultsset_nullonto EVERY field type, so built artifacts ship the key ontext/datetime/numberfields #9784 lands, an artifact-loaded baremaster_detaillogs at the coercion site (imprecision noted in the engine comment, deliberately not softened — the ruling's gate is the authored value).Deviations
claude/issue-9689-master-detail-set-null-r2(claim says…-set-null). The stale probe branch at2065e31d5is not an ancestor of currentmain(old-main history; upstream history divergence), so a plain push is non-fast-forward; branch deletion is refused by the proxy (403 on both the git and REST paths), and force-push is contract-banned for this seat even though the dispatch offered it (prompt-vs-contract conflict resolved toward the contract, as the contract instructs). The stale branch carries zero commits of its own and was left in place.engine-cascade-delete.test.tspin sat at ~:181 — confirmed exactly.Gates
Derived from the actual diff via
node scripts/pm/dispatch-gates.mjs(no paths passed; answer verified as objectstack-ai/objectstack @ cbe02bf) — per-gate exit codes in the os-dev report on #9689. Full@objectstack/specsuite (419 files / 11143 tests), full@objectstack/objectqlsuite (229 files / 4063 tests at cbe02bf), spec regen discipline (build+check:generated— all 14 generated artifacts up to date, zero movement), and the derived families run locally.needs:contract-reviewis hung card-side (clause-②): this PR stays draft and is held for contract review. Out of scope, filed separately: the publishedobjectstack-dataskill and the field designer form both still offerset_nullonmaster_detail(issue numbers in the os-dev report; the skill fix is a governed surface and must be its own PR).Generated by Claude Code