From 4d43a41acdd1409aed6042b235a9a314a9c85beb Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 25 Jul 2026 11:37:40 +0200 Subject: [PATCH] =?UTF-8?q?chore(meta):=20pql=20changelog=20=E2=80=94=20T-?= =?UTF-8?q?1190=20filed,=20T-1183=20to=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T-1190: step-canvas wire staleness signal + client sim_ttl threading (the D-253 clock seam raised in PR #204 review, adjudicated future-seam). Co-Authored-By: Claude Fable 5 --- .pql/changelog/ticket_history/2026-07.sql | 4 ++++ .pql/changelog/ticket_idmap/2026-07.sql | 1 + .pql/changelog/tickets/2026-07.sql | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/.pql/changelog/ticket_history/2026-07.sql b/.pql/changelog/ticket_history/2026-07.sql index 8fcedfac8..64edbd8d5 100644 --- a/.pql/changelog/ticket_history/2026-07.sql +++ b/.pql/changelog/ticket_history/2026-07.sql @@ -1894,3 +1894,7 @@ Live round 4 addendum: client/tests/test_atlas_window_overlay_draw_smoke.gd (3 s RE-SCOPED by D-255 (2026-07-24, body-map-viewer workshop): this becomes the capture harness for the STEPPED render architecture — goldens key on (body, step) canvases, not the retired continuous-zoom frames. The T-1152/T-1153 test suites (coverage-walk, continuous-zoom seams, compute_tile_grid mosaic) retire with their code in T-1182''s change; this re-scoped harness is their replacement and the eyeball-check substrate for every step canvas. More needed than ever — keep, don''t cancel. PR #203 fix-round inventory (2026-07-25): the retirement is now TOTAL — AtlasViewer and its whole cluster (atlas_viewer, atlas_marker_overlay, atlas_descend_geometry, atlas_legend_panel, atlas_generation_proxy, atlas_generation_state) are deleted. visual_scenarios.gd (_setup_atlas_golden_shot, 6 of 12 atlas_GJ* z2_0/z4_0 non-fit scenarios) and visual_capture.gd (_run_atlas_matrix) now call StepCanvasViewer methods that do not exist: get_heightmap_texture(), set_view(zoom, offset), get_view_offset() — the fit-zoom shots early-return before hitting them. Ruled in PR #203: NO shim (would resurrect the retired continuous-zoom API D-255 kills); the redesign here keys captures on (body, rung) via StepCanvasViewer''s real surface (enter/_scroll_rung/rung state + the echoed extent). get_overlay_defs()/set_overlay_visible() duck-type fine and survive. Until this lands, those 6 goldens + atlas_matrix are knowingly dead (manual make targets only, gate-invisible — confirmed again).', NULL, '2026-07-25 07:32:19', '2026-07-25 07:32:19.974', '2026-07-25 07:32:19.974', NULL, '85a62f8545b4e710acc57db0781b73b7', 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 ('06FS5XT949C8D2JZRWDX2CRZN8', 'status', 'review', 'done', NULL, '2026-07-25 08:39:14', '2026-07-25 08:39:14.794', '2026-07-25 08:39:14.794', NULL, 'f30bdf6e60a9ca04d4b6ecca94d2f6f8', 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 ('06FS5XZF4A978WS1J66Q2JBK8C', 'status', 'review', 'done', NULL, '2026-07-25 08:39:14', '2026-07-25 08:39:14.797', '2026-07-25 08:39:14.797', NULL, 'e766df18190ba474631380d40af1d295', 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 ('06FS5XW0ZMPVYA2CPBBTP88TQC', 'status', 'backlog', 'in_progress', NULL, '2026-07-25 08:40:03', '2026-07-25 08:40:03.632', '2026-07-25 08:40:03.632', NULL, '52b02d51cae9a585e561b1354529b50a', 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 ('06FS5XW0ZMPVYA2CPBBTP88TQC', 'status', 'in_progress', 'review', NULL, '2026-07-25 09:11:50', '2026-07-25 09:11:50.463', '2026-07-25 09:11:50.463', NULL, '6373f0431b384b13ca6bb8fbc88d92ff', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/ticket_idmap/2026-07.sql b/.pql/changelog/ticket_idmap/2026-07.sql index 05a66ca92..763437c20 100644 --- a/.pql/changelog/ticket_idmap/2026-07.sql +++ b/.pql/changelog/ticket_idmap/2026-07.sql @@ -103,3 +103,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FSDE3STZBP0Y7GQ3ZEZPKFP8', 'T-1187', '2026-07-25 00:41:40.058', '2026-07-25 00:41:40.058', NULL, '312ad3897a28515b4ea488e2ccd920e2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FSG8P6C6WXEG3X81PVCQ833M', 'T-1188', '2026-07-25 07:17:23.684', '2026-07-25 07:17:23.684', NULL, 'f49c406f5369a03b4286ca068fab95c5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FSG8PRRFJQ3Y2V3R348DDRRM', 'T-1189', '2026-07-25 07:17:28.388', '2026-07-25 07:17:28.388', NULL, '14e7563bd5e58b24ab9406713769258f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FSH4VE4TTJGFGVVQWHACDBTM', 'T-1190', '2026-07-25 09:20:26.664', '2026-07-25 09:20:26.664', NULL, '773167921a274a9d43a49cea6d3ff65b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index 702d2cb7d..b7e9572de 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -2742,3 +2742,8 @@ Live round 4 addendum: client/tests/test_atlas_window_overlay_draw_smoke.gd (3 s RE-SCOPED by D-255 (2026-07-24, body-map-viewer workshop): this becomes the capture harness for the STEPPED render architecture — goldens key on (body, step) canvases, not the retired continuous-zoom frames. The T-1152/T-1153 test suites (coverage-walk, continuous-zoom seams, compute_tile_grid mosaic) retire with their code in T-1182''s change; this re-scoped harness is their replacement and the eyeball-check substrate for every step canvas. More needed than ever — keep, don''t cancel. PR #203 fix-round inventory (2026-07-25): the retirement is now TOTAL — AtlasViewer and its whole cluster (atlas_viewer, atlas_marker_overlay, atlas_descend_geometry, atlas_legend_panel, atlas_generation_proxy, atlas_generation_state) are deleted. visual_scenarios.gd (_setup_atlas_golden_shot, 6 of 12 atlas_GJ* z2_0/z4_0 non-fit scenarios) and visual_capture.gd (_run_atlas_matrix) now call StepCanvasViewer methods that do not exist: get_heightmap_texture(), set_view(zoom, offset), get_view_offset() — the fit-zoom shots early-return before hitting them. Ruled in PR #203: NO shim (would resurrect the retired continuous-zoom API D-255 kills); the redesign here keys captures on (body, rung) via StepCanvasViewer''s real surface (enter/_scroll_rung/rung state + the echoed extent). get_overlay_defs()/set_overlay_visible() duck-type fine and survive. Until this lands, those 6 goldens + atlas_matrix are knowingly dead (manual make targets only, gate-invisible — confirmed again).', 'backlog', 'medium', NULL, 'client', NULL, '2026-07-22 09:42:47.197', '2026-07-25 07:32:19.969', NULL, '258f3e5dbabf9935a55bd33ff400608b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XT949C8D2JZRWDX2CRZN8', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Client map component: RTT terrain + screen-space annotations + stepped transport (D-255)', 'The two-layer client rebuild per D-255(a)(b)(e): a render-to-texture terrain layer (per-step data canvas colorized CPU-side — c1 ruling: Image.set_pixel, measured 77.5 ns/cell flat, NOT a hand-rolled buffer write which measured 2x slower; L8 planes where single-channel per measurement ⑤; texture.update reuse on step-cross) + an unscaled screen-space annotation sibling (names, glyphs, course polylines, markers — literal px sizes, positions world→screen). Stepped transport: discrete rung index 0-5, scroll steps, cursor-anchored, edge-scroll pan, full-zoom-out reset to the rung-0 Global frame; hold-fetch-swap on step-cross (morph/tween optional cosmetic follow-on, never re-derivation); step-dependent display ratio (1x1 deep/mid, ~5x5 shallow — presentation only, never spacing/cache-key). EXECUTES THE FULL _canvas.scale RETIREMENT PATH in the same change (tyre-round2.md ~line 1074): (1) _canvas.scale/_canvas.position/_apply_transform continuous-scale model; (2) the _zs/_zs_stroke/_zs_ring_radius compensation family in atlas_window_geometry_nature.gd — deleted wholesale; (3) select_rung + MAX_COVERAGE_M + compute_tile_grid (T-1152/T-1153 code); (4) the AtlasViewer/orbital-mosaic-vs-window two-code-path split (collapses to ''Global opener is rung 0, one viewer one path''); (5) _view_zoom float + _canonical_fit_zoom. Their test suites (coverage-walk unit tests, continuous-zoom seam tests, compute_tile_grid mosaic tests) are DELETED in the same change — never leave old goldens asserting against retired frames (the T-1157 dead-goldens rule); replacement coverage lands under re-scoped T-1157. SURVIVING SURFACES — do not delete (SI gap 1): atlas_window_tile_set.gd''s LRU SHAPE survives as the skeleton for the client cache-store ticket (adapt it, do not delete it — it is not on the retirement path); _filter_for_granularity_v2 (NEAREST/LINEAR per rung is still a real RTT draw-time question); compositing/legend/overlay-bar chrome (call-site updates only). Determinism boundary per D-255(e): client interpolates only within the closed server-supplied input set. Design sources: stig-round1.md, stig-round2.md, D-255(a)(b)(e).', 'done', 'high', NULL, 'client', 'D-255', '2026-07-24 07:11:47.490', '2026-07-25 08:39:14.793', NULL, '14d720df58c8c8a05755bfcc43a0c7f1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XZF4A978WS1J66Q2JBK8C', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Basin-outlet → course-network wiring: the endorheic cue deliverable (D-255)', 'Thread HydrologyResult overflow basins'' resolved outlet_path/spill points into RiverNetwork/courses as D8 downstream continuations, so an overflow lake''s exit river appears in the served courses and an endorheic basin''s absence of one reads as ''no course leaves this lake''. THE RULED ENDORHEIC CUE (D-227 amendment (4), converged Dudley+Araminta): outflow-course PRESENCE — no wire bit, no 18th zone (proportionality: 1,030/22,270 = 4.63% endorheic doesn''t justify widening the frozen vocabulary; the inference is definitionally sound with no misfire case — every Overflow basin has a guaranteed non-empty outlet_path (tested: overflowing_basin_has_nonempty_outlet_path), every Endorheic none). Pre-cleared ADDITIVE by T-1170 Ruling 7b: the RIVER_DOWNSTREAM_TERMINAL sentinel was reserved exactly for endorheic interior sinks, and river_course::build_edges is already no-op-safe on TERMINAL (verified) — not a wire migration. Required regardless of the cue: overflow lakes must show their exit rivers to be hydrologically honest on the map (a lake that drains but shows no outlet river is simply wrong). Until this lands, the map shows lakes (via the lake-sourcing ticket) but not the drains-vs-closed distinction — the recorded honest sequencing. Endorheic basins map to TERMINAL; overflow outlets extend the D8 network from the spill cell along outlet_path. Design sources: dudley-interview2-response.md CORRECTED section, araminta-round2.md §(e), t1177-hydrology.md, D-227 amendment (4).', 'done', 'medium', NULL, 'server', 'D-227', '2026-07-24 07:12:29.986', '2026-07-25 08:39:14.797', NULL, 'c958b35fd5cbdb472ba132591cd1f632', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XW0ZMPVYA2CPBBTP88TQC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Client cache store: FileAccess dir, two-axis eviction, version tag, retention cap (D-255)', 'The client-side cache tiers per D-255(d) and Stig''s three-tier spec (stig-round2.md (c)): in-memory LRU (adapting atlas_window_tile_set.gd''s surviving LRU skeleton from the map-component ticket) + disk-backed FileAccess cache dir with index. Two-axis eviction per D-227 amendment (1) — staleness and storage are DISTINCT: geometry entries never go stale (determinism; byte-valid forever) and are evicted only by (2a) time-since-last-visit (Jeroen''s storage-thrift ruling) and (2b) LRU capacity budget, two independent sweeps kept separate; sim-state entries (frozen/flooded planes) carry the real staleness TTL (SIM_STATE_TTL formula — served/invalidated per the serving ticket''s server-side formula). Rung-0 Global entries: retention floor, never swept. HARDENING REQUIREMENTS from D-255(d), both mandatory: (i) per-body deep-rung retention cap (max resident chunk/block-spacing tile count or disk quota per body, independent of the rung-0 floor — the structural closure of the D-226(d) accumulation gap; the AtlasAgentInterface QA channel inherits the same cap); (ii) every persistent entry carries a schema/version tag (project version or generator stamp) — read-time mismatch = cache miss + re-fetch, NEVER decode (the D-192 co-ship boundary; a disk cache survives game updates). Index-entry schema sketch in stig-round2.md (key/file_path/tier/written_at/last_read_at/size_bytes/sim_ttl/retention_floor); sweep triggers on body-open + coarse background timer, never per-frame. Explicitly rejected by the workshop: SQLite in any shape (server-side relocation, godot-sqlite addon) — plain FileAccess + index won on both agents'' independent analyses. Design sources: stig-round2.md (c), dudley-round2.md (d), D-255(d), D-227 amendments (1)(2).', 'in_progress', 'high', NULL, 'client', 'D-255', '2026-07-24 07:12:01.789', '2026-07-25 08:40:03.632', NULL, 'a08751277ca8c113ad7b9427c8b9a579', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XW0ZMPVYA2CPBBTP88TQC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Client cache store: FileAccess dir, two-axis eviction, version tag, retention cap (D-255)', 'The client-side cache tiers per D-255(d) and Stig''s three-tier spec (stig-round2.md (c)): in-memory LRU (adapting atlas_window_tile_set.gd''s surviving LRU skeleton from the map-component ticket) + disk-backed FileAccess cache dir with index. Two-axis eviction per D-227 amendment (1) — staleness and storage are DISTINCT: geometry entries never go stale (determinism; byte-valid forever) and are evicted only by (2a) time-since-last-visit (Jeroen''s storage-thrift ruling) and (2b) LRU capacity budget, two independent sweeps kept separate; sim-state entries (frozen/flooded planes) carry the real staleness TTL (SIM_STATE_TTL formula — served/invalidated per the serving ticket''s server-side formula). Rung-0 Global entries: retention floor, never swept. HARDENING REQUIREMENTS from D-255(d), both mandatory: (i) per-body deep-rung retention cap (max resident chunk/block-spacing tile count or disk quota per body, independent of the rung-0 floor — the structural closure of the D-226(d) accumulation gap; the AtlasAgentInterface QA channel inherits the same cap); (ii) every persistent entry carries a schema/version tag (project version or generator stamp) — read-time mismatch = cache miss + re-fetch, NEVER decode (the D-192 co-ship boundary; a disk cache survives game updates). Index-entry schema sketch in stig-round2.md (key/file_path/tier/written_at/last_read_at/size_bytes/sim_ttl/retention_floor); sweep triggers on body-open + coarse background timer, never per-frame. Explicitly rejected by the workshop: SQLite in any shape (server-side relocation, godot-sqlite addon) — plain FileAccess + index won on both agents'' independent analyses. Design sources: stig-round2.md (c), dudley-round2.md (d), D-255(d), D-227 amendments (1)(2).', 'review', 'high', NULL, 'client', 'D-255', '2026-07-24 07:12:01.789', '2026-07-25 09:11:50.463', NULL, '04e69662c15ff8d68e447fd1fd2f4403', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FSH4VE4TTJGFGVVQWHACDBTM', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Step-canvas wire staleness signal + client sim_ttl threading (D-253 clock seam)', 'Client disk cache (T-1183) and server StepCanvasCache (T-1181) both carry wired-ahead sim-state TTL machinery for glaciation/flooded_q, but no staleness signal crosses the wire: the server SIM_STATE_TTL formula is tick-based and server-side only, and step_canvas_request.gd calls disk_cache.put() without sim_ttl_sec, so every canvas is (correctly, today) Tier-2 geometry — flooded_q is hardwired 0 and glaciation is the static classification, byte-identical to a fresh derive (server/src/atlas/step_canvas.rs module doc, D-253 wired-ahead). The moment D-253''s sim-state driving clock lands and glaciation/flooded values actually vary with sim time, StepCanvasResponse must carry a TTL (or equivalent staleness signal) and on_response must thread it into the existing put(sim_ttl_sec) parameter so client disk-cached canvases re-request instead of serving stale sim-state indefinitely. Raised by tyre in PR #204 review; adjudicated as a future seam, not a live gap. Blocked conceptually on the D-253 driving clock existing.', 'backlog', 'low', NULL, 'client', 'D-227', '2026-07-25 09:20:26.662', '2026-07-25 09:20:26.662', NULL, '33e980c6a5a4d49fb8b6f2c56a69c274', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);