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>
131 lines
9.1 KiB
Markdown
131 lines
9.1 KiB
Markdown
# 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 |
|
||
|---|---|---|---|---|
|
||
| `mode` | `mgc.mode` (set via `set.mode` command) | string ∈ {`optimalcontrol`, `prioritycontrol`, …} | commands.basic.test.js, ncog-distribution.integration.test.js | n/a — always set from constructor default |
|
||
| `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) | number m³/s ≥ 0 | totalsCalculator.basic, dashboard-fanout (post-setup) | absent until first equalize; dashboard-fanout (state A) |
|
||
| `flowCapacityMin` | `mgc.dynamicTotals.flow.min` | number m³/s ≥ 0 | totalsCalculator.basic | same as above |
|
||
| `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) |
|
||
|
||
### 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`.
|
||
|
||
---
|
||
|
||
## 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.
|