P6: convert monster to BaseDomain + BaseNodeAdapter + concern split
Refactor of monster to use the platform infrastructure (BaseDomain, BaseNodeAdapter, ChildRouter, commandRegistry, statusBadge). Extracts concerns into focused modules per .claude/refactor/MODULE_SPLIT.md generic template. Tests stay green; CONTRACT.md generated; legacy aliases preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
src/io/output.js
Normal file
70
src/io/output.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
// Output formatter — assembles the snapshot shape getOutput returns each
|
||||
// tick. Heavy on derived fields (timeToNextPulse, targetDelta, ...) but
|
||||
// every value is read-only on the domain, so this can stay a pure function.
|
||||
|
||||
const params = require('../parameters/parameters');
|
||||
|
||||
function buildOutput(m) {
|
||||
const output = m.measurements.getFlattenedOutput();
|
||||
const flowRate = Number(m.q) || 0;
|
||||
const m3PerPulse = Number(m.m3PerPuls) || 0;
|
||||
const pulseFraction = Number(m.temp_pulse) || 0;
|
||||
const targetVolumeL = Number(m.targetVolume) > 0 ? m.targetVolume : 0;
|
||||
const targetVolumeM3 = targetVolumeL > 0 ? targetVolumeL / 1000 : 0;
|
||||
const flowToNextPulseM3 = m3PerPulse > 0 ? Math.max(0, (1 - pulseFraction) * m3PerPulse) : 0;
|
||||
const timeToNextPulseSec = flowRate > 0 && flowToNextPulseM3 > 0
|
||||
? Math.round((flowToNextPulseM3 / (flowRate / 3600)) * 100) / 100
|
||||
: 0;
|
||||
const targetProgressPct = targetVolumeL > 0
|
||||
? Math.round((m.bucketVol / targetVolumeL) * 10000) / 100
|
||||
: 0;
|
||||
const targetDeltaL = targetVolumeL > 0
|
||||
? Math.round((m.bucketVol - targetVolumeL) * 100) / 100
|
||||
: 0;
|
||||
const targetDeltaM3 = targetVolumeL > 0
|
||||
? Math.round((targetDeltaL / 1000) * 10000) / 10000
|
||||
: 0;
|
||||
|
||||
Object.assign(output, {
|
||||
pulse: m.pulse,
|
||||
running: m.running,
|
||||
bucketVol: m.bucketVol,
|
||||
bucketWeight: m.bucketWeight,
|
||||
sumPuls: m.sumPuls,
|
||||
predFlow: m.predFlow,
|
||||
predM3PerSec: m.predM3PerSec,
|
||||
timePassed: m.timePassed,
|
||||
timeLeft: m.timeLeft,
|
||||
m3Total: m.m3Total,
|
||||
q: m.q,
|
||||
nominalFlowMin: m.nominalFlowMin,
|
||||
flowMax: m.flowMax,
|
||||
invalidFlowBounds: m.invalidFlowBounds,
|
||||
minSampleIntervalSec: m.minSampleIntervalSec,
|
||||
missedSamples: m.missedSamples,
|
||||
sampleCooldownMs: params.getSampleCooldownMs(m),
|
||||
maxVolume: m.maxVolume,
|
||||
minVolume: m.minVolume,
|
||||
nextDate: m.nextDate,
|
||||
daysPerYear: m.daysPerYear,
|
||||
m3PerPuls: m.m3PerPuls,
|
||||
m3PerPulse: m.m3PerPuls,
|
||||
pulsesRemaining: Math.max(0, (m.targetPuls || 0) - (m.sumPuls || 0)),
|
||||
pulseFraction,
|
||||
flowToNextPulseM3,
|
||||
timeToNextPulseSec,
|
||||
targetVolumeM3,
|
||||
targetProgressPct,
|
||||
targetDeltaL,
|
||||
targetDeltaM3,
|
||||
predictedRateM3h: params.getPredictedFlowRate(m),
|
||||
sumRain: m.rainAggregator?.sumRain ?? 0,
|
||||
avgRain: m.rainAggregator?.avgRain ?? 0,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
module.exports = { buildOutput };
|
||||
28
src/io/statusBadge.js
Normal file
28
src/io/statusBadge.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
// Status-badge composition. Three states the editor cares about:
|
||||
// - red ring : config error (flow bounds invalid)
|
||||
// - yellow ring: sampling but cooldown is gating the next pulse
|
||||
// - green dot : sampling normally
|
||||
// - grey ring : idle
|
||||
// Shape mirrors the legacy nodeClass._updateNodeStatus output verbatim.
|
||||
|
||||
const { statusBadge } = require('generalFunctions');
|
||||
const params = require('../parameters/parameters');
|
||||
|
||||
function buildStatusBadge(m) {
|
||||
if (m.invalidFlowBounds) {
|
||||
return statusBadge.error(`Config error: nominalFlowMin (${m.nominalFlowMin}) >= flowMax (${m.flowMax})`);
|
||||
}
|
||||
if (m.running) {
|
||||
const levelText = `${m.bucketVol}/${m.maxVolume} L`;
|
||||
const cooldownMs = params.getSampleCooldownMs(m);
|
||||
if (cooldownMs > 0) {
|
||||
return statusBadge.compose([`SAMPLING (${Math.ceil(cooldownMs / 1000)}s)`, levelText], { fill: 'yellow', shape: 'ring' });
|
||||
}
|
||||
return statusBadge.compose([`AI: RUNNING`, levelText], { fill: 'green', shape: 'dot' });
|
||||
}
|
||||
return statusBadge.idle('AI: IDLE');
|
||||
}
|
||||
|
||||
module.exports = { buildStatusBadge };
|
||||
Reference in New Issue
Block a user