main
307
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fab70edd5a |
feat(ui): the stipple says WHERE the ground is broken, not just that it is (T-1194)
RimWorld's technique 4, the reference this ticket names, works by CONTRAST: the
Rockies and Appalachians carry dense hatching and the Great Plains carry none.
Ferrath's Global carried an even wash of dots over every landmass instead —
texture present, information absent.
The cause was a constant measured on the wrong rung. RUGGEDNESS_FULL_SCALE_Q = 4
comes from Region ("d4 2.38"), but the elev_q path it gates only ever RUNS at the
orbital rungs, where relief_q is flat and the stipple falls back to it. Ferrath
Global measures a mean 1-cell elev_q gradient of 0.99, so a coherent 4-cell
baseline reaches ~4 and saturates the constant exactly: every land cell read as
fully rugged.
It now normalizes against the canvas's own measured gradient — the same
self-calibrating shape T-1240 gave the hillshade, and for the same reason: one
constant cannot serve rungs whose sample spacing differs by four orders of
magnitude. A contrast curve rides on top, because even unsaturated the linear
reading puts ordinary ground mid-range and paints grain everywhere.
Measured on Global, before -> after: 1,679 -> 1,900 distinct colours, 145.90 ->
147.39 lum spread, and the pale uplands now stipple visibly denser than the
lowlands beside them.
Recorded because it cost two wrong turns: I first guessed saturation, then
talked myself out of it after measuring a 1-CELL gradient (0.99) against a full
scale meant for the 4-CELL baseline, and shipped a contrast curve alone — which
measurably did nothing (1,679 -> 1,698), because a curve cannot separate values
already clamped to 1.0. The gamma is kept; it does its job now that there is a
range to curve. The elev_q gradient joins relief_q's in the capture readout, so
the next person tuning this can read the number instead of guessing at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9b146f9e1f |
fix(simulation): derive at the octaves a rung can actually reconstruct (T-1240)
Region rendered as fine uniform stucco while District and Quarter, on identical code, read as terrain. The cause was sampling: `min_wl_m` arrives as an LOD request and defaults to 0, so every invented octave contributed at every rung. MIN_WL_BANDS_M was meant to be the floor but is built from the rung's CELL SIZE (2 x DISTRICT_M), which stopped being the sample spacing at the D-255 extent inversion — a rung fixes EXTENT now and spacing falls out of the canvas size. The bands were off by roughly the cell count, and the served path never consulted them anyway. The cutoff is now derived from the resolved spacing, which is what this ticket asked for. Two things had to be measured rather than reasoned to get it right, and both corrected me. FIRST: the field was the culprit, not the renderer. I attributed the stucco to the client stipple painting noise onto a smooth field. Surfacing the terrain layer's own mean |relief_q gradient| in the capture readout settled it in one shot: Region 18.24 steps per cell — 144 m of relief between NEIGHBOURING cells — against District's 0.30 and Quarter's 0.07. The server was sending noise. That diagnostic ships here for the same reason `plane_variety` did in T-1213: a noisy field and a renderer inventing noise look identical, and one number separates them. SECOND: Nyquist is the wrong threshold. The first version floored at 2 x spacing, the aliasing limit, and Region barely moved (56.16 -> 59.73 lum spread, gradient still 18.24) because 2 samples per cycle is unaliased but renders jagged. The rungs that already worked say what the real bar is: District reconstructs its finest surviving octave at 34 samples per cycle, Quarter at 135. At 8x, Region goes to 1.08 gradient and 70.01 spread, and shows ridges and valleys. THE TRADE, taken deliberately and recorded in the tests: an 8x floor also truncates the coast warp's 2,048 and 1,024 m octaves at Region, the band T-1160 added for "one coastline at every rung". An earlier test here asserted that band must survive; it now asserts the opposite. Same reasoning as the relief: a 1,024 m coastline wiggle at 379.3 m per cell is 2.7 samples per cycle, so drawing it draws noise rather than coastline character — a rung cannot show shape finer than its own cell. The warp is amplitude-capped sub-pixel on the working grid, so what is lost is small. If a future pass wants the warp exempt, the fix is a relief-only floor threaded through derive_at_metres, NOT a lower multiple, which takes the stucco back. Global is exempt: its floor would be ~70 km and would truncate the whole warp band, and it needs none — the orbital derive leaves relief_q flat at 50. District (3.79 m spacing) and Quarter (0.948 m) floor below every octave in play and derive byte-identically, which their own test pins. Cache-safe by construction: the floor is a pure function of (rung, extent, body_radius), all three already in the step-canvas cache key. 0.4.12 is required anyway — this changes derived BYTES at Region, so a 0.4.11 entry holds a field this build would never produce. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8787ee1844 |
feat(ui): hillshade the deep rungs — form from light across slope (T-1213)
relief_q now reaches the renderer, and the first pass spent it on brightness:
lighten where the ground is high, darken where it is low. That moved the numbers
(District 13.72 -> 77.01 lum spread) and still looked like moss, because the eye
does not read landform from absolute brightness. It reads it from light falling
ACROSS a gradient — height-shading gives a rise and a fall the same tone, so no
ridge ever reads as a ridge.
So relief drives a proper hillshade: the local gradient of the field dotted with
a light from the upper-left. The light direction is not a free choice; lit from
the lower-right the brain inverts the read and valleys pop out as ridges.
THE SCALE IS MEASURED PER CANVAS, not fixed, and the first attempt at this failed
exactly the way this file already warned a fixed gradient constant would (see
RUGGEDNESS_BASELINE_CELLS: "the same 4-cell delta reads 21.86 at Region and 0.08
at District"). With a constant full-scale of 8:
Region 81.72 but District 77.01 -> 20.01, Quarter 42.56 -> 16.44
because at District's 3.8 m per cell neighbouring cells barely differ. The
terrain layer now measures each canvas's own mean |gradient| once per rebuild and
the hillshade normalizes against it, so one constant works at every rung.
Ladder (tooling/atlas-flatness, lum p1-p99), flat -> shipped:
Global 145.69 -> 145.69 unchanged; relief_q is flat 50 at orbital
Region 33.59 -> 54.30
District 13.72 -> 74.43
Quarter 11.01 -> 73.72
District and Quarter now read as terrain — ridgelines, valleys, and the stipple
organised into contour-like bands. Judged by eye on the captures, not by the
metric alone.
Stipple full-scale 25 -> 60. The old value was calibrated against a relief_q that
never arrived, so it was tuned to the elev_q fallback; with the real plane nearly
every land cell earned a mark and Region read as static (17,599 distinct colours,
more than twice Global's, for a quarter of the legibility). Form comes from the
hillshade now; the stipple is grain on top of it.
REGION IS NOT FIXED, and the cause is T-1240 rather than this change. It renders
as fine uniform stucco: a Region cell is 379 m of ground while the relief field's
content sits in the 128-1024 m band, so the field is at or below Nyquist and the
gradient the hillshade reads is aliasing, not slope. min_wl_m defaults to 0 on
the served path, so nothing truncates the octaves Region cannot resolve — which
is precisely what T-1240 proposes to fix. That ticket said the stale cutoff was
"currently inert"; it is now the thing capping Region, and T-1240 is updated
with the measurement.
Three tests, on direction rather than magnitude so tuning does not rewrite them:
a hill's west flank lit and east flank shadowed, a uniform field shading nothing,
and the canvas edge not drawing a rim. That last one is a bug this nearly
shipped: `_l8_value` returns 0 out of bounds and 0 on relief_q means MAXIMUM
HOLLOW, so sampling off-canvas posts a full-scale false gradient all the way
round the frame. The sample position is clamped instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3ec35b87c8 |
fix(client): the deep rungs were flat because relief_q fell off the wire (T-1213)
`relief_q` is the one field with signal below District — elev_q's 80 m steps
quantise sub-district detail away, which is precisely why relief_q was invented.
The server has encoded it since
|
||
|
|
086d9ed56e |
fix(client): bake the version into the build, so an export can invalidate its cache (T-1241)
current_schema_version() line-scanned res://../project.yaml at runtime. That resolves to the repo root in a dev run and to nothing in an exported build, so a shipped game got the "?.?.?" fallback every time. Since that tag is the Atlas disk cache's ONLY invalidation signal, every exported build stamped and compared the same sentinel: a canvas cached by one build would be served by every later build, forever. T-1239 is what that failure looks like once it happens. loading_screen.gd carried a byte-for-byte copy of the same function, so the version shown to the player was "?.?.?" in exactly the builds where a version string is worth showing. Both call sites now share client/scripts/build_version.gd, which reads application/config/version out of ProjectSettings — a value Godot bakes into the PCK, identical in the editor and in an export by construction rather than by luck. No file IO, no fallback branch. project.yaml stays the source of truth (CLAUDE.md); client/project.godot mirrors it. A mirror nobody checks would be worse than the bug it replaces -- the old code failed loudly everywhere, a stale mirror fails silently -- so tooling/check-client-version compares the two and the pre-push hook runs it unconditionally. Not gated on "were those files in this push": drift persists on main once introduced, and gating would let an existing drift ride along. The test this replaces asserted that current_schema_version() did not return its fallback, and passed -- in the one environment where the code under test worked. Three tests now pin the property that actually matters: a real version, sourced from the baked setting, matching project.yaml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
07ed2a47ab |
fix(client): the Atlas was replaying a cache from a build that no longer existed (T-1239)
Ferrath's Global map drew no rivers at native resolution: 375 courses arrived and 0 were drawn. The report suspected the D-261 length cull or the water truncation. Both were innocent, and so was the renderer. The client served the canvas from its own disk cache (T-1183). Every payload for GJ820Bc predated T-1237 ( |
||
|
|
43a267439b |
feat(client): ScatterField — reusable seeded scatter for client-side paint
Extracted from the T-1194 stipple, which was the first of a family: graffiti placement, cracks in textures, drifting cloud cover — presentation decisions that must look the same when the player returns to a place, and which the simulation has no opinion about and should not be burdened with. THE LINE IT DRAWS. It answers "how is this drawn", never "what is here". A cell's biome, a settlement's position, whether a wall exists — those are world data, derived once by the server and sampled everywhere (D-255(f) mechanism B), and the player eventually stands on them; inventing those here would put the map and the ground in disagreement. Stated on the class so the next consumer does not have to re-derive it: if the answer changes what is THERE it is not a ScatterField question; if it only changes how it is DRAWN, it is. Bit-identity with the server's Rust noise is explicitly NOT a requirement (Jeroen: "a seed is a seed and the functional intended outcome is repetition here"). Nothing here is compared against a server value or round-tripped through a save, so the contract is stability across sessions, not agreement across languages — which is precisely why paint belongs on this side: it buys visual density with no cross-language determinism burden. Seeded from GameState.world_seed, so two playthroughs scatter differently and one playthrough is stable forever. API: domain() resolves a name to a salt ONCE (the first consumer runs ~700,000 times per canvas rebuild, so the hot calls take an int, never a string); value/chance/pick/jitter for discrete marks; smooth() for continuous fields like cloud cover; an optional time axis for animation. Domains keep consumers uncorrelated — without them graffiti and cracks at the same wall coordinate would mark identical spots and read as one artefact. The tests pin the CONTRACT, not the numbers — freezing outputs would make any future improvement to the mixer a breaking change for no gain. They caught a real defect immediately: (-x, -y) collided with (x, y), because negated coordinates produce negated products and the sign-bit mask folded the pair together, mirroring every mark west and south of the origin onto its north-east counterpart. Not an edge case — the descent ladder's own anchor sits at y = -5,675,959. Fixed by zigzag-encoding coordinates before mixing. 1853 client tests, 0 failed (15 new). Global capture re-verified unchanged after migrating the stipple onto the service. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
566b566519 |
feat(ui): relief and vegetation texture over the terrain hue (T-1194)
RimWorld technique 4 from the reference map: texture as data, not decoration.
WHY IT WAS FLAT. The base layer reads hue from morphology and lightness from
elev_q. Below Global, morphology resolves to exactly ONE zone per canvas, so
the frame became a single colour whose only variation was a lightness ramp too
subtle to see. Measured on Ferrath at Region: 1 morphology zone, but 49
distinct elev_q values. The information was already on the wire and arriving —
the renderer was discarding it by expressing it in lightness alone.
TWO MARKS, NOT ONE SLIDER — the ticket's design question (b), settled by
looking at the reference rather than reasoning about it:
- RELIEF stipple: fine, dense, darker, keyed to RUGGEDNESS not height. The
reference's high flat plains carry none while its ranges are dense with it,
so the driver is the local elev_q gradient; a high plateau stays clean.
- VEGETATION blotch: coarser, softer, marked on a half-frequency lattice so
it reads as patches rather than a second speckle at the same pitch.
Inline in the existing per-cell loop (question (a)) and always-on, base layer
only (question (c)). The TMP/MST/VEG toggles are ANALYTIC reads — stippling a
temperature ramp would corrupt the quantity being read.
THE BASELINE IS MEASURED, NOT GUESSED, and the first attempt got it wrong: a
1-cell ruggedness delta samples mostly quantization noise, reads
near-identically everywhere, and rendered as uniform static over flat green —
grain, not structure. The gradient saturates by about 4 cells (Region: d1 1.40,
d4 2.38, d8 2.41, d16 2.51), so the baseline is 4 and the full scale 4.
Verified by capture at native resolution: on Global the stipple now
concentrates on rugged ground and leaves plains clean.
WATER TAKES NEITHER MARK, and gets a flat tone. An earlier version excluded
Lake alone and stippled the entire ocean — the one surface with no relief to
express. Both open-water zones are excluded now.
The ocean also stops shading by elev_q, which is the same argument T-1188
already made for lakes and never applied here: elev_q on a water cell is the
bedrock UNDER the water, not the surface, so shading the sea by it paints
seabed relief nobody can see. Near a coast that bedrock rises steeply and
quantizes hard, which is exactly where it showed — a pale, pixellated, broken
fringe hugging every shore (Jeroen, on the capture). One tone for the sea reads
as water and lets the coastline be the edge.
D-255(e)-legal throughout: texture-space dithering of already-derived per-cell
values, decided per server cell by a hash of its own coordinates and values —
no sample invented between cells, identical on cache hit and miss.
Two colorize tests updated: both asserted the old ocean shading incidentally
while testing zero-fill/no-crash. Property under test unchanged.
1838 client tests, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8eb5cb9ff7 |
feat(ui): the Atlas ladder bottoms out at Quarter (D-255, T-1213)
Quarter becomes the deepest navigable rung. Block and Chunk leave the ladder. The rule, from the amendment: the deepest Atlas rung is the one at which a screen pixel shows one subtile. At the uniform 2x2 px display ratio a 3440x1440 window gives 540 gridunits on the short axis, so Quarter's 512 m extent draws 0.948 m per gridunit -- about one voxel per gridunit and one 0.5 m subtile per pixel. Block (0.237) and Chunk (0.119) magnify beneath the finest datum that can exist, and measured as exactly that on Ferrath: one morphology zone, one vegetation class, an unbroken colour field. They were not missing a feature; there was nothing left to show them. CHUNK ITSELF IS UNTOUCHED. It remains D-243's 64 m stream/derive unit and is where Phase 5 derives first-person walkable content -- D-012's load-around-the-player is expressed in chunks. Block remains the 128 m generator planning unit. Both keep their enum variants, their extent_m answers and their wire vocabulary. What was retired is the claim that a MAP of one is worth looking at. DEEP_RUNGS moved with the floor, and this is the part worth reading twice. It is a mandatory D-255(d) hardening: a per-body retention cap bounding how much ground an exhaustive pan can hold resident at fine spacing. Left as [Block, Chunk] it would have guarded rungs no client can request -- dead code -- while the accumulation gap silently re-opened under Quarter, now the finest navigable rung at ~0.95 m per gridunit. It is now [District, Quarter]. A control that names its targets by rung has to follow the ladder when the ladder moves. Six tests pinned the old floor and were updated rather than deleted, since each was protecting a real property: the clamp tests now clamp at Quarter, and the disk-cache tests use Region for "shallow" (District is capped now) and Quarter for "deep". One new test pins the distinction the change turns on -- the retired rungs are absent from RUNG_LADDER but still present in RUNG_EXTENT_M, because viewability was retired, not vocabulary. Also removes the four capture scenarios for the retired rungs, including the two blank goldens that had been passing against blank captures. 1838 client tests, 0 failed. Server clippy clean, step_canvas suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
49812e3127 |
test(client): the chaining test outlived the chaining (T-1237)
|
||
|
|
c91096a3c9 |
test(client): retire the top-down visual suite, aim the Atlas ladder at land
The visual gate had stopped measuring anything: 30 of 32 scenarios
failed, and the two that passed were the worst result of the lot.
Deleted the 15 top-down scenarios (fog, HUD, dialogue, NPC, minimap,
cursor) and their goldens. They all failed at a near-uniform ~12%, and
that uniformity across unrelated scenes is one global cause -- the
ultrawide UI stretch moved every element. They cover the renderer the
cascade freezes until Phase 5, which will need its own tests anyway, so
re-baselining would only have blessed a deprecated layer nobody is
reviewing. Jeroen's call.
The remaining problem was the goldens that PASSED. atlas_GJ338Bd_Block
and atlas_GJ445c-m1_Chunk matched at 0.0% because capture and golden
were both blank -- the same "goldens have been measuring nothing" trap
|
||
|
|
474ab90663 |
feat(ui): rivers as cartographic strokes (D-261, T-1237)
Rivers now appear on the whole-body map for the first time. Five on Ferrath's
Global canvas, drawn as 5 px strokes that stop at the coastline.
Four rules, all client-side over existing server data, computed once on canvas
adoption rather than per draw:
- fixed 5 px screen-space stroke at every rung
- contiguous geometry through the river's own cells
- never drawn over water — ocean and lake end a run
- culled below 15 px of on-screen length (3x the stroke: below that a line
is a square, not a river)
TWO THINGS THE MEASUREMENT FOUND THAT THE RECORD DID NOT ANTICIPATE.
First, the cull unit was wrong. A server "course" is an EDGE of the river
network — the stretch between two confluences — not a river. Culling per
course culls per segment, so a long river assembled from many short edges
vanishes entirely. Measured on Ferrath Global: 375 courses, 180 surviving the
water clip, and ZERO surviving a per-course cull. Edges are now chained
end-to-end into rivers before the cull is applied, which also delivers the
other half of D-261's "contiguous": per-course contiguity only makes each edge
unbroken; joining is what makes a river read as one line rather than dashes.
After chaining, 5 rivers survive at Global — the "major systems only from
orbit" behaviour the record predicted, arrived at by a different route.
Second, and worse: uses_orbital_derive() still read `Global | Region` while
the client's mirror had said Global-only since 2026-07-26. The D-255 amendment
claims "Region left the orbital derive set... it now takes the full
courses-aware derive". That was implemented against the MIRROR and never
against the authority, so Region kept running envelope-only and carrying no
courses — the exact thing the amendment said it had stopped doing. Both test
suites stayed green for two days because neither compares itself to the other.
Fixed here, with a note on each side pointing at the other, since the two
cannot be cross-checked automatically.
Also removes the two gates that withheld courses from the orbital rung — the
reason the whole-body map had no rivers at all. Whether a course is worth
drawing is measured in screen pixels, which only the client knows, so the
server now supplies geometry at every rung and the client decides.
The capture harness reports "drawn" alongside "courses", because "375 courses
arrived" and "375 rivers are drawn" are different claims and conflating them
is what made an empty map look like a data problem.
Client suite 1836 / 1810 passed / 26 skipped. Server suite green.
Co-Authored-By: Claude <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
3821e6718f | Merge remote-tracking branch 'origin/main' into header-ghost-fix | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
28a0b3e2af | Merge remote-tracking branch 'origin/main' into capture-harness-redesign | ||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
497f9dc94d |
Merge remote-tracking branch 'origin/main' into atlas-feature-names
# Conflicts: # CHANGELOG.md |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |