Skip to content

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

Description

@os-zhuang

Filing unassigned — recording, not claiming. Surfaced by the Docs Drift Check on PR #9520 (card #9438) and deliberately not fixed there, for the reasons below.

The gap

deleteBehavior: 'set_null' on a multiple: true reference field now has settled, permanent semantics:

  • the deleted member is removed from the stored array; the rest are kept;
  • an emptied set reads back as [], never null;
  • required on a multi-value lookup means non-empty array.

Ruled by the maintainer on #9447 (2026-08-18), landed as verbatim contract in packages/spec/src/data/field.zod.ts (the multiple and required doc blocks) via #9493 (df0c12de7), and implemented in the engine by #9520 (card #9438).

None of the six hand-written pages that document set_null says any of it. Each was flagged by the drift check via the set_null literal:

  • content/docs/api/data-api.mdx
  • content/docs/data-modeling/field-types.mdx
  • content/docs/data-modeling/fields.mdx
  • content/docs/data-modeling/validation-rules.mdx
  • content/docs/deployment/troubleshooting.mdx
  • content/docs/protocol/objectql/types.mdx

⛔ content/docs/releases/v15.mdx also names set_null and is release-owned / read-only — it is listed here only so nobody "helpfully" edits it. If it is factually wrong, that is a separate docs-only PR, never a rider.

Why it wasn't fixed in #9520

Two rulings, both mine as PM, recorded so they aren't re-litigated:

  1. On fix(engine): probe a multiple:true reference field with a spelling its storage answers #9437 (the interim holding position) I ruled no doc edits, because that PR shipped a deliberately temporary restrict escalation designed to be deleted in one line. Six pages describing a temporary refusal would have had to be unwritten a few hours later. I said then that the docs question "becomes real and permanent when cascadeDeleteRelations' set_null limb nulls the WHOLE multi-value array, dropping every other live reference #9438 answers the semantics; it should be settled there, once."
  2. On fix(engine): set_null on a multiple:true reference removes the deleted member; emptied set stored as [], never null; drop the #9437 interim 409 hold #9520 — that moment — I ruled still not in the code PR. The pages span surfaces that diff does not otherwise touch, the contract sentence already landed where it belongs (FieldSchema), and doc churn riding a code change is how the release-notes guardrail got written in the first place. But the gap is real now that the semantics are permanent, so it gets a card rather than evaporating.

Not a blanket "update six pages"

Each page needs judging on its own — some may only need a clause, some may need nothing:

  • data-modeling/fields.mdx and field-types.mdx are the likeliest to owe a real sentence, since they document deleteBehavior where an author chooses it.
  • protocol/objectql/types.mdx is protocol-level and may want the residual-shape guarantee stated precisely.
  • api/data-api.mdx, validation-rules.mdx and deployment/troubleshooting.mdx may only mention set_null in passing — check before writing.

⚠️ Cite packages/spec/src/data/field.zod.ts, do not paraphrase it. The spec seat owns that sentence; #9520's engine change consumes it by citation precisely so there is one source of truth. A hand-written page that restates the contract in its own words becomes a second one that can drift with nothing to catch it.

Related and adjacent

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    First-touch grading (triage seat): promoted finding → pm:queue, routed domain:devx (all six files are content/docs/** hand-written pages), type Task.

    The semantics are permanent (ruled on #9447, contract landed in field.zod.ts via #9493, engine landed via #9520), so the docs gap is real now and only ages. The card already carries the discipline the claimant needs: per-page judgement rather than a blanket edit, cite packages/spec/src/data/field.zod.ts rather than paraphrase, releases/v15.mdx is ⛔ read-only, and the validation-rules.mdx half must not promise the #9476 enforcement that isn't live yet.

    Size/model suggestion: S–M (six pages to judge, likely 2–3 needing real sentences).


    Generated by Claude Code

  2. claude commented on Aug 18, 2026

    @claude
    Contributor
    {
      "issue": 9521,
      "status": "done",
      "branch": "claude/issue-9521-set-null-multi-semantics",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9627",
      "premise_still_valid": true,
      "summary": "All six pages edited, each judged on its own job rather than given one pasted block. Classification (PM's untested assumption): exactly ONE was wrong — protocol/objectql/types.mdx stated the single-value behaviour as universal ('set_null: Set field to null'); one was imprecise — data-modeling/fields.mdx listed null / empty-string / [] as interchangeably 'exactly what set_null writes', true of none of them individually; the other four were silent, and their silences differ, so their fixes do: field-types.mdx got one clause in each of the two table cells where an author picks deleteBehavior and multiple; troubleshooting.mdx got the answer in place for the mid-incident reader (a cleanup query looking for null will not match an emptied set); data-api.mdx got the read-back shape on DELETE /data/:object/:id, where an integrator's client branches; validation-rules.mdx got the live representation guarantee plus the ruled required-means-non-empty half marked DECLARED BUT NOT YET ENFORCED (validator isMissing treats only undefined/null/blank-string as missing, so [] passes today — #9476), so the page promises nothing the runtime lacks. Contract quoted from packages/spec/src/data/field.zod.ts (the multiple and required doc blocks), and every page cites it via the generated /docs/references/data/field page, which renders those exact describe strings — a pointer that cannot drift instead of a seventh restatement. One correction to the card's paraphrase: the member-removal half is NOT in the spec's deleteBehavior describe (that one still reads 'What happens if referenced record is deleted'); it is stated in the multiple doc block's parenthetical and in cascadeDeleteRelations (engine.ts:10206-10212), which is what I cited for it. SURVEY (six is the right edit set, derived independently, not inherited): grep of content/docs for set_null gives 8 files — the six plus references/data/field.mdx (AUTO-GENERATED from field.zod.ts, already carries both sentences verbatim, spec is its fix site) and releases/v15.mdx (release-owned, untouched); grep for deleteBehavior finds 2 further hand-written pages the set_null literal key cannot see (getting-started/common-patterns.mdx, protocol/objectql/schema.mdx) — both silent, neither states an empty representation, so neither needed an edit, but the key would have missed them had they been wrong; grep for multiple:true / multi-value finds 5 more pages, none making a null/empty claim. Judgment PM may want to check: writing the not-yet-enforced required rule on validation-rules.mdx WITH an explicit 'declared but not yet enforced (#9476)' warning, rather than staying silent about it.",
      "tests": "All at the final commit 496b3de13 (git rev-parse --short HEAD after the last commit; union re-run on that tree). pnpm check:nul-bytes -> 'check-nul-bytes: OK (scanned 6173 text file(s) ... no raw ASCII control bytes)'. pnpm check:docs-audit-scope -> 'docs-accuracy-audit scope is in sync with content/docs/: 179 hand-written doc(s)' + 'release-owned pages are in scope and read-only: 9 page(s)'. pnpm check:docs-redirects -> 'OK (apps/docs/redirects.mjs: 92 entries ...)'. pnpm check:role-word -> 'OK (43 baselined file(s), no new occurrences)'. Spec liveness family (dispatch-gates derived from my actual changed paths, run under flock /tmp/os-heavy-verify.lock): check:empty-state rc=0 'all classified (1 closed, 2 open, 4 output, 9 scope)'; check:liveness rc=0; check:strictness-ledger rc=0; check:variant-docs rc=0 '18 discriminated union(s) — 8 governed, 10 exempt'. 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 'Expected a closing tag ... before the end of paragraph', rc=1 — so the harness is not silently green. No unit tests apply (docs-only). CI snapshot at report time on 496b3de13: 24 check runs — 11 success, 9 skipped, 4 in_progress, 0 failure; CI convergence is the PM's half.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9625: types.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, and the same branch refuses a delete on a required multiple:true lookup even when member removal would leave the set non-empty, which the #9447 ruling accepts. Different defect class and the correct form is a decision, so not fixed in this PR."
      ]
    }
    

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions