Mask: reconnect all harbors (2x resolution, center-sampled land, connectivity gate)#8
Conversation
…ors (Closes #6) Increase mask resolution 2200x2400 (~46m cells, was ~93m), switch land rasterization from all_touched to cell-center sampling, and switch depth reprojection from Resampling.max to Resampling.bilinear as a last resort for channels the max-resampled DTM was flattening below their documented depth. Add a 4-connected flood-fill connectivity gate to verify_mask.py (scipy.ndimage.label) so every harbor snap must reach open water at its gate depth, with a documented per-harbor exception list for genuinely shallow approaches (Augustenborg 2.8m, Marstal 2.0m, both cited from their own approachNote text). Reduces disconnected harbors from 14/33 to 5/33. The remaining 5 (Arnis, Dyvig, Graasten, Kappeln, Maasholm) are documented as known concerns in .superpowers/sdd/fix-mask-report.md rather than force-connected with unjustified exceptions - their approach notes don't support a depth exception (Kappeln's fairway is charted at ~5m) or the blocker is land, not depth (Dyvig's sub-cell-width channel, Graasten's bascule bridge). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cripts Self-review (pr-review-toolkit code-reviewer + comment-analyzer) on PR #8 found: the resampling-switch comment overstated conservatism guarantees (bilinear averaging can raise a depth estimate above what max() would have given, unlike the floor step); the OSM feature-count comment embedded a transient "this week's export" measurement as if it were a permanent invariant; the n_land threshold had no derivation comment despite two competing effects (resolution 4x vs. all_touched flip) acting on it; and verify_mask.py duplicated the lat/lon->row/col transform in both depth_m() and rc_of(), plus referenced a gitignored report file that ships to no one who clones the repo. Fixed all four; no logic changes (verify_mask.py re-run to confirm identical connectivity output). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Self-reviewRan
🤖 Generated with Claude Code |
…sions verify_mask.py exited non-zero on the 5 harbors documented as unresolvable in the PR #8 report, meaning the shipped verifier was permanently red on the committed, correct mask.bin - unable to distinguish a known limitation from a new regression. Add KNOWN_DISCONNECTED (harbor id -> reason, citing issue #9) alongside CONNECTIVITY_EXCEPTIONS_M: a listed harbor that's still disconnected is reported as KNOWN and does not fail the run; a harbor NOT listed that's disconnected still fails (regression); and a listed harbor that turns out connected also fails, with a "remove the stale entry" message, so the allowlist can't silently drift out of date as the data improves. Verified the stale-entry branch by patching a copy of the script to list a genuinely-connected harbor and confirming it fails with that exact message, then confirmed the real script exits 0 against the committed mask, printing the full 28 OK / 2 exception / 5 known-disconnected table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Controller review found the unconditional Resampling.max -> bilinear switch (62deac9) fixed the 4 named channels but also manufactured depth broadly elsewhere: measured 22,948 land->water flips vs. pure max in the actual final encoded output, 1,780 of them >= 3.0 m (the app's default safety depth), 97.6% of those >1 km from any harbor snap - i.e. mostly outside the channels this fix was meant to help, violating "never overstate depth" project-wide. Fix: compute both Resampling.max and Resampling.bilinear reprojections from the same source and blend per cell - trust bilinear only where it agrees with max within TOLERANCE_M (smooth, gently-varying depth); otherwise fall back to the conservative max value (a source discontinuity bilinear can't be trusted to interpolate correctly). TOLERANCE_M=2.0 satisfies both tuning constraints on the first value tried: verify_mask.py reports the identical 28 OK / 2 exceptions / 5 known- disconnected connectivity table (exit 0), while land->water flips crossing 3.0 m (and 5.0 m) drop from 1,780 (663) to zero. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Self-review cycle complete: implementer-run lenses (code, comments) + controller review + independent reviewer with empirical re-derivation of the resampling numbers. The PR-body staleness finding from the independent review is resolved (body rewritten at 25899b2). All 4 inline threads above are resolved; the blend's 38-cell residual is documented in-code and its permanent fix (settings-UI draft clamp) is Phase E intake. Ready to merge pending CI. |
…odeQL) CodeQL js/incomplete-multi-character-sanitization re-fired on the moved copyright-holder sanitizer (alert #8): the `.replace(/<[^>]*>/g, '')` pair-regex is flagged for the removal itself, and the trailing residual `[<>]` strip does not silence the per-call heuristic. Replace both bracket regexes with a depth-counting character scanner (stripAngleBracketed) that runs after the paren strip. The scanner never emits `<` or `>`, so the result is bracket-free by construction with no regex removal that could be "incomplete". Ordering the scanner last also catches the paren-splice case (`<(x)script` -> `<script` -> truncated). Notices output stays byte-identical for well-formed `Name <email> (url)` authors; two test expectations change under the new semantics (unclosed `<script` truncates; the splice falls back), re-derived by hand and pinned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxREWu8gax6BnxGcW3n4mH
Summary
Fixes the harbor-connectivity failures reported in #6 (Flensburg's inner harbor showed as unreachable land; 14/33 harbor snaps were disconnected from open water at the default 3.0 m safety depth).
Root causes (confirmed):
all_touched=Trueland rasterization at ~93 m cells ate a full cell of margin off both banks of every quay-lined basin/narrow channel.Resampling.max(shallowest-wins) on the ~115 m native EMODnet DTM let shallow surroundings dominate cells straddling a dredged/buoyed channel.unknown -> 0(land).Changes:
pipeline/build_mask.py: 2x resolution (2200x2400, ~46 m cells), land rasterization switched to cell-center sampling (all_touched=False), depth reprojection blendsResampling.maxandResampling.bilinearper cell (trust bilinear only where it agrees with max within a 2.0 m tolerance; otherwise fall back to the conservative max value — an unconditional switch to bilinear alone manufactured depth at unrelated shoal/land boundaries across the grid, see "Depth resampling" below), plausibility assertions rescaled for the new grid.pipeline/verify_mask.py: new connectivity gate — 4-connected flood-fill (scipy.ndimage.label) from a fixed open-water seed; every harbor snap must reach it at its gate depth (default 3.0 m, or a documented exception). Two exceptions added, each grounded in a measured reconnection threshold and the harbor's ownapproachNotetext: Augustenborg (2.8 m), Marstal (2.0 m). AKNOWN_DISCONNECTEDallowlist (5 harbors, each cited against issue Mask: 5 harbors remain disconnected — Schlei fairway, Dyvig channel, Gråsten bridge (sub-cell features) #9) means the gate reports these as tracked rather than failing the run — and fails the run if one of them ever becomes stale (i.e. turns out connected without the entry being removed).pipeline/requirements.txt: addedscipy==1.18.0for the flood-fill.app/public/data/mask.bin(1.3 MB → 5.28 MB) +mask.meta.json.Before / after connectivity
14/33 disconnected → 5/33 disconnected, all tracked (issue #9). Flensburg (the reported case) is now connected, along with 8 of the other 13 originally-named harbors.
verify_mask.pyexits 0 against the shipped mask.Full per-harbor table, diagnosis of the 5 tracked (Arnis, Dyvig, Graasten, Kappeln, Maasholm — none force-connected with unjustified exceptions; each has a specific documented reason it wasn't), and all judgment calls are in
.superpowers/sdd/fix-mask-report.md(gitignored working notes, not part of this diff).Why 5 remain open, tracked under #9 rather than force-connected: investigated with a threshold-scan (binary search over gate depth) and a raw-DTM cross-check, not just left alone:
Depth resampling: why a blend, not a straight switch
An unconditional switch from
Resampling.maxtoResampling.bilinearfixes the channels above but also manufactures depth broadly elsewhere: measured against puremaxin the actual final encoded output (post land-mask, post-Schlei-carve, post-floor), unconditional bilinear flips 22,948 cells from LAND to WATER, 1,780 of them ≥ 3.0 m (the app's default safety depth) and 663 ≥ 5.0 m — 97.6% of the ≥3.0 m flips more than 1 km from any harbor snap, i.e. mostly outside the channels this fix needed to help. That violates the project's "never overstate depth" rule.Fixed by computing both reprojections and blending per cell: bilinear only where
|bilinear − max| ≤ 2.0 m(smooth, gently-varying depth), else the conservative max value (a source discontinuity bilinear can't be trusted to interpolate). At that tolerance, land→water flips crossing 3.0 m (and 5.0 m) drop from 1,780 (663) to zero, while the connectivity gate still reports the identical 28 OK / 2 exceptions / 5 known-disconnected table.Test plan
pipeline/build_mask.pycompletes, all assertions pass.pipeline/verify_mask.pyconnectivity gate: exit code 0. 28/33 OK, 2 via documented exceptions (Augustenborg 2.8 m, Marstal 2.0 m), 5 known-disconnected and tracked under issue Mask: 5 harbors remain disconnected — Schlei fairway, Dyvig channel, Gråsten bridge (sub-cell features) #9 (Arnis, Dyvig, Graasten, Kappeln, Maasholm) — reported, not failing the run.npm --prefix app install && npm --prefix app run test -- mask— 8/8 pass (fixture-based, confirms no app-side regression from the mask format/dimension change).Closes #6
🤖 Generated with Claude Code