wiki: crisp overhaul — no decoration emoji, all 9 master pages refactored
Source-tree mirror of EVOLV.wiki.git refactor (27a42ee on wiki.git): - 7 master pages rewritten with clean design (Home, Architecture, Topology-Patterns, Topic-Conventions, Telemetry, Getting-Started, Glossary). Tables and Mermaid for visuals, gitea alert callouts for warnings, shields badges for metadata only. No emoji as decoration. - Archive.md becomes a removal-changelog pointing readers to git history and to the successor pages. - _Sidebar.md updated to navigate the new flat-name layout. - Concept / finding / manual pages: uniform mini-header (badges + "reference page" callout) added without rewriting domain content. - Every internal link now uses the flat naming that resolves on the live gitea wiki (Concept-ASM-Models, Finding-BEP-..., etc.). On wiki.git: 29 Archive-* pages hard-deleted (the git history preserves them; Archive.md documents the removal). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,170 +1,335 @@
|
||||
# Architecture
|
||||
|
||||
> **Reflects code as of `9ab9f6b` · regenerated `2026-05-11`**
|
||||

|
||||

|
||||
|
||||
How every EVOLV node is structured, and what the shared `generalFunctions` library provides.
|
||||
> [!NOTE]
|
||||
> Every EVOLV node is a three-tier sandwich: the entry registers the type with Node-RED; `nodeClass` (extends `BaseNodeAdapter`) bridges runtime to domain; `specificClass` (extends `BaseDomain`) holds pure-JS domain logic with zero `RED.*` imports. Everything shared — `BaseDomain`, `BaseNodeAdapter`, `ChildRouter`, the commands registry, `UnitPolicy`, `MeasurementContainer`, `statusBadge`, `HealthStatus`, `LatestWinsGate`, `logger`, `configManager` — lives in `generalFunctions`. Source of truth: `.claude/refactor/CONTRACTS.md`.
|
||||
|
||||
## The 3-tier node pattern
|
||||
---
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
rt["Node-RED runtime"]:::neutral
|
||||
subgraph node["Custom node (one folder under nodes/)"]
|
||||
entry["<NodeName>.js<br/>(entry — registers node type with RED)"]:::tier1
|
||||
nc["src/<NodeName>NodeClass.js<br/>(nodeClass — Node-RED adapter)"]:::tier2
|
||||
sc["src/<NodeName>SpecificClass.js<br/>(specificClass — pure domain logic)"]:::tier3
|
||||
end
|
||||
rt -->|RED.nodes.registerType| entry
|
||||
entry -->|new| nc
|
||||
nc -->|new + configure()| sc
|
||||
## The three-tier pattern
|
||||
|
||||
classDef neutral fill:#dddddd,color:#000
|
||||
classDef tier1 fill:#a9daee,color:#000
|
||||
classDef tier2 fill:#86bbdd,color:#000
|
||||
classDef tier3 fill:#50a8d9,color:#000
|
||||
```
|
||||
Node-RED runtime
|
||||
|
|
||||
+-- entry: nodes/<name>/<NodeName>.js
|
||||
| RED.nodes.registerType('<nodeName>', NodeClass)
|
||||
| HTTP admin endpoints (if any)
|
||||
|
|
||||
| +-- nodeClass: src/<...>NodeClass.js
|
||||
| | extends BaseNodeAdapter (generalFunctions)
|
||||
| | static DomainClass = SpecificClass
|
||||
| | static commands = [...descriptors]
|
||||
| | static tickInterval = null | <ms>
|
||||
| | buildDomainConfig(uiConfig, nodeId)
|
||||
| |
|
||||
| | +-- specificClass: src/<...>SpecificClass.js
|
||||
| | | extends BaseDomain (generalFunctions)
|
||||
| | | static name = '<softwareType>'
|
||||
| | | static unitPolicy = UnitPolicy.declare(...)
|
||||
| | | configure() <- wire routers + concern modules
|
||||
| | | tick() <- opt-in time-based math
|
||||
| | | getOutput() <- Port 0 / 1 snapshot
|
||||
| | | getStatusBadge() <- node.status badge
|
||||
```
|
||||
|
||||
| Tier | Owns | Touches RED.* API? | Tested by |
|
||||
|---|---|---|---|
|
||||
| entry (`<NodeName>.js`) | Type registration, HTTP admin endpoints | yes | smoke tests |
|
||||
| nodeClass (`src/...NodeClass.js`, extends `BaseNodeAdapter`) | msg routing, tick loop, output port wiring, status badge updates | yes | integration tests |
|
||||
| specificClass (`src/...SpecificClass.js`, extends `BaseDomain`) | All business logic; emits via `this.emitter`; calls `this.measurements` / `this.router` | **no** — must be free of RED imports | unit tests |
|
||||
### Tier responsibilities
|
||||
|
||||
**Rule:** never import Node-RED APIs in the specificClass. The specificClass is unit-testable by `new SpecificClass(config)`. If you find `RED.*` calls outside the entry/nodeClass tiers, that's a bug.
|
||||
| Tier | File path | Extends | Touches `RED.*` | Unit-testable |
|
||||
|:---|:---|:---|:---|:---|
|
||||
| entry | `nodes/<name>/<NodeName>.js` | (top-level) | Yes | No (smoke only) |
|
||||
| nodeClass | `src/<...>NodeClass.js` | `BaseNodeAdapter` | Yes | Integration only |
|
||||
| specificClass | `src/<...>SpecificClass.js` | `BaseDomain` | No (never) | Yes |
|
||||
|
||||
## generalFunctions — what it provides
|
||||
> [!CAUTION]
|
||||
> The specificClass must never import Node-RED APIs. If you find `RED.*` calls outside the entry or nodeClass tier, that is a bug. Pure-JS in the specificClass is what makes unit tests possible without spinning up a Node-RED runtime.
|
||||
|
||||
The `nodes/generalFunctions` submodule is a plain-JS library every node depends on. Public exports (top-level `require('generalFunctions')`):
|
||||
Source: `.claude/refactor/CONTRACTS.md` §2 and §3.
|
||||
|
||||
| Export | Role |
|
||||
|---|---|
|
||||
| `BaseDomain` | Base class for every specificClass. Owns `measurements`, `router`, `emitter`, `logger`, `unitPolicy`. |
|
||||
| `BaseNodeAdapter` | Base class for every nodeClass. Wires `commandRegistry` to `node.on('input')`, owns tick loop. |
|
||||
| `ChildRouter` | Declarative child-registration matcher. `router.onRegister(softwareType, handler)`, `router.onMeasurement(...)`. |
|
||||
| `commandRegistry` | Topic → handler descriptor map. Owns alias resolution + unit coercion. |
|
||||
| `UnitPolicy` | Per-node canonical + output units. Coerces incoming `msg.unit` to canonical. |
|
||||
| `MeasurementContainer` | Chainable storage: `type(t).variant(v).position(p).value(x, ts, unit)`. Key shape: `<type>.<variant>.<position>.<childId>`. |
|
||||
| `statusBadge` | Composer for `node.status({fill,shape,text})` updates. |
|
||||
| `HealthStatus` | Standardised `{ level: 0..3, flags: [], message, source }` shape. |
|
||||
| `LatestWinsGate` | Mutex with supersede semantics — keeps only the freshest in-flight call. |
|
||||
| `logger` | Structured logger (use this; never `console.log`). |
|
||||
| `configManager` | Loads JSON schemas from `src/configs/<node>.json`. |
|
||||
| `MenuManager` | Dynamic editor dropdowns (asset lists). |
|
||||
| `outputUtils` | Delta-compressed Port-0 + InfluxDB-line-protocol Port-1 formatting. |
|
||||
---
|
||||
|
||||
See [generalFunctions Home →](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) for the full 34-row API table.
|
||||
## generalFunctions — what the library provides
|
||||
|
||||
The `nodes/generalFunctions` submodule is a plain-JS library every node depends on. Public exports from `require('generalFunctions')`:
|
||||
|
||||
```
|
||||
generalFunctions
|
||||
|
|
||||
+-- Bases
|
||||
| BaseDomain extend in specificClass.js
|
||||
| BaseNodeAdapter extend in nodeClass.js
|
||||
|
|
||||
+-- Wiring
|
||||
| ChildRouter onRegister / onMeasurement / onPrediction
|
||||
| commandRegistry topic -> descriptor map
|
||||
|
|
||||
+-- Data
|
||||
| UnitPolicy canonical + output unit declaration
|
||||
| MeasurementContainer chainable type/variant/position/childId store
|
||||
| convert unit conversion (m3/s <-> m3/h, ...)
|
||||
|
|
||||
+-- Concurrency
|
||||
| LatestWinsGate supersede-semantics mutex
|
||||
|
|
||||
+-- Health and status
|
||||
| statusBadge node.status({fill, shape, text}) composer
|
||||
| HealthStatus {level, flags, message, source}
|
||||
|
|
||||
+-- Utilities
|
||||
logger structured (never console.log)
|
||||
configManager loads configs/<name>.json
|
||||
MenuManager dynamic editor dropdowns
|
||||
outputUtils delta-compressed Port 0 / 1 formatting
|
||||
```
|
||||
|
||||
### API one-liners
|
||||
|
||||
| Export | Contract |
|
||||
|:---|:---|
|
||||
| `BaseDomain` | Owns `emitter`, `config`, `logger`, `measurements`, `child`. Calls subclass `configure()` then `_init?()`. |
|
||||
| `BaseNodeAdapter` | Builds merged config, instantiates `DomainClass`, emits Port-2 register, wires output strategy, wires status loop, wires input dispatcher. |
|
||||
| `ChildRouter` | `.onRegister(swType, handler)` · `.onMeasurement(swType, filter, handler)` · `.onPrediction(swType, filter, handler)`. |
|
||||
| `commandRegistry` | Topic + alias map to handler. Coerces `msg.unit` to descriptor `units.default`. Logs one-time deprecation per alias. |
|
||||
| `UnitPolicy` | `.canonical(t)` · `.output(t)` · `.curve(t)` · `.resolve()` · `.convert()` · `.containerOptions()`. Dual access: method form or frozen property bag (`policy.canonical.flow`). |
|
||||
| `MeasurementContainer` | `.type(t).variant(v).position(p).value(x, ts, unit)`. Keys: `<type>.<variant>.<position>.<childId>`. |
|
||||
| `statusBadge` | `.compose([..])` · `.error(msg)` · `.idle(label)`. Returns `{fill, shape, text}`. |
|
||||
| `HealthStatus` | `{level: 0..3, flags: string[], message: string, source: string \| null}`. Lower level = healthier. |
|
||||
| `LatestWinsGate` | `.fire(v)` (no-wait) · `.fireAndWait(v)` (per-call result; superseded calls resolve with sentinel `{superseded: true}`). |
|
||||
| `logger` | `.info` · `.warn` · `.error` · `.debug`. Named after `config.general.name`. |
|
||||
| `configManager` | `buildConfig(uiConfig, baseConfig)` — validates, merges, applies defaults. |
|
||||
| `outputUtils` | `formatMsg(snapshot, 'process' \| 'influxdb')` — delta-compressed; only changed fields are emitted. |
|
||||
|
||||
The full 34-row API surface is on the [generalFunctions wiki Home](https://gitea.wbd-rd.nl/RnD/generalFunctions/wiki/Home) under "API surface".
|
||||
|
||||
---
|
||||
|
||||
## Output ports
|
||||
|
||||
Every EVOLV node emits on three ports:
|
||||
Every node emits on three ports. Source: `.claude/refactor/CONTRACTS.md` §10.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
sc[specificClass]:::tier3
|
||||
p0[(Port 0<br/>process data)]:::p0
|
||||
p1[(Port 1<br/>InfluxDB line)]:::p1
|
||||
p2[(Port 2<br/>registration / control)]:::p2
|
||||
sc --> p0
|
||||
sc --> p1
|
||||
sc --> p2
|
||||
sc["specificClass — tick() or 'output-changed'"]
|
||||
ou["outputUtils.formatMsg — delta-compress"]
|
||||
p0[("Port 0 — process")]
|
||||
p1[("Port 1 — InfluxDB line")]
|
||||
p2[("Port 2 — register / control")]
|
||||
dl["downstream Node-RED — dashboards, functions"]
|
||||
influx[("InfluxDB")]
|
||||
parent["parent EVOLV node"]
|
||||
|
||||
p0 -.-> dn1[downstream Node-RED nodes<br/>dashboards, function nodes]
|
||||
p1 -.-> influx[(InfluxDB)]
|
||||
p2 -.-> parent[parent EVOLV node<br/>via child.register]
|
||||
sc -- getOutput() --> ou
|
||||
ou --> p0 --> dl
|
||||
ou --> p1 --> influx
|
||||
sc -. child.register .-> p2 --> parent
|
||||
|
||||
class sc tier3
|
||||
class ou tier2
|
||||
class p0 p0c
|
||||
class p1 p1c
|
||||
class p2 p2c
|
||||
class dl,parent dn
|
||||
class influx ext
|
||||
|
||||
classDef tier3 fill:#50a8d9,color:#000
|
||||
classDef p0 fill:#86bbdd
|
||||
classDef p1 fill:#a9daee
|
||||
classDef p2 fill:#dddddd
|
||||
classDef tier2 fill:#86bbdd,color:#000
|
||||
classDef p0c fill:#0c99d9,color:#fff
|
||||
classDef p1c fill:#50a8d9,color:#000
|
||||
classDef p2c fill:#a9daee,color:#000
|
||||
classDef dn fill:#dddddd,color:#000
|
||||
classDef ext fill:#fff2cc,color:#000
|
||||
```
|
||||
|
||||
| Port | Carries | Format | Cardinality |
|
||||
|---|---|---|---|
|
||||
| **0** Process | Delta-compressed measurement / state snapshot for downstream Node-RED logic. | `msg.payload` = object of changed keys only. | One msg per tick when something changed. |
|
||||
| **1** Telemetry | InfluxDB line-protocol strings: `measurement,tag=val field=val ts`. | `msg.payload` = `string` (or array of strings). | One msg per tick when something changed; all numeric outputs. |
|
||||
| **2** Registration / control | `child.register` upward on adapter init; control replies. | `{topic, payload: nodeRef}` | At init time + on demand. |
|
||||
| Port | Direction | Carries | When |
|
||||
|:---|:---|:---|:---|
|
||||
| 0 | out | Process data, formatted via `outputUtils.formatMsg(..., 'process')`. Object containing only keys that changed since last tick. | `'output-changed'` fires on the emitter, or every tick if tick-driven |
|
||||
| 1 | out | InfluxDB line-protocol string via `outputUtils.formatMsg(..., 'influxdb')`. Numeric fields only. | Same trigger as Port 0 |
|
||||
| 2 | out | `child.register` upward at init plus internal control plumbing. | Once on init (after 100ms delay); on demand |
|
||||
| in | in | Commands by `msg.topic`, dispatched through the `commands/` registry. | Any time another node sends a msg |
|
||||
|
||||
See [Telemetry](Telemetry) for the full Port-1 schema and InfluxDB conventions.
|
||||
See [Telemetry](Telemetry) for the line-protocol layout and downstream wiring.
|
||||
|
||||
## Topic conventions
|
||||
---
|
||||
|
||||
| Prefix | Direction | Used for |
|
||||
|---|---|---|
|
||||
| `set.` | inbound | Set a configurable value (mode, setpoint). Idempotent. |
|
||||
| `cmd.` | inbound | Trigger an action (startup, shutdown, calibrate). Has side-effects. |
|
||||
| `data.` | inbound or outbound | Carries measurement data between child ↔ parent. |
|
||||
| `evt.` | outbound | Signal that something happened (state change, alarm). |
|
||||
| `child.` | inbound (on parent) | Child node registers itself with this parent. |
|
||||
## Lifecycle — what BaseNodeAdapter does
|
||||
|
||||
See [Topic-Conventions](Topic-Conventions) for the full list, payload shapes, alias deprecation map.
|
||||
|
||||
## Child registration
|
||||
|
||||
When a node is configured with `parent` = some other node's id, on `init()` the nodeClass emits a `child.register` message on Port 2 toward the parent. The parent's `commandRegistry` routes it into `ChildRouter`, which fires the matching `onRegister(softwareType, handler)` declared in `configure()`.
|
||||
In order, in the constructor. Source: `.claude/refactor/CONTRACTS.md` §2.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant childNc as Child nodeClass
|
||||
participant parentReg as Parent commandRegistry
|
||||
participant parentRouter as Parent ChildRouter
|
||||
participant parentSc as Parent specificClass
|
||||
autonumber
|
||||
participant rt as Node-RED runtime
|
||||
participant nc as nodeClass (BaseNodeAdapter)
|
||||
participant sc as specificClass (BaseDomain)
|
||||
participant outs as Output pipeline
|
||||
|
||||
childNc->>parentReg: msg{topic: child.register, softwareType, ref}
|
||||
parentReg->>parentRouter: dispatch(child.register, ref)
|
||||
parentRouter->>parentRouter: match softwareType
|
||||
parentRouter->>parentSc: invoke registered handler
|
||||
parentSc->>parentSc: store ref, wire emitter.on(...)
|
||||
rt->>nc: new nodeClass(uiConfig)
|
||||
nc->>nc: configManager.buildConfig(uiConfig)
|
||||
nc->>nc: this.buildDomainConfig(uiConfig)
|
||||
nc->>sc: new DomainClass(mergedConfig)
|
||||
sc->>sc: configure() — wire ChildRouter + concerns
|
||||
sc->>sc: _init?() — optional post-configure hook
|
||||
Note over nc: 100ms delay
|
||||
nc->>nc: emit Port 2 child.register
|
||||
nc->>outs: subscribe to 'output-changed' OR start tick(N ms)
|
||||
nc->>nc: start status loop (every 1000ms)
|
||||
nc->>nc: attach input handler (commands dispatcher)
|
||||
rt->>nc: msg.topic dispatch -> handler
|
||||
nc->>sc: handler.call(source, msg, ctx)
|
||||
sc->>sc: emit 'output-changed' if state shifted
|
||||
outs->>rt: Port 0 + Port 1 send (delta-compressed)
|
||||
```
|
||||
|
||||
A child is anything the parent's `configure()` declares via `router.onRegister(<softwareType>, handler)`. Examples:
|
||||
### Two output strategies
|
||||
|
||||
| Parent | Accepts children with softwareType |
|
||||
|---|---|
|
||||
| pumpingStation | `measurement`, `machine`, `machinegroup`, `pumpingstation` |
|
||||
| machineGroupControl | `machine`, `measurement` |
|
||||
| valveGroupControl | `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` (last 4 as flow sources) |
|
||||
| reactor | `measurement`, `reactor` |
|
||||
| settler | `measurement`, `reactor`, `machine` |
|
||||
| monster | `measurement` |
|
||||
| diffuser | `measurement` |
|
||||
| rotatingMachine | `measurement` |
|
||||
| valve | `measurement` |
|
||||
| dashboardAPI | any (used for Grafana provisioning) |
|
||||
| Strategy | When to pick | What domain does | What adapter does |
|
||||
|:---|:---|:---|:---|
|
||||
| Event-driven (default) | Domain reacts to incoming events and has no genuinely time-driven math | Fire `this.emitter.emit('output-changed')` when public output state shifts | Subscribes to `'output-changed'`; on each fire, calls `getOutput()` and pushes the delta-compressed msg |
|
||||
| Tick-driven (opt-in) | Domain has time-driven math — integrators, simulators, time-based thresholds | Implement `tick()`. Fire `'output-changed'` from inside it when output state shifts | Calls `tick()` every `static tickInterval` ms; listens to `'output-changed'` the same way as event-driven |
|
||||
|
||||
Both strategies funnel into the same `'output-changed'` → `getOutput()` → `formatMsg` → `node.send` pipeline.
|
||||
|
||||
---
|
||||
|
||||
## The commands registry
|
||||
|
||||
Each node has `src/commands/index.js` exporting an array of descriptors. The base adapter builds a `Map<topic | alias, descriptor>` at construction. Dispatch is one lookup. Source: `.claude/refactor/CONTRACTS.md` §4.
|
||||
|
||||
```js
|
||||
module.exports = [
|
||||
{
|
||||
topic: 'set.demand',
|
||||
aliases: ['setDemand', 'Qd'], // legacy names
|
||||
units: { measure: 'volumeFlowRate', default: 'm3/h' },
|
||||
payloadSchema: { type: 'number' },
|
||||
description: 'Operator demand setpoint.',
|
||||
handler: handlers.setDemand,
|
||||
},
|
||||
{
|
||||
topic: 'cmd.calibrate',
|
||||
payloadSchema: { type: 'none' }, // trigger-only
|
||||
description: 'Trigger a one-shot calibration.',
|
||||
handler: handlers.calibrate,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
| Descriptor field | What it does |
|
||||
|:---|:---|
|
||||
| `topic` | Canonical name. See [Topic Conventions](Topic-Conventions). |
|
||||
| `aliases` | Pre-refactor legacy names. First use of each fires a one-time deprecation warning. |
|
||||
| `units` | `{measure, default}` — pre-dispatch unit normalisation. Handler always sees `default` unit. |
|
||||
| `payloadSchema` | `{type: 'string' \| 'number' \| 'boolean' \| 'object' \| 'any' \| 'none'}` — type-check before handler. |
|
||||
| `description` | Free-text. Surfaced by `.list()` and `wikiGen` topic-contract autogen. |
|
||||
| `handler` | `(source, msg, ctx) => ...` — pure function on the domain. |
|
||||
|
||||
---
|
||||
|
||||
## Child registration — declarative routing
|
||||
|
||||
The `ChildRouter` declares which child softwareTypes the parent accepts and what to do with each. Source: `.claude/refactor/CONTRACTS.md` §5.
|
||||
|
||||
```js
|
||||
configure() {
|
||||
this.router
|
||||
.onRegister('machine', (child) => this.machines[child.id] = child)
|
||||
.onRegister('measurement', (child) => this._subscribeMeasurement(child))
|
||||
.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
|
||||
(data, child) => this._onPressure('upstream', data));
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant child as Child nodeClass
|
||||
participant reg as Parent commandRegistry
|
||||
participant rt as Parent ChildRouter
|
||||
participant sc as Parent specificClass
|
||||
|
||||
child->>reg: msg{topic: child.register, payload: {ref, softwareType}}
|
||||
reg->>rt: dispatchRegister(child, softwareType)
|
||||
rt->>rt: match softwareType in onRegister handlers
|
||||
rt->>sc: invoke handler(child)
|
||||
sc->>sc: store ref, wire emitter.on(<topic>, ...)
|
||||
```
|
||||
|
||||
### Who accepts what
|
||||
|
||||
Verified against each node's `configure()` in source.
|
||||
|
||||
| Parent | Accepted softwareTypes | Use |
|
||||
|:---|:---|:---|
|
||||
| pumpingStation | `measurement`, `machine`, `machinegroup`, `pumpingstation` | Basin sensors + pumps + groups + cascaded PS |
|
||||
| machineGroupControl | `machine`, `measurement` | Pumps + pressure sensors |
|
||||
| valveGroupControl | `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` | Valves + four flow-source softwareTypes (peer-level, not S88 children) |
|
||||
| reactor | `measurement`, `reactor` | Sensors + upstream reactor in a chain |
|
||||
| settler | `measurement`, `reactor`, `machine` | Sensors + upstream reactor + return pump |
|
||||
| monster | `measurement` | Flow + quality sensors |
|
||||
| diffuser | `measurement` | DO + airflow sensors |
|
||||
| rotatingMachine | `measurement` | Pressure / flow / power sensors |
|
||||
| valve | `measurement` | Position / pressure sensors |
|
||||
| dashboardAPI | any softwareType | Triggers Grafana dashboard generation per node |
|
||||
|
||||
---
|
||||
|
||||
## Where business logic lives
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph node["A node's src/ folder"]
|
||||
sc["specificClass.js<br/>orchestration only"]
|
||||
subgraph concerns["Concern subdirs (per-node)"]
|
||||
c1[basin/ or curves/ or kinetics/<br/>physics / math]
|
||||
c2[state/<br/>FSM transitions]
|
||||
c3[dispatch/ or safety/<br/>action / guard logic]
|
||||
c4[commands/<br/>topic → handler descriptors]
|
||||
c5[io/<br/>output composition]
|
||||
end
|
||||
end
|
||||
sc --> c1
|
||||
sc --> c2
|
||||
sc --> c3
|
||||
sc --> c4
|
||||
sc --> c5
|
||||
Each node's `src/` follows the same shape (concern modules).
|
||||
|
||||
```
|
||||
nodes/<name>/
|
||||
|
|
||||
+-- <NodeName>.js entry
|
||||
+-- <NodeName>.html editor form
|
||||
+-- package.json
|
||||
|
|
||||
+-- src/
|
||||
| <Name>NodeClass.js nodeClass (adapter)
|
||||
| <Name>SpecificClass.js specificClass (orchestrator)
|
||||
| |
|
||||
| +-- commands/
|
||||
| | index.js topic descriptors
|
||||
| | handlers.js pure handler functions
|
||||
| |
|
||||
| +-- state/ FSM (if stateful)
|
||||
| +-- <concern1>/ e.g. basin/, kinetics/, curves/
|
||||
| +-- <concern2>/ e.g. safety/, dispatch/
|
||||
| |
|
||||
| +-- io/
|
||||
| output.js getOutput() composition
|
||||
| statusBadge.js getStatusBadge()
|
||||
|
|
||||
+-- test/
|
||||
| basic/ · integration/ · edge/
|
||||
|
|
||||
+-- examples/
|
||||
01-Basic.json · 02-Integration.json · 03-Dashboard.json
|
||||
```
|
||||
|
||||
specificClass should be **stitching only** — instantiate concern modules in `configure()`, call them in `tick()` or in router handlers. Concerns are individually testable.
|
||||
> [!IMPORTANT]
|
||||
> The specificClass is stitching, not implementation. It instantiates concern modules in `configure()` and calls them in `tick()` or in router handlers. Concerns are individually testable; specificClass tests verify wiring, not math. Source: `.claude/refactor/MODULE_SPLIT.md`.
|
||||
|
||||
---
|
||||
|
||||
## Reading order for newcomers
|
||||
|
||||
1. `.claude/refactor/CONTRACTS.md` — every API shape this wiki abstracts over.
|
||||
2. `.claude/refactor/CONVENTIONS.md` — code style, file size, naming.
|
||||
3. `.claude/refactor/MODULE_SPLIT.md` — concern layout per node.
|
||||
4. One node's `wiki/Home.md` (pumpingStation is the most mature pilot — start there).
|
||||
5. The corresponding `src/` folder, top-down: specificClass → concern modules.
|
||||
| # | Read | Why |
|
||||
|:---|:---|:---|
|
||||
| 1 | `.claude/refactor/CONTRACTS.md` | Every API shape this page summarises |
|
||||
| 2 | `.claude/refactor/CONVENTIONS.md` | Code style, file size, naming, imports, tests |
|
||||
| 3 | `.claude/refactor/MODULE_SPLIT.md` | Concern layout per node |
|
||||
| 4 | [pumpingStation wiki](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home) | The refactor pilot — most mature node |
|
||||
| 5 | The corresponding `src/` folder | Top-down: specificClass → concern modules → handlers |
|
||||
|
||||
---
|
||||
|
||||
## Related pages
|
||||
|
||||
- [Topology-Patterns](Topology-Patterns) — typical plant configurations
|
||||
- [Topic-Conventions](Topic-Conventions) — naming and units
|
||||
- [Telemetry](Telemetry) — Port-1 InfluxDB schema
|
||||
- [Getting-Started](Getting-Started) — hands-on first run
|
||||
| Page | Why |
|
||||
|:---|:---|
|
||||
| [Topology Patterns](Topology-Patterns) | See the contracts above in action across a realistic plant |
|
||||
| [Topic Conventions](Topic-Conventions) | Full reference for `set.` / `cmd.` / `data.` / `query.` / `child.` / `evt.` |
|
||||
| [Telemetry](Telemetry) | Port 0 / 1 / 2 InfluxDB schema details |
|
||||
| [Getting Started](Getting-Started) | First hands-on with the contracts |
|
||||
|
||||
Reference in New Issue
Block a user