/** * HealthStatus — standardised health/quality datum. * Contract: see .claude/refactor/CONTRACTS.md §9. * * Shape (always frozen): * { level: 0|1|2|3, flags: string[], message: string, source: string|null } * * level 0 = nominal, 3 = unusable. Returned objects are frozen plain * objects (not class instances) so they round-trip cleanly through * JSON / InfluxDB serialisation. */ 'use strict'; const LABELS = ['nominal', 'minor', 'major', 'critical']; function _freeze(level, flags, message, source) { return Object.freeze({ level, flags: Object.freeze(flags.slice()), message, source: source == null ? null : String(source), }); } function _coerceDegradedLevel(level) { const n = Math.trunc(Number(level)); if (!Number.isFinite(n) || n < 1) return 1; if (n > 3) return 3; return n; } function _coerceFlags(flags) { if (!Array.isArray(flags)) return []; const out = []; for (const f of flags) { if (f == null) continue; out.push(String(f)); } return out; } function ok(message, source) { return _freeze( 0, [], typeof message === 'string' && message.length > 0 ? message : 'nominal', source != null ? source : null, ); } function degraded(level, flags, message, source) { const lvl = _coerceDegradedLevel(level); const f = _coerceFlags(flags); const m = typeof message === 'string' && message.length > 0 ? message : LABELS[lvl]; return _freeze(lvl, f, m, source != null ? source : null); } // Merge multiple statuses into one node-level status. Worst level wins // for level/message/source; flags are concatenated and de-duped. function compose(statuses) { if (!Array.isArray(statuses) || statuses.length === 0) return ok(); let worst = null; const seen = new Set(); const flags = []; for (const s of statuses) { if (!s || typeof s !== 'object') continue; const lvl = Number.isFinite(s.level) ? s.level : 0; if (worst === null || lvl > worst.level) { worst = { level: lvl, message: s.message, source: s.source ?? null }; } if (Array.isArray(s.flags)) { for (const f of s.flags) { if (f == null) continue; const k = String(f); if (!seen.has(k)) { seen.add(k); flags.push(k); } } } } if (worst === null) return ok(); const message = typeof worst.message === 'string' && worst.message.length > 0 ? worst.message : LABELS[Math.max(0, Math.min(3, worst.level))]; return _freeze(worst.level, flags, message, worst.source); } function label(level) { const n = Math.trunc(Number(level)); if (!Number.isFinite(n) || n < 0 || n > 3) return 'unknown'; return LABELS[n]; } module.exports = { ok, degraded, compose, label };