/** * Health coverage — which deployed modules nothing is watching. * * A module that has never been successfully checked is not a neutral absence * of information. It is unverified infrastructure, and it fails in exactly the * way a broken module does, only later and with less warning. Once checks are * scheduled this becomes a durable condition rather than a transient one: a * monitored module resolves to healthy or failing within one interval, so * anything still unobserved is structurally blind. * * Two ways that happens, and both are worth saying out loud: * - the module ships no `health_check` hook, so it can never be verified; * - it has one, but nothing gives it a cadence, so nothing schedules it. * * An effective cadence of `manual` raises NOTHING. The check exists to catch * blindness nobody chose; `manual` is a recorded decision, visible in `module * status` and removable with one command. A finding raised by an operator's * explicit opt-out asks for the action they just declined, so no action of * theirs could ever clear it. * * Deliberately cheap: this reads the module roster against the monitor set and * contacts nothing. It is safe to run on every sweep. * * See openspec/changes/add-alerting/design.md D15. */ import type { ModuleState } from '../../db/schema'; import type { Cadence } from '../cadence'; import { isScheduled } from './health-cadence'; import { type FailingKey, builtinAlertKey } from './keys'; export const HEALTH_COVERAGE_CHECK = 'health_coverage'; /** * States in which a module is live enough to be worth watching. A module still * being imported or configured has nothing deployed to observe yet, so an * absent monitor there is expected rather than a gap. */ const DEPLOYED_STATES: ReadonlySet = new Set(['INSTALLED', 'VERIFIED']); export interface ModuleCoverageInput { id: string; state: ModuleState; /** Whether the manifest declares a `health_check` hook at all. */ hasHealthCheckHook: boolean; /** * The module's effective health-check cadence: `null` when nobody has named * one (a gap), `'manual'` when the operator opted out (a decision). */ cadence: Cadence | null; } /** * Findings are `warning` severity: a coverage gap is real, but it is not an * outage and must not wake anyone. It shows up in `celilo alerts` and in the * `celilo module list` health column, where it reads as "not observed" rather * than being silently rendered as healthy. */ export function healthCoverageFailingKeys(modules: ModuleCoverageInput[]): FailingKey[] { const failing: FailingKey[] = []; for (const module of modules) { if (!DEPLOYED_STATES.has(module.state)) continue; const key = builtinAlertKey(HEALTH_COVERAGE_CHECK, 'module', module.id); if (!module.hasHealthCheckHook) { failing.push({ key, severity: 'warning', message: `${module.id} declares no health_check hook — it can never be verified`, details: 'celilo has no way to confirm this module works. Add a health_check\n' + 'hook to its manifest, or accept that failures here surface only when\n' + 'something downstream breaks.', }); continue; } if (module.cadence === 'manual') continue; if (!isScheduled(module.cadence)) { failing.push({ key, severity: 'warning', message: `${module.id} has a health_check hook but nothing schedules it`, details: `The module can be verified on demand but is not being watched.\nGive it a cadence: celilo module config set ${module.id} health_check_interval 15m\nor set hooks.health_check.interval in the module manifest.`, }); } } return failing; }