Skip to content

docs(spec): FieldSchema pins the ruled multi-value empty representation ([] + required means non-empty) (#9447) - #9493

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9447-multivalue-empty-representation
Aug 18, 2026
Merged

docs(spec): FieldSchema pins the ruled multi-value empty representation ([] + required means non-empty) (#9447)#9493
os-steve merged 1 commit into
mainfrom
claude/issue-9447-multivalue-empty-representation

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9447

The ruling (2026-08-18, recorded on #9447, binding)

Maintainer batch acceptance, verbatim: 「同意」. As recorded on the issue:

Ruled: the emptied multi-value lookup reads back as [], and in the same stroke: required on 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.

This PR lands exactly that text surface: the two ruled sentences into FieldSchema's doc blocks in packages/spec/src/data/field.zod.ts, on the two keys a reader consults (required, multiple), each citing the ruling per the file's convention (#9447, maintainer ruling 2026-08-18). The engine consumption (member removal in cascadeDeleteRelations, reverting the interim restrict escalation) belongs to #9438's PR and is deliberately untouched here.

Before / after

packages/spec/src/data/field.zod.tsrequired (before: line 757; after: lines 756-765):

Before:

/** Write contract (ADR-0113 — NOT a column constraint; see `storage.notNull`) */required: z.boolean().default(false).describe('Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. NOT a column constraint — …'),

After:

/** * Write contract (ADR-0113 — NOT a column constraint; see `storage.notNull`). * * On a multi-value lookup (`multiple: true`), `required` means NON-EMPTY * array: an emptied required set fails validation loudly — `[]` does not * satisfy `required` (#9447, maintainer ruling 2026-08-18). The empty set is * always representable (it reads back as `[]`, never `null` — see * `multiple`), so the required check judges emptiness, not absence. */required: z.boolean().default(false).describe('Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. 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). NOT a column constraint — …'),

packages/spec/src/data/field.zod.tsmultiple (before: line 775; after: lines 783-793):

Before:

multiple: z.boolean().default(false).describe('Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image.'),

After:

/** * Multi-value empty representation (#9447, maintainer ruling 2026-08-18): * an emptied multi-value lookup reads back as `[]`, never `null`. This * binds the field's empty representation for EVERY writer — cascade repair * (`set_null` member removal), form clears, API writes — not as a * cascade-only convention: an array field always reads as an array, so * readers (generated code, formula/filter predicates) never need a null * branch. Same ruling: `required` on a multi-value lookup means non-empty * array (see `required` above). */multiple: z.boolean().default(false).describe('Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. 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 (#9447, maintainer ruling 2026-08-18).'),

content/docs/references/data/field.mdx regenerated from the doc blocks via gen:docs (the only artifact check:generated proved stale; regenerated with --fix, narrow by design).

Measured current behavior — divergence found and filed as #9476

Per the dispatch's measurement clause, I checked what validation ACTUALLY does with [] on a required + multiple: true lookup before writing the doc. Runtime probe (temp vitest file against validateRecord, deleted after the run, never committed), at main = ad217b192:

  • insert { members: [] } on { type: 'lookup', reference: 'sys_user', multiple: true, required: true }passes (no throw)
  • update { members: [] }passes (no throw)
  • controls: members: nullrejected on insert (required) and update (required_cleared)

Read sites: packages/objectql/src/validation/record-validator.ts:171-173 (isMissing covers only undefined/null/blank string — never []), consumed at :477 (insert) and :1011 (update non-regression). No existing pinned test asserts the opposite semantics (the required suite covers text/autonumber/null/blank-string only), so this is an enforcement gap versus the ruled contract, not a ruling-vs-pin conflict. The ruled doc sentence lands regardless (it is the contract); the divergence is on record as #9476 (unassigned, measured, with read sites) — the enforcement half is not addressed here and #9476 remains open.

Verification — all from head e48b04963

  • pnpm --filter @objectstack/spec build then pnpm --filter @objectstack/spec check:generatedall 13 generated artifacts up to date (re-run at head after the final commit)
  • pnpm --filter @objectstack/spec test — 409 files, 10915 passed
  • pnpm --filter @objectstack/spec typecheck — exit 0
  • Full dispatch-gates union for the changed paths (21 derived families + check:nul-bytes): all PASS at e48b04963check:changeset-gate-self-tests, check:cross-package-test-inputs (both spellings), check:doc-formula-expressions, check:docs-audit-scope, check:docs-redirects, check:empty-state, check:liveness, check:merge-driver, check:objectui-changeset, check:quick-reference-counts, check:role-word, check:spec-parsed-alias, check:strictness-ledger, check:type-source-resolution, check:variant-docs, check-adr-0087-registration, check-changeset-no-major, check-dev-prereqs (after full pnpm build; its first run red was fresh-worktree build state, 52 packages without dist, not the diff), check-empty-changeset, check-affected-docs, check:nul-bytes
  • Changeset: .changeset/nine-camels-behave.md@objectstack/spec patch

Generated-artifacts coupling: wave-8 siblings #9463 / #9406 / #9345 — the PM serializes landings; a later lander runs scripts/pm/os-regen-merge.sh.


Generated by Claude Code

…on (#9447)
An emptied multi-value lookup reads back as [], never null — binding for
every writer (cascade repair, form clears, API writes) — and required on
a multi-value lookup means non-empty array (maintainer ruling 2026-08-18,
recorded on #9447). Doc blocks on FieldSchema.required and
FieldSchema.multiple; content/docs/references regenerated via gen:docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fs18A2DdXLVN2h8PaaFBcP
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/external-datasources.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/field-types.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/validation-rules.mdx(via FieldSchema (symbol))
  • content/docs/deployment/troubleshooting.mdx(via FieldSchema (symbol))
  • content/docs/deployment/validating-metadata.mdx(via FieldSchema (symbol))
  • content/docs/getting-started/quick-reference.mdx(via FieldSchema (symbol))
  • content/docs/kernel/contracts/data-engine.mdx(via FieldSchema (symbol))
  • content/docs/protocol/backward-compatibility.mdx(via FieldSchema (symbol))
  • content/docs/ui/forms.mdx(via /forms/:slug/lookup/:field (route))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via FieldSchema (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 113 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/mainpackageMentionDocs.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tooling labels Aug 18, 2026
@os-steve
os-steve marked this pull request as ready for review August 18, 2026 05:14
@os-steve
os-steve added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit df0c12dAug 18, 2026
27 checks passed
@os-steve
os-steve deleted the claude/issue-9447-multivalue-empty-representation branch August 18, 2026 05:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/stooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Multi-value lookup after set_null member removal: does the emptied array read back as [] or null?

2 participants

@os-steve@claude