docs(architecture): add line pool format specification (#308)

Formal spec at docs/architecture/line-pool-format.md defining YAML
structure for dialogue and monologue content files. Covers tag enums,
4-layer filtering pipeline, prerequisite-to-KG mapping, ID format,
validation rules, and Rust loader interface.

Fixes monologue schema: adds required constraint on relationship
prerequisite target/state fields.

Ref: D-028, D-032, D-035, D-041

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 00:41:18 +01:00
co-authored by Claude Opus 4.6
parent 4d8fe996fc
commit ea0a3f0b35
2 changed files with 572 additions and 1 deletions
+3 -1
View File
@@ -38,7 +38,7 @@
"type": "string",
"minLength": 1,
"maxLength": 160,
"description": "Line text — 160 char max per D-059"
"description": "Line text — 160 char max to fit monologue display without scrolling"
},
"trigger": {
"type": "string",
@@ -63,6 +63,8 @@
},
"relationship": {
"type": "object",
"required": ["target", "state"],
"additionalProperties": false,
"properties": {
"target": { "type": "string" },
"state": {
+569
View File
@@ -0,0 +1,569 @@
# 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: `fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`. |
| `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: the-last-shift_d_001
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: [fond]
topic: [personal, colleague]
tags: [kael, greeting, phase-1]
- id: the-last-shift_d_010
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: [concerned]
topic: [danger]
tags: [kael, ring-ops, nils]
- id: the-last-shift_d_012
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: [concerned]
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: the-last-shift_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: the-last-shift_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: the-last-shift_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
```
{location-slug}_{type}_{character?}_{sequence}
```
| Segment | Format | Values | Example |
|---------|--------|--------|---------|
| `location-slug` | kebab-case | Matches location directory name, or `general` | `the-last-shift`, `general` |
| `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` | `001`, `042` |
> **D-035 deviation note:** D-035 specifies `{template}_{d|m|e}_{###}`. This spec refines that to `{location-slug}_{d|m}_{###}` (dialogue) and `{location-slug}_m_{s|d}_{###}` (monologue) for two reasons: (1) location-slug is more precise than template name and matches the directory hierarchy, and (2) monologue IDs include a character segment (`s`/`d`) to ensure uniqueness across the hard character partition (D-032). All existing authored content already uses this refined format. D-035 should be updated to reflect the implemented convention.
### 5.2 Regex patterns
| Pool type | Regex | Example |
|-----------|-------|---------|
| Dialogue | `^[a-z][a-z0-9-]*_d_[0-9]{3}$` | `the-last-shift_d_001` |
| Monologue | `^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$` | `the-last-shift_m_d_021`, `general_m_s_003` |
### 5.3 Uniqueness scope
- IDs must be unique **within a single YAML file**.
- IDs are **not required to be globally unique** — location 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)
`fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`
8 values for v0.1.
### 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.