# 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`:** ```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: ```yaml # 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: ```yaml # _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 ```yaml # 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: ```yaml # 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: ```rust 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, deviations: Vec, } ``` 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: ```markdown ### 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: ```yaml # 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 ```yaml # 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` — 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.*