current_schema_version() line-scanned res://../project.yaml at runtime. That resolves to the repo root in a dev run and to nothing in an exported build, so a shipped game got the "?.?.?" fallback every time. Since that tag is the Atlas disk cache's ONLY invalidation signal, every exported build stamped and compared the same sentinel: a canvas cached by one build would be served by every later build, forever. T-1239 is what that failure looks like once it happens. loading_screen.gd carried a byte-for-byte copy of the same function, so the version shown to the player was "?.?.?" in exactly the builds where a version string is worth showing. Both call sites now share client/scripts/build_version.gd, which reads application/config/version out of ProjectSettings — a value Godot bakes into the PCK, identical in the editor and in an export by construction rather than by luck. No file IO, no fallback branch. project.yaml stays the source of truth (CLAUDE.md); client/project.godot mirrors it. A mirror nobody checks would be worse than the bug it replaces -- the old code failed loudly everywhere, a stale mirror fails silently -- so tooling/check-client-version compares the two and the pre-push hook runs it unconditionally. Not gated on "were those files in this push": drift persists on main once introduced, and gating would let an existing drift ride along. The test this replaces asserted that current_schema_version() did not return its fallback, and passed -- in the one environment where the code under test worked. Three tests now pin the property that actually matters: a real version, sourced from the baked setting, matching project.yaml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
306 lines
50 KiB
SQL
306 lines
50 KiB
SQL
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 <name> — 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 <name> — 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 <name> — 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;
|