Commit Graph
15 Commits
Author SHA1 Message Date
jpmschweitzerandClaude dac64a8a40 fix(client): the Atlas map scrolled itself off the world
Jeroen: 'it scrolls out of screen automatically.' It did, with no input.

_gui_input only fires while the pointer is over the Control, so
_last_mouse_pos freezes at wherever it was last seen. Leaving the map ALWAYS
means crossing an edge, so the frozen value is always inside the 24px edge
margin — and the viewer went on believing the cursor was held there, panning
forever. Moving the mouse elsewhere could not stop it, because 'elsewhere'
generates no events this Control ever hears.

NOTIFICATION_MOUSE_EXIT now resets to the same (-1,-1) sentinel the field is
born with, making 'pointer is not over the map' and 'pointer has never been
over the map' the same state. Neither should scroll, and there was already a
test asserting the second case — the first had no equivalent.

Considered and rejected: reading get_local_mouse_position() live instead of
caching. It is arguably cleaner, but it cannot be injected in a headless test,
so it would have traded a bug for the inability to prove the fix — and the
existing edge-scroll suite drives _last_mouse_pos directly.

This also explains the drift I had blamed on the capture harness (T-1236):
same defect, and the harness was simply exercising it faithfully.

Client suite 1833 / 1807 passed / 0 failed / 26 skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 21:36:32 +02:00
jpmschweitzerandClaude 144d35d2a5 fix(client): size Global's cell count so the display ratio divides exactly
Jeroen: 'still not filling the screen... maybe we should calculate the depth
that needs to be invented based on the canvas size somehow.' That is the fix.

The old flow picked cells from the viewport, let the SERVER impose Global's
2:1 aspect, then fitted the returned extent into the drawable area with an
INTEGER pixels-per-gridunit ratio. When that extent did not divide the area
evenly, floor() dropped a whole step — and at ratio 2 the only step below is
1, i.e. half size. Worse, because the client fitted an extent it had not
chosen, the result could fill NEITHER axis.

Inverted: global_fill_extent() chooses the cell count FROM the ratio, already
shaped 2:1, so cells * RATIO is the drawn size by construction and lands
exactly on the binding axis. Verified across window shapes — 1920x1080 fills
width, 2560x1080 fills height, 2560x1440 width, 3440x1440 height, 1280x720
width. Exactly one axis fills at every shape, which is the most a 2:1
equirectangular canvas can do in an arbitrary viewport; the other letterboxes.

Global keeps its 2:1 aspect because it is 360 degrees of longitude by 180 of
latitude — the aspect cannot follow the viewport without shearing the map.

Tests pin the invariant directly: 2:1 preserved, never overflowing the
drawable area, and never leaving slack on BOTH axes.

Client suite 1832 / 1806 passed / 0 failed / 26 skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 21:24:21 +02:00
jpmschweitzerandClaude d36d458b74 fix(client): Global drew at half size — request and fit disagreed about the legend
Jeroen: 'that does not fit the viewport'. Correct — Global rendered a real map
into roughly a quarter of the available area.

_letterbox_scale_for() reserves LEGEND_COLUMN_PX before computing the Global
fit, but _request_extent() sized the request against the FULL Control width.
Because that fit is an INTEGER pixels-per-gridunit ratio, the disagreement
does not degrade gracefully: at a 1920-wide window we asked for 960 gridunits
but could only fit floor(1628/960) = 1 px each, so the canvas drew at HALF the
intended scale with room to spare on every side.

The request is now sized to the drawable area, so both sides agree: 814
gridunits at 2 px = 1628 px, plus the 292 px legend column = exactly 1920.
Verified through the capture harness — canvas_scale went 0.5 -> 1.0.

Only Global reserves the column, so only Global adjusts; the fixed rungs are
untouched.

Also corrects the regression test I wrote yesterday, which computed its
expectation from the full viewport and so encoded the bug.

Client suite 1830 / 1804 passed / 0 failed / 26 skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 21:13:04 +02:00
jpmschweitzerandClaude e024cfb3f9 test(client): the Atlas Global goldens have been measuring nothing
Every atlas capture scenario built its body dict without body_radius_km.
Radius is Global's ONLY spacing input (2*pi*R / width), so every Global
golden has been a degenerate block since these scenarios were created — the
map rendered at 0.000 km/gridunit. The fixed rungs were unaffected, since they
derive spacing from the rung's own cell size rather than the body.

That means the "eyeball check against .cache/screenshots baselines"
discipline has been hollow at Global specifically: a baseline that is a solid
rectangle diffs clean against a new solid rectangle. It is how a 2x1 canvas
survived in front of two verification layers.

Adds the real radii to all ten scenarios (Lendel 6238.4, Vethis 6959.3,
Arbour 6711.0, Ferrath 6062.0, Threshold 5503.5), and makes the capture log
course and settlement counts alongside canvas_cells — an empty annotation
layer was previously indistinguishable from a populated one in the log, which
is exactly the signal needed to tell "no rivers" from "rivers not drawn".

Also updates three suites to the corrected Global cache contract: the key now
honours extent (collapsing it meant a resize could never miss), so the shared
_land_global_canvas helper must cache under the extent the viewer will
actually request, and the two make_key tests now assert the real rule —
centre collapsed, extent honoured — instead of the retired sentinel.

Client suite 1830 total / 1804 passed / 0 failed / 26 skipped.

Pair session with Jeroen, 2026-07-27.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 20:52:54 +02:00
jpmschweitzerandClaude 1c45cd2ec8 fix(client): Global rung derived its canvas from a pre-layout viewport
Eyeballed on Lendel: the Atlas opened on a Global map that was literally two
cells — one green, one blue — stretched across the window, reporting
19,598.512 km/gridunit, which is exactly half the body's circumference.

Two bugs, both of which the D-255 extent inversion turned from harmless into
fatal.

enter() fires its first request BEFORE this Control is laid out, and a
not-yet-laid-out size is not always exactly Vector2.ZERO — a few stray pixels
sailed past the `== Vector2.ZERO` guard, so the viewer asked for a 2x2
gridunit canvas and the server's 2:1 fit floored it to 2x1. That never
mattered while Global discarded the requested extent and took its cell counts
from the body's region grid; the moment the request became the canvas size, a
transient layout artefact became the map. Any viewport below a plausible
panel size is now treated as not-laid-out.

And Global was excluded from the refetch settle entirely, so a canvas born at
the wrong size could never heal however the window was resized. That
exclusion was correct when no viewport could change Global's extent. Global
now takes the SIZE refit like every other rung, but still never the pan
re-float — its canvas is whole-body and origin-anchored, and the server
ignores `center` for it.

Both have regression tests. The second asserts on _world_center rather than
_view_offset, because _recompute_canvas_transform() legitimately re-centres
the offset on any canvas adoption and would have made the test pass for the
wrong reason.

Worth noting for the class: no test written today could have caught this.
Every one supplies an explicit viewport. The bug lived entirely in the gap
between "scene loads" and "layout completes" — a seam a live launch
exercises and a unit test does not.

Also stages governance/README.md's pql-maintained record index (D-258).

Pair session with Jeroen, 2026-07-26.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 23:43:32 +02:00
jpmschweitzerandClaude 0a0419abcc feat(client): invert the Atlas rung relation — rung sets extent, not spacing
A rung used to fix the gridunit SPACING, with the canvas extent falling out
of spacing x cell count. That is why the top of the ladder was unusable: at
REGION_M spacing a viewport-sized canvas spanned ~251,658 km — six times
around a rocky body — so the Region rung capped to the body and redrew the
Global picture pixel-for-pixel. "Global and region look the same" was not a
rendering bug; it was this relation, stated in metres.

Inverted: a rung fixes the EXTENT and the spacing falls out of the canvas
size. The shorter viewport axis spans exactly one cell of the rung's level,
so a widescreen window shows more ground on the long axis rather than less
on the short one. Every rung now shows the ground its name promises —
Region 262x466 km, District 4.1x7.3 km — and the canvas cell count is
viewport-driven and identical at every rung, so derive cost no longer varies
with depth and resize is free.

Consequences that fell out of the inversion rather than being chosen:

- Region leaves the orbital derive set. It was envelope-only because at
  251,658 km nothing finer made sense; at 262 km it is a genuine provincial
  map and takes the full courses-aware derive. Region having no rivers at
  all was much of why the top of the ladder read flat. It also joins the
  deep display ratio for the same reason.
- The S2 station-spacing floor is deleted, not retuned. It guarded an
  O(1/spacing) blowup that the inversion makes structurally impossible (the
  canvas cell count is now constant across rungs, so stations-per-course is
  bounded however deep you scroll). Kept, it would do active harm in the
  opposite direction: a 2,048 m pitch across a 3.6 km District canvas places
  two stations and draws every river as a straight line. Station placement
  gets its own generator pass.
- cap_extent_to_body is superseded and now a documented no-op. A canvas can
  no longer over-request a body by construction. The residual question —
  whether a rung's cell exceeds the whole body — is liveness, not capping,
  and is_rung_live_on_body() answers it by omitting the rung. Empirically it
  never fires on inhabited content: all six rungs are live on all 271
  populated bodies with a radius.
- snap_to_gridunit no longer truncates its multiplier to int. Post-inversion
  the deep rungs run sub-metre (Chunk ~0.12 m at a 1080 px short axis), where
  int(spacing) floors to zero and would collapse every request centre onto
  the origin.

Both sides derive spacing from the same three inputs (rung, echoed cell
extent, body radius) rather than one telling the other, so there is nothing
to keep in sync beyond the constant table itself. Body radius already
reaches the viewer via enter(); no wire change.

Pair session with Jeroen, 2026-07-26. D-243/D-255 amendments to be backfiled.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 21:38:08 +02:00
jpmschweitzerandClaude 36482de5d6 fix(ui): Atlas fetch readout, and one settle timer for resize and pan
Two viewer fixes from the same pair session; they share
step_canvas_viewer.gd so they land together.

FETCH READOUT. A cold derive takes seconds and the map gave no honest
sign of it. Root cause found while building the replacement: a Control
paints its own _draw() BEFORE its children, so everything the viewer
drew itself — the old DERIVING TERRAIN label AND the pending wash —
was painted UNDER the terrain canvas, visible only when no texture
existed at all, i.e. never in the slow-fetch case they existed for.
That has been the state since the stepped viewer shipped. The readout
now lives in its own overlay node added after the canvas: a centered
implant-idiom panel, DOWNLOADING MAP DATA, indeterminate sweep, 250 ms
grace so cache hits never flash it, held canvas still drawing beneath.
Indeterminate by design — the server reports no derive sub-steps, and
a progress fraction we cannot source would be invented.

ONE SETTLE TIMER, THREE TRIGGERS. The viewer never re-requested a
canvas on resize, so one derived for a smaller window letterboxed
forever in a bigger one — the map not filling the frame. And the
pan-edge refetch fired from inside the per-frame pan loop the instant
its threshold was crossed, so a held edge-scroll issued a fresh
request AND snapped the view on every frame past it. Both are the same
event: the user is still moving. A shared one-shot timer now collapses
them into a single request at the resting state, with a hard pan
threshold that still fires immediately when the canvas edge is about
to enter view (waiting there would show empty background), and
drifting back inside the soft threshold cancels the pending request.
Resize reaches this Control identically whether the OS window or a
diegetic in-implant parent changed, so both sources are covered.

Two new tests pin the soft path (schedules, does not refloat or snap)
and its cancellation; the pre-existing threshold test was verified to
still discriminate — its drift trips the new hard threshold — rather
than passing vacuously.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 17:12:05 +02:00
jpmschweitzerandClaude Fable 5 a4d9a4fa09 fix(ui): PR #217 review fixes — derived legend offset, 960x540 goldens, non-overlap tests
The legend's reposition() now derives its Y from the header panel's
MEASURED bottom plus a named 12px gap (new accessors on the viewer;
deferred recompute so it reads settled layout, refreshed on header
content change) — the old hardcoded 60.0 sat 17px inside the wrapped
header's real 77px bottom, fusing the panels. Pixel-proven at the
evidence center: bottom=77.0, legend top=89.0, gap=12.0 exact. All 13
goldens regenerated at the DOCUMENTED 960x540 (the 1280x720 rider was
my own CLI flag, not a config; PIL-verified) with two-run byte
stability. Two structural tests pin the header/legend relationship
(T-1192 precedent), proven failing-first against the old constant
(-17.0px reported), and the legacy test that pinned the literal 60.0
now asserts the derived relationship so it cannot re-enforce drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 15:13:11 +02:00
jpmschweitzerandClaude Fable 5 9ac98542d1 fix(client): isolate viewer tests from the machine-shared atlas disk cache
user://atlas_cache/ is one directory for every worktree gate run, live
capture driver, and real play session, and T-1183's Tier-2/3 lookup
short-circuits BEFORE test_mode's silent-no-op IPC — so a warm shared
cache delivers real canvases into tests written against 'nothing ever
arrives'. Caught live: a concurrent GJ380c Global capture flipped the
two before-any-canvas viewer tests in another worktree's push gate.

Adds disk_cache_root_override on StepCanvasViewer (threads into
StepCanvasRequest's existing test-injection-only seam; production
leaves it empty) and routes every direct viewer construction in the
viewer + legend suites through a _make_viewer() helper pinning an
isolated root. First slice of T-1193's viewer test seam; screen-built
viewers still share the default root (noted on T-1193).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 20:45:46 +02:00
jpmschweitzerandClaude Fable 5 34dbac9bf6 feat(ui): AtlasAgentInterface — observe/act named-intent control channel (D-226, T-971)
D-226 layer 4, rebuilt against the post-D-255 stepped Atlas after the
Phase-1 reconciliation (the original intent list targeted the retired
continuous-zoom viewer). Eleven intents, each backed by the exact
production handler a click calls — select/open for systems and bodies
(extracted shared by-id tails so click and intent paths are one code
path), scroll_rung, reset_view, back, open/close_atlas, set_overlay —
plus two new first-class capabilities: jump_to_center (the fixed-center
revisit pattern proven by five eyeball drivers, via a new
StepCanvasViewer.jump_to seam that reuses _scroll_rung's exact request
tail — same extent cap, same cache keys) and get_current_canvas_summary
(allocation-light reads off the raw wire dict, courses-by-class,
draw-matched settlement dedup — no PNG decode). Contract shape: dumb
AtlasAgentBridge autoload holding the app handle (untyped per the
parse-order rule), all logic in the static AtlasAgentInterface class.
observe() is side-effect-free: current state + a generic Control-walk
affordance tree. In-process consumers only this ticket (documented);
the committed reference driver (atlas_agent_driver.gd, InputSwallower +
settle-until-ready from the T-1157 inventory) replaces the scratch
eyeball drivers as the sanctioned headless-drive pattern.
select_city/open_regional dropped with recorded rationale (no
settlement hit-test affordance exists post-D-255) — diff on the ticket.
33 new tests across three suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:51:45 +02:00
jpmschweitzerandClaude Fable 5 374b195594 fix(ui): PR #205 review round — integer px-per-gridunit fit replaces fractional (T-1189, T-1192)
The fractional fit branch is deleted, resolving both review findings at
the root: tyre showed its comments cited D-255 for an exception the
record does not contain (the language came from the lead's ticket text,
not governance), and hoshe showed it returned sub-1x for a canvas
exceeding the viewport on one axis. Replacement: fit_scale_ratio()
chooses the largest integer pixels-per-gridunit R fitting both
legend-reserved axes, floored at 1 (over-viewport draws native and
crops like every fixed rung) — the fine-grained integer lattice (GJ1c
1080p -> 9px/gu = 1593x792, ~98% width; 4K -> 20) that makes the
fractional hatch unnecessary. center_offset() floors to whole pixels
(half-pixel centering would blur the texel grid). Doc comments cite the
real sanction (D-255 amendment 2026-07-25, this branch). Legend column
constant is now canonical in transport, read directly by the legend
(was an independently-typed literal); its test asserts real geometry.
New regression test proves _global_body_extent clears on body switch
and never caps another body's requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 13:14:12 +02:00
jpmschweitzerandClaude Fable 5 c1a97166c5 feat(ui): cap rung extents to the body + fit/center the Global canvas (T-1189, T-1192)
The two client fixes for the Atlas frames Jeroen flagged. Region rung no
longer requests more planet than exists: cap_extent_to_body() caps the
viewport-fit extent at the body's own region grid (the Global canvas
extent echo — cols=regions_per_equator, rows=cols/2), matched by gridunit
SPACING not rung name, applied before the request/cache key is built;
cold-start scroll-before-Global-echo requests uncapped and lets the
server clamp (documented). Kills both the side-by-side continent repeat
and the past-the-pole stripe smear.

Global opener now fit-scales and centers through one shared letterbox
mechanism (center_offset/integer_fit_scale/fit_scale, 75%-coverage
integer-vs-fractional decision, NEAREST already forced on orbital rungs
per D-255's escape hatch) also used for the Region cap's letterbox
remainder. Legend column reserved before fitting — pinned to the legend's
own width by a cross-constant test, never overlapping the canvas.

Also fixes a latent crash: NOTIFICATION_RESIZED fires mid-_ready() before
children exist; null guard in the resize path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:52:06 +02:00
jpmschweitzerandClaude Fable 5 6b76e365bb feat(ui): client disk cache — FileAccess tiers beneath the LRU (D-255, T-1183)
step_canvas_disk_cache.gd: the Tier 2/3 store per D-255(d) and the
round-2 three-tier spec. Payload is the WIRE, pre-decode — store_var/
get_var round-trips the PNG-encoded PackedByteArrays natively, and
Image.load_png_from_buffer never runs in this file. One composite key
shared verbatim with Tier 1 (hash filenames for filesystem safety);
per-body index.json with malformed-index recovery (rebuild-or-discard,
never crash).

Three independent eviction mechanisms, exactly as ruled: rung-0 Global
carries a retention floor no sweep touches (now also threaded into
Tier 1 per the T-1182 handoff); Tier 2 geometry is byte-valid forever
and evicts only by time-since-last-visit (14d starting tunable, on
body-open) and LRU byte budget (256 MiB/body, 5-min coarse timer) —
two separate sweeps; Tier 3 sim-state TTL is wired and tested but has
no production caller yet (no sim-state field exists on
EncodedStepCanvas — the D-253 stub inheritance, documented).

Hardening per D-255(d), both mandatory: per-body deep-rung cap
(512 Block+Chunk entries, enforced synchronously in put(), floor- and
budget-independent — the ticket sanctions count-or-quota; count chosen
as the direct D-226(d) information-content proxy) and a schema/version
tag on every entry (project.yaml version via the existing
loading_screen line-scan idiom — D-192 co-ship makes the client
version the wire-schema version; exact-inequality mismatch = miss +
drop, NEVER decode, checked in both has() and get_canvas()).

Integration: request_now() checks Tier 2 on a Tier-1 miss (synchronous
promote), Ready responses write through to both tiers, Pending never
writes; viewer runs the visit sweep on body-open + the background
sweep on a 5-min timer.

30 new disk-cache tests + 7 request-integration + 3 sweep-wiring tests
(restart persistence, sweep independence both directions, cap
semantics, version-mismatch never-decode, corrupt-index recovery).
Full client suite 3,440/3,440, 0 orphans; cold-parse clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 11:08:48 +02:00
jpmschweitzerandClaude Fable 5 76d82a6a51 fix(ui): PR #203 review round — total retirement + bin decode + wired reset
Tyre finding: the orphaned AtlasViewer cluster is now actually deleted
(atlas_viewer, atlas_marker_overlay, atlas_descend_geometry,
atlas_legend_panel — a sixth orphan found beyond the review list —
atlas_generation_proxy, atlas_generation_state; ~2,497 lines), with
reachability re-verified across preload/class_name/res:// strings,
every .tscn, and the standalone companion app. Test suites triaged,
not blanket-deleted: 5 pure AtlasOverlayColors tests relocated into
test_atlas_window_colors, the live type-identity regression guard
relocated into test_step_canvas_viewer, dead coverage deleted. A real
harness gap surfaced during diligence and RULED, not patched:
visual_scenarios/visual_capture golden shots call retired
continuous-zoom API — no shim (would resurrect what D-255 kills);
inventory recorded on re-scoped T-1157 (gate-invisible, manual
targets only).

Hoshe finding 1: decode_png_field now detects the [Error,
PackedByteArray] bin-shape from messagepack.gd explicitly — a genuine
msgpack bin payload decodes correctly instead of silently collapsing
to [0,0]; test built from a real round-tripped bin decode.

Hoshe finding 2: the hard zoom-out reset is wired — ascend at rung 0
with a drifted view triggers _reset_to_global (the restored HARD
condition), behavioral tests through the real input path.

Notes folded: refloat + edge-scroll test coverage, legend smoke suite,
Vector2i narrowing-safety comment with computed headroom.

gdlint clean on touched files; full client suite 3364/3364.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 09:32:52 +02:00
jpmschweitzerandClaude Fable 5 d28d24fd26 feat(ui): step-canvas map component — RTT terrain + stepped zoom (D-255, T-1182)
The two-layer client rebuild per D-255(a)(b)(e), replacing the
_canvas.scale continuous-zoom model with one viewer, one path, all six
rungs:

- step_canvas_protocol.gd: StepCanvasRequest/Response codec against
  the T-1181 wire contract — incl. the discovered png_bytes subtlety
  (rmp_serde without serde_bytes emits a msgpack int-array, not bin;
  decode repacks via PackedByteArray before load_png_from_buffer) and
  the extent-echo rule (read the server-clamped extent, never assume
  the requested one).
- step_canvas/ component: transport (six-rung ladder, cursor-anchored
  scroll steps, edge-scroll/WASD pan with re-request on edge crossing,
  hard reset-to-Global), RTT terrain layer (Image.set_pixel colorize
  per the c1 measured ruling, texture.update reuse on step-cross,
  NEAREST coarse / LINEAR fine per rung), unscaled screen-space
  annotation sibling (courses + settlement markers at literal px),
  in-memory LRU cache (Tier 1; T-1183 layers the disk tiers beneath),
  request lifecycle (pending retry, staleness gate, extent echo).
- Full _canvas.scale retirement in the same change: the zoom-scaled
  canvas model, the _zs compensation family, select_rung /
  MAX_COVERAGE_M / compute_tile_grid, the orbital-mosaic-vs-window
  two-path split, _view_zoom/_canonical_fit_zoom — 10 source files
  deleted; their 14 test suites deleted with them (T-1157 dead-goldens
  rule; replacement visual-capture coverage is re-scoped T-1157).
- Surviving surfaces kept per the ticket: atlas_window_cache.gd's LRU
  shape (the ticket's named file atlas_window_tile_set.gd was the
  retiring orchestrator; the real LRU shape lives in
  atlas_window_cache.gd — cited in step_canvas_cache.gd), overlay
  colors, legend/overlay-bar chrome, AtlasViewer descend geometry.

Determinism boundary per D-255(e): the client interpolates only within
the closed server-supplied input set. 7 new gdUnit suites (164 cases)
incl. a real extent-echo bug caught by its own test during
implementation. Full client suite green (exit 0) with the live-gated
suites running against a worktree server build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 08:53:48 +02:00