From 684f8fdee15e8800770570b9a33ce02ed2a18786 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Mon, 2 Nov 2026 13:48:41 +0000 Subject: [PATCH] feat(conformance): TC-L4-01..05 Layer 4 audit trail validators (RFC-0005) Co-authored-by: emmanuel-a-otchere --- src/contracts/layer4-integrity.ts | 138 +++++++++++++++++++++ src/fixtures/l4-conformant.json | 171 +++++++++++++++++++++++++ src/fixtures/l4-dangling-event.json | 171 +++++++++++++++++++++++++ src/fixtures/l4-legacy.json | 157 +++++++++++++++++++++++ src/fixtures/l4-non-append-only.json | 178 +++++++++++++++++++++++++++ src/fixtures/l4-rerun-unlinked.json | 171 +++++++++++++++++++++++++ src/runner.ts | 8 +- tests/layer4.test.ts | 66 ++++++++++ 8 files changed, 1059 insertions(+), 1 deletion(-) create mode 100644 src/contracts/layer4-integrity.ts create mode 100644 src/fixtures/l4-conformant.json create mode 100644 src/fixtures/l4-dangling-event.json create mode 100644 src/fixtures/l4-legacy.json create mode 100644 src/fixtures/l4-non-append-only.json create mode 100644 src/fixtures/l4-rerun-unlinked.json create mode 100644 tests/layer4.test.ts diff --git a/src/contracts/layer4-integrity.ts b/src/contracts/layer4-integrity.ts new file mode 100644 index 0000000..1086489 --- /dev/null +++ b/src/contracts/layer4-integrity.ts @@ -0,0 +1,138 @@ +/** + * Layer 4 (Audit Trail) integrity validators. Implements TC-L4-01..05 per + * RFC-0005: the audit trail is lifecycle-complete and append-only, not + * merely present. + * + * See: https://github.com/Source-Engineering/source-engineering-specifications/blob/main/compatibility-test-suite.md + */ + +import type { Cots, AuditEntry } from "../types.js"; +import { type Result, combine, fail, warn } from "../result.js"; + +const URN_RE = /^[a-z][a-z0-9+\-.]*:/i; + +/** TC-L4-01: every decision's audit trail starts with "created" and every entry is complete. */ +export const validateAuditTrailComplete = (cots: Cots): Result => { + const errors: Result[] = []; + + for (let i = 0; i < cots.decisions.length; i++) { + const history = cots.decisions[i].audit_history; + if (history.length === 0) { + errors.push(fail("TC-L4-01", `decisions[${i}].audit_history is empty (must start with "created")`)); + continue; + } + if (history[0].action !== "created") { + errors.push(fail("TC-L4-01", `decisions[${i}].audit_history[0] is not "created" (got "${history[0].action}")`)); + } + for (let j = 0; j < history.length; j++) { + const e: AuditEntry = history[j]; + if (!e.id || !URN_RE.test(e.id)) { + errors.push(fail("TC-L4-01", `decisions[${i}].audit_history[${j}].id is missing or not a valid URI ('${e.id}')`)); + } + if (!e.action) { + errors.push(fail("TC-L4-01", `decisions[${i}].audit_history[${j}].action is empty`)); + } + if (!e.performed_by) { + errors.push(fail("TC-L4-01", `decisions[${i}].audit_history[${j}].performed_by is empty`)); + } + if (Number.isNaN(Date.parse(e.performed_at))) { + errors.push(fail("TC-L4-01", `decisions[${i}].audit_history[${j}].performed_at is not a valid ISO-8601 timestamp ('${e.performed_at}')`)); + } + } + } + + return combine(...errors); +}; + +/** TC-L4-02: append-only verifiable: unique entry ids and monotonic time order. */ +export const validateAuditTrailAppendOnly = (cots: Cots): Result => { + const errors: Result[] = []; + + for (let i = 0; i < cots.decisions.length; i++) { + const history = cots.decisions[i].audit_history; + const seen = new Set(); + for (let j = 0; j < history.length; j++) { + const e = history[j]; + if (seen.has(e.id)) { + errors.push(fail("TC-L4-02", `decisions[${i}].audit_history contains duplicate entry id '${e.id}' (append-only violated)`)); + } + seen.add(e.id); + if (j > 0) { + const prev = Date.parse(history[j - 1].performed_at); + const curr = Date.parse(e.performed_at); + if (!Number.isNaN(prev) && !Number.isNaN(curr) && curr < prev) { + errors.push(fail("TC-L4-02", `decisions[${i}].audit_history[${j}] is earlier than its predecessor (append-only violated: ${e.performed_at} < ${history[j - 1].performed_at})`)); + } + } + } + } + + return combine(...errors); +}; + +/** TC-L4-03: every non-"created" entry names a decision that exists in the COTS. */ +export const validateLifecycleEventsResolve = (cots: Cots): Result => { + const errors: Result[] = []; + const decisionIds = new Set(cots.decisions.map((d) => d.id)); + + for (let i = 0; i < cots.decisions.length; i++) { + for (let j = 0; j < cots.decisions[i].audit_history.length; j++) { + const e = cots.decisions[i].audit_history[j]; + if (e.action === "created") continue; + const m = e.notes.match(/decision_id=(\S+)/); + if (!m) { + errors.push(fail("TC-L4-03", `decisions[${i}].audit_history[${j}] (${e.action}) does not name a decision_id; the lifecycle event dangles`)); + } else if (!decisionIds.has(m[1])) { + errors.push(fail("TC-L4-03", `decisions[${i}].audit_history[${j}] references unknown decision '${m[1]}'`)); + } + } + } + + return combine(...errors); +}; + +/** TC-L4-04: rerun entries link to the original decision; superseded entries name the superseding decision. */ +export const validateLifecycleLinkage = (cots: Cots): Result => { + const errors: Result[] = []; + const decisionIds = new Set(cots.decisions.map((d) => d.id)); + + for (let i = 0; i < cots.decisions.length; i++) { + for (let j = 0; j < cots.decisions[i].audit_history.length; j++) { + const e = cots.decisions[i].audit_history[j]; + if (e.action === "rerun") { + const m = e.notes.match(/rerun_of=(\S+)/); + if (!m) { + errors.push(fail("TC-L4-04", `decisions[${i}].audit_history[${j}] is a rerun without rerun_of linkage`)); + } else if (!decisionIds.has(m[1])) { + errors.push(fail("TC-L4-04", `decisions[${i}].audit_history[${j}] rerun_of '${m[1]}' does not resolve to a decision in the COTS`)); + } + } + if (e.action === "superseded") { + const m = e.notes.match(/superseded_by=(\S+)/); + if (!m) { + errors.push(fail("TC-L4-04", `decisions[${i}].audit_history[${j}] is superseded without superseded_by linkage`)); + } else if (!decisionIds.has(m[1])) { + errors.push(fail("TC-L4-04", `decisions[${i}].audit_history[${j}] superseded_by '${m[1]}' does not resolve to a decision in the COTS`)); + } + } + } + } + + return combine(...errors); +}; + +/** TC-L4-05: a pre-RFC-0005 COTS with a created-only history passes with a legacy warning. */ +export const validateLegacyAuditTrail = (cots: Cots): Result => { + const warnings: Result[] = []; + + for (let i = 0; i < cots.decisions.length; i++) { + const history = cots.decisions[i].audit_history; + if (history.length === 1 && history[0].action === "created") { + warnings.push( + warn("TC-L4-05", `decisions[${i}] has a created-only audit history (pre-RFC-0005 producer; no lifecycle events recorded)`) + ); + } + } + + return combine(...warnings); +}; diff --git a/src/fixtures/l4-conformant.json b/src/fixtures/l4-conformant.json new file mode 100644 index 0000000..bc02869 --- /dev/null +++ b/src/fixtures/l4-conformant.json @@ -0,0 +1,171 @@ +{ + "cots_version": "0.1.0", + "cots_id": "urn:source-engineering:cots:00000000-0000-0000-0000-000000000001", + "produced_at": "2026-10-11T12:00:00Z", + "produced_by": { + "system_name": "test-fixture", + "system_version": "0.1.0" + }, + "inputs": { + "source_artifacts": [ + { + "id": "urn:source-engineering:source:contract-2026-001", + "origin": { + "system_name": "Acme Contracts", + "system_version": "1.0.0", + "configuration_hash": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "version": { + "content_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "issued_at": "2026-10-10T08:00:00Z" + }, + "authority": { + "asserted_by": "acme-contracts", + "trust_score": 0.95 + }, + "temporal_validity": { + "valid_from": "2026-10-10T08:00:00Z", + "valid_until": null + }, + "content_reference": { + "uri": "https://example.com/contracts/2026-001.pdf", + "media_type": "application/pdf" + } + } + ] + }, + "context": { + "model": { + "model_name": "claude-sonnet-4-5", + "model_version": "2026-09-15" + }, + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "prompt_cache_id": null, + "tool_budget": 8, + "retrieval_depth": 5 + }, + "constraints": [ + { + "id": "urn:source-engineering:constraint:jurisdiction-001", + "type": "jurisdiction", + "scope": "all_decisions", + "value": "US-NY" + } + ] + }, + "decisions": [ + { + "id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "decision_text": "The contract is a standard commercial services agreement with mutual indemnification, a 30-day termination notice, and a New York governing-law clause.", + "basis_id": "urn:source-engineering:basis:00000000-0000-0000-0000-000000000001", + "evidence_chain_ref": "urn:source-engineering:evidence-chain:00000000-0000-0000-0000-000000000001", + "validation_status": { + "status": "pending", + "last_changed_at": "2026-10-11T12:00:00Z", + "last_changed_by": "test-fixture" + }, + "responsible_authority": { + "authority_id": "claude-sonnet-4-5", + "authority_type": "automated" + }, + "audit_history": [ + { + "id": "urn:source-engineering:audit:00000000-0000-0000-0000-000000000001", + "action": "created", + "performed_by": "test-fixture", + "performed_at": "2026-10-11T12:00:00Z", + "notes": "Initial creation." + }, + { + "id": "urn:source-engineering:audit:l4-002", + "action": "appealed", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T09:00:00Z", + "notes": "User appealed. decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + }, + { + "id": "urn:source-engineering:audit:l4-003", + "action": "rerun", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T10:00:00Z", + "notes": "Re-run on model v2. rerun_of=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001 decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + } + ], + "created_at": "2026-10-11T12:00:00Z" + } + ], + "evidence": { + "evidence_chain": [ + { + "decision_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "supporting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "extract_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-7" + ], + "output": "Section 7.1: Each party shall indemnify and hold harmless...", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.92, + "basis": "Direct extraction from the section 7.1 text." + }, + "polarity": "supports" + } + ], + "contradicting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "search_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-12" + ], + "output": "Section 12.4: This agreement shall be governed by the laws of the State of Delaware.", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.78, + "basis": "Contradictory governing-law clause in section 12.4 (the model rejected it because section 14.1 is the prevailing clause)." + }, + "polarity": "contradicts" + } + ], + "claims": [ + { + "id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "decision_record_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "claim_text": "The contract contains a mutual indemnification clause in section 7.1.", + "claim_type": "factual", + "supporting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001" + ], + "contradicting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002" + ] + } + ] + } + ] + }, + "validation": { + "feedback_surfaces": [ + { + "id": "urn:source-engineering:surface:00000000-0000-0000-0000-000000000001", + "type": "appeal", + "target": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "endpoint": "https://example.com/appeal/00000000-0000-0000-0000-000000000001", + "description": "File an appeal against this decision." + } + ] + } +} \ No newline at end of file diff --git a/src/fixtures/l4-dangling-event.json b/src/fixtures/l4-dangling-event.json new file mode 100644 index 0000000..329af9f --- /dev/null +++ b/src/fixtures/l4-dangling-event.json @@ -0,0 +1,171 @@ +{ + "cots_version": "0.1.0", + "cots_id": "urn:source-engineering:cots:00000000-0000-0000-0000-000000000001", + "produced_at": "2026-10-11T12:00:00Z", + "produced_by": { + "system_name": "test-fixture", + "system_version": "0.1.0" + }, + "inputs": { + "source_artifacts": [ + { + "id": "urn:source-engineering:source:contract-2026-001", + "origin": { + "system_name": "Acme Contracts", + "system_version": "1.0.0", + "configuration_hash": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "version": { + "content_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "issued_at": "2026-10-10T08:00:00Z" + }, + "authority": { + "asserted_by": "acme-contracts", + "trust_score": 0.95 + }, + "temporal_validity": { + "valid_from": "2026-10-10T08:00:00Z", + "valid_until": null + }, + "content_reference": { + "uri": "https://example.com/contracts/2026-001.pdf", + "media_type": "application/pdf" + } + } + ] + }, + "context": { + "model": { + "model_name": "claude-sonnet-4-5", + "model_version": "2026-09-15" + }, + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "prompt_cache_id": null, + "tool_budget": 8, + "retrieval_depth": 5 + }, + "constraints": [ + { + "id": "urn:source-engineering:constraint:jurisdiction-001", + "type": "jurisdiction", + "scope": "all_decisions", + "value": "US-NY" + } + ] + }, + "decisions": [ + { + "id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "decision_text": "The contract is a standard commercial services agreement with mutual indemnification, a 30-day termination notice, and a New York governing-law clause.", + "basis_id": "urn:source-engineering:basis:00000000-0000-0000-0000-000000000001", + "evidence_chain_ref": "urn:source-engineering:evidence-chain:00000000-0000-0000-0000-000000000001", + "validation_status": { + "status": "pending", + "last_changed_at": "2026-10-11T12:00:00Z", + "last_changed_by": "test-fixture" + }, + "responsible_authority": { + "authority_id": "claude-sonnet-4-5", + "authority_type": "automated" + }, + "audit_history": [ + { + "id": "urn:source-engineering:audit:00000000-0000-0000-0000-000000000001", + "action": "created", + "performed_by": "test-fixture", + "performed_at": "2026-10-11T12:00:00Z", + "notes": "Initial creation." + }, + { + "id": "urn:source-engineering:audit:l4-002", + "action": "appealed", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T09:00:00Z", + "notes": "User appealed. decision_id=urn:source-engineering:decision:nope" + }, + { + "id": "urn:source-engineering:audit:l4-003", + "action": "rerun", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T10:00:00Z", + "notes": "Re-run on model v2. rerun_of=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001 decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + } + ], + "created_at": "2026-10-11T12:00:00Z" + } + ], + "evidence": { + "evidence_chain": [ + { + "decision_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "supporting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "extract_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-7" + ], + "output": "Section 7.1: Each party shall indemnify and hold harmless...", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.92, + "basis": "Direct extraction from the section 7.1 text." + }, + "polarity": "supports" + } + ], + "contradicting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "search_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-12" + ], + "output": "Section 12.4: This agreement shall be governed by the laws of the State of Delaware.", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.78, + "basis": "Contradictory governing-law clause in section 12.4 (the model rejected it because section 14.1 is the prevailing clause)." + }, + "polarity": "contradicts" + } + ], + "claims": [ + { + "id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "decision_record_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "claim_text": "The contract contains a mutual indemnification clause in section 7.1.", + "claim_type": "factual", + "supporting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001" + ], + "contradicting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002" + ] + } + ] + } + ] + }, + "validation": { + "feedback_surfaces": [ + { + "id": "urn:source-engineering:surface:00000000-0000-0000-0000-000000000001", + "type": "appeal", + "target": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "endpoint": "https://example.com/appeal/00000000-0000-0000-0000-000000000001", + "description": "File an appeal against this decision." + } + ] + } +} \ No newline at end of file diff --git a/src/fixtures/l4-legacy.json b/src/fixtures/l4-legacy.json new file mode 100644 index 0000000..6c1e28a --- /dev/null +++ b/src/fixtures/l4-legacy.json @@ -0,0 +1,157 @@ +{ + "cots_version": "0.1.0", + "cots_id": "urn:source-engineering:cots:00000000-0000-0000-0000-000000000001", + "produced_at": "2026-10-11T12:00:00Z", + "produced_by": { + "system_name": "test-fixture", + "system_version": "0.1.0" + }, + "inputs": { + "source_artifacts": [ + { + "id": "urn:source-engineering:source:contract-2026-001", + "origin": { + "system_name": "Acme Contracts", + "system_version": "1.0.0", + "configuration_hash": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "version": { + "content_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "issued_at": "2026-10-10T08:00:00Z" + }, + "authority": { + "asserted_by": "acme-contracts", + "trust_score": 0.95 + }, + "temporal_validity": { + "valid_from": "2026-10-10T08:00:00Z", + "valid_until": null + }, + "content_reference": { + "uri": "https://example.com/contracts/2026-001.pdf", + "media_type": "application/pdf" + } + } + ] + }, + "context": { + "model": { + "model_name": "claude-sonnet-4-5", + "model_version": "2026-09-15" + }, + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "prompt_cache_id": null, + "tool_budget": 8, + "retrieval_depth": 5 + }, + "constraints": [ + { + "id": "urn:source-engineering:constraint:jurisdiction-001", + "type": "jurisdiction", + "scope": "all_decisions", + "value": "US-NY" + } + ] + }, + "decisions": [ + { + "id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "decision_text": "The contract is a standard commercial services agreement with mutual indemnification, a 30-day termination notice, and a New York governing-law clause.", + "basis_id": "urn:source-engineering:basis:00000000-0000-0000-0000-000000000001", + "evidence_chain_ref": "urn:source-engineering:evidence-chain:00000000-0000-0000-0000-000000000001", + "validation_status": { + "status": "pending", + "last_changed_at": "2026-10-11T12:00:00Z", + "last_changed_by": "test-fixture" + }, + "responsible_authority": { + "authority_id": "claude-sonnet-4-5", + "authority_type": "automated" + }, + "audit_history": [ + { + "id": "urn:source-engineering:audit:00000000-0000-0000-0000-000000000001", + "action": "created", + "performed_by": "test-fixture", + "performed_at": "2026-10-11T12:00:00Z", + "notes": "Initial creation." + } + ], + "created_at": "2026-10-11T12:00:00Z" + } + ], + "evidence": { + "evidence_chain": [ + { + "decision_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "supporting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "extract_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-7" + ], + "output": "Section 7.1: Each party shall indemnify and hold harmless...", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.92, + "basis": "Direct extraction from the section 7.1 text." + }, + "polarity": "supports" + } + ], + "contradicting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "search_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-12" + ], + "output": "Section 12.4: This agreement shall be governed by the laws of the State of Delaware.", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.78, + "basis": "Contradictory governing-law clause in section 12.4 (the model rejected it because section 14.1 is the prevailing clause)." + }, + "polarity": "contradicts" + } + ], + "claims": [ + { + "id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "decision_record_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "claim_text": "The contract contains a mutual indemnification clause in section 7.1.", + "claim_type": "factual", + "supporting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001" + ], + "contradicting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002" + ] + } + ] + } + ] + }, + "validation": { + "feedback_surfaces": [ + { + "id": "urn:source-engineering:surface:00000000-0000-0000-0000-000000000001", + "type": "appeal", + "target": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "endpoint": "https://example.com/appeal/00000000-0000-0000-0000-000000000001", + "description": "File an appeal against this decision." + } + ] + } +} \ No newline at end of file diff --git a/src/fixtures/l4-non-append-only.json b/src/fixtures/l4-non-append-only.json new file mode 100644 index 0000000..0d2dbd8 --- /dev/null +++ b/src/fixtures/l4-non-append-only.json @@ -0,0 +1,178 @@ +{ + "cots_version": "0.1.0", + "cots_id": "urn:source-engineering:cots:00000000-0000-0000-0000-000000000001", + "produced_at": "2026-10-11T12:00:00Z", + "produced_by": { + "system_name": "test-fixture", + "system_version": "0.1.0" + }, + "inputs": { + "source_artifacts": [ + { + "id": "urn:source-engineering:source:contract-2026-001", + "origin": { + "system_name": "Acme Contracts", + "system_version": "1.0.0", + "configuration_hash": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "version": { + "content_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "issued_at": "2026-10-10T08:00:00Z" + }, + "authority": { + "asserted_by": "acme-contracts", + "trust_score": 0.95 + }, + "temporal_validity": { + "valid_from": "2026-10-10T08:00:00Z", + "valid_until": null + }, + "content_reference": { + "uri": "https://example.com/contracts/2026-001.pdf", + "media_type": "application/pdf" + } + } + ] + }, + "context": { + "model": { + "model_name": "claude-sonnet-4-5", + "model_version": "2026-09-15" + }, + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "prompt_cache_id": null, + "tool_budget": 8, + "retrieval_depth": 5 + }, + "constraints": [ + { + "id": "urn:source-engineering:constraint:jurisdiction-001", + "type": "jurisdiction", + "scope": "all_decisions", + "value": "US-NY" + } + ] + }, + "decisions": [ + { + "id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "decision_text": "The contract is a standard commercial services agreement with mutual indemnification, a 30-day termination notice, and a New York governing-law clause.", + "basis_id": "urn:source-engineering:basis:00000000-0000-0000-0000-000000000001", + "evidence_chain_ref": "urn:source-engineering:evidence-chain:00000000-0000-0000-0000-000000000001", + "validation_status": { + "status": "pending", + "last_changed_at": "2026-10-11T12:00:00Z", + "last_changed_by": "test-fixture" + }, + "responsible_authority": { + "authority_id": "claude-sonnet-4-5", + "authority_type": "automated" + }, + "audit_history": [ + { + "id": "urn:source-engineering:audit:00000000-0000-0000-0000-000000000001", + "action": "created", + "performed_by": "test-fixture", + "performed_at": "2026-10-11T12:00:00Z", + "notes": "Initial creation." + }, + { + "id": "urn:source-engineering:audit:l4-002", + "action": "appealed", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T09:00:00Z", + "notes": "User appealed. decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + }, + { + "id": "urn:source-engineering:audit:l4-003", + "action": "rerun", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T10:00:00Z", + "notes": "Re-run on model v2. rerun_of=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001 decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + }, + { + "id": "urn:source-engineering:audit:l4-002", + "action": "appealed", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T09:00:00Z", + "notes": "User appealed. decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + } + ], + "created_at": "2026-10-11T12:00:00Z" + } + ], + "evidence": { + "evidence_chain": [ + { + "decision_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "supporting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "extract_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-7" + ], + "output": "Section 7.1: Each party shall indemnify and hold harmless...", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.92, + "basis": "Direct extraction from the section 7.1 text." + }, + "polarity": "supports" + } + ], + "contradicting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "search_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-12" + ], + "output": "Section 12.4: This agreement shall be governed by the laws of the State of Delaware.", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.78, + "basis": "Contradictory governing-law clause in section 12.4 (the model rejected it because section 14.1 is the prevailing clause)." + }, + "polarity": "contradicts" + } + ], + "claims": [ + { + "id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "decision_record_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "claim_text": "The contract contains a mutual indemnification clause in section 7.1.", + "claim_type": "factual", + "supporting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001" + ], + "contradicting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002" + ] + } + ] + } + ] + }, + "validation": { + "feedback_surfaces": [ + { + "id": "urn:source-engineering:surface:00000000-0000-0000-0000-000000000001", + "type": "appeal", + "target": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "endpoint": "https://example.com/appeal/00000000-0000-0000-0000-000000000001", + "description": "File an appeal against this decision." + } + ] + } +} \ No newline at end of file diff --git a/src/fixtures/l4-rerun-unlinked.json b/src/fixtures/l4-rerun-unlinked.json new file mode 100644 index 0000000..8728177 --- /dev/null +++ b/src/fixtures/l4-rerun-unlinked.json @@ -0,0 +1,171 @@ +{ + "cots_version": "0.1.0", + "cots_id": "urn:source-engineering:cots:00000000-0000-0000-0000-000000000001", + "produced_at": "2026-10-11T12:00:00Z", + "produced_by": { + "system_name": "test-fixture", + "system_version": "0.1.0" + }, + "inputs": { + "source_artifacts": [ + { + "id": "urn:source-engineering:source:contract-2026-001", + "origin": { + "system_name": "Acme Contracts", + "system_version": "1.0.0", + "configuration_hash": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "version": { + "content_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "issued_at": "2026-10-10T08:00:00Z" + }, + "authority": { + "asserted_by": "acme-contracts", + "trust_score": 0.95 + }, + "temporal_validity": { + "valid_from": "2026-10-10T08:00:00Z", + "valid_until": null + }, + "content_reference": { + "uri": "https://example.com/contracts/2026-001.pdf", + "media_type": "application/pdf" + } + } + ] + }, + "context": { + "model": { + "model_name": "claude-sonnet-4-5", + "model_version": "2026-09-15" + }, + "parameters": { + "temperature": 0.3, + "top_p": 0.95, + "prompt_cache_id": null, + "tool_budget": 8, + "retrieval_depth": 5 + }, + "constraints": [ + { + "id": "urn:source-engineering:constraint:jurisdiction-001", + "type": "jurisdiction", + "scope": "all_decisions", + "value": "US-NY" + } + ] + }, + "decisions": [ + { + "id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "decision_text": "The contract is a standard commercial services agreement with mutual indemnification, a 30-day termination notice, and a New York governing-law clause.", + "basis_id": "urn:source-engineering:basis:00000000-0000-0000-0000-000000000001", + "evidence_chain_ref": "urn:source-engineering:evidence-chain:00000000-0000-0000-0000-000000000001", + "validation_status": { + "status": "pending", + "last_changed_at": "2026-10-11T12:00:00Z", + "last_changed_by": "test-fixture" + }, + "responsible_authority": { + "authority_id": "claude-sonnet-4-5", + "authority_type": "automated" + }, + "audit_history": [ + { + "id": "urn:source-engineering:audit:00000000-0000-0000-0000-000000000001", + "action": "created", + "performed_by": "test-fixture", + "performed_at": "2026-10-11T12:00:00Z", + "notes": "Initial creation." + }, + { + "id": "urn:source-engineering:audit:l4-002", + "action": "appealed", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T09:00:00Z", + "notes": "User appealed. decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + }, + { + "id": "urn:source-engineering:audit:l4-003", + "action": "rerun", + "performed_by": "reviewer-1", + "performed_at": "2026-10-12T10:00:00Z", + "notes": "Re-run. decision_id=urn:source-engineering:decision:00000000-0000-0000-0000-000000000001" + } + ], + "created_at": "2026-10-11T12:00:00Z" + } + ], + "evidence": { + "evidence_chain": [ + { + "decision_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "supporting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "extract_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-7" + ], + "output": "Section 7.1: Each party shall indemnify and hold harmless...", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.92, + "basis": "Direct extraction from the section 7.1 text." + }, + "polarity": "supports" + } + ], + "contradicting_evidence": [ + { + "id": "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002", + "claim_id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "source_id": "urn:source-engineering:source:contract-2026-001", + "transformation": { + "operation": "search_clause", + "inputs": [ + "https://example.com/contracts/2026-001.pdf#section-12" + ], + "output": "Section 12.4: This agreement shall be governed by the laws of the State of Delaware.", + "recorded_at": "2026-10-11T12:00:00Z" + }, + "uncertainty": { + "calibrated_score": 0.78, + "basis": "Contradictory governing-law clause in section 12.4 (the model rejected it because section 14.1 is the prevailing clause)." + }, + "polarity": "contradicts" + } + ], + "claims": [ + { + "id": "urn:source-engineering:claim:00000000-0000-0000-0000-000000000001", + "decision_record_id": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "claim_text": "The contract contains a mutual indemnification clause in section 7.1.", + "claim_type": "factual", + "supporting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000001" + ], + "contradicting_evidence": [ + "urn:source-engineering:evidence:00000000-0000-0000-0000-000000000002" + ] + } + ] + } + ] + }, + "validation": { + "feedback_surfaces": [ + { + "id": "urn:source-engineering:surface:00000000-0000-0000-0000-000000000001", + "type": "appeal", + "target": "urn:source-engineering:decision:00000000-0000-0000-0000-000000000001", + "endpoint": "https://example.com/appeal/00000000-0000-0000-0000-000000000001", + "description": "File an appeal against this decision." + } + ] + } +} \ No newline at end of file diff --git a/src/runner.ts b/src/runner.ts index 6666216..fabb1e9 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -20,6 +20,7 @@ import { validateFiveParts, validateFeedbackSurfaces } from "./contracts/five-pa import { validateLayer2Records } from "./contracts/layer2-integrity.js"; import { validateUrnGrammar, validateChainResolution, validateTemporalOrdering, validateRecursiveChains } from "./contracts/semantic-integrity.js"; import { validateGuardrailDecisions } from "./contracts/layer3-integrity.js"; +import { validateAuditTrailComplete, validateAuditTrailAppendOnly, validateLifecycleEventsResolve, validateLifecycleLinkage, validateLegacyAuditTrail } from "./contracts/layer4-integrity.js"; export const run = (cots: Cots): Result => { return combine( @@ -41,7 +42,12 @@ export const run = (cots: Cots): Result => { validateChainResolution(cots), validateTemporalOrdering(cots), validateRecursiveChains(cots), - validateGuardrailDecisions(cots) + validateGuardrailDecisions(cots), + validateAuditTrailComplete(cots), + validateAuditTrailAppendOnly(cots), + validateLifecycleEventsResolve(cots), + validateLifecycleLinkage(cots), + validateLegacyAuditTrail(cots) ); }; diff --git a/tests/layer4.test.ts b/tests/layer4.test.ts new file mode 100644 index 0000000..d4a4bb9 --- /dev/null +++ b/tests/layer4.test.ts @@ -0,0 +1,66 @@ +/** + * TC-L4 test cases (RFC-0005, Layer 4 audit trail floor). + * + * See: https://github.com/Source-Engineering/source-engineering-specifications/blob/main/compatibility-test-suite.md + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { run } from "../src/runner.js"; +import type { Cots } from "../src/types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES = join(__dirname, "..", "src", "fixtures"); + +const load = (name: string): Cots => { + const p = resolve(FIXTURES, name); + return JSON.parse(readFileSync(p, "utf-8")); +}; + +describe("TC-L4: Layer 4 audit trail (RFC-0005)", () => { + it("TC-L4-01: passes for a complete audit trail", () => { + const result = run(load("l4-conformant.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-L4-01"); + expect(errors).toHaveLength(0); + }); + + it("TC-L4-02: passes for unique ids in monotonic order; fails on duplicates", () => { + const ok = run(load("l4-conformant.json")); + expect(ok.errors.filter((e) => e.test_id === "TC-L4-02")).toHaveLength(0); + const bad = run(load("l4-non-append-only.json")); + const errors = bad.errors.filter((e) => e.test_id === "TC-L4-02"); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain("append-only"); + }); + + it("TC-L4-03: fails when a lifecycle event names an unknown decision", () => { + const result = run(load("l4-dangling-event.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-L4-03"); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain("urn:source-engineering:decision:nope"); + }); + + it("TC-L4-04: fails when a rerun lacks rerun_of linkage to a known decision", () => { + const result = run(load("l4-rerun-unlinked.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-L4-04"); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain("rerun_of"); + }); + + it("TC-L4-05: a created-only history passes with a legacy warning", () => { + const result = run(load("l4-legacy.json")); + expect(result.status).toBe("pass"); + const warnings = result.warnings.filter((w) => w.test_id === "TC-L4-05"); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain("pre-RFC-0005"); + }); + + it("the conformant fixture has no TC-L4 errors end-to-end", () => { + const result = run(load("l4-conformant.json")); + const errors = result.errors.filter((e) => e.test_id.startsWith("TC-L4")); + expect(errors).toHaveLength(0); + }); +});