Project: Lantern — an interactive fiction engine with 124 IF design patterns.
Task: Phase 20 — scaffold the lantern-patterns crate with pattern catalog types, TOML-based catalog loading, and full test coverage.
Stack: Rust, 9-crate workspace, #![deny(missing_docs)] enforced.
DevArch coding discipline rules active: documentation standards, command/event naming, functional tests for every command, behavioral tests for every scenario.

lantern-patterns/src/lib.rs

~ Documentation standards (rule 8): Module-level doc comment states purpose, public API, and owner context. Every public type and function has a doc comment. #![deny(missing_docs)] enforces this at compile time.
Rust lantern-core/lantern-patterns/src/lib.rs
//! Pattern catalog for Lantern's 124 IF design patterns.
//!
//! Patterns are data, not code paths. The catalog is loaded from a TOML file
//! at compile time via `include_str!` and deserialized into [`PatternCatalog`].
//!
//! # Public API
//!
//! - [`PatternCatalog`] — the top-level container; load with [`PatternCatalog::load_standard`].
//! - [`PatternDefinition`] — one pattern entry with detection signals, required entities, and codegen recipe.
//! - [`PatternCategory`] — the seven pattern families (Puzzle, Geography, Npc, Object, Narrative, Structure, Conversation).
//!
//! # Owner context
//!
//! Part of the `lantern-patterns` bounded context within `lantern-core`.

#![deny(missing_docs)]

use std::collections::HashMap;

use serde::Deserialize;

/// Compile-time embedded catalog TOML.
const CATALOG_TOML: &str = include_str!("../catalog/catalog.toml");
~ Clear boundaries (rule 7): Types expose only what callers need. Internal fields use semantic names from the domain (not generic "data" or "info"). Dependencies flow inward — only serde and toml for deserialization.
Rust Error + core types
/// Errors that can occur when loading the pattern catalog.
#[derive(Debug, thiserror::Error)]
pub enum PatternError {
    /// The catalog TOML could not be parsed.
    #[error("failed to parse catalog TOML: {0}")]
    ParseError(#[from] toml::de::Error),
}

/// The seven families of IF design patterns.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatternCategory {
    /// Puzzle mechanics (lock-and-key, light dependency, etc.)
    Puzzle,
    /// Spatial and navigation patterns (hub room, one-way passage, etc.)
    Geography,
    /// Non-player character patterns (guardian, merchant, follower, etc.)
    Npc,
    /// Object behavior patterns (container, wearable, composite, etc.)
    Object,
    /// Story structure patterns (flashback, parallel timeline, etc.)
    Narrative,
    /// Game structure patterns (chapter system, scoring, etc.)
    Structure,
    /// Conversation and dialogue patterns (ask/tell, menu, reactive, etc.)
    Conversation,
}

/// A single entity required by a pattern, with its role and expected traits.
#[derive(Debug, Clone, Deserialize)]
pub struct RequiredEntity {
    /// The role this entity plays in the pattern (e.g., "key", "locked_door").
    pub role: String,
    /// The Sharpee entity type (e.g., "item", "door", "actor").
    pub entity_type: String,
    /// Sharpee traits this entity must have (e.g., "openable", "lockable").
    pub required_traits: Vec<String>,
}

/// A relationship between two entity roles that the pattern requires.
#[derive(Debug, Clone, Deserialize)]
pub struct RequiredRelationship {
    /// The source entity role.
    pub from_role: String,
    /// The target entity role.
    pub to_role: String,
    /// The nature of the relationship (e.g., "unlocks", "blocks", "contains").
    pub relationship: String,
}

/// Maps a pattern to a codegen activity with parameterized entity roles.
#[derive(Debug, Clone, Deserialize)]
pub struct CodegenRecipe {
    /// Identifier for the codegen template/activity (e.g., "lock-and-key").
    pub template_id: String,
    /// Entity roles that must be filled in as template parameters.
    pub parameters: Vec<String>,
}

/// One item on a pattern's readiness checklist — content the author must provide.
#[derive(Debug, Clone, Deserialize)]
pub struct ReadinessCheck {
    /// Human-readable description of what is needed.
    pub what: String,
    /// The entity role this check applies to.
    pub entity_role: String,
    /// The specific field or content required.
    pub field: String,
}
~ Invariants and variants (rule 5): PatternDefinition has invariants enforced by the type system — PatternCategory is an enum (can't be invalid), detection_signals and required_entities are Vec (always present, possibly empty). The variant is the TOML data itself.
Rust PatternDefinition + detection types
/// A complete IF design pattern definition.
///
/// Each pattern is a data record describing how the LLM should detect it,
/// what entities it requires, how codegen should handle it, and what content
/// the author needs to provide before the pattern is ready for code generation.
#[derive(Debug, Clone, Deserialize)]
pub struct PatternDefinition {
    /// Unique identifier (e.g., "PUZ-001").
    pub id: String,
    /// Which family this pattern belongs to.
    pub category: PatternCategory,
    /// Human-readable name (e.g., "Lock and Key").
    pub name: String,
    /// One-paragraph description of the pattern.
    pub description: String,
    /// Prose phrases the LLM might encounter that signal this pattern.
    pub detection_signals: Vec<String>,
    /// Entities required to instantiate this pattern.
    pub required_entities: Vec<RequiredEntity>,
    /// Relationships between required entities.
    pub required_relationships: Vec<RequiredRelationship>,
    /// How codegen should generate code for this pattern.
    pub codegen_recipe: CodegenRecipe,
    /// Content the author must provide before this pattern is ready.
    pub readiness_checklist: Vec<ReadinessCheck>,
}

/// A pattern detected by the LLM during scene inference.
#[derive(Debug, Clone, Deserialize)]
pub struct DetectedPattern {
    /// The pattern ID (e.g., "PUZ-001") or "ad-hoc" for unrecognized mechanics.
    pub pattern_id: String,
    /// LLM's confidence in the detection (0.0–1.0).
    pub confidence: f32,
    /// Mapping of role names to entity IDs in the world model.
    pub entities: HashMap<String, String>,
    /// The prose fragment that triggered detection.
    pub source: Option<String>,
    /// Whether this is an ad-hoc pattern not matching any catalog entry.
    pub is_adhoc: bool,
}
Rust PatternCatalog — the public API
/// The pattern catalog — a registry of all known IF design patterns.
///
/// Load with [`PatternCatalog::load_standard`] to get the built-in 124 patterns.
/// Look up individual patterns with [`PatternCatalog::get`].
#[derive(Debug)]
pub struct PatternCatalog {
    patterns: Vec<PatternDefinition>,
}

impl PatternCatalog {
    /// Load the standard catalog embedded at compile time.
    ///
    /// # Errors
    ///
    /// Returns [`PatternError::ParseError`] if the embedded TOML is malformed.
    pub fn load_standard() -> Result<Self, PatternError> {
        let file: CatalogFile = toml::from_str(CATALOG_TOML)?;
        Ok(Self {
            patterns: file.patterns,
        })
    }

    /// Look up a pattern by its ID (e.g., "PUZ-001").
    pub fn get(&self, id: &str) -> Option<&PatternDefinition> {
        self.patterns.iter().find(|p| p.id == id)
    }

    /// Return all patterns in the catalog.
    pub fn all(&self) -> &[PatternDefinition] {
        &self.patterns
    }

    /// Return all patterns in a given category.
    pub fn by_category(&self, category: &PatternCategory) -> Vec<&PatternDefinition> {
        self.patterns.iter().filter(|p| &p.category == category).collect()
    }

    /// Return the total number of patterns in the catalog.
    pub fn len(&self) -> usize {
        self.patterns.len()
    }

    /// Return whether the catalog is empty.
    pub fn is_empty(&self) -> bool {
        self.patterns.is_empty()
    }
}

lantern-patterns/src/tests.rs

~ Functional tests (rule 10): Every public API method gets a test that asserts on real state. Tests load the actual TOML catalog — no mocks. Behavioral tests (rule 11): The final test exercises the full invariant: "every pattern in the catalog must have all required fields populated."
Rust lantern-core/lantern-patterns/src/tests.rs — 10 tests
//! Tests for the pattern catalog loading and querying.
//!
//! Validates that the embedded TOML catalog deserializes correctly and that
//! pattern entries contain the expected data.
//!
//! Owner context: lantern-patterns crate.

use super::*;

#[test]
fn catalog_loads_without_error() {
    let catalog = PatternCatalog::load_standard().expect("catalog should parse");
    assert!(!catalog.is_empty(), "catalog should contain at least one pattern");
}

#[test]
fn puz_001_is_present() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let pattern = catalog.get("PUZ-001").expect("PUZ-001 should exist");
    assert_eq!(pattern.name, "Lock and Key");
    assert_eq!(pattern.category, PatternCategory::Puzzle);
}

#[test]
fn puz_001_has_detection_signals() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let pattern = catalog.get("PUZ-001").unwrap();
    assert!(
        pattern.detection_signals.len() >= 5,
        "PUZ-001 should have at least 5 detection signals, got {}",
        pattern.detection_signals.len()
    );
}

#[test]
fn puz_001_has_required_entities() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let pattern = catalog.get("PUZ-001").unwrap();
    assert!(pattern.required_entities.len() >= 2);

    let roles: Vec<&str> = pattern.required_entities.iter()
        .map(|e| e.role.as_str()).collect();
    assert!(roles.contains(&"key"), "should have a 'key' role");
    assert!(roles.contains(&"locked_barrier"), "should have a 'locked_barrier' role");
}

#[test]
fn puz_001_has_required_relationships() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let pattern = catalog.get("PUZ-001").unwrap();
    assert_eq!(pattern.required_relationships.len(), 1);

    let rel = &pattern.required_relationships[0];
    assert_eq!(rel.from_role, "key");
    assert_eq!(rel.to_role, "locked_barrier");
    assert_eq!(rel.relationship, "unlocks");
}

#[test]
fn puz_001_has_codegen_recipe() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let pattern = catalog.get("PUZ-001").unwrap();
    assert_eq!(pattern.codegen_recipe.template_id, "lock-and-key");
    assert_eq!(pattern.codegen_recipe.parameters, vec!["key", "locked_barrier"]);
}

#[test]
fn puz_001_has_readiness_checklist() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let pattern = catalog.get("PUZ-001").unwrap();
    assert!(pattern.readiness_checklist.len() >= 3);
}

#[test]
fn get_nonexistent_pattern_returns_none() {
    let catalog = PatternCatalog::load_standard().unwrap();
    assert!(catalog.get("FAKE-999").is_none());
}

#[test]
fn by_category_returns_correct_patterns() {
    let catalog = PatternCatalog::load_standard().unwrap();
    let puzzles = catalog.by_category(&PatternCategory::Puzzle);
    assert!(!puzzles.is_empty(), "should have at least one puzzle pattern");
    for p in &puzzles {
        assert_eq!(p.category, PatternCategory::Puzzle);
    }
}

#[test]
fn all_patterns_have_non_empty_fields() {
    let catalog = PatternCatalog::load_standard().unwrap();
    for pattern in catalog.all() {
        assert!(!pattern.id.is_empty(), "pattern ID must not be empty");
        assert!(!pattern.name.is_empty(), "pattern name must not be empty");
        assert!(!pattern.description.is_empty());
        assert!(!pattern.detection_signals.is_empty(),
            "pattern {} must have detection signals", pattern.id);
        assert!(!pattern.required_entities.is_empty(),
            "pattern {} must have required entities", pattern.id);
        assert!(!pattern.codegen_recipe.template_id.is_empty(),
            "pattern {} must have a codegen template_id", pattern.id);
        assert!(!pattern.readiness_checklist.is_empty(),
            "pattern {} must have readiness checks", pattern.id);
    }
}
~ Result: 10 tests, all passing. Catalog loads from real TOML. PUZ-001 validated field by field. Every pattern checked for structural completeness. Total: 236 lines of library code + 126 lines of tests — generated in one session, committed with /fin.