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:
188
src/bin/sb.rs
Normal file
188
src/bin/sb.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user