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>
15 KiB
Reference — Architecture
Note
The shape of the library: the three-tier rule it enforces on consumer nodes, the
src/directory layout, how 12 EVOLV nodes consume each module, and the additive-only export discipline. For an intuitive overview, return to Home.
Three-tier rule the library enforces
Every consumer node follows the same three-tier sandwich. generalFunctions provides the base classes for tiers 2 and 3; the entry file is per-node.
nodes/<nodeName>/
|
+-- <nodeName>.js entry: RED.nodes.registerType(...)
|
+-- src/
nodeClass.js extends BaseNodeAdapter <-- generalFunctions
specificClass.js extends BaseDomain <-- generalFunctions
commands/index.js CommandRegistry descriptors <-- generalFunctions
| Tier | Owns | May call RED.* |
Provided by |
|---|---|---|---|
| entry | Type registration, admin endpoints | Yes | per-node <nodeName>.js |
| nodeClass | Input routing, output ports, tick / status loops, registration delay | Yes | BaseNodeAdapter (this library) |
| specificClass | Domain logic, FSM, predictions, drift — no RED.* |
No | BaseDomain (this library) |
Authoritative platform spec: .claude/refactor/CONTRACTS.md sections 2 (nodeClass), 3 (specificClass), 4 (commandRegistry), 5 (ChildRouter), 6 (UnitPolicy), 7 (statusBadge), 9 (HealthStatus).
src/ directory tree
generalFunctions/
|
+-- index.js barrel — the only contractual import path
+-- CONTRACT.md per-export stability tags + cross-refs
|
+-- src/
| +-- domain/ base classes for specificClass.js
| | BaseDomain.js
| | ChildRouter.js
| | UnitPolicy.js
| | LatestWinsGate.js
| | HealthStatus.js
| |
| +-- nodered/ base classes for nodeClass.js
| | BaseNodeAdapter.js
| | commandRegistry.js
| | statusBadge.js
| | statusUpdater.js
| |
| +-- measurements/ measurement store
| | MeasurementContainer.js
| | MeasurementBuilder.js
| | Measurement.js
| |
| +-- helper/ shared utilities
| | logger.js
| | outputUtils.js
| | childRegistrationUtils.js
| | configUtils.js
| | validationUtils.js
| | menuUtils.js
| | gravity.js
| |
| +-- configs/ schema registry
| | index.js ConfigManager
| | baseConfig.json
| | <nodeName>.json one schema per consumer node
| | assetApiConfig.js
| |
| +-- convert/ unit conversion + physics
| | index.js convert
| | fysics.js Fysics class
| |
| +-- predict/ curve prediction
| | predict_class.js
| | interpolation.js
| |
| +-- pid/ closed-loop control
| | PIDController.js
| | index.js createPidController / createCascadePidController
| |
| +-- state/ FSM scaffold (StateManager + MovementManager)
| +-- nrmse/ prediction-quality NRMSE
| +-- stats/ pure-function statistical reducers
| +-- outliers/ DynamicClusterDeviation
| +-- coolprop-node/ CoolProp thermodynamic bindings
| +-- menu/ MenuManager (editor dropdowns)
| +-- registry/ AssetResolver + FileBackend / HttpBackend
| +-- constants/ POSITIONS, POSITION_VALUES, isValidPosition
|
+-- datasets/ asset metadata (curves, model data)
| +-- assetData/
| +-- curves/ pump / blower / compressor curves
| +-- modelData/ multi-parameter model assets
|
+-- test/ unit + integration tests
+-- scripts/ maintenance scripts
+-- settings/ shared Node-RED-side settings
index.js is the only contractual import path. Anything not re-exported there is internal; consumers must not reach into src/... paths.
How nodes consume the library
| Layer | Consumer responsibility | Library responsibility |
|---|---|---|
| nodeClass | Declare static DomainClass, static commands, static tickInterval, static statusInterval. Override buildDomainConfig(uiConfig, nodeId) to translate editor values into the domain's config slice. |
BaseNodeAdapter wires config build → domain instantiation → registration delay → output strategy → status loop → input dispatch → close handler. |
| specificClass | Declare static name (matches the schema file). Implement configure(): wire ChildRouter routes, instantiate concern modules, attach measurement listeners. Implement getOutput() and getStatusBadge(). |
BaseDomain provides this.emitter, this.config, this.logger, this.measurements, this.childRegistrationUtils, this.router. |
| commands/index.js | Export an array of descriptors: {topic, aliases?, units?, payloadSchema?, description, handler}. Handler is (source, msg, ctx). |
CommandRegistry builds an O(1) lookup, normalises units via convert, warns once on alias use, generates the auto-query.units topic. |
| measurements | Write via the chain: this.measurements.type(t).variant(v).position(p, childId).value(x, ts, srcUnit). Read via getCurrentValue(unit), getAverage(unit), getFlattenedOutput(). |
MeasurementContainer auto-converts inputs to canonical units (per UnitPolicy), maintains windows, emits change events. |
| output | Implement getOutput() returning a flat snapshot object. Implement getStatusBadge() returning statusBadge.compose(parts, opts). |
outputUtils.formatMsg delta-compresses the snapshot for Port 0 + Port 1; StatusUpdater polls getStatusBadge() on statusInterval. |
All 12 nodes follow this pattern. Variations are in how richly they fill configure() — dashboardAPI has the lightest (HTTP gateway, no FSM); rotatingMachine and machineGroupControl have the densest (full curve loading, drift assessor, multi-source pressure routing).
Lifecycle — one tick or event reaches the output port
sequenceDiagram
participant RED as Node-RED runtime
participant BNA as BaseNodeAdapter
participant CMD as CommandRegistry
participant DOM as Domain (specificClass)
participant CR as ChildRouter
participant MC as MeasurementContainer
participant OU as outputUtils
participant PORT as Port 0 / 1 / 2
RED->>BNA: constructor(uiConfig, RED, node, name)
BNA->>BNA: configManager.buildConfig()
BNA->>DOM: new DomainClass(config)
DOM->>MC: new MeasurementContainer(unitPolicy.containerOptions())
DOM->>DOM: configure() — wire ChildRouter, concern modules
BNA-->>PORT: Port 2 registration msg (after 100 ms delay)
BNA->>BNA: start status loop (1000 ms)
Note over RED,PORT: Event-driven path (default)
RED->>BNA: input msg {topic: 'data.pressure', payload: 3.4}
BNA->>CMD: dispatch(msg)
CMD->>CMD: unit normalisation (Pa → mbar)
CMD->>DOM: handler(source, msg, ctx)
DOM->>MC: .type('pressure').variant('measured').position('upstream').value(3.4)
DOM->>DOM: emitter.emit('output-changed')
BNA->>DOM: getOutput()
DOM-->>BNA: flat snapshot object
BNA->>OU: formatMsg(snapshot, config, 'process')
OU-->>BNA: delta msg (only changed fields)
BNA-->>PORT: Port 0 process msg, Port 1 influx msg
Note over RED,PORT: Tick-driven path (opt-in — tickInterval set)
RED->>BNA: timer fires every tickInterval ms
BNA->>DOM: tick()
DOM->>DOM: time-based math; emitter.emit('output-changed')
BNA->>DOM: getOutput()
BNA->>OU: formatMsg(...)
BNA-->>PORT: Port 0 / 1 msgs (delta only)
The event path is the default. The tick path is opt-in via static tickInterval = 1000; — only nodes with genuinely time-based math (integrators, ramps, runtime counters) enable it.
Config schema registry
Each consumer node has one JSON schema in src/configs/. ConfigManager.buildConfig merges the schema defaults with the Node-RED editor values before the domain sees them.
| File | Node | What it defines |
|---|---|---|
baseConfig.json |
all nodes | Shared general, asset, functionality, logging sections |
rotatingMachine.json |
rotatingMachine | Curve selection, startup/shutdown ramps, safety thresholds, unit config |
machineGroupControl.json |
machineGroupControl | Demand targets, strategy selection, dispatcher settings |
pumpingStation.json |
pumpingStation | Basin geometry, hydraulics, control strategies, safety levels |
measurement.json |
measurement | Scaling, smoothing, stability threshold, digital/MQTT mode |
valve.json |
valve | Actuator travel time, position limits, FSM config |
valveGroupControl.json |
valveGroupControl | Group strategy, demand distribution |
reactor.json |
reactor | ASM kinetics, reactor type (CSTR/PFR), volume, influent |
settler.json |
settler | Sludge settling parameters, effluent quality |
monster.json |
monster | Multi-parameter monitoring, flow bounds, sample intervals |
diffuser.json |
diffuser | Aeration model, oxygen transfer parameters |
To add a new node: create src/configs/<nodeName>.json extending baseConfig.json, declare static name = '<nodeName>' in the domain class. configManager.buildConfig finds it automatically — no registration step.
Stability — additive-only export discipline
Source of truth: .claude/rules/general-functions.md 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:
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
- Implement the module under
src/<concern>/. - Re-export it from
index.js(alphabetical within the concern block). - Add a row to the appropriate table in
CONTRACT.mdwith the stability tag. - If the export is a new platform shape (a new base class or cross-node protocol), add a section to
.claude/refactor/CONTRACTS.mdin the superproject. - Add a test under
test/.
Removing an export
- Mark it deprecated in
CONTRACT.md(keep the row, change the tag, add a "removed-in" line). - Update every consumer in
nodes/*to use the replacement. - Bump submodule pin in the superproject for each touched node.
- After one release on
developmentwith no consumers, remove the export and its row.
When NOT to depend on this library
- Passive HTTP gateway nodes (e.g.
dashboardAPI) may skipBaseDomainandBaseNodeAdapterentirely if they hold no domain state. A plain Node-RED node with HTTP endpoints needs onlylogger,outputUtils, andconfigManager. - 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/orsrc/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 | Intuitive overview |
| Reference — Contracts | Full public API surface, per-export stability tags |
| Reference — Examples | Usage patterns from real consumer nodes |
| Reference — Limitations | Known issues, stability rules, deprecations |
| Platform CONTRACTS.md | The authoritative base-class + protocol spec |
| EVOLV — Architecture | Platform-wide three-tier pattern |