diff --git a/decisions/architecture.md b/decisions/architecture.md index c98975a6c..1df59b078 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -1444,4 +1444,36 @@ Technical foundation decisions that constrain implementation: engine, client-ser --- -*83 decisions (D-001 through D-223, excluding gaps). Last updated: 2026-05-22 (D-223 — Authored content as a flavored name pool)* +### D-224: SeedChain — deterministic seed-derivation contract +- **Date:** 2026-05-23 +- **Decision:** All deterministic generation — the Phase-4 world cascade now, and NPC / storyteller / economic generation later — descends from a single master world seed through one shared type, **`SeedChain`**, living at **`server/src/seed.rs`** (top-level, because seeds are broader than the atlas). It is the **only** sanctioned way to derive a child seed; ad-hoc pre-mixing (`seed.wrapping_add(C)`, currently in `skeleton_gen.rs:248`) is removed, and the doc-comment references to a "SeedChain" in `skeleton_gen.rs:42` / `generator.rs:655` become real. + + **Mixing primitive.** `splitmix64` — already used by `EntityRng::from_seed_and_id` (`simulation/rng.rs`), chosen there specifically to avoid the `wrapping_add` collision class where `(seed=0,id=N) == (seed=1,id=N-1)` — is promoted to a shared `pub(crate)` function in `server/src/seed.rs` and reused. One canonical mixer for the whole codebase. + + **Contract:** + ```rust + pub struct SeedChain(u64); // Copy + pub enum SeedDomain { Body, Layer1Topography, Layer3Settlement, Layer4Quarter, Block, Npc /* … */ } // u64 tags + impl SeedChain { + pub fn root(world_seed: u64) -> Self; // top of the chain + pub fn derive(self, domain: SeedDomain, id: u64) -> Self; + pub fn atlas_rng(self) -> AtlasRng; // integer-only LCG stream (D-010) + pub fn seed(self) -> u64; // raw — for SimRng/ChaCha or further derive() + } + ``` + + **Derivation (load-bearing — pinned, because changing it changes every generated world):** + `derive(domain, id) = splitmix64(self.0 ^ splitmix64(domain as u64)) ^ splitmix64(id)`. + Properties: deterministic; domain-separated (distinct `SeedDomain` tags never share a stream); full avalanche (splitmix64 on each input); integer-only (D-010 #4); chainable (`root → Body → Layer3Settlement → Quarter → Block`). The output is well-distributed, so `AtlasRng::new` is fed the derived seed directly — no `| 1` or golden-ratio pre-mix guard. + + **Scope of effect (verified 2026-05-23):** SeedChain changes only the RNG-*using* layers — the existing `skeleton_gen.rs` (Layer 4 block placement) and the future Layer-3 settlement placement (#955). It does **not** affect Layer 0 heightmaps (produced by the Python `planet_simulation` pipeline, seeded separately via `--seed`, committed as `heightmap.png` files) nor Layer 1 (`drainage`/`features`/`subbiome` are RNG-free — pure functions of the heightmap). The #952 Layer 0→1 golden fixtures are therefore SeedChain-independent and can be captured in any order relative to the SeedChain work. + +- **Rationale:** Three seeding paths had drifted apart — `AtlasRng` (LCG, "callers pre-mix"), `EntityRng` (correct splitmix64 mixing), and ad-hoc `wrapping_add` in atlas callers — while the code already *named* a SeedChain that didn't exist. A single typed derivation chain with domain separation makes every sub-stream reproducible from one world seed, eliminates the `(seed,id)` collision class `wrapping_add` invites, and gives the determinism harness (#952) a stable contract to verify against. Promoting one mixer prevents two divergent implementations. +- **Implementation:** #952 (Phase 4, epic #750) — add `server/src/seed.rs`, thread `SeedChain` through the existing atlas RNG callers, then capture the golden fixtures (SHA-256 of `heightmap.png` + msgpack of `Layer1Output`). Pre-Phase-5: no savegames exist, so the seed-stream change needs no migration; D-202's `schema_version` lineage covers future changes once saves exist. +- **Raised by:** Jeroen + Claude, `/whats-next` refinement of #952, 2026-05-23. +- **Cross-reference:** [D-010](#d-010) (determinism — integer-only, seed→identical output), [D-200](#d-200) (three-tier execution model), [D-208](#d-208) (RNG-free drainage), [D-223](#d-223) (names-only pool — placement uses seeded RNG), `simulation/rng.rs` (EntityRng / splitmix64 precedent) +- **Dissent:** None + +--- + +*84 decisions (D-001 through D-224, excluding gaps). Last updated: 2026-05-23 (D-224 — SeedChain seed-derivation contract)*