diff --git a/.pql/changelog/ticket_history/2026-08.sql b/.pql/changelog/ticket_history/2026-08.sql index bf621d34f..7a56ecb83 100644 --- a/.pql/changelog/ticket_history/2026-08.sql +++ b/.pql/changelog/ticket_history/2026-08.sql @@ -168,3 +168,4 @@ FOLLOW-UPS worth their own tickets, not done here: - 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.', NULL, '2026-08-14 21:13:15', '2026-08-14 21:13:15.779', '2026-08-14 21:13:15.779', NULL, 'a7cfa7d53e62cff64cd95b5738d1ee75', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXF1VDVQDQ8EFGTXX787M90R', 'status', 'in_progress', 'done', NULL, '2026-08-14 21:20:27', '2026-08-14 21:20:27.835', '2026-08-14 21:20:27.835', NULL, '815c19499dbf40e3b7a79f44c09089c2', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-08.sql b/.pql/changelog/tickets/2026-08.sql index 2605029c8..6736e22dc 100644 --- a/.pql/changelog/tickets/2026-08.sql +++ b/.pql/changelog/tickets/2026-08.sql @@ -236,3 +236,69 @@ FOLLOW-UPS worth their own tickets, not done here: 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;