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
17 changes: 5 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,18 @@ se-trace validate-runtime examples/legal-summarization.cots.json
# Walk the evidence chains and surface recursive (model-generated) items (RFC-0002)
se-trace recursive examples/legal-summarization.cots.json

# Render the Context Graph (Mermaid + HTML, RFC-0004)
se-trace context-graph examples/legal-summarization.cots.json -o ./out

# Replay the audit trail (RFC-0005)
se-trace audit examples/legal-summarization.cots.json

# COTS Explorer: one self-contained HTML page with all four views (Advancement Plan Phase 5)
se-trace explore examples/legal-summarization.cots.json -o ./out
# Print each decision's Layer 3 GuardrailDecision; re-evaluate against a rule file (RFC-0003)
se-trace check-guardrails examples/legal-summarization.cots.json
se-trace check-guardrails examples/legal-summarization.cots.json --rules rules.gr
```

## What is in this repository

- `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/context-graph.ts` - the context-graph command. Renders the Context + Evidence parts as a typed-edge graph (RFC-0004).
- `src/audit.ts` - the audit command. Replays the audit trail and flags append-only/linkage violations (RFC-0005).
- `src/explore.ts` - the COTS Explorer. One self-contained HTML page combining Reasoning Roadmap, Context Graph, summary, and audit trail.
- `src/cli/se-trace.ts` - the CLI entry point, including `validate-runtime` (RFC-0001) and `recursive` (RFC-0002 chain-walking).
- `src/cli/se-trace.ts` - the CLI entry point, including `validate-runtime` (RFC-0001), `recursive` (RFC-0002), and `check-guardrails` (RFC-0003).
- `src/guardrails/layer3.ts` - a thin consumer-side port of the reference implementation's Layer 3 for `check-guardrails --rules` re-evaluation.
- `tests/tools.test.ts` - the test suite.

## SDKs
Expand Down
109 changes: 63 additions & 46 deletions src/cli/se-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,55 @@ program
}
});

program
.command("check-guardrails")
.description("Print each decision's Layer 3 GuardrailDecision (RFC-0003). With --rules, re-evaluate the COTS against a guardrail rule file")
.argument("<path>", "Path to a COTS JSON file")
.option("-r, --rules <file>", "Path to a guardrail rule file (re-evaluates instead of printing the stored record)")
.action(async (path: string, opts: { rules?: string }) => {
const cots = loadCots(path);
if (opts.rules) {
const { Layer3, parseRules } = await import("../guardrails/layer3.js");
const dsl = readFileSync(resolve(opts.rules), "utf-8");
let rules;
try {
rules = parseRules(dsl);
} catch {
console.log("guardrail-rules-malformed");
process.exit(1);
}
const result = new Layer3(rules).evaluate(cots);
for (const d of result.cots.decisions) {
printGuardrail(d.id, d.guardrail!);
}
if (result.rejected.length > 0) {
console.log(`\nRejected: ${result.rejected.length} (binding rules; the response is stopped and the rejection recorded)`);
process.exit(2);
}
if (result.escalated.length > 0) {
console.log(`\nEscalated: ${result.escalated.length}`);
process.exit(3);
}
return;
}
let withRecord = 0;
let rejected = 0;
for (const d of cots.decisions) {
if (d.guardrail === undefined) {
console.log(`${d.id}\n No Layer 3 GuardrailDecision (the producer has not floored Layer 3).`);
continue;
}
withRecord++;
printGuardrail(d.id, d.guardrail);
if (d.guardrail.decision === "rejected") rejected++;
}
console.log(`\nDecisions with a GuardrailDecision: ${withRecord}/${cots.decisions.length}`);
if (rejected > 0) {
console.log(`Rejected: ${rejected} (binding rules; no silent rejection)`);
process.exit(2);
}
});

program
.command("inspect")
.description("Print a human-readable summary of a COTS")
Expand Down Expand Up @@ -183,51 +232,19 @@ program
console.log(`HTML: ${htmlPath}`);
});

program
.command("context-graph")
.description("Render a COTS's Context + Evidence parts as a Context Graph (Mermaid + HTML, RFC-0004)")
.argument("<path>", "Path to a COTS JSON file")
.option("-o, --out <dir>", "Output directory", ".")
.action((path: string, opts: { out: string }) => {
const cots = loadCots(path);
const baseName = basename(path, extname(path));
const mermaid = toContextGraphMermaid(cots);
const html = toContextGraphHtml(cots);
const mermaidPath = resolve(opts.out, `${baseName}.context-graph.mermaid`);
const htmlPath = resolve(opts.out, `${baseName}.context-graph.html`);
mkdirSync(dirname(mermaidPath), { recursive: true });
mkdirSync(dirname(htmlPath), { recursive: true });
writeFileSync(mermaidPath, mermaid);
writeFileSync(htmlPath, html);
console.log(`Mermaid: ${mermaidPath}`);
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
.command("explore")
.description("Render the COTS Explorer: a single self-contained HTML page with all four views (Advancement Plan Phase 5)")
.argument("<path>", "Path to a COTS JSON file")
.option("-o, --out <dir>", "Output directory", ".")
.action((path: string, opts: { out: string }) => {
const cots = loadCots(path);
const baseName = basename(path, extname(path));
const html = toExplorerHtml(cots);
const htmlPath = resolve(opts.out, `${baseName}.explorer.html`);
mkdirSync(dirname(htmlPath), { recursive: true });
writeFileSync(htmlPath, html);
console.log(`COTS Explorer: ${htmlPath}`);
});
const printGuardrail = (decisionId: string, g: {
decision: string;
rule_id: string;
rationale: string;
binding: boolean;
evaluated_at: string;
evaluated_by?: string;
}) => {
console.log(decisionId);
console.log(` decision: ${g.decision}${g.binding ? " (binding)" : " (advisory)"}`);
console.log(` rule: ${g.rule_id}`);
console.log(` rationale: ${g.rationale}`);
console.log(` evaluated_at: ${g.evaluated_at}${g.evaluated_by ? ` by: ${g.evaluated_by}` : ""}`);
};

program.parse();
13 changes: 13 additions & 0 deletions src/cots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ export interface DecisionRecord {
created_at: ISO8601;
/** Layer 2 Validation record (COTS schema v1.1.0, RFC-0001). Optional. */
validation?: Layer2Validation;
/** Layer 3 GuardrailDecision (COTS schema v1.3.0, RFC-0003). Optional. */
guardrail?: GuardrailDecision;
}

export type GuardrailOutcome = "accepted" | "rejected" | "annotated" | "escalated";

export interface GuardrailDecision {
decision: GuardrailOutcome;
rule_id: URI;
rationale: string;
binding: boolean;
evaluated_at: ISO8601;
evaluated_by?: string;
}

export interface AuditEntry {
Expand Down
156 changes: 156 additions & 0 deletions src/guardrails/layer3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Guardrail rule evaluation for the se-trace CLI. A thin consumer-side
* port of the reference implementation's Layer 3 module (RFC-0003): parses
* the guardrail DSL and evaluates rules against a COTS so reviewers can
* re-check a stored COTS against a rule file.
*
* The authoritative Layer 3 implementation lives in
* Source-Engineering/source-engineering-reference-implementation.
*/

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

export type GuardrailAction = "reject" | "annotate" | "escalate";

export interface GuardrailRule {
rule_id: string;
when: (cots: Cots, decision: DecisionRecord) => boolean;
then: GuardrailAction;
rationale: string;
}

export class GuardrailRulesMalformed extends Error {
constructor() {
super("guardrail-rules-malformed");
}
}

const CONDITION_RE = /^(?:(constraint\([a-z0-9_-]+\)|flag\([a-z-]+\)|source_expired\(\)|decision_type)(\s*(?:==|!=)\s*"[^"]*")?\s*)$/;

export const parseRules = (dsl: string): GuardrailRule[] => {
if (typeof dsl !== "string" || dsl.trim().length === 0) {
throw new GuardrailRulesMalformed();
}
const ruleBlocks = dsl.match(/rule\s+[^\s{]+\s*\{[^}]*\}/g);
if (ruleBlocks === null || ruleBlocks.length === 0) {
throw new GuardrailRulesMalformed();
}

const rules: GuardrailRule[] = [];
for (const block of ruleBlocks) {
const idMatch = block.match(/^rule\s+([^\s{]+)\s*\{/);
const whenMatch = block.match(/when\s+([^\n]+?)\s*\n\s*then/);
const thenMatch = block.match(/then\s+(reject|annotate|escalate)\b/);
const rationaleMatch = block.match(/with\s+rationale\s+"([^"]*)"/);

if (!idMatch || !whenMatch || !thenMatch || !rationaleMatch) {
throw new GuardrailRulesMalformed();
}
const condition = whenMatch[1].trim();
if (!CONDITION_RE.test(condition.replace(/\s+/g, " "))) {
throw new GuardrailRulesMalformed();
}
rules.push({
rule_id: idMatch[1],
when: compileCondition(condition),
then: thenMatch[1] as GuardrailAction,
rationale: rationaleMatch[1],
});
}
return rules;
};

const compileCondition = (condition: string): ((cots: Cots, decision: DecisionRecord) => boolean) => {
const normalized = condition.replace(/\s+/g, " ").trim();

const constraintEq = normalized.match(/^constraint\(([a-z0-9_-]+)\)\s*==\s*"([^"]*)"$/);
if (constraintEq) {
const scope = constraintEq[1];
const value = constraintEq[2];
return (cots) => cots.context.constraints.some((c) => c.scope === scope && c.value === value);
}

const constraintNeq = normalized.match(/^constraint\(([a-z0-9_-]+)\)\s*!=\s*"([^"]*)"$/);
if (constraintNeq) {
const scope = constraintNeq[1];
const value = constraintNeq[2];
return (cots) => !cots.context.constraints.some((c) => c.scope === scope && c.value === value);
}

const flagMatch = normalized.match(/^flag\(([a-z-]+)\)$/);
if (flagMatch) {
const flagType = flagMatch[1];
return (cots) =>
cots.decisions.some((d) => d.validation?.flags.some((f) => f.flag_type === flagType) ?? false);
}

if (/^source_expired\(\)$/.test(normalized)) {
return (cots) => {
const now = Date.parse(cots.produced_at);
return cots.inputs.source_artifacts.some(
(a) => a.temporal_validity.valid_until !== null && Date.parse(a.temporal_validity.valid_until) < now
);
};
}

const dtMatch = normalized.match(/^decision_type\s*==\s*"([^"]*)"$/);
if (dtMatch) {
const suffix = dtMatch[1];
return (cots) => cots.decisions.some((d) => d.id.includes(suffix) || d.basis_id.includes(suffix));
}

throw new GuardrailRulesMalformed();
};

const OUTCOME_FOR_ACTION: Record<GuardrailAction, GuardrailDecision["decision"]> = {
reject: "rejected",
annotate: "annotated",
escalate: "escalated",
};

export interface Layer3Result {
cots: Cots;
rejected: { decision_id: string; rule_id: string; rationale: string }[];
escalated: { decision_id: string; rule_id: string; rationale: string }[];
}

export class Layer3 {
constructor(private readonly rules: GuardrailRule[]) {}

evaluate(cots: Cots, options: { now?: string } = {}): Layer3Result {
const now = options.now ?? new Date().toISOString();
const rejected: Layer3Result["rejected"] = [];
const escalated: Layer3Result["escalated"] = [];

const decisions = cots.decisions.map((decision): DecisionRecord => {
let fired: GuardrailRule | undefined;
let outcome: GuardrailDecision["decision"] = "accepted";
for (const rule of this.rules) {
if (rule.when(cots, decision)) {
fired = rule;
outcome = OUTCOME_FOR_ACTION[rule.then];
break;
}
}
const guardrail: GuardrailDecision = {
decision: outcome,
rule_id: fired ? `urn:source-engineering:rule:${fired.rule_id}` : "urn:source-engineering:rule:none",
rationale: fired ? fired.rationale : "No guardrail rule matched; the decision is accepted.",
binding: fired?.then === "reject",
evaluated_at: now,
evaluated_by: "se-trace-check-guardrails",
};
let next: DecisionRecord = { ...decision, guardrail };
if (outcome === "rejected") {
next = { ...next, validation_status: { ...next.validation_status, status: "superseded", last_changed_at: now, last_changed_by: "se-trace-check-guardrails" } };
rejected.push({ decision_id: decision.id, rule_id: fired!.rule_id, rationale: fired!.rationale });
} else if (outcome === "escalated") {
next = { ...next, validation_status: { ...next.validation_status, status: "appealed", last_changed_at: now, last_changed_by: "se-trace-check-guardrails" } };
escalated.push({ decision_id: decision.id, rule_id: fired!.rule_id, rationale: fired!.rationale });
}
return next;
});

return { cots: { ...cots, decisions }, rejected, escalated };
}
}
Loading
Loading