Skip to content

docs(spec): register the user-field deleteBehavior enforcement as an ADR-0087 semantic entry and correct the delete-walk prose, protocol describe included (#29415) - #29712

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-fbdrz1
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-fbdrz1

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Requested by Yibo Wang · project thread
Fixes #29415

Supersedes #29553 (same commits, resumed on a new branch; #29553 is closed)

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

This PR carries the three commits of #29553 (44bec0e9, 21cd9672, 59709cbb) unchanged, merges origin/main twice (744211fc, then 1c1a5d40), and adds the delta that the protocol.ts freeze held back. #29553's body describes the inherited part: the semantic entry user-field-delete-behavior-enforced, the correction of the unreleased #29253 changeset, the prose in field.zod.ts, hook.zod.ts, objectql-engine.ts and the user-field-foreign-reference-refused entry, and the deleteBehavior visibility rule in both forms.

The delta:

  • packages/spec/src/api/protocol.zod.ts, GetMetaItemResponseSchema.cascadeDeleteObjects. The describe listed the cascading relations as "a master_detail by default, a lookup or tree that declares deleteBehavior: 'cascade'". It now reads "a master_detail by default, a lookup, tree or user field that declares deleteBehavior: 'cascade'", the wording of the getCascadeDeleteObjects contract docblock. The engine side already holds: getCascadeDeleteObjects walks cascadeRelationBehavior, whose family test is statesDeleteSemantics, which admits a user field that declares a deleteBehavior (packages/objectql/src/engine.ts, on 744211fc).
  • The other three sites the order named, measured and left as they are. GetMetaItemCachedResponseSchema.cascadeDeleteObjects points at "the item read's cascadeDeleteObjects" and lists no relation kinds; the two [#28001] docblocks (above GetMetaItemResponseSchema and GetMetaItemCachedResponseSchema) speak of "the rule its delete walk runs" and list none either. None of the three says or implies that a user field stays out of the walk, so none is edited.
  • packages/spec/src/data/field.zod.ts, the deleteBehavior docblock (FieldSchema.deleteBehavior defaults set_null onto EVERY field type, so built artifacts ship the key on text/datetime/number fields #9784 paragraph). "and on a user field that AUTHORS one" read as a user field authoring a reference, which the same schema refuses. It now reads "and on a user field that declares a deleteBehavior"; the rest of the parenthetical is re-wrapped, unchanged.
  • Regenerated: content/docs/references/api/protocol.mdx (1 row) and content/docs/references/api/app-bundle.mdx (5 rows), both by gen:docs. No other artifact moved (check:generated: all 14 up to date).
  • Changeset .changeset/29415-user-delete-behavior-entry.md stays a @objectstack/spec patch. Its spec-text bullet now names the cascadeDeleteObjects description on the metadata object read beside the other corrected texts.
  • DELIBERATE CORRECTION of the unreleased Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 note (.changeset/28001-cascade-delete-objects-beside-item.md, patch round 1 at abde58e1, the contract review's should-fix 1): its one sentence listing the cascading relations now reads "a lookup, tree or user field that declares deleteBehavior: 'cascade'", word for word the corrected describe, so the note is true on the day it ships. check-empty-changeset reds on this file and on .changeset/29253-user-field-delete-behavior.md, both corrections by design.
  • Maintainer authorization of the changeset corrections, verbatim in the card thread, 2026-11-03T02:56Z: 「授权你修改」.

Why

Release 6591556737 on #29415 names exactly this delta as owed before landing; the freeze that held protocol.zod.ts was lifted at 6588593530 on #28677. Triage's at-tier record 6588315424 (PASS on 59709cbb) covers the inherited commits; contract-review-hold 6588339023 covers the old head until this edit lands.

Tests

No test added: the delta is describe and comment text, and no reader parses it. Text only, so no accept set moves: Clause-②: no, Metadata-protocol: none.

pnpm --filter @objectstack/spec test and typecheck, head 72a8bffd
  • pnpm --filter @objectstack/spec test: Test Files 803 passed (803), Tests 23790 passed and 1 todo.
  • pnpm --filter @objectstack/spec typecheck: tsc --noEmit, check:scripts-typecheck and check:test-typecheck exit 0 (52 files / 240 errors / 131 pinned signatures held in the debt ledger, unchanged).
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-predicate-path-refs.test.ts: 62 passed (62), with main's predicate count 91 and this branch's literal-comparison count 63 side by side after the merge.
  • All three in one locked run: VERDICT command-exit 0.
  • The same spec suite on the pre-merge head 89540569 (before the second origin/main merge): Test Files 802 passed (802), Tests 23736 passed and 1 todo, typecheck exit 0.

Verification

Build, generated artifacts and derived gates, head 72a8bffd
  • Two merges of origin/main: 83189767 (onto 744211fc, clean) and 72a8bffd (onto 1c1a5d40). The second hit one conflict in packages/spec/src/data/field.form.ts: feat(fields, spec): show a referenceNameVisible lookup's served name as plain text, and offer its field-form row #29639 put its referenceNameVisible row directly above the deleteBehavior row this branch respells. Resolved by hand, keeping both: main's comment and row, and this branch's data.type in ['lookup','user'] gate on deleteBehavior. scripts/pm/os-regen-merge.sh ran after each merge. For app-bundle.mdx, protocol.mdx and hook.mdx it kept the branch's bytes, because main did not touch them.
  • Full build after the second merge (turbo run build --filter=!@objectstack/docs --concurrency=2, under the verify lock): 117/117 tasks, VERDICT command-exit 0. Then pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands, 105 commands on 72a8bffd), run one after another with CMD :: exit N recorded: 104 exit 0, and 1 exits 1 by design, check-empty-changeset --base origin/main on .changeset/29253-user-field-delete-behavior.md (see Acceptance notes). dispatch-gates --ran: 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.
  • No gate is left NOT MEASURED. The built-output gates (check:dual-build-cjs-loads, check:lean-entry-closure, check:i18n, check:docs-transcript-drift, spec check:skill-examples, lint check:doc-formula-expressions, lint check:doc-security-posture) ran on a full build and exit 0. check:dual-build-cjs-loads was NOT MEASURED on docs(spec): register the user-field deleteBehavior enforcement as an ADR-0087 semantic entry and correct the delete-walk prose (#29415) #29553.
  • Local-only state gates: pnpm check:dev-prereqs and pnpm check:skill-frame-freshness exit 0.

Acceptance notes

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. pnpm lint was not run locally: it is CI's run.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…emantic entry and correct the delete-walk prose (#29415)

Item A: semantic migration entry user-field-delete-behavior-enforced; the
#29253 changeset's ADR-0087 marker becomes registered and its stored-row
paragraph names the entry. Item B: FieldSchema docblocks and the user
reference refusal message, the user-field-foreign-reference-refused entry,
the hook-context referential markers and the getCascadeDeleteObjects
docblock say a user field joins the walk when it declares deleteBehavior.
Both metadata forms show deleteBehavior on a user field.

The api/protocol.zod.ts cascadeDeleteObjects describe is held for the
protocol.ts freeze.

Claude-Session: https://claude.ai/code/session_01JQwxbZAexDYPqzEmBW5tH9
Co-authored-by: Claude <noreply@anthropic.com>
…eBehavior gates respelled as in-lists (#29415)

Claude-Session: https://claude.ai/code/session_01JQwxbZAexDYPqzEmBW5tH9
Co-authored-by: Claude <noreply@anthropic.com>
…hat declares cascade, and the deleteBehavior docblock says "declares a deleteBehavior" (#29415)

The object read's `cascadeDeleteObjects` description listed the
cascading relations as a master_detail by default or a lookup or tree
that declares cascade, which left out the user field that #29253 put in
the delete walk. It now names it, matching the getCascadeDeleteObjects
contract docblock and the hook-context descriptions. The FieldSchema
deleteBehavior docblock said a user field "AUTHORS one", which read as
authoring a `reference`; it now says the field declares a
deleteBehavior. The changeset's spec-text bullet names the protocol
description too.

Claude-Session: https://claude.ai/code/session_01WGufzgmDWPyn8w47uKA3CW
Co-Authored-By: Claude <noreply@anthropic.com>
… cascadeDeleteObjects description (#29415)

Claude-Session: https://claude.ai/code/session_01WGufzgmDWPyn8w47uKA3CW
Co-Authored-By: Claude <noreply@anthropic.com>
… list now includes a user field that declares cascade (#29415)

The pending #28001 note lists the cascading relations as a master_detail
by default, or a lookup or tree that declares cascade, the same sentence
the protocol describe carried. A Supersedes paragraph in this PR's own
changeset adds the user field, leaving the #28001 file untouched.

Claude-Session: https://claude.ai/code/session_01WGufzgmDWPyn8w47uKA3CW
Co-Authored-By: Claude <noreply@anthropic.com>
…-fbdrz1

# Conflicts:
#	packages/spec/src/data/field.form.ts
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 0b2b309cb4ce737888f7280b896f7bcece9315bd.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 218 client-bound route-ledger rows — the other 157 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 157: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 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 0b2b309cb4ce737888f7280b896f7bcece9315bd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 12b2d7416b067f7a5b2ff6269acffce02eb8b91b — the merge of head abde58e1ebeeb1d9b90285689286c2f3ed755b0f into base 0b2b309cb4ce737888f7280b896f7bcece9315bd, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 12b2d7416b067f7a5b2ff6269acffce02eb8b91b && git checkout 12b2d7416b067f7a5b2ff6269acffce02eb8b91b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0b2b309cb4ce737888f7280b896f7bcece9315bd abde58e1ebeeb1d9b90285689286c2f3ed755b0f && git checkout -B drift-repro 0b2b309cb4ce737888f7280b896f7bcece9315bd && git merge --no-ff abde58e1ebeeb1d9b90285689286c2f3ed755b0f

node scripts/docs-audit/affected-docs.mjs --json 0b2b309cb4ce737888f7280b896f7bcece9315bd

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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 0b2b309cb4ce737888f7280b896f7bcece9315bd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (account at-tier budget spent, maintainer 2026-10-28; 2026-11-03T02:32Z) · head 72a8bff

Scope for the review: the delta over triage PASS 6588315424 (head 59709cbb, on #29553): the GetMetaItemResponseSchema.cascadeDeleteObjects describe in packages/spec/src/api/protocol.zod.ts, field.zod.ts ≈L1822, the two main merges (incl. the field.form.ts conflict with #29639 resolved keeping both rows), and the changeset bullet superseding the unreleased #28001 note. dev report 6593885723 on #29415. Check Changeset red is the deliberate #29253 changeset correction, as on #29553.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 72a8bffd9fa53f37ade9a33e0e3e80ab0e825a27
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts, packages/spec/src/data/field.form.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/data/object.form.ts, packages/spec/src/migrations/entries/semantic/18.user-field-delete-behavior-enforced.ts, packages/spec/src/migrations/entries/semantic/18.user-field-foreign-reference-refused.ts + packages/objectql/src/engine.ts on the head (statesDeleteSemantics, deleteWalkTargetOf, cascadeRelationBehavior, walkReferencingRelations, getCascadeDeleteObjects), packages/spec/src/migrations/types.ts (SemanticMigration), packages/spec/src/migrations/entries/semantic/18.field-master-detail-set-null-refused.ts (the precedent), packages/spec/src/data/form-delete-behavior-options.test.ts (the pin and its evaluator), .changeset/29253-user-field-delete-behavior.md, .changeset/29415-user-delete-behavior-entry.md, .changeset/28001-cascade-delete-objects-beside-item.md, scripts/check-empty-changeset.mjs (the foreign-changeset rule), ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx (the select widget the forms' deleteBehavior row renders through)

Inputs read: card #29415, body and all 8 comments; PR #29553's 5 comments (triage record 6588315424, PASS on 59709cbb; hold 6588339023; supersession 6593912044); PR #29712's body, its 15-file list and the net diff against main at the merge base 1c1a5d40 (+150 / −42); the 69 check-runs on 72a8bffd (56 success, 10 skipped, 3 failure) and the logs of the three reds. The cited code was read with git show on the head; nothing was checked out, built, run or re-run.

① Derived judgments

  1. Accept set: nothing moves — right. FieldSchema.deleteBehavior stays z.enum(['set_null','cascade','restrict']).optional() with the same .overwrite (materializes set_null on lookup / tree only). The user-field reference refusal changes its message text only, same issue path. No export, type or authorable key is added, removed or re-shaped; check:api-surface, check:authorable-surface and check:docs are green inside TypeScript Type Check.
  2. The semantic entry user-field-delete-behavior-enforced — right shape, right content. A SemanticMigration and not a conversion: no key or value moved, and whether a declaration was meant is the author's to say — the field-master-detail-set-null-refused and user-field-foreign-reference-refused precedents. It is collected by gen:migration-registry into the gitignored registry, so it is no public export; check:spec-changes and check:upgrade-guide are green. conversionIds is rightly omitted (no conversion judges it) and relevantWhen is rightly omitted (0 of the 536 semantic entries declare one, and the surface lives under object fields, not a top-level stack key).
  3. The DELIBERATE CORRECTION of .changeset/29253-user-field-delete-behavior.md — right, each sentence judged. The diff moves exactly two lines of that pending note:
  4. The forms — right. The lookup deleteBehavior row's visibleWhen becomes data.type in ['lookup','user'] in both field.form.ts and object.form.ts; the master_detail row stays disjoint; tree stays unserved (pre-existing, inherited note). A Studio surface change, not a metadata-protocol change: the key was already accepted on user.
  5. Gate verdicts on the head. 56 success, 10 skipped, 3 failure. The two Check Changeset reds are item 3. The PR-body guards red (run 41566264546, 02:27Z, "Another open PR already declares a fix for the same issue") fired while docs(spec): register the user-field deleteBehavior enforcement as an ADR-0087 semantic entry and correct the delete-walk prose (#29415) #29553 was still open; it closed at 02:30Z and the rerun 41566690516 on the same head is success, so the red is superseded. Lint UI is skipped by its path filter (no ui/ path in the diff); the other nine queue-required contexts are success, Governed Surface Queue Guard included.
  6. The packages/lint census pin 65 → 63 — right. Two == literal comparisons left the #7010 corpus census for one in-list per form. feat(fields, spec): show a referenceNameVisible lookup's served name as plain text, and offer its field-form row #29639's referenceNameVisible row, merged in from main, is an in-list too, so the count holds on the merged head; Test Core is green.
  7. Regenerated artifacts — right. protocol.mdx (1 row), app-bundle.mdx (5 rows, the same describe reached through the bundle's five item shapes) and hook.mdx (1 row) reproduce the describes verbatim; check:generated is green.

② Semver level

.changeset/29415-user-delete-behavior-entry.md carries '@objectstack/spec': patch, Clause-②: no and Metadata-protocol: none; the PR body carries the same two lines, line-initial. Right: what the diff publishes from a released package is describe / JSDoc text, a semantic entry and two form visibleWhen values, all in @objectstack/spec; packages/lint moves only a test; content/docs/references is generated output. Kind none takes patch. No skip-changeset, and none is wanted. Clause-②: no with no arm is right — nothing here widens or narrows an accept set; the narrowing is #29253's, and that pending note already declares no (narrowing). check-changeset-no-major is green.

③ Boundary flags

Dev report 6593885723: open_questions: [], security_findings: none, out_of_scope_findings: [], six deviations, each answered:

  1. Attribution link points at the card thread — cosmetic, no action.
  2. protocol.zod.ts: 1 of the 4 named sites edited — verified on the head. GetMetaItemCachedResponseSchema.cascadeDeleteObjects (≈2501) points at "the item read's cascadeDeleteObjects" and lists no kinds; the two [#28001] docblocks (≈646, ≈2480) say "the rule its delete walk runs" and list none. Nothing there implies a user field stays out. Right to leave.
  3. The "Supersedes, in the same release" bullet with the Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 note untouched — should-fix 1 in ④.
  4. The hand-resolved merge in field.form.ts — verified on the head: feat(fields, spec): show a referenceNameVisible lookup's served name as plain text, and offer its field-form row #29639's referenceNameVisible row (an in-list) sits directly above the respelled deleteBehavior row; both are present and both gates serve.
  5. No assignees or reviewers on pr_create — process, in line with the fleet-write rules.
  6. Declaration lines before the 维护者速读 block — fine.

Inherited from #29553's report 6585093974, out_of_scope_findings: (a) tree has no deleteBehavior control — pre-existing, not moved by this PR; (b) the select has no empty option, so a stored value cannot be cleared from the form — verified in SchemaForm.tsx: both select branches render value == null ? '' : String(value) and offer no empty item; for user this is more than a convenience, should-fix 2 in ④; (c) the skills-docs follow-up #29400 is open.

Triage's should-fix on 59709cbb ("AUTHORS one") is done at field.zod.ts ≈1834. Hold 6588339023's condition — the cascadeDeleteObjects describe after the freeze — is met; the freeze was lifted at 6588593530 on #28677. Nothing is escalated.

④ Design judgment

Each hits file, against the code it cites and its first consumer (the #29368 enforcement, read in engine.ts on this head):

  • protocol.zod.ts, GetMetaItemResponseSchema.cascadeDeleteObjects. The describe now reads "a master_detail by default, a lookup, tree or user field that declares deleteBehavior: 'cascade'". The engine agrees: getCascadeDeleteObjects walks collectCascadeDeleteObjects through cascadeRelationBehavior, whose family test statesDeleteSemantics admits a user field exactly when deleteBehavior is one of DECLARED_DELETE_BEHAVIORS; deleteWalkTargetOf answers sys_user for it whether or not a reference is spelled; the non-master_detail arm returns fdef.deleteBehavior || 'set_null', and a user never reaches it undeclared, so a declared cascade resolves to cascade. So the sys_user object read names every object holding a cascade user field. Holds. Its consumer (ui/packages/core/src/actions/recordDelete.ts) reads the served list and keeps no copy of the rule.
  • contracts/objectql-engine.ts, getCascadeDeleteObjects docblock. The same sentence; the re-wrap leaves one over-long line, cosmetic. Holds.
  • data/field.zod.ts. ≈1831–1839: "reads it only on a master_detail, lookup or tree field that carries a reference, and on a user field that declares a deleteBehavior" is statesDeleteSemantics plus deleteWalkTargetOf, sentence for sentence. ≈2749 and the refusal message: "a user field has no default and keeps the id unless it declares one" — true, the .overwrite writes set_null on lookup / tree only and the engine invents no default ("⛔ No default is invented here"). ≈3316, the .overwrite comment — true. The runtime string carries no tracker number. Tried to break it: a user field with deleteBehavior: 'set_null' and required: true, single-valued — the engine escalates to restrict (requiredSetNull && !multiValued), a 409; multiple: true — deferred to the per-row judgement after the dependents probe, refused only where the remainder would be empty. That is exactly what the entry says. Holds.
  • data/hook.zod.ts. referentialFieldClear: the set_null UPDATE resolves on lookup / tree by default and on a user field that declares it, never on master_detail. referentialCascadeDelete: cascade resolves on master_detail unless restrict, and on a declared cascade on lookup, tree or user. Both match cascadeRelationBehavior's two arms. Holds.
  • data/field.form.ts and data/object.form.ts. The predicates are disjoint and total over lookup / user / master_detail, and the pin is real: the test's evaluator parses in-lists with literal members and throws on any other spelling, text resolves to 0 controls, user to exactly 1 with all three values. The claim the design rests on — "the select does not prefill" — holds in the renderer: SchemaForm.tsx's enum-fallback select seeds nothing from schema.default (value == null ? '', the advertised default: 'set_null' is read nowhere on this path), so opening a user field in the designer and saving writes no deleteBehavior, and no unchosen set_null enters the walk. Tried to break it: once a value is chosen there is no empty item to return to "declares nothing", which for user is a distinct, enforced outcome (the id is kept, the field stays out of the walk) — only the Source tab clears it. See should-fix 2.
  • migrations/entries/semantic/18.user-field-delete-behavior-enforced.ts. surface mirrors the precedent's spelling; replacement names the three outcomes and "drop the key"; reason states the old family (DELETE_SEMANTICS_REFERENCE_TYPES), the new rule, the pre-FieldSchema.deleteBehavior defaults set_null onto EVERY field type, so built artifacts ship the key on text/datetime/number fields #9784 stored population and the required + set_null bound — every claim verified in engine.ts above; acceptanceCriteria is a consumer's own verify loop. The strings carry no tracker number. Holds.
  • migrations/entries/semantic/18.user-field-foreign-reference-refused.ts. "which a user field that declares no deleteBehavior does not do" — true. Holds.

Must-fix: none.

Should-fix:

  1. The pending Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 note is left false and corrected from elsewhere. .changeset/28001-cascade-delete-objects-beside-item.md (unreleased on main) still says the cascading relations are "a master_detail by default, a lookup or tree that declares deleteBehavior: 'cascade'", and it ships in the same release as objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253, so it is false on the day it is published. The "Supersedes, in the same release" bullet in the spec(migrations): register the user-field deleteBehavior enforcement (#29253) as an ADR-0087 semantic entry and stop the spec prose saying a user field never joins the delete walk #29415 note is an erratum in a later entry — the shape the CHANGELOG guardrail names as the one a reader never reaches. The honest shape is the one this PR already takes for objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253's note: rewrite that one sentence in the pending Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 note under the same DELIBERATE CORRECTION confirmation (same red, same class, this record's ①.3 extended by one sentence) and drop the Supersedes bullet. The published spec text is right either way, so the patch round is the lane's call.
  2. File a card for the form's missing empty option. The Studio deleteBehavior select (both forms, through SchemaForm.tsx's select widgets) offers no empty item, so on a user field a value once chosen cannot be returned to "declares nothing" from the form, and for user that absence is a distinct, enforced outcome. That is a metadata-authoring trap of the kind Prime Directive chore: version packages #10 says to file, not to note; the widget is ui/, so it is not this PR's to fix, but it should leave as an issue rather than an acceptance note.

Landing condition: the two Check Changeset reds are the DELIBERATE CORRECTION class, confirmed by this record (each rewritten sentence judged in ①.3); the first PR-body guards red is superseded by the green rerun on the same head. No governed path is in the file list. The PR may enter the queue with the Check Changeset red named, under the landing-operations three conditions.

Implemented-by: claude/project-thread-fbdrz1
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

…eld that declares cascade, replacing the #29415 supersedes bullet (#29415)

Contract review should-fix: correct the one sentence in the unreleased
#28001 note that lists the cascading relations, so it matches the
corrected describe, and drop the "Supersedes, in the same release"
bullet from the #29415 changeset that carried the same correction.

Claude-Session: https://claude.ai/code/session_01WGufzgmDWPyn8w47uKA3CW
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: abde58e1ebeeb1d9b90285689286c2f3ed755b0f
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts, packages/spec/src/data/field.form.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/data/object.form.ts, packages/spec/src/migrations/entries/semantic/18.user-field-delete-behavior-enforced.ts, packages/spec/src/migrations/entries/semantic/18.user-field-foreign-reference-refused.ts + .changeset/28001-cascade-delete-objects-beside-item.md, .changeset/29415-user-delete-behavior-entry.md, .changeset/29253-user-field-delete-behavior.md, scripts/check-empty-changeset.mjs (the foreign-changeset rule), packages/objectql/src/engine.ts on the head (cascadeRelationBehavior, statesDeleteSemantics, getCascadeDeleteObjects), plus every cited file of record 6594347454

Delta record over record 6594347454 (PASS on 72a8bffd9fa53f37ade9a33e0e3e80ab0e825a27), which adopted this seat's should-fix 1. abde58e1 is exactly one commit on 72a8bffd (parent verified with git rev-list --parents), no main merge, and its diff is +1 / −2 over two changeset files and nothing else (git diff 72a8bffd abde58e1): every hits file and every cited code file is byte-identical to the reviewed head. The ①, ②, ③ and ④ judgments of 6594347454 therefore carry unchanged, and this record judges only the delta.

① Derived judgments

  1. Accept set, exports, forms, entries: nothing moves. The delta touches no file under packages/, content/ or ui/; the 72a8bff judgments on the accept set, the semantic entry, the forms and the regenerated artifacts stand as written.
  2. DELIBERATE CORRECTION of the unreleased Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 note — right, judged sentence by sentence. .changeset/28001-cascade-delete-objects-beside-item.md exists on the merge base, was not added by this PR, and is pending on main. The diff moves exactly one line of it (line 14, the Served beside the object bullet), and within that line exactly one parenthetical: "(a master_detail by default, a lookup or tree that declares deleteBehavior: 'cascade')" becomes "(a master_detail by default, a lookup, tree or user field that declares deleteBehavior: 'cascade')". The new sentence is true on the head and will be true on the day it ships: it is word for word the corrected GetMetaItemResponseSchema.cascadeDeleteObjects describe (judged in 6594347454 ④ against cascadeRelationBehavior → statesDeleteSemantics, which admits a user field exactly when it declares a deleteBehavior, so a declared cascade on a user field is named by getCascadeDeleteObjects). The old sentence would have been false in the same release as objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253. Every other sentence of the note — the frontmatter, Clause-②: yes (widening), Metadata-protocol: added, the Skills-docs: line, the ETag, absence and [] statements, the objectql and spec bullets — is untouched, and nothing false is restored.
  3. The Supersedes, in the same release bullet is deleted from .changeset/29415-user-delete-behavior-entry.md — right. It was the erratum-in-a-later-entry shape 6594347454 named; with the Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 note itself corrected it had nothing left to say. The note's other bullets (Migration entry, Spec text, Studio forms), its frontmatter ('@objectstack/spec': patch) and both declaration lines are unchanged.
  4. The objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253 correction carries. .changeset/29253-user-field-delete-behavior.md is byte-identical to 72a8bffd; its two rewritten sentences were judged in 6594347454 ①.3 and that judgment stands.
  5. Gate verdicts on the head, as read at 2026-11-03T03:1xZ. 46 runs: 13 success, 9 skipped, 1 failure, 23 in progress. The one failure is Check Changeset (job 125700476084), and its log is the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 foreign-changeset refusal on exactly two files — .changeset/28001-cascade-delete-objects-beside-item.md and .changeset/29253-user-field-delete-behavior.md — and nothing else; both are the DELIBERATE CORRECTION class, both are judged here (item 2) and in 6594347454 (①.3), and this record is their confirmation under the landing rule 「其余(含改代码)照全套:同 head 达档复核 PASS 记录即确认,记录逐句判改写句」. A second Check Changeset run (job 125701086108) was still in progress; it reads the same two files and can only answer the same. Governed Surface Queue Guard, PR-body guards and Spec property liveness are success. The remaining in-progress runs — Build Core, Test Core (1–8/8), the Lint · and Type Check · lanes, Build Docs, Build UI Deps, Live E2E UI — read no changeset body, and the delta moves nothing else, so no verdict of theirs can turn on this delta; they were all success on 72a8bffd. ⛔ This record does not claim them green: the seat adopting it reads them complete before it posts (see the landing condition).

② Semver level

Unchanged from 6594347454: .changeset/29415-user-delete-behavior-entry.md keeps '@objectstack/spec': patch, Clause-②: no, Metadata-protocol: none, and the PR body carries the same two lines. The delta publishes nothing: it edits the text of two pending release notes and adds no changeset. The #28001 note's own frontmatter and declarations (minor on spec, Clause-②: yes (widening), Metadata-protocol: added) are untouched, so its level is not re-judged here.

③ Boundary flags

  • The PR body's Acceptance notes now carry a paragraph naming the Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 correction (patch round 1 at abde58e1, should-fix 1 of the contract review) and stating that check-empty-changeset reds on both files by design — read and found accurate against the tree and the job log.
  • No new dev report on the card after 6593885723 at read time; no new open_questions. The 6594347454 ③ answers carry.
  • Should-fix 2 of 6594347454 (file a card for the form's missing empty option) is not addressed by this delta and is not owed by it; it stays a should-fix, not a landing condition.

④ Design judgment

Every hits file is byte-identical to 72a8bffd; the ④ findings of 6594347454 carry whole. On the delta: the corrected #28001 sentence describes the served cascadeDeleteObjects list exactly as the engine computes it on this head (getCascadeDeleteObjects → collectCascadeDeleteObjects → cascadeRelationBehavior, family test statesDeleteSemantics; a user field with a declared cascade resolves to cascade through the non-master_detail arm), and the CHANGELOG a reader greps for cascadeDeleteObjects will now carry one true sentence where it carried a false one plus an erratum. Tried to break it: is there any other pending note that lists the cascading kinds? git grep of .changeset/ on the head for the old lookup or tree wording names only the two files this PR already corrects. Holds.

Must-fix: none.

Should-fix: none new. Should-fix 2 of 6594347454 stands as filed there.

Landing condition: both Check Changeset reds (the two DELIBERATE CORRECTIONS) are confirmed by this record and by 6594347454; the PR may enter the queue with that red named, under the landing-operations three conditions, ONCE the runs listed as in progress in ①.5 have completed and every queue-required context is success — a red among them is not this delta's (it moves no file they read) and names a new round, not a re-run.

Implemented-by: claude/project-thread-fbdrz1
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

维护者速读

席位意见:文字与迁移条目对照引擎逐句成立,changeset 更正后发版当天为真,同意落地。

席位核对 — ACCEPT · domain:spec#2 · head abde58e1ebeeb1d9b90285689286c2f3ed755b0f

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 03:27
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 3, 2026 03:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 63f962f Nov 3, 2026
68 of 70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-fbdrz1 branch November 3, 2026 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

2 participants