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)