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>
This commit is contained in:
znetsixe
2026-05-29 18:41:56 +02:00
parent b76202190d
commit efbc0d7273
12 changed files with 95 additions and 34 deletions

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.