Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
287 lines
15 KiB
Markdown
287 lines
15 KiB
Markdown
# Reference — Architecture
|
|
|
|

|
|
|
|
> [!NOTE]
|
|
> The shape of the library: the three-tier rule it enforces on consumer nodes, the `src/` directory layout, how 12 EVOLV nodes consume each module, and the additive-only export discipline. For an intuitive overview, return to [Home](Home).
|
|
|
|
---
|
|
|
|
## Three-tier rule the library enforces
|
|
|
|
Every consumer node follows the same three-tier sandwich. `generalFunctions` provides the base classes for tiers 2 and 3; the entry file is per-node.
|
|
|
|
```
|
|
nodes/<nodeName>/
|
|
|
|
|
+-- <nodeName>.js entry: RED.nodes.registerType(...)
|
|
|
|
|
+-- src/
|
|
nodeClass.js extends BaseNodeAdapter <-- generalFunctions
|
|
specificClass.js extends BaseDomain <-- generalFunctions
|
|
commands/index.js CommandRegistry descriptors <-- generalFunctions
|
|
```
|
|
|
|
| Tier | Owns | May call `RED.*` | Provided by |
|
|
|:---|:---|:---:|:---|
|
|
| entry | Type registration, admin endpoints | Yes | per-node `<nodeName>.js` |
|
|
| nodeClass | Input routing, output ports, tick / status loops, registration delay | Yes | `BaseNodeAdapter` (this library) |
|
|
| specificClass | Domain logic, FSM, predictions, drift — no `RED.*` | No | `BaseDomain` (this library) |
|
|
|
|
Authoritative platform spec: [`.claude/refactor/CONTRACTS.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) sections 2 (nodeClass), 3 (specificClass), 4 (commandRegistry), 5 (ChildRouter), 6 (UnitPolicy), 7 (statusBadge), 9 (HealthStatus).
|
|
|
|
---
|
|
|
|
## `src/` directory tree
|
|
|
|
```
|
|
generalFunctions/
|
|
|
|
|
+-- index.js barrel — the only contractual import path
|
|
+-- CONTRACT.md per-export stability tags + cross-refs
|
|
|
|
|
+-- src/
|
|
| +-- domain/ base classes for specificClass.js
|
|
| | BaseDomain.js
|
|
| | ChildRouter.js
|
|
| | UnitPolicy.js
|
|
| | LatestWinsGate.js
|
|
| | HealthStatus.js
|
|
| |
|
|
| +-- nodered/ base classes for nodeClass.js
|
|
| | BaseNodeAdapter.js
|
|
| | commandRegistry.js
|
|
| | statusBadge.js
|
|
| | statusUpdater.js
|
|
| |
|
|
| +-- measurements/ measurement store
|
|
| | MeasurementContainer.js
|
|
| | MeasurementBuilder.js
|
|
| | Measurement.js
|
|
| |
|
|
| +-- helper/ shared utilities
|
|
| | logger.js
|
|
| | outputUtils.js
|
|
| | childRegistrationUtils.js
|
|
| | configUtils.js
|
|
| | validationUtils.js
|
|
| | menuUtils.js
|
|
| | gravity.js
|
|
| |
|
|
| +-- configs/ schema registry
|
|
| | index.js ConfigManager
|
|
| | baseConfig.json
|
|
| | <nodeName>.json one schema per consumer node
|
|
| | assetApiConfig.js
|
|
| |
|
|
| +-- convert/ unit conversion + physics
|
|
| | index.js convert
|
|
| | fysics.js Fysics class
|
|
| |
|
|
| +-- predict/ curve prediction
|
|
| | predict_class.js
|
|
| | interpolation.js
|
|
| |
|
|
| +-- pid/ closed-loop control
|
|
| | PIDController.js
|
|
| | index.js createPidController / createCascadePidController
|
|
| |
|
|
| +-- state/ FSM scaffold (StateManager + MovementManager)
|
|
| +-- nrmse/ prediction-quality NRMSE
|
|
| +-- stats/ pure-function statistical reducers
|
|
| +-- outliers/ DynamicClusterDeviation
|
|
| +-- coolprop-node/ CoolProp thermodynamic bindings
|
|
| +-- menu/ MenuManager (editor dropdowns)
|
|
| +-- registry/ AssetResolver + FileBackend / HttpBackend
|
|
| +-- constants/ POSITIONS, POSITION_VALUES, isValidPosition
|
|
|
|
|
+-- datasets/ asset metadata (curves, model data)
|
|
| +-- assetData/
|
|
| +-- curves/ pump / blower / compressor curves
|
|
| +-- modelData/ multi-parameter model assets
|
|
|
|
|
+-- test/ unit + integration tests
|
|
+-- scripts/ maintenance scripts
|
|
+-- settings/ shared Node-RED-side settings
|
|
```
|
|
|
|
`index.js` is the only contractual import path. Anything not re-exported there is internal; consumers must not reach into `src/...` paths.
|
|
|
|
---
|
|
|
|
## How nodes consume the library
|
|
|
|
| Layer | Consumer responsibility | Library responsibility |
|
|
|:---|:---|:---|
|
|
| nodeClass | Declare `static DomainClass`, `static commands`, `static tickInterval`, `static statusInterval`. Override `buildDomainConfig(uiConfig, nodeId)` to translate editor values into the domain's config slice. | `BaseNodeAdapter` wires config build → domain instantiation → registration delay → output strategy → status loop → input dispatch → close handler. |
|
|
| specificClass | Declare `static name` (matches the schema file). Implement `configure()`: wire `ChildRouter` routes, instantiate concern modules, attach measurement listeners. Implement `getOutput()` and `getStatusBadge()`. | `BaseDomain` provides `this.emitter`, `this.config`, `this.logger`, `this.measurements`, `this.childRegistrationUtils`, `this.router`. |
|
|
| commands/index.js | Export an array of descriptors: `{topic, aliases?, units?, payloadSchema?, description, handler}`. Handler is `(source, msg, ctx)`. | `CommandRegistry` builds an `O(1)` lookup, normalises units via `convert`, warns once on alias use, generates the auto-`query.units` topic. |
|
|
| measurements | Write via the chain: `this.measurements.type(t).variant(v).position(p, childId).value(x, ts, srcUnit)`. Read via `getCurrentValue(unit)`, `getAverage(unit)`, `getFlattenedOutput()`. | `MeasurementContainer` auto-converts inputs to canonical units (per `UnitPolicy`), maintains windows, emits change events. |
|
|
| output | Implement `getOutput()` returning a flat snapshot object. Implement `getStatusBadge()` returning `statusBadge.compose(parts, opts)`. | `outputUtils.formatMsg` delta-compresses the snapshot for Port 0 + Port 1; `StatusUpdater` polls `getStatusBadge()` on `statusInterval`. |
|
|
|
|
All 12 nodes follow this pattern. Variations are in how richly they fill `configure()` — `dashboardAPI` has the lightest (HTTP gateway, no FSM); `rotatingMachine` and `machineGroupControl` have the densest (full curve loading, drift assessor, multi-source pressure routing).
|
|
|
|
---
|
|
|
|
## Lifecycle — one tick or event reaches the output port
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant RED as Node-RED runtime
|
|
participant BNA as BaseNodeAdapter
|
|
participant CMD as CommandRegistry
|
|
participant DOM as Domain (specificClass)
|
|
participant CR as ChildRouter
|
|
participant MC as MeasurementContainer
|
|
participant OU as outputUtils
|
|
participant PORT as Port 0 / 1 / 2
|
|
|
|
RED->>BNA: constructor(uiConfig, RED, node, name)
|
|
BNA->>BNA: configManager.buildConfig()
|
|
BNA->>DOM: new DomainClass(config)
|
|
DOM->>MC: new MeasurementContainer(unitPolicy.containerOptions())
|
|
DOM->>DOM: configure() — wire ChildRouter, concern modules
|
|
BNA-->>PORT: Port 2 registration msg (after 100 ms delay)
|
|
BNA->>BNA: start status loop (1000 ms)
|
|
|
|
Note over RED,PORT: Event-driven path (default)
|
|
|
|
RED->>BNA: input msg {topic: 'data.pressure', payload: 3.4}
|
|
BNA->>CMD: dispatch(msg)
|
|
CMD->>CMD: unit normalisation (Pa → mbar)
|
|
CMD->>DOM: handler(source, msg, ctx)
|
|
DOM->>MC: .type('pressure').variant('measured').position('upstream').value(3.4)
|
|
DOM->>DOM: emitter.emit('output-changed')
|
|
BNA->>DOM: getOutput()
|
|
DOM-->>BNA: flat snapshot object
|
|
BNA->>OU: formatMsg(snapshot, config, 'process')
|
|
OU-->>BNA: delta msg (only changed fields)
|
|
BNA-->>PORT: Port 0 process msg, Port 1 influx msg
|
|
|
|
Note over RED,PORT: Tick-driven path (opt-in — tickInterval set)
|
|
|
|
RED->>BNA: timer fires every tickInterval ms
|
|
BNA->>DOM: tick()
|
|
DOM->>DOM: time-based math; emitter.emit('output-changed')
|
|
BNA->>DOM: getOutput()
|
|
BNA->>OU: formatMsg(...)
|
|
BNA-->>PORT: Port 0 / 1 msgs (delta only)
|
|
```
|
|
|
|
The event path is the default. The tick path is opt-in via `static tickInterval = 1000;` — only nodes with genuinely time-based math (integrators, ramps, runtime counters) enable it.
|
|
|
|
---
|
|
|
|
## Config schema registry
|
|
|
|
Each consumer node has one JSON schema in `src/configs/`. `ConfigManager.buildConfig` merges the schema defaults with the Node-RED editor values before the domain sees them.
|
|
|
|
| File | Node | What it defines |
|
|
|:---|:---|:---|
|
|
| `baseConfig.json` | all nodes | Shared `general`, `asset`, `functionality`, `logging` sections |
|
|
| `rotatingMachine.json` | rotatingMachine | Curve selection, startup/shutdown ramps, safety thresholds, unit config |
|
|
| `machineGroupControl.json` | machineGroupControl | Demand targets, strategy selection, dispatcher settings |
|
|
| `pumpingStation.json` | pumpingStation | Basin geometry, hydraulics, control strategies, safety levels |
|
|
| `measurement.json` | measurement | Scaling, smoothing, stability threshold, digital/MQTT mode |
|
|
| `valve.json` | valve | Actuator travel time, position limits, FSM config |
|
|
| `valveGroupControl.json` | valveGroupControl | Group strategy, demand distribution |
|
|
| `reactor.json` | reactor | ASM kinetics, reactor type (CSTR/PFR), volume, influent |
|
|
| `settler.json` | settler | Sludge settling parameters, effluent quality |
|
|
| `monster.json` | monster | Multi-parameter monitoring, flow bounds, sample intervals |
|
|
| `diffuser.json` | diffuser | Aeration model, oxygen transfer parameters |
|
|
|
|
To add a new node: create `src/configs/<nodeName>.json` extending `baseConfig.json`, declare `static name = '<nodeName>'` in the domain class. `configManager.buildConfig` finds it automatically — no registration step.
|
|
|
|
---
|
|
|
|
## Stability — additive-only export discipline
|
|
|
|
Source of truth: [`.claude/rules/general-functions.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/general-functions.md) in the superproject.
|
|
|
|
| Category | Rule |
|
|
|:---|:---|
|
|
| **Safe to add** | New named exports. New optional methods on existing classes. New config keys with defaults in the schema. |
|
|
| **Requires decision-gate interview** | Removing or renaming any export. Changing a method signature. Changing the output key format of `MeasurementContainer.getFlattenedOutput()`. Changing the `formatMsg` delta-compression behaviour. |
|
|
| **Forbidden without migration** | Breaking the 4-segment key shape (`type.variant.position.childId`). Changing Port 0/1/2 payload envelope. Changing the CONTRACTS.md §1–§9 shapes. |
|
|
|
|
`generalFunctions` is a git submodule shared by all 12 node repos. A breaking change here requires updating every consumer in a single coordinated commit. Before modifying any module:
|
|
|
|
```bash
|
|
grep -r "require('generalFunctions')" nodes/*/
|
|
```
|
|
|
|
Run the test suites of every affected consumer, not just this library's own tests.
|
|
|
|
### Canonical units
|
|
|
|
`MeasurementContainer` and all internal processing assume canonical units:
|
|
|
|
| Quantity | Canonical |
|
|
|:---|:---|
|
|
| Pressure | `Pa` |
|
|
| Flow | `m3/s` |
|
|
| Power | `W` |
|
|
| Temperature | `K` |
|
|
|
|
Unit conversion happens at system boundaries (input via `CommandRegistry.units` normalisation, output via `UnitPolicy.output` rendering) — never in core logic.
|
|
|
|
---
|
|
|
|
## Adding a new export — the dance
|
|
|
|
1. Implement the module under `src/<concern>/`.
|
|
2. Re-export it from `index.js` (alphabetical within the concern block).
|
|
3. Add a row to the appropriate table in [`CONTRACT.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/nodes/generalFunctions/CONTRACT.md) with the stability tag.
|
|
4. If the export is a new platform shape (a new base class or cross-node protocol), add a section to [`.claude/refactor/CONTRACTS.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) in the superproject.
|
|
5. Add a test under `test/`.
|
|
|
|
## Removing an export
|
|
|
|
1. Mark it **deprecated** in `CONTRACT.md` (keep the row, change the tag, add a "removed-in" line).
|
|
2. Update every consumer in `nodes/*` to use the replacement.
|
|
3. Bump submodule pin in the superproject for each touched node.
|
|
4. After one release on `development` with no consumers, remove the export and its row.
|
|
|
|
---
|
|
|
|
## When NOT to depend on this library
|
|
|
|
- **Passive HTTP gateway nodes** (e.g. `dashboardAPI`) may skip `BaseDomain` and `BaseNodeAdapter` entirely if they hold no domain state. A plain Node-RED node with HTTP endpoints needs only `logger`, `outputUtils`, and `configManager`.
|
|
- **External scripts or standalone tools** that need only unit conversion can import just `const { convert } = require('generalFunctions')` without pulling in the full domain stack.
|
|
- **Nodes at a different S88 level** that inherit from a third-party base class must not import from `src/domain/` or `src/nodered/` internal paths — they may only use root-level exports.
|
|
|
|
---
|
|
|
|
## Where to start reading
|
|
|
|
| If you're changing... | Read first |
|
|
|:---|:---|
|
|
| Base class for a new domain | `src/domain/BaseDomain.js` + `.claude/refactor/CONTRACTS.md §3` |
|
|
| Node-RED adapter behaviour | `src/nodered/BaseNodeAdapter.js` + `.claude/refactor/CONTRACTS.md §2` |
|
|
| Topic dispatch, alias warnings, unit normalisation | `src/nodered/commandRegistry.js` + `.claude/refactor/CONTRACTS.md §4` |
|
|
| Declarative child registration | `src/domain/ChildRouter.js` + `.claude/refactor/CONTRACTS.md §5` |
|
|
| Canonical / output / curve units | `src/domain/UnitPolicy.js` + `.claude/refactor/CONTRACTS.md §6` |
|
|
| Measurement chain + flattened output | `src/measurements/MeasurementContainer.js` |
|
|
| Delta-compressed output formatting | `src/helper/outputUtils.js` |
|
|
| Editor status badge | `src/nodered/statusBadge.js`, `statusUpdater.js`, `.claude/refactor/CONTRACTS.md §7` |
|
|
| Async dispatch serialisation | `src/domain/LatestWinsGate.js` + `.claude/refactor/CONTRACTS.md §8` |
|
|
| Prediction quality / drift state | `src/domain/HealthStatus.js` + `.claude/refactor/CONTRACTS.md §9` |
|
|
| Curve fitting + flow/power prediction | `src/predict/predict_class.js`, `interpolation.js` |
|
|
| PID control | `src/pid/PIDController.js` |
|
|
| FSM (valve / machine states) | `src/state/` |
|
|
| Per-node JSON schema loading | `src/configs/index.js` |
|
|
| Asset metadata lookup | `src/registry/AssetResolver.js`, `FileBackend.js`, `HttpBackend.js` |
|
|
|
|
---
|
|
|
|
## Related pages
|
|
|
|
| Page | Why |
|
|
|:---|:---|
|
|
| [Home](Home) | Intuitive overview |
|
|
| [Reference — Contracts](Reference-Contracts) | Full public API surface, per-export stability tags |
|
|
| [Reference — Examples](Reference-Examples) | Usage patterns from real consumer nodes |
|
|
| [Reference — Limitations](Reference-Limitations) | Known issues, stability rules, deprecations |
|
|
| [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) | The authoritative base-class + protocol spec |
|
|
| [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |
|