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>
53 lines
1.7 KiB
JavaScript
53 lines
1.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Reducer-shape stats helpers shared across the platform.
|
|
*
|
|
* These were duplicated as static helpers on `Channel` and as instance
|
|
* methods on the older `measurement/specificClass.js`. Consolidated here so
|
|
* any consumer (outlier detection, monster summaries, future analytics)
|
|
* can import a single canonical implementation.
|
|
*
|
|
* Stream-shape filters (low/high/band-pass, kalman, savitzky-golay) stay
|
|
* on Channel as static helpers — they're pipeline state, not reducers.
|
|
*/
|
|
|
|
function mean(arr) {
|
|
if (!arr.length) return 0;
|
|
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
}
|
|
|
|
// Sample std dev (n-1 denominator). A single sample has no variance to
|
|
// estimate, so we return 0 rather than NaN — callers (e.g. z-score) treat
|
|
// 0 as "no spread yet" and skip rejection.
|
|
function stdDev(arr) {
|
|
if (arr.length <= 1) return 0;
|
|
const m = mean(arr);
|
|
const variance = arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1);
|
|
return Math.sqrt(variance);
|
|
}
|
|
|
|
function median(arr) {
|
|
if (!arr.length) return 0;
|
|
const sorted = [...arr].sort((a, b) => a - b);
|
|
const mid = Math.floor(sorted.length / 2);
|
|
return sorted.length % 2 !== 0
|
|
? sorted[mid]
|
|
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
}
|
|
|
|
function mad(arr) {
|
|
if (!arr.length) return 0;
|
|
const med = median(arr);
|
|
return median(arr.map((v) => Math.abs(v - med)));
|
|
}
|
|
|
|
// Degenerate-range pass-through matches Channel._lerp: callers rely on it
|
|
// for early-warmup paths where input bounds haven't separated yet.
|
|
function lerp(value, iMin, iMax, oMin, oMax) {
|
|
if (iMin >= iMax) return value;
|
|
return oMin + ((value - iMin) * (oMax - oMin)) / (iMax - iMin);
|
|
}
|
|
|
|
module.exports = { mean, stdDev, median, mad, lerp };
|