Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Audit trail replay: print a decision's full lifecycle from a COTS's
* audit_history, marking append-only and linkage violations (RFC-0005,
* TC-L4-01..04).
*/

import type { Cots } from "./cots.js";

export interface AuditIssue {
test_id: string;
message: string;
}

export interface AuditReport {
lines: string[];
issues: AuditIssue[];
}

export const replayAudit = (cots: Cots, decisionId?: string): AuditReport => {
const lines: string[] = [];
const issues: AuditIssue[] = [];
const decisions = decisionId ? cots.decisions.filter((d) => d.id === decisionId) : cots.decisions;

if (decisionId && decisions.length === 0) {
lines.push(`Decision not found: ${decisionId}`);
return { lines, issues };
}

const decisionIds = new Set(cots.decisions.map((d) => d.id));

for (const d of decisions) {
lines.push(`Decision: ${d.id}`);
lines.push(` status: ${d.validation_status.status} text: ${truncate(d.decision_text, 60)}`);
const history = [...d.audit_history].sort((a, b) => Date.parse(a.performed_at) - Date.parse(b.performed_at));
if (history.length === 0) {
issues.push({ test_id: "TC-L4-01", message: `${d.id}: empty audit_history` });
lines.push(" (no audit entries)");
continue;
}
if (history[0].action !== "created") {
issues.push({ test_id: "TC-L4-01", message: `${d.id}: audit trail does not start with "created"` });
}
const seen = new Set<string>();
for (let j = 0; j < history.length; j++) {
const e = history[j];
lines.push(` [${e.performed_at}] ${e.action.toUpperCase()} by ${e.performed_by}`);
lines.push(` ${truncate(e.notes, 100)}`);
if (seen.has(e.id)) {
issues.push({ test_id: "TC-L4-02", message: `${d.id}: duplicate audit entry id '${e.id}' (append-only violated)` });
}
seen.add(e.id);
if (j > 0 && Date.parse(e.performed_at) < Date.parse(history[j - 1].performed_at)) {
issues.push({ test_id: "TC-L4-02", message: `${d.id}: audit entry out of time order (append-only violated)` });
}
if (e.action !== "created") {
const m = e.notes.match(/decision_id=(\S+)/);
if (!m || !decisionIds.has(m[1])) {
issues.push({ test_id: "TC-L4-03", message: `${d.id}: ${e.action} event does not resolve to a known decision` });
}
}
if (e.action === "rerun" && !/rerun_of=\S+/.test(e.notes)) {
issues.push({ test_id: "TC-L4-04", message: `${d.id}: rerun event lacks rerun_of linkage` });
}
if (e.action === "superseded" && !/superseded_by=\S+/.test(e.notes)) {
issues.push({ test_id: "TC-L4-04", message: `${d.id}: superseded event lacks superseded_by linkage` });
}
}
lines.push("");
}

if (issues.length === 0) {
lines.push("Audit trail: OK (append-only, lifecycle events resolve, linkages present)");
} else {
lines.push(`Audit trail issues: ${issues.length}`);
for (const i of issues) lines.push(` [${i.test_id}] ${i.message}`);
}

return { lines, issues };
};

const truncate = (s: string, n: number): string =>
s.length > n ? s.slice(0, n) + "..." : s;
14 changes: 14 additions & 0 deletions src/cli/se-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* - cite <file> <decision-id> Print the evidence chain for a specific decision.
* - visualize <file> Render a COTS as a Reasoning Roadmap (Mermaid + HTML).
* - context-graph <file> Render a COTS's Context + Evidence as a Context Graph (RFC-0004).
* - audit <file> [decision-id] Replay a decision's audit trail (RFC-0005).
*/

import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
Expand All @@ -15,6 +16,7 @@ import { inspect } from "../inspect.js";
import { cite } from "../cite.js";
import { toMermaid, toHtml } from "../visualize.js";
import { toContextGraphMermaid, toContextGraphHtml } from "../context-graph.js";
import { replayAudit } from "../audit.js";
import type { Cots } from "../cots.js";

// Lightweight conformance check (subset of the canonical test suite). For
Expand Down Expand Up @@ -198,4 +200,16 @@ program
console.log(`HTML: ${htmlPath}`);
});

program
.command("audit")
.description("Replay the audit trail of every decision (or one decision) and flag append-only/linkage violations (RFC-0005)")
.argument("<path>", "Path to a COTS JSON file")
.argument("[decision-id]", "Optional decision id to replay")
.action((path: string, decisionId?: string) => {
const cots = loadCots(path);
const report = replayAudit(cots, decisionId);
for (const line of report.lines) console.log(line);
if (report.issues.length > 0) process.exit(1);
});

program.parse();
52 changes: 52 additions & 0 deletions tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { inspect } from "../src/inspect.js";
import { cite } from "../src/cite.js";
import { toMermaid, toHtml } from "../src/visualize.js";
import { toContextGraphMermaid, toContextGraphHtml } from "../src/context-graph.js";
import { replayAudit } from "../src/audit.js";
import type { Cots } from "../src/cots.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -140,6 +141,57 @@ describe("context-graph (RFC-0004)", () => {
});
});

describe("audit (RFC-0005)", () => {
it("replays a clean audit trail with no issues", () => {
const cots = loadFixture("legal-summarization.cots.json");
const did = cots.decisions[0].id;
cots.decisions[0].audit_history.push({
id: "urn:source-engineering:audit:test-002",
action: "appealed",
performed_by: "reviewer-1",
performed_at: "2026-11-02T09:00:00Z",
notes: `User appealed. decision_id=${did}`,
});
const report = replayAudit(cots);
expect(report.issues).toHaveLength(0);
expect(report.lines.join("\n")).toContain("APPEALED");
expect(report.lines.join("\n")).toContain("Audit trail: OK");
});

it("flags a dangling lifecycle event", () => {
const cots = loadFixture("legal-summarization.cots.json");
cots.decisions[0].audit_history.push({
id: "urn:source-engineering:audit:test-003",
action: "appealed",
performed_by: "reviewer-1",
performed_at: "2026-11-02T09:00:00Z",
notes: "User appealed. decision_id=urn:source-engineering:decision:nope",
});
const report = replayAudit(cots);
expect(report.issues.some((i) => i.test_id === "TC-L4-03")).toBe(true);
});

it("flags an unlinked rerun", () => {
const cots = loadFixture("legal-summarization.cots.json");
const did = cots.decisions[0].id;
cots.decisions[0].audit_history.push({
id: "urn:source-engineering:audit:test-004",
action: "rerun",
performed_by: "reviewer-1",
performed_at: "2026-11-02T09:00:00Z",
notes: `Re-run. decision_id=${did}`,
});
const report = replayAudit(cots, did);
expect(report.issues.some((i) => i.test_id === "TC-L4-04")).toBe(true);
});

it("reports a not-found decision", () => {
const cots = loadFixture("legal-summarization.cots.json");
const report = replayAudit(cots, "urn:source-engineering:decision:does-not-exist");
expect(report.lines.join("\n")).toContain("not found");
});
});

describe("validate-runtime", () => {
it("reports a Layer 2 Validation record when present", async () => {
const cots = loadFixture("legal-summarization.cots.json");
Expand Down
Loading