P2 wave 1: extract concerns from pumpingStation specificClass

Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.

  src/basin/         BasinGeometry + thresholdValidator (pure)
  src/measurement/   flowAggregator + measurementRouter + calibration
  src/control/       levelBased + flowBased(stub) + manual + index dispatcher
  src/safety/        safetyController split into dryRun + overfill rules
  src/commands/      registry array + handlers (canonical names from start)
  src/editor.js      260 lines of SVG basin-diagram redraw, was inline in .html
  examples/standalone-demo.js  was if(require.main===module) at bottom of specificClass.js
  CONTRACT.md        canonical inputs + outputs + emitted events

Modified:
  src/specificClass.js  removed the 170-line standalone demo block
  pumpingStation.html   oneditprepare/oneditsave delegate to editor.{init,save}
  pumpingStation.js     added admin endpoint serving src/editor.js

102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-10 20:18:49 +02:00
parent da50403c76
commit 7afcd6e54a
27 changed files with 2533 additions and 463 deletions

View File

@@ -0,0 +1,57 @@
// Threshold-ordering validator for the pumpingStation basin + control +
// safety config. Pure: returns the issues array, never logs or throws.
// The caller decides what to do (warn, surface to status badge, fail tests).
//
// Invariants enforced (level-space, bottom → top):
// 0 < outflowLevel < inflowLevel < overflowLevel ≤ basinHeight
// dryRunLevel ≤ minLevel ≤ startLevel < maxLevel ≤ overfillLevel
//
// dryRunLevel and overfillLevel are DERIVED from safety percentages — the
// validator recomputes them so a config that places minLevel below the
// effective dry-run trigger (a no-op control band) is caught here.
/**
* @param {object} basin - BasinGeometry instance OR plain {inflowLevel, outflowLevel, overflowLevel, heightBasin, minHeightBasedOn}
* @param {object} levelbased - config.control.levelbased ({ minLevel, startLevel, maxLevel })
* @param {object} safety - config.safety ({ dryRunThresholdPercent, overfillThresholdPercent })
* @returns {Array<{aName, a, op, bName, b, msg}>}
*/
function validateThresholdOrdering(basin, levelbased, safety) {
const lvl = levelbased || {};
const sfy = safety || {};
const dryRunPct = Number(sfy.dryRunThresholdPercent) || 0;
const overfillPct = Number(sfy.overfillThresholdPercent) || 100;
const refLowLevel = basin.minHeightBasedOn === 'inlet' ? basin.inflowLevel : basin.outflowLevel;
const dryRunLevel = refLowLevel * (1 + dryRunPct / 100);
const overfillLevel = basin.overflowLevel * (overfillPct / 100);
const checks = [
['outflowLevel', basin.outflowLevel, '<', 'inflowLevel', basin.inflowLevel],
['inflowLevel', basin.inflowLevel, '<', 'overflowLevel', basin.overflowLevel],
['overflowLevel', basin.overflowLevel, '<=', 'basinHeight', basin.heightBasin],
['dryRunLevel', dryRunLevel, '<=', 'minLevel', lvl.minLevel],
['minLevel', lvl.minLevel, '<=', 'startLevel', lvl.startLevel],
['startLevel', lvl.startLevel, '<', 'maxLevel', lvl.maxLevel],
['maxLevel', lvl.maxLevel, '<=', 'overfillLevel', overfillLevel],
];
const issues = [];
for (const [aName, a, op, bName, b] of checks) {
if (!Number.isFinite(a) || !Number.isFinite(b)) continue;
const ok = op === '<' ? a < b : a <= b;
if (!ok) {
issues.push({
aName,
a,
op,
bName,
b,
msg: `Threshold invariant violated: ${aName} (${a}) must be ${op} ${bName} (${b})`,
});
}
}
return issues;
}
module.exports = { validateThresholdOrdering };