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_ribbonsreadsHeightfieldProducts.heightat the ribbon centerline cell index and adds a constant Y offset.meshes.rs::write_water_meshestriangulates lake polygons at the lake'ssurfaceElevationMetersbut does not incise the surrounding terrain.tile_pyramid.rsemitsconditionedDemper LOD but Stage 12 does not consume it.- No
riverbedElevation,channelDepth,waterSurfaceElevation,bankMask,conditionedSlope, orconditionedNormalproduct 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, versionmapv10-terrain-conditioning-v1.pipeline.rsregistersid: 4, key: "c-terrain-conditioning"between Stage 4 and Stage 5 with all seven product write helpers. Stage 4c'sdependenciesarray lists"03-heightfield"and"04-hydrology".validation.rsshipsvalidate_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_maskentries 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.
- All seven raster lengths equal
architecture.md § Water Architecturegains a paragraph describing the new Stage 4c.docs/generator.md § Stage 4description gains a forward reference;docs/generator.md § Stage 4c — terrain-conditioningis a new sub-section..claude/rules/mapv10.mdgains 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):
| Product | Stage owner | Schema declared in | Wave 5 status |
|---|---|---|---|
hydrology.json | Stage 4 (water.rs) | hydrology.schema.json | Unchanged |
conditionedDem.f32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged (M11d) |
flowDirection.u8.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
downstreamIndex.u32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
flowAccumulationD8.f32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
catchments.u32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
streamOrder.u8.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
streamMagnitude.u16.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
riverSegmentId.u32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
riverWidth.f32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
waterBodyId.u32.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
waterMask.u8.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
floodplainMask.u8.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
wetlandMask.u8.bin | Stage 4 (water.rs) | hydrology.schema.json#/$defs/rasterChannels | Unchanged |
conditionedHeight.f32.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
conditionedSlope.f32.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
conditionedNormal.rg16.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
riverbedElevation.f32.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
channelDepth.f32.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
waterSurfaceElevation.f32.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
bankMask.u8.bin | Stage 4c (terrain_conditioning.rs) | terrain-conditioning.schema.json#/$defs/rasterChannels | NEW (stub at Wave 5; real at Wave 6) |
Implementation notes — Wave 5 stub semantics
terrain_conditioning::generate()allocates each of the sevenVecfields with lengthconfig.raster_width * config.raster_heightand fills them with0.0/(0, 0)/0respectively. The body is documented as a Wave-6 TODO.conditioned_heightis the one field with a meaningful Wave 5 value: the function copies bytes fromWaterProducts.conditioned_demso 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
f32values are finite (0.0is finite). The validator is allowed to FAIL on Wave 6 semantics (e.g.channel_depth > 0wherestreamOrder > 0) when those rules ship — but those rules ship in Wave 6, not Wave 5.
Wave 6 implementation hooks
Wave 6 owns:
- The stream-power incision body inside
terrain_conditioning::generate(). For each cell withstreamOrder > 0: integratedz/dt = -K * A^m * S^n(Howard 1994) to steady state.Kis a recipe-vocabulary calibration constant.AisflowAccumulationD8in km².Sis the local slope along the D8 downstream direction.m, nare Howard 1994 calibration constants (canonicalm = 0.5, n = 1; final values pinned in Wave 6). - The Leopold-Maddock depth formula. For each river segment, compute
Q = drainage_area_km2(rainfall-weighted; same source asflowAccumulationD8). Computedepth_km = c * Q^fwherec, fare the depth-equivalents of the existing width-formula constantsBASE_RIVER_WIDTH_KMandRIVER_WIDTH_REFERENCE_DISCHARGE. Pin calibrations against the same recipe vocabulary surface that owns the width-formula constants. channel_depth[i]populated for every cell withstreamOrder[i] > 0; zero elsewhere.riverbed_elevation[i] = conditioned_height[i] - channel_depth[i]for channel cells; equalsconditioned_height[i]elsewhere; equalslake.surfaceElevationMeters - lakeBedDepthfor lake interior cells.water_surface_elevation[i]populated from Mei 2007 water column (HeightfieldProducts.flow_accumulation) plusriverbed_elevation[i]for river cells; equalslake.surfaceElevationMetersfor lake interior cells; equalsconditioned_height[i](dry land surface) elsewhere.bank_mask[i] = 1for every cell within 2 cells of any cell withstreamOrder > 0; zero elsewhere.- Horn 1981 slope recomputation against
conditioned_heightto populateconditioned_slope. - Centred-difference normal recomputation against
conditioned_heightto populateconditioned_normal(rg16-packed signed normalized normal x/z). - Threading the new products into Stage 5 (
biomes_materials::generategains a&ConditioningProductsparameter), Stage 11 (tile_pyramidemits the seven new conditioning rasters at every LOD), and Stage 12 (meshes.rs::write_river_ribbonsconsumeswater_surface_elevationandriverbed_elevationinstead of rawHeightfieldProducts.height + 0.000_22 km;shoreline-transitionmeshes consumebank_maskfor bank-slope authoring).
Wave 6 extends validate_terrain_conditioning with:
channel-depth-positive-where-stream-order-positiveriverbed-below-or-equal-water-surface-everywhereconditioned-height-above-or-equal-riverbed-everywherebank-mask-narrow-band(max 2 cells from any channel cell)conditioned-slope-non-negative-finiteconditioned-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_maskdirectly, thewaterMaskprojection 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-W5is the hydrology-conditioning contract (this ADR).M11x-W6is the hydrology-incision implementation wave.M11x-W7is the viewer loader / schema alignment wave — it folds in the priorM11x-W7bwaterMask 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
-
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 invocabulary/default-world-vocabulary.json. -
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.
-
Lake-bed depth. Lake polygons in
hydrology.jsoncarrysurfaceElevationMetersbut no depth. Wave 6 derives lake bed depth fromlake_area_km2(textbook lake hypsometry:depth ≈ k * sqrt(area)for small endorheic lakes; deeper for rift lakes). The recipe vocabulary pinsk. -
Mei 2007 water column reuse. The existing
HeightfieldProducts.flow_accumulationis the Mei 2007 final water column from Stage 3 erosion. Wave 6 needs to confirm this is the right per-cell water column forwater_surface_elevation, or compute a new per-tick steady-state water column. Open question for Wave 6. -
bank_mask versus floodplain_mask radius parameterization. The
bank_maskradius (2 cells) is hard-coded in this ADR. Wave 6 promotes it to recipe vocabulary alongsideFLOODPLAIN_BAND_RADIUS_CELLS. -
Stage 11 tile-pyramid emit order. Stage 11 currently emits
conditionedDemper LOD by readingWaterProducts.conditioned_dem. Wave 6 adds the seven new conditioning rasters per LOD. Stage 11 already runs after Stage 5; the dependency is04-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. -
Determinism. The Wave 5 stub returns zero-allocated buffers and a single byte-copy of
WaterProducts.conditioned_dem. Thesame_config_recipe_intents_vocabulary_produces_same_core_product_ bytespipeline 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&HeightfieldProductsborrow preserved).examples/map/mapv10/generator/src/pipeline.rs(registers Stage 4c in Wave 5).examples/map/mapv10/generator/src/validation.rs(shipsvalidate_terrain_conditioningin Wave 5)..claude/rules/mapv10.md(this ADR contributes one no-ribbon-only-rivers invariant in Wave 5).examples/map/mapv10/roadmap.mdandexamples/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 inSidecarKeyas CPU-only sidecars.examples/map/mapv10/viewer/src/renderer/lod/RenderResolver.ts::CPU_REQUIRED_SIDECARSand::GPU_REQUIRED_SIDECARS— Wave 9 / Wave 2 / Choice A runtime expression: the conditioning channels gate at the CPU residency layer only.