Add refactor planning docs (.claude/refactor/)
Platform-wide refactor plan: README, CONVENTIONS, CONTRACTS, MODULE_SPLIT, TASKS, OPEN_QUESTIONS. Source of truth for the phased refactor across all 12 submodules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
509
.claude/refactor/CONTRACTS.md
Normal file
509
.claude/refactor/CONTRACTS.md
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
# Contracts
|
||||||
|
|
||||||
|
The exact shapes that the refactor delivers. These are the things every
|
||||||
|
node converges on. Treat them as APIs.
|
||||||
|
|
||||||
|
Order: top-down — what a Node-RED user sees, what a node author writes,
|
||||||
|
what `generalFunctions` provides.
|
||||||
|
|
||||||
|
## 1. The Node-RED-visible contract per node
|
||||||
|
|
||||||
|
Every node exposes the same three Port shapes:
|
||||||
|
|
||||||
|
| Port | Direction | Carries |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | out | Process data — formatted via `outputUtils.formatMsg(..., 'process')` |
|
||||||
|
| 1 | out | InfluxDB telemetry — formatted via `outputUtils.formatMsg(..., 'influxdb')` |
|
||||||
|
| 2 | out | Registration / control plumbing |
|
||||||
|
| in | in | Commands routed by `msg.topic` through the `commands/` registry |
|
||||||
|
|
||||||
|
Every node also publishes a per-repo `CONTRACT.md` listing:
|
||||||
|
- Every `msg.topic` it accepts on Port 0 input, with the payload schema.
|
||||||
|
- Every `topic` shape it emits on Port 0/1/2.
|
||||||
|
- Every event its `measurements.emitter` fires for parents to subscribe.
|
||||||
|
- Every position label it expects from children.
|
||||||
|
|
||||||
|
This file is generated from the node's `commands/` module + a small
|
||||||
|
hand-written events section.
|
||||||
|
|
||||||
|
### Topic naming — canonical from Phase 1
|
||||||
|
|
||||||
|
`msg.topic` always uses one of these prefixes. `<noun>` and `<verb>`
|
||||||
|
are kebab-case after the dot (`set.flow-setpoint`, not
|
||||||
|
`set.flowSetpoint`).
|
||||||
|
|
||||||
|
#### Inputs — topics the node accepts on Port-0 input
|
||||||
|
|
||||||
|
| Prefix | Meaning | Idempotent? | Examples |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `set.<noun>` | **Setter.** Replaces a state value with the supplied payload. Repeating with the same payload does nothing extra. | Yes | `set.mode`, `set.scaling`, `set.demand`, `set.inflow` |
|
||||||
|
| `cmd.<verb>` | **Imperative action.** Triggers a transition or sequence. Repeating triggers it again (or is rejected). | No | `cmd.startup`, `cmd.shutdown`, `cmd.estop`, `cmd.calibrate` |
|
||||||
|
| `data.<noun>` | **Bulk data input.** Sensor readings, measurement values, raw streams. The node consumes them. | n/a — values flow | `data.measurement`, `data.flow`, `data.pressure` |
|
||||||
|
| `child.<verb>` | **Parent/child plumbing.** Registration handshakes routed via Port 2. | n/a | `child.register`, `child.unregister` |
|
||||||
|
| `query.<noun>` | **Synchronous query.** The node responds on the same `msg` (or a sibling output). Used for read-only debug queries from a dashboard. | Yes (read-only) | `query.curves`, `query.cog`, `query.snapshot` |
|
||||||
|
|
||||||
|
#### Outputs — topics the node EMITS
|
||||||
|
|
||||||
|
| Prefix | Meaning | Where it appears |
|
||||||
|
|---|---|---|
|
||||||
|
| `evt.<noun>` | **Event.** A fact about something that just happened. Other nodes/dashboards subscribe to react. The node fires-and-forgets — no consumer is required. | `msg.topic` on Port 0 output, also fired internally on `this.emitter` so sibling modules can listen. |
|
||||||
|
|
||||||
|
`evt.*` is *one-way*: the node says "this happened", consumers can do
|
||||||
|
whatever they like with it. Examples: `evt.state-change` (state machine
|
||||||
|
moved), `evt.alarm` (a safety threshold tripped), `evt.calibrated`
|
||||||
|
(calibration completed). If you find yourself wanting to send a
|
||||||
|
command via `evt.*`, you actually want `set.*` or `cmd.*`.
|
||||||
|
|
||||||
|
The default measurement output (the delta-compressed payload from
|
||||||
|
`outputUtils.formatMsg`) keeps `msg.topic = config.general.name` per
|
||||||
|
the existing convention. `evt.*` is for *additional* event-shaped
|
||||||
|
emissions, not for the per-tick measurement stream.
|
||||||
|
|
||||||
|
#### Aliases for legacy names
|
||||||
|
|
||||||
|
Each `commands/index.js` declares the canonical name as `topic` and
|
||||||
|
lists pre-refactor names in `aliases`. The first time an alias fires,
|
||||||
|
the runtime logs a one-time deprecation warning. Aliases are removed
|
||||||
|
in Phase 7 after one release cycle.
|
||||||
|
|
||||||
|
#### Why these prefixes (the reasoning)
|
||||||
|
|
||||||
|
Today's topics mix `setMode` (verb-noun, no separator), `q_in`
|
||||||
|
(snake-case, abbreviation), `Qd` (PascalCase abbreviation),
|
||||||
|
`changemode` (lowercase joined), `execSequence` (verb-noun, camel).
|
||||||
|
A reader can't tell from the topic name whether it's a setter, an
|
||||||
|
action, or an event. The prefix system says it explicitly:
|
||||||
|
|
||||||
|
- `set.x` means "I'm replacing the value of x". Safe to retry.
|
||||||
|
- `cmd.x` means "I'm asking you to do x once". Don't retry blindly.
|
||||||
|
- `data.x` means "here's a value I'm pushing into your stream".
|
||||||
|
- `query.x` means "tell me what x is right now".
|
||||||
|
- `child.x` means "plumbing — only the parent/child machinery cares".
|
||||||
|
- `evt.x` (output only) means "this happened, do what you want".
|
||||||
|
|
||||||
|
## 2. `BaseNodeAdapter` — the shape of every nodeClass
|
||||||
|
|
||||||
|
Lives in `generalFunctions/src/nodered/BaseNodeAdapter.js`. Each node's
|
||||||
|
`nodeClass.js` extends it.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { BaseNodeAdapter } = require('generalFunctions');
|
||||||
|
const Domain = require('./specificClass');
|
||||||
|
const commands = require('./commands');
|
||||||
|
|
||||||
|
class nodeClass extends BaseNodeAdapter {
|
||||||
|
// The domain class to instantiate.
|
||||||
|
static DomainClass = Domain;
|
||||||
|
|
||||||
|
// The command registry — see section 4.
|
||||||
|
static commands = commands;
|
||||||
|
|
||||||
|
// Opt-in periodic tick. Default null = event-driven (domain emits
|
||||||
|
// 'output-changed' when output should refresh). Set to ms only when
|
||||||
|
// the domain genuinely needs a time-based heartbeat.
|
||||||
|
// Example reason (above the line): "needs delta-time for predicted
|
||||||
|
// volume integrator".
|
||||||
|
static tickInterval = null;
|
||||||
|
|
||||||
|
// Always-on status badge poll. Required for Node-RED's editor
|
||||||
|
// refresh. Set to 0 only in headless environments.
|
||||||
|
static statusInterval = 1000;
|
||||||
|
|
||||||
|
// Build the domain-specific config slice from the Node-RED uiConfig.
|
||||||
|
// Base config (general, asset, functionality, logging) is built by
|
||||||
|
// BaseNodeAdapter via configManager.buildConfig.
|
||||||
|
buildDomainConfig(uiConfig, nodeId) {
|
||||||
|
return {
|
||||||
|
basin: { volume: uiConfig.basinVolume, height: uiConfig.basinHeight, ... },
|
||||||
|
hydraulics: { ... },
|
||||||
|
control: { ... },
|
||||||
|
safety: { ... },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nodeClass;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifecycle (provided by base, do not reimplement)
|
||||||
|
|
||||||
|
In order, in the constructor:
|
||||||
|
|
||||||
|
1. Build merged config (`configManager.buildConfig` + `buildDomainConfig`).
|
||||||
|
2. Instantiate `DomainClass` with that config; store as `this.source`,
|
||||||
|
also as `this.node.source` for sibling-node lookup.
|
||||||
|
3. Send Port 2 registration message (after a 100 ms delay).
|
||||||
|
4. **Output strategy** — pick one based on `static tickInterval`:
|
||||||
|
- `tickInterval = N` (ms): start a periodic timer that calls
|
||||||
|
`this.source.tick?.()`, then formats and sends outputs.
|
||||||
|
- `tickInterval = null`: subscribe to `'output-changed'` on
|
||||||
|
`this.source.emitter`. Whenever the domain fires that event, the
|
||||||
|
adapter formats and sends outputs.
|
||||||
|
In both modes, `outputUtils.formatMsg` does delta compression — a
|
||||||
|
send only emits changed fields.
|
||||||
|
5. Start the status loop at `static statusInterval` ms:
|
||||||
|
- Call `this.source.getStatusBadge()` (see section 7), apply via
|
||||||
|
`node.status(...)`.
|
||||||
|
6. Attach the `input` handler — dispatches by `msg.topic` through the
|
||||||
|
commands registry.
|
||||||
|
7. Attach the `close` handler — clears timers, removes child
|
||||||
|
listeners, clears status.
|
||||||
|
|
||||||
|
### Event-driven is the default
|
||||||
|
|
||||||
|
A domain that doesn't need time-driven math fires
|
||||||
|
`this.emitter.emit('output-changed')` whenever its public state shifts
|
||||||
|
(e.g. after a measurement update, a state transition, a calibration).
|
||||||
|
The base adapter pushes outputs in response. No 1 Hz polling.
|
||||||
|
|
||||||
|
A domain that DOES need time-driven math (e.g. `pumpingStation`
|
||||||
|
integrating predicted volume) opts into a tick. The tick runs the
|
||||||
|
time-based update; if that update changes output state, the domain
|
||||||
|
emits `'output-changed'` and the same code path that handles
|
||||||
|
event-driven nodes pushes outputs.
|
||||||
|
|
||||||
|
This keeps the output pipeline single-shape regardless of which mode
|
||||||
|
the domain uses.
|
||||||
|
|
||||||
|
### Override hooks
|
||||||
|
|
||||||
|
A subclass may override:
|
||||||
|
|
||||||
|
| Hook | When |
|
||||||
|
|---|---|
|
||||||
|
| `buildDomainConfig(uiConfig, nodeId)` | Always — required. |
|
||||||
|
| `extraSetup()` | If a node needs custom wiring beyond the base. |
|
||||||
|
| `extraInputDispatch(msg, send, done)` | If commands registry can't express a topic. Avoid; prefer the registry. |
|
||||||
|
| `extraClose()` | Custom teardown beyond clearing intervals. |
|
||||||
|
|
||||||
|
### Forbidden in subclasses
|
||||||
|
|
||||||
|
- Re-implementing the tick or status loop. Use `getOutput()` /
|
||||||
|
`getStatusBadge()` on the domain.
|
||||||
|
- Calling `this.source._private`. Domain exposes a public surface.
|
||||||
|
- Importing from another node's `src/`.
|
||||||
|
|
||||||
|
## 3. `BaseDomain` — the shape of every specificClass
|
||||||
|
|
||||||
|
Lives in `generalFunctions/src/domain/BaseDomain.js`. Each node's
|
||||||
|
`specificClass.js` extends it.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { BaseDomain, UnitPolicy, ChildRouter } = require('generalFunctions');
|
||||||
|
|
||||||
|
class PumpingStation extends BaseDomain {
|
||||||
|
// Identifies the config in generalFunctions/src/configs/<name>.json.
|
||||||
|
static name = 'pumpingStation';
|
||||||
|
|
||||||
|
// Declarative unit policy — see section 6.
|
||||||
|
static unitPolicy = UnitPolicy.declare({
|
||||||
|
canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
|
||||||
|
output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run after BaseDomain has built emitter, config, logger, measurements,
|
||||||
|
// childRegistrationUtils. Wire concern-modules and any extra state.
|
||||||
|
configure() {
|
||||||
|
this.basin = new BasinGeometry(this.config, this.logger);
|
||||||
|
this.flowAggregator = new FlowAggregator(this.context());
|
||||||
|
this.safety = new SafetyController(this.context());
|
||||||
|
this.strategies = require('./control');
|
||||||
|
|
||||||
|
this.router = new ChildRouter(this)
|
||||||
|
.on('machinegroup', this._onMachineGroup)
|
||||||
|
.on('measurement', { type: 'pressure' }, this._onPressure)
|
||||||
|
.on('measurement', { type: 'level' }, this._onLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-tick — orchestration only, all real work is in modules.
|
||||||
|
tick() {
|
||||||
|
this.flowAggregator.update();
|
||||||
|
const safe = this.safety.evaluate();
|
||||||
|
if (safe.blocked) return;
|
||||||
|
this.strategies[this.mode]?.run(this.context());
|
||||||
|
}
|
||||||
|
|
||||||
|
// What goes on Port 0 / Port 1.
|
||||||
|
getOutput() {
|
||||||
|
return {
|
||||||
|
...this.measurements.getFlattenedOutput(),
|
||||||
|
...this.basin.snapshot(),
|
||||||
|
...this.flowAggregator.snapshot(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the Node-RED status badge shows — see section 7.
|
||||||
|
getStatusBadge() {
|
||||||
|
return statusBadge.fromState({
|
||||||
|
direction: this.flowAggregator.direction,
|
||||||
|
vol: this.measurements.type('volume').variant('measured').position('atequipment').getCurrentValue('m3'),
|
||||||
|
maxVol: this.basin.maxVolAtOverflow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PumpingStation;
|
||||||
|
```
|
||||||
|
|
||||||
|
### What `BaseDomain` provides (do not reimplement)
|
||||||
|
|
||||||
|
The base constructor sets up:
|
||||||
|
|
||||||
|
| Property | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `this.emitter` | `EventEmitter` | Internal events. Fire `'output-changed'` here when public state shifts in event-driven nodes. |
|
||||||
|
| `this.configManager`, `this.configUtils`, `this.defaultConfig` | — | Wired from `static name`. |
|
||||||
|
| `this.config` | object | Validated config. |
|
||||||
|
| `this.logger` | logger | Named after `config.general.name`. |
|
||||||
|
| `this.measurements` | `MeasurementContainer` | Built from `static unitPolicy`. |
|
||||||
|
| `this.childRegistrationUtils` | child registry | The `child` dict is auto-created. |
|
||||||
|
|
||||||
|
Then it calls `this.configure()` — your hook. Then it calls
|
||||||
|
`this._init?.()` if defined.
|
||||||
|
|
||||||
|
### Named child accessors (registry-as-truth, readable in code)
|
||||||
|
|
||||||
|
Children live in `this.child[<softwareType>][<category>]` (the
|
||||||
|
registry, populated by `childRegistrationUtils`). For readable code,
|
||||||
|
each domain declares **named getters** in `configure()` that surface
|
||||||
|
the relevant slices:
|
||||||
|
|
||||||
|
```js
|
||||||
|
configure() {
|
||||||
|
// Reads as: ps.machines, ps.machineGroups, ps.stations.
|
||||||
|
this.declareChildGetter('machines', 'machine');
|
||||||
|
this.declareChildGetter('machineGroups', 'machinegroup');
|
||||||
|
this.declareChildGetter('stations', 'pumpingstation');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`declareChildGetter(name, softwareType, category?)` (provided by
|
||||||
|
BaseDomain) installs a getter that flattens
|
||||||
|
`this.child[softwareType]` into one object keyed by child id (across
|
||||||
|
all categories) — or filters by `category` if given.
|
||||||
|
|
||||||
|
The registry is the source of truth; the getters keep call sites
|
||||||
|
readable. `Object.values(this.machines).forEach(...)` works exactly
|
||||||
|
like before; assignments like `this.machines[id] = child` no longer
|
||||||
|
work — registration goes through `this.router` (or `registerChild`).
|
||||||
|
|
||||||
|
### Two output strategies — domain decides
|
||||||
|
|
||||||
|
| Strategy | When to pick | What domain does | What adapter does |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Event-driven** (default) | Domain reacts to incoming events (measurements, state changes, commands) and has no genuinely time-driven math. | Fire `this.emitter.emit('output-changed')` whenever the public output state shifts. | Subscribes to `'output-changed'`; on each fire, calls `getOutput()` and pushes the delta-compressed message. |
|
||||||
|
| **Tick-driven** (opt-in) | Domain has time-driven math that can't be expressed as a reaction to events (integrators, simulators, time-based thresholds). | Implement `tick()`. Fire `'output-changed'` from inside it whenever the tick changes output state. | Calls `tick()` every `static tickInterval` ms (set on the nodeClass subclass). Listens to `'output-changed'` the same as event-driven nodes. |
|
||||||
|
|
||||||
|
Both strategies funnel into the same `'output-changed'` → `getOutput()`
|
||||||
|
→ `formatMsg` → `node.send` pipeline. The only difference is what
|
||||||
|
fires the event.
|
||||||
|
|
||||||
|
### `this.context()`
|
||||||
|
|
||||||
|
Returns a frozen view passed to concern-modules so they don't reach into
|
||||||
|
`this`. Default shape:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
config: this.config,
|
||||||
|
logger: this.logger,
|
||||||
|
measurements: this.measurements,
|
||||||
|
emitter: this.emitter,
|
||||||
|
child: this.child,
|
||||||
|
unitPolicy: this.unitPolicy,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A node may override `context()` to add domain-specific keys (e.g.
|
||||||
|
`pumpingStation` adds `basin`).
|
||||||
|
|
||||||
|
### `getOutput()` and `getStatusBadge()` are the only required methods
|
||||||
|
|
||||||
|
Everything else is configuration. If a domain can be expressed without a
|
||||||
|
custom `tick()` (e.g. a passive aggregator), don't define one.
|
||||||
|
|
||||||
|
## 4. The commands registry
|
||||||
|
|
||||||
|
Each node has `src/commands/index.js` that exports an array of command
|
||||||
|
descriptors:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handlers = require('./handlers');
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
topic: 'set.mode',
|
||||||
|
aliases: ['setMode', 'changemode'], // legacy names
|
||||||
|
payloadSchema: { type: 'string' },
|
||||||
|
handler: handlers.setMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: 'cmd.startup',
|
||||||
|
aliases: ['execSequence:startup'],
|
||||||
|
payloadSchema: { type: 'object', properties: { source: { type: 'string' } } },
|
||||||
|
handler: handlers.startup,
|
||||||
|
},
|
||||||
|
...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
A handler is a pure function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// handlers.js
|
||||||
|
exports.setMode = (source, msg, ctx) => {
|
||||||
|
source.setMode(msg.payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.startup = async (source, msg, ctx) => {
|
||||||
|
await source.handleInput(msg.payload?.source ?? 'parent', 'execSequence', 'startup');
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `BaseNodeAdapter` builds a `Map<topic-or-alias, descriptor>` at
|
||||||
|
construction time. Dispatch is one lookup. Aliases log a one-time
|
||||||
|
deprecation warning the first time each fires.
|
||||||
|
|
||||||
|
### Why declarative?
|
||||||
|
|
||||||
|
- Auto-generates `CONTRACT.md` per node.
|
||||||
|
- Lets us add cross-node static checks (no two nodes use the same
|
||||||
|
`set.x` for different things).
|
||||||
|
- Replaces the per-node 100-line input switch with a 5-line dispatch.
|
||||||
|
|
||||||
|
## 5. `ChildRouter` — declarative parent registration
|
||||||
|
|
||||||
|
Lives in `generalFunctions/src/domain/ChildRouter.js`. Built on top of
|
||||||
|
the existing `childRegistrationUtils`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.router = new ChildRouter(this)
|
||||||
|
// Register a callback when a child of a given software type registers.
|
||||||
|
.onRegister('machinegroup', (child) => this._onMachineGroupRegistered(child))
|
||||||
|
|
||||||
|
// Subscribe to a measurement event from any child of a given softwareType.
|
||||||
|
// The third arg filters by emit-side position.
|
||||||
|
.onMeasurement('measurement', { type: 'pressure', position: 'upstream' }, (data, child) => {
|
||||||
|
this._onPressure('upstream', data.value, data);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Subscribe to predicted-flow events from any group/machine child.
|
||||||
|
.onPrediction('machinegroup', { type: 'flow', position: 'downstream' }, (data, child) => {
|
||||||
|
this._onPredictedFlow(child, data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`ChildRouter` owns:
|
||||||
|
- The handler maps (`onRegister`, `onMeasurement`, `onPrediction`).
|
||||||
|
- Listener attachment + teardown (called from `BaseDomain` on close).
|
||||||
|
- Software-type alias resolution (already in `childRegistrationUtils`).
|
||||||
|
|
||||||
|
Per-node `registerChild` boilerplate disappears. The base
|
||||||
|
`childRegistrationUtils.registerChild` calls `this.mainClass.registerChild`
|
||||||
|
which delegates to `this.router.dispatchRegister(child, softwareType)`.
|
||||||
|
|
||||||
|
## 6. `UnitPolicy`
|
||||||
|
|
||||||
|
Lives in `generalFunctions/src/domain/UnitPolicy.js`. Replaces the
|
||||||
|
duplicated `_buildUnitPolicy` / `_resolveUnitOrFallback` /
|
||||||
|
`_convertUnitValue` in `rotatingMachine` and `machineGroupControl`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
static unitPolicy = UnitPolicy.declare({
|
||||||
|
canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
|
||||||
|
output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
|
||||||
|
curve: { flow: 'm3/h', pressure: 'mbar', power: 'kW', control: '%' }, // optional
|
||||||
|
// Types whose values must always carry a unit on write.
|
||||||
|
requireUnitForTypes: ['flow', 'pressure', 'power', 'temperature'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Methods on the resulting policy:
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `policy.canonical(type)` | Canonical unit for a measurement type. |
|
||||||
|
| `policy.output(type)` | Display / IO unit for a measurement type. |
|
||||||
|
| `policy.resolve(candidate, expectedMeasure, fallback, label)` | Validate a user-supplied unit, fall back if invalid (logs `warn`). |
|
||||||
|
| `policy.convert(value, fromUnit, toUnit, contextLabel)` | Strict conversion. |
|
||||||
|
| `policy.containerOptions()` | Returns the option bag for a `MeasurementContainer`. |
|
||||||
|
|
||||||
|
`BaseDomain` reads `static unitPolicy` and passes
|
||||||
|
`policy.containerOptions()` straight into `new MeasurementContainer(...)`.
|
||||||
|
|
||||||
|
## 7. `getStatusBadge()` shape
|
||||||
|
|
||||||
|
Every domain returns the standard Node-RED status object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
fill: 'green' | 'yellow' | 'red' | 'blue' | 'grey',
|
||||||
|
shape: 'dot' | 'ring',
|
||||||
|
text: string, // ≤ 60 chars in the Node-RED editor; aim for ≤ 50.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Helpers in `generalFunctions/src/nodered/statusBadge.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { statusBadge } = require('generalFunctions');
|
||||||
|
|
||||||
|
statusBadge.compose(['🟢 OK', `flow=${flow.toFixed(1)} m³/h`]) // joins with ' | '
|
||||||
|
statusBadge.error(message) // {fill:'red', shape:'ring', text:`⚠ ${message}`}
|
||||||
|
statusBadge.idle(label) // {fill:'blue', shape:'dot', text:`⏸️ ${label}`}
|
||||||
|
```
|
||||||
|
|
||||||
|
The badge is computed in **domain**, not in `nodeClass`. nodeClass just
|
||||||
|
calls `this.source.getStatusBadge()` once per second.
|
||||||
|
|
||||||
|
## 8. `LatestWinsGate`
|
||||||
|
|
||||||
|
Extracted from MGC's `_dispatchInFlight` + `_delayedCall` pattern. Used
|
||||||
|
anywhere a parent fires commands faster than children can absorb them.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { LatestWinsGate } = require('generalFunctions');
|
||||||
|
|
||||||
|
this.demandGate = new LatestWinsGate(async (demand) => {
|
||||||
|
await this._dispatchDemandToChildren(demand);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Caller side — never blocks. The latest demand always wins.
|
||||||
|
this.demandGate.fire(demand);
|
||||||
|
```
|
||||||
|
|
||||||
|
Guarantees:
|
||||||
|
- At most one `dispatch` running at a time per gate.
|
||||||
|
- If a new value arrives while one is running, only the latest is
|
||||||
|
enqueued; intermediate ones are dropped.
|
||||||
|
- After the in-flight call settles, the latest pending value fires.
|
||||||
|
|
||||||
|
## 9. `HealthStatus`
|
||||||
|
|
||||||
|
A standardised shape for nodes that compute prediction quality / drift
|
||||||
|
(today: `rotatingMachine.predictionHealth`, future: `MGC`, `pumpingStation`
|
||||||
|
volume confidence).
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
level: 0 | 1 | 2 | 3, // 0 = fine, 3 = unusable
|
||||||
|
flags: string[], // machine-readable tags, e.g. 'no_pressure_input'
|
||||||
|
message: string, // single-line human summary
|
||||||
|
source: string | null, // free-text origin tag
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Helpers compose multiple sub-statuses (e.g. flow drift + power drift +
|
||||||
|
pressure init) into one node-level status.
|
||||||
|
|
||||||
|
## 10. Output port payload conventions
|
||||||
|
|
||||||
|
Already documented in `.claude/rules/telemetry.md` — kept here only as a
|
||||||
|
pointer:
|
||||||
|
|
||||||
|
- Port 0: process data, formatter chosen by `config.output.process`.
|
||||||
|
- Port 1: InfluxDB line-protocol, formatter chosen by
|
||||||
|
`config.output.dbase`.
|
||||||
|
- Port 2: registration / control plumbing.
|
||||||
|
- `outputUtils.formatMsg` does delta compression — only changed fields
|
||||||
|
are sent. Consumers must cache + merge.
|
||||||
175
.claude/refactor/CONVENTIONS.md
Normal file
175
.claude/refactor/CONVENTIONS.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Conventions
|
||||||
|
|
||||||
|
These rules apply to **every file written or edited** during the refactor.
|
||||||
|
They override personal preference. Be explicit about deviations in
|
||||||
|
`OPEN_QUESTIONS.md`.
|
||||||
|
|
||||||
|
## File size
|
||||||
|
|
||||||
|
| Type | Soft target | Hard cap |
|
||||||
|
|---|---|---|
|
||||||
|
| Domain module (one class / one concern) | ≤ 200 lines | 300 lines |
|
||||||
|
| Pure-function utility module | ≤ 150 lines | 250 lines |
|
||||||
|
| Test file (one .test.js) | ≤ 300 lines | 500 lines |
|
||||||
|
| Markdown spec (in this dir) | — | — |
|
||||||
|
|
||||||
|
If you go over the soft target, ask: is this two concerns? If yes, split.
|
||||||
|
Split before refactoring callers — the smaller pieces test easier.
|
||||||
|
|
||||||
|
## Function size
|
||||||
|
|
||||||
|
- Soft target: ≤ 30 lines.
|
||||||
|
- Hard cap: 60 lines (excluding comments).
|
||||||
|
- A `switch` with mostly-trivial cases counts as one statement, not many.
|
||||||
|
- A long pure-math function (e.g. an integrator) is OK if it can't be
|
||||||
|
meaningfully split.
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
Lead with the rule: **default to no comments**. Add one only when *why*
|
||||||
|
is non-obvious to a reader who can already read the code.
|
||||||
|
|
||||||
|
✅ Good comments:
|
||||||
|
```js
|
||||||
|
// Latest-wins: if a new demand arrives mid-dispatch, queue it and
|
||||||
|
// pick up after the current dispatch settles. Without this gate
|
||||||
|
// every PS tick aborts in-flight pump ramps.
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ Bad comments:
|
||||||
|
```js
|
||||||
|
// Set inflow to the value
|
||||||
|
this.inflow = value;
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Loop over machines
|
||||||
|
for (const m of machines) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Function-level docstring policy:
|
||||||
|
- One short line above the function describing **what it produces** when
|
||||||
|
the name alone isn't enough.
|
||||||
|
- Skip JSDoc `@param` blocks unless the function is part of a public
|
||||||
|
contract (the things in `CONTRACTS.md`). Inline destructuring + good
|
||||||
|
names beats JSDoc that drifts.
|
||||||
|
- Never write multi-paragraph docstrings.
|
||||||
|
|
||||||
|
Inline comments inside a function:
|
||||||
|
- Use to flag a non-obvious invariant, a workaround, or a regression
|
||||||
|
guard. Reference a ticket / commit SHA only if the workaround is
|
||||||
|
load-bearing.
|
||||||
|
- Never narrate what the next line does.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
| Thing | Convention | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| File holding a class | `PascalCase.js` matching the class name | `BasinGeometry.js` |
|
||||||
|
| File of utilities / pure functions | `camelCase.js` | `flowAggregator.js` |
|
||||||
|
| Folder under `src/` | `camelCase` (concern, plural for collections) | `control/`, `strategies/`, `commands/` |
|
||||||
|
| Class | `PascalCase` | `class BasinGeometry` |
|
||||||
|
| Function / method | `camelCase` | `selectBestNetFlow()` |
|
||||||
|
| Private method (convention only) | leading `_` | `_validateThresholdOrdering()` |
|
||||||
|
| Constant | `UPPER_SNAKE_CASE` | `CANONICAL_UNITS` |
|
||||||
|
| Module-private | leading `_` on the local | `const _DEFAULTS = {...}` |
|
||||||
|
| Test file | `<name>.<tier>.test.js` | `flowAggregator.basic.test.js` |
|
||||||
|
|
||||||
|
## Imports
|
||||||
|
|
||||||
|
- A node may import from:
|
||||||
|
- `generalFunctions` (the shared lib)
|
||||||
|
- its own `src/` tree
|
||||||
|
- Node built-ins (`events`, `path`, ...)
|
||||||
|
- declared `dependencies` in its `package.json`
|
||||||
|
- A node MUST NOT import from another node's `src/`.
|
||||||
|
- Cross-node coupling happens only through:
|
||||||
|
- the shared `generalFunctions` API
|
||||||
|
- Node-RED messages (Port 0/1/2)
|
||||||
|
- the parent/child registration handshake (`childRegistrationUtils`)
|
||||||
|
- Avoid deep imports inside `generalFunctions`. Always import from the
|
||||||
|
package root: `const { logger } = require('generalFunctions')`.
|
||||||
|
Exception: tests for `generalFunctions` itself.
|
||||||
|
|
||||||
|
## Module shape
|
||||||
|
|
||||||
|
Default to **one default export per file** when the file is named after
|
||||||
|
the thing it exports (a class, a singleton). Use named exports for
|
||||||
|
collections of small utilities.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// File: BasinGeometry.js
|
||||||
|
class BasinGeometry { ... }
|
||||||
|
module.exports = BasinGeometry;
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// File: flowAggregator.js
|
||||||
|
function selectBestNetFlow(ctx) { ... }
|
||||||
|
function updatePredictedVolume(ctx) { ... }
|
||||||
|
module.exports = { selectBestNetFlow, updatePredictedVolume };
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
- Validate at boundaries (Node-RED input handler, child registration).
|
||||||
|
Trust internal calls — don't re-validate parameters that already
|
||||||
|
passed an outer check.
|
||||||
|
- Logging on a recoverable issue: `logger.warn` once, fall back to a safe
|
||||||
|
default, continue. Don't throw.
|
||||||
|
- Logging on an unrecoverable issue: `logger.error` and stop ticking the
|
||||||
|
affected subsystem (don't crash Node-RED).
|
||||||
|
- Hard fail (`throw`) only for invariant violations the caller can't
|
||||||
|
recover from (e.g. config schema mismatch detected at construction
|
||||||
|
time).
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
- Use the `generalFunctions` `logger` exclusively. No `console.log`.
|
||||||
|
- Log levels:
|
||||||
|
- `error`: something is wrong and downstream behaviour will be
|
||||||
|
affected.
|
||||||
|
- `warn`: something is unexpected; falling back to a safe default.
|
||||||
|
- `info`: state transitions of operational interest (mode changes,
|
||||||
|
child registrations, calibrations).
|
||||||
|
- `debug`: per-tick / per-event traces.
|
||||||
|
- Do **not** ship `enableLog: "debug"` in any default config or example
|
||||||
|
flow. Logs flood within seconds.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Three tiers per module, mirroring the existing structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
basic/<module>.basic.test.js # one module in isolation
|
||||||
|
integration/<feature>.integration.test.js # multiple modules together
|
||||||
|
edge/<scenario>.edge.test.js # edge cases / regressions
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Every new module from a refactor gets at least a basic test.
|
||||||
|
- Every regression discovered during refactor gets an edge test pinning
|
||||||
|
it.
|
||||||
|
- Tests run with `node --test`. No external test framework.
|
||||||
|
- A PR may not lower the green-test count.
|
||||||
|
- Production-readiness ("trial-ready") still requires Docker E2E in
|
||||||
|
addition to `node --test`. See per-node memory.
|
||||||
|
|
||||||
|
## Pure-domain rule (specificClass and below)
|
||||||
|
|
||||||
|
Code under `src/` (other than `nodeClass.js`) is **pure domain**. It must
|
||||||
|
not:
|
||||||
|
- Touch `RED.*`
|
||||||
|
- Read `process.env`
|
||||||
|
- Assume Node-RED is running
|
||||||
|
|
||||||
|
This makes every domain module testable from a plain Node process.
|
||||||
|
|
||||||
|
## Observability of changes
|
||||||
|
|
||||||
|
When a refactor moves logic from one file to another:
|
||||||
|
- Keep behaviour identical at first. Tests pin it.
|
||||||
|
- Behavioural changes (renaming a topic, changing a payload shape) go in
|
||||||
|
separate PRs that are explicitly behavioural.
|
||||||
|
- `git mv` for pure relocations so blame stays useful.
|
||||||
206
.claude/refactor/MODULE_SPLIT.md
Normal file
206
.claude/refactor/MODULE_SPLIT.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Per-node module split
|
||||||
|
|
||||||
|
Where each concern lives **after** the refactor. All paths are relative
|
||||||
|
to `nodes/<nodeName>/src/`.
|
||||||
|
|
||||||
|
## Generic node template (any node post-refactor)
|
||||||
|
|
||||||
|
```
|
||||||
|
nodes/<name>/
|
||||||
|
<name>.js # Node-RED entry: registerType + admin endpoints (≤ 50 lines)
|
||||||
|
<name>.html # Form template + thin oneditprepare/oneditsave (≤ 250 lines)
|
||||||
|
CONTRACT.md # Generated from commands/ + hand-written events
|
||||||
|
examples/
|
||||||
|
01-basic.json
|
||||||
|
02-integration.json
|
||||||
|
03-dashboard.json # optional
|
||||||
|
src/
|
||||||
|
nodeClass.js # extends BaseNodeAdapter; ~25 lines
|
||||||
|
specificClass.js # extends BaseDomain; orchestrator only; ~150 lines
|
||||||
|
editor.js # client-side JS for HTML, served via admin endpoint (only if non-trivial UI)
|
||||||
|
commands/
|
||||||
|
index.js # the command registry array
|
||||||
|
handlers.js # the handler functions
|
||||||
|
<concern>/ # one folder per domain concern (see per-node sections below)
|
||||||
|
...
|
||||||
|
test/
|
||||||
|
basic/
|
||||||
|
integration/
|
||||||
|
edge/
|
||||||
|
```
|
||||||
|
|
||||||
|
## pumpingStation (Process Cell — L5, `#0c99d9`)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
nodeClass.js # ~25 lines, extends BaseNodeAdapter
|
||||||
|
specificClass.js # ~150 lines, orchestrator
|
||||||
|
editor.js # extracted SVG/redraw logic from the .html (~260 lines)
|
||||||
|
commands/
|
||||||
|
index.js # set.mode | set.demand | set.inflow | calibrate.* | child.register
|
||||||
|
handlers.js
|
||||||
|
basin/
|
||||||
|
BasinGeometry.js # initBasinProperties + level<->volume conversions
|
||||||
|
thresholdValidator.js # _validateThresholdOrdering — pure function
|
||||||
|
measurement/
|
||||||
|
flowAggregator.js # _selectBestNetFlow + _updatePredictedVolume + _computeRemainingTime + _levelRate + _deriveDirection
|
||||||
|
measurementRouter.js # _handleMeasurement + _onLevelMeasurement + _onPressureMeasurement
|
||||||
|
calibration.js # calibratePredictedVolume + calibratePredictedLevel + setManualInflow
|
||||||
|
control/
|
||||||
|
levelBased.js # _controlLevelBased + _scaleLevelToFlowPercent + _applyMachineGroupLevelControl
|
||||||
|
flowBased.js # placeholder for the flow mode; clearly stubbed
|
||||||
|
manual.js # forwardDemandToChildren
|
||||||
|
index.js # { 'levelbased': ..., 'flowbased': ..., 'manual': ... }
|
||||||
|
safety/
|
||||||
|
safetyController.js # evaluate() — split internally into dryRunRule + overfillRule
|
||||||
|
io/
|
||||||
|
statusBadge.js # getStatusBadge composition (was nodeClass._updateNodeStatus)
|
||||||
|
output.js # getOutput, mostly a pass-through to measurements + basin snapshot
|
||||||
|
configBuilder.js # extracted _loadConfig mapping
|
||||||
|
examples/
|
||||||
|
standalone-demo.js # extracted from the bottom of specificClass.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## measurement (Control Module — L2, `#a9daee`)
|
||||||
|
|
||||||
|
The good news: `Channel.js` already exists and is pure. Most of the
|
||||||
|
analog mode in `specificClass.js` is duplication that vanishes when the
|
||||||
|
analog path also goes through `Channel`.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
nodeClass.js # extends BaseNodeAdapter
|
||||||
|
specificClass.js # ~150 lines, orchestrator over modes
|
||||||
|
channel/
|
||||||
|
Channel.js # KEEP — already clean, the model for everything else
|
||||||
|
modes/
|
||||||
|
analogMode.js # one Channel built from flat config; routes msg.payload number
|
||||||
|
digitalMode.js # N channels from config.channels[]; routes msg.payload object
|
||||||
|
index.js # { analog, digital }
|
||||||
|
simulation/
|
||||||
|
simulator.js # simulateInput — random walk over the configured range
|
||||||
|
calibration/
|
||||||
|
calibrator.js # calibrate + isStable + standardDeviation helpers (drop duplicates of the static helpers in Channel)
|
||||||
|
commands/
|
||||||
|
index.js # set.simulator | set.outlierDetection | cmd.calibrate | data.measurement
|
||||||
|
handlers.js
|
||||||
|
```
|
||||||
|
|
||||||
|
`statistics/` (mean/stdDev/median/etc.) — promote to
|
||||||
|
`generalFunctions/src/stats/`. Both `Channel.static helpers` and the
|
||||||
|
calibrator use them.
|
||||||
|
|
||||||
|
## machineGroupControl (Unit — L4, `#50a8d9`)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
nodeClass.js # extends BaseNodeAdapter
|
||||||
|
specificClass.js # ~200 lines orchestrator; tick/handlePressureChange/handleInput
|
||||||
|
groupOps/
|
||||||
|
groupOperatingPoint.js # _equalizeOperatingPoint, _readChildMeasurement, _writeMeasurement
|
||||||
|
groupCurves.js # _groupFlow, _groupPower, _groupNCog, _groupCalcPower
|
||||||
|
totals/
|
||||||
|
totalsCalculator.js # calcDynamicTotals, calcAbsoluteTotals, activeTotals
|
||||||
|
combinatorics/
|
||||||
|
pumpCombinations.js # validPumpCombinations + checkSpecialCases
|
||||||
|
optimizer/
|
||||||
|
bestCombination.js # calcBestCombination (CoG-based)
|
||||||
|
bepGravitation.js # calcBestCombinationBEPGravitation + redistributeFlowBySlope + estimateSlopesAtBEP
|
||||||
|
index.js # picks the optimizer by config
|
||||||
|
efficiency/
|
||||||
|
groupEfficiency.js # calcGroupEfficiency + calcDistanceBEP + helpers
|
||||||
|
dispatch/
|
||||||
|
demandDispatcher.js # uses LatestWinsGate; handleInput + per-machine fanout
|
||||||
|
registration/ # auto via ChildRouter — file may be tiny
|
||||||
|
commands/
|
||||||
|
index.js # set.mode | set.scaling | set.demand | child.register
|
||||||
|
handlers.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## rotatingMachine (Equipment Module — L3, `#86bbdd`)
|
||||||
|
|
||||||
|
The biggest specificClass (1760 lines). The split mirrors the natural
|
||||||
|
boundaries the existing comments suggest.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
nodeClass.js # extends BaseNodeAdapter
|
||||||
|
specificClass.js # ~250 lines orchestrator
|
||||||
|
curves/
|
||||||
|
curveLoader.js # loadCurve wrapper + model resolution
|
||||||
|
curveNormalizer.js # _normalizeMachineCurve + _normalizeCurveSection (unit conversion + anomaly detection)
|
||||||
|
reverseCurve.js # the existing reverseCurve helper
|
||||||
|
prediction/
|
||||||
|
predictors.js # owns predictFlow / predictPower / predictCtrl (delegates to generalFunctions/predict)
|
||||||
|
groupPredictors.js # group-scope predictors used when an MGC parent calls setGroupOperatingPoint
|
||||||
|
operatingPoint.js # current operating point: pressure source, derived flow & power
|
||||||
|
drift/
|
||||||
|
driftAssessor.js # _updateMetricDrift + assessDrift + _applyDriftPenalty
|
||||||
|
predictionHealth.js # composes flow/power/pressure drift into a HealthStatus
|
||||||
|
pressure/
|
||||||
|
virtualChildren.js # _initVirtualPressureChildren + dashboard-sim children
|
||||||
|
pressureInitialization.js # getPressureInitializationStatus + tracking real children
|
||||||
|
pressureRouter.js # updateMeasuredPressure + per-position handling
|
||||||
|
state/ # adapter to generalFunctions/state — thin glue, lifecycle hooks
|
||||||
|
stateBindings.js # the position/state event handlers that fire _updateState etc.
|
||||||
|
measurement/
|
||||||
|
measurementHandlers.js # updateMeasured{Flow,Power,Temperature} + _callMeasurementHandler
|
||||||
|
flow/
|
||||||
|
flowController.js # handleInput dispatch by source/action/parameter — feeds state machine
|
||||||
|
display/
|
||||||
|
workingCurves.js # showWorkingCurves + showCoG (admin endpoints)
|
||||||
|
commands/
|
||||||
|
index.js # set.mode | cmd.startup | cmd.shutdown | cmd.estop | cmd.setpoint | cmd.flow-setpoint | data.simulate-measurement | query.curves | query.cog
|
||||||
|
handlers.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## remaining nodes (skeleton — they get the platform refactor only)
|
||||||
|
|
||||||
|
| Node | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `valve` | Equipment Module. Smaller than rotatingMachine — concern split likely just `state/`, `commands/`, `position/`. |
|
||||||
|
| `valveGroupControl` | Unit. Similar to MGC but no flow-power optimization — straightforward `position-aggregator` + `commands/`. |
|
||||||
|
| `reactor` | Unit. Domain is biological kinetics (ASM); will need a `kinetics/` folder. Big — second-tier candidate for deeper split. |
|
||||||
|
| `settler` | Unit. Has the recently-fixed `_connectReactor` integration; keep that wired through `ChildRouter`. |
|
||||||
|
| `monster` | Unit. Multi-parameter monitoring; the parameter set itself is config-driven. |
|
||||||
|
| `diffuser` | Equipment Module. Aeration controller. Likely small. |
|
||||||
|
| `dashboardAPI` | Utility. InfluxDB endpoints. Likely no `BaseDomain` — it's a passive HTTP server. |
|
||||||
|
|
||||||
|
The "skeleton" refactor for these is just:
|
||||||
|
- Convert `nodeClass.js` to extend `BaseNodeAdapter`.
|
||||||
|
- Convert `specificClass.js` to extend `BaseDomain`.
|
||||||
|
- Move the input switch to `commands/`.
|
||||||
|
- Add `getStatusBadge()` if not present.
|
||||||
|
- Use `ChildRouter` for registration.
|
||||||
|
- File splits driven by file size — if `specificClass` < 300 lines, leave it alone for now.
|
||||||
|
|
||||||
|
## generalFunctions itself
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
configs/ # unchanged — JSON schemas per node
|
||||||
|
helper/ # eventually split into infra/ + domain/, but not in this refactor
|
||||||
|
measurements/ # MeasurementContainer — unchanged
|
||||||
|
nodered/ # NEW — node-RED-side infra
|
||||||
|
BaseNodeAdapter.js
|
||||||
|
commandRegistry.js
|
||||||
|
statusBadge.js # composition helpers
|
||||||
|
statusUpdater.js # the 1 Hz status-loop wrapper
|
||||||
|
index.js
|
||||||
|
domain/ # NEW — domain-side infra
|
||||||
|
BaseDomain.js
|
||||||
|
UnitPolicy.js
|
||||||
|
ChildRouter.js
|
||||||
|
LatestWinsGate.js
|
||||||
|
HealthStatus.js
|
||||||
|
index.js
|
||||||
|
stats/ # NEW — promoted from measurement (mean, std, median, mad, lerp)
|
||||||
|
index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing exports (`logger`, `configManager`, `outputUtils`,
|
||||||
|
`MeasurementContainer`, `predict`, `interpolation`, `state`, …) stay
|
||||||
|
exactly where they are. Imports keep working unchanged.
|
||||||
|
|
||||||
|
`generalFunctions/index.js` adds new exports alongside existing ones.
|
||||||
|
Nothing is removed in this refactor.
|
||||||
99
.claude/refactor/OPEN_QUESTIONS.md
Normal file
99
.claude/refactor/OPEN_QUESTIONS.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Open questions
|
||||||
|
|
||||||
|
Things deferred. Append, don't rewrite history. Add a date when you add
|
||||||
|
or resolve an entry. Anyone (human or agent) discovering an unclear
|
||||||
|
decision during refactor work writes it here rather than guessing.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
## YYYY-MM-DD — Short title
|
||||||
|
|
||||||
|
**Context:** what we're trying to do
|
||||||
|
**Question:** what's unresolved
|
||||||
|
**Default chosen:** what we did meanwhile
|
||||||
|
**Decision needed by:** which phase or task
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-10 — External Port-0 topic naming — RESOLVED
|
||||||
|
|
||||||
|
**Decision (2026-05-10):** Use canonical names (`set.*` / `cmd.*` /
|
||||||
|
`data.*` / `child.*` / `query.*` / `evt.*`) **from Phase 1 onwards**.
|
||||||
|
Each `commands/index.js` declares the canonical name as the topic and
|
||||||
|
lists legacy names in `aliases`. Aliases log a one-time deprecation
|
||||||
|
warning. Phase 7 shrinks to: remove aliases after one release cycle.
|
||||||
|
|
||||||
|
The full prefix glossary (with what each does and why) is now in
|
||||||
|
`CONTRACTS.md §1`. See it before naming a topic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-10 — Parent EVOLV repo `development` branch lineage — RESOLVED
|
||||||
|
|
||||||
|
**Decision (2026-05-10):** Rebase parent `development` onto
|
||||||
|
`origin/main` before the refactor proceeds. Done at the start of
|
||||||
|
Phase 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-10 — `generalFunctions` deprecated paths — RESOLVED
|
||||||
|
|
||||||
|
**Decision (2026-05-10):** Tracked as Phase 8.5 in `TASKS.md`. Cleanup
|
||||||
|
runs after promotion to main. The list of paths to remove is captured
|
||||||
|
there so it isn't lost.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-10 — Two child-storage shapes — RESOLVED
|
||||||
|
|
||||||
|
**Decision (2026-05-10):** Registry-as-truth, **with named getters** that
|
||||||
|
read clearly in code. `domain.machines` keeps working — it's a getter
|
||||||
|
that returns the rotatingMachine slice of `this.child`. Same for
|
||||||
|
`domain.stations`, `domain.machineGroups`, etc. Domain code reads
|
||||||
|
naturally; the registry is the source of truth underneath.
|
||||||
|
|
||||||
|
Named getters are declared by the domain subclass in `configure()`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
configure() {
|
||||||
|
Object.defineProperty(this, 'machines',
|
||||||
|
{ get: () => this.child?.machine?.centrifugal ?? {} });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(`BaseDomain` provides a helper for this pattern.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-10 — Async vs sync `tick()` — RESOLVED with redesign
|
||||||
|
|
||||||
|
**Decision (2026-05-10):** Default is **event-driven**. Ticks are
|
||||||
|
opt-in.
|
||||||
|
|
||||||
|
`BaseNodeAdapter` exposes two timers:
|
||||||
|
|
||||||
|
- `static tickInterval = null` — opt-in periodic tick. Default null = no
|
||||||
|
tick. Domain emits `'output-changed'` on `this.emitter` instead, and
|
||||||
|
BaseNodeAdapter subscribes to that event to push outputs.
|
||||||
|
- `static statusInterval = 1000` — always-on status badge poll.
|
||||||
|
Required because Node-RED's editor refresh expects a heartbeat. Set
|
||||||
|
to 0 only in headless test environments.
|
||||||
|
|
||||||
|
When opting into ticks:
|
||||||
|
- Document **why** in a one-line comment above
|
||||||
|
`static tickInterval = ...` (e.g. "needs delta-time for predicted
|
||||||
|
volume integrator").
|
||||||
|
- A node should opt in only when truly time-driven. Examples that need
|
||||||
|
it: `pumpingStation` (predicted volume integrates over time),
|
||||||
|
`measurement` (when simulator is enabled — ticks the random walk).
|
||||||
|
- Examples that DO NOT need it: `MGC` (recomputes on pressure events),
|
||||||
|
`rotatingMachine` (recomputes on measurement events + state changes).
|
||||||
|
|
||||||
|
`tick()` is treated as fire-and-forget (no await). A node that needs
|
||||||
|
serialisation uses `LatestWinsGate` internally.
|
||||||
|
|
||||||
|
See `CONTRACTS.md §2` for the BaseNodeAdapter shape.
|
||||||
|
|
||||||
|
---
|
||||||
77
.claude/refactor/README.md
Normal file
77
.claude/refactor/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# EVOLV Platform Refactor — Guidelines
|
||||||
|
|
||||||
|
This directory holds the durable plan and conventions for the platform-wide
|
||||||
|
refactor of the EVOLV Node-RED nodes. Anyone (human or agent) working on
|
||||||
|
this refactor reads these files first.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. **Eliminate boilerplate** — every nodeClass today is ~80% identical.
|
||||||
|
Move the shared parts into `generalFunctions/`. Each node keeps only
|
||||||
|
what is genuinely node-specific.
|
||||||
|
2. **Split big domain classes** — `pumpingStation`, `machineGroupControl`,
|
||||||
|
and `rotatingMachine` each have ~1000–1800 line monolithic
|
||||||
|
`specificClass.js` files mixing 6+ concerns. Split each into focused
|
||||||
|
concern-based modules under `src/`.
|
||||||
|
3. **Document the contract** — every msg.topic the node accepts and every
|
||||||
|
message it emits is declared in code (a `commands/` module) and
|
||||||
|
surfaced in a per-node `CONTRACT.md`.
|
||||||
|
4. **Standardise naming** — consistent topic names across the platform
|
||||||
|
(`set.<noun>`, `cmd.<verb>`, `evt.<noun>`).
|
||||||
|
5. **Keep it readable** — small files, small functions, comments that say
|
||||||
|
*why* and skip *what*.
|
||||||
|
|
||||||
|
## Constraint: this is the development branch
|
||||||
|
|
||||||
|
All 12 submodules + the parent EVOLV repo are on a `development` branch.
|
||||||
|
`main` is untouched. We can change anything without breaking deployments
|
||||||
|
that track `main`.
|
||||||
|
|
||||||
|
The refactor lands on `development`. Promotion to `main` happens once the
|
||||||
|
whole platform passes its 3-tier tests + Docker E2E.
|
||||||
|
|
||||||
|
## Layered approach
|
||||||
|
|
||||||
|
The refactor is sequenced as **tiers**, not a big bang.
|
||||||
|
|
||||||
|
| Tier | What | Risk | Reversible? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Add infra in `generalFunctions` (additive only — no breaking changes) | Low | Yes |
|
||||||
|
| 2 | Pilot one node (pumpingStation) end-to-end on the new infra | Med | Yes |
|
||||||
|
| 3 | Convert remaining core nodes (measurement, MGC, rotatingMachine) | Med | Yes |
|
||||||
|
| 4 | Convert remaining nodes (valve, VGC, reactor, settler, monster, diffuser, dashboardAPI) | Low | Yes |
|
||||||
|
| 5 | Standardise input topic names + deprecation map | Med | Behind feature flag |
|
||||||
|
| 6 | Promote `development` → `main` once Docker E2E green platform-wide | Low | Yes |
|
||||||
|
| 7 | Wiki cleanup — visual-first template + Mermaid diagrams per node (post-refactor) | Low | Yes |
|
||||||
|
|
||||||
|
Each tier is a sequence of small PRs on `development`, each with its
|
||||||
|
existing tests green.
|
||||||
|
|
||||||
|
## Files in this directory
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `README.md` | This file. |
|
||||||
|
| `CONVENTIONS.md` | Code style, file size, comments, naming, imports, tests. |
|
||||||
|
| `CONTRACTS.md` | The exact shapes — `BaseNodeAdapter`, `BaseDomain`, commands registry, child router, unit policy, status badge, output ports. |
|
||||||
|
| `MODULE_SPLIT.md` | Per-node `src/` layout for the 4 core nodes + a generic template. |
|
||||||
|
| `TASKS.md` | Phased task list. The `TaskCreate` task tree mirrors this and is the active tracker. |
|
||||||
|
| `OPEN_QUESTIONS.md` | Decisions deferred to later — collected here so we don't lose them. |
|
||||||
|
|
||||||
|
## Workflow rules for spawned agents
|
||||||
|
|
||||||
|
If you are an agent working on a refactor task:
|
||||||
|
|
||||||
|
1. Read this file, `CONVENTIONS.md`, `CONTRACTS.md`, and the relevant
|
||||||
|
section of `MODULE_SPLIT.md` before changing code.
|
||||||
|
2. Stay within the scope of one task. Don't expand scope without flagging.
|
||||||
|
3. Run the affected node's tests after every meaningful change. Commands:
|
||||||
|
```
|
||||||
|
cd nodes/<nodeName> && node --test test/basic test/integration test/edge
|
||||||
|
```
|
||||||
|
4. Don't change `generalFunctions` exports unless your task is in tier 1.
|
||||||
|
5. If you discover something unclear, append it to `OPEN_QUESTIONS.md`
|
||||||
|
with a short note. Do **not** invent a decision.
|
||||||
|
6. Comments: small, function-level, *why* not *what*. See `CONVENTIONS.md`.
|
||||||
|
7. When done, summarise: files changed, tests run, anything deferred to
|
||||||
|
`OPEN_QUESTIONS.md`.
|
||||||
236
.claude/refactor/TASKS.md
Normal file
236
.claude/refactor/TASKS.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Task list
|
||||||
|
|
||||||
|
Phased and ordered. The TaskCreate tracker mirrors this list and is the
|
||||||
|
active, mutable view; this file is the durable plan.
|
||||||
|
|
||||||
|
A task is **done** when:
|
||||||
|
- The code matches the contracts in `CONTRACTS.md`.
|
||||||
|
- All the affected node's tests are green (`node --test test/basic
|
||||||
|
test/integration test/edge`).
|
||||||
|
- A short note is appended in the task tracker if anything was deferred
|
||||||
|
to `OPEN_QUESTIONS.md`.
|
||||||
|
|
||||||
|
## Phase 1 — `generalFunctions` additive infra
|
||||||
|
|
||||||
|
Goal: add the new platform pieces. Nothing is removed; nothing existing
|
||||||
|
changes shape. All existing nodes continue to work unchanged.
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 1.1 | Add `src/domain/UnitPolicy.js` + tests | Extracted from `rotatingMachine._buildUnitPolicy`. |
|
||||||
|
| 1.2 | Add `src/domain/ChildRouter.js` + tests | Built on existing `childRegistrationUtils`. |
|
||||||
|
| 1.3 | Add `src/domain/LatestWinsGate.js` + tests | Extracted from MGC `_dispatchInFlight`/`_delayedCall`. |
|
||||||
|
| 1.4 | Add `src/domain/HealthStatus.js` + tests | Standardise the `{level, flags, message, source}` shape. |
|
||||||
|
| 1.5 | Add `src/domain/BaseDomain.js` + tests | Constructor boilerplate; calls subclass `configure()`/`_init()`. |
|
||||||
|
| 1.6 | Add `src/nodered/commandRegistry.js` + tests | Topic dispatch + alias warnings. |
|
||||||
|
| 1.7 | Add `src/nodered/statusBadge.js` + tests | `compose`, `error`, `idle`, `byState` helpers. |
|
||||||
|
| 1.8 | Add `src/nodered/statusUpdater.js` + tests | 1 Hz poller calling `source.getStatusBadge()`. |
|
||||||
|
| 1.9 | Add `src/nodered/BaseNodeAdapter.js` + tests | The thing every nodeClass extends. |
|
||||||
|
| 1.10 | Add `src/stats/index.js` + tests | Promote mean/stdDev/median/mad/lerp from `measurement`. |
|
||||||
|
| 1.11 | Update `generalFunctions/index.js` (additive) | New exports under existing pattern. |
|
||||||
|
| 1.12 | Run all 12 nodes' tests against the bumped `generalFunctions` | Sanity gate before phase 2. |
|
||||||
|
|
||||||
|
Phase-1 commit cadence: one commit per task on the `development` branch
|
||||||
|
of `generalFunctions`. Submodule pointer in parent EVOLV bumps **once**
|
||||||
|
at end of phase.
|
||||||
|
|
||||||
|
## Phase 2 — pumpingStation pilot
|
||||||
|
|
||||||
|
Goal: prove the new infrastructure end-to-end. Pumping station is a
|
||||||
|
mid-complexity node — bigger than measurement, smaller than the
|
||||||
|
curve-driven nodes.
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 2.1 | Move standalone demo from `specificClass.js` to `examples/standalone-demo.js` | Pure deletion + move; tests unchanged. |
|
||||||
|
| 2.2 | Extract `basin/` (BasinGeometry + thresholdValidator) | Pure functions. |
|
||||||
|
| 2.3 | Extract `measurement/flowAggregator.js` (incl. `_updatePredictedVolume`) | Centerpiece of the tick loop. |
|
||||||
|
| 2.4 | Extract `measurement/measurementRouter.js` + `measurement/calibration.js` | |
|
||||||
|
| 2.5 | Extract `control/` strategies + dispatcher | levelBased, flowBased (stub), manual. |
|
||||||
|
| 2.6 | Extract `safety/safetyController.js` | dryRunRule + overfillRule split internally. |
|
||||||
|
| 2.7 | Add `getStatusBadge()` on `PumpingStation`; remove badge logic from nodeClass | |
|
||||||
|
| 2.8 | Convert `nodeClass.js` to extend `BaseNodeAdapter` | |
|
||||||
|
| 2.9 | Convert `specificClass.js` to extend `BaseDomain` | Use `ChildRouter`, `UnitPolicy`. |
|
||||||
|
| 2.10 | Extract `commands/` registry + handlers | Old topic names become aliases. |
|
||||||
|
| 2.11 | Extract `editor.js` from `pumpingStation.html` (the SVG redraw logic) | Served via a `/pumpingStation/editor.js` admin endpoint. |
|
||||||
|
| 2.12 | Generate `CONTRACT.md` from `commands/` + handwritten events section | |
|
||||||
|
| 2.13 | Tests: 3-tier per extracted module + the existing suite still green | Add edge tests for any regression discovered. |
|
||||||
|
| 2.14 | Docker E2E (deploy `01-basic`/`02-integration`/`03-dashboard` flows on a running Node-RED) | Required for "trial-ready" claim. |
|
||||||
|
|
||||||
|
## Phase 3 — measurement
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 3.1 | Promote stats helpers to `generalFunctions/src/stats/` (already done in 1.10) | |
|
||||||
|
| 3.2 | Convert analog mode to use `Channel` internally (with `key=null`) | Removes the ~400-line inline pipeline duplication. |
|
||||||
|
| 3.3 | Extract `simulation/simulator.js` | |
|
||||||
|
| 3.4 | Extract `calibration/calibrator.js` | |
|
||||||
|
| 3.5 | Add `getStatusBadge()` on `Measurement` | |
|
||||||
|
| 3.6 | Convert `nodeClass.js` to `BaseNodeAdapter`; `specificClass.js` to `BaseDomain` | |
|
||||||
|
| 3.7 | Extract `commands/` | |
|
||||||
|
| 3.8 | `CONTRACT.md` | |
|
||||||
|
| 3.9 | Tests + Docker E2E | |
|
||||||
|
|
||||||
|
## Phase 4 — machineGroupControl
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 4.1 | Extract `groupOps/` (groupOperatingPoint + groupCurves) | The cluster of `_group*` helpers. |
|
||||||
|
| 4.2 | Extract `totals/totalsCalculator.js` | |
|
||||||
|
| 4.3 | Extract `combinatorics/pumpCombinations.js` | |
|
||||||
|
| 4.4 | Extract `optimizer/bestCombination.js` + `optimizer/bepGravitation.js` | |
|
||||||
|
| 4.5 | Extract `efficiency/groupEfficiency.js` | |
|
||||||
|
| 4.6 | Extract `dispatch/demandDispatcher.js` using `LatestWinsGate` | Replaces `_dispatchInFlight`/`_delayedCall` directly. |
|
||||||
|
| 4.7 | Add `getStatusBadge()` | |
|
||||||
|
| 4.8 | Convert nodeClass + specificClass to base classes; use `ChildRouter` | |
|
||||||
|
| 4.9 | `commands/` + `CONTRACT.md` | |
|
||||||
|
| 4.10 | Tests + Docker E2E | |
|
||||||
|
|
||||||
|
## Phase 5 — rotatingMachine
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 5.1 | Extract `curves/` (loader + normalizer + reverseCurve) | |
|
||||||
|
| 5.2 | Extract `prediction/` (predictors + groupPredictors + operatingPoint) | |
|
||||||
|
| 5.3 | Extract `drift/` using `HealthStatus` | |
|
||||||
|
| 5.4 | Extract `pressure/` (virtual children + initialization + router) | |
|
||||||
|
| 5.5 | Extract `state/stateBindings.js` (adapter to existing `generalFunctions/state`) | |
|
||||||
|
| 5.6 | Extract `measurement/measurementHandlers.js` | |
|
||||||
|
| 5.7 | Extract `flow/flowController.js` | |
|
||||||
|
| 5.8 | Extract `display/workingCurves.js` | |
|
||||||
|
| 5.9 | Add `getStatusBadge()` (replaces the 100-line nodeClass version) | |
|
||||||
|
| 5.10 | Convert nodeClass + specificClass | |
|
||||||
|
| 5.11 | `commands/` + `CONTRACT.md` | |
|
||||||
|
| 5.12 | Tests + Docker E2E | |
|
||||||
|
|
||||||
|
## Phase 6 — remaining nodes
|
||||||
|
|
||||||
|
For each: skeleton refactor only — extend `BaseNodeAdapter` + `BaseDomain`, use `ChildRouter`, move the input switch to `commands/`, add
|
||||||
|
`getStatusBadge()`. Domain-specific module split only if `specificClass` > 300 lines after the platform refactor.
|
||||||
|
|
||||||
|
| # | Task |
|
||||||
|
|---|---|
|
||||||
|
| 6.1 | `valve` |
|
||||||
|
| 6.2 | `valveGroupControl` |
|
||||||
|
| 6.3 | `diffuser` |
|
||||||
|
| 6.4 | `monster` |
|
||||||
|
| 6.5 | `settler` |
|
||||||
|
| 6.6 | `reactor` |
|
||||||
|
| 6.7 | `dashboardAPI` (special — likely no `BaseDomain`, it's a passive HTTP server) |
|
||||||
|
|
||||||
|
These are parallelisable — each can be its own agent.
|
||||||
|
|
||||||
|
## Phase 7 — remove legacy topic aliases
|
||||||
|
|
||||||
|
> **Note:** canonical names (`set.*`, `cmd.*`, `data.*`, `child.*`,
|
||||||
|
> `query.*`, `evt.*`) are used **from Phase 1 onwards** — see
|
||||||
|
> `CONTRACTS.md §1`. Each `commands/index.js` declares the canonical
|
||||||
|
> name as `topic` and lists pre-refactor names in `aliases`. So Phase 7
|
||||||
|
> is just the deprecation-window sweep.
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 7.1 | Audit aliases across all `commands/` files; confirm one release cycle has elapsed | If any alias was added recently, defer that node's removal another cycle. |
|
||||||
|
| 7.2 | Remove `aliases` entries; canonical name only | Each removal is a single PR. |
|
||||||
|
| 7.3 | Update example flows that still used legacy names | Should already have been updated in their phase. |
|
||||||
|
| 7.4 | Document the removal in each `CONTRACT.md` | "Removed legacy topic X (replaced by canonical Y) on YYYY-MM-DD". |
|
||||||
|
|
||||||
|
## Phase 8 — promotion to main
|
||||||
|
|
||||||
|
When every node is on the new infra and Docker E2E green:
|
||||||
|
1. Bump submodule pointers in parent EVOLV `development`.
|
||||||
|
2. Open a PR per submodule (`development` → `main`).
|
||||||
|
3. Open the parent EVOLV PR last (`development` → `main`).
|
||||||
|
4. Merge in dependency order (`generalFunctions` first, then nodes that
|
||||||
|
depend on it, finally `EVOLV`).
|
||||||
|
|
||||||
|
## Phase 8.5 — `generalFunctions` deprecated path cleanup
|
||||||
|
|
||||||
|
Removes the deprecated paths flagged in `OPEN_QUESTIONS.md`. Runs after
|
||||||
|
promotion to `main` (so callers have stopped depending on the old
|
||||||
|
paths via the platform's own consumers).
|
||||||
|
|
||||||
|
### Targets to remove
|
||||||
|
|
||||||
|
| Path | Replaced by | First flagged |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/helper/menuUtils_DEPRECATED.js` | `src/menu/` (the active menu manager) | pre-refactor |
|
||||||
|
| `loadCurve` export (in `index.js` + `datasets/assetData/curves/`) | `loadModel` | pre-refactor |
|
||||||
|
| Any `*_DEPRECATED.*` file added during the refactor | (per-file note) | refactor |
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 8.5.1 | Audit consumers of `loadCurve` across all nodes | Should be zero after Phase 5 (rotatingMachine) — verify. |
|
||||||
|
| 8.5.2 | Remove `loadCurve` export + the underlying file | Single PR. Test all nodes. |
|
||||||
|
| 8.5.3 | Remove `menuUtils_DEPRECATED.js` | Verify zero imports first. |
|
||||||
|
| 8.5.4 | Sweep `generalFunctions/src/` for `_DEPRECATED.*` files; remove with consumer audit | One PR per file. |
|
||||||
|
| 8.5.5 | Update `generalFunctions` README to drop deprecated references | |
|
||||||
|
|
||||||
|
## Phase 9 — wiki cleanup (post-refactor)
|
||||||
|
|
||||||
|
Goal: each node's gitea wiki becomes **visual-first**, scannable, and
|
||||||
|
follows one shared template. Today's wiki has lots of prose and varies
|
||||||
|
per node — once the platform is uniform, the wiki should be too.
|
||||||
|
|
||||||
|
Don't start phase 9 until phase 8 is done (the wiki documents the
|
||||||
|
post-refactor shape, not the in-flight transition).
|
||||||
|
|
||||||
|
### Standard wiki template (one file per node, this is the spec)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. One-paragraph "what is this node" (≤ 60 words).
|
||||||
|
2. Position in the platform — a Mermaid block showing the node and its
|
||||||
|
typical neighbours (parent + child types, with arrows for
|
||||||
|
data direction).
|
||||||
|
3. Capability matrix — small table of "what this node can do" with
|
||||||
|
✅ / ❌ / partial.
|
||||||
|
4. Topic contract — auto-generated from src/commands/index.js
|
||||||
|
(set.* / cmd.* / evt.* / data.* — payload schema and example).
|
||||||
|
5. Output payload — a Mermaid sequence-diagram of a typical tick
|
||||||
|
(parent → child → measurement → tick → port-0 emit).
|
||||||
|
6. Configuration — a Mermaid block diagram of the editor form sections
|
||||||
|
plus a table mapping each form field to the config key it lands at.
|
||||||
|
7. Examples — links to examples/01-basic, 02-integration, 03-dashboard
|
||||||
|
with one screenshot each.
|
||||||
|
8. State / mode chart — Mermaid stateDiagram for any node with
|
||||||
|
non-trivial states (rotatingMachine, pumpingStation, MGC).
|
||||||
|
9. "When you would NOT use this node" — explicit non-goals.
|
||||||
|
10. Issues / known limitations — single-line items with links to
|
||||||
|
repo issues.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
| # | Task | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| 9.1 | Author the canonical wiki template at `.claude/refactor/WIKI_TEMPLATE.md` (or the repo-mem rule path) | Source of truth. |
|
||||||
|
| 9.2 | Build the auto-generator: `commands/index.js` → "Topic contract" markdown section | Run via a small `npm run wiki:contract` script per node. |
|
||||||
|
| 9.3 | Pilot on `pumpingStation` wiki: replace existing pages with the new template | Visual-first, prune prose. |
|
||||||
|
| 9.4 | Apply to other 3 core nodes (`measurement`, `MGC`, `rotatingMachine`) | |
|
||||||
|
| 9.5 | Apply to remaining nodes (one per repo) | |
|
||||||
|
| 9.6 | Update parent EVOLV wiki: top-level platform overview with a Mermaid block of all 13 nodes and how they connect (S88 hierarchy + data direction) | |
|
||||||
|
| 9.7 | Add a wiki style guide (max prose per section, where Mermaid is required, screenshot conventions) | |
|
||||||
|
| 9.8 | Audit pass: every page renders, every Mermaid block compiles, every link resolves | |
|
||||||
|
|
||||||
|
### Visual primitives we'll lean on (Mermaid)
|
||||||
|
|
||||||
|
- `flowchart LR` — node connections (parent ↔ child, data direction).
|
||||||
|
- `sequenceDiagram` — tick-to-port-0 lifecycle.
|
||||||
|
- `stateDiagram-v2` — rotatingMachine / pumpingStation state machines.
|
||||||
|
- `erDiagram` — only if a node has a complex internal data model worth
|
||||||
|
visualising.
|
||||||
|
|
||||||
|
Skip: classDiagram (we don't expose classes to users); gantt (no
|
||||||
|
schedules in a node's docs).
|
||||||
|
|
||||||
|
### Hard rules
|
||||||
|
|
||||||
|
- Every page leads with the Mermaid platform-position block. No "intro
|
||||||
|
paragraph then later a diagram" — diagram first.
|
||||||
|
- Each section opens with the diagram or table; prose annotates the
|
||||||
|
visual, not the other way round.
|
||||||
|
- No more than 60 words of unbroken prose anywhere on a page.
|
||||||
|
- One canonical source of truth for the topic contract: `commands/index.js`.
|
||||||
|
The wiki page is generated from it. No hand-written drift.
|
||||||
Reference in New Issue
Block a user