[P3] VPM-B NDL throws on current-state schedule errors (e.g. the 24-hour limit), so replay of very long-deco VPM dives fails entirely while ZHL reports NDL 0 #82
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: P3
Type: bug
Source: full codebase review requested 2026-10-31 (independently reproduced against
2cac7cf)Location:
src/models/vpmb.ts:626-634,src/models/vpmb.ts:657,src/replay/diveReplay.ts:425-428,src/calculation/calculation.worker.ts:23-30,src/models/projection.ts:43-55,src/App.tsx:1720-1734Problem and evidence
The
clear()helper insidefindNoStopLimit(vpmb.ts:626-634) throwsnew Error(plan.error)whenever the current state's own ascent plan has an error other than "Current depth is shallower". That covers the "Projection exceeded the 24-hour limit" error and the critical-volume non-convergence error. Theif (!clear(state)) return 0call at line 657 therefore throws instead of returning 0. ZHL'snoDecoLimitreturns 0 for the same kind of state and leaves the error on the schedule.replayDive(diveReplay.ts:425-428) callssim.noDecoLimitto getbottomNdland does not catch errors. Its VPM tail-plan branch already handlesplan.errorby settingdecompressionRequired = true, but the uncaught NDL call still throws afterwards. The worker (calculation.worker.ts:23) runs replayDive for both the original and the comparison inside one try. As a result, the whole VPM-B replay/comparison for such a dive becomes an error message and shows no profile. The same ZHL-16C dive replays normally with bottomNdl 0 and decompressionRequired true.In the live view, VPM-B
calculateProjectionthrows throughprojectVpm. App shows NDL "Unavailable" plus an alert with the error text. ZHL instead shows "Modeled decompression required" with NDL 0 and the schedule error. That is a less severe inconsistency because the message is still shown. This only affects extreme exposures whose modeled ascent exceeds 24 hours, such as long deep dives on a single hypoxic mix.I wrote scratch scripts in /tmp/divesim-verify-Ra8a0h, run with
node --import tsxfrom the worktree. They build imported Shearwater profiles with the same normalizeShearwater/prepareImportedReplay path as tests/unit/replay-ascent-seed.test.ts, then call replayDive(dive, originalReplay).Results:
Stack trace: clear (vpmb.ts:633), then findNoStopLimit (vpmb.ts:657), then runtime noDecoLimit (runtime.ts:52), then replayDive (diveReplay.ts:427).
Direct model sweep at 120 m on tx10/70 with sim.noDecoLimit:
Suggested correction
In
findNoStopLimit, when the current state is not cleared, return 0 instead of throwing. Simplest version: drop the throw inclear()forloaded === stateand keepreturn !plan.error && !plan.stops.length. Input validation errors (depth, gas, level, state mismatch) are thrown separately and should stay as they are.projectVpmalready returns the first plan (with itserror) when ndl === 0, so the live UI will match ZHL: NDL 0 with the schedule error.If the convergence-failure case should stay distinguishable, return 0 and let the plan carry its error. As defense in depth, replayDive could also treat a noDecoLimit failure as bottomNdl 0 when decompressionRequired is already true.
Acceptance criteria
Add a regression test: an imported/synthetic tx10/70 profile (0 to 120 m by t=8, hold to t=98, surface at t=110). replayDive with VPM-B and originalReplay must not throw, and it must return bottomNdl 0 and decompressionRequired true, the same as ZHL-16C.
Also add a check that calculateProjection with model "VPM-B" for the 120 m / 60-min tx10/70 state returns ndl 0 with plan.error containing "24-hour", the same as ZHL. Existing #31 probe-horizon tests and the vpmb/model unit tests must still pass. pnpm verify passes.
Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.