# Wiki page template — every node uses this shape Canonical structure for every node's Gitea wiki landing page. **Visual-first**, scannable, ≤ 60 words per paragraph anywhere on the page. ## Why this shape The platform has 12 nodes that all share the same architectural skeleton (BaseDomain + BaseNodeAdapter + ChildRouter + commands registry). The wiki should mirror that uniformity: a reader flips between nodes and finds the same 14 sections in the same order. Diagrams lead. Tables annotate. Prose only fills gaps. ## Picking a visual The default is Mermaid (Gitea renders it natively). It's the right tool for graph-shaped things — neighbours, lifecycles, state machines, file maps. But Mermaid doesn't render data: when a section is about *what a curve looks like* or *what the predicted vs measured signal does over time*, use: | Need | Tool | Where the artifact lives | |---|---|---| | Graph (nodes + edges, hierarchy, state) | Mermaid `flowchart` / `sequenceDiagram` / `stateDiagram-v2` | inline in the wiki page | | XY data (pump curves, prediction trace, drift over time) | Generated PNG/SVG via a small `npm run wiki:plots` script | committed under `wiki/_partial-plots//*.svg` | | Table of facts / config / topics | Markdown table | inline | | Screenshot (dashboard, editor form) | PNG ≤ 200 KB | `wiki/_partial-screenshots//*.png` | | ASCII layout (when Mermaid is overkill) | code block | inline | Lead with the visual that serves the section. Don't gate it on "is this Mermaid". ## Section list Sections 1–9 and 11–14 are mandatory for every node. Section 10 (State chart) is mandatory for stateful nodes (`rotatingMachine`, `valve`, `pumpingStation`, …) and skipped for pure aggregators (`measurement`, `dashboardAPI`). | # | Section | Visual lead | Auto-gen? | |---|---|---|---| | 0 | Header band (git hash + regen date) | — | yes | | 1 | What this node is | — (single paragraph) | no | | 2 | Position in the platform | Mermaid `flowchart LR` | no | | 3 | Capability matrix | table | no | | 4 | Code map | Mermaid `flowchart TB` w/ subgraphs | no | | 5 | Topic contract | table | **yes** (`wiki:contract`) | | 6 | Child registration | Mermaid + table | no | | 7 | Lifecycle | Mermaid `sequenceDiagram` | no | | 8 | Data model — `getOutput()` | table + concrete sample | **yes** (`wiki:datamodel`) | | 9 | Configuration — form ↔ config | Mermaid `flowchart TB` | no | | 10 | State chart (stateful only) | Mermaid `stateDiagram-v2` | no | | 11 | Examples | table + screenshots | no | | 12 | Debug recipes | table | no | | 13 | When NOT to use this node | bullets | no | | 14 | Known limitations | table | no | ## Template — copy the block below as the seed for each node's wiki (The block uses standard markdown syntax. The outer fence below is for visual delimitation in this README only; when seeding a new wiki page, copy the *content* between the `BEGIN TEMPLATE` / `END TEMPLATE` markers verbatim.) ``` # > **Reflects code as of `` · regenerated `` 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 One paragraph, ≤ 60 words. Plain English. State the *role*, not the *implementation*. > Example: "**rotatingMachine** models a single pump or compressor. It takes pressure measurements from upstream and downstream, predicts the resulting flow + power from supplier-provided characteristic curves, and drives a state machine for startup/shutdown sequences. Used as a child of `machineGroupControl` when grouped, or directly under a `pumpingStation`." ## 2. Position in the platform ~~~mermaid flowchart LR parent[machineGroupControl
Unit]:::unit -->|set.demand| this[rotatingMachine
Equipment]:::equip this -->|evt.state-change| parent sensor_up[measurement up]:::ctrl -->|data.pressure| this sensor_dn[measurement down]:::ctrl -->|data.pressure| this this -->|child.register| parent classDef proc fill:#0c99d9,color:#fff classDef unit fill:#50a8d9,color:#000 classDef equip fill:#86bbdd,color:#000 classDef ctrl fill:#a9daee,color:#000 ~~~ S88 colours are mandatory **inside hierarchy diagrams** (Mermaid `classDef`, flow.json group `style.fill`). They are NOT the node-palette swatch hexes shown in the Node-RED sidebar — those are domain-hue per node. Map (hierarchy use): Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md` (§10.0 for palette, §10.1 for groups/lanes). ## 3. Capability matrix | Capability | Status | Notes | |---|---|---| | Predicts flow from pressure | ✅ | | | Receives manual setpoint | ✅ | Topic `set.setpoint` | | Auto-start on demand from parent | ✅ | | | Self-calibrating | ❌ | Calibration is operator-triggered (`cmd.calibrate`) | | Supports multi-parent registration | ⚠️ | Possible but not fully tested — see CONTRACT.md | Cap at 10 rows. Longer inventories link out. ## 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()
declares ChildRouter rules"] end subgraph concerns["src/ concern modules"] curves["curves/
characteristic curve loader"] prediction["prediction/
flow + power predictor"] drift["drift/
prediction-vs-measured assessor"] flow["flow/
aggregation + smoothing"] state["state/
FSM transitions"] io["io/
output formatting helpers"] display["display/
status badge composition"] end nc --> sc sc --> concerns ~~~ | Module | Owns | Read first if you're changing… | |---|---|---| | `curves/` | Supplier characteristic curves, interpolation | Curve fitting, asset selection | | `prediction/` | Flow + power predictors | Predicted output values | | `drift/` | Quality of prediction vs measurement | Health status / alarms | | `flow/` | Aggregation, smoothing | Flow reporting | | `state/` | FSM (off → idle → operational → …) | Startup / shutdown behaviour | Update this section when you rename or split a directory. ## 5. Topic contract > **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. The **Unit** column reflects the descriptor's `units: { measure, default }` declaration, rendered as ` (default )`. Topics without a `units` field (non-quantity payloads — mode strings, child ids, sequence triggers) show `—`. The default unit is what the commandRegistry coerces incoming `msg.unit` values to before the handler runs. The **Effect** column is sourced from the descriptor's `description` field; topics without one fall back to a generic per-prefix sentence. | Canonical topic | Aliases | Payload | Unit | Effect | |---|---|---|---|---| | `set.mode` | `setMode` | `string` (`auto`\|`manual`\|`maintenance`) | — | Switches operating mode. | | `set.demand` | `Qd` | `number` | `volumeFlowRate` (default `m3/h`) | Sets the manual demand setpoint. | | `cmd.startup` | `execSequence` (with `payload.action='startup'`) | `{source: string}` | — | Triggers startup sequence. | ## 6. Child registration What children this node accepts and what it does with each event the child can emit. Mirrors the `ChildRouter` declarations in `specificClass.js` → `configure()`. ~~~mermaid flowchart LR subgraph kids["accepted children (softwareType)"] m_up["measurement
type=pressure
position=upstream"]:::ctrl m_dn["measurement
type=pressure
position=downstream"]:::ctrl end m_up -->|data.pressure| handler1[pressure handler
updates measurements/upstream] m_dn -->|data.pressure| handler2[pressure handler
updates measurements/downstream] handler1 --> recompute[prediction.recompute] handler2 --> recompute recompute --> emit[emitter.emit 'output-changed'] classDef ctrl fill:#a9daee,color:#000 ~~~ | softwareType | filter | wired to | side-effect | |---|---|---|---| | `measurement` | `type=pressure, position=upstream` | `pressureHandlers.onUpstream` | prediction recomputes | | `measurement` | `type=pressure, position=downstream` | `pressureHandlers.onDownstream` | prediction recomputes | ## 7. Lifecycle — what one event (or tick) does ~~~mermaid sequenceDiagram participant parent participant node as this node participant sensor as measurement child participant out as Port-0 output sensor->>node: data.pressure (3.4 bar, upstream) node->>node: ChildRouter → pressure handler node->>node: prediction recomputes node->>node: drift assesses prediction vs measured node->>node: getOutput() composes snapshot node->>out: msg{topic, payload, [process|influx]} parent->>node: set.demand (15 m³/h) node->>node: state.handleInput → maybe transition ~~~ One screen max. For multiple distinct flows (idle vs running vs error), pick the most common and link out to the rest. ## 8. Data model — `getOutput()` What lands on Port 0. Composed in domain `getOutput()`, then delta-compressed by `outputUtils.formatMsg`. **Abstract schema** (always include): | Key | Type | Unit | Source | |---|---|---|---| | `...` | number | per `UnitPolicy.output(type)` | MeasurementContainer | | `state` | string | — | `state/` | | `predictionHealth.level` | 0–3 | — | `drift/` | | `predictionHealth.flags` | string[] | — | `drift/` | **Concrete sample** (include only when the *shape* is hard to grok from the schema — e.g. nested objects, sparse keys, or unit conventions a newcomer would get wrong): ~~~json { "flow.measured.downstream.default": 12.4, "pressure.measured.upstream.default": 3.4, "power.measured.atequipment.default": 18.2, "state": "operational", "predictionHealth": { "level": 1, "flags": ["pressure_init_warming"], "message": "warmup phase", "source": "rotatingMachine#pump-A" } } ~~~ Concrete samples must come from a known-good test run — never made-up values. Regenerate when concern modules change shape. ## 9. Configuration — editor form ↔ config keys ~~~mermaid flowchart TB subgraph editor["Node-RED editor form"] f1[Mode dropdown] f2[Demand input] f3[Threshold %] end subgraph config["Domain config slice"] c1[control.mode] c2[control.targets.demand] c3[safety.thresholdPercent] end f1 --> c1 f2 --> c2 f3 --> c3 ~~~ | Form field | Config key | Default | Range | Where used | |---|---|---|---|---| | Mode | `control.mode` | `auto` | enum | `control/strategies.js` | | Demand | `control.targets.demand` | `0` | ≥ 0 | `dispatch/` | | Threshold % | `safety.thresholdPercent` | `95` | 0–100 | `safety/guards.js` | ## 10. State chart (stateful nodes only) ~~~mermaid stateDiagram-v2 [*] --> off off --> idle: cmd.startup idle --> warmingup: setpoint > 0 warmingup --> operational: warmup_time elapsed operational --> coolingdown: cmd.shutdown coolingdown --> off: cooldown_time elapsed operational --> emergencystop: cmd.estop emergencystop --> off: cmd.reset ~~~ Skip this section for stateless nodes (`measurement`, `dashboardAPI`). ## 11. Examples | Tier | File | What it shows | Mandatory? | |---|---|---|---| | Basic | `examples/01-Basic.json` | Inject + dashboard, no parent | ✅ | | Integration | `examples/02-Integration.json` | Wired to `` + 1 child | ✅ if has parent | | Dashboard | `examples/03-Dashboard.json` | Live FlowFuse charts | ⭕ optional | One screenshot per tier where helpful. PNG ≤ 200 KB under `wiki/_partial-screenshots//`. Docker compose snippet under `examples/README.md`. ## 12. Debug recipes How to diagnose the common failure modes. One table row per recipe. | Symptom | First thing to check | Where to look | |---|---|---| | Status badge stuck on `⚠ no input` | Did the measurement child register? Watch Port 2. | Editor debug tap on Port 2 | | `flow.measured.downstream` not updating | Confirm the child's emitted topic matches the `ChildRouter` filter. | `specificClass.js` → `configure()` | | Prediction `level=3` | Run `enableLog: 'debug'` *temporarily*; look for drift evaluator output. | container log | > Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging. ## 13. When you would NOT use this node Two or three bullets, one sentence each. Forces explicit non-goals. - Use rotatingMachine for a **single** pump. For groups of 2+ pumps with load sharing, use `machineGroupControl` as the parent. - Don't use rotatingMachine to model a passive non-return valve — use `valve` (no curve, no FSM-driven motor). ## 14. Known limitations / current issues | # | Issue | Tracked in | |---|---|---| | 1 | Drift confidence drops to 0 when pressure missing > 30 s | `.claude/refactor/OPEN_QUESTIONS.md` | | 2 | Multi-parent teardown ordering | Gitea issue #42 | Link to repo issues when they exist. Keep this table living — it's the contract with the user about what "works". ``` ## Hard rules for editors 1. Section 2 (Position in the platform) appears **before any prose**. Diagrams lead. 2. Every section opens with a diagram, table, or chart. Prose annotates the visual; never the other way round. 3. **Max 60 words per paragraph.** A paragraph longer than that splits into bullets or moves into a table. 4. The topic contract (section 5) and data-model schema (section 8) are **auto-generated** between the `BEGIN AUTOGEN` / `END AUTOGEN` markers. Don't hand-edit between markers. 5. Mermaid is the default for graph structures. Use generated SVG/PNG for XY data (curves, time series). Use tables for facts. 6. Skip `classDiagram` (we don't expose classes to users) and `gantt` (no schedules in node docs). 7. **Concrete sample payloads must come from a known-good test run.** Made-up numbers rot silently. 8. S88 colour codes are non-negotiable in section 2. Match the palette in `.claude/rules/node-red-flow-layout.md`. ## Archive banner — paste at the top of every archived page ``` > **⚠️ ARCHIVED — pre-refactor (Tier 1–4, 2026-05)** > > This page describes the architecture before the platform refactor. > The current page is **[](../)**. > > Kept for historical reference only. **Do not update.** ``` Archived pages move to `Archive/-pre-refactor.md` in the Gitea wiki repo. After moving, the page is read-only — corrections go on the current page, not the archive. ## Auto-generation — Phase 9 follow-up Two scripts per node, wired in `package.json`: ```json "scripts": { "wiki:contract": "node scripts/generate-contract.js > wiki/_partial-topics.md", "wiki:datamodel": "node scripts/generate-datamodel.js > wiki/_partial-datamodel.md", "wiki:all": "npm run wiki:contract && npm run wiki:datamodel" } ``` - **`generate-contract.js`** walks `src/commands/index.js`, emits one table row per descriptor between the topic-contract markers. - **`generate-datamodel.js`** instantiates the domain with the default config, calls `getOutput()`, emits the abstract schema between the datamodel-schema markers. If `wiki/sample-output.fixture.json` exists, the concrete-sample block below the markers is also overwritten. - `describeSchema` walks the lightweight `{type, properties}` schema and produces a one-line readable form. ## What lives where | Artifact | Location | Hand-edited? | |---|---|---| | Canonical page source | `wiki/.md` in the node's repo | Yes (except inside AUTOGEN markers) | | Auto-generated partials | written inline between AUTOGEN markers | No — generated | | Plots | `wiki/_partial-plots//*.svg` | No — generated | | Screenshots | `wiki/_partial-screenshots//*.png` | Yes (committed) | | Gitea wiki UI | mirror — re-rendered from `wiki/` on push | No | | Archived pre-refactor pages | `Archive/-pre-refactor.md` in the wiki repo | No (read-only after archival) | The Gitea wiki repo is separate from each node's source repo. The `wiki/` directory in each node's repo is canonical; a `wiki-sync` workflow (not yet built) mirrors it into the Gitea wiki repo on each push to `development` / `main`.