Commit Graph
530 Commits
Author SHA1 Message Date
jpmschweitzerandClaude 791600dd24 test(client): skip the 3D character compositor suite while 3D is not in play
Jeroen's call during the Phase-4 Atlas work: nothing in this suite's subject
is being changed, and it is by a wide margin the most expensive thing in the
client suite. Client run drops from 135s to 96s — 29% — from this one file.

The numbers, measured across all 86 suites:
  this suite     50.0s /   26 tests  (~1.9s each)  -> 37% of the whole run
  all 86 suites 127.1s / 1830 tests
  the other 78   ~33s  / 1804 tests
Every test instantiates a fresh CharacterVisual Node3D and loads the skeleton
.glb plus body and skin-tone assets, so the cost is asset loading per test,
not assertion count. The rest of the suite is close to free.

Used gdUnit4's own suite-skip (__is_skipped) rather than a hardcoded pass, as
requested but one level more honest: a test that returns success without
exercising anything reports as COVERAGE. It inflates the pass count and reads,
to anyone scanning a summary, exactly like a suite that ran and was fine.
The skip reports these 26 as SKIPPED in the statistics — and run-godot now
parses that field and excludes it from passed — so the omission stays visible
in every run rather than being laundered into a green number. It also
short-circuits before the test bodies, so the 50s is genuinely reclaimed
rather than merely hidden.

No test was modified. Deleting the _init() restores the suite exactly as it
was, and the comment says so, along with when to do it (Phase 5 player
rendering at the latest) and the better fix to prefer then — sharing the
compositor instead of rebuilding it per test.

Pair session with Jeroen, 2026-07-27.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 00:57:15 +02:00
jpmschweitzerandClaude a005e48405 feat(config): parse sweep — verify every project script parses, not just the startup path
godot-cold-parse only ever sees scripts on the STARTUP path: autoloads and
the main scene chain. That is the correct scope for the job it was built for
(Sprint 36's `Could not find base class "MetaScreen"`, a registration-ORDER
bug), but it is far narrower than the name suggests, and most of the codebase
is invisible to it. Verified by deliberately breaking a non-startup UI script
and a test file in turn: cold-parse reported "clean", exit 0, for both.

That is the second half of today's false green. A parse error in
test_step_canvas_annotation_layer.gd survived cold-parse AND survived
gdUnit4, which reports the suites that DID load as a clean pass. Two gates,
one blind spot: neither verified that a file it never opened was openable.

godot-parse-sweep opens every .gd in the project (226 today, addons and
.godot excluded) and fails on any that will not parse.

The split between the two halves is forced, not stylistic. No Godot API
reports GDScript parse failure reliably:

  - ResourceLoader.load(path, "GDScript", CACHE_MODE_IGNORE) SEGFAULTS the
    engine on a script that fails to parse — it dies on exactly the input the
    tool exists to find.
  - GDScript.new() + source_code + reload() returns a clean error code but
    detaches the script from its resource_path, so class_name, preload() and
    relative extends stop resolving: it reported 150 of 226 healthy scripts
    as broken.
  - Plain ResourceLoader.load() neither crashes nor false-positives, but
    returns a NON-null object for a broken script, so its return value is
    useless.

The engine's own stderr is the only honest signal. So the GDScript half just
opens files and makes no verdict; the wrapper scrapes the diagnosis. The
wrapper also refuses to pass unless the sweep reported completion, so a
future break in the walk cannot itself become a false green.

Unlike cold-parse, "Cannot infer the type" is NOT filtered. That filter is
precisely why cold-parse stayed silent about the file below.

First run found a real one: client/tests/util/scene_helper.gd has not parsed
since 2026-02-25 — five months — because `func(a := null, ...)` cannot infer
a type from null. Fixed with explicit `: Variant` params. Blast radius is
zero (the helper has no importers, so nothing else was taken out with it),
but it went unseen by two gates for five months, which is the point.

Full suite green at 3660.

Pair session with Jeroen, 2026-07-27.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 00:32:42 +02:00
jpmschweitzerandClaude b2ef73256a fix(config): run-godot reported a suite that never ran as a pass
Found by walking into it. test_step_canvas_annotation_layer.gd had a parse
error from an earlier edit in this session, so gdUnit4 could not load it and
ran the other suites instead. The harness printed 3610 passed / 0 failed and
exit 0. Fifty tests had not run for hours and nothing said so — the full
suite reports 3660 with the file repaired, and that difference was invisible.

Two states are now hard harness failures rather than test results:

  load_error — a suite failed to LOAD. Any pass count excludes it, so a green
  number is a lie. The hint names the offending file.

  no_tests  — zero tests executed. A run that executes nothing can never be
  a pass; previously a mistyped --filter printed "Tests passed".

Both add a "harness_error" field to the summary JSON and exit 2. The exit
code cannot inherit gdUnit4's, which returns 0 in both states — that is
precisely why they were invisible.

Verified by injecting each failure rather than by reasoning about it. The
load_error guard was checked in the case that actually matters: one broken
file among many, where total stays large and failed stays zero. That run now
reports 3610/0 WITH harness_error and exits 2, where before it was
indistinguishable from success.

Also repairs the file itself: a missed set_frame() argument (the parse error),
and a cell-placement test still asserting pre-inversion spacing. Rewritten to
assert the invariant that survives the extent inversion, the viewport aspect
ratio and panning — half the SHORT axis is half a rung cell — instead of a
literal. Two things it deliberately does not assert, both of which the
previous version got wrong: "the corner is half a district away" holds only
on a square canvas, and the canvas is one district WIDE without sitting ON a
district. It is a free-floating window centred wherever the player panned;
zoom is stepped, pan is continuous. A rung names a scale, not a cell you are
inside. A second test pins that with a deliberately unaligned world centre,
so a future change that snaps the canvas to the rung lattice — making pan
step instead of slide — fails here.

Pair session with Jeroen, 2026-07-27.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 00:12:26 +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 9c8fcc2f95 feat(client): size the Global rung to the viewport, not the region grid
Global took its cell counts from `global_cell_counts()` — one gridunit per
region — and discarded the requested extent entirely. On GJ380c that produced
a 191x95 canvas built from a heightmap stored at 512x256: roughly seven times
the available cells thrown away before anything was drawn. The count also
shrank as REGION_M grew, so tuning the scale ladder silently degraded the
opener, which is why the top of the ladder got worse rather than better as
the ladder itself was refined.

Global now fits the largest 2:1 canvas inside the requested extent. It cannot
take its ASPECT from the viewport — the canvas is equirectangular whole-body,
360 degrees of longitude by 180 of latitude, and must stay 2:1 or the cells
stop being square and the map shears — so the existing letterbox absorbs the
remainder. A hostile extent is still clamped; sizing to the request is not
trusting the request.

Global also joins the deep display ratio, making the band uniform. At 5 px
per gridunit a 1920 px window asked for 384 cells across a body whose
heightmap holds 512x256 — discarding stored detail to save work already done.
At 2 px it asks for 960, which is heightmap-native: nothing thrown away,
nothing invented, and the same screen area filled either way.

A body with no radius is not a sphere (asteroid belt, oort cloud) and has no
equirectangular surface to fit. Those degrade to the region grid — a visibly
degenerate 1x1 canvas — rather than a plausible-looking lie at whatever size
the viewport happened to ask for.

project.yaml 0.4.3 -> 0.4.4 invalidates the persisted step-canvas disk cache.
District/Quarter/Block/Chunk kept identical 960x540 cell counts through the
extent inversion, so their cache keys are byte-identical while a District
canvas now covers 3.6 km of ground instead of 1,966 km — a warm cache would
silently serve pre-inversion canvases.

Global still rides the orbital derive, so it carries no courses yet; that is
the next step and is deliberately separate, being a cost question over the
whole body rather than a sizing one.

Pair session with Jeroen, 2026-07-26.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 22:45:40 +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 ae03ea2de7 perf(ui): Atlas deep rungs draw one gridunit per 2x2 px block
DISPLAY_RATIO_DEEP 1.0 -> 2.0, so a viewport-fit request at District/
Quarter/Block/Chunk asks for ~4x fewer gridunits (1920x1080: ~2.07M
cells -> ~518K) and the server-side derive cost falls with it. Texel-
exactness is preserved — the ratio stays a whole number of screen px
per gridunit, so every source texel still lands on whole pixels; only
the block size changes. Non-integer ratios are not an option here:
they reintroduce exactly the sub-pixel blur D-255's texel-exactness
exists to prevent.

Judged live by Jeroen against the 1x1 build (pair session): the deep
rungs read as crisp larger pixels rather than blur, and the speedup is
substantial. Looks were the gate.

Both transport tests that pinned the old literal now assert the
RELATIONSHIP instead — deep rungs share the constant, viewport-fit
divides by it — plus a new guard that the ratio stays whole, so the
value remains tunable without editing tests that are not about it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 17:11:44 +02:00
jpmschweitzer 3821e6718f Merge remote-tracking branch 'origin/main' into header-ghost-fix 2026-07-26 15:21:11 +02:00
jpmschweitzerandClaude Fable 5 c431780164 fix(client): fog perf test measures min-of-7, not median-of-5 (T-1210 done)
Three flakes in one day (0.549/0.503/0.638 vs the 0.5ms budget) proved
median-of-5 (T-1092's mitigation) insufficient when a concurrent cargo
build inflates all samples together. The assertion asks whether the
CODE meets the D-059 budget — load can only inflate wall time, never
deflate it, so the minimum is the least-noise estimator of code
capability, while a real regression shifts the minimum too. Budget
unchanged; regression-catching power preserved. 46/46 verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 15:17:51 +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 41144f317f fix(ui): Atlas header gets its implant panel scrim — legible on any terrain (T-1197)
Root cause was not the suspected fade race — no animation exists
anywhere under client/ui/implant (grep-proven, and settle+2 vs
settle+90 captures were byte-identical). _build_screen_header() was
the single ImplantHeader call site in the codebase that added the
header bare instead of through ImplantPanel.add_component(), so its
fixed light-gray text washed out against pale terrain (Quarter upland
scatter, District olive) while reading fine on dark ocean. Wrapped in
an ImplantPanel like every sibling — the theme's panel_bg scrim makes
it legible everywhere. One committed golden (atlas_GJ820Bc_District)
already carried the ghosting baked in, confirming the bug was static;
all 13 goldens regenerated per the header change, two independent
live-server runs byte-identical (T-1157 stability discipline).
Verified live at the original evidence center: header crisp at
settle+2/+30/+90.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 14:46:55 +02:00
jpmschweitzerandClaude Fable 5 2f87c1a65f data(assets): promote the four spike GLBs — first production environment assets (T-1204)
table_baroque, chair_modernist, desk_scifi (furniture, with mask
sidecars) and lion_statue (props, non-tintable) enter
client/assets/models/ under the category-first convention, byte-
identical to the spike sources (MD5-verified; spikes/ untouched,
vw_beetle excluded per the Q-067 deferral). Postprocess verified
already-applied by Blender node-graph inspection (mat_primary,
roughness=1.0/specular=0.0 shader inputs — the convenience properties
read stale defaults, the graph is authoritative), so not re-run. All
four registered in manifest.json at 'planned' with footprint_tiles
[1,1] (a flagged judgment call — normalized spike geometry erases
real-world scale; the brief ruling goes to araminta in review).
In-engine proof: clean headless import + ResourceLoader load +
instantiate for all four. Discovery documented in conventions.md §2 +
glb-gen SKILL.md: Godot externalizes the GLB's embedded texture as a
<model>_Image_0.png sidecar referenced by binary UID — it must be
committed or loading breaks with an undiscoverable dependency error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:55:28 +02:00
jpmschweitzerandClaude Fable 5 feef7aa1c5 fix(client): PR #212 review fixes — true on-screen footprint_px, self-describing shot coverage
Tyre finding 2: _log_atlas_view_transform's footprint_px now logs the
real on-screen pixel footprint (StepCanvasTransport.canvas_footprint_px
* canvas_scale — the same public pure function the terrain layer uses),
with the old cell-count field kept as canvas_cells. Live-verified the
divergence the fix exposes: Global logs footprint_px=(1528, 760) vs
canvas_cells=191x95 — the T-1192 fit-multiplier class the log exists to
root-cause, previously invisible under the mislabel.

Tyre finding 3: atlas_shots.json's one_shot_per_body note replaced by
rung_coverage stating the true distribution — 13 goldens across 7
bodies (per-rung coverage map, GJ380c's 3 shots as the invariance-proof
body, curated-subset rationale per T-1121). Corrects the prior commit
message's '12 new goldens' miscount: the true count everywhere is 13,
verified against shots array, visual.json entries, and PNGs on disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 22:36:03 +02:00
jpmschweitzer 28a0b3e2af Merge remote-tracking branch 'origin/main' into capture-harness-redesign 2026-07-25 22:13:49 +02:00
jpmschweitzerandClaude Fable 5 a45d4a5f58 feat(client): capture harness redesigned for the stepped Atlas — (body, rung) goldens (T-1157)
_setup_atlas_golden_shot and _run_atlas_matrix rebuilt on the real D-255
surface: nav.push('regional') through AtlasApp's own body-selection tail,
then StepCanvasViewer.jump_to at a fixed center (inventory item 5) — no
shim over the retired continuous-zoom API. is_pending()-aware bounded
settle (mirroring atlas_agent_driver.gd) and view-transform logging
(inventory item 3) wired into both capture paths. The 12 z2_0/z4_0/zfit
goldens are replaced by 12 (body, rung) goldens captured live against
fresh --test-mode servers; atlas_shots.json/visual.json re-keyed;
atlas_gen_open's stale gen_l1_* overlay ids fixed to gen_dw_temp.

Verification: two consecutive District runs byte-identical, and a
cold-vs-warm disk-cache invariance proof on both terrain draw branches
(Global/NEAREST, District/LINEAR) — byte-identical either way, so
capture output does not depend on the shared user://atlas_cache state.
DEVOPS.md's real-rendering exception note now records the fold-target
mapping for the smoke file T-1182 already deleted (Global + District
goldens exercise its two real-pixel draw branches). Old legacy-tracked
.import sidecars go with their PNGs; new goldens ship bare per
.gitignore's client/**/*.import rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 22:13:37 +02:00
jpmschweitzer 3351c595ab Merge remote-tracking branch 'origin/main' into visual-asset-catalog 2026-07-25 20:48:59 +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 2ca70d0228 docs(assets): PR #211 review fixes — ratified glazing tokens, D-257, manifest skip contract
Review round (Hoshe + Tyre, both REQUEST_CHANGES) fully addressed:
palette.md's glass carve-out re-keyed from retired never-shipped tokens
(precision_glass/smart_facade) to the ratified glass_curtain_wall (wall)
+ industrial_glazing (facade); the toon/PBR treatment promoted from a
docs-only ruling to D-257 (architecture, cross-refs D-235/D-244/D-043/
D-044/D-033) with palette.md §2 now citing it as authority; D-149 +
D-257 added to decision_refs; manifest.json _comment gains the explicit
underscore-prefix loader skip contract; master pipeline table Visual row
Stub -> Active; conventions.md mask pointer Section 3 -> 5; pre-existing
broken D-066 links in the mood-board transcript repaired (leave-cleaner).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 20:36:47 +02:00
jpmschweitzerandClaude Fable 5 f77e15d075 docs(assets): 3D-model+texture catalog, glb-gen production path, conventions (T-1050)
Instantiates the five D-244 catalog files (models/textures/artwork/icons/
effects — icons carries the 13 as-built SVGs, stance icons traced to their
consumer and marked final), creates client/assets/models/{furniture,props}
with a schema-documented manifest.json mirroring the character manifest,
fixes glb-gen SKILL.md's stale 'path does not exist' paragraph, and authors
docs/assets/visual/conventions.md: category-first model naming (araminta
ruling), mask sidecars per the character convention, D-235-token-keyed
texture naming, footprint_tiles as manifest metadata. Door-state and
TileSet conventions are recorded as explicit deferrals (Phase-5 owned).
visual/README.md carries both this ticket's count/link updates and
T-1052's palette-section rewrite (shared file, committed here).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 20:20:04 +02:00
jpmschweitzerandClaude Fable 5 6f63cb6f70 fix(ui): agent driver honors SR_PORT — the eyeball's own finding (T-971)
The channel eyeball caught the committed reference driver silently
relying on SimBridge's hardcoded default port, unlike every sibling
real-render driver — a caller starting the server on a chosen port got
20 silent connect retries against the wrong port and a hollow session
whose results had valid shapes but no data. Mirrors visual_capture.gd's
convention exactly: SR_LIVE=1 without SR_PORT is a hard error; SR_PORT
sets SimBridge.server_port before boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 19:30:29 +02:00
jpmschweitzerandClaude Fable 5 52304d3e37 fix(ui): PR #209 review round — current-screen guards, pending-aware settle, one body guard (T-971)
Every screen-targeted intent now routes through one
_require_current_screen() check and returns the structured error shape
instead of silently mutating an off-screen viewer (hoshe's finding:
scroll_rung from the reach screen fired real IPC and reported ok). The
reference driver's fixed 4-frame settle becomes is_pending()-aware with
a 600-frame bound, the keep-waiting decision extracted as a pure
testable function — restoring the proven eyeball-driver discipline. The
terrain_reference guard moves into AtlasApp._on_body_selected(), the
shared tail for double-click, Enter, AND the intent path — closing a
pre-existing click/Enter divergence hoshe caught this PR formalizing;
the intent layer pre-checks via the new SystemScreen.find_body() and
reports structured errors for unknown ids and terrain-less bodies.
after_test() resets AtlasAgentBridge.current_app (tyre's freed-pending
footgun). Suites 58/58 + 14/14; full suite 3,638.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 19:14:02 +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
jpmschweitzer 497f9dc94d Merge remote-tracking branch 'origin/main' into atlas-feature-names
# Conflicts:
#	CHANGELOG.md
2026-07-25 16:44:51 +02:00
jpmschweitzerandClaude Fable 5 8da9670e0f feat(simulation): feature-name pipeline wired + legacy window_granularity u32 retired (T-1169, T-1159)
One commit for two tickets whose changes share the bridge/plugin
plumbing files. T-1169 connects the three dormant feature-name pieces:
atlas_feature_names populated at regen (17,891 rows — 15,190 mountain,
2,701 river — via populate_atlas_feature_names mirroring the city-names
importer; systems.db regenerated, stamp fresh), attach_feature_names
wired into the cascade's Topography block with name pools threaded
DB-free through AnalyzeBody (D-225 pattern) and assignments stored on
Layer1Output/BodyWorldState for future consumers, and a
FeatureNamesRequest/Response read proxy as the bridge's 7th tagged
envelope (D-236 pattern, both SimBridge impls). Client label DRAW is
deliberately NOT here — implementation proved both river and mountain
labels need a wire-carried position (the pool is position-free; course
polylines aren't correlated with the named attractors by construction) —
deferred to T-1195's single design pass. cascade_layer1 golden re-pinned
(additive feature_names field).

T-1159 retires the legacy u32 granularity field fully shadowed by
window_granularity_v2: AtlasLayerRequest.window_granularity,
DistrictWindowLayer.granularity echo, the u32::MAX sentinel, and
resolve_window_granularity are gone server-side; client encode paths and
the caller-less atlas_window_cache legacy key component dropped;
msgpack fixtures regenerated; the T-1150 aliasing regression test now
drives through the surviving enum field. The district_window carrier
itself survives byte-compatible per D-255(c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:10:27 +02:00
jpmschweitzerandClaude Fable 5 686021bee8 fix(ui): PR #207 review round — crop-gated taper, true mitres, hybrid AA (T-1175)
Five findings from the araminta+hoshe round. Tapering now fires only on
a TRUE upstream source: the course's first raw world point is tested
against the canvas's own world bounds (conservative 1m epsilon) —
exact detection because the server crop keeps one point beyond the
window (layer_proxy crop_course_to_window lo = first_in-1, contract
documented), so crop passthroughs draw the old flat full-width cut and
never a false headwater. The averaged-normal joint is replaced by a
real mitre (half_w/cos(theta/2) recovered trig-free via the bisector
normal), clamped by a 2x mitre limit AND 0.45x the shorter adjacent
segment — restoring true perpendicular width at bends (the 29% pinch at
confluences is gone) and preventing the hairpin bowtie; the winding doc
now states the actual bounded guarantee. Antialiasing restored via the
hybrid: only the varying-width taper head draws as a ribbon; the
constant-width ~85% of every course keeps the original antialiased
draw_polyline (byte-identical for untapered courses), split at an
interpolated arc-length point sharing position and width — junction
capture evidence in .cache/screenshots/t1175-fix-round/. Flat-fill
single-element color array. Suite 24 -> 48 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:05:17 +02:00
jpmschweitzerandClaude Fable 5 40cf89c3cb feat(ui): river source tapering + width-grammar retune — map fluency pass (T-1175)
The single large polish pass Jeroen requested off the T-1170 captures,
benchmarked against RimWorld's world-map fluency. Courses now draw as
source-tapered ribbons: per-vertex width ramps from a hairline at the
upstream source to full class width over 15% of the course's arc length
(arc-length parameterized, not vertex-indexed, so point density doesn't
change the read), built as one draw_polygon ribbon with mitred joins.
Class width table retuned 0.9/1.4/2.2 -> 0.6/1.2/2.4: a clean ~2x
per-class ladder so a tributary-joins-trunk confluence reads as a join,
trunk held at its visually-proven weight, stream thinner per the
benchmark's thin/consistent/restrained grammar. Opacity untouched
(Araminta's T-1170 ruling stands). Coast-gradient item resolved as
already-correct (the coastal transition zones + elevation lightness
render the shoreline band; capture-verified) — no wire change. Stipple
assessment filed as T-1194. Eight new taper-geometry tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:25:30 +02:00
jpmschweitzer 324a318b17 Merge remote-tracking branch 'origin/main' into atlas-latitude-fix
# Conflicts:
#	CHANGELOG.md
2026-07-25 13:30:16 +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 dec5b0bc43 feat(simulation): lake_margin_q depth band — lake shorelines gain gradient vocabulary (T-1188)
Lake edges rendered as hard step-edges while ocean coasts got multi-tone
transition bands: every coastal-transition morphology gate keys on
ocean_fraction_q, definitionally 0 inside a lake basin (hypothesis (b)
of the ticket; (a) disproven first — a shoreline-crossing sweep at
2048/512/128m plus a 10m fine sweep all land on the same continuous
crossing, so positional refinement was never broken). New
DistrictProfile.lake_margin_q (0-100 settled-hydrology depth band, from
the same bilinear filled/elevation pair the lake test already samples;
ceiling calibrated just above the observed p90 depth on GJ338Bd's 5,043
flooded cells), threaded through both derive paths onto
EncodedStepCanvas (serde-default for shape tolerance) and down the
client: protocol decode, terrain-layer plane, colorize shades Lake cells
by depth band instead of elev_q (bedrock-under-water, the wrong signal).
project.yaml 0.4.0 -> 0.4.1: the new wire field must invalidate the
client disk cache via its version tag (T-1183's D-192 mechanism).
Acceptance gates green with the new field (lossless round-trip,
cache-hit==cache-miss, every rung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 13:08:44 +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 51fe35d00d fix(ui): harden step-canvas disk cache — PR #204 review round (T-1183)
Atomic index writes (tmp+rename), orphan-payload reconciliation sweep
folded into the background sweep, payload shape guard mirroring the
protocol's own width/height discriminator, size_bytes via get_position()
instead of a full payload re-read, and a 64-bit SHA-256 payload filename
(String.hash()'s 31-bit space made a silent wrong-map filename collision
a ~1-in-16k event per cap-full body; migration self-heals via the orphan
sweep). Tier-3 class doc rewritten to name the real D-253 seam — the
glaciation/flooded_q wire fields exist but carry static values and no
staleness signal crosses the wire; T-1190 tracks threading a wire TTL
into put(sim_ttl_sec) when the driving clock lands — replacing the false
'no sim-state wire field exists' premise. Four new tests plus a
_stub_canvas fixture helper centralizing the canvas shape contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 11:36:21 +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
jpmschweitzerandClaude Fable 5 f9ab3088ab fix(ui): mouth-ring radius floored at 2x stroke — the radius-smaller-than-stroke draw_arc regime
Two separable findings from the live A/B (real driver): (1) the
original zero-ring report was a viewport-framing crop — the ring
fired all along at screen (1755,-191), above the frame under the
COVER fit; recentering via set_view proved the path live. (2) The
real bug once in-frame: at Quarter fit zoom the compensated ring
radius (5.0/7.5=0.667 canvas) fell below the floored stroke (1.0
canvas) and draw_arc's stroke filled its own hole — a solid blob,
not a ring. Bracket: 1x stroke=blob, 1.5x=hollow recovers, 2x=clean.
Fix: zoom_compensated_ring_radius() floors the radius at 2x the
paired stroke, wired through _zs_ring_radius() for both ring and
halo arcs; verified live producing a clean hollow double-ring. The
third member of the Godot sub-canvas-unit rasterizer family (width
floor, stroke-vs-width sites, now radius-vs-stroke) — all recorded
for the T-1176 render-mechanism discussion. 10 pin tests in the
stroke-width suite; revert-verified (floor drop -> named failures);
full client suite 3956/3956; gdlint clean.

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:47:33 +02:00
jpmschweitzerandClaude Fable 5 581be31549 test(ui): spy tests for the B3 arrival-redraw fix (PR #197 Hoshe #3)
Two _CountingNatureOverlay draw-spy tests (the PR #196 pattern):
window arrival and rung-swap arrival both advance the nature
overlay's draw count past baseline. Two isolation bugs caught before
reporting: (1) _on_window_ready has a second pre-existing redraw path
via _fit_and_center -> _apply_transform that fires on first arrivals
and masks a sabotaged line 507 — isolated by setting _user_adjusted
(the real pan/zoom guard, a reachable state); (2) simulating the swap
via _enter_at_rung resets _awaiting_first_window and takes the same
masked branch — the real production trigger is _maybe_reselect_rung's
request path, so the test drives request_now directly. Revert-
verified: dropping the queue_redraw line fails exactly both tests by
name; 82/82 restored. Viewer file itself zero net diff.

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:54:10 +02:00
jpmschweitzerandClaude Fable 5 27fab8566a fix(ui): stroke widths floored against Godot's line-rasterizer hairline collapse (Araminta's pixel finding)
Her audit of the course captures found a uniform 1px alpha-255 line
where trunk should draw 2.2 screen px. Live A/B bracket (20/1.5/1.1/
0.6 forced widths + draw_line-vs-draw_polyline control) localized the
cause: Godot's line rasterizer floors stroke widths below ~1.0 canvas
units to hairline — the _zs arithmetic was correct (1.4/3.75=0.373
round-trips exactly); the value died at the driver. Fix: zoom_
compensated_stroke_width() (the _zs divide maxf'd at 1.0) via a
_zs_stroke() wrapper on EVERY stroke-width site (course polyline,
skeleton chords, mouth-ring arcs, basin boundary, attractor outlines
— same latent class everywhere even where not yet visible); radius
args proven unaffected and left on _zs. Honest degradation direction
documented: at high zoom effective width grows rather than pinning.
Class question resolved with printed ground truth: the original
window is genuinely single-drawable-class (trunk course degenerate at
1 point); a confluence window confirms real multi-class rendering.
The drive now prints per-course class/width/effective-px tables every
run. New 14-test stroke-width suite (own file, line-cap split) pins
the exact floor-engagement numbers and the PR#195-shape effective-
width >= table-value invariant; revert-verified (floor drop -> 4
named failures). Full client suite 3942/3942; gdlint clean.

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:06:56 +02:00
jpmschweitzerandClaude Fable 5 c31cc6220e feat(ui): T-1170 B3 — course polyline drawing at District/Quarter; clip restructured per Ruling 3g
_draw() splits into two independent gates: the Layer-1-gated skeleton
path (Region chords, clip retained) and the NEW DistrictWindowLayer-
gated course path — build_course_render_plan() (pure, render-free-
testable) consumed by draw_polyline with _zs-compensated widths and
opacities from the Araminta revisit tables; mouth double-rings at
Mouth termini only (EdgeDrain/ContinuesBeyondWindow/None: three
meanings, one presentation — draw to last point, stop, documented);
zero water clip on the course path by construction (courses carry
rung-consistent termini). CourseTerminus wire vocabulary kept re-
pointable pending A2's real serde names; synthetic Ruling-3h fixtures
mean the suites need zero changes when the server payload lands. Real
gap found and fixed: window arrival never redrew the nature overlay
after the first fit (one line in _on_window_ready — courses would
miss every window swap post-pan). Water-clip header rewritten to
RESTRUCTURED status (retired on course rungs; permanent at Region
until Region goes windowed, T-1143 ruling 2). Revert-verified
(visibility-gate bypass -> 4 named failures). geometry-nature
112/112, nature-overlay 58/58, viewer 78/78, zero collateral; full
sweep 3928/3928 after the full-import bootstrap; gdlint clean (viewer
1016->1017, pre-existing overage rides T-1158).

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:05:40 +02:00
jpmschweitzerandClaude Fable 5 9d7c01de02 feat(ui): T-1170 B1+B2 — visibility-table split; Region skeleton chords from downstream pointers
B1 (Ruling 5c): nature block split out of atlas_window_geometry.gd
(954/1000 cap pressure) into atlas_window_geometry_nature.gd; RIVER_
CLASS_VISIBLE_BY_RUNG replaced by SKELETON_CLASS_VISIBLE_BY_RUNG
(Region-only now) + COURSE_CLASS_VISIBLE_BY_RUNG (District trunk+
tributary; Quarter all three — the pre-announced Quarter-rivers-
return) with width/opacity companion tables as Araminta's single
revisit point; deliberately opposite unknown-tag fallbacks per reader
(skeleton->full, course->empty), documented.

B2 (Ruling 5a): Region dot-scatter upgraded to connected chords via
river_downstream — D8 direction decode (0-7 into drainage.rs's
(row,col) delta table, antimeridian wrap-aware), sentinel chain ends
(MOUTH=8 ring-on-land, EDGE_DRAIN=9 no ring, TERMINAL=10 reserved,
decodes like EDGE_DRAIN so the future endorheic server needs no
client change). Pure build_skeleton_chords() split from drawing for
render-free testability. Chord clip rule (3g pick): segment clips if
either endpoint OR midpoint is drawn water — three-point catches both
narrow-inlet and long-chord failure modes at one extra lookup;
documented. Self-caught during build: first draft misdecoded the
pointer as a river_cells INDEX; rewired to direction decode against
A1's real convention before leaving the branch. Dual revert-verified
(direction sabotage -> 6 named failures incl. the chain-threading
pin; midpoint-drop -> exactly the 1 named clip test). Suites:
geometry-nature 86/86, geometry 130/130, nature-overlay 24/24; full
sweep 3892 with only the 6 known pre-existing garment/gait failures
untouched by this batch; gdlint clean.

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:54:25 +02:00
jpmschweitzerandClaude Fable 5 0b14cdb21d fix(ui): T-1172 round 2 — tile wrap resolution mirrored to the painter's own direction
Live round: the first clip removed only 133 pixels. Root cause (dossier-
traced): resolve_morphology_zone wrapped the QUERY toward a tile's raw
canonical center — the inverse of the painter's draw_col =
nearest_wrap_image(tile_center, held_center) — so seam tiles tested
containment against the wrong wrap-image and read real-but-wrong-
location land cells. Fix mirrors the painter exactly: wrap the tile's
own center toward held_center, test the (already held-wrapped) query
against that. Index math itself was confirmed correct end-to-end and
is now factored into shared AtlasWindowGeometry.cell_index_for_local_
offset() (full painter unification not applicable — the painter only
iterates forward, never reverse-looks-up; documented). Post-fix: 1082
traced positions cross-checked against actual painted pixels, 0 real
mismatches; regenerated captures show every dot/mouth on land. The one
pre-existing test encoding the buggy direction as correct was replaced
by positive+negative wrap-semantics tests (the negative one is the
reliable revert discriminator) plus a wire-accurate seam-tile fixture
with an honestly-documented proof limit (a single-tile fixture cannot
distinguish the wrap directions; the multi-tile scan tests can).
Also: two harness traps found and fixed in the lead's drive scratch —
canvas_items stretch factor (root.size now matches the 1920x1080 base
viewport) and mid-arrival snapshots (explicit is_fully_arrived wait;
the round-1 faint result was partly a 4-of-6-tiles capture). 46/46 +
50/50 + five sibling suites regression-free; gdlint clean.

Tickets: T-1172

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 11:33:31 +02:00
jpmschweitzerandClaude Fable 5 9914dc0d91 fix(ui): T-1172 — clip river dots/confluences/mouths against the drawn waterline
Jeroen's hands-on report: rivers continuing under the ocean. Tyre's
ruling implemented: the skeleton is rung-independent, the drawn coast
is rung-indexed (warp cutoff admits more octaves per rung), so
reconciliation is a presentation-frame operation — a draw-time clip
against the SAME per-cell morphology verdict the terrain painter used,
at the rung on screen. New pure module atlas_window_water_clip.gd:
cell resolution across both paths (single-window direct; tile mode
selects the containing tile by each tile's OWN echoed n with nearest-
wrap re-expression against that tile's canonical center — response-is-
source-of-truth + wrap discipline reused, not reinvented). Strict drop
(no snap); offshore mouths suppressed (return with real termini in
T-1170 — retirement markers at the clip sites); basins untouched;
fail-open wherever no composite data has arrived (the clip refines
presentation, never gates data). The _pos split feeds the SAME wrap-
resolved district to both draw position and clip test so they can
never disagree about the wrap image. 49 new tests incl. antimeridian
and mid-progressive-arrival fail-open; revert-verified with precise
attribution (breaking water detection fails exactly the 3 water tests,
fail-open/land tests stay green); smoke-suite stub crash under a real
driver caught and fixed (headless skip-gating masked it). 7 suites
regression-free; gdlint clean.

Tickets: T-1172

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:37:56 +02:00
jpmschweitzerandClaude Fable 5 c1cae7c9b5 test(ui): named default-visibility pins for nature overlays (PR #195 Hoshe finding)
RVR-on-by-default is the single most player-visible behavior wave 1
ships (rivers appear with no toggle hunt) and was only exercised by
live captures; gen_basins' default was pinned incidentally inside the
toggle-redraw spy test and gen_attractors not at all. One named test
now asserts all three fresh-construction defaults per Araminta's
ruling (RVR on, BAS off, ATR off). Revert-verified: flipping the
gen_rivers init line fails exactly this test by name (1 failure total
in the 78-test suite). Suite 78/78; gdlint clean.

Tickets: T-1156

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 09:26:55 +02:00
jpmschweitzerandClaude Fable 5 8a747e332c fix(ui): PR #195 review round — attractor stroke widths zoom-compensated (Tyre I1/I2/I3)
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>
2026-07-23 09:14:47 +02:00
jpmschweitzerandClaude Fable 5 0366e8286f fix(ui): District trunk dot 2.0px/80% — Araminta's PR #195 capture objection
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>
2026-07-23 09:06:38 +02:00
jpmschweitzerandClaude Fable 5 60faf667a5 fix(ui): T-1156 live rounds — zoom-compensated marker sizes; toggle-redraw regression pin
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>
2026-07-23 08:55:29 +02:00
jpmschweitzerandClaude Fable 5 0d22e50e66 feat(ui): T-1156 — rivers/basins/attractors re-hosted onto the zoom ladder (nature overlay node)
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>
2026-07-23 08:23:59 +02:00
jpmschweitzerandClaude Fable 5 c9028af77e feat(ui): T-1161 — per-rung composite filter policy (Region NEAREST, District/Quarter LINEAR)
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>
2026-07-22 22:36:05 +02:00
jpmschweitzerandClaude Fable 5 8bb09abad0 test(ui): wrong-body Pending must not touch retry state (PR #193 Hoshe finding 1)
Pins on_response()'s guard ordering: body_id check BEFORE the status
branch, so another body's cold Pending can never burn one of our 30
retries or reschedule our timer (multi-body browsing / shared-broadcast
tile fan-out). Revert-verified: reordering the guards makes this test
fail; final own-body Ready assertion proves the request is genuinely
untouched, not just un-retried. 44/44 in the file.

Tickets: T-1163

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:15:47 +02:00
jpmschweitzerandClaude Fable 5 4c34327b41 docs(ui): PR #193 review capture — D-226 pending-shape protocol note + stagger comment dedupe
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>
2026-07-22 21:11:22 +02:00