b9cd26429e46014f5849bc8e860611ba3a0dbae7
2968
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b9cd26429e |
chore(meta): file T-1243 — fog perf test flakes under gate load
The pre-push gate rejected the T-1213 push on a wall-clock fog budget (0.606 vs 0.5 ms) that passes 23/23 in isolation on the same build. Second hardening cycle for the same failure mode: min-of-7 defends against one slow sample, not the sustained core saturation the gate itself creates by running cargo and tooling suites immediately before it. 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
|
||
|
|
e5224b1a44 |
chore(meta): 0.4.8 — the T-1242 gate's first false positive, paid not dodged
A test-only edit to composition.rs tripped the canvas-generation gate, which is path-based and cannot tell an assertion fix from a generator change. Bumped rather than excepted: the ruling is that a false positive costs one round of cache misses and a false negative costs a week. Second no-op bump in two days, noted in project.yaml so the rate is visible if it becomes noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
869837f728 |
test(simulation): the conservation gate's monoculture check was a tautology (T-1213)
D-258 invariant 2 says descending the ladder must reveal COMPOSITION — a cell
reading forest must be able to contain the clearings and rock the vote
suppressed. One assertion stood behind that, and it read:
assert!(tally.len() > 1 || share == 1.0, ...)
A single-class tally has a 100% share by definition, so both branches are always
satisfiable: the check could never fail, including in the exact case its own
message names, "or nothing was composed". The invariant had a test and no gate.
Split into the two bounds the invariant actually has, because it is two-sided:
conservation caps how much may be invented (majority > 50%, already asserted) and
composition sets a floor on how little (minority >= 0.1%). Verified by raising
the floor to 2% and watching it fail on the measured 1.07%, then restoring it —
the floor is a tripwire for "did anything happen", deliberately far below the
measurement rather than tuned to it.
Measured at the descent ladder's own anchor on Ferrath:
conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209}
So composition IS working in the data and conservation holds. The map is flat
anyway, and tooling/atlas-flatness (added here) says why the eye was not enough:
rung distinct lum p1-p99
Global 1581 145.69
Region 2923 33.59
District 53 13.72
Quarter 46 11.01
Region carries almost TWICE Global's distinct-colour count while holding a
quarter of its structure — the dither pass adds colour noise, not information, so
a colour-count metric would have called the flattest rung the richest. Structure
falls ~92% from Global to Quarter.
The cause is a channel mismatch rather than a missing generator: composition
perturbs moisture_q/slope_q, and the base map draws morphology hue x elev_q
lightness. The ladder scenarios pass no overlays deliberately, so the composed
fields are never rendered in the very shots that judge this work. Recorded on
T-1213 with the three ways forward; the choice touches D-258 and is Jeroen's.
The gate is still #[ignore]d — noted on the ticket as worth moving into a harness
that runs, since believability and window-derivation already load real bodies in
the normal cargo test path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6e6218d654 |
chore(meta): close T-1242
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
48fee8a0b6 |
feat(config): make the canvas-generation/version pairing a gate, not a habit (T-1242)
project.yaml's version is the Atlas disk cache's only invalidation signal, and nothing enforced that changing canvas GENERATION also moved it. It broke five times -- 0.4.2 lake_margin_q, 0.4.3 coast_warp_px, 0.4.4 the extent inversion, 0.4.5 the Global sentinel, 0.4.6 one-course-per-river -- each bumped only after someone noticed a wrong map. The failure is invisible to its author: it needs a warm cache to reproduce, so a cold checkout looks fine. T-1239 is the last one, and it took eight days. tooling/canvas_sources.py is the path registry; tooling/check-canvas-version rejects a push that touches those paths without moving project.yaml's version line. Wired into the pre-push hook, `make check-canvas-version`, and, for the parsing units, `make test-tooling`. Verified against real history rather than a synthetic branch: run over |
||
|
|
a1568d27c1 |
chore(meta): close T-1241
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
e8d522b482 |
chore(meta): close T-1239, file T-1241/T-1242 follow-ups
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 ( |
||
|
|
16348e2e89 |
chore(meta): drop the no-op Write() twins from the permission lists
A Write(<path>) permission rule matches nothing. File permission checks consult only Edit(<path>) rules, which already cover every file-editing tool — Write, Edit and NotebookEdit alike. Claude Code now warns about the dead shape at session start. All eight removed here sat directly beside their Edit() twin, so the allow grant over the repo tree and the ask gates guarding settings and hook files kept working throughout. Behaviour is unchanged. That ask block remains the pattern worth copying to the other repos in this tree — it is the only one that stops an agent quietly widening its own permissions, and it has to be ask rather than deny to stay fixable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dd0f9804b5 |
chore(meta): install pql replication hooks and cover the format marker
This repo had no .pql/hooks/ at all — the four replication hooks were never installed, because pql's installer used to ignore a redirected core.hooksPath. It works with .config/hooks now, so init prepends a two-line shim to each hook that sources the pql half. Existing hook bodies are untouched; the shim goes above them. What this buys: post-merge now runs `pql plan upgrade`, so a pull that brings in a newer changelog format migrates it forward automatically instead of replaying under superseded rules. .gitattributes gains a rule for changelog files at the root of .pql/changelog/. The existing `**/*.sql` pattern requires a directory component and so did not match the new 0000-format.sql marker, which would have made it a merge conflict rather than a union merge. Note for a follow-up: the hand-folded pql block in .config/hooks/post-merge (lines ~10-12) is now redundant with the shim, so plan import and decisions sync each run twice per pull. Both are idempotent, so this is waste rather than breakage — but that block and its stale "installer is dead" comment can be dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
962cbe83a7 |
chore(meta): migrate changelog to format 2.0.0, recovering 69 descriptions
pql 2.0.0 versions the changelog file format and carries older ones forward. The rewrite touches only the inline conflict guard on each line, which moved from a content-hash tiebreak to append position (3992 lines in, 3992 out — no row data altered). This repo carried real damage from the old rule. A ticket created and appended to within one wall-clock second produced two changelog rows tied on updated_at, and the hash decided the winner — arbitrarily, and on every replay, so the loss reappeared on each fresh clone and branch switch. Replaying the pre-upgrade changelog and diffing all 1232 tickets against the repaired state: 69 tickets gained description text, none lost any, 13270 characters recovered in total. Six had no description at all. T-1057, where this was first noticed, keeps the description a session hand-recovered from ticket_history in July; its later updated_at means the tie no longer decides it. The workaround scaffolding in that field can be tidied whenever convenient. plan rebuild --verify reports zero rows lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3a38322ce9 | Merge remote-tracking branch 'origin/ocean-guard-synthetic' | ||
|
|
d5e617eff6 |
docs(simulation): PR #218 round 3 — the round-2 fix outran its own documentation
Three findings, all doc-accuracy, and all the same root cause: folding the spacing predicate into the ring walk changed what three comments describe, and two of those comments were written by this same PR one round earlier. TYRE 1 — road_graph.rs's T-1206 gap-closure comment cited `nearest_land_cell`, which round 2 made `#[cfg(test)]`. A reader chasing that name lands on a test-only function and reasonably wonders whether they are looking at dead code. Repointed to `nearest_cell_matching`, and the paragraph's closing claim that "T-1206 guarantees the placement pixel is land" is corrected: it has been land-AND-spacing-or-skip since round 2. TYRE 2 — `max_land_search_ring`'s doc named the same test-only wrapper as the thing that walks the bound. It now names the production consumer and both callers. TYRE 3 — the D-211 amendment was written in round 1, before round 2 existed, and still described a land-only correction. It now carries a dated refinement recording what the code actually does: the walk satisfies BOTH of step 4's promises in one search, and SKIP therefore also fires where land exists but none of it clears spacing within the bound. The no-re-decision conclusion is unaffected — position remains a deterministic, non-fabricated function of seed and terrain — and the refinement notes the spacing promise is step 4's alone, since Tier A/B/C placements sit on their matched attractor and were never subject to it. HOSHE's three findings were the same three hunks, observed uncommitted while the review ran: accurate content, but not in the branch tip, so the PR would have merged a governance record that misdescribes its own commit. That is this commit. Both reviewers independently confirmed what the round-2 fix claims. Tyre traced the ring geometry and tie-break order by hand against the spacing predicate; Hoshe re-ran the full 267-body corpus scan live (850s) and reproduced the figures exactly — 267 bodies, 267 reaching Layer 3, 344 placements, 109 synthetic, 0 in water, 0 spacing violations. The shared-ring-search-helper retraction is confirmed and settled, with NEW grounds rather than a restatement: round 2 strengthened the case for keeping them separate, since this walk is now parameterized by an arbitrary predicate over native u16 terrain coordinates while road_graph's is a RouteGrid method over downsampled routing cells with a fixed cost test and an unrelated bound. 22 module tests green; clippy and fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6cfd829445 |
feat(simulation): sub-cell composition — a summarised cell can contain its minority (T-1213)
D-258 invariant 2: descending the ladder must reveal COMPOSITION, so a cell reading "forest" globally contains the clearings, marsh, rock and scrub the vote suppressed. Below Global it contained nothing: morphology carried NINE zones at Global and exactly ONE (AlluvialPlain) at every rung under it, and vegetation collapsed to one class from District down. WHY THE EXISTING TIERS COULD NOT DO IT. vegetation_invention already perturbs moisture with a massif band and a texture band — but that pair was built for cross-rung coherence and is weighted 70/30 specifically so "the texture term alone can never outweigh the massif term". It is designed NOT to change a verdict, which is the exact opposite of what composition needs. And it could not simply be turned up. Measured: 50.2% of the texture field's amplitude sits in its 32,768 m octave alone, and everything at or below 2,048 m holds 5.9% of the total. Across a District window only that 5.9% varies, which after the 30% weight and a ~29-point ceiling swings moisture by +/-0.51 points against vegetation gates 5-15 points apart. Nothing could ever cross one. That is the geometric series, not a tuning shortfall — raising the ceiling enough to matter at District would make the field violent at Region. So a third tier carries the fine band ALONE, normalized to its own full swing: quiet where the coarse tiers are loud, loud where they have nothing left to say. It feeds BOTH classification inputs, because moisture alone would have left morphology just as flat. CONSERVATION IS THE BOUND, not weighting (D-258 invariant 3). The field is zero-mean, so a downsample returns the summary it was added to. Pinned two ways: a field-level zero-mean test, and a real-terrain test that derives a District-sized patch and asserts the majority vegetation class survives. RARE INCLUSIONS, and this was a correction. The smooth term is a gentle sway around the base, so it can only flip a verdict where the ground already sits near a gate — which made deep-in-class ground immune, and the conservation test duly measured a patch that was 100% Forest. A monoculture is the flat map this ticket exists to fix, one scale down. Jeroen: "maybe a dense forest should still sometimes produce a clearing or a rocky outcropping." D-258 says CONTAIN, not border on. A sparse high-contrast term now rides on top — thresholded value noise so inclusions are connected blobs rather than stray speckled cells. The same patch now reads 98.9% Forest with 1.1% Barren outcrops. The moisture half of an inclusion obeys the envelope rule (a world with no patchiness ceiling grows no glades — caught by the zero-ceiling test, which the first version failed by putting damp pockets on airless rock); the slope half does not, because an outcrop is geology and a dead world is exactly where bare rock should break the surface. The slope ceiling is 6, not the 15 first written. Measured against the window-derivation fixtures, base slope_q on ordinary ground is 2-6, so +/-15 did not vary the signal but REPLACED it — one fixture moved 6 -> 19 and two coastal samples flipped to Wetland on invented slope alone. The morphology gates are far apart because a cliff coast is a real landform; composition must let marginal ground fall both ways, never manufacture a fjord on a flood plain. NOT applied at the orbital rung, which is envelope-only by design and documents that it never invents slope — pinned by derive_orbital_at_metres_never_invents_slope, which caught the first version. Both goldens move in the IMPROVING direction, checked before updating rather than blind-refreshed: GJ338Bd moisture 75->85 distinct, slope 28->34 (range 0-50), materials 3->4 GJ244Ad moisture 25->42, slope 15->22, morphology zones 6->7, materials 2->3 Ladder effect (was -> now): Region moisture 25->39; District moisture 3->17 and vegetation 1->2; Quarter moisture 3->8. 45 server suites green, clippy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
5eb394b36f |
feat(simulation): relief_q — a local relief signal the deep rungs can resolve (T-1213)
The District and Quarter rungs rendered as flat colour, and the cause was not the biome work everyone assumed. Measured on Ferrath through the production canvas builder: at District the mean |elev_q delta| between neighbouring gridunits is 0.02, and NOT ONE PAIR in a 1290x540 frame differs by 2. elev_q spans 0-100 across the body's whole 8 km elevation range, so ONE STEP IS 80 METRES. A District canvas covers 2,048 m of ground, where the rolling relief a walker navigates by is metres to tens of metres -- a fraction of a single step. The sub-district detail IS generated (invent_primitives' scatter and relief bands compute it) and then rounded away. Confirmed by running the diagnostic with the octave cutoff disabled: still 0.02. relief_q carries that same invented fine component against a scale chosen to resolve it: 0-100 about a flat 50, RELIEF_FULL_SCALE_M = 400 m either side, so 8 m per step -- ten times finer than elev_q. elev_q keeps its body-absolute meaning and the Atlas legend stays true. Measured effect, elev_q vs relief_q (distinct values / mean 4-cell delta): Region 49 / 2.38 -> 101 / 21.86 District 10 / 0.08 -> 35 / 0.35 Quarter 8 / 0.02 -> 19 / 0.06 FIXED metre scale, never per-canvas normalization: the value for a piece of ground must not depend on what else is in frame, or the same hillside changes tone as the viewer pans. And it excludes elev_pct deliberately -- this is the departure from the surrounding land, not height above sea level; including the base would re-introduce the body-scale dominance that makes elev_q unusable down here. 50 at the orbital rungs, which skip invent_primitives by design. Nothing is lost: Global and Region still have varied elev_q (101 and 49 distinct values), and the client takes whichever field carries signal via a max, with no rung-name branching. The client's ruggedness driver changes with it. It was an elev_q GRADIENT, which cannot work across rungs -- the same 4-cell delta reads 21.86 at Region and 0.08 at District, so any single full-scale constant either saturates one or vanishes on the other. relief_q states relief outright, so |relief_q - 50| is the answer directly and a fixed metre scale is immune to that by construction. An absent plane reads FLAT, not zero -- 0 on this field means maximum relief BELOW flat, so a payload without it would have stippled the entire map. That is reachable: the field is #[serde(default)] so old-shape payloads decode. Two colorize tests whose fixtures predate the plane caught it. 2004 server tests, 1838 client tests, 0 failed. clippy clean. 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> |
||
|
|
b429f633e6 |
docs(meta): D-255 — the ladder floor is one subtile per pixel (T-1213)
Measured during T-1213, through the production canvas builder on Ferrath: at Quarter and below, morphology collapses to ONE zone and vegetation to ONE class. The uniform frames in the 2026-08-06 descent ladder were those rungs drawing exactly what they contain. The cause is arithmetic, not a missing feature. At the uniform 2x2 px display ratio a 3440x1440 window gives 540 gridunits on the short axis, so: Quarter 512 m -> 0.948 m/gridunit -> 0.474 m/px ~1 subtile per pixel Block 128 m -> 0.237 m/gridunit 4 gridunits per voxel Chunk 64 m -> 0.119 m/gridunit 8 gridunits per voxel Block and Chunk magnify beneath the finest datum that can exist, so they can only ever draw one voxel larger. Quarter lands within 5% of one subtile per pixel and becomes the floor. Stated as a rule so it survives the constants moving: the deepest Atlas rung is the one at which a screen pixel shows one subtile. It is derived from the data model rather than chosen, and it moves automatically if the subtile does. WHAT THIS IS NOT. Chunk remains the 64 m stream/derive unit of D-243 and stays vital — it is what Phase 5 derives first-person walkable content on, and D-012's load-around-the-player is expressed in chunks. Block remains the 128 m generator planning unit. Only Atlas VIEWABILITY is retired; the containment ladder is untouched. This record governs what the map draws, not what the generator builds. The justification is the Atlas's purpose (Jeroen): it exists to give the player information, and a rung earns its place by answering a question the rung above cannot. Once a pixel is a subtile there is no finer datum to answer with. The resulting Global -> Region -> District -> Quarter steps at ~93x -> 100x -> 4x. That unevenness is NOT from this change — the rungs removed were 4x and 2x steps carrying no information — it is D-243's one non-power-of-2 rung, and T-1218 already exists to re-balance it. A compensating rung above Region was considered and declined here; it belongs with that ticket. CLAUDE.md's cascade line updated in the same commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6beb4bbea1 |
chore(meta): commit the T-1212 -> T-1213 blocker removal (T-1211 re-scope)
The write-through row tombstoning that dependency edge was left untracked by the earlier re-scope commit. It matters on its own: the tracked July file CREATES the edge, and this August file is the only record that it was removed. Without it a `pql plan rebuild` replays July unopposed and T-1213 comes back blocked by a measurement that was retired as a gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
49812e3127 |
test(client): the chaining test outlived the chaining (T-1237)
|
||
|
|
fc55bd897f |
fix(simulation): PR #218 round 2 — the guard was spending D-211's spacing promise
Hoshe and Tyre independently found the same defect, neither having seen the other's review. They were right. THE SPACING REGRESSION. synthetic_attractor's walk picks a candidate that satisfies MIN_SPACING against every already-placed city, and the ocean correction then moves that candidate up to h/2 = 128 cells with no re-validation. D-211 step 4 promises the synthetic attractor is placed "at a position that respects minimum city spacing" — the fix was buying the land half of that promise with the spacing half. road_graph::collapse_colocated is no safety net either: it dedupes by name, not position. The spacing predicate now goes INTO the ring walk (nearest_cell_matching) rather than running before it, so the search returns the nearest cell satisfying land AND spacing, with the same tie-break order and the same degrade-to-skip. A predicate on the existing walk, not a second walk. Unmeasured, and stated rather than implied: whether this was ever a LIVE violation or only a latent one. The old behaviour was replaced before it was measured. What the corpus does say is that 12 of the 13 bodies showing any sub-MIN_SPACING pair carry no synthetic placement at all. AND THE FIRST VERSION OF THAT FIX OVER-ASSERTED. Checking MIN_SPACING across ALL placement pairs found 13 violations corpus-wide, none of them the guard's doing: the promise is step 4's alone, and matched placements (Tier A greedy, Tier B/C Hungarian) sit on their terrain attractor, never subject to it. Two real river mouths 12 cells apart is geography. Shipping that assertion would have failed the gate and blamed this guard for pre-existing placements. Both checks are now scoped to pairs involving a synthetic placement, with the 13 matched-pair proximities recorded in-code so they are not re-litigated. Corpus, both invariants: 267 bodies, 267 reaching Layer 3, 344 placements, 109 synthetic, 0 in water, 0 spacing violations. ALSO FIXED: - t1206_verification_scan could still pass vacuously (Hoshe). The fast test got bodies_loaded>0 / synthetic_seen>0 guards last round; the scan — the one test whose entire purpose is being the re-runnable evidence — did not, and !bodies.is_empty() only proves the directory listing worked. Both added. - cascade_snapshot_for_body's doc-comment claimed the snapshot "still carries the transient TerrainAnalysis" (Tyre). It is always None for a full-cascade call. Corrected in place, with the re-derivation recipe and a note that this sentence cost a false-clean 267-body scan. - The passthrough test's comment described a land-island fixture and claimed (0,0) is not returned; the fixture is ta_all_land and the test asserts (0,0) IS returned (both reviewers). Rewritten to match reality. - max_land_search_ring's cost note said ~(h/2)^2 = 16k candidates (Hoshe). That is one quadrant's area, not cumulative ring cost: sum of 8r over 1..=128 is 66,048. Conclusion unchanged; the arithmetic is the executable recalibration rationale, so it has to be right. - The fast test ran 25.8s, close to the long pole of the whole --lib suite (Hoshe). Trimmed 6 bodies to 3: 13.0s. Not to one — synthetic_seen>0 needs only one body, but resting on one is how the first draft ended up depending on GJ903c alone. RETRACTED: the shared-ring-search-helper finding. Round 1 ruled duplicate- over-share CORRECT for this walk and said so "stated so it isn't re-litigated"; round 2 asks to factor it with no new evidence and no change to either implementation. Fixing the spacing gap by predicate rather than by a second walk moves that direction anyway. Full cargo test green (40 binaries). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6147529fe8 |
test(config): reject captures where the renderer drew nothing
run-visual verified only that the captured PNG was non-empty AS A FILE. A
blank screen is a perfectly valid ~19 KB PNG, so it passed — and once a blank
capture had been recorded as a golden, every later blank capture matched it at
0.0% and the scenario PASSED. atlas_GJ338Bd_Block and atlas_GJ445c-m1_Chunk
sat green against blank goldens while the suite's other 30 scenarios failed.
That is the worst kind of test result: indistinguishable from success, and
load-bearing for exactly the work it fails to cover.
|
||
|
|
aeab41555a |
docs(meta): D-258 — the storage question was premature (T-1211 re-scope)
The 2026-07-27 amendment closed by naming the live question: does biome un-summarisation need a stored layer, or does it ride the existing sample-fresh-at-every-rung mechanism. A descent ladder answers a prior one: it is not happening in any form, stored or derived. One body, one land-anchored point, one capture per rung, no overlays, at the panel's native 3440x1440. Ferrath's heightmap is 1024x512 over a 38,089 km circumference -- 37.2 km per source pixel. Global draws at 35.267 km/gridunit, about 1:1 with the source, and reads as a world. Region draws at 0.379 -- 98x finer than anything stored -- and is a uniform colour field with dither. District, at 0.0038, is ~9,800x finer and identical in character. The Atlas is legible exactly where it samples the heightmap and flat everywhere it invents. So the D-227 carve-out cannot be argued yet on any basis, disproven or measured, because there is no artefact to store. Ruling: build the expansion as a pure function first, following the mechanism that already exists (D-255(f) mechanism B), measure that, and reopen storage only if the numbers force it. T-1211 re-scoped, T-1212 retired as a gate with its measurement moved downstream, T-1213 unblocked as the epic's first child. Recorded with the same process note the previous amendment earned: this was found by capturing the ladder and looking at it, after the goldens had been failing for 15 commits with two of them passing against blank screens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4b75be5975 |
fix(simulation): PR #218 review round — real-body evidence, D-211, derived bound
Three findings from Hoshe (QA) and Tyre (architecture), plus a bug the first of them uncovered. HOSHE — the headline evidence lived only in a deleted scratch scan. All nine tests used synthetic fixtures, so nothing committed held the "46 of 109 synthetic placements in water" claim on real data. Two tests now do. The T-1206 verification scan itself is committed as an #[ignore]d full-corpus test (267 bodies, run with --ignored), which also makes the recalibration instruction on the search bound executable rather than aspirational; a fast test pins the bodies it identifies. That scan promptly caught a bug in its own first draft, and it is the reason this commit is worth reading. `CascadeSnapshot::terrain_analysis` is transient — the cascade nulls it the moment DistrictProfile and RoadGraph are done (D-203/T-1048, ~2 MB a body) — so it is ALWAYS None on a returned snapshot, whatever cascade_snapshot_for_body's doc-comment implies. Reading the ocean mask off the snapshot and skipping when absent therefore skipped every body while reporting success: 267 bodies "scanned", 0 findings, a green assert over an empty set, in 697 seconds. Terrain is now re-derived through the same run_layer1_with_moisture call the cascade used, reproducing the grid the placements were computed against. Two habits caught it, both prompted by Hoshe's finding: a vacuity guard that refuses to pass when no synthetic placement was seen, and counters that stop "none found" and "never got that far" from looking identical. Corrected figures at seed 42: 267 bodies, all reaching Layer 3, 344 placements, 109 synthetic, 0 in water — the synthetic count matching the original scan, so the claim is reproducible now rather than anecdotal. TYRE 1 — MAX_LAND_SEARCH_RING was justified as grid_h/2 but written as a literal 128, leaving the 512x256 coupling implicit. It is now derived from the grid in scope, so the value cannot drift from its own rationale. On the current working grid it evaluates to exactly 128: no behaviour change, and the byte-identical-placement guarantee is untouched. Recalibration owner recorded. That derivation does change one test. nearest_land_cell_clamps_rows_no_wrap uses a 16x16 fixture, so its bound drops 128 -> 8, which now sits BETWEEN the clamped distance to the far pole (15) and the wrapped one (1). The assertion moves from position to absence and gets sharper for it: previously both implementations returned Some((15,0)) and only the position could be pinned; now any Some at all proves rows wrapped. TYRE 2 — D-211 carried no note though its behaviour changed. Dated amendment added: step 4's outcome set is no longer total (synthetic overflow may now resolve to a defined SKIP), and step 5's warning fires for a new legitimate reason. No re-decision needed — position remains a pure function of seed and terrain — and the dead-end cross-reference to D-210's closure is now a live anchor. Full cargo test green (30 binaries). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e69503e77 |
test(client): capture offscreen under gamescope, at the native 3440x1440
Two faults, one fix.
Captures were stealing the desktop. Only the golden path even tried to go
offscreen, via xvfb-run -- which is not installed here, so it took the
"using visible window" fallback; --screenshot and --movie never wrapped at
all. Every capture opened a Godot window on the machine Jeroen is working
and gaming on. Now a single wrapper covers all three paths.
gamescope, not the alternatives, for two independent reasons. It renders on
the real AMD GPU, and the goldens are pinned to this box's Mesa/AMD output
(T-1121 -- they do not port across rendering stacks), so xvfb-run's llvmpipe
would shift every pixel: offscreen must not silently mean a different
renderer. And it is the only installed option that lets the output size be
set. cage is also present and also GPU-backed, but it is a kiosk compositor
and forces its client to the headless output's default -- measured, a
960x540 request produced a 1280x720 PNG. A wrapper that quietly changes
resolution is worse than none here.
Worse, because resolution is not cosmetic on this map. D-255's extent
inversion makes the shorter viewport axis span exactly one cell of the rung,
so the viewport decides how much world a rung shows and at what cell count
-- a small capture is a DIFFERENT map, not a scaled one. 960x540 was also
16:9, so it never exercised the ultrawide aspect added in
|
||
|
|
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
|
||
|
|
4e503c3565 |
fix(simulation): a river course was one D8 hop, not a river (T-1237)
D-261 asked for contiguous river strokes, and the client got them by chaining hops back together after the fact. That could not work: each hop was warped independently, so a shared confluence point arrived as two points that no longer coincided -- 375 hops rejoined into 260 pieces, and Ferrath's Global map showed scratches rather than watercourses. The join belongs before invention, so it now happens on the server. river_course::build_paths walks the D8 cell graph into whole rivers from headwater to mouth, edge-drain, or junction with an already-walked river (including the joint cell, so a tributary visibly meets its trunk). step_canvas emits one course per river instead of one per cell, which also drops the per-hop warp and resampling -- a path's shape is the terrain's, so there is nothing left to invent. It is cheaper too: one point per river cell rather than three. The client's _chain_runs() and its endpoint index are deleted. Runs survive only for the reason D-261 gives them -- water splits a course, and a river crossing a lake is genuinely two strokes that must not be rejoined. Pinned by a real-terrain test on GJ380c rather than a synthetic graph, because the bug was caught by eye on real terrain: a lake must have an outflow that runs to sea level, and no such line existed. It asserts the property the eye was checking -- rivers are long, at least one reaches the sea, and every path is a contiguous walk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
41b6ceb47e |
chore(meta): close T-1236 — the capture drift was the product bug, not the harness
Filed blaming the capture harness for parking the mouse in a corner. The
harness was faithfully reproducing a real edge-scroll defect, fixed in
|
||
|
|
f5044926da |
docs(meta): D-261 — rivers are a stroke, not a scaled feature
A river is sub-pixel at almost every Atlas scale (a 100 m channel against Global's ~17.6 km per screen pixel), so drawing it to scale draws nothing. The line becomes a symbol whose job is legibility: a fixed 5 px screen-space stroke, contiguous through the river's own cell centres, never drawn over water, and culled when too small to read. The cull threshold derives from the stroke rather than being stipulated. A mark reads as a line at roughly 3x its own width, so the minimum is 15 px of on-screen length — and the kilometre thresholds then fall out of each rung's scale: 264 km at Global, 2.8 km at Region, 28 m at District. A level-of-detail ladder with no hand-tuned constants, which self-corrects if the width changes. Measured on the visible extent, not total river length: a course crossing the window always spans it and passes, so only a course wholly inside the view and small is culled. Correct at both ends of the ladder, and no new wire field. Records a tension that had to be resolved rather than split. Jeroen proposed a flat 100 km cutoff, then noted that 5x5 px still reads as a dot and asked for 15-20. Those are incompatible: the km threshold depends only on the pixel length, so 100 km implies ~6 px, which is dot-shaped at a 5 px stroke. Admitting 100 km rivers AND keeping them line-shaped needs a ~2 px stroke. Thick lines mean fewer rivers; thin lines mean more. Ruled for the bold stroke. Client-side, and provably so: the cull is measured in screen pixels, and the display ratio is a client-side viewport-dependent parameter that never reaches a wire request or cache key, so the server cannot know how many pixels a course occupies. Water-clipping needs nothing new either — the per-cell classification is already in the adopted canvas. Built on the 2026-07-27 measurement in D-258's amendment: 375 courses present, 458 of 518,400 pixels different. The rivers were not failing to render, they were correctly beneath notice; the defect was drawing specks instead of drawing properly or not at all. T-1237 implements. T-1238 restores size-varying width as polish, deliberately deferred — noting width and cull are coupled, since a thinner stroke lowers its own visibility threshold. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
21e263d0a4 |
feat(ui): stretch the UI to the window aspect — support ultrawide properly
Jeroen on a 3440x1440 panel: 'it does not fit the viewport', then 'widescreen users are fully supported so we add stretching ui'. client/project.godot declares a 1920x1080 base viewport with stretch/mode=canvas_items, and stretch/aspect was unset — which defaults to 'keep'. On a 21:9 display Godot letterboxes the entire 16:9 UI inside it, so every screen in the game was pillarboxed, not just the Atlas. Measured: the window came out 2432x1368, exactly 16:9; with aspect=expand it becomes 3440x1368, the full panel width. Worth being explicit that the Atlas was innocent here. It was filling its logical viewport correctly the whole time; that viewport was being boxed inside the monitor. The three genuine Atlas sizing bugs fixed today (the zeroed extent, the legend-column mismatch, the integer-ratio floor) were all real and all separate from this. expand gives every screen more space on a wide display instead of bars, which is the right default for a UI-heavy game with a HUD and implant panels. The tradeoff is that layouts must tolerate a variable aspect ratio rather than assuming 16:9 — accepted deliberately, since widescreen is a supported target. Verified: full client suite 1833 / 1807 passed / 0 failed, cold-parse clean, parse sweep clean across 226 scripts. 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> |
||
|
|
3a17624ddc |
style(simulation): cargo fmt the Global extent fix
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> |
||
|
|
dc1c8ca900 |
docs(meta): D-258 amendment — the hydrology rationale was false
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> |
||
|
|
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> |
||
|
|
bdea719530 |
fix(simulation): Global was a 2x1 canvas — a sentinel outlived the extent inversion
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>
|
||
|
|
b7dfc8c50f |
docs(meta): D-259 + D-260 — the two rulings that only existed in /tmp
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>
|
||
|
|
58cd87d48c |
chore(meta): record the atlas shutdown leak as intermittent, not constant
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> |
||
|
|
363574d687 |
fix(config): pre-push names which check failed
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> |
||
|
|
145e3c8b11 |
style(simulation): cargo fmt the extent-inversion tests
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> |
||
|
|
885a10af09 |
chore(meta): close the two harness tickets, file the gaps they exposed
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> |
||
|
|
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> |
||
|
|
263a98f3ed |
fix(config): run-godot reported double the real test count, and could not see skips
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> |
||
|
|
bf1976613f |
chore(config): enforce the parse sweep at the push gate, ahead of the suite
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> |
||
|
|
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>
|