docs(skills): the invariant tool freezes a violating row, it does not brick it — and name the "required when X" trap - #15010
Merged
Conversation
…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
This was referenced Sep 3, 2026
os-zhuang
approved these changes
Sep 3, 2026
os-zhuang
marked this pull request as ready for review
September 3, 2026 17:49
os-zhuang
enabled auto-merge
September 3, 2026 17:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/main77a532d6— 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:
After:
Two deltas, both traded for words inside the bullet rather than added to it:
2.
rules/validation.md, the Script Validation section (two-sentence pointer added)The section taught the
scriptmechanics with no tool-choice statement at all. It now opens with: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.mdhad 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. SoSKILL.mdcarries the choice and its consequence andrules/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.
script/cross_fieldjudge the merged record on every write, no pre-state exemptionpackages/objectql/src/validation/rule-validator.ts→evaluateRulecase 'script': case 'cross_field': return checkPredicate(rule as PredicateRule, ctx.merged, ctx.previous, ...)— the record handed to the predicate isctx.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.requiredWhenlets legacy rows restrule-validator.ts→evaluateValidationRules, the ADR-0113 non-regression branchconst 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/maxLengthjudge the written value onlypackages/objectql/src/validation/record-validator.ts→validateRecordObject.entries(data)— the payload — with the comment "Update — validate only supplied fields; an OMITTED field never 400s"; the bound comparisons themselves (string length againstmaxLengthandminLength, number againstminandmax) run on the written value.One correctness note that shaped the wording:
formatandjson_schemarules are handedctx.data, notctx.merged. So "everything invalidations[]is judged on the merged record" would have been false, and the prose namesscript(SKILL.md) andscript/cross_field(validation.md) rather thanvalidations[]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 unrelatedrequiredWhenmentions (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.mjsskills/objectstack-data/SKILL.mdskills/objectstack-data/rules/validation.mdSKILL.mdnet is +9 tokens against the +13 the budget allowed (the bullet grew 439 → 476 bytes; the convention isceil(utf8 bytes / 4)). Verdict line, after:Gates — the derived family, run on this PR's head
4036ac3cThe family was re-derived from the diff that actually exists:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat4036ac3creturns 21 commands, identical to the dispatch's list — no additions to run. Every exit code was captured before any pipe.check-ci-filter-parityOK: all 133 declared cross-package glob(s) (93 unique) are covered by 'core' or 'crosspkg' ...check-closing-keyword-paritycheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators ...)check-comment-mask-corpus✓ comment-mask corpus sweep: 5826 files, 0 disagree, 0 unparseablecheck-cross-package-test-inputs(node)OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-shard-attestation✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check-skills-token-ratchet✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.check-test-completenessturbo run testlog; exit 3 is explicitly not a finding.check:doc-formula-expressions(@objectstack/lint)✓ 9 @example(s) judged clean across 1137 packages/spec/src files+✓ 14 predicate(s) on a statically determinable field layer judged cleancheck:skill-docs(@objectstack/spec)✅ Skill docs in synccheck:agent-test-spelling✓ 0 violations — 432 file(s) · 5850 bare separator token(s) · 1390 launcher-rooted run(s)check:corpus-claim-driftcheck-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.check:cross-package-test-inputsOK: 25 package(s) read outside themselves, all declaredcheck:doc-authoring✓ doc authoring guard: 14589 customer-facing string(s) across 723 spec sources clean — no internal issue-id referencescheck:nul-bytes✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)check:pm-governed-merges✓ check-governed-merges --self-test: 243 assertionscheck:refd-timer-probeOK check-refd-timer-probe: 5821 source file(s) sweptcheck:role-wordcheck-role-word: OK, no new occurrences of the reserved word.check:skill-compatibility✓ 11 SKILL.md file(s) reconciled against 79 workspace packagescheck:skill-frame-sync✓ 2 copies of the decision frame are structurally isomorphic across 2 filescheck:skill-identifier-livenesscheck-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✓ 48 declaration(s) across 4 rostered name(s)scripts/pm/check-skill-id-lint.mjs✓ 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 -naPover the C0 range plus DEL): zero hits.Changeset —
skip-changeset, and the rule that says soAGENTS.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.yamlenumeratespackages/*,packages/apps/*,packages/drivers/*,packages/plugins/*,packages/qa/*,packages/triggers/*,packages/services/*,packages/adapters/*,packages/connectors/*,apps/*,examples/*, andskills/matches none of them.package.jsonunderpackages/mentionsskills, and the root manifest is"private": true..changeset/*.mdentry 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.mdexits 3 with: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
rules/field-types.mdwas left untouched (6 tokens of headroom), and the neighbouringreferences/_index.mdis generator-owned.🤖 Generated with Claude Code
https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Generated by Claude Code
Generated by Claude Code