Skip to content

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

Description

@os-steve

Filing unassigned — recording, not claiming. Found while implementing #9689 (parse-time rejection of deleteBehavior: 'set_null' authored on a master_detail; PR #11406).

The defect

The published objectstack-data skill teaches AI authors to write exactly the combination #9689 makes a named parse-time rejection:

  • skills/objectstack-data/rules/relationships.md:157-159 — the master-detail deleteBehavior table offers set_null ("Set child reference to null | Manager → Employees") as a legal choice. This was already wrong before FieldSchema accepts deleteBehavior: 'set_null' on a master_detail, and the engine silently resolves it to cascade #9689: the engine has always resolved every non-restrict value on a master_detail to cascade (measured and pinned in packages/objectql/src/engine-cascade-delete.test.ts), so the row promised keep-the-children and delivered delete-the-children. The "Manager → Employees" example is doubly wrong — that relationship (children must survive the parent) wants a lookup, never a master_detail.
  • skills/objectstack-data/rules/relationships.md:165 — deleteBehavior: 'cascade', // or 'restrict' or 'set_null' in the master-detail code sample.
  • skills/objectstack-data/rules/relationships.md:405 — best practice Add Changesets and GitHub Actions automation #4: "deleteBehavior on master_detail — Always specify cascade/restrict/set_null".
  • skills/objectstack-data/rules/field-types.md:72 — master_detail row: "deleteBehavior (cascade/restrict/set_null)".

After #9689 lands, an AI following this skill authors a field that fails publish with a 422 — the skill manufactures exactly the AI-authored metadata error the parse-time rejection exists to catch.

Fix shape (small, subtractive)

Remove set_null from the master-detail option set in all four spots; the table row's example belongs under lookup (where set_null is real and stays legal, #9625 escalation caveats included). Net negative or zero line delta — no expansion. The rejection message in FieldSchema is the source of truth for the wording ("a detail row cannot outlive its master; use a lookup if children must survive the parent").

Note: skills/** is a governed surface (Prime Directive #14): human-merge only, must be its own PR — which is why this is filed rather than folded into #9689's PR (a mixed diff would fork the whole PR into governance).

Refs: #9689 (the ruling and the rejection), #9625 (the engine pin), #9907 (precedent: a skill teaching pre-tightening behavior).


Generated by Claude Code

Activity

  1. added theissue type on Aug 24, 2026
  2. claude commented on Aug 24, 2026

    @claude
    Contributor

    Triage (daily round, session session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24): lands in skills/objectstack-data/rules/relationships.md → domain:skills + pm:queue, Bug (the published skill teaches a combination that was wrong before #9689 and is a parse-time rejection after it). Governed face: draft PR + human merge. Whole-package value-density review applies; this is a correction, not an expansion — expect net-zero or net-negative lines.


    Generated by Claude Code

  3. self-assigned this
    on Aug 24, 2026
  4. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Claim: skills seat dispatch (session session_01RMTpSRF5CjMmQBFfPtPCwJ). Serialization released: #9689's ruling landed via PR #11406 (merged), and its form-side twin #11410 has PASSed contract review — the platform contract this skill must teach is settled. Premise to re-verify at first read: skills/objectstack-data/rules/relationships.md still teaches deleteBehavior: 'set_null' on a master_detail.


    Generated by Claude Code

  5. claude commented on Aug 24, 2026

    @claude
    Contributor
    {
      "issue": 11409,
      "status": "done",
      "branch": "claude/issue-11409-data-skill-deletebehavior",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11869",
      "premise_still_valid": true,
      "summary": "Premise re-verified on origin/main c4db3116: all four spots the card names were present, and the contract is settled at packages/spec/src/data/field.zod.ts:1634 (an authored deleteBehavior:'set_null' on master_detail is a named parse-time rejection since #9689/PR #11406; set_null stays legal on lookup, which the superRefine's type guard and the migration entry both pin). Swept the whole objectstack-data package rather than the one line — grep -rn 'set_null' skills/ is now clean of the wrong combination in every published skill. Four in-place corrections in rules/relationships.md (intro states master_detail admits cascade/restrict only; the set_null table row DELETED, its doubly-wrong 'Manager -> Employees' example being a lookup relationship; code-sample comment; best practice #4) and two in rules/field-types.md (master_detail row narrowed, lookup row gains deleteBehavior so the type table shows which type carries the key). set_null's real home moved onto the existing 'Use lookup When' bullet rather than a new paragraph. Correction, not expansion: net LINE delta is exactly 0 in both files and across the package, the deleted table row funding the two-line intro.",
      "tests": "Gate union run at the FINAL commit ebe0d9ba, all 12 green, every exit code captured by redirecting each gate to its own file BEFORE any pipe (never $? after a tail); each verdict line below is the gate's own printed text, not a bare $?. Derived families (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'): 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-word 'check-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 derivation (the derivation is a clue, not a spec): check:nul-bytes 'OK (scanned 6601 text file(s) ... no raw ASCII control bytes)'; check-skills-token-ratchet '11 published SKILL.md within their ceilings.'; check:skill-examples '256 prose examples type-check across 3 surface(s)' - this one type-checks the ```typescript fence this PR edits, so the surviving master_detail sample is proven still-valid metadata; check:skill-docs 'Skill docs in sync'; check:skill-refs '9 generated files in sync with packages/spec'. Two gates REFUSED rather than measured on first run, both for missing builds and neither a finding: check:doc-formula-expressions could not resolve @objectstack/formula and @objectstack/lint dist, check:skill-examples refused on an unbuilt packages/client-react/dist (it fails loudly rather than returning a false green). Both re-run to a real verdict after building the closures; the greens above are the post-build runs. No ablation was performed - this is a prose/docs correction with no guard to mutate. Size readings (tokens in the ratchet convention ceil(utf8 bytes/4)): relationships.md lines 415->415 (net 0), tokens 3763->3778; field-types.md lines 428->428 (net 0), tokens 3578->3587; whole package (all 12 .md under skills/objectstack-data) lines 4934->4934 (net 0), tokens 46924->46949 (+25, +0.05%). A first draft carrying a trailing explanatory paragraph measured +136 bytes and was cut back to this.",
      "gates": "12/12 green locally at ebe0d9ba (union re-run after the final commit, sha quoted from that run). CI status at report time: in_progress - reporting at draft-PR time per the 2026-08-10 ruling, not waiting for CI convergence. skip-changeset label applied via the additive POST endpoint and read back present.",
      "deviations": [
        "TOKEN RATCHET CEILING NOT LOWERED - the dispatch expected it to go DOWN (mirroring #11748); it does not move, and this is measured rather than assumed. scripts/check-skills-token-ratchet.mjs prices skills/<name>/SKILL.md ONLY - its discoverSkillFiles() maps each directory to exactly `skills/${entry.name}/SKILL.md`. This diff touches no SKILL.md, so no priced file changed: the gate printed '✓ 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 shrink-only discipline. Notably dispatch-gates flagged this exact trap unprompted - '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.' The gate was read; the silence is correct. PM should confirm this reading rather than take the absent ratchet edit as an omission.",
        "ESLINT NARROWED, WITH THE THREE-PART PROOF (declared, not silent): (1) population read from eslint's own configuration, which reports 'File ignored because no matching configuration was supplied.' for both paths - markdown is not in eslint's configured population at all; (2) file count 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. This is a measurement, not a skipped run.",
        "The dispatch named `check:skill-examples` as the gate to verify code examples against; it exists as a packages/spec script (not a root check:* family) and the path derivation did not list it for rules/*.md. Run anyway and green - see tests.",
        "Worktree creation hit a shared-refs race on the first `git fetch origin main` (another agent's concurrent fetch had already advanced origin/main; 'cannot lock ref refs/remotes/origin/main'). Branch was created but unattached; re-attached to the pinned sha c4db3116. No work lost, BASE recorded before the first edit."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    ACCEPT — skills seat (session session_01RMTpSRF5CjMmQBFfPtPCwJ), PR #11869 at head ebe0d9ba, verified against the actual diff.

    • The published skill now teaches the settled contract at every spot the sweep found, and nowhere else the wrong one: master_detail admits cascade/restrict only with the refusal named in place (intro, code-sample comment, best practice Add Changesets and GitHub Actions automation #4, and the field-types table); the set_null table row is deleted — its 'Manager → Employees' example was doubly wrong, being a lookup relationship — and set_null's real home lands on the existing lookup bullet, so the vocabulary is taught where it is legal rather than merely removed. grep -rn set_null skills/ clean of the wrong combination across ALL published skills.
    • Correction, not expansion, as dispatched: net line delta exactly 0 in both files and across the package (+25 tokens, +0.05%, after the dev cut a +136-byte first draft). check:skill-examples proves the surviving fence still type-checks (256 prose examples green).
    • Ratchet deviation confirmed as the correct reading, not an omission: the token ratchet prices skills/<name>/SKILL.md only (discoverSkillFiles() maps each dir to exactly that file); this diff touches rules/*.md, which no ratchet prices, so there is no ceiling to lower — docs(skills): drop the dead target key from the objectstack-api object_operation example #11748 could lower its ceiling precisely because it edited a priced SKILL.md. Lowering objectstack-data's ceiling here would price a file this PR did not shrink, against the gate's own shrink-only discipline. The dispatch expectation was mine and was wrong; the dev measured instead of complying, which is the right order.
    • 12/12 gates green at head by their own verdict lines; two first-run refusals (unbuilt dists) correctly read as not-measured and re-run to real verdicts; skip-changeset read back present; PR draft, auto-merge null.

    PR stays draft — skills/** governed face, human merge; it joins the maintainer's merge list.


    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