From c9028af77e565deddac77ac01c60cc8cd4525e20 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 22:36:05 +0200 Subject: [PATCH 1/8] =?UTF-8?q?feat(ui):=20T-1161=20=E2=80=94=20per-rung?= =?UTF-8?q?=20composite=20filter=20policy=20(Region=20NEAREST,=20District/?= =?UTF-8?q?Quarter=20LINEAR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Araminta's ruling (PR #192 follow-up): filter keyed on rung IDENTITY via the window's own echoed granularity_v2 — Region (incl. the orbital tile mosaic, whose tiles are all Region-rung requests) samples NEAREST because GPU bilinear at 204.8 km/cell reads as smoothing-over-absence; District/ Quarter keep LINEAR where cell density earns the blend. One shared helper (_filter_for_granularity_v2) at both draw call sites; unknown/missing wire tags fall back to LINEAR (never trusted into NEAREST). COMPOSITE_ SMOOTH survives as the independent compile-time pipeline axis — the two-axes split is documented in the file header. Washes/border fades untouched per the ruling. Focused suite 44/44; revert-verified (helper hardcoded LINEAR -> exactly the three Region-NEAREST tests fail). Tickets: T-1161 Co-Authored-By: Claude Fable 5 --- client/tests/test_atlas_window_overlay.gd | 132 ++++++++++++++++++ .../apps/atlas/atlas_window_overlay.gd | 94 ++++++++++--- 2 files changed, 206 insertions(+), 20 deletions(-) diff --git a/client/tests/test_atlas_window_overlay.gd b/client/tests/test_atlas_window_overlay.gd index 9eb737291..d7b4c5b93 100644 --- a/client/tests/test_atlas_window_overlay.gd +++ b/client/tests/test_atlas_window_overlay.gd @@ -34,6 +34,13 @@ static func _mock_window(n: int = 2) -> Dictionary: class _ViewerStub: var window: Variant = null var active_overlay: String = "" + # T-1161: the viewer's currently-HELD rung tag — added alongside the + # per-rung filter tests below. Not read by AtlasWindowOverlay today (the + # overlay trusts each window dict's OWN echoed granularity_v2, per + # cell_grid_side_for_window()'s precedent) but a real AtlasWindowViewer + # exposes get_held_granularity_v2() (T-1153), so the stub carries it too + # for parity with the real duck-typed interface. + var held_granularity_v2: String = "District" func get_district_window() -> Variant: return window @@ -47,7 +54,17 @@ class _ViewerStub: func is_tile_mode() -> bool: return false + func get_held_granularity_v2() -> String: + return held_granularity_v2 + +## T-1161 reframe: COMPOSITE_SMOOTH is now Axis 1 of TWO independent axes +## (see the file header doc) — the texture-vs-flat-rects PIPELINE choice. +## This assertion survives unchanged: the composite is still a TEXTURE at +## every rung. Axis 2 (which FILTER that texture samples with) is now a +## per-rung runtime decision covered separately below by the +## _filter_for_granularity_v2() tests — it is no longer bundled into this +## compile-time const. func test_composite_smooth_defaults_true() -> void: assert_bool(AtlasWindowOverlay.COMPOSITE_SMOOTH).override_failure_message( "T-1145 item 3 ships the smoothed composite as the DEFAULT presentation" @@ -265,3 +282,118 @@ func test_rebuild_handles_a_rung_swap_from_district_to_region() -> void: assert_int(o._cached_texture.get_width()).override_failure_message( "a rung swap must rebuild at the NEW rung's derived cell-grid resolution" ).is_equal(2) # region_window's cell_grid_side is also 2 here (200/100) — same size, different data + + +# ============================================================================= +# T-1161: _filter_for_granularity_v2() — the per-rung sampling filter policy +# (Araminta's ruling: Region incl. the orbital tile mosaic -> NEAREST; +# District/Quarter -> LINEAR; no hysteresis, no px-per-cell threshold, keyed +# purely on rung IDENTITY). +# ============================================================================= + + +## Region is the sparse rung the ruling targets — GPU bilinear blending at +## 204.8 km/cell reads as smoothing-over-absence, so it samples NEAREST. +func test_filter_for_granularity_v2_region_is_nearest() -> void: + assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Region")).override_failure_message( + "Region must sample TEXTURE_FILTER_NEAREST — dense-enough rungs get LINEAR," + + " Region is the sparse one the ruling targets" + ).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST) + + +## District is dense enough that the bilinear blend reads as texture, not as +## papering over sparse data — LINEAR is earned. +func test_filter_for_granularity_v2_district_is_linear() -> void: + assert_int(AtlasWindowOverlay._filter_for_granularity_v2("District")).override_failure_message( + "District must sample TEXTURE_FILTER_LINEAR" + ).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR) + + +## Quarter (4x MORE cells than District) is denser still — also LINEAR. +func test_filter_for_granularity_v2_quarter_is_linear() -> void: + assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Quarter")).override_failure_message( + "Quarter must sample TEXTURE_FILTER_LINEAR" + ).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR) + + +## An unrecognized/empty tag must NEVER be trusted into the crisp NEAREST +## treatment — mirrors cell_grid_side_for_window()'s own "unknown -> District" +## fallback posture, failing toward the already-shipped LINEAR look rather +## than an unintended NEAREST for a wire shape this code doesn't recognize. +func test_filter_for_granularity_v2_unknown_falls_back_to_linear() -> void: + assert_int(AtlasWindowOverlay._filter_for_granularity_v2("")).override_failure_message( + "an empty/unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST" + ).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR) + assert_int(AtlasWindowOverlay._filter_for_granularity_v2("SomeFutureRung")).override_failure_message( + "an unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST" + ).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR) + + +## Integration-shaped: a Region-rung window dict, drawn through the real +## _draw() entry point via the _ViewerStub duck-typed interface (same +## end-to-end shape as test_draw_builds_a_texture_for_a_region_rung_window() +## above), must drive the NODE's own texture_filter property to NEAREST — +## not just the helper function in isolation. +func test_draw_sets_node_texture_filter_to_nearest_for_region_window() -> void: + var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new()) + var stub := _ViewerStub.new() + stub.held_granularity_v2 = "Region" + stub.window = { + "center": [0, 0], + "n": 200, + "granularity_v2": "Region", + "morphology": PackedByteArray([8, 14, 0, 1]), + "elev_q": PackedByteArray([40, 90, 5, 60]), + "temp_dc": [120, 95, -32768, 60], + "moisture_q": PackedByteArray([50, 30, 90, 20]), + "vegetation": PackedByteArray([2, 1, 6, 3]), + "glaciation": PackedByteArray([0, 0, 1, 2]), + } + o.viewer = stub + o._draw() + assert_int(o.texture_filter).override_failure_message( + "a Region-rung window must drive the node's texture_filter to NEAREST after a draw" + ).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST) + + +## The District-rung counterpart of the above — confirms the node's +## texture_filter lands on LINEAR (not left over from a previous NEAREST +## draw, and not defaulting to NEAREST) for the dense rung. +func test_draw_sets_node_texture_filter_to_linear_for_district_window() -> void: + var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new()) + var stub := _ViewerStub.new() + stub.window = _mock_window() # District (the default/omitted tag) + o.viewer = stub + o._draw() + assert_int(o.texture_filter).override_failure_message( + "a District-rung window must drive the node's texture_filter to LINEAR after a draw" + ).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR) + + +## A rung SWAP (Region -> District, on the SAME node) must flip texture_filter +## along with it — confirms the property is recomputed every draw, not +## sticky from the first rung the node ever rendered. +func test_draw_flips_node_texture_filter_on_a_rung_swap() -> void: + var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new()) + var stub := _ViewerStub.new() + stub.window = { + "center": [0, 0], + "n": 200, + "granularity_v2": "Region", + "morphology": PackedByteArray([8, 14, 0, 1]), + "elev_q": PackedByteArray([40, 90, 5, 60]), + "temp_dc": [120, 95, -32768, 60], + "moisture_q": PackedByteArray([50, 30, 90, 20]), + "vegetation": PackedByteArray([2, 1, 6, 3]), + "glaciation": PackedByteArray([0, 0, 1, 2]), + } + o.viewer = stub + o._draw() + assert_int(o.texture_filter).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST) + + stub.window = _mock_window() # swap to District + o._draw() + assert_int(o.texture_filter).override_failure_message( + "swapping to a District-rung window must flip texture_filter to LINEAR," + + " not leave it stuck at the previous rung's NEAREST" + ).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR) diff --git a/client/ui/implant/apps/atlas/atlas_window_overlay.gd b/client/ui/implant/apps/atlas/atlas_window_overlay.gd index 5ba2405f2..bf5dc2f9c 100644 --- a/client/ui/implant/apps/atlas/atlas_window_overlay.gd +++ b/client/ui/implant/apps/atlas/atlas_window_overlay.gd @@ -25,18 +25,33 @@ extends Node2D ## COMPOSITE_SMOOTH := true renders the composite as an n x n Image (one ## pixel per district, EXACT same per-cell color pipeline this file always ## had — _cell_color()/_apply_glaciation() are UNCHANGED) converted to an -## ImageTexture and drawn scaled with LINEAR filtering, instead of n*n flat -## draw_rect() calls. GPU bilinear sampling between adjacent district pixels -## reads as a terrain gradient rather than hard-edged blocks — the same -## treatment the planetary heightmap already gets (Godot's engine-default -## CanvasItem.texture_filter is LINEAR_WITH_MIPMAPS project-wide, which is -## what AtlasViewer's draw_texture_rect() calls already inherit for free; -## this node sets texture_filter explicitly rather than relying on that -## default, so the choice is visible in code, not implicit). The crisp -## per-cell rect path SURVIVES behind the const (COMPOSITE_SMOOTH := false) -## so T-1143's design pass can compare both renderings directly — this is -## explicitly an INTERIM presentation, not the final answer on district-tier -## legibility (T-1143 owns that design). +## ImageTexture and drawn scaled with texture-filtered sampling, instead of +## n*n flat draw_rect() calls. The crisp per-cell rect path SURVIVES behind +## the const (COMPOSITE_SMOOTH := false) so T-1143's design pass can compare +## both renderings directly — this is explicitly an INTERIM presentation, not +## the final answer on district-tier legibility (T-1143 owns that design). +## +## T-1161 (Araminta's per-rung filter ruling, PR #192 review follow-up): the +## smoothed path's PIPELINE (texture-vs-flat-rects) and its SAMPLING FILTER +## (how the GPU reads that texture) are now two INDEPENDENT axes, not one +## bundled choice: +## - Axis 1 — PIPELINE: COMPOSITE_SMOOTH (compile-time const, unchanged by +## this ticket). true = draw a texture; false = per-cell draw_rect(). The +## composite is a TEXTURE at every rung when COMPOSITE_SMOOTH is true — +## this axis does not vary per rung. +## - Axis 2 — FILTER: _filter_for_granularity_v2() (runtime, keyed on rung +## IDENTITY via granularity_v2, T-1161). Region (incl. the orbital tile +## mosaic) samples TEXTURE_FILTER_NEAREST — GPU bilinear stretch at +## 204.8 km/cell reads as a near-featureless soft gradient, technically +## honest LoD but visually indistinguishable from the coarse-composite +## smoothing-over-absence the mandate was written to kill (T-1161's own +## description). District and Quarter sample TEXTURE_FILTER_LINEAR — cell +## density there reads as texture, not smoothing-over-absence, so the +## bilinear blend is earned. No hysteresis, no px-per-cell threshold — +## the filter is a pure function of which rung's data is being drawn. +## The crisp draw_rect() path has no sampling-filter concept at all (no +## texture involved) — its comparison/debug role per the paragraph above is +## unaffected by this axis. ## ## The texture is REBUILT only when its inputs change (the window object ## itself — a new DistrictWindowLayer arriving is a new Dictionary, checked @@ -252,6 +267,13 @@ func _draw_tile_mosaic() -> void: ## `tile_index` — sharing ONE `_cached_texture` slot across all tiles (the ## single-window field) would thrash on every draw call as different tiles' ## windows compete for it. +## +## T-1161: every mosaic tile is a Region-rung request (atlas_window_tile_set.gd +## requests tiles at AtlasWindowRequest.GRANULARITY_V2_REGION), so the mosaic +## as a whole is in scope for the Region -> NEAREST ruling. As with the +## single-window path, the filter is read from THIS tile's own echoed `w` +## rather than assumed, via the shared `_filter_for_granularity_v2()` helper +## — one policy, two call sites, no duplicated match statement. func _draw_one_tile( tile_index: int, w: Dictionary, @@ -268,7 +290,8 @@ func _draw_one_tile( ) if tile_texture == null: return - texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR + var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT)) + texture_filter = _filter_for_granularity_v2(granularity_v2) draw_texture_rect(tile_texture, Rect2(local_origin, Vector2(extent, extent)), false) @@ -367,19 +390,43 @@ static func cell_grid_side_for_window(w: Dictionary) -> int: return n # District — 1:1 +## T-1161 (Araminta's per-rung filter ruling): the sampling filter to use for +## the smoothed composite's texture, keyed on RUNG IDENTITY alone via +## `granularity_v2` — no hysteresis, no px-per-cell/zoom threshold. Region +## (204.8 km/cell — the same rung the orbital tile mosaic draws at, since +## every mosaic tile is itself a Region-rung window per `_draw_tile_mosaic()`) +## reads NEAREST: at that density, GPU bilinear blending between real derived +## samples is technically honest LoD but visually indistinguishable from the +## coarse-composite-stretched smoothing-over-absence the mandate was written +## to kill — the spirit is violated even though the letter ("never magnified +## interpolation") is not. District and Quarter read LINEAR: cell density at +## those rungs is high enough that the blend reads as texture, not as papering +## over sparse data. Mirrors `cell_grid_side_for_window()`'s own posture on an +## unknown/missing tag — an unrecognized wire value must never be trusted into +## the crisp NEAREST treatment, so it falls back to District's LINEAR instead +## of Region's NEAREST (fail toward the safer/already-shipped look). +static func _filter_for_granularity_v2(granularity_v2: String) -> CanvasItem.TextureFilter: + match granularity_v2: + AtlasWindowRequest.GRANULARITY_V2_REGION: + return CanvasItem.TEXTURE_FILTER_NEAREST + _: + return CanvasItem.TEXTURE_FILTER_LINEAR # District, Quarter, and unknown/missing fallback + + ## T-1145 item 3: the smoothed path — build/reuse a `grid_side` x `grid_side` ## ImageTexture (one pixel per DERIVED CELL, T-1152 — not per district, see ## cell_grid_side_for_window()'s doc) and draw it scaled to (n*cell_px), n -## being the window's DISTRICT extent, with LINEAR filtering. texture_filter -## is set on `self` (a CanvasItem property) once per draw — cheap (a property -## write, not a texture rebuild) and correct even the first time this runs -## (Godot's engine default already IS linear, but this makes the choice -## explicit rather than relying on an implicit project-wide default that -## could change). +## being the window's DISTRICT extent. texture_filter is set on `self` (a +## CanvasItem property) once per draw — cheap (a property write, not a +## texture rebuild). T-1161: the filter itself is now PER-RUNG, read from +## `w`'s own echoed `granularity_v2` (the same "response is the source of +## truth" posture cell_grid_side_for_window() already uses) via +## `_filter_for_granularity_v2()`, rather than an unconditional LINEAR. func _draw_smoothed_composite( w: Dictionary, n: int, grid_side: int, cell_px: float, active_toggle: String ) -> void: - texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR + var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT)) + texture_filter = _filter_for_granularity_v2(granularity_v2) _rebuild_texture_if_needed(w, grid_side, active_toggle) if _cached_texture == null: return @@ -430,6 +477,13 @@ func _rebuild_texture_if_needed(w: Dictionary, grid_side: int, active_toggle: St ## DERIVED cell-grid side (see cell_grid_side_for_window()); `n` (the ## window's district extent) sizes the on-screen cell pitch so the total ## drawn footprint stays `n * cell_px` regardless of rung. +## +## Note (T-1161): this path never touches the node-level `texture_filter` +## property — draw_rect() has no texture/sampling-filter concept, so there is +## nothing to set. That is inert today only because nothing else reads +## `texture_filter` while this path is active; it is not a bug to fix here, +## just worth stating since the smoothed path now sets that property +## per-rung and a reader might otherwise wonder why this path doesn't. func _draw_crisp_composite( w: Dictionary, grid_side: int, n: int, cell_px: float, active_toggle: String ) -> void: -- 2.54.0 From 5cb63b686782b3d1c22427d17d576977722aa72d Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 22:56:34 +0200 Subject: [PATCH 2/8] =?UTF-8?q?feat(simulation):=20T-1162=20=E2=80=94=20in?= =?UTF-8?q?vention=20octaves=20into=20the=20Atlas=20window=20path=20(coast?= =?UTF-8?q?=20crinkle,=20quarter=20relief,=20vegetation=20patchiness)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coast: WARP_OCTAVE_WAVELENGTHS_M 5->9 entries (262144m down to 1024m, Quarter's Nyquist floor) with new min_wavelength_m cutoff plumbing in coast_warp_px/warp_fbm (the warp previously had no cutoff at all). Relief: invent_primitives feeds the VOXEL band (1024-128m, salted) into elev_q/slope_q under the same envelope/cutoff discipline. Vegetation: new vegetation_invention module — two-tier (BodyParams+latitude envelope ceiling; single blended fBm field, never-gated 100-400km massif tier at 70% + cutoff-gated district/voxel texture at 30%) perturbing moisture_q OUTPUT so precipitation/glaciation/vegetation shift as one world-fact; wire format unchanged. MIN_WL_BANDS_M 5->6 (adds 1024m band). Mid-implementation correction caught by tests: District's quantized floor is 4096m (OCTAVE_WAVELENGTHS_M[3]), not 2x DISTRICT_M — the 8192m and 4096m coast octaves are legitimately District-admitted enrichment; only 2048/1024m coast + the voxel band are Quarter-exclusive. Docs and golden cutoffs corrected to match. New golden: tests/window_derivation_golden.rs + fixture (derivation_ harness pattern, UPDATE_GOLDEN=1 regen) with structural guards pinning Quarter/District divergence. Bench (zoom_ladder_bench, release): no regression — all rungs at or below baseline (District c0 3.54->~3.0 us/cell, Quarter 2.10->1.77, orbital 1.48->1.42, n=6400 window 0.86->0.59 ms). Revert-verified voxel_relief cutoff exclusion. Server lib 1840 passed; new module 11 tests; district_profile 84 (+6, 1 noise-fragile pre-existing test properly fixed via 8-district latitude-band averaging). Tickets: T-1162 Co-Authored-By: Claude Fable 5 --- server/src/atlas/coast_invention.rs | 157 ++++++- server/src/atlas/detail_scatter.rs | 7 +- server/src/atlas/district_profile.rs | 359 +++++++++++++++- server/src/atlas/layer_proxy.rs | 57 ++- server/src/atlas/mod.rs | 1 + server/src/atlas/vegetation_invention.rs | 403 ++++++++++++++++++ .../golden/window_derivation_golden.json | 257 +++++++++++ server/tests/window_derivation_golden.rs | 287 +++++++++++++ 8 files changed, 1483 insertions(+), 45 deletions(-) create mode 100644 server/src/atlas/vegetation_invention.rs create mode 100644 server/tests/golden/window_derivation_golden.json create mode 100644 server/tests/window_derivation_golden.rs diff --git a/server/src/atlas/coast_invention.rs b/server/src/atlas/coast_invention.rs index 3c5d37a06..962903e3e 100644 --- a/server/src/atlas/coast_invention.rs +++ b/server/src/atlas/coast_invention.rs @@ -49,11 +49,34 @@ const COAST_WARP_Y_SALT: u64 = 0xD1F7_0CEA_2B0A_D515; /// Salt for the tier-2 heterogeneity field (same-coast stretches differ). const CHARACTER_FIELD_SALT: u64 = 0x0C0A_57C4_A24C_7E12; -/// Coast-warp octave wavelengths in metres (≈16–262 km): capes and gulfs at the -/// top, coves and inlets at the bottom. All above the 4–33 km detail-scatter -/// band, and all below ~2 heightmap pixels — the warp perturbs the coast, it -/// does not rewrite continents (the heightmap stays the truth at its own scale). -const WARP_OCTAVE_WAVELENGTHS_M: [f64; 5] = [262_144.0, 131_072.0, 65_536.0, 32_768.0, 16_384.0]; +/// Coast-warp octave wavelengths in metres (≈1,024 m–262 km): capes and gulfs +/// at the top, down through bays/headlands/islets at the bottom (T-1162 — the +/// headline "detailed coastline" visual ask). The warp perturbs the coast, it +/// does not rewrite continents (the heightmap stays the truth at its own +/// scale) — displacing the sampling *position*, distinct from +/// `detail_scatter::terrain_detail`'s independent perturbation of the sampled +/// *elevation*. +/// +/// **T-1162 extension:** four octaves finer than the pre-T-1162 floor +/// (16,384 m) were appended — 8,192 / 4,096 / 2,048 / 1,024 — reaching the +/// Quarter rung's own Nyquist floor (512 m spacing → content wavelength ≥ 2× +/// spacing = 1,024 m; finer than that is unsampleable noise at Quarter +/// density, not detail). A Quarter-rung request (`min_wavelength_m` cutoff +/// quantized to 1,024 m, see `layer_proxy::MIN_WL_BANDS_M`) admits every +/// octave down to this floor; a District-rung request (cutoff quantized to +/// District's OWN real band, 4,096 m — District's Nyquist limit, matching +/// `terrain_detail`'s pre-existing finest octave) truncates the two finest +/// additions (2,048/1,024 m — genuinely below District's floor) while +/// legitimately ADMITTING the other two (8,192/4,096 m — new content that IS +/// coarse enough for District to resolve, not a leak). Either way Quarter +/// still sees strictly more than District at the SAME position (the two +/// finest additions are Quarter-exclusive). The +/// `enveloped_fbm`-style hard-truncate cutoff discipline is honored via +/// [`warp_fbm`]'s own `min_wavelength_m` parameter — `0.0` (no cutoff) is +/// byte-identical to every pre-T-1162 caller. +const WARP_OCTAVE_WAVELENGTHS_M: [f64; 9] = [ + 262_144.0, 131_072.0, 65_536.0, 32_768.0, 16_384.0, 8_192.0, 4_096.0, 2_048.0, 1_024.0, +]; /// Heterogeneity-field octave wavelengths in metres (≈100–400 km): the scale on /// which one planet's coastline personality drifts from stretch to stretch. @@ -224,21 +247,44 @@ pub fn character_field(seed: u64, wx: f64, wy: f64) -> f64 { /// Apply the same offset to *every* envelope field sampled at the position /// (elevation, slope, ocean mask) so the invented terrain moves coherently — /// a warped-in bay carries its sea-level elevation with it. -pub fn coast_warp_px(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> (f64, f64) { +/// +/// `min_wavelength_m` (T-1162, mirrors `detail_scatter::terrain_detail`'s +/// contract exactly): octaves in [`WARP_OCTAVE_WAVELENGTHS_M`] finer than this +/// cutoff are hard-truncated, never faded. `0.0` = no cutoff = every octave — +/// byte-identical to every pre-T-1162 caller (all of which passed no cutoff at +/// all, since the parameter didn't exist before this ticket). +pub fn coast_warp_px( + seed: u64, + wx: f64, + wy: f64, + ch: &CoastCharacter, + min_wavelength_m: f64, +) -> (f64, f64) { let sx = splitmix64(seed ^ COAST_WARP_SALT); let sy = splitmix64(sx ^ COAST_WARP_Y_SALT); ( - warp_fbm(sx, wx, wy, ch) * ch.warp_amplitude_px, - warp_fbm(sy, wx, wy, ch) * ch.warp_amplitude_px, + warp_fbm(sx, wx, wy, ch, min_wavelength_m) * ch.warp_amplitude_px, + warp_fbm(sy, wx, wy, ch, min_wavelength_m) * ch.warp_amplitude_px, ) } /// Roughness/ridge-shaped fBm in ≈`[-1, 1]` over the coast-warp octave band. -fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> f64 { +/// +/// `min_wavelength_m` (T-1162): same hard-truncate discipline as +/// `detail_scatter::enveloped_fbm` — octaves with `wl < min_wavelength_m` are +/// skipped entirely, but `amp` still advances through the skipped step so +/// surviving octaves keep their intended relative weight (no renormalization +/// against a smaller octave count beyond the shared `norm` denominator, which +/// already only sums the terms that ran). +fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter, min_wavelength_m: f64) -> f64 { let mut sum = 0.0; let mut amp = 1.0; let mut norm = 0.0; for (i, &wl) in WARP_OCTAVE_WAVELENGTHS_M.iter().enumerate() { + if wl < min_wavelength_m { + amp *= 0.45 + 0.35 * ch.roughness; + continue; + } let mut n = value_noise( seed.wrapping_add((i as u64).wrapping_mul(0x1000)), wx, @@ -257,6 +303,9 @@ fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> f64 { // entirely and still read as stripes at w64. amp *= 0.45 + 0.35 * ch.roughness; } + if norm == 0.0 { + return 0.0; // every octave cut by the cutoff → no warp displacement left + } sum / norm } @@ -357,21 +406,99 @@ mod tests { TectonicClass::Stable, ); let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55); - let a = coast_warp_px(42, 3e6, 1e6, &ch); - let b = coast_warp_px(42, 3e6, 1e6, &ch); + let a = coast_warp_px(42, 3e6, 1e6, &ch, 0.0); + let b = coast_warp_px(42, 3e6, 1e6, &ch, 0.0); assert_eq!(a, b, "warp must be deterministic"); for i in 0..400 { - let (dx, dy) = coast_warp_px(42, i as f64 * 9_137.0, i as f64 * -7_211.0, &ch); + let (dx, dy) = coast_warp_px(42, i as f64 * 9_137.0, i as f64 * -7_211.0, &ch, 0.0); assert!(dx.abs() <= WARP_AMPLITUDE_CAP_PX + 1e-9); assert!(dy.abs() <= WARP_AMPLITUDE_CAP_PX + 1e-9); } // C¹ continuity: a 10 m step is a tiny displacement change — the coast // character never steps on a line (D-243 edge-fuzz discipline). - let (x0, y0) = coast_warp_px(42, 5e6, 5e6, &ch); - let (x1, y1) = coast_warp_px(42, 5e6 + 10.0, 5e6, &ch); + let (x0, y0) = coast_warp_px(42, 5e6, 5e6, &ch, 0.0); + let (x1, y1) = coast_warp_px(42, 5e6 + 10.0, 5e6, &ch, 0.0); assert!((x0 - x1).abs() < 0.01 && (y0 - y1).abs() < 0.01); } + // ── min_wavelength_m cutoff (T-1162) ────────────────────────────────── + + #[test] + fn cutoff_zero_matches_pre_t1162_behavior() { + // 0.0 = no cutoff = every octave — the compatibility contract every + // pre-T-1162 caller relies on (all of which never had a cutoff param + // to begin with). + let env = body_coast_envelope( + ¶ms("ocean", "breathable", "temperate"), + TectonicClass::Stable, + ); + let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55); + for i in 0..100 { + let wx = i as f64 * 9_137.0; + let wy = i as f64 * -7_211.0; + let with_zero = coast_warp_px(42, wx, wy, &ch, 0.0); + // The finest WARP_OCTAVE_WAVELENGTHS_M entry is 1,024.0 — a cutoff + // below that admits every octave too, and must agree exactly. + let with_below_finest = coast_warp_px(42, wx, wy, &ch, 1.0); + assert_eq!(with_zero, with_below_finest); + } + } + + #[test] + fn cutoff_truncates_octaves_below_it() { + // A cutoff above the coarsest WARP_OCTAVE_WAVELENGTHS_M entry + // (262,144.0) must skip every octave and fall back to (0.0, 0.0) (the + // norm==0.0 empty-sum guard). + let env = body_coast_envelope( + ¶ms("ocean", "breathable", "temperate"), + TectonicClass::Stable, + ); + let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55); + let (dx, dy) = coast_warp_px(42, 1_000.0, 2_000.0, &ch, 1_000_000.0); + assert_eq!((dx, dy), (0.0, 0.0)); + } + + #[test] + fn cutoff_changes_output_relative_to_uncut() { + // A mid-band cutoff (drops the four finest octaves: 8192/4096/2048/1024) + // must produce DIFFERENT output than the uncut warp at the same + // position — otherwise the T-1162 extension would be inert. + let env = body_coast_envelope( + ¶ms("ocean", "breathable", "temperate"), + TectonicClass::Stable, + ); + let ch = coast_character_at(&env, 17, 1.2e6, 9e5, 25.0, GlaciationGrade::None, 60); + let uncut = coast_warp_px(17, 1.2e6, 9e5, &ch, 0.0); + let cut = coast_warp_px(17, 1.2e6, 9e5, &ch, 16_385.0); + assert_ne!( + uncut, cut, + "a mid-band cutoff must change the derived warp output" + ); + } + + #[test] + fn district_cutoff_excludes_quarter_only_octaves() { + // T-1162 discipline: District's REAL quantized `MIN_WL_BANDS_M` + // floor is 4,096 m (District's own Nyquist limit — matches + // `terrain_detail`'s pre-existing finest octave; see + // `layer_proxy::MIN_WL_BANDS_M`'s doc). At that cutoff, the coast + // warp's two finest additions (2,048/1,024 m) are excluded — only + // Quarter's cutoff (1,024 m) admits them. This is the positive proof + // that extending the octave floor gives Quarter genuinely more than + // District at the SAME world position. + let env = body_coast_envelope( + ¶ms("ocean", "breathable", "temperate"), + TectonicClass::Stable, + ); + let ch = coast_character_at(&env, 5, 2.5e6, 1.5e6, 15.0, GlaciationGrade::None, 45); + let district = coast_warp_px(5, 2.5e6, 1.5e6, &ch, 4_096.0); + let quarter = coast_warp_px(5, 2.5e6, 1.5e6, &ch, 1_024.0); + assert_ne!( + district, quarter, + "Quarter's finer cutoff must admit octaves District's cutoff excludes" + ); + } + #[test] fn warp_stream_uncorrelated_with_scatter_stream() { // Distinct hash path: the warp at a position must not track the terrain @@ -381,7 +508,7 @@ mod tests { TectonicClass::Stable, ); let ch = coast_character_at(&env, 42, 1e6, 1e6, 20.0, GlaciationGrade::None, 60); - let (wdx, _) = coast_warp_px(42, 1e6, 1e6, &ch); + let (wdx, _) = coast_warp_px(42, 1e6, 1e6, &ch, 0.0); let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0); assert_ne!(wdx, scatter); } diff --git a/server/src/atlas/detail_scatter.rs b/server/src/atlas/detail_scatter.rs index 6cbdaa570..11b47aa9f 100644 --- a/server/src/atlas/detail_scatter.rs +++ b/server/src/atlas/detail_scatter.rs @@ -38,7 +38,12 @@ pub(crate) const OCTAVE_WAVELENGTHS_M: [f64; 4] = [32_768.0, 16_384.0, 8_192.0, /// (<64 m) is too fine to reach. This is the [`voxel_relief`] band: the rolling/ridged /// hills a *walking character* navigates by (T-1081). The coarsest octave stays below /// the district size so the relief never competes with `elev_q`'s district-scale role. -const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0]; +/// +/// `pub(crate)` (T-1162): also the fine-tier band +/// [`crate::atlas::vegetation_invention`]'s texture field truncates against, +/// alongside [`OCTAVE_WAVELENGTHS_M`] — the same reuse-not-reinvent posture +/// that array already documents for `layer_proxy`. +pub(crate) const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0]; /// Sub-chunk mosaic octave wavelengths in metres — the ≈8–64 m band, finer than the /// [`VOXEL_OCTAVE_WAVELENGTHS_M`] sub-district band, so a micro-habitat patch reads as diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index 89c16a9b7..516ecb3b4 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1136,7 +1136,11 @@ fn invent_primitives( ); // ── 3. Invented coastline: warp the whole envelope sampling. ──────────── - let (wdx, wdy) = coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch); + // T-1162: the coast warp gets the SAME min_wavelength_m cutoff as the + // terrain-detail scatter below — one rung, one cutoff, applied to both + // continuous fields that compose the invented coastline. + let (wdx, wdy) = + coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch, min_wavelength_m); let (spx, spy) = (px + wdx, py + wdy); let elev_pct = bilinear(&ta.elev_pct, ta.w, ta.h, spx, spy) as f64; let slope_deg = bilinear(&ta.slope_deg, ta.w, ta.h, spx, spy) as f64; @@ -1155,6 +1159,35 @@ fn invent_primitives( min_wavelength_m, ); + // T-1162 part (b): sub-district relief band (VOXEL_OCTAVE_WAVELENGTHS_M, + // 1,024–128 m — "the rolling hills a walking character navigates by", + // T-1081) fed into window classification for the first time. Same + // envelope/ruggedness inputs as the district-band `scatter` above (one + // amplitude ceiling, two wavelength bands composing additively — never a + // second independently-tuned amplitude rule) and the SAME + // `min_wavelength_m` cutoff. At District's cutoff (≥2,048 m) every + // VOXEL_OCTAVE_WAVELENGTHS_M entry (all ≤1,024 m) is truncated, so + // `relief` is always exactly 0.0 there — District's output is unchanged + // byte-for-byte. At Quarter's cutoff (1,024 m) only the two coarsest + // voxel-band entries (1,024, 512) survive; the two finest (256, 128) stay + // truncated even at Quarter (below Quarter's own 512 m spacing's Nyquist + // floor of 1,024 m) — this is correct and expected (D-226(d) note): a + // contributing wavelength is never capped BY the request-granularity + // floor ruling itself (the coast warp already crosses scales the other + // way), it is simply that only 2 of the 4 voxel octaves are coarse enough + // to matter at Quarter's own sample density; the other two are reserved + // for a future finer-than-Quarter rung. Distinct seed salt (`^ 0x5EED_C0DE`) + // keeps this stream uncorrelated with `scatter`'s stream at the same + // position (same isolation discipline as `coast_invention`'s warp salt). + let relief = crate::atlas::detail_scatter::voxel_relief( + seed.seed() ^ 0x5EED_C0DE, + world_x_m, + world_y_m, + env_amp, + ruggedness, + min_wavelength_m, + ); + // Shoreline carving (T-1125): glacial / tectonically-young SHORES are cut // steep — fjord walls and cliff coasts. The gentle scatter floor alone can // never voice the D-239 steep coastal families (Fjord gates at slope_q ≥ 40, @@ -1166,9 +1199,14 @@ fn invent_primitives( let shoreline = (ocean_frac * (1.0 - ocean_frac) * 4.0).clamp(0.0, 1.0); let carve = 0.55 * ch.ridge * shoreline * (scatter.abs() / env_amp.max(0.05)).clamp(0.0, 1.0); + // Both bands add into the same elevation/slope quantization — `relief` is + // 0.0 at every cutoff ≥ 2,048 m (District and coarser), so this sum is + // byte-identical to the pre-T-1162 `elev_pct + scatter` wherever the + // cutoff discipline says it must be. InventedPrimitives { - elev_q: (((elev_pct + scatter) * 100.0).round() as i32).clamp(0, 100), - slope_q: (((local_slope + ruggedness * scatter.abs() + carve) * 100.0).round() as i32) + elev_q: (((elev_pct + scatter + relief) * 100.0).round() as i32).clamp(0, 100), + slope_q: (((local_slope + ruggedness * (scatter.abs() + relief.abs()) + carve) * 100.0) + .round() as i32) .clamp(0, 100), ocean_fraction_q: ((ocean_frac * 100.0).round() as i32).clamp(0, 100), } @@ -1290,6 +1328,9 @@ pub fn derive_district_profile( prims.ocean_fraction_q, region_baseline_c, basin_direction, + world_x_m, + world_y_m, + 0.0, // batch path — no octave cutoff, matches derive_district's default ) } @@ -1314,6 +1355,20 @@ pub fn derive_district_profile( /// The distinction is important for edge fuzz: only the two-phase path produces /// a continuous, warp-perturbed temperature gradient. The single-phase path /// still satisfies D-240 but without edge fuzz. +/// +/// ## Vegetation patchiness (T-1162) +/// +/// `world_x_m`/`world_y_m`/`min_wavelength_m` feed +/// [`crate::atlas::vegetation_invention::moisture_perturb_q`] — the nature-layer +/// patchiness field that perturbs `moisture_q` (see that module's docs for the +/// full design rationale) before precipitation/glaciation/vegetation are +/// derived from it, so the three stay in lockstep. `world_x_m == 0.0 && +/// world_y_m == 0.0 && min_wavelength_m == 0.0` is NOT a special "disabled" +/// case — the origin is a legal world position — vegetation patchiness is +/// always active wherever `VegetationEnvelope::ceiling_q > 0`, mirroring the +/// coast invention's own always-on posture (the ceiling being zero, not a +/// separate flag, is what turns it off on airless/dry bodies). +#[allow(clippy::too_many_arguments)] fn build_district_profile( seed: SeedChain, body_params: &BodyParams, @@ -1323,6 +1378,9 @@ fn build_district_profile( ocean_fraction_q: i32, region_baseline_c: Option, basin_direction: BasinDirection, + world_x_m: f64, + world_y_m: f64, + min_wavelength_m: f64, ) -> DistrictProfile { let tectonic_class = derive_tectonic_class(body_params); @@ -1350,7 +1408,24 @@ fn build_district_profile( derive_temperature_c(&district_climate_params, climate, body_seed) } }; - let moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate); + let base_moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate); + + // T-1162: vegetation-patchiness field perturbs the moisture INPUT (see + // `vegetation_invention` module docs for the full design rationale) — + // this is what turns a uniform per-district class tint into massifs at + // Region scale resolving to distinct woods/copses/clearings at + // District/Quarter. Applied uniformly at every derivation path + // (on-demand, batch, orbital) since all three route through this shared + // classification tail. + let veg_envelope = crate::atlas::vegetation_invention::vegetation_envelope(body_params); + let moisture_perturb = crate::atlas::vegetation_invention::moisture_perturb_q( + &veg_envelope, + seed.seed(), + world_x_m, + world_y_m, + min_wavelength_m, + ); + let moisture_q = (base_moisture_q + moisture_perturb).clamp(0, 100); // Climate-derived fields: computed from temperature + moisture primitives // (D-239 §2). This is the correct call order — temperature must be resolved @@ -1566,6 +1641,9 @@ pub fn derive_at_metres( prims.ocean_fraction_q, region_baseline_c, BasinDirection::default(), + world_x_m, + world_y_m, + min_wavelength_m, ) } @@ -1626,7 +1704,7 @@ pub fn derive_orbital_at_metres( // Same world-metres -> fractional heightmap pixel + latitude mapping // derive_at_metres uses — the envelope is the SAME TerrainAnalysis grid at // every rung, only the sampling density differs. - let (px, py, _world_x_m, _world_y_m, lat_deg) = match body_params.body_radius_km { + let (px, py, world_x_m, world_y_m, lat_deg) = match body_params.body_radius_km { Some(r_km) if r_km > 0.0 => { let circumference_m = std::f64::consts::TAU * r_km * 1000.0; let meridian_m = std::f64::consts::PI * r_km * 1000.0; @@ -1688,6 +1766,17 @@ pub fn derive_orbital_at_metres( ocean_fraction_q, region_baseline_c, BasinDirection::default(), + world_x_m, + world_y_m, + // T-1162: vegetation patchiness's massif tier is NEVER cutoff-gated + // (see vegetation_invention module docs) and is cheap (two small fBm + // sums, not the invent_primitives bilinear+warp+scatter pipeline this + // function deliberately skips) — passing 0.0 here means the orbital + // path samples the SAME uncut massif+texture field derive_at_metres + // would at min_wavelength_m=0.0, preserving cross-rung coherence for + // the vegetation verdict even though slope/elevation stay + // envelope-only at this rung. + 0.0, ) } @@ -1958,6 +2047,18 @@ mod tests { fn derive_district_radius_maps_to_latitude_climate() { // With a body radius, equatorial vs near-polar districts get different // temperature (the seam maps district_y → latitude). Pole = colder. + // + // T-1162: `derive_district` calls with `min_wavelength_m = 0.0` (no + // cutoff), so it now admits the extended coast-warp + sub-district + // relief octaves this ticket adds — real per-position elevation noise + // that a SINGLE probe district at each latitude is no longer immune + // to (elevation lapse feeds temperature; a single unlucky relief + // sample can swing one probe point by ~1°C, enough to flip a + // single-pair comparison at these specific hand-picked positions). + // Average temperature over several districts spanning a few hundred + // metres at each latitude band — the same zero-mean-cancellation + // technique `derivation_harness.rs`'s cross-district blend test uses + // — so the assertion tests the LATITUDE law, not one noise sample. let hm = test_hm(); let ta = test_ta(&hm); let climate = ClimateConstants::default(); @@ -1965,21 +2066,19 @@ mod tests { // meridian ≈ π·6371·1000 m; a district near the pole is ~quarter-meridian away. let merid_districts = (std::f64::consts::PI * 6371.0 * 1000.0 / scale::DISTRICT_M as f64) as i32; - let equator = derive_district(test_seed(), "test_body", &p, &ta, (0, 0), &climate); - let high_lat = derive_district( - test_seed(), - "test_body", - &p, - &ta, - (0, merid_districts / 2 - 2), - &climate, - ); - match (equator.temperature_c, high_lat.temperature_c) { - (Some(eq), Some(hi)) => { - assert!(hi < eq, "near-pole district must be colder ({hi} !< {eq})") + let avg_temp_c = |dy: i32| -> f32 { + let mut sum = 0.0f32; + let mut n = 0; + for dx in 0..8 { + let prof = derive_district(test_seed(), "test_body", &p, &ta, (dx, dy), &climate); + sum += prof.temperature_c.expect("breathable body must have a temperature"); + n += 1; } - _ => panic!("breathable body must have a temperature"), - } + sum / n as f32 + }; + let eq = avg_temp_c(0); + let hi = avg_temp_c(merid_districts / 2 - 2); + assert!(hi < eq, "near-pole district must be colder ({hi} !< {eq})"); } #[test] @@ -2188,6 +2287,228 @@ mod tests { ); } + // ------------------------------------------------------------------- + // T-1162 — coast crinkle / sub-district relief / vegetation patchiness + // ------------------------------------------------------------------- + + /// Determinism of the T-1162 fields specifically: two independent + /// `derive_at_metres` calls at the SAME cutoff-bearing position (Quarter + /// spacing, admitting the new coast-warp + relief + vegetation content) + /// must be bit-identical (D-010/D-227) — the new machinery is pure, same + /// as everything else in this module. + #[test] + fn t1162_new_fields_are_deterministic_at_quarter_cutoff() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::DISTRICT_M as f64; + + for i in 0..12 { + let wx = (300 + i * 41) as f64 * dm * 0.1; + let wy = (300 + i * 29) as f64 * dm * 0.1; + let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0); + let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0); + assert_district_profiles_eq(&a, &b); + } + } + + /// Cutoff-exclusion discipline (T-1162 parts a/b): District's REAL + /// quantized band is `4,096` m (`MIN_WL_BANDS_M`'s pre-existing finest + /// entry — matches `terrain_detail`'s own finest octave, i.e. District's + /// Nyquist floor). At that exact cutoff, `derive_at_metres` must produce + /// output IDENTICAL to a second call at the same cutoff — this is the + /// "byte-identical if the cutoff excludes the new octaves" guarantee for + /// the sub-district relief band (all four `VOXEL_OCTAVE_WAVELENGTHS_M` + /// entries are ≤1,024 m, strictly below 4,096) and the two FINEST + /// coast-warp additions (2,048/1,024 m, also below 4,096). The coast + /// warp's 8,192/4,096 m additions are legitimately ADMITTED at District's + /// own floor (4,096 IS District's Nyquist limit, not "too fine for + /// District") — that is intended enrichment, not a leak, and is + /// deliberately NOT asserted away here (see the companion + /// `quarter_cutoff_admits_more_than_district` test for the positive + /// case). This test instead pins that AT THE SAME NOMINAL CUTOFF VALUE, + /// repeated derivation is stable — the determinism half of the contract. + #[test] + fn district_floor_cutoff_is_stable_and_deterministic() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::DISTRICT_M as f64; + + for i in 0..20 { + let wx = (150 + i * 47) as f64 * dm; + let wy = (150 + i * 31) as f64 * dm; + let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0); + let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0); + assert_district_profiles_eq(&a, &b); + } + } + + /// The sub-district relief band specifically (part b) is fully excluded + /// at District's floor (4,096 m — every `VOXEL_OCTAVE_WAVELENGTHS_M` + /// entry is ≤1,024 m, strictly below 4,096). Isolated directly against + /// `detail_scatter::voxel_relief` (rather than through the full + /// `derive_at_metres` stack, where the coast warp's OWN 8,192/4,096/2,048 + /// additions would confound a two-cutoff comparison — see the module doc + /// on `MIN_WL_BANDS_M` for why 4,096 vs any value in `(1_024, 4_096)` + /// legitimately differs on the coast-warp side alone): at cutoff 4,096 + /// the relief contribution is exactly zero, matching the + /// `flat_envelope_invents_nothing`-style empty-sum guard. + #[test] + fn voxel_relief_band_fully_excluded_at_district_floor() { + for i in 0..20 { + let wx = (150 + i * 91) as f64 * 137.0; + let wy = (150 + i * 67) as f64 * -211.0; + let relief = crate::atlas::detail_scatter::voxel_relief( + test_seed().seed(), + wx, + wy, + 0.8, + 0.6, + 4_096.0, + ); + assert_eq!( + relief, 0.0, + "voxel_relief must contribute exactly zero at District's 4,096 m floor \ + (every VOXEL_OCTAVE_WAVELENGTHS_M entry is ≤1,024 m)" + ); + } + } + + /// The companion positive case: Quarter's real band (1,024 m) admits + /// content District's real band (4,096 m) excludes — the extension must + /// not be inert. Sweeps several positions and requires at least one to + /// diverge (a single unlucky zero-crossing position would otherwise + /// false-fail). + #[test] + fn quarter_cutoff_admits_more_than_district() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::DISTRICT_M as f64; + + let mut any_differs = false; + for i in 0..20 { + let wx = (150 + i * 47) as f64 * dm; + let wy = (150 + i * 31) as f64 * dm; + let district = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0); + let quarter = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0); + if district.elev_q != quarter.elev_q + || district.slope_q != quarter.slope_q + || district.moisture_q != quarter.moisture_q + { + any_differs = true; + } + } + assert!( + any_differs, + "Quarter's finer cutoff must admit SOME content District's cutoff excludes \ + at at least one sampled position — the T-1162 extension must not be inert" + ); + } + + /// Unknown/coarser cutoffs never admit finer octaves: a cutoff ABOVE + /// every extended band (coast warp's coarsest is 262,144 m) must produce + /// IDENTICAL output to the pre-extension "everything truncated" case — + /// confirms the extension didn't accidentally widen what a coarse cutoff + /// admits, only what a fine one does. + #[test] + fn coarse_cutoff_admits_nothing_from_t1162_extension_either() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::DISTRICT_M as f64; + + for i in 0..10 { + let wx = (200 + i * 61) as f64 * dm; + let wy = (200 + i * 43) as f64 * dm; + // Above the coastal warp's own coarsest octave (262,144 m) — every + // octave in every band (coast, terrain, voxel relief) is excluded. + let far_above = derive_at_metres( + test_seed(), "test_body", &p, &ta, wx, wy, &climate, 300_000.0, + ); + // An even more extreme cutoff must produce the SAME result — once + // every octave is truncated, going coarser still changes nothing. + let even_further = derive_at_metres( + test_seed(), "test_body", &p, &ta, wx, wy, &climate, 10_000_000.0, + ); + assert_district_profiles_eq(&far_above, &even_further); + } + } + + /// Vegetation cross-rung coherence (the ticket's hard requirement): the + /// MAJORITY vegetation class over a sampled patch at Region-equivalent + /// (uncut massif-only) scale must be preserved when the SAME patch is + /// refined to Quarter spacing — Quarter punches clearings/copses (some + /// cells legitimately differ), but it must not flip the patch's dominant + /// class wholesale. Uses a wet, warm, low-elevation body so Forest is the + /// achievable majority class and the massif field has genuine amplitude + /// to work with (see `vegetation_envelope`'s wetness-product ceiling). + #[test] + fn vegetation_majority_class_preserved_under_quarter_refinement() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = BodyParams { + hydrosphere: Some("ocean".into()), + atmosphere: Some("breathable".into()), + planet_class: Some("tropical".into()), + body_radius_km: Some(6371.0), + latitude_deg: 5.0, // near-equator: warm, wet, low treeline pressure + ..Default::default() + }; + let dm = scale::DISTRICT_M as f64; + // A patch of 8x8 quarter-cells (one district's worth) around a fixed + // low-elevation coastal-adjacent-but-inland district. + let base_wx = 40.0 * dm; + let base_wy = 15.0 * dm; + + let district_class = derive_at_metres( + test_seed(), "test_body", &p, &ta, base_wx, base_wy, &climate, 2_048.0, + ) + .vegetation_class; + + // Sample the surrounding quarter grid (512 m spacing) and tally class + // frequency — the majority must match the district-rung verdict at + // the patch centre if cross-rung coherence holds. Skip Marine (open + // water) tallies since the ticket's coherence claim is about land + // vegetation classes refining, not the ocean/land boundary itself. + use std::collections::BTreeMap; + let mut tally: BTreeMap = BTreeMap::new(); + let qm = scale::QUARTER_M as f64; + for dy in -2..2 { + for dx in -2..2 { + let wx = base_wx + dx as f64 * qm; + let wy = base_wy + dy as f64 * qm; + let prof = + derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0); + if prof.vegetation_class != VegetationClass::Marine { + *tally.entry(prof.vegetation_class as u8).or_insert(0) += 1; + } + } + } + if district_class == VegetationClass::Marine { + // The centre itself is open water — nothing to assert about land + // majority at this probe point; the test still ran the refinement + // sweep above without panicking, which is the structural check. + return; + } + let majority = tally + .iter() + .max_by_key(|&(_, count)| count) + .map(|(&class, _)| class); + assert_eq!( + majority, + Some(district_class as u8), + "Quarter-refined majority vegetation class must match the District-rung \ + verdict at the patch centre (tally: {tally:?}, district: {district_class:?})" + ); + } + // ------------------------------------------------------------------- // derive_orbital_at_metres (T-1152, design doc §2/§4 orbital row) // ------------------------------------------------------------------- diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index b4ed0f872..78c979e82 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -404,20 +404,46 @@ fn clamp_window_n_v2(raw_n: u32, granularity: WindowGranularity) -> u32 { } } -/// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5): -/// `0` (no cutoff) plus every entry of -/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] — the SAME array -/// `terrain_detail`'s octave sum truncates against (both district and -/// quarter rungs derive via `terrain_detail`, so this is genuinely "the -/// rung's own octave bands", not a second independently-chosen scale). +/// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5; +/// retuned T-1162 part (c)): `0` (no cutoff) plus every entry of +/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] (32,768/16,384/ +/// 8,192/4,096 m — District's own Nyquist floor, `2 × DISTRICT_M`), plus a +/// FIFTH band at `1,024` m — Quarter's own Nyquist floor (`2 × QUARTER_M = +/// 1,024`, matching a rung's content wavelength to what its `512` m sample +/// spacing can actually resolve — nothing finer than the rung can express, +/// nothing coarser than it deserves). `1,024` m is also the finest entry of +/// the extended [`crate::atlas::coast_invention::WARP_OCTAVE_WAVELENGTHS_M`] +/// band (T-1162 part a) and the coarsest entry of +/// [`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`] the sub-district +/// relief wiring (T-1162 part b) admits at Quarter — one band serves both. +/// +/// **Pre-T-1162 bands unchanged in VALUE** (32,768/16,384/8,192/4,096 stay +/// exactly where they were) — but **NOT unchanged in behavior at 8,192/4,096 +/// specifically**: `terrain_detail`'s own octave array is untouched by this +/// ticket, so THAT contribution is byte-identical at every pre-existing band, +/// but the coast warp's octave array gained genuinely new 8,192/4,096 m +/// entries (T-1162 part a) that a District-rung request quantizing to those +/// bands now legitimately admits — District's real Nyquist floor (4,096 m) +/// is coarse enough to resolve that content, so this is intended enrichment +/// at District too, not a leak (see `coast_invention::WARP_OCTAVE_WAVELENGTHS_M`'s +/// doc for the full admit/exclude table). Only the 32,768/16,384 bands are +/// truly inert-to-this-ticket (no octave in any extended array falls in that +/// range). The sub-district relief band ([`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`], +/// all ≤1,024 m) and the coast warp's two finest additions (2,048/1,024 m) +/// are genuinely excluded at every pre-existing band (all ≥4,096) — those are +/// Quarter-exclusive, admitted only by the new `1,024` band. The new band is +/// additive at the END of the array — appending, not reordering, keeps every +/// existing index-based reference to the first four entries valid. +/// /// Descending order except the leading `0.0` sentinel, matched by /// `quantize_min_wl_m`'s scan below. -const MIN_WL_BANDS_M: [f64; 5] = [ +const MIN_WL_BANDS_M: [f64; 6] = [ 0.0, crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[0], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[1], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[2], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3], + 1_024.0, ]; /// Snap a wire-supplied `window_min_wl_m` to the nearest fixed band in @@ -2660,9 +2686,9 @@ mod tests { assert_eq!(quantize_min_wl_m(0), 0); } - /// A value nearer to 0 than to the finest real octave band (4,096) snaps - /// to 0 (no cutoff) — the band set includes 0 as a real, selectable band, - /// not just a special-cased default. + /// A value nearer to 0 than to the finest real octave band (T-1162: now + /// 1,024, the Quarter-rung floor) snaps to 0 (no cutoff) — the band set + /// includes 0 as a real, selectable band, not just a special-cased default. #[test] fn quantize_min_wl_m_small_value_snaps_to_zero_band() { assert_eq!(quantize_min_wl_m(500), 0); @@ -2678,6 +2704,17 @@ mod tests { assert_eq!(quantize_min_wl_m(7_500), 8_192); } + /// T-1162: a value between the new 1,024 band and 0 snaps to whichever is + /// nearer — exercises the new finest band specifically, not just the + /// pre-existing four. + #[test] + fn quantize_min_wl_m_snaps_to_new_quarter_floor_band() { + // Nearer 1,024 than 0 (dist 224 vs 800). + assert_eq!(quantize_min_wl_m(800), 1_024); + // Nearer 1,024 than 4,096 (dist 476 vs 2,596). + assert_eq!(quantize_min_wl_m(1_500), 1_024); + } + /// A value far above the coarsest band snaps to the coarsest band, never /// panics or overflows — quantization must be a TOTAL function over all /// u32 input (never trust the wire). diff --git a/server/src/atlas/mod.rs b/server/src/atlas/mod.rs index 3725d3834..8734be590 100644 --- a/server/src/atlas/mod.rs +++ b/server/src/atlas/mod.rs @@ -39,6 +39,7 @@ pub mod trait_catalog_reader; pub mod trait_draw; pub mod trait_exterior; pub mod trait_swerve; +pub mod vegetation_invention; pub mod voxel; pub use plugin::GenerationPlugin; diff --git a/server/src/atlas/vegetation_invention.rs b/server/src/atlas/vegetation_invention.rs new file mode 100644 index 000000000..4f9850cc9 --- /dev/null +++ b/server/src/atlas/vegetation_invention.rs @@ -0,0 +1,403 @@ +//! Vegetation-patchiness invention field (T-1162, D-227, nature-layer scope +//! per the ticket's SCOPE CLARIFICATION amendment). +//! +//! Jeroen's brief: "even the nature layer should produce rivers, forests, +//! copses, bushes, deserts, elevation" — vegetation must read as spatial +//! CONTENT (massifs, distinct woods, copses, clearings) rather than a uniform +//! per-cell class tint that only ever changes at a climate-driven district +//! boundary. This module is the vegetation twin of +//! [`crate::atlas::coast_invention`]'s two-tier character model, adapted to +//! the constraint that vegetation has no warp target of its own (there is no +//! "vegetation heightmap" to displace sampling against) — instead it +//! perturbs [`crate::atlas::district_profile::derive_moisture_q`]'s **output** +//! before that value reaches [`crate::atlas::district_profile::derive_vegetation`]. +//! +//! ## Design choice: perturb the moisture INPUT, not the class thresholds +//! +//! Two composition points were available: (a) bias `derive_vegetation`'s +//! internal treeline/moisture thresholds directly, or (b) perturb the +//! `moisture_q` value it (and its siblings) consume. **(b) was chosen.** +//! `moisture_q` already feeds THREE downstream classifiers in lockstep — +//! [`crate::atlas::district_profile::derive_precipitation_class_from_climate`], +//! [`crate::atlas::district_profile::derive_glaciation_grade_from_climate`], +//! and `derive_vegetation` itself. Perturbing the shared input means a patchy-dry +//! cell reads dry across precipitation AND vegetation AND (at the right +//! temperature) glaciation grade — one coherent world-fact, not a +//! vegetation-only illusion sitting on top of an unperturbed climate. Biasing +//! `derive_vegetation`'s thresholds alone would let a cell "look wetter" in +//! its tree cover while its precipitation class and glaciation gate stayed on +//! the unperturbed value — an internally inconsistent world-state exactly the +//! kind D-227/D-010 determinism discipline exists to prevent. +//! +//! ## Two-tier model (mirrors `coast_invention`) +//! +//! - **Tier 1 — [`VegetationEnvelope`] (body/region-scale, "where forests are +//! POSSIBLE"):** a region-scale, latitude-modulated ceiling on how far the +//! patchiness field is allowed to swing `moisture_q`. Derived from +//! [`crate::atlas::district_profile::BodyParams`] only (D-240 discipline, +//! same posture as `coast_invention::BodyCoastEnvelope`) plus latitude — a +//! hyper-arid world's patchiness ceiling is narrow (deserts stay deserts; +//! no patch of forest should ever invent itself on a bone-dry world), a +//! temperate wet world's ceiling is wide (real massif-to-massif variation). +//! - **Tier 2 — [`moisture_perturb_q`] (position, "which patches manifest"):** +//! a signed integer offset in `[-ceiling, +ceiling]`, built from a +//! MULTI-SCALE deterministic field (region/massif → district/wood → +//! quarter/copse octave bands, §"Cross-rung coherence" below) so a forest +//! massif at Region scale resolves to distinct woods at District and +//! copses/clearings at Quarter — never contradicting the coarser verdict, +//! only refining its boundary. +//! +//! ## Cross-rung coherence (the ticket's hard requirement) +//! +//! A forest at Region scale MUST still read as forest-majority when sampled +//! at Quarter — the finer rungs REFINE the boundary and punch clearings, they +//! never contradict the coarse verdict wholesale. This is achieved the SAME +//! way `coast_invention`'s warp achieves cross-rung coherence: a SINGLE fBm +//! sum whose octave terms span coarse-to-fine wavelengths, sampled at +//! whatever position/cutoff a rung asks for — not two independently-seeded +//! fields for "coarse" and "fine" that could disagree. The coarse octaves +//! (massif-scale, ≈100–400 km, always present regardless of cutoff — same +//! posture as `coast_invention::character_field`, which is NEVER +//! cutoff-gated) set the DC bias (which side of "possible forest" a broad +//! area sits on); the fine octaves (district/quarter-scale, cutoff-gated +//! exactly like `terrain_detail`/`voxel_relief`) add zero-mean texture on top +//! that punches clearings and copses WITHOUT being able to flip the coarse +//! bias's sign at typical amplitudes (the fine-octave contribution is capped +//! well inside the coarse term's own swing — see [`moisture_perturb_q`]'s +//! amplitude split). +//! +//! ## Wavelength discipline (per rung, identical to parts 1–2 of T-1162) +//! +//! The fine tier reuses [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] +//! (district band, 32,768–4,096 m) and +//! [`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`] (quarter/sub-district +//! band, 1,024–128 m) — the SAME two bands the coast warp and relief +//! extension already truncate against, via the identical `min_wavelength_m` +//! hard-truncate cutoff convention (`enveloped_fbm`'s discipline). At +//! District's cutoff (≥2,048 m) only the district band's terms survive; at +//! Quarter's cutoff (1,024 m) the two coarsest voxel-band terms additionally +//! survive — exactly parts (a)/(b)'s discipline, reused rather than +//! reinvented for vegetation. +//! +//! ## Determinism & isolation (D-227/D-010) +//! +//! Pure functions of `(seed, body, position)`. Distinct salted hash streams +//! ([`VEGETATION_MASSIF_SALT`], [`VEGETATION_TEXTURE_SALT`]) — never +//! correlated with the coast warp, the terrain/voxel relief scatter, or the +//! climate edge-fuzz warp, the same isolation convention every invention +//! field in this cascade follows. +//! +//! ## Wire format (unchanged) +//! +//! This module produces an `i32` moisture offset consumed BEFORE +//! `DistrictProfile` is built — `vegetation_class` remains the single +//! discriminant per cell on the wire (`WindowCell::vegetation: u8`, +//! `layer_proxy.rs`). The patchiness lives in WHICH class a cell resolves to, +//! never in additional sub-cell wire data. + +use crate::atlas::detail_scatter::{value_noise, OCTAVE_WAVELENGTHS_M, VOXEL_OCTAVE_WAVELENGTHS_M}; +use crate::atlas::district_profile::BodyParams; +use crate::seed::splitmix64; + +/// Distinct hash-path salt for the massif (tier-1 coarse DC bias) stream. +const VEGETATION_MASSIF_SALT: u64 = 0x7EA5_04E5_71E1_D044; + +/// Distinct hash-path salt for the texture (tier-2 fine detail) stream. +const VEGETATION_TEXTURE_SALT: u64 = 0x7EA5_04E5_7EC7_DE7A; + +/// Massif-scale octave wavelengths in metres (≈100–400 km) — the SAME scale +/// family as `coast_invention::CHARACTER_OCTAVE_WAVELENGTHS_M`, reused for the +/// same reason: this is the scale at which a body's climate personality +/// drifts from stretch to stretch. Never cutoff-gated (always summed in +/// full) — this is the coarse DC bias that guarantees cross-rung coherence +/// (§ module docs); gating it would let District and Quarter disagree on +/// which side of "possible forest" a broad area sits. +const MASSIF_OCTAVE_WAVELENGTHS_M: [f64; 3] = [409_600.0, 204_800.0, 102_400.0]; + +/// The tier-1 body/region-scale envelope — the ceiling on how far the +/// patchiness field is allowed to swing `moisture_q` (T-1162). +/// +/// Mirrors [`crate::atlas::coast_invention::BodyCoastEnvelope`]'s posture: +/// derived from [`BodyParams`] only (D-240), one per body. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct VegetationEnvelope { + /// Maximum |offset| the patchiness field may apply to `moisture_q` + /// (points, 0–100 scale). Hyper-arid/frozen worlds get a narrow ceiling + /// (deserts stay deserts — no invented forest patch on a bone-dry world); + /// temperate/wet worlds get a wide ceiling (real massif-scale variation). + pub ceiling_q: i32, +} + +/// Derive the tier-1 envelope from body params. +/// +/// The `hydrosphere`/`atmosphere` buckets mirror +/// [`crate::atlas::district_profile::derive_moisture_q`]'s own vocabulary +/// grouping (same systems.db vocabulary; kept in sync deliberately — a body +/// with a narrow moisture *ceiling* should also have a narrow patchiness +/// *swing*, so both derivations read the same underlying wetness signal). +pub fn vegetation_envelope(params: &BodyParams) -> VegetationEnvelope { + let hydro_wetness: i32 = match params.hydrosphere.as_deref().unwrap_or("none") { + "liquid_water" | "ocean" | "ocean-coastal" | "extensive" => 100, + "rivers" | "rivers-lakes" | "moderate" => 70, + "ice" => 35, + "subsurface_liquid" => 25, + "subsurface" | "subsurface_ice" => 15, + "minimal" | "trace" => 8, + "none" => 0, + _ => 40, + }; + let atmo_density: i32 = match params.atmosphere.as_deref().unwrap_or("none") { + "dense" => 100, + "standard" | "breathable" => 85, + "toxic" => 70, + "thin" => 35, + "none" => 0, + _ => 50, + }; + // A world needs both water AND atmosphere to grow patchy vegetation at + // all — same multiplicative posture as coast_invention's erosion chain. + // Ceiling caps at 35 points (a bit over 1/3 of the moisture_q scale) so + // the fine-octave contribution can never overwhelm the base climate + // signal outright (D-239 §8's climate→vegetation law still governs). + let wetness_product = (hydro_wetness * atmo_density) / 100; + let ceiling_q = (wetness_product * 35 / 100).clamp(0, 35); + VegetationEnvelope { ceiling_q } +} + +/// The deterministic massif-scale field in `[-1, 1]` — the coarse DC bias +/// that decides which side of "possible forest/desert" a broad area sits on. +/// Never cutoff-gated (see module docs' cross-rung-coherence section). +fn massif_field(seed: u64, wx: f64, wy: f64) -> f64 { + let seed = splitmix64(seed ^ VEGETATION_MASSIF_SALT); + let mut sum = 0.0; + let mut amp = 1.0; + let mut norm = 0.0; + for (i, &wl) in MASSIF_OCTAVE_WAVELENGTHS_M.iter().enumerate() { + sum += value_noise( + seed.wrapping_add((i as u64).wrapping_mul(0x1000)), + wx, + wy, + wl, + ) * amp; + norm += amp; + amp *= 0.5; + } + sum / norm +} + +/// The cutoff-gated fine-texture field in `[-1, 1]` — district-band +/// ([`OCTAVE_WAVELENGTHS_M`]) octaves that punch woods at District spacing, +/// PLUS the sub-district ([`VOXEL_OCTAVE_WAVELENGTHS_M`]) octaves that punch +/// copses/clearings once a request's cutoff admits them (Quarter and finer). +/// Same hard-truncate discipline as `detail_scatter::enveloped_fbm` and the +/// T-1162 coast-warp extension: `wl < min_wavelength_m` skips the term +/// entirely, `amp` still advances so surviving terms keep relative weight. +fn texture_field(seed: u64, wx: f64, wy: f64, min_wavelength_m: f64) -> f64 { + let seed = splitmix64(seed ^ VEGETATION_TEXTURE_SALT); + let mut sum = 0.0; + let mut amp = 1.0; + let mut norm = 0.0; + let mut idx: u64 = 0; + for &wl in OCTAVE_WAVELENGTHS_M.iter().chain(VOXEL_OCTAVE_WAVELENGTHS_M.iter()) { + if wl < min_wavelength_m { + amp *= 0.5; + idx += 1; + continue; + } + sum += value_noise(seed.wrapping_add(idx.wrapping_mul(0x1000)), wx, wy, wl) * amp; + norm += amp; + amp *= 0.5; + idx += 1; + } + if norm == 0.0 { + return 0.0; + } + sum / norm +} + +/// The deterministic `moisture_q` perturbation (T-1162) — a signed offset in +/// `[-ceiling_q, +ceiling_q]` to add to `moisture_q` BEFORE it reaches +/// `derive_vegetation`/`derive_precipitation_class_from_climate`/ +/// `derive_glaciation_grade_from_climate` (see module docs' design-choice +/// rationale). +/// +/// Composition: the massif field (never cutoff-gated) supplies 70% of the +/// swing — the coarse DC bias that guarantees a Region-scale forest verdict +/// survives refinement — and the cutoff-gated texture field supplies the +/// remaining 30%, which is what punches woods/copses/clearings at +/// District/Quarter without being able to flip the massif's sign at typical +/// amplitudes (0.3 < 0.7, so the texture term alone can never outweigh the +/// massif term). `min_wavelength_m` — `0.0` = no cutoff on the fine tier +/// (full detail; the coarse tier is unaffected either way). +pub fn moisture_perturb_q( + env: &VegetationEnvelope, + seed: u64, + wx: f64, + wy: f64, + min_wavelength_m: f64, +) -> i32 { + if env.ceiling_q == 0 { + return 0; // envelope rule: a world with no patchiness ceiling invents nothing + } + let massif = massif_field(seed, wx, wy); // [-1, 1], never gated + let texture = texture_field(seed, wx, wy, min_wavelength_m); // [-1, 1], gated + let blended = (0.70 * massif + 0.30 * texture).clamp(-1.0, 1.0); + ((blended * env.ceiling_q as f64).round() as i32).clamp(-env.ceiling_q, env.ceiling_q) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn params(hydro: &str, atmo: &str) -> BodyParams { + BodyParams { + hydrosphere: Some(hydro.to_string()), + atmosphere: Some(atmo.to_string()), + planet_class: Some("temperate".to_string()), + tectonic_activity: None, + latitude_deg: 0.0, + elevation_km: 0.0, + body_radius_km: Some(6000.0), + } + } + + #[test] + fn envelope_wet_body_has_wider_ceiling_than_dry() { + let wet = vegetation_envelope(¶ms("ocean", "breathable")); + let dry = vegetation_envelope(¶ms("minimal", "thin")); + assert!( + wet.ceiling_q > dry.ceiling_q, + "wet+breathable body must have a wider patchiness ceiling than dry+thin" + ); + } + + #[test] + fn envelope_airless_or_dry_body_has_zero_ceiling() { + let none = vegetation_envelope(¶ms("none", "none")); + assert_eq!( + none.ceiling_q, 0, + "a body with neither water nor atmosphere must have zero patchiness ceiling" + ); + } + + #[test] + fn perturb_is_deterministic() { + let env = vegetation_envelope(¶ms("ocean", "breathable")); + let a = moisture_perturb_q(&env, 42, 1_000_000.0, 2_000_000.0, 0.0); + let b = moisture_perturb_q(&env, 42, 1_000_000.0, 2_000_000.0, 0.0); + assert_eq!(a, b); + } + + #[test] + fn perturb_bounded_by_ceiling() { + let env = vegetation_envelope(¶ms("ocean", "breathable")); + for i in 0..400 { + let wx = i as f64 * 91_137.0; + let wy = i as f64 * -53_211.0; + let p = moisture_perturb_q(&env, 99, wx, wy, 0.0); + assert!( + p.abs() <= env.ceiling_q, + "perturb {p} exceeded ceiling {}", + env.ceiling_q + ); + } + } + + #[test] + fn perturb_zero_ceiling_is_always_zero() { + let env = VegetationEnvelope { ceiling_q: 0 }; + for i in 0..50 { + let p = moisture_perturb_q(&env, 7, i as f64 * 1234.0, i as f64 * -987.0, 0.0); + assert_eq!(p, 0); + } + } + + #[test] + fn perturb_varies_at_massif_scale() { + // Positions ~600 km apart must show real massif-scale variation — + // otherwise the whole body would read as one uniform patchiness value. + let env = vegetation_envelope(¶ms("ocean", "breathable")); + let vals: Vec = (0..8) + .map(|i| moisture_perturb_q(&env, 11, i as f64 * 600_000.0, 300_000.0, 0.0)) + .collect(); + let min = *vals.iter().min().unwrap(); + let max = *vals.iter().max().unwrap(); + assert!( + max - min > 2, + "massif-scale variation too flat across the body: range {}", + max - min + ); + } + + #[test] + fn perturb_stream_uncorrelated_with_coast_and_scatter_streams() { + let env = vegetation_envelope(¶ms("ocean", "breathable")); + let p = moisture_perturb_q(&env, 42, 1e6, 1e6, 0.0); + let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0); + // Different units/ranges so a raw equality is not the real assertion — + // what matters is the two are NOT trivially the same salted-noise call. + // Compare fields directly by re-deriving each with the OTHER's salt to + // confirm distinct hash streams are actually used (isolation, not luck). + let massif_direct = massif_field(splitmix64(42 ^ VEGETATION_MASSIF_SALT), 1e6, 1e6); + assert_ne!(massif_direct, scatter, "sanity: distinct value spaces"); + let _ = p; + } + + // ── min_wavelength_m cutoff discipline (T-1162, mirrors parts a/b) ──────── + + #[test] + fn cutoff_zero_matches_full_texture_field() { + let env = vegetation_envelope(¶ms("ocean", "breathable")); + for i in 0..50 { + let wx = i as f64 * 7_331.0; + let wy = i as f64 * -4_177.0; + let with_zero = moisture_perturb_q(&env, 21, wx, wy, 0.0); + // The finest combined band entry is VOXEL_OCTAVE_WAVELENGTHS_M's + // 128.0 — a cutoff below that admits every octave too. + let with_below_finest = moisture_perturb_q(&env, 21, wx, wy, 1.0); + assert_eq!(with_zero, with_below_finest); + } + } + + #[test] + fn district_cutoff_admits_fewer_octaves_than_quarter_cutoff() { + // District's REAL quantized MIN_WL_BANDS_M floor is 4,096 m + // (District's own Nyquist limit — OCTAVE_WAVELENGTHS_M's pre-existing + // finest entry). At that cutoff EVERY VOXEL_OCTAVE_WAVELENGTHS_M + // entry (all ≤1,024 m) is excluded, and nothing in OCTAVE_WAVELENGTHS_M + // survives below its own finest (4,096) either way. Quarter's cutoff + // (1,024 m) additionally admits the two coarsest voxel-band entries + // (1,024, 512). The texture field must therefore differ between the + // two cutoffs at the same position — otherwise Quarter's finer + // classification would be inert. + let seed_tex = splitmix64(21 ^ VEGETATION_TEXTURE_SALT); + let district_tex = texture_field(seed_tex, 3_500_000.0, 1_200_000.0, 4_096.0); + let quarter_tex = texture_field(seed_tex, 3_500_000.0, 1_200_000.0, 1_024.0); + assert_ne!( + district_tex, quarter_tex, + "Quarter's finer cutoff must admit voxel-band octaves District's cutoff excludes" + ); + } + + #[test] + fn cutoff_truncates_every_octave_above_coarsest_band() { + // A cutoff above the coarsest combined-band entry (32,768.0, from + // OCTAVE_WAVELENGTHS_M) must skip every texture-field term and fall + // back to 0.0 — the massif field is unaffected (never gated), so the + // perturb is exactly 70% of the massif term alone. + let seed_tex = splitmix64(5 ^ VEGETATION_TEXTURE_SALT); + let tex = texture_field(seed_tex, 1_000.0, 2_000.0, 1_000_000.0); + assert_eq!(tex, 0.0); + } + + #[test] + fn massif_field_never_gated_by_cutoff() { + // The coarse DC bias must be IDENTICAL regardless of min_wavelength_m + // — this is what cross-rung coherence rests on. + let a = massif_field(42, 5_000_000.0, 2_000_000.0); + let b = massif_field(42, 5_000_000.0, 2_000_000.0); + assert_eq!(a, b); + // massif_field takes no cutoff parameter at all — this test documents + // that fact structurally (the function signature itself enforces it). + } +} diff --git a/server/tests/golden/window_derivation_golden.json b/server/tests/golden/window_derivation_golden.json new file mode 100644 index 000000000..be79ce965 --- /dev/null +++ b/server/tests/golden/window_derivation_golden.json @@ -0,0 +1,257 @@ +[ + { + "label": "coastal_a", + "rung": "district", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 3, + "elev_q": 27, + "ocean_fraction_q": 0, + "temperature_dc": -48, + "moisture_q": 59, + "vegetation": 2 + }, + { + "label": "coastal_a", + "rung": "quarter", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 2, + "precipitation": 2, + "slope_q": 4, + "elev_q": 32, + "ocean_fraction_q": 0, + "temperature_dc": -74, + "moisture_q": 59, + "vegetation": 2 + }, + { + "label": "coastal_a", + "rung": "region", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 0, + "morphology": 16, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 0, + "elev_q": 26, + "ocean_fraction_q": 0, + "temperature_dc": -43, + "moisture_q": 60, + "vegetation": 2 + }, + { + "label": "coastal_b", + "rung": "district", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 2, + "precipitation": 1, + "slope_q": 3, + "elev_q": 30, + "ocean_fraction_q": 0, + "temperature_dc": -68, + "moisture_q": 55, + "vegetation": 2 + }, + { + "label": "coastal_b", + "rung": "quarter", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 4, + "elev_q": 26, + "ocean_fraction_q": 0, + "temperature_dc": -47, + "moisture_q": 56, + "vegetation": 2 + }, + { + "label": "coastal_b", + "rung": "region", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 0, + "elev_q": 26, + "ocean_fraction_q": 0, + "temperature_dc": -47, + "moisture_q": 57, + "vegetation": 2 + }, + { + "label": "coastal_c", + "rung": "district", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 3, + "elev_q": 24, + "ocean_fraction_q": 0, + "temperature_dc": -30, + "moisture_q": 59, + "vegetation": 2 + }, + { + "label": "coastal_c", + "rung": "quarter", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 2, + "precipitation": 2, + "slope_q": 4, + "elev_q": 28, + "ocean_fraction_q": 0, + "temperature_dc": -51, + "moisture_q": 58, + "vegetation": 2 + }, + { + "label": "coastal_c", + "rung": "region", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 0, + "elev_q": 27, + "ocean_fraction_q": 0, + "temperature_dc": -45, + "moisture_q": 58, + "vegetation": 2 + }, + { + "label": "inland", + "rung": "district", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 4, + "elev_q": 25, + "ocean_fraction_q": 0, + "temperature_dc": 0, + "moisture_q": 52, + "vegetation": 3 + }, + { + "label": "inland", + "rung": "quarter", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 5, + "elev_q": 20, + "ocean_fraction_q": 0, + "temperature_dc": 26, + "moisture_q": 52, + "vegetation": 3 + }, + { + "label": "inland", + "rung": "region", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 1, + "precipitation": 2, + "slope_q": 0, + "elev_q": 22, + "ocean_fraction_q": 0, + "temperature_dc": 16, + "moisture_q": 52, + "vegetation": 3 + }, + { + "label": "high_lat", + "rung": "district", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 2, + "precipitation": 1, + "slope_q": 3, + "elev_q": 63, + "ocean_fraction_q": 0, + "temperature_dc": -69, + "moisture_q": 43, + "vegetation": 1 + }, + { + "label": "high_lat", + "rung": "quarter", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 2, + "precipitation": 1, + "slope_q": 4, + "elev_q": 61, + "ocean_fraction_q": 0, + "temperature_dc": -59, + "moisture_q": 43, + "vegetation": 1 + }, + { + "label": "high_lat", + "rung": "region", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 2, + "precipitation": 1, + "slope_q": 0, + "elev_q": 62, + "ocean_fraction_q": 0, + "temperature_dc": -64, + "moisture_q": 43, + "vegetation": 1 + } +] diff --git a/server/tests/window_derivation_golden.rs b/server/tests/window_derivation_golden.rs new file mode 100644 index 000000000..19fd38fbb --- /dev/null +++ b/server/tests/window_derivation_golden.rs @@ -0,0 +1,287 @@ +//! Window-derivation golden regression (T-1162). +//! +//! Pins `derive_at_metres` (District/Quarter rungs) and +//! `derive_orbital_at_metres` (Region rung) output at a fixed +//! (seed, body, coords) sweep, across all three rung cutoffs — the window +//! path `layer_proxy::derive_window_cell` actually calls in production. +//! Mirrors `tests/derivation_harness.rs`'s golden pattern exactly (same +//! regen convention, same double-derive determinism check, same JSON-value +//! comparison so formatting drift doesn't false-positive). +//! +//! **T-1162 regen rationale:** this golden is generated AFTER the T-1162 +//! octave-extension changes (extended coast-warp band, sub-district relief at +//! Quarter, vegetation patchiness) — it deliberately pins the NEW output. +//! There is no "pre-T-1162" golden to preserve: this file did not exist +//! before this ticket, so there is nothing to regress against except itself +//! from this point forward. Any future change to the coast warp, the voxel +//! relief band, the vegetation-patchiness field, or the MIN_WL_BANDS_M +//! quantization will change this golden's values — regenerate deliberately +//! (per the asset-pipeline discipline: source changes, not hand-edits). +//! +//! Run: `cargo test --test window_derivation_golden` +//! Regenerate: `UPDATE_GOLDEN=1 cargo test --test window_derivation_golden` + +use std::path::PathBuf; + +use settled_reach_server::atlas::district_profile::{ + derive_at_metres, derive_orbital_at_metres, BodyParams, ClimateConstants, +}; +use settled_reach_server::atlas::drainage; +use settled_reach_server::atlas::features::TerrainAnalysis; +use settled_reach_server::atlas::heightmap::BodyHeightmap; +use settled_reach_server::atlas::scale; +use settled_reach_server::seed::{SeedChain, SeedDomain}; + +const GOLDEN_FILE: &str = "tests/golden/window_derivation_golden.json"; + +/// Compact representation of a `DistrictProfile` sample for golden pinning. +/// Integer-discriminant fields only (D-010) — no float equality flakiness. +#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)] +struct GoldenSample { + label: String, + rung: String, + wx_m: i64, + wy_m: i64, + min_wl_m: i64, + morphology: u8, + tectonic: u8, + glaciation: u8, + precipitation: u8, + slope_q: i32, + elev_q: i32, + ocean_fraction_q: i32, + temperature_dc: i32, // deci-°C, i32::MIN sentinel for None (airless) + moisture_q: i32, + vegetation: u8, +} + +fn sample_hm() -> BodyHeightmap { + // Deterministic gradient with enough variance for coast/relief/vegetation + // content to actually differ across the sweep positions — same shape + // convention as district_profile.rs's own test_hm / zoom_ladder_bench's + // bench_hm, sized a bit larger so the sweep coordinates land on distinct + // heightmap cells rather than a single interpolated patch. + let (w, h) = (128u32, 64u32); + let n = (w * h) as usize; + let data = (0..n) + .map(|i| { + let r = (i / w as usize) as f32 / h as f32; + let c = (i % w as usize) as f32 / w as f32; + // A gentle sine ripple on top of the linear gradient gives the + // coastline invention real slope/ocean-mask variance to warp. + let ripple = (c * std::f32::consts::TAU * 3.0).sin() * 0.08; + (r * 0.55 + c * 0.35 + ripple + 0.05).clamp(0.0, 1.0) + }) + .collect(); + BodyHeightmap { + body_id: "golden_body".into(), + width: w, + height: h, + data, + sea_level: 0.32, + } +} + +fn sample_ta(hm: &BodyHeightmap) -> TerrainAnalysis { + let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); + TerrainAnalysis::analyze(hm, &dr) +} + +fn sample_params() -> BodyParams { + BodyParams { + hydrosphere: Some("ocean".into()), + atmosphere: Some("breathable".into()), + planet_class: Some("temperate".into()), + body_radius_km: Some(6371.0), + ..Default::default() + } +} + +/// Fixed sweep positions (world metres from origin) — a handful of points +/// spanning a coastal stretch (per the heightmap's ripple) plus a couple of +/// clearly inland/high-latitude points, so the golden exercises coast warp, +/// sub-district relief, and vegetation patchiness all at once. +fn sweep_positions() -> Vec<(&'static str, f64, f64)> { + vec![ + ("coastal_a", 2_000_000.0, 1_500_000.0), + ("coastal_b", 2_050_000.0, 1_500_000.0), + ("coastal_c", 2_100_000.0, 1_560_000.0), + ("inland", 500_000.0, 3_000_000.0), + ("high_lat", 1_200_000.0, 8_500_000.0), + ] +} + +fn derive_golden_sample( + label: &str, + rung: &str, + seed: SeedChain, + body_id: &str, + params: &BodyParams, + ta: &TerrainAnalysis, + climate: &ClimateConstants, + wx: f64, + wy: f64, + min_wl_m: f64, + orbital: bool, +) -> GoldenSample { + let prof = if orbital { + derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate) + } else { + derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wl_m) + }; + GoldenSample { + label: label.to_string(), + rung: rung.to_string(), + wx_m: wx as i64, + wy_m: wy as i64, + min_wl_m: min_wl_m as i64, + morphology: prof.morphology_zone as u8, + tectonic: prof.tectonic_class as u8, + glaciation: prof.glaciation_grade as u8, + precipitation: prof.precipitation_class as u8, + slope_q: prof.slope_q, + elev_q: prof.elev_q, + ocean_fraction_q: prof.ocean_fraction_q, + temperature_dc: prof + .temperature_c + .map(|t| (t * 10.0).round() as i32) + .unwrap_or(i32::MIN), + moisture_q: prof.moisture_q, + vegetation: prof.vegetation_class as u8, + } +} + +/// District's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s +/// pre-existing finest entry — District's own Nyquist floor, matching +/// `detail_scatter::OCTAVE_WAVELENGTHS_M`'s finest octave). NOT `2 * +/// DISTRICT_M` by construction coincidence alone — see the T-1162 discovery +/// documented on `MIN_WL_BANDS_M` and on `district_floor_cutoff_is_stable_and_deterministic` +/// in `district_profile.rs`: a District-rung request in production quantizes +/// to exactly this value, so the golden pins the SAME cutoff a real window +/// request would actually carry. +const DISTRICT_MIN_WL_M: f64 = 4_096.0; + +/// Quarter's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s +/// new T-1162 entry — Quarter's own Nyquist floor, `2 * QUARTER_M`). +const QUARTER_MIN_WL_M: f64 = 1_024.0; + +/// Build the full fixed sweep: every position × the three rung cutoffs +/// (District / Quarter use their REAL production `MIN_WL_BANDS_M` values / +/// Region via `derive_orbital_at_metres`, which takes no cutoff parameter — +/// see its own doc on why). +fn golden_samples() -> Vec { + let hm = sample_hm(); + let ta = sample_ta(&hm); + let params = sample_params(); + let climate = ClimateConstants::default(); + let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7); + let body_id = "golden_body"; + + let mut out = Vec::new(); + for (label, wx, wy) in sweep_positions() { + out.push(derive_golden_sample( + label, "district", seed, body_id, ¶ms, &ta, &climate, wx, wy, + DISTRICT_MIN_WL_M, false, + )); + out.push(derive_golden_sample( + label, "quarter", seed, body_id, ¶ms, &ta, &climate, wx, wy, + QUARTER_MIN_WL_M, false, + )); + out.push(derive_golden_sample( + label, "region", seed, body_id, ¶ms, &ta, &climate, wx, wy, 0.0, true, + )); + } + out +} + +#[test] +fn window_derivation_golden_regression() { + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let golden_path = manifest.join(GOLDEN_FILE); + + // Double-derive determinism check (D-010) before ever touching the golden. + let run1 = golden_samples(); + let run2 = golden_samples(); + assert_eq!( + run1, run2, + "double-derivation mismatch — determinism is broken (D-010)" + ); + + let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n"; + + if std::env::var("UPDATE_GOLDEN").is_ok() { + std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden"); + std::fs::write(&golden_path, &actual_json).expect("write golden"); + eprintln!( + "Golden written: {} ({} bytes)", + golden_path.display(), + actual_json.len() + ); + return; + } + + let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| { + panic!( + "Golden file not found: {}.\n\ + First run: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n{e}", + golden_path.display() + ) + }); + + let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual"); + let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden"); + + if actual_v != golden_v { + panic!( + "Window-derivation golden mismatch — derivation chain changed.\n\ + Update: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n\ + Golden: {}\nActual: {}", + golden_json.trim(), + actual_json.trim() + ); + } +} + +/// Cross-rung coherence sanity check on the golden's own fixed sweep: for +/// each coastal label, the Quarter-rung sample must differ from the +/// District-rung sample at the SAME position (the whole point of the +/// extension — Quarter sees finer content District's coarser cutoff +/// truncates). This is a structural companion to the golden file itself, +/// not a replacement for it — it fails loudly if the golden ever gets +/// regenerated with `min_wl_m` accidentally identical across rungs. +#[test] +fn quarter_and_district_rungs_diverge_at_the_same_position() { + let samples = golden_samples(); + let mut any_diverged = false; + for (label, _, _) in sweep_positions() { + let district = samples + .iter() + .find(|s| s.label == label && s.rung == "district") + .unwrap(); + let quarter = samples + .iter() + .find(|s| s.label == label && s.rung == "quarter") + .unwrap(); + if district.elev_q != quarter.elev_q + || district.slope_q != quarter.slope_q + || district.moisture_q != quarter.moisture_q + { + any_diverged = true; + } + } + assert!( + any_diverged, + "no sweep position showed ANY difference between District and Quarter \ + rungs — the T-1162 octave extension would be structurally inert" + ); +} + +/// `scale::DISTRICT_M` / `scale::QUARTER_M` sanity — documents WHY 2,048/1,024 +/// are the cutoffs used above (District's own spacing; Quarter's own Nyquist +/// floor, `2 × QUARTER_M`), so a future scale-ladder change surfaces here. +#[test] +fn golden_cutoffs_match_the_scale_ladder() { + assert_eq!(scale::DISTRICT_M, 2_048); + assert_eq!(scale::QUARTER_M, 512); + assert_eq!(2 * scale::QUARTER_M, 1_024); +} -- 2.54.0 From d33256a8bad36007451b4396a8a9b20d4de1b160 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 22:56:48 +0200 Subject: [PATCH 3/8] =?UTF-8?q?test(simulation):=20regenerate=20believabil?= =?UTF-8?q?ity=20golden=20=E2=80=94=20deliberate=20T-1162=20output=20chang?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The octave extension changes derived window output by design (coast crinkle below 16384m, voxel relief at Quarter, vegetation moisture perturbation) — this regen records the sanctioned new baseline per the ticket's part (d) discipline. The pre-existing BELIEVABILITY_STRICT=1 'vegetation present' advisory failure predates this change (Q-123- tracked debt) and is unaffected. Tickets: T-1162 Co-Authored-By: Claude Fable 5 --- server/tests/golden/believability.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index 0e2f02e82..b76329edc 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -6,9 +6,9 @@ "voxel_sampled_districts": 64, "contrast": { "moisture_q": { - "min": 31, - "max": 80, - "distinct": 48 + "min": 16, + "max": 97, + "distinct": 74 }, "elev_q": { "min": 0, @@ -17,18 +17,18 @@ }, "slope_q": { "min": 0, - "max": 30, - "distinct": 30 + "max": 31, + "distinct": 31 }, "ocean_fraction_q": { "min": 0, "max": 100, - "distinct": 44 + "distinct": 42 }, "morphology_zones": 9, "vegetation_classes": 4, "terrain_materials": 4, - "voxel_relief_m": 24, + "voxel_relief_m": 27, "micro_habitat_distinct": 2 }, "coherence": { @@ -37,7 +37,7 @@ "drainage_samples": 0, "drainage_monotonic": 0, "vegetation_samples": 64, - "vegetated_districts": 7 + "vegetated_districts": 5 } }, { @@ -48,8 +48,8 @@ "contrast": { "moisture_q": { "min": 0, - "max": 20, - "distinct": 20 + "max": 24, + "distinct": 25 }, "elev_q": { "min": 0, @@ -59,12 +59,12 @@ "slope_q": { "min": 0, "max": 15, - "distinct": 14 + "distinct": 15 }, "ocean_fraction_q": { "min": 0, "max": 100, - "distinct": 38 + "distinct": 36 }, "morphology_zones": 6, "vegetation_classes": 2, -- 2.54.0 From a5b9c28e740e3cfdb7fac69e7f4b49ff8811ee5c Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 23:09:48 +0200 Subject: [PATCH 4/8] =?UTF-8?q?style(simulation):=20cargo=20fmt=20?= =?UTF-8?q?=E2=80=94=20gate=20bounce=20on=20T-1162=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- server/src/atlas/district_profile.rs | 37 ++++++++++++++++++++---- server/src/atlas/vegetation_invention.rs | 5 +++- server/tests/window_derivation_golden.rs | 26 ++++++++++++++--- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index 516ecb3b4..d4c422c12 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -2071,7 +2071,9 @@ mod tests { let mut n = 0; for dx in 0..8 { let prof = derive_district(test_seed(), "test_body", &p, &ta, (dx, dy), &climate); - sum += prof.temperature_c.expect("breathable body must have a temperature"); + sum += prof + .temperature_c + .expect("breathable body must have a temperature"); n += 1; } sum / n as f32 @@ -2394,8 +2396,10 @@ mod tests { for i in 0..20 { let wx = (150 + i * 47) as f64 * dm; let wy = (150 + i * 31) as f64 * dm; - let district = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0); - let quarter = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0); + let district = + derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0); + let quarter = + derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0); if district.elev_q != quarter.elev_q || district.slope_q != quarter.slope_q || district.moisture_q != quarter.moisture_q @@ -2429,12 +2433,26 @@ mod tests { // Above the coastal warp's own coarsest octave (262,144 m) — every // octave in every band (coast, terrain, voxel relief) is excluded. let far_above = derive_at_metres( - test_seed(), "test_body", &p, &ta, wx, wy, &climate, 300_000.0, + test_seed(), + "test_body", + &p, + &ta, + wx, + wy, + &climate, + 300_000.0, ); // An even more extreme cutoff must produce the SAME result — once // every octave is truncated, going coarser still changes nothing. let even_further = derive_at_metres( - test_seed(), "test_body", &p, &ta, wx, wy, &climate, 10_000_000.0, + test_seed(), + "test_body", + &p, + &ta, + wx, + wy, + &climate, + 10_000_000.0, ); assert_district_profiles_eq(&far_above, &even_further); } @@ -2468,7 +2486,14 @@ mod tests { let base_wy = 15.0 * dm; let district_class = derive_at_metres( - test_seed(), "test_body", &p, &ta, base_wx, base_wy, &climate, 2_048.0, + test_seed(), + "test_body", + &p, + &ta, + base_wx, + base_wy, + &climate, + 2_048.0, ) .vegetation_class; diff --git a/server/src/atlas/vegetation_invention.rs b/server/src/atlas/vegetation_invention.rs index 4f9850cc9..b027bae3f 100644 --- a/server/src/atlas/vegetation_invention.rs +++ b/server/src/atlas/vegetation_invention.rs @@ -198,7 +198,10 @@ fn texture_field(seed: u64, wx: f64, wy: f64, min_wavelength_m: f64) -> f64 { let mut amp = 1.0; let mut norm = 0.0; let mut idx: u64 = 0; - for &wl in OCTAVE_WAVELENGTHS_M.iter().chain(VOXEL_OCTAVE_WAVELENGTHS_M.iter()) { + for &wl in OCTAVE_WAVELENGTHS_M + .iter() + .chain(VOXEL_OCTAVE_WAVELENGTHS_M.iter()) + { if wl < min_wavelength_m { amp *= 0.5; idx += 1; diff --git a/server/tests/window_derivation_golden.rs b/server/tests/window_derivation_golden.rs index 19fd38fbb..c66672dde 100644 --- a/server/tests/window_derivation_golden.rs +++ b/server/tests/window_derivation_golden.rs @@ -180,12 +180,30 @@ fn golden_samples() -> Vec { let mut out = Vec::new(); for (label, wx, wy) in sweep_positions() { out.push(derive_golden_sample( - label, "district", seed, body_id, ¶ms, &ta, &climate, wx, wy, - DISTRICT_MIN_WL_M, false, + label, + "district", + seed, + body_id, + ¶ms, + &ta, + &climate, + wx, + wy, + DISTRICT_MIN_WL_M, + false, )); out.push(derive_golden_sample( - label, "quarter", seed, body_id, ¶ms, &ta, &climate, wx, wy, - QUARTER_MIN_WL_M, false, + label, + "quarter", + seed, + body_id, + ¶ms, + &ta, + &climate, + wx, + wy, + QUARTER_MIN_WL_M, + false, )); out.push(derive_golden_sample( label, "region", seed, body_id, ¶ms, &ta, &climate, wx, wy, 0.0, true, -- 2.54.0 From 3368891976a438a54db0aa329f36f3625f3a270e Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 23:31:14 +0200 Subject: [PATCH 5/8] =?UTF-8?q?fix(simulation):=20PR=20#194=20review=20rou?= =?UTF-8?q?nd=20=E2=80=94=20rung-floor=20dependency=20direction,=20golden?= =?UTF-8?q?=20body=20coverage,=20doc=20honesty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tyre's I1 (his ruling: the cutoff's job is Nyquist, a property of the RUNG): MIN_WL_BANDS_M's District band now derives from 2*DISTRICT_M directly, decoupled from OCTAVE_WAVELENGTHS_M[3] — a detail_scatter retune can no longer silently redefine the Atlas rung floor. Direction- agnostic const assert pins the coincidence so drift on either side breaks the build for a deliberate human decision; golden_cutoffs_match_ the_scale_ladder now pins the District coupling too. I2: D-226 paraphrase replaced with cites to the T-1150 wire-contract note + T-1162 refinement resolution (2), plus a self-found stale 2048m claim fixed in the same comments. I4: golden extended with airless/dry (ceiling_q==0 short-circuit) and volcanic-coast (ridged warp) body rows via body_sweep_samples() — fixture regen verified 510 insertions, 0 deletions (existing rows byte-identical, purely appended). Q2 nit: vegetation_invention module doc now states majority-preservation as the coherence guarantee, not per-cell class stability (a boundary cell's flip IS the clearing mechanism). Forward-contract for T-1156: 0x5EED_C0DE promoted to named WINDOW_RELIEF_SALT const. 158 focused tests green; cargo check --tests clean. Tickets: T-1162, T-1161 Co-Authored-By: Claude Fable 5 --- server/src/atlas/coast_invention.rs | 15 +- server/src/atlas/district_profile.rs | 43 +- server/src/atlas/layer_proxy.rs | 51 +- server/src/atlas/vegetation_invention.rs | 45 +- .../golden/window_derivation_golden.json | 510 ++++++++++++++++++ server/tests/window_derivation_golden.rs | 163 +++++- 6 files changed, 751 insertions(+), 76 deletions(-) diff --git a/server/src/atlas/coast_invention.rs b/server/src/atlas/coast_invention.rs index 962903e3e..b83c46d6c 100644 --- a/server/src/atlas/coast_invention.rs +++ b/server/src/atlas/coast_invention.rs @@ -64,13 +64,14 @@ const CHARACTER_FIELD_SALT: u64 = 0x0C0A_57C4_A24C_7E12; /// density, not detail). A Quarter-rung request (`min_wavelength_m` cutoff /// quantized to 1,024 m, see `layer_proxy::MIN_WL_BANDS_M`) admits every /// octave down to this floor; a District-rung request (cutoff quantized to -/// District's OWN real band, 4,096 m — District's Nyquist limit, matching -/// `terrain_detail`'s pre-existing finest octave) truncates the two finest -/// additions (2,048/1,024 m — genuinely below District's floor) while -/// legitimately ADMITTING the other two (8,192/4,096 m — new content that IS -/// coarse enough for District to resolve, not a leak). Either way Quarter -/// still sees strictly more than District at the SAME position (the two -/// finest additions are Quarter-exclusive). The +/// District's OWN real band, `2 × DISTRICT_M = 4,096` m — District's own +/// Nyquist limit, derived independently from the rung's spacing per PR #194 +/// I1, NOT from any invention field's octave array) truncates the two +/// finest additions (2,048/1,024 m — genuinely below District's floor) +/// while legitimately ADMITTING the other two (8,192/4,096 m — new content +/// that IS coarse enough for District to resolve, not a leak). Either way +/// Quarter still sees strictly more than District at the SAME position (the +/// two finest additions are Quarter-exclusive). The /// `enveloped_fbm`-style hard-truncate cutoff discipline is honored via /// [`warp_fbm`]'s own `min_wavelength_m` parameter — `0.0` (no cutoff) is /// byte-identical to every pre-T-1162 caller. diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index d4c422c12..a73f7bcc2 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1058,6 +1058,16 @@ pub fn derive_moisture_q( // T-1125 — invented primitives (shared by both derivation paths) // --------------------------------------------------------------------------- +/// Distinct hash-path salt for the T-1162 sub-district relief stream (part +/// b) — keeps `voxel_relief`'s noise uncorrelated with the district-band +/// `terrain_detail` scatter sampled at the same position. Promoted to a +/// named const (Tyre, PR #194 forward-contract for T-1156) matching +/// `vegetation_invention::VEGETATION_MASSIF_SALT`/`VEGETATION_TEXTURE_SALT`'s +/// named-and-greppable convention, ahead of T-1156 adding a fourth +/// isolated stream (rivers) — a fourth inline hex literal here would have +/// made the pattern harder to audit at a glance. +const WINDOW_RELIEF_SALT: u64 = 0x5EED_C0DE; + /// The invented `(slope_q, elev_q, ocean_fraction_q)` triple (T-1125, D-227). struct InventedPrimitives { slope_q: i32, @@ -1165,22 +1175,25 @@ fn invent_primitives( // envelope/ruggedness inputs as the district-band `scatter` above (one // amplitude ceiling, two wavelength bands composing additively — never a // second independently-tuned amplitude rule) and the SAME - // `min_wavelength_m` cutoff. At District's cutoff (≥2,048 m) every - // VOXEL_OCTAVE_WAVELENGTHS_M entry (all ≤1,024 m) is truncated, so - // `relief` is always exactly 0.0 there — District's output is unchanged - // byte-for-byte. At Quarter's cutoff (1,024 m) only the two coarsest - // voxel-band entries (1,024, 512) survive; the two finest (256, 128) stay - // truncated even at Quarter (below Quarter's own 512 m spacing's Nyquist - // floor of 1,024 m) — this is correct and expected (D-226(d) note): a - // contributing wavelength is never capped BY the request-granularity - // floor ruling itself (the coast warp already crosses scales the other - // way), it is simply that only 2 of the 4 voxel octaves are coarse enough - // to matter at Quarter's own sample density; the other two are reserved - // for a future finer-than-Quarter rung. Distinct seed salt (`^ 0x5EED_C0DE`) - // keeps this stream uncorrelated with `scatter`'s stream at the same - // position (same isolation discipline as `coast_invention`'s warp salt). + // `min_wavelength_m` cutoff. At District's real Nyquist floor (4,096 m, + // `layer_proxy::MIN_WL_BANDS_M`) every VOXEL_OCTAVE_WAVELENGTHS_M entry + // (all ≤1,024 m) is truncated, so `relief` is always exactly 0.0 there — + // District's output is unchanged byte-for-byte. At Quarter's cutoff + // (1,024 m) only the two coarsest voxel-band entries (1,024, 512) + // survive; the two finest (256, 128) stay truncated even at Quarter + // (below Quarter's own 512 m spacing's Nyquist floor of 1,024 m) — this + // is correct and expected per the D-226 T-1150 wire-contract note and + // T-1162 refinement resolution (2): a contributing wavelength is never + // capped by the rung's sample-density floor alone (the coast warp + // already crosses scales the other way), it is simply that only 2 of + // the 4 voxel octaves are coarse enough to matter at Quarter's own + // sample density; the other two are reserved for a future + // finer-than-Quarter rung. Distinct seed salt + // ([`WINDOW_RELIEF_SALT`]) keeps this stream uncorrelated with + // `scatter`'s stream at the same position (same isolation discipline as + // `coast_invention`'s warp salt). let relief = crate::atlas::detail_scatter::voxel_relief( - seed.seed() ^ 0x5EED_C0DE, + seed.seed() ^ WINDOW_RELIEF_SALT, world_x_m, world_y_m, env_amp, diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index 78c979e82..5500e6013 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -405,17 +405,27 @@ fn clamp_window_n_v2(raw_n: u32, granularity: WindowGranularity) -> u32 { } /// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5; -/// retuned T-1162 part (c)): `0` (no cutoff) plus every entry of -/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] (32,768/16,384/ -/// 8,192/4,096 m — District's own Nyquist floor, `2 × DISTRICT_M`), plus a -/// FIFTH band at `1,024` m — Quarter's own Nyquist floor (`2 × QUARTER_M = -/// 1,024`, matching a rung's content wavelength to what its `512` m sample -/// spacing can actually resolve — nothing finer than the rung can express, -/// nothing coarser than it deserves). `1,024` m is also the finest entry of -/// the extended [`crate::atlas::coast_invention::WARP_OCTAVE_WAVELENGTHS_M`] -/// band (T-1162 part a) and the coarsest entry of -/// [`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`] the sub-district -/// relief wiring (T-1162 part b) admits at Quarter — one band serves both. +/// retuned T-1162 part (c); band 4 re-derived per Tyre's PR #194 review I1). +/// `0` (no cutoff) plus the two coarse legacy bands +/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`]`[0..2]` (32,768/ +/// 16,384/8,192 m — carried over unchanged, no rung claims these as its own +/// Nyquist floor), plus **District's own Nyquist floor** computed directly +/// from the rung's spacing (`2 × DISTRICT_M = 4,096` m), plus **Quarter's own +/// Nyquist floor** (`2 × QUARTER_M = 1,024` m). +/// +/// **Dependency direction (Tyre, PR #194 I1):** the cutoff's semantic job is +/// Nyquist truncation — a property of the RUNG (its sample spacing), never of +/// any one invention field's octave choices. Band 4 is therefore derived from +/// `scale::DISTRICT_M` directly, NOT from +/// `detail_scatter::OCTAVE_WAVELENGTHS_M[3]` (the old T-1162 wiring) — that +/// coupling ran backwards: a future `detail_scatter` retune could silently +/// redefine the Atlas rung floor, and the coast warp already extends its OWN +/// octaves below `OCTAVE_WAVELENGTHS_M`'s range (T-1162 part a), proving no +/// single invention field's array is entitled to dictate the floor. The two +/// values happen to coincide today (4,096 = `2 × DISTRICT_M` = also +/// `OCTAVE_WAVELENGTHS_M[3]`) — the `const _: () = assert!(...)` immediately +/// below pins that coincidence so a future drift on EITHER side breaks the +/// build loudly instead of silently decoupling the two meanings. /// /// **Pre-T-1162 bands unchanged in VALUE** (32,768/16,384/8,192/4,096 stay /// exactly where they were) — but **NOT unchanged in behavior at 8,192/4,096 @@ -442,10 +452,25 @@ const MIN_WL_BANDS_M: [f64; 6] = [ crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[0], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[1], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[2], - crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3], - 1_024.0, + 2.0 * DISTRICT_M as f64, + 2.0 * crate::atlas::scale::QUARTER_M as f64, ]; +/// Direction-agnostic guard (Tyre, PR #194 I1): `MIN_WL_BANDS_M`'s District +/// band is now derived from `2 × DISTRICT_M`, deliberately decoupled from +/// `detail_scatter::OCTAVE_WAVELENGTHS_M[3]` (see the doc above). The two +/// values are expected to keep coinciding — `terrain_detail`'s finest octave +/// IS meant to sit at District's Nyquist floor — but nothing in the type +/// system enforces that anymore now that the dependency runs one direction +/// only. This assert exists so that if either side ever drifts (a +/// `detail_scatter` octave retune, or a `DISTRICT_M` scale-ladder change). +/// the build breaks loudly and a human decides deliberately whether the +/// coincidence should be restored or the two meanings were meant to diverge +/// — never a silent redefinition of the Atlas rung floor. +const _: () = assert!( + crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3] as i64 == 2 * DISTRICT_M as i64 +); + /// Snap a wire-supplied `window_min_wl_m` to the nearest fixed band in /// [`MIN_WL_BANDS_M`] (T-1150, design doc §5's gap-fix): "as specified, /// `window_min_wl_m` is viewport-continuous while the cache key/echo tuple is diff --git a/server/src/atlas/vegetation_invention.rs b/server/src/atlas/vegetation_invention.rs index b027bae3f..cf1a06235 100644 --- a/server/src/atlas/vegetation_invention.rs +++ b/server/src/atlas/vegetation_invention.rs @@ -49,22 +49,35 @@ //! //! ## Cross-rung coherence (the ticket's hard requirement) //! -//! A forest at Region scale MUST still read as forest-majority when sampled -//! at Quarter — the finer rungs REFINE the boundary and punch clearings, they -//! never contradict the coarse verdict wholesale. This is achieved the SAME -//! way `coast_invention`'s warp achieves cross-rung coherence: a SINGLE fBm -//! sum whose octave terms span coarse-to-fine wavelengths, sampled at -//! whatever position/cutoff a rung asks for — not two independently-seeded -//! fields for "coarse" and "fine" that could disagree. The coarse octaves -//! (massif-scale, ≈100–400 km, always present regardless of cutoff — same -//! posture as `coast_invention::character_field`, which is NEVER -//! cutoff-gated) set the DC bias (which side of "possible forest" a broad -//! area sits on); the fine octaves (district/quarter-scale, cutoff-gated -//! exactly like `terrain_detail`/`voxel_relief`) add zero-mean texture on top -//! that punches clearings and copses WITHOUT being able to flip the coarse -//! bias's sign at typical amplitudes (the fine-octave contribution is capped -//! well inside the coarse term's own swing — see [`moisture_perturb_q`]'s -//! amplitude split). +//! A forest at Region scale MUST still read as forest-**majority** when +//! sampled at Quarter — the finer rungs REFINE the boundary and punch +//! clearings, they never contradict the coarse verdict wholesale. **This is +//! a majority-preservation guarantee, not a per-cell class-stability +//! guarantee** — punching an individual clearing or copse INTO a forest +//! massif is the fine tier doing its job, not a violation of coherence; the +//! two are compatible because the guarantee is about the aggregate reading +//! over a patch, not about any single cell's class surviving refinement +//! unchanged. This is achieved the SAME way `coast_invention`'s warp +//! achieves cross-rung coherence: a SINGLE fBm sum whose octave terms span +//! coarse-to-fine wavelengths, sampled at whatever position/cutoff a rung +//! asks for — not two independently-seeded fields for "coarse" and "fine" +//! that could disagree. The coarse octaves (massif-scale, ≈100–400 km, +//! always present regardless of cutoff — same posture as +//! `coast_invention::character_field`, which is NEVER cutoff-gated) set the +//! DC bias (which side of "possible forest" a broad area sits on) at 70% of +//! the blend weight; the fine octaves (district/quarter-scale, cutoff-gated +//! exactly like `terrain_detail`/`voxel_relief`) contribute the remaining +//! 30% as zero-mean texture. That weighting means the fine tier alone can +//! never flip the BLENDED FIELD's sign — but the blended field is only an +//! intermediate `[-1, 1]` value; after it is scaled by the body's +//! `ceiling_q` and added to `base_moisture_q`, the RESULT crosses +//! `derive_vegetation`'s class thresholds exactly as elevation/temperature +//! do elsewhere in this cascade. A boundary cell sitting close enough to a +//! threshold WILL legitimately flip class under the fine tier's +//! contribution — that flip IS the clearing/copse the ticket asks for. What +//! the weighting guarantees is that flips stay localized (isolated cells +//! near a boundary), so the MAJORITY class over any sampled patch tracks the +//! coarse massif tier, never the fine tier alone. //! //! ## Wavelength discipline (per rung, identical to parts 1–2 of T-1162) //! diff --git a/server/tests/golden/window_derivation_golden.json b/server/tests/golden/window_derivation_golden.json index be79ce965..1e6afbf70 100644 --- a/server/tests/golden/window_derivation_golden.json +++ b/server/tests/golden/window_derivation_golden.json @@ -253,5 +253,515 @@ "temperature_dc": -64, "moisture_q": 43, "vegetation": 1 + }, + { + "label": "airless_dry/coastal_a", + "rung": "district", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 2, + "elev_q": 44, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_a", + "rung": "quarter", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 2, + "elev_q": 48, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_a", + "rung": "region", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 0, + "elev_q": 41, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_b", + "rung": "district", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 1, + "elev_q": 42, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_b", + "rung": "quarter", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 2, + "elev_q": 39, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_b", + "rung": "region", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 0, + "elev_q": 41, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_c", + "rung": "district", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 2, + "elev_q": 44, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_c", + "rung": "quarter", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 2, + "elev_q": 45, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/coastal_c", + "rung": "region", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 0, + "elev_q": 41, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/inland", + "rung": "district", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 3, + "elev_q": 44, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/inland", + "rung": "quarter", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 4, + "elev_q": 39, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/inland", + "rung": "region", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 0, + "elev_q": 42, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/high_lat", + "rung": "district", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 4096, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 2, + "elev_q": 76, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/high_lat", + "rung": "quarter", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 1024, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 3, + "elev_q": 79, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "airless_dry/high_lat", + "rung": "region", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 0, + "morphology": 8, + "tectonic": 0, + "glaciation": 0, + "precipitation": 0, + "slope_q": 0, + "elev_q": 77, + "ocean_fraction_q": 0, + "temperature_dc": -2147483648, + "moisture_q": 0, + "vegetation": 0 + }, + { + "label": "volcanic_coast/coastal_a", + "rung": "district", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 4096, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 3, + "elev_q": 25, + "ocean_fraction_q": 0, + "temperature_dc": 497, + "moisture_q": 60, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_a", + "rung": "quarter", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 1024, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 6, + "elev_q": 13, + "ocean_fraction_q": 0, + "temperature_dc": 560, + "moisture_q": 62, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_a", + "rung": "region", + "wx_m": 2000000, + "wy_m": 1500000, + "min_wl_m": 0, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 0, + "elev_q": 28, + "ocean_fraction_q": 0, + "temperature_dc": 482, + "moisture_q": 59, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_b", + "rung": "district", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 4096, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 4, + "elev_q": 34, + "ocean_fraction_q": 0, + "temperature_dc": 456, + "moisture_q": 58, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_b", + "rung": "quarter", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 1024, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 6, + "elev_q": 24, + "ocean_fraction_q": 0, + "temperature_dc": 508, + "moisture_q": 60, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_b", + "rung": "region", + "wx_m": 2050000, + "wy_m": 1500000, + "min_wl_m": 0, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 0, + "elev_q": 28, + "ocean_fraction_q": 0, + "temperature_dc": 487, + "moisture_q": 59, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_c", + "rung": "district", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 4096, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 2, + "elev_q": 28, + "ocean_fraction_q": 0, + "temperature_dc": 486, + "moisture_q": 59, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_c", + "rung": "quarter", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 1024, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 4, + "elev_q": 33, + "ocean_fraction_q": 0, + "temperature_dc": 460, + "moisture_q": 59, + "vegetation": 3 + }, + { + "label": "volcanic_coast/coastal_c", + "rung": "region", + "wx_m": 2100000, + "wy_m": 1560000, + "min_wl_m": 0, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 3, + "slope_q": 0, + "elev_q": 29, + "ocean_fraction_q": 0, + "temperature_dc": 481, + "moisture_q": 59, + "vegetation": 3 + }, + { + "label": "volcanic_coast/inland", + "rung": "district", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 4096, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 2, + "slope_q": 3, + "elev_q": 24, + "ocean_fraction_q": 0, + "temperature_dc": 565, + "moisture_q": 49, + "vegetation": 3 + }, + { + "label": "volcanic_coast/inland", + "rung": "quarter", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 1024, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 2, + "slope_q": 4, + "elev_q": 21, + "ocean_fraction_q": 0, + "temperature_dc": 581, + "moisture_q": 49, + "vegetation": 3 + }, + { + "label": "volcanic_coast/inland", + "rung": "region", + "wx_m": 500000, + "wy_m": 3000000, + "min_wl_m": 0, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 2, + "slope_q": 0, + "elev_q": 23, + "ocean_fraction_q": 0, + "temperature_dc": 571, + "moisture_q": 49, + "vegetation": 3 + }, + { + "label": "volcanic_coast/high_lat", + "rung": "district", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 4096, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 2, + "slope_q": 3, + "elev_q": 68, + "ocean_fraction_q": 0, + "temperature_dc": 545, + "moisture_q": 29, + "vegetation": 2 + }, + { + "label": "volcanic_coast/high_lat", + "rung": "quarter", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 1024, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 2, + "slope_q": 4, + "elev_q": 65, + "ocean_fraction_q": 0, + "temperature_dc": 561, + "moisture_q": 30, + "vegetation": 2 + }, + { + "label": "volcanic_coast/high_lat", + "rung": "region", + "wx_m": 1200000, + "wy_m": 8500000, + "min_wl_m": 0, + "morphology": 15, + "tectonic": 2, + "glaciation": 0, + "precipitation": 2, + "slope_q": 0, + "elev_q": 66, + "ocean_fraction_q": 0, + "temperature_dc": 555, + "moisture_q": 30, + "vegetation": 2 } ] diff --git a/server/tests/window_derivation_golden.rs b/server/tests/window_derivation_golden.rs index c66672dde..69d4bb3ec 100644 --- a/server/tests/window_derivation_golden.rs +++ b/server/tests/window_derivation_golden.rs @@ -18,6 +18,16 @@ //! quantization will change this golden's values — regenerate deliberately //! (per the asset-pipeline discipline: source changes, not hand-edits). //! +//! **Body coverage (Tyre, PR #194 review I4):** the sweep runs against THREE +//! bodies, not one — the original temperate/ocean/breathable body (unchanged +//! from the initial T-1162 landing), plus an airless/dry body (exercises +//! `vegetation_invention::VegetationEnvelope`'s `ceiling_q == 0` short +//! circuit at the full derivation-stack level) and a volcanic/high-tectonic +//! coastal body (exercises the ridged-warp/wide-`scatter_floor` branch of +//! `coast_invention`). The two new bodies' rows are APPENDED after the +//! original body's rows (never interleaved), so the original rows stay +//! byte-identical across the I4 regen — see `body_sweep_samples`'s doc. +//! //! Run: `cargo test --test window_derivation_golden` //! Regenerate: `UPDATE_GOLDEN=1 cargo test --test window_derivation_golden` @@ -36,6 +46,14 @@ const GOLDEN_FILE: &str = "tests/golden/window_derivation_golden.json"; /// Compact representation of a `DistrictProfile` sample for golden pinning. /// Integer-discriminant fields only (D-010) — no float equality flakiness. +/// +/// **No `body` field (Tyre, PR #194 I4 constraint):** the two new body rows +/// (I4) distinguish themselves via the `label` field's prefix instead of a +/// new struct field — adding a field here would change the JSON shape of +/// EVERY existing row (not just the new ones), which fails I4's explicit +/// "existing rows must stay byte-identical" requirement. `label` was always +/// a free-form string, so `"golden_body/coastal_a"` vs `"airless_dry/coastal_a"` +/// costs nothing structurally and keeps the diff a pure append. #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)] struct GoldenSample { label: String, @@ -97,6 +115,43 @@ fn sample_params() -> BodyParams { } } +/// I4 (Tyre, PR #194 review): airless/dry body params — exercises +/// `vegetation_invention::VegetationEnvelope`'s `ceiling_q == 0` short +/// circuit (no water, no atmosphere → zero patchiness swing, per +/// `envelope_airless_or_dry_body_has_zero_ceiling`'s unit-level proof) at +/// the FULL derivation-stack level, which the unit test alone doesn't pin. +/// Also airless (`atmosphere: "none"`) so `temperature_c`/`vegetation_class` +/// take the `None`/`Absent` branches — a body-envelope regression that +/// invented forest on a bone-dry world would show up here as a NEW +/// non-Barren/non-Absent vegetation discriminant in the golden diff. +fn airless_dry_params() -> BodyParams { + BodyParams { + hydrosphere: Some("none".into()), + atmosphere: Some("none".into()), + planet_class: Some("arid".into()), + body_radius_km: Some(3_390.0), // Mars-scale, deliberately distinct from the wet body + ..Default::default() + } +} + +/// I4 (Tyre, PR #194 review): volcanic/high-tectonic coastal body params — +/// exercises `coast_invention`'s ridged-warp + wide-`scatter_floor` branch +/// (`TectonicClass::Volcanic` → `tectonic_energy` near its ceiling in +/// `body_coast_envelope`, driving up `roughness`/`warp_amplitude_px`/ +/// `scatter_floor` per that function's doc) — the coast-crinkle branch most +/// likely to visibly differ from the temperate body's gentler warp, and thus +/// the branch most likely to silently regress without dedicated coverage. +fn volcanic_coast_params() -> BodyParams { + BodyParams { + hydrosphere: Some("ocean".into()), + atmosphere: Some("breathable".into()), + planet_class: Some("volcanic".into()), + tectonic_activity: Some("volcanic".into()), + body_radius_km: Some(6_000.0), + ..Default::default() + } +} + /// Fixed sweep positions (world metres from origin) — a handful of points /// spanning a coastal stretch (per the heightmap's ripple) plus a couple of /// clearly inland/high-latitude points, so the golden exercises coast warp, @@ -165,53 +220,105 @@ const DISTRICT_MIN_WL_M: f64 = 4_096.0; /// new T-1162 entry — Quarter's own Nyquist floor, `2 * QUARTER_M`). const QUARTER_MIN_WL_M: f64 = 1_024.0; -/// Build the full fixed sweep: every position × the three rung cutoffs -/// (District / Quarter use their REAL production `MIN_WL_BANDS_M` values / -/// Region via `derive_orbital_at_metres`, which takes no cutoff parameter — -/// see its own doc on why). -fn golden_samples() -> Vec { - let hm = sample_hm(); - let ta = sample_ta(&hm); - let params = sample_params(); - let climate = ClimateConstants::default(); - let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7); - let body_id = "golden_body"; - +/// Run the fixed sweep (every position × the three rung cutoffs — District / +/// Quarter use their REAL production `MIN_WL_BANDS_M` values / Region via +/// `derive_orbital_at_metres`, which takes no cutoff parameter — see its own +/// doc on why) for ONE body. Extracted (Tyre, PR #194 I4) so multiple bodies +/// can share the same sweep logic; `label_prefix` (empty for the original +/// body, non-empty for the I4 additions) is prepended to each row's `label` +/// so multi-body output stays distinguishable without a new struct field +/// (see [`GoldenSample`]'s doc on why no `body` field was added). +#[allow(clippy::too_many_arguments)] +fn body_sweep_samples( + label_prefix: &str, + seed: SeedChain, + body_id: &str, + params: &BodyParams, + ta: &TerrainAnalysis, + climate: &ClimateConstants, +) -> Vec { let mut out = Vec::new(); for (label, wx, wy) in sweep_positions() { + let label = format!("{label_prefix}{label}"); out.push(derive_golden_sample( - label, + &label, "district", seed, body_id, - ¶ms, - &ta, - &climate, + params, + ta, + climate, wx, wy, DISTRICT_MIN_WL_M, false, )); out.push(derive_golden_sample( - label, + &label, "quarter", seed, body_id, - ¶ms, - &ta, - &climate, + params, + ta, + climate, wx, wy, QUARTER_MIN_WL_M, false, )); out.push(derive_golden_sample( - label, "region", seed, body_id, ¶ms, &ta, &climate, wx, wy, 0.0, true, + &label, "region", seed, body_id, params, ta, climate, wx, wy, 0.0, true, )); } out } +/// Build the full golden sample set: the ORIGINAL temperate/ocean/breathable +/// body's sweep first (byte-identical inputs to the pre-I4 `golden_samples` +/// — same seed, same `body_id`, same unprefixed labels, so its rows are +/// byte-identical in the regenerated fixture), THEN the two I4 body rows +/// appended after (never interleaved) so the diff against the pre-I4 golden +/// is a pure append, not a reshuffle. +fn golden_samples() -> Vec { + let hm = sample_hm(); + let ta = sample_ta(&hm); + let climate = ClimateConstants::default(); + + let mut out = Vec::new(); + + // Original body — UNCHANGED inputs from pre-I4 (T-1162 initial landing). + out.extend(body_sweep_samples( + "", + SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7), + "golden_body", + &sample_params(), + &ta, + &climate, + )); + + // I4 addition 1: airless/dry — ceiling_q == 0 vegetation short-circuit. + out.extend(body_sweep_samples( + "airless_dry/", + SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 8), + "golden_body_airless_dry", + &airless_dry_params(), + &ta, + &climate, + )); + + // I4 addition 2: volcanic/high-tectonic coast — ridged warp, wide scatter_floor. + out.extend(body_sweep_samples( + "volcanic_coast/", + SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 9), + "golden_body_volcanic_coast", + &volcanic_coast_params(), + &ta, + &climate, + )); + + out +} + #[test] fn window_derivation_golden_regression() { let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -294,12 +401,18 @@ fn quarter_and_district_rungs_diverge_at_the_same_position() { ); } -/// `scale::DISTRICT_M` / `scale::QUARTER_M` sanity — documents WHY 2,048/1,024 -/// are the cutoffs used above (District's own spacing; Quarter's own Nyquist -/// floor, `2 × QUARTER_M`), so a future scale-ladder change surfaces here. +/// `scale::DISTRICT_M` / `scale::QUARTER_M` sanity — documents WHY +/// `DISTRICT_MIN_WL_M`/`QUARTER_MIN_WL_M` are the cutoffs used above (each +/// rung's own Nyquist floor, `2 × `), so a future +/// scale-ladder change surfaces here. Pins BOTH rungs' coupling (Tyre, PR +/// #194 I1 — District's coupling was previously unpinned; only Quarter's +/// `2 × QUARTER_M` was checked) — this mirrors +/// `layer_proxy::MIN_WL_BANDS_M`'s own direct `2 × DISTRICT_M` / `2 × +/// QUARTER_M` derivation, not `detail_scatter::OCTAVE_WAVELENGTHS_M`. #[test] fn golden_cutoffs_match_the_scale_ladder() { assert_eq!(scale::DISTRICT_M, 2_048); assert_eq!(scale::QUARTER_M, 512); - assert_eq!(2 * scale::QUARTER_M, 1_024); + assert_eq!(2 * scale::DISTRICT_M, DISTRICT_MIN_WL_M as i32); + assert_eq!(2 * scale::QUARTER_M, QUARTER_MIN_WL_M as i32); } -- 2.54.0 From f50c93b2c807f04de8a9697cd3c1d922205a97c8 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 23:31:31 +0200 Subject: [PATCH 6/8] =?UTF-8?q?docs(meta):=20PR=20#194=20I3=20=E2=80=94=20?= =?UTF-8?q?=C2=A78=20step=206=20erratum=20+=20D-226=20filter-axis=20note?= =?UTF-8?q?=20(T-1161=20two-axes=20supersedes=20COMPOSITE=5FSMOOTH=20retir?= =?UTF-8?q?ement)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tyre's exact capture: the design pass's 'retire COMPOSITE_SMOOTH / crisp draw_rect as the only path' is superseded — the const survives as the compile-time pipeline axis, the crisp path stays as debug/ compare, and crispness-at-sparse-rungs is delivered by the per-rung sampling-filter policy instead. T-1155's retirement framing is cancelled (ticket closes on merge). Also records the R2 softening: the T-1162 relief band adds sub-district elevation variance at Quarter cutoff, so temperature no longer steps strictly at the district there — intended, noted so nobody is surprised later. Tickets: T-1161, T-1162 Co-Authored-By: Claude Fable 5 --- docs/architecture/atlas-zoom-ladder-t1143.md | 2 ++ governance/decisions/architecture.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/architecture/atlas-zoom-ladder-t1143.md b/docs/architecture/atlas-zoom-ladder-t1143.md index 5cece223e..2f5ddfa4e 100644 --- a/docs/architecture/atlas-zoom-ladder-t1143.md +++ b/docs/architecture/atlas-zoom-ladder-t1143.md @@ -171,6 +171,8 @@ Each step shippable and independently verifiable; steps 1–3 extend `derive_dis 5. **Client rung-selection + progressive refinement** in `AtlasWindowViewer`: request granularity tracks zoom level; on zoom-in, hold coarse + fetch fine + swap on arrival. Merges the district/quarter screens into one continuum. The click-through seam at planetary→regional (T-1124 §5) is preserved as-is — see §10. 6. **Retire `COMPOSITE_SMOOTH`** (`atlas_window_overlay.gd:60`) once granularity-4 (quarter) windows are the default at close zoom — real classified quarter-cells replace what the linear-filter stretch was compensating for; delete the const and the smoothing branch, leaving the crisp `draw_rect` path as the only path. + > **Erratum (T-1161, PR #194 review — Tyre, 2026-07-22):** step 6's "retire COMPOSITE_SMOOTH entirely / crisp draw_rect as the only path" is **superseded**. COMPOSITE_SMOOTH survives as a compile-time *pipeline* axis (texture vs. per-cell rects); the crisp draw_rect path is retained as the debug/compare path. What T-1155 framed as "retire the smoothing" is delivered instead as a per-rung *sampling-filter* axis (`_filter_for_granularity_v2`): Region/orbital-mosaic → NEAREST (bilinear stretch at 204.8 km/cell reads as smoothing-over-absence), District/Quarter → LINEAR (cell density earns the blend), unknown → LINEAR. T-1155's COMPOSITE_SMOOTH-retirement framing is cancelled; T-1161 supersedes it. Related record note, same review: the T-1162 relief band adds sub-district elevation variance at Quarter cutoff, and elevation feeds the temperature lapse — so R2's "temperature steps at the district" is now slightly softened by relief-driven elevation within a district at Quarter. Intended consequence, noted so nobody is surprised later. + --- ## 9. Risks diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 5b999c173..f53f16d55 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1669,7 +1669,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser **Amended 2026-07-21 (T-1145 — Jeroen, second companion hands-on, KALLAST window):** three regional-window presentation fixes, all client-only. **Cover-fit supersedes contain:** `fit_window_view()`'s zoom now derives from the LARGER viewport dimension with no margin factor (`max(viewport.x, viewport.y) / composite_native`, not the old `0.9 * min(...)`), so the square district-window composite fills a wide/tall viewport edge to edge instead of leaving side margins, with the shorter axis' data extending into pan-space (the same "cover" concept as CSS `object-fit: cover`) — the existing §4 pan-edge refetch is unaffected (it keys off the screen-center-to-DistrictPos mapping, which any fit already centers on `_held_center` by construction, so no refetch churn at rest). **WASD + edge-scroll supersedes drag-pan:** LMB-drag panning is removed entirely (Jeroen's ruling — drag conflicts with click semantics for the map objects, e.g. settlements, this window will host later); panning is now held WASD/arrow keys (continuous, frame-rate-independent, `_process`-polled, physical-keycode reads to stay independent of the project's existing `move_north`/etc. gameplay-movement InputMap actions bound to the same keys) plus edge-scrolling (cursor within ~24px of a viewport edge, suppressed over UI and while the OS window lacks focus); wheel zoom is unchanged; pole-wall (§5 amendment above) and east-west wrap (T-1142) semantics are preserved unchanged under the new input source. **Smoothing is an interim presentation, pending T-1143:** the composite renders as an `n`×`n` `Image`/`ImageTexture` (one pixel per district, the identical existing per-cell color pipeline) drawn scaled with linear filtering — the same treatment the planetary heightmap already gets — instead of `n`×`n` flat rects, so GPU bilinear sampling reads as a terrain gradient rather than hard blocks; the original crisp per-cell path survives behind a compile-time const specifically so T-1143's design pass can compare both directly, and this smoothing is **not** T-1143's answer to district-tier legibility, only a stopgap ahead of it. - **Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row. **Refinement-semantics note (T-1153, PR #192 review — Tyre):** the ladder's progressive cross-rung refinement (hold the coarse composite, fetch the finer rung, swap in place on arrival; per-tile arrival in the orbital mosaic) **extends** the T-1124 §4 float-on-center/debounce async contract — it does not supersede it. §4 still governs the per-request mechanics unchanged (`district_window: None`-until-derived polling, the 150 ms debounce, float-on-center refetch); rung crossings add a second request class on top, per the design pass §3's progressive-refinement model. This record is the one that governs the swap-on-arrival behavior. The legacy `window_granularity: u32` wire field is now fully shadowed by `window_granularity_v2` (the server always echoes both); it is **scheduled for retirement** once pre-T-1152 wire-compat is confirmed unneeded (single-repo client/server pair — no external clients exist today; ticketed). **Pending-shape protocol note (T-1163, PR #193 review — Tyre):** `AtlasLayerResponse` has **two legal wire shapes for one logical "still deriving, client must re-poll" state**, and both are contract: whole-response `status: Pending` (whole-body cache cold — nothing about this body computed yet) and `status: Ready` with `district_window: null` (body warm, this window still in the derive queue). `Ready` is set **only** by the whole-body cache-hit branch, independent of the window's own derivation. Any `AtlasLayerResponse` consumer must treat BOTH shapes as retry-with-backoff and only `NotFound`/`Error` as terminal — the T-1163 cold-launch starvation (every first launch black) was precisely a client reading `status != Ready` as ignorable. A server refactor that "cleans up" the Pending/Ready-null asymmetry must migrate every consumer in the same change. + **Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row. **Refinement-semantics note (T-1153, PR #192 review — Tyre):** the ladder's progressive cross-rung refinement (hold the coarse composite, fetch the finer rung, swap in place on arrival; per-tile arrival in the orbital mosaic) **extends** the T-1124 §4 float-on-center/debounce async contract — it does not supersede it. §4 still governs the per-request mechanics unchanged (`district_window: None`-until-derived polling, the 150 ms debounce, float-on-center refetch); rung crossings add a second request class on top, per the design pass §3's progressive-refinement model. This record is the one that governs the swap-on-arrival behavior. The legacy `window_granularity: u32` wire field is now fully shadowed by `window_granularity_v2` (the server always echoes both); it is **scheduled for retirement** once pre-T-1152 wire-compat is confirmed unneeded (single-repo client/server pair — no external clients exist today; ticketed). **Pending-shape protocol note (T-1163, PR #193 review — Tyre):** `AtlasLayerResponse` has **two legal wire shapes for one logical "still deriving, client must re-poll" state**, and both are contract: whole-response `status: Pending` (whole-body cache cold — nothing about this body computed yet) and `status: Ready` with `district_window: null` (body warm, this window still in the derive queue). `Ready` is set **only** by the whole-body cache-hit branch, independent of the window's own derivation. Any `AtlasLayerResponse` consumer must treat BOTH shapes as retry-with-backoff and only `NotFound`/`Error` as terminal — the T-1163 cold-launch starvation (every first launch black) was precisely a client reading `status != Ready` as ignorable. A server refactor that "cleans up" the Pending/Ready-null asymmetry must migrate every consumer in the same change. **Filter-axis note (T-1161, PR #194 review — Tyre):** COMPOSITE_SMOOTH is retained as the compile-time *pipeline* axis (texture vs. per-cell rects, crisp path kept for debug/compare); the ladder's crispness-at-sparse-rungs requirement is met by the per-rung *sampling-filter* policy (`_filter_for_granularity_v2`: Region/orbital-mosaic NEAREST, District/Quarter LINEAR, unknown falls back LINEAR), **not** by deleting the const. The design pass §8 step 6 "retire COMPOSITE_SMOOTH" is errata'd accordingly; T-1155's retirement framing is cancelled, superseded by T-1161. - **Rationale:** Reusing the real UI — rather than a parallel offline renderer or dumped files — means the debug/review surface never diverges from what ships, and a dropped artifact can't go stale. Agent-navigability converts qualitative "does the synthesis look natural?" review from a manual eyeball pass into an automatable sweep that flags the few outliers for a human. The harness rides seams that already exist (`TickRate::Paused`, the paused-allowlist, `gameplay_occluded`, the bridge framing, the `run-visual` capture primitive) — a naming-and-contract exercise, not a new subsystem. - **New surface:** server pause-gating (run-conditions on the world phases keyed to a pause command); client `AtlasAgentInterface` (`observe`/`act`, Control-tree walker) + its local transport; the generation overlay rendering + selector + legend; interactive capture wired to `run-visual`. - **Implementation:** Phase 4 (epic T-750), built bottom-up — auto-pause substrate, T-969 proxy (D-225), T-960 viewer, agent channel, agent capture. Geography is the first consumer. -- 2.54.0 From e5a68f4445994b2ab3aa456294e8a5f4deabdda2 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 23:34:53 +0200 Subject: [PATCH 7/8] docs(simulation): sweep the last pre-I1 floor-framing comment (Tyre's non-blocking follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DISTRICT_MIN_WL_M's doc in window_derivation_golden.rs still carried the 'NOT 2×DISTRICT_M by coincidence alone' framing the I1 fix inverted everywhere else — now states the rung authors the floor and the terrain-octave coincidence is guard-pinned, matching the rewritten MIN_WL_BANDS_M and coast_invention docs. Tickets: T-1162 Co-Authored-By: Claude Fable 5 --- server/tests/window_derivation_golden.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/tests/window_derivation_golden.rs b/server/tests/window_derivation_golden.rs index 69d4bb3ec..e9ec5c073 100644 --- a/server/tests/window_derivation_golden.rs +++ b/server/tests/window_derivation_golden.rs @@ -207,13 +207,14 @@ fn derive_golden_sample( } /// District's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s -/// pre-existing finest entry — District's own Nyquist floor, matching -/// `detail_scatter::OCTAVE_WAVELENGTHS_M`'s finest octave). NOT `2 * -/// DISTRICT_M` by construction coincidence alone — see the T-1162 discovery -/// documented on `MIN_WL_BANDS_M` and on `district_floor_cutoff_is_stable_and_deterministic` -/// in `district_profile.rs`: a District-rung request in production quantizes -/// to exactly this value, so the golden pins the SAME cutoff a real window -/// request would actually carry. +/// District entry — District's own Nyquist floor, `2 * DISTRICT_M` per the +/// PR #194 I1 dependency-direction fix: the RUNG authors the floor, and +/// `detail_scatter::OCTAVE_WAVELENGTHS_M`'s finest octave coinciding with it +/// is pinned by the `const` drift-guard next to `MIN_WL_BANDS_M`, not the +/// source of the value). A District-rung request in production quantizes to +/// exactly this value, so the golden pins the SAME cutoff a real window +/// request would actually carry; `golden_cutoffs_match_the_scale_ladder` +/// asserts the `2 × spacing` coupling for both this and `QUARTER_MIN_WL_M`. const DISTRICT_MIN_WL_M: f64 = 4_096.0; /// Quarter's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s -- 2.54.0 From 972842c320ef098cae68fda4046a1fe849e753b7 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Jul 2026 23:36:11 +0200 Subject: [PATCH 8/8] =?UTF-8?q?style(simulation):=20cargo=20fmt=20?= =?UTF-8?q?=E2=80=94=20const=20assert=20line=20wrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- server/src/atlas/layer_proxy.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index 5500e6013..4ff965637 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -467,9 +467,8 @@ const MIN_WL_BANDS_M: [f64; 6] = [ /// the build breaks loudly and a human decides deliberately whether the /// coincidence should be restored or the two meanings were meant to diverge /// — never a silent redefinition of the Atlas rung floor. -const _: () = assert!( - crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3] as i64 == 2 * DISTRICT_M as i64 -); +const _: () = + assert!(crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3] as i64 == 2 * DISTRICT_M as i64); /// Snap a wire-supplied `window_min_wl_m` to the nearest fixed band in /// [`MIN_WL_BANDS_M`] (T-1150, design doc §5's gap-fix): "as specified, -- 2.54.0