fix(runner): kill stale semantic-catalog counts and drift-check the doc

The catalog doc claimed 36 tools / 22 optional while the catalog holds 37 / 23
(the 36-vs-37 drift the reconciliation plan flagged). Correct the prose and add
src/catalog/catalog-docs.test.ts, which recomputes the total/always/optional
counts and the operation-id membership from the catalog so a stale hand count
can no longer pass.

Refs PAP-17025.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-08-11 14:24:35 +00:00
1 parent 63d691aeb6
commit 3d6a45e4d7
2 files changed
+65 -2

No files matched your search

@@ -17,9 +17,12 @@ list, not a disposition — they are never exposed as tools. See
[capability disposition](capability-disposition.md) and
[authorization and exposure](capability-authorization-and-exposure.md).
The catalog holds **36 tools**: 14 always-agent tools and 22 optional tools
The catalog holds **37 tools**: 14 always-agent tools and 23 optional tools
across 10 groups.
<!-- These counts are drift-checked against src/tools/capability-semantic-tool-catalog.ts
by src/catalog/catalog-docs.test.ts; update the catalog, not the numbers. -->
### Always-agent tools (14)
`get_task_context`, `get_task_history`, `list_documents`, `read_document`,
@@ -27,7 +30,7 @@ across 10 groups.
`finish_task`, `block_task`, `request_review`, `write_document`,
`request_human_input`, `register_deliverable`, `inspect_operation_result`.
### Optional tools (22), by group
### Optional tools (23), by group
| Group | Tools |
| --- | --- |
@@ -0,0 +1,60 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import {
CAPABILITY_SEMANTIC_TOOL_CATALOG as SCENARIO_CATALOG,
capabilitySemanticTool,
} from "../tools/capability-semantic-tool-catalog.js";
const docPath = resolve(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"docs",
"capability-semantic-tools.md",
);
const doc = readFileSync(docPath, "utf8");
/** The catalog counts region, between the disposition prose and descriptor shape. */
function countsRegion(): string {
const start = doc.indexOf("## Two dispositions");
const end = doc.indexOf("## Descriptor shape");
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
return doc.slice(start, end);
}
describe("capability-semantic-tools.md stays in sync with the catalog", () => {
const always = SCENARIO_CATALOG.filter((tool) => tool.disposition === "always_agent_tool");
const optional = SCENARIO_CATALOG.filter((tool) => tool.disposition === "optional_agent_tool");
it("states the current total, always, and optional counts", () => {
const region = countsRegion();
const total = /holds \*\*(\d+) tools\*\*/.exec(region)?.[1];
const alwaysProse = /(\d+) always-agent tools and (\d+) optional tools/.exec(region);
const alwaysHeader = /### Always-agent tools \((\d+)\)/.exec(region)?.[1];
const optionalHeader = /### Optional tools \((\d+)\)/.exec(region)?.[1];
expect(Number(total)).toBe(SCENARIO_CATALOG.length);
expect(Number(alwaysProse?.[1])).toBe(always.length);
expect(Number(alwaysProse?.[2])).toBe(optional.length);
expect(Number(alwaysHeader)).toBe(always.length);
expect(Number(optionalHeader)).toBe(optional.length);
// Guard against the historical 36-vs-37 drift the reconciliation plan flagged.
expect(SCENARIO_CATALOG.length).toBe(37);
});
it("lists exactly the catalog's operation ids in the counts region", () => {
const region = countsRegion();
const backticked = new Set(
[...region.matchAll(/`([a-z_]+)`/g)]
.map((match) => match[1])
.filter((id) => capabilitySemanticTool(id) !== undefined),
);
expect([...backticked].sort()).toEqual(
SCENARIO_CATALOG.map((tool) => tool.operationId).sort(),
);
});
});