import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { readdir, readFile } from "node:fs/promises"; import { relative, resolve, sep } from "node:path"; import Ajv2020 from "ajv/dist/2020.js"; export const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"; export const PRP_SCHEMA_ID_PREFIX = "https://paperclip.dev/schemas/prp/"; export const PRP_V1_SCHEMA_ID_PREFIX = `${PRP_SCHEMA_ID_PREFIX}v1/`; export const SUPPORTED_FIXTURE_VERSION = 1; export const MIN_SUPPORTED_PROTOCOL_VERSION = 1; export const SUPPORTED_PROTOCOL_VERSION = 2; export const SUPPORTED_EVENT_SCHEMA_VERSION = 1; function contractError(code, detail) { return new Error(`${code}: ${detail}`); } export async function readJson(path) { const source = await readFile(path, "utf8"); try { return { source, value: JSON.parse(source) }; } catch (error) { throw contractError("invalid_json", `${path}: ${error.message}`); } } export async function listJsonFiles(root) { const entries = await readdir(root, { recursive: true, withFileTypes: true }); return entries .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) .map((entry) => resolve(entry.parentPath, entry.name)) .sort(); } export function portableRelative(root, path) { return relative(root, path).split(sep).join("/"); } export function sha256(source) { return createHash("sha256").update(source).digest("hex"); } function collectReferences(value, output = []) { if (Array.isArray(value)) { for (const item of value) collectReferences(item, output); return output; } if (value === null || typeof value !== "object") return output; for (const [key, child] of Object.entries(value)) { if (key === "$ref" && typeof child === "string") output.push(child); else collectReferences(child, output); } return output; } function resolveJsonPointer(value, fragment) { if (fragment === "") return value; if (!fragment.startsWith("/")) return undefined; return fragment .slice(1) .split("/") .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~")) .reduce((current, part) => { if (current === null || typeof current !== "object") return undefined; return Object.hasOwn(current, part) ? current[part] : undefined; }, value); } export async function loadSchemaCatalog(schemaDirectory) { const files = await listJsonFiles(schemaDirectory); const records = await Promise.all(files.map(async (path) => ({ path, ...(await readJson(path)) }))); const ids = new Map(); for (const record of records) { const schema = record.value; if (schema.$schema !== JSON_SCHEMA_DIALECT) { throw contractError("unsupported_schema_dialect", portableRelative(schemaDirectory, record.path)); } if (typeof schema.$id !== "string" || !schema.$id.startsWith(PRP_SCHEMA_ID_PREFIX)) { throw contractError("invalid_schema_id", portableRelative(schemaDirectory, record.path)); } if (ids.has(schema.$id)) throw contractError("duplicate_schema_id", schema.$id); if (typeof schema.title !== "string" || schema.title.length === 0) { throw contractError("missing_schema_title", schema.$id); } ids.set(schema.$id, record); } for (const record of records) { for (const reference of collectReferences(record.value)) { const [targetId, fragment = ""] = reference.split("#", 2); const target = targetId === "" ? record : ids.get(targetId); if (target === undefined) { throw contractError("unresolved_schema_reference", `${record.value.$id} -> ${reference}`); } if (resolveJsonPointer(target.value, fragment) === undefined) { throw contractError("unresolved_schema_fragment", `${record.value.$id} -> ${reference}`); } } } return records; } export function compileProtocolValidators(schemaRecords) { const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, formats: { "date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/, }, }); for (const record of schemaRecords) ajv.addSchema(record.value); const get = (name) => { const id = `${PRP_V1_SCHEMA_ID_PREFIX}${name}.schema.json`; const validator = ajv.getSchema(id); if (validator === undefined) throw contractError("missing_schema_validator", id); return validator; }; const getVersioned = (version, name) => { const id = `${PRP_SCHEMA_ID_PREFIX}v${version}/${name}.schema.json`; const validator = ajv.getSchema(id); if (validator === undefined) throw contractError("missing_schema_validator", id); return validator; }; return { conformanceFixture: get("conformance-fixture"), conformanceOutput: get("conformance-output"), fixture: get("fixture"), providerDescriptor: get("provider-descriptor"), questionAdapterFixture: get("question-adapter-fixture"), capabilitiesV2: getVersioned(2, "capabilities"), commandV2: getVersioned(2, "command"), eventV2: getVersioned(2, "event"), }; } export function assertSchemaInstance(validator, value, location, expectedValid = true) { const valid = validator(value); if (valid !== expectedValid) { const detail = (validator.errors ?? []) .slice(0, 8) .map((error) => `${error.instancePath || "/"} ${error.message}`) .join("; "); const expectation = expectedValid ? "accepted" : "rejected"; throw contractError("schema_validation_failed", `${location} must be ${expectation}: ${detail || "no AJV error"}`); } return value; } function requireSchema(value, expected, location) { if (value?.schema !== expected) { throw contractError("unsupported_required_schema", `${location} requires ${String(value?.schema)}`); } } function requireVersion(value, expected, name) { if (value !== expected) { throw contractError("unsupported_required_version", `${name}=${String(value)}; supported=${expected}`); } } function requireSupportedProtocolVersion(value) { if (!Number.isInteger(value) || value < MIN_SUPPORTED_PROTOCOL_VERSION || value > SUPPORTED_PROTOCOL_VERSION) { throw contractError( "unsupported_required_version", `protocolVersion=${String(value)}; supported=${MIN_SUPPORTED_PROTOCOL_VERSION}-${SUPPORTED_PROTOCOL_VERSION}`, ); } } export function assertReplayFixtureCompatibility(fixture) { requireSchema(fixture, "paperclip.prp.fixture.v1", "fixture"); requireVersion(fixture.fixtureVersion, SUPPORTED_FIXTURE_VERSION, "fixtureVersion"); requireSupportedProtocolVersion(fixture.protocolVersion); requireSchema(fixture.identity, "paperclip.prp.identity.v1", "identity"); requireSchema(fixture.capabilities, "paperclip.prp.capabilities.v1", "capabilities"); if (!Array.isArray(fixture.commands)) throw contractError("invalid_fixture", "commands must be an array"); for (const [index, command] of fixture.commands.entries()) { if (command?.schema !== "paperclip.prp.command.v1" && command?.schema !== "paperclip.prp.command.v2") { throw contractError("unsupported_required_schema", `commands[${index}] requires ${String(command?.schema)}`); } } if (!Array.isArray(fixture.events) || fixture.events.length === 0) { throw contractError("invalid_fixture", "events must be a non-empty array"); } for (const [index, event] of fixture.events.entries()) { requireSchema(event, "paperclip.prp.event.v1", `events[${index}]`); requireVersion(event.schemaVersion, SUPPORTED_EVENT_SCHEMA_VERSION, `events[${index}].schemaVersion`); const semanticToolVersion = event.payload?.semantic_tool?.schemaVersion; if (semanticToolVersion !== undefined) { requireVersion(semanticToolVersion, 1, `events[${index}].payload.semantic_tool.schemaVersion`); } } requireSchema(fixture.result, "paperclip.run_result.v1", "result"); return fixture; } export function assertQuestionAdapterFixture(fixture) { requireSchema(fixture, "paperclip.question_adapter_fixture.v1", "question fixture"); requireSchema(fixture.canonicalQuestionSet, "paperclip.question_set.v1", "canonicalQuestionSet"); requireSchema(fixture.canonicalResponse, "paperclip.question_response.v1", "canonicalResponse"); const questions = fixture.canonicalQuestionSet.questions; if (!Array.isArray(questions) || questions.length === 0) { throw contractError("invalid_question_adapter_fixture", "questions must be non-empty"); } const questionsById = new Map(); const optionIdsByQuestion = new Map(); for (const question of questions) { if (typeof question.id !== "string" || question.id.length === 0 || questionsById.has(question.id)) { throw contractError("invalid_question_adapter_fixture", "question IDs must be unique"); } questionsById.set(question.id, question); const optionIds = new Set(); for (const option of question.options ?? []) { if (typeof option.id !== "string" || option.id.length === 0 || optionIds.has(option.id)) { throw contractError("invalid_question_adapter_fixture", `option IDs for ${question.id} must be unique`); } optionIds.add(option.id); } optionIdsByQuestion.set(question.id, optionIds); const validation = question.textValidation; if ( validation?.minLength !== undefined && validation.maxLength !== undefined && validation.minLength > validation.maxLength ) { throw contractError("invalid_question_adapter_fixture", `text validation for ${question.id} has minLength greater than maxLength`); } if ( validation?.minimum !== undefined && validation.maximum !== undefined && validation.minimum > validation.maximum ) { throw contractError("invalid_question_adapter_fixture", `text validation for ${question.id} has minimum greater than maximum`); } if (validation?.pattern !== undefined) { try { new RegExp(validation.pattern); } catch { throw contractError("invalid_question_adapter_fixture", `text validation pattern for ${question.id} is invalid`); } } } const answers = fixture.canonicalResponse.answers ?? {}; for (const [answerId, answer] of Object.entries(answers)) { if (!questionsById.has(answerId)) { throw contractError("invalid_question_adapter_fixture", `answer has unknown question ID ${answerId}`); } if (answer === null || typeof answer !== "object" || Array.isArray(answer)) { throw contractError("invalid_question_adapter_fixture", `answer for ${answerId} must be an object`); } const selectedOptionIds = answer.selectedOptionIds ?? []; if (!Array.isArray(selectedOptionIds) || selectedOptionIds.some((id) => typeof id !== "string")) { throw contractError("invalid_question_adapter_fixture", `answer for ${answerId} has invalid option IDs`); } if (new Set(selectedOptionIds).size !== selectedOptionIds.length) { throw contractError("invalid_question_adapter_fixture", `answer for ${answerId} repeats an option ID`); } for (const optionId of selectedOptionIds) { if (!optionIdsByQuestion.get(answerId)?.has(optionId)) { throw contractError("invalid_question_adapter_fixture", `answer has unknown option ID ${optionId}`); } } } for (const question of questions) { const answer = answers[question.id]; if (answer === undefined) { if (question.required) { throw contractError("invalid_question_adapter_fixture", `answer for required question ${question.id} is missing`); } continue; } const selectedOptionIds = answer.selectedOptionIds ?? []; const text = answer.text; const customText = answer.customText; if (question.answerMode === "text") { if (selectedOptionIds.length > 0 || customText !== undefined) { throw contractError("invalid_question_adapter_fixture", `text answer for ${question.id} carries select-only fields`); } } else { if (text !== undefined) { throw contractError("invalid_question_adapter_fixture", `select answer for ${question.id} carries text`); } if (question.answerMode === "single_select" && selectedOptionIds.length > 1) { throw contractError("invalid_question_adapter_fixture", `single-select answer for ${question.id} chooses more than one option`); } if (customText !== undefined && question.customAnswer?.enabled !== true) { throw contractError("invalid_question_adapter_fixture", `custom answer for ${question.id} is not enabled`); } if ( question.answerMode === "single_select" && typeof customText === "string" && customText.trim().length > 0 && selectedOptionIds.length > 0 ) { throw contractError("invalid_question_adapter_fixture", `single-select answer for ${question.id} mixes option and custom values`); } } const hasValue = fixtureAnswerHasValue(answer); if (question.required && !hasValue) { throw contractError("invalid_question_adapter_fixture", `answer for required question ${question.id} is empty`); } const boundedText = question.answerMode === "text" ? text : customText; if (boundedText !== undefined) { const validation = question.textValidation; if (validation?.minLength !== undefined && boundedText.length < validation.minLength) { throw contractError("invalid_question_adapter_fixture", `answer for ${question.id} must contain at least ${validation.minLength} characters`); } if (validation?.maxLength !== undefined && boundedText.length > validation.maxLength) { throw contractError("invalid_question_adapter_fixture", `answer for ${question.id} must contain at most ${validation.maxLength} characters`); } if (validation?.pattern !== undefined) { let pattern; try { pattern = new RegExp(validation.pattern); } catch { throw contractError("invalid_question_adapter_fixture", `text validation pattern for ${question.id} is invalid`); } if (!testFixturePatternWithDeadline(pattern.source, boundedText, question.id)) { throw contractError("invalid_question_adapter_fixture", `answer for ${question.id} does not match the required format`); } } if (validation?.inputType === "number" || validation?.inputType === "integer") { const numeric = Number(boundedText); if (!Number.isFinite(numeric) || (validation.inputType === "integer" && !Number.isInteger(numeric))) { throw contractError("invalid_question_adapter_fixture", `answer for ${question.id} must be a valid ${validation.inputType}`); } if (validation.minimum !== undefined && numeric < validation.minimum) { throw contractError("invalid_question_adapter_fixture", `answer for ${question.id} must be at least ${validation.minimum}`); } if (validation.maximum !== undefined && numeric > validation.maximum) { throw contractError("invalid_question_adapter_fixture", `answer for ${question.id} must be at most ${validation.maximum}`); } } } } return fixture; } export function assertCodexQuestionFixture(fixture) { assertQuestionAdapterFixture(fixture); if (fixture.adapter !== "codex") throw contractError("unsupported_provider", String(fixture.adapter)); if (fixture.nativeRequest?.method !== "item/tool/requestUserInput") { throw contractError("invalid_codex_question_fixture", "native request method"); } return fixture; } export function assertAcpxQuestionFixture(fixture) { assertQuestionAdapterFixture(fixture); if (fixture.adapter !== "acpx") { throw contractError("unsupported_provider", String(fixture.adapter)); } const request = fixture.nativeRequest; if (!isPlainRecord(request) || request.method !== "elicitation/create") { throw contractError("invalid_acpx_question_fixture", "native request method"); } const params = request.params; const requestedSchema = isPlainRecord(params) ? params.requestedSchema : null; if ( !isPlainRecord(params) || params.mode !== "form" || !isPlainRecord(requestedSchema) || !isPlainRecord(requestedSchema.properties) ) { throw contractError("invalid_acpx_question_fixture", "native form request"); } const properties = Object.entries(requestedSchema.properties); if (properties.length === 0 || properties.length > 64) { throw contractError("invalid_acpx_question_fixture", "native form property count"); } const required = normalizedAcpxRequired(requestedSchema); for (const [name, property] of properties) { assertAcpxProperty(name, property); if ( isPlainRecord(property) && property.type === "string" && acpxEnumValues(property, "oneOf").length === 0 && optionalFixtureText(property.pattern) !== undefined ) { throw contractError( "invalid_acpx_question_fixture", `native property ${name} uses an unsupported pattern`, ); } } const response = fixture.nativeResponse; if ( !isPlainRecord(response) || response.action !== "accept" || !isPlainRecord(response.content) ) { throw contractError("invalid_acpx_question_fixture", "native accept response"); } for (const [name] of properties) { if (required.has(name) && !Object.hasOwn(response.content, name)) { throw contractError("invalid_acpx_question_fixture", `native response omits required property ${name}`); } } for (const [name, value] of Object.entries(response.content)) { const property = requestedSchema.properties[name]; if (!isPlainRecord(property)) { throw contractError("invalid_acpx_question_fixture", `native response has unknown property ${name}`); } assertAcpxPropertyValue(name, property, value); } const projected = projectAcpxFixture(params, fixture.canonicalResponse); if (canonicalFixtureJson(projected.questionSet) !== canonicalFixtureJson(fixture.canonicalQuestionSet)) { throw contractError("invalid_acpx_question_fixture", "native and canonical question sets differ"); } if (canonicalFixtureJson(projected.nativeResponse) !== canonicalFixtureJson(fixture.nativeResponse)) { throw contractError("invalid_acpx_question_fixture", "canonical and native responses differ"); } return fixture; } function projectAcpxFixture(params, canonicalResponse) { const schema = params.requestedSchema; const required = normalizedAcpxRequired(schema); const bindings = Object.entries(schema.properties).map(([name, property], index) => projectAcpxProperty(name, property, index, required.has(name)) ); const title = optionalFixtureText(schema.title) ?? "Additional information needed"; const descriptions = [ optionalFixtureText(params.message), optionalFixtureText(schema.description), ].filter((value, index, all) => value !== undefined && value !== title && all.indexOf(value) === index); const questionSet = { schema: "paperclip.question_set.v1", title, ...(descriptions.length > 0 ? { description: descriptions.join("\n\n") } : {}), submitLabel: "Submit answers", questions: bindings.map((binding) => binding.question), }; const content = {}; for (const binding of bindings) { const answer = canonicalResponse.answers?.[binding.question.id]; // The production parser accepts explicit empty optional answers and omits // them from its normalized response. Mirror that before projecting ACP // content so the fixture gate certifies the same wire behavior. if (!answer || !fixtureAnswerHasValue(answer)) continue; if (binding.type === "string" && binding.question.answerMode === "text") { if (answer.text !== undefined) defineAcpxResponseProperty(content, binding.name, answer.text); } else if (binding.type === "number" || binding.type === "integer") { if (answer.text !== undefined) { defineAcpxResponseProperty(content, binding.name, Number(answer.text)); } } else if (binding.type === "boolean") { const selected = answer.selectedOptionIds?.[0]; if (selected !== undefined) { defineAcpxResponseProperty( content, binding.name, binding.optionValues.get(selected) === "true", ); } } else { const selected = (answer.selectedOptionIds ?? []).map((id) => binding.optionValues.get(id)); if (binding.type === "array") { defineAcpxResponseProperty(content, binding.name, selected); } else if (selected[0] !== undefined) { defineAcpxResponseProperty(content, binding.name, selected[0]); } } } return { questionSet, nativeResponse: { action: "accept", content } }; } function fixtureAnswerHasValue(answer) { return ( (typeof answer.text === "string" && answer.text.trim().length > 0) || (typeof answer.customText === "string" && answer.customText.trim().length > 0) || (Array.isArray(answer.selectedOptionIds) && answer.selectedOptionIds.length > 0) ); } function defineAcpxResponseProperty(content, name, value) { Object.defineProperty(content, name, { value, enumerable: true, configurable: true, writable: true, }); } function normalizedAcpxRequired(schema) { const propertyNames = new Set( isPlainRecord(schema.properties) ? Object.keys(schema.properties) : [], ); return new Set( Array.isArray(schema.required) ? schema.required .filter( (value) => typeof value === "string" && propertyNames.has(value), ) : [] ); } function projectAcpxProperty(name, property, index, required) { const id = stableAcpxFieldId(name, index); const header = optionalFixtureText(property.title) ?? name; const base = { id, header, prompt: optionalFixtureText(property.description) ?? header, required, }; if (property.type === "string") { const options = acpxNativeOptions(property, "oneOf"); if (options.length > 0) return projectedAcpxOptions(name, property.type, base, options, "single_select"); const pattern = optionalFixtureText(property.pattern); return { name, type: property.type, optionValues: new Map(), question: { ...base, answerMode: "text", textValidation: { inputType: "text", ...(finiteNonNegativeFixtureInteger(property.minLength) !== undefined ? { minLength: property.minLength } : {}), ...(finiteNonNegativeFixtureInteger(property.maxLength) !== undefined ? { maxLength: property.maxLength } : {}), ...(pattern !== undefined ? { pattern } : {}), }, }, }; } if (property.type === "number" || property.type === "integer") { return { name, type: property.type, optionValues: new Map(), question: { ...base, answerMode: "text", textValidation: { inputType: property.type, ...(Number.isFinite(property.minimum) ? { minimum: property.minimum } : {}), ...(Number.isFinite(property.maximum) ? { maximum: property.maximum } : {}), }, }, }; } if (property.type === "boolean") { return projectedAcpxOptions(name, property.type, base, [ { value: "true", label: "Yes" }, { value: "false", label: "No" }, ], "single_select"); } return projectedAcpxOptions( name, property.type, base, acpxNativeOptions(property.items, "anyOf"), "multi_select", ); } function testFixturePatternWithDeadline(pattern, value, questionId) { const outcome = spawnSync( process.execPath, [ "--input-type=module", "--eval", "const chunks=[];for await(const chunk of process.stdin)chunks.push(chunk);const {pattern,value}=JSON.parse(Buffer.concat(chunks).toString());process.stdout.write(new RegExp(pattern).test(value)?'1':'0');", ], { input: JSON.stringify({ pattern, value }), encoding: "utf8", timeout: 1_000, maxBuffer: 1_024, windowsHide: true, }, ); if (outcome.error || outcome.signal || outcome.status !== 0) { throw contractError( "invalid_question_adapter_fixture", `text validation pattern for ${questionId} could not be evaluated safely`, ); } return outcome.stdout === "1"; } function projectedAcpxOptions(name, type, base, nativeOptions, answerMode) { const optionValues = new Map(); const options = nativeOptions.map((option, index) => { const id = `option-${index + 1}`; optionValues.set(id, option.value); return { id, label: option.label, ...(option.description !== undefined ? { description: option.description } : {}), }; }); return { name, type, optionValues, question: { ...base, answerMode, options } }; } function acpxNativeOptions(property, preferredTitledKey) { const alternateTitledKey = preferredTitledKey === "anyOf" ? "oneOf" : "anyOf"; const titled = property[preferredTitledKey] ?? property[alternateTitledKey]; if (Array.isArray(titled)) { return titled.map((entry) => ({ value: entry.const, label: optionalFixtureText(entry.title) ?? entry.const, ...(optionalFixtureText(entry.description) !== undefined ? { description: entry.description } : {}), })); } return (Array.isArray(property.enum) ? property.enum : []) .map((value) => ({ value, label: value })); } function stableAcpxFieldId(value, index) { const readable = value .trim() .replace(/[^A-Za-z0-9._:-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 96); return `field-${index + 1}-${readable || "value"}-${sha256(value).slice(0, 12)}`; } function optionalFixtureText(value) { return typeof value === "string" && value.length > 0 ? value : undefined; } function finiteNonNegativeFixtureInteger(value) { return Number.isSafeInteger(value) && value >= 0 ? value : undefined; } function canonicalFixtureJson(value) { if (Array.isArray(value)) return `[${value.map(canonicalFixtureJson).join(",")}]`; if (isPlainRecord(value)) { return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalFixtureJson(value[key])}` ).join(",")}}`; } return JSON.stringify(value); } function assertAcpxProperty(name, value) { if (!isPlainRecord(value)) { throw contractError("invalid_acpx_question_fixture", `native property ${name}`); } if (!["string", "number", "integer", "boolean", "array"].includes(value.type)) { throw contractError("invalid_acpx_question_fixture", `unsupported native property ${name}`); } if (value.type === "array") { if (!isPlainRecord(value.items) || acpxEnumValues(value.items, "anyOf").length === 0) { throw contractError("invalid_acpx_question_fixture", `native array property ${name} requires options`); } } else if (value.type === "string") { acpxEnumValues(value, "oneOf"); } } function assertAcpxPropertyValue(name, property, value) { const typeMatches = property.type === "string" ? typeof value === "string" : property.type === "number" ? typeof value === "number" && Number.isFinite(value) : property.type === "integer" ? Number.isInteger(value) : property.type === "boolean" ? typeof value === "boolean" : Array.isArray(value) && value.every((item) => typeof item === "string"); if (!typeMatches) { throw contractError("invalid_acpx_question_fixture", `native response type for ${name}`); } const enumValues = property.type === "array" ? acpxEnumValues(property.items, "anyOf") : property.type === "string" ? acpxEnumValues(property, "oneOf") : []; const responseValues = Array.isArray(value) ? value : [value]; if (enumValues.length > 0 && responseValues.some((item) => !enumValues.includes(item))) { throw contractError("invalid_acpx_question_fixture", `native response option for ${name}`); } } function acpxEnumValues(property, preferredTitledKey) { if (!isPlainRecord(property)) return []; const alternateTitledKey = preferredTitledKey === "anyOf" ? "oneOf" : "anyOf"; const titled = property[preferredTitledKey] ?? property[alternateTitledKey]; const values = Array.isArray(titled) ? titled.map((entry) => isPlainRecord(entry) ? entry.const : undefined) : Array.isArray(property.enum) ? property.enum : []; if ( !Array.isArray(values) || values.length > 128 || values.some((value) => typeof value !== "string" || value.length === 0) ) { throw contractError("invalid_acpx_question_fixture", "native options must be non-empty bounded strings"); } return values; } function isPlainRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } export function assertConformanceFixturePair(fixture, output) { if (fixture?.schemaVersion !== "paperclip.runner.conformance.fixture.v1") { throw contractError("unsupported_required_schema", `conformance fixture requires ${String(fixture?.schemaVersion)}`); } if (output?.schemaVersion !== "paperclip.runner.conformance.output.v1") { throw contractError("unsupported_required_schema", `conformance output requires ${String(output?.schemaVersion)}`); } if (fixture.run?.runId !== output.runIdentity?.runId || fixture.run?.sessionId !== output.runIdentity?.sessionId) { throw contractError("conformance_identity_mismatch", "run or session identity differs"); } for (const [index, event] of (fixture.events ?? []).entries()) { if (event.runId !== fixture.run.runId || event.sequence !== index + 1) { throw contractError("invalid_conformance_event", `events[${index}] does not match the run sequence`); } } if ( fixture.result?.status !== output.result?.status || fixture.result?.summary !== output.result?.summary || fixture.result?.runId !== fixture.run.runId ) { throw contractError("conformance_result_mismatch", "expected output does not match the fixture result"); } return { fixture, output }; }