Repository navigation
docs(spec): a cleared optional multi-value field is stored as null, in FieldSchema's multiple and required docs - #29763
Conversation
Amend the FieldSchema `multiple` and `required` doc blocks and the `multiple` describe to the ruled empty representation: a cleared optional multi-value field stores null at the write door, `[]` and `''` store null, `required` still means a non-empty list, and the `$empty` table is unchanged. Claude-Session: https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 5ba46c89e5f4f1e7c2824cea3f57e8b87469d2e1 && git checkout 5ba46c89e5f4f1e7c2824cea3f57e8b87469d2e1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3eeb81d19495b9e4064685fc9662936a834f017c 3e61ed737b30c8d0dbea4cf7b0a0fa5b9e335926 && git checkout -B drift-repro 3eeb81d19495b9e4064685fc9662936a834f017c && git merge --no-ff 3e61ed737b30c8d0dbea4cf7b0a0fa5b9e335926
node scripts/docs-audit/affected-docs.mjs --json 3eeb81d19495b9e4064685fc9662936a834f017c
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 3e61ed7 · 2026-11-03T03:51Z |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: Inputs read: card #29652 body and all four comments (triage 6592466576, the two claims, os-dev-report 6594958096); ruling record 6591577985 on #29343; PR #29763 body, file list (6 files, +45/-20) and the net diff against the merge base Check-runs on the head at review time: 23 success, 8 skipped, 31 still in progress, none failed. The landing still waits for the required set to conclude; this record judges the design, not the unfinished gates. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:裁决 A 的四个条款逐条落在文本里,与 #29343 已建的写入口逐条对得上;同意落地。 席位核对 — ACCEPT · domain:spec#3 · head
|
Requested by Yibo Wang · project thread
Fixes #29652
Clause-②: yes
Metadata-protocol: semantic
Skills-docs: none —
git grepoverskills/**for the empty-representation claim (never null,reads back as [],emptied) finds no page that teaches it; the hand-writtencontent/docs/**pages that do are listed under the census below and move with the write-door PR that changes the behaviour维护者速读
FieldSchema的multiple/required说明(docblock 与multiple的 describe)改为裁决后的规则:可选多值字段被清空时存null(与从未赋值相同),写入[]或''也存null;required仍表示非空列表;$empty表不变。重新生成了 3 个 references 页面。[]ornull? #9447 spec 文本」),它推翻了 [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447「清空后读回[]、永不null」对可选列的那半句;按裁决先落 spec 文本,objectql: a multi-value unique column (multiselect, or lookup with multiple: true) cleared to [] on two records refuses the second with 409 UNIQUE_VIOLATION #29343 的写入口 PR 被本卡阻塞。packages/spec/src/data/field.zod.ts(multiple的 docblock 与 describe,≈:1357-1380;required的 docblock,≈:1314-1321)、.changeset/29652-multi-value-empty-null-docs.md。git grep -n "never .null" -- packages/spec/src/data/field.zod.ts content/docs/references,应只剩一行:field.zod.ts:1367,docblock 里被推翻的原句引文(修改前 field.zod.ts 3 处、3 个 references 页面共 5 处)。What changes
Text only, in
packages/spec/src/data/field.zod.ts(measured onorigin/main1c1a5d40; the line numbers are unchanged at the final merge base0b2b309c), plus one comment infilter-empty-operator.tsand the regenerated references. No key, default, accept set or export moves.requireddoc block (≈:1314).[], nevernull— seemultiple), so the required check judges emptiness, not absence."null, the same as one never set (seemultiple), so the required check judges emptiness:nulland[]both fail it." The cite now reads "([Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447, maintainer ruling 2026-08-18; this half stands under ruling A on objectql: a multi-value unique column (multiselect, or lookup with multiple: true) cleared to [] on two records refuses the second with 409 UNIQUE_VIOLATION #29343, record 6591577985)".requireddescribe is unchanged: its sentence ("[]does not satisfy it") is the half of [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447 the ruling keeps.multipledoc block (≈:1357).[]ornull? #9447, maintainer ruling 2026-08-18): an emptied multi-value lookup reads back as[], nevernull. This binds … EVERY writer … an array field always reads as an array, so readers (generated code, formula/filter predicates) never need a null branch."[]ornull? #9447, maintainer ruling 2026-08-18): a cleared OPTIONAL multi-value field — every typeisMultiValueFieldanswers true for … — is stored asnullby the write door, the same as one never set, unique-covered or not:[]and''written to it storenull. This binds … EVERY writer … It reverses [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447's sentence for optional columns: readers meetnullfor an empty set, and readnulland[]alike as empty, since a row stored before the rule can still hold[]. The$emptytable'smulti_valuerow is unchanged … [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447's other half stands:requiredon a multi-value field means non-empty list."multipledescribe (≈:1380). The rule only, no tracker id.[], nevernull— the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair."null, the same as one never set: an empty list[]or an empty string written to it storesnull, and the rule binds every writer (cascade repair, form clears, API writes). Readnulland[]alike as empty, as theis_emptyfilter ($empty) does, since a row stored before this rule can still hold[]. On arequiredmulti-value field the empty list is refused:requiredmeans a non-empty list."filter-empty-operator.tsdocblock (≈:91), the one otherpackages/spechit.[]."[](a cleared optional one storesnull, but a row stored before that rule can still hold[])." The$emptytable andexpandEmptyOperatorare untouched.Generated.
pnpm --filter @objectstack/spec build, thencheck:generatednamed one stale artifact (content/docs/references/**);gen:docsrewrote themultiplerow in three pages, becauseFieldSchemais rendered in each:references/data/field.mdx,references/data/object.mdx(2 rows) andreferences/system/migration.mdx(2 rows).check:generatedthen reported all 14 artifacts current.Changeset.
.changeset/29652-multi-value-empty-null-docs.md:@objectstack/specminor, both declaration lines at line start.Why
Ruling A on #29343 (record 6591577985), verbatim: 「A:存 null,先修 #9447 spec 文本」 — 「spec 席先改 field.zod.ts 的 multiple/required 说明,再落本卡 PR(代码已建好),另开一张清理已存 '[]' 的 migrate 卡。」
The declarations, measured.
Metadata-protocol: semantic. TheCheck Changeset"WHICH LEVEL" text: "semantic— the meaning or default of an authorable key changes →minor".multipleis an authorable key, and what it promises about the stored empty value changes; the published describe is that promise. The counter-reading is thenonerow's "description or error text", which is for wording that leaves the meaning alone. Sosemantic, and@objectstack/spectakesminor.Clause-②: yes, no arm. A published semantic contract changes, and no accept set moves, so neither(widening)nor(narrowing)applies.check-adr-0087-registrationasks for a disposition only on a changeset that declares a breaking change; this one does not. The gate exits 0. NoSemanticMigrationentry is written. Stored'[]'values are the ruling's step 3, a separateos migratedata card.Census: the "
[], nevernull" claimgit grepon1c1a5d40fornever null,never `null`,reads back as [],emptied,9447, andmulti-valuenear[]:packages/spec/**, amended here:field.zod.ts:1316-1317(requireddocblock),:1357-1364(multipledocblock),:1371(multipledescribe);filter-empty-operator.ts:91.packages/spec/**, read and left:value-roundtrip-conformance.ts:216(m_empty, "empty array must not degrade to null") is the driver round-trip table, below the write door, and the ruling does not change what a driver returns for a written[]. Thenull-or-[]rows infilter-empty-operator.ts,filter.zod.ts:1802/:2948andfilter-logic-conformance.ts:80are the unchanged$emptytable.CHANGELOG.md:48935is release-owned. The othernever nullhits (organization.zod.ts,solution-blueprint.zod.ts, tests) are other subjects.skills/**: 0 hits.Hand-written
content/docs/**, findings, not edited here: each says an emptiedmultiple: trueset is written or read back as[], nevernull, and most point at themultipledoc block as their source:content/docs/api/data-api.mdx:279content/docs/data-modeling/field-types.mdx:1025content/docs/data-modeling/fields.mdx:295content/docs/data-modeling/validation-rules.mdx:265(and the [Decision] Multi-value lookup after set_null member removal: does the emptied array read back as[]ornull? #9447 cite at:274)content/docs/deployment/troubleshooting.mdx:254content/docs/protocol/objectql/types.mdx:730They describe what the runtime does today, which changes only with the write-door PR on objectql: a multi-value unique column (multiselect, or lookup with multiple: true) cleared to [] on two records refuses the second with 409 UNIQUE_VIOLATION #29343. Moving them there keeps each page true on every merged commit.
Tests
No test changes: no tested behaviour moves.
pnpm --filter @objectstack/spec test: on the final head3e61ed73, underos-verify-lock: Test Files 803 passed (803), Tests 23795 passed, 1 todo.pnpm --filter @objectstack/spec typecheckran in the same chain, lockVERDICT command-exit 0. The two earlier heads read the same:f05af6cb(803/803 files, 23786 passed) and8b1ecbbcafter the firstorigin/mainmerge (803/803, 23792 passed).Acceptance notes
[]as sent on these columns, so for a while the describe states a rule the runtime has not applied yet.requireddescribe still says "On a multi-value lookup (multiple: true)". It is narrower than the rule (which covers every multi-value type) but not false. Widening it is outside the file surface this card claims.field.zod.tsalso moved onmainwhile this was open: spec(data): the name-only reference value a refused reader is served has a named schema, and referenceNameVisible says which name it carries #29623 (87956dcc) landed and was merged in, a different region, and the regenerated references keep only this PR'smultiplerows againstmain. PR docs(spec): register the user-field deleteBehavior enforcement as an ADR-0087 semantic entry and correct the delete-walk prose (#29415) #29553 (spec(migrations): register theuser-field deleteBehavior enforcement (#29253) as an ADR-0087 semantic entry and stop the spec prose saying auserfield never joins the delete walk #29415) edits the same file near :1822, also a different region; the later lander mergesorigin/main.domain:spec#3 · os-support-ai · session_01WGufzeW5cprrZRccVNgPFr. Lines 1 to 3 belong to the attribution block and the closing line, so the signature is here rather than first.Verification
Every reading here is on the final head
3e61ed73: the branch merged withorigin/mainat0b2b309c. The delta againstmainis exactly the 6 files above, 45 insertions and 20 deletions.pnpm exec turbo run build --concurrency=2: 118/118 tasks, lockVERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date".node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 96 families from 6 changed paths. Each ran serially and was recorded asCMD :: exit N: 96 exit 0.dispatch-gates --ranreported "96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN".GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event FILE, with this body as the event: exit 0. Its verdict: "LEVEL AXIS: this PR declares clause-②yesand metadata-protocolsemantic, and no package whose published source … it moves is gradedpatch".check-adr-0087-registration --base origin/main: exit 0. No changeset here declares a breaking change, so the gate asks for no disposition.Earlier heads
On
f05af6cb(before any merge), the same 96 ran: 89 exit 0 and 7 exit 3 (PREREQUISITE NOT MET). The 7 lacked a builtdistfor@objectstack/formula,@objectstack/lint,@objectstack/client-reactand@objectstack/objectql, or needed thecheck-plugin-teardown-shapefixture commit that the shallow clone was missing. After a full build and a fetch of that fixture commit, all 7 exited 0. On8b1ecbbc(after the first merge), all 96 exited 0.Not run locally, left for CI:
pnpm lint, thetest:repoproject, and the full dogfood suite (the title has no!and the PR declares no(narrowing)arm).Generated by Claude Code