Three coherent additions that the MGC rendezvous planner depends on:
- machineGroupControl.json: new `planner.useRendezvous` boolean (default
true). Used by both `_optimalControl` and `equalFlowControl` (via the
shared `_dispatchFlowDistribution` helper) to gate same-time-landing.
- state.js: external aborts (returnToOperational=false) bump a monotonic
`sequenceAbortToken`. executeSequence captures it at entry and bails
out of its for-loop if it advances mid-sequence, so a shutdown that's
past its ramp-down step doesn't barge through stopping → coolingdown
when a fresher demand re-engages the pump.
- stateManager.js: new `getRemainingTransitionS()` returns the seconds
remaining in a timed state by reading the wall-clock entry timestamp.
buildProfile() reads it so the planner can compute exact eta for a
child that's currently mid-ladder (warmingup / starting / cooling).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes bundled together because the diffuser curve files
only make sense once the registry namespace they live in exists.
src/registry — new asset-metadata resolver:
- AssetResolver with synchronous resolve(namespace, id) + lazy cache,
async refresh() for future remote pulls.
- FileBackend (per-id or single-file layouts, case-insensitive) and a
stub HttpBackend (disabled unless EVOLV_ASSET_REMOTE=1).
- Namespaces: curves, menu, monsterSamples, monsterSpecs, units. Menu
namespace re-keys by inner softwareType + filename so editors that
pass either string resolve to the same tree.
- README explains how to add a namespace.
- AssetCategoryManager (datasets/assetData/index.js) becomes a thin
facade over the resolver so existing consumers don't move.
- 246/246 tests pass — including the 39-test registry suite.
datasets/assetData — file moves + new diffuser data:
- modelData/*.json deleted; curves/*.json is the canonical home.
- New diffuser.json menu tree with GVA, Jäger, Aquaconsult/Entec,
PIK/PRK suppliers.
- gva-elastox-r.json migrated from the inline _loadSpecs hardcode,
re-tagged coverageBasis="bottom-coverage-pct" (the legacy 2.4
elements/m² was a prior mis-conversion; we can't recover the
original % so it's a single-point curve under key "0").
- jaeger-jetflex-td-65-2-g-epdm-1000.json — extracted from the Jäger
EPDM-1000mm SSOTE/DWP chart on the data sheet (vector-PDF read).
SSOTE 8.20→6.40 %/m, DWP 25→48 mbar across Q 2-12 Nm³/h. Single
coverage (vendor doesn't state test conditions).
- aerostrip-phoenix.json — 4-coverage SOTE family at 4.75 m water
depth (DD 5/10/15/20 %, flux 10-70 Nm³/h·m²) from the Entec/de
Winter 2023-11-22 dataset; DWP curve from the 21 % @ 4.05 m chart.
- pik300.json / prk300.json — 5-coverage SOTE + SSOTR (DD 5-25 %)
with split DWP per model variant, water depth ≈ 4.0 m inferred from
the SOTE↔SSOTR ratio in the source spreadsheet.
src/configs/diffuser.json:
- New asset.{model, assetTagNumber} block so the editor's selected
model id survives validation.
- diffuser.density description corrected to "Bottom coverage [%]";
default 2.4 → 15 (typical fine-bubble install).
src/configs/{rotatingMachine,valve}.json: small alignment edits that
came with the registry phase.
src/menu/asset.js + src/menu/aquonSamples.js: rewritten as facades
over assetResolver, keeping the editor-side cascade behaviour intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buildConfig() was doing Object.assign(config, domainConfig) — a shallow
merge. When buildDomainConfig returned subsets like {general: {unit}} or
{asset: {curveUnits}}, the assign replaced config.general / config.asset
wholesale, wiping general.id (nodeId), general.name, asset.model, and
asset.supplier that buildConfig had just populated.
Downstream consequences:
- MGC.onRegister('machine') keys by config.general.id; two children with
the same null id collide and the second registration is rejected.
- rotatingMachine curve-lookup uses asset.model; with model "Unknown" the
pump curve fails to resolve.
Replace Object.assign with an in-place recursive _deepMerge: arrays and
primitives in src replace dst, plain objects merge key-by-key so siblings
on dst survive.
Two regression tests added — single-config field survival and the MGC
multi-child id-collision case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B2.3 LatestWinsGate fireAndWait:
Added fireAndWait(value, ctx?) returning per-fire settlement promise.
Supersede resolves with frozen sentinel {superseded: true} (no
rejection — callers branch on value without try/catch). Dispatch
errors also resolve (with undefined); error surfaces via gate.lastError.
LatestWinsGate.js 75 → 116 lines. 12/12 tests pass.
P11.1 convert.possibilities(measure):
New helper returning sorted+deduped unit names for a measure.
Cached per measure. Reuses existing convert measures map. Also
exposed convert.measures() listing all known measures.
convert/index.js +21 lines. New test file: 90 lines, 12/12 tests.
P11.2 commandRegistry.units field:
Pre-dispatch normalisation pipeline. descriptor.units = {measure,
default}; commandRegistry extracts msg.payload + msg.unit (3 shapes),
validates against measure, converts to default, falls back + warns
with accepted-list on unknown/wrong-measure. Falls back gracefully
if convert.possibilities is missing. commandRegistry.js 164 → 237.
+7 new tests covering all 4 paths.
monster schema fix (P11.2 sibling):
generalFunctions/src/configs/monster.json was stripping four
legitimate constraint keys (nominalFlowMin, flowMax, maxRainRef,
minSampleIntervalSec). Added them with defaults matching the
legacy nodeClass coercion. Side effect: this also UNBLOCKED the
monster cooldown-guard test (separate ROOT-CAUSE entry below).
CONTRACTS.md §4 + §8 updated. 144/144 basic tests + 206/206 full
generalFunctions tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B3.1 ChildRouter per-listener fan-out (drop emit monkey-patch):
Partial-filter subscriptions enumerate every concrete
<type>.measured.<position> event name (cartesian product over
the canonical POSITIONS list + 19 KNOWN_TYPES) and register a
plain emitter.on() per combo. Multi-parent semantics are trivial:
each ChildRouter's listeners are independent. Drop the wrap/unwrap
bookkeeping in tearDown. ChildRouter.js 184→164 lines.
B3.2 commandRegistry 'none' + description:
Add 'none' to payloadSchema.type — handler still fires; logs warn
if msg.payload is non-empty (catches accidental passes). Add
optional `description` field per descriptor; surfaced via .list()
so wikiGen can render per-topic effect text.
commandRegistry.js 157→164 lines. 23/23 tests pass.
B3.3 UnitPolicy dual-shape:
policy.canonical/output/curve are now BOTH callable methods AND
frozen property bags. policy.canonical('flow') === 'm3/s' and
policy.canonical.flow === 'm3/s' both work. Property bags are
frozen (assign/delete/redefine throw in strict). Drops the
_unitView workaround in MGC + rotatingMachine specificClass.
UnitPolicy.js 149→163 lines, 15/15 tests pass.
CONTRACTS.md §4 + §6 updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- package.json test script now covers test/ recursively + nrmse/errorMetric.
- Removed 5 broken test files that used Mocha-style describe()/it() globals
with no test runner installed. All 5 have working kebab-case node:test
equivalents (e.g. childRegistration.test.js → child-registration-utils.test.js).
- 4 remaining pre-existing assertion failures in output-utils + validation-utils
logged in OPEN_QUESTIONS.md for Phase 10.5.
166/170 tests pass (4 known pre-existing failures).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests 1, 4, 5 constructed an Adapter with the default
statusInterval=1000 and no mock for setInterval, leaking a real
status timer that held the event loop open past the assertions.
Single-file runs masked it; node --test test/basic/ blocked the
whole runner.
Fix: set static statusInterval = 0 + invoke node.handlers.close()
(or mock setInterval where the test asserts on registration timing).
113/113 basic tests pass in batch in ~400 ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/nodered/BaseNodeAdapter.js — base class for every nodeClass.js
Lifecycle: config build → domain instantiate → child.register on
Port 2 → tick (opt-in) or 'output-changed' subscription (default
event-driven) → status updater → input dispatch via commandRegistry →
close handler with clean teardown.
- index.js — additive exports of all Phase 1 modules:
UnitPolicy, ChildRouter, LatestWinsGate, HealthStatus, BaseDomain,
statusBadge, StatusUpdater, createRegistry, CommandRegistry,
BaseNodeAdapter, stats. Existing exports unchanged.
113 unit tests pass under node:test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/domain/BaseDomain.js — base class for every specificClass; wires emitter/config/logger/measurements/childRouter
- src/nodered/commandRegistry.js — declarative msg.topic dispatch with alias deprecation
- src/nodered/statusUpdater.js — 1Hz status badge poller with error-resilient loop
Additive. 43 new tests; all 99 basic tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds platform infrastructure used by the upcoming refactor of
nodeClass / specificClass across all 12 nodes:
- src/domain/UnitPolicy.js — extracted from rotatingMachine/MGC
- src/domain/ChildRouter.js — declarative event routing on top of childRegistrationUtils
- src/domain/LatestWinsGate.js — extracted from MGC dispatch gate
- src/domain/HealthStatus.js — standardised {level, flags, message, source}
- src/nodered/statusBadge.js — compose / error / idle / byState / text helpers
- src/stats/index.js — mean / stdDev / median / mad / lerp
All additive — no existing exports change shape.
56 unit tests pass under node:test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace hardcoded position strings with POSITIONS.* constants
- Prefix unused variables with _ to resolve no-unused-vars warnings
- Fix no-prototype-builtins with Object.prototype.hasOwnProperty.call()
- Extract menuUtils.js (543 lines) into 6 focused modules under menu/
- menuUtils.js now 35 lines, delegates via prototype mixin pattern
- Add 158 unit tests for ConfigManager, MeasurementContainer, ValidationUtils
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>