diff --git a/.claude/rules/repo-mem.md b/.claude/rules/repo-mem.md new file mode 100644 index 0000000..da11746 --- /dev/null +++ b/.claude/rules/repo-mem.md @@ -0,0 +1,80 @@ +# repo-mem MCP Tools + +This repo has a per-repo memory MCP server (`repo-mem`) wired via `.mcp.json`. It exposes 5 tools backed by a Hopfield substrate trained on EVOLV's source plus a BM25 index over file chunks. **Use them. They are faster and better-targeted than `grep` for concept queries, and they accumulate institutional memory of repairs.** + +If `/mcp` does not list `repo-mem` as Connected, the rest of this file does not apply for this session — fall back to `grep` / `Read`. + +## When to call which tool + +### `repo_search(query, k=8)` — primary lookup tool +Use **before** `grep` / `find` / `Explore` agent for any natural-language "where is X handled / find all places that do Y / what code implements Z" question. + +- ✅ "where is the predicted volume integrator?" → `repo_search` +- ✅ "find places that emit InfluxDB line protocol" → `repo_search` +- ❌ "find every occurrence of `_updatePredictedVolume`" → `grep` (exact symbol — BM25 doesn't beat grep at exact-string lookup) +- ❌ "list all `.test.js` files" → `find` / `ls` (no concept query) + +Returns top-K files with `file:line` ranges and snippets. Read the snippet first; only open the file if the snippet doesn't answer the question. + +### `repo_similar_fixes(query, failure?, files?, tags?, k=5)` — start-of-task context +Call at the **start** of any non-trivial bug fix or behavioral change. Cheap (BM25 + file overlap + atom cosine), zero downside if it returns nothing useful. + +- Pass the user's task description as `query`. +- If there's a failing test or stack trace, pass it as `failure`. +- If you already know which files are involved, pass them as `files`. +- Skim the returned traces; surface any near-match to the user before starting. + +### `repo_record_fix({task, failure, files, diff_summary, patch, tests, outcome, tags})` — end-of-task persist +Call at the **end** of a landed fix or behavioral change, **before** reporting completion to the user. Skip for trivial typo/comment commits. Required fields: `task` and `outcome`. Recommended: +- `failure`: the symptom that prompted the work (test output, user description, stack trace). +- `files`: the files actually changed. +- `diff_summary`: 1–3 sentences on *what* changed and *why*. +- `patch`: the unified diff (truncate to the load-bearing hunks if huge). +- `tests`: the verification command(s) you ran. +- `outcome`: `passed` / `failed` / `partial` / `reverted`. +- `tags`: short labels (`overflow-clamp`, `tokenizer`, `migration`, etc.) for retrieval bias. + +Rule of thumb: if the change took more than one read+edit pair, record it. + +### `substrate_score(text, worst_k=5)` — OOD-token check +Use **sparingly**. After generating a non-trivial code block (≥ ~30 lines of new logic, not test scaffolding), pass it through `substrate_score` and inspect the worst-confidence positions for typos, wrong identifiers, or out-of-house style. Noisy on small additions — don't use it for one-line tweaks. + +### `substrate_top_next(context, k=10)` — rarely +Predicts next BPE-subword tokens in the local style. Mostly useful for autonomous solver loops; in interactive review it's diagnostic only. If you find yourself wanting it, you probably want `repo_search` instead. + +## Workflow shape + +``` +new task arrives + ↓ +repo_similar_fixes(query=user_task) ← cheap, always do this for non-trivial tasks + ↓ +repo_search(query=concept) ← when scoping + ↓ +[normal Read / Edit / Bash work] + ↓ +[after generating non-trivial new code] +substrate_score(text=new_block) ← optional, only if block is big + ↓ +[verify: tests / build / smoke] + ↓ +repo_record_fix({...}) ← before final user-facing summary +``` + +## Anti-patterns + +- ❌ Calling `repo_search` when you already know the file path. Just `Read` it. +- ❌ Calling `repo_record_fix` after every micro-edit. Only at meaningful task boundaries. +- ❌ Treating `substrate_top_next` results as authoritative — they reflect repo style, not correctness. +- ❌ Passing the full conversation to `substrate_score` — it's per-snippet, not per-session. + +## Refresh model + +The post-commit hook auto-runs `--quick --lock` (re-ingest + BM25 + chunk re-embed; substrate retrain skipped) so retrieval stays current within ~2 s of any commit. The substrate itself is only retrained when you (or a maintainer) run `--full` manually: + +```bash +node ~/anchor-net-master/tools/repo-mem/refresh.mjs \ + --repo . --in .repo-mem --full +``` + +Re-train when the repo gains substantially new vocabulary (new node, new domain, new dependency surface). Otherwise BM25 + existing atoms keep up. diff --git a/.claude/rules/telemetry.md b/.claude/rules/telemetry.md index bd454cf..7aa7273 100644 --- a/.claude/rules/telemetry.md +++ b/.claude/rules/telemetry.md @@ -5,10 +5,7 @@ paths: # Telemetry Rules -## Output Port Convention -- Port 0: Process data (downstream node consumption) -- Port 1: InfluxDB telemetry payload -- Port 2: Registration/control plumbing +Output port convention (Port 0/1/2) is documented in `.claude/rules/node-architecture.md`. This file covers only the Port 1 payload shape and downstream contracts. ## InfluxDB Payload Structure Port 1 payloads must follow InfluxDB line protocol conventions: diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index e663cfd..275b264 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -5,18 +5,18 @@ paths: # Testing Rules -## 3-Tier Test Structure -Every node must have: -- `test/basic/*.test.js` — Unit tests for individual functions +## Test Structure +Every node has at minimum: +- `test/basic/*.test.js` — Unit tests for individual functions (specificClass domain logic) - `test/integration/*.test.js` — Node interaction and message passing tests -- `test/edge/*.test.js` — Edge cases, error conditions, boundary values - `test/helpers/` (optional) — Shared test utilities for this node +Edge-case tests live wherever they fit (in `basic/` for pure-logic edges, in `integration/` for runtime edges). Don't require a separate `test/edge/` directory. + ## Test Runner ```bash node --test nodes//test/basic/*.test.js node --test nodes//test/integration/*.test.js -node --test nodes//test/edge/*.test.js ``` ## Test Requirements @@ -25,11 +25,7 @@ node --test nodes//test/edge/*.test.js - Example flows (`examples/`) must stay in sync with implementation ## Example Flows -Each node must maintain: -- `examples/README.md` -- `examples/basic.flow.json` -- `examples/integration.flow.json` -- `examples/edge.flow.json` +Each node should ship at least one runnable example under `examples/` plus an `examples/README.md` describing it. Beyond that, add only what the node's complexity demands — not every node needs separate basic/integration/edge flow files. ## No Node-RED Runtime in Unit Tests Basic tests should test specificClass domain logic without requiring a running Node-RED instance. diff --git a/.gitignore b/.gitignore index ca330a2..9e93305 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,9 @@ npm-debug.log* .env.* # Build artifacts -*.tgz \ No newline at end of file +*.tgz +# repo-mem regenerable indexes +.repo-mem/ + +# Per-session runtime locks (scheduled_tasks, etc.) +.claude/*.lock diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..1d55836 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "repo-mem": { + "type": "stdio", + "command": "node", + "args": [ + "/home/znetsixe/anchor-net-master/tools/repo-mem/server.mjs", + "--in", + "/home/znetsixe/EVOLV/.repo-mem" + ], + "env": {} + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 608167e..d835b8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # EVOLV - Claude Code Project Guide +> **READ FIRST, BEFORE ANY OTHER WORK:** `.claude/rules/repo-mem.md` — this repo has an MCP server (`repo-mem`) exposing a substrate-trained `repo_search` and a persistent fix-trace store. Use those instead of grep for concept queries, and record completed fixes via `repo_record_fix`. Triggers, anti-patterns, and refresh model are in that rule. + ## What This Is Node-RED custom nodes package for wastewater treatment plant automation. Developed by Waterschap Brabantse Delta R&D team. Follows ISA-88 (S88) batch control standard. @@ -23,7 +25,7 @@ Each node follows a three-layer pattern: - S88 color scheme: Area=#0f52a5, ProcessCell=#0c99d9, Unit=#50a8d9, Equipment=#86bbdd, ControlModule=#a9daee - Config JSON files in `generalFunctions/src/configs/` define defaults, types, enums per node - Tick loop runs at 1000ms intervals for time-based updates -- Three outputs per node: [process, dbase, parent] +- Output ports + 3-tier architecture: see `.claude/rules/node-architecture.md` - **Multi-tab demo flows**: see `.claude/rules/node-red-flow-layout.md` for the tab/link-channel/spacing rule set used by `examples/` ## Development Notes diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..e0cab18 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,602 @@ +# EVOLV Developer Guide: Creating a New Node + +This guide walks through creating a new EVOLV node from scratch, following the project's three-layer architecture pattern. + +## Prerequisites + +- **Node.js** (v18+) +- **Node-RED** installed globally or as a dev dependency +- Clone the repo and run `npm install` in the root (no build step required) +- The `generalFunctions` submodule must be initialized (`git submodule update --init`) + +## Architecture Overview + +Every EVOLV node follows a **three-layer pattern**: + +| Layer | File | Responsibility | +|-------|------|---------------| +| 1 - Wrapper | `.js` | Registers the node type with Node-RED, sets up HTTP endpoints for menus/config | +| 2 - Node Adapter | `src/nodeClass.js` | Bridges Node-RED with domain logic: config loading, tick loop, input routing, lifecycle | +| 3 - Domain Logic | `src/specificClass.js` | Pure business logic with no Node-RED dependencies | + +Plus a UI definition: `.html` for the Node-RED editor. + +## Step-by-Step: Creating a New Node + +We will create a hypothetical `flowMeter` node as an example. + +### Step 1: Create Directory Structure + +``` +nodes/flowMeter/ + flowMeter.js # Layer 1 - wrapper + flowMeter.html # UI definition + src/ + nodeClass.js # Layer 2 - node adapter + specificClass.js # Layer 3 - domain logic + test/ + specificClass.test.js +``` + +### Step 2: Write the Wrapper (`flowMeter.js`) + +The wrapper registers the node type with Node-RED and exposes HTTP endpoints for dynamic menus and config data. + +```js +const nameOfNode = 'flowMeter'; +const nodeClass = require('./src/nodeClass.js'); +const { MenuManager, configManager } = require('generalFunctions'); + +module.exports = function(RED) { + // Register the node type + RED.nodes.registerType(nameOfNode, function(config) { + RED.nodes.createNode(this, config); + this.nodeClass = new nodeClass(config, RED, this, nameOfNode); + }); + + // Menu endpoint (dynamic dropdowns in the editor UI) + const menuMgr = new MenuManager(); + RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => { + try { + const script = menuMgr.createEndpoint(nameOfNode, ['asset', 'logger', 'position']); + res.type('application/javascript').send(script); + } catch (err) { + res.status(500).send(`// Error generating menu: ${err.message}`); + } + }); + + // Config data endpoint (exposes JSON config to the editor) + const cfgMgr = new configManager(); + RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => { + try { + const script = cfgMgr.createEndpoint(nameOfNode); + res.type('application/javascript').send(script); + } catch (err) { + res.status(500).send(`// Error generating configData: ${err.message}`); + } + }); +}; +``` + +Key points: +- `nameOfNode` must match the file name, the `registerType` name, and the HTML `data-template-name`. +- Menu categories (`['asset', 'logger', 'position']`) control which shared UI sections appear. +- The config endpoint is optional if you do not need dynamic config in the editor. + +### Step 3: Write `nodeClass.js` (Node Adapter) + +This class bridges Node-RED's API with your domain logic. + +```js +const { outputUtils, configManager } = require('generalFunctions'); +const Specific = require('./specificClass'); + +class nodeClass { + constructor(uiConfig, RED, nodeInstance, nameOfNode) { + this.node = nodeInstance; + this.RED = RED; + this.name = nameOfNode; + + this._loadConfig(uiConfig); + this._setupSpecificClass(); + this._bindEvents(); + this._registerChild(); + this._startTickLoop(); + this._attachInputHandler(); + this._attachCloseHandler(); + } + + _loadConfig(uiConfig) { + const cfgMgr = new configManager(); + this.defaultConfig = cfgMgr.getConfig(this.name); + + // buildConfig merges base sections (general, asset, functionality) + // with node-specific domain config from the UI + this.config = cfgMgr.buildConfig(this.name, uiConfig, this.node.id, { + // Add domain-specific config sections here: + flowSettings: { + maxFlow: uiConfig.maxFlow, + pipeSize: uiConfig.pipeSize, + }, + }); + + this._output = new outputUtils(); + } + + _setupSpecificClass() { + this.source = new Specific(this.config); + this.node.source = this.source; + } + + _bindEvents() { + // Subscribe to domain events for Node-RED status display + this.source.emitter.on('flowUpdate', (val) => { + this.node.status({ fill: 'green', shape: 'dot', text: `${val} ${this.config.general.unit}` }); + }); + } + + _registerChild() { + // Delayed to avoid Node-RED startup race conditions + setTimeout(() => { + this.node.send([ + null, + null, + { + topic: 'registerChild', + payload: this.node.id, + positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment', + distance: this.config?.functionality?.distance || null, + }, + ]); + }, 100); + } + + _startTickLoop() { + setTimeout(() => { + this._tickInterval = setInterval(() => this._tick(), 1000); + }, 1000); + } + + _tick() { + this.source.tick(); + const raw = this.source.getOutput(); + const processMsg = this._output.formatMsg(raw, this.config, 'process'); + const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb'); + this.node.send([processMsg, influxMsg]); + } + + _attachInputHandler() { + this.node.on('input', (msg, send, done) => { + switch (msg.topic) { + case 'measurement': + if (typeof msg.payload === 'number') { + this.source.inputValue = parseFloat(msg.payload); + } + break; + // Add more input topics as needed + } + done(); + }); + } + + _attachCloseHandler() { + this.node.on('close', (done) => { + clearInterval(this._tickInterval); + done(); + }); + } +} + +module.exports = nodeClass; +``` + +Essential methods every `nodeClass` must implement: +- `_loadConfig()` -- merges default JSON config with UI config via `configManager.buildConfig()` +- `_setupSpecificClass()` -- instantiates the domain class +- `_registerChild()` -- sends a `registerChild` message on output port 2 (parent) +- `_startTickLoop()` -- drives periodic output at 1-second intervals +- `_tick()` -- calls `source.getOutput()` and formats via `outputUtils.formatMsg()` +- `_attachInputHandler()` -- routes incoming `msg.topic` to domain methods +- `_attachCloseHandler()` -- clears timers on node removal + +### Step 4: Write `specificClass.js` (Domain Logic) + +This is pure JavaScript with no Node-RED dependencies. + +```js +const EventEmitter = require('events'); +const { logger, configUtils, configManager, MeasurementContainer } = require('generalFunctions'); + +class FlowMeter { + constructor(config = {}) { + this.emitter = new EventEmitter(); + this.configManager = new configManager(); + this.defaultConfig = this.configManager.getConfig('flowMeter'); + this.configUtils = new configUtils(this.defaultConfig); + this.config = this.configUtils.initConfig(config); + + this.logger = new logger( + this.config.general.logging.enabled, + this.config.general.logging.logLevel, + this.config.general.name + ); + + // MeasurementContainer stores typed/positioned values + this.measurements = new MeasurementContainer({ + autoConvert: true, + windowSize: this.config.smoothing?.smoothWindow || 10, + }); + this.measurements.setChildId(this.config.general.id); + this.measurements.setChildName(this.config.general.name); + + // Domain state + this.currentFlow = 0; + } + + tick() { + // Called every 1 second by nodeClass._tick() + this.calculateFlow(); + } + + calculateFlow() { + // Your domain logic here + const flow = this.currentFlow; + + // Store in MeasurementContainer using the chainable API: + // .type(measType).variant(variant).position(pos).value(val, timestamp, unit) + this.measurements + .type(this.config.asset.type) + .variant('measured') + .position(this.config.functionality.positionVsParent) + .value(flow, Date.now(), this.config.asset.unit); + + this.emitter.emit('flowUpdate', flow); + } + + getOutput() { + return { + flow: this.currentFlow, + }; + } +} + +module.exports = FlowMeter; +``` + +Key patterns: +- Always create an `emitter` (EventEmitter) -- parents subscribe to child events through it. +- Use `MeasurementContainer` for storing measurements. The chainable API follows the pattern: `measurements.type(t).variant(v).position(p).value(val, timestamp, unit)`. +- Expose `tick()` and `getOutput()` for the node adapter to call. +- Use `logger` instead of `console.log`. + +### Step 5: Write the HTML (UI Definition) + +```html + + + + + + + + +``` + +**S88 color scheme** (pick based on your node's hierarchy level): + +| S88 Level | Color | Text Color | +|-----------|-------|-----------| +| Area | `#0f52a5` | white | +| Process Cell | `#0c99d9` | white | +| Unit | `#50a8d9` | black | +| Equipment | `#86bbdd` | black | +| Control Module | `#a9daee` | black | + +All nodes must have **3 outputs**: `[process, dbase, parent]`. + +### Step 6: Create Config JSON Schema + +Create `nodes/generalFunctions/src/configs/flowMeter.json`. This defines defaults and validation rules for every config property. The `configManager` reads this file by node name. + +```json +{ + "general": { + "name": { + "default": "FlowMeter", + "rules": { "type": "string", "description": "Human-readable name." } + }, + "id": { + "default": null, + "rules": { "type": "string", "nullable": true } + }, + "unit": { + "default": "m3/h", + "rules": { "type": "string" } + }, + "logging": { + "logLevel": { + "default": "info", + "rules": { + "type": "enum", + "values": [ + { "value": "debug" }, { "value": "info" }, + { "value": "warn" }, { "value": "error" } + ] + } + }, + "enabled": { "default": true, "rules": { "type": "boolean" } } + } + }, + "functionality": { + "softwareType": { "default": "flowMeter", "rules": { "type": "string" } }, + "role": { "default": "Sensor", "rules": { "type": "string" } }, + "positionVsParent": { + "default": "atEquipment", + "rules": { + "type": "enum", + "values": [ + { "value": "atEquipment" }, { "value": "upstream" }, { "value": "downstream" } + ] + } + } + }, + "asset": { + "supplier": { "default": "Unknown", "rules": { "type": "string" } }, + "category": { "default": "sensor", "rules": { "type": "string" } }, + "type": { "default": "flow", "rules": { "type": "string" } }, + "model": { "default": "Unknown", "rules": { "type": "string" } }, + "unit": { "default": "m3/h", "rules": { "type": "string" } } + } +} +``` + +Each property has a `default` value and a `rules` object specifying the type (`string`, `number`, `boolean`, `enum`, `object`), optional constraints (`min`, `max`, `nullable`), and a description. + +### Step 7: Register with `package.json` + +Add your node to the root `package.json` under `node-red.nodes`: + +```json +{ + "node-red": { + "nodes": { + "flowMeter": "nodes/flowMeter/flowMeter.js" + } + } +} +``` + +Restart Node-RED to pick up the new node. + +### Step 8: Add Tests + +Create `nodes/flowMeter/test/specificClass.test.js`. Tests target Layer 3 (domain logic) directly, without Node-RED. + +```js +const FlowMeter = require('../src/specificClass'); + +function makeConfig(overrides = {}) { + const base = { + general: { name: 'TestFlow', id: 'test-1', logging: { enabled: false, logLevel: 'error' } }, + functionality: { softwareType: 'flowMeter', role: 'sensor', positionVsParent: 'atEquipment' }, + asset: { category: 'sensor', type: 'flow', model: 'test', supplier: 'Test', unit: 'm3/h' }, + }; + for (const key of Object.keys(overrides)) { + base[key] = typeof overrides[key] === 'object' ? { ...base[key], ...overrides[key] } : overrides[key]; + } + return base; +} + +describe('FlowMeter specificClass', () => { + it('should create an instance', () => { + const fm = new FlowMeter(makeConfig()); + expect(fm).toBeDefined(); + }); + + it('should return output with expected keys', () => { + const fm = new FlowMeter(makeConfig()); + const out = fm.getOutput(); + expect(out).toHaveProperty('flow'); + }); + + it('tick() should not throw', () => { + const fm = new FlowMeter(makeConfig()); + expect(() => fm.tick()).not.toThrow(); + }); +}); +``` + +Run tests with: `npm test` (uses Jest). The project also supports `node:test` for basic smoke tests. + +**Test organization conventions** (based on existing nodes): +- `test/specificClass.test.js` -- unit tests for domain logic +- `test/basic/*.test.js` -- structural/smoke tests (module loads, exports exist) +- `test/edge/*.test.js` -- edge case and boundary tests +- `test/integration/*.test.js` -- multi-component integration tests + +## Key APIs Reference + +### MeasurementContainer + +Chainable storage for typed, positioned measurements. Used by every domain class. + +```js +const { MeasurementContainer } = require('generalFunctions'); +const mc = new MeasurementContainer({ autoConvert: true, windowSize: 10 }); +mc.setChildId('node-id'); +mc.setChildName('PT-001'); + +// Store a value +mc.type('pressure').variant('measured').position('upstream').value(3.5, Date.now(), 'bar'); + +// Parents subscribe to events via mc.emitter +mc.emitter.on('pressure.measured.upstream', (data) => { /* { value, unit, ... } */ }); +``` + +The event name follows the pattern: `{type}.{variant}.{position}`. + +### configManager.buildConfig() + +Merges the JSON config schema defaults with UI-provided values. Called in `nodeClass._loadConfig()`. + +```js +const { configManager } = require('generalFunctions'); +const cfgMgr = new configManager(); +const defaults = cfgMgr.getConfig('myNode'); // loads myNode.json +const config = cfgMgr.buildConfig('myNode', uiConfig, nodeId, domainOverrides); +``` + +### POSITIONS + +Canonical position constants. Use these instead of hardcoded strings. + +```js +const { POSITIONS } = require('generalFunctions'); +// POSITIONS.UPSTREAM = 'upstream' +// POSITIONS.DOWNSTREAM = 'downstream' +// POSITIONS.AT_EQUIPMENT = 'atEquipment' +// POSITIONS.DELTA = 'delta' +``` + +### outputUtils.formatMsg() + +Formats raw output data into either `process` or `influxdb` messages. Only sends changed fields. + +```js +const { outputUtils } = require('generalFunctions'); +const out = new outputUtils(); +const processMsg = out.formatMsg(rawData, config, 'process'); +const influxMsg = out.formatMsg(rawData, config, 'influxdb'); +node.send([processMsg, influxMsg]); +``` + +### childRegistrationUtils + +Manages parent-child node relationships. Parents use this to accept child registrations. + +```js +const { childRegistrationUtils } = require('generalFunctions'); +const regUtils = new childRegistrationUtils(this); // 'this' is the parent specificClass +// Called when a child's registerChild message arrives: +regUtils.registerChild(childSource, positionVsParent, distance); +``` + +The parent's `registerChild()` method subscribes to the child's `measurements.emitter` events for data propagation. + +## Common Patterns + +### Parent-Child Registration + +1. Child sends `{ topic: 'registerChild', payload: nodeId, positionVsParent }` on output port 2. +2. Parent's `nodeClass._attachInputHandler()` catches `msg.topic === 'registerChild'`. +3. Parent calls `childRegistrationUtils.registerChild(child, position)`. +4. Parent subscribes to child's `measurements.emitter` events (e.g., `'flow.measured.downstream'`). +5. When the child updates a measurement, the parent's listener fires and updates its own state. + +### Tick Loop + +Every node runs a 1-second tick loop that: +1. Calls `source.tick()` to advance domain logic. +2. Calls `source.getOutput()` for current state. +3. Formats into `process` and `influxdb` messages via `outputUtils.formatMsg()`. +4. Sends on ports 0 (process) and 1 (dbase). + +The tick loop starts with a 1-second delay to allow child registration to complete. + +### Three-Output Format + +All nodes send on three ports: `node.send([processMsg, influxMsg, parentMsg])`. + +| Port | Purpose | When | +|------|---------|------| +| 0 | Process data for downstream nodes | Every tick (if changed) | +| 1 | InfluxDB line protocol for persistence | Every tick (if changed) | +| 2 | Parent registration/control messages | On startup; on parent commands | + +### Event-Driven Communication + +Nodes communicate via `EventEmitter`, not Node-RED wires: +- `measurements.emitter` fires `{type}.{variant}.{position}` events. +- Parents listen to children's emitters after registration. +- The `emitter` on the specificClass itself is used for internal state changes (e.g., updating Node-RED node status display). + +## Checklist + +Before submitting a new node, verify: + +- [ ] Three-layer structure: wrapper, nodeClass, specificClass +- [ ] Config JSON in `generalFunctions/src/configs/.json` +- [ ] Registered in root `package.json` under `node-red.nodes` +- [ ] HTML registers under category `'EVOLV'` with correct S88 color +- [ ] Three outputs: `[process, dbase, parent]` +- [ ] Uses `logger` (not `console.log`) +- [ ] Uses `MeasurementContainer` for measurement storage +- [ ] Uses `outputUtils.formatMsg()` for output formatting +- [ ] Tick loop cleans up in `_attachCloseHandler()` +- [ ] Tests exist for specificClass domain logic +- [ ] Node-specific UI fields plus shared placeholders (asset, logger, position) diff --git a/nodes/pumpingStation b/nodes/pumpingStation index d8490aa..6ab585b 160000 --- a/nodes/pumpingStation +++ b/nodes/pumpingStation @@ -1 +1 @@ -Subproject commit d8490aa94973c90b207f997da4ddef058c92e33b +Subproject commit 6ab585bcc23f7a779600583c0907ed394394ae8d diff --git a/package.json b/package.json index 022a105..c67a17e 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,6 @@ "license": "SEE LICENSE", "dependencies": { "@flowfuse/node-red-dashboard": "^1.30.2", - "@tensorflow/tfjs": "^4.22.0", - "@tensorflow/tfjs-node": "^4.22.0", "generalFunctions": "file:nodes/generalFunctions", "mathjs": "^13.2.0" },