Skip to main content

AD-MAPV10-M11X: Hydrology Conditioning / River Incision Contract

Status

Accepted, 2026-05-17. Authority: this ADR is the canonical mapv10 contract for the M11x hydrology-conditioning seam between Stage 4 hydrology truth and downstream terrain-conditioning consumers. Wave 5 of M11x ships the contract and the stage scaffold. Wave 6 implements the incision algorithm against this contract. Wave 7 wires the new mesh-family + conditioning surface into the viewer (typed loaders, render passes, sidecars, inspector, forbidden-pattern gates) AND folds the prior waterMask-edge cleanup into the same ripple. The Final wave (M11l-Final-B sub-row) regenerates fixtures, scenarios, and capture baselines once the chain is fully live.

This ADR composes with — does not override — the committed M11 source-of-truth ADR (docs/ad-mapv10-m11-world-genesis-source-of-truth.md), the committed M11d hydrology truth contract (same ADR § M11d), and the committed M11x coastline-geometry contract (docs/ad-mapv10-m11x-coast-geometry-contract.md). Hydrology truth (river network topology, lake polygons, sinks, catchments, flow direction, accumulation, stream order) remains owned by Stage 4. This ADR adds a distinct downstream seam — terrain conditioning, river incision, and the riverbed/channel-depth/water-surface product family — that consumes the immutable hydrology truth and the immutable post-erosion heightfield to produce the geometric truth that the mesh, biome, and tile-pyramid stages need to render rivers as carved channels rather than ribbons floating above raw terrain.

Context

mapv10 Stage 4 (generator/src/stages/water.rs) ships canonical hydrology truth: priority-flood-conditioned DEM (conditioned_dem), D8 flow direction, topological flow accumulation, Strahler/Shreve stream order, marching-squares lake polygons, Chaikin-smoothed river centerlines, and Leopold-Maddock 1953 hydraulic-geometry widths. Every river segment carries an averageWidthKm field and every river cell carries a streamOrder byte.

Stage 12 (generator/src/stages/meshes.rs) emits river ribbon meshes by extruding a 2D strip of constant 0.000_22 km (≈ 0.22 m) above the raw post-erosion HeightfieldProducts.height at every river centerline vertex. The ribbon does not consume conditioned_dem. It does not consume any "incised channel depth" product, because no such product exists in the generator output today. The viewer therefore renders rivers as thin painted stripes floating above flat terrain. The Wave 5 explorer A4 audit confirmed:

  • meshes.rs::write_river_ribbons reads HeightfieldProducts.height at the ribbon centerline cell index and adds a constant Y offset.
  • meshes.rs::write_water_meshes triangulates lake polygons at the lake's surfaceElevationMeters but does not incise the surrounding terrain.
  • tile_pyramid.rs emits conditionedDem per LOD but Stage 12 does not consume it.
  • No riverbedElevation, channelDepth, waterSurfaceElevation, bankMask, conditionedSlope, or conditionedNormal product exists in the generator schema family today.

The fix is contractual, not parametric. Lowering the ribbon offset, darkening the river fragment shader, or shifting the river ribbon Y position relative to the terrain does not fix the architectural hole: rivers in mapv10 do not own any geometric truth other than a 2D centerline and a width scalar. A schema that still permits ribbon-only rivers — meshes authored above an unmodified heightfield with no matching channel-depth or riverbed-elevation product — satisfies the letter of the no-bbox-quad-or- billboard tenet but not the spirit. Witcher 3, RDR2, and Horizon Zero Dawn all carry their rivers as incised geometric channels with bed elevation, water-surface elevation, and bank slope as authored or computed truth, not as ribbons painted onto raw terrain.

The Wave 5 explorer flagged a second architectural concern: the obvious "fix" of mutating HeightfieldProducts from water.rs would violate M11d's determinism invariant ("height.f32.bin is the canonical Stage 3 truth and its bytes are stable across runs at the same seed"). Stage 3 must remain immutable; Stage 4 must remain immutable; conditioning must live in a new stage with its own outputs.

Decision

Wave 5 commits seven decisions. Together they make ribbon-only rivers impossible to bless as the canonical river truth.

1. Stage split — new Stage 4c terrain-conditioning sub-stage

A new generator stage terrain_conditioning is inserted between Stage 4 (hydrology truth) and Stage 5 (biomes-materials). It is registered with id: 4 and key: "c-terrain-conditioning", mirroring the existing Stage 4b b-hydrology-locations sub-stage convention. The on-disk stage folder becomes stages/04-c-terrain-conditioning. The pipeline dependency edge is:

Stage 3 (heightfield) → Stage 4 (hydrology) → Stage 4b (hydrology-locations, runs after Stage 6)
Stage 3 (heightfield) ↓
Stage 4 (hydrology) → Stage 4c (terrain-conditioning) → Stage 5 (biomes-materials) and downstream

Both &HeightfieldProducts and &WaterProducts are passed to terrain_conditioning::generate as immutable borrows. Stage 3's height.f32.bin bytes remain canonical and identical to the pre-Wave-5 emit. Stage 4's conditioned_dem field remains canonical and identical to the pre-Wave-5 emit. Conditioning is a separate stage with its own outputs; mutating any prior stage's products is forbidden.

Rationale: the M11d determinism invariant requires Stage 3 byte-stability and the Wave 5 explorer report § Step 8 explicitly recommended option (b) "split Stage 4 into Stage 4a (hydrology truth, current immutable read) plus Stage 4c-terrain-conditioning (new sub-stage, consumes immutable WaterProducts + immutable HeightfieldProducts, produces conditioning products as new outputs)." Option (a) "mutate HeightfieldProducts" was rejected because it would break Stage 3 byte-stability and force every downstream consumer to coordinate read-vs-write phases of a once-immutable struct.

2. ConditioningProducts struct surface

Stage 4c returns a ConditioningProducts struct with seven required raster fields:

pub struct ConditioningProducts {
/// Stage 4c post-incision heightfield. Equals
/// `WaterProducts.conditioned_dem` at Wave 5 (stub); Wave 6 carves it
/// with stream-power incision at every cell where `streamOrder > 0`.
/// Emitted as `conditionedHeight.f32.bin`.
pub conditioned_height: Vec<f32>,
/// Horn 1981 slope magnitude derived from `conditioned_height`. At
/// Wave 5 (stub) this is a zero-allocation; Wave 6 recomputes it from
/// the incised height. Emitted as `conditionedSlope.f32.bin`.
pub conditioned_slope: Vec<f32>,
/// Centred-difference normal map (rg16-packed) derived from
/// `conditioned_height`. Zero at Wave 5; Wave 6 recomputes.
/// Emitted as `conditionedNormal.rg16.bin`.
pub conditioned_normal: Vec<(i16, i16)>,
/// Bed elevation of every river/lake cell in metres. For cells with
/// `streamOrder > 0`: `conditioned_height - channelDepth`. For lake
/// cells: `lake.surfaceElevationMeters - lakeBedDepth`. For non-
/// channel cells: equals `conditioned_height`. Zero at Wave 5; Wave 6
/// computes. Emitted as `riverbedElevation.f32.bin`.
pub riverbed_elevation: Vec<f32>,
/// Depth of the active channel column in metres at every river cell.
/// Zero on non-channel cells. For channel cells: derived from
/// Leopold-Maddock 1953 hydraulic geometry `depth = c * Q^f` where
/// `Q` is the per-segment discharge proxy (rainfall-weighted
/// drainage area in km²) and the calibration constants `c, f` are
/// pinned in Wave 6. Zero at Wave 5; Wave 6 computes.
/// Emitted as `channelDepth.f32.bin`.
pub channel_depth: Vec<f32>,
/// Elevation of the free water surface in metres at every wet cell.
/// For river cells: `riverbed_elevation + waterColumnDepth` where the
/// water column reuses the Mei 2007 truth from
/// `HeightfieldProducts.flow_accumulation` (Stage 3 final water
/// column). For lake cells: `lake.surfaceElevationMeters`. For dry
/// cells: equals `conditioned_height`. Zero at Wave 5; Wave 6
/// computes. Emitted as `waterSurfaceElevation.f32.bin`.
pub water_surface_elevation: Vec<f32>,
/// Narrow band of cells immediately adjacent to the active channel
/// (within 2 cells of any cell with `streamOrder > 0`). Distinct
/// from `WaterProducts.floodplain_mask`, which is a wider band
/// (`FLOODPLAIN_BAND_RADIUS_CELLS = 6.0` cells around high-order
/// streams). `bank_mask` is the strip that Stage 12 uses to author
/// bank-slope geometry on the `shoreline-transition` mesh family;
/// `floodplain_mask` is the strip that Stage 5 uses for biome
/// classification. Zero at Wave 5; Wave 6 populates.
/// Emitted as `bankMask.u8.bin`.
pub bank_mask: Vec<u8>,
}

Wave 5 SCAFFOLDS the struct: declares the seven fields, registers the seven products as required, declares the contract via terrain_conditioning:: contract(). The body of generate() returns zero-allocated Vec<f32> / Vec<(i16, i16)> / Vec<u8> buffers of length config.raster_width * config.raster_height. The schema declares all seven products required (not optional). The validator (validate_terrain_ conditioning) asserts product length matches raster_width * raster_height. Wave 6 fills the buffers in with real values and extends the validator to assert channel_depth[i] > 0 where streamOrder[i] > 0, riverbed_elevation[i] <= water_surface_elevation[i] everywhere, conditioned_height[i] >= riverbed_elevation[i] everywhere.

Optional or Option<...> typing for any of the seven fields is rejected: the no-fallback doctrine forbids "missing required infrastructure data falling back to raw height" (per the M11 invariant 4 / no-fallback rule). Any None reaching downstream would silently degrade Stage 5 / Stage 12 to read HeightfieldProducts.height instead of ConditioningProducts. conditioned_height, restoring the ribbon-only failure mode.

3. New schema file schema/terrain-conditioning.schema.json

A new schema file is added, version mapv10-terrain-conditioning-v1. It declares all seven products as required entries of its rasterChannels block with additionalProperties: false. The schema is distinct from hydrology.schema.json (whose rasterChannels block is closed with additionalProperties: false and adding new fields there would be a v1 breaking change for every existing hydrology consumer).

Rationale: adding the conditioning channels to hydrology.schema.json would break the M11d contract surface for every existing reader. A new schema file keeps the M11d hydrology contract clean and lets Wave 6 / future waves evolve the conditioning surface independently.

4. bank_mask versus floodplain_mask — distinct, not aliased

floodplain_mask stays in WaterProducts. It is a wide zone (radius = FLOODPLAIN_BAND_RADIUS_CELLS = 6.0 cells around streams with order ≥ FLOODPLAIN_MIN_ORDER) used by Stage 5 biome classification (alluvial flood-deposit soils) and Stage 4b hydrology-locations (floodplainPct per Location).

bank_mask lives in ConditioningProducts. It is a narrow strip (radius = 2 cells from any active channel cell, regardless of stream order) used by Stage 12 shoreline-transition mesh authoring (the bank-slope geometry adjacent to the incised channel) and Wave 6's incised-channel banks. The two masks have different radii, different downstream consumers, and different lifetimes; aliasing them would force every consumer to negotiate both meanings.

5. conditioned_slope and conditioned_normal live in Stage 4c

Slope and normal recomputation lives in Stage 4c (not in Stage 3 or Stage 4), so all downstream stages share one computation. Stage 5 biomes-materials, Stage 11 tile-pyramid, and Stage 12 meshes will consume ConditioningProducts.conditioned_slope and ConditioningProducts.conditioned_normal in Wave 6. Wave 5's stub leaves them zero-allocated; the schema still requires them.

Rationale: HeightfieldProducts.slope and HeightfieldProducts.normals are derived from the post-erosion-but-pre-incision height field. Once Wave 6 carves channels into conditioned_height, every consumer that reads slope/normal needs to read the post-incision version, or rivers will appear as flat-slope stripes on a non-flat terrain. Recomputing once in Stage 4c lets Stage 5 / Stage 11 / Stage 12 share one truth.

6. conditioned_dem migration — Wave 5 keeps it in WaterProducts

WaterProducts.conditioned_dem stays in place at Wave 5. The conditionedDem.f32.bin product remains owned by Stage 4 hydrology. ConditioningProducts.conditioned_height is a logically distinct field that, at Wave 5 (stub), happens to be zero-allocated; at Wave 6, it diverges from conditioned_dem because Wave 6 carves stream-power channels into it.

The alternative — moving conditioned_dem out of WaterProducts into ConditioningProducts.conditioned_height and retiring the Stage 4 emit — was considered. It was rejected because Stage 11 tile-pyramid (stages/tile_pyramid.rs::TilePyramidInputs.conditioned_dem) already consumes WaterProducts.conditioned_dem per the M11d emit and migrating it would create coordination churn with M11x-W3 (the parallel coast-geometry implementation wave). The Wave 5 explorer report §Step 11 decision 6 listed this as the safer of the two alternatives. Wave 6 may revisit if the two fields diverge enough that holding both becomes a maintenance hazard.

This is a deliberate "leave the previous-stage emit as the conditioning- input surface" choice; it is not a fallback. Wave 5's conditioned_height stub copies bytes from WaterProducts.conditioned_dem at the boundary inside terrain_conditioning::generate(). Wave 6 starts diverging the two when stream-power incision lands.

7. Schema and code surface versions

  • schema/terrain-conditioning.schema.json — new file, version mapv10-terrain-conditioning-v1.
  • pipeline.rs registers id: 4, key: "c-terrain-conditioning" between Stage 4 and Stage 5 with all seven product write helpers. Stage 4c's dependencies array lists "03-heightfield" and "04-hydrology".
  • validation.rs ships validate_terrain_conditioning(&ConditioningProducts, &GeneratorConfig, &BoundsKm) -> ValidationReport. Wave 5 asserts:
    • All seven raster lengths equal raster_width * raster_height.
    • All Vec<f32> entries are finite (no NaN, no infinity).
    • bank_mask entries are zero (Wave 5 stub guarantee — Wave 6 relaxes). Wave 6 extends with channel-depth-positive / riverbed-below-surface / no-NaN-in-incised-fields assertions.
  • architecture.md § Water Architecture gains a paragraph describing the new Stage 4c.
  • docs/generator.md § Stage 4 description gains a forward reference; docs/generator.md § Stage 4c — terrain-conditioning is a new sub-section.
  • .claude/rules/mapv10.md gains one new invariant under the existing M11x block.

Canonical product naming and stage owners

Hydrology and terrain-conditioning truth product ownership after Wave 5 lands (BoundaryId-equivalent table for hydrology):

ProductStage ownerSchema declared inWave 5 status
hydrology.jsonStage 4 (water.rs)hydrology.schema.jsonUnchanged
conditionedDem.f32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged (M11d)
flowDirection.u8.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
downstreamIndex.u32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
flowAccumulationD8.f32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
catchments.u32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
streamOrder.u8.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
streamMagnitude.u16.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
riverSegmentId.u32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
riverWidth.f32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
waterBodyId.u32.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
waterMask.u8.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
floodplainMask.u8.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
wetlandMask.u8.binStage 4 (water.rs)hydrology.schema.json#/$defs/rasterChannelsUnchanged
conditionedHeight.f32.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)
conditionedSlope.f32.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)
conditionedNormal.rg16.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)
riverbedElevation.f32.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)
channelDepth.f32.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)
waterSurfaceElevation.f32.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)
bankMask.u8.binStage 4c (terrain_conditioning.rs)terrain-conditioning.schema.json#/$defs/rasterChannelsNEW (stub at Wave 5; real at Wave 6)

Implementation notes — Wave 5 stub semantics

  • terrain_conditioning::generate() allocates each of the seven Vec fields with length config.raster_width * config.raster_height and fills them with 0.0 / (0, 0) / 0 respectively. The body is documented as a Wave-6 TODO.
  • conditioned_height is the one field with a meaningful Wave 5 value: the function copies bytes from WaterProducts.conditioned_dem so downstream consumers in Wave 6 can begin reading the field even before the stream- power incision lands. This is NOT a fallback to raw height — it is the same priority-flood-conditioned surface Stage 4 already computed, lifted into the Stage 4c product slot it logically belongs to. Wave 6 begins diverging the two when stream-power incision is added.
  • Schema-required fields stay required; the validator passes because all buffer lengths match the world raster dimensions and all f32 values are finite (0.0 is finite). The validator is allowed to FAIL on Wave 6 semantics (e.g. channel_depth > 0 where streamOrder > 0) when those rules ship — but those rules ship in Wave 6, not Wave 5.

Wave 6 implementation hooks

Wave 6 owns:

  1. The stream-power incision body inside terrain_conditioning::generate(). For each cell with streamOrder > 0: integrate dz/dt = -K * A^m * S^n (Howard 1994) to steady state. K is a recipe-vocabulary calibration constant. A is flowAccumulationD8 in km². S is the local slope along the D8 downstream direction. m, n are Howard 1994 calibration constants (canonical m = 0.5, n = 1; final values pinned in Wave 6).
  2. The Leopold-Maddock depth formula. For each river segment, compute Q = drainage_area_km2 (rainfall-weighted; same source as flowAccumulationD8). Compute depth_km = c * Q^f where c, f are the depth-equivalents of the existing width-formula constants BASE_RIVER_WIDTH_KM and RIVER_WIDTH_REFERENCE_DISCHARGE. Pin calibrations against the same recipe vocabulary surface that owns the width-formula constants.
  3. channel_depth[i] populated for every cell with streamOrder[i] > 0; zero elsewhere.
  4. riverbed_elevation[i] = conditioned_height[i] - channel_depth[i] for channel cells; equals conditioned_height[i] elsewhere; equals lake.surfaceElevationMeters - lakeBedDepth for lake interior cells.
  5. water_surface_elevation[i] populated from Mei 2007 water column (HeightfieldProducts.flow_accumulation) plus riverbed_elevation[i] for river cells; equals lake.surfaceElevationMeters for lake interior cells; equals conditioned_height[i] (dry land surface) elsewhere.
  6. bank_mask[i] = 1 for every cell within 2 cells of any cell with streamOrder > 0; zero elsewhere.
  7. Horn 1981 slope recomputation against conditioned_height to populate conditioned_slope.
  8. Centred-difference normal recomputation against conditioned_height to populate conditioned_normal (rg16-packed signed normalized normal x/z).
  9. Threading the new products into Stage 5 (biomes_materials::generate gains a &ConditioningProducts parameter), Stage 11 (tile_pyramid emits the seven new conditioning rasters at every LOD), and Stage 12 (meshes.rs::write_river_ribbons consumes water_surface_elevation and riverbed_elevation instead of raw HeightfieldProducts.height + 0.000_22 km; shoreline-transition meshes consume bank_mask for bank-slope authoring).

Wave 6 extends validate_terrain_conditioning with:

  • channel-depth-positive-where-stream-order-positive
  • riverbed-below-or-equal-water-surface-everywhere
  • conditioned-height-above-or-equal-riverbed-everywhere
  • bank-mask-narrow-band (max 2 cells from any channel cell)
  • conditioned-slope-non-negative-finite
  • conditioned-normal-unit-vector-after-unpack

Final wave fixture refresh consequences

The committed viewer/public/continent-lod6/ and viewer/public/sample-run/ fixtures do not contain any conditioning products (they predate the contract). The Final wave (M11l-Final-B sub-row) regenerates them under the new pipeline. The fixtures must validate against terrain-conditioning.schema.json v1.

Wave 7 waterMask edge removal (folded into the viewer ripple)

Wave 7 closes the loop by dropping the waterMask source-dependency edge from Stage 12 terrain-family sourceDependencies arrays. With Wave 6's conditioning products live and Stage 12 consuming water_surface_elevation

  • bank_mask directly, the waterMask projection is no longer the geometry-truth ownership channel. The raster channel itself stays — other stages consume it. Wave 7 absorbs this clean-up so the mesh-family / conditioning-sidecar / waterMask-edge changes land in a single ripple. The PRIOR M11x-W7b "Retire waterMask in viewer" row is retired: its content lives inside Wave 7.

Wave 7 viewer integration consequences

Wave 7 adds typed loaders to viewer/src/data/types.ts and viewer/src/data/manifestLoader.ts for the seven new conditioning rasters. Missing required products fail loudly per the no-fallback doctrine. Viewer-side hydrology conditioning — any attempt to derive conditionedHeight, riverbedElevation, channelDepth, or waterSurfaceElevation from raster scans inside the viewer — is forbidden by this ADR. The generator owns conditioning truth; the viewer consumes it. The forbidden-pattern-check.mjs grep guard is extended in Wave 7 to flag any viewer-side conditioning derivation AND any viewer-side coastline extraction.

Wave label collision resolution

roadmap.md and next-work.md previously registered M11x-W5 as the "retire Stage 12 waterMask dependency" wave (a viewer-side cleanup wave that depended on the M11x-W3 mesh-family rewrite landing first). Wave 5 then reused M11x-W5 for the hydrology-conditioning contract. The Wave 7 viewer-loader prompt then collapsed the subsequent rows further.

Resolution (final, committed in Wave 7):

  • M11x-W5 is the hydrology-conditioning contract (this ADR).
  • M11x-W6 is the hydrology-incision implementation wave.
  • M11x-W7 is the viewer loader / schema alignment wave — it folds in the prior M11x-W7b waterMask edge cleanup as a single ripple along with the mesh-family split and the seven conditioning sidecars.
  • The PRIOR M11x-W7 ("Artifact refresh"), M11x-W7b ("Retire waterMask in viewer"), M11x-W8 ("Viewer typed loaders"), and M11x-W9 ("Baseline + capture refresh") rows are retired; the fixture-regen + scenario + capture-baseline work consolidates into the existing M11l-Final-B baseline wave under a "Final" label.

The PRIOR labels are not kept alive as aliases — per the no-fallback / no-compatibility-synonym doctrine, the only live labels are the new ones.

Risks and open questions

  1. Wave 6 calibration vocabulary. The stream-power incision constants (K, m, n), the Leopold-Maddock depth formula constants (c, f), and the lake-bed-depth formula need to land in the recipe vocabulary alongside the existing width-formula constants. Wave 6 ADR addendum pins them; the slot exists today in vocabulary/default-world-vocabulary.json.

  2. Steady-state versus iterative incision. Stream-power incision can be integrated to steady state (closed-form per cell given upstream area and downstream slope) or iteratively to mimic geologic timescales. Wave 6 chooses; the contract surface does not depend on the choice.

  3. Lake-bed depth. Lake polygons in hydrology.json carry surfaceElevationMeters but no depth. Wave 6 derives lake bed depth from lake_area_km2 (textbook lake hypsometry: depth ≈ k * sqrt(area) for small endorheic lakes; deeper for rift lakes). The recipe vocabulary pins k.

  4. Mei 2007 water column reuse. The existing HeightfieldProducts.flow_accumulation is the Mei 2007 final water column from Stage 3 erosion. Wave 6 needs to confirm this is the right per-cell water column for water_surface_elevation, or compute a new per-tick steady-state water column. Open question for Wave 6.

  5. bank_mask versus floodplain_mask radius parameterization. The bank_mask radius (2 cells) is hard-coded in this ADR. Wave 6 promotes it to recipe vocabulary alongside FLOODPLAIN_BAND_RADIUS_CELLS.

  6. Stage 11 tile-pyramid emit order. Stage 11 currently emits conditionedDem per LOD by reading WaterProducts.conditioned_dem. Wave 6 adds the seven new conditioning rasters per LOD. Stage 11 already runs after Stage 5; the dependency is 04-c-terrain- conditioning (Stage 4c) → 05-biomes-materials (Stage 5) → 11-tile- pyramid (Stage 11). The Wave 5 contract does not change Stage 11; Wave 6 does.

  7. Determinism. The Wave 5 stub returns zero-allocated buffers and a single byte-copy of WaterProducts.conditioned_dem. The same_config_recipe_intents_vocabulary_produces_same_core_product_ bytes pipeline test extends in Wave 5 to cover the seven new product keys with their stub bytes. The test is trivially deterministic at Wave 5; Wave 6 must preserve determinism under the real stream-power integration.

Cross-references

  • examples/map/mapv10/architecture.md § Water Architecture (this ADR edits that section in Wave 5).
  • examples/map/mapv10/docs/generator.md § Stage 4 — hydrology (this ADR adds a Stage 4c sub-section in Wave 5).
  • examples/map/mapv10/docs/ad-mapv10-m11-world-genesis-source-of-truth.md § M11d (committed M11d hydrology contract; this ADR composes with it).
  • examples/map/mapv10/docs/ad-mapv10-m11x-coast-geometry-contract.md (committed M11x coast contract; this ADR composes with it).
  • examples/map/mapv10/schema/hydrology.schema.json (referenced unchanged).
  • examples/map/mapv10/schema/terrain-conditioning.schema.json (new file in Wave 5).
  • examples/map/mapv10/generator/src/stages/terrain_conditioning.rs (new module in Wave 5).
  • examples/map/mapv10/generator/src/stages/water.rs (referenced unchanged; immutable &HeightfieldProducts borrow preserved).
  • examples/map/mapv10/generator/src/pipeline.rs (registers Stage 4c in Wave 5).
  • examples/map/mapv10/generator/src/validation.rs (ships validate_terrain_conditioning in Wave 5).
  • .claude/rules/mapv10.md (this ADR contributes one no-ribbon-only-rivers invariant in Wave 5).
  • examples/map/mapv10/roadmap.md and examples/map/mapv10/next-work.md (carry the wave label collision resolution in Wave 5).
  • examples/map/mapv10/viewer/src/renderer/TextureResidency.ts::GpuBoundSidecarKey — Wave 9 / Wave 2 / Choice A type-level expression of § 4 + § 5: the 8-element shader-bound subset excludes all seven conditioning channels; the seven channels stay in SidecarKey as CPU-only sidecars.
  • examples/map/mapv10/viewer/src/renderer/lod/RenderResolver.ts::CPU_REQUIRED_SIDECARS and ::GPU_REQUIRED_SIDECARS — Wave 9 / Wave 2 / Choice A runtime expression: the conditioning channels gate at the CPU residency layer only.