docs(wiki): full 5-page wiki matching the rotatingMachine reference format

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>
This commit is contained in:
znetsixe
2026-05-19 09:42:15 +02:00
parent 48fa54363d
commit 8b28f8969e
6 changed files with 1191 additions and 311 deletions

View File

@@ -1,33 +1,41 @@
# generalFunctions
> **Reflects code as of `f21e2aa` · regenerated `2026-05-11` (hand-written)**
> No `npm run wiki:all` script exists for this library. The API surface block (section 5) is hand-maintained between the AUTOGEN markers. If the banner is stale, treat this page as informative, not authoritative.
![code-ref](https://img.shields.io/badge/code--ref-48fa543-blue) ![kind](https://img.shields.io/badge/kind-Shared_Library-dddddd) ![status](https://img.shields.io/badge/status-stable-brightgreen)
**generalFunctions** is the shared infrastructure every EVOLV node depends on. It provides the base classes all nodes extend (`BaseDomain`, `BaseNodeAdapter`), the command dispatch engine, the measurement store, unit-policy system, child-registration machinery, InfluxDB output formatting, and a set of domain utilities (PID, curve interpolation, prediction, statistics, coolprop). Nodes hold zero duplicated scaffolding &mdash; they only write the logic that differs.
---
## 1. What this library is
## At a glance
**generalFunctions** is the shared infrastructure every EVOLV node depends on. It provides the base classes all nodes extend (`BaseDomain`, `BaseNodeAdapter`), the command dispatch engine, the measurement store, unit-policy system, child-registration machinery, InfluxDB output formatting, and a set of domain utilities (PID, curve interpolation, prediction, statistics, coolprop). Nodes hold zero duplicated scaffolding — they only write the logic that differs.
| Thing | Value |
|:---|:---|
| What it is | The shared library &mdash; not a Node-RED node, never placed in a flow |
| Kind | Shared library (`require('generalFunctions')`) |
| Consumed by | All 12 EVOLV nodes (rotatingMachine, MGC, pumpingStation, valve, VGC, reactor, settler, monster, measurement, diffuser, dashboardAPI) |
| Import style | Package root only &mdash; `const { BaseDomain, UnitPolicy } = require('generalFunctions');` |
| Side effects on a flow | None &mdash; the library has no editor form, no node registration |
| Cross-node coupling | Through this library's API surface + Node-RED messages only &mdash; never direct imports between node packages |
---
## 2. Position in the platform
## How it fits
```mermaid
flowchart LR
gf["generalFunctions\n(shared library)"]:::lib
gf["generalFunctions<br/>(shared library)"]:::lib
rm["rotatingMachine\nEquipment"]:::equip
mgc["machineGroupControl\nUnit"]:::unit
ps["pumpingStation\nProcess Cell"]:::proc
meas["measurement\nControl Module"]:::ctrl
valve["valve\nEquipment"]:::equip
vgc["valveGroupControl\nUnit"]:::unit
reactor["reactor\nUnit"]:::unit
settler["settler\nUnit"]:::unit
monster["monster\nUnit"]:::unit
diffuser["diffuser\nEquipment"]:::equip
dashAPI["dashboardAPI\nutility"]:::util
rm["rotatingMachine<br/>Equipment"]:::equip
mgc["machineGroupControl<br/>Unit"]:::unit
ps["pumpingStation<br/>Process Cell"]:::proc
meas["measurement<br/>Control Module"]:::ctrl
valve["valve<br/>Equipment"]:::equip
vgc["valveGroupControl<br/>Unit"]:::unit
reactor["reactor<br/>Unit"]:::unit
settler["settler<br/>Unit"]:::unit
monster["monster<br/>Unit"]:::unit
diffuser["diffuser<br/>Equipment"]:::equip
dashAPI["dashboardAPI<br/>utility"]:::util
gf --> rm
gf --> mgc
@@ -49,37 +57,48 @@ flowchart LR
classDef util fill:#dddddd,color:#000
```
Every EVOLV node declares `generalFunctions` as a `dependencies` entry and imports from the package root only (`require('generalFunctions')`). Cross-node coupling happens exclusively through this library's API surface and Node-RED messages — never through direct imports between node packages.
Every EVOLV node declares `generalFunctions` as a `dependencies` entry and imports from the package root only. The library has no S88 level of its own &mdash; it is the substrate the S88-classified nodes are built on.
---
## 3. Capability matrix
## How to import
| Capability | Status | Notes |
|---|---|---|
| Base domain scaffolding (`BaseDomain`) | ✅ | Constructor, emitter, logger, measurements, child registry wired automatically |
| Base Node-RED adapter (`BaseNodeAdapter`) | ✅ | Tick/event loop, status badge, input dispatch, Port 0/1/2 output |
| Declarative command dispatch (`CommandRegistry`) | ✅ | Alias deprecation warnings, unit normalisation, `query.units` auto-topic |
| Declarative child-registration routing (`ChildRouter`) | ✅ | Replaces per-node `registerChild` switch blocks |
| Unit policy + conversion (`UnitPolicy`, `convert`) | ✅ | Canonical ↔ output ↔ curve unit sets; dual method/property access |
| Measurement store (`MeasurementContainer`) | ✅ | Chainable, windowed, auto-convert, 4-segment key output |
| InfluxDB + process output formatting (`outputUtils`) | ✅ | Delta-compressed; consumers must cache and merge |
| Status badge helpers (`statusBadge`, `StatusUpdater`) | ✅ | Converged look-and-feel across all nodes |
| Latest-wins async gate (`LatestWinsGate`) | ✅ | Extracted from MGC; shared by PS, VGC, MGC |
| Prediction quality / drift tracking (`HealthStatus`) | ✅ | Frozen plain-object shape; composable |
| Config schema registry (`configManager`) | ✅ | One JSON schema per node in `src/configs/` |
| PID control (`PIDController`, `CascadePIDController`) | ✅ | Full-featured discrete PID with bumpless transfer |
| Curve interpolation (`interpolation`, `predict`) | ✅ | Multidimensional characteristic-curve predictor |
| Statistical helpers (`stats`, `nrmse`, `outliers`) | ✅ | Mean, stddev, median, NRMSE, dynamic-cluster outlier detection |
| Thermodynamic properties (`coolprop`) | ✅ | CoolProp bindings for fluid/gas property lookup |
| FSM for valve/machine states (`state`) | ✅ | StateManager + MovementManager |
| Gravity calculations (`gravity`) | ✅ | WGS-84 model |
| Physical constants (`Fysics`) | ✅ | Air density, viscosity, etc. |
| Browser-side editor dropdowns (`MenuManager`, `menuUtils`) | ✅ | Node-RED editor form population |
Single root import, destructure what you need:
```js
const {
// Platform base classes
BaseDomain, BaseNodeAdapter, ChildRouter, UnitPolicy, HealthStatus, LatestWinsGate,
// Node-RED bridge
createRegistry, CommandRegistry, statusBadge, StatusUpdater,
// Measurement + config
MeasurementContainer, configManager, configUtils, validation,
// Output formatting + logging
outputUtils, logger,
// Child registration
childRegistrationUtils,
// Unit conversion + physics
convert, Fysics, gravity, coolprop,
// Control + prediction
PIDController, CascadePIDController, createPidController, createCascadePidController,
predict, interpolation, nrmse, stats, state,
// Editor menus
MenuManager,
// Asset registry
assetResolver, AssetResolver, FileBackend, HttpBackend,
// Constants
POSITIONS, POSITION_VALUES, isValidPosition,
} = require('generalFunctions');
```
> [!IMPORTANT]
> Never import internal paths (`require('generalFunctions/src/domain/UnitPolicy')`). Only the package root is contractual; internal layout may move.
For the full export list with signatures and stability tags, see [Reference &mdash; Contracts](Reference-Contracts).
---
## 4. Module map
## Module map &mdash; what lives where
```mermaid
flowchart TB
@@ -125,17 +144,17 @@ flowchart TB
end
subgraph math["numeric & domain utilities"]
PID["src/pid/ — PIDController"]
NRMSE["src/nrmse/ — ErrorMetrics"]
STATS["src/stats/ — mean/stddev/median"]
OUT["src/outliers/ — DynamicClusterDeviation"]
STATE["src/state/ — state FSM"]
CONV["src/convert/ — unit conversion"]
COOL["src/coolprop-node/ — thermodynamics"]
FYS["src/convert/fysics.js — physical constants"]
PID["src/pid/"]
NRMSE["src/nrmse/"]
STATS["src/stats/"]
OUT["src/outliers/"]
STATE["src/state/"]
CONV["src/convert/"]
COOL["src/coolprop-node/"]
FYS["src/convert/fysics.js"]
end
subgraph menu_grp["src/menu/ — editor menus"]
subgraph menu_grp["src/menu/"]
MM["MenuManager"]
end
@@ -155,298 +174,93 @@ flowchart TB
```
| Directory | Primary export | Read first if you're changing… |
|---|---|---|
|:---|:---|:---|
| `src/domain/` | `BaseDomain`, `ChildRouter`, `UnitPolicy`, `LatestWinsGate`, `HealthStatus` | Base class contracts, child routing, unit system |
| `src/nodered/` | `BaseNodeAdapter`, `CommandRegistry`, `statusBadge`, `StatusUpdater` | Input dispatch, output loops, editor status |
| `src/measurements/` | `MeasurementContainer` | Measurement storage, statistics, 4-segment key output |
| `src/helper/` | `logger`, `outputUtils`, `childRegistrationUtils`, `configUtils`, `validationUtils`, `menuUtils`, `gravity` | Logging, InfluxDB formatting, child registration |
| `src/configs/` | `ConfigManager` + per-node JSON schemas | Schema loading, config validation, default values |
| `src/predict/` | `predict`, `interpolation` | Characteristic curve fitting and flow/power prediction |
| `src/predict/` | `predict`, `interpolation` | Characteristic curve fitting + flow/power prediction |
| `src/pid/` | `PIDController`, `CascadePIDController` | Closed-loop control |
| `src/nrmse/` | `ErrorMetrics` (NRMSE) | Prediction quality scoring |
| `src/stats/` | `stats` (mean, stddev, median) | Statistical reducers |
| `src/outliers/` | `DynamicClusterDeviation` | Online outlier detection |
| `src/state/` | `state`, `StateManager`, `MovementManager` | FSM for valve/machine state machines |
| `src/state/` | `state`, `StateManager`, `MovementManager` | FSM for valve / machine state machines |
| `src/convert/` | `convert`, `Fysics` | Unit conversion, physical constants |
| `src/coolprop-node/` | `coolprop` | Thermodynamic property lookup |
| `src/menu/` | `MenuManager` | Editor-form dropdown population |
| `src/registry/` | `assetResolver`, `AssetResolver`, `FileBackend`, `HttpBackend` | Asset metadata lookup (replaces ad-hoc JSON readers) |
| `src/constants/` | `POSITIONS`, `POSITION_VALUES`, `isValidPosition` | Canonical spatial position constants |
---
## 5. API surface
## What you'll send (the platform contract)
<!-- BEGIN AUTOGEN: api-surface -->
This library doesn't accept `msg.topic` directly &mdash; nodes do. But every node's `nodeClass.js` and `specificClass.js` route through the same primitives:
All imports use the package root: `const { X } = require('generalFunctions');`
| Primitive | Role |
|:---|:---|
| `BaseNodeAdapter.input(msg)` | Routes incoming Node-RED messages through the node's `CommandRegistry`, applies unit normalisation, then dispatches to the handler. |
| `CommandRegistry` | Topic + alias map. Handlers are pure functions; `units: {measure, default}` triggers automatic `convert` normalisation. |
| `ChildRouter` | Declarative parent-side routing. `.onRegister(type, cb)`, `.onMeasurement(type, filter, cb)`, `.onPrediction(type, filter, cb)`. |
| `MeasurementContainer.type().variant().position().value()` | Chainable write. Flattened output emits 4-segment keys `<type>.<variant>.<position>.<childId>`. |
| `UnitPolicy.declare({canonical, output, curve?})` | The per-node unit triple. Used by `MeasurementContainer` (auto-convert on write) and by the output formatter (render in `output` units). |
| `outputUtils.formatMsg(snapshot, config, mode)` | Delta-compresses successive snapshots. Returns `undefined` when nothing changed. |
| `HealthStatus.ok / degraded / compose` | Frozen plain-object factory for prediction-quality state. |
| `LatestWinsGate.fire(value)` | Serialises async dispatches; the latest call wins, intermediates are marked `SUPERSEDED`. |
| Export | Import name | Source file | Contract |
|---|---|---|---|
| `BaseDomain` | `BaseDomain` | `src/domain/BaseDomain.js` | Abstract base class for every `specificClass.js`. Provides `emitter`, `config`, `logger`, `measurements`, `childRegistrationUtils`, `router`. Subclass must declare `static name` (maps to schema JSON) and implement `configure()`. See CONTRACTS.md §3. |
| `BaseNodeAdapter` | `BaseNodeAdapter` | `src/nodered/BaseNodeAdapter.js` | Abstract base for every `nodeClass.js`. Wires config build → domain instantiation → registration delay → output strategy → status loop → input dispatch → close handler. Subclass declares `static DomainClass`, `static commands`, `static tickInterval`, `static statusInterval`, and overrides `buildDomainConfig(uiConfig, nodeId)`. See CONTRACTS.md §2. |
| `ChildRouter` | `ChildRouter` | `src/domain/ChildRouter.js` | Declarative parent-side child registration. Replaces per-node `registerChild` switch. Chain `.onRegister(softwareType, cb)`, `.onMeasurement(softwareType, filter, cb)`, `.onPrediction(softwareType, filter, cb)`. See CONTRACTS.md §5. |
| `CommandRegistry` | `CommandRegistry` | `src/nodered/commandRegistry.js` | Class form of the command registry. Accepts array of descriptors (topic, aliases, payloadSchema, units, description, handler). Dispatches by O(1) lookup, normalises units before handler runs, warns on alias use. |
| `createRegistry` | `createRegistry` | `src/nodered/commandRegistry.js` | Factory: `createRegistry(descriptors, options)``CommandRegistry`. Used by `BaseNodeAdapter`; rarely needed directly. |
| `UnitPolicy` | `UnitPolicy` | `src/domain/UnitPolicy.js` | Declare unit sets: `UnitPolicy.declare({ canonical, output, curve?, requireUnitForTypes? })`. Returns policy with dual method/property access (`policy.canonical('flow')` and `policy.canonical.flow`). Methods: `canonical`, `output`, `curve`, `resolve`, `convert`, `containerOptions`, `setLogger`. See CONTRACTS.md §6. |
| `LatestWinsGate` | `LatestWinsGate` | `src/domain/LatestWinsGate.js` | Serialises async dispatches so only the latest value wins. `fire(value)` — non-blocking. `fireAndWait(value)``Promise` that resolves with dispatch result or `LatestWinsGate.SUPERSEDED`. `drain()` — await idle. See CONTRACTS.md §8. |
| `HealthStatus` | `HealthStatus` | `src/domain/HealthStatus.js` | Factory functions for frozen health objects: `HealthStatus.ok(msg, src)`, `HealthStatus.degraded(level, flags, msg, src)`, `HealthStatus.compose(statuses)`. Shape: `{ level: 03, flags: string[], message, source }`. See CONTRACTS.md §9. |
| `MeasurementContainer` | `MeasurementContainer` | `src/measurements/MeasurementContainer.js` | Chainable measurement store: `.type().variant().position().value(v, ts, srcUnit)`. Query: `getCurrentValue(unit)`, `getAverage(unit)`, `difference({ from, to, unit })`. Introspect: `getFlattenedOutput()` returns 4-segment keyed object (`type.variant.position.childId`). |
| `outputUtils` | `outputUtils` | `src/helper/outputUtils.js` | Singleton-per-node delta-compression engine. `formatMsg(output, config, format)` returns `msg` only when fields changed, or `undefined`. `format` is `'process'` or `'influxdb'`. Consumers must cache and merge. |
| `logger` | `logger` | `src/helper/logger.js` | `new logger(enabled, logLevel, moduleName)`. Methods: `debug`, `info`, `warn`, `error`, `setLogLevel`, `toggleLogging`. Never use `console.log` directly. |
| `configManager` | `configManager` | `src/configs/index.js` | `new configManager()`. Methods: `getConfig(name)`, `buildConfig(name, uiConfig, nodeId, domainSlice?)`, `getAvailableConfigs()`, `hasConfig(name)`. Config files live in `src/configs/*.json`. |
| `configUtils` | `configUtils` | `src/helper/configUtils.js` | `new configUtils(defaultConfig)`. `initConfig(userConfig)` validates and merges user values over defaults via `validationUtils`. |
| `validation` | `validation` | `src/helper/validationUtils.js` | `new validation(logEnabled, logLevel)`. `validateSchema(config, schema, name)` walks schema, clamps numbers, coerces types, strips unknown keys. |
| `childRegistrationUtils` | `childRegistrationUtils` | `src/helper/childRegistrationUtils.js` | `new childRegistrationUtils(parentDomain)`. `registerChild(child, positionVsParent, distance?)` stores child by softwareType/category with alias normalisation. `getChildrenOfType(softwareType, category?)`, `getChildById(id)`, `getAllChildren()`. Normally used via `ChildRouter` — direct use is for advanced cases. |
| `statusBadge` | `statusBadge` | `src/nodered/statusBadge.js` | Pure-function badge builder. `statusBadge.compose(parts, opts?)``{ fill, shape, text }`. `statusBadge.error(msg)`, `statusBadge.idle(label)`. Text clipped to 60 chars. See CONTRACTS.md §7. |
| `StatusUpdater` | `StatusUpdater` | `src/nodered/statusUpdater.js` | `new StatusUpdater({ node, source, intervalMs, logger })`. `start()`, `stop()`. Calls `source.getStatusBadge()` on interval; catches errors and shows a red badge. Owned by `BaseNodeAdapter` — rarely needed directly. |
| `convert` | `convert` | `src/convert/index.js` | unit-converter factory. `convert(value).from(unit).to(unit)`. `convert.possibilities(measure)` lists accepted units. Measures: `volumeFlowRate`, `pressure`, `power`, `temperature`, `volume`, `length`, `mass`, `energy`, `reactivePower`, `apparentPower`, `reactiveEnergy`, and more. |
| `Fysics` | `Fysics` | `src/convert/fysics.js` | `new Fysics()`. Physical constants: `air_density`, `g0`; methods for gravity and viscosity calculations. |
| `gravity` | `gravity` | `src/helper/gravity.js` | Singleton-style `Gravity` instance. `getStandardGravity()` → 9.80665 m/s². WGS-84 latitude/altitude corrections available. |
| `predict` | `predict` | `src/predict/predict_class.js` | `new predict(config, logger)`. Multidimensional characteristic-curve predictor; emits results via internal EventEmitter. |
| `interpolation` | `interpolation` | `src/predict/interpolation.js` | Class for 1-D and 2-D curve interpolation (linear, cubic-spline). Used internally by `predict`. |
| `PIDController` | `PIDController` | `src/pid/PIDController.js` | Discrete PID with bumpless auto/manual transfer, anti-windup, derivative filtering, rate limiting, gain scheduling, feedforward. |
| `CascadePIDController` | `CascadePIDController` | `src/pid/PIDController.js` | Outer-inner PID cascade built on `PIDController`. |
| `createPidController` | `createPidController` | `src/pid/index.js` | Factory shorthand: `createPidController(options)``PIDController`. |
| `createCascadePidController` | `createCascadePidController` | `src/pid/index.js` | Factory shorthand for cascade PID. |
| `nrmse` | `nrmse` | `src/nrmse/index.js` | `ErrorMetrics` class for normalised-root-mean-squared-error tracking. Multi-metric via `registerMetric(id)`, `update(id, predicted, measured)`. |
| `stats` | `stats` | `src/stats/index.js` | Pure functions: `mean(arr)`, `stdDev(arr)`, `median(arr)`. No state; safe to call on any numeric array. |
| `state` | `state` | `src/state/index.js` | `new state(config, logger)`. FSM for valve/machine: StateManager (transitions) + MovementManager (timed moves). Emits state-change events. |
| `MenuManager` | `MenuManager` | `src/menu/index.js` | `new MenuManager()`. Manages editor dropdown menus (asset, logger, position, aquon). `registerMenu(type, factory)`. Used in node entry files to power Node-RED editor forms. |
| `menuUtils` / `MenuUtils` | via `menuUtils` in helper | `src/helper/menuUtils.js` | Browser-side editor helper. Toggles, data fetching, URL construction, dropdown population, HTML generation. Served to browser via `endpointUtils`. |
| `POSITIONS` | `POSITIONS` | `src/constants/positions.js` | Frozen enum: `{ UPSTREAM, DOWNSTREAM, AT_EQUIPMENT, DELTA }`. |
| `POSITION_VALUES` | `POSITION_VALUES` | `src/constants/positions.js` | `string[]` of all four position strings. |
| `isValidPosition` | `isValidPosition` | `src/constants/positions.js` | `(pos: string) => boolean`. |
| `coolprop` | `coolprop` | `src/coolprop-node/src/index.js` | CoolProp fluid/gas thermodynamic property lookup. Used by nodes that model heat transfer or gas compression. |
| `loadModel` | `loadModel` | `datasets/assetData/modelData/index.js` | Load a JSON model-data asset by dataset type and asset ID (with LRU cache). Preferred over deprecated `loadCurve`. |
| `loadCurve` | `loadCurve` | `datasets/assetData/curves/index.js` | **Deprecated** — load a pump-curve JSON. Replaced by `loadModel`. |
<!-- END AUTOGEN: api-surface -->
For full signatures and stability tags see [Reference &mdash; Contracts](Reference-Contracts).
---
## 6. Config schema registry
## What you'll see come out
One JSON file per node in `src/configs/`. `ConfigManager.buildConfig` merges the schema defaults with the Node-RED editor values before the domain sees them.
A node that imports `BaseNodeAdapter` automatically gets the three EVOLV ports:
| 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 |
| Port | Carries | Built by |
|:---|:---|:---|
| 0 (process) | Delta-compressed state snapshot (the `getOutput()` return) | `outputUtils.formatMsg(snapshot, config, 'process')` |
| 1 (telemetry) | InfluxDB line-protocol payload (same fields) | `outputUtils.formatMsg(snapshot, config, 'influxdb')` |
| 2 (register / control) | Parent-child handshake messages | `childRegistrationUtils` via `BaseNodeAdapter` |
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.
The 4-segment key shape **`<type>.<variant>.<position>.<childId>`** is the contractual output of `MeasurementContainer.getFlattenedOutput()`. Position labels are normalised to lowercase. Changing this shape is a forbidden breaking change &mdash; see [Reference &mdash; Limitations](Reference-Limitations#stability--versioning).
---
## 7. Lifecycle — how a node tick or event reaches the output port
## Capability matrix
The sequence below uses `rotatingMachine` as the example. Every stateful EVOLV node follows the same path. See the [rotatingMachine wiki](../rotatingMachine/Home.md) for node-specific detail.
```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)
```
| Capability | Status | Notes |
|:---|:---|:---|
| Base domain scaffolding (`BaseDomain`) | ✅ | Constructor, emitter, logger, measurements, child registry wired automatically |
| Base Node-RED adapter (`BaseNodeAdapter`) | ✅ | Tick/event loop, status badge, input dispatch, Port 0/1/2 output |
| Declarative command dispatch (`CommandRegistry`) | ✅ | Alias deprecation warnings, unit normalisation, `query.units` auto-topic |
| Declarative child-registration routing (`ChildRouter`) | ✅ | Replaces per-node `registerChild` switch blocks |
| Unit policy + conversion (`UnitPolicy`, `convert`) | ✅ | Canonical &harr; output &harr; curve unit sets; dual method/property access |
| Measurement store (`MeasurementContainer`) | ✅ | Chainable, windowed, auto-convert, 4-segment key output |
| InfluxDB + process output formatting (`outputUtils`) | ✅ | Delta-compressed; consumers must cache and merge |
| Status badge helpers (`statusBadge`, `StatusUpdater`) | ✅ | Converged look-and-feel across all nodes |
| Latest-wins async gate (`LatestWinsGate`) | ✅ | Extracted from MGC; shared by PS, VGC, MGC |
| Prediction quality / drift tracking (`HealthStatus`) | ✅ | Frozen plain-object shape; composable |
| Config schema registry (`configManager`) | ✅ | One JSON schema per node in `src/configs/` |
| PID control (`PIDController`, `CascadePIDController`) | ✅ | Full-featured discrete PID with bumpless transfer |
| Curve interpolation (`interpolation`, `predict`) | ✅ | Multidimensional characteristic-curve predictor |
| Statistical helpers (`stats`, `nrmse`, `outliers`) | ✅ | Mean, stddev, median, NRMSE, dynamic-cluster outlier detection |
| Thermodynamic properties (`coolprop`) | ✅ | CoolProp bindings for fluid/gas property lookup |
| FSM for valve/machine states (`state`) | ✅ | StateManager + MovementManager |
| Gravity calculations (`gravity`) | ✅ | WGS-84 model |
| Physical constants (`Fysics`) | ✅ | Air density, viscosity, etc. |
| Browser-side editor dropdowns (`MenuManager`, `menuUtils`) | ✅ | Node-RED editor form population |
| Asset metadata registry (`assetResolver`) | ✅ | Replaces `loadCurve`, `AssetCategoryManager`, ad-hoc JSON readers |
---
## 8. Stability + versioning
## Need more?
Source of truth: `.claude/rules/general-functions.md`.
| Page | What you'll find |
|:---|:---|
| [Reference &mdash; Contracts](Reference-Contracts) | Full public API surface table &mdash; one row per export, with source file, stability tag, and signature |
| [Reference &mdash; Architecture](Reference-Architecture) | Three-tier rules, `src/` directory tree, how 12 nodes consume the library, additive-only export discipline |
| [Reference &mdash; Examples](Reference-Examples) | Usage patterns: extending `BaseDomain` and `BaseNodeAdapter`, registering commands, declaring child routes, `MeasurementContainer` chaining |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues (deprecated `loadCurve`, `outlierDetection` logs to console, `configUtils` silent strip, …) and stability/versioning rules |
| 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, run `grep -r "require('generalFunctions')" nodes/*/` to identify all call sites.
---
## 9. No editor form — consumers' config forms map to config slices
`generalFunctions` has no Node-RED editor form of its own. The library is never placed directly in a flow.
Consumer nodes expose their own editor forms. Each form field writes into a config key that `configManager.buildConfig` validates against the node's schema (in `src/configs/<nodeName>.json`). The resulting merged config is passed to the domain constructor.
For the form-to-config mapping of a specific node, see section 9 of that node's wiki page.
---
## 10. Examples — usage snippets from a real node
### 10.1 Extending `BaseDomain` (from `pumpingStation/specificClass.js` pattern)
```js
const { BaseDomain, UnitPolicy, ChildRouter } = require('generalFunctions');
class PumpingStation extends BaseDomain {
static name = 'pumpingStation';
static unitPolicy = UnitPolicy.declare({
canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
});
configure() {
// Declare named child getters — readable in code, registry is source of truth
this.declareChildGetter('machines', 'machine');
this.declareChildGetter('machineGroups', 'machinegroup');
// Declarative child routing — no per-node registerChild switch
this.router
.onRegister('machinegroup', (child) => this._onMachineGroupRegistered(child))
.onMeasurement('measurement', { type: 'level' }, (data, child) => {
this._onLevel(data.value, data);
});
}
getOutput() {
return {
...this.measurements.getFlattenedOutput(),
...this.basin.snapshot(),
};
}
getStatusBadge() {
const { statusBadge } = require('generalFunctions');
return statusBadge.compose(['filling', 'V=12.4/50.0 m³']);
}
}
module.exports = PumpingStation;
```
### 10.2 Extending `BaseNodeAdapter` (from `pumpingStation/nodeClass.js` pattern)
```js
const { BaseNodeAdapter } = require('generalFunctions');
const Domain = require('./specificClass');
const commands = require('./commands');
class nodeClass extends BaseNodeAdapter {
static DomainClass = Domain;
static commands = commands;
static tickInterval = 1000; // ms — only for time-driven math
static statusInterval = 1000;
buildDomainConfig(uiConfig, nodeId) {
return {
basin: {
volume: Number(uiConfig.basinVolume),
height: Number(uiConfig.basinHeight),
surfaceArea: Number(uiConfig.basinSurface),
},
hydraulics: {
inflowPipeArea: Number(uiConfig.inflowArea),
},
};
}
}
module.exports = nodeClass;
```
### 10.3 Command descriptor with unit normalisation
```js
// src/commands/index.js
module.exports = [
{
topic: 'set.demand',
aliases: ['Qd'], // legacy name — logs one-time deprecation
units: { measure: 'volumeFlowRate', default: 'm3/h' },
payloadSchema: { type: 'number' },
description: 'Operator demand setpoint. Unit-normalised before handler runs.',
handler: (source, msg) => { source.setDemand(msg.payload); },
},
{
topic: 'cmd.startup',
payloadSchema: { type: 'none' },
description: 'Trigger startup sequence.',
handler: (source, msg) => { source.startup(msg.payload?.source); },
},
];
```
---
## 11. Debug recipes
| Symptom | First check | Where to look |
|---|---|---|
| Child never registers (no `registerChild` log) | Is the child's `softwareType` in the `SOFTWARE_TYPE_ALIASES` map? | `src/helper/childRegistrationUtils.js` line 112 and `src/domain/ChildRouter.js` |
| Port 0 sends nothing after an input | `outputUtils` only emits on changes. Is the field actually different from the last call? | Add a debug tap after `formatMsg`; check `outputUtils._output[format]` state |
| Unit mismatch — handler receives wrong value | Did the command descriptor declare `units: { measure, default }`? Is `msg.unit` set by the sender? | `commandRegistry.js``_normaliseUnit()`; check the warn log |
| `query.units` returns empty object | The commands array has no descriptors with a `units` field. | `BaseNodeAdapter._buildImplicitUnitsCommand()` |
| `MeasurementContainer.getFlattenedOutput()` returns unexpected key shape | Key is `type.variant.position.childId` — position is always lowercase. Check `setChildId()` was called. | `src/measurements/MeasurementContainer.js``getFlattenedOutput()` |
| `LatestWinsGate` promise never resolves | A superseded fire resolves with `{ superseded: true }`, not `undefined`. Branch on `r && r.superseded`. | `src/domain/LatestWinsGate.js` |
| Status badge stuck at grey | `getStatusBadge()` threw and `StatusUpdater` caught it. Look for `statusBadge.error(...)` in the container log. | `src/nodered/statusUpdater.js` |
> Never ship `enableLog: 'debug'` in a demo or production config — it fills the container log within seconds and obscures real errors.
---
## 12. 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`. See the `dashboardAPI` wiki for the rationale.
- **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.
---
## 13. Known limitations
| # | Issue | Tracked in |
|---|---|---|
| 1 | `loadCurve` is deprecated; replacement `loadModel` exists but not all nodes have migrated | `OPEN_QUESTIONS.md` — Phase 8.5 cleanup |
| 2 | `outlierDetection` (`DynamicClusterDeviation`) prints to `console.log` internally — not routed through `logger` | Code review backlog |
| 3 | `configUtils.initConfig` strips unknown keys silently; schema must include every key the domain reads or defaults are lost | `OPEN_QUESTIONS.md` — e.g. monster schema fix 2026-05-11 |
| 4 | `state` (FSM) and `predict` are not yet integrated with `BaseDomain` lifecycle — nodes wire them manually in `configure()` | Architecture backlog |
| 5 | `menuUtils` / `MenuManager` are served as browser JavaScript and bypass the normal Node.js import path — deep changes require testing in both environments | `endpointUtils.js` |
| 6 | `CascadePIDController` has no dedicated test suite | Test backlog |
| 7 | Wiki autogen script (`wiki:all`) not yet wired for this library; API surface block is hand-maintained | Phase 9 follow-up |
[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) &middot; [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) &middot; [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)

View File

@@ -0,0 +1,286 @@
# Reference &mdash; Architecture
![code-ref](https://img.shields.io/badge/code--ref-48fa543-blue)
> [!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 &mdash; 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 &rarr; domain instantiation &rarr; registration delay &rarr; output strategy &rarr; status loop &rarr; input dispatch &rarr; 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()` &mdash; `dashboardAPI` has the lightest (HTTP gateway, no FSM); `rotatingMachine` and `machineGroupControl` have the densest (full curve loading, drift assessor, multi-source pressure routing).
---
## Lifecycle &mdash; 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;` &mdash; 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 &mdash; no registration step.
---
## Stability &mdash; 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&ndash;§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) &mdash; never in core logic.
---
## Adding a new export &mdash; 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 &mdash; 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 &mdash; Contracts](Reference-Contracts) | Full public API surface, per-export stability tags |
| [Reference &mdash; Examples](Reference-Examples) | Usage patterns from real consumer nodes |
| [Reference &mdash; 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 &mdash; Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |

180
wiki/Reference-Contracts.md Normal file
View File

@@ -0,0 +1,180 @@
# Reference &mdash; Contracts
![code-ref](https://img.shields.io/badge/code--ref-48fa543-blue)
> [!NOTE]
> The full public API surface &mdash; one row per export from `require('generalFunctions')`, with source file, stability tag, and contract summary. Source of truth: `index.js` (the barrel). For an intuitive overview, return to [Home](Home).
>
> **Stability tags:**
>
> - `stable` — API change requires a deprecation cycle and a CONTRACT update.
> - `experimental` — may change without warning; do not depend on the exact shape in production code paths.
> - `deprecated` — kept for backwards compatibility, slated for removal.
---
## Platform base classes
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `BaseDomain` | stable | `src/domain/BaseDomain.js` | Abstract base class for every `specificClass.js`. Provides `emitter`, `config`, `logger`, `measurements`, `childRegistrationUtils`, `router`. Subclass must declare `static name` (maps to the schema JSON file in `src/configs/`) and implement `configure()`. See [CONTRACTS.md §3](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `BaseNodeAdapter` | stable | `src/nodered/BaseNodeAdapter.js` | Abstract base for every `nodeClass.js`. Wires config build &rarr; domain instantiation &rarr; registration delay &rarr; output strategy &rarr; status loop &rarr; input dispatch &rarr; close handler. Subclass declares `static DomainClass`, `static commands`, `static tickInterval`, `static statusInterval`, and overrides `buildDomainConfig(uiConfig, nodeId)`. See [CONTRACTS.md §2](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `ChildRouter` | stable | `src/domain/ChildRouter.js` | Declarative parent-side child registration. Replaces per-node `registerChild` switch. Chain `.onRegister(softwareType, cb)`, `.onMeasurement(softwareType, filter, cb)`, `.onPrediction(softwareType, filter, cb)`. See [CONTRACTS.md §5](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `CommandRegistry` | stable | `src/nodered/commandRegistry.js` | Class form of the command registry. Accepts array of descriptors `{topic, aliases, payloadSchema, units, description, handler}`. Dispatches by `O(1)` lookup, normalises units before handler runs, warns on alias use. |
| `createRegistry` | stable | `src/nodered/commandRegistry.js` | Factory: `createRegistry(descriptors, options) → CommandRegistry`. Used by `BaseNodeAdapter`; rarely needed directly. |
| `UnitPolicy` | stable | `src/domain/UnitPolicy.js` | Declare unit sets: `UnitPolicy.declare({ canonical, output, curve?, requireUnitForTypes? })`. Returns policy with dual method/property access (`policy.canonical('flow')` and `policy.canonical.flow`). Methods: `canonical`, `output`, `curve`, `resolve`, `convert`, `containerOptions`, `setLogger`. See [CONTRACTS.md §6](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `LatestWinsGate` | stable | `src/domain/LatestWinsGate.js` | Serialises async dispatches so only the latest value wins. `fire(value)` &mdash; non-blocking. `fireAndWait(value) → Promise` that resolves with dispatch result or `LatestWinsGate.SUPERSEDED`. `drain()` &mdash; await idle. See [CONTRACTS.md §8](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `HealthStatus` | stable | `src/domain/HealthStatus.js` | Factory functions for frozen health objects: `HealthStatus.ok(msg, src)`, `HealthStatus.degraded(level, flags, msg, src)`, `HealthStatus.compose(statuses)`. Shape: `{ level: 0..3, flags: string[], message, source }`. See [CONTRACTS.md §9](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `statusBadge` | stable | `src/nodered/statusBadge.js` | Pure-function badge builder. `statusBadge.compose(parts, opts?) → {fill, shape, text}`. `statusBadge.error(msg)`, `statusBadge.idle(label)`. Text clipped to 60 chars. See [CONTRACTS.md §7](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md). |
| `StatusUpdater` | stable | `src/nodered/statusUpdater.js` | `new StatusUpdater({ node, source, intervalMs, logger })`. `start()`, `stop()`. Calls `source.getStatusBadge()` on interval; catches errors and shows a red badge. Owned by `BaseNodeAdapter` &mdash; rarely needed directly. |
---
## Measurements
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `MeasurementContainer` | stable | `src/measurements/MeasurementContainer.js` | Chainable measurement store: `.type().variant().position().value(v, ts, srcUnit)`. Query: `getCurrentValue(unit)`, `getAverage(unit)`, `difference({ from, to, unit })`. Introspect: `getFlattenedOutput()` returns 4-segment keyed object (`type.variant.position.childId`). Auto-converts on write to canonical units per the supplied `UnitPolicy`. |
| `POSITIONS` | stable | `src/constants/positions.js` | Frozen enum: `{ UPSTREAM, DOWNSTREAM, AT_EQUIPMENT, DELTA }`. |
| `POSITION_VALUES` | stable | `src/constants/positions.js` | `string[]` of all position strings. |
| `isValidPosition` | stable | `src/constants/positions.js` | `(pos: string) => boolean`. |
### 4-segment output key
The contractual output of `MeasurementContainer.getFlattenedOutput()` is:
```
<type>.<variant>.<position>.<childId>
```
| Segment | Examples | Notes |
|:---|:---|:---|
| `type` | `flow`, `pressure`, `power`, `temperature`, `level`, `efficiency` | Lowercase. |
| `variant` | `predicted`, `measured`, `setpoint`, `max`, `min` | Lowercase. |
| `position` | `upstream`, `downstream`, `atequipment`, `delta` | Always lowercase &mdash; e.g. `atequipment`, not `atEquipment`. |
| `childId` | `default`, `<child.general.id>`, `dashboard-sim-upstream`, &hellip; | `default` for the node's own predictions; otherwise the registering child's id. |
Changing this shape is a forbidden breaking change &mdash; see [Reference &mdash; Limitations](Reference-Limitations#stability--versioning).
---
## Output formatting
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `outputUtils` | stable | `src/helper/outputUtils.js` | Singleton-per-node delta-compression engine. `formatMsg(output, config, format)` returns `msg` only when fields changed, or `undefined`. `format` is `'process'` or `'influxdb'`. Consumers must cache and merge. |
| `logger` | stable | `src/helper/logger.js` | `new logger(enabled, logLevel, moduleName)`. Methods: `debug`, `info`, `warn`, `error`, `setLogLevel`, `toggleLogging`. Use this instead of `console.log`. |
---
## Configuration
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `configManager` | stable | `src/configs/index.js` | `new configManager()`. Methods: `getConfig(name)`, `buildConfig(name, uiConfig, nodeId, domainSlice?)`, `getAvailableConfigs()`, `hasConfig(name)`. Config files live in `src/configs/*.json`. |
| `configUtils` | stable | `src/helper/configUtils.js` | `new configUtils(defaultConfig)`. `initConfig(userConfig)` validates and merges user values over defaults via `validationUtils`. |
| `validation` | stable | `src/helper/validationUtils.js` | `new validation(logEnabled, logLevel)`. `validateSchema(config, schema, name)` walks schema, clamps numbers, coerces types, strips unknown keys. |
| `assertions` | stable | `src/helper/` | Runtime validation primitives. |
| `assetApiConfig` | stable | `src/configs/assetApiConfig.js` | Asset-registry HTTP backend config. |
| `MenuManager` | stable | `src/menu/index.js` | `new MenuManager()`. Manages editor dropdown menus (asset, logger, position, aquon). `registerMenu(type, factory)`. Used in node entry files to power Node-RED editor forms. |
---
## Child registration
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `childRegistrationUtils` | stable | `src/helper/childRegistrationUtils.js` | `new childRegistrationUtils(parentDomain)`. `registerChild(child, positionVsParent, distance?)` stores child by softwareType/category with alias normalisation. `getChildrenOfType(softwareType, category?)`, `getChildById(id)`, `getAllChildren()`. Normally used via `ChildRouter` &mdash; direct use is for advanced cases. |
---
## Unit conversion + physics
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `convert` | stable | `src/convert/index.js` | Unit-converter factory. `convert(value).from(unit).to(unit)`. `convert.possibilities(measure)` lists accepted units. Measures: `volumeFlowRate`, `pressure`, `power`, `temperature`, `volume`, `length`, `mass`, `energy`, `reactivePower`, `apparentPower`, `reactiveEnergy`, and more. |
| `Fysics` | stable | `src/convert/fysics.js` | `new Fysics()`. Physical constants: `air_density`, `g0`; methods for gravity and viscosity calculations. |
| `gravity` | stable | `src/helper/gravity.js` | Singleton-style `Gravity` instance. `getStandardGravity() → 9.80665 m/s²`. WGS-84 latitude / altitude corrections available. |
| `coolprop` | stable | `src/coolprop-node/src/index.js` | CoolProp fluid/gas thermodynamic property lookup. Used by nodes that model heat transfer or gas compression. |
---
## Control & prediction
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `PIDController` | stable | `src/pid/PIDController.js` | Discrete PID with bumpless auto/manual transfer, anti-windup, derivative filtering, rate limiting, gain scheduling, feedforward. |
| `CascadePIDController` | stable | `src/pid/PIDController.js` | Outer-inner PID cascade built on `PIDController`. |
| `createPidController` | stable | `src/pid/index.js` | Factory shorthand: `createPidController(options) → PIDController`. |
| `createCascadePidController` | stable | `src/pid/index.js` | Factory shorthand for cascade PID. |
| `predict` | stable | `src/predict/predict_class.js` | `new predict(config, logger)`. Multidimensional characteristic-curve predictor; emits results via internal `EventEmitter`. |
| `interpolation` | stable | `src/predict/interpolation.js` | Class for 1-D and 2-D curve interpolation (linear, cubic-spline). Used internally by `predict`. |
| `nrmse` | stable | `src/nrmse/index.js` | `ErrorMetrics` class for normalised-root-mean-squared-error tracking. Multi-metric via `registerMetric(id)`, `update(id, predicted, measured)`. |
| `stats` | stable | `src/stats/index.js` | Pure functions: `mean(arr)`, `stdDev(arr)`, `median(arr)`. No state; safe to call on any numeric array. |
| `state` | stable | `src/state/index.js` | `new state(config, logger)`. FSM for valve / machine: `StateManager` (transitions) + `MovementManager` (timed moves). Emits state-change events. |
---
## Asset registry
| Export | Stability | Source | Contract |
|:---|:---|:---|:---|
| `assetResolver` | stable | `src/registry/index.js` | Singleton. `.resolve(category, modelId)` &mdash; sync, case-insensitive, returns `null` on miss. |
| `AssetResolver` | stable | `src/registry/index.js` | Resolver class (for testing / alternate backends). |
| `FileBackend` | stable | `src/registry/` | File-system asset backend. |
| `HttpBackend` | stable | `src/registry/` | HTTP asset backend. |
| `loadCurve` | **deprecated** | `index.js` (shim) | Thin shim over `assetResolver.resolve('curves', modelId)`. New code uses the resolver directly. |
---
## Canonical units (the platform-wide contract)
`MeasurementContainer` and all internal processing assume canonical units. Unit conversion happens at system boundaries (input via `CommandRegistry.units` normalisation, output via `UnitPolicy.output` rendering) &mdash; never in core logic.
| Quantity | Canonical (internal) | Typical output | Typical curve |
|:---|:---|:---|:---|
| Pressure | `Pa` | `mbar` | `mbar` |
| Atmospheric pressure | `Pa` | `Pa` | &mdash; |
| Flow | `m3/s` | `m3/h` | `m3/h` |
| Power | `W` | `kW` | `kW` |
| Temperature | `K` | `°C` | &mdash; |
| Control | &mdash; | &mdash; | `%` |
Each node declares its own `UnitPolicy` (typically as `static unitPolicy = UnitPolicy.declare({...})` on the domain class). The policy is passed to `MeasurementContainer` via `unitPolicy.containerOptions()`.
---
## Output ports (provided by `BaseNodeAdapter`)
Every node that extends `BaseNodeAdapter` automatically gets three ports:
| Port | Carries | Built by | Notes |
|:---|:---|:---|:---|
| 0 (process) | Delta-compressed state snapshot &mdash; the `getOutput()` return | `outputUtils.formatMsg(snapshot, config, 'process')` | Emits only when fields change. Consumers must cache and merge. |
| 1 (telemetry) | InfluxDB line-protocol payload (same fields as Port 0) | `outputUtils.formatMsg(snapshot, config, 'influxdb')` | Tags + fields per the schema. |
| 2 (register / control) | Parent-child handshake messages | `childRegistrationUtils` via `BaseNodeAdapter` | `child.register` at startup; subsequent `child.measurement` / `child.prediction` events. |
---
## Adding a new export &mdash; the dance
See [Reference &mdash; Architecture](Reference-Architecture#adding-a-new-export--the-dance) for the full step-by-step. Summary:
1. Implement under `src/<concern>/`.
2. Re-export from `index.js` (alphabetical within 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 stability tag.
4. If it's a new platform shape, also update [`.claude/refactor/CONTRACTS.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md).
5. Add a test under `test/`.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Architecture](Reference-Architecture) | Three-tier rule, `src/` layout, consumer responsibilities |
| [Reference &mdash; Examples](Reference-Examples) | Usage patterns: extending base classes, registering commands, declaring child routes |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues, deprecations, stability rules |
| [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) | The authoritative platform base-class + protocol spec |
| [Library CONTRACT.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/nodes/generalFunctions/CONTRACT.md) | Per-export source-of-truth with stability tags |

361
wiki/Reference-Examples.md Normal file
View File

@@ -0,0 +1,361 @@
# Reference &mdash; Examples
![code-ref](https://img.shields.io/badge/code--ref-48fa543-blue)
> [!NOTE]
> Usage patterns: how a consumer node imports and extends the library's base classes, how to register topic commands, how to declare child routes, and how to chain `MeasurementContainer` writes. Snippets are pulled from real consumer nodes (`rotatingMachine`, `pumpingStation`, `machineGroupControl`). For an intuitive overview, return to [Home](Home).
---
## 1. Single root import &mdash; the contract
```js
const {
BaseDomain, BaseNodeAdapter, UnitPolicy, ChildRouter, HealthStatus, LatestWinsGate,
MeasurementContainer, outputUtils, logger, statusBadge,
convert, PIDController,
} = require('generalFunctions');
```
The package root (`require('generalFunctions')`) is the only contractual import path. Internal subpaths (`require('generalFunctions/src/domain/UnitPolicy')`) are NOT contractual and may move at any time.
---
## 2. Extending `BaseDomain` &mdash; pattern from `pumpingStation/specificClass.js`
```js
const { BaseDomain, UnitPolicy } = require('generalFunctions');
class PumpingStation extends BaseDomain {
// static name must match src/configs/<nodeName>.json on the library side.
static name = 'pumpingStation';
// Declarative unit triple. canonical = internal storage. output = render units.
// curve = supplier curve units (only if the node consumes a characteristic curve).
static unitPolicy = UnitPolicy.declare({
canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
requireUnitForTypes: ['pressure', 'flow', 'power', 'temperature'],
});
configure() {
// Named child getters — readable in code, but the registry remains source of truth.
this.declareChildGetter('machines', 'machine');
this.declareChildGetter('machineGroups', 'machinegroup');
// Declarative child routing — no per-node registerChild switch needed.
this.router
.onRegister('machinegroup', (child) => this._onMachineGroupRegistered(child))
.onMeasurement('measurement', { type: 'level' }, (data, child) => {
this._onLevel(data.value, data);
});
}
getOutput() {
return {
...this.measurements.getFlattenedOutput(),
...this.basin.snapshot(),
};
}
getStatusBadge() {
return statusBadge.compose(['filling', 'V=12.4/50.0 m³']);
}
}
module.exports = PumpingStation;
```
Key points:
- `static name = '...'` &mdash; tells `configManager.buildConfig()` which `src/configs/<n>.json` file to merge defaults from.
- `static unitPolicy` &mdash; pre-built `UnitPolicy` instance; `BaseDomain` passes `unitPolicy.containerOptions()` to the `MeasurementContainer` so it auto-converts on write.
- `configure()` is where you wire `ChildRouter` routes and instantiate concern modules. The constructor is owned by `BaseDomain`.
- `getOutput()` and `getStatusBadge()` are the only two methods `BaseNodeAdapter` calls on the domain to produce ports + status &mdash; everything else is event-driven.
---
## 3. Extending `BaseNodeAdapter` &mdash; pattern from `pumpingStation/nodeClass.js`
```js
const { BaseNodeAdapter } = require('generalFunctions');
const Domain = require('./specificClass');
const commands = require('./commands');
class nodeClass extends BaseNodeAdapter {
static DomainClass = Domain; // The specificClass to instantiate.
static commands = commands; // Array of command descriptors.
static tickInterval = 1000; // ms — only for time-driven math. Omit for event-driven nodes.
static statusInterval = 1000; // ms — how often to re-render the status badge.
// Translate Node-RED editor field values into the domain's config slice.
// The base class already merges schema defaults from src/configs/<nodeName>.json;
// this hook lets the adapter shape per-node values before the domain sees them.
buildDomainConfig(uiConfig, nodeId) {
return {
basin: {
volume: Number(uiConfig.basinVolume),
height: Number(uiConfig.basinHeight),
surfaceArea: Number(uiConfig.basinSurface),
},
hydraulics: {
inflowPipeArea: Number(uiConfig.inflowArea),
},
};
}
}
module.exports = nodeClass;
```
`BaseNodeAdapter` wires the full lifecycle: schema merge &rarr; domain instantiation &rarr; Port 2 registration after a 100 ms delay &rarr; status loop start &rarr; input dispatch via the registry &rarr; close handler that drains everything. The subclass only declares the static config and overrides `buildDomainConfig`.
---
## 4. Command descriptors with unit normalisation
```js
// src/commands/index.js
module.exports = [
{
topic: 'set.demand',
aliases: ['Qd'], // Legacy name — first use logs a one-time deprecation.
units: { measure: 'volumeFlowRate', default: 'm3/h' },
payloadSchema: { type: 'number' },
description: 'Operator demand setpoint. Unit-normalised before handler runs.',
handler: (source, msg) => { source.setDemand(msg.payload); },
},
{
topic: 'cmd.startup',
payloadSchema: { type: 'none' },
description: 'Trigger startup sequence.',
handler: (source, msg) => { source.startup(msg.payload?.source); },
},
{
topic: 'set.flow-setpoint',
aliases: ['flowMovement'],
units: { measure: 'volumeFlowRate', default: 'm3/h' },
payloadSchema: { type: 'object', properties: { setpoint: { type: 'number' } } },
description: 'Set a flow-unit setpoint. Auto-converted to canonical m³/s.',
handler: (source, msg) => { source.setFlowSetpoint(msg.payload.setpoint); },
},
];
```
When `units` is declared, `CommandRegistry` reads `msg.unit` from the incoming message (falling back to `default`) and converts via the `convert` library to the canonical unit before invoking the handler. The handler always sees a canonical value &mdash; it never has to do its own unit conversion.
A free side-effect: every command descriptor with a `units` field contributes a row to the auto-generated `query.units` reply, which dashboards can use to introspect a node's unit contract at runtime.
---
## 5. Declarative child routing &mdash; `ChildRouter`
```js
configure() {
this.router
// Trigger a callback the first time a machine-group child registers.
.onRegister('machinegroup', (child) => {
this.logger.info(`MachineGroup ${child.general.id} attached`);
this._mgcChild = child;
})
// Filter on a measurement child's asset.type.
.onMeasurement('measurement', { type: 'pressure', position: 'upstream' }, (data, child) => {
this._onUpstreamPressure(data.value, data);
})
.onMeasurement('measurement', { type: 'pressure', position: 'downstream' }, (data, child) => {
this._onDownstreamPressure(data.value, data);
})
.onMeasurement('measurement', { type: 'flow' }, (data, child) => {
// No position filter → matches any position.
this._onFlow(data.value, data, child);
})
// React to a child's own predictions (e.g. a downstream MGC publishing predicted group flow).
.onPrediction('machinegroup', { type: 'flow' }, (data, child) => {
this._onChildPrediction(data, child);
});
}
```
Pre-refactor, the same code lived as a `registerChild(child)` method on every node with a 30-line `switch (child.softwareType)` block. `ChildRouter` makes the wiring declarative; the underlying `childRegistrationUtils` calls are unchanged.
---
## 6. `MeasurementContainer` chaining
```js
// Write: chainable, auto-converts from srcUnit to canonical per UnitPolicy.
this.measurements
.type('pressure')
.variant('measured')
.position('upstream', child.general.id) // childId narrows the storage slot.
.value(3.4, Date.now(), 'mbar'); // value, timestamp, srcUnit.
// Read: latest value in canonical or arbitrary unit.
const p_Pa = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue();
const p_mbar = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue('mbar');
// Read: windowed average.
const avg = this.measurements.type('flow').variant('measured').position('atequipment').getAverage('m3/h');
// Read: difference over a time window (e.g. for integrators).
const dV = this.measurements
.type('level').variant('measured').position('atequipment')
.difference({ from: Date.now() - 60_000, to: Date.now(), unit: 'm' });
// Introspect: the 4-segment flat output (used by getOutput()).
const flat = this.measurements.getFlattenedOutput();
// → {
// 'pressure.measured.upstream.dashboard-sim-upstream': 0,
// 'pressure.measured.downstream.dashboard-sim-downstream': 1100,
// 'flow.predicted.downstream.default': 12.4,
// 'power.predicted.atequipment.default': 18.2,
// }
```
Key shape: `<type>.<variant>.<position>.<childId>`. Position labels are always lowercase in keys (`atequipment`, not `atEquipment`). The `childId` is `default` for the node's own predictions; otherwise the registering child's `general.id`.
---
## 7. `HealthStatus` &mdash; prediction quality / drift state
```js
const { HealthStatus } = require('generalFunctions');
// Ok state.
const ok = HealthStatus.ok('Pressure source healthy', 'real-child');
// Degraded with reason flags.
const warm = HealthStatus.degraded(1, ['pressure_init_warming'], 'Pressure not yet initialised', 'dashboard-sim');
// Compose multiple sub-statuses into the worst case.
const overall = HealthStatus.compose([ok, warm, flowDrift, powerDrift]);
// → frozen { level: max(level_i), flags: union(flags_i), message, source }
```
Levels: `0 = good`, `1 = warming`, `2 = degraded`, `3 = invalid`. The shape is frozen; you cannot mutate a `HealthStatus` instance, only compose new ones.
---
## 8. `LatestWinsGate` &mdash; latest-write-wins async dispatch
```js
const { LatestWinsGate } = require('generalFunctions');
// Construct.
this._dispatchGate = new LatestWinsGate({
dispatch: async (value) => { await this._reallySetDemand(value); },
logger: this.logger,
});
// Fire (non-blocking; intermediate calls are superseded).
this._dispatchGate.fire(newDemand);
// Fire and await result.
const result = await this._dispatchGate.fireAndWait(newDemand);
if (result === LatestWinsGate.SUPERSEDED) {
// A newer fire pre-empted this one; nothing to do.
}
// Wait until idle (useful in tests and clean shutdown).
await this._dispatchGate.drain();
```
Originally extracted from `machineGroupControl` to coordinate fast successive demand changes against a slow dispatcher. Now shared by `pumpingStation`, `valveGroupControl`, `machineGroupControl`.
---
## 9. PID controller
```js
const { createPidController } = require('generalFunctions');
const pid = createPidController({
kp: 1.2, ki: 0.4, kd: 0.05,
outputLimits: { min: 0, max: 100 },
rateLimitPerSec: 5, // %/s ramp cap
derivativeFilterTau: 0.2, // first-order LPF on the D term
antiWindup: 'clamping',
setpoint: 50,
});
pid.setSetpoint(60); // bumpless on the next compute call
const output = pid.compute(processValue); // discrete tick
```
For cascaded loops (outer = level &rarr; inner = flow), use `createCascadePidController({ outer: {...}, inner: {...} })`.
---
## 10. Status badge composition
```js
const { statusBadge } = require('generalFunctions');
getStatusBadge() {
const state = this.state.getCurrentState();
const flowFmt = `${(this._predictedFlow * 3600).toFixed(1)} m³/h`;
const powerFmt = `${(this._predictedPower / 1000).toFixed(1)} kW`;
if (state === 'emergencystop') {
return statusBadge.error('E-stop active');
}
if (state === 'idle') {
return statusBadge.idle('idle');
}
return statusBadge.compose([state, flowFmt, powerFmt]);
// → { fill: 'green', shape: 'dot', text: 'operational | 12.4 m³/h | 18.2 kW' }
}
```
`StatusUpdater` polls `getStatusBadge()` every `statusInterval` ms and calls `node.status(...)`. Text clipped to 60 chars to fit the Node-RED editor.
---
## 11. Unit conversion (when you really do need it directly)
```js
const { convert } = require('generalFunctions');
const m3s = convert(80).from('m3/h').to('m3/s'); // 0.0222...
// What units can a measure take?
const units = convert.possibilities('volumeFlowRate');
// → ['m3/s', 'm3/h', 'l/s', 'l/min', 'gpm', ...]
```
In domain code, you should usually be relying on the `UnitPolicy` + `MeasurementContainer` pipeline to convert at the boundary &mdash; calling `convert` directly is a smell unless you're processing a one-off ad-hoc payload.
---
## 12. Loading a per-node JSON schema
```js
const { configManager } = require('generalFunctions');
const cm = new configManager();
// What schemas are registered?
const names = cm.getAvailableConfigs();
// → ['baseConfig', 'rotatingMachine', 'pumpingStation', 'measurement', ...]
// Merge editor values over schema defaults.
const merged = cm.buildConfig('pumpingStation', uiConfig, nodeId, domainSlice);
```
`BaseNodeAdapter` does this for you in the constructor. Direct use is for tests and migration tooling.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Full public API surface, per-export stability tags |
| [Reference &mdash; Architecture](Reference-Architecture) | Three-tier rule, `src/` layout, consumer responsibilities |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues, deprecations, stability rules |
| [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) | The authoritative base-class spec |
| [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | A consumer node that uses every primitive |

View File

@@ -0,0 +1,217 @@
# Reference &mdash; Limitations
![code-ref](https://img.shields.io/badge/code--ref-48fa543-blue)
> [!NOTE]
> What `generalFunctions` does not do, current rough edges, stability/versioning rules, and open questions. For an intuitive overview, return to [Home](Home).
---
## 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`. See the `dashboardAPI` wiki for the rationale.
- **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 &mdash; they may only use root-level exports.
---
## Known limitations
### 1. `loadCurve` is deprecated
`loadCurve(modelId)` is kept as a thin shim over `assetResolver.resolve('curves', modelId)` so legacy consumers don't have to change in one go. New code should use `assetResolver` directly. Replacement `loadModel` exists but not every node has migrated.
- **Tracked in**: `OPEN_QUESTIONS.md` &mdash; Phase 8.5 cleanup.
### 2. `outlierDetection` (`DynamicClusterDeviation`) prints to `console.log`
The dynamic-cluster outlier detector emits diagnostic lines via `console.log` directly, bypassing the structured `logger`. This means its output cannot be silenced per-node and doesn't honour `logLevel`. Fix is routing through `logger` like the rest of the library.
- **Tracked in**: Code review backlog.
### 3. `configUtils.initConfig` silently strips unknown keys
When the user config carries a key that isn't in the schema, `configUtils.initConfig` (via `validationUtils.validateSchema`) silently drops it. This means a typo in an editor field name or a missed schema entry results in the default value being used &mdash; with no error, no warning, no log line.
Workaround: the schema must include every key the domain reads, with a sensible default. The 2026-05-11 monster schema fix was a direct consequence of this gotcha.
- **Tracked in**: `OPEN_QUESTIONS.md` &mdash; e.g. monster schema fix.
### 4. `state` (FSM) and `predict` are not yet integrated with `BaseDomain` lifecycle
The state machine and the prediction class are exported but not lifecycle-managed by `BaseDomain`. Consumer nodes wire them manually in `configure()` &mdash; constructor, event subscriptions, teardown. A second wave of refactor work will move them under the `BaseDomain` umbrella so subclasses get them for free.
- **Tracked in**: Architecture backlog.
### 5. `menuUtils` / `MenuManager` bypass the Node.js import path
These are served as browser JavaScript via the admin `endpointUtils` and run in the Node-RED editor's iframe. Deep changes require testing in both environments (Node-side schema validation, browser-side editor form rendering). There is no automated test harness for the browser side.
- **Tracked in**: `endpointUtils.js` comments.
### 6. `CascadePIDController` has no dedicated test suite
`PIDController` is unit-tested; the cascade variant is not. Adding tests is on the backlog.
- **Tracked in**: Test backlog.
### 7. Wiki autogen is hand-maintained
The API surface section is hand-maintained between the `<!-- BEGIN/END AUTOGEN: api-surface -->` markers in `CONTRACT.md`. There is no `npm run wiki:all` script (yet); when an export is added or changed, the table must be edited by hand. Mitigation: the source-of-truth is the barrel (`index.js`); when in doubt, trust the barrel.
- **Tracked in**: Phase 9 follow-up.
### 8. Single-side pressure handling lives in consumers
Consumer-node concerns like single-side pressure degradation, residue handling, and sequence-abort semantics are NOT centralised in this library &mdash; each consumer (`rotatingMachine`, `valveGroupControl`, &hellip;) implements its own variant. Cross-node consistency is by convention, not by enforcement. A future `BaseDomain` extension could pull common pressure-routing patterns up.
- **Tracked in**: Internal architecture notes.
### 9. Asset registry backends are not fully symmetric
`FileBackend` is the production default (sync, in-process JSON). `HttpBackend` is provided for remote-resolver scenarios but has fewer call sites and less test coverage. If you switch to `HttpBackend` in production, expect to find edge-case differences.
- **Tracked in**: Internal &mdash; not yet ticketed.
### 10. No editor form
`generalFunctions` is never placed in a flow. It has no Node-RED type registration, no `.html`, no admin endpoint of its own. Consumer nodes expose their own editor forms; each form field writes into a config key that `configManager.buildConfig` validates against the node's schema in `src/configs/<nodeName>.json`. This is a deliberate design choice, not a limitation &mdash; documented here for visitors searching for "where's the editor form".
---
## Stability + versioning
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](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) §1&ndash;§9 shapes. |
### Cross-node impact
`generalFunctions` is a git submodule shared by all 12 node repos. **Any change here can break any node.** Before modifying any module:
```bash
# Identify all consumers of the symbol you're touching.
grep -r "require('generalFunctions')" nodes/*/
# Or for a specific export:
grep -rn "BaseDomain\|UnitPolicy\|MeasurementContainer" nodes/*/src/
```
After changes, run the test suites of every affected consumer node, not just `generalFunctions/test/`.
### 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) &mdash; never in core logic. Code that assumes anything else is a bug.
---
## Deprecations
| Symbol | Status | Replacement | Plan |
|:---|:---|:---|:---|
| `loadCurve(modelId)` | deprecated | `assetResolver.resolve('curves', modelId)` | Remove after every consumer migrates. Tracked in Phase 8.5. |
When a symbol is marked deprecated:
1. The row in `CONTRACT.md` flips to `deprecated` and gains a "removed-in" line.
2. Consumers in `nodes/*` are updated to the replacement.
3. Each touched node's submodule pin is bumped in the superproject.
4. After one release on `development` with no consumers, the export and its row are removed.
---
## Open questions (tracked)
| Question | Where it lives |
|:---|:---|
| Phase 8.5: complete `loadCurve` &rarr; `assetResolver` migration | Internal |
| Route `DynamicClusterDeviation` log lines through `logger` | Code review backlog |
| Surface a warning when `configUtils.initConfig` strips a key not in schema | `OPEN_QUESTIONS.md` |
| Move `state` (FSM) and `predict` under `BaseDomain` lifecycle | Architecture backlog |
| Browser-side test harness for `menuUtils` | `endpointUtils.js` |
| Test suite for `CascadePIDController` | Test backlog |
| Wiki autogen script (`npm run wiki:all`) for the API surface section | Phase 9 follow-up |
| `HttpBackend` test coverage parity with `FileBackend` | Internal |
| Centralised single-side-pressure handling pattern in `BaseDomain` | Internal architecture notes |
---
## Migration notes
### Pre-refactor: per-node `registerChild` switch
The `ChildRouter` replaces hand-written `registerChild(child)` methods. The mechanical migration:
```js
// Before:
registerChild(child) {
switch (child.softwareType) {
case 'measurement':
if (child.config.asset.type === 'pressure' && child.positionVsParent === 'upstream') {
this._onUpstream(child);
} else if (child.config.asset.type === 'flow') {
this._onFlow(child);
}
break;
case 'machinegroup':
this._onMgcChild(child);
break;
}
}
// After (in configure()):
this.router
.onMeasurement('measurement', { type: 'pressure', position: 'upstream' }, (data, child) => this._onUpstream(child))
.onMeasurement('measurement', { type: 'flow' }, (data, child) => this._onFlow(child))
.onRegister('machinegroup', (child) => this._onMgcChild(child));
```
Behaviour is identical (the underlying `childRegistrationUtils` calls are unchanged); the wiring is just declarative.
### Pre-refactor: per-node `getStatusBadge` duplication
The `statusBadge` pure-function helpers replaced 12 copies of slightly different status-text formatters. New domains should use `statusBadge.compose(parts, opts)`, `statusBadge.error(msg)`, `statusBadge.idle(label)` instead of building `{fill, shape, text}` by hand. Text is clipped to 60 chars to fit the Node-RED editor.
### Pre-AssetResolver: `loadCurve` shim
Old code:
```js
const { loadCurve } = require('generalFunctions');
const curve = loadCurve('SomeModel');
```
New code (preferred):
```js
const { assetResolver } = require('generalFunctions');
const curve = assetResolver.resolve('curves', 'SomeModel');
```
The shim still works, but the next API-surface review may remove it. Migrate when next touching the file.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Full public API surface, per-export stability tags |
| [Reference &mdash; Architecture](Reference-Architecture) | Three-tier rule, `src/` layout, consumer responsibilities |
| [Reference &mdash; Examples](Reference-Examples) | Usage patterns: extending base classes, registering commands, declaring child routes |
| [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) | The authoritative platform base-class + protocol spec |
| [`.claude/rules/general-functions.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/general-functions.md) | Stability + change-impact rules |

22
wiki/_Sidebar.md Normal file
View File

@@ -0,0 +1,22 @@
### generalFunctions (Library)
- [Home](Home)
**Reference**
- [Contracts](Reference-Contracts)
- [Architecture](Reference-Architecture)
- [Examples](Reference-Examples)
- [Limitations](Reference-Limitations)
**Related**
- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home)
- [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md)
- [Library CONTRACT.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/nodes/generalFunctions/CONTRACT.md)
- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home)
- [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home)
- [pumpingStation wiki](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home)
- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns)
- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)