Files
settled-reach/docs/architecture/seed-configuration-schema.md
T
jpmschweitzerandClaude Opus 4.6 d3b68012d9 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 <noreply@anthropic.com>
2026-02-13 01:19:34 +01:00

31 KiB

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<PoolId, PoolDraw>
├── template_assignments: BTreeMap<TemplateId, TemplateAssignment>
├── triangle_config: TriangleConfig
├── entanglement: EntanglementConfig
├── contraband: ContrabandSelection
└── starting_knowledge: BTreeMap<CharacterId, Vec<KnowledgeEntry>>

4. Schema Detail

4.1 Meta

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

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<CharacterDefinition>,
}

/// 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<AccessTier>,
}

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.

/// 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<NpcId>,

    /// 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:

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.

/// 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<LocationId>,

    /// Role slot → NPC assignments
    role_assignments: BTreeMap<RoleSlotId, RoleAssignment>,
}

/// 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<ReferenceLink>,
}

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<DayPhase>,
}

Example — v0.1 Sova Transit District:

# 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

struct TriangleConfig {
    /// All triangles instantiated in this seed
    triangles: BTreeMap<TriangleId, TriangleInstance>,

    /// Which triangles are initially active (storyteller can activate others later)
    initially_active: Vec<TriangleId>,
}

/// 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<String, NpcId>,

    /// 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<String>,

    /// 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

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<NpcId, EntanglementTier>,

    /// 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

struct ContrabandSelection {
    /// Primary contraband type for this seed
    primary: ContrabandType,

    /// Secondary contraband types available (for variety in future seeds)
    secondary: Vec<ContrabandType>,
}

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

/// 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<StartingFact>,

    /// Entity knowledge at game start (NPCs the character already knows about)
    entities: Vec<StartingEntityKnowledge>,
}

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<String, AttributeKnowledge>,
    /// 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)

# 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:

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:

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

# 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 schemapools.schema.json in content/_schema/. ~0.5 day.
  3. Pool loader — Extend content loader to parse pools.yaml. ~1 day.
  4. Seeder systemserver/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.