139 lines
4.8 KiB
JavaScript
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();
|