Skip to content

docs(skills): the invariant tool freezes a violating row, it does not brick it — and name the "required when X" trap - #15010

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-14893-data-skill-tool-choice
Sep 3, 2026
Merged

docs(skills): the invariant tool freezes a violating row, it does not brick it — and name the "required when X" trap#15010
os-zhuang merged 1 commit into
mainfrom
claude/issue-14893-data-skill-tool-choice

Conversation

@os-steve

@os-steve os-steve commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14893

Deliverable 2 of #13879: publish the transition-gate vs invariant tool-choice line in the data-modeling skill. The card's one executable criterion was already met in substance at origin/main 77a532d6 — the "Choose by intent — invariant or transition gate" bullet landed on 2026-09-01 and already names both tools in both directions — so this PR lands the three measured deltas the dispatching seat re-scoped it to, and nothing else.

What changed — two sites, both in skills/objectstack-data/

1. SKILL.md, the "Choose by intent" bullet (rewritten in place)

Before:

Choose by intent — invariant or transition gate. A fact that must hold for every stored record is an invariant: express it in validations[]. A condition on a transition ("required once the record reaches paid") is requiredWhen / field bounds, which let already-stored rows through. A transition gate written as an invariant bricks existing data; an invariant written as a transition gate never enforces itself.

After:

Choose by intent — invariant or transition gate. A fact true of every stored record is an invariant: a validations[] script rule. A row that violates it is refused on any edit until repaired. A transition condition ("required once the record reaches paid") is requiredWhen / field bounds, which judge the write, not the stored row. "Required when X" reads like an invariant and is not one; an invariant written as a gate never enforces itself.

Two deltas, both traded for words inside the bullet rather than added to it:

  • (a) "bricks existing data" was wrong in the direction that matters. The engine freezes the row, it does not brick it: the violating row is refused on any edit until a repairing write lands, and a repairing write is accepted. "Bricks" tells an author the data is unrecoverable, which is exactly the false belief that pushes them onto the gate tool for an invariant.
  • (b) the trap is now named. "Required when X" reads like an invariant and is not one — the downstream pull is three rules written as invariants, all built with the gate tool, zero signal.

2. rules/validation.md, the Script Validation section (two-sentence pointer added)

The section taught the script mechanics with no tool-choice statement at all. It now opens with:

Tool choice — this is the invariant tool. script and cross_field are judged against the merged record on every write, with no pre-state exemption, so a row that already violates the rule is refused on any edit until a repairing write lands (frozen, not bricked). A condition that must hold only from a transition onward is not this tool: use requiredWhen or a field bound (min / max / minLength / maxLength), which judge the write, not the stored row (see Choose by intent — invariant or transition gate in SKILL.md).

The cross-reference points at the bullet by heading text, not by line number.

One deliberate placement call, made by the token budget. The mechanism — "judged against the merged record, no pre-state exemption" — lives only in rules/validation.md, not in both files. SKILL.md had 13 tokens of headroom and (a)+(b) alone cost 9 of them; carrying the mechanism there too measured at 10011 tokens against a ceiling of 10009, i.e. red. So SKILL.md carries the choice and its consequence and rules/validation.md (610 tokens of headroom after this PR) carries the why. No ceiling was touched.

Engine evidence — read on this branch, not taken from the card

All three assumptions the dispatch flagged for verification hold as stated.

Claim in the prose Where it is enforced What the code does
script / cross_field judge the merged record on every write, no pre-state exemption packages/objectql/src/validation/rule-validator.tsevaluateRule case 'script': case 'cross_field': return checkPredicate(rule as PredicateRule, ctx.merged, ctx.previous, ...) — the record handed to the predicate is ctx.merged, built as { ...(previous ?? {}), ...data }. Nothing branches on the pre-state, so a legacy violator is refused on every write until the merged state stops violating.
requiredWhen lets legacy rows rest rule-validator.tsevaluateValidationRules, the ADR-0113 non-regression branch const preViolated = pre.ok && pre.value === true && isMissing(previous[name]); if (preViolated) continue; // legacy rows rest — refused iff the MERGED state violates AND the PRE state complied.
min / max / minLength / maxLength judge the written value only packages/objectql/src/validation/record-validator.tsvalidateRecord On update it iterates Object.entries(data) — the payload — with the comment "Update — validate only supplied fields; an OMITTED field never 400s"; the bound comparisons themselves (string length against maxLength and minLength, number against min and max) run on the written value.

One correctness note that shaped the wording: format and json_schema rules are handed ctx.data, not ctx.merged. So "everything in validations[] is judged on the merged record" would have been false, and the prose names script (SKILL.md) and script / cross_field (validation.md) rather than validations[] as a whole.

No duplicate site. git grep -n -i 'invariant\|transition gate\|requiredWhen' -- skills/objectstack-data/ on this branch returns the bullet and three unrelated requiredWhen mentions (the quick-start template at :260, the conditional-requiredness bullet at :282, and the destructive-migration note at :383). Nothing else in the package states (a) or (b), so nothing is duplicated by this PR.

Token figures — node scripts/check-skills-token-ratchet.mjs

File Before After Ceiling Headroom after
skills/objectstack-data/SKILL.md 9996 10005 10009 4
skills/objectstack-data/rules/validation.md 2362 2499 3109 610

SKILL.md net is +9 tokens against the +13 the budget allowed (the bullet grew 439 → 476 bytes; the convention is ceil(utf8 bytes / 4)). Verdict line, after:

✓ check-skills-token-ratchet: 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.

Gates — the derived family, run on this PR's head 4036ac3c

The family was re-derived from the diff that actually exists: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 4036ac3c returns 21 commands, identical to the dispatch's list — no additions to run. Every exit code was captured before any pipe.

Gate Exit Verdict line it printed
check-ci-filter-parity 0 OK: all 133 declared cross-package glob(s) (93 unique) are covered by 'core' or 'crosspkg' ...
check-closing-keyword-parity 0 check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators ...)
check-comment-mask-corpus 0 ✓ comment-mask corpus sweep: 5826 files, 0 disagree, 0 unparseable
check-cross-package-test-inputs (node) 0 OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check-shard-attestation 0 ✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
check-skills-token-ratchet 0 ✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
check-test-completeness 3 NOT MEASURED, in the gate's own words: "Nothing was measured: this gate exited before parsing a single summary line". It needs a saved turbo run test log; exit 3 is explicitly not a finding.
check:doc-formula-expressions (@objectstack/lint) 0 ✓ 9 @example(s) judged clean across 1137 packages/spec/src files + ✓ 14 predicate(s) on a statically determinable field layer judged clean
check:skill-docs (@objectstack/spec) 0 ✅ Skill docs in sync
check:agent-test-spelling 0 ✓ 0 violations — 432 file(s) · 5850 bare separator token(s) · 1390 launcher-rooted run(s)
check:corpus-claim-drift 0 check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
check:cross-package-test-inputs 0 OK: 25 package(s) read outside themselves, all declared
check:doc-authoring 0 ✓ doc authoring guard: 14589 customer-facing string(s) across 723 spec sources clean — no internal issue-id references
check:nul-bytes 0 ✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)
check:pm-governed-merges 0 ✓ check-governed-merges --self-test: 243 assertions
check:refd-timer-probe 0 OK check-refd-timer-probe: 5821 source file(s) swept
check:role-word 0 check-role-word: OK, no new occurrences of the reserved word.
check:skill-compatibility 0 ✓ 11 SKILL.md file(s) reconciled against 79 workspace packages
check:skill-frame-sync 0 ✓ 2 copies of the decision frame are structurally isomorphic across 2 files
check:skill-identifier-liveness 0 check-skill-identifier-liveness OK — Leg 1: 465 citation(s) over 46 published file(s) ... Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
check:watch-hint-literal 0 ✓ 48 declaration(s) across 4 rostered name(s)
scripts/pm/check-skill-id-lint.mjs 0 ✓ check-skill-id-lint: 23 file(s) clean (pattern for a hash followed by 3+ digits).

Also swept the two edited files for raw control bytes (grep -naP over the C0 range plus DEL): zero hits.

Changeset — skip-changeset, and the rule that says so

AGENTS.md's Post-Task Checklist item 3: a changeset is owed by "anything that publishes", and skip-changeset "is for a diff that publishes nothing from any released package". Measured, not recalled:

  • skills/ is not a workspace package — pnpm-workspace.yaml enumerates packages/*, packages/apps/*, packages/drivers/*, packages/plugins/*, packages/qa/*, packages/triggers/*, packages/services/*, packages/adapters/*, packages/connectors/*, apps/*, examples/*, and skills/ matches none of them.
  • No published package ships it either: no package.json under packages/ mentions skills, and the root manifest is "private": true.
  • Prior art agrees: the four most recent skills-only landings before this one carry no .changeset/*.md entry at all.

So the diff publishes nothing, the label applies, and no changeset is added.

Governed surface — this PR waits for a human

node scripts/pm/check-governed-merges.mjs --test skills/objectstack-data/SKILL.md skills/objectstack-data/rules/validation.md exits 3 with:

⛔  GOVERNED — a human merge is the review record for this PR (#9495 regime).
    skills/** ×2 — the published skills catalog

Draft on purpose. Not marked ready, no auto-merge, no reviewers requested — the dispatching seat reviews in-seat and requests the approvers.

Why this shape, on the four axes

  • Real business need. Measured, not speculative: the downstream pull is three production rules written as invariants and implemented with the gate tool, all silently unenforced. The skill is the only place an app author reads before making that choice, and it was telling them the wrong consequence.
  • Long-term soundness. The prose now matches the engine's measured behaviour rather than a plausible-sounding summary of it, and the mechanism is stated once, in the page that owns the mechanism, instead of being copied into two files that then drift.
  • Making it structurally harder for an AI to author bad metadata. This is the axis that decided the wording. "Bricks existing data" is a deterrent — it steers an author away from the correct tool out of fear of an outcome the engine does not produce. "Frozen until repaired" plus "'required when X' reads like an invariant and is not one" is a discriminator: it names the trap in the exact words an author is about to type. Contract-first would be a publish-time refusal, which is out of scope here and belongs to the spec seat that already shipped the contract text.
  • Startup-stage focus — no scope spread. Two files, one bullet and one two-sentence pointer, no new section, no new heading, no example block, and no third file. rules/field-types.md was left untouched (6 tokens of headroom), and the neighbouring references/_index.md is generator-owned.

🤖 Generated with Claude Code

https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox

Generated by Claude Code


Generated by Claude Code

…quired when X" trap

The data skill's "Choose by intent" bullet said a transition gate written as an
invariant "bricks existing data". Measured against the engine, that is wrong in
the direction that matters: a `validations[]` `script` / `cross_field` rule is
judged on the merged record on every write with no pre-state exemption, so a row
that already violates it is refused on any edit until a repairing write lands.
Frozen, not bricked. The bullet also never named the trap the tool choice exists
for: "required when X" reads like an invariant and is not one.

Both deltas are traded inside the bullet's own length, and the Script Validation
section of rules/validation.md gains the tool-choice pointer it carried nowhere:
script and cross_field are the invariant tools, requiredWhen and field bounds
judge the write, not the stored row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 17:49
@os-zhuang
os-zhuang enabled auto-merge September 3, 2026 17:49
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit c454640 Sep 3, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14893-data-skill-tool-choice branch September 3, 2026 18:22
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 size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish the transition-gate vs invariant tool-choice line in the data-modeling skill (deliverable 2 of #13879)

3 participants