# 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. --- ## 2026-05-11 — Interview round — resolved decisions | Topic | Decision | |---|---| | Ramp foot for run-zone curve (control/levelBased) | `inflowLevel` (current). startLevel is the 0% minimum, not the curve foot. | | `overfillLevel` vs `highVolumeSafetyLevel` | **`highVolumeSafetyLevel` canonical**; drop the legacy alias. | | measurement `isStable` tautology | Fix now with a config-driven absolute threshold (`stabilityThreshold` in scaling-units). Add to schema + editor UI. | | monster cooldown-guard pre-existing fail | **RESOLVED 2026-05-11** — root cause was missing `nominalFlowMin`/`flowMax`/`maxRainRef`/`minSampleIntervalSec` in `monster.json`, stripped by `configUtils.initConfig` before reaching the domain. Added the four keys to the schema. | | pumpingStation plain child dicts | Migrate to `declareChildGetter`; rewrite affected tests. | | VGC custom `registerChild` overload | Adopt ChildRouter; rewrite disambiguation tests. | | MGC inline dispatch gate vs LatestWinsGate | Extend LatestWinsGate with `fireAndWait(value)` returning the per-fire settlement promise. Migrate MGC. | | measurement legacy `'mAbs'` event | Remove now. | | ChildRouter wildcard emit-patch | Per-listener fan-out using canonical POSITIONS. No more emit patching. | | commandRegistry payload schema | Add `'none'` type + per-command `description` field (wikiGen consumes). | | UnitPolicy property-vs-method shape | Expose both. Frozen property bags alongside the methods. Drop `_unitView` workarounds. | | rotatingMachine + reactor private-method-pinning tests (13 files) | Rewrite all to drive only the public BaseNodeAdapter surface. Phase 10. | | **Unit-aware commands (new)** | Each numeric setter declares `units: { measure, default }`. commandRegistry normalises + warns + lists accepted units. `query.units` topic returns spec. Phase 11. | 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. --- ## 2026-05-10 — ChildRouter wildcard subscriptions monkey-patch `emit` — RESOLVED **Resolution (2026-05-11):** Switched to per-listener fan-out using the canonical `POSITIONS` list and a 19-type set (`MeasurementContainer.measureMap` keys + synthetic EVOLV types). Each partial-filter subscription enumerates every concrete `..` event name and registers a plain `emitter.on()` per combo. Multi-parent works without emit patching. ChildRouter.js 184 → 164 lines; 12/12 tests pass including a new multi-parent regression test. ### Original entry below ## 2026-05-10 — ChildRouter wildcard subscriptions monkey-patch `emit` (history) **Context:** P1.2 implementation. EventEmitter has no native wildcard. Subscriptions with a partial filter (`{type}`-only or `{position}`-only) install a per-variant `emit` proxy on the child's emitter; concrete `{type, position}` filters use plain `emitter.on`. **Question:** Multi-parent children. `child.parent` is already an array in `childRegistrationUtils`, so a child can be registered under several parents. If two parents each install ChildRouter wildcard proxies on the same `child.measurements.emitter`, the wraps stack — but `tearDown` only unwraps when its own bookkeeping is empty. Is this correct semantics for multi-parent teardown ordering? Or should we switch to per-listener fan-out (subscribe to every known `..` enumerated from a registry)? **Default chosen:** Stacked wrappers. The current `childRegistrationUtils` multi-parent path is rarely exercised in production. Revisit if Phase 2 / Phase 4 hits a real multi-parent case. **Decision needed by:** Phase 4. --- ## 2026-05-10 — `predictionHealth` migration in rotatingMachine **Context:** P1.4 implementation flagged that the existing `rotatingMachine.predictionHealth` carries `quality` (string) + `confidence` (0..1 numeric) on top of the new `HealthStatus` shape's `{level, flags, message, source}`. **Question:** Where does `confidence` live after migration? **Default chosen:** Keep `confidence` on the per-metric drift container as a sibling to a `health: HealthStatus` field. Drift diagnostics (`nrmse`, `longTermNRMSD`, `immediateLevel`) stay as siblings too. `HealthStatus` carries only the standardised five fields. **Decision needed by:** Phase 5 (`rotatingMachine` refactor). --- ## 2026-05-10 — `dashboardAPI` basic test broken (pre-existing) — RESOLVED **Context:** P1.12 sanity gate. `dashboardAPI/test/basic/structure-module-load.basic.test.js` uses Mocha-style `describe()` globals which don't exist under `node:test`. Reports 0 pass / 1 fail with `ReferenceError: describe is not defined`. **Action:** Pre-existing — not caused by Phase 1. Convert to `node:test` form during Phase 6 when `dashboardAPI` gets its skeleton refactor. Tracked here so it isn't lost. **Update (P6.7, 2026-05-10):** Converted to `node:test` form (`const test = require('node:test')` + `assert.doesNotThrow`). Basic test now reports 1 pass / 0 fail. The Mocha-style `test/dashboardapi.test.js`, `test/nodeClass.test.js`, `test/integration/`, and `test/edge/` files still use jest/Mocha globals — out of scope for P6.7; deferred to P10 test-suite refactor. --- ## 2026-05-10 — `dashboardAPI` skipped BaseNodeAdapter + BaseDomain **Context:** P6.7. dashboardAPI is a passive HTTP-emitter utility node: no `generalFunctions/src/configs/dashboardapi.json`, no periodic Port-0/1 telemetry stream, no parent registration, no measurements, no tick loop, no status badge. `BaseDomain` constructor would throw on the missing config file; `BaseNodeAdapter._scheduleRegistration` would emit a spurious `child.register` for a node that has no parent; the `outputUtils.formatMsg` pipeline assumes a measurement-shaped output which dashboardAPI lacks. **Default chosen:** `nodeClass` stays a plain class (does **not** extend `BaseNodeAdapter`); `specificClass` (`DashboardApi`) stays a plain class (does **not** extend `BaseDomain`). Only the shared `commandRegistry` is adopted (canonical topic `child.register` with `registerChild` alias + deprecation warning). One handler module in `src/commands/`. nodeClass shrunk from 134 → 73 lines. **Decision needed by:** Phase 7 / Phase 8 — revisit if `BaseNodeAdapter` grows a passive/HTTP-only mode (skip-registration + skip-output-stream flags) or if a `dashboardapi.json` config gets added to generalFunctions. Either makes adoption straightforward; until then the bespoke shape is correct. --- ## 2026-05-10 — pumpingStation: plain dicts vs `declareChildGetter` **Context:** P2.7+P2.8+P2.9. The 2026-05-10 "Two child-storage shapes" decision says use `declareChildGetter` (registry-as-truth), but the existing pumpingStation test suite mutates `ps.machineGroups['mgc1'] = {...}` directly to inject mock children before driving `_controlLevelBased`. A getter-backed `machineGroups` returns a fresh object per call, so the mutation is on a throwaway and the orchestrator never sees the mock. **Default chosen:** Keep `machines / stations / machineGroups` as plain id-keyed dicts on `this`. ChildRouter `onRegister` handlers populate them on real registration; tests can still assign directly. Registry remains the upstream source of truth (handshake still flows through it), but the flat dicts are also writable. Revisit if other domains can adopt `declareChildGetter` cleanly without test rewrites. **Decision needed by:** Phase 10 (test-suite refactor). --- ## 2026-05-10 — `reactor` test runtime is mathjs-bound (pre-existing) **Context:** P1.12 sanity gate. Every reactor test file takes ~13 s because `require('mathjs')` alone is ~12.5 s on this machine (mathjs is huge and loads its full operator set eagerly). With basic tests parallelised by `node --test`, each subprocess pays the cost. A 90 s outer timeout doesn't accommodate the parallel load. **Action:** Pre-existing — not caused by Phase 1. Two options to track for Phase 5/6 cleanup: 1. Switch to a tree-shaken mathjs subset (only ops actually used). 2. Cache the mathjs instance at module top and pass into Reactor classes. Tracked; not blocking the refactor. --- ## 2026-05-10 — measurement `isStable` tautology (pre-existing bug) — RESOLVED **Resolution (2026-05-11):** Replaced the tautological `stdDev < stdDev*2` check with a config-driven absolute threshold. New schema field `calibration.stabilityThreshold` (number, ≥ 0, default `0.01` in scaling-units) added to `generalFunctions/src/configs/measurement.json` so all callers see it. `Calibrator.isStable()` now returns `true` when `stdDev === 0` or `stdDev <= threshold`, falling back to the default when the config slot is missing or non-numeric. The two BUG-PRESERVED calibrator tests were rewritten — high-variance buffers now correctly report unstable under the default and only flip to stable when an explicit relaxed threshold is supplied. Added edge tests for the relaxed-threshold path, constant-buffer-with-zero-threshold path, just-above-threshold path, and missing-config fallback. `nodeClass.buildDomainConfig` and `measurement.html` (defaults + form field + oneditsave) propagate the UI value through to the domain. 100/100 measurement tests pass; 70/70 generalFunctions basic tests pass. ### Original entry below ## 2026-05-10 — measurement `isStable` tautology (pre-existing bug) (history) **Context:** P3.4. The existing `isStable` in `measurement/src/specificClass.js` does: ```js stableThreshold = stdDev * marginFactor; // marginFactor = 2 return { isStable: (stdDev < stableThreshold || stdDev == 0), stdDev }; ``` `stdDev < stdDev * 2` is always true for `stdDev > 0`, and the OR catches the zero case. So `isStable` returns `true` for every non-empty buffer. That makes `calibrate()` essentially un-gateable (it only aborts when there are < 2 samples) and `evaluateRepeatability()` happily reports a huge stdDev as "repeatability". **Action:** Preserved verbatim by the new `Calibrator` (additive). A behavioural fix needs an external reference (config-driven absolute threshold, or % of full scale). Two BUG-PRESERVED tests pin the current shape so a follow-up behavioural PR is intentional. **Decision needed by:** Phase 10 (test-suite refactor) — naturally adjacent to the calibration test cleanup. --- ## 2026-05-10 — `commandRegistry` payload schema needs `'none'`/`'void'` type — RESOLVED **Resolution (2026-05-11):** Added `'none'` to the payloadSchema.type enum. Handler still fires; logs `warn` if `msg.payload` is non-empty (catches accidental object payloads on trigger topics). Also added an optional `description` field per descriptor for wikiGen consumption. 23/23 commandRegistry tests pass; CONTRACTS.md §4 updated. ### Original entry below ## 2026-05-10 — commandRegistry payload schema needs 'none'/'void' type (history) **Context:** P3.7+P3.8. Trigger-only commands (`set.simulator`, `set.outlier-detection`, `cmd.calibrate`) ignore their payload. The current registry's `payloadSchema.type` enum is `'string'|'number'|'object'|'boolean'|'any'`. Trigger commands fall into `'any'`, which is too permissive (an object slipped past would not be flagged). **Default chosen:** Use `'any'` for now. Add `'none'`/`'void'` to the registry schema enum during Phase 7 (topic-name standardisation). **Decision needed by:** Phase 7. --- ## 2026-05-10 — measurement legacy `'mAbs'` emitter event — RESOLVED **Resolution (2026-05-11):** Removed the on-emit subscription that bridged the analog channel's `.measured.` event to `source.emitter` as `'mAbs'`. No production consumer was reading it. 96/96 measurement tests pass. ### Original entry below ## 2026-05-10 — measurement legacy 'mAbs' emitter event (history) **Context:** P3.7+P3.8 CONTRACT.md noted that the existing `Measurement` class emits `'mAbs'` on `source.emitter` whenever the analog output updates. This was a pre-MeasurementContainer broadcast. It's still fired but no production consumer reads it (per the existing comment "DEPRECATED: Use measurements container instead"). **Default chosen:** Keep firing it through Phase 3 (post-integration). Remove in Phase 7 alongside the topic-rename cleanup, or in Phase 8.5 deprecated-path cleanup. **Update (P3.2+P3.5+P3.6+P3.9, 2026-05-10):** Re-emitted from the analog specificClass by subscribing to the MeasurementContainer's `.measured.` event (position lowercased to match container normalisation). Channel itself stays event-name-agnostic. **Decision needed by:** Phase 7 / Phase 8.5. --- ## 2026-05-10 — measurement legacy property mirrors **Context:** P3.2+P3.5+P3.6+P3.9. The analog pipeline now lives inside `Channel`. The pre-refactor test suite pins many fields directly on the Measurement instance: `outputAbs`, `outputPercent`, `storedValues`, `totalMinValue`, `totalMaxValue`, `totalMinSmooth`, `totalMaxSmooth`, `inputRange`, `processRange`. Some tests *write* `m.storedValues` / `m.totalMinValue` directly before calling pipeline helpers. **Default chosen:** Install getter/setter mirrors on the Measurement instance (`_installChannelMirrors`) that forward read/write through `this.analogChannel`. Storage stays single-sourced in Channel, the legacy public surface stays writable, no test rewrites required. **Decision needed by:** Phase 10 (test-suite refactor) — replace these with direct `m.analogChannel.xxx` access in tests, then drop the mirrors. --- ## 2026-05-10 — measurement `handleScaling` mutates config.scaling **Context:** P3.2+P3.5+P3.6+P3.9. Channel's `_applyScaling` resets its *own* `scaling.inputMin/inputMax` to `[0,1]` when the input range collapses (`inputMax <= inputMin`). The pre-refactor `handleScaling` mutated `this.config.scaling.inputMin/inputMax` instead, and a basic test pins that contract. **Default chosen:** The Measurement-level `handleScaling` delegate copies Channel's reset back to `config.scaling` after the call so the visible behaviour is preserved. Long-term, the test should read the new state from `m.analogChannel.scaling` and we drop the mirror write. **Decision needed by:** Phase 10 (test-suite refactor). --- ## 2026-05-10 — measurement nodeClass routing tests pin private wiring **Context:** P3.2+P3.5+P3.6+P3.9. The basic `nodeclass-routing` and edge `invalid-payload` tests instantiated `NodeClass.prototype` and called `_attachInputHandler()` / `_registerChild()` directly. The BaseNodeAdapter superclass renamed these to `_attachInputHandler` (unchanged) and `_scheduleRegistration` (was `_registerChild`), and dispatch now goes through `this._commands` built in the constructor. **Action:** Adjusted the two tests in-place to seed `inst._commands` via `createRegistry(commands, …)` and to call `_scheduleRegistration` instead of `_registerChild`. The on-the-wire payload topic also moved from `'registerChild'` → `'child.register'` (BaseNodeAdapter convention); the test assertion was updated accordingly. **Decision needed by:** Phase 10 — these tests should be rewritten to drive a full nodeClass through `new nodeClass(uiConfig, RED, node, 'measurement')` rather than poking at private members. --- ## 2026-05-10 — MGC `calcAbsoluteTotals` implicit pressure-key coupling **Context:** P4.1/4.2 extracted `totals/totalsCalculator.js` preserving original behaviour. `calcAbsoluteTotals` iterates `machine.predictFlow.inputCurve` and re-uses the same pressure key to index `machine.predictPower.inputCurve[pressure]`. If the two curves were sampled at different pressures (legitimate when power was extrapolated separately from flow), the lookup is `undefined` and the call throws. **Question:** should the totals calculator defensively skip mismatched pressure keys, or should the invariant "flow + power curves share pressure keys" be enforced upstream in rotatingMachine's curveLoader/normalizer? **Default chosen:** preserved the implicit coupling — no behavioural change. **Decision needed by:** P5 (rotatingMachine refactor) — curveLoader/Normalizer is the natural place to enforce or document the pairing. --- ## 2026-05-10 — MGC concern modules use legacy unitPolicy object shape — RESOLVED **Resolution (2026-05-11):** UnitPolicy.declare() now exposes canonical/output/curve as BOTH callable methods AND frozen property bags. Both shapes work: `policy.canonical('flow')` and `policy.canonical.flow`. Dropped the `_unitView`/`unitPolicyView` workaround in both MGC (specificClass 336→318) and rotatingMachine (400→377). CONTRACTS.md §6 updated. All platform tests stay green. ### Original entry below ## 2026-05-10 — MGC concern modules use legacy unitPolicy object shape (history) **Context:** The MGC concern modules (groupOps/groupOperatingPoint, totals/totalsCalculator, combinatorics/pumpCombinations, control/strategies) extracted in Wave 1 read units as `ctx.unitPolicy.canonical.flow` — the old plain-object shape carried on the pre-refactor specificClass. `BaseDomain` now wires `this.unitPolicy` to a `UnitPolicy` instance whose canonical/output are methods (`canonical('flow')`). **Question:** Should the concern modules be updated to call the methods, or should we keep the object-shaped view long-term? **Default chosen:** specificClass builds a frozen `this._unitView` ({ canonical: {flow,pressure,power,temperature}, output: {…} }) and passes it to the modules. Two surface shapes live side-by-side in the same node. **Decision needed by:** P5 (rotatingMachine) — the same concern-module shape will likely repeat. Pick one and migrate before the second node lands on the pattern. --- ## 2026-05-10 — rotatingMachine Machine constructor takes 3 positional args **Context:** P5.9/5.10/5.12. The pre-refactor Machine class accepted `(machineConfig, stateConfig, errorMetricsConfig)`. BaseDomain's constructor only knows about the first slot. The whole test suite (~30 files) constructs Machines directly with two positional args, and BaseNodeAdapter instantiates DomainClass with just `this.config`. **Question:** Where do the extra positional configs travel? Schema validation in `configUtils.initConfig` strips unknown top-level keys, so embedding them in machineConfig doesn't work. Subclass-overriding constructor before super() is blocked by ES6's pre-super `this` rule. **Default chosen:** Static stash on the class itself (`Machine._pendingExtras`) assigned just before `super()` (or by nodeClass.buildDomainConfig before BaseNodeAdapter instantiates the domain). `configure()` reads + clears it. Single-threaded JS makes the hand-off race-free. **Decision needed by:** P10 (test-suite refactor) — when tests get rewritten to use the BaseNodeAdapter-built domain, drop the multi-arg constructor and fold stateConfig/errorMetricsConfig into machineConfig slices. --- ## 2026-05-10 — rotatingMachine private nodeClass tests (4 files adjusted) **Context:** P5.9/5.10/5.12. Four pre-refactor tests pinned private nodeClass methods: `_loadConfig`, `_setupSpecificClass`, `_updateNodeStatus`, and the inline `_attachInputHandler` switch. After the BaseNodeAdapter migration those private methods are gone — config build lives in `buildDomainConfig()`, dispatch in `commands/`, status badge in `source.getStatusBadge()`. **Default chosen:** Updated the four test files to drive the new surface: `buildDomainConfig` returns the per-node slice (and stamps `Machine._pendingExtras`); routing tests seed `inst._commands` via `createRegistry(commands, …)` and assert through that path; status-badge tests call `io.buildStatusBadge(source)` directly. **Decision needed by:** P10 — these tests still poke private members (`_commands`, `_attachInputHandler`). The right shape is constructing a full `new nodeClass(uiConfig, RED, node, 'rotatingMachine')` and asserting against the resulting `node._sent` / `node._statuses`. --- ## 2026-05-10 — monster schema strips command-line constraint keys — RESOLVED 2026-05-11 **Context:** P6.3. The monster JSON schema in `generalFunctions/src/configs/monster.json` defines `samplingtime`, `minVolume`, `maxWeight` and others under `constraints`, but NOT `nominalFlowMin`, `flowMax`, `maxRainRef`, `minSampleIntervalSec`. `configUtils.initConfig` strips these unknown keys with a `Unknown key … Removing it.` warning. The legacy code read them anyway — `Number.isFinite(undefined)` returns false, so guards naturally route into invalid-bounds territory and tests pass via the undefined cascade. **Resolution (2026-05-11, B1.4):** Added the four missing fields (`nominalFlowMin`, `flowMax`, `maxRainRef`, `minSampleIntervalSec`) to the `constraints` section of `generalFunctions/src/configs/monster.json` with sensible defaults (0/0/10/60). The unknown-key warning disappears and user-supplied values now propagate through validation to the domain, restoring the documented sampling behaviour. --- ## 2026-05-10 — monster sampling-guards cooldown test fails on development (pre-existing) — RESOLVED 2026-05-11 **Context:** P6.3 baseline run. `test/edge/sampling-guards.edge.test.js` "cooldown guard blocks pulses when flow implies oversampling" already fails on `development` BEFORE the refactor (`assert.ok(monster.sumPuls > 0)` — sumPuls stays at 0 across 80 ticks). The legacy in-file equivalent in `test/monster.test.js` (Mocha-style wrapper, not picked up by `node:test`) appears to have passed in an earlier era. **Resolution (2026-05-11, B1.4):** Root cause was the schema-stripping issue documented immediately above — `nominalFlowMin`/`flowMax`/`minSampleIntervalSec` were stripped by `configUtils.initConfig` before reaching the domain, so `validateFlowBounds` saw NaN/NaN and routed every `i_start` into the invalid-bounds early return, which prevented `_beginRun` from ever firing. With the four constraint keys now declared in `monster.json`, the test config propagates intact: `_beginRun` runs, the m3PerTick integrator accumulates ~0.056 m3/tick, `temp_pulse` crosses 1 at tick ~18, the first pulse fires, subsequent pulses are correctly blocked by the 60 s cooldown, and `sumPuls > 0` / `missedSamples > 0` / `bucketVol > 0` / `getSampleCooldownMs() > 0` all hold. Added a guard-site comment in `parameters/parameters.js#validateFlowBounds` pointing back at the schema contract. All 10/10 monster tests green. --- ## 2026-05-10 — MGC handleInput retained inline latest-wins (not DemandDispatcher) — RESOLVED **Resolution (2026-05-11):** Extended `LatestWinsGate` with `fireAndWait(value)` that returns a per-fire settlement promise. A parked call superseded by a later fire resolves with the frozen sentinel `LatestWinsGate.SUPERSEDED = { superseded: true }` (not a reject) so callers branch on a value without try/catch. Dispatch errors still resolve the promise (with `undefined`) and surface via `gate.lastError`. MGC's `handleInput` now delegates to `DemandDispatcher.fireAndWait`; the inline `_dispatchInFlight` + `_delayedCall` block is gone. `turnOffAllMachines` calls `cancelPending()` on the dispatcher instead of zeroing `_delayedCall`. `LatestWinsGate.js` 75 → 116 lines (under the 150 cap). MGC `specificClass.js` net −14 lines. The `turnoff-deadlock` test that pinned `_delayedCall` was rewritten to assert against the parked `fireAndWait` resolving as superseded. Other awaiting tests (`ncog-distribution`, `idle-startup-deadlock`, `demand-cycle-walkthrough`) needed no change since `fireAndWait` preserves the "await waits for the call's dispatch" shape for non-superseded calls. All 77/77 MGC tests pass; 12/12 LatestWinsGate basic tests pass. ### Original entry below ## 2026-05-10 — MGC handleInput retained inline latest-wins (not DemandDispatcher) (history) **Context:** Wave 1 added `src/dispatch/demandDispatcher.js` wrapping `LatestWinsGate`. Tests (`turnoff-deadlock`, `idle-startup-deadlock`, `ncog-distribution`) call `await mgc.handleInput(...)` and rely on the awaited promise resolving after the dispatch completes; they also pin the exact `_delayedCall` field. `LatestWinsGate.fire(value)` returns void. **Question:** Should `handleInput` switch to the gate (changing the test contract), or stay inline (keeping the awaitable shape)? **Default chosen:** kept the inline `_dispatchInFlight + _delayedCall` gate verbatim. `DemandDispatcher` remains exported but unused by the orchestrator for now — its basic test still passes since it tests the wrapper in isolation. **Decision needed by:** P7 (topic-name standardisation) or P10 (test-suite refactor) — adopting the gate requires either rewriting tests to drain the gate or changing the gate to return a settle promise. --- ## 2026-05-10 — valveGroupControl registerChild overload (skipped ChildRouter) **Context:** P6.2. `ValveGroupControl.registerChild(child, posOrType)` is called from two distinct paths: (a) `childRegistrationUtils.registerChild` delegates with the canonical softwareType as 2nd arg, and (b) tests + a few in-process callers invoke it directly passing **either** a position (`'atEquipment'`) **or** a softwareType (`'machine'`). The legacy code disambiguated via a `KNOWN_POSITIONS` set lookup and returned a boolean indicating registration success (used by `flow-distribution` regression test to assert a non-valve payload yields `false`). **Default chosen:** kept the legacy resolver in the domain — override `this.registerChild` inside `configure()` so the boolean return + dual semantics survive. `ChildRouter` is **not** used for VGC (no `onRegister` / `onMeasurement` handlers declared). Source-side event wiring still lives in `src/sources/fluidContract.js` (raw emitter `.on` on each `SOURCE_FLOW_EVENTS` name) because the source family includes mixed-case event names (`flow.predicted.atEquipment` and lowercase variants both fire). **Decision needed by:** P7 — once topic names + position casing are standardised, the source listener set collapses and a ChildRouter `onMeasurement('machine', { type:'flow' }, …)` declaration becomes sufficient. At that point `registerChild` can return to base + ChildRouter and the boolean-return test can be rewritten to assert via side-effects. --- ## 2026-05-10 — valveGroupControl `set.position` placeholder **Context:** P6.2 command registry. Task spec required canonical name `setpoint → set.position`, but VGC's pre-refactor input switch did not implement a `setpoint` topic — valve position is driven by `data.totalFlow` re-distribution, not direct per-valve setpoints. Registering `set.position` with an empty handler keeps the canonical name reserved without breaking the contract surface. **Default chosen:** registered `set.position` with a no-op handler that debug-logs the payload. `setpoint` listed as alias so a legacy emitter gets the same no-op path. **Decision needed by:** P7 — decide whether VGC actually needs a per-valve setpoint topic (probably yes when virtualControl mode lands). At that point promote the handler from no-op to real dispatch. --- ## 2026-05-10 — reactor private nodeClass tests (8 files adjusted) **Context:** P6.5. Eight pre-refactor reactor tests pinned private nodeClass methods (`_loadConfig`, `_setupClass`, `_registerChild`, inline `_attachInputHandler` switch, `_tick`, `_startTickLoop`, `_attachCloseHandler`). After the BaseNodeAdapter migration those private methods are gone — config build lives in `buildDomainConfig()`, dispatch in `commands/`, registration in `_scheduleRegistration` (renamed), and the periodic emit lives in `_emitOutputs` (overridden so the Fluent / GridProfile Port-0 contract is preserved — delta-compressed payloads can't carry the C-vector). **Default chosen:** Adjusted in place: `test/basic/constructor.basic.test.js`, `test/basic/input-routing.basic.test.js`, `test/basic/register-child.basic.test.js`, `test/basic/speedup-factor.basic.test.js`, `test/edge/invalid-topic.edge.test.js`, `test/edge/missing-child.edge.test.js`, `test/edge/invalid-reactor-type.edge.test.js`, `test/integration/tick-loop.integration.test.js`. Routing tests seed `inst._commands` via `createRegistry(commands, …)`; topic moved from `'registerChild'` → `'child.register'`. The "unknown reactor_type throws" edge case became "falls back to CSTR" — the legacy bottom-of-switch already fell back to CSTR; only the surface changed (warning channel now via domain logger, not `node.warn`). **Decision needed by:** Phase 10 — same shape as the rotatingMachine / measurement adjustments. The right fix is to drive a full `new nodeClass(...)` and assert against `node._sent` / `node._statuses` instead of poking private members. --- ## 2026-05-10 — reactor schema enum lowercases `reactor_type` **Context:** P6.5. The reactor JSON schema defines `reactor.reactor_type` as `type: 'enum'` with values `'CSTR'` / `'PFR'`. The shared enum validator lowercases the user-supplied value before comparing, so an inbound `'PFR'` ends up stored as `'pfr'` in the validated config. The pre-refactor nodeClass switched on the raw uiConfig value and never saw the lowercased form; after the BaseDomain migration the wrapper reads the validated config and would always fall back to CSTR. **Default chosen:** `Reactor._buildEngine` upper-cases the value before switching. The schema is left intact so external Phase-7 enum-casing work can decide whether to preserve original casing globally. **Decision needed by:** Phase 7 (topic-name + schema standardisation) — once enums standardise on a canonical casing, drop the `.toUpperCase()` guard here.