feat: implement storybook DSL with template composition and validation

Add complete domain-specific language for authoring narrative content for
agent simulations.

Features:
- Complete parser using LALRPOP + logos lexer
- Template composition (includes + multiple inheritance)
- Strict mode validation for templates
- Reserved keyword protection
- Semantic validators (trait ranges, schedule overlaps, life arcs, behaviors)
- Name resolution and cross-reference tracking
- CLI tool (validate, inspect, query commands)
- Query API with filtering
- 260 comprehensive tests (unit, integration, property-based)

Implementation phases:
- Phase 1 (Parser): Complete
- Phase 2 (Resolution + Validation): Complete
- Phase 3 (Public API + CLI): Complete

BREAKING CHANGE: Initial implementation
This commit is contained in:
2026-02-08 13:24:35 +00:00
commit 9c20dd4092
59 changed files with 25484 additions and 0 deletions

188
src/bin/sb.rs Normal file
View File

@@ -0,0 +1,188 @@
//! Storybook CLI tool
//!
//! Commands:
//! - `sb validate <path>` - Parse and validate entire project
//! - `sb inspect <entity>` - Show fully resolved entity details
//! - `sb watch <path>` - Continuous validation on file changes
use std::path::PathBuf;
use clap::{
Parser,
Subcommand,
};
use miette::{
IntoDiagnostic,
Result,
};
use storybook::Project;
#[derive(Parser)]
#[command(name = "sb")]
#[command(about = "Storybook DSL tool", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Validate a storybook project or file
Validate {
/// Path to a .sb file or directory containing .sb files
#[arg(default_value = ".")]
path: PathBuf,
},
/// Inspect a specific entity
Inspect {
/// Entity name to inspect
name: String,
/// Path to the storybook project directory
#[arg(short, long, default_value = ".")]
path: PathBuf,
},
/// Watch a project for changes and re-validate
Watch {
/// Path to the storybook project directory
#[arg(default_value = ".")]
path: PathBuf,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
| Commands::Validate { path } => validate(&path)?,
| Commands::Inspect { name, path } => inspect(&name, &path)?,
| Commands::Watch { path } => watch(&path)?,
}
Ok(())
}
fn validate(path: &PathBuf) -> Result<()> {
println!("Validating storybook at: {}", path.display());
let project = Project::load(path)?;
let char_count = project.characters().count();
let rel_count = project.relationships().count();
let inst_count = project.institutions().count();
let sched_count = project.schedules().count();
let behavior_count = project.behaviors().count();
let arc_count = project.life_arcs().count();
println!("✓ Validation successful!");
println!();
println!("Project contents:");
println!(" Characters: {}", char_count);
println!(" Relationships: {}", rel_count);
println!(" Institutions: {}", inst_count);
println!(" Schedules: {}", sched_count);
println!(" Behaviors: {}", behavior_count);
println!(" Life Arcs: {}", arc_count);
Ok(())
}
fn inspect(name: &str, path: &PathBuf) -> Result<()> {
println!("Loading project from: {}", path.display());
let project = Project::load(path)?;
// Try to find the entity as different types
if let Some(character) = project.find_character(name) {
println!("Character: {}", character.name);
println!("Fields:");
for (field_name, value) in &character.fields {
println!(" {}: {:?}", field_name, value);
}
println!("Prose blocks:");
for (tag, prose) in &character.prose_blocks {
println!(" ---{}", tag);
println!("{}", prose.content);
println!(" ---");
}
return Ok(());
}
if let Some(relationship) = project.find_relationship(name) {
println!("Relationship: {}", relationship.name);
println!("Participants:");
for participant in &relationship.participants {
println!(" {}", participant.name.join("::"));
}
println!("Fields:");
for (field_name, value) in &relationship.fields {
println!(" {}: {:?}", field_name, value);
}
return Ok(());
}
if let Some(institution) = project.find_institution(name) {
println!("Institution: {}", institution.name);
println!("Fields:");
for (field_name, value) in &institution.fields {
println!(" {}: {:?}", field_name, value);
}
return Ok(());
}
println!("Entity '{}' not found in project", name);
Ok(())
}
fn watch(path: &PathBuf) -> Result<()> {
use std::sync::mpsc::channel;
use notify::{
Event,
EventKind,
RecursiveMode,
Watcher,
};
println!("Watching for changes in: {}", path.display());
println!("Press Ctrl+C to stop");
println!();
// Initial validation
match Project::load(path) {
| Ok(_) => println!("✓ Initial validation successful"),
| Err(e) => println!("✗ Initial validation failed: {}", e),
}
let (tx, rx) = channel::<notify::Result<Event>>();
let mut watcher = notify::recommended_watcher(tx).into_diagnostic()?;
watcher
.watch(path, RecursiveMode::Recursive)
.into_diagnostic()?;
for res in rx {
match res {
| Ok(event) => {
// Only re-validate on write events for .sb files
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) &&
event
.paths
.iter()
.any(|p| p.extension().and_then(|s| s.to_str()) == Some("sb"))
{
println!("\n--- Change detected, re-validating... ---");
match Project::load(path) {
| Ok(_) => println!("✓ Validation successful"),
| Err(e) => println!("✗ Validation failed: {}", e),
}
}
},
| Err(e) => println!("Watch error: {:?}", e),
}
}
Ok(())
}