Skip to main content

AD-MAPV10-M11X: Coastline Geometry Extraction Contract

Status

Accepted, 2026-05-17. Authority: this ADR is the canonical mapv10 contract for M11x coastline geometry. Wave 2 of M11x ships the contract. Wave 3 implements the generator-side clipping/triangulation against this contract. Wave 4 ships validators. Wave 5 retires legacy raster channels redundant under the new contract. Wave 6 refreshes generator artifacts. Wave 7 wires viewer-side consumption against the new mesh families. Wave 8 (M11l-Final-B) refreshes baselines once the chain is fully live.

This ADR composes with — does not override — the committed M11d hydrology truth contract and the committed M11 source-of-truth ADR (docs/ad-mapv10-m11-world-genesis-source-of-truth.md). Coastline geometry is a geometry-product surface; hydrology remains the source of truth for water classification, river/lake topology, and flow.

Context

mapv10 Stage 12 (generator/src/stages/meshes.rs) currently emits a single terrain mesh family. The terrain mesh for any tile whose water_mask contains any land sample is written as the full rectangular grid spanning the tile's source_window. The committed rationale is: "Coastal terrain tiles emit the full heightfield grid so terrain forms a continuous seabed under the sea mesh; only fully deep-sea tiles emit no triangles" (architecture.md § Water Architecture). The sea mesh is then triangulated from seaRegions.json with the continent polygons as holes (meshes.rs::write_sea_surface_meshes) and overlays the terrain seabed.

The Wave 2 explorer audit (M11x Wave 2) found that this bbox-grid behavior violates the workspace tenet "Never bbox-quad a complex shape — if a feature has a non-rectangular outline (lakes, regions, areas), extract the actual polygon (marching squares) and triangulate (ear clipping or Delaunay). Do not draw a bounding-box quad and discard fragments." The continent coastline is a non-rectangular polyline (recursive midpoint subdivision plus fjord Z-folds plus optional isthmus extrusions plus offshore islands). Tiles that straddle the coastline emit a full grid that is then visually masked by the sea mesh's alpha blend. The grid does not conform to the coastline; the illusion of a coastline relies on the sea mesh painting over a rectangular seabed.

The fix is contractual, not parametric. A schema that still permits a full-rectangular-grid terrain product for mixed coast tiles satisfies the letter of the tenet but not the spirit. Wave 2 redesigns the product family boundary so the bbox-grid is no longer expressible.

The geometry primitives required for the rewrite already exist in the generator: marching_squares_polygons in stages/water.rs (used for M11d marching-squares lake polygons), clip_polygon_rect, clip_subject_against_concave, and polygon_surface_mesh / triangulate_polygon (via the earcutr crate, used for M11d lake meshes). Wave 3 chooses the specific algorithm combination.

Decision

Wave 2 commits seven decisions. Together they make the bbox-grid coast tile impossible to express in the product schema. They do not commit to a specific Wave 3 clipping algorithm.

1. Mesh family split

The current terrain mesh family is superseded by two distinct families: terrain-land and terrain-seafloor. The terrain family enum value is retired.

Rationale: independent streaming, independent LOD scaling, and independent material binding per Cesium 3D Tiles boundary-stitching practice. Witcher 3 and RDR2 manage their below-water terrain as a distinct render lane from their above-water terrain; the generator must reflect that separation.

The single-family-with-meshKind-discriminator alternative was considered and rejected: a discriminator preserves the temptation to emit one mesh covering both sides of the coastline, which keeps the bbox-grid path live. Splitting the family forces Wave 3 to clip explicitly to two distinct polygons.

Amendment 2026-05-18 (lakebed-is-terrain). Lakebed is terrain. terrain-land mesh assets emit triangles continuously across lake interiors, with lake polygon edges installed as constrained triangulation edges. Lake-edge BoundaryIds remain owned by the family pair {water, shoreline-transition} only — terrain-land triangulates ACROSS the lake polygon (lakebed is land) and does not own the lake edge as a render-pass seam. This matches AAA shipping references (Witcher 3, RDR2, Horizon) where lake floors are part of the continuous terrain heightfield and the water surface is a separate mesh on top.

This amendment supersedes the earlier "lake interiors removed from the terrain-land clip" wording (carried in this section and in §6 "Hydrology composition" before the amendment): lake interiors are NOT removed from terrain-land; only the lake polygon EDGES are honoured as CDT constraints. The pre-amendment subtraction path was found to leave tiles fully submerged in a lake (1,444 tiles at z4..z7 on the committed continent-lod6 fixture) with zero assets in terrain-land, terrain-seafloor, and shoreline-transition combined, violating the per-tile mesh-slot-keys invariant. The amended architecture restores those tiles' lakebed coverage as terrain-land geometry, which is the architecturally correct surface.

2. Shoreline transition mesh family

A new mesh family shoreline-transition distinct from terrain-land, terrain-seafloor, and water is introduced. Its assets are ribbon meshes that own the geometry along every coastline BoundaryId. The shoreline transition mesh is the explicit transition geometry between the land surface and the seafloor surface; it is also the only mesh permitted to emit triangles that straddle the mean-sea-level isoline along a coast.

Rationale: AAA terrain pipelines (Cesium 3D Tiles, Witcher 3) ship explicit seam ribbons rather than relying on per-vertex blend attributes on the upland mesh. Seam ribbons make LOD continuity and material blend authorable as a single product.

The per-vertex-attribute alternative was considered and rejected: it bakes the coast into the land mesh, prevents independent LOD selection for the shoreline strip, and re-enables full-grid terrain output as long as specific vertices carry the right attribute mask.

3. BoundaryId namespace

Boundary identifiers are strings in two namespaces:

coast-<continent_polygon_id>-seg-<N> // mainland and island coast edges
lake-<lake_id>-seg-<N> // lake edges

<continent_polygon_id> is the existing PolygonFeature.id from continentPolygons.json (e.g. continent-main, continent-island-00). <lake_id> is the existing LakePolygon.id from lakePolygons.json / hydrology.json. <N> is a zero-based, deterministic segment index along the polygon ring, starting from the segment whose first vertex has the lowest (x, y) lexicographic key and continuing counter-clockwise.

Rationale: string keys match the existing entity_id, mesh_asset.key, and lake.id conventions. A u32 would be opaque in the product JSON and would require a side index to be human-readable.

4. coastlines.json evolution

coastlines.json stays canonical as the coastline polyline product and gains an explicit boundaryEdges: BoundaryEdge[] array per coastline. Each BoundaryEdge carries:

{
"boundaryId": string,
"polygonId": string,
"startVertexIndex": integer,
"endVertexIndex": integer
}

boundaryId matches the segment naming in §3. polygonId repeats sourcePolygonId for cross-reference convenience. startVertexIndex and endVertexIndex index into the existing points array. A segment is a single straight edge; a full coastline ring becomes one BoundaryEdge per adjacent point pair.

The schema version bumps from mapv10-coastlines-v1 to mapv10-coastlines-v2. Old viewers reading the v1 contract would silently miss boundaryEdges and could not validate boundary-id agreement; the bump makes the change loud.

The "supersede with a separate coastline-geometry.json product" alternative was considered and rejected: the polyline data and the boundary-edge data are joined at the vertex level. Splitting them across two products creates a join the viewer must reassemble at load time and introduces a window where the two products can disagree.

5. Tile-local clipped polygon products

No new per-tile clipped-polygon product is added. Wave 3 performs the tile-local clip of the continent polygon against the tile rectangle inside terrain_mesh_for_tile. The clipped polygon is an implementation detail of Wave 3, not a separately persisted product.

Rationale: the tile pyramid manifests are already large at z6/z7. Adding a per-tile coast polygon multiplies the manifest entry count without giving the viewer any new authoritative data. The tile-local clip is deterministic from the continent polygon plus the tile rectangle; the viewer can reproduce it on demand but does not need to because the terrain-land and shoreline-transition mesh assets already encode the clipped geometry as triangles.

6. Hydrology composition

terrain-land and terrain-seafloor mesh assets consume hydrology.json directly when they need water-classification truth. They do not consume the legacy waterMask.u8.bin raster projection in meshes.rs to decide which polygon to clip against. Lake polygons in hydrology.json contribute lake-<lake_id>-seg-<N> BoundaryIds to the shoreline-transition and water (lake-surface) families, and their polygon edges are installed as constrained triangulation edges inside the terrain-land clip.

Amendment 2026-05-18 (lakebed-is-terrain). Lakebed is terrain. hydrology.json lake polygon EDGES participate in terrain-land triangulation as CDT constraints; lake polygon INTERIORS are NOT removed from terrain-land. Triangles inside lake polygons are legitimate lakebed terrain. Lake-edge BoundaryId ownership stays in the family pair {water, shoreline-transition} only — terrain-land triangulates across the lake polygon and does not declare lake BoundaryIds.

Rationale: the legacy waterMask raster is a derived projection. M11d (landed) made hydrology.json the canonical hydrology truth. Stage 12 should consume the truth product directly, anticipating the M11h / Wave T retirement of the legacy raster projections under §M11 invariant 2.

7. Schema version bumps

  • mesh-product.schema.json: the family enum is extended with terrain-land, terrain-seafloor, shoreline-transition and the terrain value is removed. The renderPass enum is extended with shoreline-transition. A new optional boundaryIds: string[] field is added to each mesh asset. The schema version bumps to mapv10-mesh-family-v2. Removing the "terrain" enum value is a hard break: existing fixtures under viewer/public/continent-lod6/ and viewer/public/sample-run/ were emitted with v1's "terrain" family and will not validate against v2 until they are regenerated in Wave 6. The viewer surface still references "terrain" at viewer/src/data/types.ts:701, 720 and viewer/src/data/manifestLoader.ts; Wave 7 brings the viewer in line. This Wave 2 -> Wave 3..7 transition window is the explicit consequence of making the bbox-grid impossible to express in the schema as soon as the contract lands; the alternative (deferring the schema bump until the emitter catches up) leaves the contract toothless during the interval and is rejected.
  • coastlines.schema.json (new file under schema/): version mapv10-coastlines-v2. Wave 2 ships the file; Wave 3 generator code writes v2 bytes.
  • manifest.schema.json and stage-index.schema.json need no version bump; they reference family names by string and tolerate enum extension in the consumer.

Product family changes

Before Wave 3:

terrain terrain-meshes.json terrain-chunk renderPass=terrain
water water-meshes.json sea-surface renderPass=water
lake-surface renderPass=water
river-ribbon renderPass=water
route-ribbons route-ribbons.json route-ribbon renderPass=border-route

After Wave 3:

terrain-land terrain-land-meshes.json land-chunk renderPass=terrain
terrain-seafloor terrain-seafloor-meshes.json seafloor-chunk renderPass=terrain
shoreline-transition shoreline-transition-meshes.json coast-ribbon renderPass=shoreline-transition
lake-edge-ribbon renderPass=shoreline-transition
water water-meshes.json sea-surface renderPass=water
lake-surface renderPass=water
river-ribbon renderPass=water
route-ribbons route-ribbons.json route-ribbon renderPass=border-route

terrain-land is required and contains only triangles whose vertices lie on or inside the tile-local continent-polygon intersection. terrain-seafloor is required and contains only triangles whose vertices lie on or inside the tile-local sea-polygon intersection (the complement of the continent polygon within the tile rectangle, minus any lake polygons that intersect the tile). Triangles that straddle the coastline polyline are forbidden in both families; the shoreline-transition family owns the seam.

The renderPass "shoreline-transition" enum entry is new. Renderer pipelines bind a dedicated material/pass for the seam ribbon, allowing foam, wet-rock blend, and breaker spray to live in one place.

BoundaryId definition

BoundaryId := "coast-" continent_polygon_id "-seg-" segment_index
| "lake-" lake_id "-seg-" segment_index

continent_polygon_id is the PolygonFeature.id from continentPolygons.json
lake_id is the LakePolygon.id from hydrology.json
segment_index is a zero-padded integer (width = max(4, ceil(log10(ring_len))))

Ordering: ring vertices are enumerated starting from the vertex whose
(x, y) lexicographic key is the minimum (ties broken by y, then by polygon
id). Counter-clockwise traversal defines positive direction. Segment i
spans the directed edge from vertex i to vertex (i + 1) mod ring_len.

Identity: a BoundaryId is shared by exactly two mesh families per the
EXACTLY-TWO family-pair table below. Wave 4 validators check that every
BoundaryId appearing in coastlines.json.boundaryEdges[] or in a
hydrology lake polygon ring is referenced by both expected families and
only by them.

Family-pair table (amended 2026-05-18):

coast-<polygon>-seg-<N> → { terrain-land, shoreline-transition }
lake-<lake>-seg-<N> → { water, shoreline-transition }

Both pairs are EXACTLY-TWO. Coast BoundaryIds live in the coastline
seam between the land surface and the seafloor/sea-surface; the
land-side member is `terrain-land` because the coast is an EDGE of the
land surface. Lake BoundaryIds live in the lake-edge seam between the
water surface and its surroundings; the water-side member is `water`
(`lake-surface` slot) because the lake surface is a separate mesh on
top of the continuous terrain (Witcher 3 / RDR2 model). Lake
BoundaryIds are NOT owned by `terrain-land`: under the lakebed-is-
terrain amendment, `terrain-land` triangulates ACROSS the lake polygon
with the lake polygon edges installed as CDT constraints — the lake
polygon edge is a constraint on the triangulation, not a render-pass
seam owned by `terrain-land`.

Wave 4 will codify validator names; the contract requires that:

  • every mesh asset whose family is terrain-land, terrain-seafloor, shoreline-transition, or water (for lake-surface and sea-surface assets adjacent to coastlines) declares the BoundaryIds its edges touch in its boundaryIds field;
  • every BoundaryId declared by any mesh asset exists in coastlines.json.boundaryEdges[];
  • every BoundaryId in coastlines.json.boundaryEdges[] is declared by the expected pair of mesh assets and no others;
  • no mesh asset emits a triangle that crosses a BoundaryId segment.

Wave 3 implementation hooks

Wave 3 owns:

  1. The rewrite of terrain_mesh_for_tile in meshes.rs to clip against the tile-local continent polygon, producing a terrain-land mesh asset only when the clipped polygon is non-empty.
  2. A new seafloor_mesh_for_tile writer that clips against the tile-local sea polygon (sea region intersected with tile rectangle, minus lake interiors), producing a terrain-seafloor mesh asset only when the clipped polygon is non-empty.
  3. A new shoreline_transition_mesh_for_tile writer that builds the seam ribbon along every BoundaryId segment touching the tile, producing a shoreline-transition mesh asset only when at least one segment touches.
  4. The deletion of the unit test terrain_mesh_emits_full_seabed_for_mixed_coastline_tiles at meshes.rs::tests (its assertion that mixed coast tiles emit the full rectangular grid contradicts §1 of this ADR). Wave 3 replaces it with tests that assert: a mixed coast tile emits a non-empty terrain-land asset whose triangle count is strictly less than the full-grid count; a mixed coast tile emits a non-empty terrain-seafloor asset; the two assets share no triangles; a shoreline-transition asset is emitted for any tile crossed by a coast BoundaryId.
  5. The deletion (or no-emit when all-sea) of the test terrain_mesh_emits_empty_asset_for_fully_sea_tile and replacement with an equivalent test for terrain-land-empty / terrain-seafloor-non-empty on a fully-sea tile (sea floor is still terrain).
  6. Choosing the specific clipping algorithm. The supported AAA techniques referenced in this ADR are:
    • Sutherland-Hodgman polygon clipping (1974) for convex clip regions (tile rectangle ∩ convex polygon).
    • Greiner-Hormann (1998) or Vatti (1992) for general polygon clipping (tile rectangle ∩ concave continent polygon, especially around fjord Z-folds and isthmus extrusions).
    • Constrained Delaunay triangulation (Shewchuk 1996) for the resulting clipped polygon. Earcutting via the existing earcutr crate is also acceptable if Wave 3 validates that the clipped polygon's worst case triangle quality stays inside the precision budget.
    • Marching squares (Lorensen-Cline 1987) — already implemented in stages/water.rs for lakes — may be reused for any contour extraction needed inside the clip.
  7. The decision to compose hydrology.json lake polygons into the clip, so lake interiors are removed from terrain-land.

Wave 4 validator names

Wave 4 ships:

  • coast_mesh_terrain_land_clipped_to_continent_polygon
  • coast_mesh_terrain_seafloor_clipped_to_sea_polygon
  • coast_mesh_no_triangle_straddles_boundary_id
  • coast_mesh_shoreline_transition_owns_every_coast_segment
  • coast_mesh_shoreline_transition_owns_every_lake_segment
  • coast_mesh_boundary_ids_referenced_by_exactly_two_families
  • coast_mesh_terrain_land_lake_edge_constraints (amended 2026-05-18, supersedes the retired coast_mesh_terrain_land_excludes_lake_interiors; enforces (a) no terrain-land triangle interior crosses a lake-*-seg-* segment and (b) no terrain-land asset declares a lake-*-seg-* BoundaryId in boundary_ids[])
  • coastlines_json_boundary_edges_match_segment_count
  • coast_mesh_no_full_rectangular_grid_for_mixed_coast_tile

These names are declarative for now; Wave 4 implements them inside the generator's validation.rs and the viewer's scenario gate vocabulary.

Shoreline-transition LOD policy (Wave 9 / Wave 1 — Policy C: close-semantic-band emission)

Wave 9 / Wave 1 commits the shoreline-transition LOD policy: the shoreline-transition mesh family is emitted at every tile whose semantic_band == "close" and ONLY at those tiles. All other semantic bands (continent, realm, province, location) emit zero shoreline-transition assets, period. The binding is to the TileZoomLevel.semanticBand == "close" field, NOT to a literal z integer — a future world preset that retires or renames the close band requires updating this LOD policy binding.

Rationale

AAA shipping references (Witcher 3, RDR2, Horizon, Star Citizen) handle ocean at continent overview range via stylized water shaders and place per-segment ribbon geometry in the close terrain LOD chain where the seam is multi-pixel resolvable. Emitting the shoreline-transition family at the deepest LOD only (Policy A) was acceptable only when every coarser LOD was either SEAM-INVISIBLE or SEAM-HIDDEN-BY-WATER. The Wave 1 explorer's per-LOD seam-visibility audit found z5+ tiles in SEAM-VISIBLE territory: a continent that looks like a clay model at z5 because the shoreline transition geometry is missing from that band's render pass. Policy C widens the emission set to the entire close semantic band (z6 + z7 in the current continent-lod6 fixture) so the seam is owned by mesh geometry at every LOD where the seam is visually resolvable.

Per-LOD seam-visibility verdict

zsemantic_bandseam visibilityshoreline emission under Policy C
0continentSEAM-INVISIBLENONE
1realmSEAM-INVISIBLENONE
2realmSEAM-INVISIBLENONE
3provinceSEAM-HIDDEN-BY-WATERNONE
4provinceSEAM-HIDDEN-BY-WATERNONE
5locationSEAM-VISIBLE (S3.4 finding)NONE (Wave 4 may escalate to z5)
6closeSEAM-VISIBLEEMITTED
7closeSEAM-VISIBLEEMITTED

The z5 SEAM-MARGINAL evidence is deferred to Wave 4 baseline refresh. If Wave 4's regenerated fixture surfaces a z5 visible-seam regression, Policy C escalates to Policy C+z5 (location + close band). Wave 1 lands Policy C as drafted; the per-LOD validator scope is structurally reusable if Wave 4 finds the emission set must widen.

Generator-side validators

Wave 9 / Wave 1 reworks two existing validators and adds one new one, all under the same Wave 4 ownership envelope:

  • validate_coast_mesh_shoreline_transition_owns_every_coast_segment is reworked from flat-asset enumeration (HashMap<boundary_id, Vec<asset_key>>) to per-LOD partition (HashMap<(tile_z, boundary_id), Vec<asset_key>>). The new contract: every coast BoundaryId declared in coastlines.json.boundary_edges[] must be referenced by EXACTLY ONE shoreline-transition mesh asset PER EMITTING LOD (every tile z whose semantic_band == "close") and by ZERO shoreline-transition mesh assets at every non-emitting LOD.
  • validate_coast_mesh_shoreline_transition_owns_every_lake_segment receives the same per-LOD partition rework for lake BoundaryIds.
  • validate_coast_mesh_shoreline_transition_lod_policy_close_band is the new Wave 9 LOD-policy contract. It enforces two clauses on top of the per-LOD ownership rework:
    1. Every shoreline-transition asset's tile.semantic_band MUST be "close" — no shoreline-transition asset may be emitted at any other semantic band.
    2. Every z value present in TileCoordinateIndex.tiles whose tiles' semantic_band == "close" MUST be represented by at least one shoreline-transition asset — the close band must not be silently empty at any close-band z.

The W3R5 inline comment in meshes.rs::write_split_terrain_meshes previously cited the family-pair validator (validate_coast_mesh_boundary_ids_referenced_by_exactly_two_families) as one of the invariants that fails under multi-LOD emission. That rationale was incorrect. The family-pair validator collapses per-family ownership into a HashSet<family_name>, which is insensitive to per-family multiplicity — N shoreline-transition owners collapse to the single membership entry "shoreline-transition". The actual invariants that failed under the old union-keyed enumeration were the two *_owns_every_*_segment validators only; those are the two validators reworked into a per-LOD partition by Wave 1. The family-pair validator's HashSet collapse needed no change.

Viewer-side scenario gate

The boundaryIdsCrossReference scenario gate is reworked to mirror the generator-side per-LOD partition. Under Policy C the gate's invariants are:

  • Every coast BoundaryId is owned by EXACTLY one terrain-land asset across ALL LODs combined (the family-pair invariant on the terrain-land side is not LOD-partitioned because terrain-land emits per-tile per-LOD).
  • For every close-band z, every coast BoundaryId is owned by EXACTLY one shoreline-transition asset at that z.
  • For every non-emitting z, zero shoreline-transition assets carry the coast BoundaryId at that z.

Scenario coverage

A new scenario shoreline_transition_close_band_lod_policy lands the visual + gate coverage of Policy C in viewer/src/scenarios/mapv10_scenarios.json. The scenario carries a continent top-down camera and asserts the boundaryIdsCrossReference gate, the tilesSettled gate, and the noFailedTilesInReady gate.

Test fixture

viewer/src/renderer/lod/__tests__/fixtures.ts ships a Policy C default: the synthetic mesh-tile manifest populates shorelineTransitionMeshKeys[] ONLY on close-band tiles. Tests that need to exercise a Policy A or other non-default shoreline-transition population overwrite individual slots after construction; the default matches the contract truthfully rather than carrying a Policy A lie.

Wave 5 retirement targets

Wave 5 retires the raster channels whose only consumer in Stage 12 was the old bbox-grid path. Concretely:

  • water_mask use inside terrain_mesh_for_tile is removed by Wave 3 (the clip operates on the polygon, not the raster mask). The raster channel itself stays — it has other consumers — but Stage 12's dependency on it drops from sourceDependencies of the land and seafloor families.
  • The all_sea fast path is replaced by "if clipped land polygon is empty, emit no terrain-land asset; if clipped sea polygon is empty, emit no terrain-seafloor asset".

Wave 5 confirms no other consumer relies on Stage 12 reading water_mask and updates sourceDependencies arrays accordingly.

Wave 6 artifact refresh

Wave 6 regenerates continent-lod6 and writes the new manifest entries. The viewer fixtures under viewer/public/continent-lod6/ and viewer/public/sample-run/ move to v2 schemas in a single deterministic regeneration pass. Wave 6 does NOT update screenshot baselines; that is Wave 8.

Wave 7 viewer consequences

Wave 7 wires the new mesh families into the viewer:

  • viewer/src/data/types.ts and viewer/src/data/manifestLoader.ts accept terrain-land, terrain-seafloor, and shoreline-transition families. Missing required families fail loudly per the no-fallback doctrine.
  • The renderer terrain pipeline binds two terrain materials (terrain-land and terrain-seafloor) plus a shoreline-transition material. The existing single-terrain pipeline is retired.
  • Viewer-side coastline extraction (polygon recovery from rasters, runtime marching-squares passes, etc.) is forbidden by this ADR. The generator owns coastline truth; the viewer consumes it. The forbidden-pattern-check.mjs grep guard is extended in Wave 7 to flag any viewer-side coastline extraction.

Wave T / M11h integration

The committed M11d hydrology.json becomes a Stage 12 input for Wave 3 clipping. This is consistent with §M11 invariant 2 — Stage 12 stops reading the legacy water-mask projection and stops reading the legacy lake_polygons raster-derived shapes inside meshes.rs::write_water_meshes (the lake polygon already lives in hydrology.json). Wave T-M11h continues to own the map-feature anchor consumption of intent directives; this ADR does not change that boundary.

Risks and open questions

  1. The continent polygon is sampled at variable density (recursive midpoint subdivision with up to 4 rounds plus per-edge fjord Z-folds). Wave 3 must decide whether to densify or decimate the polygon along tile edges so adjacent tile clips share identical vertices on the shared edge (required for crack-free LOD).

  2. The BoundaryId segment ordering must remain stable across runs at the same seed. Wave 3 must confirm that the recursive midpoint subdivision produces deterministic vertex insertion order regardless of platform f64 rounding inside the trigonometric setup. The seed key (stage-1-coast-midpoint) covers this today; the segment-index assignment must use the same deterministic walk.

  3. Coast-touching lake polygons (lakes that open to the sea) create a topological ambiguity: is the shared edge a coast-... boundary or a lake-... boundary? Wave 3 ADR addendum resolves it; the candidate answer is "the union of the two interiors becomes a single sea region, the lake id is retired, and a single coast boundary owns the shared edge", but the call is Wave 3's.

  4. Island coastlines: each island generates its own coast boundary set. Wave 3 must confirm that the tileMembership for tiny single-tile islands does not duplicate the same boundary id across families.

  5. Wave 7 viewer wiring depends on Wave D (M11i) typed loaders for new mesh families. The Wave D dependency is captured in next-work.md.

  6. Policy C close-band selection binds to TileZoomLevel.semanticBand == "close". A future world preset that retires or renames the "close" band requires updating this LOD policy binding. The generator-side binding lives in meshes.rs::write_split_terrain_meshes (the close_band_zs computation) and in the new LOD-policy validator validate_coast_mesh_shoreline_transition_lod_policy_close_band.

  7. Wave 4 regenerates the continent-lod6 fixture under Policy C. If the regenerated fixture surfaces a z5 visible-seam regression (S3.4 deferred evidence), Policy C escalates to Policy C+z5 (location + close band) — the per-LOD validator scope already supports a wider emission set, so the rework is a binding change in meshes.rs plus a tile-pyramid policy update, not a fresh validator architecture.

Cross-references

  • examples/map/mapv10/architecture.md § Water Architecture (this ADR edits that section in Wave 2).
  • examples/map/mapv10/docs/generator.md § Stage 12 (this ADR edits that section in Wave 2).
  • examples/map/mapv10/docs/ad-mapv10-m11-world-genesis-source-of-truth.md § invariants 2, 4 (this ADR composes with those invariants; it does not override them).
  • examples/map/mapv10/schema/mesh-product.schema.json (this ADR extends the schema in Wave 2).
  • examples/map/mapv10/schema/coastlines.schema.json (new file in Wave 2).
  • examples/map/mapv10/generator/src/stages/meshes.rs::contract (this ADR edits the contract() text in Wave 2; the implementation is Wave 3's).
  • examples/map/mapv10/generator/src/stages/water.rs::marching_squares_polygons (geometry primitive Wave 3 may reuse).
  • .claude/rules/mapv10.md (this ADR contributes one forbidden-pattern bullet in Wave 2).
  • examples/map/mapv10/generator/src/validation.rs::validate_coast_mesh_shoreline_transition_lod_policy_close_band (Wave 9 / Wave 1 LOD policy validator: enforces close-band emission and close-band coverage).
  • examples/map/mapv10/viewer/src/scenarios/mapv10_scenarios.json::shoreline_transition_close_band_lod_policy (Wave 9 / Wave 1 scenario: continent overview + boundary-ids gate).
  • examples/map/mapv10/viewer/src/renderer/lod/__tests__/fixtures.ts (Wave 9 / Wave 1: Policy-C-conformant default fixture).
  • examples/map/mapv10/docs/ad-mapv10-m11x-hydrology-conditioning-contract.md § 4 — Wave 9 / Wave 2 (Choice A) binds to this ADR's CPU-only bank_mask consumer contract: the shoreline-transition material does not bind bankMask to a shader sampler; the bank-slope distinction is generator-side Stage 12 mesh authoring.
  • examples/map/mapv10/viewer/src/renderer/TextureResidency.ts::GpuBoundSidecarKey — Wave 9 / Wave 2 type-level expression: the 8-element shader-bound subset excludes all seven Stage 4c conditioning channels.
  • examples/map/mapv10/viewer/src/renderer/lod/RenderResolver.ts::GPU_REQUIRED_SIDECARS — Wave 9 / Wave 2 runtime expression of the same CPU/GPU split.