diff --git a/src/contracts/context-graph-integrity.ts b/src/contracts/context-graph-integrity.ts new file mode 100644 index 0000000..07b8d87 --- /dev/null +++ b/src/contracts/context-graph-integrity.ts @@ -0,0 +1,93 @@ +/** + * Context Graph integrity validators. Implements TC-CG-01 (context + * parameters surfaced), TC-CG-02 (constraints surfaced), TC-CG-03 + * (contextualises edges to decisions), per RFC-0004. + * + * The Context Graph is derived from the COTS alone; these validators check + * the data-level contract that makes the derivation complete: every + * parameter key is present, every constraint is declared, and every + * decision is linked to the context. + * + * See: https://github.com/Source-Engineering/source-engineering-specifications/blob/main/compatibility-test-suite.md + */ + +import type { Cots } from "../types.js"; +import { type Result, combine, fail } from "../result.js"; + +/** TC-CG-01: every context parameter is recorded and therefore surfaceable. */ +export const validateContextParametersSurfaced = (cots: Cots): Result => { + const errors: Result[] = []; + const p = cots.context.parameters; + + const required: Array<[string, unknown]> = [ + ["temperature", p.temperature], + ["top_p", p.top_p], + ["prompt_cache_id", p.prompt_cache_id], + ["tool_budget", p.tool_budget], + ["retrieval_depth", p.retrieval_depth], + ]; + + for (const [key, value] of required) { + if (value === undefined) { + errors.push( + fail("TC-CG-01", `context.parameters.${key} is missing; it cannot be surfaced in the Context Graph`) + ); + } + } + + for (const key of Object.keys(p.additional ?? {})) { + if (p.additional?.[key] === undefined) { + errors.push( + fail("TC-CG-01", `context.parameters.additional.${key} is present but undefined; it cannot be surfaced`) + ); + } + } + + return combine(...errors); +}; + +/** TC-CG-02: every constraint is declared with an id so it can carry a constrains edge. */ +export const validateConstraintsSurfaced = (cots: Cots): Result => { + const errors: Result[] = []; + + for (let i = 0; i < cots.context.constraints.length; i++) { + const c = cots.context.constraints[i]; + if (!c.id || !/^[a-z][a-z0-9+\-.]*:/i.test(c.id)) { + errors.push( + fail("TC-CG-02", `context.constraints[${i}].id is missing or not a valid URI ('${c.id}'); the constraint cannot be surfaced in the Context Graph`) + ); + } + } + + return combine(...errors); +}; + +/** TC-CG-03: every decision is made under the recorded context. */ +export const validateDecisionsContextualised = (cots: Cots): Result => { + const errors: Result[] = []; + + if (cots.decisions.length === 0) { + return combine(); + } + + const paramKeys = Object.keys(cots.context.parameters).filter( + (k) => k !== "additional" + ); + if (paramKeys.length === 0) { + errors.push( + fail("TC-CG-03", "context.parameters has no named parameters; no contextualises edge can be drawn to any decision") + ); + } + + for (let i = 0; i < cots.decisions.length; i++) { + const d = cots.decisions[i]; + const chain = cots.evidence.evidence_chain.find((c) => c.decision_id === d.id); + if (!chain) { + errors.push( + fail("TC-CG-03", `decisions[${i}] (${d.id}) has no evidence chain; the decision cannot be contextualised in the Context Graph`) + ); + } + } + + return combine(...errors); +}; diff --git a/src/fixtures/cg-bad-constraint.json b/src/fixtures/cg-bad-constraint.json new file mode 100644 index 0000000..6438bfe --- /dev/null +++ b/src/fixtures/cg-bad-constraint.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": "not-a-uri", + "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/cg-missing-param.json b/src/fixtures/cg-missing-param.json new file mode 100644 index 0000000..fbaf16f --- /dev/null +++ b/src/fixtures/cg-missing-param.json @@ -0,0 +1,156 @@ +{ + "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": { + "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/cg-no-chain.json b/src/fixtures/cg-no-chain.json new file mode 100644 index 0000000..b5bab55 --- /dev/null +++ b/src/fixtures/cg-no-chain.json @@ -0,0 +1,99 @@ +{ + "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": [] + }, + "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 741ad36..6666216 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -12,6 +12,7 @@ import type { Cots } from "./types.js"; import { type Result, combine } from "./result.js"; import { validateSourceAddresses } from "./contracts/source-integrity.js"; import { validateContextRecorded, validateSourceValidity } from "./contracts/contextual-integrity.js"; +import { validateContextParametersSurfaced, validateConstraintsSurfaced, validateDecisionsContextualised } from "./contracts/context-graph-integrity.js"; import { validateEvidenceChain } from "./contracts/evidential-integrity.js"; import { validateDecisionBasis, validateCounterevidence } from "./contracts/decision-integrity.js"; import { validateAuditHistory, validateAuditHistoryOrdered } from "./contracts/lifecycle-integrity.js"; @@ -25,6 +26,9 @@ export const run = (cots: Cots): Result => { validateSourceAddresses(cots), validateContextRecorded(cots), validateSourceValidity(cots), + validateContextParametersSurfaced(cots), + validateConstraintsSurfaced(cots), + validateDecisionsContextualised(cots), validateEvidenceChain(cots), validateDecisionBasis(cots), validateCounterevidence(cots), diff --git a/tests/context-graph.test.ts b/tests/context-graph.test.ts new file mode 100644 index 0000000..b15fdf4 --- /dev/null +++ b/tests/context-graph.test.ts @@ -0,0 +1,62 @@ +/** + * TC-CG test cases (RFC-0004, context graph surfacing contract). + * + * 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-CG: context graph surfacing (RFC-0004)", () => { + it("TC-CG-01: passes when every context parameter is recorded and surfaceable", () => { + const result = run(load("conformant.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-CG-01"); + expect(errors).toHaveLength(0); + }); + + it("TC-CG-01: fails when a parameter is missing and cannot be surfaced", () => { + const result = run(load("cg-missing-param.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-CG-01"); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain("temperature"); + }); + + it("TC-CG-02: passes when every constraint has a valid id", () => { + const result = run(load("conformant.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-CG-02"); + expect(errors).toHaveLength(0); + }); + + it("TC-CG-02: fails when a constraint id is not a valid URI", () => { + const result = run(load("cg-bad-constraint.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-CG-02"); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain("not-a-uri"); + }); + + it("TC-CG-03: passes when every decision has an evidence chain to contextualise", () => { + const result = run(load("conformant.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-CG-03"); + expect(errors).toHaveLength(0); + }); + + it("TC-CG-03: fails when a decision has no evidence chain", () => { + const result = run(load("cg-no-chain.json")); + const errors = result.errors.filter((e) => e.test_id === "TC-CG-03"); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].message).toContain("contextualised"); + }); +});