refactor(meter-values): split CoherentMeterValuesGenerator into three modules (issue #1936 item i) (#1938)
* refactor(meter-values): split CoherentMeterValuesGenerator into three modules (issue #1936 item i)
Split the 820 LOC `CoherentMeterValuesGenerator.ts` (introduced by PR
#1935) into three focused modules per issue #1936 item (i):
- `CoherentSampleComputer.ts` (311 LOC): physics chain V→P→I→ΔE→SoC with
INV-1/2/3 by construction (guard bundle, ramp factor, voltage noise,
EVSE/EV/capacity clamps, integer-rounded emission, energy accrual,
monotone SoC saturation), plus `advanceEnergyRegister` which the
builder invokes once per sample so `meterStop` stays correct even when
`Energy.Active.Import.Register` is not in the configured MeterValues.
- `CoherentMeterValueBuilder.ts` (364 LOC): OCPP {@link MeterValue}
assembly — phase family classifier, cross-measurand emit order
(SoC→Voltage→Power→Current→Energy), within-measurand phase rank
(no-phase → L1/L1-N → L2/L2-N → L3/L3-N → L1-L2 → L2-L3 → L3-L1 → N),
kilo/unit divider, template resolution, and the `buildCoherentMeterValue`
entry point that orchestrates `computeCoherentSample` +
`advanceEnergyRegister` + per-template emission.
- `CoherentMeterValuesGenerator.ts` (204 LOC, shrunk from 820): session
lifecycle (`createCoherentSession`, `CreateSessionOptions`), PRNG
helpers (`createStreamPrng` with the `tx:`-namespaced deriveSeed leg,
`resolveRootSeed`), the `isCoherentModeActive` type guard, and the
module-scope WeakMap runtime state (`SessionRuntime`,
`getSessionRuntime`, `disposeCoherentSessionRuntime`) accessed by the
computer.
The three modules stack: Generator (WeakMap + PRNG helpers) ← Computer
(imports `getSessionRuntime`, `createStreamPrng`) ← Builder (imports
`computeCoherentSample`, `advanceEnergyRegister`, `ROUNDING_SCALE`,
`CoherentSample`, `ComputeSampleOptions`). No cycles.
The barrel (`meter-values/index.ts`) re-exports `buildCoherentMeterValue`
+ `BuildVersionedSampledValue` from the builder and
`createCoherentSession` / `isCoherentModeActive` / `resolveRootSeed`
from the generator so external consumers
(`CoherentMeterValuesManager`, `OCPPServiceUtils`, `OCPP16ServiceUtils`)
keep their existing imports.
Preserved invariants (per mission constraints):
- `__injectCoherentSession` NODE_ENV production-guard throw
(unchanged — lives on `CoherentMeterValuesManager`).
- Session-snapshot reads for `voltageOutNominal` / `currentType` /
`numberOfPhases` (still read from `session.*` inside
`computeCoherentSample`).
- `tx:` PRNG namespace on the transactionId leg of `createStreamPrng`
(preserves the XOR self-inverse guard).
- non-finite-`intervalMs` defensive zero-sample guard
preserved verbatim in `computeCoherentSample`.
Test file imports updated to point at the new module boundaries; no test
logic changes. Test invariant `fail: 0, skipped: 6` holds.
Closes issue #1936 item (i).
* refactor(meter-values): align banner and tighten JSDoc on new split modules (issue #1936 i)
Revert the copyright banner on the three files introduced by the split
(CoherentSampleComputer.ts, CoherentMeterValueBuilder.ts, index.ts) from
'Copyright ... 2021-2026' to 'Partial Copyright ... 2021-2025' to match
the sibling verbatim convention (AutomaticTransactionGenerator.ts,
ChargingStation.ts, CoherentMeterValuesManager.ts, PerformanceStatistics.ts).
Wider repo-wide banner year sweep to 2021-2026 is deferred to a separate
chore commit.
Tighten two hedge-worded JSDoc lines in CoherentSampleComputer.ts:
- 'Sample timestamp in milliseconds (typically Date.now())' now spells
out 'callers pass Date.now() in production and a test-controlled clock
in unit tests'.
- Defensive-guard rationale 'a template override or future dynamic
supply could return 0' tightened to 'a template override may set
voltageOut to 0'.
CoherentMeterValuesGenerator.ts is intentionally not touched here to
avoid four-way merge collisions with #1940 (Prng rename), #1941
(physics refinements h), #1942 (RegisterValuesWithoutPhases k), and
#1944 (EVSE-level template fallback g). Design-MAJOR findings (inverted
Computer → Generator dependency, widened intra-package exports,
'Generator' semantic misnomer) are documented in the PR body as a
post-stack-merge follow-up.
Drop the 'Partial' prefix on the three files introduced by the split.
Round-1 aligned to parent-folder siblings (ChargingStation.ts,
AutomaticTransactionGenerator.ts, CoherentMeterValuesManager.ts) which
use 'Partial Copyright ... 2021-2025', but same-folder siblings
(EvProfiles.ts, Prng.ts, types.ts) use 'Copyright ... 2021-2025'
without the prefix. Aligning to same-folder convention restores
banner uniformity inside src/charging-station/meter-values/.
CoherentMeterValuesGenerator.ts (2021-2026 outlier) remains untouched
to avoid the four-way merge collision with #1940 / #1941 / #1942 /
#1944 — repo-wide banner sweep is deferred as documented.
* refactor(meter-values): unidirectional DAG for coherent split (issue #1936 i)
Address the design findings raised by post-split review by relocating
runtime state and PRNG plumbing to their responsibility owners:
- Move SessionRuntime, sessionRuntimes, getSessionRuntime to
CoherentSampleComputer.ts as module-private (they exist only to cache
the voltage-noise PRNG closure across samples, which is a physics
concern, not a session-lifecycle concern).
- Move disposeCoherentSessionRuntime to CoherentSampleComputer.ts
(exported for CoherentMeterValuesManager). Update the manager's
direct import path accordingly.
- Move createStreamPrng to Prng.ts alongside deriveSeed / mulberry32 /
hashLabel. It composes those three helpers and is a PRNG primitive,
not a session helper.
- Rename CoherentMeterValuesGenerator.ts to CoherentSession.ts. After
the moves the file owns only session identity (createCoherentSession,
CreateSessionOptions), the strategy-gate type guard
(isCoherentModeActive), and the root-seed resolver (resolveRootSeed).
The 'Generator' name no longer describes the file — the entry point
buildCoherentMeterValue lives in the builder.
- Align banner on the renamed file to same-folder convention (Copyright
... 2021-2025, matching EvProfiles / Prng / types).
- Update barrel and test imports.
Result: strictly unidirectional import DAG within meter-values/:
CoherentSession no longer depends on any coherent sibling; Computer no
longer depends on Session (the pre-refactor Computer → Session inversion
is eliminated). Three previously-widened intra-package exports revert to
module-private (SessionRuntime, sessionRuntimes, getSessionRuntime);
ROUNDING_SCALE stays exported (Computer → Builder is the correct
direction). Barrel public surface unchanged.
Wire behavior and physics chain byte-identical — the move preserves
every function body verbatim, only relocating them. Test invariant
fail:0, skipped:6 preserved (2925 pass / 2931 total).
The four open PRs touching CoherentMeterValuesGenerator.ts (#1940 c,
#1941 h, #1942 k, #1944 g) will need manual rebase resolution on top
of this refactor — accepted trade-off per the maintainer's directive
to do the design work now rather than defer.
- Retarget the stale '{@link ./CoherentMeterValuesGenerator}' reference
in CoherentSampleComputer.ts @file JSDoc to describe the current state
(physics chain + session-runtime WeakMap + disposeCoherentSessionRuntime
teardown hook) instead of the pre-split historical evolution, per the
AGENTS.md 'document current state; exclude historical evolution' rule.
- Restore the getSessionRuntime JSDoc dropped during the Generator →
Computer move. Documents the load-bearing invariant that the
voltage-noise PRNG closure must be cached across samples so the
stream advances rather than restarting from the same seed each draw.
- Rename CoherentMeterValuesGenerator.test.ts to CoherentMeterValues.test.ts.
Update @file header and describe() label to match the new subject —
the test file now exercises CoherentSession, CoherentSampleComputer,
CoherentMeterValueBuilder, and Prng; no single 'generator' module
remains post-refactor.
- Fix dangling '{@link ../../CoherentMeterValuesManager...}' at
CoherentMeterValueBuilder.ts:287. From src/charging-station/meter-values/,
'../../' resolves to src/ (target does not exist there); correct
path is '../CoherentMeterValuesManager' (src/charging-station/). This
matches the fixes already applied to the sibling occurrences in
CoherentSampleComputer.ts:9 and CoherentSession.ts:37; Builder was
missed during the round-3 sweep.
- Add 'satisfies readonly MeterValueMeasurand[]' to MEASURAND_EMIT_ORDER
in CoherentMeterValueBuilder.ts. Matches the 'as const satisfies
Record<...>' idiom already used by PHASE_FAMILY and PHASE_RANK.
Gates against a typo-introduced non-measurand value at compile time.
- Expand the label-style '// EV acceptance from the curve at running
SoC.' comment in CoherentSampleComputer.ts to document the load-bearing
physics invariant: the taper MUST interpolate at session.socPercent
(live state advanced by advanceEnergyRegister) rather than a constant
or the initial SoC. Without this, a future maintainer could replace
the live read with a constant, breaking the taper (P would not
decrease as SoC rises, over-charging past the capacity clamp,
violating INV-3).
The round-4 physics comment at CoherentSampleComputer.ts:299 stated
that session.socPercent is advanced 'by the previous
advanceEnergyRegister tick', but advanceEnergyRegister (line 180)
only mutates connectorStatus energy registers — it does not touch
session.socPercent. The actual mutation happens at the end of
computeCoherentSample itself (line 354).
Retarget the comment's mutation-site reference to computeCoherentSample
and inline-reference the assignment below. This preserves the
load-bearing invariant that round-4 documented (taper must interpolate
at live SoC, not initial) while pointing the future maintainer to the
correct code location — a maintainer following the wrong pointer to
advanceEnergyRegister would find no session mutation and might
delete the 'stale' comment or replace the live read with a constant,
silently breaking the taper (P would not decrease as SoC rises →
over-charging past capacity clamp → violation of INV-3).
* docs(coherent): strip historical narrative from JSDoc + test prose (issue #1936 i)
Apply AGENTS.md 'document current state; exclude historical evolution'
across the coherent-mode surface (both this PR's meter-values files
and the CoherentMeterValuesManager landed in PR #1937):
- CoherentMeterValuesManager.ts:@file — 'Extracted from ChargingStation'
narrated origin. Rewrite to 'Sits alongside ChargingStation' — same
design rationale (keeping the strictly opt-in coherent surface off
the main class body) without the historical framing.
- CoherentMeterValuesManager.ts:injectSession JSDoc — 'mirroring the
seam previously exposed on ChargingStation' claimed the seam was
no longer on ChargingStation, but __injectCoherentSession is still
a live delegator on ChargingStation (points at manager.injectSession).
Drop 'previously' — the seam mirrors the ChargingStation delegator.
- CoherentMeterValueBuilder.ts:@file — 'Extracted from the original
single-file generator as part of the issue #1936 (item i) file
split to keep each module under the 250 LOC ceiling' was pure
historical narrative; drop the sentence entirely.
- CoherentMeterValueBuilder.ts:resolveTemplates JSDoc — 'tracked as a
follow-up in issue #1936' was TODO-adjacent but redundant with
git-blame trail. Keep the current-state limitation description
(EVSE-level template inheritance needs getEvseStatus on the port).
- meter-values/index.ts:@description — 'Module layout after the
issue #1936 (item i) split:' → 'Module layout:'.
- CoherentMeterValues.test.ts:@file — 'Exercises the split modules
together' → 'Cross-module tests spanning'.
- CoherentMeterValues.test.ts:797 assertion — '(moved to module-scope
runtime state)' → '(owned by module-scope runtime state in
CoherentSampleComputer)'. Same invariant, current-state phrasing.