Repository navigation
fix(lint): skill tool references stay a whole-stack verdict; the runtime door never judges them - #19898
Conversation
…e runtime door's The skill hold-out at the runtime publish gate is restated from a measurement awaiting a wider snapshot to a design boundary: cross-item reference resolution belongs to the whole-stack rule, so ai-skill-tool-unresolved is reported by os validate / os lint / os build and never by the runtime door. The door snapshot carries neither stack.tools nor stack.actions, and no snapshot can carry a plugin-registered tool, so the member has no truthful unresolved verdict there. RuntimeStackContext and CONTEXT_STACK_KEYS are unchanged. skill stays outside the door (no runtimeTypes declaration, no stack-key row): the wiring guard's invariant is over runtime-gated types, which are derived from rule declarations, so a type with no rule to run owes no row. Pins: the door gives no tool-reference verdict for a skill naming a stack-level action, a plugin tool or a missing tool (with an in-process firing control on the door's snapshot shape); no write can carry a skill into a door snapshot; the whole-stack runner still reports the missing tool on validate, build and lint; the guard invariant holds. The LIT reason pin stays. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
Comment-only source change that ships: the reworded blocks are emitted into @objectstack/lint dist, so the package takes a patch changeset rather than skip-changeset. No behaviour change. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1259b0454b4d735940d3fa7d3e4afadf36dcaa8f && git checkout 1259b0454b4d735940d3fa7d3e4afadf36dcaa8f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dabf8d795ee9279c18b4b00bfabb322544106b2f fd206c3164f033f08ba9b06caa7afc6864b5b3f7 && git checkout -B drift-repro dabf8d795ee9279c18b4b00bfabb322544106b2f && git merge --no-ff fd206c3164f033f08ba9b06caa7afc6864b5b3f7
node scripts/docs-audit/affected-docs.mjs --json dabf8d795ee9279c18b4b00bfabb322544106b2f
|
Contract reviewServed-tier: 54/54 Isolated at-tier reviewer subagent, run by the Refs read: ① Derived judgmentsAccept-set / behaviour: none moves — verified, not trusted. For each of the three source files the non-comment changed-line filter is empty: Regression in loudness: none, because no door judged Ruling item 1 (restate the hold-out and its two pins as by-design, citing the ruling; LIT stays): done, right. Ruling item 2 (report which shape the guard allows, land that one): reported OUTSIDE, and the reasoning holds. The guard is over Ruling item 3 (no snapshot widening): honoured. New pins: weight-bearing on the ruling's two halves. (a) Governing text truthfully restated. ADR-0109 Decision §3 at Left advising falsely: nothing found. Every downstream reader of the rule id describes an Required and missing: nothing. Beyond the ruling: nothing functional (see ③ for the file-surface note). ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19527
Clause-②: no
This carries out ruling
5791822697on #19527 (batch 215 item 4, letter B, maintainer 「215 同意」): 「cross-item reference resolution belongs to the whole-stack rule」.ai-skill-tool-unresolvedis reported byos validate/os lint/os build. The runtime publish door never emits it.RuntimeStackContextandCONTEXT_STACK_KEYSare unchanged (ruling item 3), and no rule was added (item 2).What was measured first, on
origin/main@1f89ba0d70(before any edit)The measurement ran from a scratch
tsxscript againstpackages/lint/src. I deleted the script afterwards and did not commit it.runtimeGatedTypes()action app book dashboard dataset datasource email_template flow hook mapping object page permission position report seed view.skillis not one of themruntimeAuthoringRulesFor('skill')/stackKeyForType('skill')[]/nullruntimeGatedTypes().filter(t => stackKeyForType(t) === null)[]skillwrite naming a stack-level action / a plugin tool / a tool that exists nowhereerrors: [] · advisories: [] · rulesRun: []for all threevalidateAiToolReferenceson the door's snapshot shape (objects permissions books datasets, withactionsdropped by the builder)ai-skill-tool-unresolvedfor all threestack.actions)[]· plugin tool:ai-skill-tool-unresolved· missing tool:ai-skill-tool-unresolvedflowwrite, which dispatches the suite with this member on its frozenflowdefaultai-skill-tool-unresolved: no door snapshot carriesskills(WRITTEN_STACK_KEYShas noskills)maintoday.skillhas been outside the door since #19542, and the card says so itself ("Nothing is broken today"). The advisory appears only if the type is crossed. The reproduction is the rule run on the door's snapshot shape (the LIT case), and ablation 1 below produces it through the real door. So this PR changes no runtime behaviour. It turns a hold-out that was waiting on a wider snapshot into a design boundary, and it pins both halves of the ruling. I re-ran the measurement script after the edits and its output was byte-identical to the base run.Which of the two shapes the wiring guard allows:
skillstays OUTSIDE the doorThe guard (
authoring-rule-wiring.test.ts, 「every runtime-gated metadata type maps to a stack key」) checksruntimeGatedTypes(). That set is built from ruleruntimeTypesdeclarations, not listed by hand.runRuntimeAuthoringRulesreturnsemptyatrules.length === 0, before it looks atTYPE_TO_STACK_KEY. So a type with an empty runtime rule set is not a gated type, and "crossed with an empty rule set" can only be written in two ways:skillon some entry (for example the reference-integrity suite) with no member claiming it. ThenrulesRunnames a rule that judges nothing, which is the ruling's ⛔ on inventing a rule to give the door something to run.skill: 'skills'row with no rule declaring it. The gate never reads such a row, which is the mapping-ahead-of-its-rules shaperuntime-gate.tsrefuses (theseed: 'data'note).So both absences stay, and the guard holds with
skilloutside it.The change
Comments are restated from 「held out on a measurement」 to 「by design: cross-item reference resolution belongs to the whole-stack rule」, citing the ruling. Each place also drops the old "cross it once
actions/toolsare carried" direction, which the ruling rejected:packages/lint/src/authoring-rules.ts, theskillblock on thevalidateReferenceIntegrityentry.packages/lint/src/reference-integrity-suite.ts, thevalidateAiToolReferencesmember. This member held the measurement and still prescribed widening the snapshot. It now cites ADR-0109 Decision §3 as the governing text.packages/lint/src/runtime-gate.ts, theskillnote onTYPE_TO_STACK_KEY.packages/lint/src/runtime-gate.inert-type-writes.test.ts: the file header, theGROUP_Adocblock, theskillblock and the report/skill fence comment. The two DARK pins keep their assertions, and their reason text now points at the ruling.packages/lint/src/runtime-gate.datasource-writes.test.ts: two comments that callskill"held out on the measurement". This is the second place that pins both halves (runtimeAuthoringRulesFor('skill')is[]andstackKeyForType('skill')is null).The LIT case 「one skill, one rule, two universes」 stays as the reason pin. Its snapshot-bound assertion now says what a red means under the ruling: someone widened
RuntimeStackContextagainst item 3. It no longer saysskillmay now cross.New cases in the
skillblock ofruntime-gate.inert-type-writes.test.ts:it.eachover three tools (a stack-level AI-exposed action, a plugin-registered tool, a tool that exists nowhere). Askillwrite through the real door returnserrors [],advisories []andrulesRun []. The in-process firing control: the member the door does not dispatch does returnai-skill-tool-unresolvedon the door's snapshot shape for each of the three.skills, andskillsis not a context key.runAuthoringRulesfor each ofvalidate,buildandlintover a whole stack reports exactly[{ path: 'skills[1].tools[0]', severity: 'warning' }]: the tool that exists nowhere is reported, and the stack-level action is not.skillwrite'srulesRunis[].Firing controls: ablations on the committed tree (
eb4f0cae09)Each mutation went through
node scripts/ablation-replace.mjsin wrap mode. The tool checked the anchor count and blob hash on disk after each write, and each restore was confirmed asblob == HEADwith an emptygit diff HEAD. No build step was involved: every subject is a relative import insidepackages/lint, so vitest readssrc.skillfully: suite entryruntimeTypes+ memberruntimeTypes: ['flow','skill']+skill: 'skills'rowadvisories: [{ rule: 'ai-skill-tool-unresolved', path: 'skills[0].tools[0]' }]foraction_showcase_portfolio_snapshotskillon the suite entry onlyvalidateAiToolReferencesmemberos validate must still report the tool that exists nowhere: expected [] …-t, only (b) selected)addActionFamily(stack.actions)fromcollectToolUniverse-t, only (b) and LIT selected)The first attempt at ablation 1 did nothing: its third anchor was a substring of its own replacement,
ablation-replacerefused with "anchor count moved 1 -> 1", and no test ran. All three files were confirmed restored. It was re-run with a non-overlapping multi-line anchor, and those are the readings above.Downstream readers of
ai-skill-tool-unresolvedI ran
git grepoverpackages/,skills/,content/,examples/andapps/. None of these expects the runtime door to emit the rule id, so none needed updating:content/docs/ai/agents.mdx:166,content/docs/ai/tools.mdx:92,packages/cli/src/commands/generate.ts:394,packages/spec/src/ai/skill.zod.ts:373,skills/objectstack-ai/SKILL.md:85,packages/mcp/src/skill-prompts.ts:17,packages/lint/src/reference-integrity-suite.test.ts:362andpackages/lint/src/validate-ai-tool-references.test.ts. Each one describes a whole-stack (os validate) verdict, which is the ruled state. The only door-facing readers were the lint tests edited above.@objectstack/metadata-protocol(runtime-authoring-gate.ts, the only source file that imports@objectstack/lint/runtime) has noskill-specific door logic.Changeset:
@objectstack/lintpatchThe dispatch assumed this changes runtime behaviour. The measurement shows it does not (see above). The repo rule still asks for a changeset, because the published bytes change.
@objectstack/lintshipsdist, and its build keeps source comments: after the rebuild, the ruling id is indist/index.js,dist/index.cjs,dist/runtime.jsanddist/runtime.cjs(3 hits each, one per restated block), and 「held out on a measurement」 is in none of them. That phrase was inauthoring-rules.tson the base, so it shipped there. Under AGENTS.md,skip-changesetis only for a diff that publishes nothing from a released package, so this takes apatch. There is precedent for comment-only changes that ship todist(for example.changeset/15295-serve-observability-mirror-comment.md).Clause-② reading: no, with no arm. No authorable key or accept set moves in either direction. The door judged no
skillwrite before this PR and judges none after it, and the whole-stack verdict is unchanged. It is neither widening nor narrowing.Local verification, at HEAD
fd206c3164pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 108 files / 4127 tests passed.pnpm --filter @objectstack/lint typecheck(tsc --noEmitandcheck:test-typecheck): exit 0.check:test-typecheck: OK.tsc -p tsconfig.test.json --listFilesOnlyincludes both edited test files.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2(the package that wires the runtime gate): 188 files passed, 3 skipped · 2673 tests passed, 19 skipped.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, derived again after the changeset commit: 59 commands, all run.--ranreports: 「59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED」. The first pass got exit 3 (PREREQUISITE NOT MET) fromcheck:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt. After a fullturbo run build --filter='./packages/*' --filter='./packages/*/*'(72/72 tasks), all three measured green at the same HEAD.pnpm testandpnpm lintare left to CI (Test Coreshards,Lint & Repo Gates).origin/mainhas moved 4 commits past the base, and none of them touchespackages/lint,packages/metadata-protocolorpackages/spec/src/ai.Acceptance notes
validateAiToolReferenceskeeps its frozenflowdefault, so it still runs on everyflowwrite. It judges nothing there, because a flow snapshot has noskills. I considered an explicit empty memberruntimeTypesto make "never" structural, but did not make that change, since it would change which members run on flow writes and the ruling did not ask for it. The new (a) case pins that "never" holds for every write type.Generated by Claude Code