From d3b68012d9b1554db8f8108c0513c67926c8bb67 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 13 Feb 2026 01:19:34 +0100 Subject: [PATCH] docs(architecture): add seed configuration schema design (#394) Design document defining what the randomizer produces at game-start: FRIEND selections, pool draws, template assignments, triangle config, entanglement config, contraband selection, starting knowledge. Includes ChaCha20 RNG protocol, validation rules, and implementation roadmap. Co-Authored-By: Claude Opus 4.6 --- .../architecture/seed-configuration-schema.md | 815 ++++++++++++++++++ 1 file changed, 815 insertions(+) create mode 100644 docs/architecture/seed-configuration-schema.md diff --git a/docs/architecture/seed-configuration-schema.md b/docs/architecture/seed-configuration-schema.md new file mode 100644 index 000000000..46768595b --- /dev/null +++ b/docs/architecture/seed-configuration-schema.md @@ -0,0 +1,815 @@ +# Seed Configuration Schema — Design Document + +**Ticket:** #394 +**Author:** Tyre (Technical Architect) +**Status:** Draft +**Date:** 2026-02-13 +**Decisions referenced:** D-010, D-024, D-025, D-027, D-029, D-034, D-035, D-036, D-037, D-041 + +--- + +## 1. Purpose + +The seed configuration is the **handoff contract** between authored content (pools, templates, triangles in `content/`) and a running game instance (ECS entities in memory). It answers one question: *given this content and this seed value, what specific world do we instantiate?* + +At game start, the seeder reads content definitions and a seed value, then produces a `SeedConfig` — a deterministic, serializable record of every randomized selection. This record is saved with the game state and replayed identically on reload. + +**What the seed config is NOT:** +- Not a content authoring format (content authors write pools/templates/triangles — the seed config *consumes* them) +- Not a runtime state snapshot (that's the ECS world — the seed config is the *recipe* that built it) +- Not a save file (the save file *contains* the seed config alongside mutable game state) + +## 2. Design Constraints + +| Constraint | Source | Impact | +|-----------|--------|--------| +| Deterministic reproduction | D-010 principle 4, D-030 #7 | Same seed + same content version = identical `SeedConfig`. No HashMap iteration, no platform-dependent RNG. | +| BTreeMap for ordered collections | D-041 | All maps in the seed config use BTreeMap, not HashMap. | +| 30/50/20 entanglement ratio (variable) | D-029 | ~30% flat, ~50% mundane triangles, ~20% intrigue-entangled. Ratios vary per seed to prevent metagaming. | +| Single-candidate pools in v0.1 | Sprint briefing | Architecture supports N candidates; v0.1 pools contain exactly 1 candidate each. | +| Template instantiation via role slots | D-025 | Social sites define roles; the seed assigns NPCs to roles. Single ownership with reference links. | +| Two playable characters | D-027 | Smuggler + detective. Seed config records which character the player selected. | +| Saved with game state | Ticket #394 | Serialized into save files. Must be self-contained (no external content references that could drift). | +| Content version pinning | Implicit | Seed config records content version to detect content/save incompatibility. | + +## 3. Schema Overview + +``` +SeedConfig +├── meta +│ ├── seed: u64 +│ ├── content_version: String +│ ├── schema_version: u32 +│ └── generated_at: String (ISO 8601) +├── character_selection: CharacterSelection +├── pool_draws: BTreeMap +├── template_assignments: BTreeMap +├── triangle_config: TriangleConfig +├── entanglement: EntanglementConfig +├── contraband: ContrabandSelection +└── starting_knowledge: BTreeMap> +``` + +## 4. Schema Detail + +### 4.1 Meta + +```rust +struct SeedMeta { + /// The seed value. u64 for sufficient randomness space. + /// v0.1: displayed nowhere; v0.2+: player can enter a seed for shared runs. + seed: u64, + + /// Content version string from content/content.yaml. + /// If the save's content_version doesn't match the loaded content, + /// the loader warns or refuses to load (prevents desync). + content_version: String, + + /// Schema version for forward compatibility. Increment on breaking changes. + schema_version: u32, + + /// ISO 8601 timestamp of generation (informational, not used in logic). + generated_at: String, +} +``` + +**Rationale:** `seed` is the root of determinism — every randomized decision traces back to this value through a deterministic RNG (see section 6). `content_version` pins the content snapshot to prevent save/content drift. + +### 4.2 Character Selection + +```rust +struct CharacterSelection { + /// Which character the player chose. Determines starting knowledge, + /// access tiers, monologue pools, perception modes. + player_character: CharacterId, + + /// All available characters for this campaign (for reference/validation). + available_characters: Vec, +} + +/// CharacterId is a string enum matching content definitions. +/// v0.1: "smuggler" | "detective" +type CharacterId = String; + +struct CharacterDefinition { + id: CharacterId, + display_name: String, + /// Starting social site (determines spawn location) + home_template: TemplateId, + /// Starting access tiers for NPC interactions + default_access: Vec, +} +``` + +**Note:** Character selection is the one non-deterministic input — the player chooses. Everything else flows from `seed` + `player_character`. + +### 4.3 Pool Draws + +Pools are the core randomization mechanism. Each pool defines N candidates for a role; the seeder draws one. + +```rust +/// Pool identifier matching content pool definitions. +/// Format: "{scope}:{pool_name}" — e.g., "transit:friend_smuggler" +type PoolId = String; + +struct PoolDraw { + /// Which pool this draw came from + pool_id: PoolId, + + /// The selected candidate's NPC canonical ID + selected: NpcId, + + /// All candidates that were available (for debugging/replay verification) + candidates: Vec, + + /// Index into candidates that was selected (for replay verification) + selected_index: usize, +} +``` + +**v0.1 pools (single-candidate each):** + +| Pool ID | Selected | Purpose | +|---------|----------|---------| +| `transit:friend_smuggler` | `npc:kael-davan` | Smuggler's FRIEND (D-034) | +| `transit:friend_detective` | `npc:sera-venn` | Detective's FRIEND (D-034) | +| `transit:bar_regulars` | (set of NPCs) | Bar regular population | +| `transit:compromised_inspector` | `npc:torek-lintar` | The compromised Commission inspector | +| `transit:primary_contraband` | `contraband:lattice-components` | What's being smuggled | + +**v0.2+ expansion:** Pools grow to N candidates. `friend_smuggler` might offer 3 dock workers who could each be the FRIEND, with different contradiction arcs. The seeder draws one. Same schema, more candidates. + +**Pool categories:** + +```rust +enum PoolCategory { + /// Selects one NPC for a named narrative role + NpcRole, + /// Selects a set of NPCs for a group (bar regulars, shift workers) + NpcGroup, + /// Selects a contraband type + Contraband, + /// Selects an entanglement pattern (which NPCs are intrigue-connected) + EntanglementPattern, +} +``` + +### 4.4 Template Assignments + +Templates (D-025 social sites) define role slots; the seeder fills them with NPCs. + +```rust +/// Template identifier matching content template definitions. +/// Format: location slug — e.g., "logistics-hub", "bar", "smuggling-ring" +type TemplateId = String; + +struct TemplateAssignment { + template_id: TemplateId, + + /// Which location(s) this template is instantiated in + locations: Vec, + + /// Role slot → NPC assignments + role_assignments: BTreeMap, +} + +/// Role slot identifier from template definition. +/// Format: "{template}:{role}" — e.g., "logistics-hub:shift-supervisor" +type RoleSlotId = String; + +struct RoleAssignment { + /// The NPC assigned to this role slot + npc_id: NpcId, + + /// Whether this NPC is the primary owner of this template (D-025 single ownership) + is_owner: bool, + + /// If not owner, this is a reference link with relationship metadata + reference_metadata: Option, +} + +struct ReferenceLink { + /// The template that owns this NPC + owning_template: TemplateId, + + /// Why this NPC appears in this template (relationship context) + relationship: String, + + /// How many time phases this NPC spends at this template's location + presence_phases: Vec, +} +``` + +**Example — v0.1 Sova Transit District:** + +```yaml +# Logistics Hub template +template: logistics-hub +locations: [the-terminal] +roles: + shift-supervisor: + npc: npc:voss + is_owner: true + dock-worker-1: + npc: npc:kael-davan + is_owner: true + dock-worker-2: + npc: npc:drin + is_owner: true + new-hire: + npc: npc:renn + is_owner: true + scheduler: + npc: npc:maret-korr + is_owner: true + courier: + npc: npc:harek + is_owner: true + +# Bar template +template: bar +locations: [the-last-shift] +roles: + bar-owner: + npc: npc:lera-sessik + is_owner: true + bartender: + npc: npc:pell + is_owner: true + bar-regular-1: + npc: npc:sera-venn + is_owner: false + reference: + owning_template: null # Sera owns herself (Commission field tech, not bar staff) + relationship: "social anchor — evening regular" + presence_phases: [evening] + bar-regular-2: + npc: npc:resha + is_owner: true + +# Smuggling ring template +template: smuggling-ring +locations: [maintenance-corridors] +roles: + ring-operative: + npc: npc:devra + is_owner: true + contact: + npc: npc:kael-davan + is_owner: false + reference: + owning_template: logistics-hub + relationship: "ring member — dual role" + presence_phases: [night] +``` + +### 4.5 Triangle Configuration + +```rust +struct TriangleConfig { + /// All triangles instantiated in this seed + triangles: BTreeMap, + + /// Which triangles are initially active (storyteller can activate others later) + initially_active: Vec, +} + +/// Triangle identifier matching content triangle definitions. +type TriangleId = String; + +struct TriangleInstance { + triangle_id: TriangleId, + + /// The 3 NPCs assigned to this triangle's member slots. + /// Maps triangle role → NPC ID. + members: BTreeMap, + + /// Initial fork state. In v0.1: all triangles start at their default state. + /// In v0.2+: seed can randomize starting fork positions for variety. + initial_fork_state: Option, + + /// Whether this triangle is "active" (storyteller managing) or "passive" (running on its own) + activation_mode: TriangleActivationMode, +} + +enum TriangleActivationMode { + /// Storyteller actively manages fork progression based on player proximity + Active, + /// Triangle runs on background simulation, forks resolve without storyteller intervention + Passive, +} +``` + +**v0.1 triangle instances:** + +| Triangle | Members | Mode | Notes | +|----------|---------|------|-------| +| `hub-power` | Voss (authority), Kael (subordinate), Maret (caught-between) | Active | Workplace hierarchy tension | +| `worried-knowledge` | Sera (holder), Torek (subject), Naia (protected) | Active | Sera's unreported evidence | +| `bar-tensions` | Lera (owner), Resha (regular), Pell (bartender) | Passive | Mundane social friction | +| `worried-partner` | Kael (partner), Naia (worried), Devra (cause) | Active | Ring pressure on relationship | +| `informant-question` | Torek (inspector), Drin (worker), Olin (bystander) | Passive | Mundane workplace gossip | + +### 4.6 Entanglement Configuration + +```rust +struct EntanglementConfig { + /// The target ratio for this seed (varies around 30/50/20 per D-029) + target_ratio: EntanglementRatio, + + /// The actual ratio achieved after assignment (may differ slightly due to rounding) + actual_ratio: EntanglementRatio, + + /// Per-NPC entanglement tier assignment + npc_tiers: BTreeMap, + + /// Module attachment ratio: known vs stranger NPCs for intrigue connections + module_attachment_ratio: ModuleAttachmentRatio, +} + +struct EntanglementRatio { + /// Percentage of NPCs that are truly flat (routine + greeting only) + flat_pct: u8, + /// Percentage of NPCs in mundane triangles (no conspiracy connection) + mundane_pct: u8, + /// Percentage of NPCs entangled with intrigue content + entangled_pct: u8, +} + +struct ModuleAttachmentRatio { + /// Percentage of intrigue-connected NPCs that are known to the player character + known_pct: u8, + /// Percentage that are strangers + stranger_pct: u8, +} + +enum EntanglementTier { + /// Routine + greeting, no triangle membership, social wallpaper + Flat, + /// Member of mundane triangle(s), no conspiracy connection + Mundane, + /// Connected to intrigue content (ring member, compromised, witness, etc.) + Entangled, +} +``` + +**v0.1 entanglement breakdown (17 NPCs):** + +| Tier | Count | Pct | NPCs | +|------|-------|-----|------| +| Flat | 5 | 29% | Renn, Harek, Sess, Sabel, Tav | +| Mundane | 9 | 53% | Voss, Lera, Pell, Resha, Maret, Drin, Olin, Naia, Torek* | +| Entangled | 3 | 18% | Kael (ring), Devra (ring), Sera (witness) | + +*Torek straddles mundane/entangled — he's compromised (entangled) but his triangle surface reads as mundane institutional friction. The seed config records him as entangled; the player discovers this through gameplay.* + +**Revised breakdown with Torek entangled:** + +| Tier | Count | Pct | NPCs | +|------|-------|-----|------| +| Flat | 5 | 29% | Renn, Harek, Sess, Sabel, Tav | +| Mundane | 8 | 47% | Voss, Lera, Pell, Resha, Maret, Drin, Olin, Naia | +| Entangled | 4 | 24% | Kael, Devra, Sera, Torek | + +This lands at 29/47/24 — within D-029's variable range around 30/50/20. + +### 4.7 Contraband Selection + +```rust +struct ContrabandSelection { + /// Primary contraband type for this seed + primary: ContrabandType, + + /// Secondary contraband types available (for variety in future seeds) + secondary: Vec, +} + +struct ContrabandType { + /// Identifier matching content/global/knowledge/contraband.yaml + id: String, + + /// Display name for content systems (dialogue lines reference this) + display_name: String, + + /// What the ring calls it internally (used in insider-access dialogue) + ring_codename: String, + + /// Moral valence — affects monologue tone when player discovers it + moral_ambiguity: MoralAmbiguity, +} + +enum MoralAmbiguity { + /// Clearly wrong (weapons, poisons) + Clear, + /// Morally complex (medical supplies, access tech) + Ambiguous, + /// Arguably justified (survival supplies, freedom tech) + Sympathetic, +} +``` + +**v0.1:** Single contraband type — unlicensed lattice components (D-037). `moral_ambiguity: Ambiguous`. The ring is smuggling *access*, not weapons. + +### 4.8 Starting Knowledge + +```rust +/// Per-character starting knowledge state. +/// Loaded into KnowledgeGraph components at entity creation time (D-041). +struct StartingKnowledge { + /// Facts this character knows at game start + facts: Vec, + + /// Entity knowledge at game start (NPCs the character already knows about) + entities: Vec, +} + +struct StartingFact { + /// Fact ID from content/global/knowledge/*.yaml + fact_id: String, + /// Starting confidence level + confidence: ConfidenceLevel, + /// Source of this knowledge + source: KnowledgeSource, +} + +struct StartingEntityKnowledge { + /// NPC stable ID + entity_id: NpcId, + /// What attributes the character knows about this NPC at start + known_attributes: BTreeMap, + /// Starting confidence + confidence: ConfidenceLevel, + /// Source + source: KnowledgeSource, +} + +/// Maps to D-041's 4-level hierarchy +enum ConfidenceLevel { + Suspects, + KnowsOf, + KnowsDetails, + Direct, +} + +enum KnowledgeSource { + /// Character background — they knew this before game start + Background, + /// Institutional knowledge — comes with the job + Institutional, +} +``` + +**Smuggler starting knowledge:** +- Knows colleagues at logistics hub (KnowsOf: Voss, Drin, Renn, Maret, Harek) +- Knows FRIEND deeply (KnowsDetails: Kael) +- Knows bar regulars casually (Suspects: Lera, Pell) +- Knows ring exists, knows Devra (KnowsDetails: Devra, ring operations) +- Does NOT know Sera, Torek, or Commission personnel (detective's world) +- Knows contraband type (KnowsDetails: lattice components) + +**Detective starting knowledge:** +- Knows Commission chain of command (KnowsOf: Torek) +- Knows FRIEND (KnowsDetails: Sera) +- Knows bar casually (Suspects: Lera — goes there off-duty) +- Knows assignment briefing (Suspects: smuggling activity on Sova) +- Does NOT know ring members, specific smugglers, or insider logistics operations +- Does NOT know contraband type (investigation target) + +## 5. File Format and Location + +### 5.1 Authored content (input to seeder) + +Lives in `content/` under the campaign hierarchy. Relevant files: + +``` +content/ +├── global/ +│ └── knowledge/ +│ └── contraband.yaml # Contraband type definitions +├── campaigns/main/systems/krenn/stations/sova/districts/transit/ +│ ├── npcs/*.yaml # NPC profiles (candidates for pool draws) +│ ├── triangles/*.yaml # Triangle definitions (instantiated by seed) +│ ├── locations/*.yaml # Location definitions +│ ├── routines/schedules.yaml # NPC daily schedules +│ └── pools.yaml # Pool definitions (NEW — ticket #389) +``` + +### 5.2 Pool definition format (content/...pools.yaml) + +```yaml +# Pool definitions for Sova Transit District +# Each pool defines N candidates for a named role. +# The seeder draws from these pools using the seed value. + +pools: + - pool_id: "transit:friend_smuggler" + category: npc_role + description: "Smuggler's FRIEND — closest colleague, emotional anchor" + constraints: + - must_be_in_template: "logistics-hub" + - must_have_pattern: "FRIEND" + - bonded_character: "smuggler" + candidates: + - npc_id: "npc:kael-davan" + weight: 1 # v0.1: only candidate + # v0.2+: additional candidates with different contradiction arcs + + - pool_id: "transit:friend_detective" + category: npc_role + description: "Detective's FRIEND — social anchor, information holder" + constraints: + - bonded_character: "detective" + candidates: + - npc_id: "npc:sera-venn" + weight: 1 + + - pool_id: "transit:compromised_inspector" + category: npc_role + description: "The Commission inspector compromised by the ring" + constraints: + - must_have_access: "authority" + candidates: + - npc_id: "npc:torek-lintar" + weight: 1 + + - pool_id: "transit:primary_contraband" + category: contraband + description: "What the ring is smuggling" + candidates: + - id: "contraband:lattice-components" + weight: 1 +``` + +### 5.3 Generated seed config (runtime output) + +**Format:** RON (Rusty Object Notation) for Rust-native deserialization. Mirrors the Rust structs from section 4. + +**Location:** Embedded in save files. Not a standalone file during gameplay — the seeder generates it in memory, the ECS consumes it, and the save system serializes it alongside game state. + +**Debug dump location:** `runtime/debug/seed-config-{seed}.ron` — written only in debug builds or when `--dump-seed` flag is passed. Useful for content authors testing pool behavior. + +### 5.4 Schema file + +A JSON Schema for validating pool definition files goes into `content/_schema/pools.schema.json`. The seed config itself is validated at the Rust type level (serde deserialization), not via JSON Schema. + +## 6. Generation Algorithm + +### 6.1 Seeder pipeline + +``` +Input: seed: u64, player_character: CharacterId, content: LoadedContent +Output: SeedConfig + +1. Initialize deterministic RNG from seed + └── Use `rand_chacha::ChaCha20Rng::seed_from_u64(seed)` + └── ChaCha20 is platform-independent, deterministic, cryptographically strong + +2. Draw pool selections (order: alphabetical by pool_id for determinism) + ├── For each pool in sorted order: + │ ├── Compute weighted random selection from candidates + │ ├── Record PoolDraw { selected, candidates, selected_index } + │ └── Advance RNG state (consumed regardless of pool size) + └── Validate: no NPC selected for conflicting roles + +3. Assign templates + ├── For each template in sorted order: + │ ├── Fill mandatory role slots from pool draws + │ ├── Fill remaining slots from available NPCs (weighted by fit) + │ ├── Record ownership (first template assigned = owner) + │ └── Create reference links for cross-template NPCs + └── Validate: every NPC has exactly one owning template + +4. Configure triangles + ├── Map pool-drawn NPCs into triangle member slots + ├── Determine activation mode per triangle + │ └── Active if any member is entangled; passive otherwise + └── Set initial fork states (v0.1: all default) + +5. Compute entanglement + ├── Generate target ratio (vary around 30/50/20 using seed RNG) + │ └── flat_pct: 25-35 (uniform draw) + │ └── entangled_pct: 15-25 (uniform draw) + │ └── mundane_pct: 100 - flat - entangled + ├── Classify NPCs: pool draws determine entangled set, triangle + │ membership determines mundane, remainder is flat + └── Record actual ratio achieved + +6. Select contraband + └── Draw from contraband pool (v0.1: single candidate) + +7. Generate starting knowledge + ├── For each character: + │ ├── Background knowledge from character definition + │ ├── Institutional knowledge from character role + │ ├── Social knowledge from template assignments + │ │ └── Character knows NPCs in their home template (KnowsOf) + │ │ └── Character knows FRIEND deeply (KnowsDetails) + │ └── Investigation knowledge (detective only: assignment briefing) + └── Validate: no character knows things they shouldn't + +8. Assemble SeedConfig and return +``` + +### 6.2 Determinism guarantees + +The seeder MUST produce identical output given identical inputs. This requires: + +1. **Platform-independent RNG:** ChaCha20 (not platform `thread_rng`). Same byte stream on Linux, macOS, Windows. +2. **Sorted iteration:** All collections iterated in sorted order (BTreeMap handles this; Vec collections must be pre-sorted or iteration order must be specified). +3. **No floating-point in selection logic:** Weights are integers. Selection uses integer arithmetic only. +4. **Content version pinning:** The `content_version` field detects if content changed between save and load. +5. **RNG consumption order:** The RNG advances in a fixed order regardless of pool sizes or skip conditions. This prevents "butterfly effect" where adding a candidate to one pool shifts all subsequent draws. + +### 6.3 RNG consumption protocol + +To prevent butterfly effects when pools change size between content versions: + +``` +For each pool (sorted alphabetically): + 1. Consume exactly `max_candidates` RNG values (configurable per pool, default 8) + 2. Use the first consumed value to select from actual candidates + 3. Remaining consumed values are discarded + +This means adding a candidate to pool A doesn't shift the RNG +sequence for pool B. +``` + +**v0.1 simplification:** With single-candidate pools, all draws are deterministic regardless. The protocol matters for v0.2+ when pools have real variation. + +## 7. Integration Points + +### 7.1 Who writes seed configs + +| Component | Responsibility | +|-----------|---------------| +| **Seeder system** (Rust, `server/src/simulation/seeder.rs`) | Generates `SeedConfig` from seed + content | +| **Save system** (Rust) | Serializes `SeedConfig` into save files | +| **Content loader** (Rust, ticket #408) | Reads pool definitions from content YAML | +| **Debug CLI** (Rust, `--dump-seed` flag) | Writes debug RON dump | + +### 7.2 Who reads seed configs + +| Component | What it reads | Why | +|-----------|--------------|-----| +| **Entity spawner** | Pool draws, template assignments | Creates ECS entities with correct components | +| **Knowledge initializer** | Starting knowledge | Populates KnowledgeGraph components (D-041) | +| **Storyteller** | Triangle config, entanglement | Knows which triangles to manage, which NPCs are intrigue-relevant | +| **Save/load** | Full SeedConfig | Restores game state from save | +| **Replay system** | SeedMeta | Verifies deterministic reproduction | +| **Line previewer** (ticket #407) | Full SeedConfig | Simulates line selection for a given seed | + +### 7.3 Content author workflow + +1. Author writes NPC profiles, templates, triangles, pools in `content/` +2. Author runs `make validate-content` to check schemas +3. Author runs line previewer (`tooling/line-previewer`) with a test seed to verify line selection +4. Author can inspect seed config via `--dump-seed` to verify NPC assignments match expectations + +## 8. v0.1 vs v0.2+ Scope + +| Aspect | v0.1 | v0.2+ | +|--------|------|-------| +| Pool candidates | 1 per pool (deterministic) | N per pool (randomized) | +| Entanglement ratio | Fixed at ~29/47/24 | Variable per seed (25-35 / 40-55 / 15-25) | +| Contraband types | 1 (lattice components) | 3+ with different moral valences | +| Triangle initial states | All default | Seed-randomized starting positions | +| Module attachment | Fixed known/stranger split | Variable per seed (D-029: 60-70/30-40) | +| Starting knowledge | Hardcoded per character | Generated from character definition + pool draws | +| Cross-district pools | N/A (one district) | NPCs can be drawn across district boundaries | +| Template variants | Fixed templates | Template variants (same social site, different layouts) | + +## 9. Validation Rules + +The seeder validates invariants after generation: + +```rust +fn validate(config: &SeedConfig, content: &LoadedContent) -> Result<(), SeedError> { + // 1. Every NPC has exactly one owning template + assert_single_ownership(&config.template_assignments)?; + + // 2. FRIEND NPCs are assigned to the correct character + assert_friend_bonds(&config.pool_draws)?; + + // 3. Triangle members match NPC assignments + assert_triangle_consistency(&config.triangle_config, &config.template_assignments)?; + + // 4. Entanglement ratio is within acceptable range + assert_entanglement_bounds(&config.entanglement)?; + + // 5. No circular ownership in reference links + assert_no_circular_refs(&config.template_assignments)?; + + // 6. Starting knowledge respects information boundaries + // (smuggler doesn't know detective-only facts, etc.) + assert_knowledge_boundaries(&config.starting_knowledge, content)?; + + // 7. All NPC IDs reference valid content profiles + assert_npc_ids_valid(&config, content)?; + + // 8. Content version matches + assert_content_version(&config.meta, content)?; + + Ok(()) +} +``` + +## 10. Serialization Format + +The `SeedConfig` serializes to RON for save files and debug dumps: + +```ron +SeedConfig( + meta: SeedMeta( + seed: 42, + content_version: "0.1.0", + schema_version: 1, + generated_at: "2026-02-13T10:00:00Z", + ), + character_selection: CharacterSelection( + player_character: "smuggler", + available_characters: [ + CharacterDefinition( + id: "smuggler", + display_name: "Dock Worker", + home_template: "logistics-hub", + default_access: [Public, Insider], + ), + CharacterDefinition( + id: "detective", + display_name: "Commission Investigator", + home_template: "logistics-hub", + default_access: [Public, Authority], + ), + ], + ), + pool_draws: { + "transit:compromised_inspector": PoolDraw( + pool_id: "transit:compromised_inspector", + selected: "npc:torek-lintar", + candidates: ["npc:torek-lintar"], + selected_index: 0, + ), + "transit:friend_detective": PoolDraw( + pool_id: "transit:friend_detective", + selected: "npc:sera-venn", + candidates: ["npc:sera-venn"], + selected_index: 0, + ), + "transit:friend_smuggler": PoolDraw( + pool_id: "transit:friend_smuggler", + selected: "npc:kael-davan", + candidates: ["npc:kael-davan"], + selected_index: 0, + ), + "transit:primary_contraband": PoolDraw( + pool_id: "transit:primary_contraband", + selected: "contraband:lattice-components", + candidates: ["contraband:lattice-components"], + selected_index: 0, + ), + }, + // ... (template_assignments, triangle_config, etc.) +) +``` + +## 11. Crate Dependencies + +```toml +# In server/Cargo.toml or wherever the seeder lives +[dependencies] +serde = { version = "1", features = ["derive"] } +ron = "0.8" # RON serialization +rand = "0.8" # RNG traits +rand_chacha = "0.3" # Platform-independent deterministic RNG +``` + +No new dependencies beyond what the server already uses. `rand` and `serde` are existing deps. `ron` and `rand_chacha` are standard Rust ecosystem crates with no transitive bloat. + +## 12. Open Questions + +| # | Question | Impact | Suggested Resolution | +|---|----------|--------|---------------------| +| 1 | Should the seed config include routine schedule overrides, or should routines be purely content-driven? | If the seeder can modify routines (e.g., FRIEND's deviation schedule depends on which FRIEND was drawn), routines become partially generated. If not, content must pre-author all variants. | Content-driven for v0.1 (one candidate = one routine). v0.2+: seeder generates routine deviations based on drawn contradiction arcs. | +| 2 | Should `max_candidates` (RNG consumption budget per pool) be configurable per pool or global? | Per-pool allows fine-grained control but complicates the protocol. Global is simpler but wastes RNG state for small pools. | Global default of 8, with per-pool override in pool definition YAML. 8 handles up to 8 candidates without waste, which covers v0.2 comfortably. | +| 3 | How does the seed config interact with the storyteller's module activation system (D-023 Tier 1)? | The storyteller needs to know which Tier 1 modules are *available* (not yet activated) vs *activated* vs *completed*. Does the seed config pre-select available modules, or does the storyteller draw from its own pool at runtime? | Seed config pre-selects *available* modules from a module pool. Storyteller activates them based on player proximity. This keeps all randomization in the seeder for determinism. | +| 4 | Should the debug dump include a human-readable narrative summary (e.g., "Kael Davan is the smuggler's FRIEND, smuggling lattice components...")? | Useful for content authors, trivial to generate, but adds code surface. | Yes. Add a `summary: String` field to `SeedMeta` generated at dump time. Not serialized into save files. | + +## 13. Implementation Sequence + +This is a design document. Implementation is Sprint 5+. Suggested build order: + +1. **Rust types** — Define `SeedConfig` and all sub-structs with serde derives. ~1 day. +2. **Pool definition schema** — `pools.schema.json` in `content/_schema/`. ~0.5 day. +3. **Pool loader** — Extend content loader to parse pools.yaml. ~1 day. +4. **Seeder system** — `server/src/simulation/seeder.rs`. Core generation logic. ~2 days. +5. **Validation** — Invariant checks from section 9. ~1 day. +6. **Save integration** — Serialize/deserialize SeedConfig in save system. ~0.5 day. +7. **Debug dump** — `--dump-seed` CLI flag. ~0.5 day. +8. **Starting knowledge generation** — Section 4.8 logic. ~1 day. + +**Total estimate:** ~7-8 developer-days. Parallelizable with content authoring work. + +--- + +*Design document for ticket #394. Implementation deferred to Sprint 5+.* +*Cross-references: D-010, D-024, D-025, D-027, D-029, D-034, D-035, D-036, D-037, D-041.*