feat(ui): rivers as cartographic strokes (D-261, T-1237)

Rivers now appear on the whole-body map for the first time. Five on Ferrath's
Global canvas, drawn as 5 px strokes that stop at the coastline.

Four rules, all client-side over existing server data, computed once on canvas
adoption rather than per draw:

  - fixed 5 px screen-space stroke at every rung
  - contiguous geometry through the river's own cells
  - never drawn over water — ocean and lake end a run
  - culled below 15 px of on-screen length (3x the stroke: below that a line
    is a square, not a river)

TWO THINGS THE MEASUREMENT FOUND THAT THE RECORD DID NOT ANTICIPATE.

First, the cull unit was wrong. A server "course" is an EDGE of the river
network — the stretch between two confluences — not a river. Culling per
course culls per segment, so a long river assembled from many short edges
vanishes entirely. Measured on Ferrath Global: 375 courses, 180 surviving the
water clip, and ZERO surviving a per-course cull. Edges are now chained
end-to-end into rivers before the cull is applied, which also delivers the
other half of D-261's "contiguous": per-course contiguity only makes each edge
unbroken; joining is what makes a river read as one line rather than dashes.
After chaining, 5 rivers survive at Global — the "major systems only from
orbit" behaviour the record predicted, arrived at by a different route.

Second, and worse: uses_orbital_derive() still read `Global | Region` while
the client's mirror had said Global-only since 2026-07-26. The D-255 amendment
claims "Region left the orbital derive set... it now takes the full
courses-aware derive". That was implemented against the MIRROR and never
against the authority, so Region kept running envelope-only and carrying no
courses — the exact thing the amendment said it had stopped doing. Both test
suites stayed green for two days because neither compares itself to the other.
Fixed here, with a note on each side pointing at the other, since the two
cannot be cross-checked automatically.

Also removes the two gates that withheld courses from the orbital rung — the
reason the whole-body map had no rivers at all. Whether a course is worth
drawing is measured in screen pixels, which only the client knows, so the
server now supplies geometry at every rung and the client decides.

The capture harness reports "drawn" alongside "courses", because "375 courses
arrived" and "375 rivers are drawn" are different claims and conflating them
is what made an empty map look like a data problem.

Client suite 1836 / 1810 passed / 26 skipped. Server suite green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-28 17:51:58 +02:00
co-authored by Claude
parent 41b6ceb47e
commit 474ab90663
8 changed files with 430 additions and 68 deletions
@@ -3280,3 +3280,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
RESOLVED 2026-07-27 by the edge-scroll fix (commit dac64a8a4) — this ticket''s diagnosis was wrong. I filed it blaming the capture harness for parking the mouse in a corner. The harness was faithfully reproducing a REAL product bug: _gui_input only fires while the pointer is over the Control, so _last_mouse_pos froze wherever it was last seen, and leaving the map always means crossing an edge, so the frozen value was always inside the edge margin. The viewer then panned forever with no input. NOTIFICATION_MOUSE_EXIT now resets to the (-1,-1) sentinel. Evidence: captures previously reported canvas_position=(292.0, 657.3336) — off-centre AND fractional, which ruled out center_offset() since that floors; they now report whole-number positions like (423.0, 0.0) with no drift across runs. No harness change was needed or made. Kept rather than deleted because the mis-attribution is worth seeing: a tooling explanation was reached for before the product was checked.', NULL, '2026-07-28 15:11:31', '2026-07-28 15:11:31.728', '2026-07-28 15:11:31.728', NULL, 'eaf52c758698ad7deace9e670825a90b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTA26EM8YXW086EV2K0N2WZR', 'status', 'backlog', 'done', NULL, '2026-07-28 15:11:31', '2026-07-28 15:11:31.754', '2026-07-28 15:11:31.754', NULL, '6a20816c1df4d1cb2287c801186fdcf1', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTEX51V1CH70BS8WAPZX0HH4', 'status', 'backlog', 'in_progress', NULL, '2026-07-28 15:12:52', '2026-07-28 15:12:52.681', '2026-07-28 15:12:52.681', NULL, 'cf672e646d4478421b6ee3e908a9bc45', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -5580,3 +5580,4 @@ RESOLVED 2026-07-27 by the edge-scroll fix (commit dac64a8a4) — this ticket''s
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTA26EM8YXW086EV2K0N2WZR', 'bug', '06FBPPMZNNEV052DBYYY3A897C', 'visual_capture drifts the Atlas view — edge-scroll fires during the settle', 'Found 2026-07-27 while verifying the Global fill fix. An atlas capture reported canvas_position=(292.0, 657.3336) where the letterbox centre should be a whole-pixel value near y=133-277. The Y is both wrong AND fractional, which rules out center_offset() -- that floors to whole pixels for texel-exactness. The cause is edge scroll: the capture harness leaves the mouse at the viewport corner, StepCanvasViewer reads it as a held edge-scroll, and pans continuously through the 240 settle ticks, pushing the canvas down and right (x pinned at the legend column, y drifting). Consequences: (1) every Atlas golden is captured mid-pan, so the baselines encode an arbitrary drift offset rather than the canonical centred frame; (2) a real centring regression would be invisible against them; (3) captures are not reproducible run-to-run if tick timing varies. Fix shape: the capture harness should suppress edge-scroll (park the mouse centre-screen, or expose a viewer flag the harness sets), then assert the settled position IS the centred letterbox. Worth doing before any Atlas golden is trusted for centring -- note the goldens were ALSO degenerate for a separate reason until today (missing body_radius_km), so this rung''s baselines have never been meaningful.
RESOLVED 2026-07-27 by the edge-scroll fix (commit dac64a8a4) — this ticket''s diagnosis was wrong. I filed it blaming the capture harness for parking the mouse in a corner. The harness was faithfully reproducing a REAL product bug: _gui_input only fires while the pointer is over the Control, so _last_mouse_pos froze wherever it was last seen, and leaving the map always means crossing an edge, so the frozen value was always inside the edge margin. The viewer then panned forever with no input. NOTIFICATION_MOUSE_EXIT now resets to the (-1,-1) sentinel. Evidence: captures previously reported canvas_position=(292.0, 657.3336) — off-centre AND fractional, which ruled out center_offset() since that floors; they now report whole-number positions like (423.0, 0.0) with no drift across runs. No harness change was needed or made. Kept rather than deleted because the mis-attribution is worth seeing: a tooling explanation was reached for before the product was checked.', 'done', 'medium', NULL, 'client', NULL, '2026-07-27 19:24:05.666', '2026-07-28 15:11:31.754', NULL, '39a5284eeee2c2637ed19a6c4a080e0a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTEX51V1CH70BS8WAPZX0HH4', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Render rivers as cartographic strokes (D-261)', 'Implements D-261 in step_canvas_annotation_layer.gd. All four rules are client-side over existing server data -- no wire change needed. (1) FIXED 5 px screen-space stroke at every rung, replacing the current per-class ladder (0.6/1.2/2.4). (2) CONTIGUOUS polyline through the river''s own cell centres, so geometry is a function of the river rather than the canvas pitch -- this is the actual fix for the fragments, since courses are presently resampled at canvas pitch and a course shorter than one gridunit collapses to a dot. (3) NEVER DRAWN OVER WATER: truncate each course where it meets ocean or lake, using the per-cell classification already present in the adopted canvas. (4) CULL below 15 px of on-screen length (3x the stroke width -- below that it reads as a square, not a line). Measure the VISIBLE extent, not total river length: a course crossing the window always spans it and passes, so only a course lying wholly inside the view and small is culled, and no new wire field is required. PERFORMANCE: compute the clipped/culled polylines ONCE on canvas adoption (set_frame), never per draw -- the layer redraws every frame and a per-point water lookup across ~375 courses per frame is waste for geometry that only changes when a canvas arrives. VERIFY by capture, not by reasoning: the pre-change baseline is 375 courses on Ferrath Global differing from courses-off by 458 of 518,400 pixels. After this, Global should show only major systems (>264 km on Ferrath) as clean unbroken strokes that stop at the coastline, with tributaries appearing on descent. NOTE the per-class width grammar is deliberately flattened here; the thinning-by-size polish is a separate follow-up (see D-261''s Deferred section).', 'in_progress', 'medium', NULL, 'client', 'D-261', '2026-07-28 06:41:06.520', '2026-07-28 15:12:52.681', NULL, 'cd504a7925b6e2a16387efc5896bbaf9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);