/** * Module integrity as a drift category * (openspec/changes/module-integrity-rigor, D8). * * One implementation, three surfaces: `celilo module verify ` for one * module, `celilo system audit` for every module, and `system doctor`'s fleet * section. `system doctor` answers "can this box run celilo, and is the runtime * wired up"; `system audit` answers "has anything drifted from desired state", * and module integrity is a drift category by every structural test. * * Pure of I/O in the same way its siblings are: the caller does the auditing * and passes the results in, so this file is testable without a filesystem. */ import type { AuditResult } from '../../module/packaging/audit'; import type { DriftFinding } from './types'; export interface ModuleIntegrityAuditDeps { /** One `auditModule` result per installed module. */ results: AuditResult[]; } /** * A verify failure celilo could not perform at all — no baseline row, module * directory gone — is `unmeasured`, not `drift`. It is the difference between * "the files moved" and "I never looked", which is the whole point of D7. */ export function auditModuleIntegrity(deps: ModuleIntegrityAuditDeps): DriftFinding[] { const findings: DriftFinding[] = []; for (const result of deps.results) { if (result.error) { findings.push({ category: 'module_integrity', severity: 'unmeasured', code: 'module_integrity_unmeasured', message: `${result.moduleId}: integrity could not be checked — ${result.error}`, remediation: `celilo module verify ${result.moduleId}`, actionable: true, subject: result.moduleId, }); continue; } // Ahead of the file findings it explains. When the baseline describes a // different version, every file difference beneath it is a consequence of // that and not independent evidence. const stale = result.violations.filter((v) => v.type === 'stale-baseline'); for (const violation of stale) { findings.push({ category: 'module_integrity', severity: 'unmeasured', code: 'module_integrity_stale_baseline', message: `${result.moduleId}: ${violation.message}`, // No command restamps a baseline. `module update` takes a module // source PATH, not an id (celilo#1308: prescribing it with the id // named an argument form that does not exist), and `module upgrade` // deploys the registry version instead. Say so rather than name a // command that cannot run. remediation: 'no automatic remediation: the baseline predates version stamping', actionable: false, subject: result.moduleId, }); } // File differences are only meaningful once the baseline is trustworthy. // Reporting them under a stale baseline is what made `module verify` // unreadable: 72 violations across two healthy modules, nearly all of them // consequences of one frozen row. const files = result.violations.filter( (v) => v.type === 'modified' || v.type === 'missing' || v.type === 'extra', ); if (files.length > 0 && stale.length === 0) { findings.push({ category: 'module_integrity', severity: 'drift', code: 'module_files_drifted', message: `${result.moduleId}: ${files.length} installed file(s) do not match the ${result.baselineVersion ?? 'recorded'} baseline`, details: files.map((v) => ` • [${v.type}] ${v.path}`).join('\n'), remediation: `celilo module verify ${result.moduleId} --json`, actionable: true, subject: result.moduleId, }); } for (const host of result.hostPlane?.findings ?? []) { if (host.state === 'converged') continue; findings.push({ category: 'module_integrity', severity: host.state === 'drift' ? 'drift' : 'unmeasured', code: host.state === 'drift' ? 'module_host_drifted' : 'module_host_unmeasured', message: `${result.moduleId} on ${host.hostname}: ${host.detail}`, remediation: host.state === 'drift' ? `celilo module deploy ${result.moduleId}` : `celilo module verify ${result.moduleId} --deep`, actionable: true, subject: result.moduleId, }); } } return findings; }