Repository navigation
Commit 4ab2e7e
docs(spec): a cleared optional multi-value field is stored as null, in FieldSchema's multiple and required docs (#29763)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBAoBepdw1XbHyLW2fvste1k)_
Fixes #29652
Clause-②: yes
Metadata-protocol: semantic
Skills-docs: none — `git grep` over `skills/**` for the
empty-representation claim (`never null`, `reads back as []`, `emptied`)
finds no page that teaches it; the hand-written `content/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 页面。
- 为什么改:执行 #29343 裁决 A(记录 6591577985,「A:存 null,先修 #9447 spec 文本」),它推翻了
#9447「清空后读回 `[]`、永不 `null`」对可选列的那半句;按裁决先落 spec 文本,#29343 的写入口 PR 被本卡阻塞。
- 风险与回滚:只改文字,不动任何键、默认值或可接受集合;运行时行为由 #29343 的 PR 改,本 PR 落地后、那个 PR
落地前,文档先于行为。回滚即还原本 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 on
`origin/main` `1c1a5d40`; the line numbers are unchanged at the final
merge base `0b2b309c`), plus one comment in `filter-empty-operator.ts`
and the regenerated references. No key, default, accept set or export
moves.
**`required` doc block (≈:1314).**
- Before: "The empty set is always representable (it reads back as `[]`,
never `null` — see `multiple`), so the required check judges emptiness,
not absence."
- After: "An emptied OPTIONAL multi-value field is stored as `null`, the
same as one never set (see `multiple`), so the required check judges
emptiness: `null` and `[]` both fail it." The cite now reads "(#9447,
maintainer ruling 2026-08-18; this half stands under ruling A on #29343,
record 6591577985)".
- The `required` describe is unchanged: its sentence ("`[]` does not
satisfy it") is the half of #9447 the ruling keeps.
**`multiple` doc block (≈:1357).**
- Before: "Multi-value empty representation (#9447, maintainer ruling
2026-08-18): an emptied multi-value lookup reads back as `[]`, never
`null`. This binds … EVERY writer … an array field always reads as an
array, so readers (generated code, formula/filter predicates) never need
a null branch."
- After: "Multi-value empty representation (ruling A on #29343, record
6591577985, amending #9447, maintainer ruling 2026-08-18): a cleared
OPTIONAL multi-value field — every type `isMultiValueField` answers true
for … — is stored as `null` by the write door, the same as one never
set, unique-covered or not: `[]` and `''` written to it store `null`.
This binds … EVERY writer … It reverses #9447's sentence for optional
columns: readers meet `null` for an empty set, and read `null` and `[]`
alike as empty, since a row stored before the rule can still hold `[]`.
The `$empty` table's `multi_value` row is unchanged … #9447's other half
stands: `required` on a multi-value field means non-empty list."
- "readers never need a null branch" is dropped.
**`multiple` describe (≈:1380).** The rule only, no tracker id.
- Before: "An emptied multi-value lookup reads back as `[]`, never
`null` — the rule binds every writer (cascade repair, form clears, API
writes), not just cascade repair."
- After: "A cleared optional multi-value field is stored as `null`, the
same as one never set: an empty list `[]` or an empty string written to
it stores `null`, and the rule binds every writer (cascade repair, form
clears, API writes). Read `null` and `[]` alike as empty, as the
`is_empty` filter (`$empty`) does, since a row stored before this rule
can still hold `[]`. On a `required` multi-value field the empty list is
refused: `required` means a non-empty list."
**`filter-empty-operator.ts` docblock (≈:91), the one other
`packages/spec` hit.**
- Before: "a field whose value is a list is emptied to `[]`."
- After: "a field whose value is a list can hold `[]` (a cleared
optional one stores `null`, but a row stored before that rule can still
hold `[]`)." The `$empty` table and `expandEmptyOperator` are untouched.
**Generated.** `pnpm --filter @objectstack/spec build`, then
`check:generated` named one stale artifact
(`content/docs/references/**`); `gen:docs` rewrote the `multiple` row in
three pages, because `FieldSchema` is rendered in each:
`references/data/field.mdx`, `references/data/object.mdx` (2 rows) and
`references/system/migration.mdx` (2 rows). `check:generated` then
reported all 14 artifacts current.
**Changeset.** `.changeset/29652-multi-value-empty-null-docs.md`:
`@objectstack/spec` minor, 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`. The `Check Changeset` "WHICH LEVEL"
text: "`semantic` — the meaning or default of an authorable key changes
→ `minor`". `multiple` is an authorable key, and what it promises about
the stored empty value changes; the published describe is that promise.
The counter-reading is the `none` row's "description or error text",
which is for wording that leaves the meaning alone. So `semantic`, and
`@objectstack/spec` takes `minor`.
- `Clause-②: yes`, no arm. A published semantic contract changes, and no
accept set moves, so neither `(widening)` nor `(narrowing)` applies.
- **ADR-0087.** `check-adr-0087-registration` asks for a disposition
only on a changeset that declares a breaking change; this one does not.
The gate exits 0. No `SemanticMigration` entry is written. Stored `'[]'`
values are the ruling's step 3, a separate `os migrate` data card.
## Census: the "`[]`, never `null`" claim
`git grep` on `1c1a5d40` for `never null`, `` never `null` ``, `reads
back as []`, `emptied`, `9447`, and `multi-value` near `[]`:
- **`packages/spec/**`, amended here:** `field.zod.ts:1316-1317`
(`required` docblock), `:1357-1364` (`multiple` docblock), `:1371`
(`multiple` describe); `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 `[]`.
The `null`-or-`[]` rows in `filter-empty-operator.ts`,
`filter.zod.ts:1802`/`:2948` and `filter-logic-conformance.ts:80` are
the unchanged `$empty` table. `CHANGELOG.md:48935` is release-owned. The
other `never null` hits (`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 emptied `multiple: true` set is written or read back as `[]`,
never `null`, and most point at the `multiple` doc block as their
source:
- `content/docs/api/data-api.mdx:279`
- `content/docs/data-modeling/field-types.mdx:1025`
- `content/docs/data-modeling/fields.mdx:295`
- `content/docs/data-modeling/validation-rules.mdx:265` (and the #9447
cite at `:274`)
- `content/docs/deployment/troubleshooting.mdx:254`
- `content/docs/protocol/objectql/types.mdx:730`
They describe what the runtime does today, which changes only with the
write-door PR on #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 head `3e61ed73`, under
`os-verify-lock`: Test Files 803 passed (803), Tests 23795 passed, 1
todo. `pnpm --filter @objectstack/spec typecheck` ran in the same chain,
lock `VERDICT command-exit 0`. The two earlier heads read the same:
`f05af6cb` (803/803 files, 23786 passed) and `8b1ecbbc` after the first
`origin/main` merge (803/803, 23792 passed).
## Acceptance notes
- Docs before behaviour, by the ruling's order: until the PR on #29343
lands, the write door still stores `[]` as sent on these columns, so for
a while the describe states a rule the runtime has not applied yet.
- The `required` describe 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.ts` also moved on `main` while this was open: #29623
(`87956dcc`) landed and was merged in, a different region, and the
regenerated references keep only this PR's `multiple` rows against
`main`. PR #29553 (#29415) edits the same file near :1822, also a
different region; the later lander merges `origin/main`.
- Author of this body: `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
with `origin/main` at `0b2b309c`. The delta against `main` is exactly
the 6 files above, 45 insertions and 20 deletions.
- `pnpm exec turbo run build --concurrency=2`: 118/118 tasks, lock
`VERDICT 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/objectstack` derived 96 families from 6 changed paths.
Each ran serially and was recorded as `CMD :: exit N`: **96 exit 0**.
`dispatch-gates --ran` reported "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-② `yes` and metadata-protocol `semantic`, and no package
whose published source … it moves is graded `patch`".
- `check-adr-0087-registration --base origin/main`: exit 0. No changeset
here declares a breaking change, so the gate asks for no disposition.
<details><summary>Earlier heads</summary>
On `f05af6cb` (before any merge), the same 96 ran: 89 exit 0 and 7 exit
3 (PREREQUISITE NOT MET). The 7 lacked a built `dist` for
`@objectstack/formula`, `@objectstack/lint`, `@objectstack/client-react`
and `@objectstack/objectql`, or needed the `check-plugin-teardown-shape`
fixture commit that the shallow clone was missing. After a full build
and a fetch of that fixture commit, all 7 exited 0. On `8b1ecbbc` (after
the first merge), all 96 exited 0.
</details>
Not run locally, left for CI: `pnpm lint`, the `test:repo` project, and
the full dogfood suite (the title has no `!` and the PR declares no
`(narrowing)` arm).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e82bf64 commit 4ab2e7e
6 files changed
Lines changed: 45 additions & 20 deletions
File tree
- .changeset
- content/docs/references
- data
- system
- packages/spec/src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
230 | | - | |
| 230 | + | |
231 | 231 | | |
232 | 232 | | |
233 | 233 | | |
| |||
598 | 598 | | |
599 | 599 | | |
600 | 600 | | |
601 | | - | |
| 601 | + | |
602 | 602 | | |
603 | 603 | | |
604 | 604 | | |
| |||
0 commit comments