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
19 changes: 18 additions & 1 deletion src/cots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface Cots {
inputs: { source_artifacts: SourceArtifact[] };
context: Context;
decisions: DecisionRecord[];
evidence: { evidence_chain: EvidenceChainEntry[] };
evidence: Evidence;
validation: { feedback_surfaces: FeedbackSurface[] };
}

Expand Down Expand Up @@ -104,6 +104,21 @@ export interface AuditEntry {
notes: string;
}

export type SourceType = "human-authored" | "human-curated" | "model-generated" | "mixed";

export type ChainTerminationPolicy = "terminal" | "bounded_recursion" | "unbounded_recursion";

export interface ChainTermination {
policy: ChainTerminationPolicy;
max_levels?: number;
}

export interface Evidence {
/** RFC-0002 (COTS schema v1.2.0). Optional; the discipline's default is terminal. */
chain_termination?: ChainTermination;
evidence_chain: EvidenceChainEntry[];
}

export interface EvidenceChainEntry {
decision_id: URI;
supporting_evidence: EvidenceItem[];
Expand All @@ -127,6 +142,8 @@ export interface EvidenceItem {
transformation: { operation: string; inputs: string[]; output: string; recorded_at: ISO8601 };
uncertainty: { calibrated_score: CalibratedScore; basis: string };
polarity: Polarity;
/** RFC-0002 (COTS schema v1.2.0). Optional; absent on pre-RFC-0002 COTSs. */
source_type?: SourceType;
}

export interface FeedbackSurface {
Expand Down
35 changes: 33 additions & 2 deletions src/layer2/layer2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Layer 3, which is the layer that can reject.
*/

import type { Cots, DecisionRecord, Context, ValidationRecord } from "../cots.js";
import type { Cots, DecisionRecord, Context, ValidationRecord, DecisionOnValidation } from "../cots.js";

export type FlagType =
| "drift-detected"
Expand Down Expand Up @@ -101,6 +101,29 @@ const validateDecision = (
};
};

const validateRecursiveChains = (cots: Cots, now: string): ValidationRecord["flags"] => {
const flags: ValidationRecord["flags"] = [];
const termination = cots.evidence.chain_termination;

for (const chain of cots.evidence.evidence_chain) {
const items = [...chain.supporting_evidence, ...chain.contradicting_evidence];
const recursive = items.filter((item) => item.source_type === "model-generated");
if (recursive.length > 0) {
const policy = termination?.policy ?? "terminal";
const admitted = policy === "bounded_recursion" || policy === "unbounded_recursion";
flags.push({
flag_type: "segment-signal-divergence",
message: admitted
? `Recursive evidence chain: ${recursive.length} model-generated evidence item(s) under policy '${policy}'. The chain does not terminate at a human-readable source. (RFC-0002)`
: `Recursive evidence chain under terminal policy: ${recursive.length} model-generated evidence item(s). The chain must resolve to human-authored sources. (RFC-0002)`,
detected_at: now,
});
}
}

return flags;
};

export class Layer2 {
constructor(private readonly calibration: CalibrationData) {}

Expand All @@ -118,9 +141,17 @@ export class Layer2 {
const now = options.now ?? new Date().toISOString();
const validatedBy = options.validatedBy ?? "layer2-runtime-validation";

const recursiveFlags = validateRecursiveChains(cots, now);

const decisions = cots.decisions.map((decision) => {
const record = validateDecision(decision, cots.context, this.calibration, now);
return { ...decision, validation: { ...record, validated_by: validatedBy } };
const flags = [...record.flags, ...recursiveFlags];
const decision_on_validation: DecisionOnValidation =
flags.length > 0 ? (record.decision_on_validation === "escalated" ? "escalated" : "flagged") : "passed";
return {
...decision,
validation: { ...record, flags, decision_on_validation, validated_by: validatedBy },
};
});

return { ...cots, decisions };
Expand Down
35 changes: 35 additions & 0 deletions tests/layer2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,38 @@ describe("Layer 2 (Runtime Validation)", () => {
expect(cots.decisions[0].validation).toBeUndefined();
});
});

describe("Layer 2 recursive chain flagging (RFC-0002)", () => {
const buildRecursiveCots = async () => {
const cots = await buildCots();
cots.evidence.chain_termination = { policy: "bounded_recursion", max_levels: 1 };
cots.evidence.evidence_chain[0].supporting_evidence[0].source_type = "model-generated";
return cots;
};

it("flags a COTS with model-generated evidence (bounded policy)", async () => {
const cots = await buildRecursiveCots();
const layer2 = new Layer2({});
const validated = layer2.validate(cots, { now: NOW });
const v = validated.decisions[0].validation!;
expect(v.decision_on_validation).toBe("flagged");
expect(v.flags.some((f) => f.message.includes("Recursive evidence chain"))).toBe(true);
});

it("flags a model-generated item even under the terminal default", async () => {
const cots = await buildRecursiveCots();
cots.evidence.chain_termination = undefined;
const layer2 = new Layer2({});
const validated = layer2.validate(cots, { now: NOW });
expect(validated.decisions[0].validation!.flags.some((f) => f.message.includes("terminal policy"))).toBe(true);
});

it("does not flag a COTS whose evidence is human-authored", async () => {
const cots = await buildCots();
cots.evidence.chain_termination = { policy: "terminal" };
cots.evidence.evidence_chain[0].supporting_evidence[0].source_type = "human-authored";
const layer2 = new Layer2({});
const validated = layer2.validate(cots, { now: NOW });
expect(validated.decisions[0].validation!.decision_on_validation).toBe("passed");
});
});
Loading