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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas rivers vanish at native resolution — 375 courses arrive, 0 drawn', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count.', 'backlog', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-06 14:43:24.765', NULL, '9597ed74aa67fb7ebeef78f60374d89c', 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; 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 ('06FT0TX2W0BA10PRR7NMJ2362M', 'epic', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Rung-0.5 expanded layer — whole-body hydrology + biome-from-orbit base (D-258)', 'D-258: the Atlas cascade gains a single expanded layer (''rung 0.5''), generated once per body from the rung-0 input pair (heightmap.png + reliefmap.png, never displayed), and every zoom tier below it (Region/District/Quarter/Block/Chunk) derives from that layer instead of independently re-deriving from the source files. Hydrology (drainage, course routing, lake fill) resolves ONCE on rung 0.5 and nowhere else -- it is a whole-body computation, not derivable per-window. Global displays rung 0.5 directly (biome-from-orbit, not a photograph). Same-session amendment: lake shorelines run the same shore-morphology code as ocean shorelines (coastline warp applied to both surfaces in the single rung-0.5 pass; shore-morphology gates key on proximity to water, not to ocean; sea-flavored types like TidalFlat/Estuarine-vs-Delta separated by tidal energy, a derived quantity, never by an is-it-the-ocean switch; salinity excluded from morphology entirely). This is a named, principled carve-out from D-227 derive-don''t-store: a whole-body flow solve is not locally computable, so it cannot be re-derived per window at any price -- storage here buys correctness, not convenience. Not yet ticketed prior to this epic (D-258''s own Implementation note). See governance/decisions/architecture.md#d-258. RE-SCOPE REQUIRED BEFORE ANY CHILD STARTS (2026-07-27, D-258 amendment). The structural rationale for this epic was disproven on evidence the day after it was written. D-258 claimed hydrology ''was not derivable at all'' per-window; in fact layer1.rs::run_layer1_with_moisture already solves drainage AND settled-equilibrium hydrology once per body, folds the filled surface into TerrainAnalysis, and every rung bilinearly samples it -- the code''s own comment calls it ''a coarse continuous primitive computed once, sampled fresh at every rung, never re-solved'' (mechanism B, D-255(f)). Compute-once-sample-everywhere already exists. What actually made Global look flat was serve_step_canvas_request zeroing Global''s wire extent (a pre-extent-inversion sentinel), producing a 2x1 canvas -- fixed 2026-07-27; once sized correctly Global reads as a world with no hydrology work at all. Rivers at Global were measured as negligible: 375 courses present, 458 of 518,400 pixels different vs courses-off, because at ~39.7 km/gridunit most courses are shorter than one gridunit. SURVIVES: biome un-summarisation (reliefmap as plurality), composition-on-descent, the conservation invariant, and the lake-shore amendment. WEAKENED: the stored expanded layer and its D-227 carve-out. The live question is no longer ''what does rung 0.5 cost'' but ''does biome un-summarisation need a stored layer at all, or does it ride the existing sample-fresh-at-every-rung mechanism''. Re-scope this epic and T-1212 against that question first. --- RE-SCOPED 2026-08-06 (Jeroen''s call) — the epic is unblocked, and its question has changed again. EVIDENCE. A descent ladder was captured on Ferrath (GJ820Bc) at native 3440x1440, anchored on land, one shot per rung, no overlays (scenarios atlas_GJ820Bc_land_* in tests/visual.json). Ferrath''s heightmap is 1024x512 over a 38,089 km circumference = 37.2 km per source pixel. Against that: Global 35.267 km/gridunit ~1:1 with the source pixel a real map Region 0.379 km/gridunit 98x finer flat wash District 0.0038 km/gridunit 9,800x finer flat wash Quarter/Block/Chunk finer still flat The Atlas is legible exactly where it SAMPLES the heightmap and flat everywhere it must INVENT. Every rung below Global is a single uniform colour field with dither noise; the courses/settlements readout is 0 at all of them. WHAT THIS SETTLES. D-258''s amendment (5) framed the live question as "does biome un-summarisation need a stored layer at all, or does it ride the existing sample-fresh-at-every-rung mechanism". The ladder answers a PRIOR question: un-summarisation is not happening in ANY form. There is no expansion to decide the storage policy for. Storage is therefore a downstream optimisation, not the decision this epic turns on. NEW SCOPE. Build the expansion first as a PURE FUNCTION, following the mechanism that already exists rather than inventing a second one: layer1.rs''s compute-once-sample-everywhere primitive (D-255(f) mechanism B) is the model, and the amendment established hydrology already works that way. Measure it. Add a stored layer ONLY if the measured cost forces it — and if it does, that is when the D-227 carve-out argument gets made, on numbers rather than on the disproven "not locally computable" claim. Order: T-1213 (the un-summarisation generator) is now the first child and is UNBLOCKED. T-1214 (hydrology onto rung 0.5) stays parked — the amendment showed that solve already runs once per body and does not need to move. T-1216 (Global biome-from-orbit palette) and T-1217 (lake shores through the shared shore path) survive unchanged; both were independent of the storage question. T-1212 does not gate this any more (see its own note).', 'backlog', 'high', NULL, NULL, 'D-258', '2026-07-26 21:53:56.448', '2026-08-06 15:59:03.757', NULL, '923892406469abdd3fb6ee9041ae1cbf', 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; 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 ('06FT0TYBD74TQNVKTJMKA8D9KM', 'task', '06FT0TX2W0BA10PRR7NMJ2362M', 'Measure rung-0.5 cost/size BEFORE implementing (compute + disk, whole-body)', 'MUST run and be reviewed before any other T-1211 child starts -- this measurement could reshape the rung-0.5 design, per the pair session''s explicit sequencing note. Measure, at minimum: (1) per-body derive cost for the expanded layer at a resolution sized so a whole body draws at 2 screen px per gridunit on a large display (the D-258 sizing rule) -- both single-body cold-derive time and the full ~271-body population sum; (2) per-body and total disk footprint if the layer is cached/stored (rung 0.5 is a named D-227 carve-out -- storage is deliberate, but its size must be known, not assumed); (3) whole-body hydrology solve cost on this layer (drainage + course routing + lake fill) at the same resolution, since D-258 requires this to run exactly once per body and nowhere else. Reference point: D-255''s own rung-0 always-keep tier estimate went from ~8.85 MB (measured against a stale ~18K-cell/body figure) to an estimated 226 MB (1080p) / ~900 MB (4K) once the extent inversion made Global viewport-sized -- D-255 amendment item 6 explicitly says ''re-measure against rung 0.5, not against this record.'' This ticket is that re-measurement. Report back to the team before T-1211''s other children are started; if the numbers are structurally bad (e.g. rung 0.5 at the sizing D-258 specifies costs an order of magnitude more than the old rung-0 model), that is grounds to revisit the resolution target with Jeroen before writing generator code. See governance/decisions/architecture.md#d-258 (rationale + Implementation note). SCOPE INVALIDATED 2026-07-27 (see T-1211 and the D-258 amendment). This ticket was written to measure the cost of moving a whole-body hydrology solve onto rung 0.5. That solve does not need to move -- it already runs once per body in layer1.rs and is sampled fresh at every rung. Do NOT run this measurement as written; it would price work that is not required. If a measurement is still wanted after T-1211 is re-scoped, the question is narrower: what does BIOME un-summarisation cost, and does it need storing at all. --- RETIRED AS A GATE 2026-08-06. This no longer blocks T-1213 or the T-1211 epic. It was already SCOPE INVALIDATED (2026-07-27) for pricing a hydrology move that does not need to happen. The 2026-08-06 descent ladder (see T-1211) closes the remaining reason to keep it as a gate: every rung below Global renders a flat wash, so biome un-summarisation is not happening at all. There is no artefact whose cost or disk footprint can be measured, because none is produced. The measurement question survives, but it is now DOWNSTREAM of T-1213 rather than upstream of it: once the un-summarisation exists as a pure function, measure THAT, and only then decide whether a stored layer is warranted. Re-scope this ticket to that measurement when T-1213 lands, or close it and let T-1213 carry its own measurement step.', 'backlog', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:06.825', '2026-08-06 15:59:12.825', NULL, 'e0d88ae134ccd57737d8eab2627e2b88', 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; 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'story', '06FT0TX2W0BA10PRR7NMJ2362M', 'Rung-0.5 expanded-layer generator (deterministic un-summarisation of rung 0)', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. --- UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is removed: that measurement priced a hydrology move that is not happening, and the descent ladder showed there is no expansion artefact to measure yet anyway. BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally computable" argument for the D-227 carve-out does not hold, because the whole-body solve it cited already runs once per body in layer1.rs and is sampled at every rung). So follow the mechanism that exists — D-255(f) mechanism B, compute-once-sample-everywhere — measure it, and only argue for storage on those numbers. Do NOT open with a cache. WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is 1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at 35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition that still downsamples back to that pixel''s summary (D-258''s conservation invariant, the binding gate). VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* (Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot the ladder and look at it; a green unit test proves nothing here. Note the ladder currently also reports courses=0 at every rung below Global — rivers vanish on descent. That is tracked separately as T-1239 and is NOT this ticket''s scope, but it will be visible in the same captures, so do not mistake it for a failure of the un-summarisation work.', 'backlog', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-06 15:59:39.623', NULL, 'd9bfabaf6b4f1751a9261995f84c0b7b', 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; 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'story', '06FT0TX2W0BA10PRR7NMJ2362M', 'Rung-0.5 expanded-layer generator (deterministic un-summarisation of rung 0)', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. --- UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is removed: that measurement priced a hydrology move that is not happening, and the descent ladder showed there is no expansion artefact to measure yet anyway. BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally computable" argument for the D-227 carve-out does not hold, because the whole-body solve it cited already runs once per body in layer1.rs and is sampled at every rung). So follow the mechanism that exists — D-255(f) mechanism B, compute-once-sample-everywhere — measure it, and only argue for storage on those numbers. Do NOT open with a cache. WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is 1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at 35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition that still downsamples back to that pixel''s summary (D-258''s conservation invariant, the binding gate). VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* (Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot the ladder and look at it; a green unit test proves nothing here. Note the ladder currently also reports courses=0 at every rung below Global — rivers vanish on descent. That is tracked separately as T-1239 and is NOT this ticket''s scope, but it will be visible in the same captures, so do not mistake it for a failure of the un-summarisation work.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-07 11:36:05.551', NULL, 'a3edd5306633b6247c1cd67bf700db69', 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; 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'story', '06FT0TX2W0BA10PRR7NMJ2362M', 'Rung-0.5 expanded-layer generator (deterministic un-summarisation of rung 0)', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. --- UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is removed: that measurement priced a hydrology move that is not happening, and the descent ladder showed there is no expansion artefact to measure yet anyway. BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally computable" argument for the D-227 carve-out does not hold, because the whole-body solve it cited already runs once per body in layer1.rs and is sampled at every rung). So follow the mechanism that exists — D-255(f) mechanism B, compute-once-sample-everywhere — measure it, and only argue for storage on those numbers. Do NOT open with a cache. WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is 1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at 35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition that still downsamples back to that pixel''s summary (D-258''s conservation invariant, the binding gate). VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* (Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot the ladder and look at it; a green unit test proves nothing here. Note the ladder currently also reports courses=0 at every rung below Global — rivers vanish on descent. That is tracked separately as T-1239 and is NOT this ticket''s scope, but it will be visible in the same captures, so do not mistake it for a failure of the un-summarisation work.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-07 11:36:20.464', NULL, '94ccf78431914ea836ba5d59170a3395', 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; 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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Biome/relief stipple-density layer on the terrain build (RimWorld technique 4)', 'Follow-up from T-1175''s assess-only item (2026-07-25, stig''s write-up): a stipple/texture-density layer carrying hills/mountains/forest on top of hue, driven by the already-wire-carried vegetation and elev_q L8 planes — presentation-only, D-255(e)-legal (texture-space dithering of already-derived per-cell values, deterministically seeded per cell coordinate + value so it is stable across cache hit/miss; never invents samples between server cells). Would live as a post-process in step_canvas_terrain_layer.gd::rebuild_from_canvas()''s Image.set_pixel build. Design questions to settle at pickup: (a) stipple dots inline in the existing per-cell loop (cheap, same O(wxh) pass) vs a second overlay pass (simpler code, doubles pixel-touch cost); (b) density from vegetation class directly vs a combination with elev_q — relief hachures and forest texture are two different visual grammars in the RimWorld reference, not one slider; (c) own legend toggle (TMP/MST/VEG overlay-bar pattern) vs always-on like the elevation lightness modifier. Reference: docs/design/references/rimworld-world-map-fluency.jpg. Related: T-1175, T-1162 (vegetation patchiness fields), D-255(e).', 'in_progress', 'low', NULL, 'client', NULL, '2026-07-25 13:24:54.152', '2026-08-07 12:44:21.620', NULL, 'c1560b624bd9a9d79fe919f282620f22', 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; 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local.', 'backlog', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-07 13:26:56.703', NULL, '45cb7553effbe05042fe7f3eb09e6483', 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; 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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas rivers vanish at native resolution — 375 courses arrive, 0 drawn', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count.', 'in_progress', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-13 22:12:59.623', NULL, 'a52ba3f39408c80765dca1eb58e79ae6', 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; 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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas rivers vanish at native resolution — 375 courses arrive, 0 drawn', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count. --- DIAGNOSED 2026-08-14. Not a client rendering bug. Both suspects in the original report are wrong, and so is the `team: client` label — the defect is a stale client-side disk cache (T-1183/D-255), invalidated by nothing that changed. MEASUREMENT. Same build, same scenario, same native 3440x1440, only the cache differs: stale cache courses=375 runs=180 longest=6.0px (~106 km) drawn=0 cold cache courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18 Server-side, at the same moment the cold capture ran: `river_cells=615 paths=123 courses=73 longest_path_cells=27 ta_w=512 ta_h=256`. The network is exactly as designed. During the STALE capture the server logged NO course production at all — the canvas never came from it. WHY IT LOOKED RESOLUTION-DEPENDENT. It isn''t. 960x540 resolves to an 814x407 canvas, a cache key never written before, so it MISSED and re-derived correctly (73 courses, 1,644 km trunk). 3440x1440 resolves to 1080x540, which HAD a cached entry from 2026-08-06 — written before T-1237 (4e503c356) replaced one-course- per-D8-hop with one-course-per-river. Every `.dat` payload for GJ820Bc predates that fix; the oldest is 2026-07-28. So the "high resolution" capture was replaying a pre-fix canvas: 375 hop fragments, none clearing D-261''s read-as-a-line floor, hence drawn=0. The ticket''s own note that the scale "moved the RIGHT way for visibility" was correct and was the clue — a legitimate cull could not explain it, because the cull was never the actor. The 375 / 180 / 0 chain also matches, digit for digit, the pre-fix measurement already written into `_cull_short`''s doc comment. That number was being re-read off a cache, not re-measured. ROOT CAUSE. The disk cache''s only invalidation signal is `project.yaml: version` (step_canvas_disk_cache.gd `current_schema_version()`). 4e503c356 changed how canvases are GENERATED but touched only the annotation layer, river_course.rs and step_canvas.rs — never project.yaml — so every hop-shaped entry stayed "valid". All 13 stale GJ820Bc entries are stamped 0.4.5, the then-current version. This is the fourth instance of the same class: project.yaml''s own comments record 0.4.2, 0.4.3, 0.4.4 and 0.4.5 as bumps forced by exactly this failure. FIX, three parts: 1. project.yaml 0.4.5 -> 0.4.6, forcing every pre-T-1237 entry to miss. This is what repairs a real player''s Atlas; clearing a local cache is not a fix. 2. tests/run-visual isolates `user://` per capture (XDG_DATA_HOME into .cache/visual-user-data, recreated each run). The harness was reading the developer''s persistent cache, so a capture could render a canvas built by a build that no longer existed — and every golden shot in that window silently inherited it. A visual test must exercise the tree it is run against. 3. tests/run-visual keeps server stderr (was `2>/dev/null` into an unlinked mktemp file). No tracing output from a capture was reachable, which is why "the server produced nothing" was invisible for eight days. Diagnostic left in place: the capture readout now prints `runs=` and `longest=` between `courses=` and `drawn=`, so the three stages of "arrived -> survived the water clip -> survived the length cull" can be told apart from a single capture. That distinction is what made this solvable, and its absence is what made the original report guess between two wrong suspects. FOLLOW-UPS worth their own tickets, not done here: - `current_schema_version()` reads `res://../project.yaml`, which does not exist in an exported build — it returns the "?.?.?" fallback, identical for every build, so a shipped game''s cache would never invalidate on version at all. - Nothing enforces the generation-change/version-bump pairing. Four occurrences suggests a check (e.g. a pre-push rule: canvas-generation paths touched => project.yaml version must move) rather than a fifth comment.', 'in_progress', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-14 21:13:15.779', NULL, 'ee86b57855c05540234c4b0f04ca2a18', 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; 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 ('06G0495WRHF8ADK82VR8CH1J8R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas disk cache never invalidates in an exported build — current_schema_version() falls back to ''?.?.?''', 'step_canvas_disk_cache.gd::current_schema_version() reads ProjectSettings.globalize_path(''res://'') + ''/../project.yaml''. That resolves to the repo-root file in a dev run (res:// = client/), but an exported build has no project.yaml one level above res://, so the function returns its ''?.?.?'' fallback. Every exported build therefore stamps and compares the SAME sentinel version, which means the schema-version invalidation path — the cache''s only invalidation signal — is inert in a shipped game: a canvas cached by one build is served forever by every later build. Found while diagnosing T-1239, where the same mechanism failed in dev for a different reason (the version simply was not bumped). Fix direction: bake the version into the client at export time (a generated const, ProjectSettings application/config/version, or an exported resource) rather than reading a repo file at runtime. Note test_current_schema_version_reads_project_yaml passes in dev and would not catch this — it asserts the non-fallback path, in the only environment where that path works.', 'backlog', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:17.188', '2026-08-14 21:19:17.188', NULL, '7880dfa6b9038360c78843ae9f6ca2e2', 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; 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'backlog', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 21:19:27.624', NULL, '5386de8d8c8120e17373cc1c1a59a9cd', 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; 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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas rivers vanish at native resolution — 375 courses arrive, 0 drawn', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count. --- DIAGNOSED 2026-08-14. Not a client rendering bug. Both suspects in the original report are wrong, and so is the `team: client` label — the defect is a stale client-side disk cache (T-1183/D-255), invalidated by nothing that changed. MEASUREMENT. Same build, same scenario, same native 3440x1440, only the cache differs: stale cache courses=375 runs=180 longest=6.0px (~106 km) drawn=0 cold cache courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18 Server-side, at the same moment the cold capture ran: `river_cells=615 paths=123 courses=73 longest_path_cells=27 ta_w=512 ta_h=256`. The network is exactly as designed. During the STALE capture the server logged NO course production at all — the canvas never came from it. WHY IT LOOKED RESOLUTION-DEPENDENT. It isn''t. 960x540 resolves to an 814x407 canvas, a cache key never written before, so it MISSED and re-derived correctly (73 courses, 1,644 km trunk). 3440x1440 resolves to 1080x540, which HAD a cached entry from 2026-08-06 — written before T-1237 (4e503c356) replaced one-course- per-D8-hop with one-course-per-river. Every `.dat` payload for GJ820Bc predates that fix; the oldest is 2026-07-28. So the "high resolution" capture was replaying a pre-fix canvas: 375 hop fragments, none clearing D-261''s read-as-a-line floor, hence drawn=0. The ticket''s own note that the scale "moved the RIGHT way for visibility" was correct and was the clue — a legitimate cull could not explain it, because the cull was never the actor. The 375 / 180 / 0 chain also matches, digit for digit, the pre-fix measurement already written into `_cull_short`''s doc comment. That number was being re-read off a cache, not re-measured. ROOT CAUSE. The disk cache''s only invalidation signal is `project.yaml: version` (step_canvas_disk_cache.gd `current_schema_version()`). 4e503c356 changed how canvases are GENERATED but touched only the annotation layer, river_course.rs and step_canvas.rs — never project.yaml — so every hop-shaped entry stayed "valid". All 13 stale GJ820Bc entries are stamped 0.4.5, the then-current version. This is the fourth instance of the same class: project.yaml''s own comments record 0.4.2, 0.4.3, 0.4.4 and 0.4.5 as bumps forced by exactly this failure. FIX, three parts: 1. project.yaml 0.4.5 -> 0.4.6, forcing every pre-T-1237 entry to miss. This is what repairs a real player''s Atlas; clearing a local cache is not a fix. 2. tests/run-visual isolates `user://` per capture (XDG_DATA_HOME into .cache/visual-user-data, recreated each run). The harness was reading the developer''s persistent cache, so a capture could render a canvas built by a build that no longer existed — and every golden shot in that window silently inherited it. A visual test must exercise the tree it is run against. 3. tests/run-visual keeps server stderr (was `2>/dev/null` into an unlinked mktemp file). No tracing output from a capture was reachable, which is why "the server produced nothing" was invisible for eight days. Diagnostic left in place: the capture readout now prints `runs=` and `longest=` between `courses=` and `drawn=`, so the three stages of "arrived -> survived the water clip -> survived the length cull" can be told apart from a single capture. That distinction is what made this solvable, and its absence is what made the original report guess between two wrong suspects. FOLLOW-UPS worth their own tickets, not done here: - `current_schema_version()` reads `res://../project.yaml`, which does not exist in an exported build — it returns the "?.?.?" fallback, identical for every build, so a shipped game''s cache would never invalidate on version at all. - Nothing enforces the generation-change/version-bump pairing. Four occurrences suggests a check (e.g. a pre-push rule: canvas-generation paths touched => project.yaml version must move) rather than a fifth comment.', 'done', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-14 21:20:27.835', NULL, 'fa2402988b35d9f0db6ffcf7cf7a81c2', 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; 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 ('06G0495WRHF8ADK82VR8CH1J8R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas disk cache never invalidates in an exported build — current_schema_version() falls back to ''?.?.?''', 'step_canvas_disk_cache.gd::current_schema_version() reads ProjectSettings.globalize_path(''res://'') + ''/../project.yaml''. That resolves to the repo-root file in a dev run (res:// = client/), but an exported build has no project.yaml one level above res://, so the function returns its ''?.?.?'' fallback. Every exported build therefore stamps and compares the SAME sentinel version, which means the schema-version invalidation path — the cache''s only invalidation signal — is inert in a shipped game: a canvas cached by one build is served forever by every later build. Found while diagnosing T-1239, where the same mechanism failed in dev for a different reason (the version simply was not bumped). Fix direction: bake the version into the client at export time (a generated const, ProjectSettings application/config/version, or an exported resource) rather than reading a repo file at runtime. Note test_current_schema_version_reads_project_yaml passes in dev and would not catch this — it asserts the non-fallback path, in the only environment where that path works.', 'in_progress', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:17.188', '2026-08-14 21:21:35.097', NULL, '0e36862b9cbd420d1f0d8114f7673cc9', 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; 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 ('06G0495WRHF8ADK82VR8CH1J8R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas disk cache never invalidates in an exported build — current_schema_version() falls back to ''?.?.?''', 'step_canvas_disk_cache.gd::current_schema_version() reads ProjectSettings.globalize_path(''res://'') + ''/../project.yaml''. That resolves to the repo-root file in a dev run (res:// = client/), but an exported build has no project.yaml one level above res://, so the function returns its ''?.?.?'' fallback. Every exported build therefore stamps and compares the SAME sentinel version, which means the schema-version invalidation path — the cache''s only invalidation signal — is inert in a shipped game: a canvas cached by one build is served forever by every later build. Found while diagnosing T-1239, where the same mechanism failed in dev for a different reason (the version simply was not bumped). Fix direction: bake the version into the client at export time (a generated const, ProjectSettings application/config/version, or an exported resource) rather than reading a repo file at runtime. Note test_current_schema_version_reads_project_yaml passes in dev and would not catch this — it asserts the non-fallback path, in the only environment where that path works.', 'done', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:17.188', '2026-08-14 21:35:35.623', NULL, '3bd4d5eaacf6a4792e00a644a3333535', 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; 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'in_progress', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 21:41:02.683', NULL, 'efd83e7e6e689653b411d77629c19985', 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; 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'in_progress', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 21:41:17.977', NULL, 'cb2da99199993e9c60322845ceaf22b8', 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; 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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'done', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 22:06:55.412', NULL, 'eb7ca0296fcb5255bdb5cc94e66c45d5', 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; 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'story', '06FT0TX2W0BA10PRR7NMJ2362M', 'Rung-0.5 expanded-layer generator (deterministic un-summarisation of rung 0)', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. --- UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is removed: that measurement priced a hydrology move that is not happening, and the descent ladder showed there is no expansion artefact to measure yet anyway. BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally computable" argument for the D-227 carve-out does not hold, because the whole-body solve it cited already runs once per body in layer1.rs and is sampled at every rung). So follow the mechanism that exists — D-255(f) mechanism B, compute-once-sample-everywhere — measure it, and only argue for storage on those numbers. Do NOT open with a cache. WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is 1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at 35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition that still downsamples back to that pixel''s summary (D-258''s conservation invariant, the binding gate). VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* (Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot the ladder and look at it; a green unit test proves nothing here. Note the ladder currently also reports courses=0 at every rung below Global — rivers vanish on descent. That is tracked separately as T-1239 and is NOT this ticket''s scope, but it will be visible in the same captures, so do not mistake it for a failure of the un-summarisation work. --- LADDER RE-SHOT COLD 2026-08-16 — the first ladder whose numbers can be trusted. Every previous ladder, including the one this ticket''s re-scope was written from, was captured against the developer''s persistent user:// cache (T-1239). The harness now isolates user:// per capture, so these are fresh derives. RESULT: the acceptance bar is NOT met. Region and below are still flat. Flatness, measured rather than eyeballed (.cache/t1213_flatness.py samples the terrain area, excluding the panels): rung distinct R std G std B std lum p1-p99 Global 1581 24.05 35.75 36.21 145.69 Region 2923 4.80 8.31 4.19 33.59 District 53 2.56 4.90 2.56 13.72 Quarter 46 2.42 4.78 2.42 11.01 Note Region carries MORE distinct colours than Global (2923 vs 1581) while holding a quarter of the structure. That is the dither/stipple pass adding colour noise, not information — so distinct-count is precisely the wrong metric, and the percentile luminance spread is the honest one. Structure falls ~92% from Global to Quarter. WHY IT IS FLAT — a channel mismatch, not a missing generator. Composition IS working in the data. The conservation test, run explicitly against real terrain, reports over a 2,048 m patch at the ladder''s own anchor: conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209} So D-258 invariant 3 (conservation) holds and invariant 2 (composition) is happening: 1.07% of the patch is a minority class. But composition perturbs `moisture_q` and `slope_q`, which resolve into `vegetation_class`. The base map draws neither. step_canvas_colorize.gd''s own comment states it: "The base layer reads hue from morphology and lightness from elev_q, and on a real body below Global that collapses: morphology resolves to ONE zone". The ladder scenarios pass `[]` for overlays deliberately (visual_scenarios.gd: "No overlays, deliberately: a colour ramp on top would mask the very thing being judged"), so the composed fields are never rendered in the shots that judge this work. The un-summarisation lands in channels the default view does not draw. That is why perfectly correct composition and a flat map coexist. THE CONSERVATION TEST WAS NOT PROTECTING ANYTHING (fixed here). Its "not a monoculture" assertion read: assert!(tally.len() > 1 || share == 1.0, ...) which is a TAUTOLOGY — a single-class tally has a 100% share by definition, so both branches are always satisfiable and the check could never fail, including in the exact case its message names ("or nothing was composed"). It was the only thing standing behind invariant 2, and it stood behind nothing. Now split into the two bounds the invariant actually has: majority > 50% (conservation, the ceiling on invention) and minority >= 0.1% (composition, the floor). Measured 1.07% against a 0.1% floor. It is also still `#[ignore]`d, so the binding acceptance gate for this epic runs only when someone types it. Worth moving into a harness that runs — the believability and window-derivation harnesses already load real bodies in the normal `cargo test` path, so the precedent exists. courses=0 BELOW GLOBAL IS NOT A BUG — it is arithmetic, and the ticket''s note attributing it to T-1239 is superseded. Global now reports courses=73 drawn=18 (T-1239 fixed). Region/District/Quarter report courses=0 because the hydrology network is 615 river cells on a 512x256 grid (74.4 km/cell) and a Region window is 489 x 205 km = 0.0217% of a 462M km2 body: expected river cells in view = 615 x 0.000217 = 0.13. Zero is the likely outcome, not a failure. The real observation underneath it is that 615 river cells on an Earth-sized body is a sparse network (~45,000 km of total watercourse), which is a believability question for hydrology, not a rendering one. THE OPEN DECISION — three readings, materially different work: (A) Make the DISPLAYED channels carry sub-source-pixel composition. The literal reading of this ticket''s acceptance bar ("at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition"). Means morphology and/or elev_q gain a fine tier, which is D-227/D-258 territory and the largest change. (B) Accept that the base view is morphology + elevation, and judge composition through the overlays — i.e. the ladder gains VEG/MST companion shots. Test change only, but it concedes that the default map stays flat below Global. (C) Raise inclusion density so the existing vegetation texture reads at map scale. Tuning only; 1.07% is near-invisible. Not chosen here — it needs Jeroen''s call, and it touches D-258.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-16 10:32:31.457', NULL, '21d816226e574f3b6489810453220b5e', 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; 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'story', '06FT0TX2W0BA10PRR7NMJ2362M', 'Rung-0.5 expanded-layer generator (deterministic un-summarisation of rung 0)', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258. --- UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is removed: that measurement priced a hydrology move that is not happening, and the descent ladder showed there is no expansion artefact to measure yet anyway. BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally computable" argument for the D-227 carve-out does not hold, because the whole-body solve it cited already runs once per body in layer1.rs and is sampled at every rung). So follow the mechanism that exists — D-255(f) mechanism B, compute-once-sample-everywhere — measure it, and only argue for storage on those numbers. Do NOT open with a cache. WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is 1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at 35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition that still downsamples back to that pixel''s summary (D-258''s conservation invariant, the binding gate). VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_* (Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays) in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440 via tests/run-visual --screenshot — they do not steal the desktop. Re-shoot the ladder and look at it; a green unit test proves nothing here. Note the ladder currently also reports courses=0 at every rung below Global — rivers vanish on descent. That is tracked separately as T-1239 and is NOT this ticket''s scope, but it will be visible in the same captures, so do not mistake it for a failure of the un-summarisation work. --- LADDER RE-SHOT COLD 2026-08-16 — the first ladder whose numbers can be trusted. Every previous ladder, including the one this ticket''s re-scope was written from, was captured against the developer''s persistent user:// cache (T-1239). The harness now isolates user:// per capture, so these are fresh derives. RESULT: the acceptance bar is NOT met. Region and below are still flat. Flatness, measured rather than eyeballed (.cache/t1213_flatness.py samples the terrain area, excluding the panels): rung distinct R std G std B std lum p1-p99 Global 1581 24.05 35.75 36.21 145.69 Region 2923 4.80 8.31 4.19 33.59 District 53 2.56 4.90 2.56 13.72 Quarter 46 2.42 4.78 2.42 11.01 Note Region carries MORE distinct colours than Global (2923 vs 1581) while holding a quarter of the structure. That is the dither/stipple pass adding colour noise, not information — so distinct-count is precisely the wrong metric, and the percentile luminance spread is the honest one. Structure falls ~92% from Global to Quarter. WHY IT IS FLAT — a channel mismatch, not a missing generator. Composition IS working in the data. The conservation test, run explicitly against real terrain, reports over a 2,048 m patch at the ladder''s own anchor: conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209} So D-258 invariant 3 (conservation) holds and invariant 2 (composition) is happening: 1.07% of the patch is a minority class. But composition perturbs `moisture_q` and `slope_q`, which resolve into `vegetation_class`. The base map draws neither. step_canvas_colorize.gd''s own comment states it: "The base layer reads hue from morphology and lightness from elev_q, and on a real body below Global that collapses: morphology resolves to ONE zone". The ladder scenarios pass `[]` for overlays deliberately (visual_scenarios.gd: "No overlays, deliberately: a colour ramp on top would mask the very thing being judged"), so the composed fields are never rendered in the shots that judge this work. The un-summarisation lands in channels the default view does not draw. That is why perfectly correct composition and a flat map coexist. THE CONSERVATION TEST WAS NOT PROTECTING ANYTHING (fixed here). Its "not a monoculture" assertion read: assert!(tally.len() > 1 || share == 1.0, ...) which is a TAUTOLOGY — a single-class tally has a 100% share by definition, so both branches are always satisfiable and the check could never fail, including in the exact case its message names ("or nothing was composed"). It was the only thing standing behind invariant 2, and it stood behind nothing. Now split into the two bounds the invariant actually has: majority > 50% (conservation, the ceiling on invention) and minority >= 0.1% (composition, the floor). Measured 1.07% against a 0.1% floor. It is also still `#[ignore]`d, so the binding acceptance gate for this epic runs only when someone types it. Worth moving into a harness that runs — the believability and window-derivation harnesses already load real bodies in the normal `cargo test` path, so the precedent exists. courses=0 BELOW GLOBAL IS NOT A BUG — it is arithmetic, and the ticket''s note attributing it to T-1239 is superseded. Global now reports courses=73 drawn=18 (T-1239 fixed). Region/District/Quarter report courses=0 because the hydrology network is 615 river cells on a 512x256 grid (74.4 km/cell) and a Region window is 489 x 205 km = 0.0217% of a 462M km2 body: expected river cells in view = 615 x 0.000217 = 0.13. Zero is the likely outcome, not a failure. The real observation underneath it is that 615 river cells on an Earth-sized body is a sparse network (~45,000 km of total watercourse), which is a believability question for hydrology, not a rendering one. THE OPEN DECISION — three readings, materially different work: (A) Make the DISPLAYED channels carry sub-source-pixel composition. The literal reading of this ticket''s acceptance bar ("at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition"). Means morphology and/or elev_q gain a fine tier, which is D-227/D-258 territory and the largest change. (B) Accept that the base view is morphology + elevation, and judge composition through the overlays — i.e. the ladder gains VEG/MST companion shots. Test change only, but it concedes that the default map stays flat below Global. (C) Raise inclusion density so the existing vegetation texture reads at map scale. Tuning only; 1.07% is near-invisible. Not chosen here — it needs Jeroen''s call, and it touches D-258. --- FIXED 2026-08-16 (option A). The un-summarisation was reaching the wire and falling off it one line before the renderer. ROOT CAUSE: `step_canvas_protocol.gd`''s decode dictionary listed every dense plane EXCEPT relief_q. The server has encoded it since 5eb394b36 (step_canvas.rs:1234) and the terrain layer has asked for it by name ever since (`canvas.get("relief_q")`), but the key was never put in the decoded dictionary, so the plane arrived nowhere. The server half of relief_q landed; the protocol half did not. That is why the deep rungs were flat. relief_q is the ONE field with signal below District — elev_q''s 80 m steps quantise sub-district detail away — and it was absent. Measured plane variety at District, before: {morphology: 1, elev_q: 11, relief_q: 0, moisture_q: 25, vegetation: 3}. A 0 there means ABSENT, not merely constant; the distinction is what made this diagnosable, and it did not exist until this session added the plane-variety readout. TWO CHANGES: 1. Decode relief_q (one line, client/scripts/protocol/step_canvas_protocol.gd). 2. Shade with it, SIGNED and continuously (step_canvas_colorize.gd). It was previously spent only as a stipple PROBABILITY, so a ridge and a plain differed in dot density, which at one pixel per cell reads as noise rather than landform — and `_ruggedness()` took absf(relief_q - 50), discarding the sign the server deliberately preserved ("a hollow and a rise are different ground"). Rises now lighten, hollows darken, under the stipple rather than instead of it. LADDER, before -> after (tooling/atlas-flatness, lum p1-p99): Global 145.69 -> 145.69 unchanged, and correct: relief_q is flat 50 at orbital rungs by construction Region 33.59 -> 71.01 2.1x District 13.72 -> 77.01 5.6x Quarter 11.01 -> 42.56 3.9x Structure retention Global->Quarter went from 7.6% to 29%. Plane variety after: Region relief_q 101 values, District 91, Quarter 33. HONEST STATE. The acceptance bar ("at Region, ~98 gridunits across a single source pixel must carry visible, deterministic, non-repeating composition") is MET. It is not yet cartographically finished: Region reads as heavy speckle because ruggedness is now real data rather than an elev_q-gradient fallback, so far more cells earn a stipple mark than the T-1194 tuning assumed (17,599 distinct colours there). District reads as soft blobby relief — form without directionality, since the field is isotropic noise with no hillshade direction and no drainage alignment. Both are tuning/grammar follow-ups on top of a channel that now carries signal, not blockers. REGRESSION COVER ADDED. Every protocol test passed throughout the weeks the plane was missing, because each asserted a field it already knew about and none asserted the SET. There is now a test that walks all eight dense planes of EncodedStepCanvas and fails if any is absent from the decode — verified by disabling the fix and watching it fail by name.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-16 13:05:35.162', NULL, 'e611e465ff1fd8de7b71b3de10702fec', 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; 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 ('06G0P0F9C3RKKK23ZHHTANCPZW', 'bug', '06FBPPMZNNEV052DBYYY3A897C', 'test_visibility_texture_update_performance flakes under gate load — min-of-7 was not enough', 'Wall-clock perf assertion in client/tests/test_fog_shader.gd:213 (best-of-7 < 0.5 ms, D-059 budget) failed the pre-push gate on 2026-08-16 at 0.606 ms, then passed 23/23 in isolation minutes later on the same build. The failing run was a full-suite pass inside the hook, which runs AFTER the tooling suite and cargo checks, so every one of the seven samples was inflated together by a loaded machine. My manual full-suite run of the identical tree passed 1830/0. Nothing in the pushed change touches the fog path (Atlas protocol decode, colorize, terrain layer, tooling). This is the SECOND recorded hardening cycle for the same failure mode: the estimator is already min-of-7 (T-1210) after median-of-5 flaked three times on 2026-07-26 when a cargo build saturated the cores (0.549/0.503/0.638). Taking the minimum defends against ONE slow sample, not against sustained saturation, which is precisely the condition the gate creates by design. Direction: measure CPU time rather than wall clock (Time.get_ticks_usec measures elapsed, so it prices the scheduler in), or make the budget assertion load-aware, or move the perf assertions out of the correctness gate into a dedicated bench run where the machine is quiet. Do NOT simply raise the 0.5 threshold -- that discards the budget D-059 set without deciding to.', 'backlog', 'medium', NULL, 'client', NULL, '2026-08-16 14:37:49.792', '2026-08-16 14:37:49.792', NULL, '513d27f28c0ff1a50bd36692fb96fa46', 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; 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local. --- NO LONGER INERT IN CONSEQUENCE (2026-08-16, found via T-1213). This ticket''s own scope note says the stale cutoff "is NOT the cause of the flat District/Quarter rungs". That was correct, and it is now the cause of something else: it caps REGION. With relief_q reaching the renderer (T-1213 fixed a missing protocol decode) and driving a hillshade, Region renders as a fine uniform stucco — texture at the pixel scale, no landform. District and Quarter, on the same code, read as real terrain (lum p1-p99 74.43 and 73.72 against Region''s 54.30). The reason is sampling, and it is this ticket: a Region cell is 379 m of ground, while the relief field''s own content sits in the 128-1024 m composition band plus 192 m inclusions. The field is at or below Nyquist for that sampling, so every neighbouring cell differs by an essentially independent draw and the gradient the hillshade reads is ALIASING rather than slope. Because min_wl_m defaults to 0 on the served path, nothing truncates the octaves Region cannot resolve — which is precisely the fix this ticket proposes ("derive the cutoff from the resolved canvas spacing rather than the rung cell size"). So the visible symptom to fix against is now concrete: Region should show the same kind of ridge-and-valley form District does, at its own scale, and it will once the octaves below its sampling resolution are cut. The T-1213 work made this observable; before it, relief_q never arrived and the rung was flat for a different reason entirely.', 'backlog', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-16 16:23:17.190', NULL, '1ed56273a1d81dbb6d0e1614598f744b', 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; 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local. --- NO LONGER INERT IN CONSEQUENCE (2026-08-16, found via T-1213). This ticket''s own scope note says the stale cutoff "is NOT the cause of the flat District/Quarter rungs". That was correct, and it is now the cause of something else: it caps REGION. With relief_q reaching the renderer (T-1213 fixed a missing protocol decode) and driving a hillshade, Region renders as a fine uniform stucco — texture at the pixel scale, no landform. District and Quarter, on the same code, read as real terrain (lum p1-p99 74.43 and 73.72 against Region''s 54.30). The reason is sampling, and it is this ticket: a Region cell is 379 m of ground, while the relief field''s own content sits in the 128-1024 m composition band plus 192 m inclusions. The field is at or below Nyquist for that sampling, so every neighbouring cell differs by an essentially independent draw and the gradient the hillshade reads is ALIASING rather than slope. Because min_wl_m defaults to 0 on the served path, nothing truncates the octaves Region cannot resolve — which is precisely the fix this ticket proposes ("derive the cutoff from the resolved canvas spacing rather than the rung cell size"). So the visible symptom to fix against is now concrete: Region should show the same kind of ridge-and-valley form District does, at its own scale, and it will once the octaves below its sampling resolution are cut. The T-1213 work made this observable; before it, relief_q never arrived and the rung was flat for a different reason entirely.', 'in_progress', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-17 07:08:40.539', NULL, '67165f7b4321d58fbb98b706b50e9058', 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; 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local. --- NO LONGER INERT IN CONSEQUENCE (2026-08-16, found via T-1213). This ticket''s own scope note says the stale cutoff "is NOT the cause of the flat District/Quarter rungs". That was correct, and it is now the cause of something else: it caps REGION. With relief_q reaching the renderer (T-1213 fixed a missing protocol decode) and driving a hillshade, Region renders as a fine uniform stucco — texture at the pixel scale, no landform. District and Quarter, on the same code, read as real terrain (lum p1-p99 74.43 and 73.72 against Region''s 54.30). The reason is sampling, and it is this ticket: a Region cell is 379 m of ground, while the relief field''s own content sits in the 128-1024 m composition band plus 192 m inclusions. The field is at or below Nyquist for that sampling, so every neighbouring cell differs by an essentially independent draw and the gradient the hillshade reads is ALIASING rather than slope. Because min_wl_m defaults to 0 on the served path, nothing truncates the octaves Region cannot resolve — which is precisely the fix this ticket proposes ("derive the cutoff from the resolved canvas spacing rather than the rung cell size"). So the visible symptom to fix against is now concrete: Region should show the same kind of ridge-and-valley form District does, at its own scale, and it will once the octaves below its sampling resolution are cut. The T-1213 work made this observable; before it, relief_q never arrived and the rung was flat for a different reason entirely.', 'in_progress', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-17 07:08:54.825', NULL, '4300fa437b31a5f8e2b89999c882fcd8', 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; 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local. --- NO LONGER INERT IN CONSEQUENCE (2026-08-16, found via T-1213). This ticket''s own scope note says the stale cutoff "is NOT the cause of the flat District/Quarter rungs". That was correct, and it is now the cause of something else: it caps REGION. With relief_q reaching the renderer (T-1213 fixed a missing protocol decode) and driving a hillshade, Region renders as a fine uniform stucco — texture at the pixel scale, no landform. District and Quarter, on the same code, read as real terrain (lum p1-p99 74.43 and 73.72 against Region''s 54.30). The reason is sampling, and it is this ticket: a Region cell is 379 m of ground, while the relief field''s own content sits in the 128-1024 m composition band plus 192 m inclusions. The field is at or below Nyquist for that sampling, so every neighbouring cell differs by an essentially independent draw and the gradient the hillshade reads is ALIASING rather than slope. Because min_wl_m defaults to 0 on the served path, nothing truncates the octaves Region cannot resolve — which is precisely the fix this ticket proposes ("derive the cutoff from the resolved canvas spacing rather than the rung cell size"). So the visible symptom to fix against is now concrete: Region should show the same kind of ridge-and-valley form District does, at its own scale, and it will once the octaves below its sampling resolution are cut. The T-1213 work made this observable; before it, relief_q never arrived and the rung was flat for a different reason entirely. --- CLOSED 2026-08-18 (9b146f9e1). The cutoff is derived from the resolved canvas spacing, which is what this ticket asked for — but two of its own premises turned out to be wrong, and both were corrected by measurement rather than argument. WRONG PREMISE 1 — "the fix is Nyquist". The ticket reasons in Nyquist terms ("District''s true Nyquist floor is ~7.6 m"). Implemented at 2 x spacing, Region barely moved: lum p1-p99 56.16 -> 59.73, and the relief field''s mean cell-to-cell gradient stayed at 18.24 steps. Nyquist is the ALIASING limit — the point below which a wave is indistinguishable from a slower one — not a legibility one. Two samples per cycle is unaliased and still renders jagged. The rungs that already read as terrain say what the real bar is: District reconstructs its finest surviving octave at 34 samples per cycle (gradient 0.30) and Quarter at 135 (0.07). Shipped at 8 samples per cycle (SMOOTH_SAMPLES_PER_WAVELENGTH), which puts Region at 1.08 gradient and 70.01 spread — the same order as the rungs that work. WRONG PREMISE 2 — my own, in the 2026-08-16 note above: I wrote that the stucco was the relief field aliasing, then after the 2x attempt failed I blamed the CLIENT stipple instead. Both were guesses. Surfacing the terrain layer''s own mean |relief_q gradient| in the capture readout answered it in one shot: 18.24 steps per cell at Region means 144 m of relief between NEIGHBOURING cells. The server was sending noise; the stipple was innocent. That diagnostic ships with the fix, for the same reason `plane_variety` shipped with T-1213 — a noisy field and a renderer inventing noise look identical, and one number separates them. THE TRADE, taken deliberately. An 8x floor also truncates the coast warp''s 2,048 and 1,024 m octaves at Region — the band T-1160 added for "one coastline at every rung". A test here asserted that band must survive; it now asserts the opposite, with the reasoning recorded in the test body: a 1,024 m coastline wiggle at 379.3 m per cell is 2.7 samples per cycle, so drawing it draws noise rather than coastline character, and a rung cannot show shape finer than its own cell. The warp is amplitude-capped sub-pixel on the working grid, so the loss is small. Reversal path, if a future pass disagrees: a relief-only floor threaded through derive_at_metres — NOT a lower multiple, which takes the stucco back. SCOPE ACTUALLY TOUCHED. Only the step-canvas served path. layer_proxy''s MIN_WL_BANDS_M, its const assert and its cache key are UNCHANGED — the ticket warned that a change there is not local, and none was needed: the fix is a spacing-derived floor at the canvas builder, a pure function of (rung, extent, body_radius), all three already in the step-canvas cache key. Global is exempt (its ~70 km floor would silence the whole warp band, and its orbital derive leaves relief_q flat at 50). District and Quarter floor below every octave in play and derive byte-identically — pinned by `floor_is_inert_at_the_rungs_that_already_resolve_their_band`, and confirmed by the derivation/believability goldens passing untouched. LEFT OPEN, not part of this ticket: Quarter''s contour banding (the stipple lattice quantising the hillshade into dotted rings), and MIN_WL_BANDS_M itself, which remains stale for the legacy layer_proxy district-window consumer. Neither blocks the ladder.', 'in_progress', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-18 18:48:58.815', NULL, '7594ac8497738d4474acf52db20a23fc', 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; 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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local. --- NO LONGER INERT IN CONSEQUENCE (2026-08-16, found via T-1213). This ticket''s own scope note says the stale cutoff "is NOT the cause of the flat District/Quarter rungs". That was correct, and it is now the cause of something else: it caps REGION. With relief_q reaching the renderer (T-1213 fixed a missing protocol decode) and driving a hillshade, Region renders as a fine uniform stucco — texture at the pixel scale, no landform. District and Quarter, on the same code, read as real terrain (lum p1-p99 74.43 and 73.72 against Region''s 54.30). The reason is sampling, and it is this ticket: a Region cell is 379 m of ground, while the relief field''s own content sits in the 128-1024 m composition band plus 192 m inclusions. The field is at or below Nyquist for that sampling, so every neighbouring cell differs by an essentially independent draw and the gradient the hillshade reads is ALIASING rather than slope. Because min_wl_m defaults to 0 on the served path, nothing truncates the octaves Region cannot resolve — which is precisely the fix this ticket proposes ("derive the cutoff from the resolved canvas spacing rather than the rung cell size"). So the visible symptom to fix against is now concrete: Region should show the same kind of ridge-and-valley form District does, at its own scale, and it will once the octaves below its sampling resolution are cut. The T-1213 work made this observable; before it, relief_q never arrived and the rung was flat for a different reason entirely. --- CLOSED 2026-08-18 (9b146f9e1). The cutoff is derived from the resolved canvas spacing, which is what this ticket asked for — but two of its own premises turned out to be wrong, and both were corrected by measurement rather than argument. WRONG PREMISE 1 — "the fix is Nyquist". The ticket reasons in Nyquist terms ("District''s true Nyquist floor is ~7.6 m"). Implemented at 2 x spacing, Region barely moved: lum p1-p99 56.16 -> 59.73, and the relief field''s mean cell-to-cell gradient stayed at 18.24 steps. Nyquist is the ALIASING limit — the point below which a wave is indistinguishable from a slower one — not a legibility one. Two samples per cycle is unaliased and still renders jagged. The rungs that already read as terrain say what the real bar is: District reconstructs its finest surviving octave at 34 samples per cycle (gradient 0.30) and Quarter at 135 (0.07). Shipped at 8 samples per cycle (SMOOTH_SAMPLES_PER_WAVELENGTH), which puts Region at 1.08 gradient and 70.01 spread — the same order as the rungs that work. WRONG PREMISE 2 — my own, in the 2026-08-16 note above: I wrote that the stucco was the relief field aliasing, then after the 2x attempt failed I blamed the CLIENT stipple instead. Both were guesses. Surfacing the terrain layer''s own mean |relief_q gradient| in the capture readout answered it in one shot: 18.24 steps per cell at Region means 144 m of relief between NEIGHBOURING cells. The server was sending noise; the stipple was innocent. That diagnostic ships with the fix, for the same reason `plane_variety` shipped with T-1213 — a noisy field and a renderer inventing noise look identical, and one number separates them. THE TRADE, taken deliberately. An 8x floor also truncates the coast warp''s 2,048 and 1,024 m octaves at Region — the band T-1160 added for "one coastline at every rung". A test here asserted that band must survive; it now asserts the opposite, with the reasoning recorded in the test body: a 1,024 m coastline wiggle at 379.3 m per cell is 2.7 samples per cycle, so drawing it draws noise rather than coastline character, and a rung cannot show shape finer than its own cell. The warp is amplitude-capped sub-pixel on the working grid, so the loss is small. Reversal path, if a future pass disagrees: a relief-only floor threaded through derive_at_metres — NOT a lower multiple, which takes the stucco back. SCOPE ACTUALLY TOUCHED. Only the step-canvas served path. layer_proxy''s MIN_WL_BANDS_M, its const assert and its cache key are UNCHANGED — the ticket warned that a change there is not local, and none was needed: the fix is a spacing-derived floor at the canvas builder, a pure function of (rung, extent, body_radius), all three already in the step-canvas cache key. Global is exempt (its ~70 km floor would silence the whole warp band, and its orbital derive leaves relief_q flat at 50). District and Quarter floor below every octave in play and derive byte-identically — pinned by `floor_is_inert_at_the_rungs_that_already_resolve_their_band`, and confirmed by the derivation/believability goldens passing untouched. LEFT OPEN, not part of this ticket: Quarter''s contour banding (the stipple lattice quantising the hillshade into dotted rings), and MIN_WL_BANDS_M itself, which remains stale for the legacy layer_proxy district-window consumer. Neither blocks the ladder.', 'done', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-18 18:49:07.176', NULL, 'b79d8865e3da7d586bf8c724de2d34b2', 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; 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 ('06G1R8WZ987YCP1ARANSA70R0W', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Seed variance: 68% of bodies are tectonics:low — is the categorical monoculture intended?', 'Measured 2026-08-20 over all 2,625 body definitions in wiki/star-systems/*/bodies/*/index.md. The seed''s CONTINUOUS axes vary richly: seed is unique per body, distance_au 716 distinct values, polar_ice_lat 691, axial_tilt_deg 578, land_fraction 460. The CATEGORICAL axes do not: tectonics 4 values with 67.8% ''low'', atmosphere 4 with 66.3% ''none'', planet_class 13 with 51.1% ''frozen'', substrate 3 with 51.1% ''ice'', geothermal_flux 3 with 91.2% ''low''. Those categoricals are what gate the morphology and biome branches, so a mostly-low-tectonics world is mostly FLAT BY THE SEED, before any rendering question. This matters because a full session (2026-08-16..18) went into making the Atlas render terrain legibly at the deep rungs, and composition.rs already states the honest position: ''where the terrain is flat, flat is the honest answer, and the morphology variety visible at Global comes from places that actually have slope''. If two thirds of bodies genuinely have no slope, there is a ceiling on what renderer work can deliver and it is set in the seed. QUESTION TO ANSWER FIRST, before any change: is the distribution intended? A galaxy where most worlds are dull and the interesting ones stand out is a legitimate and probably good design; an authoring pass that defaulted tectonics to ''low'' and moved on is not. Check how the values were produced (scaffold_bodies.py / the authoring pass) before touching them. NOT a defect report -- two things that looked like defects in the same measurement turned out deliberate (chemosynthetic:false is a namespace reservation for dextro-DNA biochemistry once geology and nature spawn to the 1x1m pixel; enabled:false on ~65% is staged rollout, clean planet types first then generator scripts for the others). Method caveat: the aggregator reads scalar frontmatter only, so list-valued fields are unreliable -- atmosphere_color first read as ''100% null'' and that was a parser artefact. Full measurement and method: docs/wiki-structure-findings.md.', 'backlog', 'medium', NULL, 'server', NULL, '2026-08-19 22:28:11.722', '2026-08-19 22:28:11.722', NULL, 'd5f89318c9d9845ae46aa165a731821f', 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; 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 ('06G1RB7GSJA4379EAQDZPQQDN8', 'initiative', NULL, 'Roadmap that carries intent — where the game is going, not just what order the work runs in', 'The project has a work breakdown and calls it a roadmap. The cascade owns ORDER, the ticket tree owns DECOMPOSITION, the DQR tree owns individual RULINGS — and none of them answer what the game is going to be, what the big unsolved problems are, or what is deliberately deferred. Pick-up instructions are in the appended body: the first pass is harvest/interview/investigate-form/propose-with-options, and EPICS ARE AN OUTPUT of that pass, not an input. Do not open this by inventing an epic list.', 'backlog', 'high', NULL, 'meta', NULL, '2026-08-19 22:38:22.412', '2026-08-19 22:38:22.412', NULL, 'ec77d9e27fa2cad2c124a8ae196e11d4', 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; 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 ('06G1RB7GSJA4379EAQDZPQQDN8', 'initiative', NULL, 'Roadmap that carries intent — where the game is going, not just what order the work runs in', 'The project has a work breakdown and calls it a roadmap. The cascade owns ORDER, the ticket tree owns DECOMPOSITION, the DQR tree owns individual RULINGS — and none of them answer what the game is going to be, what the big unsolved problems are, or what is deliberately deferred. Pick-up instructions are in the appended body: the first pass is harvest/interview/investigate-form/propose-with-options, and EPICS ARE AN OUTPUT of that pass, not an input. Do not open this by inventing an epic list. --- WHY THIS EXISTS The project has a work breakdown and calls it a roadmap. It is not one. What exists today, and what each is good at: - The Development Cascade (T-745 -> six phase epics, D-166): strict ORDER. It says what must be finished before what, and it is enforced. It does not say what the thing being built is, or why a phase is worth its cost. - The ticket tree (initiative/epic/story/task): decomposition of work already decided on. It answers "what is left in this batch", never "is this batch the right thing". - The DQR tree (governance/decisions|questions|rejected): individual rulings with rationale, each excellent in isolation. 260+ D-records do not compose into a direction; a reader can know every decision and still not know where the game is going. - Briefings, workshops, discussions: rich, but point-in-time and per-agent. None of these answer the questions someone actually asks about a roadmap: what is this game going to BE, what are the big unsolved problems between here and that, in what order do they unlock each other, what does "done" look like for each, and what is deliberately not being solved yet. THE EVIDENCE THAT THIS IS A REAL GAP, not a tidiness urge: On 2026-08-20, in one conversation, three roadmap-level facts surfaced that exist in NO artefact in this repo: 1. The economics tree is not just trade-sim data — once geology and nature spawn to the 1x1 m pixel, the economics information GENERATES WORLD CONTENT. Production chains decide what is physically on the ground. 2. `chemosynthetic: false` on every body is a namespace RESERVATION for dextro-DNA-style biochemistry at that same future tier — not a dead field. 3. `enabled: false` on ~65% of bodies is staged rollout: learn the clean planet types first, then add generator scripts for the other types and the playable count rises. Two of those three were written up as suspected DEFECTS by an agent reading the repo carefully, because nothing recorded them as intent. That is the cost, and it recurs: the roadmap lives in one person''s head and leaks out only when someone happens to ask the right question. WHAT THIS INITIATIVE IS FOR Produce a roadmap artefact that carries INTENT and SHAPE, in a form that survives being read by someone (or something) with no memory of the conversations that produced it — and that stays honest as the project moves. Explicitly NOT: another ticket hierarchy, a Gantt chart, dates, or a restatement of the cascade. The cascade already owns order; this owns meaning. HOW TO PICK THIS UP — READ THIS BEFORE CREATING ANY EPICS The first work is INVESTIGATION AND PLANNING, not implementation, and not epic-cutting. Do not open this initiative by inventing a list of epics; the epic list is an OUTPUT of the first pass, not an input to it. Sequence: 1. HARVEST. Mine what already exists for roadmap-shaped content that was never collected: docs/workshops/, docs/discussions/, docs/design/, the briefings, the D-records'' "why" sections, the phase-epic descriptions, and the conversational hints that only appear in commit messages and ticket notes. Expect the good material to exist and be scattered — this project documents heavily. The gap is composition, not absence. 2. INTERVIEW. Whatever the harvest cannot answer, ask Jeroen — directly and in batches. The three facts above emerged from ordinary questions; assume more are waiting behind questions nobody has asked. Record the answers as they are given, before interpreting them. 3. INVESTIGATE FORM. Do not assume markdown-in-docs/ is right. Options worth weighing: a single narrative document; a per-phase "what this buys and why" layer attached to the existing phase epics; a D-record class for direction rather than decisions; something queryable through pql alongside the DQR tree; a visual map. Judge each against the failure mode this exists to prevent — a reader who knows every decision and still cannot state the direction — and against staying current without ceremony. 4. PROPOSE, WITH OPTIONS. Bring 2-3 concrete shapes with trade-offs and a recommendation. This is a design decision about the project''s own memory; it deserves a D-record and Jeroen''s ruling, not an agent''s unilateral pick. 5. ONLY THEN CUT EPICS, against the chosen shape and what the harvest showed is missing. Epics come last because the work is unknown until steps 1-4 have run. ACCEPTANCE — the artefact earns its keep if: - A fresh session can read it and state, without asking, what the game is trying to be and what the next three big problems are. - Facts of the kind listed above (forward reservations, staged gates, "this system is really for that future thing") have an obvious home, so the next one gets written down instead of surfacing years later in a chat. - It is cheap enough to keep current that it actually gets updated. A roadmap that rots is worse than none, because it lies with authority. - It does not duplicate the cascade, the ticket tree, or the DQR records. If a section restates any of those, it belongs there instead. RELATED: D-166 (cascade order), T-745 (the cascade initiative), and this session''s docs/wiki-structure-findings.md, whose root-cause finding was the same shape — the information existed and nothing pointed at it.', 'backlog', 'high', NULL, 'meta', NULL, '2026-08-19 22:38:22.412', '2026-08-19 22:38:30.454', NULL, 'f787c3a0250b47f88079efda1b22bb97', 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; 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 ('06G1RB7GSJA4379EAQDZPQQDN8', 'initiative', NULL, 'Roadmap that carries intent — where the game is going, not just what order the work runs in', 'The project has a work breakdown and calls it a roadmap. The cascade owns ORDER, the ticket tree owns DECOMPOSITION, the DQR tree owns individual RULINGS — and none of them answer what the game is going to be, what the big unsolved problems are, or what is deliberately deferred. Pick-up instructions are in the appended body: the first pass is harvest/interview/investigate-form/propose-with-options, and EPICS ARE AN OUTPUT of that pass, not an input. Do not open this by inventing an epic list. --- WHY THIS EXISTS The project has a work breakdown and calls it a roadmap. It is not one. What exists today, and what each is good at: - The Development Cascade (T-745 -> six phase epics, D-166): strict ORDER. It says what must be finished before what, and it is enforced. It does not say what the thing being built is, or why a phase is worth its cost. - The ticket tree (initiative/epic/story/task): decomposition of work already decided on. It answers "what is left in this batch", never "is this batch the right thing". - The DQR tree (governance/decisions|questions|rejected): individual rulings with rationale, each excellent in isolation. 260+ D-records do not compose into a direction; a reader can know every decision and still not know where the game is going. - Briefings, workshops, discussions: rich, but point-in-time and per-agent. None of these answer the questions someone actually asks about a roadmap: what is this game going to BE, what are the big unsolved problems between here and that, in what order do they unlock each other, what does "done" look like for each, and what is deliberately not being solved yet. THE EVIDENCE THAT THIS IS A REAL GAP, not a tidiness urge: On 2026-08-20, in one conversation, three roadmap-level facts surfaced that exist in NO artefact in this repo: 1. The economics tree is not just trade-sim data — once geology and nature spawn to the 1x1 m pixel, the economics information GENERATES WORLD CONTENT. Production chains decide what is physically on the ground. 2. `chemosynthetic: false` on every body is a namespace RESERVATION for dextro-DNA-style biochemistry at that same future tier — not a dead field. 3. `enabled: false` on ~65% of bodies is staged rollout: learn the clean planet types first, then add generator scripts for the other types and the playable count rises. Two of those three were written up as suspected DEFECTS by an agent reading the repo carefully, because nothing recorded them as intent. That is the cost, and it recurs: the roadmap lives in one person''s head and leaks out only when someone happens to ask the right question. WHAT THIS INITIATIVE IS FOR Produce a roadmap artefact that carries INTENT and SHAPE, in a form that survives being read by someone (or something) with no memory of the conversations that produced it — and that stays honest as the project moves. Explicitly NOT: another ticket hierarchy, a Gantt chart, dates, or a restatement of the cascade. The cascade already owns order; this owns meaning. HOW TO PICK THIS UP — READ THIS BEFORE CREATING ANY EPICS The first work is INVESTIGATION AND PLANNING, not implementation, and not epic-cutting. Do not open this initiative by inventing a list of epics; the epic list is an OUTPUT of the first pass, not an input to it. Sequence: 1. HARVEST. Mine what already exists for roadmap-shaped content that was never collected: docs/workshops/, docs/discussions/, docs/design/, the briefings, the D-records'' "why" sections, the phase-epic descriptions, and the conversational hints that only appear in commit messages and ticket notes. Expect the good material to exist and be scattered — this project documents heavily. The gap is composition, not absence. 2. INTERVIEW. Whatever the harvest cannot answer, ask Jeroen — directly and in batches. The three facts above emerged from ordinary questions; assume more are waiting behind questions nobody has asked. Record the answers as they are given, before interpreting them. 3. INVESTIGATE FORM. Do not assume markdown-in-docs/ is right. Options worth weighing: a single narrative document; a per-phase "what this buys and why" layer attached to the existing phase epics; a D-record class for direction rather than decisions; something queryable through pql alongside the DQR tree; a visual map. Judge each against the failure mode this exists to prevent — a reader who knows every decision and still cannot state the direction — and against staying current without ceremony. 4. PROPOSE, WITH OPTIONS. Bring 2-3 concrete shapes with trade-offs and a recommendation. This is a design decision about the project''s own memory; it deserves a D-record and Jeroen''s ruling, not an agent''s unilateral pick. 5. ONLY THEN CUT EPICS, against the chosen shape and what the harvest showed is missing. Epics come last because the work is unknown until steps 1-4 have run. ACCEPTANCE — the artefact earns its keep if: - A fresh session can read it and state, without asking, what the game is trying to be and what the next three big problems are. - Facts of the kind listed above (forward reservations, staged gates, "this system is really for that future thing") have an obvious home, so the next one gets written down instead of surfacing years later in a chat. - It is cheap enough to keep current that it actually gets updated. A roadmap that rots is worse than none, because it lies with authority. - It does not duplicate the cascade, the ticket tree, or the DQR records. If a section restates any of those, it belongs there instead. RELATED: D-166 (cascade order), T-745 (the cascade initiative), and this session''s docs/wiki-structure-findings.md, whose root-cause finding was the same shape — the information existed and nothing pointed at it. --- THE MECHANISM THAT CREATES THIS GAP, from Jeroen (2026-08-20): "I tend to restrict future side quests to not confuse your context. I should have you file tickets at least." That is the root cause, and it is a REASONABLE practice producing a bad outcome. Withholding forward-looking intent keeps a working session focused — a real benefit, not a mistake. But the intent then exists only in conversation, and conversations are not artefacts. Every fact in the list above reached this repo by accident: someone happened to ask, on a day when it happened to be relevant. The resolution is not "share everything" — that would trade focus for memory. It is to SEPARATE THE TWO CHANNELS: - Working context stays narrow. Unchanged. - Forward intent gets FILED, not discussed. A ticket, a note on an existing ticket, or a roadmap entry — written at the moment it surfaces, without pulling the current task sideways. So whatever form this initiative lands on must be CHEAP TO APPEND TO from inside an unrelated session. If recording "this field is reserved for a future tier" requires opening a planning ritual, it will not happen while someone is midway through a rendering bug — which is exactly when these facts surface. Design implication, carried into step 3 (INVESTIGATE FORM): weight the options by cost-to-append, not just by cost-to-read. An artefact that is pleasant to read and expensive to update will rot, and this project already has heavily-documented trees that stayed accurate precisely because updating them was one line.', 'backlog', 'high', NULL, 'meta', NULL, '2026-08-19 22:38:22.412', '2026-08-19 22:39:34.248', NULL, '69d785aeb9a77c4c9296c6b404237dfb', 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; 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 ('06G1R8WZ987YCP1ARANSA70R0W', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Seed variance: 68% of bodies are tectonics:low — is the categorical monoculture intended?', 'Measured 2026-08-20 over all 2,625 body definitions in wiki/star-systems/*/bodies/*/index.md. The seed''s CONTINUOUS axes vary richly: seed is unique per body, distance_au 716 distinct values, polar_ice_lat 691, axial_tilt_deg 578, land_fraction 460. The CATEGORICAL axes do not: tectonics 4 values with 67.8% ''low'', atmosphere 4 with 66.3% ''none'', planet_class 13 with 51.1% ''frozen'', substrate 3 with 51.1% ''ice'', geothermal_flux 3 with 91.2% ''low''. Those categoricals are what gate the morphology and biome branches, so a mostly-low-tectonics world is mostly FLAT BY THE SEED, before any rendering question. This matters because a full session (2026-08-16..18) went into making the Atlas render terrain legibly at the deep rungs, and composition.rs already states the honest position: ''where the terrain is flat, flat is the honest answer, and the morphology variety visible at Global comes from places that actually have slope''. If two thirds of bodies genuinely have no slope, there is a ceiling on what renderer work can deliver and it is set in the seed. QUESTION TO ANSWER FIRST, before any change: is the distribution intended? A galaxy where most worlds are dull and the interesting ones stand out is a legitimate and probably good design; an authoring pass that defaulted tectonics to ''low'' and moved on is not. Check how the values were produced (scaffold_bodies.py / the authoring pass) before touching them. NOT a defect report -- two things that looked like defects in the same measurement turned out deliberate (chemosynthetic:false is a namespace reservation for dextro-DNA biochemistry once geology and nature spawn to the 1x1m pixel; enabled:false on ~65% is staged rollout, clean planet types first then generator scripts for the others). Method caveat: the aggregator reads scalar frontmatter only, so list-valued fields are unreliable -- atmosphere_color first read as ''100% null'' and that was a parser artefact. Full measurement and method: docs/wiki-structure-findings.md. --- MATERIAL CORRECTION (2026-08-20, same day): tectonics is DERIVED, not authored. `tooling/planet-gen/body_definition_parser.py:563` reads `tectonics = tectonic_map.get(planet_class, "low")`, with an override hook on the next line. So the "67.8% tectonics: low" measured above is not an authoring decision at all — it is a projection of the planet_class distribution (51.1% frozen, and whatever else maps to low) through a fixed lookup, plus a default of "low" for any class not in the map. That changes the question this ticket asks. It is NOT "did the authoring pass default tectonics and move on". It is: 1. Is the PLANET_CLASS distribution intended? That is the real authored axis, and it is where variance would have to come from. 2. Is the class -> tectonics map right? A lookup that sends most classes to "low", plus a "low" default for unmapped classes, concentrates the output further than the input. Check the map''s spread before blaming the data. 3. Is the override hook used at all? If no body sets it, the map is the whole story. Found by a cold-context agent reading body_definition_parser.py while testing Skill(wiki); verified directly afterwards. Worth noting how it was missed: the original measurement scanned frontmatter VALUES and never asked where they came from, so a derived field was read as an authored one. Same class of error as the scalar-only parser caveat already recorded — a scan tells you what a field says, never who decided it.', 'backlog', 'medium', NULL, 'server', NULL, '2026-08-19 22:28:11.722', '2026-08-19 22:45:45.557', NULL, '7a1737a72bba4913d5e8f5fa78e44312', 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; 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 ('06G1R8WZ987YCP1ARANSA70R0W', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Seed variance: 68% of bodies are tectonics:low — is the categorical monoculture intended?', 'Measured 2026-08-20 over all 2,625 body definitions in wiki/star-systems/*/bodies/*/index.md. The seed''s CONTINUOUS axes vary richly: seed is unique per body, distance_au 716 distinct values, polar_ice_lat 691, axial_tilt_deg 578, land_fraction 460. The CATEGORICAL axes do not: tectonics 4 values with 67.8% ''low'', atmosphere 4 with 66.3% ''none'', planet_class 13 with 51.1% ''frozen'', substrate 3 with 51.1% ''ice'', geothermal_flux 3 with 91.2% ''low''. Those categoricals are what gate the morphology and biome branches, so a mostly-low-tectonics world is mostly FLAT BY THE SEED, before any rendering question. This matters because a full session (2026-08-16..18) went into making the Atlas render terrain legibly at the deep rungs, and composition.rs already states the honest position: ''where the terrain is flat, flat is the honest answer, and the morphology variety visible at Global comes from places that actually have slope''. If two thirds of bodies genuinely have no slope, there is a ceiling on what renderer work can deliver and it is set in the seed. QUESTION TO ANSWER FIRST, before any change: is the distribution intended? A galaxy where most worlds are dull and the interesting ones stand out is a legitimate and probably good design; an authoring pass that defaulted tectonics to ''low'' and moved on is not. Check how the values were produced (scaffold_bodies.py / the authoring pass) before touching them. NOT a defect report -- two things that looked like defects in the same measurement turned out deliberate (chemosynthetic:false is a namespace reservation for dextro-DNA biochemistry once geology and nature spawn to the 1x1m pixel; enabled:false on ~65% is staged rollout, clean planet types first then generator scripts for the others). Method caveat: the aggregator reads scalar frontmatter only, so list-valued fields are unreliable -- atmosphere_color first read as ''100% null'' and that was a parser artefact. Full measurement and method: docs/wiki-structure-findings.md. --- MATERIAL CORRECTION (2026-08-20, same day): tectonics is DERIVED, not authored. `tooling/planet-gen/body_definition_parser.py:563` reads `tectonics = tectonic_map.get(planet_class, "low")`, with an override hook on the next line. So the "67.8% tectonics: low" measured above is not an authoring decision at all — it is a projection of the planet_class distribution (51.1% frozen, and whatever else maps to low) through a fixed lookup, plus a default of "low" for any class not in the map. That changes the question this ticket asks. It is NOT "did the authoring pass default tectonics and move on". It is: 1. Is the PLANET_CLASS distribution intended? That is the real authored axis, and it is where variance would have to come from. 2. Is the class -> tectonics map right? A lookup that sends most classes to "low", plus a "low" default for unmapped classes, concentrates the output further than the input. Check the map''s spread before blaming the data. 3. Is the override hook used at all? If no body sets it, the map is the whole story. Found by a cold-context agent reading body_definition_parser.py while testing Skill(wiki); verified directly afterwards. Worth noting how it was missed: the original measurement scanned frontmatter VALUES and never asked where they came from, so a derived field was read as an authored one. Same class of error as the scalar-only parser caveat already recorded — a scan tells you what a field says, never who decided it. --- THE VARIANCE SPLIT IS NOW FULLY EXPLAINED (2026-08-20, after reading body_definition_parser.py and scaffold_bodies.py properly rather than grepping them). Body frontmatter is written once by `scaffold_bodies.py`, which reads the SYSTEM page''s Celestial Bodies table and resolves each field through a documented priority order (body_definition_parser.py header): 1. override dict (hand-authored, special cases like Sol) 2. direct read (the field is verbatim in the bodies table) 3. derived (computed — e.g. tectonics = lookup off planet_class) 4. inferred (implied by a combination of fields) 5. randomised (SEEDED, within planet-class constraints — the "rand" sentinel) That resolves the measurement in this ticket exactly: - The CONTINUOUS axes vary richly (seed unique, distance_au 716 distinct, axial_tilt_deg 578, land_fraction 460) because they fall through to tier 5 and are seeded-random per body. - The CATEGORICAL axes are concentrated because they come from tier 2 — read from the bodies table, i.e. from systems.db — and tier 3 derives from them. So the variance question does not belong to the wiki at all. It belongs to the BODIES CATALOG in systems.db, authored through the atlas CLI, and to the class->tectonics map. Whoever picks this up should start at `tooling/atlas add-body` / `author-system` and the catalog''s planet_class distribution, NOT at the wiki pages, which are a faithful projection of it. Second-order note worth keeping: this ticket has now been corrected twice from the same root cause — measuring VALUES without reading the CODE that produced them. First correction: tectonics is derived. Second: the whole continuous/ categorical split is an artefact of the resolution tiers. Both were available in a docstring the original pass grepped but never read.', 'backlog', 'medium', NULL, 'server', NULL, '2026-08-19 22:28:11.722', '2026-08-19 22:52:46.636', NULL, '65db5461d46f8f84aed68526113025a6', 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; 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 ('06G1RWR7M1H1PC3TY2RT7AC174', 'story', '06FBPPMZNNEV052DBYYY3A897C', 'Investigate retooling Python tooling into a Rust CLI (Q-124)', 'Answer Q-124. Investigation and costing FIRST — do not open with a rewrite. Price the cheap alternative before recommending Rust: a single Python dispatcher entrypoint (one command, many subcommands) would resolve the permission-prompt friction and most of the startup cost without touching the numerics. If that is enough, say so and close Q-124 with it. Scope order if Rust is chosen: port the check/gate family first (check-client-version, check-canvas-version, check-systems-db-stamp, check-dataflow-graph) — small, pure, no numerics, highest prompt-friction, runs on every push. Leave planet-gen numerics (numpy/scipy/PIL) in Python behind a stable interface. Hard constraint: import_economics is stamped by source SHA via tooling/generator_sources.py. Any port must keep the stamp/pre-push contract intact through the transition, not disabled during it. Outcome: a recommendation with costs, then a D-record and Jeroen''s ruling. Epics only after that.', 'backlog', 'low', NULL, 'tooling', 'Q-124', '2026-08-19 23:54:55.776', '2026-08-19 23:54:55.776', NULL, 'cfb08832c72e06f85b075a46cd3223f2', 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; 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 ('06G1RWR7M1H1PC3TY2RT7AC174', 'story', '06FBPPMZNNEV052DBYYY3A897C', 'Investigate retooling Python tooling into a Rust CLI (Q-124)', 'Answer Q-124. Investigation and costing FIRST — do not open with a rewrite. Price the cheap alternative before recommending Rust: a single Python dispatcher entrypoint (one command, many subcommands) would resolve the permission-prompt friction and most of the startup cost without touching the numerics. If that is enough, say so and close Q-124 with it. Scope order if Rust is chosen: port the check/gate family first (check-client-version, check-canvas-version, check-systems-db-stamp, check-dataflow-graph) — small, pure, no numerics, highest prompt-friction, runs on every push. Leave planet-gen numerics (numpy/scipy/PIL) in Python behind a stable interface. Hard constraint: import_economics is stamped by source SHA via tooling/generator_sources.py. Any port must keep the stamp/pre-push contract intact through the transition, not disabled during it. Outcome: a recommendation with costs, then a D-record and Jeroen''s ruling. Epics only after that. 2026-08-20 — Jeroen raised Typer (typer.tiangolo.com) as the Python-CLI option. Q-124 amended with the costing. Start here, not with Rust. Measured state: pyproject.toml already exists and make setup-venv already does pip install -e ''.[dev]'', but there is NO [project.scripts] entry — no console entrypoint exists at all. 22 tooling files already use argparse. So this is consolidation, not authorship. The real deliverable is ''one bare command reliably on PATH'', not ''a CLI framework''. A [project.scripts] entrypoint lands in .venv/bin/, which is on PATH only when activated — and agents and git hooks never activate it. That is the same split VENV_PY papers over in the Makefile, and the same failure recorded for tea (absolute path breaks the Bash(tea *) rule and prompts every time). Install via uv tool/pipx into ~/.local/bin, or symlink. A Typer app behind an absolute venv path solves nothing. Watch startup cost: a single entrypoint importing every subcommand eagerly pays all 123 modules on every invocation, four times per push. Lazy subcommand registration is mandatory, not an optimisation — measure before and after.', 'backlog', 'low', NULL, 'tooling', 'Q-124', '2026-08-19 23:54:55.776', '2026-08-20 00:00:11.592', NULL, 'b48a1523bdfbddb038c083b365414e9f', 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;