paperclip/packages/paperclip-runner/scripts/generate-protocol-schema-mo...

116 lines
4.8 KiB
JavaScript

import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv2020 from "ajv/dist/2020.js";
import standaloneCode from "ajv/dist/standalone/index.js";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const schemaDirectory = resolve(packageRoot, "protocol/schemas");
const outputPath = resolve(packageRoot, "src/protocol/generated/schema-bundle.ts");
const validatorsOutputPath = resolve(
packageRoot,
"src/protocol/generated/standalone-validators.ts",
);
const schemaNames = [
"identity",
"capabilities",
"capabilities-v2",
"command",
"command-v2",
"provider-descriptor",
"provider-event",
"workspace-diff",
"workspace-file-reference",
"semantic-tool",
"usage",
"stop-reason",
"terminal",
"question-set",
"question-response",
"question-adapter-fixture",
"request",
"result",
"event",
"event-v2",
"session-goal",
"fixture",
];
const schemas = await Promise.all(
schemaNames.map(async (name) => ({
name,
value: JSON.parse(await readFile(resolve(schemaDirectory, `${name}.schema.json`), "utf8")),
})),
);
const identifier = (name) => name.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
const declarations = schemas
.map(({ name, value }) => `export const ${identifier(name)}Schema = ${JSON.stringify(value, null, 2)} as const;`)
.join("\n\n");
const bundle = schemaNames.map((name) => ` ${JSON.stringify(name)}: ${identifier(name)}Schema,`).join("\n");
const generated = `// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.\n\n${declarations}\n\nexport const prpSchemaBundle = {\n${bundle}\n} as const;\n`;
const schemaByName = Object.fromEntries(schemas.map(({ name, value }) => [name, value]));
const ajv = new Ajv2020({
allErrors: true,
strict: true,
strictRequired: false,
code: { esm: true, source: true },
formats: {
"date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/,
},
});
for (const { value } of schemas) ajv.addSchema(value);
const standaloneValidators = standaloneCode(ajv, {
fixtureValidator: schemaByName.fixture.$id,
eventValidator: schemaByName.event.$id,
eventV2Validator: schemaByName["event-v2"].$id,
resultValidator: schemaByName.result.$id,
});
const ucs2RuntimePattern = /const (func\d+) = require\("ajv\/dist\/runtime\/ucs2length"\)\.default;/;
if (!ucs2RuntimePattern.test(standaloneValidators)) {
throw new Error("Ajv standalone output no longer has the expected ucs2length runtime binding");
}
const equalRuntimePattern = /const (func\d+) = require\("ajv\/dist\/runtime\/equal"\)\.default;/;
if (!equalRuntimePattern.test(standaloneValidators)) {
throw new Error("Ajv standalone output no longer has the expected equal runtime binding");
}
const validatorsWithRuntimeImports = standaloneValidators.replace(
ucs2RuntimePattern,
"const $1 = typeof ucs2LengthModule === \"function\" ? ucs2LengthModule : ucs2LengthModule.default;",
).replace(
equalRuntimePattern,
"const $1 = typeof equalModule === \"function\" ? equalModule : equalModule.default;",
);
const validatorExportPattern = /export const (fixtureValidator|eventValidator|resultValidator) =/g;
const validatorExports = [...validatorsWithRuntimeImports.matchAll(validatorExportPattern)];
if (validatorExports.length !== 3) {
throw new Error("Ajv standalone output no longer has the expected validator exports");
}
// Ajv's generated functions are intentionally unchecked runtime JavaScript.
// Bound their exported types so TypeScript does not infer the full generated
// implementation graph. replay-contract.ts applies the checked public types.
const validators = validatorsWithRuntimeImports.replace(
validatorExportPattern,
"export const $1: unknown =",
);
const generatedValidators = `// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.\n// @ts-nocheck -- Ajv standalone output is JavaScript compiled from the checked-in schemas.\n\nimport equalModule from "ajv/dist/runtime/equal.js";\nimport ucs2LengthModule from "ajv/dist/runtime/ucs2length.js";\n\n${validators}\n`;
if (process.argv.includes("--check")) {
const current = await readFile(outputPath, "utf8").catch(() => "");
const currentValidators = await readFile(validatorsOutputPath, "utf8").catch(() => "");
if (current !== generated || currentValidators !== generatedValidators) {
process.stderr.write("Generated PRP schema modules are stale. Run pnpm generate:protocol-types.\n");
process.exitCode = 1;
} else {
process.stdout.write("Generated PRP schema modules match the JSON Schema sources.\n");
}
} else {
await Promise.all([
writeFile(outputPath, generated),
writeFile(validatorsOutputPath, generatedValidators),
]);
process.stdout.write("Generated PRP schema and standalone validator modules.\n");
}