diff --git a/package.json b/package.json
index 6a3c694..e8ba2df 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,10 @@
"description": "Control module rotatingMachine",
"main": "rotatingMachine.js",
"scripts": {
- "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
+ "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js",
+ "wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md",
+ "wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md",
+ "wiki:all": "npm run wiki:contract && npm run wiki:datamodel"
},
"repository": {
"type": "git",
diff --git a/wiki/Home.md b/wiki/Home.md
new file mode 100644
index 0000000..c9b8602
--- /dev/null
+++ b/wiki/Home.md
@@ -0,0 +1,348 @@
+# rotatingMachine
+
+> **Reflects code as of `afc304b` · regenerated `2026-05-11` via `npm run wiki:all`**
+> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
+
+## 1. What this node is
+
+**rotatingMachine** models a single pump, compressor, or blower. It loads a supplier characteristic curve, takes upstream + downstream pressure measurements (or simulated values), predicts the resulting flow + power, drives a startup/shutdown state machine, and assesses prediction drift against measured flow / power. Used as a child of `machineGroupControl` when grouped, or directly under a `pumpingStation`.
+
+## 2. Position in the platform
+
+```mermaid
+flowchart LR
+ parent[machineGroupControl /
pumpingStation]:::unit -->|flowmovement
execsequence| rm[rotatingMachine
Equipment]:::equip
+ m_up[measurement
pressure upstream]:::ctrl -.data.-> rm
+ m_dn[measurement
pressure downstream]:::ctrl -.data.-> rm
+ sim[dashboard-sim
virtual pressure children]:::ctrl -.data.-> rm
+ rm -->|child.register| parent
+ rm -.->|flow.predicted.*
power.predicted.atequipment| parent
+ classDef unit fill:#50a8d9,color:#000
+ classDef equip fill:#86bbdd,color:#000
+ classDef ctrl fill:#a9daee,color:#000
+```
+
+S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
+
+## 3. Capability matrix
+
+| Capability | Status | Notes |
+|---|---|---|
+| Curve-based flow prediction | ✅ | Built from `asset.model` via `curves/curveLoader`. |
+| Curve-based power prediction | ✅ | Reverse curve composed inside `buildPredictors`. |
+| FSM (startup / shutdown / movement) | ✅ | Shared `state/state.js` from generalFunctions. |
+| Interruptible movements | ✅ | `abortMovement` from MGC overrides on new demand. |
+| Drift assessment (flow + power) | ✅ | `DriftAssessor` with EWMA + alignment tolerance. |
+| Virtual pressure children for sim | ✅ | `dashboard-sim-upstream / -downstream`. |
+| Real-pressure child preference | ✅ | `pressureSelector` prefers real over virtual. |
+| Group operating-point prediction | ✅ | `setGroupOperatingPoint` for MGC integration. |
+| `cmd.estop` hard cut | ✅ | Forces `emergencystop` state. |
+| `data.simulate-measurement` injection | ✅ | Pressure / flow / power / temperature. |
+| Auto-recovery from prediction loss | ⚠️ | Reverts to null predictors silently — health falls to `invalid`. |
+| Multi-parent registration | ⚠️ | Accepted but not exercised in production. |
+
+## 4. Code map
+
+```mermaid
+flowchart TB
+ subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
+ nc["buildDomainConfig()
static DomainClass, commands"]
+ end
+ subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
+ sc["Machine.configure()
_setupCurves / _setupState /
_setupDrift / _setupPressure /
_setupChildren"]
+ end
+ subgraph concerns["src/ concern modules"]
+ curves["curves/
loadModelCurve + normalize"]
+ prediction["prediction/
buildPredictors + math"]
+ drift["drift/
DriftAssessor + healthRefresh"]
+ pressure["pressure/
init + router + selector + virtual"]
+ state["state/
FSM bindings + sequenceController"]
+ measurement["measurement/
handlers + childRegistrar"]
+ flow["flow/
flowController (handleInput)"]
+ display["display/
workingCurves + CoG"]
+ io["io/
output + status"]
+ commands["commands/
topic registry + handlers"]
+ end
+ nc --> sc
+ sc --> curves
+ sc --> prediction
+ sc --> drift
+ sc --> pressure
+ sc --> state
+ sc --> measurement
+ sc --> flow
+ sc --> display
+ sc --> io
+ nc --> commands
+```
+
+| Module | Owns | Read first if you're changing… |
+|---|---|---|
+| `curves/` | Supplier curve loader + normaliser + reverse | Curve fitting, unit mismatches, fallback. |
+| `prediction/` | Per-machine + group predictors, math helpers | Predicted flow / power values. |
+| `drift/` | DriftAssessor (EWMA, alignment), healthRefresh | Prediction quality, flags, confidence. |
+| `pressure/` | init + router + selector + virtual children | Pressure plumbing, sim vs real preference. |
+| `state/` | FSM bindings + setpoint / sequence orchestration | Startup / shutdown sequences. |
+| `measurement/` | Measurement handlers + child registrar | Measured value plumbing per type. |
+| `flow/` | `flowController.handle(source, action, parameter)` | Top-level input dispatch. |
+| `display/` | `showWorkingCurves`, `showCoG` | `query.curves` / `query.cog` outputs. |
+| `io/` | `getOutput`, `getStatusBadge` | Output shape, badge text. |
+| `commands/` | Input-topic registry and handlers | New input topics, payload validation. |
+
+## 5. Topic contract
+
+> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
+
+
+
+| Canonical topic | Aliases | Payload | Effect |
+|---|---|---|---|
+| `set.mode` | `setMode` | `string` | Replaces the named state value with the supplied payload. |
+| `cmd.startup` | _(none)_ | `any` | Triggers an action / sequence — not idempotent. |
+| `cmd.shutdown` | _(none)_ | `any` | Triggers an action / sequence — not idempotent. |
+| `cmd.estop` | `emergencystop` | `any` | Triggers an action / sequence — not idempotent. |
+| `execSequence` | _(none)_ | `object` | _(see handler)_ |
+| `set.setpoint` | `execMovement` | `object` | Replaces the named state value with the supplied payload. |
+| `set.flow-setpoint` | `flowMovement` | `object` | Replaces the named state value with the supplied payload. |
+| `data.simulate-measurement` | `simulateMeasurement` | `object` | Pushes a value into the node's measurement stream. |
+| `query.curves` | `showWorkingCurves` | `any` | Read-only query; node replies on the same msg. |
+| `query.cog` | `CoG` | `any` | Read-only query; node replies on the same msg. |
+| `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. |
+
+
+
+## 6. Child registration
+
+`measurement` children register through `childRegistrationUtils`; the machine subscribes to the matching `.measured.` event.
+
+```mermaid
+flowchart LR
+ subgraph kids["accepted children (softwareType)"]
+ m_pu["measurement
type=pressure
position=upstream"]:::ctrl
+ m_pd["measurement
type=pressure
position=downstream"]:::ctrl
+ m_f["measurement
type=flow"]:::ctrl
+ m_pw["measurement
type=power"]:::ctrl
+ m_t["measurement
type=temperature"]:::ctrl
+ end
+ m_pu -->|pressure.measured.upstream| router[pressureRouter.route]
+ m_pd -->|pressure.measured.downstream| router
+ m_f -->|flow.measured.| mh[measurementHandlers]
+ m_pw -->|power.measured.atequipment| mh
+ m_t -->|temperature.measured.| mh
+ router --> upd[updatePosition + drift refresh]
+ mh --> upd
+ classDef ctrl fill:#a9daee,color:#000
+```
+
+| softwareType | filter | wired to | side-effect |
+|---|---|---|---|
+| `measurement` | `type=pressure, position=upstream` | `pressureRouter.route('upstream', ...)` | Sets upstream pressure; refresh prediction + drift. |
+| `measurement` | `type=pressure, position=downstream` | `pressureRouter.route('downstream', ...)` | Sets downstream pressure; refresh prediction + drift. |
+| `measurement` | `type=flow, position=*` | `measurementHandlers.updateMeasuredFlow` | Stored; drift assessed against predicted. |
+| `measurement` | `type=power, position=atEquipment` | `measurementHandlers.updateMeasuredPower` | Stored; drift assessed against predicted. |
+| `measurement` | `type=temperature, position=*` | `measurementHandlers.updateMeasuredTemperature` | Stored; used by power correction if relevant. |
+
+Two **virtual children** are auto-registered at startup: `dashboard-sim-upstream` and `dashboard-sim-downstream`. `data.simulate-measurement` payloads land on these. Real pressure children, when registered, are preferred over the virtuals by `pressureSelector`.
+
+## 7. Lifecycle — what one event does
+
+```mermaid
+sequenceDiagram
+ participant parent as MGC / pumpingStation
+ participant rm as rotatingMachine
+ participant fsm as state FSM
+ participant pred as predictors
+ participant out as Port-0 output
+
+ parent->>rm: flowmovement (Q)
+ rm->>rm: flowController.handle('parent', 'flowmovement', Q)
+ rm->>fsm: setpoint(Q) → maybe transitionToState('accelerating')
+ Note over fsm: state.emitter 'positionChange' per tick
+ fsm-->>rm: positionChange → updatePosition()
+ rm->>pred: calcFlowPower(x) → cFlow, cPower
+ rm->>rm: calcEfficiency / cog / distance-BEP
+ rm->>rm: drift refresh on every measured tick
+ rm->>out: msg{topic, payload} (delta-compressed)
+ parent->>rm: execsequence ('startup' | 'shutdown')
+ rm->>fsm: transitionToState('starting' | 'stopping')
+ fsm-->>rm: stateChange → _updateState()
+```
+
+## 8. Data model — `getOutput()`
+
+Composed in `io/output.js → buildOutput(this)`, then delta-compressed.
+
+
+
+| Key | Type | Unit | Sample |
+|---|---|---|---|
+| `NCog` | number | — | `0` |
+| `NCogPercent` | number | — | `0` |
+| `atmPressure.measured.atequipment.wikigen-rotatingmachine-id` | number | — | `101325` |
+| `cog` | number | — | `0` |
+| `ctrl` | number | — | `0` |
+| `effDistFromPeak` | number | — | `0` |
+| `effRelDistFromPeak` | number | — | `0` |
+| `flow.predicted.max.wikigen-rotatingmachine-id` | number | m3/s | `0` |
+| `flow.predicted.min.wikigen-rotatingmachine-id` | number | m3/s | `0` |
+| `maintenanceTime` | number | — | `0` |
+| `mode` | string | — | `"auto"` |
+| `moveTimeleft` | number | — | `0` |
+| `predictionConfidence` | number | — | `0` |
+| `predictionFlags` | array | — | `[…]` |
+| `predictionPressureSource` | null | — | `null` |
+| `predictionQuality` | string | — | `"invalid"` |
+| `pressureDriftFlags` | array | — | `[…]` |
+| `pressureDriftLevel` | number | — | `0` |
+| `pressureDriftSource` | null | — | `null` |
+| `runtime` | number | — | `0` |
+| `state` | string | — | `"idle"` |
+| `temperature.measured.atequipment.wikigen-rotatingmachine-id` | number | K | `15` |
+
+
+
+**Concrete sample** (live, from a known-good test run — pump warming up with simulated upstream/downstream pressure):
+
+~~~json
+{
+ "state": "warmingup",
+ "flow.predicted.downstream.default": 0.00345,
+ "flow.predicted.atequipment.default": 0.00345,
+ "power.predicted.atequipment.default": 1820,
+ "pressure.measured.upstream.dashboard-sim-upstream": 101325,
+ "pressure.measured.downstream.dashboard-sim-downstream": 145000,
+ "temperature.measured.atequipment.default": 15,
+ "atmPressure.measured.atequipment.default": 101325,
+ "predictionHealth": {
+ "quality": "warming",
+ "confidence": 0.35,
+ "pressureSource": "dashboard-sim",
+ "flags": ["pressure_init_warming"]
+ },
+ "cog": 0.62, "NCog": 0.71,
+ "absDistFromPeak": 0.04, "relDistFromPeak": 0.12
+}
+~~~
+
+Position labels are normalised to lowercase in MeasurementContainer keys (`atequipment`, `downstream`, `upstream`, `max`, `min`). The trailing `` segment is the registering child's id (or `default` for own predictions / virtuals tagged via `dashboard-sim-*`).
+
+## 9. Configuration — editor form ↔ config keys
+
+```mermaid
+flowchart TB
+ subgraph editor["Node-RED editor form"]
+ f1[Asset model dropdown]
+ f2[Mode current]
+ f3[Position vs parent]
+ f4[State times: starting, warmingup, ...]
+ f5[Movement mode + speed]
+ f6[Position min / max / initial]
+ f7[Allowed sources / actions per mode]
+ f8[Output unit (flow, pressure, power)]
+ end
+ subgraph cfg["Domain config slice"]
+ c1[asset.model]
+ c2[mode.current]
+ c3[functionality.positionVsParent]
+ c4[time.starting / warmingup / stopping / coolingdown]
+ c5[movement.mode / speed / maxSpeed / interval]
+ c6[position.min / max / initial]
+ c7[mode.allowedSources / allowedActions]
+ c8[general.unit / asset.unit]
+ end
+ f1 --> c1
+ f2 --> c2
+ f3 --> c3
+ f4 --> c4
+ f5 --> c5
+ f6 --> c6
+ f7 --> c7
+ f8 --> c8
+```
+
+| Form field | Config key | Default | Range | Where used |
+|---|---|---|---|---|
+| Asset model | `asset.model` | `Unknown` | string (must resolve in curve loader) | `_setupCurves` |
+| Mode | `mode.current` | `auto` | enum (`auto`, `manual`) | `flowController.handle` source check |
+| Position vs parent | `functionality.positionVsParent` | `atEquipment` | enum | child-register payload + event suffix |
+| State time — starting | `time.starting` | `10` (s) | ≥ 0 | FSM timing |
+| State time — warmingup | `time.warmingup` | `5` (s) | ≥ 0 | FSM timing |
+| State time — stopping | `time.stopping` | `5` (s) | ≥ 0 | FSM timing |
+| State time — coolingdown | `time.coolingdown` | `10` (s) | ≥ 0 | FSM timing |
+| Movement mode | `movement.mode` | `dynspeed` | enum (`staticspeed`, `dynspeed`) | position trajectory |
+| Movement speed | `movement.speed` | `1` | ≤ `maxSpeed` | trajectory rate |
+| Position min/max | `position.min` / `position.max` | `0` / `100` | numeric | setpoint clamp |
+| Output unit (flow) | `general.unit` | `l/s` | unit string | unit policy `output.flow` |
+
+## 10. State chart
+
+The FSM is the canonical state set declared in `generalFunctions/src/state/stateConfig.json`. `emergencystop` is reachable from *every* state. Allowed transitions per `stateConfig.allowedTransitions`.
+
+```mermaid
+stateDiagram-v2
+ [*] --> idle
+ idle --> starting: execsequence(startup)
+ idle --> off: off
+ idle --> maintenance: maintenance
+ starting --> warmingup: timer
+ warmingup --> operational: timer
+ operational --> accelerating: flowmovement / setpoint up
+ operational --> decelerating: flowmovement / setpoint down
+ accelerating --> operational: target reached
+ decelerating --> operational: target reached
+ operational --> stopping: execsequence(shutdown)
+ stopping --> coolingdown: timer
+ stopping --> idle: timer
+ coolingdown --> idle: timer
+ coolingdown --> off: off
+ off --> idle: execsequence(startup)
+ off --> maintenance: maintenance
+ maintenance --> idle: maintenance done
+ maintenance --> off: off
+
+ note right of operational
+ any state -> emergencystop
+ via cmd.estop
+ end note
+```
+
+`accelerating` / `decelerating` are abortable on new demand via `abortMovement(reason)`; the controller does **not** auto-transition back to `operational` after an abort (see `state.js` comment "Abort path"). `warmingup` and `coolingdown` are **protected** — abort signals are dropped for safety. `activeStates = { operational, starting, warmingup, accelerating, decelerating }` is the set MGC treats as "machine alive".
+
+## 11. Examples
+
+| Tier | File | What it shows | Status |
+|---|---|---|---|
+| Basic | `examples/01 - Basic Manual Control.json` | Inject + dashboard, simulated pressure, manual startup/shutdown | ✅ validated |
+| Integration | `examples/02 - Integration with Machine Group.json` | rotatingMachine wired under MGC | ⏳ pending validation |
+| Dashboard | `examples/03 - Dashboard Visualization.json` | FlowFuse charts: flow / power / pressure trends | ✅ in repo |
+| Legacy | `examples/basic.flow.json` / `integration.flow.json` / `edge.flow.json` | Pre-refactor flows | ⚠️ kept until new Tier 2 is validated |
+
+Screenshots will land under `wiki/_partial-screenshots/rotatingMachine/` once captured from the live demo.
+
+## 12. Debug recipes
+
+| Symptom | First thing to check | Where to look |
+|---|---|---|
+| `state` stuck on `idle`, no startup | Source not in `mode.allowedSources[currentMode]`. Check `flowController` warn log. | `_setupState` + `isValidSourceForMode`. |
+| `flow.predicted.*` is 0 or `NaN` | Pressure not initialised — `predictionHealth.flags` will say `pressure_init_warming`. Inject pressure via `data.simulate-measurement` or wire real measurement children. | `getMeasuredPressure` + `pressureSelector`. |
+| `predictionHealth.quality='invalid'` | Curve normalisation failed at startup — null predictors installed. Check container log for `Curve normalization failed for model …`. | `_setupCurves`. |
+| Drift `level=3` after startup | Less than 10 paired samples (`minSamplesForLongTerm`) — wait a few ticks before judging. | `driftProfiles.minSamplesForLongTerm`. |
+| `cmd.estop` doesn't recover | After `emergencystop`, only `idle` / `off` / `maintenance` are allowed. Send `cmd.shutdown` then `cmd.startup`, or reset via maintenance. | `stateConfig.allowedTransitions.emergencystop`. |
+| Position bounces around target | Movement mode `dynspeed` ease-in/out may overshoot at high speed; try `staticspeed`. | `movement.mode`. |
+
+> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors.
+
+## 13. When you would NOT use this node
+
+- Use rotatingMachine for a **single** pump / compressor / blower. For groups of 2+ with load sharing, wire `machineGroupControl` as the parent.
+- Don't use rotatingMachine to model a **passive non-return valve** — use `valve` (no curve, no FSM-driven motor).
+- Don't use rotatingMachine without a **curve model** — flow / power predictions degrade to zero and drift is meaningless.
+
+## 14. Known limitations / current issues
+
+| # | Issue | Tracked in |
+|---|---|---|
+| 1 | Drift confidence drops to 0 when pressure source is missing > 30 s — health flips to `invalid` silently. | `pressure/pressureInitialization.js`. |
+| 2 | Multi-parent registration accepted by `childRegistrationUtils` but ordering of teardown is not test-covered. | Open question — `OPEN_QUESTIONS.md`. |
+| 3 | `data.simulate-measurement` does not unset previous values on missing keys — stale sim data can persist after toggling off. | `measurementHandlers.updateSimulatedMeasurement`. |
+| 4 | `execSequence` legacy umbrella topic kept alive in registry; planned removal in Phase 7. | `commands/index.js` `_legacy: true`. |