I1: _draw_attractor_shape's three stroke-width args (Confluence arc,
Coastal/NaturalHarbor arc, Oasis spokes) were raw screen-space literals
— Godot multiplies stroke widths by canvas scale exactly like radii, so
at the Region orbital fit zoom the outlines rasterized at ~0.01px, the
identical sub-pixel class the dot/ring compensation fixed, missed on
glyph internals (and attractors are Region-only — precisely where it
bites). Widths now arrive pre-compensated via a px_w param, keeping the
primitive pure. Regression pin: a source-scan test asserting no
draw_arc/draw_line in the function carries a bare numeric width (the
draw-smoke suite documents its own vacuous-pass mode, so source-scan is
the environment-independent gate); revert-verified by name. I2: D-226
visibility-direction sentence — Araminta's fade-down inversion recorded
as pre-T-1170 with its single revisit point named. I3: class-header
call-site claim corrected (enter() funnels through _enter_at_rung).
Tickets: T-1156
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At 1.6px/60% the trunk dot under the mouth ring read as 'mouth glyph
over plain terrain' — invisible without knowing where to look,
underselling the ruling's own 'a major river crosses near here'
intent. 2.0px/80% preserves the fade-down ladder vs Region's
2.2px/100% without reading as accidentally-erased. Doc comment
records the revision provenance.
Tickets: T-1156
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live round 2 (the real bug): every nature-overlay marker size was a raw
screen-space constant drawn inside _canvas, whose scale IS view_zoom —
at Lendel's orbital fit zoom (0.0063) a 2.2px trunk dot rendered at
~0.014px, invisible; the same code at District's 3.75 zoom produced the
correctly-visible mouth ring, which is why one capture worked and the
headline rung didn't. Fixed via AtlasWindowGeometry.zoom_compensated_
size() (pure, floor-guarded) wired through every radius/line-width;
basin FILL points are positions and correctly stay unscaled. Suspect
tile-mode-rung-detection was ruled out live (granularity_v2=Region
confirmed in tile mode) but pinned with a named regression test anyway.
+8 pure-function tests incl. a numeric pin of the pre-fix magnitude
(<0.02px at orbital zoom); revert-verified by name. Draw-smoke suite
documented as supplementary (the shared SubViewport background harness
can pass vacuously under X11 BadMatch — the pure suite is the gate).
Live round 3 (drive-script bug, no product change): the lead's scratch
drive passed the button LABEL to set_overlay_visible() and the unknown-
id guard silently no-op'd — but the chase banked a real pin:
test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_
overlay (draw-counting spy per the cold-start precedent; is_queued_for_
redraw does not exist in this build). Revert-verified.
Basins verified live: 7 Lendel watershed boundaries render at the
ruling's alphas. Suites: viewer 76/76, geometry-nature 42/42, nature-
overlay 22/22, zoom-ladder 50/50, no regressions across the cluster.
Tickets: T-1156
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New AtlasWindowNatureOverlay (Node2D on the viewer canvas, above
terrain): self-connects to the shared atlas_layers_received broadcast
(the established one-signal-N-consumers shape) and consumes the whole-
body layer1 skeleton per Tyre's carrier ruling — no windowed wire
touched. Coordinate chain layer1_pixel_to_world_m -> world_m_to_
district -> canvas-local lives in atlas_window_geometry as pure tested
functions; vertical convention (row 0 = North pole, wy increases
south) verified against the server's own pixel_to_world_m, pinned by
pole tests and revert-verified (sign flip fails them by name).
Araminta's per-rung presentation table implemented exactly: Region
full skeleton (radii 0.9/1.4/2.2, confluence 3.5, mouth double-ring),
District trunk-only 1.6px at 60% with mouths at full landmark styling,
Quarter off until T-1170 course invention; retired palette reused
verbatim; RVR on / BAS off / ATR off defaults; missing river_class
falls back to trunk. 58 new tests (34 geometry, 22 lifecycle, 2 real-
driver draw smoke actually rendered); 11 existing atlas suites
regression-free. atlas_window_viewer.gd runs 16 lines past the
advisory 1000-line cap on trivial wiring — accepted, rides T-1158's
decomposition.
Tickets: T-1156
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Araminta's ruling (PR #192 follow-up): filter keyed on rung IDENTITY via
the window's own echoed granularity_v2 — Region (incl. the orbital tile
mosaic, whose tiles are all Region-rung requests) samples NEAREST because
GPU bilinear at 204.8 km/cell reads as smoothing-over-absence; District/
Quarter keep LINEAR where cell density earns the blend. One shared helper
(_filter_for_granularity_v2) at both draw call sites; unknown/missing
wire tags fall back to LINEAR (never trusted into NEAREST). COMPOSITE_
SMOOTH survives as the independent compile-time pipeline axis — the
two-axes split is documented in the file header. Washes/border fades
untouched per the ruling. Focused suite 44/44; revert-verified (helper
hardcoded LINEAR -> exactly the three Region-NEAREST tests fail).
Tickets: T-1161
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tyre's APPROVE items (T-1163): record the two-legal-wire-shapes-for-one-
logical-state protocol invariant (whole-response Pending AND Ready+null
district_window both mean 're-poll'; only NotFound/Error are terminal) as
a D-226 note under the T-1124 §4 amendment area, so a future server
refactor of the asymmetry must migrate every consumer in the same change.
Drop the duplicated 5-line stagger comment in atlas_window_tile_set.gd.
Follow-up tickets filed on main: T-1164 (tiled terminal-recovery),
T-1165 (queue_redraw edge root-cause), T-1166 (cold-launch test tier).
Tickets: T-1163
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The true cause of the black first launch, server-confirmed after four
disproven theories: a stone-cold body answers the FIRST window request
with a whole-response status 'Pending' (only the whole-body cache-hit
branch sets Ready), and on_response()'s very first check — status !=
Ready -> return — swallowed it before the retry machinery could run.
retries stayed 0 forever; the DERIVING state never resolved. Second
connections worked by luck (the first request warms the whole-body
cache, so they read Ready and take the healthy path). Both tile fan-out
AND single-window first-descents were affected — one shared function,
one fix: branch on the outer status first (the atlas_generation_proxy
reference shape): Pending -> retry, Ready -> existing null-window retry,
NotFound/Error -> give up immediately (the principled give-up policy,
replacing the elapsed-retries ceiling).
Hardening in the same round: deterministic exponential backoff (0.5s
doubling, 4s cap) + per-tile stagger (0.1s * index — six tiles retry at
0.5/0.6/0.7/0.8/0.9/1.0s, strictly-increasing asserted, not jittered);
MAX_RETRIES 20->30 (~110s horizon under backoff).
Regressions are wire-accurate by construction: whole-response Pending
(body_id only, no center/n/granularity — verified against the server's
own response construction) delivered through the REAL fan-out
(tile_set._on_atlas_layers_received, never tile.on_response directly),
mirrored at single-window level. Two first-draft tests that passed with
the bug reverted were caught and strengthened before reporting; every
fix and hardening piece revert-verified independently (7+1 failing
tests without them).
Item 1: while zero tiles have arrived, the viewer draws a centered
screen-space 'DERIVING TERRAIN…' label (text_dim role, no new hue),
dropping the instant the first tile lands — a cold wait now reads as
loading, not broken. New has_any_tile_arrived() predicate (distinct
from has_pending_tiles(): both true mid-arrival, tested exactly there).
Item 2: legend re-fit root-caused empirically, two plausible fixes
disproven by trace before the real one: a manually-positioned Control's
size NEVER tracks a shrinking minimum in this parenting shape, and
RichTextLabel.fit_content reports degenerate minimums until laid out at
real width once — so reset must be DEFERRED and run after refill, not
inside clear(). ImplantPanel.reset_to_content_size() (call_deferred),
wired into both legend refresh()es. The load-bearing test compares
size.y to get_minimum_size().y — a size-to-size comparison passed
trivially with both numbers equally stuck (caught on first draft).
Item 3: make atlas now builds the RELEASE server and passes
SR_SERVER_BIN (a cold DEBUG server delivers zero tiles for >10s on a
new body — live-measured — vs 210ms warm; release serves cold in well
under a second). atlas_standalone._server_binary_path() honors the env
override per the SR_PORT two-tier precedent, debug path unchanged
when unset.
All fixes revert-verified; nine suites green collateral-checked;
gdlint clean.
Jeroen hit a black mosaic zooming into a body from a fresh make-atlas
spawn. Live diagnosis showed all six tiles held with colored textures
and no repaint; the original line-specific diagnosis (unpaired
queue_redraw in _on_tile_ready) turned out WRONG — the pairing already
existed (lead's truncated grep misread the function; Stig verified via
git log -p before acting). Rather than chase the exact dropped signal
edge, _process() now self-heals: both viewer and overlay redraw every
frame while the tile set has pending tiles (has_pending_tiles(), new) —
a strict superset that closes the black regardless of which edge drops,
pinned by a _draw()-counting real-subclass spy test.
Legend stacking root cause found by trace, not guess: ImplantApp.
_on_screen_changed() re-runs enter() unconditionally on repeat
same-screen pushes — each re-entry tore down and rebuilt all six tile
requests (the round-6 orphaning fingerprint via a new trigger) and
stacked another legend (~10 deep, full-height dark panel). Fixed both
ends: ImplantPanel.clear() frees immediately (same-frame re-entrant
refresh can never observe stale children — protects every implant app),
and RegionalScreen.enter() no-ops for the same body (different body
still re-enters fresh). The load-bearing regression asserts an ARRIVED
TILE'S DATA survives a repeat push — node identity would not catch the
teardown (the tile-set Node is a fixed field; only its internals reset).
Cold-start UX: pending tiles now draw the single-window path's
COLOR_BORDER_FADE wash instead of raw background — a deriving mosaic
reads as loading, not broken.
Suites green (zoom_ladder 48, viewer 74, tile_set 20, overlay 30,
overlays 46 + 2 new files), gdlint clean, revert-verified throughout.
All Hoshe/Araminta findings addressed (Tyre approved outright), none
retracted — plus a Dudley stop-and-flag discovery that improved on the
asked-for fix:
- Legend 100x lie (Araminta, blocking): subtitle computed via
spacing_for_rung() and refreshed at all three _held_granularity_v2
write sites. Region test asserts 204.800 km/cell; the District
direction needed a stale-header-aware helper — a District-only test
spuriously passes against the old literal by coincidence.
- Region coalescing coverage (Hoshe 1): both directions tested
(Region-vs-District separate slots; Region-vs-Region coalesces).
- Clamp boundary tests + dangling citations (Hoshe 2): writing the
requested halving-loop-fires test surfaced that the loop is PROVABLY
UNREACHABLE at current constants (per-axis clamp forecloses it —
brute-forced independently on both server and client sides). Ruling:
the loop stays as defensive code; the test became a property sweep
pinning both the wire-cap invariant and the loop's no-op status (a
future constant change breaks it loudly); doc comments on both sides
drop the load-bearing framing and state the truth; the old client
mirror test that claimed the loop fires (passing on the per-axis
clamp alone) is replaced the same way. Client citations now name the
real server tests verbatim.
- Governance (Tyre): D-226 amendment note — progressive cross-rung
refinement EXTENDS T-1124 §4 (not supersedes); legacy u32 field
scheduled for retirement (T-1159).
Server: 1818 lib tests green, clippy/fmt clean. Client: zoom_ladder
48/48, window_request 26/26, viewer 74/74; gdlint clean. Every fix
revert-verified.
The round-5 per-tick zoom comparison inside the reset guard was level-
triggered: at the canonical frame every continued zoom-out tick drifted
_view_zoom a hair below fit, read as 'not there', and re-fired
enter_orbital — tile set torn down/recreated per tick, in-flight
responses orphaned (black entry), server flooded (889 of 897 responses
in one drive phase). The drift concern now lives in _zoom_at as a ZOOM
FLOOR (once at the canonical frame, zoom-out clamps at fit); the guard
keys only on fields that never drift (_held_center/_held_granularity_v2/
_tile_mode) — genuinely edge-triggered. Three shared helpers keep the
guard and the floor on one definition of 'canonical frame'.
Anti-storm regression drives 100 real _zoom_at ticks past the first
reset and asserts tile-set child node identities never change; the
black-entry repro interleaves Lendel's six wire-accurate tile responses
with continued ticks and asserts all six held. Both revert-verified with
the exact live symptoms. Round-5 reset tests pass unchanged. Live-
verified twice: exactly 6 requests at entry and at reset, no storm.
The wrapped tile column (Lendel: -6400 canonicalized to 12739) drew at
its canonical column — off-canvas right — leaving the mosaic's left
third black. nearest_wrap_image() re-expresses a tile column as the
wrap-image closest to held_center for DRAWING only (requests/cache keys
stay canonical). The draw-position test asserts overlap FRACTION, not
bare intersects() — the buggy placement still clipped ~2px of viewport
edge at Lendel scale, so intersects() alone would false-pass.
The full-zoom-out reset restored center but not the fit zoom, and its
'already there' guard keyed on a lagging field so it could only ever
fire once. The guard now also matches tile mode and compares _view_zoom
against the freshly computed fit — restoring the FULL canonical
transform (center, offset, fit zoom) and re-firing as a continued
gesture keeps zooming out (Jeroen's hard condition, both live shapes).
+8 revert-verified tests incl. the continued-gesture reset repro; smoke
stub gained get_body_radius_km (crash confirmed real under a real
driver before fixing). Suites 286 green; gdlint clean.
Three live-found rendering bugs, all invisible to green unit suites:
- _draw_tile_mosaic placed tiles from absolute district (0,0); canvas-
local (0,0) is held_center - held_n/2 everywhere else, and tile-mode
held_n is the whole-body extent — the entire mosaic drew tens of
thousands of px off-canvas. New pure district_to_canvas_local() +
viewer accessors route every tile through the shared frame.
- _maybe_reselect_rung updated held_n across crossings without
recomputing _view_offset — the single-window composite landed off-
canvas the moment any crossing happened (why District/Quarter were
black too). New pure recompute_offset_for_held_n_change().
- _build_tile_texture created an unstored ImageTexture per _draw,
racing the RenderingServer's deferred upload — CPU pixels correct,
screen white. Per-tile-index texture cache, same reference-identity
discipline as the single-window _cached_texture.
Structural close of the twice-bitten 'nothing asserts pixels' gap:
test_atlas_window_overlay_draw_smoke.gd renders overlay output into a
SubViewport and asserts visible pixels for both modes — runs under a
real driver (invocation documented in DEVOPS.md, visual_capture
precedent; migration to the T-1157 harness noted on that ticket), skips
loud-but-green under the gate's headless run (verified green-with-skips
AND genuinely red with detection forced off). +7 geometry/crossing
tests, all revert-verified. Targeted suites 270 green; gdlint clean.
Bug A (zoom reselection dead): select_rung() redesigned from the two-gate
split to a unified per-rung coverage-ceiling walk (MAX_COVERAGE_M,
finest-first) — which also restores District as a reachable rung
(33-131km extent band; the two-gate model had made it structurally
unreachable). Plus the THIRD instance of the PR #191 C1 clamp-mirror bug
class: _maybe_reselect_rung fed the old rung's _held_n raw into the new
rung's request — the server clamped, the held value didn't, every
cross-rung response stale-dropped. Re-clamped at the boundary; the
leaving_tile_mode flag also fixes a stale request-granularity edge case.
Bug B (orbital = whole body): genuine tile-set model. compute_tile_grid
composes ceil-divided Region-tile centers (canonicalized column wrap,
row clamp, Dictionary-set dedup; Lendel = 3x2 = 6 tiles);
AtlasWindowTileSet owns one AtlasWindowRequest per tile (reusing all
tested request/cache/debounce machinery), fans responses out, per-tile
tile_ready — genuinely progressive; the overlay draws arrived tiles as a
mosaic under the one view transform. enter_orbital branches to tile mode
when >1 tile; single-window path unchanged below the coverage ceiling.
E/W wrap at tile seams live-confirmed (col -6400 wraps to 12739). Pole-
row dedup verified correct but flagged honestly as currently-unreachable
defensive logic (no real radius triggers row collision).
All fixes + fallout verified via revert/confirm-fails/restore. Targeted
suites 256/256; gdlint clean; new 20-test tile-set suite.
Second live-round blocker: the T-1150 legacy granularity comparison
stayed armed alongside the v2 check, and the server ALWAYS sends the
u32::MAX Region sentinel in the legacy slot — which can never equal the
client's pinned legacy value, so every Region response was stale-dropped
after the v2 check passed. _echoed_granularity_matches() now branches on
PRESENCE of granularity_v2: present -> v2 is the only comparison; absent
(old server) -> legacy fallback.
Mock-fleet audit while fixing: three tests had responses diverging from
the real wire — the oversized-orbital round-trip omitted the legacy
sentinel (passed for the wrong reason), the Region-accept test used
legacy=1, and the mismatch-drop test left v2 at a masking default that
would have inverted under the new rule. All wire-accurate now with a
named SERVER_LEGACY_GRANULARITY_REGION_SENTINEL const; +2 tests (legacy-
only old-server acceptance; v2-wins-regardless-of-legacy precedence
proof). All three verified to fail against the reverted fix. Full suite
3524/3524.
The live round against a real server caught what 3518 unit tests missed:
enter_orbital stored the UNCLAMPED n (Lendel: 19139 raw circumference
cols) into _held_n while AtlasWindowRequest clamped independently to
6400 before sending — every orbital response on a real-sized body was
dropped by the viewer's own w_n != _held_n staleness check. Permanently
blank ladder. The PR #191 C1 clamp-mirror lesson, one layer up: the
mirror must apply wherever held/expected state is recorded, not only at
the request layer.
_enter_at_rung now clamps via _clamp_window_n_mirror_v2 before storing
(the file's only _held_n write site — reselect/refloat read and forward).
Regression closes the coverage hole: the oversized-orbital RESPONSE
round-trip (enter_orbital on GJ380c -> deliver echo n=6400 Region ->
assert accepted). Both new tests verified to fail against the reverted
fix before trusting green. Full suite 3520/3520.
The atlas 'regional' screen now opens the LADDER at the canonical orbital
frame (Region granularity, whole body fitted and centered) and wheel zoom
descends continuously — cursor-anchored, unclamped across rungs, with
progressive refinement (held composite keeps drawing, finer rung swaps in
place on arrival; no blank frame, no mode flip). Full-zoom-out resets to
the canonical planetary frame per Jeroen's HARD condition
(is_fully_zoomed_out = extent >= body circumference, not a zoom-value
heuristic). The district_screen nav hop is deleted — D-013 restored:
descent is a zoom gesture, not a nav push. AtlasViewer's heightmap-texture
path is unreachable from nav (code intact; overlay surface deferred, see
report/tickets).
Rung selection: design doc §5's literal formula has NO legal District band
at any real viewport (visual-tolerance band and n=64 coverage ceiling
never overlap — pinned by executable boundary tests at 1600x900);
select_rung() splits it into a coverage ceiling (decides Region) then the
2x visual tolerance (District vs Quarter), documented at the function.
In practice the ladder steps Region -> Quarter directly.
Wire: window_granularity_v2 encoded (omitted at District for byte-compat),
granularity_v2 echoed value keyed + staleness-guarded end to end; Region
clamp mirror replicates the server's bounded halving loop (no closed
form). MIN/MAX_ZOOM widened to [0.0005, 64] — the old 0.5 floor would
have clamped a real body's canonical fit zoom, violating the reset
condition.
Real pre-existing bug fixed in atlas_window_overlay.gd: the draw path used
echoed n as both cell-grid dimension and district extent — only
coincidentally correct at District granularity; Quarter/Region would have
read wrong array offsets. cell_grid_side_for_window() now mirrors the
server's WindowGranularity::cell_grid_side.
Tests: +26 pure-function geometry tests, new 30-test zoom-ladder suite,
extensions across the window cache/request/overlay/delivery suites.
Full suite 3518 green; cold-parse clean.
All seven Hoshe/Tyre findings addressed, none retracted:
- n-clamp/echo/staleness triangle (Tyre C1): client _clamp_window_n_mirror
(bit-for-bit twin of the server clamp, canonicalize_district_center
precedent) applied before _n is stored/sent; server test pins the
quarter n=32 -> echo 16 contract.
- min_wl band quantization (Hoshe 1/Tyre C3): quantize_min_wl_m snaps to
MIN_WL_BANDS_M {0, 32768, 16384, 8192, 4096} before cache key and echo
(design doc §5's unbounded-key fix), reusing the one true
OCTAVE_WAVELENGTHS_M array; docstrings now state the server-quantizes/
client-sends-raw split; same-band cache-sharing test.
- coalescing granularity axis (Hoshe 2): two tests pin different-
granularity requests as separate in-flight slots and same-granularity
coalescing unchanged.
- orphaned fixture (Hoshe 3): test_protocol.gd consumer decodes
atlas_response_ready_with_window.msgpack through the real IPC path and
asserts the new fields.
- atlas_window_request coverage (Hoshe 4): new test file — stale-drop on
granularity mismatch, old-server-shape defaults accepted, clamp mirror
formula + wiring. First draft's quarter-via-request_now test would have
passed for the wrong reason (request_now resets granularity by design
until T-1153) — split into formula pin + reachable-path wiring proof.
- granularity type seam (Tyre C2): field + resolver docstrings state
finer-only integer multiples with resolve_window_granularity as the
single widening point; matching contract note added to the D-226
T-1143-rulings amendment.
cargo --lib 1807/1807; goldens bit-identical; gdlint clean.
T-1151: build_district_window_layer dispatches one Rayon task per row
(pure derive_window_cell via derive_at_metres), scattered row-major into
the flat arrays; a cfg(test) serial path backs the bit-identical
parallel-vs-serial golden.
T-1150: serde-default window_granularity (1=district, 4=quarter) +
window_min_wl_m on AtlasLayerRequest — additive, no sixth demux shape,
old frames decode unchanged (tested). Quarter mode = full
reclassification at 512m spacing over the same world rect ((4n)x(4n)
cells); WIRE_CAP_CELLS=4096 enforces n*granularity <= cap (quarter
clamps n to 16, the design doc's worked example). Granularity + min_wl
key ALL five touch points: DistrictWindowLayer echo, server FIFO-256
cache key (now a 5-tuple), per-connection coalescing key, client
request codec (omitted-at-default wire fields), client LRU key.
Mandatory aliasing regressions on both ends: identical (body, center, n)
at granularity 1 vs 4 produce distinct cache entries and correct
per-granularity payload shapes (server, 3-thread queue to avoid the
AnalyzeBody thread contention found while writing it) and distinct
client cache keys (gdUnit). Replay fixture regenerated — the layer
struct grew two echoed fields (231->254 bytes, content verified).
Client requests stay district-granularity by default — quarter requests
arrive with T-1153's rung selection.
Cover-fit: fit_window_view zooms from the viewport's LARGER dimension,
no margin factor (any factor under 1.0 leaves a long-axis gap — checked
numerically) — the composite fills edge to edge, overhanging the short
axis into pan-space; the refloat center-equality early-return already
prevents refetch churn at rest (proved, not just tested).
Input model (Jeroen: drag breaks click semantics with map objects):
LMB-drag pan REMOVED from the regional window; clicks are
object-reserved. Pan = held WASD/arrows polled in _process (delta- and
zoom-scaled, camera-pans-toward-key convention verified numerically)
plus edge-scroll within 24px of the viewport border; both suppressed
over UI and on OS focus loss; both set _user_adjusted; wheel zoom and
Esc unchanged. Reads RAW physical keycodes deliberately — independent
of the shared D-054 move_* InputMap actions bound to the same keys
(whose occlusion-leak is pre-existing and now ticketed as T-1146).
Pole wall + east-west wrap unchanged, re-driven through the new
inputs; drag tests replaced, not kept.
Smoothed composite (interim pending T-1143): per-cell colors bake into
an n x n Image/ImageTexture (exact existing colorizer incl. overlay +
ice tint) drawn once with LINEAR filtering — GPU bilinear reads as
terrain, the planetary heightmap's own treatment. Crisp per-cell path
preserved behind COMPOSITE_SMOOTH for T-1143 A/B. Rebuild only on
reference-identity change of window/toggle (is_same — verified true
reference equality; value-equal distinct dicts DO rebuild).
Governance: T-1145 amendment paragraph on D-226 T-1124 SS5 (all three
supersessions); pql decisions validate ok.
Suites: window_viewer 74/74, window_geometry 32/32, window_overlay
(new) 16/16; gdlint clean on all six files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bounds gate: AtlasDescendGeometry.is_on_texture() — ONE helper feeding
both the reticle guard and the click fall-through (the T-1140 lesson:
the visible affordance always matches the click); half-open
[0,tex_w)x[0,tex_h) boundary pinned at the exact edge. Letterbox
clicks no longer show a reticle or descend.
Fit-and-center: pure fit_window_view() (new atlas_window_geometry.gd)
wired into enter(), the FIRST window arrival, and NOTIFICATION_RESIZED
— gated by a _user_adjusted flag so the fit never fights manual
zoom/pan (flag clears only on a fresh enter). Found-own-bug: RESIZED
can fire mid-_ready() before _canvas exists — null-guarded like the
sibling panels.
Pole wall (Jeroen's ruling): clamp_pan_offset_to_pole_wall() clamps
the WINDOW EDGE, not the center, in screen space from the fitted
transform — Y only; wired into the drag handler and every fit (a
fresh fit can itself need the wall on a tiny body — the
window-taller-than-planet case is handled and tested). Three numeric
hand-traces preceded the code; a first-draft test using GJ380c's huge
radius silently never exercised the clamp — replaced with a synthetic
small radius.
East-west wrap (Jeroen's ruling): canonicalize_district_center() —
posmod column wrap (verified against a live Godot process to match
Rust rem_euclid bit-for-bit), clamped row; district_extent() shares
the exact formula (incl. .max(1)) with the server's
normalize_window_center so echoes and cache keys agree on canonical
form. Canonicalization applies only to the FINAL refetch center — the
edge-crossing decision stays in absolute district space (first-pass
math error caught by hand-trace). Seam-adjacent cache-key sharing
tested. Pan offset itself has no x wall — circumnavigation is
seamless.
Header: body proper_name/body_id ahead of the coordinates (the cheap
half of T-1141, noted in code). Drag-pan verified through the REAL
DistrictScreen-to-viewer chain and pinned by test (no fix needed).
140 tests across three suites, 0 failures; 94 sibling tests no
ripple; gdlint clean (atlas_viewer.gd at the 1000-line cap a second
round — structural extraction flagged for maintenance).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoshe's blocking find: district_pos_at divided the latitude axis by
tex_h where the server forward map uses ta.h.saturating_sub(1)
(district_profile.rs:1436; ground-truth inverse aliveness_probe.rs:511)
— every off-equator click descended into the wrong district, up to
~20km drift, live-repro'd at 10.2km. Fixed to /(tex_h - 1.0) with the
reference implementation's degenerate-texture guard; verified against
the repro (row 50: old formula recovered 41, fixed recovers 50 exact).
Columns were already correct (longitude wraps — the asymmetry the
docstring documented but the code didn't implement).
Regression tests transcribe the server forward formula (source cited)
and round-trip three off-equator rows — one per hemisphere plus
near-pole — asserting EXACT recovery; a dedicated drift guard is
framed as Tyre's C2 (a client-side twin of a server mapping owns its
own round-trip proof). Root-caused the coverage hole: the old
equator test sampled tex_h*0.5 believing it was the equator pixel —
the true equator is (tex_h-1)*0.5, so the test never verified what it
claimed; corrected with the why documented.
Araminta's find: the descend reticle drew unconditionally on hover —
over a city the marker flared white (city-click signal) while the
reticle+extent label promised descent, though the click correctly
resolved to city data. Extracted _should_draw_descend_reticle() with
the missing _hovered_city.is_empty() clause; three state tests pin
shows-over-map / hides-over-city / hides-when-idle.
test_atlas_descend_entry 32/32; adjacent clusters no ripple; gdlint
clean (atlas_viewer.gd at exactly the 1000-line cap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entry per Jeroen's 2026-07-21 revision: planetary heightmap is now
FIXED — all drag-pan/wheel-zoom input removed (set_view/get_view_*
capture API survives for the golden harness); hover shows a
not-to-scale bracket reticle with the real extent labeled (a true
n=32 rectangle is sub-pixel on the planetary canvas — the honest
representation given the morph transition is deferred), and a click
that misses every city marker descends (city-click wins — one
gesture, two contextual reads, no modifier). Descent pushes a new
'district' nav screen centered on the click point's DistrictPos via
atlas_descend_geometry.district_pos_at (the pixel-to-district inverse
of the server mapping, verified against scale.rs).
Regional mode: atlas_window_viewer draws the composite (morphology x
elev_q lightness base; temp/moisture/veg toggles — temp reuses the
region-ramp colorizer exactly; Marine=6 transparent; glaciation
always-on tint matching apply_ice_tint's REAL gate, None|Light no-op,
over the amendment's looser prose — documented); pan-on-held-composite
with edge-crossing refetch + border-fade during the queue-based
derive wait; zoom never refetches. atlas_window_cache: LRU keyed
(body_id, center, n), touch-on-read, evict-only, no freshness (D-227).
atlas_window_request mirrors the generation-proxy pending-retry shape
for None-until-derived. Codec: window params omitted from the wire
when absent — byte-identical for every existing caller.
Live-verified against the T-1137 server in-worktree: real round-trip
on a GJ380c coastal district (6 fields x 1024 cells), echo staleness
guard, genuine ~1.4s background-derive wait, pan-edge refetch to an
adjacent window, cache-hit on re-descent with zero network. Full
client suite 3194/3194; gdlint clean on all 18 files.
Open follow-ups flagged in-code: header location label always falls
back to coordinates (nearest-settlement needs a join the district
window does not carry); atlas_standalone.gd's 'atlas_app.gd is never
modified' doc line is now imprecise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoshe's finding: the wrap fix gave the bar container the full
header-adjacent width, and with ALIGNMENT_END + the pre-existing
MOUSE_FILTER_STOP that left a ~700px dead strip (1920px screens) left
of the chips silently swallowing map clicks/drags near the top edge —
a regression the wrap change introduced by widening the rect without
revisiting the filter. Two edits: the container is now
MOUSE_FILTER_IGNORE (the legend-panel/header/empty-notice idiom; chip
Buttons STOP their own events so toggles and tooltips are unaffected),
and _is_over_ui() no longer checks the bar rect (with IGNORE, chip
events never reach the viewer — checking the wide rect would recreate
the dead strip). Regression test pins both: filter mode + an
empty-strip point not registering as UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Jeroen's first make atlas hands-on: 17 overlay chips in a single
HBoxContainer row ran off the right screen edge (positioning also
guessed the bar's width with a 520px fallback). The bar is now an
HFlowContainer with ALIGNMENT_END; the viewer gives it everything
right of the header (OVERLAY_BAR_HEADER_RESERVE) and rows wrap
right-aligned — verified via live capture, all 18 chips visible.
OVERLAY_DEFS tooltips leaked dev provenance into player-facing text —
ticket ids (#960, T-960, T-1046, T-1118, T-1119) and decision ids
(D-181, D-226, D-243) mean nothing in-game. Stripped to plain
language; the visibility-ladder semantics (public signal / observable
/ needs corporate contact / needs insider access) stay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T-1118: gen_region_grid overlay (label TMP) — _draw_gen_region_grid
copies _draw_gen_district's self-contained mapping (dims from the
layer dict); mean-temp cold-to-hot ramp over -50..+50 C;
REGION_TEMP_NONE_DC airless sentinel = skip-cell (undrawn, never an
invented color); legend entry.
T-1119 (touch points 3-6): quarter_footprints protocol passthrough;
gen_l4_quarters overlay (label QTR) — density-scaled glyph anchored
on the L3 settlement dot joined by city_id, shape = dominant district
type (corner-tab/diamond marks for Commercial/Industrial/
Administrative), color = density ramp on the settlement-gold family,
zoom-gated at SETTLEMENT_LABEL_MIN_ZOOM; landmark/corridor counts
never drawn (D-226(d) tooltip-only ceiling); legend entry.
Structure: atlas_viewer.gd and protocol.gd were over gdlint's
1000-line cap before this batch; cleanly-separable responsibilities
extracted on existing precedent — atlas_generation_state.gd (per-layer
data + accessors), atlas_generation_proxy.gd (polling/retry/pending
machinery), atlas_overlay_colors.gd (pure ramp/shape lookups),
atlas_map_protocol.gd (atlas/starmap/citynames codec, the
browse_protocol.gd delegate pattern). Public APIs preserved exactly;
_gen_state stays a field default (RefCounted, pre-_ready safe) because
_ready()-construction breaks every bare AtlasViewer.new() test —
documented inline.
Tests: registration + round-trip for both overlays; pure-function
suites for the temp ramp (endpoints/midpoint/clamp/sentinel) and
quarter glyph (scaling, zoom gate, ramp, notch across all 9
DistrictTypes); Tier-2 replay asserts exact literals from the real
server-generated fixture incl. the airless sentinel. Color.lerp(a,b,
1.0) is not bit-exact to b — endpoint assertions use per-component
is_equal_approx. Full suite 3094/3094; gdlint zero warnings incl. the
two previously-over-cap files. Live capture: legend grows to 7
sections, TMP/QTR toggles clean against a live server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New implant app (app_path implant/browser, key B, fullscreen), sibling
to the Atlas per Jeroen's IA ruling: kind picker -> generic filterable
index (live search-mode typing) -> generic detail, parameterized per
kind, composed entirely from D-169 components. available_in_companion
left unset (default true) — the app appears in the companion shell
automatically via the generic-host seam, zero companion-side wiring.
browser_adapter.gd is the sole home of literal wire field names: maps
Oscar's BrowseResponse contract ({id, primary, secondary} index rows;
BrowseDetail enum-as-single-key-map) to view models for all six kinds,
folding join partners (system economy/factions/culture, corporation
presence, commodity production chains with nested Leontief inputs).
browse_protocol.gd split out of protocol.gd (max-file-lines);
sim_bridge gains browse_response_received + request_browse_index/detail.
Live-data catch: a present-but-NULL key (unnamed asteroid belt
proper_name) bypasses Dictionary.get fallbacks and rendered '<null>' —
_display_or() null-vs-absent helper applied across all six detail
mappers, 4 regression tests distinct from the absent-key cases.
43 gdUnit adapter cases; full suite 3074 green. Live-verified against
a real server + real systems.db: all six kinds Ready with real row
counts (301/3240/466/165/36/28), detail drill-down, NotFound on bogus
ids. Spawn-mode DB resolution issue found during verification is
pre-existing (cwd-relative data/systems.db) — server-side fix follows
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New atlas_standalone scene/script: attach-or-spawn boot (one REAL
connect_to_sim attempt at SR_PORT-or-9876 — a separate throwaway TCP
probe was proven by live run to kill a pre-accept-loop server via
broken handshake pipe; never abandon a connected socket), else spawn
--port 0 via new ServerProcess.start_with_pipe + LISTENING:{port}
stdout parse, retry against the resolved port. Reader role wired end
to end: protocol.encode_startup_message optional role param (empty
omits the wire key — byte-identical for all existing callers),
sim_bridge.connection_role suppresses the post-handshake
RequestAllSettings auto-send, hud_groups skips AutoPause/AutoResume
sends for readers (all three would otherwise burn Reader violation
strikes per the T-1130 matrix — endorsed by Oscar).
Generic implant host per D-254 SS3: the shell instantiates ALL
registered implant apps; implant_app_manifest gains
available_in_companion (opt-out, default true) and
implant_registry.instantiate_all a standalone filter param (default
preserves hud.gd behavior byte-identically). Boot order is
instantiate_all THEN open_app (reverse renders a permanently black
window — app_changed fires with no listener; matches hud.gd's order).
Owned-server lifecycle: _exit_tree stops a spawned child, attached
servers survive companion close. Known engine limitation documented:
raw SIGTERM bypasses all Godot notifications and orphans a spawned
server; WM close paths verified clean.
Live-verified: spawn-mode (301 systems rendered from systems.db over
the wire), attach-mode, two simultaneous readers, clean shutdown with
zero orphan processes. 14 new gdUnit tests (port/LISTENING parsing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AtlasViewer gains public set_view(zoom, offset)/get_view_offset (clamped, review-flagged API gap) + gdUnit suite. visual_capture.gd gains an atlas_matrix scenario (one boot, tests/atlas_shots.json-driven: 7 verified bodies x zoom x overlay sets, signal-gated waits on atlas_layers_received/city_names_received with timeout fallback). 12 individually-addressable golden scenarios registered in tests/visual.json. Root-level settings.db* gitignored (capture-spawned servers write their settings store to cwd). Fixes found live: GDScript lambdas capture value-type locals BY VALUE (wait-flag moved to Array carrier); RegionalScreen.enter() no-ops when the screen id is unchanged (direct enter() per body after the first).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
H1 demux: ShapeProbe defensive multi-shape rejection (union frames now Err, not first-match; +2 tests) and doc claim made honest. H2/T1 SystemIndex.reset_test_state() folded into SimBridge.reset_test_state() (load() inline per autoload rule) + has_pending_request() accessor. H3 no-op tests now assert the replay flag both directions. H4 retry test actually ingests a failure and asserts the retry semantic. H5 error fixture uses the normalized status string. H6 bridge_tcp e2e sends all five frame shapes over real TCP (star-map + city-names buffers asserted). H7 positive replay-on-CONNECTED test via the test_local_bridge test-mode-flip precedent (stub bridge captures + decodes the request bytes). H8/T2 stale PLACEHOLDER doc replaced with the confirmed contract. H9 is_capital doc matches the COALESCE reality. T-r1 demux ceiling written down (next shape = tagged envelope). T-r2 AtlasLayerResponse governance ceiling comment. Lead item: the four cargo-fmt-formatted files from the gate round are now committed (layer_proxy/plugin/bridge-mod/main). H10 note for the record: the 13 snapshot_*.msgpack fixtures in commit 845737617 were regenerated because they were stale against their own generator (pre-existing version-key removal) — verified harmless, no client reads that key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T-960: gen_l2_roads (MaintenanceAuthority-colored polylines, rail styling, junction markers) + gen_l3_settlements (size-scaled markers, capital shape, name labels) overlays — cities render on generated bodies for the first time; left-side generation legend panel (D-226 item 3, data-driven per-overlay spec, implant component library); protocol.gd decodes road_graph/settlements + the two new response types. T-949: system_index/atlas_app/overview_screen migrated off the direct star_map_data.json read to StarMapRequest over the bridge (loading state + replay-on-connect, no silent file fallback); atlas_viewer _load_markers requests CityNamesResponse for non-Sol bodies; Sol keeps the legacy authored markers.json geometry read (D-236/T-1073, load-bearing guard). Lead fix: _send_star_map_request now carries the same guard as request_star_map — the autoload's _star_map_wanted leaked across gdUnit suites and the unguarded replay-on-CONNECTED crashed 8 pre-existing flow tests on a Nil bridge; reset_test_state clears the flag. Fixtures regenerated via gen_fixtures (road/settlement samples). Full suite 2946/2946.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client half — the cascade's district tier is now visible in the Atlas map.
- protocol.gd: decode the district_grid field from AtlasLayerResponse.
- atlas_viewer.gd: a gen_district ('MRPH') toggle in OVERLAY_DEFS + the
district-grid generation state (set/get) + wire it from the Ready response.
- atlas_marker_overlay.gd: _draw_gen_district() paints the coarse cols×rows grid,
each cell coloured by its MorphologyZone discriminant (D-239 §6, 17-zone
palette), semi-transparent under the Layer-1 line overlays. Planetary map view
(not the 2km on-demand districts — those are Phase 5 in-world).
gdUnit4 test: overlay registered + district grid round-trips through the viewer
and the protocol decode. Visual tuning of the palette can follow once eyeballed
in the running Atlas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Production fixes surfaced by honest test triage:
- hud_groups.gd: _set_group_z crashed on freed HUD nodes — the typed loop
variable errors before the is_instance_valid guard runs; prune first
- fog_state.gd: _resize cleared _prev_visible (world-space keys survive
resizes), so pre-resize tiles never decayed VISIBLE→EXPLORED (D-059)
Test debt (T-928/929/934/935/936/937/938/939, T-864, T-973): lambda
local-capture bugs rewritten with array captures (now assert exact
emission counts), e2e suites updated to the current handshake +
StartupMessage protocol and stream-aware reads against the live binary,
fog perf test measures steady state, chime test pins the shipped 800ms
catalog asset (D-067 amended separately), monologue gdUnit4 API typo,
battery-warning tests follow the MetaScreen on_open lifecycle. 3 sprint2
proof tests revived (corner_reveal had passed from the wrong tile — NPC3
blocks (18,14); route corrected). Soft-skips converted to real do_skip
reporting. T-1068: 7 orphan .gd.uid deleted, _format_pop/_format_radius
deduped into atlas_format.gd (preload, no class_name — headless cache).
Suite: 1264 cases/20 failures → 1268/0, independently re-verified
(2536/2536, exit 0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Generation overlays map positions from Layer1Output.grid_w/grid_h onto the
displayed texture (not the markers.json grid), fixing the off-by-2 scale.
- AtlasViewer loads ImplantPending by path instead of referencing the new
class_name, so a stale global-class cache (a session that hasn't re-imported
after the class was added) can't fail to parse AtlasViewer and break the
atlas from opening.
- Rivers and mouths use the reliefmap's own water colour (sampled median ocean
blue) so river lines blend into surface water instead of reading as a
distinct-coloured line flowing onto the sea.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A reusable implant-layer busy indicator (Araminta's spec): an animated
bracket sweep `[ >>>>···· ]` with a caption, themed entirely via ImplantTheme.
The Atlas shows it centered over the canvas while the Layer-1 proxy is Pending
and hides it on Ready — so cache hits never flash it and serverless mode never
shows it.
Layer separation (per direction): ImplantPending belongs to the diegetic
implant UI only. The global, non-diegetic UI layer must use its own busy
indicator with its own visuals — sharing the sweep logic is fine, resting on
this theme/these glyphs is not. Documented in the component header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
show_body() now requests the body's Layer-1 cascade output via
SimBridge.request_atlas_layers(); the response feeds set_generation_layer1().
The proxy returns Pending on a cache miss and generates in the background
(D-225), so the viewer re-requests every 0.5s (20-retry ceiling) until Ready,
guarding against stale responses by body_id. Serverless/test mode is a no-op
(overlays stay empty). Connected in _ready, torn down in _exit_tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render the proxied Layer1Output (D-225) as toggleable Atlas overlays. The
viewer stores the decoded Layer1Output via set_generation_layer1() and the
marker overlay draws three new layers gated by their visibility flags:
RVR river_cells as dots, confluences as small circles, mouths as
double-ring sea-terminus markers
BAS drainage-basin boundaries as thin closed polylines + faint fill
ATR geographic attractors — shape by attractor_type (Araminta's 7-shape
vocabulary), color by sub_biome, size by strength; <0.15 culled
Layer-1 positions are [row, col] in the 512x256 working grid, which matches
the viewer's grid_to_canvas transform, so they project directly onto the
heightmap. OVERLAY_DEFS gains gen_l1_rivers/basins/attractors (toggle group)
so AtlasOverlayBar auto-exposes them as toggle buttons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- heightmap.rs: read sea_level from the PNG tEXt chunk (bake writes it),
default-fallback param; new test reads_sea_level_from_text_chunk.
- client atlas_viewer.gd: load reliefmap.png (color display) instead of
heightmap.png (now 16-bit grayscale elevation, cascade-only).
- drop atlas_body_heightmaps: removed from systems-schema.sql; DROP TABLE in
import_economics MIGRATION_SQL (the PNG is the store now).
- D-202 amendment: implementation-status note (consumer + producer done),
resolving the review's 'reads done but producer pending' point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Heightmap and markers load from wiki filesystem path via FileAccess
instead of res:// (wiki is outside the Godot project root). Overlay
bar buttons now receive mouse events — viewer skips input processing
when cursor is over UI elements. Header gets explicit minimum width
to prevent vertical text stacking. Shadow economy overlay removed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Body info panel with globe artwork renders on the right side of the
system map instead of navigating to a separate screen. Single-click
shows/swaps panel, double-click opens regional heightmap view, ESC
closes panel. Station panel also moved to right side for consistency.
Moon and station positioning clears parent body radius + label space.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Half-sun on left edge, planets left-to-right with radius-based sizing
(Earth = 24px reference, unclamped — gas giants fill the view). Globe
textures loaded from wiki at runtime. Double-click on reach map opens
system view. M closes atlas from any screen, ESC navigates back.
Fixes Godot 4.6 type inference parse errors in main.gd (var zoom/speed).
Uses physical_keycode throughout for layout-independent key handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export pre-computed drainage basin boundaries from atlas_province_boundaries
into markers.json during generate_atlas. AtlasMarkerOverlay draws province
polylines with semi-transparent fill under the political_zones overlay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Protocol.gd is an autoload — replace Messagepack class_name refs with
inline load() calls via a static helper. main_menu.gd extends MetaScreen
by class_name which fails at parse time; switch to path-based extends.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix autoload parse-order violations: sim_bridge.gd, input_mapper.gd,
audio_manager.gd now use load() for class_name types instead of
direct references (LocalBridge, ServerProcess, Constants)
- Collapse redundant tween validity guard in dialogue_box.gd to
is_instance_valid(panel) only
- Add clarifying comments to fog test resize assertions (8-tile
padding trigger, 32x32 fixture assumption)
- Fix test_examine_display_sprint18 case 2: GameState.has() →
"field" in GameState (Node vs Dictionary API)
- Fix test_game_state_sprint20: rename before_each → before_test
(GdUnit4 lifecycle hook)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add free camera mode (F4 toggle): WASD pan, scroll zoom, decoupled
from player position (#898)
- Strip archetype-driven code: remove character_archetype, lattice_profile,
and lattice color palettes from client (#882)
- Fix confrontation_monologue signal not firing in headless test mode (#867)
- Revive fog state behavioral tests: EXP_EXPLORED persistence, grow-only
bounds, texture-resize copy, BoundaryWall handling (#879)
- Triage pre-existing test failures: fix examine_display dismiss timing,
fog test position fragility, rendering snapshot assertions,
time_display format (#871)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- character_creation.gd:1614: type-annotate dir_name to fix GDScript
inference parse error. CARDINAL_DIRS is an untyped const Array, so
`var dir_name := CARDINAL_DIRS[idx]` failed type inference and blocked
test_character_creation_sprint28.gd test discovery. Runtime was lenient
but gdUnit4's scanner is strict.
- test_merge_path_flows_sprint37.gd (_load_char_create): call add_child
before _update_start_btn_state so @onready vars (_footer_start) are
bound when the helper dereferences them. Unblocks 2 merge-path tests.
- test_anti_tedium.gd (test_bug_report_sends_unpause_on_close): call
dialog.close() instead of the old dialog._close() rename casualty.
`on_close()` is a lifecycle hook — it doesn't actually transition
state; `close()` is what MetaScreen exposes.
- test_anti_tedium.gd (before_test): clear MetaStack._stack. Prior tests
leave stale freed dialog refs on the stack; _any_pausing() iterates
the stack during close() and crashes with "previously freed".
Net: sprint-37 test files (test_merge_path_flows_sprint37.gd,
test_anti_tedium.gd) now pass. Overall suite: 2428/2488 passing
(60 remaining failures are pre-existing, unrelated to sprint 37).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the version-mismatch guard from Protocol.decode_snapshot() and the
PROTOCOL_VERSION constant from the client (server side done in #874).
Core changes:
- protocol.gd: remove const PROTOCOL_VERSION, remove version mismatch guard,
remove "version" from return dict, add gauntlet_mode/room_id decode
- sim_bridge.gd: remove handshake version check; relax handshake guard to
require only a valid Dictionary (server no longer sends protocol_version);
emit handshake_complete(0) for API compat
- loading_screen.gd: drop "· protocol N" suffix from version label
- test_harness.gd: replace Protocol.PROTOCOL_VERSION with literal 23
Test updates (21 files): replace "version": Protocol.PROTOCOL_VERSION with
"version": 23 in all snapshot bytes dicts; remove snapshot.version == N
assertions; remove version-rejection tests (test_rejects_version_6,
test_decode_snapshot_rejects_missing_version, test_decode_snapshot_rejects_old_version,
test_protocol_rejects_version_mismatch, test_sim_bridge_test_snapshot_uses_current_protocol_version).
Also includes: #872 bookmark_catalog carry-forward regression test, and
#873 merge-path flow tests (test_merge_path_flows_sprint37.gd).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses Tyre's 9 architecture items from the sprint-36 client review.
- decisions/architecture.md (Tyre #1): D-192 now says "deprecate; removal
tracked in #868" instead of "remove". The branch does not remove the
version field or guard — that belongs in the coordinated server+client
PR. The decision text now matches the code on this branch.
- meta_stack.gd (#2): handle_escape() on a screen with
closable_by_escape=false now consumes the event unconditionally. Was
returning whatever on_escape() returned, which default-returned false
and leaked ESC into main.gd's implant/settings chain — opening the
settings dialog behind the loading screen.
- debug_console.gd (#4): drop the direct KEY_ESCAPE branch in
_unhandled_input. ESC now falls through to main.gd → MetaStack, which
finds the console on top of the stack and closes it via the normal
path. Other keys are still consumed so movement/action can't leak.
- main.gd (#6, #10): extract the ESC priority chain into
_handle_menu_key() so "MetaStack → implant → settings" is a named
thing. Add a comment near connect_to_sim explaining that
GameState.bookmark_catalog survives the Option A scene transition via
the autoload.
- main_menu.gd (#7): header comment documenting the double LoadingScreen
lifecycle — safe today because main_menu.tscn and main.tscn never
co-exist, noted for future promotion to autoload if that changes.
- meta_screen.gd (#8): apply captures_input symmetrically in open()/
close() — was set in open() only, so a screen changing the flag
between open+close kept the opened value forever.
- meta_screen.gd (#9): on_escape() docstring clarifies the tri-state
(consume-and-hold / consume-and-close / ignore) — and that
closable_by_escape=false is the screen-wide way to say
"consume-and-hold".
- bug_report_dialog.gd (#11): capture_cancelled now emits from
on_close() (covers any close path — ESC, MetaStack pop, programmatic
close) rather than only on_escape(). A new _completed flag
distinguishes completion from cancel so the two signals stay
mutually exclusive.