Files
monster/src/parameters/parameters.js
znetsixe 0038a8c2c2 B1.4: cooldown-guard ROOT CAUSE — schema strip of constraint keys
Root cause: configUtils.initConfig was silently stripping the four
unknown constraint keys (nominalFlowMin, flowMax, maxRainRef,
minSampleIntervalSec) because they weren't declared in the schema.
With those keys gone, validateFlowBounds saw NaN/NaN and routed every
i_start into the invalid-bounds branch — _beginRun never fired, so
sumPuls stayed 0.

Fix is in generalFunctions/src/configs/monster.json (declared the
four keys with sensible defaults). Plus a 4-line comment at the guard
site documenting the schema dependency. 10/10 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:29:17 +02:00

61 lines
2.2 KiB
JavaScript

'use strict';
// Sampling-cabinet boundary + target math + rain-scaled flow prediction.
// All operations are pure given a domain handle — the domain owns the
// mutable fields (maxVolume, targetPuls, …) so legacy tests that read
// `monster.maxVolume` keep working.
const RAIN_STALE_MS = 2 * 60 * 60 * 1000;
function applyBoundsAndTargets(m) {
m.maxVolume = m.maxWeight - m.emptyWeightBucket;
m.minPuls = Math.round(m.minVolume / m.volume_pulse);
m.maxPuls = Math.round(m.maxVolume / m.volume_pulse);
m.absMaxPuls = Math.round(m.cap_volume / m.volume_pulse);
m.targetVolume = m.minVolume * Math.sqrt(m.maxVolume / m.minVolume);
m.targetPuls = Math.round(m.targetVolume / m.volume_pulse);
}
function validateFlowBounds(m) {
// nominalFlowMin / flowMax must be declared in monster.json's `constraints`
// section. If they aren't, configUtils strips them as unknown keys and the
// bounds collapse to undefined → NaN → invalid here, which silently blocks
// every sampling run. (Fixed 2026-05-11.)
const min = Number(m.nominalFlowMin);
const max = Number(m.flowMax);
const valid = Number.isFinite(min) && Number.isFinite(max) && min >= 0 && max > 0 && min < max;
m.invalidFlowBounds = !valid;
if (!valid) m.logger.warn(`Invalid flow bounds. nominalFlowMin=${m.nominalFlowMin}, flowMax=${m.flowMax}`);
return valid;
}
function getRainIndex(m) {
if (!m.lastRainUpdate) return 0;
if (Date.now() - m.lastRainUpdate > RAIN_STALE_MS) return 0;
return Number.isFinite(m.avgRain) ? m.avgRain : 0;
}
function getPredictedFlowRate(m) {
const min = Number(m.nominalFlowMin);
const max = Number(m.flowMax);
if (!Number.isFinite(min) || !Number.isFinite(max) || min < 0 || max <= 0 || min >= max) return 0;
const rainIndex = getRainIndex(m);
const scale = Math.max(0, Math.min(1, m.rainMaxRef > 0 ? rainIndex / m.rainMaxRef : 0));
return min + (max - min) * scale;
}
function getSampleCooldownMs(m) {
if (!m.lastSampleTime) return 0;
const remaining = (m.minSampleIntervalSec * 1000) - (Date.now() - m.lastSampleTime);
return Math.max(0, remaining);
}
module.exports = {
applyBoundsAndTargets,
validateFlowBounds,
getRainIndex,
getPredictedFlowRate,
getSampleCooldownMs,
RAIN_STALE_MS,
};