Files
EVOLV/.claude/refactor/OPEN_QUESTIONS.md
znetsixe 30928ce378 Wave A: bump submodule pointers + mark 4 OPEN_QUESTIONS resolved
generalFunctions ff9aec8 → f117546  B3.1+B3.2+B3.3 infra
  measurement      2aa8021 → e6e212a  B2.4 drop 'mAbs' event
  machineGroupControl 045a941 → 0e8cab5  B3.3 drop _unitView
  rotatingMachine  9e8463b → 84126e9  B3.3 drop _unitView
  pumpingStation   e991ea6 → ef81013  B1.2 drop 'overfillLevel'

OPEN_QUESTIONS.md: 4 entries marked RESOLVED (ChildRouter monkey-patch,
commandRegistry 'none' type, measurement 'mAbs' event, MGC unitPolicy
shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:14:00 +02:00

29 KiB

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 Debug + fix the sampling-pulse logic.
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():

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 <type>.<variant>.<position> 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 <type>.<variant>.<position> 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)

Context: P3.4. The existing isStable in measurement/src/specificClass.js does:

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 <type>.measured.<position> 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 <type>.measured.<position> 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

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.

Default chosen: Preserved — refactor reads the same stripped fields the same way. The schema warning is harmless but noisy in test output.

Decision needed by: Phase 7 (topic-name standardisation) — add the four missing constraint keys to the schema, OR move them into a samplingControl section. Either fix removes the warning and lets the values actually pass through.


2026-05-10 — monster sampling-guards cooldown test fails on development (pre-existing)

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.

Default chosen: Refactor preserves behaviour byte-for-byte — same failure remains, same line. Not a refactor regression.

Decision needed by: Phase 10 (test-suite refactor) — fix the test OR debug the underlying behaviour (likely the interaction between _beginRun resetting state inside the same sampling_program call that the integrator then runs, leaving the first second's m3PerTick stranded).


2026-05-10 — MGC handleInput retained inline latest-wins (not DemandDispatcher)

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.