Files
settled-reach/docs/workshops/wiki-review/round1-tyre.md
T
jpmschweitzerandClaude Opus 4.6 b672b9c269 feat(docs): restructure wiki taxonomy with templates and Krenn System entry (#301, #319)
Move wiki from docs/wiki/ to root wiki/. Add 14 category templates with
YAML frontmatter, governance model, hierarchical spatial structure
(system > station > district > location). Populate Krenn System entry
as first concrete star system. Update cross-references in workshop and
sprint docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:58:45 +01:00

23 KiB

Wiki Review Workshop — Round 1: Tyre (Technical Architect)

Topics: 4 (Taxonomy Nesting) and 5 (Content Directory Structure + Mod Overlay)


Topic 4: Wiki Taxonomy Hardening

The Problem, Stated Precisely

We have 45 wiki files today. The v0.1 vertical slice has 1 system, 1 station, 1 district, 3 social sites, 17 NPCs. When we add a second system — or a second station in Krenn — every generic name collides. "The Terminal" exists on every logistics station. "Maintenance Corridors" is universal. "Drin" is a common enough name to recur.

The question isn't whether to namespace. It's how deep to nest before the directory structure becomes a tax on contributors instead of an aid.

Proposal: Two Rules, One Principle

Principle: The filesystem is navigation, not identity. Unique identity lives in content frontmatter (YAML/markdown metadata). The directory path is for humans browsing the tree. It should be shallow enough to tab-complete and deep enough to disambiguate.

Rule 1: Maximum 2 levels of spatial nesting for locations.

locations/
  {system}/
    {location-slug}.md

Current state (locations/krenn-system/the-terminal.md) already follows this. Good. Don't go deeper.

Why not 3 levels (locations/{system}/{station}/{slug}.md)? Because:

  • Tab-completing a 4-segment path is annoying. Contributors will get the nesting wrong.
  • Station context belongs in the file's frontmatter, not the path. A location is in a station, but it's addressed by system + slug.
  • Most systems have 1-3 stations. Nesting buys almost nothing for the navigation cost.

If two locations in the same system share a slug (e.g., two stations both have "The Terminal"), disambiguate the slug: the-terminal-sova.md vs the-terminal-orbital.md. The frontmatter carries the canonical name.

Example — v0.2 adds a second system (Tavros):

locations/
  krenn-system/
    the-terminal.md           # Station Sova's logistics hub
    the-last-shift.md
    maintenance-corridors.md
  tavros-system/
    the-terminal-central.md   # Tavros Central Station's hub
    the-strip.md              # Bar district
    dry-dock-corridors.md

No restructuring of existing files. New system = new directory. Done.

Rule 2: NPCs, factions, technology, contraband, and knowledge are FLAT until collision.

npcs/
  kael-davan.md      # unique name → flat
  sera-venn.md
  drin.md            # unique in v0.1

# If v0.2 introduces a second "Drin":
npcs/
  drin.md            # rename to drin-sova.md
  drin-sova.md       # Krenn System Drin (retroactive rename)
  drin-tavros.md     # Tavros System Drin

Factions are inherently global — factions/lattice-commission.md never collides. Technology and contraband are likewise global concepts.

Wait — do we need the retroactive rename? Yes. But only on actual collision, which is rare. 17 NPCs fit flat. 50 NPCs probably fit flat. At 100+ NPCs across 3+ systems, you'll get collisions, but they'll be individual renames, not restructures.

If the team wants to avoid any future renames, we could namespace NPCs by district from the start:

npcs/
  sova-transit/
    kael-davan.md
    drin.md
  tavros-central/
    drin.md

I don't recommend this for v0.1 (17 NPCs, one district, unnecessary nesting). But I acknowledge it's a valid choice if the team prefers "never rename" over "shallow paths now."

Rule 3: World hierarchy is flat with cross-references, not nesting.

world/
  krenn-system.md              # links to station-sova.md
  station-sova.md              # links to sova-transit-district.md
  sova-transit-district.md     # links to location pages

The spatial hierarchy lives in the content (wiki cross-links), not the directory. This is the same principle as Rule 1 — identity in content, navigation in paths. If we nested world/krenn-system/station-sova/sova-transit-district.md, contributors would drown in path segments. Three flat files with hyperlinks do the job better.

Scaling Analysis

Content growth Current structure works? Change needed?
v0.1 (1 system, 17 NPCs) Yes None
v0.2 (2 systems, ~40 NPCs) Yes New locations/{system}/ dir only
v1.0 (5+ systems, 100+ NPCs) Mostly Occasional NPC slug disambiguation
DLC/mod content Yes (see Topic 5) Content packs are separate trees

The structure survives because the nesting is shallow and disambiguation is on-demand. The only painful moment is if two NPCs share a first name across systems and we rename files — but that's a single git mv, not a restructure.

Summary: Taxonomy Rules

  1. Locations: locations/{system}/{slug}.md — max 2 levels. Disambiguate slug on collision.
  2. NPCs: npcs/{slug}.md — flat. Disambiguate with {slug}-{district}.md on collision only.
  3. Factions, technology, contraband, knowledge: flat. No foreseeable collision risk.
  4. World: flat with cross-references. Hierarchy is content, not structure.
  5. Authoring guides: authoring/{slug}.md — flat. Few files, no collision risk.

Topic 5: Content Directory Structure & Mod Overlay

cracks knuckles

This is the architectural meat. The wiki (wiki/) is the authoring source of truth — human-readable, team-navigable. The content directory (content/) is the engine-readable delivery format — YAML, schema-validated, loaded at runtime. They mirror each other structurally but serve different consumers.

Architecture: Three-Layer Content Stack

Layer 3 (highest priority):  mods/       # User mods (override/extend)
Layer 2:                     content/dlc/ # Official DLC packs
Layer 1 (base):              content/     # Base game

Later layers override or extend earlier layers. The engine walks the stack top-down, resolving each content address to the highest-priority file.

Proposed content/ Directory Structure

content/
  _meta/
    manifest.yaml              # Pack metadata: version, id, dependencies
    load-order.yaml            # Explicit load ordering for multi-pack scenarios

  _schema/
    npc.schema.yaml            # Validation schema per content type
    location.schema.yaml
    faction.schema.yaml
    template.schema.yaml
    fact.schema.yaml
    dialogue.schema.yaml
    monologue.schema.yaml
    district.schema.yaml

  global/
    factions/
      concord-assembly.yaml
      lattice-commission.yaml
      syndics.yaml
      the-ring.yaml
      guardians-of-autonomy.yaml
      veil-institute.yaml
      the-unbound.yaml
    technology/
      neural-lattice.yaml
      meridian.yaml
      span-gates.yaml
      founder-gates.yaml
      clone-transfer.yaml
      severance-tech.yaml
    contraband/
      lattice-components.yaml
      medical-grade-replacements.yaml
      severance-equipment.yaml
    knowledge/
      facts.yaml               # All FactId definitions (24 for v0.1)
      entity-attributes.yaml   # Canonical EntityKnowledge keys
      relationship-states.yaml # RelationshipState enum reference

  districts/
    sova-transit/
      district.yaml            # District metadata, spatial refs, ambient config
      npcs/
        kael-davan.yaml        # 10-axis NPC definition
        sera-venn.yaml
        voss.yaml
        lera-sessik.yaml
        torek-lintar.yaml
        devra.yaml
        maret-korr.yaml
        resha.yaml
        hael.yaml
        renn.yaml
        pell.yaml
        harek.yaml
        drin.yaml
        sess.yaml
        olin.yaml
        sabel.yaml
        tav.yaml
      locations/
        the-terminal.yaml      # Spatial definition, sightlines, Meridian coverage
        the-last-shift.yaml
        maintenance-corridors.yaml
      templates/
        logistics-hub.yaml     # Social site: roles, triangles, NPC assignments
        bar.yaml
        smuggling-ring.yaml
      lines/
        terminal/
          dialogue.yaml
          monologue-smuggler.yaml
          monologue-detective.yaml
        bar/
          dialogue.yaml
          monologue-smuggler.yaml
          monologue-detective.yaml
        corridor/
          dialogue.yaml
          monologue-smuggler.yaml
          monologue-detective.yaml

Key Design Decisions

Why districts/ as the atomic content pack?

A district maps to D-025 (social site / functional cluster as atomic template unit). It's the natural unit of content authoring, the natural unit of mod creation, and the natural unit of engine loading. "Add a district" = drop a new directory under districts/. The engine discovers it.

The monologue guide already organizes content by location within a district (terminal, bar, corridor). The lines/ subdirectory matches this exactly.

Why global/ vs districts/?

Some content is inherently Reach-wide: factions, technology, contraband definitions, FactId vocabulary. An NPC references faction: "lattice-commission" — that faction definition lives in global/, not per-district. If a mod changes how the Commission works, it changes global/factions/lattice-commission.yaml once, and every district that references it picks up the change.

District-scoped content is everything that's spatially local: NPCs, locations, social site templates, dialogue/monologue pools.

Why lines/ not content/ or dialogue/?

Three reasons: (1) avoids recursive content/content/ path confusion, (2) "lines" is what authors call it — monologue lines, dialogue lines, (3) distinguishes textual content from structural definitions (NPC YAML is structural, monologue YAML is lines).

Why _schema/ and _meta/ with underscore prefix?

Infrastructure files sort to top of directory listings and are visually distinct from game content. Modders know not to touch underscore directories. Same convention as _config.yml in Jekyll, __init__.py in Python.

Mod Overlay: How It Works

A mod is a content pack with the same directory structure as content/. Mods live under mods/{mod-id}/.

Example: "Add Jax the bartender to The Last Shift"

mods/
  extra-bartender/
    _meta/
      manifest.yaml            # mod-id: extra-bartender, requires: base >= 0.1
    districts/
      sova-transit/
        npcs/
          jax.yaml             # New NPC definition (10-axis)
        templates/
          bar.yaml             # EXTENDS bar template (adds Jax to role list)
        lines/
          bar/
            dialogue.yaml      # EXTENDS dialogue pool (adds Jax's lines)

Three overlay operations:

Operation When Mechanic
ADD New file at a path that doesn't exist in base File is loaded as-is
REPLACE File at same path as base, entity-type content Entire file replaces base version
MERGE File at same path as base, pool-type content Lists are concatenated, deduplicated by id

The critical distinction: entity definitions REPLACE, line pools MERGE.

If a mod provides districts/sova-transit/npcs/kael-davan.yaml, it replaces Kael's entire 10-axis definition. You can't partially patch an NPC — that's a recipe for broken state.

If a mod provides districts/sova-transit/lines/bar/dialogue.yaml, its lines are appended to the base pool. Each line has a unique id — if a mod provides a line with an existing id, the mod's version wins (override by ID). New IDs are simply added. This means a modder can:

  • Add 5 new dialogue lines (new IDs) without touching existing content
  • Override a specific line (same ID, different text) without replacing the whole pool
  • Both in the same file

Merge semantics in YAML:

# Base: content/districts/sova-transit/lines/bar/dialogue.yaml
lines:
  - id: bar_d_001
    text: "What'll you have?"
    role: bartender
    access: [public]
    trust: surface
    situation: [bar_evening]

# Mod: mods/extra-bartender/districts/sova-transit/lines/bar/dialogue.yaml
lines:
  - id: bar_d_001           # OVERRIDE: same id, different text
    text: "Evening. The usual?"
    role: bartender
    access: [public]
    trust: surface
    situation: [bar_evening]

  - id: bar_d_200           # ADD: new id
    text: "Jax says the grain spirit's from a new batch. Smoother."
    role: bartender
    access: [peer]
    trust: surface
    situation: [social]

Load order resolution:

1. Discover all content packs: content/ (base), content/dlc/* (DLC), mods/* (user)
2. Read each _meta/manifest.yaml for dependencies and load-order hints
3. Topological sort by dependencies (base → DLC → mods, respecting inter-mod deps)
4. For each content address (e.g., districts/sova-transit/npcs/kael-davan.yaml):
   a. Entity types: last writer wins (highest priority pack)
   b. Pool types: merge all, deduplicate by line id (highest priority wins on collision)
5. Validate merged content against _schema/

Content Validation Pipeline

Proposal: Schema per content type, not one schema.

Each content type has its own YAML schema in _schema/. This is the right granularity because:

  • Different types have completely different shapes (NPC ≠ dialogue ≠ faction)
  • Schema changes are localized (adding a new NPC axis doesn't touch dialogue schema)
  • Mod validation checks only the types the mod provides

Validation runs at three points:

When What Speed
Author-time CLI validator: make validate-content <2 seconds for full base
CI Same validator, blocks merge on failure Same
Load-time Engine validates on startup, panics on schema violation ~50ms for v0.1 content

What the validator checks:

  1. Structural validity: YAML parses, required fields present, enums match allowed values.
  2. Cross-reference integrity:
    • NPC definitions reference valid FactIds from global/knowledge/facts.yaml
    • Dialogue prerequisites reference valid FactIds and entity slugs
    • Template role lists reference valid NPC slugs in the same district
    • Monologue character values are valid enum members
  3. Pool consistency:
    • No duplicate line IDs within a merged pool
    • All access and trust values are valid enums
    • All trigger types match the 9 canonical types
    • All situation, topic, mood values match canonical enums
  4. Completeness (warning, not error):
    • Every NPC in a template has at least N dialogue lines
    • Every location has at least one monologue line per character
    • Tier 1 NPCs meet minimum line counts from D-034

Schema format: YAML schemas in a subset of JSON Schema (YAML-formatted for readability, functionally equivalent). The Rust loader and the Python/CLI validator both consume the same schema files.

Example NPC schema (abbreviated):

# _schema/npc.schema.yaml
type: object
required: [id, name, tier, axes]
properties:
  id:
    type: string
    pattern: "^[a-z][a-z0-9_-]+$"
  name:
    type: string
  tier:
    type: integer
    enum: [1, 2, 3]
  district:
    type: string
  axes:
    type: object
    required: [want, routine, personality]
    properties:
      want: { type: string }
      secret: { type: string }
      relationships:
        type: array
        items: { type: object }
      tolerance: { type: string }
      routine: { type: string }
      information: { type: string }
      contentment: { type: string }
      personality: { type: string }
      tell: { type: string }
      skills: { type: string }
  voice_sample:
    type: array
    items: { type: string }
  dual_lens:
    type: object
    properties:
      smuggler: { type: string }
      detective: { type: string }

Hot-Reloading During Development

Feasibility tier: straightforward for line pools, moderate for entity definitions.

The Rust server watches content/ via notify crate (cross-platform filesystem events). On file change:

Content type Reload strategy Latency
Dialogue/monologue pools Full pool reload for the affected location <5ms
NPC definitions Update ECS component fields in-place <10ms
Location definitions Update spatial metadata <10ms
Templates Requires re-linking NPC-to-role assignments ~50ms
Global definitions (factions, facts) Full reload of global registry ~20ms
Schema changes Requires restart (schema drives parser) N/A

Line pool hot-reload is the money feature. Writers edit monologue-smuggler.yaml, save, and the next monologue trigger in-game uses the updated pool. No restart, no reload command. This is critical for the authoring workflow — Mellanie and Paula need to iterate on lines without leaving the game running.

Implementation sketch:

// Simplified hot-reload system
fn content_watcher_system(
    mut events: EventReader<FileChangedEvent>,
    mut pools: ResMut<DialoguePoolRegistry>,
    mut npcs: Query<&mut NpcDefinition>,
    schemas: Res<ContentSchemas>,
) {
    for event in events.read() {
        match categorize_path(&event.path) {
            ContentType::DialoguePool(location, pool_type) => {
                // Re-parse, re-validate, swap pool
                if let Ok(new_pool) = parse_and_validate(&event.path, &schemas) {
                    pools.replace(location, pool_type, new_pool);
                    info!("Hot-reloaded {} pool for {}", pool_type, location);
                }
            }
            ContentType::NpcDefinition(npc_id) => {
                // Re-parse, update matching ECS component
                if let Ok(new_def) = parse_and_validate(&event.path, &schemas) {
                    for mut npc in npcs.iter_mut() {
                        if npc.id == npc_id {
                            *npc = new_def;
                            break;
                        }
                    }
                }
            }
            // ... other types
        }
    }
}

Hot-reload is development-only (behind #[cfg(feature = "dev")]). Release builds load content once at startup and never watch the filesystem. This avoids the performance cost of file watching and the security surface of runtime file loading in shipped builds.

How Wiki and Content Relate

wiki/           → Human-authored source of truth (markdown)
                         ↓ (authoring pipeline: wiki → YAML)
content/             → Engine-readable delivery format (YAML)
                         ↓ (engine loads at startup)
Runtime ECS          → In-memory game state

The wiki is where decisions are made and content is authored. The content directory is where the engine looks. The pipeline between them can be:

  1. Manual (v0.1): Authors write YAML by hand, referencing wiki pages. The wiki and YAML coexist as parallel representations. The validator ensures they don't drift.
  2. Semi-automated (v0.2+): A conversion tool extracts structured data from wiki markdown (NPC axes, FactId definitions) into YAML. Authors maintain the wiki; the tool generates YAML.
  3. Full pipeline (v1.0): Wiki entries have embedded YAML frontmatter that IS the content definition. The wiki page is simultaneously the human-readable doc and the machine-readable source.

For v0.1, option 1 is correct. The content volume (17 NPCs, 3 locations, ~200 lines) doesn't justify pipeline tooling. Authors write both wiki markdown and content YAML. The schema validator catches inconsistencies.

Mod Structure: Complete Example

A modder who wants to "add a new NPC to the bar with custom dialogue" creates:

mods/
  jax-the-veteran/
    _meta/
      manifest.yaml
    districts/
      sova-transit/
        npcs/
          jax-korrenson.yaml
        templates/
          bar.yaml               # Extends: adds jax_korrenson to regular role
        lines/
          bar/
            dialogue.yaml        # Jax's dialogue lines (new IDs)
            monologue-smuggler.yaml   # Smuggler's observations of Jax
            monologue-detective.yaml  # Detective's observations of Jax

manifest.yaml:

id: jax-the-veteran
name: "Jax the Veteran"
version: "1.0.0"
author: "ModderName"
requires:
  - id: base
    version: ">=0.1.0"
description: "Adds Jax Korrenson, a retired military veteran, to The Last Shift."
content_types: [npc, template, dialogue, monologue]

jax-korrenson.yaml:

id: jax-korrenson
name: "Jax Korrenson"
tier: 3
district: sova-transit
axes:
  want: "Quiet drink, nobody asking questions about the war."
  routine: "Arrives at bar 18:00, corner booth, leaves 22:00."
  personality: "Gruff, observant, protective of regulars."
voice_sample:
  - "I've seen enough to know when someone's lying. You're not good at it."

dialogue.yaml (merge extension):

lines:
  - id: bar_d_300
    text: "You don't look like logistics. What's your story?"
    role: bar_regular
    access: [public]
    trust: surface
    situation: [bar_evening, social]
    topic: [personal]
    mood: [suspicious]

The engine loads this, validates it against _schema/, merges the line pool, assigns the NPC to the template, and Jax appears at the bar.

Feasibility Assessment

Component Difficulty Sprint estimate
Directory structure + schema definitions Easy 0.5 sprint
YAML content parser (Rust, serde_yaml) Easy 0.5 sprint
Schema validator CLI (make validate-content) Moderate 1 sprint
Mod discovery + load-order resolution Moderate 1 sprint
Merge semantics for line pools Moderate 0.5 sprint
Hot-reload (dev-only) Moderate 1 sprint
Cross-reference validation Hard 1 sprint

Total for full content pipeline: ~4.5 sprints. But we don't need all of it for v0.1:

v0.1 scope (1 sprint): Directory structure + YAML parser + basic schema validation. No mod overlay, no hot-reload. Content is loaded once at startup from content/.

v0.2 scope (2 sprints): Mod overlay (ADD + REPLACE), schema validator CLI, hot-reload for line pools.

v0.3+ scope (1.5 sprints): MERGE semantics, cross-reference validation, full mod pipeline.

Open Questions for the Team

  1. Gestalt/Nigel: The mod MERGE mechanic for line pools needs game design input. If a mod adds 50 dialogue lines to a pool of 20, the NPC talks like a mod 71% of the time. Should there be weighting? A max-lines-per-mod cap? Or is this the modder's problem?

  2. Paula/Mellanie: The lines/ directory uses location shortnames (terminal, bar, corridor). These come from the monologue guide. Are these stable identifiers you're comfortable with, or should they match the wiki slugs (the-terminal, the-last-shift, maintenance-corridors)?

  3. Gestalt: Is district the right atomic content pack unit? Or should it be template (social site)? A district contains multiple templates — packing at the district level means you can't add a single social site without the whole district. Packing at the template level is more granular but more complex.


Cross-Topic: How Wiki Taxonomy Maps to Content Structure

Wiki path Content path Notes
wiki/npcs/kael-davan.md content/districts/sova-transit/npcs/kael-davan.yaml NPC is district-scoped
wiki/locations/krenn-system/the-terminal.md content/districts/sova-transit/locations/the-terminal.yaml Location is district-scoped
wiki/factions/lattice-commission.md content/global/factions/lattice-commission.yaml Faction is global
wiki/knowledge/fact-catalog.md content/global/knowledge/facts.yaml All FactIds in one file
wiki/authoring/monologue-guide.md N/A (authoring reference, not engine content) Wiki-only
N/A content/districts/sova-transit/lines/terminal/dialogue.yaml Engine-only (authored YAML)

The mapping is consistent but not 1:1. Some wiki pages are references that don't produce engine content. Some engine content (line pools) doesn't have a wiki counterpart — it IS the content.


Tyre out. Ready for cross-pollination in Round 2.