Files
generalFunctions/wiki/Reference-Architecture.md
znetsixe 8b28f8969e docs(wiki): full 5-page wiki matching the rotatingMachine reference format
Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:42:15 +02:00

15 KiB
Raw Blame History

Reference — Architecture

code-ref

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

  1. Implement the module under src/<concern>/.
  2. Re-export it from index.js (alphabetical within the concern block).
  3. Add a row to the appropriate table in CONTRACT.md 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 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

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