paperclip/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs

139 lines
4.8 KiB
JavaScript

import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
SUPPORTED_FIXTURE_VERSION,
SUPPORTED_PROTOCOL_VERSION,
assertAcpxQuestionFixture,
assertCodexQuestionFixture,
assertConformanceFixturePair,
assertQuestionAdapterFixture,
assertReplayFixtureCompatibility,
assertSchemaInstance,
compileProtocolValidators,
listJsonFiles,
loadSchemaCatalog,
portableRelative,
readJson,
sha256,
} from "./protocol-contract.mjs";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const protocolRoot = resolve(packageRoot, "protocol");
const schemaDirectory = resolve(protocolRoot, "schemas");
const fixtureDirectory = resolve(protocolRoot, "fixtures");
const outputPath = resolve(protocolRoot, "manifest.json");
const expectedRejectedFixtures = new Set([
"fixtures/replay/unsupported-required-version.json",
"fixtures/replay/semantic-tool-unsupported-required-version.json",
]);
export async function buildProtocolManifest() {
const schemas = await loadSchemaCatalog(schemaDirectory);
const validators = compileProtocolValidators(schemas);
const fixtureFiles = await listJsonFiles(fixtureDirectory);
const fixtures = [];
const fixtureValues = new Map();
for (const path of fixtureFiles) {
const relativePath = portableRelative(protocolRoot, path);
const { source, value } = await readJson(path);
fixtureValues.set(relativePath, value);
let expectation = "accept";
let compatibilityCase = "canonical";
if (relativePath.startsWith("fixtures/replay/golden/")) {
compatibilityCase = "deterministic-replay-oracle";
} else if (relativePath.startsWith("fixtures/replay/")) {
if (expectedRejectedFixtures.has(relativePath)) {
expectation = "reject";
compatibilityCase = "unknown-required-version";
try {
assertReplayFixtureCompatibility(value);
throw new Error(`${relativePath} did not fail closed`);
} catch (error) {
if (
!String(error.message).startsWith("unsupported_required_version:")
)
throw error;
}
assertSchemaInstance(validators.fixture, value, relativePath, false);
} else {
assertReplayFixtureCompatibility(value);
assertSchemaInstance(validators.fixture, value, relativePath);
if (relativePath.endsWith("unknown-optional-fields.json")) {
compatibilityCase = "additive-optional-fields";
}
}
} else if (relativePath.startsWith("fixtures/questions/")) {
assertSchemaInstance(
validators.questionAdapterFixture,
value,
relativePath,
);
assertQuestionAdapterFixture(value);
if (relativePath === "fixtures/questions/codex.json") {
assertCodexQuestionFixture(value);
} else if (relativePath === "fixtures/questions/acpx.json") {
assertAcpxQuestionFixture(value);
}
compatibilityCase = `${value.adapter}-structured-input`;
} else if (relativePath === "fixtures/conformance-minimal-run.json") {
assertSchemaInstance(validators.conformanceFixture, value, relativePath);
compatibilityCase = "cross-language-input";
} else if (relativePath === "fixtures/conformance-expected-output.json") {
assertSchemaInstance(validators.conformanceOutput, value, relativePath);
compatibilityCase = "cross-language-output";
}
fixtures.push({
path: relativePath,
sha256: sha256(source),
expectation,
compatibilityCase,
});
}
assertConformanceFixturePair(
fixtureValues.get("fixtures/conformance-minimal-run.json"),
fixtureValues.get("fixtures/conformance-expected-output.json"),
);
return {
schema: "paperclip.prp.contract_manifest.v1",
protocolVersion: SUPPORTED_PROTOCOL_VERSION,
fixtureVersion: SUPPORTED_FIXTURE_VERSION,
generatedFrom: ["protocol/schemas", "protocol/fixtures"],
schemas: schemas.map((record) => ({
path: portableRelative(protocolRoot, record.path),
id: record.value.$id,
sha256: sha256(record.source),
})),
fixtures,
};
}
async function main() {
const encoded = `${JSON.stringify(await buildProtocolManifest(), null, 2)}\n`;
if (process.argv.includes("--check")) {
const current = await readFile(outputPath, "utf8").catch(() => "");
if (current !== encoded) {
process.stderr.write(
"The generated PRP contract manifest is stale. Run pnpm generate:protocol-manifest.\n",
);
process.exitCode = 1;
} else {
process.stdout.write(
"The generated PRP contract manifest matches its sources.\n",
);
}
} else {
await writeFile(outputPath, encoded);
process.stdout.write(`Wrote ${outputPath}\n`);
}
}
if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url))
await main();