diff --git a/README.md b/README.md index eab78c1..a5ed1c4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ se-trace visualize examples/legal-summarization.cots.json -o ./out # Quick conformance check se-trace validate examples/legal-summarization.cots.json + +# Surface the Layer 2 (Runtime Validation) record of a COTS (RFC-0001) +se-trace validate-runtime examples/legal-summarization.cots.json ``` ## What is in this repository @@ -33,7 +36,7 @@ se-trace validate examples/legal-summarization.cots.json - `src/inspect.ts` - the inspect command. Prints a human-readable summary of a COTS. - `src/cite.ts` - the cite command. Prints the evidence chain for a specific decision. - `src/visualize.ts` - the visualize command. Renders a COTS as a Mermaid diagram + HTML page. -- `src/cli/se-trace.ts` - the CLI entry point. +- `src/cli/se-trace.ts` - the CLI entry point, including `validate-runtime` (Layer 2 ValidationFlags surfacing, RFC-0001). - `tests/tools.test.ts` - the test suite. ## SDKs diff --git a/src/cli/se-trace.ts b/src/cli/se-trace.ts index 8e78ead..722b2ad 100644 --- a/src/cli/se-trace.ts +++ b/src/cli/se-trace.ts @@ -1,6 +1,7 @@ /** * The se-trace CLI. Three subcommands: * - validate Run the conformance suite against a COTS. + * - validate-runtime Surface the Layer 2 ValidationFlags of a COTS. * - inspect Print a human-readable summary of a COTS. * - cite Print the evidence chain for a specific decision. * - visualize Render a COTS as a Reasoning Roadmap (Mermaid + HTML). @@ -68,6 +69,40 @@ program } }); +program + .command("validate-runtime") + .description("Surface the Layer 2 (Runtime Validation) record of a COTS (RFC-0001, COTS schema v1.1.0)") + .argument("", "Path to a COTS JSON file") + .action((path: string) => { + const cots = loadCots(path); + let withRecord = 0; + let flagged = 0; + for (const decision of cots.decisions) { + const v = decision.validation; + if (v === undefined) { + console.log(`${decision.id}\n No Layer 2 Validation record (the producer has not floored Layer 2).`); + continue; + } + withRecord++; + console.log(`${decision.id}`); + console.log(` decision_on_validation: ${v.decision_on_validation}`); + console.log(` validated_at: ${v.validated_at} by: ${v.validated_by}`); + if (v.flags.length === 0) { + console.log(" flags: none"); + } else { + if (v.decision_on_validation !== "passed") flagged++; + for (const flag of v.flags) { + console.log(` [${flag.flag_type}] ${flag.message} (detected ${flag.detected_at})`); + } + } + } + console.log(`\nDecisions with a Validation record: ${withRecord}/${cots.decisions.length}`); + if (flagged > 0) { + console.log(`Flagged decisions: ${flagged} (flags are warnings, not rejections; Layer 3 decides)`); + process.exit(2); + } + }); + program .command("inspect") .description("Print a human-readable summary of a COTS") diff --git a/src/cots.ts b/src/cots.ts index ffdc4df..f07aa1e 100644 --- a/src/cots.ts +++ b/src/cots.ts @@ -47,6 +47,27 @@ export interface Constraint { value: string; } +export type FlagType = + | "drift-detected" + | "segment-signal-divergence" + | "parameter-set-out-of-range" + | "temporal-validity-expired"; + +export type DecisionOnValidation = "passed" | "flagged" | "escalated"; + +export interface ValidationFlag { + flag_type: FlagType; + message: string; + detected_at: ISO8601; +} + +export interface Layer2Validation { + flags: ValidationFlag[]; + decision_on_validation: DecisionOnValidation; + validated_at: ISO8601; + validated_by: string; +} + export interface DecisionRecord { id: URI; decision_text: string; @@ -56,6 +77,8 @@ export interface DecisionRecord { responsible_authority: { authority_id: string; authority_type: string }; audit_history: AuditEntry[]; created_at: ISO8601; + /** Layer 2 Validation record (COTS schema v1.1.0, RFC-0001). Optional. */ + validation?: Layer2Validation; } export interface AuditEntry { diff --git a/tests/tools.test.ts b/tests/tools.test.ts index 852a7b3..6dd43f0 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -64,3 +64,45 @@ describe("visualize", () => { expect(html).toContain("Reasoning Roadmap"); }); }); + +describe("validate-runtime", () => { + it("reports a Layer 2 Validation record when present", async () => { + const cots = loadFixture("legal-summarization.cots.json"); + cots.decisions[0].validation = { + flags: [ + { + flag_type: "drift-detected", + message: "Calibration drift detected on segment 'contract-law' (0.14 divergence, threshold 0.10).", + detected_at: "2026-10-11T12:00:00Z", + }, + ], + decision_on_validation: "flagged", + validated_at: "2026-10-11T12:00:00Z", + validated_by: "layer2-runtime-validation", + }; + const { execFileSync } = await import("node:child_process"); + const tmp = resolve(__dirname, "..", "examples", "tmp-validate-runtime.cots.json"); + const { writeFileSync, rmSync } = await import("node:fs"); + writeFileSync(tmp, JSON.stringify(cots)); + try { + try { + execFileSync("node", [resolve(__dirname, "..", "dist", "cli", "se-trace.js"), "validate-runtime", tmp], { encoding: "utf-8" }); + throw new Error("expected exit code 2 for a flagged decision"); + } catch (e) { + const err = e as { stdout?: string; status?: number }; + expect(err.status).toBe(2); + const out = err.stdout ?? ""; + expect(out).toContain("decision_on_validation: flagged"); + expect(out).toContain("[drift-detected]"); + expect(out).toContain("Flagged decisions: 1"); + } + } finally { + rmSync(tmp, { force: true }); + } + }); + + it("reports the absence of a Validation record without failing", () => { + const cots = loadFixture("legal-summarization.cots.json"); + expect(cots.decisions[0].validation).toBeUndefined(); + }); +});