You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fd920ca
Browse filesBrowse the repository at this point in the historyBrowse files
fix(lint): skill tool references stay a whole-stack verdict; the runtime door never judges them (#19898)
Fixes#19527
Clause-②: no
This carries out ruling `5791822697` on #19527 (batch 215 item 4, letter
B, maintainer 「215 同意」): 「cross-item reference resolution belongs to the
whole-stack rule」. `ai-skill-tool-unresolved` is reported by `os
validate` / `os lint` / `os build`. The runtime publish door never emits
it. `RuntimeStackContext` and `CONTEXT_STACK_KEYS` are 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 `tsx` script against
`packages/lint/src`. I deleted the script afterwards and did not commit
it.
| probe | reading |
|---|---|
| `runtimeGatedTypes()` | 17 types: `action app book dashboard dataset
datasource email_template flow hook mapping object page permission
position report seed view`. **`skill` is not one of them** |
| `runtimeAuthoringRulesFor('skill')` / `stackKeyForType('skill')` |
`[]` / `null` |
| the wiring guard's unmapped set, `runtimeGatedTypes().filter(t =>
stackKeyForType(t) === null)` | `[]` |
| the real door, a `skill` write naming a stack-level action / a plugin
tool / a tool that exists nowhere | `errors: [] · advisories: [] ·
rulesRun: []` for all three |
| the card's reproduction: `validateAiToolReferences` on the door's
snapshot shape (`objects permissions books datasets`, with `actions`
dropped by the builder) | `ai-skill-tool-unresolved` for all three |
| the same member over the whole stack (with `stack.actions`) |
stack-level action: `[]` · plugin tool: `ai-skill-tool-unresolved` ·
missing tool: `ai-skill-tool-unresolved` |
| a `flow` write, which dispatches the suite with this member on its
frozen `flow` default | 0 `ai-skill-tool-unresolved`: no door snapshot
carries `skills` (`WRITTEN_STACK_KEYS` has no `skills`) |
⚠️ **The door does not emit the false advisory on `main` today.**
`skill` has 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: `skill` stays
OUTSIDE the door
The guard (`authoring-rule-wiring.test.ts`, 「every runtime-gated
metadata type maps to a stack key」) checks `runtimeGatedTypes()`. That
set is built from rule `runtimeTypes` declarations, not listed by hand.
`runRuntimeAuthoringRules` returns `empty` at `rules.length === 0`,
before it looks at `TYPE_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:
- declare `skill` on some entry (for example the reference-integrity
suite) with no member claiming it. Then `rulesRun` names a rule that
judges nothing, which is the ruling's ⛔ on inventing a rule to give the
door something to run.
- add a `skill: 'skills'` row with no rule declaring it. The gate never
reads such a row, which is the mapping-ahead-of-its-rules shape
`runtime-gate.ts` refuses (the `seed: 'data'` note).
So both absences stay, and the guard holds with `skill` outside 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` /
`tools` are carried" direction, which the ruling rejected:
- `packages/lint/src/authoring-rules.ts`, the `skill` block on the
`validateReferenceIntegrity` entry.
- `packages/lint/src/reference-integrity-suite.ts`, the
`validateAiToolReferences` member. 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`, the `skill` note on
`TYPE_TO_STACK_KEY`.
- `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: the file
header, the `GROUP_A` docblock, the `skill` block 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 call `skill` "held out on the measurement". This is the
second place that pins both halves (`runtimeAuthoringRulesFor('skill')`
is `[]` and `stackKeyForType('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 `RuntimeStackContext` against item 3. It no
longer says `skill` may now cross.
New cases in the `skill` block of
`runtime-gate.inert-type-writes.test.ts`:
- **(a)** `it.each` over three tools (a stack-level AI-exposed action, a
plugin-registered tool, a tool that exists nowhere). A `skill` write
through the real door returns `errors []`, `advisories []` and `rulesRun
[]`. The in-process firing control: the member the door does not
dispatch **does** return `ai-skill-tool-unresolved` on the door's
snapshot shape for each of the three.
- **(a)** No write type can carry a skill into a door snapshot. No gated
type maps to `skills`, and `skills` is not a context key.
- **(b)** `runAuthoringRules` for each of `validate`, `build` and `lint`
over 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.
- **(c)** The guard invariant holds over the real tables, and a `skill`
write's `rulesRun` is `[]`.
## Firing controls: ablations on the committed tree (`eb4f0cae09`)
Each mutation went through `node scripts/ablation-replace.mjs` in wrap
mode. The tool checked the anchor count and blob hash on disk after each
write, and each restore was confirmed as `blob == HEAD` with an empty
`git diff HEAD`. No build step was involved: every subject is a relative
import inside `packages/lint`, so vitest reads `src`.
| # | mutation | red | green |
|---|---|---|---|
| 1 | cross `skill` fully: suite entry `runtimeTypes` + member
`runtimeTypes: ['flow','skill']` + `skill: 'skills'` row | 9: all three
(a) cases, the (a) no-other-write case, (c), both DARK pins, the
report/skill fence, and the datasource DARK pin. The (a) failure is the
exact false advisory through the real door: `advisories: [{ rule:
'ai-skill-tool-unresolved', path: 'skills[0].tools[0]' }]` for
`action_showcase_portfolio_snapshot` | 68, including the wiring guard (a
full crossing is consistent) |
| 2 | declare `skill` on the suite entry only | 3: the repo's own 「every
runtime-gated metadata type maps to a stack key」, (c), DARK | 64 |
| 3 | delete the `validateAiToolReferences` member | (b): `os validate
must still report the tool that exists nowhere: expected [] …` | none
(run with `-t`, only (b) selected) |
| 4 | drop `addActionFamily(stack.actions)` from `collectToolUniverse` |
(b) gets 2 findings instead of 1, and the LIT lit-control leg fails |
none (run with `-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-replace` refused 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-unresolved`
I ran `git grep` over `packages/`, `skills/`, `content/`, `examples/`
and `apps/`. 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:362` and
`packages/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 no
`skill`-specific door logic.
## Changeset: `@objectstack/lint` patch
The 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/lint` ships `dist`,
and its build keeps source comments: after the rebuild, the ruling id is
in `dist/index.js`, `dist/index.cjs`, `dist/runtime.js` and
`dist/runtime.cjs` (3 hits each, one per restated block), and 「held out
on a measurement」 is in none of them. That phrase was in
`authoring-rules.ts` on the base, so it shipped there. Under AGENTS.md,
`skip-changeset` is only for a diff that publishes nothing from a
released package, so this takes a `patch`. There is precedent for
comment-only changes that ship to `dist` (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 `skill` write 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 `fd206c3164`
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`:
**108 files / 4127 tests passed**.
- `pnpm --filter @objectstack/lint typecheck` (`tsc --noEmit` and
`check:test-typecheck`): exit 0. `check:test-typecheck: OK`. `tsc -p
tsconfig.test.json --listFilesOnly` includes 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. `--ran` reports: 「59 derived famil(ies) accounted for — 59 run,
0 NOT-MEASURED」. The first pass got exit 3 (PREREQUISITE NOT MET) from
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt`. After a full `turbo run build
--filter='./packages/*' --filter='./packages/*/*'` (72/72 tasks), all
three measured green at the same HEAD.
- **Scoping, declared:** the repo-wide `pnpm test` and `pnpm lint` are
left to CI (`Test Core` shards, `Lint & Repo Gates`). `origin/main` has
moved 4 commits past the base, and none of them touches `packages/lint`,
`packages/metadata-protocol` or `packages/spec/src/ai`.
## Acceptance notes
- `validateAiToolReferences` keeps its frozen `flow` default, so it
still runs on every `flow` write. It judges nothing there, because a
flow snapshot has no `skills`. I considered an explicit empty member
`runtimeTypes` to 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.
- The pair card 19477 (written without the sigil on purpose) still
answers 404, as the claim comment recorded.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`ai-skill-tool-unresolved` is a whole-stack verdict by design: `os validate` / `os lint` / `os build` report it, and the runtime publish gate never does (#19527)
6
+
7
+
A skill's `tools[]` entries resolve against three sources: `stack.tools`, the
8
+
platform tool registry, and the `action_<name>` tools materialised from
9
+
AI-exposed actions. The runtime publish gate judges one written item against a
10
+
per-write snapshot that carries neither `stack.tools` nor `stack.actions`, and
11
+
no snapshot can carry a tool that a runtime plugin registers outside the
12
+
registry. At that door the rule could only ever produce a false
13
+
`ai-skill-tool-unresolved`: a skill naming a real stack-level action would be
14
+
told the tool does not exist. The ruling on #19527 (letter B) places this
15
+
check at the whole-stack rule, following ADR-0109 Decision §3, and keeps the
16
+
per-write snapshot as it is.
17
+
18
+
`skill` was already outside the runtime gate. The earlier comments called that
19
+
a temporary hold-out, pending a wider snapshot. They now describe it as the
20
+
design, so no later change should add `actions` / `tools` to the snapshot in
21
+
order to move this check onto the door. New tests pin both halves:
22
+
23
+
- a `skill` write through the runtime gate gets no tool-reference finding. This
24
+
holds for a stack-level action tool, a plugin-registered tool, and a tool
25
+
that exists nowhere. No other write type can put a skill into a door
26
+
snapshot;
27
+
-`validate`, `build` and `lint` still report a tool that exists nowhere, at
28
+
warning severity, and do not report the stack-level action that resolves.
29
+
30
+
⛔ No behaviour changes. Before and after this change, the runtime gate runs no
31
+
rule for a `skill` write, and the whole-stack commands report the same
32
+
findings. No authorable key, accept set or export changes.
33
+
34
+
**This ships, so it carries a changeset rather than `skip-changeset`.**
35
+
`@objectstack/lint` publishes `dist`, and its build keeps source comments. A
36
+
rebuilt artifact shows the reworded block in `dist/index.js`,
37
+
`dist/index.cjs`, `dist/runtime.js` and `dist/runtime.cjs`, and the old
38
+
wording (「held out on a measurement」) in none of them. So the published JS
39
+
bytes change, but behaviour and the declaration surface do not.
0 commit comments