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:
znetsixe
2026-05-10 22:09:25 +02:00
parent 5a43f90569
commit 2a6a0bc34b
12 changed files with 710 additions and 1075 deletions

70
src/io/output.js Normal file
View 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
View 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 };