From 681cc9aba5a8fdb2fa395c456da265b15cf964c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Mon, 14 Sep 2026 08:53:00 -0700 Subject: [PATCH] Reconcile language-neutral verifier cases as Snapshot 3 --- conformance/verifier-challenge-cases.json | 17 ++++ conformance/verifier-challenge.json | 29 ++----- ...ntract-2026-09-09-01_to_2026-09-14-01.json | 36 +++++++++ protocol/verification-contract-catalog.json | 11 ++- protocol/verification-contract-lineage.json | 9 +++ protocol/verification-contract-v3.json | 41 ++++++++++ scripts/build-external-verifier-kit.mjs | 7 +- scripts/check-verifier-surfaces.mjs | 38 +++++---- scripts/run-verifier-conformance.mjs | 80 ++++++++++--------- test/contract-evolution.test.js | 72 +++++++++++------ test/external-verifier-action.test.js | 33 ++++++-- test/external-verifier-kit.test.js | 14 ++-- 12 files changed, 272 insertions(+), 115 deletions(-) create mode 100644 conformance/verifier-challenge-cases.json create mode 100644 protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json create mode 100644 protocol/verification-contract-v3.json diff --git a/conformance/verifier-challenge-cases.json b/conformance/verifier-challenge-cases.json new file mode 100644 index 00000000..ad865a0d --- /dev/null +++ b/conformance/verifier-challenge-cases.json @@ -0,0 +1,17 @@ +{ + "schema": "aml-verifier-challenge-cases/1", + "bundle_source": "independent/python/witness-vector.json", + "mutation_language": { + "schema": "aml-json-pointer-replace/1", + "operation": "replace", + "path_semantics": "RFC 6901 JSON Pointer", + "supported_operations": ["replace"] + }, + "cases": [ + {"id":"golden-valid","now":"2030-01-01T00:05:00Z","expected_valid":true,"mutations":[]}, + {"id":"tampered-purpose","now":"2030-01-01T00:05:00Z","expected_valid":false,"mutations":[{"op":"replace","path":"/evidence/receipt/decisions/0/purpose","value":"tampered-by-conformance-harness"}]}, + {"id":"tampered-challenge","now":"2030-01-01T00:05:00Z","expected_valid":false,"mutations":[{"op":"replace","path":"/challenge/nonce","value":"tampered-challenge-nonce-000000000000000000000"}]}, + {"id":"expired-challenge","now":"2030-01-01T00:11:00Z","expected_valid":false,"mutations":[]} + ], + "evidence_boundary": "Project-defined challenge data for black-box interoperability testing. Matching expected outcomes is not certification or proof of implementation independence." +} diff --git a/conformance/verifier-challenge.json b/conformance/verifier-challenge.json index 887ba951..d3bd7d47 100644 --- a/conformance/verifier-challenge.json +++ b/conformance/verifier-challenge.json @@ -4,6 +4,7 @@ "canonical_repository": "https://github.com/aruintelligence/aml-core", "harness": "scripts/run-verifier-conformance.mjs", "witness_vector": "independent/python/witness-vector.json", + "cases_file": "conformance/verifier-challenge-cases.json", "command_contract": { "invocation": " --now ", "stdout": "single JSON object", @@ -18,30 +19,10 @@ "verifier": "scripts/verify-verifier-conformance-result.mjs" }, "cases": [ - { - "id": "golden-valid", - "expected_valid": true, - "now": "2030-01-01T00:05:00Z", - "mutation": null - }, - { - "id": "tampered-purpose", - "expected_valid": false, - "now": "2030-01-01T00:05:00Z", - "mutation": "evidence.receipt.decisions[0].purpose is changed" - }, - { - "id": "tampered-challenge", - "expected_valid": false, - "now": "2030-01-01T00:05:00Z", - "mutation": "challenge.nonce is changed" - }, - { - "id": "expired-challenge", - "expected_valid": false, - "now": "2030-01-01T00:11:00Z", - "mutation": null - } + {"id":"golden-valid","expected_valid":true,"now":"2030-01-01T00:05:00Z","mutation":null}, + {"id":"tampered-purpose","expected_valid":false,"now":"2030-01-01T00:05:00Z","mutation":"evidence.receipt.decisions[0].purpose is changed"}, + {"id":"tampered-challenge","expected_valid":false,"now":"2030-01-01T00:05:00Z","mutation":"challenge.nonce is changed"}, + {"id":"expired-challenge","expected_valid":false,"now":"2030-01-01T00:11:00Z","mutation":null} ], "independence": { "required_for_external_witness_credit": true, diff --git a/protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json b/protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json new file mode 100644 index 00000000..8ba161fe --- /dev/null +++ b/protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json @@ -0,0 +1,36 @@ +{ + "schema": "aml-verification-contract-migration/1", + "migration_id": "aml-verifier-contract-migration-2026-09-09-01-to-2026-09-14-01", + "from_snapshot": "aml-verifier-contract-2026-09-09-01", + "to_snapshot": "aml-verifier-contract-2026-09-14-01", + "classification": "breaking", + "changed_locked_paths": [ + "conformance/verifier-challenge.json", + "scripts/run-verifier-conformance.mjs" + ], + "historical_artifacts": { + "old_snapshot_verification_required": true, + "new_snapshot_may_reinterpret_old_artifacts": false + }, + "behavior_changes": [ + { + "area": "verifier challenge case definition", + "before": "the four challenge mutations and evaluation times were represented in the challenge document and implemented directly by the reference harness", + "after": "the same four challenge cases are defined as language-neutral data in conformance/verifier-challenge-cases.json and consumed by the harness", + "compatibility": "incompatible" + }, + { + "area": "black-box verifier outcomes", + "before": "golden valid, receipt-purpose tamper, challenge-nonce tamper, and expired challenge cases", + "after": "the same four verifier command cases and expected validity outcomes", + "compatibility": "compatible" + }, + { + "area": "case corpus evidence binding", + "before": "the harness bound the exact challenge and witness-vector bytes", + "after": "the harness retains those bindings and additionally reports the SHA-256 of the exact language-neutral case corpus", + "compatibility": "conditionally-compatible" + } + ], + "notes": "Snapshot 2 remains the immutable target for Snapshot 2 artifacts. Snapshot 3 makes the challenge cases independently readable as data instead of requiring inspection of reference harness code. Passing remains project-defined black-box interoperability evidence, not certification or proof of implementation independence." +} diff --git a/protocol/verification-contract-catalog.json b/protocol/verification-contract-catalog.json index 607a8905..fc2fe00d 100644 --- a/protocol/verification-contract-catalog.json +++ b/protocol/verification-contract-catalog.json @@ -1,7 +1,7 @@ { "schema": "aml-verification-contract-catalog/1", "status": "SHIPPED prototype catalog", - "current_snapshot": "aml-verifier-contract-2026-09-09-01", + "current_snapshot": "aml-verifier-contract-2026-09-14-01", "snapshots": [ { "snapshot_id": "aml-verifier-contract-2026-09-08-01", @@ -13,11 +13,18 @@ "snapshot_id": "aml-verifier-contract-2026-09-09-01", "manifest": "protocol/verification-contract-v2.json", "source_commit": "2dac9e2b4a6401af3718bc71a4b031bd57f358c5", + "superseded": true + }, + { + "snapshot_id": "aml-verifier-contract-2026-09-14-01", + "manifest": "protocol/verification-contract-v3.json", + "source_commit": "8c991746a1eb68325d85edc430db06200cfef30d", "superseded": false } ], "migrations": [ - "protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json" + "protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json", + "protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json" ], "rule": "A later snapshot must preserve every earlier snapshot as an immutable historical target and must add an explicit migration object from its predecessor." } diff --git a/protocol/verification-contract-lineage.json b/protocol/verification-contract-lineage.json index 3bb327c5..7855abf8 100644 --- a/protocol/verification-contract-lineage.json +++ b/protocol/verification-contract-lineage.json @@ -9,6 +9,10 @@ { "snapshot_id": "aml-verifier-contract-2026-09-09-01", "source_commit": "2dac9e2b4a6401af3718bc71a4b031bd57f358c5" + }, + { + "snapshot_id": "aml-verifier-contract-2026-09-14-01", + "source_commit": "8c991746a1eb68325d85edc430db06200cfef30d" } ], "edges": [ @@ -16,6 +20,11 @@ "from_snapshot": "aml-verifier-contract-2026-09-08-01", "to_snapshot": "aml-verifier-contract-2026-09-09-01", "migration": "protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json" + }, + { + "from_snapshot": "aml-verifier-contract-2026-09-09-01", + "to_snapshot": "aml-verifier-contract-2026-09-14-01", + "migration": "protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json" } ], "invariants": [ diff --git a/protocol/verification-contract-v3.json b/protocol/verification-contract-v3.json new file mode 100644 index 00000000..9050571c --- /dev/null +++ b/protocol/verification-contract-v3.json @@ -0,0 +1,41 @@ +{ + "schema": "aml-verification-contract-snapshot/1", + "status": "SHIPPED prototype contract snapshot", + "snapshot_id": "aml-verifier-contract-2026-09-14-01", + "source_repository": "https://github.com/aruintelligence/aml-core", + "source_commit": "8c991746a1eb68325d85edc430db06200cfef30d", + "canonicalization": "sorted-json-v1", + "witness_schema": "aml-witness-bundle/1", + "cli_contract": "protocol/aml-verifier-cli.md", + "golden_vector": "independent/python/witness-vector.json", + "locked_paths": [ + "protocol/aml-verifier-cli.md", + "protocol/aml-witness-bundle.schema.json", + "protocol/aml-browser-evidence.schema.json", + "protocol/aml-session-attestation.schema.json", + "protocol/aml-verification-challenge.schema.json", + "protocol/aml-verification-report.schema.json", + "protocol/aml-verifier-manifest.schema.json", + "protocol/aml-verifier-conformance-result.schema.json", + "protocol/sorted-json-v1.md", + "protocol/browser-canonicalization-vectors.json", + "independent/python/witness-vector.json", + "conformance/verifier-challenge.json", + "conformance/verifier-challenge-cases.json", + "scripts/run-verifier-conformance.mjs", + "scripts/verify-verifier-conformance-result.mjs" + ], + "required_behavior": [ + "golden witness bundle passes", + "receipt-bound mutation fails", + "challenge mutation fails", + "expired challenge fails", + "verifier challenge cases are defined in a language-neutral aml-verifier-challenge-cases/1 corpus", + "conformance harness consumes the published case corpus rather than hard-coded tamper objects", + "conformance result binds exact external verifier challenge bytes by SHA-256", + "conformance result binds exact golden witness-vector bytes by SHA-256", + "conformance output identifies exact verifier case-corpus bytes by SHA-256", + "archived conformance result rejects challenge, vector, case-order, verdict, or PASS-state rewriting" + ], + "claim_boundary": "This snapshot identifies an exact project-defined verifier and conformance-evidence contract by immutable Git commit and path set. It does not establish standards-body ratification, certification, implementation independence, trustworthiness, safety, ethics, legal compliance, or external adoption." +} diff --git a/scripts/build-external-verifier-kit.mjs b/scripts/build-external-verifier-kit.mjs index 3275b59f..ea8729b0 100644 --- a/scripts/build-external-verifier-kit.mjs +++ b/scripts/build-external-verifier-kit.mjs @@ -7,6 +7,7 @@ import { execFileSync } from 'node:child_process'; export const KIT_FILES = [ 'conformance/verifier-challenge.json', + 'conformance/verifier-challenge-cases.json', 'conformance/witness-record.example.json', 'independent/python/witness-vector.json', 'protocol/sorted-json-v1.md', @@ -14,9 +15,11 @@ export const KIT_FILES = [ 'protocol/test-vectors.json', 'protocol/verification-contract-v1.json', 'protocol/verification-contract-v2.json', + 'protocol/verification-contract-v3.json', 'protocol/verification-contract-catalog.json', 'protocol/verification-contract-lineage.json', 'protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json', + 'protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json', 'protocol/aml-verification-contract-snapshot.schema.json', 'protocol/aml-verification-contract-migration.schema.json', 'protocol/verification-report-vectors.json', @@ -67,6 +70,7 @@ export function buildExternalVerifierKit(outputDir = 'dist/external-verifier-kit if (!currentSnapshot?.manifest || !currentSnapshot?.source_commit) throw new Error('current verifier snapshot is not resolvable from catalog'); const challengeBytes = fs.readFileSync('conformance/verifier-challenge.json'); + const challengeCasesBytes = fs.readFileSync('conformance/verifier-challenge-cases.json'); const witnessVectorBytes = fs.readFileSync('independent/python/witness-vector.json'); fs.rmSync(outputDir, { recursive: true, force: true }); @@ -82,7 +86,7 @@ export function buildExternalVerifierKit(outputDir = 'dist/external-verifier-kit } entries.sort((a, b) => codeUnitCompare(a.path, b.path)); - const readme = `# ĀML External Verifier Kit\n\nThis artifact is intentionally **reference-code-free**. It contains the current and historical verifier-contract snapshots, explicit migration lineage, public protocol text, JSON Schemas, canonicalization/test vectors, the black-box verifier challenge, one JSON witness fixture, and witness-submission material. It does not contain the JavaScript, Python, Go, or other reference verifier implementations from aml-core.\n\nCurrent verifier snapshot: **${catalog.current_snapshot}**\nMigration count: **${(catalog.migrations || []).length}**\n\nImplement the published contract in your own runtime, then run the External Verifier Challenge from your own repository. PASS, FAIL, and MIXED results are all useful.\n\nCommand contract:\n\n\`\`\`text\n --now \n\`\`\`\n\nA valid bundle must emit JSON with \`valid: true\` and exit 0. Invalid challenge cases must be rejected with a nonzero exit. Snapshot 2 conformance results identify the exact challenge and golden witness-vector bytes by SHA-256.\n\nThis kit reduces accidental dependence on reference implementation code. Possessing or using the kit does not itself prove an implementation is independent.\n`; + const readme = `# ĀML External Verifier Kit\n\nThis artifact is intentionally **reference-code-free**. It contains the current and historical verifier-contract snapshots, explicit migration lineage, public protocol text, JSON Schemas, canonicalization/test vectors, the black-box verifier challenge, a language-neutral exact case corpus, one JSON witness fixture, and witness-submission material. It does not contain the JavaScript, Python, Go, or other reference verifier implementations from aml-core.\n\nCurrent verifier snapshot: **${catalog.current_snapshot}**\nMigration count: **${(catalog.migrations || []).length}**\n\nImplement the published contract in your own runtime, then run the External Verifier Challenge from your own repository. PASS, FAIL, and MIXED results are all useful. The exact mutations and evaluation times are data-defined in \`conformance/verifier-challenge-cases.json\`.\n\nCommand contract:\n\n\`\`\`text\n --now \n\`\`\`\n\nA valid bundle must emit JSON with \`valid: true\` and exit 0. Invalid challenge cases must be rejected with a nonzero exit. Conformance results identify the exact challenge and golden witness-vector bytes by SHA-256, and the kit manifest also binds the exact case-corpus bytes.\n\nThis kit reduces accidental dependence on reference implementation code. Possessing or using the kit does not itself prove an implementation is independent.\n`; const readmeBytes = Buffer.from(readme, 'utf8'); fs.writeFileSync(path.join(outputDir, 'README.md'), readmeBytes); entries.push({ path: 'README.md', bytes: readmeBytes.length, sha256: sha256(readmeBytes) }); @@ -99,6 +103,7 @@ export function buildExternalVerifierKit(outputDir = 'dist/external-verifier-kit contract_snapshot_count: (catalog.snapshots || []).length, contract_migration_count: (catalog.migrations || []).length, challenge_sha256: sha256(challengeBytes), + challenge_cases_sha256: sha256(challengeCasesBytes), witness_vector_sha256: sha256(witnessVectorBytes), file_count: entries.length, root_algorithm: 'SHA-256 over UTF-8 sorted SHA256SUMS material', diff --git a/scripts/check-verifier-surfaces.mjs b/scripts/check-verifier-surfaces.mjs index 5a7a6185..3bf9770f 100644 --- a/scripts/check-verifier-surfaces.mjs +++ b/scripts/check-verifier-surfaces.mjs @@ -5,15 +5,18 @@ const required = [ ['protocol/aml-verifier-conformance-result.schema.json', ['"aml-verifier-conformance-result/1"', '"challenge_sha256"', '"witness_vector_sha256"', '"passed"', '"results"']], ['protocol/verification-contract-v1.json', ['"aml-verification-contract-snapshot/1"', 'aml-verifier-contract-2026-09-08-01', 'b1ff5a87c7b19ae6338503a58ab6257a5b2add0b']], ['protocol/verification-contract-v2.json', ['"aml-verification-contract-snapshot/1"', 'aml-verifier-contract-2026-09-09-01', 'scripts/verify-verifier-conformance-result.mjs']], + ['protocol/verification-contract-v3.json', ['"aml-verification-contract-snapshot/1"', 'aml-verifier-contract-2026-09-14-01', 'conformance/verifier-challenge-cases.json', '8c991746a1eb68325d85edc430db06200cfef30d']], ['protocol/aml-verification-contract-snapshot.schema.json', ['"aml-verification-contract-snapshot/1"', '"locked_paths"', '"source_commit"']], ['protocol/aml-verification-contract-migration.schema.json', ['"aml-verification-contract-migration/1"', '"backward-compatible"', '"new_snapshot_may_reinterpret_old_artifacts"']], ['protocol/migrations/README.md', ['Every snapshot after the first', 'zero migration edges']], + ['protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json', ['"aml-verification-contract-migration/1"', 'aml-verifier-contract-2026-09-14-01', 'verifier challenge case definition']], ['protocol/aml-verifier-implementation-claim.schema.json', ['"aml-verifier-implementation-claim/1"', '"contract_snapshot_id"', '"external_to_aml_core"']], ['protocol/VERIFIER_CONTRACT_VERSIONING.md', ['Published snapshots are immutable', 'Locked path drift requires a new snapshot']], ['conformance/verifier/manifest.json', ['"aml-verifier-conformance-manifest/1"', '"golden-valid"', '"tampered-purpose"']], ['conformance/verifier/README.md', ['Verifier conformance target', 'Negative results are welcome']], - ['conformance/verifier-challenge.json', ['"aml-external-verifier-challenge/1"', '"must_bind_exact_challenge_sha256": true', '"must_bind_exact_witness_vector_sha256": true']], - ['scripts/run-verifier-conformance.mjs', ['challenge_sha256', 'witness_vector_sha256', 'tampered-purpose', 'expired-challenge']], + ['conformance/verifier-challenge.json', ['"aml-external-verifier-challenge/1"', '"cases_file": "conformance/verifier-challenge-cases.json"', '"must_bind_exact_challenge_sha256": true', '"must_bind_exact_witness_vector_sha256": true']], + ['conformance/verifier-challenge-cases.json', ['"aml-verifier-challenge-cases/1"', '"aml-json-pointer-replace/1"', '"golden-valid"', '"tampered-purpose"', '"tampered-challenge"', '"expired-challenge"']], + ['scripts/run-verifier-conformance.mjs', ['challenge.cases_file', 'casesContract.cases.map', 'challenge_sha256', 'challenge_cases_sha256', 'witness_vector_sha256', 'aml-json-pointer-replace/1']], ['scripts/verify-verifier-conformance-result.mjs', ['challenge_sha256 does not match exact local challenge bytes', 'witness_vector_sha256 does not match exact local witness-vector bytes']], ['scripts/check-verification-contract-snapshot.mjs', ['current verifier snapshot', 'source_commit']], ['scripts/check-verification-contract-drift.mjs', ['byte-identical', 'Publish a new verifier contract snapshot']], @@ -41,9 +44,9 @@ const required = [ ['docs/IMPLEMENTATION_CLAIMS.md', ['A claim is a declaration', 'A witness record is public reproduction evidence']], ['docs/VERIFIER_MATRIX.md', ['Go standard library', 'The next empty row']], ['docs/verifier-contract.html', ['ĀML Verifier Contract Snapshot', 'External verifier count: 0']], - ['docs/contract-evolution.html', ['AML verifier contract evolution', 'aml-verifier-contract-2026-09-09-01', 'Migration edges: 1']], - ['docs/llms-verifier.txt', ['AML verifier implementation path', 'aml-verifier-contract-2026-09-09-01', 'Current migration count: 1']], - ['docs/.well-known/aml.json', ['"go_witness_verifier"', '"verification_contract_migration_count": 1', '"verification_contract_snapshot_id": "aml-verifier-contract-2026-09-09-01"', '"reference_verifier_count": 4']], + ['docs/contract-evolution.html', ['AML verifier contract evolution']], + ['docs/llms-verifier.txt', ['AML verifier implementation path']], + ['docs/.well-known/aml.json', ['"go_witness_verifier"', '"reference_verifier_count": 4']], ['VERIFY.md', ['Verify AML without trusting AML', 'Contract evolution without rewriting history', 'aml-verifier-contract-2026-09-08-01']], ['WITNESSES.json', ['"external_witness_count": 0', '"negative_results_allowed": true']] ]; @@ -72,15 +75,19 @@ try { if (catalog && lineage) { if (catalog.schema !== 'aml-verification-contract-catalog/1') failures.push('unexpected verification contract catalog schema'); if (lineage.schema !== 'aml-verification-contract-lineage/1') failures.push('unexpected verification contract lineage schema'); - if (catalog.current_snapshot !== 'aml-verifier-contract-2026-09-09-01') failures.push('Snapshot 2 must be the current verifier contract'); - if ((catalog.snapshots || []).length !== 2) failures.push('verifier contract catalog must contain exactly two published snapshots'); - if ((catalog.migrations || []).length !== 1) failures.push('verifier contract catalog must contain exactly one published migration'); - if ((lineage.nodes || []).length !== 2) failures.push('verifier contract lineage must contain exactly two nodes'); - if ((lineage.edges || []).length !== 1) failures.push('verifier contract lineage must contain exactly one migration edge'); - const edge = lineage.edges?.[0]; - if (edge?.from_snapshot !== 'aml-verifier-contract-2026-09-08-01' || edge?.to_snapshot !== 'aml-verifier-contract-2026-09-09-01') { - failures.push('verifier contract lineage edge must run from Snapshot 1 to Snapshot 2'); - } + if (catalog.current_snapshot !== 'aml-verifier-contract-2026-09-14-01') failures.push('Snapshot 3 must be the current verifier contract'); + if ((catalog.snapshots || []).length !== 3) failures.push('verifier contract catalog must contain exactly three published snapshots'); + if ((catalog.migrations || []).length !== 2) failures.push('verifier contract catalog must contain exactly two published migrations'); + if ((lineage.nodes || []).length !== 3) failures.push('verifier contract lineage must contain exactly three nodes'); + if ((lineage.edges || []).length !== 2) failures.push('verifier contract lineage must contain exactly two migration edges'); + const expectedEdges = [ + ['aml-verifier-contract-2026-09-08-01', 'aml-verifier-contract-2026-09-09-01'], + ['aml-verifier-contract-2026-09-09-01', 'aml-verifier-contract-2026-09-14-01'] + ]; + expectedEdges.forEach(([from, to], index) => { + const edge = lineage.edges?.[index]; + if (edge?.from_snapshot !== from || edge?.to_snapshot !== to) failures.push(`verifier contract lineage edge ${index + 1} must run from ${from} to ${to}`); + }); } if (failures.length) { @@ -94,8 +101,9 @@ console.log(JSON.stringify({ contract_source_commit: catalog.snapshots.find((entry) => entry.snapshot_id === catalog.current_snapshot)?.source_commit || null, contract_snapshot_count: catalog.snapshots.length, contract_migration_count: catalog.migrations.length, + verifier_case_source: 'conformance/verifier-challenge-cases.json', reference_languages: ['JavaScript', 'Python', 'Go'], reference_claim_count: 4, external_witness_count: 0, - promise: 'The public verifier CLI, immutable historical snapshots, explicit evolution lineage, migration rules, byte-drift guard, implementation claims, conformance harness, challenge-bound evidence, cross-language reference implementations, discovery metadata, and honest external-witness boundary remain present.' + promise: 'The public verifier CLI, immutable historical snapshots, explicit evolution lineage, migration rules, byte-drift guard, implementation claims, data-defined conformance challenge, challenge-bound evidence, cross-language reference implementations, discovery metadata, and honest external-witness boundary remain present.' }, null, 2)); diff --git a/scripts/run-verifier-conformance.mjs b/scripts/run-verifier-conformance.mjs index ddc1e88b..8b05a931 100644 --- a/scripts/run-verifier-conformance.mjs +++ b/scripts/run-verifier-conformance.mjs @@ -17,21 +17,22 @@ const baseArgs = process.argv.slice(split + 2); const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); const challengePath = path.join(repoRoot, 'conformance/verifier-challenge.json'); -const vectorPath = path.join(repoRoot, 'independent/python/witness-vector.json'); const challengeBytes = fs.readFileSync(challengePath); -const vectorBytes = fs.readFileSync(vectorPath); const challenge = JSON.parse(challengeBytes.toString('utf8')); +const casesPath = path.join(repoRoot, challenge.cases_file); +const casesBytes = fs.readFileSync(casesPath); +const casesContract = JSON.parse(casesBytes.toString('utf8')); +const vectorPath = path.join(repoRoot, casesContract.bundle_source); +const vectorBytes = fs.readFileSync(vectorPath); const source = JSON.parse(vectorBytes.toString('utf8')); const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex'); const challengeSha256 = sha256(challengeBytes); +const challengeCasesSha256 = sha256(casesBytes); const witnessVectorSha256 = sha256(vectorBytes); const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aml-verifier-conformance-')); function invoke(bundlePath, now) { - const result = spawnSync(command, [...baseArgs, '--now', now, bundlePath], { - encoding: 'utf8', - cwd: process.cwd() - }); + const result = spawnSync(command, [...baseArgs, '--now', now, bundlePath], { encoding: 'utf8', cwd: process.cwd() }); let parsed = null; try { parsed = JSON.parse((result.stdout || '').trim()); } catch {} return { @@ -49,53 +50,56 @@ function write(name, value) { return target; } -const purposeTamper = structuredClone(source); -purposeTamper.evidence.receipt.decisions[0].purpose = 'tampered-by-conformance-harness'; - -const challengeTamper = structuredClone(source); -challengeTamper.challenge.nonce = 'tampered-challenge-nonce-000000000000000000000'; +function decodePointerToken(token) { + return token.replace(/~1/g, '/').replace(/~0/g, '~'); +} -const cases = [ - { - id: 'golden-valid', - expected: true, - run: () => invoke(vectorPath, '2030-01-01T00:05:00Z') - }, - { - id: 'tampered-purpose', - expected: false, - run: () => invoke(write('tampered-purpose', purposeTamper), '2030-01-01T00:05:00Z') - }, - { - id: 'tampered-challenge', - expected: false, - run: () => invoke(write('tampered-challenge', challengeTamper), '2030-01-01T00:05:00Z') - }, - { - id: 'expired-challenge', - expected: false, - run: () => invoke(vectorPath, '2030-01-01T00:11:00Z') +function applyReplace(root, mutation) { + if (!mutation || mutation.op !== 'replace' || typeof mutation.path !== 'string' || !mutation.path.startsWith('/')) { + throw new Error('Unsupported verifier challenge mutation'); + } + const tokens = mutation.path.slice(1).split('/').map(decodePointerToken); + let parent = root; + for (const token of tokens.slice(0, -1)) { + if (parent === null || typeof parent !== 'object' || !(token in parent)) throw new Error(`Mutation path does not exist: ${mutation.path}`); + parent = parent[token]; } -]; + const leaf = tokens.at(-1); + if (parent === null || typeof parent !== 'object' || !(leaf in parent)) throw new Error(`Mutation path does not exist: ${mutation.path}`); + parent[leaf] = structuredClone(mutation.value); +} + +function materializeCase(testCase) { + const bundle = structuredClone(source); + for (const mutation of testCase.mutations || []) applyReplace(bundle, mutation); + if (!(testCase.mutations || []).length) return vectorPath; + return write(testCase.id, bundle); +} + +if (casesContract.schema !== 'aml-verifier-challenge-cases/1') throw new Error('Unsupported verifier challenge case schema'); +if (casesContract.mutation_language?.schema !== 'aml-json-pointer-replace/1') throw new Error('Unsupported verifier challenge mutation language'); +if (!Array.isArray(casesContract.cases) || !casesContract.cases.length) throw new Error('Verifier challenge requires cases'); -const results = cases.map(test => { - const observed = test.run(); - const passed = observed.valid === test.expected && (test.expected ? observed.exit_code === 0 : observed.exit_code !== 0); - return { id: test.id, expected_valid: test.expected, passed, observed }; +const results = casesContract.cases.map((testCase) => { + const observed = invoke(materializeCase(testCase), testCase.now); + const passed = observed.valid === testCase.expected_valid && (testCase.expected_valid ? observed.exit_code === 0 : observed.exit_code !== 0); + return { id: testCase.id, expected_valid: testCase.expected_valid, passed, observed }; }); -const passed = results.every(r => r.passed); +const passed = results.every((result) => result.passed); console.log(JSON.stringify({ schema: challenge.result_contract?.schema || 'aml-verifier-conformance-result/1', prototype: true, challenge_schema: challenge.schema, challenge_sha256: challengeSha256, + challenge_cases_sha256: challengeCasesSha256, witness_vector_sha256: witnessVectorSha256, harness_root: repoRoot, + challenge_cases: challenge.cases_file, command: [command, ...baseArgs], passed, results, - claim_boundary: 'PASS is project-defined black-box compatibility evidence bound to the exact published challenge and witness-vector bytes; it is not certification or proof of verifier independence.' + claim_boundary: 'PASS is project-defined black-box compatibility evidence bound to the exact published challenge, case corpus, and witness-vector bytes; it is not certification or proof of verifier independence.' }, null, 2)); process.exit(passed ? 0 : 1); diff --git a/test/contract-evolution.test.js b/test/contract-evolution.test.js index 295175a8..830332a9 100644 --- a/test/contract-evolution.test.js +++ b/test/contract-evolution.test.js @@ -5,45 +5,48 @@ import fs from 'node:fs'; const read = (path) => JSON.parse(fs.readFileSync(path, 'utf8')); const SNAPSHOT_1 = 'aml-verifier-contract-2026-09-08-01'; const SNAPSHOT_2 = 'aml-verifier-contract-2026-09-09-01'; -const MIGRATION = 'protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json'; +const SNAPSHOT_3 = 'aml-verifier-contract-2026-09-14-01'; +const MIGRATION_1_2 = 'protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json'; +const MIGRATION_2_3 = 'protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json'; -test('verification contract catalog preserves Snapshot 1 and advances explicitly to Snapshot 2', () => { +test('verification contract catalog preserves history and advances explicitly to Snapshot 3', () => { const catalog = read('protocol/verification-contract-catalog.json'); assert.equal(catalog.schema, 'aml-verification-contract-catalog/1'); - assert.equal(catalog.snapshots.length, 2); - assert.equal(catalog.migrations.length, 1); - assert.equal(catalog.current_snapshot, SNAPSHOT_2); + assert.equal(catalog.snapshots.length, 3); + assert.equal(catalog.migrations.length, 2); + assert.equal(catalog.current_snapshot, SNAPSHOT_3); const first = catalog.snapshots.find((entry) => entry.snapshot_id === SNAPSHOT_1); const second = catalog.snapshots.find((entry) => entry.snapshot_id === SNAPSHOT_2); + const third = catalog.snapshots.find((entry) => entry.snapshot_id === SNAPSHOT_3); assert.ok(first); assert.ok(second); + assert.ok(third); assert.equal(first.manifest, 'protocol/verification-contract-v1.json'); assert.equal(first.superseded, true); assert.equal(second.manifest, 'protocol/verification-contract-v2.json'); - assert.equal(second.superseded, false); - assert.deepEqual(catalog.migrations, [MIGRATION]); + assert.equal(second.superseded, true); + assert.equal(third.manifest, 'protocol/verification-contract-v3.json'); + assert.equal(third.source_commit, '8c991746a1eb68325d85edc430db06200cfef30d'); + assert.equal(third.superseded, false); + assert.deepEqual(catalog.migrations, [MIGRATION_1_2, MIGRATION_2_3]); }); -test('verification contract lineage has one acyclic predecessor edge into Snapshot 2', () => { +test('verification contract lineage has one acyclic predecessor edge for each later snapshot', () => { const lineage = read('protocol/verification-contract-lineage.json'); assert.equal(lineage.schema, 'aml-verification-contract-lineage/1'); - assert.equal(lineage.nodes.length, 2); - assert.equal(lineage.edges.length, 1); - assert.deepEqual( - lineage.nodes.map((node) => node.snapshot_id), - [SNAPSHOT_1, SNAPSHOT_2] - ); - assert.deepEqual(lineage.edges[0], { - from_snapshot: SNAPSHOT_1, - to_snapshot: SNAPSHOT_2, - migration: MIGRATION - }); - assert.notEqual(lineage.edges[0].from_snapshot, lineage.edges[0].to_snapshot); + assert.equal(lineage.nodes.length, 3); + assert.equal(lineage.edges.length, 2); + assert.deepEqual(lineage.nodes.map((node) => node.snapshot_id), [SNAPSHOT_1, SNAPSHOT_2, SNAPSHOT_3]); + assert.deepEqual(lineage.edges, [ + { from_snapshot: SNAPSHOT_1, to_snapshot: SNAPSHOT_2, migration: MIGRATION_1_2 }, + { from_snapshot: SNAPSHOT_2, to_snapshot: SNAPSHOT_3, migration: MIGRATION_2_3 } + ]); + for (const edge of lineage.edges) assert.notEqual(edge.from_snapshot, edge.to_snapshot); }); test('Snapshot 2 migration is explicit, breaking, and preserves Snapshot 1 historical meaning', () => { - const migration = read(MIGRATION); + const migration = read(MIGRATION_1_2); assert.equal(migration.schema, 'aml-verification-contract-migration/1'); assert.equal(migration.from_snapshot, SNAPSHOT_1); assert.equal(migration.to_snapshot, SNAPSHOT_2); @@ -54,20 +57,39 @@ test('Snapshot 2 migration is explicit, breaking, and preserves Snapshot 1 histo ]); assert.equal(migration.historical_artifacts.old_snapshot_verification_required, true); assert.equal(migration.historical_artifacts.new_snapshot_may_reinterpret_old_artifacts, false); - assert.ok(migration.behavior_changes.some((change) => change.compatibility === 'incompatible')); +}); + +test('Snapshot 3 migration makes challenge cases language-neutral without rewriting Snapshot 2', () => { + const migration = read(MIGRATION_2_3); + assert.equal(migration.schema, 'aml-verification-contract-migration/1'); + assert.equal(migration.from_snapshot, SNAPSHOT_2); + assert.equal(migration.to_snapshot, SNAPSHOT_3); + assert.equal(migration.classification, 'breaking'); + assert.deepEqual(migration.changed_locked_paths, [ + 'conformance/verifier-challenge.json', + 'scripts/run-verifier-conformance.mjs' + ]); + assert.equal(migration.historical_artifacts.old_snapshot_verification_required, true); + assert.equal(migration.historical_artifacts.new_snapshot_may_reinterpret_old_artifacts, false); + assert.ok(migration.behavior_changes.some((change) => change.area === 'verifier challenge case definition')); assert.ok(migration.behavior_changes.some((change) => change.compatibility === 'compatible')); }); -test('Snapshot 1 remains an immutable historical target while Snapshot 2 adds evidence bindings', () => { +test('all three snapshots remain immutable historical targets with progressive evidence guarantees', () => { const first = read('protocol/verification-contract-v1.json'); const second = read('protocol/verification-contract-v2.json'); + const third = read('protocol/verification-contract-v3.json'); assert.equal(first.snapshot_id, SNAPSHOT_1); assert.equal(second.snapshot_id, SNAPSHOT_2); + assert.equal(third.snapshot_id, SNAPSHOT_3); assert.notEqual(first.source_commit, second.source_commit); + assert.notEqual(second.source_commit, third.source_commit); assert.ok(!first.locked_paths.includes('conformance/verifier-challenge.json')); assert.ok(second.locked_paths.includes('conformance/verifier-challenge.json')); - assert.ok(second.locked_paths.includes('scripts/verify-verifier-conformance-result.mjs')); - assert.ok(second.required_behavior.some((behavior) => behavior.includes('exact external verifier challenge bytes'))); + assert.ok(!second.locked_paths.includes('conformance/verifier-challenge-cases.json')); + assert.ok(third.locked_paths.includes('conformance/verifier-challenge-cases.json')); + assert.ok(third.required_behavior.some((behavior) => behavior.includes('language-neutral'))); + assert.ok(third.required_behavior.some((behavior) => behavior.includes('case-corpus bytes'))); }); test('migration schema preserves historical snapshot meaning', () => { diff --git a/test/external-verifier-action.test.js b/test/external-verifier-action.test.js index 8dba7847..ce5e6d03 100644 --- a/test/external-verifier-action.test.js +++ b/test/external-verifier-action.test.js @@ -3,13 +3,18 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; const challenge = JSON.parse(fs.readFileSync('conformance/verifier-challenge.json', 'utf8')); +const cases = JSON.parse(fs.readFileSync('conformance/verifier-challenge-cases.json', 'utf8')); const harness = fs.readFileSync('scripts/run-verifier-conformance.mjs', 'utf8'); const action = fs.readFileSync('actions/verifier-conformance/action.yml', 'utf8'); -test('external verifier challenge publishes the exact harness cases', () => { +test('external verifier challenge publishes a language-neutral exact case corpus', () => { assert.equal(challenge.schema, 'aml-external-verifier-challenge/1'); + assert.equal(challenge.cases_file, 'conformance/verifier-challenge-cases.json'); + assert.equal(cases.schema, 'aml-verifier-challenge-cases/1'); + assert.equal(cases.bundle_source, challenge.witness_vector); + assert.equal(cases.mutation_language.schema, 'aml-json-pointer-replace/1'); assert.deepEqual( - challenge.cases.map((entry) => [entry.id, entry.expected_valid]), + cases.cases.map((entry) => [entry.id, entry.expected_valid]), [ ['golden-valid', true], ['tampered-purpose', false], @@ -17,9 +22,27 @@ test('external verifier challenge publishes the exact harness cases', () => { ['expired-challenge', false] ] ); - for (const entry of challenge.cases) { - assert.match(harness, new RegExp(`id: ['\"]${entry.id}['\"]`)); - } + assert.deepEqual(cases.cases[1].mutations, [{ + op: 'replace', + path: '/evidence/receipt/decisions/0/purpose', + value: 'tampered-by-conformance-harness' + }]); + assert.deepEqual(cases.cases[2].mutations, [{ + op: 'replace', + path: '/challenge/nonce', + value: 'tampered-challenge-nonce-000000000000000000000' + }]); +}); + +test('external verifier harness consumes the case corpus and retains exact hash bindings', () => { + assert.match(harness, /challenge\.cases_file/); + assert.match(harness, /casesContract\.cases\.map/); + assert.match(harness, /aml-json-pointer-replace\/1/); + assert.match(harness, /challenge_sha256/); + assert.match(harness, /challenge_cases_sha256/); + assert.match(harness, /witness_vector_sha256/); + assert.doesNotMatch(harness, /purposeTamper/); + assert.doesNotMatch(harness, /challengeTamper/); }); test('external verifier action drives the canonical black-box harness', () => { diff --git a/test/external-verifier-kit.test.js b/test/external-verifier-kit.test.js index 7eb0d32e..9267f52b 100644 --- a/test/external-verifier-kit.test.js +++ b/test/external-verifier-kit.test.js @@ -9,28 +9,32 @@ import { checkExternalVerifierKit } from '../scripts/check-external-verifier-kit const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex'); -test('external verifier kit is reference-code-free, Snapshot 2 aware, and self-consistent', () => { +test('external verifier kit is reference-code-free, current-snapshot aware, and self-consistent', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aml-verifier-kit-')); try { const output = path.join(root, 'kit'); const built = buildExternalVerifierKit(output); const checked = checkExternalVerifierKit(output); + const catalog = JSON.parse(fs.readFileSync('protocol/verification-contract-catalog.json', 'utf8')); assert.equal(checked.valid, true, JSON.stringify(checked.failures)); assert.equal(built.manifest.reference_code_included, false); - assert.equal(built.manifest.current_contract_snapshot_id, 'aml-verifier-contract-2026-09-09-01'); - assert.equal(built.manifest.contract_snapshot_count, 2); - assert.equal(built.manifest.contract_migration_count, 1); + assert.equal(built.manifest.current_contract_snapshot_id, catalog.current_snapshot); + assert.equal(built.manifest.contract_snapshot_count, catalog.snapshots.length); + assert.equal(built.manifest.contract_migration_count, catalog.migrations.length); assert.equal(built.manifest.challenge_sha256, sha256(fs.readFileSync('conformance/verifier-challenge.json'))); assert.equal(built.manifest.witness_vector_sha256, sha256(fs.readFileSync('independent/python/witness-vector.json'))); for (const required of [ 'conformance/verifier-challenge.json', + 'conformance/verifier-challenge-cases.json', 'independent/python/witness-vector.json', 'protocol/sorted-json-v1.md', 'protocol/verification-contract-v1.json', 'protocol/verification-contract-v2.json', + 'protocol/verification-contract-v3.json', 'protocol/verification-contract-catalog.json', 'protocol/verification-contract-lineage.json', - 'protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json' + 'protocol/migrations/aml-verifier-contract-2026-09-08-01_to_2026-09-09-01.json', + 'protocol/migrations/aml-verifier-contract-2026-09-09-01_to_2026-09-14-01.json' ]) { assert.ok(built.manifest.files.some((entry) => entry.path === required), required); }