fix(spec): list the five service-ai-studio tools the AI tool registry was omitting - #16844
Conversation
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 131 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 dab174f4e2d2d755302afe0ab111f7aa359eafe9 && git checkout dab174f4e2d2d755302afe0ab111f7aa359eafe9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5ea982d971cb2550d35a845694a6cbc2575ea52 f7d76284421933afad4b0747e1f43d3c01754350 && git checkout -B drift-repro c5ea982d971cb2550d35a845694a6cbc2575ea52 && git merge --no-ff f7d76284421933afad4b0747e1f43d3c01754350
node scripts/docs-audit/affected-docs.mjs --json c5ea982d971cb2550d35a845694a6cbc2575ea52 |
…omitting `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']` omitted five tools the cloud AI runtime actually registers — `get_authoring_rules`, `load_tools`, `open_record`, `test_flow` and `toggle_flow` — so under ADR-0109, where the default authoring path declares no tool records at all, a skill naming any of them resolved against nothing and was reported by `validate`/`lint` as a FICTIONAL tool reference. That is the exact failure the registry exists to end. Added in the list's existing alphabetical order (verified: both groups were in fact sorted before this change, and the package's own shape test pins it). Nothing else in the registry moves. The pin lives in `packages/lint`, where "refused" and "accepted" are the verdicts: the five now resolve, and a sixth name shaped like them and registered by nobody is still reported — the control, without which the widening pin cannot fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
ce12e11 to
f7d7628
Compare
Contract review — VERDICT: PASS WITH FINDINGS, no file correction required (at
|
| # | Row | Reading | Finding |
|---|---|---|---|
| 1 | The widening | MEASURED | As above; consumer path confirmed in code (resolves() is universe.has(ref)). Secondary consumers move too: @objectstack/mcp now emits openWorldHint: false for the five (its pins are registry-derived, subset-direction, so nothing breaks), and objectui's tool-step labels read the same set. |
| 2 | Nothing ELSE widened | MEASURED | Machine diff of the constant: added exactly the five, removed none, service-ai group byte-identical, group keys unchanged, base order preserved as a subsequence, and the file with list lines stripped is identical base→head — docblock and maintenance-contract wording untouched. |
| 3 | Alphabetical order | MEASURED | Base groups sorted (6/6, 24/24), head sorted (6/6, 29/29), unique under JS default .sort() — which is what the spec-side pin enforces. The todo_write < toggle_flow trap is honoured (tod < tog). |
| 4 | The pin discriminates | READ | The pin's stack carries no tools and no actions, so the universe is exactly the registry ⇒ toEqual([]) cannot pass with the registry unchanged (five absent at base = five findings), and the control demands exactly one finding at skills[0].tools[5] naming archive_flow, which an over-widened universe would violate. No other name in the suite collides. |
| 5 | Changeset minor |
READ | Right by the repo's own rule: an additive widening takes at least minor, and fix( may raise but never lower ⇒ the earlier patch refusal was correct. Every checkable claim in the prose holds. |
| 6 | The cross-repo premise | NOT MEASURED | |
| 7 | The reverse risk | READ | Per name: get_authoring_rules and open_record carry runtime-grade evidence (objectui's AiChatPage.tsx:1518 handles the open_record hand-off; a live zh transcript rendered Get authoring rules); load_tools, test_flow, toggle_flow rest on two independent static observers with no contrary evidence. None looks fabricated. The only way one becomes a legalised fiction is a cloud rename after 03:57Z — row 6. |
| 8 | Scope | MEASURED | Exactly three files, 43 insertions, 0 deletions, one commit. The documentation label is the labeler matching the changeset .md, not a docs file. |
| 9 | Releases / governed paths | MEASURED | 0 of 3 diff paths match the register's own five globs; positive control: every one of those paths exists in the head tree (10 / 139 / 42 / 47 / 1 / 1 entries), so the zero is the diff's. |
| 10 | The SKILL.md staleness left unfixed | READ | Leaving it was right — skills/** is governed and one path hit makes the whole diff human-merge-only, which would have displaced this very review. Classified a documentation nit, not defect class (a)/(b)/(c): the passage subordinates its numbers to the file and nothing in the tree reads them. validate-ai-tool-references.ts:148-171 citation points past collectToolUniverse, now 144-164), and after landing it reads 30/24 against 35/29. |
| 11 | CI | NOT MEASURED | PM-held gate — read once for metadata only, not polled, no timer, no watcher. |
⛔ The one condition that gates merging, and it cannot be met from this repository
Triage acceptance term #1 required the list to be re-read against cloud's plugin.ts at the moment of landing. objectstack-ai/cloud is out of this session's repository scope, so neither the dev nor the reviewer could meet it, and ⛔ 「仓不可达 ⛔ 不当查过了干净」.
⇒ Filed as #16852, with a named reader (a session scoped to objectstack-ai/cloud) and the two questions reduced to their smallest form: are the five still registered under exactly these spellings, and has a sixth static tool appeared since 2026-09-08T03:57Z.
What is already established is on that card: triage's direct cloud reading at 03:57Z, plus objectui#7481 as an independent second observer naming exactly these five.
Disposition
- ⛔
needs:contract-reviewstays until POST-MERGE (the gate it guarded has passed): re-read cloud's service-ai-studio plugin.ts and confirm the five tool names PR #16844 adds are still the whole static set #16852 answers; the review itself is satisfied. - skills/objectstack-ai/SKILL.md restates the platform-tool registry's counts and line ranges instead of citing it — three numbers go stale the moment PR #16844 lands, and one citation is already stale #16853 filed for the
skills/objectstack-ai/SKILL.mdstaleness — docs-only, its own PR, ⛔ never a rider, sinceskills/**is governed. - Downstream, ⛔ not this PR: once objectui bumps its spec pin,
toolLabels-locale-parity-7481.test.tsfails by design untilAHEAD_OF_PINis emptied. That is the intended tripwire and ⛔ must not be cleared here.
Generated by Claude Code
Contract review (
|
|
| channel | reading at 14:29Z |
|---|---|
| #16852 (the card carrying the owed re-read) | open, state_reason null, 0 comments, updated_at still its creation time 10:50:19Z |
| this PR's thread | no comment since 13:00Z; plugin.ts / service-ai-studio / cloud mentioned 0 times in that window |
⇒ If the reading was taken, it is recorded in neither place.
What is owed, and why the direction matters
Triage acceptance term #1 required the five names re-read against objectstack-ai/cloud's packages/service-ai-studio/src/plugin.ts at the moment of landing — are get_authoring_rules, load_tools, open_record, test_flow, toggle_flow still registered under exactly those spellings, and has a sixth appeared since 03:57Z.
PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio'] 24 → 29 and PLATFORM_PROVIDED_TOOL_NAMES 30 → 35, which stops validate / lint from reporting a skill.tools[] entry naming one of the five as fictional. A name added here that cloud does not register makes a genuinely fictional tool reference legal — the exact failure the registry was built to end, running backwards. A rename or a removal upstream is invisible to this diff.
⛔ What this seat is NOT claiming
⛔ Nothing suggests the five are wrong. Triage measured cloud origin/main directly at 2026-09-08T03:57Z (four definition groups imported at plugin.ts:33/38/40/42, one tool file per name); objectui#7481 names exactly these five as registered-by-cloud-but-unpinned, from an independent read; and two of them carry runtime-grade evidence beyond the static listing. The gap is time, not doubt — the reading is ~10.5 hours old and a rename or a sixth registration since is invisible here.
Why this seat did not simply take the reading
It cannot. objectstack-ai/cloud is outside this session's repository scope, and an add_repo attempt at 12:31Z was denied by the permission classifier. ⛔ Not retried. 「仓不可达 ⛔ 不当查过了干净」 — an unreachable repo is not a clean read, which is why #16852 exists as a card with a named reader rather than as a check this seat skipped.
What closes it — one line, from anyone with cloud scope
Post on #16852: the five spellings still present, and whether a sixth static tool has appeared since 2026-09-08T03:57Z. That discharges triage term #1 and this comment becomes a non-event.
⇒ If the director seat already took that reading before approving, ⭐ please drop it on #16852 so the card can close and the next seat is not left reading an open card as an open question. If it has not been taken, then this PR is landing with its stated acceptance term undischarged, and that should be a deliberate call rather than an oversight — which is the only reason this comment exists.
Generated by Claude Code
Clause-②: yes
Fixes #16512
PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']now lists the five tools the cloud AI runtime registers that it had been omitting —get_authoring_rules,load_tools,open_record,test_flow,toggle_flow— inserted in the list's existing alphabetical order. Nothing else in the registry moves, and the module's maintenance-contract wording is untouched.What this widens
PLATFORM_PROVIDED_TOOL_NAMESis the load-bearing half ofskill.tools[]reference integrity under ADR-0109: the default third-party authoring path declares no tool records at all, so askill.tools[]entry resolves against this registry, or against the materialisedaction_NAMEfamily, or against nothing. While the five were absent, a skill naming any of them was reported byvalidate/lintas a fictional tool reference (ai-skill-tool-unresolved) — the precise failure the registry exists to end. Five previously-refused references become legal here; a name registered by nobody is still refused.Premise re-measured on the current
origin/main, with the positive controlMeasured at
f89812e4din a dedicated worktree, inpackages/spec/src/system/constants/platform-tool-names.ts. A searched zero without a positive control is not a reading, so both legs were run under the identical query:test_flowtoggle_flowget_authoring_rulesload_toolsopen_recordcreate_metadataverify_buildThe zero is the registry's, not the query's. None of the five was already present, so the card's premise still holds.
The alphabetical claim was verified, not assumed. Both groups were in fact sorted before this change (
service-ai6/6,service-ai-studio24/24, machine-compared against their own sort), and the package's own shape test pins it —each group is sorted, so diffs stay reviewable.Not measured here: the cloud side.
objectstack-ai/cloudis outside this session's repository scope, so the "re-check the list against the currentpackages/service-ai-studio/src/plugin.ts" step was NOT MEASURED by me and is not claimed as done. The standing cross-repo reading is triage comment5578936460(2026-09-08T03:57Z), which measured both sides and found all five registered in cloud, each with its own tool file and its definition group imported byplugin.ts(FLOW_TOOL_DEFINITIONS,AUTHORING_RULE_TOOL_DEFINITIONS,LOAD_TOOLS_TOOL_DEFINITIONS,OPEN_RECORD_TOOL_DEFINITIONS).The pin, and the ablation that proves it can fail
The behavioural pin lives in
packages/lint/src/validate-ai-tool-references.test.ts, extending the existing suite rather than adding a parallel one — "refused" and "accepted" are that rule's verdicts, while the constant's own file can only pin the list's shape. It asserts the five now resolve, and carries its control in the same case: a sixth name shaped exactly like them and registered by nobody is still reported atskills[0].tools[5]. Without that control a universe widened until everything resolves would pass.packages/lintresolves@objectstack/spec/systemthroughexportsto spec'sdist/with no vitest alias (it is aKNOWN_UNALIASED_TEST_IMPORTSpair), so each leg rebuilt spec and proved the mutation reached the artifact the suite consumes:dist/proof5 → 0; blobaeaa3a67adiffers from HEAD blob275579d33ablation-dist-preflight … --absentexit 0, marker absent from all 218 built filesexpected [ { severity: 'warning', …(5) }, …(4) ] to deeply equal [], the other 13 cases in the file still greengit checkout HEAD -- PATH(the file, named); blob back to275579d33; whole-treegit status --porcelainemptyablation-dist-preflightexit 0, marker present in 4 built filesDirection as predicted (turns red). Both legs ran from the committed state under a
trap … EXIT INT TERMrestore with absolute paths; nothing of the ablation is left in the tree.Verification
Everything below was re-run at
f7d762844, the head after the review amend described under Amend below; exit codes were captured by redirecting to a file first, never across a pipe. The test and typecheck rows are quoted from their run atce12e1127, whose tree differs from this head by one word inside.changeset/and by nothing a test, a typechecker or a build reads.pnpm --filter @objectstack/spec test— 465 files / 12955 tests passed, exit 0.pnpm --filter @objectstack/lint test— 102 files / 3565 passed, 5 skipped, exit 0 (its dependency closure built first; without it 40 files fail to collect on missing siblingdist/, a pre-existing worktree state and not this diff).pnpm --filter @objectstack/spec typecheckandpnpm --filter @objectstack/lint typecheck— exit 0 each, both test-layer debt ledgers held.pnpm --filter @objectstack/spec check:generated— exit 0, all 15 generated artifacts up to date; nothing to regenerate (the export set is unchanged; only a const's value moved).pnpm lint(repo-wideeslint . --no-inline-config) — exit 0 in 98s. No narrowing, so none is claimed.node scripts/pm/dispatch-gates.mjs --commandsgave 76 commands for this change set; all 76 were run and reconciled with--ran, which reports 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loadsandpnpm check:type-check-debt, both of which need a full monorepo build (pnpm build/ the whole-workspace turbo build) that this worktree does not carry. Neither can be reached by this diff — no export is added, removed or renamed. CI builds fresh and measures them.Amend — two review findings, one push
CI on the first head (
ce12e1127) returned two reds, both about carriers rather than about the diff. Both were fixed in a single amend, so the branch still carries exactly one commit and the three-file diff is unchanged.Part-of PR must not also close its card— RULE 2 ofscripts/check-partof-closing-keyword.mjs: no commit on the pull request may carry a card-relation trailer at all; the body is the only carrier of the relation. The commit message carried the closing trailer for this card. It was removed from the commit message only — it stays in this body, where it belongs, and theCo-Authored-By:andClaude-Session:trailers are untouched. Readings:--self-testexit 0 (92 cases), and the main leg driven with this PR real body and commit list exit 0 — carries no Part-of/closing-keyword contradiction … and its 1 commit message(s) carry no card-relation trailer. Control on the same leg with the pre-amend message: exit 1, so the instrument fires on this exact input.Check Changeset— the level axis: a clause-② declaration may not gradepatcha package whosepackages/*/src/**the PR moves, because a purely additive widening of a published surface takes at leastminor. The changeset movedpatchtominor; the declaration line and theneeds:contract-reviewcarrier were not touched, and the changeset prose is unchanged. Readings:node scripts/check-changeset-no-major.mjs --base f89812e4dexit 0, and re-run with this PR own event payload so the axis is really judged rather than skipped, exit 0 — LEVEL AXIS: this PR declares clause-②yes, and no package whosepackages/*/src/**it moves is gradedpatch; carrier and declaration line both read.The amend was a message-only rewrite for finding 1 (tree hash identical across it,
6cac29427before and after) plus the one-word changeset change for finding 2. Every derived gate family was then re-run on the new head — 76 derived, 76 run, same two prerequisite gaps and nothing else — and the repo-widepnpm lintre-run atf7d762844, exit 0.Downstream — named, not acted on
objectstack-ai/cloud#1995carriesBlocked-bythis card and is the half that stops the drift recurring: it makes the conformance test derive the union from whatplugin.tsregisters instead of restating it. Re-copying the list correctly today only resets the clock, which is accepted — this PR is the data half. Not touched here.objectstack-ai/objectui#7481's tool-step labels read this registry; all five currently fall through to an English title-caser mid-Chinese conversation, and that card holds a hand-written workaround list meant to empty once this lands and the pin moves. Not touched here.pm:blocking; no downstream card was edited, relabelled or moved out of its open state.验收备注
One observation, noted and deliberately not filed — it is a documentation nit, and acting on it is outside this card's ruled scope:
skills/objectstack-ai/SKILL.md:75-79restates this registry rather than deriving it, and this PR makes three of its numbers stale: "the 30 statically-registered platform tools" becomes 35, "24 schema / metadata / package tools fromservice-ai-studio" becomes 29, and the citationplatform-tool-names.ts:38-82becomes38-87. Verified that nothing checks them: no gate reads those numbers,check:skill-docsandcheck:skill-refsare both green on this diff, and the passage itself already tells the reader "Read that file for the exact set", so the count is a summary and not the authority. It was left alone on purpose —skills/**is a governed surface carrying its own line and token ratchets, and editing it would convert this PR's landing path from an at-tier contract review to a governed human merge, which this card's ruling does not authorize. It is the same restate-instead-of-derive shape the cloud half addresses, one repo over. 承接者: thedomain:specseat, at this PR's contract review — it ownspackages/specand its published skill docs and is already reading this diff.No other out-of-scope finding. The
test_flowhits elsewhere inpackages/areservice-automation's flow fixtures, unrelated to tool names.Landing
⛔ Draft on purpose.
needs:contract-reviewis on both the card and this PR. Do not flip out of draft, do not enqueue, do not arm auto-merge — the seat is arranging an at-tier contract review.Authored by Claude Code in session
session_016N6xmWt5hYm94ffVEwGH8x.Generated by Claude Code