/** * In-memory evidence store with ship-time validation. * * The store accumulates {@link EvidenceRecord}s during a run and provides * validation helpers to decide whether the evidence set is sufficient to ship. */ import type { EvidenceRecord, EvidenceRecordKind, EvidenceSourceTrust, } from "./evidence-source.js"; /* ------------------------------------------------------------------ */ /* Constants */ /* ------------------------------------------------------------------ */ const VALID_TRUST_LEVELS: ReadonlySet = new Set([ "local-command", "project-metadata", "project-source", "user-provided", "external-system", "manual-note", ]); const VALID_KINDS: ReadonlySet = new Set([ "sdk-signature", "metadata", "tdd-red", "tdd-green", "verify", "evaluation", "manual", ]); /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ /** * Returns `true` only when the record qualifies as **strong** evidence: * * - `local-command` with `exitCode === 0` and at least one `sourceRef` * - `project-metadata` * - `project-source` */ export function isStrongEvidence(record: EvidenceRecord): boolean { switch (record.sourceTrust) { case "local-command": return record.exitCode === 0 && record.sourceRefs.length > 0; case "project-metadata": case "project-source": return true; default: return false; } } /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ /** A single risk item surfaced during ship validation. */ export interface ShipRisk { recordId: string; risk: string; } /** Result returned by {@link EvidenceStore.validateForShip}. */ export interface ShipValidationResult { passed: boolean; risks: ShipRisk[]; canShip: boolean; } /* ------------------------------------------------------------------ */ /* Store */ /* ------------------------------------------------------------------ */ /** * Append-only, in-memory evidence store. * * All mutations go through {@link EvidenceStore.add} which enforces * basic integrity checks (non-empty path, valid trust level, valid kind). */ export class EvidenceStore { private readonly records: Map = new Map(); private seq = 0; /** * Add a record to the store. * * `id` is auto-generated if not provided (format: `E-NNNN`). * * @throws {Error} if `path` is empty, `sourceTrust` is invalid, or `kind` is invalid. */ add( record: Omit & Partial> ): EvidenceRecord { if (!record.path || record.path.trim().length === 0) { throw new Error("EvidenceRecord.path must be non-empty"); } if (!VALID_TRUST_LEVELS.has(record.sourceTrust)) { throw new Error( `Invalid EvidenceSourceTrust: "${record.sourceTrust}"` ); } if (!VALID_KINDS.has(record.kind)) { throw new Error(`Invalid EvidenceRecordKind: "${record.kind}"`); } const id = record.id ?? this.nextId(); const full: EvidenceRecord = { ...record, id }; if (this.records.has(id)) { throw new Error(`Duplicate evidence id: "${id}"`); } this.records.set(id, full); return full; } /** Retrieve a record by id, or `undefined` if not found. */ get(id: string): EvidenceRecord | undefined { return this.records.get(id); } /** Return all records whose `sourceTrust` matches the given value. */ listByTrust(trust: EvidenceSourceTrust): EvidenceRecord[] { const out: EvidenceRecord[] = []; for (const rec of this.records.values()) { if (rec.sourceTrust === trust) { out.push(rec); } } return out; } /** * Validate the full evidence set before shipping. * * Rules: * - `manual-note` records are residual risk — they **cannot** serve as * green-light evidence (e.g. cannot prove tdd-green or verify passed). * - `user-provided` records are residual risk in ship. * - `external-system` records are residual risk in ship. * - `local-command` records are acceptable as strong evidence when * `exitCode === 0` and `sourceRefs` are present. * - `project-metadata` / `project-source` are acceptable as strong evidence. * * `canShip` is `true` when at least one strong-evidence record exists * **and** no hard blockers are present (manual-note used as green-light). */ validateForShip(records?: EvidenceRecord[]): ShipValidationResult { const pool = records ?? [...this.records.values()]; const risks: ShipRisk[] = []; let hasStrong = false; let hardBlock = false; for (const rec of pool) { switch (rec.sourceTrust) { case "manual-note": risks.push({ recordId: rec.id, risk: `manual-note cannot serve as green-light evidence (${rec.kind})`, }); // If someone tries to use manual-note as tdd-green or verify, that's a hard block. if (rec.kind === "tdd-green" || rec.kind === "verify") { hardBlock = true; } break; case "user-provided": risks.push({ recordId: rec.id, risk: "user-provided evidence carries residual trust risk", }); if (isStrongEvidence(rec)) { hasStrong = true; } break; case "external-system": risks.push({ recordId: rec.id, risk: "external-system evidence carries residual trust risk", }); if (isStrongEvidence(rec)) { hasStrong = true; } break; default: if (isStrongEvidence(rec)) { hasStrong = true; } break; } } return { passed: hasStrong && !hardBlock, risks, canShip: hasStrong && !hardBlock, }; } /* -------------------------------------------------------------- */ /* Private */ /* -------------------------------------------------------------- */ private nextId(): string { this.seq += 1; return `E-${String(this.seq).padStart(4, "0")}`; } }