Skip to content

docs(skills): master_detail deleteBehavior admits cascade/restrict only — drop the set_null teaching #9689 refuses - #11869

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-11409-data-skill-deletebehavior
Aug 25, 2026
Merged

docs(skills): master_detail deleteBehavior admits cascade/restrict only — drop the set_null teaching #9689 refuses#11869
hotlong merged 1 commit into
mainfrom
claude/issue-11409-data-skill-deletebehavior

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11409

The published objectstack-data skill taught deleteBehavior: 'set_null' on a master_detail field. That was already wrong before #9689 — the engine resolved every non-restrict value on this type to cascade, so the declaration promised keep-the-children and delivered delete-the-children — and since #9689 landed (PR #11406) it is a named parse-time rejection: an AI author following the skill produced metadata that fails publish with a 422, manufacturing exactly the error the rejection exists to catch.

Premise re-verified on origin/main (c4db3116)

Holds. All four spots the card names were present at the branch point, and the contract is settled in packages/spec/src/data/field.zod.ts:1634:

if(field.type==='master_detail'&&field.deleteBehavior==='set_null'){ctx.addIssue({code: 'custom',path: ['deleteBehavior'],message: ... });}

whose message is the source of truth for the wording: "a detail row cannot outlive its master … declare 'restrict' … declare 'cascade' (or omit the key) … or use a lookup field if the children must survive the parent."set_null remains legal on lookup — the superRefine fires only on master_detail, and the migration entry pins it: "set_null on lookup parse [unchanged]".

What changed — the per-type vocabulary, in four in-place edits

Whole-package sweep, not one line: grep -rn "set_null" skills/ was the entry point and is now clean of the wrong combination in every published skill.

rules/relationships.md

  • Delete Behaviors intro now states the rule: master_detail admits cascade or restrictonly; an authored set_null is refused at publish.
  • The set_null table row is deleted. Its "Manager → Employees" example was doubly wrong — a relationship whose children must survive the parent wants a lookup, never a master_detail.
  • Code sample comment: // or 'restrict' or 'set_null'// or 'restrict' — 'set_null' is refused here.
  • Best practice Add Changesets and GitHub Actions automation #4: "Always specify cascade/restrict/set_null" → "Always specify cascade or restrict".
  • set_null's real home is now stated where it belongs, on the existing Use lookup When bullet — no new paragraph.

rules/field-types.md

  • master_detail row: deleteBehavior (cascade/restrictset_null is refused).
  • lookup row gains deleteBehavior to its Key Config, so the reader of the type table alone can see which type carries the key without it.

Size discipline (maintainer ruling 2026-08-21)

Both readings, tokens in the ratchet convention ceil(utf8 bytes / 4):

ReadingBeforeAfterNet
rules/relationships.md (whole file) — lines4154150
rules/relationships.md (whole file) — tokens37633778+15
rules/field-types.md (whole file) — lines4284280
rules/field-types.md (whole file) — tokens35783587+9
Whole package (all 12 .md under skills/objectstack-data) — lines493449340
Whole package — tokens4692446949+25

Net line delta is exactly zero in both files and across the package: every correction is paid for in place, and the deleted table row funds the two-line intro. The +25 tokens (+0.05% of the package) is the residue of replacing a shorter wrong statement with a slightly longer right one; a first draft that added a trailing explanatory paragraph measured +136 bytes and was cut back to this. Nothing was expanded, no section was added, and one table row is gone.

Token ratchet: nothing to lower here, and that is measured, not assumed

The card's shape (mirroring #11748) expected a ceiling to come down. It does not move, for a checkable reason: scripts/check-skills-token-ratchet.mjs prices skills/<name>/SKILL.mdonly — its discoverSkillFiles() maps each directory to exactly skills/${entry.name}/SKILL.md. This diff touches no SKILL.md, so no priced file changed:

✓ check-skills-token-ratchet: skills/objectstack-data/SKILL.md is 13817 tokens (ceiling 13817; headroom 0).+0

#11748 differed precisely here: it edited skills/objectstack-api/SKILL.md, which is priced, so it could lock its saving in at 6348 → 6342. rules/*.md is unpriced by any ratchet in the tree. Lowering the objectstack-data ceiling anyway would price a file this PR did not shrink, against the gate's own discipline.

scripts/pm/dispatch-gates.mjs flags this gate for exactly this trap — "⛔ One of YOUR paths is under skills. … this silent is not evidence about your path in EITHER direction — it is the shape that reads as a clearance and is not. Read the gate before treating it as one." It was read; the silence is correct.

Changeset: none, skip-changeset applied

Judged from the diff, same test as #11748: skills/ has no package.json, no published workspace package's files[] covers it, and the repo root is private: true. Both touched paths are published-catalog source, not an npm artifact — nothing publishable moves.

Gates — all run locally at ebe0d9ba (the final commit), each quoting its own verdict line

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no path args), re-derived after the commit — same 7 families, change set read as committed 2, working tree 0. Exit codes captured by redirecting to a file before any pipe, never from $? after a tail.

GateVerdict line
check:agent-test-spelling✓ 0 violations — 357 file(s)
check:doc-authoring✓ doc authoring guard: 389 files clean — no bare metadata literals.
check:doc-formula-expressions✓ 22 record-scoped formula example(s) across 421 files / 1448 TS blocks judged clean
check:pm-governed-merges✓ check-governed-merges --self-test: 129 assertions
check:role-wordcheck-role-word: OK, no new occurrences of the reserved word.
check:skill-compatibility✓ 11 SKILL.md file(s) reconciled against 78 workspace packages
check:skill-frame-sync✓ 4 copies of the decision frame are structurally isomorphic across 3 files

Plus the families a skills/** edit earns beyond the path derivation — run because the derivation is a clue, not a spec:

GateVerdict line
check:nul-bytescheck-nul-bytes: OK (scanned 6601 text file(s) … no raw ASCII control bytes)
check-skills-token-ratchet.mjs✓ 11 published SKILL.md within their ceilings.
check:skill-examples✅ 256 prose examples type-check across 3 surface(s)
check:skill-docs✅ Skill docs in sync
check:skill-refs✅ 9 generated files in sync with packages/spec

check:skill-examples type-checks the ```typescript fence this PR edits — the edited master_detail sample compiles, which is the proof the surviving example is still valid metadata.

Two gates refused rather than measured on first run, both for missing builds, and neither was a finding: check:doc-formula-expressions could not resolve @objectstack/formula/@objectstack/lintdist, and check:skill-examples refused on an unbuilt packages/client-react/dist (it fails loudly rather than returning a false green). Both were re-run to a real verdict after building the closures; the greens above are the post-build runs, at the final commit.

ESLint — a measured narrowing, not a skipped run.pnpm lint was narrowed to the two changed paths, and the narrowing is proven rather than asserted: (1) the population is read from eslint's own configuration, which reports File ignored because no matching configuration was supplied. for both — markdown is not in eslint's configured population at all; (2) the file count is 2, read from --format json, 0 errors; (3) invariance for untouched files follows from (1) — a diff confined to a file type eslint has no configuration for cannot move any other file's verdict.

Landing

Governed surface (skills/**, Prime Directive #14) — this PR stays draft, is never queued, never armed for auto-merge, and never flipped ready. Human merge only.


Generated by Claude Code

The published objectstack-data skill taught `deleteBehavior: 'set_null'` on a
`master_detail` field in four places. That was already wrong before #9689 — the
engine resolved every non-`restrict` value on this type to `cascade`, so the
declaration promised keep-the-children and delivered delete-the-children — and
since #9689 landed (PR #11406) it is a named parse-time rejection: an AI author
following the skill produced metadata that fails publish with a 422.
Teaches the per-type vocabulary instead: `master_detail` admits `cascade` /
`restrict`; `set_null` belongs on `lookup`, where children survive the parent.
Correction, not expansion — net line delta is exactly 0 in both files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/s documentation Improvements or additions to documentation labels Aug 24, 2026
@hotlong
hotlong marked this pull request as ready for review August 25, 2026 00:34
@hotlong
hotlong added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 79cdfe9Aug 25, 2026
27 checks passed
@hotlong
hotlong deleted the claude/issue-11409-data-skill-deletebehavior branch August 25, 2026 00:52
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.

objectstack-data skill teaches deleteBehavior:'set_null' on master_detail — wrong before #9689, a parse-time rejection after it

2 participants

@hotlong@claude