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

762
src/resolve/merge.rs Normal file
View File

@@ -0,0 +1,762 @@
//! Template composition and merge engine
//!
//! Handles two types of template composition:
//! 1. Template includes (vertical composition): `template Person { include
//! Human ... }`
//! 2. Character template inheritance (horizontal composition): `character
//! Martha from Person, Worker { ... }`
//!
//! Also handles legacy @BaseTemplate { ... } syntax for template overrides
//! with:
//! - Set operations (field: value) - replace or add field
//! - Remove operations (remove field) - delete field
//! - Append operations (append field: value) - add new field (error if exists)
use std::collections::HashSet;
use crate::{
resolve::{
names::NameTable,
ResolveError,
Result,
},
syntax::ast::{
Character,
Declaration,
Field,
OverrideOp,
Template,
Value,
},
};
// ===== Template Composition =====
/// Resolve a template by recursively merging all its includes
///
/// Algorithm:
/// 1. Recursively resolve all included templates (depth-first)
/// 2. Merge included template fields (later includes override earlier ones)
/// 3. Add the template's own fields on top
///
/// Returns the fully merged fields for this template
pub fn resolve_template_includes(
template: &Template,
declarations: &[Declaration],
name_table: &NameTable,
visited: &mut HashSet<String>,
) -> Result<Vec<Field>> {
// Detect circular includes
if !visited.insert(template.name.clone()) {
return Err(ResolveError::CircularDependency {
cycle: format!(
"Circular template include detected: {} -> {}",
visited.iter().cloned().collect::<Vec<_>>().join(" -> "),
template.name
),
});
}
let mut merged_fields = Vec::new();
// Resolve all includes first
for include_name in &template.includes {
// Look up the included template
let entry = name_table
.lookup(std::slice::from_ref(include_name))
.ok_or_else(|| ResolveError::NameNotFound {
name: include_name.clone(),
suggestion: name_table.find_suggestion(include_name),
})?;
// Get the template declaration
let included_template = match &declarations[entry.decl_index] {
| Declaration::Template(t) => t,
| _ => {
return Err(ResolveError::ValidationError {
message: format!(
"Cannot include '{}': it's not a template",
include_name
),
help: Some(format!(
"The 'include' keyword can only reference templates. '{}' is a different type of declaration. Make sure you're including the correct name and that it refers to a template.",
include_name
)),
});
},
};
// Recursively resolve the included template
let included_fields =
resolve_template_includes(included_template, declarations, name_table, visited)?;
// Merge included fields (replacing any existing fields with same name)
merged_fields = merge_field_lists(merged_fields, included_fields);
}
// Add this template's own fields on top
merged_fields = merge_field_lists(merged_fields, template.fields.clone());
// Remove this template from visited set (allow it to be used in other branches)
visited.remove(&template.name);
Ok(merged_fields)
}
/// Merge character templates into character fields
///
/// Algorithm:
/// 1. Resolve each template (which may itself include other templates)
/// 2. Merge templates left to right (later templates override earlier ones)
/// 3. Add character's own fields on top
/// 4. If any template is strict, validate that all its fields are concrete
///
/// Returns the fully merged fields for this character
pub fn merge_character_templates(
character: &Character,
declarations: &[Declaration],
name_table: &NameTable,
) -> Result<Vec<Field>> {
let mut merged_fields = Vec::new();
let mut strict_templates = Vec::new();
// If character has templates, merge them
if let Some(template_names) = &character.template {
for template_name in template_names {
// Look up the template
let entry = name_table
.lookup(std::slice::from_ref(template_name))
.ok_or_else(|| ResolveError::NameNotFound {
name: template_name.clone(),
suggestion: name_table.find_suggestion(template_name),
})?;
// Get the template declaration
let template = match &declarations[entry.decl_index] {
| Declaration::Template(t) => t,
| _ => {
return Err(ResolveError::ValidationError {
message: format!(
"Character '{}' cannot inherit from '{}': it's not a template",
character.name, template_name
),
help: Some(format!(
"The 'from' keyword can only reference templates. '{}' is a different type of declaration. Make sure you're inheriting from the correct name and that it refers to a template.",
template_name
)),
});
},
};
// Track strict templates for validation
if template.strict {
strict_templates.push(template_name.clone());
}
// Resolve template (which handles includes recursively)
let mut visited = HashSet::new();
let template_fields =
resolve_template_includes(template, declarations, name_table, &mut visited)?;
// Merge template fields into accumulated fields
merged_fields = merge_field_lists(merged_fields, template_fields);
}
}
// Add character's own fields on top
merged_fields = merge_field_lists(merged_fields, character.fields.clone());
// Validate strict mode: all strict template fields must have concrete values
if !strict_templates.is_empty() {
validate_strict_templates(&character.name, &merged_fields, &strict_templates)?;
}
Ok(merged_fields)
}
/// Merge two field lists, with fields from the second list overriding the first
fn merge_field_lists(base: Vec<Field>, override_fields: Vec<Field>) -> Vec<Field> {
let mut merged = base;
for field in override_fields {
// If field exists, replace it; otherwise add it
if let Some(existing) = merged.iter_mut().find(|f| f.name == field.name) {
existing.value = field.value.clone();
existing.span = field.span.clone();
} else {
merged.push(field);
}
}
merged
}
/// Validate that strict template requirements are met
///
/// For strict templates, all fields must have concrete values (not ranges)
fn validate_strict_templates(
character_name: &str,
fields: &[Field],
strict_templates: &[String],
) -> Result<()> {
for field in fields {
if let Value::Range(_, _) = &field.value {
return Err(ResolveError::ValidationError {
message: format!(
"Character '{}' inherits from strict template(s) {}, but field '{}' has a range value instead of a concrete value",
character_name,
strict_templates.join(", "),
field.name
),
help: Some(format!(
"Strict templates require all fields to have concrete values. Replace the range in '{}' with a specific value. For example, instead of '18..65', use a specific age like '34'.",
field.name
)),
});
}
}
Ok(())
}
// ===== Legacy Override System =====
/// Apply an override to a base template's fields
///
/// This performs a structural merge:
/// 1. Start with all fields from base
/// 2. Apply each override operation in order
/// 3. Return merged field list
pub fn apply_override(base_fields: Vec<Field>, override_ops: &[OverrideOp]) -> Result<Vec<Field>> {
let mut merged = base_fields;
for op in override_ops {
match op {
| OverrideOp::Set(field) => {
// Replace existing field or add new one
if let Some(existing) = merged.iter_mut().find(|f| f.name == field.name) {
existing.value = field.value.clone();
existing.span = field.span.clone();
} else {
merged.push(field.clone());
}
},
| OverrideOp::Remove(name) => {
// Remove field if it exists
merged.retain(|f| f.name != *name);
},
| OverrideOp::Append(field) => {
// Add field only if it doesn't exist
if merged.iter().any(|f| f.name == field.name) {
return Err(ResolveError::ValidationError {
message: format!(
"Cannot append field '{}': field already exists",
field.name
),
help: Some(format!(
"The 'append' operation is used to add new fields that don't exist in the base template. The field '{}' already exists. Use 'set' instead to update an existing field, or use a different field name.",
field.name
)),
});
}
merged.push(field.clone());
},
}
}
Ok(merged)
}
/// Recursively resolve overrides in a value
///
/// If the value contains an Override, look up the base template
/// and apply the override operations
pub fn resolve_value_overrides(value: &Value, name_table: &NameTable) -> Result<Value> {
match value {
| Value::Override(override_spec) => {
// Look up the base template
let _base_entry = name_table.lookup(&override_spec.base).ok_or_else(|| {
ResolveError::NameNotFound {
name: override_spec.base.join("::"),
suggestion: name_table
.find_suggestion(override_spec.base.last().unwrap_or(&String::new())),
}
})?;
// For now, we'll return an error since we need the actual template fields
// In a full implementation, we'd extract the fields from the base declaration
Err(ResolveError::ValidationError {
message: format!(
"Override resolution not yet fully implemented for base '{}'",
override_spec.base.join("::")
),
help: Some("Template overrides are not yet supported. This feature is planned for a future release. For now, define characters directly without using template inheritance.".to_string()),
})
},
| Value::List(items) => {
// Recursively resolve overrides in list items
let resolved: Result<Vec<_>> = items
.iter()
.map(|v| resolve_value_overrides(v, name_table))
.collect();
Ok(Value::List(resolved?))
},
| Value::Object(fields) => {
// Recursively resolve overrides in object fields
let resolved_fields: Result<Vec<_>> = fields
.iter()
.map(|f| {
let resolved_value = resolve_value_overrides(&f.value, name_table)?;
Ok(Field {
name: f.name.clone(),
value: resolved_value,
span: f.span.clone(),
})
})
.collect();
Ok(Value::Object(resolved_fields?))
},
// Other value types don't contain overrides
| _ => Ok(value.clone()),
}
}
/// Check if applying the same override twice gives the same result
/// (idempotence)
pub fn is_idempotent(base: &[Field], ops: &[OverrideOp]) -> bool {
let result1 = apply_override(base.to_vec(), ops);
if result1.is_err() {
return false;
}
let intermediate = result1.unwrap();
let result2 = apply_override(intermediate.clone(), ops);
if result2.is_err() {
return false;
}
// Should get the same result
intermediate == result2.unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::ast::Span;
fn make_field(name: &str, value: i64) -> Field {
Field {
name: name.to_string(),
value: Value::Int(value),
span: Span::new(0, 10),
}
}
#[test]
fn test_set_replaces_existing_field() {
let base = vec![make_field("age", 25), make_field("health", 100)];
let ops = vec![OverrideOp::Set(make_field("age", 30))];
let result = apply_override(base, &ops).unwrap();
assert_eq!(result.len(), 2);
let age_field = result.iter().find(|f| f.name == "age").unwrap();
assert_eq!(age_field.value, Value::Int(30));
}
#[test]
fn test_set_adds_new_field() {
let base = vec![make_field("age", 25)];
let ops = vec![OverrideOp::Set(make_field("health", 100))];
let result = apply_override(base, &ops).unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().any(|f| f.name == "health"));
}
#[test]
fn test_remove_deletes_field() {
let base = vec![
make_field("age", 25),
make_field("health", 100),
make_field("energy", 50),
];
let ops = vec![OverrideOp::Remove("health".to_string())];
let result = apply_override(base, &ops).unwrap();
assert_eq!(result.len(), 2);
assert!(!result.iter().any(|f| f.name == "health"));
assert!(result.iter().any(|f| f.name == "age"));
assert!(result.iter().any(|f| f.name == "energy"));
}
#[test]
fn test_remove_nonexistent_field_is_noop() {
let base = vec![make_field("age", 25)];
let ops = vec![OverrideOp::Remove("nonexistent".to_string())];
let result = apply_override(base, &ops).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
}
#[test]
fn test_append_adds_new_field() {
let base = vec![make_field("age", 25)];
let ops = vec![OverrideOp::Append(make_field("health", 100))];
let result = apply_override(base, &ops).unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().any(|f| f.name == "health"));
}
#[test]
fn test_append_existing_field_errors() {
let base = vec![make_field("age", 25)];
let ops = vec![OverrideOp::Append(make_field("age", 30))];
let result = apply_override(base, &ops);
assert!(result.is_err());
}
#[test]
fn test_multiple_operations() {
let base = vec![
make_field("age", 25),
make_field("health", 100),
make_field("energy", 50),
];
let ops = vec![
OverrideOp::Set(make_field("age", 30)),
OverrideOp::Remove("energy".to_string()),
OverrideOp::Append(make_field("strength", 75)),
];
let result = apply_override(base, &ops).unwrap();
assert_eq!(result.len(), 3);
let age = result.iter().find(|f| f.name == "age").unwrap();
assert_eq!(age.value, Value::Int(30));
assert!(!result.iter().any(|f| f.name == "energy"));
assert!(result.iter().any(|f| f.name == "strength"));
}
#[test]
fn test_set_is_idempotent() {
let base = vec![make_field("age", 25)];
let ops = vec![OverrideOp::Set(make_field("age", 30))];
assert!(is_idempotent(&base, &ops));
}
#[test]
fn test_remove_is_idempotent() {
let base = vec![make_field("age", 25), make_field("health", 100)];
let ops = vec![OverrideOp::Remove("health".to_string())];
assert!(is_idempotent(&base, &ops));
}
#[test]
fn test_append_is_not_idempotent() {
let base = vec![make_field("age", 25)];
let ops = vec![OverrideOp::Append(make_field("health", 100))];
// Append is NOT idempotent because second application would try to
// append to a list that already has the field
assert!(!is_idempotent(&base, &ops));
}
// ===== Template Composition Tests =====
use crate::syntax::ast::File;
fn make_file(declarations: Vec<Declaration>) -> File {
File { declarations }
}
fn make_template(
name: &str,
fields: Vec<Field>,
includes: Vec<&str>,
strict: bool,
) -> Template {
Template {
name: name.to_string(),
fields,
includes: includes.iter().map(|s| s.to_string()).collect(),
strict,
span: Span::new(0, 10),
}
}
fn make_character(name: &str, fields: Vec<Field>, templates: Vec<&str>) -> Character {
Character {
name: name.to_string(),
fields,
template: if templates.is_empty() {
None
} else {
Some(templates.iter().map(|s| s.to_string()).collect())
},
span: Span::new(0, 10),
}
}
#[test]
fn test_resolve_template_with_no_includes() {
let template = make_template("Person", vec![make_field("age", 25)], vec![], false);
let declarations = vec![Declaration::Template(template.clone())];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result =
resolve_template_includes(&template, &declarations, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
assert_eq!(result[0].value, Value::Int(25));
}
#[test]
fn test_resolve_template_with_single_include() {
let base = make_template("Human", vec![make_field("age", 0)], vec![], false);
let derived = make_template("Person", vec![make_field("name", 0)], vec!["Human"], false);
let declarations = vec![
Declaration::Template(base),
Declaration::Template(derived.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result =
resolve_template_includes(&derived, &declarations, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().any(|f| f.name == "age"));
assert!(result.iter().any(|f| f.name == "name"));
}
#[test]
fn test_resolve_template_with_chained_includes() {
let base = make_template("Being", vec![make_field("alive", 1)], vec![], false);
let middle = make_template("Human", vec![make_field("age", 0)], vec!["Being"], false);
let top = make_template("Person", vec![make_field("name", 0)], vec!["Human"], false);
let declarations = vec![
Declaration::Template(base),
Declaration::Template(middle),
Declaration::Template(top.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result =
resolve_template_includes(&top, &declarations, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 3);
assert!(result.iter().any(|f| f.name == "alive"));
assert!(result.iter().any(|f| f.name == "age"));
assert!(result.iter().any(|f| f.name == "name"));
}
#[test]
fn test_resolve_template_field_override() {
let base = make_template("Human", vec![make_field("age", 0)], vec![], false);
let derived = make_template(
"Person",
vec![make_field("age", 25)], // Override with concrete value
vec!["Human"],
false,
);
let declarations = vec![
Declaration::Template(base),
Declaration::Template(derived.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result =
resolve_template_includes(&derived, &declarations, &name_table, &mut visited).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
assert_eq!(result[0].value, Value::Int(25)); // Should be overridden
// value
}
#[test]
fn test_merge_character_templates_single() {
let template = make_template("Person", vec![make_field("age", 0)], vec![], false);
let character = make_character("Martha", vec![make_field("age", 34)], vec!["Person"]);
let declarations = vec![
Declaration::Template(template),
Declaration::Character(character.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "age");
assert_eq!(result[0].value, Value::Int(34)); // Character's value
// overrides template
}
#[test]
fn test_merge_character_templates_multiple() {
let physical = make_template("Physical", vec![make_field("height", 0)], vec![], false);
let mental = make_template("Mental", vec![make_field("iq", 0)], vec![], false);
let character = make_character(
"Martha",
vec![make_field("height", 165), make_field("iq", 120)],
vec!["Physical", "Mental"],
);
let declarations = vec![
Declaration::Template(physical),
Declaration::Template(mental),
Declaration::Character(character.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table).unwrap();
assert_eq!(result.len(), 2);
assert!(result
.iter()
.any(|f| f.name == "height" && f.value == Value::Int(165)));
assert!(result
.iter()
.any(|f| f.name == "iq" && f.value == Value::Int(120)));
}
#[test]
fn test_merge_character_templates_with_includes() {
let base = make_template("Human", vec![make_field("age", 0)], vec![], false);
let derived = make_template("Person", vec![make_field("name", 0)], vec!["Human"], false);
let character = make_character(
"Martha",
vec![make_field("age", 34), make_field("name", 1)],
vec!["Person"],
);
let declarations = vec![
Declaration::Template(base),
Declaration::Template(derived),
Declaration::Character(character.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table).unwrap();
assert_eq!(result.len(), 2);
assert!(result
.iter()
.any(|f| f.name == "age" && f.value == Value::Int(34)));
assert!(result
.iter()
.any(|f| f.name == "name" && f.value == Value::Int(1)));
}
#[test]
fn test_strict_template_validation_passes() {
let template = make_template("Person", vec![make_field("age", 0)], vec![], true);
let character = make_character("Martha", vec![make_field("age", 34)], vec!["Person"]);
let declarations = vec![
Declaration::Template(template),
Declaration::Character(character.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table);
assert!(result.is_ok());
}
#[test]
fn test_strict_template_validation_fails_with_range() {
let template = make_template(
"Person",
vec![Field {
name: "age".to_string(),
value: Value::Range(Box::new(Value::Int(18)), Box::new(Value::Int(65))),
span: Span::new(0, 10),
}],
vec![],
true,
);
let character = make_character("Martha", vec![], vec!["Person"]);
let declarations = vec![
Declaration::Template(template),
Declaration::Character(character.clone()),
];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let result = merge_character_templates(&character, &declarations, &name_table);
assert!(result.is_err());
if let Err(ResolveError::ValidationError { message, .. }) = result {
assert!(message.contains("strict template"));
assert!(message.contains("range value"));
}
}
#[test]
fn test_circular_include_detection() {
let a = make_template("A", vec![], vec!["B"], false);
let b = make_template("B", vec![], vec!["A"], false);
let declarations = vec![Declaration::Template(a.clone()), Declaration::Template(b)];
let name_table = NameTable::from_file(&make_file(declarations.clone())).unwrap();
let mut visited = HashSet::new();
let result = resolve_template_includes(&a, &declarations, &name_table, &mut visited);
assert!(result.is_err());
if let Err(ResolveError::CircularDependency { .. }) = result {
// Expected
} else {
panic!("Expected CircularDependency error");
}
}
#[test]
fn test_merge_field_lists_override() {
let base = vec![make_field("age", 25), make_field("health", 100)];
let overrides = vec![make_field("age", 30)];
let result = merge_field_lists(base, overrides);
assert_eq!(result.len(), 2);
let age = result.iter().find(|f| f.name == "age").unwrap();
assert_eq!(age.value, Value::Int(30));
}
#[test]
fn test_merge_field_lists_add_new() {
let base = vec![make_field("age", 25)];
let overrides = vec![make_field("health", 100)];
let result = merge_field_lists(base, overrides);
assert_eq!(result.len(), 2);
assert!(result.iter().any(|f| f.name == "age"));
assert!(result.iter().any(|f| f.name == "health"));
}
}