P3 wave 1: extract measurement simulator/calibration/commands + CONTRACT

src/simulation/simulator.js  random-walk generator (was simulateInput inline)
  src/calibration/calibrator.js  calibrate + isStable + evaluateRepeatability,
                                using generalFunctions/stats. NB: isStable
                                tautology preserved verbatim — see
                                OPEN_QUESTIONS.md 2026-05-10 for the bug.
  src/commands/                  registry + handlers (canonical names from start)
  CONTRACT.md                    inputs/outputs/events surface

77 basic tests pass (62 pre-refactor + 15 new across the three new files).
specificClass.js / nodeClass.js untouched — integration is P3 wave 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-10 20:32:26 +02:00
parent 998b2002e9
commit b990f67df1
8 changed files with 725 additions and 0 deletions

59
CONTRACT.md Normal file
View File

@@ -0,0 +1,59 @@
# measurement — Contract
Hand-maintained for Phase 3; the `## Inputs` table is generated from
`src/commands/index.js` (see Phase 9 generator). Keep ≤ 80 lines.
## Inputs (msg.topic on Port 0)
| Canonical | Aliases (deprecated) | Payload | Effect |
|---|---|---|---|
| `set.simulator` | `simulator` | none (payload ignored) | Toggles `source.toggleSimulation()` — flips `config.simulation.enabled`. |
| `set.outlier-detection` | `outlierDetection` | none (payload ignored) | Toggles `source.toggleOutlierDetection()` — flips `config.outlierDetection.enabled`. |
| `cmd.calibrate` | `calibrate` | none | Calls `source.calibrate()` — captures the current input as the zero/reference offset. |
| `data.measurement` | `measurement` | mode-dependent — see below | Pushes a sensor reading into the pipeline. Analog: numeric scalar (number or numeric string) → `source.inputValue`. Digital: object payload keyed by channel name → `source.handleDigitalPayload(payload)`. Wrong shape for the configured mode logs a helpful warning suggesting the other mode. |
Aliases log a one-time deprecation warning the first time they fire.
## Outputs (msg.topic on Port 0/1/2)
- **Port 0 (process):** `msg.topic = config.general.name`. Payload built by
`outputUtils.formatMsg(..., 'process')` from `getOutput()` (analog) or
`getDigitalOutput()` (digital). Delta-compressed — only changed fields are
emitted.
- **Port 1 (InfluxDB telemetry):** same shape as Port 0, formatted with the
`'influxdb'` formatter.
- **Port 2 (registration):** at startup the node sends one
`{ topic: 'registerChild', payload: <node.id>, positionVsParent, distance }`
to its parent.
## Events emitted by `source.measurements.emitter`
The `MeasurementContainer` fires `<type>.measured.<position>` whenever a
matching series receives a new value. The type / position labels are set
from `config.asset.type` and `config.functionality.positionVsParent`
(analog), or per-channel from `config.channels[*]` (digital). Examples:
- `pressure.measured.upstream`
- `flow.measured.atequipment`
- `level.measured.downstream`
- `temperature.measured.atequipment`
Position labels are always lowercase in the event name. Parents subscribe
through the generic `child.measurements.emitter.on(eventName, ...)` handshake
established by `childRegistrationUtils`.
In digital mode one input message can fan out into several events — one
per channel that accepted a value on that tick.
The legacy internal `source.emitter` also fires `'mAbs'` with the current
scaled absolute value (analog mode only). This is deprecated in favour of
`measurements.emitter` and kept only for the editor status badge during the
refactor window.
## Children registered by this node
None — `measurement` is a leaf in the S88 hierarchy (Control Module). It
registers itself as a child of an upstream parent (rotatingMachine,
pumpingStation, reactor, monster, …) but does not accept its own children.
Registration goes via Port 2 at startup and is keyed off
`positionVsParent` / `distance` in the node's UI config.