# 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__` 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 | |---|---|---| | `_measured_` | 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: }` | 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` - `.measured.` — 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** (`_measured_`) 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.