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: thefamilyenum is extended withterrain-land,terrain-seafloor,shoreline-transitionand theterrainvalue is removed. TherenderPassenum is extended withshoreline-transition. A new optionalboundaryIds: string[]field is added to each mesh asset. The schema version bumps tomapv10-mesh-family-v2. Removing the"terrain"enum value is a hard break: existing fixtures underviewer/public/continent-lod6/andviewer/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"atviewer/src/data/types.ts:701, 720andviewer/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 underschema/): versionmapv10-coastlines-v2. Wave 2 ships the file; Wave 3 generator code writes v2 bytes.manifest.schema.jsonandstage-index.schema.jsonneed 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
familyisterrain-land,terrain-seafloor,shoreline-transition, orwater(forlake-surfaceandsea-surfaceassets adjacent to coastlines) declares theBoundaryIds its edges touch in itsboundaryIdsfield; - every
BoundaryIddeclared by any mesh asset exists incoastlines.json.boundaryEdges[]; - every
BoundaryIdincoastlines.json.boundaryEdges[]is declared by the expected pair of mesh assets and no others; - no mesh asset emits a triangle that crosses a
BoundaryIdsegment.
Wave 3 implementation hooks
Wave 3 owns:
- The rewrite of
terrain_mesh_for_tileinmeshes.rsto clip against the tile-local continent polygon, producing aterrain-landmesh asset only when the clipped polygon is non-empty. - A new
seafloor_mesh_for_tilewriter that clips against the tile-local sea polygon (sea region intersected with tile rectangle, minus lake interiors), producing aterrain-seafloormesh asset only when the clipped polygon is non-empty. - A new
shoreline_transition_mesh_for_tilewriter that builds the seam ribbon along everyBoundaryIdsegment touching the tile, producing ashoreline-transitionmesh asset only when at least one segment touches. - The deletion of the unit test
terrain_mesh_emits_full_seabed_for_mixed_coastline_tilesatmeshes.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-emptyterrain-landasset whose triangle count is strictly less than the full-grid count; a mixed coast tile emits a non-emptyterrain-seafloorasset; the two assets share no triangles; ashoreline-transitionasset is emitted for any tile crossed by a coastBoundaryId. - The deletion (or no-emit when all-sea) of the test
terrain_mesh_emits_empty_asset_for_fully_sea_tileand replacement with an equivalent test forterrain-land-empty /terrain-seafloor-non-empty on a fully-sea tile (sea floor is still terrain). - 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
earcutrcrate 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.rsfor lakes — may be reused for any contour extraction needed inside the clip.
- The decision to compose
hydrology.jsonlake polygons into the clip, so lake interiors are removed fromterrain-land.
Wave 4 validator names
Wave 4 ships:
coast_mesh_terrain_land_clipped_to_continent_polygoncoast_mesh_terrain_seafloor_clipped_to_sea_polygoncoast_mesh_no_triangle_straddles_boundary_idcoast_mesh_shoreline_transition_owns_every_coast_segmentcoast_mesh_shoreline_transition_owns_every_lake_segmentcoast_mesh_boundary_ids_referenced_by_exactly_two_familiescoast_mesh_terrain_land_lake_edge_constraints(amended 2026-05-18, supersedes the retiredcoast_mesh_terrain_land_excludes_lake_interiors; enforces (a) noterrain-landtriangle interior crosses alake-*-seg-*segment and (b) noterrain-landasset declares alake-*-seg-*BoundaryId inboundary_ids[])coastlines_json_boundary_edges_match_segment_countcoast_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
| z | semantic_band | seam visibility | shoreline emission under Policy C |
|---|---|---|---|
| 0 | continent | SEAM-INVISIBLE | NONE |
| 1 | realm | SEAM-INVISIBLE | NONE |
| 2 | realm | SEAM-INVISIBLE | NONE |
| 3 | province | SEAM-HIDDEN-BY-WATER | NONE |
| 4 | province | SEAM-HIDDEN-BY-WATER | NONE |
| 5 | location | SEAM-VISIBLE (S3.4 finding) | NONE (Wave 4 may escalate to z5) |
| 6 | close | SEAM-VISIBLE | EMITTED |
| 7 | close | SEAM-VISIBLE | EMITTED |
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_segmentis 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 coastBoundaryIddeclared incoastlines.json.boundary_edges[]must be referenced by EXACTLY ONEshoreline-transitionmesh asset PER EMITTING LOD (every tilezwhosesemantic_band == "close") and by ZEROshoreline-transitionmesh assets at every non-emitting LOD.validate_coast_mesh_shoreline_transition_owns_every_lake_segmentreceives the same per-LOD partition rework for lakeBoundaryIds.validate_coast_mesh_shoreline_transition_lod_policy_close_bandis the new Wave 9 LOD-policy contract. It enforces two clauses on top of the per-LOD ownership rework:- Every shoreline-transition asset's
tile.semantic_bandMUST be"close"— no shoreline-transition asset may be emitted at any other semantic band. - Every
zvalue present inTileCoordinateIndex.tileswhose 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-bandz.
- Every shoreline-transition asset's
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
BoundaryIdis owned by EXACTLY oneterrain-landasset 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
BoundaryIdis owned by EXACTLY oneshoreline-transitionasset at that z. - For every non-emitting z, zero
shoreline-transitionassets carry the coastBoundaryIdat 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_maskuse insideterrain_mesh_for_tileis 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 fromsourceDependenciesof the land and seafloor families.- The
all_seafast path is replaced by "if clipped land polygon is empty, emit noterrain-landasset; if clipped sea polygon is empty, emit noterrain-seafloorasset".
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.tsandviewer/src/data/manifestLoader.tsacceptterrain-land,terrain-seafloor, andshoreline-transitionfamilies. Missing required families fail loudly per the no-fallback doctrine.- The renderer terrain pipeline binds two terrain materials
(
terrain-landandterrain-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.mjsgrep 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
-
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).
-
The
BoundaryIdsegment 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 platformf64rounding inside the trigonometric setup. The seed key (stage-1-coast-midpoint) covers this today; the segment-index assignment must use the same deterministic walk. -
Coast-touching lake polygons (lakes that open to the sea) create a topological ambiguity: is the shared edge a
coast-...boundary or alake-...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. -
Island coastlines: each island generates its own coast boundary set. Wave 3 must confirm that the
tileMembershipfor tiny single-tile islands does not duplicate the same boundary id across families. -
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. -
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 inmeshes.rs::write_split_terrain_meshes(theclose_band_zscomputation) and in the new LOD-policy validatorvalidate_coast_mesh_shoreline_transition_lod_policy_close_band. -
Wave 4 regenerates the
continent-lod6fixture 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 inmeshes.rsplus 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 thecontract()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-onlybank_maskconsumer contract: the shoreline-transition material does not bindbankMaskto 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.