paperclip/packages/paperclip-runner/scripts/check-capability-inventory.mjs

59 lines
2.6 KiB
JavaScript

import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import {
buildMcpInventory,
buildSkillInventory,
decodeInventory,
renderContractModule,
renderDocumentation,
validateInventorySchema,
validateInventories,
} from "./lib/capability-inventory.mjs";
const packageRoot = resolve(import.meta.dirname, "..");
const readInventory = async (name) => decodeInventory(await readFile(resolve(packageRoot, `spec/capability/${name}.yaml`), "utf8"));
const inventories = {
capabilities: await readInventory("capabilities"),
evaluations: await readInventory("eval-traceability"),
legacyMcpAliases: await readInventory("mcp-tool-map"),
};
const inventorySchema = JSON.parse(await readFile(resolve(packageRoot, "spec/capability/inventory.schema.json"), "utf8"));
const errors = [
...validateInventorySchema(inventories, inventorySchema),
...validateInventories(inventories),
];
const repoRoot = resolve(packageRoot, "../..");
const [liveCapabilities, liveMcpTools] = await Promise.all([
buildSkillInventory(repoRoot),
buildMcpInventory(repoRoot),
]);
for (const [name, liveInventory] of [["capabilities", liveCapabilities], ["legacyMcpAliases", liveMcpTools]]) {
const generatedIds = new Set(inventories[name].rows.map((row) => row.id));
const liveIds = new Set(liveInventory.rows.map((row) => row.id));
for (const id of liveIds) if (!generatedIds.has(id)) errors.push(`${name} is missing live source row ${id}.`);
for (const id of generatedIds) if (!liveIds.has(id)) errors.push(`${name} has stale source row ${id}.`);
const liveRows = new Map(liveInventory.rows.map((row) => [row.id, row]));
for (const row of inventories[name].rows) {
const liveRow = liveRows.get(row.id);
if (!liveRow) continue;
for (const field of ["sourceAnchor", "expectedSemantics", "foldedInto"]) {
if (Object.hasOwn(liveRow, field) && JSON.stringify(row[field]) !== JSON.stringify(liveRow[field])) {
errors.push(`${name}:${row.id} has stale ${field}.`);
}
}
}
}
const expected = {
[resolve(packageRoot, "src/generated/capability-contract.ts")]: renderContractModule(inventories),
[resolve(packageRoot, "docs/capability-contract.md")]: renderDocumentation(inventories),
};
for (const [path, source] of Object.entries(expected)) {
if (await readFile(path, "utf8").catch(() => "") !== source) errors.push(`Generated output is stale: ${path}.`);
}
if (errors.length > 0) {
process.stderr.write(`Capability inventory check failed:\n${errors.map((error) => `- ${error}`).join("\n")}\n`);
process.exitCode = 1;
} else {
process.stdout.write("Capability inventory completeness and generated-output checks passed.\n");
}