Commit Graph

18 Commits

Author SHA1 Message Date
znetsixe
dfaa0c3ae8 feat(pumpingstation): warn when control engages with no machine group registered
A station engaged above startLevel computes a real demand, but if no machine
group is registered (e.g. the Port 2 parent↔group registration was dropped by a
partial redeploy) the demand is silently forwarded nowhere and the pumps never
react — invisible to the operator. levelBased now warns once when engaged with
an empty machineGroups map (throttled via host._warnedNoMachineGroup, re-arms
when a group reappears); manual.forwardDemand warns when neither a group nor a
direct machine is registered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:58:34 +02:00
znetsixe
6e727d929b fix(pumpingstation): replay child measurement value on subscribe
A measurement child that already holds a value when the pumpingStation
registers it (e.g. a once:true inject that fired during startup before the
parent subscribed) was never surfaced — the emitter only delivers future
updates. _subscribeMeasurement now seeds from the child's current sample via
getLaggedSample(0), so late subscribers pick up present state. This is what
makes a measured upstream inflow register as inflow on a clean startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:45:44 +02:00
znetsixe
2d68a4f504 test: rewire integration test to renamed 02-Dashboard.json
Example flows were renamed to the numbered-tier convention
(02-Dashboard.json). The integration test still loaded the old
basic-dashboard.flow.json and asserted the old 6-output parser shape
+ raw-number payloads. Update both the filename and the assertions
to match the current 14-output fn_status_split (topic labels like
'Level', payload strings like '3.25 m').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:30:02 +02:00
znetsixe
a3536b7b7f fix(level): pass timestamp on level samples for level-rate fallback
MeasurementRouter.onLevelMeasurement was writing level samples via
.value(value).unit(context.unit), which dropped the timestamp. The
level-rate fallback in FlowAggregator derives netFlow from dlevel/dt,
so without a timestamp on each sample it had nothing to differentiate.

Switch to the positional .value(value, timestamp, unit) form so the
fallback works. Add a basic test that drives two level samples 2 s
apart and asserts the aggregator produces direction=filling with a
finite dlevel/dt-derived netFlow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:29:56 +02:00
znetsixe
f5c6282478 refactor(units): use UnitPolicy.convert instead of hardcoded m3/h<->m3/s scalars
Replace the M3H_TO_M3S constant in control/manual.js and the `* 3600`
inline conversion in the status badge with this.unitPolicy.convert
calls. Expose unitPolicy on the frozen control context so manual
strategies pick it up without reaching into host. Matches the
contract direction in .claude/refactor/CONTRACTS.md §6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:43:35 +02:00
znetsixe
2e4ad8d3f1 fix(levelBased): drop hold zone, route through MGC.setDemand, add holdLevel + integrator variant pick; slim npm pack
levelBased ramp + engagement:
- Ramp foot is now max(startLevel, holdLevel) — was max(startLevel,
  inflowLevel). inflowLevel is basin geometry, not a control setpoint;
  the implicit hold zone it created was causing pumps to "start at
  inflowLevel" instead of startLevel.
- New optional `holdLevel` config (defaults to startLevel = no hold band).
  When raised, pumps engage at startLevel and hold at 0 % = MGC flow.min
  across [startLevel, holdLevel], then ramp 0..100 % to maxLevel.
- Engagement decided in run() (not in `_applyMachineGroupLevelControl`):
  rising-edge hysteresis arming gates a clean turnOff early-return.
  Once armed, the helper always forwards setDemand(pct, '%') — 0 %
  legitimately means "engaged at min flow", no more soft-turnOff at
  the boundary.
- Disengagement paths (minLevel hard-stop, stopLevel falling-edge,
  pre-arming idle) now all clear the shifted-ramp hysteresis state too.
- Threshold validator drops the startLevel ≤ inflowLevel rule; adds
  startLevel ≤ holdLevel < maxLevel (only checked when holdLevel is
  explicitly set, so default-null doesn't false-flag).

MGC unit math:
- Replace direct group.handleInput(percent) with group.setDemand(pct, '%')
  in _applyMachineGroupLevelControl. The percent → m³/s resolution now
  lives in MGC.setDemand (committed separately in the MGC submodule).

FlowAggregator variant picking:
- New _pickFlowSum() helper mirrors selectBestNetFlow's variant
  precedence (measured first, then predicted) and resolves each side
  independently. Realistic mixed case — real measured upstream sensor +
  predicted pump outflow — now feeds the predicted-volume integrator.
  Was reading only `flow.predicted.*` so a real upstream sensor
  (which writes `flow.measured.*`) never moved the level.

Editor:
- New `holdLevel` and `deadZoneKeepAlivePercent` defaults + side-panel
  input rows in the levelbased mode preview.
- Add the missing `ps-mode-line-holdLevel` SVG marker (was declared in
  the side-panel coupling but the SVG element didn't exist, so the
  dashed line never rendered).
- Relax stopLevel marker gate so it renders for any non-negative typed
  value — start/stop ordering is the ribbon's job, not the marker's
  (was hiding the line whenever startLevel was momentarily smaller).
- Add holdLevel to the marker loop in mode-preview so changes track.
- Add stopLevel + holdLevel + maxLevel to all three bindRedraw lists
  (basin-diagram, mode-preview, bounds.apply) so the SVG, validation
  ribbon, and HTML5 min/max attrs update on every edit.
- Initialise stopLevel + holdLevel + deadZoneKeepAlivePercent inputs
  in oneditprepare so reopening the editor shows the saved values.
- nodeClass passes holdLevel + deadZoneKeepAlivePercent into the
  domain config.

Tests:
- New test/basic/_probe_upstream_emit.test.js: confirms the parent
  surfaces flow.measured.upstream.* on Port 0 after a measurement
  child write — pins the previously-invisible measured variant flow.
- flowAggregator.basic.test.js: two new regression cases — measured
  inflow when predicted side is empty, and the measured-in /
  predicted-out mixed case.
- control-levelBased.basic.test.js: new cases for the holdLevel hold
  band, the [stopLevel, startLevel] keep-alive, the engagement gate,
  and the "0 % at startLevel = setDemand" contract.
- specificClass.test.js: zone tests adjusted to the new ramp foot.
  Shifted-ramp tests pin holdLevel = 3 explicitly so their legacy
  arithmetic (ramp foot at inflowLevel) stays self-consistent.
- shifted-ramp-end-to-end.test.js: same holdLevel pin for the same
  reason.

Packaging:
- Add .gitignore + .npmignore so the published tarball drops the
  wiki/, simulations/, test/, tools/, .claude/ etc. The pack went
  from 1.5 MB (72 files) to ~57 KB (30 files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:36:29 +02:00
znetsixe
5f1c9ae2ff P11.5 + B2.1/B2.2: per-command units + description (where applicable)
Adds  to scalar setters whose payloads are
plain numbers OR {value, unit}. Skipped where payload is compound or
mode-dependent (control-%, {F, C: [...]}, etc.) — documented inline.
Every command gains a description field for wikiGen consumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:41:07 +02:00
znetsixe
ef81013e96 B1.2: drop legacy 'overfillLevel' alias from thresholdValidator
Decision 2026-05-11: 'highVolumeSafetyLevel' is canonical. The legacy
'overfillLevel' name is gone from computeSafetyPoints + the validator
issue tuple. 'overfillVol' parallel alias kept (out of scope for this
task; flagged for follow-up). 130/130 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:13:21 +02:00
znetsixe
e991ea64ef Merge origin/basin-docs-update: per-mode SVG + stopLevel hysteresis + shifted ramp
Reconciles the 7-commit basin-docs-update feature branch (which never
landed on main before the platform refactor) with the post-refactor
architecture on development. Each basin-docs feature ported into the
relevant concern module:

  control/levelBased.js
    - stopLevel Schmitt-trigger + dead-band keep-alive
    - Shifted ramp (arm % → hold @ 100% → ramp down to shiftLevel)
    - Linear vs log up-curve (curveType + logCurveFactor)

  measurement/flowAggregator.js
    - Predicted-volume overflow clamp + spill flow stream
    - Cumulative overflowVolume + underflowVolume
    - Hard floor at 0 + dry-run-on-transition handling

  basin/thresholdValidator.js
    - computeSafetyPoints exposes dryRunLevel + highVolumeSafetyLevel
    - startLevel ≤ inflowLevel invariant added

  measurement/calibration.js + commands/
    - Manual q_out path (set.outflow / q_out alias)

  safety/safetyController.js
    - Accepts both legacy + new high-volume threshold names

UI:
  pumpingStation.html — restored the side-panel + SVG mode-preview block,
  added defaults for stopLevel/shiftLevel/shiftArmPercent/levelCurveType/
  logCurveFactor/enableShiftedRamp.
  src/editor/* — basin-docs' 7-file modular editor (replaces single
  src/editor.js, which is deleted).
  pumpingStation.js — admin endpoint serves editor/:file.

Tests: 130/130 pass (125 basic + 5 integration). Two basin-docs test
files added: nodeClass-config.test.js, basic-dashboard-flow.test.js,
shifted-ramp-end-to-end.test.js. One pre-refactor control-levelBased
test adapted to match basin-docs canonical "no-shutdown in dead zone"
behaviour.

Human-review items (see commit context):
  - rampFoot = inflowLevel (matches basin-docs test); basin-docs source
    used rampFoot = startLevel. Domain owner: confirm intent.
  - Naming kept dual (overfillLevel + highVolumeSafetyLevel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:19:55 +02:00
znetsixe
7afcd6e54a P2 wave 1: extract concerns from pumpingStation specificClass
Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.

  src/basin/         BasinGeometry + thresholdValidator (pure)
  src/measurement/   flowAggregator + measurementRouter + calibration
  src/control/       levelBased + flowBased(stub) + manual + index dispatcher
  src/safety/        safetyController split into dryRun + overfill rules
  src/commands/      registry array + handlers (canonical names from start)
  src/editor.js      260 lines of SVG basin-diagram redraw, was inline in .html
  examples/standalone-demo.js  was if(require.main===module) at bottom of specificClass.js
  CONTRACT.md        canonical inputs + outputs + emitted events

Modified:
  src/specificClass.js  removed the 170-line standalone demo block
  pumpingStation.html   oneditprepare/oneditsave delegate to editor.{init,save}
  pumpingStation.js     added admin endpoint serving src/editor.js

102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:18:49 +02:00
Rene De Ren
6ab585bcc2 Docs + simulations refresh; align spill-flow keys with new position
- wiki/functional-description.md: rename Overfill Protection → High-volume
  Safety; tighten basin-ordering chain; relocate level-based mode
  diagrams under wiki/diagrams/modes/level-based/; document the new
  flow.predicted.overflow.default position (replaces the previous
  child='overflow' under position 'out'); add underflowVolume +
  predictedUnderflowVolume entries.
- wiki/modes/{levelbased,powerbased}.md: paragraph cleanups.
- wiki/diagrams: move level-linear basin diagram under modes/level-based/
  alongside a new level-log variant.
- simulations/run.js: add max_demand_gt expectation.
- simulations/scenarios/*: minor fixture updates.
- test/basic/nodeClass-config.test.js: new config-shape coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:23:20 +02:00
Rene De Ren
d8490aa949 Predicted-volume hard-floor at 0 + spill flow position refactor
Volume integrator changes:
- Hard physical floor at 0 added to _updatePredictedVolume. Without
  it, a basin seeded below dryRunSafetyVol (calibration / startup
  / low seed) under continued net-outflow drifted volume arbitrarily
  negative; the level output looked clamped only because
  _calcLevelFromVolume floors at 0, masking the underlying drift.
- New cumulative diagnostic: underflowVolume.predicted.atequipment
  (m³) + getOutput().predictedUnderflowVolume. Non-zero indicates a
  flow-balance error (over-reported outflow / missing inflow).
- The transition-only dryRunSafetyVol clamp is preserved so
  startup-from-empty doesn't snap to 2.1 m³ on tick 1.

Spill flow refactor (taxonomic + bug fix):
- Synthetic spill moved from flow.predicted.out.<child='overflow'>
  to its own position flow.predicted.overflow.<default>. The spill
  is a derived quantity, not a physical sub-source sharing a position
  with pumps — .child() was the wrong knob.
- Removes the spillPrev self-subtraction in the integrator (no longer
  needed: outflowTotal at ['out','downstream'] cleanly excludes spill).
- Closes a latent fall-through bug exposed during this work:
  .child('overflow').getCurrentValue() returned the value of any
  available sibling child when overflow itself didn't yet exist.
  Hardened separately in generalFunctions@a516c2b.
- _selectBestNetFlow folds the overflow position into the outflow
  side so the predicted net-flow balance still reads ~0 while pinned.

Tests: 70/70 pass. 4 new subtests cover the 0-floor, accumulated
underflow tracking, getOutput surface, and refill-from-empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:18:23 +02:00
Rene De Ren
6b46a8a8f0 Predicted-volume overflow clamp + spill tracking
Predicted volume is now clamped to [dryRunSafetyVol, maxVolAtOverflow]
in _updatePredictedVolume — the integrator can no longer drift above
the weir crest (only a real measurement can show level > overflow,
e.g. inflow exceeding pump+weir capacity). Excess is recorded as:

  - overflowVolume.predicted.atequipment.default — cumulative spill (m3)
  - flow.predicted.out.overflow — instantaneous spill rate (m3/s),
    registered as a synthetic outflow so net-flow balance reads ~0
    while pinned. The integrator subtracts the prior tick's synthetic
    flow before integrating so it never feeds back into volume math.

Lower clamp at dryRunSafetyVol fires only on the transition — a low
seed/calibration is left alone; inflow is what brings it back up.

_selectBestNetFlow holds the last non-zero level-rate net flow when
level pins at overflowLevel and dL/dt collapses to 0, so dashboards
keep showing roughly what's coming in. Auto-refreshes once level
drops.

getOutput() exposes predictedOverflowVolume + predictedOverflowRate
as top-level convenience keys; the underlying measurements flow to
InfluxDB via the standard MeasurementContainer flatten path.

9 new test assertions cover the upper-clamp + spill increment, stable
spill across ticks, net-flow ~0 while pinned, spill clearing when
inflow stops, low-seed left alone, drain-across-threshold clamp, and
the new top-level output keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:47:46 +02:00
Rene De Ren
de9a79b888 Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
  hold-then-ramp hysteresis driven by output %, not level:
  • Up-curve % crosses shiftArmPercent on the way up → ARM.
  • Filling→draining transition while armed → capture the up-curve %
    at that moment as _shiftHoldValue.
  • Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
    (horizontal hold, matching the dashed segment in the SVG).
  • Draining + level in [start, shift] → output ramps holdValue → 0 %
    along the same curve shape (linear or log) as the up curve.
  • Draining + level < startLevel → 0 % AND disarm.
  • Returning to filling clears holdValue, stays armed; next drain
    transition captures a fresh hold so bouncing fills rearm cleanly.
  • Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
  _updateShiftArmed in favour of the inline state machine.

Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.

Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
  (only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
  this is the "% Threshold triggering shifted ramp down" line from
  the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
  100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
  startLevel down to 0 %, OFF below startLevel. Preview shows the
  worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
  preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
  current value is missing or out of range.

Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
  MeasurementContainer flatten format includes the implicit 'default'
  childId; consumers must include it. Comment in the parser points
  at the documenting source in generalFunctions.

Tests:
- test/basic: replace old level-armed-shift tests with two new ones
  that exercise the hold-then-ramp arming, capture, hold, ramp-down,
  disarm, and the bounce case (filling→draining→filling→draining
  captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
  Q_IN/Q_OUT through the full runtime tick with a controllable clock,
  asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
  to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
Rene De Ren
8a6ca1baeb Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
  Arms when level rises past shiftLevel; disarms when level drops below
  startLevel. While armed, ramp foot moves to startLevel and ramp top
  to shiftLevel — both ends shift left, then saturate at 100 % up to
  maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
  the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.

Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.

Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
  basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
  oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
  flat from start→inlet, ramp inlet→max, optional shifted-down curve
  start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
  overflow), level markers (dryRun derived, start, inlet, max, shift,
  overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
  always visible.
- All level inputs moved to a side panel left of each diagram, color-
  coded to match line strokes; hover-couple highlights the paired SVG
  line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
  pipelineLength, maxDischargeHead, staticHead, defaultFluid,
  maxInflowRate, temperatureReferenceDegC,
  timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
  outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
  side panel).

Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
  trio so the basin can be exercised end-to-end without a connected
  rotating-machine downstream.

Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
  hold-zone behaviour and shift-armed/disarmed transitions through
  shiftLevel and startLevel boundaries. 53/53 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
znetsixe
016433abe6 Add threshold guardrails, fix calibratePredictedLevel bug, rewrite tests
### Guardrails (specificClass.js)

New _validateThresholdOrdering() runs in the constructor. Checks every
ordered pair of basin + control + derived-safety levels and logs a
warning for each violation; returns the list as this.thresholdIssues
so tests and the eval harness can inspect. Non-fatal — we prefer a
running-but-warned station to a refusal-to-start (availability-first).

Strict invariants (bottom → top):
  0 < outflowLevel < inflowLevel < overflowLevel ≤ basinHeight
  dryRunLevel ≤ minLevel ≤ startLevel < maxLevel ≤ overfillLevel

Uses a list-of-checks pattern rather than a switch — easier to add new
invariants without reflowing cases, and the list itself is readable
documentation.

### Bug fix (specificClass.js)

calibratePredictedLevel was writing the volume value into the LEVEL
slot. Root cause: MeasurementContainer is stateful — its type()/
variant()/position() calls mutate the container's own cursor, so
caching chain references (const levelChain = ...; const volumeChain
= ...) doesn't isolate them. The second cached chain ended up sharing
the state of the last type() call. Rebuilt chains fresh each time,
matching the calibratePredictedVolume pattern that already worked.

### Tests (test/basic/specificClass.test.js)

Ported from Jest to node:test + node:assert — the project's standard
per .claude/rules/testing.md. Deleted the stale test/specificClass.test.js
(tests referenced methods that no longer exist post-rename).

New coverage, 42 passing subtests:
- Basin geometry derivations + minHeightBasedOn
- Level/volume roundtrip
- Threshold guardrails (5 violation cases)
- Direction derivation
- Mode change accept/reject
- Calibration (volume and level paths — catches the bug above)
- Levelbased control zones (STOP / DEAD ZONE / RAMP / saturate)
- getOutput flattening
- setManualInflow

Run with: node --test test/basic/*.test.js

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:38:41 +02:00
znetsixe
a2189457f6 Rename basin/control thresholds to wiki naming; trim stale comments
Aligns the code with the 5-threshold convention used throughout the
wiki (basin model + per-mode transfer-function diagrams):

  heightInlet       → inflowLevel
  heightOutlet      → outflowLevel
  heightOverflow    → overflowLevel
  stopLevel         → minLevel
  maxFlowLevel      → maxLevel
  minFlowLevel      → removed (collapsed into startLevel; they were
                      always supposed to hold the same value)
  minVolIn          → minVolAtInflow
  minVolOut         → minVolAtOutflow
  maxVolOverflow    → maxVolAtOverflow
  startLevel        → unchanged

Config schema (generalFunctions/src/configs/pumpingStation.json) is
updated in a parallel commit in that submodule.

Also:
- Stripped the ~150-line ASCII basin diagram from initBasinProperties
  JSDoc; it now points at wiki/functional-description.md#basin-model.
- Trimmed the top-of-class JSDoc — the config-sections breakdown was
  drifting from the schema anyway; wiki is now the source of truth.
- Tidied inline comments in _controlLevelBased, _scaleLevelToFlowPercent.
- Editor order reshuffled to match the bottom→top basin order:
  minLevel, startLevel, maxLevel.

Breaking change for saved flows: existing pumpingStation nodes in
production flows reference the old field names and will need to be
re-entered in the editor. No compat shim — node is RnD/trial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:13:59 +02:00
Rene De Ren
f01b0bcb19 fix: rename _calcTimeRemaining to _calcRemainingTime + add tests
Fix method name mismatch in tick() that called non-existent _calcTimeRemaining
instead of _calcRemainingTime. Add 27 unit tests for specificClass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:31:47 +01:00