Files
machineGroupControl/test/_output-manifest.md
znetsixe f41e319b30 test(mgc): cover fn_status_split output 17 (% of capacity); fix stale 17→18 count
The dashboard fan-out grew to 18 outputs (output 17 = '% of capacity' chart)
but dashboard-fanout.integration.test.js still asserted 17 and had no PORT
entry or coverage for output 17. Add chart_pctcap (17) with populated (State C,
flow/capMax×100) and degraded (State A → null-drop) assertions, fix the count
assertion, and add the fan-out enumeration table to _output-manifest.md per
.claude/rules/output-coverage.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:24:22 +02:00

12 KiB
Raw Blame History

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; 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
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)
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)

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)

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.