Files
settled-reach/docs/workshops/wiki-review/round2-tyre.md
T
jpmschweitzerandClaude Opus 4.6 0100b33635 docs(workshops): archive wiki review workshop (4 rounds + lead interview)
Long-term content strategy workshop: 300-world generator model,
cultural ingredients menu, three-system NPC architecture (9 patterns
x 6 motivations), Sacred/Profane/Middle Kingdom framework. 9 agents
across 4 rounds plus lead interview establishing the production path
from hand-authored Sova to generated 300 worlds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:18:56 +01:00

29 KiB

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

Cross-pollination synthesis. Responding to all Round 1 outputs with architectural reconciliation.


1. Unified Content Directory Structure

Gestalt proposed _global/ + districts/. I proposed _meta/ + _schema/ + global/ + districts/. The structures are 90% aligned. The 10% divergence matters because it signals moddability.

The principle: Underscore prefix = engine infrastructure (modders don't touch). No underscore = game content (modders override freely). Factions, technology, and contraband are game content. Schemas and manifests are infrastructure.

Reconciled Structure

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

  _schema/
    npc.schema.yaml                  # Per-type validation (see Section 3)
    location.schema.yaml
    faction.schema.yaml
    template.schema.yaml
    fact.schema.yaml
    dialogue.schema.yaml
    monologue.schema.yaml
    district.schema.yaml
    pool.schema.yaml                 # NEW: Pool definition validation

  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         # 14 canonical EntityKnowledge keys
      relationship-states.yaml       # RelationshipState enum reference
    enums/
      situations.yaml                # 13 situation values (from Gestalt's _global/schema/)
      topics.yaml                    # 9 topic values
      moods.yaml                     # 8 mood values
      triggers.yaml                  # 9 monologue trigger types
      access-tiers.yaml              # public/peer/insider/authority/hostile
      activities.yaml                # NEW: canonical routine activity enum
    regions/
      krenn-system.yaml              # Regional style reference (from Miri's brief)

  districts/
    sova-transit/
      district.yaml                  # District metadata, location list, faction presence
      pools.yaml                     # NEW: Pool definitions for seed-time selection
      npcs/
        kael-davan.yaml
        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
        the-last-shift.yaml
        maintenance-corridors.yaml
      templates/
        logistics-hub.yaml           # Social site: roles, slot counts, NPC assignments
        bar.yaml
        smuggling-ring.yaml
      triangles/                     # NEW: Separated from templates (Gestalt's proposal)
        hub-power.yaml
        worried-knowledge.yaml
        bar-tensions.yaml
        worried-partner.yaml
        informant-question.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

What changed from Round 1

Change Source Rationale
global/enums/ added Gestalt's _global/schema/ tag enums Enum definitions are game content (modders can extend), not schema infrastructure
triangles/ separated from templates/ Gestalt's Round 1 structure Triangles are relationship structures, templates are spatial/social structures. Separation lets mods add triangles without touching templates.
pools.yaml added Nigel's pool_eligible concept + Gore's FRIEND pool Seed-time selection needs explicit pool definitions (see Section 2)
global/enums/activities.yaml added Gestalt's routine YAML format Routine activity values need a canonical enum (see Section 4)
global/regions/ added Miri's Krenn brief Regional style data as engine-readable YAML, not just wiki docs
Underscore-prefix convention formalized Tyre Round 1, refined Clear signal: _foo = infrastructure, foo = content

Location shortcodes (Mellanie's dependency)

Mellanie correctly identified that location shortcodes are ambiguous (hub vs terminal, bar vs last_shift). The canonical shortcodes are the directory names under lines/:

Location Wiki slug Content directory Line ID prefix Rationale
The Terminal (Logistics Hub) the-terminal lines/terminal/ terminal_ "Terminal" is what workers call it. Not "hub."
The Last Shift (Bar) the-last-shift lines/bar/ bar_ "Bar" is the functional category. "Last Shift" is the proper name. Line IDs use function.
Maintenance Corridors / B-7 maintenance-corridors lines/corridor/ corridor_ Workers call it "B-7" or "the back käik." Line IDs use generic.

One name per location. Enforced in schema validation. The monologue guide should be updated to use these consistently — the current hub_m_010 vs terminal_m_001 inconsistency is a bug. terminal_m_ is canonical.


2. FRIEND Pool + Mod Overlay: Seed-Time Selection

Gore's principle is correct: THE FRIEND is never procedurally generated — but FRIEND selection from a pool of authored candidates is the right architecture for replayability. Nigel's pool_eligible flag is the mechanism.

How it works architecturally

districts/sova-transit/pools.yaml:

# Pool definitions for seed-time selection
# Engine draws from these at game start using seeded RNG

pools:
  # THE FRIEND selection — exactly 1 per character per playthrough
  friend_smuggler:
    description: "Smuggler's Tier 1 FRIEND NPC"
    select: 1
    candidates:
      - npc: kael-davan           # v0.1: only candidate
        weight: 1.0
      # v0.2+: additional candidates added here
      # - npc: renn
      #   weight: 1.0

  friend_detective:
    description: "Detective's Tier 1 FRIEND NPC"
    select: 1
    candidates:
      - npc: sera-venn            # v0.1: only candidate
        weight: 1.0

  # Social site population — variable count per seed
  bar_regulars:
    description: "Regular NPCs present at The Last Shift"
    select: 3-5
    required: [torek-lintar]      # Always present (triangle dependency)
    candidates:
      - npc: torek-lintar
        weight: 1.0
      - npc: olin
        weight: 1.0
      - npc: harek
        weight: 0.8
      # Mods add candidates here via MERGE

  # Contraband type (Nigel's moral shuffle)
  primary_contraband:
    description: "Primary contraband type for this playthrough"
    select: 1
    candidates:
      - type: lattice-components
        weight: 1.0
      # v0.2+: additional types
      # - type: medical-grade-replacements
      #   weight: 0.8
      # - type: severance-equipment
      #   weight: 0.5

  # Role assignment (Nigel's network shuffle)
  compromised_inspector:
    description: "Which NPC is the compromised inspector"
    select: 1
    candidates:
      - npc: drin
        weight: 1.0
      # v0.2+: additional candidates

How mods extend pools

A mod adds candidates via the MERGE mechanic. The mod provides districts/sova-transit/pools.yaml with only the pools it extends:

# Mod: jax-the-veteran
# File: districts/sova-transit/pools.yaml
pools:
  bar_regulars:
    candidates:
      - npc: jax-korrenson
        weight: 0.8

The engine merges this with the base pool definition. jax-korrenson is now a candidate for bar regular slots. The select: 3-5 and required: [torek-lintar] from the base remain unchanged.

Seed-time resolution

1. Engine loads all pool definitions (base + DLC + mods, merged)
2. Initialize seeded RNG from game seed
3. For each pool:
   a. Include all `required` candidates
   b. Draw remaining candidates up to `select` count, weighted by `weight`
   c. Non-selected candidates are either:
      - Absent from the district (pool_eligible NPCs with no other role)
      - Present but demoted to background (if they have non-pool roles)
4. Instantiate selected NPCs with full content
5. Validate: all triangle dependencies satisfied (all triangle members present)
6. If validation fails, re-draw with constraint satisfaction

FRIEND pool + content loading implications

When friend_smuggler selects kael-davan, the engine loads:

  • npcs/kael-davan.yaml (full 10-axis Tier 1 data)
  • All monologue lines with subject: kael_davan as a FRIEND prerequisite
  • The specific contradiction arc content

When (in v0.2+) it selects renn instead, it loads Renn's Tier 1 data and Renn's contradiction arc content. Both sets of content exist on disk simultaneously. The pool selects which one activates.

This means content storage is larger than content runtime. A district with 3 FRIEND candidates stores 3x the FRIEND content, but only 1x runs per playthrough. That's cheap — YAML text is tiny. The expensive resource is authoring time, not disk space. Gore's point stands: each FRIEND candidate is ~70-100 hand-authored lines.

v0.1 scope

For v0.1, every pool has exactly 1 candidate. No randomization — deterministic. But the pool INFRASTRUCTURE exists. v0.2 adds candidates without restructuring. This follows the D-009/D-010 principle: design for it now, build the simple version.

Feasibility: Easy. Pool definition parsing is a YAML file read. Seed-time selection is a weighted random draw. Constraint validation is a graph check. Total implementation: <0.5 sprint on top of the base content loader.


3. Schema Validation: Gestalt's Tables as Validators

Gestalt's hard/soft requirement tables from Round 1 are exactly what I need. Let me map them to the validation pipeline.

Gestalt's hard requirements → JSON Schema required fields

Gestalt requirement Schema enforcement
Routine must specify hourly location presence npc.schema.yaml: routine.schedule is required, items must have time, location, activity
Routine must reference canonical location slugs Cross-reference validation: location value must exist in districts/{district}/locations/
Every NPC must have faction from canonical set npc.schema.yaml: faction is required, enum from global/enums/factions.yaml
Every NPC must have role npc.schema.yaml: role is required, type: string
Secret must map to FactId Cross-reference validation: secret.fact_id must exist in global/knowledge/facts.yaml
Tell must map to behavior_flags value npc.schema.yaml: each tell entry requires flag field
Contradiction must specify contradiction_flagged value Tier 1 conditional: if tier == 1, contradiction.flag is required
Relationships must reference existing NPCs Cross-reference validation: all npc references resolve within district

Gestalt's soft requirements → Schema warnings (non-blocking)

Gestalt requirement Validation behavior
Personality traits should use established vocabulary Warning if trait not in global/enums/personality-traits.yaml
Voice samples should cover 2+ moods Warning if voice_sample array length < tier-minimum
Information inventory must distinguish knows/doesn't-know Warning if information.knows or information.unknown is empty
Contentment should use high/moderate/low vocabulary Warning if value not in {high, moderate-high, moderate, moderate-low, low}

Per-tier validation (Gestalt's checklists)

Rather than separate schema files per tier, I'd use a single schema with conditional validation:

# _schema/npc.schema.yaml (simplified)
type: object
required: [id, name, tier, role, faction, axes]
properties:
  tier:
    type: integer
    enum: [1, 2, 3]
  axes:
    type: object
    required: [want, routine, personality]     # Tier 3 minimum
    # Conditional: Tier 2+ requires all 10 axes
    # Conditional: Tier 1 requires contradiction arc

allOf:
  - if:
      properties: { tier: { const: 2 } }
    then:
      properties:
        axes:
          required: [want, secret, relationships, tolerance, routine,
                     information, contentment, personality, tell, skills]
        voice_sample:
          minItems: 2

  - if:
      properties: { tier: { const: 1 } }
    then:
      properties:
        axes:
          required: [want, secret, relationships, tolerance, routine,
                     information, contentment, personality, tell, skills]
        contradiction:
          required: [phases, flag, tell_progression]
        voice_sample:
          minItems: 5
        dual_lens:
          required: [smuggler, detective]

This is one schema file, tier-aware. The validator reads the tier field and applies the right constraints.

Implementation cost (revised from Round 1)

Component Effort Sprint
Schema definitions (from Gestalt's tables) 2-3 days v0.1
Structural validation (make validate-content) 2-3 days v0.1
Cross-reference validation (FactId, NPC slug resolution) 3-4 days v0.2
Per-tier conditional validation 1-2 days v0.1
Warning-level soft validation 1 day v0.2

v0.1 total: ~1 sprint. Structural + tier-conditional validation. Cross-reference validation deferred to v0.2 — for v0.1, the human review catches reference errors (only 17 NPCs, 24 FactIds, manageable).

Mellanie's prerequisite validation (FactId typo detection) lands in the cross-reference sprint. Agreed it's critical — contraband.ring_exist vs contraband.ring_exists is the kind of bug that wastes hours. But for v0.1, a simple grep-based pre-commit check is cheaper than a full cross-reference validator. I'd do both: grep check in v0.1, schema cross-reference in v0.2.


4. Routine YAML Format + Cultural Schedule Patterns

Gestalt's structured routine block is the right format. Miri's cultural brief provides the content vocabulary. Here's how they connect.

Gestalt's format, canonicalized

# In npcs/kael-davan.yaml
routine:
  schedule:
    - time: "06:00-06:30"
      location: terminal             # Canonical shortcode
      activity: shift_startup        # Canonical activity enum
    - time: "06:30-10:30"
      location: terminal
      activity: work_freight
    - time: "10:30-11:00"
      location: terminal
      activity: social_break         # Renamed from "social_lunch" for generality
    - time: "11:00-14:00"
      location: terminal
      activity: work_freight
    - time: "14:30-16:00"
      location: bar
      activity: social_offshift      # The "shift-end" culture pattern (Miri)
    - time: "16:00-22:00"
      location: residential
      activity: home
    - time: "22:00-06:00"
      location: residential
      activity: sleep
  deviations:
    - trigger: ring_operation
      replaces: "22:00-06:00"        # Replaces sleep block
      schedule:
        - time: "22:00-23:30"
          location: corridor
          activity: ring_operation
        - time: "23:30-06:00"
          location: residential
          activity: sleep
      frequency: irregular           # Storyteller-controlled

Canonical activity enum (global/enums/activities.yaml)

Derived from Gestalt's examples + Miri's cultural patterns:

# global/enums/activities.yaml
activities:
  # Work activities
  - shift_startup          # Arriving, checking in, prep
  - work_freight           # Core logistics work
  - work_maintenance       # Maintenance/repair tasks
  - work_admin             # Office/scheduling work (Maret, Voss)
  - work_inspection        # Inspection rounds (Drin)
  - work_commission        # Commission field work (Sera)

  # Social activities
  - social_break           # On-shift break (lunch, kuum)
  - social_offshift        # Post-shift bar visit (the "shift-end" pattern)
  - social_evening         # Evening socializing (card games, conversation)
  - social_errand          # Off-shift personal tasks

  # Domestic
  - home                   # At residential quarters
  - sleep                  # Sleeping (low interaction priority)

  # Ring activities (deviation-only, never in base schedule)
  - ring_operation         # Active smuggling work
  - ring_meeting           # Coordination with ring members
  - ring_lookout           # Watchpost duty (Tav)

  # Special
  - idle                   # No scheduled activity
  - patrol                 # Security rounds (Harek on duty)

How Miri's cultural patterns inform schedule authoring

Miri's brief describes the 5+2 work cycle and shift-end bar migration as Krenn cultural patterns. These aren't engine data — they're authoring guidance:

Cultural pattern Schedule implication Authoring rule
5+2 work cycle NPCs work 5 shifts, then 2 off Routine must define both on-cycle and off-cycle days (v0.2+, when multi-day cycles matter)
Shift-end bar migration After shift, workers go to The Last Shift Most Tier 2-3 hub NPCs should have social_offshift at bar
Kuum at transitions Hot drink at shift boundaries social_break activity at shift start/end = where the casual dialogue fires
Card game evenings Kolm at the bar, Harek's regular game Harek + 2-3 regulars with social_evening at bar overlapping

The cultural brief lives in global/regions/krenn-system.yaml as reference data. It doesn't drive the engine directly — it drives the AUTHORS who fill in the routine YAML.

Engine consumption

The ScheduleSystem in the Rust server reads the routine YAML as:

struct ScheduleEntry {
    start_tick: u64,     // Converted from time string
    end_tick: u64,
    location: LocationId, // Resolved from shortcode
    activity: Activity,   // Enum from activities.yaml
}

struct NpcSchedule {
    base: Vec<ScheduleEntry>,
    deviations: Vec<ScheduleDeviation>,
}

Time strings ("06:00-14:00") are converted to tick ranges at load time using D-031's mapping (10 ticks = 1 game-minute). location shortcodes resolve against the district's location registry. activity values are validated against the canonical enum.

Deviation triggers (like ring_operation) are fired by the storyteller system, not the schedule system. The schedule system just needs to know "when trigger X fires, replace block Y with schedule Z." The storyteller decides when to fire the trigger.

Feasibility: Straightforward. This is a data-driven schedule with event-driven overrides. bevy_ecs handles this naturally — NpcSchedule is a component, the ScheduleSystem runs every tick, checks current time against entries, and moves NPCs. Deviations are applied when the storyteller emits the trigger event.


5. Wiki Taxonomy + Miri's canonical_id

Miri proposed two things I want to address: wiki/cultural-groups/ as a new directory, and canonical_id in YAML frontmatter for collision-safe cross-referencing.

cultural-groups/ — fits perfectly

wiki/cultural-groups/krenn-system.md is a flat directory. No nesting. Exactly like factions/, technology/, contraband/. This is a new top-level wiki category, not a nesting violation. Add it.

The wiki index should add:

### Cultural Groups (Regional Style Briefs)
- [Krenn System](cultural-groups/krenn-system.md) — Baltic-Nordic blend, logistics culture

canonical_id — endorsed with format refinement

Miri proposed: canonical_id: krenn.sova.transit.the-terminal. I'd standardize the format:

# Format: {system}.{station}.{district}.{entity-type}.{slug}
# Examples:
canonical_id: krenn.sova.transit.location.the-terminal
canonical_id: krenn.sova.transit.npc.kael-davan
canonical_id: global.faction.lattice-commission
canonical_id: global.technology.neural-lattice

Adding the entity type segment prevents collisions between, say, a location and an NPC with the same slug. It also makes the ID self-describing — you can parse the type from the ID without reading the file.

For the content directory, the canonical_id maps to file paths:

canonical_id Content path
krenn.sova.transit.npc.kael-davan content/districts/sova-transit/npcs/kael-davan.yaml
krenn.sova.transit.location.the-terminal content/districts/sova-transit/locations/the-terminal.yaml
global.faction.lattice-commission content/global/factions/lattice-commission.yaml

The mapping is derivable — content loader can resolve canonical_id to file path and vice versa. Cross-references in content YAML use canonical_id, not file paths. This is Miri's proposal, architecturally validated.

Miri's deeper nesting — gentle pushback

Miri's Round 1 shows a wiki structure with 4 levels of spatial nesting:

wiki/star-systems/krenn/station-sova/sova-transit-district/the-terminal.md

The current wiki uses 2 levels: locations/krenn-system/the-terminal.md. I maintain that 2 levels is the right cap for navigation paths. The canonical_id carries the full spatial address — the filesystem doesn't need to.

Compromise: Keep wiki paths at max 2 levels for locations. Use canonical_id in frontmatter for full spatial addressing. The wiki index provides the navigational hierarchy through cross-links, not directory depth. This gives Miri's disambiguation without my tab-completion nightmare.


6. Template Role Slots (Nigel's Request)

Nigel asked for explicit role slot definitions in social site templates. This is a direct question to me and the answer is: yes, and here's the spec.

Template role slot definition

# content/districts/sova-transit/templates/bar.yaml
template:
  id: bar
  name: "The Last Shift"
  location: the-last-shift

  role_slots:
    owner:
      count: 1
      required: true           # Must be filled every seed
      default: lera-sessik     # Base game assignment
    staff:
      count: 1-2
      required: true
      default: [sess]
    regular:
      count: 3-5               # Variable per seed
      required: false
      pool: bar_regulars       # References pools.yaml
    outsider:
      count: 0-2               # May or may not appear
      required: false
      pool: bar_outsiders

  # Triangle integration: which triangles must have all members present
  triangle_constraints:
    - triangle: bar-tensions
      required_members: [lera-sessik, torek-lintar]
      # olin can be absent — triangle fires differently without the outsider node

This tells the engine: "The bar has 1 owner (always Lera), 1-2 staff (always Sess in v0.1), 3-5 regulars drawn from a pool, and 0-2 outsiders drawn from a pool." The randomizer fills slots. Mods extend the pools.

Why this matters for mods: A modder adding Jax doesn't need to know the template internals. They add Jax to the bar_regulars pool (via MERGE on pools.yaml), and the template's slot system handles placement. The modder never touches templates/bar.yaml.

Why this matters for replayability: Different seeds populate the bar differently. Playthrough 1 has Torek, Harek, and Olin as regulars. Playthrough 2 has Torek, Harek, and Jax (mod). Playthrough 3 has Torek and Olin only (smaller bar night). The bar feels alive and different each time.


7. Paula's Smuggler-Specific Attributes

Paula identified a real architectural gap: the EntityKnowledge.known_attributes vocabulary is detective-biased. Her proposed smuggler attributes are mechanically sound:

Key Values Engine use
operational_reliability reliable, compromised, wavering Smuggler monologue tone, ring operation risk calculation
exposure_risk low, escalating, critical Smuggler stress system, storyteller escalation trigger
loyalty_assessment solid, uncertain, turning Smuggler dialogue access (who to trust with ring talk)
leverage_held Free text (e.g., gambling_debt) Smuggler-specific confrontation options
social_debt they_owe_me, i_owe_them, mutual, none Social manipulation mechanics

Architecture impact: None. known_attributes is a BTreeMap<String, String> — any key-value pair works. Adding these keys requires zero engine changes. The content schema and monologue prerequisites just reference the new keys.

Add these to global/knowledge/entity-attributes.yaml and to the wiki's entity-attributes.md. Total canonical keys goes from 14 to 19. The Rust types don't change — it's all string keys in the BTreeMap.

Feasibility: Trivial. The hardest part is writing the monologue lines that use these as prerequisites — and that's Mellanie's job, not an engine task.


8. Cross-Cutting: What Needs to Happen

Summarizing everything into concrete outputs.

Decisions to record (new D-entries)

ID Decision Source
D-042 Content directory structure: _meta/ + _schema/ + global/ + districts/ with underscore = infrastructure, no-underscore = moddable content Tyre R1/R2 + Gestalt R1, reconciled
D-043 canonical_id format: {system}.{station}.{district}.{type}.{slug} for collision-safe cross-referencing Miri R1 + Tyre R2 refinement
D-044 Mod overlay mechanics: ADD (new files), REPLACE (entity definitions), MERGE (pool-type content, deduplicate by ID) Tyre R1 + Nigel R1 pool integration
D-045 Pool-based seed-time selection: FRIEND, social site population, contraband type, role assignment. pools.yaml per district. v0.1 ships with 1 candidate per pool; architecture supports N candidates. Nigel R1 + Gore R1 + Tyre R2 synthesis
D-046 Schema-per-content-type validation with tier-conditional rules. Structural validation in v0.1, cross-reference validation in v0.2. Gestalt R1 tables + Tyre R1/R2 implementation
D-047 Template role slots: social site templates declare named slots with count ranges, required flags, and pool references. Modders extend pools, engine fills slots. Nigel R1 request + Tyre R2 spec
D-048 Canonical location shortcodes: terminal, bar, corridor. One name per location, enforced in schema, used in line ID prefixes. Mellanie R1 dependency + Tyre R2 formalization
D-049 Smuggler-specific entity attributes added to canonical vocabulary: operational_reliability, exposure_risk, loyalty_assessment, leverage_held, social_debt. Total canonical keys: 19. Paula R1 proposal + Tyre R2 validation

Documents to create

Document Owner Blocks
wiki/cultural-groups/krenn-system.md Miri All environmental text authoring
Cultural Groups section in wiki/index.md Miri/Qatux Wiki navigation
Location shortcode table Mellanie/Tyre All YAML content authoring
Freeform tag vocabulary (living list) Mellanie Content consistency
Environmental text type catalog Mellanie v0.1 environmental text authoring
Voice cards per playable character Mellanie Monologue authoring consistency
NPC-format briefs for smuggler + detective PCs Paula PC-as-NPC concept
Tier 1/2/3 template as standalone style guide Paula All future NPC authoring

Amendments to existing wiki pages

  1. wiki/index.md: Add Cultural Groups section, add Thematic Core section (Gore's proposal)
  2. wiki/knowledge/entity-attributes.md: Add 5 smuggler-specific attribute keys
  3. wiki/knowledge/fact-catalog.md: Add smuggler-perspective progression text for all FactIds where smuggler starts at KnowsDetails
  4. wiki/authoring/monologue-guide.md: Standardize location shortcodes (terminal_m_ not hub_m_); add situation overlap rules, mood exclusivity rules
  5. NPC profiles (all): Add canonical full names for single-name NPCs; add ## Thematic Question to Tier 1 template (Gore)

New tickets for SI

  1. Create _schema/ directory with schema definitions from Gestalt's tables
  2. Implement make validate-content CLI (structural + tier-conditional)
  3. Create content/ directory skeleton matching the unified structure above
  4. Create pools.yaml template with v0.1 single-candidate pools
  5. Create global/enums/ YAML files from D-035 enum values
  6. Add global/knowledge/entity-attributes.yaml with 19 canonical keys (14 existing + 5 smuggler)
  7. Create templates/ YAML files with role slot definitions for 3 social sites
  8. Create triangles/ YAML files for 5 v0.1 triangles
  9. Stub NPC profiles for Nils Davan (off-stage, referenced in 5+ profiles)
  10. Add pre-commit grep check for FactId typos in YAML content

Tyre out. The architecture converges. The content team now has a concrete spec to write against, and the engine team has a concrete loading pipeline to build. Ready for Qatux to record and SI to ticket.