Skip to content

fix(routing): stamp visited only for frontier-cap survivors (#67)#94

Closed
DocGerd wants to merge 1 commit into
developfrom
fix/67-frontier-cap-stamp
Closed

fix(routing): stamp visited only for frontier-cap survivors (#67)#94
DocGerd wants to merge 1 commit into
developfrom
fix/67-frontier-cap-stamp

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner

What

The isochrone solver's solve() stamped visited over all byKey ring
winners before the MAX_FRONTIER truncation. A node stamped and then capped
out never expands, yet its visited-stamp permanently prunes every later arrival
into that same prune cell (visitedDominates: stamp no later AND no more
maneuvers). When a sole gateway cell's first arrival is the capped-out one,
the cell is sealed with no surviving subtree, so a still-connected destination is
reported unreachable — routes can be missed under a large frontier. Filed
out-of-scope from PR #66.

Fix

Stamp visited after the cap slice, over the survivors only (recomputing
each survivor's pruneKey). Survivors always expand next ring, so every
domination guarantee is preserved; capped-out nodes no longer seal cells they
never grew a subtree in. When the frontier fits under the cap, the survivor set
equals every byKey winner, so the uncapped path — every real-mask route whose
frontier peaks below MAX_FRONTIER — is byte-identical to before.

MAX_FRONTIER's value (30 000) and the domination rule are unchanged.

Test seam + regression

Verification

  • npm --prefix app run typecheck — clean
  • npm --prefix app run lint — clean
  • isochrone (unit + followups) — 12/12 pass
  • realmask.repro.test.ts — 6/6 pass; wall-time 87 s (pre-fix) vs 91 s (fix),
    within run-to-run variance (real routes never hit the cap → the fix is a no-op
    there)
  • invariants.property + planRoute + motor + postprocess + viaPoints +
    relaxedDepth — 35/35 pass

Closes #67

The isochrone solver stamped `visited` over ALL byKey ring winners BEFORE
the MAX_FRONTIER truncation. A node that was stamped and then capped out
never expands, yet its visited-stamp permanently prunes every later
arrival into that same prune cell (visitedDominates: stamp no later AND
no more maneuvers). When a sole gateway cell's first arrival is the
capped-out one, the cell is sealed with no surviving subtree, so a
still-connected destination is reported unreachable — routes can be
missed under a large frontier (filed out-of-scope from PR #66).

Move the stamp to after the cap slice, stamping only the survivors
(recomputing each survivor's pruneKey). Survivors always expand next
ring, so every domination guarantee is preserved; capped-out nodes no
longer seal cells they never grew a subtree in. When the frontier fits
under the cap, the survivor set equals every byKey winner, so the
uncapped path — every real-mask route whose frontier peaks below
MAX_FRONTIER — is byte-identical to before.

Expose `maxFrontier` as an injectable SolveParams field (defaults to
MAX_FRONTIER) so a regression test can drive the cap into the truncating
regime without a 30 000-node mask. New test builds an H-shaped water
region with a dead-end decoy gulf pointing at the destination and a sole
through-corridor: under small caps stamp-before-cap sealed the corridor
and returned 'unreachable' for a provably connected destination
(mask.cellsConnected), while the fix returns a valid detour route at
every cap tested. realmask.repro wall-time unchanged (~90 s; real routes
never hit the cap).

Closes #67

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DocGerd

DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

Self-review (pre-merge) — ran a multi-lens adversarial review of this diff. Verdict: clean, no findings → no review threads to post/resolve.

  • Correctness of the reorder: the uncapped path is byte-identical. Every node stored under key k was inserted via byKey.set(pruneKey(child…), child), so the post-truncation pruneKey(n…) recompute equals k; componentwise-minima makes stamp order irrelevant. In the capped regime the fix stamps strictly fewer cells (survivors only) → visitedDominates prunes strictly less → it can only admit more re-arrivals, never wrongly prune. Capped-out stampers have no surviving subtree — exactly the unsound seal being removed.
  • Test integrity (repo lesson feat(profile): depth-over-time route profile with wind/heading indicators #50, mutation lens): RED-check executed — restoring stamp-before-cap makes the regression test fail (cap=11: expected 'no-route' to be 'ok'); the fix turns it green. The oracles (mask.cellsConnected/mask.segmentNavigable BFS over the byte grid, haversineNm) are independent of solve(), so the assertion is not a tautology.
  • Performance: provably a no-op at the production cap — the real-mask frontier peaks ~15k < MAX_FRONTIER (30k), so the truncation branch never fires; realmask.repro 6/6.
  • Conventions: typecheck + lint clean; maxFrontier? is a documented optional seam defaulting via ??, with no test-only surface leaking into production callers (exactOptionalPropertyTypes-safe).

Merging to develop once required checks (app + e2e) are green.

@DocGerd

DocGerd commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

Merged into develop: fix commit 677095a landed via merge commit 83e2ecf (Merge pull request #94), which is now the develop tip. GitHub returned a 504 Gateway Timeout during the merge, so the ref updated correctly but the PR merged-flag, branch auto-delete, and the Closes #67 auto-close did not fire. Reconciling manually: closing this PR (the change is already in develop), deleting the branch, and closing #67 with a reference to the merge commit.

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.

Solver: visited stamps written before the MAX_FRONTIER cap can seal prune cells with no surviving subtree

1 participant