/** * Snapshot generation, validation, and comparison for the kernel layer. * * A KernelSnapshot is a plain, fully-serialisable data object that captures * the complete point-in-time state of an active run. It is designed to be * written to disk, transmitted across process boundaries, or stored in an * in-memory ring buffer without any special handling. */ import type { ActiveRun, GateResult, KdFact, KdHarnessMode, KdPhase, KdResumeSnapshot, KdRunStatus, KdToolResultContract, KdWriteTransaction, } from "../types.ts"; import { HARNESS_MODES, PHASE_ORDER } from "../types.ts"; import type { EvidenceIndex } from "../evidence.ts"; import type { KdLedgerEvent } from "../ledger.ts"; // ── Summary types ──────────────────────────────────────────────────── /** Aggregated fact status distribution. */ export interface FactsSummary { /** Source: run.facts – total count of fact entries. */ total: number; /** Source: run.facts – count where status === "current". */ current: number; /** Source: run.facts – count where status === "superseded". */ superseded: number; /** Source: run.facts – count where status === "rejected". */ rejected: number; } /** Aggregated evidence index distribution. */ export interface EvidenceSummary { /** Source: evidenceIndex.entries – total count of evidence entries. */ total: number; /** Source: evidenceIndex.entries – distinct kind values. */ kinds: string[]; /** Source: evidenceIndex.entries – count grouped by source trust level. */ sourceTrust: Record; } /** Aggregated tool result distribution. */ export interface ToolResultSummary { /** Source: run.toolResults – total count of tool results. */ total: number; /** Source: run.toolResults – count where status === "success". */ success: number; /** Source: run.toolResults – count where status === "failed". */ failed: number; /** Source: run.toolResults – count where status === "blocked". */ blocked: number; } /** Aggregated write transaction distribution. */ export interface WriteTransactionSummary { /** Source: run.writeTransactions – total count of write transactions. */ total: number; /** Source: run.writeTransactions – count grouped by status. */ byStatus: Record; } /** Aggregated source anchor summary. */ export interface SourceAnchorSummary { /** Source: run.sourceAnchors – total count of anchor snapshots. */ total: number; /** Source: run.sourceAnchors – distinct file paths. */ paths: string[]; } // ── KernelSnapshot ─────────────────────────────────────────────────── /** * Plain serialisable snapshot of a harness run. * * Every field carries a `Source:` annotation in its JSDoc so that * consumers can trace where the value originates. */ export interface KernelSnapshot { /** Source: run.id – unique run identifier (duplicated for convenience). */ id: string; /** Source: run.id – unique run identifier. */ runId: string; /** Source: run.status ?? "active" – current run lifecycle status. */ runStatus: KdRunStatus; /** Source: run.phase – current harness phase. */ phase: KdPhase; /** Source: run.mode – harness mode (quick / normal). */ mode: KdHarnessMode; /** Source: run.product – product identifier, if set. */ product?: string; /** Source: run.facts – aggregated fact status counts. */ factsSummary: FactsSummary; /** Source: run.questions – count of questions with status === "open". */ openQuestionsCount: number; /** Source: run.gate – current gate result. */ gateResult: GateResult; /** Source: run.resumeSnapshot – current resume snapshot, if any. */ resumeSnapshot?: KdResumeSnapshot; /** Source: evidenceIndex.entries – aggregated evidence summary. */ evidenceSummary: EvidenceSummary; /** Source: run.toolResults – aggregated tool result summary. */ toolResultSummary: ToolResultSummary; /** Source: run.writeTransactions – aggregated write transaction summary. */ writeTransactionsSummary: WriteTransactionSummary; /** Source: run.sourceAnchors – aggregated source anchor summary. */ sourceAnchorsSummary: SourceAnchorSummary; /** Source: ledgerEvents – latest ledger event sequence number (0 if empty). */ ledgerSeq: number; /** Source: new Date().toISOString() – snapshot generation timestamp. */ generatedAt: string; } // ── ConsistencyCheckResult ──────────────────────────────────────────── export interface ConsistencyIssue { /** The field that failed validation. */ field: string; /** Severity of the issue. */ severity: "error" | "warning"; /** Human-readable description of the problem. */ message: string; } export interface ConsistencyCheckResult { /** True when no error-severity issues are present. */ valid: boolean; /** All issues found during validation. */ issues: ConsistencyIssue[]; } // ── Snapshot generation ────────────────────────────────────────────── /** * Generate a KernelSnapshot from the raw run data. * * This is a pure function – it does not read from the filesystem. * Callers are responsible for providing the run, evidence index, and * ledger events. */ export function generateSnapshot( run: ActiveRun, evidenceIndex: EvidenceIndex, ledgerEvents: KdLedgerEvent[], ): KernelSnapshot { return { // Source: run.id id: run.id, // Source: run.id runId: run.id, // Source: run.status ?? "active" runStatus: run.status ?? "active", // Source: run.phase phase: run.phase, // Source: run.mode mode: run.mode, // Source: run.product product: run.product, // Source: run.facts – aggregated counts factsSummary: summarizeFacts(run.facts), // Source: run.questions – count of open questions openQuestionsCount: (run.questions ?? []).filter((q) => q.status === "open").length, // Source: run.gate gateResult: run.gate, // Source: run.resumeSnapshot resumeSnapshot: run.resumeSnapshot, // Source: evidenceIndex.entries evidenceSummary: summarizeEvidence(evidenceIndex), // Source: run.toolResults toolResultSummary: summarizeToolResults(run.toolResults), // Source: run.writeTransactions writeTransactionsSummary: summarizeWriteTransactions(run.writeTransactions), // Source: run.sourceAnchors sourceAnchorsSummary: summarizeSourceAnchors(run.sourceAnchors), // Source: ledgerEvents – latest seq ledgerSeq: latestLedgerSeq(ledgerEvents), // Source: generated timestamp generatedAt: new Date().toISOString(), }; } // ── Snapshot validation ────────────────────────────────────────────── /** * Validate a snapshot for internal consistency. * * Checks: * - runId is present and non-empty * - phase is a valid KdPhase * - mode is a valid KdHarnessMode * - facts summary counts are internally consistent * - no stale fact references (superseded facts should have supersededBy) * - resume snapshot references a valid question if present * - ledger seq is non-negative * - generatedAt is a valid ISO timestamp */ export function validateSnapshot(snapshot: KernelSnapshot): ConsistencyCheckResult { const issues: ConsistencyIssue[] = []; // ── runId ─────────────────────────────────────────────────────── if (!snapshot.runId || typeof snapshot.runId !== "string" || snapshot.runId.trim().length === 0) { issues.push({ field: "runId", severity: "error", message: "runId 必须为非空字符串" }); } // ── phase ─────────────────────────────────────────────────────── if (!isKdPhase(snapshot.phase)) { issues.push({ field: "phase", severity: "error", message: `无效的阶段值:${snapshot.phase}` }); } // ── mode ──────────────────────────────────────────────────────── if (!isKdHarnessMode(snapshot.mode)) { issues.push({ field: "mode", severity: "error", message: `无效的模式值:${snapshot.mode}` }); } // ── facts summary internal consistency ────────────────────────── const { factsSummary } = snapshot; if (factsSummary.current + factsSummary.superseded + factsSummary.rejected !== factsSummary.total) { issues.push({ field: "factsSummary", severity: "error", message: `事实计数不一致:current(${factsSummary.current}) + superseded(${factsSummary.superseded}) + rejected(${factsSummary.rejected}) ≠ total(${factsSummary.total})`, }); } // ── tool result summary internal consistency ──────────────────── const { toolResultSummary } = snapshot; if (toolResultSummary.success + toolResultSummary.failed + toolResultSummary.blocked !== toolResultSummary.total) { issues.push({ field: "toolResultSummary", severity: "error", message: `工具结果计数不一致:success(${toolResultSummary.success}) + failed(${toolResultSummary.failed}) + blocked(${toolResultSummary.blocked}) ≠ total(${toolResultSummary.total})`, }); } // ── write transaction summary internal consistency ────────────── const { writeTransactionsSummary } = snapshot; const txnStatusSum = Object.values(writeTransactionsSummary.byStatus).reduce((a, b) => a + b, 0); if (txnStatusSum !== writeTransactionsSummary.total) { issues.push({ field: "writeTransactionsSummary", severity: "error", message: `写事务计数不一致:byStatus 总和(${txnStatusSum}) ≠ total(${writeTransactionsSummary.total})`, }); } // ── ledger seq ────────────────────────────────────────────────── if (!Number.isFinite(snapshot.ledgerSeq) || snapshot.ledgerSeq < 0) { issues.push({ field: "ledgerSeq", severity: "error", message: `ledgerSeq 必须为非负整数:${snapshot.ledgerSeq}`, }); } // ── generatedAt ───────────────────────────────────────────────── if (!isValidIsoTimestamp(snapshot.generatedAt)) { issues.push({ field: "generatedAt", severity: "error", message: `generatedAt 不是有效的 ISO 时间戳:${snapshot.generatedAt}`, }); } // ── resume snapshot cross-reference ───────────────────────────── if (snapshot.resumeSnapshot) { const rs = snapshot.resumeSnapshot; if (rs.status === "open" && !rs.pendingQuestionId) { issues.push({ field: "resumeSnapshot.pendingQuestionId", severity: "warning", message: "resumeSnapshot 状态为 open 但缺少 pendingQuestionId", }); } } // ── open questions count ──────────────────────────────────────── if (snapshot.openQuestionsCount < 0) { issues.push({ field: "openQuestionsCount", severity: "error", message: `openQuestionsCount 不能为负数:${snapshot.openQuestionsCount}`, }); } // ── facts summary non-negative ────────────────────────────────── for (const key of ["total", "current", "superseded", "rejected"] as const) { if (factsSummary[key] < 0) { issues.push({ field: `factsSummary.${key}`, severity: "error", message: `factsSummary.${key} 不能为负数:${factsSummary[key]}`, }); } } return { valid: !issues.some((i) => i.severity === "error"), issues, }; } // ── Snapshot comparison ────────────────────────────────────────────── export interface SnapshotDiff { /** Fields that changed between the two snapshots. */ changed: string[]; /** Fields that are identical. */ unchanged: string[]; /** Whether the phase advanced. */ phaseAdvanced: boolean; /** Whether the gate status changed. */ gateChanged: boolean; /** Whether new questions were opened. */ newQuestions: boolean; /** Whether the run status changed. */ statusChanged: boolean; } /** * Compare two snapshots and produce a diff summary. * * This is a lightweight structural comparison – it does not deep-compare * nested objects beyond the summary level. */ export function compareSnapshots(a: KernelSnapshot, b: KernelSnapshot): SnapshotDiff { const changed: string[] = []; const unchanged: string[] = []; const fields: Array = [ "runId", "runStatus", "phase", "mode", "product", "openQuestionsCount", "ledgerSeq", ]; for (const field of fields) { if (a[field] !== b[field]) { changed.push(field); } else { unchanged.push(field); } } // Compare summary objects by serialising (cheap and deterministic) const summaryFields: Array = [ "factsSummary", "evidenceSummary", "toolResultSummary", "writeTransactionsSummary", "sourceAnchorsSummary", ]; for (const field of summaryFields) { if (JSON.stringify(a[field]) !== JSON.stringify(b[field])) { changed.push(field); } else { unchanged.push(field); } } // Gate comparison const gateChanged = a.gateResult.passed !== b.gateResult.passed || a.gateResult.reason !== b.gateResult.reason; if (gateChanged) changed.push("gateResult"); else unchanged.push("gateResult"); return { changed, unchanged, phaseAdvanced: PHASE_ORDER.indexOf(b.phase) > PHASE_ORDER.indexOf(a.phase), gateChanged, newQuestions: b.openQuestionsCount > a.openQuestionsCount, statusChanged: a.runStatus !== b.runStatus, }; } // ── Internal helpers ───────────────────────────────────────────────── function summarizeFacts(facts: KdFact[] | undefined): FactsSummary { const list = facts ?? []; return { total: list.length, current: list.filter((f) => f.status === "current").length, superseded: list.filter((f) => f.status === "superseded").length, rejected: list.filter((f) => f.status === "rejected").length, }; } function summarizeEvidence(index: EvidenceIndex): EvidenceSummary { const entries = index.entries; const kindSet = new Set(); const trustMap = new Map(); for (const entry of entries) { kindSet.add(entry.kind); const trust = entry.source ?? "unknown"; trustMap.set(trust, (trustMap.get(trust) ?? 0) + 1); } return { total: entries.length, kinds: [...kindSet].sort(), sourceTrust: Object.fromEntries(trustMap), }; } function summarizeToolResults(results: KdToolResultContract[] | undefined): ToolResultSummary { const list = results ?? []; return { total: list.length, success: list.filter((r) => r.status === "success").length, failed: list.filter((r) => r.status === "failed").length, blocked: list.filter((r) => r.status === "blocked").length, }; } function summarizeWriteTransactions(txs: KdWriteTransaction[] | undefined): WriteTransactionSummary { const list = txs ?? []; const byStatus: Record = {}; for (const tx of list) { byStatus[tx.status] = (byStatus[tx.status] ?? 0) + 1; } return { total: list.length, byStatus }; } function summarizeSourceAnchors(anchors: ActiveRun["sourceAnchors"]): SourceAnchorSummary { const list = anchors ?? []; const pathSet = new Set(); for (const anchor of list) { pathSet.add(anchor.path); } return { total: list.length, paths: [...pathSet].sort(), }; } function latestLedgerSeq(events: KdLedgerEvent[]): number { if (events.length === 0) return 0; return Math.max(...events.map((e) => e.seq)); } function isKdPhase(value: string): value is KdPhase { return (PHASE_ORDER as readonly string[]).includes(value); } function isKdHarnessMode(value: string): value is KdHarnessMode { return (HARNESS_MODES as readonly string[]).includes(value); } function isValidIsoTimestamp(value: string | undefined): boolean { if (!value || typeof value !== "string") return false; const date = new Date(value); return !Number.isNaN(date.getTime()) && value.includes("T"); }