One commit for two tickets whose changes share the bridge/plugin
plumbing files. T-1169 connects the three dormant feature-name pieces:
atlas_feature_names populated at regen (17,891 rows — 15,190 mountain,
2,701 river — via populate_atlas_feature_names mirroring the city-names
importer; systems.db regenerated, stamp fresh), attach_feature_names
wired into the cascade's Topography block with name pools threaded
DB-free through AnalyzeBody (D-225 pattern) and assignments stored on
Layer1Output/BodyWorldState for future consumers, and a
FeatureNamesRequest/Response read proxy as the bridge's 7th tagged
envelope (D-236 pattern, both SimBridge impls). Client label DRAW is
deliberately NOT here — implementation proved both river and mountain
labels need a wire-carried position (the pool is position-free; course
polylines aren't correlated with the named attractors by construction) —
deferred to T-1195's single design pass. cascade_layer1 golden re-pinned
(additive feature_names field).
T-1159 retires the legacy u32 granularity field fully shadowed by
window_granularity_v2: AtlasLayerRequest.window_granularity,
DistrictWindowLayer.granularity echo, the u32::MAX sentinel, and
resolve_window_granularity are gone server-side; client encode paths and
the caller-less atlas_window_cache legacy key component dropped;
msgpack fixtures regenerated; the T-1150 aliasing regression test now
drives through the surviving enum field. The district_window carrier
itself survives byte-compatible per D-255(c).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoshe finding 1 (live-firing on GJ1c: 2 spill collisions + 1 i==1
collision among 51 Overflow basins): adjacency adjudicated
INSUFFICIENT for the cue — a course's visible anchor is its upstream
cell, so nothing pre-existing belongs to the lake unless wired. The
spill cell (outlet_path[0]) now always gets a real entry: appended
when new, OVERWRITTEN IN PLACE when it collided with an existing river
cell (append would duplicate edge_id; the hydrology solve is the more
authoritative downstream answer for that cell than flat D8
extraction). Interior stop-on-collision stays, now provably safe.
Internal lookup is a dense Vec<Option<usize>>, never iterated (D-010).
Two non-vacuous regression tests prove the cue through build_edges
output; end-to-end on GJ1c all 51 Overflow basins now build a readable
edge (was: one silently missing).
Hoshe finding 2: both doc sites now state the fallback-vs-production
split explicitly (fallback moisture 55: 51/2; production GJ1c moisture
80: 53/53 all-Overflow) — the golden's Endorheic pair is a
fallback-constant artifact, not a fact about GJ1c.
Golden re-regenerated: river_cells 143->192, position-identity diff
purely additive (zero removed, one legitimate in-place overwrite at
the spill-collision cell); attractors/basins/mouths/confluences
byte-identical. Suites: hydrology 26/26, full lib 1943, cascade_golden
1/1, window goldens + believability untouched green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
extend_river_network_with_basin_outlets threads each Overflow basin's
resolved outlet_path into RiverNetwork as real D8 downstream
continuations (spill cell skipped as lake interior; Sea target ends at
RIVER_DOWNSTREAM_MOUTH with a seaward entry, Basin/OpenSpillway at
RIVER_DOWNSTREAM_EDGE_DRAIN); Endorheic basins contribute nothing —
the ruled zero-bit cue is outflow-course PRESENCE (D-227 amendment
(4)): a lake with no outgoing course reads as closed. Additive per
T-1170 Ruling 7b; no wire migration, no new zone, no endorheic bit.
Wired in run_layer1_with_moisture strictly AFTER attractor extraction
so outlet cells never perturb settlement placement (tested). Edge
identity is the existing pack_cell_id (row,col) convention; wiring
determinism proven by three independent double-solve byte-identity
tests. 13 new tests across hydrology_equilibrium and layer1.
Goldens: cascade_layer1.json re-pinned (third deliberate re-pin,
documented in cascade_golden.rs) — river_cells 93->143 on GJ1c,
append-only prefix byte-identical, 51/53 real basins Overflow with
short OpenSpillway stubs (the dominant honest pattern: most basins
spill onto adjacent open ground, not a long channel to sea).
window_derivation_golden and river_course_golden verified structurally
unreachable and unchanged.
Known scope boundary (documented in-code): outlet cells carry
river_class 0 (stream) — outlet-channel classification/meander width
scaled to catchment is a named follow-up, not guessed at here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The batch's real bug: extract_river_network computed the seaward
neighbor (nr,nc) to decide the MOUTH sentinel then discarded it, and
build_edges placeholder-pointed mouth edges at themselves — zero
chord, invent_course's degenerate 1-point return, resolve_mouth_
terminus dead code on real data, ALL real mouths resolving None, and
(because Ruling 3g retired the D/Q clip on the promise of real
termini) mouths vanishing at District/Quarter. The second instance of
the threw-away-the-answer anti-pattern Ruling 2b fixed for interior
pointers. Fix: river_seaward: Vec<(u16,u16)> on RiverNetwork
(additive, serde-default, parallel array; meaningful only at MOUTH
entries), captured in the same extraction pass; build_edges gives
Mouth edges the real one-D8-step chord. Permanent acceptance:
all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none — 3/3, revert-
verified failing at the golden's first mouth (38,47). Mouth golden
coverage added (river_course_golden gains district_mouth/quarter_mouth
samples + non-degeneracy test; the previously Interior-only filter
gap closed). Tyre 1: the land-probe's dead dx/len*len arithmetic
replaced — station_spacing_m threaded through the crop path, probe
steps one real cell spacing, comment reconciled. Tyre 2: boxing
comment reattributed to variant-size balancing (Layer1Output retention
lives on TerrainAnalysisCache, not BodyWorldState). Hoshe #4:
cascade_golden's doc now states the attractor cascade accurately
(count-parity, not byte-identity — water_dist seeds from mouths).
Full cargo test green; goldens re-pinned deliberately; bench +3.0%.
Tickets: T-1170
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RiverNetwork gains river_downstream: Vec<u8> (serde-default, parallel
to river_cells): values 0-7 index drainage::D8 (row,col deltas, N/S/E/
W/NE/NW/SE/SW order); sentinels MOUTH=8, EDGE_DRAIN=9, TERMINAL=10
(reserved — the future endorheic-basin hook, Ruling 2c/7b). Captured
in extract_river_network's existing pass (fdir already in scope, one
map, no new grid pass). Pole-edge D8 exits reclassify as EDGE_DRAIN
and leave the mouths list (Ruling 3f); flat-peak interior no-outflow
cells get EDGE_DRAIN too. cascade_golden deliberately re-pinned: GJ1c
mouths 19->3 — sixteen were pole-edge artifacts, exactly Jeroen's
'circles with no sea in sight'; river_cells/attractors counts
unchanged (pure reclassification + additive field). 21/21 drainage
tests incl. mouth-sentinel/mouths-list bijection on the real fixture
and a synthetic pole-draining-grid case.
Tickets: T-1170
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one additive server change of wave 1 per Tyre's carrier ruling:
river_class: Vec<u8> parallel to river_cells, serde-default-safe, no
new wire field. Log-scaled binning between RIVER_THRESHOLD and the
RIVER-RESTRICTED max accumulation (max over cells passing the is_river
elevation filter — NOT the grid-wide max, which peaks past the
coastline on wet large-ocean bodies and would starve the trunk class
exactly where Araminta's District-shows-trunk-only table needs it;
caught in lead review of round 1, fixed round 2). Log not linear
because accumulation grows combinatorially downstream. By construction
every body with rivers has trunk cells — pinned by a non-synthetic
regression on the committed GJ1c heightmap at the golden's exact
downsample. cascade_layer1.json regenerated: every pre-existing field
byte-identical (python-verified), only the new river_class arrays
added — GJ1c distribution stream 72 / tributary 18 / trunk 3.
drainage 16/16 (+10), layer1 3/3, layer_proxy 50/50, build --tests
clean; revert-verified (non-monotonic binning fails the monotonicity
test by name).
Tickets: T-1156
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Close the last D-234 piece — terrain water-direction extraction wired through
to founding orientation and the quarter waterfront rule:
- Layer 1: TerrainAnalysis::water_bearing — 8-octant integer bearing toward the
nearest water from the water_dist gradient (D-010, no atan2). Stored on
GeographicAttractor.water_bearing (360 = none).
- #956 founding orientation: coastal/river settlements now get a real
water-facing bearing (the anchoring attractor's), replacing the 0 stub.
- #957 waterfront rule (D-234b): the water-facing quarter edge (from the
settlement's Coastal founding orientation) drops its block setback to 0 so
buildings present flush to the quay (dock-orthogonal). Typed Edge + coastal_edge
+ per-block gating.
Golden + atlas_response fixture rebaked (additive water_bearing field only).
8 new tests. All integer-deterministic (D-010).
Pending: the waterfront rule reads context.founding_orientation, which
city_context_reader still stubs to Cardinal — real per-settlement orientation
reaches quarter generation once the Layer-3 placement -> Layer-4 GenerateSkeleton
dispatch is wired (the remaining cross-layer integration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DrainageBasin gained a territorial_status field, so the Layer-1 golden and the
atlas_response msgpack fixture pick it up. Both changes are additive/
representational — basin/attractor/river selection is unchanged.
- cascade_golden.rs: pass None dominant_faction; golden rebaked (only
"territorial_status": "FrontierUnclaimed" added per basin).
- gen_fixtures.rs: set the new field in the atlas_response generator; fixture
regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the existing attractor-matching engine (#919/#925) into the
generation cascade as Layer 3, and make the whole placement-scoring path
integer-deterministic.
Layer 3 (D-211):
- CascadeLayer::Settlement + Layer3Output (placements) on the snapshot;
BodyWorldState gains a `placements` field (the D-203 hot cache).
- run_layer3 runs the five-phase match_cities against Layer-1 attractors
via the authored D-195 compatibility matrix; pure function of
(attractors, cities) — no RNG. cities are passed in by the caller so the
cascade stays DB-free and testable. A `// cache seam` marks where a
persistent cache wraps it later (#1021).
- gen_queue passes &[] for now (Topography needs no cities); the runtime
settlement read (gen_queue/layer_proxy) is the #955 follow-on.
Integer determinism (D-010 / D-227 — D-195 amended):
- Wiring match_cities into the deterministic cascade made its f32 scoring
a live cross-platform divergence risk (a near-tie comparison or the
Hungarian's f32 reductions can round differently per platform → a
different world from the same seed). Converted the entire path to
integers: CompatibilityMatrix is a 0-100 affinity table; attractor
strength is 0-100 and terrain cost is a percent (100 = baseline),
quantized once at the Layer-1 feature boundary; cell_score, the
Hungarian, and CityPlacement.score are i64. No f32 in any placement or
ranking decision.
- Layer-1 golden fixture rebaked: confirmed selection/positions are
unchanged (same 256 attractors, 93 river cells) — only the strength/cost
representation changed.
Tests: lib green (1292); new settlement_layer_places_cities_deterministically
covers placement + determinism + propagation into BodyWorldState.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two Layer-1 generation fixes surfaced by the per-layer atlas viewer:
- Layer1Output now carries grid_w/grid_h (the downsampled working-grid the
positions live in). The client maps overlays from these, so the scale is
correct for any source heightmap resolution rather than assuming the texture
size — fixes overlays projecting at half scale into a corner.
- Drainage basin boundaries are traced as ordered, non-self-crossing contours
via Moore-neighbour tracing instead of an angle-from-centroid sort. The sort
produced star-shaped, self-crossing polygons for concave basins that rendered
as straight chords across the map.
Golden (cascade_layer1.json) and the cross-language atlas_response_ready
fixture regenerated. 100 atlas lib tests + the new tracer test pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the Hoshe (QA) + Tyre (architecture) review of the SeedChain/cascade
work:
- Golden was pinning an empty river network (128x64 produced 0 river cells).
Bumped to 256x128, where GJ1c yields a real network (93 river cells, 19
mouths) — Layer 1's rivers are now actually guarded, not just attractors.
- SeedChain::for_body(world_seed, body_id) + fnv1a_64: the single canonical
body_id(String) -> u64 path (FNV-1a, the repo convention), so callers can't
derive divergent worlds from the same seed via different ad-hoc hashes. The
golden now uses it.
- Stability guards: seed_domain_discriminants_are_pinned test (CI fails if a
SeedDomain tag is renumbered); AttractorType gains #[repr(u8)] + explicit
discriminants (it's cast as a sort key in features.rs).
- Tests: SeedChain::root(0) non-degenerate; run_cascade error path (missing
file -> Err, not panic).
- Comments: clarified the id=0 derivations (sibling separation is caller-side
via the per-district/quarter chain; #957 threads the index), tightened the
all_district_types reachability comment (it pins the seed-0 sequence, not a
probabilistic claim), and noted the golden's WORLD_SEED is cosmetic at
Layers 0-1 + the x86_64 f32 capture caveat.
Deferred with reason: the run_cascade -> CascadeInputs struct refactor (Tyre)
is left for #954 — designing Layer-2's context shape now would be later-phase
detail, and there's a single caller to migrate then. EntityRng keeps its
domainless combine (migrating is stream-changing) — noted in D-224.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end determinism guard. cascade_golden.rs pins two artifacts for a real
committed body heightmap (GJ1c) in one diffable JSON golden
(server/tests/golden/cascade_layer1.json):
- Layer 0: SHA-256 of the source heightmap.png bytes (flips if the Python
heightmap generator or the file changes)
- Layer 1: the serialized Layer1Output of run_cascade on a 128x64 downsample
(flips if the Rust drainage/feature/sub-biome code changes)
JSON (not the msgpack discussed in refinement) to match the existing
golden_suite.rs convention and stay diffable — a failure shows what drifted.
UPDATE_GOLDEN=1 regenerates; wired into `make golden-update`.
Adds Serialize/Deserialize to RiverNetwork/DrainageBasin/Layer1Output and a
sha2 dev-dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>