CONTRACTS.md §4: full payloadSchema.type table including 'none', plus the optional description field example. Matches the B3.2 implementation. WIKI_TEMPLATE.md §5: Unit column appears with explanatory paragraph. Matches the P11.4 wikiGen output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
350 lines
16 KiB
Markdown
350 lines
16 KiB
Markdown
# 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/<NodeName>/*.svg` |
|
||
| Table of facts / config / topics | Markdown table | inline |
|
||
| Screenshot (dashboard, editor form) | PNG ≤ 200 KB | `wiki/_partial-screenshots/<NodeName>/*.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.)
|
||
|
||
```
|
||
<!-- BEGIN TEMPLATE — wiki/<NodeName>.md -->
|
||
|
||
# <Node name>
|
||
|
||
> **Reflects code as of `<git short hash>` · regenerated `<YYYY-MM-DD>` 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<br/>Unit]:::unit -->|set.demand| this[rotatingMachine<br/>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. Map: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
|
||
|
||
## 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()<br/>static DomainClass, commands"]
|
||
end
|
||
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
|
||
sc["Machine.configure()<br/>declares ChildRouter rules"]
|
||
end
|
||
subgraph concerns["src/ concern modules"]
|
||
curves["curves/<br/>characteristic curve loader"]
|
||
prediction["prediction/<br/>flow + power predictor"]
|
||
drift["drift/<br/>prediction-vs-measured assessor"]
|
||
flow["flow/<br/>aggregation + smoothing"]
|
||
state["state/<br/>FSM transitions"]
|
||
io["io/<br/>output formatting helpers"]
|
||
display["display/<br/>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 `<measure> (default <unit>)`. 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.
|
||
|
||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||
|
||
| 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. |
|
||
|
||
<!-- END AUTOGEN: topic-contract -->
|
||
|
||
## 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<br/>type=pressure<br/>position=upstream"]:::ctrl
|
||
m_dn["measurement<br/>type=pressure<br/>position=downstream"]:::ctrl
|
||
end
|
||
m_up -->|data.pressure| handler1[pressure handler<br/>updates measurements/upstream]
|
||
m_dn -->|data.pressure| handler2[pressure handler<br/>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):
|
||
|
||
<!-- BEGIN AUTOGEN: datamodel-schema -->
|
||
|
||
| Key | Type | Unit | Source |
|
||
|---|---|---|---|
|
||
| `<type>.<variant>.<position>.<childId>` | number | per `UnitPolicy.output(type)` | MeasurementContainer |
|
||
| `state` | string | — | `state/` |
|
||
| `predictionHealth.level` | 0–3 | — | `drift/` |
|
||
| `predictionHealth.flags` | string[] | — | `drift/` |
|
||
|
||
<!-- END AUTOGEN: datamodel-schema -->
|
||
|
||
**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 `<parent>` + 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/<NodeName>/`. 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".
|
||
|
||
<!-- END TEMPLATE -->
|
||
```
|
||
|
||
## 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 **[<NodeName>](../<NodeName>)**.
|
||
>
|
||
> Kept for historical reference only. **Do not update.**
|
||
```
|
||
|
||
Archived pages move to `Archive/<NodeName>-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/<NodeName>.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/<NodeName>/*.svg` | No — generated |
|
||
| Screenshots | `wiki/_partial-screenshots/<NodeName>/*.png` | Yes (committed) |
|
||
| Gitea wiki UI | mirror — re-rendered from `wiki/` on push | No |
|
||
| Archived pre-refactor pages | `Archive/<NodeName>-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`.
|