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