Repository navigation
[Decision] Multi-value lookup after set_null member removal: does the emptied array read back as [] or null? #9447
Description
Activity
Maintainer ruling recorded — maintainer, 2026-08-18, batch acceptance of the triage seat's decision-inbox recommendations, verbatim: 「同意」. Four-facet analysis delivered in-session; provenance: triage session
session_pm_triage_20260818T0104Z.Ruled: the emptied multi-value lookup reads back as
[], and in the same stroke:requiredon a multi-value lookup means non-empty array — an emptied required set fails validation loudly. The rule binds the field's empty representation generally (every writer: cascade repair, form clears, API writes), not as a cascade-only convention.Landing: the ruling sentence goes into
FieldSchema's doc block (spec text surface — spec seat, clause-② semantic face, contract-review tier); #9438's option 1 (member removal,next = current.filter(...), empty ⇒[]) consumes it and reverts the interimrestrictescalation in the same PR.Label
needs-user-decision→pm:queuein the same stroke.
Generated by Claude Code
Claim: session
session_01Fs18A2DdXLVN2h8PaaFBcP(thedomain:specexecution seat, #6017), branchclaude/issue-9447-multivalue-empty-representation. Clause-②: yes per the ruling comment (semantic face: defines the empty representation and whatrequiredmeans for multi-value lookups) — contract-review tier; if the Fable tier is quota-refused at launch, the measured quota exemption applies (→ opus, recorded here, compensating measured review by this seat before landing). Scope: the ruled sentences land inFieldSchema's doc block ([]general rule, every writer;required= non-empty array); the engine consumption belongs to #9438's PR, not this card. Generated-artifacts coupling declared: wave-8 siblings #9463/#9406/#9345 — landings serialize, later landers run os-regen-merge.
Generated by Claude Code
- added a commit that references this issue
on Aug 18, 2026 { "issue": 9447, "outcome": "delivered", "pr": 9493, "branch": "claude/issue-9447-multivalue-empty-representation", "head": "e48b04963", "before_after": "field.zod.ts `required` (was line 757, now 756-765): describe + new doc block gain 'On a multi-value lookup (multiple: true) required means NON-EMPTY array — an emptied required set fails validation loudly; [] does not satisfy it (#9447, maintainer ruling 2026-08-18)'. `multiple` (was line 775, now 783-793): describe + new doc block gain 'An emptied multi-value lookup reads back as [], never null — binds every writer (cascade repair, form clears, API writes), not just cascade repair (#9447, maintainer ruling 2026-08-18)'. content/docs/references/data/field.mdx regenerated via gen:docs. Full excerpts quoted in the PR body.", "current_behavior_measured": "Runtime probe (temp vitest vs validateRecord, deleted, never committed) at main=ad217b192: [] on a required+multiple:true lookup PASSES validation on both insert and update; null is rejected on both (required / required_cleared). Cause: record-validator.ts:171-173 isMissing covers undefined/null/blank-string only, read at :477 (insert) and :1011 (update). No pinned test asserts the opposite semantics — enforcement gap, not a ruling-vs-pin conflict, so the ruled doc landed and the divergence was filed.", "divergence_finding": 9476, "files": [ "packages/spec/src/data/field.zod.ts", "content/docs/references/data/field.mdx", ".changeset/nine-camels-behave.md" ], "gates": [ "check:generated (all 13 up to date, re-run at head e48b04963 after final commit)", "spec test 409 files / 10915 passed", "spec typecheck exit 0", "dispatch-gates union 21 families all PASS at e48b04963 (check-dev-prereqs green after full pnpm build; its first red was fresh-worktree build state, 52 missing dists, not the diff)", "check:nul-bytes PASS" ], "deviations": [] }
Generated by Claude Code
Generated by Claude Code
ACCEPT — review of record against PR #9493 (head
e48b04963), full diff read (3 files).- Both ruled sentences landed where a reader will meet them:
required's doc block + describe carry "non-empty array,[]does not satisfy it", andmultiple's carry "reads back as[], nevernull— binds every writer", each citing the ruling date and cross-referencing the other. The reference page regenerated to match. Acceptance byte-identical; patch changeset. - The measured-divergence instruction was executed exactly right: a runtime probe (temporary, deleted, never committed — with
nullcontrols both directions) proved[]on a required+multiple: truelookup PASSES validation today (record-validator.ts:171-173isMissingcovers undefined/null/blank only, read at:477insert and:1011update). No pinned test asserts the opposite, so this is an enforcement gap versus the ruled contract, not a ruling-vs-pin conflict — the doc landed as ruled and the gap is on record as record-validator:[]satisfiesrequiredon amultiple: truelookup — diverges from the #9447 ruling (required means non-empty array) #9476 with the read sites. The cascadeDeleteRelations' set_null limb nulls the WHOLE multi-value array, dropping every other live reference #9438 lane (which consumes this ruling) and triage now have the enforcement question in queueable form. - Gates: 21 derived families green at head, spec 10915 tests,
check:generated13/13 re-run post-final-commit. No deviations. - Clause-② dispatch ran AT the contract-review tier — no label loop.
Landing: wave-8 serial relay — #9489 (#9463) holds the current slot; this PR lands next (ready + auto-merge squash after #9489 merges and CI here is green; os-regen-merge if dirty), ahead of #9492 (awaiting contract-review clearance) and #9406 (in flight).
Generated by Claude Code
- Both ruled sentences landed where a reader will meet them:
- added a commit that references this issue
on Aug 18, 2026 - added 4 commits that reference this issue
on Aug 23, 2026 - added a commit that references this issue
on Sep 28, 2026
Split out of #9438 by the triage seat (the same shape as the #9364 → #9389 split): the interim repair on #9438 (escalate defaulted
set_nulltorestrictformultiple: true, mirroring the pinned required-FK escalation) is queued and decides no semantics; this card carries the one question that blocks the real fix (#9438 option 1 — remove the member instead of refusing).The question
When
cascadeDeleteRelationsremoves the deleted record's id from amultiple: truelookup array and the array becomes empty, what does the field read back as —[]ornull?The difference is observable on the read path, and it decides what a
requiredmulti-value validator sees ([]is a present-but-empty value;nullis an absent one).FieldSchemacurrently says nothing, no sibling declaration pins it, and no landed ruling covers it — which is exactly why #9438's implementer refused to guess it inside a P0 hotfix (#9362 / PR #9437).Four-facet analysis
showcase_field_zoo.f_lookups). Until option 1 lands, the interimrestrictrefusal (409) is what users see.[]keeps the field's type stable (an array field always reads as an array) — one less null-branch for generated code and formula/filter predicates to mishandle;nullmatches the single-value lookup's cleared state but makes the field's type a union in practice.FieldSchema's doc; the cost is only in changing it later, so rule once, now.Recommendation:
[]— type-stable for every reader, consistent with "the set lost a member" rather than "the field was cleared", and it keepsrequiredsemantics honest (an emptied required set should fail validation loudly, which[]does and a vanishednullmay not, depending on the validator's absent-vs-empty treatment — the ruling should state which of thoserequiredmeans for multi-value lookups, since that is the same question wearing validation clothes).On the ruling
The answer unblocks #9438 option 1 (member removal); the interim
restrictescalation reverts in the same PR. Ruling text lands inFieldSchema's doc block (spec text surface — spec seat) and the engine repair consumes it.Refs: #9438 (the defect and the interim) · #9362 / PR #9437 (what makes the limb reachable) · the pinned required-FK
set_null→restrictescalation in the same block ofpackages/objectql/src/engine.ts.Filed by the triage seat routine — session
session_pm_triage_20260818T0104Z.