Files
machineGroupControl/test/_output-manifest.md

181 lines
13 KiB
Markdown
Raw Normal View History

governance + unit-self-describing demand + dashboard fixes Two governance items from the 2026-05-14 quality review: - test/_output-manifest.md enumerates every Port 0/1/2 key MGC emits, its source, type, range, and which tests cover it in populated/degraded states (per .claude/rules/output-coverage.md). - src/control/strategies.js extracts computeEqualFlowDistribution as a pure function so the equal-flow algorithm is testable without an MGC fixture. test/basic/equalFlowDistribution.basic.test.js (6 tests) covers all three demand branches and pins the legacy quirk where the default branch counts active machines but iterates priority-ordered first-N (documented in the test so the future cleanup is a deliberate change). Plus rolled-up session work that landed alongside: - set.demand is now unit-self-describing ({value, unit:'m3/h'|'l/s'|'%'|...} or bare number = %); setScaling/scaling.current removed from MGC, commands, editor (mgc.html), specificClass. - _optimalControl + equalFlowControl now compute eta = (Q*dP)/P_shaft rather than Q/P, keeping the metric in the same scale as each child's cog. - groupEfficiency.calcRelativeDistanceFromPeak returns undefined (was 1) when pumps are homogeneous (|max-min| < 1e-9). Dashboard treats undefined as '-' instead of showing a misleading 100% / 0% reading. - examples/02-Dashboard.json: auto-init inject so the dashboard populates at deploy, NCog formatter normalizes the SUM emitted by MGC by machineCountActive, Q-H fanout trims the flat-Q tail so the H axis isn't stretched to 40m by curve-envelope clamp points, num/pct treat null AND undefined as no-data (closes the +null === 0 trap). - new test/integration/dashboard-fanout.integration.test.js (17 tests), bep-distance-demand-sweep.integration.test.js (3 tests), group-bep-cascade.integration.test.js -- total suite now 108/108 green. - .gitignore: wiki/test.gif (143 MB screen recording, kept locally only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:31:25 +02:00
# machineGroupControl — Output Manifest
Per `.claude/rules/output-coverage.md`. Single source of truth for what MGC
emits on Port 0/1/2, where the value comes from, and which test exercises it
in populated AND degraded states.
**Convention for missing values:** keys are **absent** when the underlying
source has not produced a value yet (pre-first-tick, no demand, no pressure).
Once produced, a key may be **explicitly null/undefined** only in the
documented degenerate cases below. The dashboard formatter must treat both
absent and null/undefined as "no data" (display `'—'`) — see the
`pct`/`num` helpers in `examples/02-Dashboard.json :: fn_status_split`.
---
## Port 0 — process data
Built by `src/io/output.js :: getOutput(mgc)`. Delta-compressed by
`outputUtils.formatMsg(..., 'process')` — only changed keys appear in each emit.
### Static fields (always emitted once MGC has been initialised)
| Key | Source | Type / Range | Populated test | Degraded test |
|---|---|---|---|---|
feat(mgc): rendezvous planner — same-time landing across all modes Routes every dispatch through a tick-aware planner so all pumps reach their setpoint at the same wall-clock instant t* = max(eta_i), regardless of control strategy or per-pump reaction speed. Architecture (src/movement/): - machineProfile.js – pure snapshot of a registered child (state, position, velocityPctPerS, ladder timings, flowAt / positionForFlow). Reads timings from child.state.config.time (the actual storage location — previous fallback paths silently produced 0 s, collapsing every eta to ramp-only). - moveTrajectory.js – seconds-to-target per machine; handles idle / starting / warmingup / operational / cooling. - movementScheduler.js – t* = max eta over ALL non-noop moves. Every command is delayed so its move finishes at t*. Startup execsequence fires at 0; its flowmovement is gated by max(ladderS, t* − rampS) so a fast pump waits before ramping rather than landing early. useRendezvous=false collapses to all fireAtTickN=0 (legacy fire-and-forget). - movementExecutor.js – wall-clock virtual cursor: each tick fires every command whose fireAtTickN ≤ floor(elapsed/tickS). tick() no longer awaits pending fireCommand promises — the synchronous prologue of handleInput claims the latest-wins gate, which is what race-favouring relies on. Shared dispatch path (src/specificClass.js): - _dispatchFlowDistribution(distribution) — extracted from _optimalControl. Builds profiles, calls movementScheduler.plan, replans the executor, ticks once. Reads config.planner.useRendezvous (default true). - _optimalControl computes its bestCombination and hands off. - equalFlowControl (priorityControl mode) computes its flowDistribution and hands off via ctx.mgc._dispatchFlowDistribution. Same-time landing now applies in BOTH modes. Editor toggle (mgc.html + src/nodeClass.js): - New "Same-time landing" checkbox under Control Strategy. - nodeClass.buildDomainConfig bridges uiConfig.useRendezvous → config.planner.useRendezvous. Default ON. Tests: - New: planner-convergence.integration.test.js (real-time end-to-end diagnostic — drives a 3-pump mixed-state dispatch and asserts both convergence to the demand setpoint AND same-time landing within one tick). - New: planner-rendezvous.integration.test.js (schedule-shape assertions against real pump objects). - New: movementScheduler.basic.test.js — includes a mixed-speed multi-startup case proving the fast pumps wait so all three land together (the regression that prompted this work). - New: movementExecutor.basic.test.js + moveTrajectory.basic.test.js. - Updated executor contract test: tick() must NOT await pending fires. Commands + wiki: - handlers.js: source/mode allow-list gate moved into a shared _gate() helper; every command now checks isValidActionForMode + isValidSourceForMode before dispatching. Status-level commands (set.mode, set.scaling) are allowed in every mode. - commands.basic.test.js: coverage for the new gate behaviour. - wiki regen: Home.md visual-first rewrite + Reference-{Architecture, Contracts,Examples,Limitations}.md split with _Sidebar.md index. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:43:55 +02:00
| `mode` | `mgc.mode` (set via `set.mode` command; normalised by `specificClass.setMode`) | string ∈ {`optimalControl`, `priorityControl`, `maintenance`} (canonical camelCase) | commands.basic.test.js, ncog-distribution.integration.test.js | n/a — always set from constructor default |
governance + unit-self-describing demand + dashboard fixes Two governance items from the 2026-05-14 quality review: - test/_output-manifest.md enumerates every Port 0/1/2 key MGC emits, its source, type, range, and which tests cover it in populated/degraded states (per .claude/rules/output-coverage.md). - src/control/strategies.js extracts computeEqualFlowDistribution as a pure function so the equal-flow algorithm is testable without an MGC fixture. test/basic/equalFlowDistribution.basic.test.js (6 tests) covers all three demand branches and pins the legacy quirk where the default branch counts active machines but iterates priority-ordered first-N (documented in the test so the future cleanup is a deliberate change). Plus rolled-up session work that landed alongside: - set.demand is now unit-self-describing ({value, unit:'m3/h'|'l/s'|'%'|...} or bare number = %); setScaling/scaling.current removed from MGC, commands, editor (mgc.html), specificClass. - _optimalControl + equalFlowControl now compute eta = (Q*dP)/P_shaft rather than Q/P, keeping the metric in the same scale as each child's cog. - groupEfficiency.calcRelativeDistanceFromPeak returns undefined (was 1) when pumps are homogeneous (|max-min| < 1e-9). Dashboard treats undefined as '-' instead of showing a misleading 100% / 0% reading. - examples/02-Dashboard.json: auto-init inject so the dashboard populates at deploy, NCog formatter normalizes the SUM emitted by MGC by machineCountActive, Q-H fanout trims the flat-Q tail so the H axis isn't stretched to 40m by curve-envelope clamp points, num/pct treat null AND undefined as no-data (closes the +null === 0 trap). - new test/integration/dashboard-fanout.integration.test.js (17 tests), bep-distance-demand-sweep.integration.test.js (3 tests), group-bep-cascade.integration.test.js -- total suite now 108/108 green. - .gitignore: wiki/test.gif (143 MB screen recording, kept locally only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:31:25 +02:00
| `scaling` | `mgc.scaling` | string ∈ {`absolute`, `normalized`} or undefined | commands.basic.test.js | dashboard-fanout (undefined → raw-rows shows '—') |
| `absDistFromPeak` | `groupEfficiency.calcDistanceFromPeak` (specificClass.js:132) | number ≥ 0 (η-points) | bep-distance-demand-sweep, group-bep-cascade, groupEfficiency.basic | groupEfficiency.basic test 7 (undefined when current = null) |
| `relDistFromPeak` | `groupEfficiency.calcRelativeDistanceFromPeak` | number ∈ [0,1] **OR `undefined`** for degenerate (homogeneous pumps) | bep-distance-demand-sweep, group-bep-cascade | groupEfficiency.basic tests 5/6/7 (undefined cases), dashboard-fanout test 11 (undefined → '—' display) |
| `flowCapacityMax` | `mgc.dynamicTotals.flow.max` (totalsCalculator), **converted to `unitPolicy.output.flow` (m³/h)** in output.js:62 | number m³/h ≥ 0; `0` when envelope unresolved (Infinity/NaN) | totalsCalculator.basic, dashboard-fanout (post-setup), demand-telemetry.basic | absent until first equalize; dashboard-fanout (state A); demand-telemetry (Infinity → 0) |
| `flowCapacityMin` | `mgc.dynamicTotals.flow.min`, **converted to output flow unit (m³/h)** | number m³/h ≥ 0; `0` when unresolved | totalsCalculator.basic, demand-telemetry.basic | same as above |
| `demandFlow` | `mgc._lastDemand.clamped` (set in `_runDispatch`, output.js:62) | number, canonical m³/s clamped to envelope, converted to `unitPolicy.output.flow` | demand-telemetry.basic (populated) | demand-telemetry.basic (absent before first demand); turnOff → 0 |
| `demandPct` | derived `(clamped flow.min)/(flow.max flow.min)·100` (output.js:62) | number ∈ [0,100], `0` when capacity span ≤ 0 | demand-telemetry.basic (populated) | demand-telemetry.basic (absent before first demand) |
governance + unit-self-describing demand + dashboard fixes Two governance items from the 2026-05-14 quality review: - test/_output-manifest.md enumerates every Port 0/1/2 key MGC emits, its source, type, range, and which tests cover it in populated/degraded states (per .claude/rules/output-coverage.md). - src/control/strategies.js extracts computeEqualFlowDistribution as a pure function so the equal-flow algorithm is testable without an MGC fixture. test/basic/equalFlowDistribution.basic.test.js (6 tests) covers all three demand branches and pins the legacy quirk where the default branch counts active machines but iterates priority-ordered first-N (documented in the test so the future cleanup is a deliberate change). Plus rolled-up session work that landed alongside: - set.demand is now unit-self-describing ({value, unit:'m3/h'|'l/s'|'%'|...} or bare number = %); setScaling/scaling.current removed from MGC, commands, editor (mgc.html), specificClass. - _optimalControl + equalFlowControl now compute eta = (Q*dP)/P_shaft rather than Q/P, keeping the metric in the same scale as each child's cog. - groupEfficiency.calcRelativeDistanceFromPeak returns undefined (was 1) when pumps are homogeneous (|max-min| < 1e-9). Dashboard treats undefined as '-' instead of showing a misleading 100% / 0% reading. - examples/02-Dashboard.json: auto-init inject so the dashboard populates at deploy, NCog formatter normalizes the SUM emitted by MGC by machineCountActive, Q-H fanout trims the flat-Q tail so the H axis isn't stretched to 40m by curve-envelope clamp points, num/pct treat null AND undefined as no-data (closes the +null === 0 trap). - new test/integration/dashboard-fanout.integration.test.js (17 tests), bep-distance-demand-sweep.integration.test.js (3 tests), group-bep-cascade.integration.test.js -- total suite now 108/108 green. - .gitignore: wiki/test.gif (143 MB screen recording, kept locally only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:31:25 +02:00
| `machineCount` | `Object.keys(mgc.machines).length` | integer ≥ 0 | demand-cycle-walkthrough, ncog-distribution | n/a — always reflects current registration count |
| `machineCountActive` | filtered count excluding `off`/`maintenance` states | integer ≥ 0 | demand-cycle-walkthrough, ncog-distribution | dashboard-fanout (state A: 0 active) |
| `movementState` | `mgc.getMovementState()` (specificClass) — `'working'` while any child is ramping/sequencing or the executor has pending commands, else `'ready'` | string `'working'`\|`'ready'`, never null | movement-gate.basic (working: accelerating/warmingup/delayedMove/moveTimeLeft/executor-pending) | movement-gate.basic (ready: no machines, all settled) |
governance + unit-self-describing demand + dashboard fixes Two governance items from the 2026-05-14 quality review: - test/_output-manifest.md enumerates every Port 0/1/2 key MGC emits, its source, type, range, and which tests cover it in populated/degraded states (per .claude/rules/output-coverage.md). - src/control/strategies.js extracts computeEqualFlowDistribution as a pure function so the equal-flow algorithm is testable without an MGC fixture. test/basic/equalFlowDistribution.basic.test.js (6 tests) covers all three demand branches and pins the legacy quirk where the default branch counts active machines but iterates priority-ordered first-N (documented in the test so the future cleanup is a deliberate change). Plus rolled-up session work that landed alongside: - set.demand is now unit-self-describing ({value, unit:'m3/h'|'l/s'|'%'|...} or bare number = %); setScaling/scaling.current removed from MGC, commands, editor (mgc.html), specificClass. - _optimalControl + equalFlowControl now compute eta = (Q*dP)/P_shaft rather than Q/P, keeping the metric in the same scale as each child's cog. - groupEfficiency.calcRelativeDistanceFromPeak returns undefined (was 1) when pumps are homogeneous (|max-min| < 1e-9). Dashboard treats undefined as '-' instead of showing a misleading 100% / 0% reading. - examples/02-Dashboard.json: auto-init inject so the dashboard populates at deploy, NCog formatter normalizes the SUM emitted by MGC by machineCountActive, Q-H fanout trims the flat-Q tail so the H axis isn't stretched to 40m by curve-envelope clamp points, num/pct treat null AND undefined as no-data (closes the +null === 0 trap). - new test/integration/dashboard-fanout.integration.test.js (17 tests), bep-distance-demand-sweep.integration.test.js (3 tests), group-bep-cascade.integration.test.js -- total suite now 108/108 green. - .gitignore: wiki/test.gif (143 MB screen recording, kept locally only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:31:25 +02:00
### Conditional pressure-header fields (emitted only when equalize resolved a positive ΔP)
| Key | Source | Type / Range | Populated test | Degraded test |
|---|---|---|---|---|
| `headerDiffPa` | `mgc.operatingPoint.headerDiffPa` (groupOperatingPoint.equalize) | number Pa > 0 | groupOperatingPoint.basic, dashboard-fanout (state B/C) | dashboard-fanout (state A — absent) |
| `headerDiffMbar` | derived `headerDiffPa / 100` when `unitPolicy.output.pressure === 'mbar'` | number mbar > 0 | dashboard-fanout (state B/C) | absent when output pressure unit ≠ mbar — **not explicitly tested** |
### Dynamic measurement fields — pattern `{position}_{variant}_{type}`
Built by the loop at `io/output.js:23-39`. For each type×variant×position the
container holds, one key is emitted **only if the value is non-null**.
Positions: `downstream`, `upstream`, `atEquipment`. Plus `differential_<variant>_<type>` when both `downstream` and `upstream` exist.
**Predicted measurements MGC writes itself (via writeOwn):**
| Key | Source (write site) | Type / Range | Populated test | Degraded test |
|---|---|---|---|---|
| `atEquipment_predicted_flow` | `handlePressureChange` (specificClass:153), `_optimalControl` (specificClass:214), `equalFlowControl` (control/strategies:118), `turnOffAllMachines` (specificClass:297) | number, canonical m³/s converted to `unitPolicy.output.flow` | bep-distance-demand-sweep, dashboard-fanout (state B/C), ncog-distribution | dashboard-fanout (state A: absent), turnoff-deadlock (post-shutdown = 0) |
| `downstream_predicted_flow` | `handlePressureChange` (specificClass:156 — mirrors AT_EQUIPMENT for PS contract), `turnOffAllMachines` (specificClass:296) | same as above | implicit in bep-distance-demand-sweep getOutput | turnoff-deadlock (post-shutdown = 0) |
| `atEquipment_predicted_power` | same call sites as flow (specificClass:157, 213; strategies:117; specificClass:298) | number, canonical W converted to `unitPolicy.output.power` | bep-distance-demand-sweep, dashboard-fanout, distribution-power-table | turnoff-deadlock (= 0) |
| `atEquipment_predicted_efficiency` | `_optimalControl` (specificClass:221), `equalFlowControl` (strategies:122) — only when `dP > 0 && bestPower > 0` | number ∈ [0, 1] hydraulic η = (Q·ΔP)/P | bep-distance-demand-sweep, dashboard-fanout (state C) | **absent** when dP ≤ 0 or bestPower ≤ 0 — guarded but not explicitly tested |
| `atEquipment_predicted_Ncog` | `_optimalControl` (specificClass:224), `equalFlowControl` (strategies:125) | number, range **0..N where N = active pumps** (SUM of per-pump NCog from `bepGravitation.js:162` totalCog) — NOT 0..1; see [[project-mgc-bep-metrics-semantics]] | ncog-distribution (9 tests), bep-distance-demand-sweep, dashboard-fanout (state C) | dashboard-fanout normalizes by `machineCountActive` for display — tests 6/7/8/9/10 |
**Measured pressures forwarded from children:**
MGC subscribes to each registered measurement child (specificClass.js:91-104)
and re-emits the child's reading on its own `MeasurementContainer`. If a
pressure measurement child registers at position `downstream`, MGC will
emit `downstream_measured_pressure` on Port 0 the next time `getOutput` runs.
| Key pattern | Source | Tests |
|---|---|---|
| `<position>_measured_<type>` | child measurement node forwarded via `MeasurementContainer.emitter` (specificClass:91-105) | indirect — group-bep-cascade.integration drives pressure events through registered children; not asserted as a named output key |
| `differential_measured_pressure` | computed when both `downstream_measured_pressure` and `upstream_measured_pressure` exist (output.js:33-37) | indirect via dashboard-fanout (used by fn_qh_point for header ΔP fallback) |
---
## Port 1 — InfluxDB telemetry
Built by `outputUtils.formatMsg(..., 'influxdb')` — same `getOutput` source,
different formatter. Emits the same key set as Port 0 with InfluxDB
line-protocol tag/field discipline (cardinality rules per `.claude/rules/telemetry.md`).
| Concern | Status |
|---|---|
| Keys | Identical to Port 0; the influxdb formatter (`generalFunctions/src/helper/formatters/influxdbFormatter.js`) decides which become tags vs fields. |
| Test coverage | **None.** No test file imports/asserts the influxdb formatter for MGC. Regression vector if a key is added/renamed without checking cardinality. Tracked. |
---
## Port 2 — registration / control plumbing
Emitted on startup by `BaseNodeAdapter` (one message per node).
| Topic | Payload shape | Source | Tests |
|---|---|---|---|
| `registerChild` | `{ id: node.id, positionVsParent: <string> }` | BaseNodeAdapter init — sends to upstream parent so it can subscribe to this node's measurements | structure-examples.integration, commands.basic.test.js test 5 (`child.register`) — receiver side |
---
## Events emitted on `mgc.source.measurements.emitter`
These are NOT Port 0/1/2 emissions — they're in-process events that downstream
EVOLV nodes (e.g., pumpingStation) subscribe to via the parent-child handshake.
Listed here for completeness; covered by `.claude/rules/telemetry.md` rather
than this manifest.
- `flow.predicted.atequipment` — fired on every `writeOwn` to flow/predicted/AT_EQUIPMENT
- `flow.predicted.downstream` — fired on every `writeOwn` to flow/predicted/DOWNSTREAM (the live aggregate the PS subscribes to)
- `power.predicted.atequipment`
- `efficiency.predicted.atequipment`
- `Ncog.predicted.atequipment`
- `<type>.measured.<position>` — re-emit of any registered measurement child
Documented in `CONTRACT.md`; tested indirectly via `group-bep-cascade.integration.test.js` and `ncog-distribution.integration.test.js`.
---
## Example flow fan-out — `examples/02-Dashboard.json :: fn_status_split` (outputs: 18)
Delta-caches Port 0 then fans one msg per dashboard widget. Charts return the
whole msg as `null` (drop the output) when their source is missing — never
`{ payload: null }`. All ports covered by `test/integration/dashboard-fanout.integration.test.js`.
| # | Target widget | Topic / payload | Populated | Degraded (missing source) |
|---|---|---|---|---|
| 0 | ui_txt_mode | string | ✔ State C | ✔ State A → mode string |
| 1 | ui_txt_flow | `'… m³/h'` | ✔ | ✔ State A → `—` |
| 2 | ui_txt_power | `'… kW'` | ✔ | ✔ → `—` |
| 3 | ui_txt_capacity | `'min max m³/h'` | ✔ State B | ✔ → `—` |
| 4 | ui_txt_machines | `'nAct / nTot'` | ✔ | ✔ → `—` |
| 5 | ui_txt_bep (rel%) | `'… %'` | ✔ | ✔ null/undefined → `—` |
| 6 | ui_txt_eta | `'… %'` | ✔ | ✔ → `—` |
| 7 | ui_txt_eta_peak | `'… %'` | ✔ | ✔ → `—` |
| 8 | ui_txt_bep_abs | `'…'` (η pts, 3dp) | ✔ | ✔ → `—` |
| 9 | ui_txt_ncog | `'… %'` (sum/nAct) | ✔ | ✔ nAct=0/missing → `—` |
| 10 | ui_chart_flow | `{topic:'Flow', payload:number}` | ✔ | ✔ → null (drop) |
| 11 | ui_chart_flow (capacity) | `{topic:'Capacity', …}` | ✔ | ✔ → null |
| 12 | ui_chart_power | `{topic:'Power', …}` | ✔ | ✔ → null |
| 13 | ui_chart_bep | `{topic:'BEP rel %', ×100}` | ✔ | ✔ → null |
| 14 | ui_chart_eta | `{topic:'η (%)', ×100}` | ✔ | ✔ → null |
| 15 | ui_tpl_raw | `[{key,value}]` rows | ✔ | ✔ |
| 16 | ui_chart_qh (passthrough) | raw `msg.payload` | ✔ | ✔ |
| 17 | ui_chart_mgc_pctcap | `{topic:'% of capacity', payload:flow/capMax×100}` | ✔ State C | ✔ State A → null (drop) |
## Example flow fan-out — `examples/02-Dashboard.json :: fn_chart_pump_a/b/c` (outputs: 2 each)
Each per-pump fan-out delta-caches the pump's Port 0 then emits two chart msgs.
The ctrl output carries a **-1 OFF sentinel**: when the cached pump `state` is
`off` / `idle` / `maintenance` the pump is not running, so it plots `-1` (below
the 0100 band) — a clear OFF rail distinct from a pump genuinely running at 0%.
`ui_chart_pumps_ctrl` has `ymin: "-5"` so the sentinel is visible. Charts return
the whole msg as `null` (drop the output) when their source is missing — never
`{ payload: null }`. All ports covered by
`test/integration/per-pump-ctrl-fanout.integration.test.js`.
| # | Target chart | Topic / payload | Populated | Degraded |
|---|---|---|---|---|
| 0 | ui_chart_per_pump_flow | `{topic:'Pump A/B/C', payload:flow m³/h}` | ✔ running state | ✔ no `flow.predicted.downstream.*` key → null (drop) |
| 1 | ui_chart_pumps_ctrl | `{topic:'Pump A/B/C', payload:ctrl%}`, or `payload:-1` when state ∈ {off,idle,maintenance} | ✔ running → +ctrl; ✔ off/idle/maintenance → -1 | ✔ no state + ctrl missing/NaN/null → null (drop); ✔ ctrl-only delta keeps cached OFF state |
`fn_chart_total` (outputs: 1) feeds the same flow chart with the group total
(`downstream_predicted_flow ?? atEquipment_predicted_flow`); returns `null` when
both are absent.
governance + unit-self-describing demand + dashboard fixes Two governance items from the 2026-05-14 quality review: - test/_output-manifest.md enumerates every Port 0/1/2 key MGC emits, its source, type, range, and which tests cover it in populated/degraded states (per .claude/rules/output-coverage.md). - src/control/strategies.js extracts computeEqualFlowDistribution as a pure function so the equal-flow algorithm is testable without an MGC fixture. test/basic/equalFlowDistribution.basic.test.js (6 tests) covers all three demand branches and pins the legacy quirk where the default branch counts active machines but iterates priority-ordered first-N (documented in the test so the future cleanup is a deliberate change). Plus rolled-up session work that landed alongside: - set.demand is now unit-self-describing ({value, unit:'m3/h'|'l/s'|'%'|...} or bare number = %); setScaling/scaling.current removed from MGC, commands, editor (mgc.html), specificClass. - _optimalControl + equalFlowControl now compute eta = (Q*dP)/P_shaft rather than Q/P, keeping the metric in the same scale as each child's cog. - groupEfficiency.calcRelativeDistanceFromPeak returns undefined (was 1) when pumps are homogeneous (|max-min| < 1e-9). Dashboard treats undefined as '-' instead of showing a misleading 100% / 0% reading. - examples/02-Dashboard.json: auto-init inject so the dashboard populates at deploy, NCog formatter normalizes the SUM emitted by MGC by machineCountActive, Q-H fanout trims the flat-Q tail so the H axis isn't stretched to 40m by curve-envelope clamp points, num/pct treat null AND undefined as no-data (closes the +null === 0 trap). - new test/integration/dashboard-fanout.integration.test.js (17 tests), bep-distance-demand-sweep.integration.test.js (3 tests), group-bep-cascade.integration.test.js -- total suite now 108/108 green. - .gitignore: wiki/test.gif (143 MB screen recording, kept locally only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:31:25 +02:00
## Coverage gaps (open items)
These are known holes flagged during the 2026-05-14 governance review; not yet
fixed but documented so they don't regress silently.
1. **Port 1 (InfluxDB) has no dedicated tests.** Any rename of a Port 0 key
should add an explicit Port 1 assertion to prevent silent cardinality
regressions.
2. **`headerDiffMbar` only emitted when `unitPolicy.output.pressure === 'mbar'`.**
The fallback (non-mbar configurations) isn't explicitly tested.
3. **`atEquipment_predicted_efficiency` absent-state isn't asserted.** The
`dP > 0 && bestPower > 0` guard exists but no test pins the absence.
4. **Forwarded measured measurements** (`<position>_measured_<type>`) aren't
asserted as named output keys — only their underlying behaviour is exercised.
5. **`scaling` undefined behaviour** — schema removed `scaling.current` for
several modes; what MGC emits for those is implicit, not tested.
When any of these is closed, move the row up into the appropriate table and
delete the entry here.