diff --git a/client/tests/golden/visual/atlas_GJ338Bd_Block.png b/client/tests/golden/visual/atlas_GJ338Bd_Block.png deleted file mode 100644 index 7f7e1f169..000000000 Binary files a/client/tests/golden/visual/atlas_GJ338Bd_Block.png and /dev/null differ diff --git a/client/tests/golden/visual/atlas_GJ445c-m1_Chunk.png b/client/tests/golden/visual/atlas_GJ445c-m1_Chunk.png deleted file mode 100644 index 11c372be2..000000000 Binary files a/client/tests/golden/visual/atlas_GJ445c-m1_Chunk.png and /dev/null differ diff --git a/client/tests/test_step_canvas_disk_cache.gd b/client/tests/test_step_canvas_disk_cache.gd index 0765591e2..b3ae515e7 100644 --- a/client/tests/test_step_canvas_disk_cache.gd +++ b/client/tests/test_step_canvas_disk_cache.gd @@ -254,13 +254,18 @@ func test_capacity_sweep_never_touches_an_entry_under_budget() -> void: func test_deep_rung_cap_evicts_oldest_deep_rung_entry_when_exceeded() -> void: var cache := StepCanvasDiskCache.new(_test_root) - # Fill past the cap: MAX_DEEP_RUNG_ENTRIES_PER_BODY + 2 distinct Chunk + # Fill past the cap: MAX_DEEP_RUNG_ENTRIES_PER_BODY + 2 distinct Quarter # windows for one body, backdating each so eviction order is # deterministic (oldest last_read_at goes first). + # + # Quarter, not Chunk: DEEP_RUNGS became [District, Quarter] when the ladder + # floor moved (D-255 amendment 2026-08-07). Quarter is now the finest + # navigable rung and therefore the exhaustive-pan accumulation case this + # cap exists to bound. var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY for i in range(cap + 2): - cache.put("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64), _stub_canvas({"i": i})) - var key := StepCanvasDiskCache.make_key("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64)) + cache.put("GJ1c", "Quarter", Vector2i(i, 0), Vector2i(64, 64), _stub_canvas({"i": i})) + var key := StepCanvasDiskCache.make_key("GJ1c", "Quarter", Vector2i(i, 0), Vector2i(64, 64)) cache._debug_backdate_entry("GJ1c", key, i, i) # strictly increasing "recency" assert_int(cache.entry_count("GJ1c")).override_failure_message( @@ -268,20 +273,24 @@ func test_deep_rung_cap_evicts_oldest_deep_rung_entry_when_exceeded() -> void: ).is_equal(cap) # The two OLDEST (i=0, i=1) must have been evicted; the newest must survive. - assert_bool(cache.has("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))).is_false() - assert_bool(cache.has("GJ1c", "Chunk", Vector2i(1, 0), Vector2i(64, 64))).is_false() + assert_bool(cache.has("GJ1c", "Quarter", Vector2i(0, 0), Vector2i(64, 64))).is_false() + assert_bool(cache.has("GJ1c", "Quarter", Vector2i(1, 0), Vector2i(64, 64))).is_false() assert_bool( - cache.has("GJ1c", "Chunk", Vector2i(cap + 1, 0), Vector2i(64, 64)) + cache.has("GJ1c", "Quarter", Vector2i(cap + 1, 0), Vector2i(64, 64)) ).override_failure_message("the most-recently-written deep-rung entry must survive").is_true() +## Region, not District: the ladder floor moved on 2026-08-07 and DEEP_RUNGS +## moved with it, from [Block, Chunk] to [District, Quarter]. District is now a +## capped rung, so it can no longer stand for "shallow" — Region is the finest +## rung the cap does not apply to. func test_deep_rung_cap_does_not_apply_to_shallow_rungs() -> void: var cache := StepCanvasDiskCache.new(_test_root) var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY for i in range(cap + 5): - cache.put("GJ1c", "District", Vector2i(i, 0), Vector2i(64, 64), _stub_canvas({"i": i})) + cache.put("GJ1c", "Region", Vector2i(i, 0), Vector2i(64, 64), _stub_canvas({"i": i})) assert_int(cache.entry_count("GJ1c")).override_failure_message( - "District is not a DEEP_RUNGS member — the deep-rung cap must not evict it" + "Region is not a DEEP_RUNGS member — the deep-rung cap must not evict it" ).is_equal(cap + 5) @@ -290,7 +299,7 @@ func test_deep_rung_cap_never_evicts_the_global_floor_entry() -> void: cache.put("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO, _stub_canvas()) var cap: int = StepCanvasDiskCache.MAX_DEEP_RUNG_ENTRIES_PER_BODY for i in range(cap + 3): - cache.put("GJ1c", "Chunk", Vector2i(i, 0), Vector2i(64, 64), _stub_canvas({"i": i})) + cache.put("GJ1c", "Quarter", Vector2i(i, 0), Vector2i(64, 64), _stub_canvas({"i": i})) assert_bool(cache.has("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)).override_failure_message( "the Global floor entry must survive deep-rung cap eviction regardless of fill order" ).is_true() diff --git a/client/tests/test_step_canvas_transport.gd b/client/tests/test_step_canvas_transport.gd index 9fc3ebbad..f37beaccd 100644 --- a/client/tests/test_step_canvas_transport.gd +++ b/client/tests/test_step_canvas_transport.gd @@ -20,13 +20,30 @@ func test_rung_at_index_zero_is_global() -> void: assert_str(StepCanvasTransport.rung_at_index(0)).is_equal(StepCanvasTransport.RUNG_GLOBAL) -func test_rung_at_index_five_is_chunk_the_deepest() -> void: - assert_str(StepCanvasTransport.rung_at_index(5)).is_equal(StepCanvasTransport.RUNG_CHUNK) +## The ladder bottoms out at Quarter (D-255 amendment 2026-08-07): the deepest +## rung is the one where a screen pixel shows one subtile, and Quarter's 512 m +## extent draws ~0.948 m per gridunit — about one voxel per gridunit at the 2x2 +## display ratio. Block and Chunk drew 0.237 and 0.119 m per gridunit, below +## the finest datum that exists, so they magnified rather than informed. +func test_rung_at_index_three_is_quarter_the_deepest() -> void: + assert_str(StepCanvasTransport.rung_at_index(3)).is_equal(StepCanvasTransport.RUNG_QUARTER) + + +## Block and Chunk remain legal WIRE vocabulary (RUNG_EXTENT_M still carries +## them, and the server enum still has the variants) — they are simply not +## reachable by scrolling. This pins that distinction: the ladder does not +## offer them, whatever the index. +func test_retired_rungs_are_not_reachable_on_the_ladder() -> void: + assert_int(StepCanvasTransport.RUNG_LADDER.size()).is_equal(4) + assert_bool(StepCanvasTransport.RUNG_LADDER.has(StepCanvasTransport.RUNG_BLOCK)).is_false() + assert_bool(StepCanvasTransport.RUNG_LADDER.has(StepCanvasTransport.RUNG_CHUNK)).is_false() + # Still nameable — the retirement is of viewability, not of the vocabulary. + assert_bool(StepCanvasTransport.RUNG_EXTENT_M.has(StepCanvasTransport.RUNG_CHUNK)).is_true() func test_rung_at_index_clamps_out_of_range_indices() -> void: assert_str(StepCanvasTransport.rung_at_index(-3)).is_equal(StepCanvasTransport.RUNG_GLOBAL) - assert_str(StepCanvasTransport.rung_at_index(99)).is_equal(StepCanvasTransport.RUNG_CHUNK) + assert_str(StepCanvasTransport.rung_at_index(99)).is_equal(StepCanvasTransport.RUNG_QUARTER) func test_index_for_rung_round_trips_every_ladder_entry() -> void: @@ -49,7 +66,8 @@ func test_scroll_step_ascends_one_notch_at_a_time() -> void: func test_scroll_step_clamps_at_the_deepest_rung() -> void: - assert_int(StepCanvasTransport.scroll_step(5, 1)).is_equal(5) + # Index 3 = Quarter, the floor since the 2026-08-07 ladder amendment. + assert_int(StepCanvasTransport.scroll_step(3, 1)).is_equal(3) func test_scroll_step_clamps_at_the_global_opener() -> void: diff --git a/client/tests/test_step_canvas_viewer.gd b/client/tests/test_step_canvas_viewer.gd index 177e7c9ae..e19f29804 100644 --- a/client/tests/test_step_canvas_viewer.gd +++ b/client/tests/test_step_canvas_viewer.gd @@ -66,7 +66,9 @@ func test_scroll_rung_clamps_at_the_deepest_rung() -> void: v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) for _i in range(10): v._scroll_rung(1, Vector2(400.0, 300.0)) - assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_CHUNK) + # Quarter is the floor (D-255 amendment 2026-08-07) — scrolling past it + # would magnify below one subtile per pixel rather than show more. + assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_QUARTER) func test_reset_to_global_returns_from_a_deep_rung() -> void: diff --git a/client/tests/visual_scenarios.gd b/client/tests/visual_scenarios.gd index dafc22671..a8106ac3a 100644 --- a/client/tests/visual_scenarios.gd +++ b/client/tests/visual_scenarios.gd @@ -179,20 +179,6 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool: ["gen_dw_moisture"] ) - "atlas_GJ338Bd_Block": - _setup_atlas_golden_shot( - tree_root, - { - "body_id": "GJ338Bd", - "body_radius_km": 6711.0, - "proper_name": "Arbour", - "terrain_reference": "wiki/star-systems/GJ-338B/bodies/GJ338Bd/heightmap.png", - }, - {"system_id": "GJ 338B", "proper_name": "Arbour"}, - "Block", - [] - ) - "atlas_GJ820Bc_Global": _setup_atlas_golden_shot( tree_root, @@ -247,15 +233,6 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool: tree_root, _FERRATH, _CYGNI_B, "Quarter", [], _FERRATH_LAND ) - "atlas_GJ820Bc_land_Block": - _setup_atlas_golden_shot( - tree_root, _FERRATH, _CYGNI_B, "Block", [], _FERRATH_LAND - ) - - "atlas_GJ820Bc_land_Chunk": - _setup_atlas_golden_shot( - tree_root, _FERRATH, _CYGNI_B, "Chunk", [], _FERRATH_LAND - ) "atlas_GJ251c_Global": _setup_atlas_golden_shot( @@ -296,19 +273,6 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool: ["gen_dw_temp"] ) - "atlas_GJ445c-m1_Chunk": - _setup_atlas_golden_shot( - tree_root, - { - "body_id": "GJ445c-m1", - "proper_name": "Jinghu", - "terrain_reference": "wiki/star-systems/GJ-445/bodies/GJ445c-m1/heightmap.png", - }, - {"system_id": "GJ 445", "proper_name": "Jinghu"}, - "Chunk", - [] - ) - _: push_warning("VisualScenarios: unknown scenario '%s'" % scenario_name) return false diff --git a/client/ui/implant/apps/atlas/step_canvas/step_canvas_disk_cache.gd b/client/ui/implant/apps/atlas/step_canvas/step_canvas_disk_cache.gd index 6eac12773..d725e4d41 100644 --- a/client/ui/implant/apps/atlas/step_canvas/step_canvas_disk_cache.gd +++ b/client/ui/implant/apps/atlas/step_canvas/step_canvas_disk_cache.gd @@ -58,8 +58,9 @@ extends RefCounted ## existing `put(sim_ttl_sec)` parameter — tracked in T-1190. ## ## **HARDENING (D-255(d), both mandatory):** -## (i) Per-body deep-rung retention cap — DEEP_RUNGS (Block, Chunk; the -## two finest, matching troblum-round2.md S3's specific pan-assembly +## (i) Per-body deep-rung retention cap — DEEP_RUNGS (District, Quarter as +## of the 2026-08-07 ladder-floor amendment; the two finest, matching +## troblum-round2.md S3's specific pan-assembly ## concern) are capped at MAX_DEEP_RUNG_ENTRIES_PER_BODY resident ## entries, independent of the Global floor and independent of the ## general Tier-2 byte budget — closes the D-226(d) accumulation gap @@ -120,7 +121,15 @@ const SUB_GLOBAL_BYTE_BUDGET: int = 256 * 1024 * 1024 ## bytes — capping entry count directly bounds how much ground a client can ## have simultaneously resident at fine spacing, which is the D-226(d) ## purpose (information content), not a byte-budget proxy for it. -const DEEP_RUNGS: Array = ["Block", "Chunk"] +## Re-pointed 2026-08-07 (D-255 ladder-floor amendment): was ["Block", +## "Chunk"]. Those two left the navigable ladder — a screen pixel there shows +## less than one subtile, so they magnify rather than inform — which would have +## left this cap guarding rungs no client can request, i.e. dead code, while the +## accumulation gap it exists to close re-opened silently under the new finest +## rungs. The control follows the ladder: District and Quarter are now the two +## finest, and Quarter at ~0.95 m per gridunit is exactly the fine-spacing +## exhaustive-pan case S3 raised. +const DEEP_RUNGS: Array = ["District", "Quarter"] const MAX_DEEP_RUNG_ENTRIES_PER_BODY: int = 512 ## (ii) Schema/version tag source: the game's `project.yaml` `version:` diff --git a/client/ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd b/client/ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd index 18e043b05..8b0c1f802 100644 --- a/client/ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd +++ b/client/ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd @@ -20,14 +20,27 @@ const RUNG_QUARTER: String = "Quarter" const RUNG_BLOCK: String = "Block" const RUNG_CHUNK: String = "Chunk" -## Coarsest (0) to deepest (5) — the SIX levels D-255(a) names, index IS the -## rung. scroll_step()/rung_at_index() below are the only places this table -## is walked; every other consumer works in terms of the rung NAME (matching -## the wire's own bare-string vocabulary, never a raw integer sent over the -## bridge). -const RUNG_LADDER: Array = [ - RUNG_GLOBAL, RUNG_REGION, RUNG_DISTRICT, RUNG_QUARTER, RUNG_BLOCK, RUNG_CHUNK -] +## Coarsest (0) to deepest (3) — the navigable ladder, index IS the rung. +## scroll_step()/rung_at_index() below are the only places this table is +## walked; every other consumer works in terms of the rung NAME (matching the +## wire's own bare-string vocabulary, never a raw integer sent over the bridge). +## +## BLOCK AND CHUNK ARE DELIBERATELY ABSENT (D-255 amendment 2026-08-07). The +## deepest Atlas rung is the one at which a screen pixel shows one subtile; +## below that a rung magnifies rather than informs. At the uniform 2x2 px +## display ratio a 3440x1440 window gives 540 gridunits on the short axis, so +## Quarter's 512 m extent is 0.948 m per gridunit — about one voxel per +## gridunit and one 0.5 m subtile per pixel. Block (0.237 m/gridunit) and Chunk +## (0.119) magnify beneath the finest datum that can exist, and measured as +## exactly that: one morphology zone and one vegetation class, an unbroken +## colour field. +## +## They remain valid WIRE vocabulary (RUNG_EXTENT_M and the server enum still +## carry them) — this is the list the user can scroll through, not the set of +## rungs that can be named. And the 64 m chunk itself is untouched as the +## generator's stream/derive unit: it is what Phase 5 builds first-person +## walkable content on. Only the map stops there. +const RUNG_LADDER: Array = [RUNG_GLOBAL, RUNG_REGION, RUNG_DISTRICT, RUNG_QUARTER] ## D-243 world-metre CELL SIZE for every FIXED rung — mirrors ## server/src/atlas/scale.rs's own constants exactly (REGION_M/DISTRICT_M/ diff --git a/server/src/atlas/step_canvas.rs b/server/src/atlas/step_canvas.rs index 0aba6f1a9..84fe92c7a 100644 --- a/server/src/atlas/step_canvas.rs +++ b/server/src/atlas/step_canvas.rs @@ -80,13 +80,31 @@ pub enum StepCanvasRung { Region, /// Rung 2 — 2,048 m spacing (D-243 `DISTRICT_M`). District, - /// Rung 3 — 512 m spacing (D-243 `QUARTER_M`). + /// Rung 3 — 512 m extent (D-243 `QUARTER_M`). **The deepest NAVIGABLE + /// rung** (D-255 amendment 2026-08-07): at the uniform 2×2 px display + /// ratio it draws ~0.948 m per gridunit, about one voxel per gridunit and + /// one 0.5 m subtile per screen pixel — the floor at which a further step + /// would magnify rather than inform. Quarter, - /// Rung 4 — 128 m spacing (D-243 `BLOCK_M`). + /// Rung 4 — 128 m extent (D-243 `BLOCK_M`). **Retired from the navigable + /// ladder** (D-255 amendment 2026-08-07): 0.237 m per gridunit is four + /// gridunits per voxel, so it can only draw one voxel larger. Measured on + /// Ferrath as a single morphology zone and a single vegetation class. + /// + /// The variant is KEPT: it is still legal wire vocabulary, `extent_m` + /// still answers for it, and `BLOCK_M` remains the generator's 128 m + /// planning unit. Only the client's `RUNG_LADDER` stopped offering it. Block, - /// Rung 5 — 64 m spacing (D-243 `CHUNK_M`), the deepest Atlas rung - /// (D-255(a): tile/voxel is Phase-5 in-world content, never an Atlas - /// rung). + /// Rung 5 — 64 m extent (D-243 `CHUNK_M`). **Retired from the navigable + /// ladder** (D-255 amendment 2026-08-07): 0.119 m per gridunit, eight + /// gridunits per voxel. + /// + /// Retired from the MAP only. The 64 m chunk remains D-243's stream/derive + /// unit and is where Phase 5 derives first-person walkable content + /// ([D-012](#d-012)'s load/unload-around-the-player is expressed in + /// chunks) — it is arguably the most load-bearing unit in the cascade, and + /// nothing about it changes here. What was removed is the claim that a + /// *map* of one is worth looking at. Chunk, } @@ -2469,4 +2487,117 @@ mod tests { ); assert!(raw.settlement_id.iter().all(|&v| v == 0)); } + + // ----------------------------------------------------------------- + // T-1213 diagnostic — where the ladder goes flat + // ----------------------------------------------------------------- + + /// Per-field variance down the rung ladder on a REAL body, at the real + /// spacing, through the production canvas builder. + /// + /// The descent ladder captured on 2026-08-06 showed every rung below + /// Global rendering a uniform colour field. This reports WHICH field is + /// flat and by how much, so the un-summarisation work targets a measured + /// gap rather than an inferred one — the captures show the symptom, this + /// shows which of the six derived fields produces it. + /// + /// Reference numbers it exists to explain: Ferrath's heightmap is 1024x512 + /// over a 38,089 km circumference = 37.2 km per source pixel. Global draws + /// at ~35.3 km/gridunit (about 1:1 with the source, and reads as a world); + /// Region at 0.379 km/gridunit is ~98x finer than any stored datum, and + /// District at ~0.0038 km/gridunit is ~9,800x finer. Everything below + /// Global is therefore invented, not sampled. + /// + /// `#[ignore]`: runs the full cascade for a body. Diagnostic, not a gate. + /// + /// ```text + /// cargo test --lib t1213_ladder_field_variance -- --ignored --nocapture + /// ``` + #[test] + #[ignore = "diagnostic — full cascade for one body; run explicitly"] + fn t1213_ladder_field_variance() { + let body = "GJ820Bc"; + let Ok((snapshot, params)) = + crate::atlas::believability::cascade_snapshot_for_body(42, body) + else { + eprintln!("skip: {body} not loadable"); + return; + }; + // `terrain_analysis` is transient and always None on a returned + // snapshot — re-derive through the same call the cascade used. See + // `believability::cascade_snapshot_for_body`'s doc. + let (_l1, ta) = crate::atlas::layer1::run_layer1_with_moisture( + &snapshot.heightmap, + crate::atlas::district_profile::derive_moisture_ceiling_q(¶ms), + ); + let river_network = snapshot + .layer1 + .as_ref() + .map(|l1| l1.river_network.clone()) + .unwrap_or_default(); + let placements = snapshot + .layer3 + .as_ref() + .map(|l3| l3.placements.clone()) + .unwrap_or_default(); + let climate = ClimateConstants::default(); + let seed = SeedChain::root(42).derive(crate::seed::SeedDomain::Body, 1); + + // The land point the descent-ladder captures are anchored on (the + // cascade's highest-scoring placement on this body). + let center = (29_422_009_i64, -5_675_959_i64); + // A 1290x540 canvas is what a 3440x1440 window produces at the 2x2 + // display ratio — the shape the captures were taken at. + let extent = (1290u32, 540u32); + + for rung in [ + StepCanvasRung::Global, + StepCanvasRung::Region, + StepCanvasRung::District, + StepCanvasRung::Quarter, + StepCanvasRung::Block, + StepCanvasRung::Chunk, + ] { + let raw = build_step_canvas( + seed, + body, + ¶ms, + &ta, + &river_network, + &placements, + rung, + center, + extent, + &climate, + 0, + ); + let spacing = + rung.spacing_m(raw.width, raw.height, params.body_radius_km.unwrap_or(0.0)); + + let uniq_u8 = |v: &Vec| { + v.iter().collect::>().len() + }; + let span_u8 = |v: &Vec| match (v.iter().min(), v.iter().max()) { + (Some(a), Some(b)) => *b as i32 - *a as i32, + _ => 0, + }; + + println!( + "{:8?} {:>9.1} m/gu {}x{} elev[uniq {:3} span {:3}] moist[uniq {:3} span {:3}] \ + morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}", + rung, + spacing, + raw.width, + raw.height, + uniq_u8(&raw.elev_q), + span_u8(&raw.elev_q), + uniq_u8(&raw.moisture_q), + span_u8(&raw.moisture_q), + uniq_u8(&raw.morphology), + uniq_u8(&raw.vegetation), + uniq_u8(&raw.glaciation), + raw.courses.len(), + ); + } + } } diff --git a/tests/visual.json b/tests/visual.json index fb4f5da78..4a80fab61 100644 --- a/tests/visual.json +++ b/tests/visual.json @@ -58,11 +58,6 @@ "live": true, "description": "T-1157 curated Atlas golden (D-255): Arbour (GJ338Bd), Global rung, moisture overlay" }, - "atlas_GJ338Bd_Block": { - "ticks": 240, - "live": true, - "description": "T-1157 curated Atlas golden (D-255): Arbour (GJ338Bd), Block rung, no overlay" - }, "atlas_GJ820Bc_Global": { "ticks": 240, "live": true, @@ -89,16 +84,6 @@ "live": true, "description": "Descent ladder: Ferrath (GJ820Bc), Quarter rung, land-anchored, no overlay" }, - "atlas_GJ820Bc_land_Block": { - "ticks": 240, - "live": true, - "description": "Descent ladder: Ferrath (GJ820Bc), Block rung, land-anchored, no overlay" - }, - "atlas_GJ820Bc_land_Chunk": { - "ticks": 240, - "live": true, - "description": "Descent ladder: Ferrath (GJ820Bc), Chunk rung (deepest), land-anchored, no overlay" - }, "atlas_GJ251c_Global": { "ticks": 240, "live": true, @@ -113,11 +98,6 @@ "ticks": 240, "live": true, "description": "T-1157 curated Atlas golden (D-255): Edict (GJ244Ad), Global rung, temperature overlay — frozen planet_class pick" - }, - "atlas_GJ445c-m1_Chunk": { - "ticks": 240, - "live": true, - "description": "T-1157 curated Atlas golden (D-255): Jinghu (GJ445c-m1), Chunk rung (deepest), no overlay — inhabited moon pick" } }, "flows": {