# 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 |