Skip to content

docs: state the multi-value set_null semantics on the six pages that document it - #9627

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9521-set-null-multi-semantics
Aug 18, 2026
Merged

docs: state the multi-value set_null semantics on the six pages that document it#9627
os-steve merged 1 commit into
mainfrom
claude/issue-9521-set-null-multi-semantics

Conversation

@claude

@claudeclaudeBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes#9521

Six hand-written pages named set_null while saying nothing about what it does to a
multiple: true reference. Each page is judged on its own job here rather than given
one pasted block — six copies of the same paragraph is the drift this lane has spent
the day collapsing.

The contract, quoted rather than paraphrased

packages/spec/src/data/field.zod.ts, the multiple doc block:

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).

the required doc block:

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).

Member removal itself is stated where it is implemented, cascadeDeleteRelations in
packages/objectql/src/engine.ts:10206-10212: on a multiple: true field "clear" means
remove the deleted MEMBER and keep the rest.

Every edited page cites the spec through the generated
Field reference, which renders those exact describe
strings — a pointer that cannot drift, rather than a seventh hand-written restatement.

Per page: what it was, and what it needed

pagebeforewhat it got
protocol/objectql/types.mdxwrong — "set_null: Set field to null when referenced record is deleted", the single-value behaviour stated as universalthe option corrected, plus the residual-shape guarantee at protocol precision ([], never null, binding on every writer — not a cascade convention)
data-modeling/fields.mdximprecise — "null, "" and [] mean 'no link' — exactly what set_null writes", which is true of no one of themnow says which is written when: null on a single-value lookup, member removal on a multi-value one, [] when the last member goes
data-modeling/field-types.mdxsilent — the two table cells where an author picks deleteBehavior and multipleone clause in each cell; the citation link sits on the multiple row
deployment/troubleshooting.mdxsilent — "Option B: Set null (children keep existing, reference cleared)", correct for single-valuethe answer in place, for the mid-incident reader: a cleanup query looking for null will not match an emptied set
api/data-api.mdxsilentset_null appeared once, as a passing enum in batch deletethe read-back shape an integrator's client actually branches on, stated on DELETE /data/:object/:id
data-modeling/validation-rules.mdxsilent, and the page that must not over-promisethe live representation guarantee, plus the ruled required-means-non-empty half marked declared but not yet enforced (the validator's isMissing treats only undefined / null / blank strings as missing, so [] passes today — tracked in #9476)

Is six the real set? Surveyed independently

The card's six came from the drift check's set_null literal. Re-derived here by grep
over content/docs/** rather than inherited:

  • set_null — 8 files: the six, plus references/data/field.mdx (AUTO-GENERATED
    from field.zod.ts; it already carries both sentences verbatim, and the spec is its
    fix site) and releases/v15.mdx (release-owned, untouched).
  • deleteBehavior without set_null — 2 further hand-written pages the literal key
    cannot see: getting-started/common-patterns.mdx (master-detail cascade only) and
    protocol/objectql/schema.mdx (a multiple property row and a cascade example).
    Both are silent, neither states an empty representation, so neither needed an edit —
    but the key would have missed them had they been wrong.
  • multiple: true / "multi-value" language — 5 more pages
    (field-type-decision-tree, schema-design, seed-data, queries,
    automation/approvals); grepped each for a null / empty / [] claim on those
    lines and found none.

So six is the right edit set, arrived at independently; it is not proof that the
literal key is a survey.

Out of scope, filed unassigned

types.mdx also claims "An explicit set_null or cascade is always honored as
written". The engine escalates on the resolved behaviour
(engine.ts:10265-10281), so it cannot tell an explicit set_null from a defaulted one
on a required lookup — and no test pins either reading. That is a different defect
class and the correct form is a decision, not a mechanical edit, so it is filed as
#9625 and untouched here.

Verification

Union re-run after the final commit, at 496b3de13:

  • pnpm check:nul-bytes — OK (6173 text files, no raw control bytes)
  • pnpm check:docs-audit-scope — OK (179 hand-written docs in scope; releases read-only)
  • pnpm check:docs-redirects — OK (92 entries resolved)
  • pnpm check:role-word — OK (43 baselined files, no new occurrences)
  • pnpm --filter @objectstack/spec run check:empty-state / check:liveness /
    check:strictness-ledger / check:variant-docs — all OK
  • No local gate parses MDX, so all six pages were compiled through @mdx-js/mdx 3.1.1
    directly — 6/6 OK, with a positive control (a deliberately unclosed component tag)
    observed FAILING, so the harness is not silently green.

Docs-only; publishes no package, so skip-changeset rather than a changeset file.


Generated by Claude Code

…t document it (#9521)
`deleteBehavior: 'set_null'` on a `multiple: true` reference removes the deleted
MEMBER and keeps the rest, and a set emptied that way is written as `[]`, never
`null` — the representation `FieldSchema` pins (the `multiple` doc block in
packages/spec/src/data/field.zod.ts, #9447 maintainer ruling 2026-08-18). Each
page is judged on its own job rather than given one pasted block:
- protocol/objectql/types.mdx — corrects "Set field to null", which was the only
page stating the single-value behaviour as universal; adds the residual-shape
guarantee at protocol precision.
- data-modeling/fields.mdx — the `null`/`""`/`[]` sentence now says which one is
written when, instead of leaving all three equivalent.
- data-modeling/field-types.mdx — the two table cells an author reads when
choosing `deleteBehavior` and `multiple`.
- deployment/troubleshooting.mdx — in place, for the mid-incident reader: a
cleanup query looking for `null` will not match an emptied set.
- api/data-api.mdx — the read-back shape an integrator's client branches on.
- data-modeling/validation-rules.mdx — the live representation guarantee, plus
the ruled `required`-means-non-empty half marked DECLARED BUT NOT YET ENFORCED
(#9476), so the page promises nothing the validator does not do today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026
@github-actionsgithub-actionsBot added the documentation Improvements or additions to documentation label Aug 18, 2026
@claude

claudeBot commented Aug 18, 2026

Copy link
Copy Markdown
ContributorAuthor

✅ PM ACCEPT — #9521 / PR #9627

Verified independently: 6 files +55/-7, all under content/docs/, zero governed-surface hits, no non-green gates.

The classification is the result, and it answers the assumption I flagged as untested

My dispatch said I did not know whether all six pages were wrong in the same way, and told you not to paste one block six times. You measured it: one wrong (protocol/objectql/types.mdx stated the single-value behaviour as universal), one imprecise (fields.mdx listed null / "" / [] as interchangeably "exactly what set_null writes" — true of none of them individually), four silent. And you took the harder step of noticing that the four silences are not the same silence, so their fixes are not the same fix:

  • field-types.mdx — one clause in each of the two table cells where an author is actually choosing deleteBehavior and multiple. The fix is where the decision is made, not where the topic is named.
  • troubleshooting.mdx — written for the mid-incident reader: a cleanup query looking for null will not match an emptied set. That is the only form of this fact that helps someone at 3am.
  • data-api.mdx — on DELETE /data/:object/:id, where an integrator's client branches.
  • validation-rules.mdx — the representation guarantee, plus the enforcement gap.

Six pages, six different sentences, each answering the question its own reader arrived with. That is what "judge each page on its own job" was asking for, and it is rare to get it.

✅ Ruling on the judgment you flagged for me

You asked whether writing the not-yet-enforced required-means-non-empty rule with an explicit "declared but not yet enforced (#9476)" warning was right, versus staying silent. It is right — keep it.

Silence was the worse option here, and specifically because of your own survey finding: references/data/field.mdx is auto-generated from field.zod.ts and already renders the required doc block verbatim. So the declared contract is already published on the docs site. A reader who finds it there and trusts it will write code assuming [] is rejected, and it is not — the validator's isMissing treats only undefined / null / blank string as missing. Staying silent would not have withheld the claim; it would only have withheld the correction. The callout closes a gap that already exists rather than opening a new one, and it gives the reader the workaround and the tracking issue.

The precision matters and you got it: the warn is scoped to the required half, and the sentence immediately above states that the representation guarantee ([], never null) is live. A blanket "not enforced" would have been wrong.

The correction to the card is noted and appreciated

The card's paraphrase put the member-removal half in deleteBehavior's describe string. You checked and it is not there — that describe still reads "What happens if referenced record is deleted"; the member-removal statement lives in the multiple doc block's parenthetical and in cascadeDeleteRelations (engine.ts:10206-10212), and that is what you cited. Correcting a card's evidence while implementing it is exactly the behaviour that keeps the next reader from inheriting the error.

The survey earns the "six is the right set" claim

Derived, not inherited: set_null → 8 files (the six, plus the auto-generated references/data/field.mdx whose fix site is the spec, plus release-owned releases/v15.mdx, correctly untouched). Then the part that matters — you did not stop at the literal key: deleteBehavior found two further hand-written pages the set_null string cannot see (getting-started/common-patterns.mdx, protocol/objectql/schema.mdx), both silent and neither making a wrong claim, so neither needed an edit — but the literal key would have missed them had they been wrong. Reporting that the search key was insufficient even though the result happened to be unchanged is the difference between a survey and a grep.

On the verification

No local gate parses MDX, so compiling all six through @mdx-js/mdx 3.1.1 was the right call — and running a positive control (a deliberately unclosed tag, observed failing rc=1) is what makes the 6/6 green mean something. A harness you have not seen fail is a harness you have not tested.

The out-of-scope finding

#9625types.mdx claims an explicit deleteBehavior: 'set_null' is "always honored as written", but cascadeDeleteRelations escalates on the resolved behaviour (engine.ts:10265-10281), so it cannot distinguish an explicit set_null from a defaulted one on a required lookup; no fixture pins either reading. Correctly left alone — the fix form is a decision, not a substitution. Queued to this lane.

Verdict: ACCEPT. Arming for the merge queue once gates converge — surface is ungoverned.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 18, 2026 13:56
@os-steve
os-steve added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit 161b5ceAug 18, 2026
25 checks passed
@os-steve
os-steve deleted the claude/issue-9521-set-null-multi-semantics branch August 18, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Six hand-written pages document set_null without the multi-value semantics that #9438 settled — remove-the-member, emptied set reads back as []

1 participant

@os-steve