[P2] Original replay applies planned gas switches that happen mid-tick only at the next recorded sample, so its peak ppO₂, cylinder pressures and tissues differ from the live dive and summary #112
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Priority: P2
Type: bug
Source: full codebase review requested 2026-10-31 (independently reproduced against
2cac7cf)Location:
src/replay/diveReplay.ts:280-306,src/App.tsx:887-905,src/App.tsx:197-211,src/models/zhl16c.ts:170-193,src/simulation/summaryRecording.ts:16-42Problem and evidence
During a live ascent, the engine splits a tick at the exact planned-switch depth (zhl16c.ts gas travel). It loads tissues on the old gas down to the switch depth and on the new gas after it. It also records a gasEvent with the exact time and depth. chargeCylinders then bills each gas's usage to its own cylinder. The App recorder (App.tsx ~887-905) pushes only the end-of-tick DiveSample. That sample is already shallower than the switch depth and already carries the new breathingGas and cylinder. replayDive (diveReplay.ts ~284-306) integrates the whole a->b leg on a.breathingGas and charges it to a.activeCylinderId. The #19 "incoming gas" point is then evaluated only at b.depth. As a result, the reconstruction:
The summary is correct, because recordedProfile adds a peak from dive.gasEvents at the exact switch depth. Replay ignores CompletedDive.gasSwitches and dive.history turn points. The larger the tick (high warp speed, +1:00 steps, background-capped ticks), the larger the divergence. This also breaks the PROJECT_SCOPE acceptance item "replay identical exposure at different speeds".
Suggested correction
Pick one of these:
(a) In the App recorder, when a tick produced new dive.gasEvents (or new dive.history turn points), push a synthetic DiveSample at each event's time and depth before the end-of-tick sample. Each synthetic sample needs the breathingGas in effect and the activeCylinderId chosen by the chargeCylinders rule.
(b) In replayDive, split each a->b leg at the CompletedDive.gasSwitches (and profile turn points) that fall inside it. Integrate and charge the pre-switch part on a's gas and cylinder and the remainder on the switched gas. Evaluate the incoming-gas ppO₂ at the switch depth. This requires storing the cylinder id (and full gas) with each recorded switch.
Acceptance criteria
Regression test: simulate a live dive with an enabled ascent switch (e.g. EAN50 at 21 m or O2 at 6 m) where the switch falls inside a single large tick (e.g. a 1-minute +1:00 step or a 128x tick). Then run replayDive(originalReplay(completed)).
Browser check: the Playwright scenario above must show the same Peak ppO₂ (1.60 ATA) in the summary and in the Original replay column. pnpm verify passes.
Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.