Files
settled-reach/docs/architecture/line-pool-format.md
T
jpmschweitzerandClaude Opus 4.6 3100190b40 docs(docs): add frontmatter to architecture docs
Standardized YAML frontmatter on all 10 docs/architecture/ files with
title, description, type, status, ticket, decision_refs, and author
fields. Enables context-aware document loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:22:03 +01:00

595 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Line Pool Format Specification"
description: "YAML format spec for dialogue and monologue line pools — file structure, tag enums, four-layer filtering pipeline, ID format, validation rules, and Rust loader interface"
type: architecture
status: active
ticket: "#308"
decision_refs: [D-028, D-032, D-035, D-041]
author: ""
created: YYYY-MM-DD
updated: YYYY-MM-DD
---
# Line Pool Format Specification
**Ticket:** #308 | **Sprint:** 7 | **Priority:** HIGH (blocks #326, #305)
**Decisions:** D-028 (dialogue architecture), D-032 (separate monologue pools), D-035 (tag taxonomy), D-041 (KG data model)
**Audience:** Content authors (Mellanie, Paula), engine developers (Dudley), QA (Hoshe)
---
## 1. Overview
Line pools are the atomic content units for The Settled Reach's dialogue and monologue systems. Each pool is a YAML file containing tagged lines that the engine selects from at runtime using a four-layer filtering pipeline (D-028).
There are two pool types:
| Pool type | Scope | Selection model | Partition |
|-----------|-------|-----------------|-----------|
| **Dialogue** | Per-location, per-role | 4-layer filter (access > situation > trust > topic+mood) | None (role-based, character-agnostic) |
| **Monologue** | Per-location, per-character | Trigger-based with prerequisite gates | Hard partition by character (D-032) |
Both types share a common tag vocabulary defined in `content/global/enums/`.
---
## 2. Directory Structure
Content lives under `content/campaigns/{campaign}/systems/{system}/stations/{station}/districts/{district}/`. Below the district level:
```
{district}/
dialogue/
{location}/ # one subdirectory per location
{role}.yaml # one file per template role at that location
{named-npc}.yaml # named NPCs get dedicated files
pc-smuggler.yaml # PC-specific dialogue (if applicable)
pc-detective.yaml
monologue/
smuggler/ # hard partition per D-032
general.yaml # location-independent lines
{location}.yaml # location-specific lines
{topic-slug}.yaml # topic-specific cross-location pools
detective/
general.yaml
{location}.yaml
{topic-slug}.yaml
```
### Naming conventions
| Element | Pattern | Examples |
|---------|---------|----------|
| Location directory | `kebab-case` matching location YAML slug | `the-terminal`, `the-last-shift`, `maintenance-corridors` |
| Dialogue file | `{role-slug}.yaml` or `{npc-slug}.yaml` | `dock-worker.yaml`, `kael-davan.yaml`, `pc-detective.yaml` |
| Monologue file | `{location-slug}.yaml` or `general.yaml` or `{topic}.yaml` | `the-terminal.yaml`, `general.yaml`, `pc-detective-tells.yaml` |
| Character directory | `smuggler/` or `detective/` | Matches `character` enum exactly |
### What is NOT encoded in per-line tags
Per D-035, the following are **implicit from directory structure** and never appear as line-level tags:
- **Location** — derived from the parent directory name
- **Content type** — derived from whether the file is under `dialogue/` or `monologue/`
- **Character** (monologue only) — derived from the parent `smuggler/` or `detective/` directory, confirmed by the `character` field in the YAML header
---
## 3. Dialogue Pool Format
### 3.1 File structure
```yaml
# {NPC name or role description}
# Context notes for authors (not consumed by engine)
location: {location-slug} # required, must match parent directory name
role: {role-slug} # required, template role (not NPC name)
lines:
- id: {line-id}
text: "Dialogue line text."
role: {role-slug}
access: [{access-tier}, ...]
trust: {trust-tier}
situation: [{situation}, ...]
topic: [{topic}, ...] # optional, defaults to []
mood: [{mood}, ...] # optional, defaults to []
tags: [{freeform}, ...] # optional, defaults to []
knowledge_grant: # optional
fact_id: {category.fact_id}
confidence: {confidence-level}
```
### 3.2 Header fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `location` | string | YES | Location slug, must match parent directory. Pattern: `^[a-z][a-z0-9-]*$` |
| `role` | string | YES | Template role slug. Pattern: `^[a-z][a-z0-9-]*$`. This is the abstract role (e.g. `dock-worker`), not a specific NPC name. NPC assignment to roles happens at runtime via template instantiation. |
### 3.3 Line fields
#### Structural tags (required on every line)
| Field | Type | Required | Validation | Description |
|-------|------|----------|------------|-------------|
| `id` | string | YES | `^[a-z][a-z0-9-]*_d_[0-9]{3}$` | Stable line identifier. See [Section 5: ID Format](#5-id-format). |
| `text` | string | YES | Non-empty | The authored dialogue line. No length limit (unlike monologue). |
| `role` | string | YES | `^[a-z][a-z0-9-]*$` | Template role this line belongs to. Must match header `role` or be a valid role at this location. |
| `access` | list\<enum\> | YES | Min 1 item, unique | Access tiers this line is eligible for. **Hard filter** — line is invisible if player's access tier is not in this list. Values: `public`, `insider`, `authority`, `peer`, `hostile`. |
| `trust` | enum | YES | Single value | Minimum trust tier required. **Hard filter** — line is invisible below this tier. Values: `surface`, `real`, `secret`. |
| `situation` | list\<enum\> | YES | Min 1 item, unique | Situation contexts when this line can fire. **Context filter** — engine activates situations based on simulation state. Values: `arrival`, `shift_start`, `shift_end`, `shift_transition`, `bar_evening`, `night_shift`, `investigation`, `confrontation`, `social`, `alone`, `emergency`, `routine`, `observation`. |
#### Selection tags (optional, influence weighted selection)
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `topic` | list\<enum\> | no | `[]` | Topic tags for Layer 4 weighted selection. Lines without topic tags are eligible for any topic context. Values: `colleague`, `routine`, `cargo`, `money`, `trust`, `danger`, `institution`, `personal`, `investigation`. |
| `mood` | list\<enum\> | no | `[]` | Mood tags for Layer 4 weighted selection. Lines without mood tags are eligible for any mood context. Values: `anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`. |
| `tags` | list\<string\> | no | `[]` | Freeform escape hatch. Not consumed by the filtering engine — used for author organization, content queries, and the line previewer. No validation on values. |
#### Knowledge grant (optional)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `knowledge_grant` | object | no | Knowledge the player gains from hearing this line. |
| `knowledge_grant.fact_id` | string | YES (if grant present) | Fact identifier in `{category}.{fact_id}` format. Must reference a fact defined in `content/global/knowledge/{category}.yaml`. |
| `knowledge_grant.confidence` | enum | YES (if grant present) | Confidence level granted. Values: `suspects`, `knows_of`, `knows_details`, `direct`. |
### 3.4 Four-layer filtering pipeline (D-028)
The engine processes dialogue lines through four layers in sequence:
```
All lines in pool
├─ Layer 1: ACCESS FILTER (hard)
│ Keep lines where player's access tier ∈ line.access
├─ Layer 2: SITUATION FILTER (context)
│ Keep lines where any active situation ∈ line.situation
├─ Layer 3: TRUST FILTER (hard)
│ Keep lines where player's trust ≥ line.trust
│ (surface < real < secret)
└─ Layer 4: TOPIC + MOOD SELECTION (weighted)
Score remaining lines by topic and mood match.
Lines with no topic/mood tags get a neutral weight (always eligible,
never boosted). Select from top-scored candidates with randomization.
```
**Authoring implication:** Every line must pass Layers 1-3 to be eligible. Layers 1 and 3 are hard gates — get them wrong and the line is invisible. Layer 2 controls when the line fires. Layer 4 is a soft preference.
### 3.5 Complete dialogue example
```yaml
# Kael Davan — dock worker at The Last Shift
# Voice: direct, practical, short sentences, warm to trusted people
location: the-last-shift
role: dock-worker
lines:
- id: kael-davan_d_015
text: "Saved you a seat. Lera's got the spiced rice tonight."
role: dock-worker
access: [insider, peer]
trust: surface
situation: [bar_evening, social, arrival]
mood: [warm]
topic: [personal, colleague]
tags: [kael, greeting, phase-1]
- id: kael-davan_d_024
text: "Nils wants to talk. Tomorrow, bay side. Said it's about volume."
role: dock-worker
access: [insider]
trust: real
situation: [bar_evening, social]
mood: [anxious]
topic: [danger]
tags: [kael, ring-ops, nils]
- id: kael-davan_d_026
text: "Lera knows more than she lets on. She won't say anything — but don't test it."
role: dock-worker
access: [insider]
trust: real
situation: [bar_evening, social, alone]
mood: [anxious]
topic: [colleague, danger]
tags: [kael, ring-ops, lera, caution]
knowledge_grant:
fact_id: knowledge.bar_ring_awareness
confidence: suspects
```
---
## 4. Monologue Pool Format
### 4.1 File structure
```yaml
character: {character} # required, hard partition (D-032)
location: {location-slug} # required, or "general" for location-independent
lines:
- id: {line-id}
text: "Internal monologue text."
trigger: {trigger-type}
prerequisites: # optional
facts:
- fact_id: {category.fact_id}
min_confidence: {confidence-level}
entity_attributes:
- entity: {entity-ref}
key: {attribute-key}
value: {attribute-value}
relationship:
target: {entity-ref}
state: {relationship-state}
priority: {0-10} # optional, default 5
cooldown: {ticks} # optional, minimum re-fire interval
tags: [{freeform}, ...] # optional
```
### 4.2 Header fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `character` | enum | YES | `smuggler` or `detective`. Hard partition per D-032 — pools are completely separate. The engine never crosses this boundary. |
| `location` | string | YES | Location slug, or `general` for location-independent lines. Pattern: `^[a-z][a-z0-9-]*$\|^general$` |
### 4.3 Line fields
#### Core fields (required)
| Field | Type | Required | Validation | Description |
|-------|------|----------|------------|-------------|
| `id` | string | YES | `^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$` | Stable line identifier. See [Section 5: ID Format](#5-id-format). |
| `text` | string | YES | 1-160 characters | The monologue line. 160-char max — authoring constraint to fit the monologue display without scrolling (established in `monologue-pool.schema.json`, not yet formalized as a decision). |
| `trigger` | enum | YES | Single value | What causes this line to fire. Values: `enter_location`, `observe_npc`, `hear_sound`, `observe_anomaly`, `post_conversation`, `discover_evidence`, `witness_interaction`, `time_idle`, `return_visit`. |
#### Prerequisites (optional, AND-combined)
All prerequisite conditions are AND-combined: every specified condition must be true for the line to be eligible.
| Field | Type | Description |
|-------|------|-------------|
| `prerequisites` | object | Knowledge state gates. If omitted, the line has no prerequisites (always eligible given trigger). |
| `prerequisites.facts` | list\<object\> | Fact-based gates. Each entry requires `fact_id` (string, `{category}.{fact_id}` format) and `min_confidence` (enum: `suspects`, `knows_of`, `knows_details`, `direct`). The player's confidence for the referenced fact must be ≥ the specified minimum. |
| `prerequisites.entity_attributes` | list\<object\> | Entity attribute gates. Each entry requires `entity` (string, entity reference like `npc:kael-davan`), `key` (string, attribute name), `value` (string, expected value). |
| `prerequisites.relationship` | object | Relationship state gate. Requires `target` (string, entity reference) and `state` (enum: `unknown`, `known`, `friendly`, `person_of_interest`, `hostile`). The player's relationship with the target must be at or beyond the specified state. |
**Prerequisite-to-KG mapping:**
| Prerequisite type | KG query | D-041 structure |
|-------------------|----------|-----------------|
| `facts[].fact_id` + `min_confidence` | Look up `FactId` in `BTreeMap<FactId, FactKnowledge>`, check `confidence ≥ min` | `FactKnowledge.confidence` (4-level: Suspects < KnowsOf < KnowsDetails < Direct) |
| `entity_attributes[].entity` | Look up `StableId` via `EntityRegistry`, check attribute | `EntityKnowledge` attribute fields |
| `relationship.target` + `state` | Look up entity in `BTreeMap<StableId, EntityKnowledge>`, check relationship state | `EntityKnowledge.relationship_state` |
#### Selection fields (optional)
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `priority` | integer | `5` | Selection priority, 0-10. Higher = more likely to fire when multiple lines match the same trigger + prerequisites. Priority 9-10 should be reserved for critical narrative moments. |
| `cooldown` | integer | `0` | Minimum ticks before this line can fire again for the same player. At 10 tps (D-031), a cooldown of 600 = 1 game-minute. |
| `tags` | list\<string\> | `[]` | Freeform tags. Not consumed by the selection engine — used for author organization and the line previewer. |
### 4.4 Monologue selection flow
```
Trigger fires (e.g. enter_location)
├─ Character partition: select pool for current PC (D-032)
├─ Location filter: prefer location-specific pool, fall back to general
├─ Trigger filter: keep lines matching this trigger type
├─ Prerequisite gate: evaluate all prerequisites against KG state
│ (AND-combined — all must pass)
├─ Cooldown check: exclude recently fired lines
└─ Priority-weighted selection: pick from eligible lines
Higher priority = higher weight. Randomized among equal-priority.
```
### 4.5 Complete monologue example
```yaml
character: detective
location: the-last-shift
lines:
# Basic atmospheric line — no prerequisites, any visit
- id: pc-detective_m_d_001
text: "The Last Shift. Only place in this district that doesn't smell like freight lubricant."
trigger: enter_location
tags: [arrival, atmospheric]
# Knowledge-gated observation — requires prior suspicion
- id: pc-detective_m_d_021
text: "Sera left when Torek arrived. Second time. Different excuse. Same result."
trigger: observe_anomaly
prerequisites:
facts:
- fact_id: behavioral.sera_avoidance_pattern
min_confidence: suspects
priority: 7
tags: [npc, sera, torek, tell, friend-arc]
# Relationship-gated line — requires person_of_interest status
- id: pc-detective_m_d_026
text: "Same booth. Same warm smile. Same offer to buy me a drink. Everything except the truth."
trigger: observe_npc
prerequisites:
relationship:
target: npc:sera-venn
state: person_of_interest
priority: 8
tags: [npc, sera, contaminated-trust, friend-arc]
```
---
## 5. ID Format
### 5.1 Pattern
```
{npc-slug}_{type}_{character?}_{sequence}
```
| Segment | Format | Values | Example |
|---------|--------|--------|---------|
| `npc-slug` | kebab-case | NPC name in kebab-case. Each NPC has an independent sequence. | `kael-davan`, `dock-worker`, `pc-detective` |
| `type` | single char | `d` = dialogue, `m` = monologue, `e` = environmental (future) | `d`, `m` |
| `character` | single char | `s` = smuggler, `d` = detective. **Monologue only.** | `s`, `d` |
| `sequence` | 3-digit zero-padded | `001``999` per NPC | `001`, `042` |
**General validation regex** (matches both dialogue and monologue IDs):
```
^[a-z0-9-]+_(d|m)(_[a-z])?_\d{3}$
```
- `[a-z0-9-]+` — npc slug (kebab-case, at least one character)
- `(d|m)` — pool type: `d` for dialogue, `m` for monologue
- `(_[a-z])?` — optional character segment (monologue only): `_s` or `_d`
- `\d{3}` — three-digit zero-padded sequence number
Use the pool-specific regexes in [Section 5.2](#52-regex-patterns) for strict per-type validation. This general regex is useful for quick format checks that accept either type.
> **D-035 Sprint 15 amendment:** IDs are NPC-scoped, not location-scoped. The old `{location-slug}_{d|m}_{###}` format caused collisions when the same NPC appeared at multiple locations (e.g. `the-terminal_d_039` appeared in multiple NPC files). The new format `{npc-slug}_{d|m}_{###}` gives each NPC an independent 999-line ceiling. For multi-location NPCs, sequences are globally continuous across files (e.g. kael-davan uses _001-_014 at maintenance-corridors, _015-_033 at the-last-shift, _034-_075 at the-terminal). Single-location NPCs start at _001. Monologue uses per-file restart with composite key (file_path + line_id).
### 5.2 Regex patterns
| Pool type | Regex | Example |
|-----------|-------|---------|
| Dialogue | `^[a-z][a-z0-9-]*_d_[0-9]{3}$` | `kael-davan_d_001` |
| Monologue | `^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$` | `pc-detective_m_d_021`, `pc-smuggler_m_s_003` |
### 5.3 Uniqueness scope
- IDs must be unique **within a single YAML file**.
- IDs are **not required to be globally unique** — npc-slug + file path provides global uniqueness. The engine uses `(file_path, line_id)` as the composite key.
- Sequence numbers need not be contiguous. Gaps are expected when lines are removed or reordered.
### 5.4 ID stability
IDs are **stable references**. Once assigned, a line ID should not change. Other systems (cooldown tracking, analytics, the line previewer) reference lines by ID. If a line's text changes, keep the ID. Only assign a new ID when creating a genuinely new line.
---
## 6. Tag Enums Reference
All enum values are defined in `content/global/enums/` and validated by the JSON schemas in `content/_schema/`. This section is a quick reference — see the enum YAML files for full descriptions.
### 6.1 Access tiers (D-028 Layer 1)
| Value | Description |
|-------|-------------|
| `public` | Available to anyone. Surface-level conversation. |
| `insider` | Available to group members. Ring membership, established regulars. |
| `authority` | Available to institutional figures. Commission agents, security. |
| `peer` | Available to social equals with earned personal trust. |
| `hostile` | Available when relationship has broken down. |
### 6.2 Trust tiers (D-028 Layer 3)
| Value | Gate level | Description |
|-------|------------|-------------|
| `surface` | Default | Safe, non-committal. What NPCs tell strangers. |
| `real` | Earned | Genuine opinions, workplace complaints, personal concerns. |
| `secret` | Highest | Information they'd normally hide. Ring involvement, vulnerabilities. |
### 6.3 Situations (D-028 Layer 2)
`arrival`, `shift_start`, `shift_end`, `shift_transition`, `bar_evening`, `night_shift`, `investigation`, `confrontation`, `social`, `alone`, `emergency`, `routine`, `observation`
13 values for v0.1. See `content/global/enums/situations.yaml` for full descriptions.
### 6.4 Topics (D-028 Layer 4)
`colleague`, `routine`, `cargo`, `money`, `trust`, `danger`, `institution`, `personal`, `investigation`
9 values for v0.1. Note: `crime` is deliberately excluded — NPCs think of smuggling as `cargo` or `money`.
### 6.5 Moods (D-028 Layer 4)
`anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`
8 values for v0.1. (Updated Sprint 14 amendment to D-035.)
### 6.6 Monologue triggers
`enter_location`, `observe_npc`, `hear_sound`, `observe_anomaly`, `post_conversation`, `discover_evidence`, `witness_interaction`, `time_idle`, `return_visit`
9 values for v0.1.
### 6.7 Confidence levels (D-041)
| Value | Gate meaning | Maps to D-028 |
|-------|-------------|----------------|
| `suspects` | "Something's off" | Gates initial investigation |
| `knows_of` | "X is involved in Y" | Gates `real` trust dialogue, peer access |
| `knows_details` | Actionable detail | Gates confrontation, `secret` trust dialogue |
| `direct` | Currently in LOS | Live position data, maximum rendering fidelity |
---
## 7. Authoring-Only Fields
These fields are consumed by the line previewer and authoring tools but **not by the runtime engine**. They may appear in any pool file.
| Field | Type | Scope | Description |
|-------|------|-------|-------------|
| `dual_lens` | map | Per-line | Per-character authoring notes for mirror moments. Keys: `smuggler`, `detective`. Values: string notes about how each character interprets this line/moment differently. |
| `notes` | string | Per-line | Free-text author notes. Context, intent, voice reminders. |
These fields are defined in the JSON schemas with `additionalProperties: false` — if you need to add them, the schemas must be updated first. Current schemas do not include these fields; they are reserved for a future schema update when the previewer authoring workflow matures.
---
## 8. Validation Rules
### 8.1 Structural validation (`make validate-content`)
The existing `make validate-content` target (#392) validates against JSON schemas. The following rules are enforced:
| Rule | Scope | Error level |
|------|-------|-------------|
| All required fields present | Both | ERROR |
| `id` matches regex pattern | Both | ERROR |
| `id` unique within file | Both | ERROR |
| Enum values match defined sets | Both | ERROR |
| `access` has ≥ 1 item | Dialogue | ERROR |
| `situation` has ≥ 1 item | Dialogue | ERROR |
| `text` is non-empty | Both | ERROR |
| `text` ≤ 160 chars | Monologue | ERROR |
| `character` matches parent directory | Monologue | ERROR |
| `location` matches parent directory | Both | ERROR |
| `priority` is 0-10 | Monologue | ERROR |
| `knowledge_grant.fact_id` references existing fact | Dialogue | WARNING |
| `prerequisites.facts[].fact_id` references existing fact | Monologue | WARNING |
| Lists have `uniqueItems` | Both | ERROR |
### 8.2 Content-level validation (line previewer, future)
These are not yet enforced by tooling but are authoring guidelines:
- Every template role should have ≥ 5 `surface` trust lines at `public` access (baseline conversation)
- Every location should have ≥ 3 monologue lines per character for `enter_location` trigger (first-visit coverage)
- Lines tagged with `phase-5` (contaminated trust) should have corresponding `phase-1` baseline lines
- `knowledge_grant` confidence should not exceed `knows_of` from a single dialogue line (hearing one line shouldn't grant `knows_details`)
- Named NPC files should have lines across ≥ 2 trust tiers
---
## 9. Rust Loader Interface
For #326 (YAML content loader), the engine parses these files into in-memory structures. This section defines the target API — not the implementation.
### 9.1 Core types
```rust
/// A loaded dialogue line, fully parsed and validated.
struct DialogueLine {
id: LineId,
text: String,
role: RoleSlug,
access: Vec<AccessTier>, // D-028 Layer 1
trust: TrustTier, // D-028 Layer 3
situation: Vec<Situation>, // D-028 Layer 2
topic: Vec<Topic>, // D-028 Layer 4
mood: Vec<Mood>, // D-028 Layer 4
tags: Vec<String>,
knowledge_grant: Option<KnowledgeGrant>,
}
/// A loaded monologue line, fully parsed and validated.
struct MonologueLine {
id: LineId,
text: String, // ≤ 160 chars
trigger: Trigger,
prerequisites: Option<Prerequisites>,
priority: u8, // 0-10, default 5
cooldown: u32, // ticks, default 0
tags: Vec<String>,
}
/// Pool container — one per file loaded.
struct DialoguePool {
location: LocationSlug,
role: RoleSlug,
lines: Vec<DialogueLine>,
}
struct MonologuePool {
character: Character, // smuggler | detective
location: LocationSlug, // or "general"
lines: Vec<MonologueLine>,
}
```
### 9.2 Query API
```rust
/// Query dialogue lines through the 4-layer pipeline.
fn query_dialogue(
pool: &DialoguePool,
player_access: AccessTier,
active_situations: &[Situation],
player_trust: TrustTier,
npc_topics: &[Topic], // weighted preference, not hard filter
npc_mood: &[Mood], // weighted preference, not hard filter
) -> Vec<&DialogueLine>;
/// Query monologue lines for a trigger event.
fn query_monologue(
pools: &[MonologuePool], // all pools for current character + location
character: Character,
trigger: Trigger,
knowledge: &KnowledgeGraph, // D-041
fired_cooldowns: &BTreeMap<LineId, Tick>, // BTreeMap per D-041
) -> Vec<&MonologueLine>;
```
### 9.3 Indexing strategy
Per D-041 determinism requirements, all internal maps use `BTreeMap`:
- **Primary index:** `BTreeMap<(LocationSlug, RoleSlug), DialoguePool>` — dialogue pools by location + role
- **Access pre-filter:** Lines within each pool pre-sorted by access tier for O(1) hard filter
- **Monologue index:** `BTreeMap<(Character, LocationSlug), Vec<MonologuePool>>` — monologue pools by character + location
- **Trigger index:** Within each monologue pool, lines grouped by trigger type for fast lookup
---
## 10. Content Volume Estimates
Per D-028, target line counts for v0.1 vertical slice (Sova Transit District):
| Content type | Per template role | Per location | District total |
|--------------|-------------------|--------------|----------------|
| Dialogue lines | 165-210 authored | ~500-700 | ~2,000-3,000 |
| Monologue lines (per character) | — | 20-40 | ~100-200 |
| Generation-expanded (4x) | 660-840 | ~2,000-2,800 | ~8,000-12,000 |
The generation pass (write 10, generate 40) is a future pipeline step — authored YAML files contain only human-written lines.
---
## 11. Schema Files
The canonical JSON Schema files that validate this format:
| Schema | Path | Validates |
|--------|------|-----------|
| Dialogue pool | `content/_schema/dialogue-pool.schema.json` | `dialogue/**/*.yaml` |
| Monologue pool | `content/_schema/monologue-pool.schema.json` | `monologue/**/*.yaml` |
| Fact catalog | `content/_schema/fact-catalog.schema.json` | `global/knowledge/*.yaml` |
These schemas are the machine-enforceable subset of this specification. This document is the authoritative reference; the schemas enforce the structural rules.