Skip to content

Phase B: routing domain core#5

Merged
DocGerd merged 26 commits into
mainfrom
worktree-phase-b-routing
Jul 15, 2026
Merged

Phase B: routing domain core#5
DocGerd merged 26 commits into
mainfrom
worktree-phase-b-routing

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

Phase B of the implementation plan: the complete routing domain core — 13 tasks (B1–B13), each implemented and reviewed via subagent-driven development with per-task review loops, closed by a whole-branch review (final verdict: ready to merge).

  • B1–B5 foundations: geodesy helpers; polar interpolation with no-go taper; wind grid space/time interpolation on u/v components; land/depth mask queries + 300 m navigable snapping; board & tack/gybe classification
  • B6–B8 solver: isochrone routing with 45 s maneuver penalties and board-aware pruning; motor fallback (< 2.5 kn sailing → 6.5 kn motor legs, always flagged); collinear leg merging with mask+wind re-validation
  • B9 two-rig orchestration (planRoute runs genoa + fock, recommends the faster; genoa on ties)
  • B10 fast-check property invariants — land/shallow avoidance, time monotonicity, continuity, motor flagging, maneuver-count consistency, board-flip↔maneuver pricing — 25 seeded scenarios × 2 rigs, zero counterexamples
  • B11 GPX 1.1 route export (rtept per leg + destination, ISO times, escaped desc with maneuver prefixes)
  • B12 Web Worker protocol + typed RoutingClient: pure testable handler, transferred (not copied) mask buffer, per-rig progress throttling, promises settled on fatal/dispose (incl. dispose-race guard)
  • B13 via-waypoint segmented routing — the router half of Movable via-waypoints on planned routes with automatic re-route #4 (Refs Movable via-waypoints on planned routes with automatic re-route #4; the draggable-marker UI half is E8, Phase E)

Test evidence

59/59 tests across 16 files (~209 s incl. property suite), typecheck + lint clean at a3a71a2. CI runs the same battery.

Notable review-driven hardening

Per-task reviews ran with adversarial fix loops; several defects originated in the plan's own code listings and were fixed additively: property-suite vacuous-pass guard (B10), CI-breaking unused import + empty-route guard (B11), promise settling on global fatal/dispose + dispose-race flag (B12), two borderline solver-test timeouts made explicit.

Deferred with named owners (tracked in the SDD ledger)

  • Phase D: optional MaskMeta metadata fields (encoding/verticalDatum/sources) when mask.meta.json is first parsed; Plan is structured-clone-safe but NOT JSON-safe (documented on the type) — Garmin export (Garmin Boating (ActiveCaptain) route sync — import, and basic push of updates #3) needs a dedicated serializer
  • E8: coincident-waypoint dedupe guard; multi-via E2E
  • Phase E: clamp progress tMs monotone per rig at via joints
  • F2: throttle-window E2E

Refs #4

🤖 Generated with Claude Code

Patrick Kuhn and others added 22 commits July 14, 2026 23:55
- Define LatLon interface and sailing domain types (Rig, Board, LegKind, ManeuverKind)
- Implement Settings with defaults (depth, speed, penalties)
- Add complete domain models: PolarTable, WindGrid, Leg, RigResult, Plan, Harbor, MaskMeta
- Implement spherical geo helpers: angle normalization, haversine distance, bearing, destination point calculation
- Add cross-track and along-track distance functions for isochrone routing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
…rd convention

- Update Leg.twaDeg comment to clarify >= 0 starboard, < 0 port (head-to-wind edge case favors starboard)
- Add crossTrackNm test: verify sign convention (north-of-eastbound-track negative, south positive)
- Add alongTrackFraction test: verify behavior on segment (0..1) and extensions (<0, >1)
- Format geo.ts with prettier (no behavior change)

All tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements Polar class with bilinear interpolation over TWA/TWS grid,
TWS clamping/scaling, no-go zone taper, and beat/gybe angle lookup.
Test fixture (TEST_POLAR) and comprehensive test coverage included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
Implements bilinear spatial + linear temporal interpolation for wind grids,
with u/v vector component handling to correctly wrap across 0°/360°. Includes
fixture helpers (uniformWindGrid, makeWindGrid) and comprehensive test suite
covering uniform fields, direction wrapping, temporal interpolation, and
spatial/temporal clamping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…napping

Implement NavMask class with Amanatides–Woo grid traversal for segment collision tests,
depth queries with safety depth thresholds, and expanding ring search for snap-to-navigable.
Cells outside bbox and with depth=0 (land) are non-navigable; 255→25.4m; other bytes→depth/10.
Constructor uses explicit fields per erasableSyntaxOnly constraint. Fine-grid test for snap
verifies 300m radius behavior within floating-point precision limits of coordinate indexing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
- Add toRad import to mask.ts and use both latitude and longitude cell
  dimensions in snapToNavigable maxRing calculation. At high latitudes,
  longitude cells narrow (cos(lat) factor) and become the limiting axis.
  The old lat-only bound under-searched the narrower axis.
- Add regression test for narrow longitude cells at 54.75°N: 1500 m search
  should find water at col 155 (east edge of col 150 is ~1450 m away).
  Old code capped at ring 4 = col 154 due to lat-only cell height.
- Delete unused testSnapMask fixture (dead code with wrong comment).
- Simplify fine-grid snap test: use makeMask(fn, meta) instead of
  hand-building NavMask inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
Implement maneuver detection module for isochrone routing:
- boardOf: derive sailing board from signed TWA (≥0 → starboard, <0 → port)
- boardForCandidate: handle ±180° ambiguity by inheriting parent board
- classifyManeuver: distinguish tack vs gybe by turn angle (|prev|+|next| ≤ 180 → tack)

Tests cover edge cases: head-to-wind (0° starboard), dead-run board inheritance,
mixed tack/gybe classification, and turn-through-wind rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… minor solver cleanups

Applies four review fixes:
- FIX 1: Strengthen island test with proper geometry (lat 54.60 origin/dest, requires 24.3 nm detour through gap at lat 54.75–54.80)
- FIX 2: Make frontier-cap comparator 3-way (handle a==b case with proper 0 return)
- FIX 3: Replace Math.min with spread-array to plain loop (no array allocation, no arg limit)
- FIX 4: Add comment clarifying pruning heuristic for death-counter classification

Observed island test: maxLat=54.7528, distance=24.28 nm (within bounds 54.74 and 20–30 nm)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements post-processing pass for merging consecutive sail/motor legs when:
- Same kind and board, no maneuver at joint
- Heading difference ≤ 10°
- Merged segment passes segmentNavigable check
- For sail legs, merged heading preserves board at the joint

Iterates to fixpoint. TDD: 3 test cases (dog-leg merge, barriers to merge,
land-clipping detection) all passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
Implements planRoute with TDD: snap origin/destination to navigable cells
with 500m radius; build WindField; run solve for each rig with independent
polars and performance factors; postprocess mergeCollinearLegs; compute
totals (maneuverCount, motorDistanceNm, durationMs); recommend faster ETA
(ties → genoa; single survivor → that rig); both-fail → genoa's reason.

All 4 tests passing:
- beam reach with genoa/fock recommends faster
- snap handles land edge cases
- progress callback fires for each rig
- snap-failed-origin propagates correctly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
- Remove explicit 500m radius from snapToNavigable calls in planRoute.ts
  so the 300m default (per spec) applies consistently
- Update test fixture origins/destinations to cell centers to ensure they
  snap successfully within 300m (test grid cell centers are ~300-320m apart;
  test points at arbitrary decimals would miss by floating-point rounding)
- Updated shared req object to use cell centers (54.7525, 10.0025) and
  (54.7525, 10.4025) instead of (54.75, 10.0) and (54.75, 10.4) so all
  route-planning tests pass with stricter snap radius
- "snaps origin off land" test: origin now (54.7525, 10.2095) ~192m west
  of water col 162; destination now (54.7525, 10.6025) at cell center
- Deep-inland test unchanged; still correctly returns snap-failed-origin

4/4 tests + lint + typecheck pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WC1Dw7ZLH3Az7TY73Q2htL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread app/src/routing/workerClient.ts
Comment thread app/src/types.ts
Comment thread app/src/routing/isochrone.ts
Comment thread app/src/routing/protocol.ts
Comment thread app/src/routing/workerClient.test.ts
Comment thread app/src/routing/viaPoints.test.ts
Comment thread app/src/routing/isochrone.test.ts
Comment thread app/src/lib/mask.test.ts
Comment thread app/src/routing/maneuver.test.ts
Comment thread app/src/routing/protocol.test.ts
Comment thread app/src/routing/planRoute.test.ts
Comment thread app/src/lib/wind.test.ts
Comment thread app/src/lib/polar.test.ts
Comment thread app/src/types.ts
Comment thread app/src/types.ts
Comment thread app/src/lib/wind.ts
Comment thread app/src/routing/isochrone.ts Outdated
Comment thread app/src/routing/workerClient.ts Outdated
Comment thread app/src/types.ts
Comment thread app/src/lib/mask.ts Outdated
Comment thread app/src/routing/isochrone.ts Outdated
Comment thread app/src/routing/invariants.property.test.ts Outdated
Comment thread app/src/routing/protocol.test.ts Outdated
Comment thread app/src/routing/planRoute.ts Outdated
Comment thread app/src/routing/isochrone.ts
Comment thread app/src/lib/geo.ts
Comment thread app/src/lib/gpx.ts
DocGerd pushed a commit that referenced this pull request Jul 15, 2026
… Leg-union task per user decisions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Patrick Kuhn and others added 2 commits July 15, 2026 11:45
…verage batch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DocGerd

DocGerd commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

CI note: the first run failed with 10 pure-timeout failures (no behavioral failures) — the Actions runner executes the isochrone solver ~6-10× slower than dev machines. The whole-branch review had triaged the timeout policy as "keep per-test explicit timeouts only", calibrated on local timings; CI evidence overturns that at per-file granularity. Landing: vi.setConfig({ testTimeout: 120_000 }) in the six solver-heavy test files (fast files keep the 5 s default so hang detection stays meaningful), property-suite cap 300 s → 900 s (observed 374 s on CI).

Patrick Kuhn and others added 2 commits July 15, 2026 12:06
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant