P4 wave 1: extract MGC concerns into focused modules

src/groupOps/        groupOperatingPoint + groupCurves (pure functions)
  src/totals/          totalsCalculator (dynamic + absolute + active)
  src/combinatorics/   pumpCombinations (validPumpCombinations + checkSpecialCases)
  src/optimizer/       bestCombination (CoG) + bepGravitation (BEP-G + marginal-cost)
  src/efficiency/      groupEfficiency (calc + distance helpers)
  src/dispatch/        demandDispatcher (LatestWinsGate-based; replaces
                       _dispatchInFlight + _delayedCall)
  src/commands/        canonical names from start (set.mode/scaling/demand,
                       child.register) + legacy aliases
  CONTRACT.md          inputs/outputs/events surface

53 basic tests pass (52 new + 1 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P4 wave 2.

Findings flagged via agents (TODO append to OPEN_QUESTIONS.md):
  - calcGroupEfficiency.maxEfficiency is actually the mean (misleading name)
  - checkSpecialCases has a no-op `return false` inside forEach
  - MGC doesn't route cmd.startup/shutdown/estop — confirm if station broadcasts need it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-10 20:45:23 +02:00
parent ea2857fb25
commit 619b1311d2
21 changed files with 1895 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
'use strict';
const { LatestWinsGate } = require('generalFunctions');
// Thin wrapper around LatestWinsGate for the MGC demand path. Replaces
// the original `_dispatchInFlight` + `_delayedCall` pair in
// specificClass.handleInput: a new demand arriving while a dispatch is
// in flight overwrites any pending one, so the latest value always wins
// and intermediates are dropped silently.
class DemandDispatcher {
constructor(ctx = {}, runFn) {
if (typeof runFn !== 'function') {
throw new TypeError('DemandDispatcher requires a runFn');
}
this.ctx = ctx;
this.logger = ctx.logger || null;
this._runFn = runFn;
this._gate = new LatestWinsGate(
async (demand) => this._runFn(demand, this.ctx),
{ logger: this.logger },
);
}
fire(demand) {
this._gate.fire(demand);
}
drain() {
return this._gate.drain();
}
get inFlight() {
return this._gate.size > 0;
}
}
module.exports = DemandDispatcher;