Filing unassigned — recording, not claiming. Measured while working #9689; this finding survives whatever #9689 decides, which is why it is carded separately rather than folded in.
Measured
packages/spec/src/data/field.zod.ts (one line, currently ~906):
deleteBehavior: z.enum(['set_null','cascade','restrict']).optional().default('set_null')The default is on the shared field schema with no per-type gating, so it materializes at parse for every field of every type. Measured with a real FieldSchema.safeParse:
bare type='lookup' -> deleteBehavior = "set_null"
bare type='datetime' -> deleteBehavior = "set_null"
bare type='text' -> deleteBehavior = "set_null"
deleteBehavior has no meaning on a non-reference field — nothing reads it there. cascadeDeleteRelations only ever reaches the key after an fdef.reference guard, so on a text field the value is inert by construction.
Why it is not merely cosmetic
The materialized key ships in the app artifact. packages/objectql/src/registry.ts already documents the consequence for a sibling key, from a real incident (#4447):
the showcase artifact ships a materialized created_at carrying only FieldSchema DEFAULTS (readonly: false), which shadowed AUDIT_FIELD_DEFS.created_at (readonly: true)
That is the same mechanism: a default materialized at parse becomes an explicit declaration downstream, and explicit declarations win merges. Two fixtures in the tree carry the artifact shape verbatim and show deleteBehavior: 'set_null' sitting on a datetime field:
packages/objectql/src/engine-audit-anchor-write.test.ts:251 (commented "Verbatim from examples/app-showcase/dist/objectstack.json")packages/metadata-protocol/src/protocol.audit-field-governance.test.ts:144
This is the ADR-0049 declared-but-inert shape, and it is also an AI-authoring hazard in the direction ADR-0033 cares about: a model reading a built artifact sees deleteBehavior on a text field and reasonably concludes the key is meaningful there.
Interaction with #9689 (why this is separate)
#9689 asks whether deleteBehavior: 'set_null' should be refused on a master_detail. The measured answer is that any such rule — and equally any delete-time "you declared set_null" log — must first relocate this default (the #7918 Option A pattern), because otherwise it fires on all 95 bare master_detail declarations rather than the 1 authored one.
But the recommended relocation deliberately keeps parse output byte-identical (.overwrite() re-materializes set_null), so after #9689 lands, text fields still carry the inert key. This finding therefore outlives it.
Not prescribing the fix
Roughly two shapes, and they are not equivalent:
- Gate the default on reference types — materialize
deleteBehavior only for lookup / master_detail / tree. Cleanest, but changes parse output for every non-reference field, so it moves the app artifact and is a real (if mechanical) migration. - Drop the property-level default entirely and let each consumer apply its own fallback — the engine already spells
fdef.deleteBehavior || 'set_null' on the lookup branch, so the fallback exists there already.
Blocked-by: #9689 (the relocation it needs is the same edit; doing them in the other order means touching the line twice).
Refs: #9689, #9625, #7918 (the default-relocation precedent), #4447 (the materialized-default shadowing incident), ADR-0049.
Generated by Claude Code
Filing unassigned — recording, not claiming. Measured while working #9689; this finding survives whatever #9689 decides, which is why it is carded separately rather than folded in.
Measured
packages/spec/src/data/field.zod.ts(one line, currently ~906):The default is on the shared field schema with no per-type gating, so it materializes at parse for every field of every type. Measured with a real
FieldSchema.safeParse:deleteBehaviorhas no meaning on a non-reference field — nothing reads it there.cascadeDeleteRelationsonly ever reaches the key after anfdef.referenceguard, so on atextfield the value is inert by construction.Why it is not merely cosmetic
The materialized key ships in the app artifact.
packages/objectql/src/registry.tsalready documents the consequence for a sibling key, from a real incident (#4447):That is the same mechanism: a default materialized at parse becomes an explicit declaration downstream, and explicit declarations win merges. Two fixtures in the tree carry the artifact shape verbatim and show
deleteBehavior: 'set_null'sitting on adatetimefield:packages/objectql/src/engine-audit-anchor-write.test.ts:251(commented "Verbatim from examples/app-showcase/dist/objectstack.json")packages/metadata-protocol/src/protocol.audit-field-governance.test.ts:144This is the ADR-0049 declared-but-inert shape, and it is also an AI-authoring hazard in the direction ADR-0033 cares about: a model reading a built artifact sees
deleteBehavioron a text field and reasonably concludes the key is meaningful there.Interaction with #9689 (why this is separate)
#9689 asks whether
deleteBehavior: 'set_null'should be refused on amaster_detail. The measured answer is that any such rule — and equally any delete-time "you declared set_null" log — must first relocate this default (the #7918 Option A pattern), because otherwise it fires on all 95 baremaster_detaildeclarations rather than the 1 authored one.But the recommended relocation deliberately keeps parse output byte-identical (
.overwrite()re-materializesset_null), so after #9689 lands,textfields still carry the inert key. This finding therefore outlives it.Not prescribing the fix
Roughly two shapes, and they are not equivalent:
deleteBehavioronly forlookup/master_detail/tree. Cleanest, but changes parse output for every non-reference field, so it moves the app artifact and is a real (if mechanical) migration.fdef.deleteBehavior || 'set_null'on the lookup branch, so the fallback exists there already.Blocked-by: #9689 (the relocation it needs is the same edit; doing them in the other order means touching the line twice).
Refs: #9689, #9625, #7918 (the default-relocation precedent), #4447 (the materialized-default shadowing incident), ADR-0049.
Generated by Claude Code