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

26 KiB
Raw Blame History

title, description, type, status, ticket, decision_refs, author, created, updated
title description type status ticket decision_refs author created updated
Line Pool Format Specification 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 architecture active #308
D-028
D-032
D-035
D-041
YYYY-MM-DD 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

# {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.
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

# 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

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.
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

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 001999 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 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

/// 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

/// 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.