P6: convert reactor to platform infrastructure

Refactor of reactor to use BaseNodeAdapter + commandRegistry + statusBadge.
reactor follows the platform refactor plan in .claude/refactor/MODULE_SPLIT.md.
Tests stay green; CONTRACT.md generated; legacy aliases preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-10 22:23:43 +02:00
parent c5fc5c1b59
commit 7bf464b467
16 changed files with 780 additions and 919 deletions

51
CONTRACT.md Normal file
View File

@@ -0,0 +1,51 @@
# reactor — Contract
Hand-maintained for Phase 6; the `## Inputs` table is generated from
`src/commands/index.js` (see Phase 9 generator). Keep ≤ 80 lines.
## Inputs (msg.topic on Port 0)
| Canonical | Aliases (deprecated) | Payload | Effect |
|---|---|---|---|
| `data.clock` | `clock` | `msg.timestamp` (ms since epoch) | Calls `source.updateState(timestamp)` — advances the ASM kinetics integrator by `n_iter` time steps that fit between `currentTime` and the supplied timestamp (scaled by `speedUpFactor`). |
| `data.fluent` | `Fluent` | `{ inlet: number, F: number, C: number[13] }` | Writes the per-inlet flow rate (`F`, m³/d) and concentration vector (`C`) into `engine.Fs[inlet]` / `engine.Cs_in[inlet]`. |
| `data.otr` | `OTR` | numeric | Sets the externally-supplied oxygen transfer rate (used when `kla` is NaN). |
| `data.temperature` | `Temperature` | numeric or `{ value: number }` | Sets `engine.temperature` (°C). Non-numeric payloads are warned and ignored. |
| `data.dispersion` | `Dispersion` | numeric | PFR only — sets the axial dispersion coefficient `D` (m²/d). |
| `child.register` | `registerChild` | child node id (string) | Looks up the sibling node via `RED.nodes.getNode(id)` and delegates to `source.childRegistrationUtils.registerChild` with `msg.positionVsParent`. |
Aliases log a one-time deprecation warning the first time they fire.
## Outputs (msg.topic on Port 0/1/2)
- **Port 0 (process):** every tick emits the engine's effluent:
`{ topic: 'Fluent', payload: { inlet: 0, F, C: number[13] }, timestamp }`.
For a PFR an additional `{ topic: 'GridProfile', payload: { grid, n_x, d_x, length, species, timestamp } }`
message goes out on the same port before the effluent.
- **Port 1 (InfluxDB telemetry):** formatted via `outputUtils.formatMsg(..., 'influxdb')`
from `getOutput()` — carries `flow_total`, `temperature`, and one field per ASM3
species (`S_O`, `S_I`, `S_S`, `S_NH`, `S_N2`, `S_NO`, `S_HCO`, `X_I`, `X_S`, `X_H`,
`X_STO`, `X_A`, `X_TS`).
- **Port 2 (registration):** at startup the node sends one
`{ topic: 'child.register', payload: <node.id>, positionVsParent, distance }`
to its parent.
## Events emitted by `source.emitter`
- `stateChange` — fires after every `updateState()` that advances the integrator.
Payload is the new `currentTime` (ms since epoch). Downstream reactors register
via `child.register` and subscribe to this event to pull the upstream
effluent on each advance.
- `output-changed` — base notification fired by `updateState()` so the
BaseNodeAdapter pipeline pushes outputs (currently used only as a heartbeat;
effluent is emitted directly from the periodic tick).
## Children accepted
- `measurement` — subscribes to `<type>.measured.<position>` on the child's
`measurements.emitter`. Recognised reconciliations: `temperature.measured.atEquipment`
writes `engine.temperature`; PFR additionally honours
`quantity (oxygen).measured.<distance>` to reconcile dissolved-oxygen
concentration into the nearest grid cell.
- `reactor` — registers as the upstream reactor; the downstream `updateState`
pulls the upstream effluent into `Fs[0]` / `Cs_in[0]` before integrating.