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

---

## 1. What this library is

**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.

---

## 2. Position in the platform

```mermaid
flowchart LR
    gf["generalFunctions\n(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

    gf --> rm
    gf --> mgc
    gf --> ps
    gf --> meas
    gf --> valve
    gf --> vgc
    gf --> reactor
    gf --> settler
    gf --> monster
    gf --> diffuser
    gf --> dashAPI

    classDef lib fill:#222,color:#fff,stroke:#444
    classDef proc fill:#0c99d9,color:#fff
    classDef unit fill:#50a8d9,color:#000
    classDef equip fill:#86bbdd,color:#000
    classDef ctrl fill:#a9daee,color:#000
    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.

---

## 3. Capability matrix

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

---

## 4. Module map

```mermaid
flowchart TB
    subgraph domain["src/domain/ — base classes"]
        BD["BaseDomain.js"]
        CR["ChildRouter.js"]
        UP["UnitPolicy.js"]
        LWG["LatestWinsGate.js"]
        HS["HealthStatus.js"]
    end

    subgraph nodered["src/nodered/ — Node-RED adapter layer"]
        BNA["BaseNodeAdapter.js"]
        CMR["commandRegistry.js"]
        SB["statusBadge.js"]
        SU["statusUpdater.js"]
    end

    subgraph measurements["src/measurements/ — measurement store"]
        MC["MeasurementContainer.js"]
        MB["MeasurementBuilder.js"]
        Meas["Measurement.js"]
    end

    subgraph helper["src/helper/ — shared utilities"]
        LOG["logger.js"]
        OU["outputUtils.js"]
        CRU["childRegistrationUtils.js"]
        CFG["configUtils.js"]
        VAL["validationUtils.js"]
        MU["menuUtils.js"]
        GR["gravity.js"]
    end

    subgraph predict_grp["src/predict/ — curve prediction"]
        PRED["predict_class.js"]
        INTERP["interpolation.js"]
    end

    subgraph configs["src/configs/ — schema registry"]
        CFGM["index.js (ConfigManager)"]
        JSON["*.json — per-node schemas"]
    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"]
    end

    subgraph menu_grp["src/menu/ — editor menus"]
        MM["MenuManager"]
    end

    subgraph constants["src/constants/"]
        POS["positions.js"]
    end

    BD --> CR
    BD --> UP
    BD --> MC
    BD --> CRU
    BD --> LOG
    BNA --> BD
    BNA --> CMR
    BNA --> OU
    BNA --> SU
```

| 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/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/convert/` | `convert`, `Fysics` | Unit conversion, physical constants |
| `src/coolprop-node/` | `coolprop` | Thermodynamic property lookup |
| `src/menu/` | `MenuManager` | Editor-form dropdown population |
| `src/constants/` | `POSITIONS`, `POSITION_VALUES`, `isValidPosition` | Canonical spatial position constants |

---

## 5. API surface

<!-- BEGIN AUTOGEN: api-surface -->

All imports use the package root: `const { X } = require('generalFunctions');`

| 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`. |

<!-- END AUTOGEN: api-surface -->

---

## 6. Config schema registry

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.

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

---

## 7. Lifecycle — how a node tick or event reaches the output port

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)
```

---

## 8. Stability + versioning

Source of truth: `.claude/rules/general-functions.md`.

| Category | Rule |
|---|---|
| **Safe to add** | New named exports. New optional methods on existing classes. New config keys with defaults in the schema. |
| **Requires decision-gate interview** | Removing or renaming any export. Changing a method signature. Changing the output key format of `MeasurementContainer.getFlattenedOutput()`. Changing the `formatMsg` delta-compression behaviour. |
| **Forbidden without migration** | Breaking the 4-segment key shape (`type.variant.position.childId`). Changing Port 0/1/2 payload envelope. Changing the CONTRACTS.md §1–§9 shapes. |

`generalFunctions` is a git submodule shared by all 12 node repos. A breaking change here requires updating every consumer in a single coordinated commit. Before modifying any module, run `grep -r "require('generalFunctions')" nodes/*/` to identify all call sites.

---

## 9. No editor form — consumers' config forms map to config slices

`generalFunctions` has no Node-RED editor form of its own. The library is never placed directly in a flow.

Consumer nodes expose their own editor forms. Each form field writes into a config key that `configManager.buildConfig` validates against the node's schema (in `src/configs/<nodeName>.json`). The resulting merged config is passed to the domain constructor.

For the form-to-config mapping of a specific node, see section 9 of that node's wiki page.

---

## 10. Examples — usage snippets from a real node

### 10.1 Extending `BaseDomain` (from `pumpingStation/specificClass.js` pattern)

```js
const { BaseDomain, UnitPolicy, ChildRouter } = require('generalFunctions');

class PumpingStation extends BaseDomain {
  static name = 'pumpingStation';

  static unitPolicy = UnitPolicy.declare({
    canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
    output:    { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
  });

  configure() {
    // Declare named child getters — readable in code, registry is source of truth
    this.declareChildGetter('machines',      'machine');
    this.declareChildGetter('machineGroups', 'machinegroup');

    // Declarative child routing — no per-node registerChild switch
    this.router
      .onRegister('machinegroup', (child) => this._onMachineGroupRegistered(child))
      .onMeasurement('measurement', { type: 'level' }, (data, child) => {
        this._onLevel(data.value, data);
      });
  }

  getOutput() {
    return {
      ...this.measurements.getFlattenedOutput(),
      ...this.basin.snapshot(),
    };
  }

  getStatusBadge() {
    const { statusBadge } = require('generalFunctions');
    return statusBadge.compose(['filling', 'V=12.4/50.0 m³']);
  }
}
module.exports = PumpingStation;
```

### 10.2 Extending `BaseNodeAdapter` (from `pumpingStation/nodeClass.js` pattern)

```js
const { BaseNodeAdapter } = require('generalFunctions');
const Domain  = require('./specificClass');
const commands = require('./commands');

class nodeClass extends BaseNodeAdapter {
  static DomainClass   = Domain;
  static commands      = commands;
  static tickInterval  = 1000;   // ms — only for time-driven math
  static statusInterval = 1000;

  buildDomainConfig(uiConfig, nodeId) {
    return {
      basin: {
        volume:         Number(uiConfig.basinVolume),
        height:         Number(uiConfig.basinHeight),
        surfaceArea:    Number(uiConfig.basinSurface),
      },
      hydraulics: {
        inflowPipeArea: Number(uiConfig.inflowArea),
      },
    };
  }
}
module.exports = nodeClass;
```

### 10.3 Command descriptor with unit normalisation

```js
// src/commands/index.js
module.exports = [
  {
    topic:         'set.demand',
    aliases:       ['Qd'],          // legacy name — logs one-time deprecation
    units:         { measure: 'volumeFlowRate', default: 'm3/h' },
    payloadSchema: { type: 'number' },
    description:   'Operator demand setpoint. Unit-normalised before handler runs.',
    handler:       (source, msg) => { source.setDemand(msg.payload); },
  },
  {
    topic:         'cmd.startup',
    payloadSchema: { type: 'none' },
    description:   'Trigger startup sequence.',
    handler:       (source, msg) => { source.startup(msg.payload?.source); },
  },
];
```

---

## 11. Debug recipes

| Symptom | First check | Where to look |
|---|---|---|
| Child never registers (no `registerChild` log) | Is the child's `softwareType` in the `SOFTWARE_TYPE_ALIASES` map? | `src/helper/childRegistrationUtils.js` line 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 | `substrate_score` / wiki autogen script (`wiki:all`) not yet wired for this library; API surface block is hand-maintained | Phase 9 follow-up |
