diff --git a/CONTRACT.md b/CONTRACT.md index 36d5561..6652332 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -25,8 +25,9 @@ Aliases log a one-time deprecation warning the first time they fire. - **Port 1 (InfluxDB telemetry):** same shape as Port 0, formatted with the `'influxdb'` formatter. - **Port 2 (registration):** at startup the node sends one - `{ topic: 'registerChild', payload: , positionVsParent, distance }` - to the upstream parent. + `{ topic: 'child.register', payload: , positionVsParent, distance }` + to the upstream parent (`child.register` is canonical; `registerChild` is the + deprecated *input* alias, not what this node emits). ## Events emitted by `source.measurements.emitter` diff --git a/test/_output-manifest.md b/test/_output-manifest.md new file mode 100644 index 0000000..2157a34 --- /dev/null +++ b/test/_output-manifest.md @@ -0,0 +1,101 @@ +# pumpingStation output manifest + +> Single source of truth for **what this node emits and where it is tested**, per +> [`.claude/rules/output-coverage.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/output-coverage.md). +> Generated against code-ref `a83a85e`. Regenerate the wiki contract with +> `npm run wiki:all` and re-check this table whenever `getOutput()`, +> `src/commands/index.js`, or an `examples/*.json` fan-out changes. + +**Null convention for this node:** a Port-0 key whose source is not yet +available is emitted as **explicit `null`** (e.g. `timeleft`, `flowSource`, +`manualDemand` outside manual mode), never silently absent. Delta-compression on +Port 0 then drops keys whose value is unchanged since the previous tick. + +## Port 0 (process data) — `specificClass.getOutput()` → `outputUtils.formatMsg(..., 'process')` + +`msg.topic = config.general.name`. Keys below are the full pre-delta-compression set. + +| Key | Source | Type | States tested | Test file | +|---|---|---|---|---| +| `mode` | `getOutput` ← `this.mode` | string (`levelbased`/`manual`/`flowbased`/`none`) | populated (`manual`) | test/basic/specificClass.test.js | +| `manualDemand` | `getOutput` ← `_manualDemand` | number m³/h, `null` outside manual | populated, null | test/basic/specificClass.test.js | +| `direction` | `getOutput` ← `state.direction` | string (`filling`/`draining`/`steady`) | present | test/basic/specificClass.test.js | +| `flowSource` | `getOutput` ← `state.flowSource` | string, `null` when no source | null (pre-child) | test/basic/specificClass.test.js | +| `timeleft` | `getOutput` ← `state.seconds` | number s, `null` when steady | present, null | test/basic/specificClass.test.js | +| `percControl` | `getOutput` ← `controlState.percControl` | number % 0..100 | 0, 25, 50, 75, 85, 100 | test/basic/specificClass.test.js | +| `dryRunLevel` | `_computeSafetyPoints` | number m | populated | test/basic/specificClass.test.js | +| `dryRunSafetyVol` | `_computeSafetyPoints` | number m³ | populated | test/basic/specificClass.test.js | +| `highVolumeSafetyLevel` | `_computeSafetyPoints` | number m | populated | test/basic/specificClass.test.js | +| `highVolumeSafetyVol` | `_computeSafetyPoints` | number m³ | populated | test/basic/specificClass.test.js | +| `predictedOverflowVolume` | `measurements` overflowVolume | number m³ | populated, 0 | test/basic/specificClass.test.js | +| `predictedOverflowRate` | `measurements` flow.overflow | number m³/s | populated, 0 | test/basic/specificClass.test.js | +| `predictedUnderflowVolume` | `measurements` underflowVolume | number m³ | 0 | test/basic/specificClass.test.js | +| `volume.predicted.atequipment.` | `measurements.getFlattenedOutput` | number m³ | populated | test/basic/specificClass.test.js | +| basin geometry: `heightBasin`, `surfaceArea`, `maxVol`, `minVol`, `maxVolAtOverflow`, `minVolAtInflow`, `minVolAtOutflow`, `volEmptyBasin`, `inflowLevel`, `outflowLevel`, `overflowLevel`, `inletPipeDiameter`, `outletPipeDiameter`, `minHeightBasedOn` | `basin.snapshot()` | number (m/m²/m³) / string | populated | test/basic/specificClass.test.js, test/basic/BasinGeometry.basic.test.js | + +## Port 1 (InfluxDB telemetry) — `formatMsg(..., 'influxdb')` + +Same key set as Port 0 (formatted via the `influxdb` formatter rather than +`process`). Field names == Port-0 keys; `config.general.name` is the measurement +tag. No Port-1-only fields. Covered transitively by the Port-0 tests above; a +dedicated Port-1 line-protocol assertion is a **gap** (see below). + +## Port 2 (registration / control plumbing) — `BaseNodeAdapter._scheduleRegistration` + +| Topic | Source | Payload shape | States tested | Test file | +|---|---|---|---|---| +| `child.register` | `BaseNodeAdapter.js:122` | `{ topic:'child.register', payload:, positionVsParent, distance }` | — | _(gap — see below)_ | + +> Note: the canonical outgoing topic is **`child.register`** (matching the input +> registry). Earlier docs said `registerChild`; that is the deprecated input +> alias, not what this node emits. + +## Child-facing events — `measurements.emitter` + +Fired as `..` when a series receives a value. Parents +subscribe by event name (data-driven, not a fixed catalogue): + +| Event | When | Test file | +|---|---|---| +| `volume.predicted.atequipment` | each integrator tick | test/basic/flowAggregator.basic.test.js | +| `level.predicted.atequipment` | recomputed from volume | test/basic/specificClass.test.js | +| `flow.predicted.in` (child `manual-qin`) | `set.inflow` handler | test/basic/measurementRouter.basic.test.js | +| `overflowVolume`/`underflowVolume`/`flow.predicted.overflow` | integrator hits a physical bound | test/basic/flowAggregator.basic.test.js | + +## Example-flow function-node fan-out + +### examples/02-Dashboard.json :: `fn_status_split` (outputs: 15) + +| # | Target widget | Payload | Populated | Degraded/null | +|---|---|---|---|---| +| 0 | ui-text "Mode" | string | ✔ structure | gap | +| 1 | ui-text "Direction" | string | ✔ | gap | +| 2 | ui-text "Level" | number m | ✔ | gap | +| 3 | ui-text "Volume" | number m³ | ✔ | gap | +| 4 | ui-text "Volume %" | number % | ✔ | gap | +| 5 | ui-text "percControl" | number % | ✔ | gap | +| 6 | ui-text "Manual demand" | number m³/h or — | gap | gap | +| 7 | ui-chart "Level (m)" | `{topic,payload:number}` or no-msg | ✔ | gap | +| 8 | ui-chart "Volume (m³)" | ″ | ✔ | gap | +| 9 | ui-chart "Volume %" | ″ | ✔ | gap | +| 10 | ui-chart "Flow (m³/h)" — Inflow | ″ | ✔ | gap | +| 11 | ui-chart "Flow (m³/h)" — Outflow | ″ | ✔ | gap | +| 12 | ui-chart "Flow (m³/h)" — Net | ″ | ✔ | gap | +| 13 | ui-template "Raw output table" | whole object (array) | ✔ | gap | +| 14 | ui-chart "percControl" | `{topic:'percControl',payload:number}` | ✔ | gap | + +Populated/structure coverage: test/integration/basic-dashboard-flow.test.js +(asserts output count = 15 and routes outputs 0–14). **Degraded/empty-input** +coverage (no `payload:null` reaching any `ui-chart`) is still a gap — see below. + +## Known coverage gaps (tracked, prospective per the rule) + +The output-coverage rule applies prospectively. Outstanding items for this node: + +- [ ] Dedicated `test/basic/output-port0.test.js` exercising **every** key above + in both populated and degraded (pre-tick / null) states. +- [ ] Port-1 line-protocol assertion (field names + tag). +- [ ] Port-2 `child.register` payload-shape test. +- [ ] `fn_status_split` degraded/empty-input fan-out test (no `payload:null` to + any `ui-chart`) — the failure mode the rule was written for. The structure + test in `basic-dashboard-flow.test.js` covers the populated path only. diff --git a/test/integration/basic-dashboard-flow.test.js b/test/integration/basic-dashboard-flow.test.js index d86a7d6..3d8a476 100644 --- a/test/integration/basic-dashboard-flow.test.js +++ b/test/integration/basic-dashboard-flow.test.js @@ -35,7 +35,7 @@ test('basic dashboard flow contains the pumpingStation node and trend widgets', assert.equal(ps.inletPipeDiameter, 0.3); assert.equal(ps.outletPipeDiameter, 0.3); assert.ok(parser, 'fn_status_split should exist'); - assert.equal(parser.outputs, 14); + assert.equal(parser.outputs, 15); assert.equal(levelChart.type, 'ui-chart'); assert.equal(volumeChart.type, 'ui-chart'); assert.equal(flowChart.type, 'ui-chart'); @@ -72,7 +72,7 @@ test('basic dashboard parser routes process fields to charts and state text', () }, context, node); assert.ok(Array.isArray(out)); - assert.equal(out.length, 14); + assert.equal(out.length, 15); assert.equal(out[0].payload, 'levelbased'); assert.equal(out[1].payload, 'filling'); assert.equal(out[2].payload, '3.25 m'); @@ -86,6 +86,7 @@ test('basic dashboard parser routes process fields to charts and state text', () assert.deepEqual(out[11], { topic: 'Outflow', payload: 7.2 }); assert.deepEqual(out[12], { topic: 'Net', payload: 10.8 }); assert.ok(Array.isArray(out[13].payload)); + assert.deepEqual(out[14], { topic: 'percControl', payload: 25 }); }); test('basic dashboard parser keeps previous values when process output sends only changed fields', () => { diff --git a/wiki/Reference-Contracts.md b/wiki/Reference-Contracts.md index e6647b5..703ac9a 100644 --- a/wiki/Reference-Contracts.md +++ b/wiki/Reference-Contracts.md @@ -1,6 +1,6 @@ # Reference — Contracts -![code-ref](https://img.shields.io/badge/code--ref-b825ac1-blue) ![autogen](https://img.shields.io/badge/sections-autogenerated-orange) +![code-ref](https://img.shields.io/badge/code--ref-a83a85e-blue) ![autogen](https://img.shields.io/badge/sections-autogenerated-orange) > [!NOTE] > Full topic contract, configuration schema, and child-registration filters for `pumpingStation`. The topic-contract and data-model sections are **regenerated by `npm run wiki:all`** — do not hand-edit between the `BEGIN AUTOGEN` / `END AUTOGEN` markers. Source of truth for everything on this page: the node's `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/pumpingStation.json`. @@ -19,11 +19,11 @@ The **Unit** column reflects each descriptor's `units: { measure, default }` dec |---|---|---|---|---| | `set.mode` | `changemode` | `string` | — | Switch the station between auto / manual control modes. | | `child.register` | `registerChild` | `string` | — | Register a child node (machine group, measurement, …) with this station. | -| `cmd.calibrate.volume` | `calibratePredictedVolume` | any | `volume` (default `m3`) | Calibrate the predicted-volume integrator to a known basin volume. | -| `cmd.calibrate.level` | `calibratePredictedLevel` | any | `length` (default `m`) | Calibrate the predicted-volume integrator to a known basin level. | -| `set.inflow` | `q_in` | any | `volumeFlowRate` (default `m3/h`) | Push a measured inflow value into the basin balance. | -| `set.outflow` | `q_out` | any | `volumeFlowRate` (default `m3/h`) | Push a measured outflow value into the basin balance. | -| `set.demand` | `Qd` | any | `volumeFlowRate` (default `m3/h`) | Operator outflow demand setpoint for the station. | +| `cmd.calibrate.volume` | `calibratePredictedVolume` | `any` | `volume` (default `m3`) | Calibrate the predicted-volume integrator to a known basin volume. | +| `cmd.calibrate.level` | `calibratePredictedLevel` | `any` | `length` (default `m`) | Calibrate the predicted-volume integrator to a known basin level. | +| `set.inflow` | `q_in` | `any` | `volumeFlowRate` (default `m3/h`) | Push a measured inflow value into the basin balance. | +| `set.outflow` | `q_out` | `any` | `volumeFlowRate` (default `m3/h`) | Push a measured outflow value into the basin balance. | +| `set.demand` | `Qd` | `any` | `volumeFlowRate` (default `m3/h`) | Operator outflow demand setpoint for the station. | @@ -39,35 +39,42 @@ Keys composed each tick by `specificClass.getOutput()` and emitted via `outputUt |---|---|---|---| | `direction` | string | — | `"steady"` | | `dryRunLevel` | number | — | `0.20400000000000001` | -| `dryRunSafetyVol` | number | — | `0.20400000000000001` | +| `dryRunSafetyVol` | number | — | `2.55` | | `flowSource` | null | — | `null` | -| `heightBasin` | number | m | `1` | -| `highVolumeSafetyLevel` | number | — | `2.45` | -| `highVolumeSafetyVol` | number | — | `2.45` | -| `inflowLevel` | number | m | `2` | +| `heightBasin` | number | m | `4` | +| `highVolumeSafetyLevel` | number | — | `3.7239999999999998` | +| `highVolumeSafetyVol` | number | — | `46.55` | +| `inflowLevel` | number | m | `1.5` | | `inletPipeDiameter` | number | — | `0.4` | -| `maxVol` | number | m3 | `1` | -| `maxVolAtOverflow` | number | m3 | `2.5` | +| `manualDemand` | null | — | `null` | +| `maxVol` | number | m3 | `50` | +| `maxVolAtOverflow` | number | m3 | `47.5` | | `minHeightBasedOn` | string | — | `"outlet"` | -| `minVol` | number | m3 | `0.2` | -| `minVolAtInflow` | number | m3 | `2` | -| `minVolAtOutflow` | number | m3 | `0.2` | +| `minVol` | number | m3 | `2.5` | +| `minVolAtInflow` | number | m3 | `18.75` | +| `minVolAtOutflow` | number | m3 | `2.5` | +| `mode` | string | — | `"levelbased"` | | `outflowLevel` | number | m | `0.2` | | `outletPipeDiameter` | number | — | `0.4` | -| `overflowLevel` | number | m | `2.5` | +| `overflowLevel` | number | m | `3.8` | | `percControl` | number | % | `0` | | `predictedOverflowRate` | number | — | `0` | | `predictedOverflowVolume` | number | — | `0` | | `predictedUnderflowVolume` | number | — | `0` | -| `surfaceArea` | number | m2 | `1` | +| `surfaceArea` | number | m2 | `12.5` | | `timeleft` | null | s | `null` | -| `volEmptyBasin` | number | m3 | `1` | -| `volume.predicted.atequipment.wikigen-pumpingstation-id` | number | m3 | `0.2` | +| `volEmptyBasin` | number | m3 | `50` | +| `volume.predicted.atequipment.wikigen-pumpingstation-id` | number | m3 | `2.5` | Sample values come from a stub instantiation in `wikiGen` — in a live deployment the volume key is shaped `volume...` per the standard [Measurement Key Shape](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions#measurement-key-shape). +> [!NOTE] +> Two control-state keys carry the live operating mode rather than a measurement: +> - `mode` — string, the active control strategy (`levelbased` / `manual` / `flowbased` / `none`). Echoes the most recent `set.mode` input. +> - `manualDemand` — number (m³/h) or `null`. The operator outflow setpoint last accepted via `set.demand`; `null` outside `manual` mode. + --- ## Configuration schema — editor form to config keys