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"); }