Real code produced by Claude Code with DevArch installed. Not cleaned up, not cherry-picked — exactly what was committed.
#![deny(missing_docs)] enforces this at compile time.
//! 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");
serde and toml for deserialization.
/// 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, }
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.
/// 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, }
/// 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() } }
//! 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); } }
/fin.