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:
325
src/resolve/links.rs
Normal file
325
src/resolve/links.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
//! Bidirectional relationship resolution
|
||||
//!
|
||||
//! Handles relationships that can be declared from either participant's
|
||||
//! perspective, merging self/other blocks and validating consistency.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
resolve::{
|
||||
ResolveError,
|
||||
Result,
|
||||
},
|
||||
syntax::ast::{
|
||||
Declaration,
|
||||
Field,
|
||||
File,
|
||||
Participant,
|
||||
Relationship,
|
||||
},
|
||||
};
|
||||
|
||||
/// A relationship key that's order-independent
|
||||
/// (Martha, David) and (David, Martha) map to the same key
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct RelationshipKey {
|
||||
participants: Vec<String>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl RelationshipKey {
|
||||
fn new(mut participants: Vec<String>, name: String) -> Self {
|
||||
// Sort participants to make key order-independent
|
||||
participants.sort();
|
||||
Self { participants, name }
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a relationship declaration
|
||||
#[derive(Debug, Clone)]
|
||||
struct RelationshipDecl {
|
||||
relationship: Relationship,
|
||||
/// Which participant is "self" (index into participants)
|
||||
self_index: Option<usize>,
|
||||
}
|
||||
|
||||
/// Resolved bidirectional relationship
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedRelationship {
|
||||
pub name: String,
|
||||
pub participants: Vec<Participant>,
|
||||
pub fields: Vec<Field>,
|
||||
/// Merged self/other blocks for each participant
|
||||
pub participant_fields: Vec<ParticipantFields>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParticipantFields {
|
||||
pub participant_name: Vec<String>,
|
||||
pub role: Option<String>,
|
||||
/// Fields from this participant's "self" block
|
||||
pub self_fields: Vec<Field>,
|
||||
/// Fields from this participant's "other" block (about other participants)
|
||||
pub other_fields: Vec<Field>,
|
||||
}
|
||||
|
||||
/// Resolve bidirectional relationships in a file
|
||||
pub fn resolve_relationships(file: &File) -> Result<Vec<ResolvedRelationship>> {
|
||||
// Group relationships by key
|
||||
let mut relationship_groups: HashMap<RelationshipKey, Vec<RelationshipDecl>> = HashMap::new();
|
||||
|
||||
for decl in &file.declarations {
|
||||
if let Declaration::Relationship(rel) = decl {
|
||||
// Extract participant names
|
||||
let participant_names: Vec<String> =
|
||||
rel.participants.iter().map(|p| p.name.join("::")).collect();
|
||||
|
||||
let key = RelationshipKey::new(participant_names, rel.name.clone());
|
||||
|
||||
// Determine which participant is "self" based on self/other blocks
|
||||
let self_index = rel
|
||||
.participants
|
||||
.iter()
|
||||
.position(|p| p.self_block.is_some() || p.other_block.is_some());
|
||||
|
||||
relationship_groups
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(RelationshipDecl {
|
||||
relationship: rel.clone(),
|
||||
self_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Merge grouped relationships
|
||||
let mut resolved = Vec::new();
|
||||
for (key, decls) in relationship_groups {
|
||||
let merged = merge_relationship_declarations(&key, decls)?;
|
||||
resolved.push(merged);
|
||||
}
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// Merge multiple declarations of the same relationship
|
||||
fn merge_relationship_declarations(
|
||||
key: &RelationshipKey,
|
||||
decls: Vec<RelationshipDecl>,
|
||||
) -> Result<ResolvedRelationship> {
|
||||
if decls.is_empty() {
|
||||
return Err(ResolveError::ValidationError {
|
||||
message: "Empty relationship group".to_string(),
|
||||
help: Some("This is an internal error - relationship groups should never be empty. Please report this as a bug.".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// Start with the first declaration
|
||||
let base = &decls[0].relationship;
|
||||
let mut participant_fields: Vec<ParticipantFields> = base
|
||||
.participants
|
||||
.iter()
|
||||
.map(|p| ParticipantFields {
|
||||
participant_name: p.name.clone(),
|
||||
role: p.role.clone(),
|
||||
self_fields: p.self_block.clone().unwrap_or_default(),
|
||||
other_fields: p.other_block.clone().unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Merge additional declarations
|
||||
for decl in decls.iter().skip(1) {
|
||||
// If this declaration specifies a different participant as "self",
|
||||
// merge their self/other blocks appropriately
|
||||
if let Some(self_idx) = decl.self_index {
|
||||
let participant_name = &decl.relationship.participants[self_idx].name;
|
||||
|
||||
// Find this participant in our merged list
|
||||
if let Some(idx) = participant_fields
|
||||
.iter()
|
||||
.position(|pf| &pf.participant_name == participant_name)
|
||||
{
|
||||
// Merge self blocks
|
||||
let self_block = decl.relationship.participants[self_idx]
|
||||
.self_block
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
merge_fields(&mut participant_fields[idx].self_fields, self_block)?;
|
||||
|
||||
// Merge other blocks
|
||||
let other_block = decl.relationship.participants[self_idx]
|
||||
.other_block
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
merge_fields(&mut participant_fields[idx].other_fields, other_block)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge shared fields (fields outside self/other blocks)
|
||||
let mut merged_fields = base.fields.clone();
|
||||
for decl in decls.iter().skip(1) {
|
||||
merge_fields(&mut merged_fields, decl.relationship.fields.clone())?;
|
||||
}
|
||||
|
||||
Ok(ResolvedRelationship {
|
||||
name: key.name.clone(),
|
||||
participants: base.participants.clone(),
|
||||
fields: merged_fields,
|
||||
participant_fields,
|
||||
})
|
||||
}
|
||||
|
||||
/// Merge field lists, detecting conflicts
|
||||
fn merge_fields(target: &mut Vec<Field>, source: Vec<Field>) -> Result<()> {
|
||||
for new_field in source {
|
||||
// Check if field already exists
|
||||
if let Some(existing) = target.iter().find(|f| f.name == new_field.name) {
|
||||
// Fields must have the same value
|
||||
if existing.value != new_field.value {
|
||||
return Err(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Conflicting values for field '{}' in relationship",
|
||||
new_field.name
|
||||
),
|
||||
help: Some(format!(
|
||||
"The field '{}' has different values in different declarations of the same relationship. Make sure all declarations of this relationship use the same value for shared fields.",
|
||||
new_field.name
|
||||
)),
|
||||
});
|
||||
}
|
||||
// Same value, no need to add again
|
||||
} else {
|
||||
// New field, add it
|
||||
target.push(new_field);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::syntax::ast::{
|
||||
Span,
|
||||
Value,
|
||||
};
|
||||
|
||||
fn make_participant(name: &str, role: Option<&str>) -> Participant {
|
||||
Participant {
|
||||
name: vec![name.to_string()],
|
||||
role: role.map(|s| s.to_string()),
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
span: Span::new(0, 10),
|
||||
}
|
||||
}
|
||||
|
||||
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_relationship_key_order_independent() {
|
||||
let key1 = RelationshipKey::new(
|
||||
vec!["Martha".to_string(), "David".to_string()],
|
||||
"Marriage".to_string(),
|
||||
);
|
||||
let key2 = RelationshipKey::new(
|
||||
vec!["David".to_string(), "Martha".to_string()],
|
||||
"Marriage".to_string(),
|
||||
);
|
||||
assert_eq!(key1, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_relationship_declaration() {
|
||||
let file = File {
|
||||
declarations: vec![Declaration::Relationship(Relationship {
|
||||
name: "Friendship".to_string(),
|
||||
participants: vec![
|
||||
make_participant("Alice", None),
|
||||
make_participant("Bob", None),
|
||||
],
|
||||
fields: vec![make_field("bond", 80)],
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
};
|
||||
|
||||
let resolved = resolve_relationships(&file).unwrap();
|
||||
assert_eq!(resolved.len(), 1);
|
||||
assert_eq!(resolved[0].name, "Friendship");
|
||||
assert_eq!(resolved[0].participants.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bidirectional_relationship_merge() {
|
||||
let mut martha_participant = make_participant("Martha", Some("spouse"));
|
||||
martha_participant.self_block = Some(vec![make_field("bond", 90)]);
|
||||
martha_participant.other_block = Some(vec![make_field("trust", 85)]);
|
||||
|
||||
let mut david_participant = make_participant("David", Some("spouse"));
|
||||
david_participant.self_block = Some(vec![make_field("bond", 90)]);
|
||||
david_participant.other_block = Some(vec![make_field("trust", 85)]);
|
||||
|
||||
let file = File {
|
||||
declarations: vec![
|
||||
Declaration::Relationship(Relationship {
|
||||
name: "Marriage".to_string(),
|
||||
participants: vec![
|
||||
martha_participant.clone(),
|
||||
make_participant("David", Some("spouse")),
|
||||
],
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
}),
|
||||
Declaration::Relationship(Relationship {
|
||||
name: "Marriage".to_string(),
|
||||
participants: vec![
|
||||
david_participant.clone(),
|
||||
make_participant("Martha", Some("spouse")),
|
||||
],
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
let resolved = resolve_relationships(&file).unwrap();
|
||||
assert_eq!(resolved.len(), 1);
|
||||
assert_eq!(resolved[0].name, "Marriage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflicting_field_values() {
|
||||
let mut p1 = make_participant("Alice", None);
|
||||
p1.self_block = Some(vec![make_field("bond", 80)]);
|
||||
|
||||
let mut p2 = make_participant("Alice", None);
|
||||
p2.self_block = Some(vec![make_field("bond", 90)]); // Different value
|
||||
|
||||
let file = File {
|
||||
declarations: vec![
|
||||
Declaration::Relationship(Relationship {
|
||||
name: "Test".to_string(),
|
||||
participants: vec![p1, make_participant("Bob", None)],
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
}),
|
||||
Declaration::Relationship(Relationship {
|
||||
name: "Test".to_string(),
|
||||
participants: vec![p2, make_participant("Bob", None)],
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
let result = resolve_relationships(&file);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user