reactor.json declared `timeStep.unit = "h"` and `default = 0.001`, but:
- reactor.html labels the field [s] and defaults to 1.
- baseEngine.js line 40 converts via (1/86400) — seconds-per-day —
meaning the engine internally treats the input as seconds.
A reader trusting the schema would have entered an hours value; the
engine would then have run the integrator at 1/3600× the intended step,
silently producing wrong rates.
Schema now matches the actual contract: `unit = "s"`, `default = 1`,
`min = 0.001` (1 ms minimum). Description block calls out the
seconds→days conversion so future readers don't need to dig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Different shape from per-node CONTRACT.md files: nodes contract on msg.topic,
this library contracts on what require('generalFunctions') exports. Every
export tagged stable/experimental/deprecated, grouped by concern, with
pointers to .claude/refactor/CONTRACTS.md §N in the EVOLV superproject for
the deep platform-shape specs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* iconHelpers.js (new): shared SVG library + renderSelectPicker /
renderToggle helpers, injected once per editor session by MenuManager.
Pulls the visual layer out of machineGroupControl so every node that
loads /<node>/menu.js inherits the cards without per-node code.
* logger.js, physicalPosition.js: new initVisuals() step that upgrades
the native checkbox + select to icon cards using the shared helpers.
Native controls stay in the DOM (hidden) as the save targets.
* asset.js: rewrite the asset selector into a left->right wizard —
chip strip (Supplier > Type > Model > Unit), per-stage type-to-filter
combobox, node-aware spec strip + curve mini-chart sparkline. Models
are server-side enriched with a slim previewCurve per softwareType
(rotatingMachine Q-H, valve Cv, diffuser SOTE; measurement has no
curve data yet). Hidden native selects remain canonical save targets.
* MenuManager: each menu's initEditor now owns its own initVisuals
call so async-data menus (asset) can sequence visuals after loadData.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- configs/machineGroupControl.json: drop prioritypercentagecontrol mode
(unused — set.demand became unit-self-describing, so percentage-vs-absolute
is decided per-message, not by a node-wide scaling mode). Add output.process
/ output.dbase enums + functionality.distance{,Unit,Description} so the
editor's distance offset persists. Fixes the runtime warnings 'Unknown key
optimization/scaling/movement/curvePressureUnit etc.' the validator was
logging on every MGC instantiation.
- configs/measurement.json: same output.process/dbase block + nullable
position.x for the rare case a measurement has no parent yet.
- datasets/assetData/machine.json -> rotatingmachine.json: rename so
AssetMenu's softwareType lookup matches. AssetMenu.getActiveCategoryKey
no longer silently falls back to keys[0] (which mis-showed diffuser models
for rotatingMachine nodes) — returns null with a console.warn instead.
- menu/asset.js: re-derive supplier/assetType from saved model id on reopen.
The save handler intentionally discards the denormalized registry copies
to keep the persisted node small, so the cascade dropdown booted at
'Select...' even when a model was saved. Walk the registry tree to
reconstitute.
- predict/predict_class.js: minor.
- configs/index.js: minor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously each curve file stored x in whatever convention the vendor
used (per-element Nm³/h for Jäger/GVA, per-element Sm³/h for PIK/PRK,
per-m² for Aerostrip). That meant the diffuser physics couldn't read
the data uniformly — selecting Aerostrip vs Jäger would feed wildly
different axes to the same interpolator.
All five curves now use the same canonical X: specific air flux in
Nm³/(h·m² membrane). Y stays SSOTR in g O₂/(Nm³·m); coverage % is the
parametric key.
Per-file conversions:
- jaeger-jetflex-td-65-2-g-epdm-1000: x divided by 0.18 m² perforated
area (stated on the data sheet).
- gva-elastox-r: x divided by 0.18 m² placeholder mirroring Jäger
TD-65 (no real GVA sheet exists — see _meta note).
- aerostrip-phoenix: native already in Nm³/(h·m²) — no x change. _meta
area normalised to 1.0 m² per "element" so users set `elements` =
total installed membrane area in m².
- pik300, prk300 (Sulzer ABS 300 mm disc): native was Sm³/h/disc on
X and g O₂/(Sm³·m) on Y. Converted to canonical Nm³ basis (DIN-1343)
by X × 0.9319 / 0.07, Y × 1.0732. Each disc = 0.07 m² membrane.
Supplier naming fixed: pikprk → sulzer, aquaconsult → aquaconsult-entec.
PIK = perforated EPDM, PRK = perforated PUR.
Config schema: new diffuser.membraneAreaPerElement field (nullable,
default null) so a node can override the curve's stored area. When
null, specificClass reads _meta.membraneArea_m2_per_element from the
resolved curve.
246/246 generalFunctions tests still pass.
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>
Schema defaults now match a realistic 50 m³ × 4 m basin (volume=50,
height=4, inflowLevel=1.5, overflowLevel=3.8, maxLevel=3.8, minLevel=0.3).
stopLevel default stays null so the hysteresis remains opt-in for
programmatic configs (per levelBased.js:111-113); the editor HTML supplies
0.5 m as the drag-in default.
startLevel description corrected: the ramp foot is inflowLevel, not
startLevel — demand stays at 0 % across [startLevel, inflowLevel] and
scales 0 → 100 % across [inflowLevel, maxLevel]. The previous text claimed
[startLevel, maxLevel], contradicting the runtime.
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>
outputUtils.formatMsg: fall back to \`<softwareType>_<id>\` when
config.general.name is unset — restores the convention the original
test expected; safer for nodes whose name isn't required at registration.
collectionValidators.validateArray + validateSet: replace \`|| 1\` with
\`?? 1\` so explicit minLength: 0 lets through empty arrays. Was
swallowing the 0 as falsy and clamping minimum to 1.
validationUtils: add public validateCurve wrapper around the helper so
callers can validate raw curves without going through validateSchema.
Test suite: 170 total / 170 pass (was 4 fail).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two subcommands consumed by each node's package.json wiki:* scripts:
contract <commands.js> — generates the topic-contract markdown table
datamodel <specific.js> — instantiates the domain + walks getOutput()
Both can splice between <!-- BEGIN/END AUTOGEN: topic-contract|data-model -->
markers in a target file via --write <path>, or print to stdout.
First consumer: pumpingStation (wired in its package.json wiki:contract
+ wiki:datamodel + wiki:all). Other 11 nodes will wire when each gets
its first wiki page.
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>
The P6.4 diffuser refactor reads headerPressure, localAtmPressure,
waterDensity, and zoneVolume out of the config. validateSchema strips
unknown keys, so without these definitions the values fell out of the
config object before specificClass could read them. Added with
sensible defaults that match the pre-refactor inline constants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 243-line legacy AssetMenu was retained for backwards compatibility
but no code in the refactored platform references it. Removed.
loadCurve removal stays deferred — rotatingMachine + valve still call
it through src/curves/curveLoader.js and src/curve/supplierCurve.js.
Migration to loadModel is a follow-up after the platform refactor lands
on main.
113/113 basic tests pass.
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>
When MGC's per-tick abortActiveMovements parks the FSM in
'accelerating'/'decelerating' to avoid a bounce loop, a subsequent
moveTo previously fell into the early-return path and saved the new
setpoint to delayedMove — which never fired because nothing transitioned
back to 'operational'. Now distinguish residue states from genuine
non-operational states (starting/warmingup/...) and force-transition
out of residue so the new setpoint actually executes. Also picks up
in-flight predict shareInputsFrom plumbing and pumpingStation.json
stopLevel doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
generalFunctions has no production deps of its own, so any
package-lock.json found here is a stub from a stray `npm install`
inside the submodule directory. Don't track it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A read chain `.child(name).getCurrentValue()` previously fell through
silently to the implicit-default child or the first available sibling
when the named child did not exist. Caller asked for X, got Y, no
warning. Surfaced via pumpingStation spillPrev: a fresh basin's
.child('overflow').getCurrentValue() returned the value of
'manual-qout' (the only existing child at that position).
Split the resolution into two strictness levels:
_currentChildId (per-chain .child(name)) → STRICT, missing = null.
this.childId (persistent setChildId) → HINT, falls back to
'default' then first.
The persistent path is what registered children (rotatingMachine etc.)
rely on: they write under composed ids ('up-<id>') but expect reads
without explicit .child() to still resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- control.levelbased.shiftArmPercent (default 95): output % threshold that
arms the shift on the way up. Once armed, the up-curve % at the
filling→draining transition becomes the held value, kept until level
drops to shiftLevel; from there it ramps to 0 % at startLevel.
- shiftLevel description updated — it is no longer the arming trigger,
it's the level at which the held output begins ramping down.
- MeasurementContainer.js: prominent doc block on the class plus a
JSDoc on getFlattenedOutput documenting the `${type}.${variant}.
${position}.${childId}` flatten format and the implicit 'default'
childId convention. This was the #1 footgun for new dashboard
consumers — the comments now make the rule impossible to miss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the 5-threshold convention (dryRunLevel, minLevel, startLevel,
maxLevel, overflowLevel) introduced in the pumpingStation wiki:
basin.heightInlet → basin.inflowLevel
basin.heightOutlet → basin.outflowLevel
basin.heightOverflow → basin.overflowLevel
control.levelbased.stopLevel → control.levelbased.minLevel
control.levelbased.maxFlowLevel → control.levelbased.maxLevel
control.levelbased.minFlowLevel → removed (redundant with startLevel)
control.levelbased.startLevel → unchanged
Description strings tightened to reference the semantic role instead
of generic "min level to scale flow" prose.
Breaking change for existing saved flows. Ties in with pumpingStation
commit a218945 which updates the consumer code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The unconditional transition to 'operational' after every movement abort
caused a bounce loop when MGC called abortActiveMovements on each demand
tick: abort→operational→new-flowmovement→abort→operational→... endlessly.
Pumps never reached their setpoint.
Fix: abortCurrentMovement now takes an options.returnToOperational flag
(default false). Routine MGC aborts leave the pump in accelerating/
decelerating — the pump continues its residual movement and reaches
operational naturally. Shutdown/emergency-stop paths pass
returnToOperational:true so the FSM unblocks for the stopping transition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The schema default for machineCurve.nq had a dummy pressure slice at
key "1" with x=[1..5] y=[10..50]. configUtils.updateConfig deep-merges
defaults into the real config, so this fake slice survived alongside the
real pressure slices (70000, 80000, ..., 390000 Pa). The predict class
then included it in its pressure-dimension spline, pulling all
interpolated y-values toward the dummy data at low pressures and
producing NEGATIVE flow predictions (e.g. -243 m³/h) where the real
curve is strictly positive.
Fix: default to empty objects {nq: {}, np: {}} so the deep merge adds
nothing. The validateMachineCurve function already returns the whole
default if the real curve is missing or invalid, so the empty default
doesn't break the no-curve-data path — it just stops poisoning the
real curve data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The MGC and pumpingStation registerChild handlers dispatch on
softwareType === 'machine' / 'machinegroup' / 'pumpingstation' /
'measurement'. But buildConfig sets functionality.softwareType to the
lowercased node name, so in production rotatingMachine reports
'rotatingmachine' and machineGroupControl reports 'machinegroupcontrol'.
Result: the MGC <-> rotatingMachine and pumpingStation <-> MGC wiring
silently never hit the right branch in production, even though every
unit test passes (tests pass an already-aliased softwareType manually).
Fix: tiny SOFTWARE_TYPE_ALIASES map at the central registerChild
dispatcher in childRegistrationUtils. Real production names get
translated to the dispatch keys parents already check for, while tests
that pass already-aliased keys are unaffected (their values aren't in
the alias map and pass through unchanged).
rotatingmachine -> machine
machinegroupcontrol -> machinegroup
Verified end-to-end on Dockerized Node-RED: MGC now reports
'3 machine(s) connected' when wired to 3 rotatingMachine ports;
pumpingStation registers MGC as a machinegroup child and listens to
its predicted-flow stream.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reproduction (any node using assetMenu — measurement, rotatingMachine,
pumpingStation, monster, …):
open node -> pick Vega supplier -> pick Pressure type
-> model dropdown stays "Awaiting Type Selection"
Root cause: two interacting bugs in the chained dropdown wiring.
1. populate() inside both wireEvents() and loadData() auto-dispatched a
synthetic 'change' event whenever the value of the rebuilt <select>
differed from before the rebuild. That meant rebuilding 'type' inside
the supplier change handler could fire the *type* change handler
mid-way through, populate the model dropdown, and then return — only
for the supplier handler to continue and unconditionally call
populate(elems.model, [], '', undefined, 'Awaiting Type Selection'),
wiping the model dropdown back to empty.
2. loadData() ran the same auto-dispatch path, so on initial open of a
saved node the synthetic change cascaded through wireEvents listeners
AND loadData's own sequential populate calls double-populated each
level. The visible state depended on which path won the race.
Fix: convert the chain to an explicit downward cascade.
- populate() no longer dispatches change events. It simply rebuilds the
<select> with placeholder + options and assigns the requested value.
- New cascadeFromSupplier / cascadeFromType / cascadeFromModel helpers
read the *current DOM value* of each upstream <select>, look up the
matching item in menuData, and rebuild the next level — then call the
next cascade explicitly. Order is now deterministic and the parent
handler can never wipe the child after the child was populated.
- Each <select>'s native 'change' listener is just the corresponding
cascade function. Same code path runs for user picks AND for initial
load, so saved-node restore behaves identically to a fresh pick.
- The cascades are exposed under window.EVOLV.nodes.<name>.assetMenu._cascade
so loadData (or future sync code) can re-run them after async data
arrives without duplicating logic.
No new DOM dependencies, no test framework changes. Existing
generalFunctions tests still 52/61 (same 9 pre-existing failures
unrelated to this change).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MeasurementContainer.isUnitCompatible now short-circuits to accept any unit
when the measurement type is not in the built-in measureMap. Known types
(pressure, flow, power, temperature, volume, length, mass, energy) still
validate strictly. This unblocks user-defined types in the measurement
node's new digital/MQTT mode — e.g. 'humidity' with unit '%', 'co2' with
'ppm' — without forcing those units into the convert-module unit system.
measurement.json schema: add 'mode.current' (analog | digital) and
'channels' (array) so the validator stops stripping them from the runtime
config. Ignored in analog mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
state.js: When moveTo catches a 'Movement aborted' or 'Transition aborted'
error, transition the FSM back to 'operational'. This ensures a subsequent
shutdown or emergency-stop sequence is accepted — previously the FSM stayed
stuck in 'accelerating'/'decelerating' and rejected stopping/idle
transitions, silently dropping shutdown commands issued mid-ramp. Also
emits a 'movementAborted' event for observability.
rotatingMachine.json: Add schema entries for functionality.distance,
functionality.distanceUnit, functionality.distanceDescription, and top-level
output.{process,dbase}. These keys are produced by buildConfig / the HTML
editor but were previously stripped by the validator with an
'Unknown key' warning on every deploy.
configs/index.js: Trim buildConfig so it no longer unconditionally injects
distanceUnit/distanceDescription — those keys are rotatingMachine-specific
and would otherwise produce Unknown-key warnings on every other node.
Verified via Docker-hosted Node-RED E2E: shutdown from accelerating now
reaches idle; emergency stop from accelerating reaches off; 0 Unknown-key
warnings in container logs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
At pressures 1600, 3200, and 3300 mbar, flow values had leaked into the
np (power) section. Replaced with linearly interpolated values from
adjacent pressure levels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
migrateConfig stamps a version string into config schemas. validateSchema
then iterates the string's character indices, causing infinite recursion.
Skip the 'version' key and guard against any non-object schema entries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConfigManager.migrateConfig() was called but never defined — would crash at runtime.
Added config version checking, migration support, and fixed createEndpoint indentation.
New formatters module (csv, influxdb, json) for pluggable output formatting.
Co-Authored-By: Claude Opus 4.6 <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>
Break the 548-line monolith into focused modules:
- validators/typeValidators.js (number, integer, boolean, string, enum)
- validators/collectionValidators.js (array, set, object)
- validators/curveValidator.js (curve, machineCurve, dimensionStructure)
validationUtils.js now uses a VALIDATORS registry map and delegates to
extracted modules. Reduced from 548 to 217 lines.
Closes#2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ConfigManager.buildConfig() now lowercases softwareType
- Updated config JSON defaults to lowercase
- childRegistrationUtils lowercases softwareType on extraction
- Closes#8
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New baseConfig.json: shared schema for general/logging/functionality/asset sections
- ConfigManager.buildConfig(): builds runtime config from UI inputs + domain overrides
- Eliminates the need for each nodeClass to manually construct base config sections
- All nodes can now use: cfgMgr.buildConfig(name, uiConfig, nodeId, domainConfig)
Closes#1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>