Phase 1 wave 1: domain + nodered + stats infra (additive)
Adds platform infrastructure used by the upcoming refactor of
nodeClass / specificClass across all 12 nodes:
- src/domain/UnitPolicy.js — extracted from rotatingMachine/MGC
- src/domain/ChildRouter.js — declarative event routing on top of childRegistrationUtils
- src/domain/LatestWinsGate.js — extracted from MGC dispatch gate
- src/domain/HealthStatus.js — standardised {level, flags, message, source}
- src/nodered/statusBadge.js — compose / error / idle / byState / text helpers
- src/stats/index.js — mean / stdDev / median / mad / lerp
All additive — no existing exports change shape.
56 unit tests pass under node:test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
src/domain/HealthStatus.js
Normal file
102
src/domain/HealthStatus.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user