diff --git a/package.json b/package.json index 330d0d91b7..d637db7fea 100644 --- a/package.json +++ b/package.json @@ -291,8 +291,8 @@ "verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs", "verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs", "mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check", - "compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/bundle-runtime.mjs", - "compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/bundle-runtime.mjs", + "compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", + "compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", "artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs", "artifacts:skills": "node scripts/generate-skill-metadata.mjs", "artifacts:product-catalog": "node scripts/generate-product-catalog.mjs", diff --git a/scripts/record-catalog-digests.mjs b/scripts/record-catalog-digests.mjs new file mode 100644 index 0000000000..82807c121f --- /dev/null +++ b/scripts/record-catalog-digests.mjs @@ -0,0 +1,19 @@ +import { writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +// Record the default catalog digests into the compiled runtime. Run after +// `tsc` and before bundling, so the digests come from the same compiled +// contracts and pinned schema converter that the runtime serves. +const compiledRoot = resolve( + dirname(fileURLToPath(import.meta.url)), + "../dist", +); +const { createCatalogDigests } = await import( + pathToFileURL(join(compiledRoot, "catalogIdentity.js")).href +); +await writeFile( + join(compiledRoot, "recordedCatalogDigests.js"), + `/** Recorded from the compiled default catalog by scripts/record-catalog-digests.mjs; do not edit. */\nexport const RECORDED_CATALOG_DIGESTS = Object.freeze(${JSON.stringify(createCatalogDigests())});\n`, + "utf8", +); diff --git a/src/catalogIdentity.ts b/src/catalogIdentity.ts index 676251dd02..80425d26ee 100644 --- a/src/catalogIdentity.ts +++ b/src/catalogIdentity.ts @@ -17,6 +17,7 @@ import { type CompactInputSchemaPresentation, } from "./contracts/compactInputSchema.js"; import type { McpOutputSchemaProfile } from "./config/mcpOutputSchemaProfile.js"; +import { RECORDED_CATALOG_DIGESTS } from "./recordedCatalogDigests.js"; const sortByName = ( contracts: readonly ToolContract[], @@ -102,6 +103,8 @@ export const createCatalogDigests = ( } as const; }; +export type CatalogDigests = ReturnType; + export const createCatalogIdentity = ( compactPresentation: CompactInputSchemaPresentation | undefined = undefined, outputSchemaProfile: McpOutputSchemaProfile = "full", @@ -110,7 +113,12 @@ export const createCatalogIdentity = ( compactPresentation === undefined ? undefined : { budgetBytes: compactPresentation.budgetBytes }; - let catalogDigests: ReturnType | undefined; + // Building and canonicalizing every advertised schema dominates doctor and + // server startup, so the compiled runtime records the default digests once. + let catalogDigests: CatalogDigests | undefined = + selectedPresentation === undefined && outputSchemaProfile === "full" + ? RECORDED_CATALOG_DIGESTS + : undefined; return { counts: { cli_commands: CLI_COMMAND_NAMES.length, @@ -128,5 +136,8 @@ export const createCatalogIdentity = ( } as const; }; -/** Stable, schema-sensitive identity; schema digests are computed on first use. */ +/** + * Stable, schema-sensitive identity; schema digests are recorded at build time + * or computed on first use. + */ export const CATALOG_IDENTITY = createCatalogIdentity(); diff --git a/src/recordedCatalogDigests.ts b/src/recordedCatalogDigests.ts new file mode 100644 index 0000000000..fc37c24d50 --- /dev/null +++ b/src/recordedCatalogDigests.ts @@ -0,0 +1,8 @@ +import type { CatalogDigests } from "./catalogIdentity.js"; + +/** + * Digests of the default catalog (full input and output schemas), recorded + * from the compiled contracts by `scripts/record-catalog-digests.mjs` after + * compilation. Source runs leave this undefined and compute them on first use. + */ +export const RECORDED_CATALOG_DIGESTS: CatalogDigests | undefined = undefined; diff --git a/turbo.json b/turbo.json index 8ca759ba1a..37783a4afd 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,7 @@ "scripts/check-dependency-install.mjs", "scripts/clean-build-output.mjs", "scripts/bundle-runtime.mjs", + "scripts/record-catalog-digests.mjs", "scripts/generate-package-metadata.mjs", "src/**/*.{cts,mts,ts,tsx}", "src/**/package.json",