/** * Types for the system-wide drift audit (CELILO_UPDATE D6). * * `runAudit` returns a `SystemAuditReport` aggregating findings from * each drift category. Each finding has a category (one of * `DriftCategory`), a severity (`todo`, `drift`, or `blocked`), a stable * machine-readable code, and a human-readable message + suggested * remediation. * * The overall verdict is computed from the findings: * - any `blocked` finding → BLOCKED * - any `unmeasured` finding (no blocked) → UNKNOWN * - any `drift` finding (no blocked, no unmeasured) → DRIFT * - only `todo` findings (or none) → READY * * `todo` exists because some categories surface "next-step reminders" * rather than actual divergence — `unconfigured_modules` and * `undeployed_modules` are the canonical examples. The operator * imported a module and hasn't gotten around to configuring/deploying; * that's a TODO list, not a drifted system. Calling it DRIFT * overloaded the term and made `system update` look like it had * unfinished work even when it didn't. */ export type DriftCategory = | 'cli_version' | 'schema' | 'capability_abi' | 'browser_pin' | 'terraform_plan' | 'module_versions' | 'module_configs' | 'health' | 'backups' | 'abandoned_operations' | 'undeployed_modules' | 'unconfigured_modules' | 'services_credentials' | 'secrets_decryptable' | 'services_reachable' | 'machines_reachable' | 'public_dns' | 'disk_space' | 'transport_reads' | 'trusted_sources' | 'interface_classification' | 'module_integrity' | 'detect_without_converge' | 'jail_exemptions'; /** * `unmeasured` is a check that could not reach its subject * (openspec/changes/module-integrity-rigor, D7). * * It exists because there was nowhere to put that. A category that could not * measure either invented a finding of the wrong severity or contributed * nothing, and contributing nothing renders as READY — the same defect as * everything else this change is about, sitting in the framework that is * supposed to catch it. * * Every claim celilo makes about the fleet is either measured against the thing * it describes, or reported as unmeasured. There is no third category, and * unmeasured never renders as green. */ export type DriftSeverity = 'todo' | 'drift' | 'unmeasured' | 'blocked'; /** * `UNKNOWN` is deliberately distinct from `DRIFT` rather than folded into it. * "I could not tell" and "I can tell, and it is wrong" call for different * operator responses, and collapsing them recreates exactly the ambiguity D1 * exists to remove one level down. It ranks below `BLOCKED` — a hard stop is * still a hard stop — and above `DRIFT`, because you cannot act on a diff you * are not sure you have. */ export type AuditVerdict = 'READY' | 'DRIFT' | 'UNKNOWN' | 'BLOCKED'; /** * A single drift finding produced by a category check. * * - `category`: which check produced this * - `severity`: whether it gates `system update` or just informs * - `code`: stable machine-readable identifier (e.g. `module_version_drift`), * intended for `--json` consumers * - `message`: short, human-readable description * - `details`: optional multi-line elaboration (e.g. release messages * between two versions, or step-by-step guidance for non-actionable * findings) * - `remediation`: suggested next action — either a runnable * `celilo …` command (when `actionable: true`) or human-facing * guidance prose (when `actionable: false`) * - `actionable`: when `true`, `remediation` is a runnable * `celilo …` command and the TUI surfaces a one-keypress * "Remediate" modal. When `false`, `remediation` is descriptive * guidance and the modal is suppressed (the user has to do code * work, edit a config file, or run something outside celilo). * Default is `false` — conservative, since a non-runnable * remediation surfaced as runnable would crash on submit. * - `subject`: the entity the finding is about — usually a module ID, * capability name, or `'system'`. Lets the UI group findings. */ export interface DriftFinding { category: DriftCategory; severity: DriftSeverity; code: string; message: string; details?: string; remediation?: string; actionable?: boolean; subject: string; } /** * The aggregated report. Stable JSON shape — do not break consumers. * * `version` is bumped on incompatible schema changes to this object. */ export interface SystemAuditReport { version: 1; verdict: AuditVerdict; generatedAt: string; // ISO-8601 UTC findings: DriftFinding[]; } /** * Compute the overall verdict from a set of findings. `todo` findings * never escalate beyond READY — they're informational reminders, not * a signal that the system has moved away from a desired state. */ export function computeVerdict(findings: DriftFinding[]): AuditVerdict { if (findings.some((f) => f.severity === 'blocked')) return 'BLOCKED'; if (findings.some((f) => f.severity === 'unmeasured')) return 'UNKNOWN'; if (findings.some((f) => f.severity === 'drift')) return 'DRIFT'; return 'READY'; }