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,66 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { groupFlow, groupPower, groupNCog, groupCalcPower } = require('../../src/groupOps/groupCurves');
function predictView(min, max, current = (min + max) / 2) {
return {
currentF: current,
currentFxyYMin: min,
currentFxyYMax: max,
};
}
test('groupFlow returns the same shape as the original _groupFlow (groupPredictFlow preferred)', () => {
const machine = {
predictFlow: predictView(0, 1, 0.5),
groupPredictFlow: predictView(0.1, 0.9, 0.4),
};
const v = groupFlow(machine);
assert.equal(v, machine.groupPredictFlow);
assert.equal(v.currentFxyYMin, 0.1);
assert.equal(v.currentFxyYMax, 0.9);
assert.equal(v.currentF, 0.4);
});
test('groupFlow falls back to predictFlow when groupPredictFlow is absent', () => {
const machine = { predictFlow: predictView(0, 1) };
assert.equal(groupFlow(machine), machine.predictFlow);
});
test('groupPower returns groupPredictPower when present, else predictPower', () => {
const m1 = { predictPower: predictView(0, 100), groupPredictPower: predictView(10, 90) };
assert.equal(groupPower(m1), m1.groupPredictPower);
const m2 = { predictPower: predictView(0, 100) };
assert.equal(groupPower(m2), m2.predictPower);
});
test('groupNCog returns the group value when groupPredictFlow is present', () => {
const m = { groupPredictFlow: predictView(0, 1), groupNCog: 0.42, NCog: 0.99, predictFlow: predictView(0, 1) };
assert.equal(groupNCog(m), 0.42);
});
test('groupNCog falls back to NCog when no groupPredictFlow', () => {
const m = { predictFlow: predictView(0, 1), NCog: 0.7 };
assert.equal(groupNCog(m), 0.7);
});
test('groupNCog defaults to 0 when neither is defined', () => {
const m = { predictFlow: predictView(0, 1) };
assert.equal(groupNCog(m), 0);
});
test('groupCalcPower prefers machine.groupCalcPower', () => {
let lastFlow = null;
const m = {
groupCalcPower(flow) { lastFlow = flow; return flow * 2; },
inputFlowCalcPower(flow) { return flow * 999; },
};
assert.equal(groupCalcPower(m, 0.3), 0.6);
assert.equal(lastFlow, 0.3);
});
test('groupCalcPower falls back to inputFlowCalcPower when groupCalcPower missing', () => {
const m = { inputFlowCalcPower(flow) { return flow + 1; } };
assert.equal(groupCalcPower(m, 5), 6);
});