36 Commits

Author SHA1 Message Date
znetsixe
a3a20f53d6 fix(examples,flow-lint): all example flows lint-clean of errors
- flow-lint: strip comments before the payload:null check so a comment that
  merely mentions `{ payload: null }` no longer false-positives (this was the
  only "error" on MGC 02-Dashboard; its fan-out is actually correct).
- Bump monster, rotatingMachine, coresync, machineGroupControl — ui-chart
  required-property + numeric width/height fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:19:00 +02:00
znetsixe
c932382022 docs(rules): examples are hand-authored one-offs, not generated
- node-red-flow-layout §7: drop the "generate flows from a Python builder — it's
  the source of truth" mandate. Per-node examples are hand-authored one-offs
  validated by flow-lint; no per-node generator. Scripting layout stays an option
  only for a large, owned production dashboard.
- Bump pumpingStation (generator removed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:04:59 +02:00
znetsixe
2c50cac908 chore(submodule): bump pumpingStation — 01-Basic command-envelope example
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 18:47:28 +02:00
znetsixe
efbc0d7273 feat(commands): unify command envelope across all nodes — msg.origin + unit shorthand
Platform-wide command-message contract:
- Document the envelope in .claude/refactor/CONTRACTS.md §4: unit shorthand +
  derived measure, always-convert (incl. numeric strings), msg.origin
  provenance (parent|GUI|fysical, default parent) + gated mode arbitration.
- wiki-gen: normalise descriptors through createRegistry().list() so the Unit
  column resolves both the unit: shorthand and legacy units:{} shapes.
- Bump submodule pointers: generalFunctions (registry), rotatingMachine, valve,
  valveGroupControl, machineGroupControl (msg.origin), diffuser, pumpingStation,
  monster (unit shorthand + handler dedup), dashboardAPI (wiki sync).
- Log decision in OPEN_QUESTIONS.md (2026-05-29).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 18:41:56 +02:00
znetsixe
b76202190d merge: integrate dev-lzm's unique additions into development (on slice/43 base)
Some checks are pending
CI / lint-and-test (push) Waiting to run
development now = slice/43 (latest real work, 31 commits incl. full
dashboardAPI body de957cb) + the bits ONLY dev-lzm had:
  - .npmrc (npm supply-chain hardening: ignore-scripts, min-release-age)
  - YAML frontmatter on .claude/agents/*.md
  - measurement node -> d7f6613 (editor JS modularization)
  - pumpingStation docs (2fb083d) carried under fc6491d

Submodule pointers resolved to the NEWER side (auto-merge had regressed
coresync->aefec90 and pumpingStation->2fb083d):
  coresync=21d77a8 (slice/43)  dashboardAPI=de957cb (slice/43)
  measurement=d7f6613 (dev-lzm) pumpingStation=fc6491d (dev-lzm+m³/s)

package version set to 1.0.35.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 16:49:02 +02:00
znetsixe
2d98d97b34 chore: bump pumpingStation — fix stopLevel/holdLevel not persisting in editor
Submodule a83a85e: editor was storing numeric inputs as strings via
Node-RED's auto-form-bind, causing the stopLevel/holdLevel fields to
blank out on Done → reopen. oneditsave now explicitly parseFloats them,
and the reader helpers coerce strings for backward compatibility with
already-saved flows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:22:16 +02:00
znetsixe
4668901122 chore: bump dashboardAPI — resolve InfluxDB datasource uid at push time
Bumps dashboardAPI submodule pointer to include the runtime datasource-uid
resolver. Templates no longer have to ship with the right uid baked in; any
Grafana instance with an influxdb datasource will now render the dashboards
correctly. Fixes the "Datasource <uid> not found" exclamation marks observed
when pushing to a fresh Grafana (e.g. the VPS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:33:52 +02:00
znetsixe
0826d62997 chore: bump dashboardAPI — rim/floor labels match other label size
Larger margins ensure the size-14 rim and floor captions never overlap
the topmost or bottommost threshold line in the tank.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:59:59 +02:00
znetsixe
f8d8b64be3 chore: bump dashboardAPI — min visual gap between basin threshold lines
Lines now have a minimum 3.7% gap so labels never collide with adjacent
threshold lines, regardless of how close the underlying levels are.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:52:20 +02:00
znetsixe
2b38560d5f chore: bump dashboardAPI — tank canvas centered via scale constraints
Canvas elements now use 'constraint: { horizontal: scale, vertical: scale }'
with percentage-based margin placement so the tank fills the card edge to
edge and stays centered as the panel resizes. Threshold labels split
left/right with a gap and at font size 14 for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:22:13 +02:00
znetsixe
e66e4566e6 chore: bump dashboardAPI — center tank, keep floor labels readable
Tank vertically centered in canvas frame. Labels for thresholds at the
very bottom of the tank (small dryRunThresholdPercent) no longer extend
below the visible card area.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:39:00 +02:00
znetsixe
9071d6acb4 chore: bump dashboardAPI — center basin labels, clear of threshold lines
Threshold labels now sit above or below their lines (never on top) and
are centered horizontally inside the tank.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:32:47 +02:00
znetsixe
6d03d4301f chore: enable experimental agent teams via shared settings.json
Adds .claude/settings.json with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
so every contributor gets the `team` keyword + TeamCreate tool on clone,
without each person having to set the env var locally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:19:24 +02:00
znetsixe
d8f14610cd chore: bump dashboardAPI — Tank Layout fills card top to bottom
Canvas frame stretched vertically to match the card's aspect ratio so
the tank visual fills the entire card height with no letterboxing below.
Redundant in-canvas readouts dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:12:02 +02:00
znetsixe
473cbb6951 chore: bump dashboardAPI — basin labels inline, tank fills card width
Tank Layout visual now fills the Canvas card edge-to-edge. Each
threshold's name + value live INSIDE the tank near its line instead of
in external label columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:59:33 +02:00
znetsixe
3cc8b65b69 chore: bump dashboardAPI — Tank Layout card matches visual width
Canvas card shrunk to w:6 and frame to 400 px so the basin visual fills
the card edge-to-edge. Level/Volume timeseries widen to w:14 to absorb
the freed columns. Right value labels and bottom readouts no longer clip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:53:10 +02:00
znetsixe
b04b3bb628 chore: bump dashboardAPI — double basin row height for pumpingStation
Tank Layout canvas + bar gauge + Level/Volume timeseries grow to h:20
so the basin visual occupies more vertical space and reads at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:45:54 +02:00
znetsixe
ba4e41e640 chore: bump dashboardAPI — basin canvas + bar gauge for pumpingStation
Replaces Heights/Volume-Limits/Fill% stats with an integrated basin visual:
vertical bar gauge bound to live level + threshold markers, plus Canvas
showing tank zones, threshold lines, named labels, and live readouts.
Level + Volume timeseries reflow next to the basin in the renamed Basin row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:33:19 +02:00
znetsixe
2aafc38968 chore: bump dashboardAPI — MGC drop dead Scaling panel, show group Mode/RelDistPeak
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:48:04 +02:00
znetsixe
aaf8dd1498 chore: bump dashboardAPI — clean stat panels (dedup, value-only text, meter units)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:42:57 +02:00
znetsixe
d830a6a991 chore: bump dashboardAPI — string fields render in stat panels (reduceOptions.fields)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:09:34 +02:00
znetsixe
cb42740ee1 chore: bump dashboardAPI — resolve Grafana folder by name (stale folderUid fix)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:02:50 +02:00
znetsixe
c529819696 chore: bump submodules — MGC per-pump OFF sentinel + PS m³/s canonical / m³/h output
- machineGroupControl f18f3cc: fn_chart_pump_a/b/c emit -1 OFF sentinel on the
  per-pump % control chart when state is off/idle/maintenance; ui_chart_pumps_ctrl
  ymin=-5; new per-pump-ctrl-fanout output-coverage test + manifest update.
- pumpingStation e041877: revert canonical flow to m³/s (platform convention),
  keep output m³/h for dashboard parity. No demand smoothing/hysteresis — that
  belongs in a dedicated intermediate node per design review.

Also cleaned stale InfluxDB series (out-of-tree): dropped "MGC Isolated" and
"MGC — Pump Group" measurements and the category="undefined" rows in
"Machine Group" (135,416 stale rows; live untagged data preserved).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:10:23 +02:00
znetsixe
62ad5605e8 chore: bump MGC (rendezvous lock + emergency bypass) + gF (emergencyPressurePa config)
machineGroupControl 2af6c90, generalFunctions 5c091cd. Rendezvous lock verified
live on the isolated rig: clean monotonic 1→2 pump staging, no wait/hunt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:48:06 +02:00
znetsixe
2562ed2e9f chore: bump machineGroupControl — just-in-time startup (no staging flow bump) + fn_status_split output-17 coverage
machineGroupControl f41e319 (movementScheduler just-in-time start, dashboard
fan-out output-17 coverage, manifest fan-out table).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:24:45 +02:00
znetsixe
52c091bd92 chore: bump submodules — MGC movement gate + demand telemetry, gF tag/alwaysEmit/movement fixes, RM ctrl emit, PS m³/h, dashboardAPI slice47 panels
Bumps: generalFunctions c0be50d, machineGroupControl b59d8e6,
rotatingMachine 889221f, pumpingStation 8216480, dashboardAPI 5533293.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:10:04 +02:00
00a6fc5306 chore: bump dashboardAPI for slice #43 output-coverage manifest
Refs #43
2026-05-26 18:08:56 +02:00
d1412bc53c chore: bump dashboardAPI for slice #42 example flow round-trip
Refs #42
2026-05-26 18:06:59 +02:00
d036646060 chore: bump dashboardAPI for slice #41 manual regen
Refs #41
2026-05-26 18:05:40 +02:00
8224d15d51 chore: bump dashboardAPI for slice #40 MGC template
Refs #40
2026-05-26 18:03:33 +02:00
b3e24175de chore: bump dashboardAPI for slice #39 no-duplication rule
Refs #39
2026-05-26 18:02:03 +02:00
9a9bfdafc1 chore: bump dashboardAPI for slice #38 dashed-bounds overrides
Refs #38
2026-05-26 18:00:48 +02:00
52bf827e9d chore: bump dashboardAPI for slice #37 emittedFields metadata
Refs #37
2026-05-26 17:59:43 +02:00
f867929634 chore: bump dashboardAPI submodule for slice #36 diff-skip regen
Refs #36
2026-05-26 17:57:39 +02:00
042a5cc4ba chore: bump dashboardAPI submodule for slice #35 perf + uid tests
Refs #35
2026-05-26 17:55:45 +02:00
a65cdc3562 chore: ship slice #34 — dashboardAPI walking skeleton + Grafana pin
- Bumps nodes/dashboardAPI submodule to slice/34-walking-skeleton@7fdab73
  (credentials block for bearer token, folderUid config field, basic tests).
- Pins grafana/grafana to 11.3.0 — legacy /api/dashboards/db is the
  generator target; G12 K8s-style API is out of scope (PRD constraint).

Refs #34
2026-05-26 17:53:58 +02:00
18 changed files with 120 additions and 40 deletions

View File

@@ -378,45 +378,71 @@ A descriptor may include a free-text 1-line `description` string. It is surfaced
{ topic: 'cmd.calibrate', payloadSchema: { type: 'none' }, description: 'Trigger a one-shot calibration.', handler: handlers.calibrate }
```
### Optional `units` field — pre-dispatch unit normalisation
### Optional `unit` field — pre-dispatch unit normalisation
A descriptor for a numeric setter / data topic may declare:
A descriptor for a numeric setter / data topic declares the unit the handler
always receives. **Preferred form** is the shorthand:
```js
units: { measure: '<measureName>', default: '<unitAbbr>' }
unit: 'm3/h' // operator-friendly: 'm3/h', 'mbar', 'kW', 'C', 'm', ...
```
- `measure`: a `convert`-recognised measure name (`volumeFlowRate`, `pressure`, `power`, `temperature`, `volume`, `length`, …).
- `default`: the unit the handler always receives. Operator-friendly (e.g. `m3/h`, `mbar`, `kW`, `C`).
The dimensional **measure is derived from the unit** (`volumeFlowRate` for `m3/h`,
`length` for `m`, …) — it is never declared separately, so it can never drift
from the unit. The legacy `units: { measure, default }` object is still accepted
(its `measure` is ignored and re-derived from `default`). An unrecognised unit
**throws at construction**, catching descriptor typos early.
Validation: if `units` is present, both fields must be non-empty strings. The registry throws at construction otherwise.
At dispatch time, **before** the handler runs and **before** payload-schema
validation, the registry normalises the incoming msg so the handler always sees
a finite number in the declared unit:
At dispatch time, **before** the handler runs and **before** payload-schema validation, the registry normalises the incoming msg:
1. Extract value + unit. Three accepted shapes:
- `msg.payload` is a number → `value = msg.payload`, `unit = msg.unit`.
- `msg.payload = { value: <number>, unit?: <string> }` → use those (falls back to `msg.unit` if `payload.unit` is absent).
- Anything else (string, object without `value`, missing payload, …) → normalisation is skipped; the handler receives the raw msg unchanged. No crash.
1. Extract value + unit. Accepted shapes (a numeric **string** counts as a
number everywhere — `"60"` is normalised exactly like `60`):
- `msg.payload` is a number / numeric string → `value = msg.payload`, `unit = msg.unit`.
- `msg.payload = { value, unit? }` (value number or numeric string) → use those (falls back to `msg.unit` if `payload.unit` is absent).
- Anything else (non-numeric string, object without numeric `value`, missing payload) → normalisation is skipped; the handler receives the raw msg. No crash.
2. Determine the unit-of-record:
- **No unit supplied** → silently assume `units.default`.
- **Unit recognised + correct measure** → `convert(value).from(unit).to(default)`.
- **Unit recognised but wrong measure** → log `warn` with the topic, the actual measure, the expected measure, and the accepted-unit list. Fall through with the supplied value assumed to already be in `default`.
- **Unit unrecognised** → log `warn` with the topic, the unknown unit, and the accepted-unit list. Fall through with the supplied value assumed to already be in `default`.
3. Rewrite the msg so the handler sees uniform inputs:
- `msg.payload` becomes the normalised number in `units.default` (the object form `{value, unit}` is flattened to a number).
- `msg.unit` is set to `units.default`.
- **No unit supplied** → silently assume the declared unit.
- **Unit recognised + correct measure** → `convert(value).from(unit).to(declared)`.
- **Unit recognised but wrong measure** / **unit unrecognised** → log `warn` (topic, the unit, the accepted-unit list) and fall through, assuming the value is already in the declared unit.
3. Rewrite the msg: `msg.payload` becomes the normalised number in the declared
unit (the object form `{value, unit}` is flattened), and `msg.unit` is set to
the declared unit.
Accepted-unit lists come from `convert.possibilities(measure)`. If that helper is unavailable, the warn falls back to `(see convert docs)`.
Accepted-unit lists come from `convert.possibilities(measure)`. The normalised
`{ measure, default }` pair is surfaced by `.list()` (so wikiGen + `query.units`
render the contract) and is `null` for descriptors that don't declare a unit.
The `units` field is surfaced by `.list()` (so wikiGen + `query.units` can render the contract) and is `null` for descriptors that don't declare it.
### `msg.origin` + optional `gated` field — control-authority arbitration
Every dispatch stamps **`msg.origin`** — the control authority that issued the
command — before the handler runs. Values reuse the `allowedSources` vocabulary:
| `msg.origin` | Meaning |
|---|---|
| `parent` | automation / parent controller (**default** when `msg.origin` is absent) |
| `GUI` | SCADA / HMI operator |
| `fysical` | physical buttons / panel |
Precedence is **fysical > GUI > parent** (physical > virtual > auto). A descriptor
opts into enforcement with `gated: true`: the registry then accepts the command
only if `msg.origin ∈ source.config.mode.allowedSources[source.currentMode]`,
logging a `warn` and dropping it otherwise. Nodes without a control-mode model
are advisory (allow-all), so `gated` is inert until a node has a mode. **Releasing
control is done by changing the mode** (`set.mode`), not a separate command.
Handlers read `msg.origin` for provenance (control nodes keep a legacy fallback
to `payload.source` for old flows). Descriptors may also set `defaultOrigin` to
override the global `parent` default.
Example:
```js
{
topic: 'set.demand',
units: { measure: 'volumeFlowRate', default: 'm3/h' },
payloadSchema: { type: 'number' },
unit: 'm3/h', // measure (volumeFlowRate) derived from the unit
payloadSchema: { type: 'any' },
description: 'Operator demand setpoint.',
handler: handlers.setDemand,
}
@@ -431,7 +457,7 @@ exports.setMode = (source, msg, ctx) => {
};
exports.startup = async (source, msg, ctx) => {
await source.handleInput(msg.payload?.source ?? 'parent', 'execSequence', 'startup');
await source.handleInput(msg.origin, 'execSequence', 'startup'); // msg.origin defaults to 'parent'
};
```

View File

@@ -763,3 +763,29 @@ guard here.
**Open follow-ups:**
- If `coresync` ends up classified as a process-data node rather than infrastructure, repick a non-slate hue.
- Consider a `tools/palette-lint/` check that diffs declared palette hexes vs. this table to catch future drift (low priority).
---
## 2026-05-29 — Command-message envelope: origin provenance, mode-gated arbitration, derive-measure-from-unit (in progress)
**Context:** Review of `cmd.calibrate.level` / `set.inflow` exposed three smells in the shared command path (`generalFunctions/src/nodered/commandRegistry.js`, consumed by every node's `commands/index.js`): (1) two accepted input shapes (`payload:number + msg.unit` AND `payload:{value,unit}`) re-parsed by hand in each handler (`setInflow`/`setOutflow` duplicated 10 lines the registry already did); (2) "always convert" was false — numeric *string* payloads bypassed `_extractValueAndUnit` and reached handlers unconverted; (3) `units.measure` was declared redundantly alongside `units.default` (derivable from the unit, prone to drift). Separately, the user wants command **provenance** tracked globally with control-authority precedence **physical > virtual > auto** for every field asset that accepts commands.
**Prior art (the model — already fully implemented in rotatingMachine):** `mode.current``auto | virtualControl | fysicalControl` (+`maintenance`); `mode.allowedSources` per mode = `auto→[parent,GUI,fysical]`, `virtualControl→[GUI,fysical]`, `fysicalControl→[fysical]`; enforced in `flowController.handle` via `isValidSourceForMode`/`isValidActionForMode`. Release = `setMode`. The decision is to **lift this into the shared registry** so every node inherits it.
**Decisions (user-confirmed 2026-05-29):**
- Provenance field on the message = **`msg.origin`** (NOT `source` — that's the handler's domain-instance arg). Values reuse the existing `allowedSources` vocabulary **`parent | GUI | fysical`**, default **`parent`**. (Considered `auto|virtual|physical`; rejected to avoid renaming `allowedSources` across the schema — lowest churn, "how it works now".)
- **Release/handback is handled by the mode**, not a separate command — exactly as rotatingMachine does today.
- Primary on-wire shape = **payload envelope**: `msg.topic='set.level'`, `msg.payload={value,unit}` (or bare scalar), `msg.origin='...'`. `msg.set.<noun>` namespace sugar deferred.
- **Drop `units.measure`** — descriptors declare only the unit (`unit:'m3/h'` shorthand, or `units:{default:'m3/h'}`); the registry derives the measure via `convert().describe(unit).measure`. Legacy `units:{measure,default}` still accepted (measure ignored, re-derived). An unrecognised declared unit now **throws at construction** (catches descriptor typos early).
- **Always convert** — `_extractValueAndUnit` now accepts numeric strings and `{value:"60"}`; after normalisation `msg.payload` is always a finite number in the descriptor unit. Non-numeric strings still pass through unchanged (handler guards).
**Implemented (slice 1):**
- `commandRegistry.js`: numeric-string extraction; `unit:` shorthand + derive-measure (`_validateUnits`); `msg.origin` stamped on every dispatch (default `parent`); opt-in **`gated: true`** origin arbitration (`_originAllowed`) consulting `source.config.mode.allowedSources[currentMode]` — advisory allow-all when a node has no mode model, so it's inert until opted in. Handles Set- or array-valued `allowedSources`. +18 tests (45 total, green).
- `pumpingStation`: 5 descriptors switched to `unit:` shorthand; `setInflow`/`setOutflow` collapsed to guarded one-liners. 143/143 green. rotatingMachine (legacy `units` form) 117/117, diffuser green, generalFunctions 170 green.
**Open follow-ups (slice 2+):**
- Migrate rotatingMachine to read `msg.origin` (instead of `payload.source`) and consume the shared `gated` arbiter, retiring the inline `flowController` gating duplication.
- Roll `unit:` shorthand to the remaining `units:{measure,default}` descriptors (diffuser, rotatingMachine).
- Lift the `mode`/`allowedSources`/`allowedActions` schema fragment into a shared schema partial so non-pump nodes gain the mode model.
- Decide canonical-unit storage (m³/s vs the m³/h display default currently stored in MeasurementContainer) — separate task, ripples into basin-balance math + output formatting.
- Per-node `CONTRACT.md` updates for `msg.origin` + the envelope shape; `tools/contract-verify` run.

View File

@@ -203,8 +203,13 @@ If you only fill the top-level fields, `payload_type=json` is silently treated a
✅ Do:
- Generate flows from a Python builder (`build_flow.py`) — it's the source of truth.
- Use deterministic IDs (`pump_a`, `meas_pump_a_u`, `lin_demand_to_mgc`) — reproducible diffs across regenerations.
- **Hand-author per-node `examples/` flows — they are one-offs, not generated.**
The JSON is the source of truth; edit it directly and validate with `flow-lint`.
Do **not** add a per-node flow generator: it rots out of sync, silently emits
lint-failing flows, and costs more to maintain than the one-off it produces.
(Scripting layout *may* still pay off for a large, frequently-rebuilt production
dashboard — but that is an explicit, owned tool, never a per-node example.)
- Use stable, readable IDs (`pump_a`, `meas_pump_a_u`, `lin_demand_to_mgc`) so diffs stay legible.
- Tag every channel name with `cmd:` / `evt:` / `setup:`.
- Comment every section, even short ones.
- Verify trends with a `ui-chart` of synthetic data first, before plumbing real data through.

5
.claude/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}

View File

@@ -67,7 +67,10 @@ services:
# Grafana — dashboard visualization
# ---------------------------------------------------------
grafana:
image: grafana/grafana:latest
# Pinned per dashboardAPI v2 PRD: legacy POST /api/dashboards/db is the
# generator target; Grafana 12 K8s-style API is out of scope. Bump
# deliberately, not via `pull --latest`.
image: grafana/grafana:11.3.0
container_name: evolv-grafana
restart: unless-stopped
ports:

View File

@@ -1,6 +1,6 @@
{
"name": "EVOLV",
"version": "1.0.32",
"version": "1.0.35",
"description": "Modular Node-RED package containing all control and automation nodes developed under the EVOLV project.",
"keywords": [
"node-red",

View File

@@ -185,7 +185,13 @@ function checkFunctionFanOut(n, findings) {
msg: `function "${n.name || n.id}" declares outputs=${outputs} but wires has ${Array.isArray(n.wires) ? n.wires.length : 'no'} arrays.`,
});
}
if (typeof n.func === 'string' && /payload\s*:\s*null\b/.test(n.func)) {
// Strip comments first — a comment that merely *mentions* `{ payload: null }`
// (e.g. explaining why the code avoids it) must not trip the rule. Heuristic,
// not a full parser, but it kills the common false positive.
const funcCode = typeof n.func === 'string'
? n.func.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
: '';
if (/payload\s*:\s*null\b/.test(funcCode)) {
findings.push({
rule: 'FN_PAYLOAD_NULL_LITERAL',
severity: 'error',

View File

@@ -17,8 +17,17 @@ function loadRegistry(nodeDir) {
if (!Array.isArray(descriptors)) {
throw new Error('commands/index.js must export an array of descriptors');
}
// Normalise through the registry so both the legacy `units: {measure, default}`
// and the `unit: 'm3/h'` shorthand resolve to the same `{measure, default}`
// shape (measure derived from the unit). Falls back to the raw descriptors if
// the registry can't be loaded so the tool never hard-fails on an edge case.
try {
const { createRegistry } = require('../../../nodes/generalFunctions');
return createRegistry(descriptors).list();
} catch (_) {
return descriptors;
}
}
function renderTopicTable(descriptors) {
const lines = [];