From f5044926da0fd03a0b8083f3b6d5ceab54cb3ed6 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 28 Jul 2026 08:41:44 +0200 Subject: [PATCH] =?UTF-8?q?docs(meta):=20D-261=20=E2=80=94=20rivers=20are?= =?UTF-8?q?=20a=20stroke,=20not=20a=20scaled=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A river is sub-pixel at almost every Atlas scale (a 100 m channel against Global's ~17.6 km per screen pixel), so drawing it to scale draws nothing. The line becomes a symbol whose job is legibility: a fixed 5 px screen-space stroke, contiguous through the river's own cell centres, never drawn over water, and culled when too small to read. The cull threshold derives from the stroke rather than being stipulated. A mark reads as a line at roughly 3x its own width, so the minimum is 15 px of on-screen length — and the kilometre thresholds then fall out of each rung's scale: 264 km at Global, 2.8 km at Region, 28 m at District. A level-of-detail ladder with no hand-tuned constants, which self-corrects if the width changes. Measured on the visible extent, not total river length: a course crossing the window always spans it and passes, so only a course wholly inside the view and small is culled. Correct at both ends of the ladder, and no new wire field. Records a tension that had to be resolved rather than split. Jeroen proposed a flat 100 km cutoff, then noted that 5x5 px still reads as a dot and asked for 15-20. Those are incompatible: the km threshold depends only on the pixel length, so 100 km implies ~6 px, which is dot-shaped at a 5 px stroke. Admitting 100 km rivers AND keeping them line-shaped needs a ~2 px stroke. Thick lines mean fewer rivers; thin lines mean more. Ruled for the bold stroke. Client-side, and provably so: the cull is measured in screen pixels, and the display ratio is a client-side viewport-dependent parameter that never reaches a wire request or cache key, so the server cannot know how many pixels a course occupies. Water-clipping needs nothing new either — the per-cell classification is already in the adopted canvas. Built on the 2026-07-27 measurement in D-258's amendment: 375 courses present, 458 of 518,400 pixels different. The rivers were not failing to render, they were correctly beneath notice; the defect was drawing specks instead of drawing properly or not at all. T-1237 implements. T-1238 restores size-varying width as polish, deliberately deferred — noting width and cull are coupled, since a thinner stroke lowers its own visibility threshold. Co-Authored-By: Claude --- .pql/changelog/ticket_deps/2026-07.sql | 1 + .pql/changelog/ticket_idmap/2026-07.sql | 2 ++ .pql/changelog/tickets/2026-07.sql | 2 ++ governance/README.md | 1 + governance/decisions/architecture.md | 35 ++++++++++++++++++++++++- 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.pql/changelog/ticket_deps/2026-07.sql b/.pql/changelog/ticket_deps/2026-07.sql index 00ec4ca37..6cc09d51d 100644 --- a/.pql/changelog/ticket_deps/2026-07.sql +++ b/.pql/changelog/ticket_deps/2026-07.sql @@ -46,3 +46,4 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT0V7D4VQ6YHAWGFH472ZAJM', '06FT0VCC9809R320XQ28RNWXN4', '2026-07-26 21:56:03.787', '2026-07-26 22:17:17.637', NULL, '9f360f6ebe6de1ab57fa033f445d03ae', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT0TX2W0BA10PRR7NMJ2362M', '06FT0VERS6AKQHB30Y1RSRW1FM', '2026-07-26 21:56:23.415', '2026-07-26 22:17:17.650', NULL, 'd3311889283355a96b181d15ab94d904', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT0V4HQKPYPVQYAKYVFK68C4', '06FT0VERS6AKQHB30Y1RSRW1FM', '2026-07-26 21:56:23.446', '2026-07-26 22:17:17.664', NULL, 'f39feaa76cb9202261b5617e7a9351ec', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTEX51V1CH70BS8WAPZX0HH4', '06FTEX6JGRQCTH8S5RYAZ1BBPR', '2026-07-28 06:41:19.019', '2026-07-28 06:41:19.019', NULL, 'a975b4ec64097e4acd1f6155eb3134af', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); diff --git a/.pql/changelog/ticket_idmap/2026-07.sql b/.pql/changelog/ticket_idmap/2026-07.sql index ed1753e9e..86d6b649f 100644 --- a/.pql/changelog/ticket_idmap/2026-07.sql +++ b/.pql/changelog/ticket_idmap/2026-07.sql @@ -150,3 +150,5 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT4SWTWSMBG0PEPQNK8DT7ZW', 'T-1234', '2026-07-27 07:08:46.695', '2026-07-27 07:08:46.695', NULL, '582e004a5dd3b45f3c00afa335ab4845', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FT4SY5BK5SBDA8G2MCPCB3AW', 'T-1235', '2026-07-27 07:08:57.564', '2026-07-27 07:08:57.564', NULL, '040d8befbcc613e958d46601dab04438', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTA26EM8YXW086EV2K0N2WZR', 'T-1236', '2026-07-27 19:24:05.667', '2026-07-27 19:24:05.667', NULL, '09377e9155fd39e91049ffb0356e3c45', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTEX51V1CH70BS8WAPZX0HH4', 'T-1237', '2026-07-28 06:41:06.522', '2026-07-28 06:41:06.522', NULL, '4f83b59a615c1ac423b13bc8f0ba65e7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FTEX6JGRQCTH8S5RYAZ1BBPR', 'T-1238', '2026-07-28 06:41:18.987', '2026-07-28 06:41:18.987', NULL, 'a5bc6c18ebeced8743aed48cf926cf9e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index d6f68cec7..3cade5fc4 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -5572,3 +5572,5 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat 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 ('06FT0VDA5Q7JZFXG7XYWFR6M3C', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'step_canvas_viewer.gd is 1,133 lines against the 1,000-line gdlint cap', 'Pre-existing lint-cap violation, made worse by the 2026-07-26 pair session''s fetch-overlay wiring work (still net additive to a file already over cap). Split step_canvas_viewer.gd so it comes back under the 1,000-line gdlint limit -- likely candidate: extract the fetch-overlay wiring / cache-store plumbing into a separate component now that step_canvas_fetch_overlay.gd exists as a dedicated node, reducing what step_canvas_viewer.gd itself needs to own directly. Verify gdlint passes clean after the split and that the viewer test suite (92/92 as of this session) stays green. ALSO test_step_canvas_viewer.gd, which crossed the 1,000-line gdlint cap on 2026-07-27 (now 1,094) as regression tests accumulated during the Global debugging session. Same cap, same file cluster, so fix together: the viewer suite splits naturally along the seams its own section comments already mark (rung transport / Global sizing + letterbox / cache-key contract / edge-scroll + pan). Note the production file and the test file hit the cap for opposite reasons — the viewer because it does too much, the suite because it covers a lot — so the remedies differ: extract collaborators from the viewer, split the suite by concern.', 'backlog', 'low', NULL, NULL, NULL, '2026-07-26 21:56:09.390', '2026-07-27 19:36:19.691', NULL, '706b91aeeb64634aa9a40e473b56efab', 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).', 'backlog', 'medium', NULL, 'client', 'D-261', '2026-07-28 06:41:06.520', '2026-07-28 06:41:06.520', NULL, 'bae3e0eba124da884036c7588523e99f', 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 ('06FTEX6JGRQCTH8S5RYAZ1BBPR', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'Polish: river stroke width varies with river size (D-261 deferred)', 'Deferred out of T-1237 by Jeroen: ''I do like your suggestion of thinning shorter rivers (especially if that shows tributaries following those rules) but maybe that is for a future polish iteration.'' D-261 flattens the ratified stream/tributary/trunk width grammar (0.6/1.2/2.4 px, tuned so a tributary joining a trunk visibly reads as a join) to a uniform 5 px. This restores a size-varying width on top of the D-261 stroke model. Worth knowing before starting: width and cull are COUPLED. D-261 derives the visibility threshold as 3x the stroke width, so a thinner stroke lowers its own threshold -- a 2 px tributary would become visible at ~106 km on Ferrath Global where a 5 px trunk needs 264 km. That is arguably the correct behaviour (smaller rivers are both thinner AND appear later) and makes the two rules compose rather than conflict, but it means the cull can no longer be a single per-canvas constant: it becomes per-course, derived from that course''s own width. Blocked in spirit on T-1237 landing first, since it modifies the model that ticket establishes.', 'backlog', 'low', NULL, 'client', 'D-261', '2026-07-28 06:41:18.982', '2026-07-28 06:41:18.982', NULL, '87d7cb4a08bbfce9ddea16e26dc1c2c9', 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); diff --git a/governance/README.md b/governance/README.md index 4ebea3aec..063a7ae0e 100644 --- a/governance/README.md +++ b/governance/README.md @@ -326,6 +326,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it. - [D-258: Rung-0.5 expanded layer — one derived base for the whole ladder](decisions/architecture.md#d-258-rung-05-expanded-layer--one-derived-base-for-the-whole-ladder) — _architecture_ - [D-259: Enclosed settlements — open-air requires a breathable atmosphere](decisions/architecture.md#d-259-enclosed-settlements--open-air-requires-a-breathable-atmosphere) — _architecture_ - [D-260: Generator scope — Sol is authored, not generated](decisions/architecture.md#d-260-generator-scope--sol-is-authored-not-generated) — _architecture_ +- [D-261: River rendering — a stroke, not a scaled feature](decisions/architecture.md#d-261-river-rendering--a-stroke-not-a-scaled-feature) — _architecture_ ## Open questions diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index d837aa1f7..66a16725d 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -2448,4 +2448,37 @@ Technical foundation decisions that constrain implementation: engine, client-ser --- -*114 decisions (D-001 through D-260, excluding gaps). Last updated: 2026-07-27 (D-259 — enclosed settlements: open-air requires a breathable atmosphere, branch on an explicit property, D-220's density model declared inapplicable to pressure vessels; D-260 — generator scope: Sol encoded as `Authored { deepest_rung: Global }`, the DLC gate a depth not a boolean, Sol art an authored data canvas).* +### D-261: River rendering — a stroke, not a scaled feature + +- **Date:** 2026-07-28 +- **Decision:** A river on an Atlas canvas is drawn as a **cartographic stroke**, never as terrain scaled to its true width. Four rules, all client-side: + - **Fixed 5 px width, screen-space, identical at every rung.** A real river is sub-pixel at almost every Atlas scale (a 100 m channel against Global's ~17.6 km per screen pixel), so drawing it to scale draws nothing. The line is a *symbol* whose job is legibility, not a measurement. + - **Contiguous, through the river's own cells.** The polyline passes through each river cell's centre, so its geometry is a function of the river, not of how far out the player is zoomed. + - **Never drawn over water.** A course is truncated where it meets ocean or lake. A blue line across open sea asserts something false. + - **Culled below 15 px of on-screen length** — see the derivation below. +- **The cull threshold is derived from the stroke, not stipulated.** A mark only reads as a *line* at roughly 3–4× its own width; at 5 px wide and 5 px long it is a square. The rule is therefore **minimum on-screen length = 3 × stroke width = 15 px**, and the kilometre thresholds fall out of each rung's own scale rather than being written down: + + | Rung | ground per screen px | river must exceed | + |---|---|---| + | Global | 17.6 km | **264 km** | + | Region | 190 m | 2.8 km | + | District | 1.9 m | 28 m | + | Chunk | 0.06 m | 0.9 m | + + This is a genuine level-of-detail ladder with no hand-tuned constants: from orbit only major systems are visible, and tributaries emerge on the way down, which is how an atlas behaves. It self-corrects — change the stroke width and every threshold moves with it. + + **Measure the VISIBLE extent, not total river length.** A course crossing the window always spans it and so always passes; only a course lying wholly inside the view *and* small is culled. That is exactly "too small to see", it behaves correctly at both ends of the ladder, and it requires no new wire field. + + **Note the tension resolved here.** Jeroen first proposed a flat 100 km cutoff, then observed that 5 px × 5 px still reads as a dot and asked for 15–20 px. Those are incompatible: the kilometre threshold depends **only** on the pixel length (stroke width does not enter it), so 100 km at Global implies ~6 px, which is dot-shaped at a 5 px stroke. Admitting 100 km rivers *and* keeping them line-shaped would require dropping the stroke to ~2 px. **Thick lines mean fewer rivers; thin lines mean more.** Ruled in favour of the bold stroke and the 264 km cutoff: at whole-planet scale an atlas shows major systems only, and weakening the rivers that matter to admit ones you can barely follow is a poor trade. +- **This is a CLIENT function over server data, and cannot be otherwise.** The split follows [D-255](#d-255)'s own premises: the **server** owns which courses exist, where their cells run, and their class (content determination); the **client** is the map-art function, so width, culling and water-clipping are all presentation. The cull in particular *cannot* move server-side — it is measured in screen pixels, and the display ratio is explicitly a client-side, viewport-dependent parameter that never touches a wire request or a cache key, so the server cannot know how many pixels a course occupies. Water-clipping likewise needs nothing new over the wire: the per-cell classification is already in the canvas the client was sent. + - **Compute once per canvas adoption (`set_frame`), never per draw.** The annotation layer redraws every frame; a per-point water lookup across several hundred courses every frame would be waste for geometry that only changes when a new canvas arrives. +- **Rationale:** Measured, not assumed. With course suppression disabled experimentally on 2026-07-27, Ferrath's Global canvas carried **375 courses** and differed from courses-off by **458 of 518,400 pixels**. The rivers were not failing to render — they were correctly beneath notice, because station spacing was tied to the *canvas* pitch, so a course shorter than one gridunit collapsed to a fragment or a dot. The defect was drawing them as specks rather than either drawing them properly or not at all. Both halves of this record address that: contiguity fixes the geometry, the cull fixes the specks. +- **Raised by:** Jeroen, pair session 2026-07-27/28: *"the rivers should probably not be rendered at scale, but as a 5 px contiguous blue line curving to follow the river pixels as their center point. These lines should never be drawn in a pixel that is water blue, since that makes no sense"*, then *"the ones that are sub pixel are logically invisible from orbit"*, then *"if the width is five and the length is five it will still read as a dot"*. +- **Deferred — width varying with river size.** The existing ratified stream/tributary/trunk width grammar (0.6 / 1.2 / 2.4 px, tuned so a tributary joining a trunk reads as a join) **collapses to a uniform 5 px under this record**. Jeroen: *"I do like your suggestion of thinning shorter rivers (especially if that shows tributaries following those rules) but maybe that is for a future polish iteration."* Recorded so the flattening is understood as a deliberate deferral rather than a lost feature — and note the thinning idea composes naturally with the cull, since a thinner stroke lowers its own visibility threshold and would let smaller tributaries appear at the same rung. +- **Implementation:** Not yet built. Consumers: `step_canvas_annotation_layer.gd` (stroke width, contiguity, water-clip, cull — all four live here), which already holds the per-cell classification and the world→screen projection it needs. +- **Cross-reference:** [D-255](#d-255) (stepped ladder — premise (1) server-determines-content / premise (2) client-is-a-map-art-function, the split this record applies; and the 2026-07-26 extent inversion that made screen-pixel reasoning coherent per rung), [D-258](#d-258) (rung 0.5 — its amendment records the 375-course / 458-pixel measurement this record is built on), [D-227](#d-227) (derive-don't-store — culling is presentation, not derivation, so it is outside that rule). +- **Dissent:** None. + +--- + +*115 decisions (D-001 through D-261, excluding gaps). Last updated: 2026-07-28 (D-261 — river rendering: a fixed 5 px screen-space stroke through cell centres, never over water, culled below 15 px of on-screen length so the km threshold derives per rung from the stroke itself; client-side because the cull is measured in pixels the server cannot know).*