[P3] ZHL deco schedule holds at a fallback stop deeper than the GF anchor until it can reach the anchor directly, skipping the regular stops in between #81

Open
opened 2026-10-31 14:26:44 -05:00 by pedro · 0 comments
Owner

Priority: P3
Type: bug
Source: full codebase review requested 2026-10-31 (independently reproduced against 2cac7cf)

Location: src/models/zhl16c.ts:765-770, src/models/zhl16c.ts:783-796, src/models/zhl16c.ts:797-811, src/models/vpmb.ts:492

Problem and evidence

In decompressionPlan, next is set to anchorDepth whenever current > anchorDepth + 1e-8, and this happens on every loop iteration. The #26 fallback (lines 783-796) can stop the ascent at a regular stop deeper than the anchor, such as 24 m when the anchor is 15 m. That happens when slow tissues load during a slow ascent from a deep bounce dive. On the next iteration next becomes the anchor again. The fallback search for the shallowest reachable stop between the anchor and the current depth runs once, when the diver first arrives. At that moment the next stop up is not yet clear, so nothing is found. The hold loop then checks only canTravel(anchorDepth) each minute. The diver stays at the deep stop until a direct ascent to the anchor is clear, even though the next 3 m stop was reachable much earlier, and the intermediate stops (21 m and 18 m) are skipped. Every ceiling check still passes, so the schedule is conservative rather than unsafe. The problem is that the stop table breaks the documented 3 m / 10 ft spacing and total ascent time is inflated. VPM-B's projectVpm uses the same kernel.

I ran the reviewer's probe /tmp/divesim-review-oQNcyA/probe18.ts with node --import tsx from the worktree. It imports the real HEAD source. zhl16c_mod.ts differs from HEAD in only two ways: a stopped flag that turns off the anchor target once a hold occurs or a stop deeper than the anchor is chosen, and a type-only change.

  • Air, 90 m for 2 min, GF 50/80, 1 m/min: HEAD gives anchor 15 and stops 24:46 15:23 12:35 9:55 6:102 3:247 (598 min). With the fix: 24:4 21:10 18:15 15:21 12:30 9:48 6:89 3:210 (517 min). The 24 to 21 m travel is already clear after 4 min of the 46-min hold.
  • Same dive at 2 m/min: HEAD 21:11 15:8 ... (212 min). With the fix: 21:4 18:6 15:8 ... (206 min).
  • Trimix 18/45, GF 85/85, 1 m/min: HEAD 21:13 15:15 ... (556 min). With the fix: 21:1 18:10 15:15 ... (544 min).

Tracing the code confirms this: at current=24, next=15. canTravel(15) fails, so the fallback checks 18 and 21; both fail on arrival. The hold loop while (!canTravel(next)) then never looks at 21 again.

Reachability: the live Ascent input accepts 1 to 18 m/min (App.tsx:2399-2416), and depth goes up to 300 m.

Suggested correction

Use anchorDepth as the travel target only until the first stop has been reached. Keep a stopped flag and set it after an iteration where held > 0, or where next > anchorDepth + 1e-8 (meaning the fallback picked a stop deeper than the anchor). After that, compute next as the regular next stop, Math.max(0, (Math.ceil((current - 1e-8) / spacing) - 1) * spacing). This is the change in /tmp/divesim-review-oQNcyA/zhl16c_mod.ts. Another option is to re-run the shallowest-reachable-stop search inside the hold loop each minute.

Acceptance criteria

  • Regression test in the zhl16c tests: tissues from segment(initialTissues(), 0, 90, 5, AIR) followed by 2 min at 90 m, then decompressionPlan(s, 90, 1, 0.5, 0.8, 3, AIR, []). Expect anchorDepth 15, and every pair of consecutive stops from the first stop down to the anchor exactly 3 m apart. The expected stops are 24, 21, 18, 15, ..., with no 24 to 15 jump.
  • Total time should be well below the current 598 min (about 517 min).
  • The existing decompression, VPM-B and planner tests still pass.
  • Ordinary plans (30-100 m, 5-40 min) stay unchanged. pnpm verify passes.

Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.

Priority: P3 Type: bug Source: full codebase review requested 2026-10-31 (independently reproduced against `2cac7cf`) Location: `src/models/zhl16c.ts:765-770`, `src/models/zhl16c.ts:783-796`, `src/models/zhl16c.ts:797-811`, `src/models/vpmb.ts:492` Problem and evidence In decompressionPlan, `next` is set to `anchorDepth` whenever `current > anchorDepth + 1e-8`, and this happens on every loop iteration. The #26 fallback (lines 783-796) can stop the ascent at a regular stop deeper than the anchor, such as 24 m when the anchor is 15 m. That happens when slow tissues load during a slow ascent from a deep bounce dive. On the next iteration `next` becomes the anchor again. The fallback search for the shallowest reachable stop between the anchor and the current depth runs once, when the diver first arrives. At that moment the next stop up is not yet clear, so nothing is found. The hold loop then checks only `canTravel(anchorDepth)` each minute. The diver stays at the deep stop until a direct ascent to the anchor is clear, even though the next 3 m stop was reachable much earlier, and the intermediate stops (21 m and 18 m) are skipped. Every ceiling check still passes, so the schedule is conservative rather than unsafe. The problem is that the stop table breaks the documented 3 m / 10 ft spacing and total ascent time is inflated. VPM-B's projectVpm uses the same kernel. I ran the reviewer's probe /tmp/divesim-review-oQNcyA/probe18.ts with `node --import tsx` from the worktree. It imports the real HEAD source. zhl16c_mod.ts differs from HEAD in only two ways: a `stopped` flag that turns off the anchor target once a hold occurs or a stop deeper than the anchor is chosen, and a type-only change. - Air, 90 m for 2 min, GF 50/80, 1 m/min: HEAD gives anchor 15 and stops 24:46 15:23 12:35 9:55 6:102 3:247 (598 min). With the fix: 24:4 21:10 18:15 15:21 12:30 9:48 6:89 3:210 (517 min). The 24 to 21 m travel is already clear after 4 min of the 46-min hold. - Same dive at 2 m/min: HEAD 21:11 15:8 ... (212 min). With the fix: 21:4 18:6 15:8 ... (206 min). - Trimix 18/45, GF 85/85, 1 m/min: HEAD 21:13 15:15 ... (556 min). With the fix: 21:1 18:10 15:15 ... (544 min). Tracing the code confirms this: at current=24, next=15. canTravel(15) fails, so the fallback checks 18 and 21; both fail on arrival. The hold loop `while (!canTravel(next))` then never looks at 21 again. Reachability: the live Ascent input accepts 1 to 18 m/min (App.tsx:2399-2416), and depth goes up to 300 m. Suggested correction Use anchorDepth as the travel target only until the first stop has been reached. Keep a `stopped` flag and set it after an iteration where `held > 0`, or where `next > anchorDepth + 1e-8` (meaning the fallback picked a stop deeper than the anchor). After that, compute `next` as the regular next stop, `Math.max(0, (Math.ceil((current - 1e-8) / spacing) - 1) * spacing)`. This is the change in /tmp/divesim-review-oQNcyA/zhl16c_mod.ts. Another option is to re-run the shallowest-reachable-stop search inside the hold loop each minute. Acceptance criteria - Regression test in the zhl16c tests: tissues from segment(initialTissues(), 0, 90, 5, AIR) followed by 2 min at 90 m, then decompressionPlan(s, 90, 1, 0.5, 0.8, 3, AIR, []). Expect anchorDepth 15, and every pair of consecutive stops from the first stop down to the anchor exactly 3 m apart. The expected stops are 24, 21, 18, 15, ..., with no 24 to 15 jump. - Total time should be well below the current 598 min (about 517 min). - The existing decompression, VPM-B and planner tests still pass. - Ordinary plans (30-100 m, 5-40 min) stay unchanged. pnpm verify passes. Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: pedro/DiveSim#81
No description provided.