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>
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>
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>
Caught by the push gate, whose new named-failure block reported it as
'- cargo fmt' with the fix command — the information the old anonymous
'1 check(s) failed' withheld, on its first real outing.
No behaviour change.
Co-Authored-By: Claude <noreply@anthropic.com>
Written one day after the record, on evidence, and it narrows D-258's scope.
The Rationale claimed hydrology 'was not derivable at all under the old
model'. Not true, and not true when written: layer1.rs already solves drainage
AND settled-equilibrium hydrology once per body, folds the filled surface into
TerrainAnalysis, and every rung bilinearly samples it. The code's own comment
names it — 'a coarse continuous primitive computed once, sampled fresh at
every rung, never re-solved', mechanism B, D-255(f). The pattern rung 0.5 was
invented to provide already existed. I inferred the claim from Region having
no rivers without reading the layer-1 pipeline.
What actually made Global flat was a stale sentinel zeroing its extent, giving
a 2x1 canvas. Once sized correctly Global reads as a world with no hydrology
work at all. Rivers there measured negligible: 375 courses present, 458 of
518,400 pixels changed versus courses-off, because at ~39.7 km/gridunit most
courses are shorter than one gridunit.
Survives: reliefmap-as-plurality, composition-on-descent, the conservation
invariant, and the lake-shore amendment — none depend on hydrology moving.
Weakened: the stored expanded layer and its D-227 carve-out, since the
compute-once-sample-everywhere mechanism it argued for is already shipped.
T-1211 and T-1212 flagged needs-refinement with the reasoning attached; the
measurement as scoped would have priced work that is not required.
The amendment also records why this survived review: the sizing fix's tests
called resolve_canvas_extent directly rather than the serve path, and the
capture goldens could not have caught it either since they never supplied a
body radius. Two verification layers, both green, neither looking at the thing.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Jeroen's Global map has been two coloured blocks all along. Not missing
hydrology, not a missing layer: serve_step_canvas_request zeroed Global's wire
extent, so every request arrived downstream as (0,0), clamped to (1,1), and
resolved to a 2x1 canvas.
That sentinel was correct when Global's size came from the body's region grid
and the client's extent field was meaningless. The D-255 extent inversion made
Global viewport-sized and this line silently outlived it. The commit titled
"size the Global rung to the viewport" was therefore correct and completely
unreachable — its tests passed by calling resolve_canvas_extent directly
rather than through the serve path, i.e. they tested the function that changed
instead of the path the data takes.
Two more places carried the same dead premise, both meaning the first canvas
ever built answered every later request and a resize could never take effect:
- GlobalTierCache keyed on body id alone. Now treats a size mismatch as a
miss, so the re-derive replaces it. Deliberately still ONE entry per body
rather than one per size: keying by size would make a tier that never
evicts accumulate an entry per viewport a player has ever used.
- The client's make_key collapsed Global's extent to a sentinel. Centre
stays collapsed — Global's canvas really is whole-body and origin-anchored
— but extent is now part of the key.
project.yaml 0.4.5 forces the 2x1 canvases already on disk to miss.
Verified through the capture harness, not by reasoning: server probe shows
req=(960,540) radius=6238.4 resolved=960x480, and Ferrath's Global now renders
continents, oceans, inland lakes and polar ice where it previously rendered
one solid rectangle.
Server suite green (45 binaries), client 1830 total / 1804 passed / 26 skipped.
Pair session with Jeroen, 2026-07-27.
Co-Authored-By: Claude <noreply@anthropic.com>
Everything else from the pair session was either shipped or ticketed with its
rationale in git. These two were design decisions Jeroen made, living only in
a scratchpad under /tmp, one cleanup away from gone.
D-259 — ENCLOSED SETTLEMENTS. Open-air requires atmosphere == standard;
everything else is enclosed, as point locations on the Atlas with interiors
deferred to a separate generator and handled like embedded stations. Branch on
an explicit `enclosed` property rather than the atmosphere string, so enclosed
settlements on breathable worlds (hostile-biome posts, undersea, arcologies)
need no second code path. NULL defaults to enclosed, which fails safe.
The load-bearing claim is that D-220's density model is PHYSICALLY
INAPPLICABLE to a pressure vessel, not mistuned: it measures people per km2 of
footprint driven by utility cost per metre of street, so run airless it yields
1,500 ppl/km2 of open-air streets in vacuum. The handoff mechanism was already
planned — D-166's door boundary contract, where a dome's airlock IS that door.
Atmosphere keeps its full range for the surface-expedition layer; the binary
collapse applies to settlement layout only, and the record says so to stop the
collapse being read as atmosphere not mattering.
D-260 — GENERATOR SCOPE. Sol is encoded as Authored { deepest_rung: Global }
rather than excluded by convention. The DLC gate is a DEPTH, not a boolean, so
a Sol expansion changes one value instead of unpicking an exclusion. No new
wire status: "viewable at Global, no deeper" IS the existing rung-liveness
path. Sol art is an authored data canvas, not a finished image, so it rides
the existing wire and is drawn by the same map-art function as all 269
procedural bodies.
The hazard that made encoding necessary is recorded: systems.db still carries
terrain_reference rows for Sol bodies pointing at heightmaps deliberately never
baked, so a real Earth DEM dropped at that exact path would silently start
generating a procedural homeworld. The importer comment said Sol was excluded;
nothing enforced it.
Also files the last three parked items, which were likewise /tmp-only:
T-1233 tiled/interlaced map loading (workshop-sized D-255 revision, blocked in
spirit on the rung-0.5 cost numbers), T-1234 enumerate the flow tweaks the
fetch panel exposed but which were never written down, T-1235 zoom
discoverability — the person who specified wheel-only zoom could not find it.
T-1231 closed.
Co-Authored-By: Claude <noreply@anthropic.com>
Observed twice today with different outcomes. The crash is deterministic —
every shutdown logs the null-instance error at server_process.gd:87 via
_stop_spawned_server. The orphaned server is NOT: one run leaked a process
that had to be killed by hand, the next reaped cleanly with the same error in
the log.
That combination is the awkward one. A fix verified by a single clean
shutdown proves nothing, so T-1224 now says to reproduce by repeated
launch/close while watching for surviving processes, rather than by reading
one log.
Co-Authored-By: Claude <noreply@anthropic.com>
The hook incremented a bare counter at 13 sites and ended with "N check(s)
failed. Fix the errors above." — naming nothing. Six of those sites (fmt,
clippy, cargo test, deny, ruff, tooling) print no FAIL line at all, so a
failure was only inferable from the ABSENCE of an "— OK" line.
Hit for real today: a push aborted on cargo fmt, and the verdict was
indistinguishable from any other failure. Finding the cause meant scrolling
past thousands of lines of unrelated test-fixture output, because the one
actionable line said only that something, somewhere, had failed.
Failed checks are now collected by name and printed in a self-contained
final block, so tailing the log always shows WHAT broke — plus a pointer to
grep the failing check's own output, and the reminder that fmt auto-fixes.
Note this is NOT a verbosity reduction, which was the tempting fix. Detail is
exactly what you want when something fails; the defect was that the verdict
carried no information, not that the log carried too much.
Co-Authored-By: Claude <noreply@anthropic.com>
Hand-written test bodies in step_canvas.rs did not match rustfmt. Caught by
the pre-push gate, which is exactly its job — team-patterns.md's note that
fmt auto-fixes and clippy is a quick lead patch, rather than something agents
should pre-emptively duplicate.
No behaviour change.
Co-Authored-By: Claude <noreply@anthropic.com>
T-1222 and T-1223 shipped today but were still sitting in backlog. Closed,
with what actually landed recorded on each — including that T-1223's title
premise was wrong: godot-cold-parse does not miss client/tests specifically,
it only ever sees the startup path, so the fix was a new tool rather than a
widened filter.
Three gaps opened after the reconciliation pass and had no ticket:
T-1230 — re-enable test_character_visual_sprint28 and fix the per-test
compositor rebuild that made it 37% of the client suite. Skipping it bought
39s; the skip must not become permanent, and the ticket says so with the
deadline (Phase 5 player rendering) and the better fix to prefer.
T-1231 — the enclosed-settlement and Sol GeneratorScope rulings still exist
only in a scratchlog under /tmp with no D-record behind them. Qatux flagged
this and correctly refused to invent the governance itself.
T-1232 — scene_helper.gd turns out to have no importers at all, which is why
its five-month parse breakage cost nothing. Delete or adopt: user's call.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Two parsing bugs in the summary, found while measuring suite times.
DOUBLE COUNT. gdUnit4 prints one "Statistics:" line per suite and then a
single "Overall Summary:" line whose numbers are the sum of all of them. The
pattern matched both shapes and summed all 87 lines, so every total was
exactly twice the truth: a full run reported 3,660 tests against an actual
1,830, and a 26-test suite reported 52. It was invisible because it doubled
UNIFORMLY — nothing ever looked inconsistent, only large. Every count quoted
from this harness, in this session and before it, was 2x.
Now prefers the Overall Summary, which is gdUnit4's own arithmetic over the
whole run and so cannot disagree with itself; per-suite summing survives only
as a fallback for a run that dies before printing it.
ANSI. gdUnit4 colourises output and the escape sequences sit BETWEEN the
fields of the summary line, so patterns matching the raw log silently fell
through to the weaker "Executed test cases" fallback — which cannot see skips
and reported a fully skipped suite as 26 FAILED. All parsing now runs against
a de-ANSI'd copy, including the load-error guards.
SKIPS are now parsed and surfaced as their own JSON field, and excluded from
passed. Counting a skipped test as passing is the same false-green shape the
harness guards exist to prevent, and it stops being hypothetical the moment a
suite is deliberately skipped.
Verified against a fully-skipped suite (26 total / 0 passed / 0 failed / 26
skipped, was 26 FAILED) and a full run (1,830 total / 1,804 passed / 0 failed
/ 26 skipped, was 3,660/3,660).
Pair session with Jeroen, 2026-07-27.
Co-Authored-By: Claude <noreply@anthropic.com>
Placed in the pre-push hook rather than /pr-process, because the hook is where
enforcement actually lives — and notably the hook never ran godot-cold-parse
at all, so until now nothing enforced "does this script parse" for any file
outside the startup path.
Ordered BEFORE the test suite deliberately. That makes failures cheaper rather
than the gate slower: a script that does not parse is caught in ~4s instead of
after ~135s of tests that could never have covered it. A clean push pays 3.7s;
a broken one saves over two minutes.
Not redundant with the suite. gdUnit4 reports the suites that DID load as a
clean pass, so an unparseable file reads as success — guarded now in
tests/run-godot, but only for test files. The sweep covers all 226 scripts,
including the roughly half of the codebase no test ever loads.
/pr-process gains a scope note instead of a second invocation: cold-parse sees
only the startup path and filters "Cannot infer the type" (which hid a
genuinely broken file for five months), so it must not be read as a general
parse check. Per team-patterns.md the skill does not duplicate the gate.
Hooks run from .config/hooks via core.hooksPath, so this is live without an
install step.
Pair session with Jeroen, 2026-07-27.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
CLAUDE.md's Phase-4 row described the pre-inversion ladder — "every step a
server-derived data canvas at its native gridunit spacing" — which the D-255
amendment reversed. Corrected, with rung 0.5 noted as ruled (D-258) but not
implemented rather than restated there. The D-243 scale-ladder section is
deliberately untouched: scale.rs still holds the old constants, so it is
still accurate, and amending it now would make it wrong in the other
direction.
CHANGELOG gains three player-facing entries for today's shipped work, with
the whole-body-map fix carrying an explicit "still open: no rivers or lakes
yet" caveat so it does not read as finished.
Tickets T-1211..T-1229 filed: the rung-0.5 epic with its cost measurement
gating every child, the scale-constant change, Sol's GeneratorScope, the two
test-harness false greens, the make-atlas shutdown bug, two data gaps and
three cleanups. Golden regeneration is blocked on both the rung-0.5 epic and
the scale-constant change so the revalidation is paid once.
Review corrections applied to the delegated pass:
- The blocker graph was reported but never created — all 8 claimed edges were
absent. Added. `pql ticket list --under T-1211 --unblocked` now correctly
returns only the measurement, which was the structural point of the epic.
- A changelog entry credited T-1206, which is an unrelated open bug about
synthetic settlements landing in open water. Re-attributed to the D-255
amendment.
- Tickets have no --decision link to D-258/D-255. Not repairable: --decision
exists only on `ticket new` and `refine write` rejects it. Filed upstream as
pql FR-5 rather than worked around; the descriptions reference the records
in prose meanwhile.
Pair session with Jeroen, 2026-07-27.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
D-255 described a system that stopped existing this morning. It said a rung
fixes gridunit SPACING and that spacing is "never viewport-derived"; both are
now exactly backwards. Anything reading it — a refinement agent, a reviewer,
a future session — would have built against a fiction with no way to tell.
Records the inversion (a rung fixes EXTENT, the shorter viewport axis spans
one cell of that level, spacing falls out), Global moving from the body's
region grid to a viewport-sized 2:1 canvas, Region leaving the orbital derive
set, and the display-ratio band collapsing to a uniform 2x2.
Two corrections matter beyond bookkeeping.
D-255 justified Global's D-226(d) legality by it being COARSER than the region
grid. It is now finer — 40.8 km against 204.8 km. The conclusion survives,
since D-226(d) prohibits tile-level maps and caps at settlement/quarter
granularity and 40.8 km is twenty times coarser than a district, but the
premise is dead and nothing downstream should lean on it.
And the always-keep cache figure is invalidated. The "~8.85 MB across 267
bodies, trivially process-resident" number assumed ~18,073 cells per body; a
viewport-sized Global is 460,800 on a 1080p display, which is 25x — about
226 MB, and roughly 900 MB on a 4K display, with per-body derive going from
~16-21 ms to about half a second. An always-keep tier whose size scales with
the user's monitor is the wrong shape, which is an independent argument for
D-258: rung 0.5 is fixed-resolution and baked, and Global becomes a view of
it rather than a canvas retained in its own right.
Also records the two retired mechanisms (the S2 station-spacing floor,
cap_extent_to_body superseded by rung liveness) and states plainly that
Global is still broken — correctly sized now, but with no hydrology until
rung 0.5 lands. Region is eyeball-confirmed working.
Pair session with Jeroen, 2026-07-26.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Jeroen, eyeballing the lakes: "they did not seem to run the same coastline
code as ocean does". Correct, in two separate ways.
The coastline warp was ocean-only. invent_primitives displaces the sample
through coast_warp_px before reading the ocean mask, but the lake test read
the UNWARPED position, so ocean coasts got invented bays and capes while lake
shores traced the bare elevation contour. It cannot be fixed by warping the
lake sample alone: a lake is where a filled surface sits above terrain, two
reads that must agree, so moving one and not the other puts water on
hillsides or holes inside lakes. Both surfaces move together in the rung-0.5
pass, or neither does.
And shore morphology was structurally unreachable at a lake edge. Every gate
keyed on ocean_fraction_q, which is always 0 in a lake basin because lakes sit
above sea level. Ruled: lakes get full shore morphology — cliffs, beaches,
deltas. Gates key on proximity to water, not to ocean. No new vocabulary
needed; MorphologyZone already carries Fjord, Delta, Wetland, CliffCoast and
DuneStrand.
The interesting part is what separates the sea-flavoured types, because it
isn't salinity. A delta builds land outward where the river deposits faster
than the water removes; an estuary is the inverse, a drowned valley widening
seaward. The discriminator is tidal energy: the microtidal Mediterranean is
ringed with deltas (Nile, Rhone, Po) despite being salt, while the macrotidal
Atlantic gives estuaries (Thames, Severn, Gironde). So lakes always resolve to
Delta — and so does a tideless sea, which an ocean-vs-lake switch would have
got wrong. Tidal energy governs TidalFlat too, so one derived quantity
replaces two stipulations and no "is it the ocean" branch survives.
Salinity is a property of water, not a landform, and is excluded from
morphology entirely. Derive it from below-sea-level connectivity if gameplay
ever needs it. Parked.
Pair session with Jeroen, 2026-07-26.
Co-Authored-By: Claude <noreply@anthropic.com>
Every Atlas rung currently re-derives from the heightmap independently. D-258
inserts one deterministic whole-body layer between the baked inputs and the
ladder, and points every deeper rung at it instead of at the source files.
Two failures forced it. The Global rung was deriving a five-class hue map
while a per-body artefact labelled "clean color hypsometric render (display /
Atlas)" sat unused beside it. And hydrology was not derivable at all: flow is
a global solve, so no per-window derivation could produce a coherent water
system — Region carried no courses and lakes could not fill.
The record also fixes what the reliefmap IS. It is a plurality, not a
classification: each cell names the biome dominating ~38 km, a vote already
counted and discarded. So rung 0.5 un-summarises it rather than upscaling it,
which binds three consequences — biome edges are gradients never lines
(D-243's climate rule extended to biome), descending reveals composition
rather than sharpness, and invented detail must downsample back to the
summary it came from. That last one is the acceptance gate for any sub-biome
algorithm.
Rung 0.5 is a stored derived artefact and therefore a named carve-out from
D-227's derive-don't-store. The boundary is principled: D-227 governs what is
LOCALLY computable, where storage is pure cost. A whole-body flow solve is not
locally computable by construction — that is why it must exist — so storage
here buys correctness, not convenience. Everything below rung 0.5 stays
derive-don't-store.
Record precedes implementation; no code changes here. Complements the same
session's D-255 extent inversion, which governs how a canvas is sized rather
than what it is made of.
Pair session with Jeroen, 2026-07-26.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
Finding 2 became a real code fix: interstitial_fill_into now enforces
the footprint-wins conflict rule (column_has_voxel range probe) — the
FilledChunk absence contract was previously a documented promise the
code didn't keep against conflicting inputs; pinned by a fully-
overlapping-leaf test asserting per-tile resolution. The tautological
overlap test replaced with a real rects_overlap() geometric helper
(itself sanity-tested) applied pairwise. The degenerate-setback fix is
now a standalone pure fn shrink_lot_or_interstitial with four boundary
tests — honestly documented as unreachable from live traffic today
(every min_lot exceeds every setback), a robustness guard for future
recalibration. Both sub-chunk clip tests now reconstruct the full
32-tile union across the seam (disjoint + complete), including the
pre-existing footprint clip test (leave-cleaner). Brief's ChunkLayout
claim tightened to the verified no-production-consumer statement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BSP leaves that lose the D-233 coverage roll in
subdivide_block_footprints were computed and discarded; they are now
surfaced as the interstitial rect set (BlockSubdivision), making the
ground-plane classification exhaustive by construction: footprint /
interstitial / street-margin-or-reserved. FillChunk carries the leaves
plus a minimal BlockFillContext (interstitial_character + setback_tier,
re-derived at block level via the existing pure fn); FilledChunk gains
a sparse interstitial map whose absence contract is stated on the
struct (missing key = footprint/street/reserved, never unknown). The
pure resolution maps OperationsSurface (D-233) first, else setback_tier
onto five of D-235's seven interstitial values — dock_slip/market_pad
have no specified trigger in the record and point at T-1209 rather
than an invented mapping. Design brief with the geometry model at
docs/architecture/interstitial-fill-t1098.md (lead-approved
checkpoint). Bonus fix: a degenerate setback shrink previously vanished
from BOTH lists silently; it now falls through to interstitial. 14 new
tests; full cargo test green incl. all golden harnesses; purity per
T-987 (plan-time compute, pre-resolved work items, no cache reads).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D-130->D-131 (broad life-verb vocabulary, x3) and D-118->D-133 (skills
affect outcome, x2), each verified by full record-body match and lead
spot-check. The T-1205 audit body-matched all 66 unique decision ids
across the remaining 11 briefings — the other 10 are citation-clean.
Final f4c72e148 tally: 16 mis-citations across 6 of 18 briefings, all
fixed (T-1199 + T-1205). Follow-ups: T-1208 (eight wholesale-stale
briefings, rewrite backlog), and tiger.md confirmed deleted/off-roster.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The surcharge is now COASTAL_ACCESS_SURCHARGE_HOPS_PER_RING=1 added
directly to length_cells (a pure hop count) — the old cost-unit
constant div_ceil'd through MIN_CELL_COST silently produced 4 hops per
ring, worst-case +24 (double the waypoint threshold) for physically
short edges; worst case is now 6. A formula-pinning test asserts both
the arithmetic and the constant. GJ251c's repro tightened to the
documented 2 edges. The always-land citation now points at the real
guarantee (features.rs::extract_attractors, D-209) — and checking the
D-211 Phase-4 synthetic-overflow path exposed a real gap: it has no
ocean-mask guard at all (T-1206 filed); documented, not papered over.
D-210 gains a dated amendment recording the surrogate-anchor-at-cost
carve-out and the relaxation-over-nudge adjudication. The bare 100
dependency dissolved with the unit fix. Edge counts on both repro
bodies verified unchanged (reachability was never affected).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A routing cell folds up to 64 native pixels, so a coastal settlement's
own land pixel (placement always filters !ocean_mask) can sit inside a
water-majority cell that RouteGrid marks IMPASSABLE — and astar()
hard-returned None for every pair touching it, zeroing whole road
graphs (GJ251c: all 3 placements; GJ380c: Sethvale). The fix relaxes
only the start/goal anchor lookup: nearest_passable_cell (ring BFS,
deterministic row-major tie-break, bounded at COASTAL_ANCHOR_MAX_RING=3)
finds a surrogate anchor and prices it via COASTAL_ACCESS_COST_PER_RING
— a short, honestly-costed access road, never a free water crossing.
IMPASSABLE semantics untouched everywhere else (D-210 transit costs,
open-ocean). The placement-nudge alternative was rejected: it would
move Layer-3 state D-211 promises is seed-derived, for no gain.
Boundary semantics pinned by test: exactly-half-water cells stay
passable (strict-majority rule); a settlement with no passable cell
within the search ring degrades to an isolated 0-edge node, never a
panic or fabricated route. Failing-first repro on real bodies
(GJ251c 0->2 edges, GJ380c 0->1) via the real cascade entry point,
plus same-seed determinism. Full cargo test green; believability and
cascade goldens verified unaffected (Layer-2-only change).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
footprint_tiles is an authored value, never derived from normalized
mesh geometry — [1,1] defaults only for plausibly-single-tile classes;
ordinarily-multi-tile classes require an explicitly stated footprint at
promotion. Her exact wording in furniture-props.md §3; conventions.md
§4 gains the authored-not-derived sentence pointing at the full rule
(and a pre-existing 'Single Sunday-tile' typo fixed in passing). The
[1,1] values shipped for the four promoted items stand as ruled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The 2026-03-13 'update all 18 briefings' commit transcribed a family of
adjacent-id slips while summarizing the Where's the Fun? workshop batch,
copied forward by every later maintenance pass: D-126<->D-134 swapped,
D-136 written as D-131, D-137 as D-136, D-130 as D-131, D-120 as D-137.
Fixed in gore/mellanie/ozzie/stig/nigel briefings; every correction
verified against the record's actual body (D-130's emergent-moral-arc
and D-120's no-skill-ceiling were resolved beyond the clerk's audit by
body-matching — the nigel D-137 fix would otherwise have created
contradictory citations in one file). Governance records untouched
(they were always correct); sprint archives verified already-correct.
Remaining 13 briefings from the same commit: T-1205.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doors.md now presents D-231's actual struct shape (initial_state
Open/Closed/Locked/Sealed; TemporalWindow lives in the independent
credential field) and attaches the Phase-5 runtime bridge to
initial_state only. furniture-props.md inventories all five spike GLBs
— acceptance is 4 promotable (3 furniture + lion_statue), vw_beetle
explicitly excluded to the Q-067 deferral. deferred.md + station-walls
gain the axis-completeness note: roof/facade/street textures ride the
wall-brief convention as a same-family follow-on, so the register
accounts for all four D-235 axes. The briefing's D-257 pointer
corrected to palette.md §2.1–2.2, and both the briefing and palette.md
(leave-cleaner, gap inherited from PR #211) now carry D-154's amendment
to D-033 — relationship colors display only in the insert/perception
overlay; normal gameplay is a uniform #1a1a1a outline (D-150).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the 2026-03-13 v0.2-pivot briefing (sprite pipeline, dropped
scope framing, pre-Atlas) with the current shape: palette.md +
conventions.md as the live authorities the briefing points at rather
than restates, her standing rulings (category-first naming, D-257
toon/glazing) marked as hers to extend, D-235 co-maintenance with Miri,
the wardrobe pipeline as reference model, D-255 stepped Atlas as active
co-designed work, and her real open items (T-1049, D-257 shader,
Q-119, T-1198). Every v0.2-era decision citation verified before
dropping: D-114/D-117 superseded; D-119/D-122/D-128/D-135 live but not
hers; D-033/D-045 kept. D-126/D-131 found REPURPOSED to unrelated
content since the era docs cited them — the ID-drift sweep is T-1199.
Lead added the briefs/ + deferred.md pointers (araminta's cross-agent
note; her SendMessage to the author couldn't be delivered).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The asset-brief template (scope/visual-reference/naming/gen-path/
acceptance, one page) and five real briefs — station walls, rural walls,
doors, floors, furniture/props — each citing its D-235 axis tokens and
palette.md register rows (or the D-257 toon default), conventions.md
naming/mask/footprint rules, and the image-gen -> glb-gen -> promotion
path with concrete acceptance counts. Doors carry the negative
no-state-frame-files constraint (runtime mechanism stays Phase 5);
furniture/props' first acceptance is promoting the three unpromoted
spike GLBs. deferred.md is the formal register for the T-1051 four
(lamp posts, barns, TVs, billboards) + cars/Q-067, with rationale and
revisit triggers — satisfying T-1051's own 'or record formal deferral'
deliverable. Child stories under T-961 are created lead-side at merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Tyre's PR #212 finding 1: the review-status row existed only in a live
DB, unrecorded in any git-tracked changelog — a rebuild reverted it.
Root cause: pql run from the capture worktree resolved the MAIN vault
(FR-4), so the write-through row landed here, uncommitted. Committing
it main-side makes the transition rebuild-safe; branch and main
changelog rows union at merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_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>
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>
Authors the full style bible: camera table separating D-148 gameplay from
D-019 offline-renderer use; the delegated toon-vs-PBR ruling (environment
props share the character toon treatment, with a minimal-PBR carve-out for
glass/polished metal so sightlines read truthfully under occlusion-based
perception); a color/material register for every D-235 ObjectTag (hue,
grain, D-217-keyed weathering); and explicit supersession notes both ways
with visual-grammar-v01.md and the mood-board workshop transcript.
Includes branch-side pql changelog rows (T-1050/T-1052 -> review); the
worktree pre-commit export step failed benignly (write-through had
already refreshed the files) so they are staged explicitly here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Guard becomes land_districts <= 1 (both reviewers converged — a lone
island definitionally cannot show two distinct directions; same
nothing-to-vary condition one value short), with a lone-island vacuous-
pass fixture; golden confirmed untouched. Oasis scaling adjudicated as
LIVE, not future — GRID_W is already 1024 on main, so ring iterations
change 2/4 -> 4/8 today: extracted a pure oasis_ring_iterations()
helper pinned by tests at both 512 and 1024, and traced exactly why the
determinism hash stayed green (it reads only elevation; the rings touch
only biome — a genuinely different array, not a coincidence). The
drainage merge-logic question answered byte-precisely: zero logic
changed vs main (comment-only diff) — and the deeper dig PROVED the
'isolated basin with another basin to escape to' branch is
mathematically unreachable for any connected grid (contracting vertex
groups of a connected graph cannot disconnect it), so the comment now
states that instead of narrating a divergence that never fires; two
direct merge-target tests added regardless. Wrap test renamed to what
it actually pins (non-wrap-awareness). D-010 docstring softened to
same-process purity, naming the cascade golden as the cross-run layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Tyre's PR #209 review flagged T-971's re-scope appends as invisible to
the branch-side planning store — same bookkeeping-lag class as PR #208's
T-1195 finding: the write-through rows were awaiting this commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tyre's PR #209 finding: every prior D-226 re-scope carries a dated
amendment in the record, and this vocabulary change existed only in
code comments and ticket appends. Records the drops (select_city,
open_regional — no settlement hit-test affordance post-D-255; one
screen Region..Chunk), the additions (open_atlas, jump_to_center via
the constrained jump_to seam), the summary-field reconciliation, and
the in-process-only transport narrowing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A real D-245 gate-shape strengthening, not just a test: the D-256/T-1174
finding proved the believability golden byte-identical under a total
all-North basin_direction collapse — every scalar contrast field is
structurally blind to the one field that regressed. ContrastMetrics
gains land_districts and basin_directions_distinct (both over ALL
districts, no new derive calls — the evidence-backed pick over the
voxel-transect proxy, which washes out at production sample density),
and evaluate_criteria gains 'basin direction variety': pass when
land_districts == 0 (the drained-body guard — an all-ocean body has no
cells that can cast a D8 vote per the aggregator's own exclusion rule,
so a uniform default is legitimate, mirroring the file's existing
nothing-to-vary idiom) or distinct >= 2. Negative test proves the
criterion catches the land-bearing all-North regression; vacuous-pass
test proves the waterworld guard. Golden regenerated and rerun-stable;
both validation bodies (Arbour, Edict) pass at distinct=2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verified-still-open coverage list: per-type attractor reachability
fixtures (LakeShore via enclosed depression, PassEntrance via crafted
saddle, PlainCenter via flat terrain, RiverCrossing via confluence) plus
thin_by_spacing behavior (collision, strict-< boundary, equirectangular
column wrap); heightmap 8-bit decode, sea_level passthrough, downsample
identity and zero-target early-return; drainage area_pct bit-for-bit
determinism plus the isolated-basin-fallback divergence comment (Tyre
N1, citing the pre-#953 behavior it deliberately departs from); the
layer1 mountain-branch pairing test (investigated first — the cascade
test supplies a mountain pool but only ever asserted river counts, a
genuine gap); an importer idempotency test covering atlas_city_names
AND atlas_feature_names plus the Sol exemption, wired into
make test-tooling; and the oasis_water dilation radius scaled by
GRID_W/512 (Tyre N2, hash-stable). One stale item dropped per the
refinement trim (test_sim_determinism wiring — already done).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>