2026-02-08 13:24:35 +00:00
//! Semantic validation for Storybook entities
//!
//! This module validates semantic constraints that can't be checked during
//! parsing:
//! - Reserved keyword conflicts in field names
//! - Trait value ranges
//! - Schedule time overlaps
//! - Life arc transition validity
//! - Behavior tree action registry checks
//! - Relationship bond values (0.0 .. 1.0)
use std ::collections ::HashSet ;
use crate ::{
resolve ::{
ErrorCollector ,
ResolveError ,
Result ,
} ,
syntax ::ast ::* ,
} ;
/// List of reserved keywords that cannot be used as field names
const RESERVED_KEYWORDS : & [ & str ] = & [
// Top-level declaration keywords
" character " ,
" template " ,
" life_arc " ,
" schedule " ,
" behavior " ,
" institution " ,
" relationship " ,
" location " ,
" species " ,
" enum " ,
// Statement keywords
" use " ,
" state " ,
" on " ,
" as " ,
" remove " ,
" append " ,
" strict " ,
" include " ,
" from " ,
// Expression keywords
" self " ,
" other " ,
" forall " ,
" exists " ,
" in " ,
" where " ,
" and " ,
" or " ,
" not " ,
" is " ,
" true " ,
" false " ,
] ;
/// Validate that field names don't conflict with reserved keywords
pub fn validate_no_reserved_keywords ( fields : & [ Field ] , collector : & mut ErrorCollector ) {
for field in fields {
if RESERVED_KEYWORDS . contains ( & field . name . as_str ( ) ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Field name '{}' is a reserved keyword and cannot be used " ,
field . name
) ,
help : Some ( format! (
" The name '{}' is reserved by the Storybook language. Try using a different name like '{}Type', '{}Value', or 'my{}'. " ,
field . name ,
field . name ,
field . name ,
field . name . chars ( ) . next ( ) . unwrap_or ( 'x' ) . to_uppercase ( ) . collect ::< String > ( ) + & field . name [ 1 .. ]
) ) ,
} ) ;
}
}
}
/// Validate trait values are within valid ranges
pub fn validate_trait_ranges ( fields : & [ Field ] , collector : & mut ErrorCollector ) {
for field in fields {
match & field . value {
2026-02-14 14:03:21 +00:00
| Value ::Decimal ( f ) = > {
2026-02-08 13:24:35 +00:00
// Normalized trait values should be 0.0 .. 1.0
if ( field . name . ends_with ( " _normalized " ) | |
field . name = = " bond " | |
field . name = = " trust " | |
field . name = = " love " ) & &
! ( 0. 0 ..= 1.0 ) . contains ( f )
{
collector . add ( ResolveError ::TraitOutOfRange {
field : field . name . clone ( ) ,
value : f . to_string ( ) ,
min : 0.0 ,
max : 1.0 ,
} ) ;
}
} ,
2026-02-14 14:03:21 +00:00
| Value ::Number ( i ) = > {
2026-02-08 13:24:35 +00:00
// Age should be reasonable
if field . name = = " age " & & ( * i < 0 | | * i > 150 ) {
collector . add ( ResolveError ::TraitOutOfRange {
field : " age " . to_string ( ) ,
value : i . to_string ( ) ,
min : 0.0 ,
max : 150.0 ,
} ) ;
}
} ,
| _ = > { } ,
}
}
}
/// Validate relationship bond values are in [0.0, 1.0]
pub fn validate_relationship_bonds ( relationships : & [ Relationship ] , collector : & mut ErrorCollector ) {
for rel in relationships {
for field in & rel . fields {
if field . name = = " bond " {
2026-02-14 14:03:21 +00:00
if let Value ::Decimal ( f ) = field . value {
2026-02-08 13:24:35 +00:00
if ! ( 0. 0 ..= 1.0 ) . contains ( & f ) {
collector . add ( ResolveError ::TraitOutOfRange {
field : " bond " . to_string ( ) ,
value : f . to_string ( ) ,
min : 0.0 ,
max : 1.0 ,
} ) ;
}
}
}
}
2026-02-13 21:52:03 +00:00
// Validate participant fields
2026-02-08 13:24:35 +00:00
for participant in & rel . participants {
2026-02-13 21:52:03 +00:00
validate_trait_ranges ( & participant . fields , collector ) ;
2026-02-08 13:24:35 +00:00
}
}
}
/// Validate schedule blocks don't overlap in time
pub fn validate_schedule_overlaps ( schedule : & Schedule , collector : & mut ErrorCollector ) {
// Sort blocks by start time
let mut sorted_blocks : Vec < _ > = schedule . blocks . iter ( ) . collect ( ) ;
sorted_blocks . sort_by_key ( | b | ( b . start . hour as u32 ) * 60 + ( b . start . minute as u32 ) ) ;
// Check for overlaps
for i in 0 .. sorted_blocks . len ( ) {
for j in ( i + 1 ) .. sorted_blocks . len ( ) {
let block1 = sorted_blocks [ i ] ;
let block2 = sorted_blocks [ j ] ;
let end1 = ( block1 . end . hour as u32 ) * 60 + ( block1 . end . minute as u32 ) ;
let start2 = ( block2 . start . hour as u32 ) * 60 + ( block2 . start . minute as u32 ) ;
// Check if blocks overlap
if end1 > start2 {
collector . add ( ResolveError ::ScheduleOverlap {
block1 : format ! (
" {} ({}:{:02}-{}:{:02}) " ,
2026-02-13 21:52:03 +00:00
block1 . name . as_ref ( ) . unwrap_or ( & block1 . activity ) ,
2026-02-08 13:24:35 +00:00
block1 . start . hour ,
block1 . start . minute ,
block1 . end . hour ,
block1 . end . minute
) ,
block2 : format ! (
" {} ({}:{:02}-{}:{:02}) " ,
2026-02-13 21:52:03 +00:00
block2 . name . as_ref ( ) . unwrap_or ( & block2 . activity ) ,
2026-02-08 13:24:35 +00:00
block2 . start . hour ,
block2 . start . minute ,
block2 . end . hour ,
block2 . end . minute
) ,
end1 : format ! ( " {}:{:02} " , block1 . end . hour , block1 . end . minute ) ,
start2 : format ! ( " {}:{:02} " , block2 . start . hour , block2 . start . minute ) ,
} ) ;
}
}
}
}
/// Validate life arc state machine has valid transitions
pub fn validate_life_arc_transitions ( life_arc : & LifeArc , collector : & mut ErrorCollector ) {
// Collect all state names
let mut state_names = HashSet ::new ( ) ;
for state in & life_arc . states {
state_names . insert ( state . name . clone ( ) ) ;
}
// Validate all transitions point to valid states
for state in & life_arc . states {
for transition in & state . transitions {
if ! state_names . contains ( & transition . to ) {
let available_states = state_names
. iter ( )
. map ( | s | format! ( " ' {} ' " , s ) )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ;
collector . add ( ResolveError ::UnknownLifeArcState {
life_arc : life_arc . name . clone ( ) ,
state : state . name . clone ( ) ,
target : transition . to . clone ( ) ,
available_states ,
} ) ;
}
}
}
// Warn if states have no outgoing transitions (terminal states)
// This is not an error, just informational
}
/// Validate behavior tree actions are known
///
/// If action_registry is empty, skips validation (no schema provided).
pub fn validate_behavior_tree_actions (
tree : & Behavior ,
action_registry : & HashSet < String > ,
collector : & mut ErrorCollector ,
) {
// Skip validation if no action schema was provided
if action_registry . is_empty ( ) {
return ;
}
validate_tree_node_actions ( & tree . root , action_registry , & tree . name , collector )
}
fn validate_tree_node_actions (
node : & BehaviorNode ,
action_registry : & HashSet < String > ,
tree_name : & str ,
collector : & mut ErrorCollector ,
) {
match node {
2026-02-13 21:52:03 +00:00
| BehaviorNode ::Sequence { children , .. } | BehaviorNode ::Selector { children , .. } = > {
2026-02-08 13:24:35 +00:00
for child in children {
validate_tree_node_actions ( child , action_registry , tree_name , collector ) ;
}
} ,
| BehaviorNode ::Action ( name , _params ) = > {
if ! action_registry . contains ( name ) {
collector . add ( ResolveError ::UnknownBehaviorAction {
tree : tree_name . to_string ( ) ,
action : name . clone ( ) ,
} ) ;
}
} ,
| BehaviorNode ::Condition ( _ ) = > {
// Conditions are validated separately via expression validation
} ,
2026-02-13 21:52:03 +00:00
| BehaviorNode ::Decorator { child , .. } = > {
2026-02-08 13:24:35 +00:00
validate_tree_node_actions ( child , action_registry , tree_name , collector ) ;
} ,
| BehaviorNode ::SubTree ( _path ) = > {
// SubTree references validated separately
} ,
}
}
2026-02-13 21:52:03 +00:00
/// Validate character resource linking
///
/// Checks:
/// 1. No duplicate behavior tree references
/// 2. Priority values are valid (handled by type system)
pub fn validate_character_resource_links ( character : & Character , collector : & mut ErrorCollector ) {
// Check for duplicate behavior tree references
if let Some ( ref behavior_links ) = character . uses_behaviors {
let mut seen_trees : HashSet < String > = HashSet ::new ( ) ;
for link in behavior_links {
let tree_name = link . tree . join ( " :: " ) ;
if seen_trees . contains ( & tree_name ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Character '{}' has duplicate behavior tree reference: {} " ,
character . name ,
tree_name
) ,
help : Some ( format! (
" The behavior tree '{}' is referenced multiple times in the uses behaviors list. Each behavior tree should only be referenced once. If you want different conditions or priorities, combine them into a single entry. " ,
tree_name
) ) ,
} ) ;
}
seen_trees . insert ( tree_name ) ;
}
}
// Check for duplicate schedule references
if let Some ( ref schedules ) = character . uses_schedule {
let mut seen_schedules : HashSet < String > = HashSet ::new ( ) ;
for schedule in schedules {
if seen_schedules . contains ( schedule ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Character '{}' has duplicate schedule reference: {} " ,
character . name ,
schedule
) ,
help : Some ( format! (
" The schedule '{}' is referenced multiple times. Each schedule should only be referenced once. " ,
schedule
) ) ,
} ) ;
}
seen_schedules . insert ( schedule . clone ( ) ) ;
}
}
}
/// Validate institution resource linking
pub fn validate_institution_resource_links (
institution : & Institution ,
collector : & mut ErrorCollector ,
) {
// Check for duplicate behavior tree references
if let Some ( ref behavior_links ) = institution . uses_behaviors {
let mut seen_trees : HashSet < String > = HashSet ::new ( ) ;
for link in behavior_links {
let tree_name = link . tree . join ( " :: " ) ;
if seen_trees . contains ( & tree_name ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Institution '{}' has duplicate behavior tree reference: {} " ,
institution . name ,
tree_name
) ,
help : Some ( format! (
" The behavior tree '{}' is referenced multiple times. Each behavior tree should only be referenced once. " ,
tree_name
) ) ,
} ) ;
}
seen_trees . insert ( tree_name ) ;
}
}
// Check for duplicate schedule references
if let Some ( ref schedules ) = institution . uses_schedule {
let mut seen_schedules : HashSet < String > = HashSet ::new ( ) ;
for schedule in schedules {
if seen_schedules . contains ( schedule ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Institution '{}' has duplicate schedule reference: {} " ,
institution . name , schedule
) ,
help : Some ( format! (
" The schedule '{}' is referenced multiple times. " ,
schedule
) ) ,
} ) ;
}
seen_schedules . insert ( schedule . clone ( ) ) ;
}
}
}
/// Validate schedule composition requirements
///
/// Checks:
/// 1. All blocks in extended schedules have names
/// 2. Override blocks reference valid block names (requires name resolution)
pub fn validate_schedule_composition ( schedule : & Schedule , collector : & mut ErrorCollector ) {
// If schedule extends another, all blocks must have names for override system
if schedule . extends . is_some ( ) {
for block in & schedule . blocks {
if block . name . is_none ( ) & & ! block . activity . is_empty ( ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Schedule '{}' extends another schedule but has unnamed blocks " ,
schedule . name
) ,
help : Some (
" When a schedule extends another, all blocks must have names to support the override system. Use 'block name { ... }' syntax instead of 'time -> time : activity { ... }'. " . to_string ( )
) ,
} ) ;
}
}
}
// Validate that new-style blocks have action references
for block in & schedule . blocks {
if block . name . is_some ( ) & & block . action . is_none ( ) & & block . activity . is_empty ( ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Schedule '{}' block '{}' missing action reference " ,
schedule . name ,
block . name . as_ref ( ) . unwrap ( )
) ,
help : Some (
" Named blocks should specify a behavior using 'action: BehaviorName'. Example: 'block work { 9:00 -> 17:00, action: WorkBehavior }' " . to_string ( )
) ,
} ) ;
}
}
}
2026-02-14 14:07:39 +00:00
/// Validate that all sub_concepts reference existing parent concepts
pub fn validate_sub_concept_parents ( file : & File , collector : & mut ErrorCollector ) {
// Collect all concept names
let concept_names : HashSet < String > = file
. declarations
. iter ( )
. filter_map ( | decl | {
if let Declaration ::Concept ( c ) = decl {
Some ( c . name . clone ( ) )
} else {
None
}
} )
. collect ( ) ;
// Check each sub_concept references an existing parent
for decl in & file . declarations {
if let Declaration ::SubConcept ( sc ) = decl {
if ! concept_names . contains ( & sc . parent_concept ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Parent concept '{}' not found for sub_concept '{}.{}' " ,
sc . parent_concept , sc . parent_concept , sc . name
) ,
help : Some ( format! (
" Add 'concept {}' before defining sub_concepts for it. " ,
sc . parent_concept
) ) ,
} ) ;
}
}
}
}
2026-02-14 14:17:40 +00:00
/// Get the type name of a Value for error messages
fn value_type_name ( value : & Value ) -> & 'static str {
match value {
| Value ::Number ( _ ) = > " Number " ,
| Value ::Decimal ( _ ) = > " Decimal " ,
| Value ::Text ( _ ) = > " Text " ,
| Value ::Boolean ( _ ) = > " Boolean " ,
| Value ::Range ( _ , _ ) = > " Range " ,
| Value ::Time ( _ ) = > " Time " ,
| Value ::Duration ( _ ) = > " Duration " ,
| Value ::Identifier ( _ ) = > " Identifier " ,
| Value ::List ( _ ) = > " List " ,
| Value ::Object ( _ ) = > " Object " ,
| Value ::ProseBlock ( _ ) = > " ProseBlock " ,
| Value ::Override ( _ ) = > " Override " ,
| Value ::Any = > " Any " ,
}
}
/// Check if two values have compatible types
fn values_type_compatible ( a : & Value , b : & Value ) -> bool {
matches! (
( a , b ) ,
( Value ::Number ( _ ) , Value ::Number ( _ ) ) |
( Value ::Decimal ( _ ) , Value ::Decimal ( _ ) ) |
( Value ::Text ( _ ) , Value ::Text ( _ ) ) |
( Value ::Boolean ( _ ) , Value ::Boolean ( _ ) ) |
( Value ::Time ( _ ) , Value ::Time ( _ ) ) |
( Value ::Duration ( _ ) , Value ::Duration ( _ ) ) |
( Value ::Identifier ( _ ) , Value ::Identifier ( _ ) ) |
( Value ::List ( _ ) , Value ::List ( _ ) ) |
( Value ::Object ( _ ) , Value ::Object ( _ ) ) |
( Value ::ProseBlock ( _ ) , Value ::ProseBlock ( _ ) ) |
( Value ::Range ( _ , _ ) , Value ::Range ( _ , _ ) ) |
( Value ::Range ( _ , _ ) , Value ::Number ( _ ) ) |
( Value ::Number ( _ ) , Value ::Range ( _ , _ ) ) |
( Value ::Range ( _ , _ ) , Value ::Decimal ( _ ) ) |
( Value ::Decimal ( _ ) , Value ::Range ( _ , _ ) ) |
( Value ::Any , _ ) |
( _ , Value ::Any )
)
}
/// Validate type invariance in species inheritance
///
/// When a template extends a species, any shared field names must have
/// compatible types. For example, if species Human has `age: 0` (Number),
/// then template Person: Human cannot have `age: "old"` (Text).
pub fn validate_species_type_invariance ( file : & File , collector : & mut ErrorCollector ) {
for decl in & file . declarations {
if let Declaration ::Template ( template ) = decl {
if let Some ( species_name ) = & template . species_base {
// Find the species declaration
let species = file . declarations . iter ( ) . find_map ( | d | {
if let Declaration ::Species ( s ) = d {
if s . name = = * species_name {
return Some ( s ) ;
}
}
None
} ) ;
if let Some ( species ) = species {
// Check each template field against species fields
for template_field in & template . fields {
if let Some ( species_field ) = species
. fields
. iter ( )
. find ( | f | f . name = = template_field . name )
{
if ! values_type_compatible ( & species_field . value , & template_field . value )
{
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Field '{}' has type {} in species {} but {} in template {} " ,
template_field . name ,
value_type_name ( & species_field . value ) ,
species_name ,
value_type_name ( & template_field . value ) ,
template . name ,
) ,
help : Some ( format! (
" Template '{}' inherits from species '{}'. Field '{}' must keep the same type ({}) as defined in the species. " ,
template . name ,
species_name ,
template_field . name ,
value_type_name ( & species_field . value ) ,
) ) ,
} ) ;
}
}
}
}
}
}
}
}
2026-02-14 14:24:05 +00:00
/// Validate concept_comparison patterns against the concept registry
///
/// Checks:
/// 1. The concept_comparison name references an existing concept
/// 2. All variant names in patterns exist in the concept's enum sub-concepts
/// 3. Patterns are exhaustive (all variants are covered)
pub fn validate_concept_comparison_patterns (
file : & File ,
registry : & crate ::resolve ::types ::ConceptRegistry ,
collector : & mut ErrorCollector ,
) {
for decl in & file . declarations {
if let Declaration ::ConceptComparison ( comp ) = decl {
// Check that the concept exists
let concept = match registry . lookup_concept ( & comp . name ) {
| Some ( c ) = > c ,
| None = > {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Concept '{}' not found for concept_comparison " ,
comp . name
) ,
help : Some ( format! (
" Add 'concept {}' before defining a concept_comparison for it. " ,
comp . name
) ) ,
} ) ;
continue ;
} ,
} ;
// Collect all enum variants from all enum sub-concepts
let mut all_variants : HashSet < String > = HashSet ::new ( ) ;
for sc_info in concept . sub_concepts . values ( ) {
if let SubConceptKind ::Enum { variants } = & sc_info . kind {
for variant in variants {
all_variants . insert ( variant . clone ( ) ) ;
}
}
}
// Check each variant pattern references valid variants
let mut covered_variants : HashSet < String > = HashSet ::new ( ) ;
for pattern in & comp . variants {
// Check that the variant name is a known variant
if ! all_variants . contains ( & pattern . name ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Variant '{}' not found in concept '{}' " ,
pattern . name , comp . name
) ,
help : Some ( format! (
" Available variants for '{}': {} " ,
comp . name ,
if all_variants . is_empty ( ) {
" (none - add sub_concept with enum variants) " . to_string ( )
} else {
let mut sorted : Vec < _ > = all_variants . iter ( ) . collect ( ) ;
sorted . sort ( ) ;
sorted
. iter ( )
. map ( | s | s . as_str ( ) )
. collect ::< Vec < _ > > ( )
. join ( " , " )
}
) ) ,
} ) ;
} else {
covered_variants . insert ( pattern . name . clone ( ) ) ;
}
// Check field conditions reference valid fields in the Is values
for condition in & pattern . conditions {
if let Condition ::Is ( values ) = & condition . condition {
for value in values {
// Check that the value references a valid variant
if ! all_variants . contains ( value ) {
collector . add ( ResolveError ::ValidationError {
message : format ! (
" Value '{}' in condition for variant '{}' not found in concept '{}' " ,
value , pattern . name , comp . name
) ,
help : Some ( format! (
" Available variants: {} " ,
{
let mut sorted : Vec < _ > = all_variants . iter ( ) . collect ( ) ;
sorted . sort ( ) ;
sorted . iter ( ) . map ( | s | s . as_str ( ) ) . collect ::< Vec < _ > > ( ) . join ( " , " )
}
) ) ,
} ) ;
}
}
}
}
}
// Check exhaustiveness - all variants should be covered
let uncovered : Vec < _ > = all_variants . difference ( & covered_variants ) . collect ( ) ;
if ! uncovered . is_empty ( ) {
let mut sorted : Vec < _ > = uncovered . into_iter ( ) . collect ( ) ;
sorted . sort ( ) ;
collector . add ( ResolveError ::ValidationError {
message : format ! (
" concept_comparison '{}' is not exhaustive: missing variants {} " ,
comp . name ,
sorted
. iter ( )
. map ( | s | format! ( " ' {} ' " , s ) )
. collect ::< Vec < _ > > ( )
. join ( " , " )
) ,
help : Some ( format! (
" Add patterns for all variants of concept '{}'. Missing: {} " ,
comp . name ,
sorted
. iter ( )
. map ( | s | s . as_str ( ) )
. collect ::< Vec < _ > > ( )
. join ( " , " )
) ) ,
} ) ;
}
}
}
}
2026-02-08 13:24:35 +00:00
/// Validate an entire file
///
/// Collects all validation errors and returns them together instead of failing
/// fast.
pub fn validate_file ( file : & File , action_registry : & HashSet < String > ) -> Result < ( ) > {
let mut collector = ErrorCollector ::new ( ) ;
2026-02-14 14:07:39 +00:00
// Type system validation
validate_sub_concept_parents ( file , & mut collector ) ;
2026-02-14 14:17:40 +00:00
validate_species_type_invariance ( file , & mut collector ) ;
2026-02-14 14:24:05 +00:00
let concept_registry = crate ::resolve ::types ::ConceptRegistry ::from_file ( file ) ;
validate_concept_comparison_patterns ( file , & concept_registry , & mut collector ) ;
2026-02-14 14:07:39 +00:00
2026-02-08 13:24:35 +00:00
for decl in & file . declarations {
match decl {
| Declaration ::Character ( c ) = > {
validate_trait_ranges ( & c . fields , & mut collector ) ;
2026-02-13 21:52:03 +00:00
validate_character_resource_links ( c , & mut collector ) ;
} ,
| Declaration ::Institution ( i ) = > {
validate_institution_resource_links ( i , & mut collector ) ;
2026-02-08 13:24:35 +00:00
} ,
| Declaration ::Relationship ( r ) = > {
validate_relationship_bonds ( std ::slice ::from_ref ( r ) , & mut collector ) ;
} ,
| Declaration ::Schedule ( s ) = > {
validate_schedule_overlaps ( s , & mut collector ) ;
2026-02-13 21:52:03 +00:00
validate_schedule_composition ( s , & mut collector ) ;
2026-02-08 13:24:35 +00:00
} ,
| Declaration ::LifeArc ( la ) = > {
validate_life_arc_transitions ( la , & mut collector ) ;
} ,
| Declaration ::Behavior ( bt ) = > {
validate_behavior_tree_actions ( bt , action_registry , & mut collector ) ;
} ,
| _ = > {
// Other declarations don't need validation yet
} ,
}
}
collector . into_result ( ( ) )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn test_valid_trait_ranges ( ) {
let fields = vec! [
Field {
name : " bond " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Decimal ( 0.8 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ,
Field {
name : " age " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Number ( 30 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ,
] ;
let mut collector = ErrorCollector ::new ( ) ;
validate_trait_ranges ( & fields , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_invalid_bond_value_too_high ( ) {
let fields = vec! [ Field {
name : " bond " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Decimal ( 1.5 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ] ;
let mut collector = ErrorCollector ::new ( ) ;
validate_trait_ranges ( & fields , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_invalid_bond_value_negative ( ) {
let fields = vec! [ Field {
name : " bond " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Decimal ( - 0.1 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ] ;
let mut collector = ErrorCollector ::new ( ) ;
validate_trait_ranges ( & fields , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_invalid_age_negative ( ) {
let fields = vec! [ Field {
name : " age " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Number ( - 5 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ] ;
let mut collector = ErrorCollector ::new ( ) ;
validate_trait_ranges ( & fields , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_invalid_age_too_high ( ) {
let fields = vec! [ Field {
name : " age " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Number ( 200 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ] ;
let mut collector = ErrorCollector ::new ( ) ;
validate_trait_ranges ( & fields , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_valid_relationship_bond ( ) {
let relationship = Relationship {
name : " Test " . to_string ( ) ,
participants : vec ! [ ] ,
fields : vec ! [ Field {
name : " bond " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Decimal ( 0.9 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ] ,
span : Span ::new ( 0 , 100 ) ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_relationship_bonds ( & [ relationship ] , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_invalid_relationship_bond ( ) {
let relationship = Relationship {
name : " Test " . to_string ( ) ,
participants : vec ! [ ] ,
fields : vec ! [ Field {
name : " bond " . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Decimal ( 1.2 ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
} ] ,
span : Span ::new ( 0 , 100 ) ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_relationship_bonds ( & [ relationship ] , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_life_arc_valid_transitions ( ) {
let life_arc = LifeArc {
name : " Test " . to_string ( ) ,
states : vec ! [
ArcState {
name : " start " . to_string ( ) ,
2026-02-08 15:45:56 +00:00
on_enter : None ,
2026-02-08 13:24:35 +00:00
transitions : vec ! [ Transition {
to : " end " . to_string ( ) ,
condition : Expr ::Identifier ( vec! [ " ready " . to_string ( ) ] ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
span : Span ::new ( 0 , 50 ) ,
} ,
ArcState {
name : " end " . to_string ( ) ,
2026-02-08 15:45:56 +00:00
on_enter : None ,
2026-02-08 13:24:35 +00:00
transitions : vec ! [ ] ,
span : Span ::new ( 50 , 100 ) ,
} ,
] ,
span : Span ::new ( 0 , 100 ) ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_life_arc_transitions ( & life_arc , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_life_arc_invalid_transition ( ) {
let life_arc = LifeArc {
name : " Test " . to_string ( ) ,
states : vec ! [ ArcState {
name : " start " . to_string ( ) ,
2026-02-08 15:45:56 +00:00
on_enter : None ,
2026-02-08 13:24:35 +00:00
transitions : vec ! [ Transition {
to : " nonexistent " . to_string ( ) ,
condition : Expr ::Identifier ( vec! [ " ready " . to_string ( ) ] ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
span : Span ::new ( 0 , 50 ) ,
} ] ,
span : Span ::new ( 0 , 100 ) ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_life_arc_transitions ( & life_arc , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_behavior_tree_valid_actions ( ) {
let mut registry = HashSet ::new ( ) ;
registry . insert ( " walk " . to_string ( ) ) ;
registry . insert ( " eat " . to_string ( ) ) ;
let tree = Behavior {
name : " Test " . to_string ( ) ,
2026-02-13 21:52:03 +00:00
root : BehaviorNode ::Sequence {
label : None ,
children : vec ! [
BehaviorNode ::Action ( " walk " . to_string ( ) , vec! [ ] ) ,
BehaviorNode ::Action ( " eat " . to_string ( ) , vec! [ ] ) ,
] ,
} ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 100 ) ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_behavior_tree_actions ( & tree , & registry , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_behavior_tree_invalid_action ( ) {
// Create a registry with some actions (but not "unknown_action")
let mut registry = HashSet ::new ( ) ;
registry . insert ( " walk " . to_string ( ) ) ;
registry . insert ( " work " . to_string ( ) ) ;
let tree = Behavior {
name : " Test " . to_string ( ) ,
root : BehaviorNode ::Action ( " unknown_action " . to_string ( ) , vec! [ ] ) ,
span : Span ::new ( 0 , 100 ) ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_behavior_tree_actions ( & tree , & registry , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
2026-02-14 14:07:39 +00:00
#[ test ]
fn test_sub_concept_with_valid_parent ( ) {
let file = File {
declarations : vec ! [
Declaration ::Concept ( ConceptDecl {
name : " Cup " . to_string ( ) ,
span : Span ::new ( 0 , 10 ) ,
} ) ,
Declaration ::SubConcept ( SubConceptDecl {
name : " Type " . to_string ( ) ,
parent_concept : " Cup " . to_string ( ) ,
kind : SubConceptKind ::Enum {
variants : vec ! [
" Small " . to_string ( ) ,
" Medium " . to_string ( ) ,
" Large " . to_string ( ) ,
] ,
} ,
span : Span ::new ( 20 , 60 ) ,
} ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_sub_concept_parents ( & file , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_sub_concept_with_missing_parent ( ) {
let file = File {
declarations : vec ! [ Declaration ::SubConcept ( SubConceptDecl {
name : " Type " . to_string ( ) ,
parent_concept : " Cup " . to_string ( ) ,
kind : SubConceptKind ::Enum {
variants : vec ! [ " Small " . to_string ( ) ] ,
} ,
span : Span ::new ( 0 , 40 ) ,
} ) ] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_sub_concept_parents ( & file , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_multiple_sub_concepts_same_valid_parent ( ) {
let file = File {
declarations : vec ! [
Declaration ::Concept ( ConceptDecl {
name : " Cup " . to_string ( ) ,
span : Span ::new ( 0 , 10 ) ,
} ) ,
Declaration ::SubConcept ( SubConceptDecl {
name : " Type " . to_string ( ) ,
parent_concept : " Cup " . to_string ( ) ,
kind : SubConceptKind ::Enum {
variants : vec ! [ " Mug " . to_string ( ) ] ,
} ,
span : Span ::new ( 20 , 40 ) ,
} ) ,
Declaration ::SubConcept ( SubConceptDecl {
name : " Material " . to_string ( ) ,
parent_concept : " Cup " . to_string ( ) ,
kind : SubConceptKind ::Enum {
variants : vec ! [ " Glass " . to_string ( ) ] ,
} ,
span : Span ::new ( 50 , 70 ) ,
} ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_sub_concept_parents ( & file , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_sub_concept_wrong_parent_name ( ) {
let file = File {
declarations : vec ! [
Declaration ::Concept ( ConceptDecl {
name : " Plate " . to_string ( ) ,
span : Span ::new ( 0 , 10 ) ,
} ) ,
Declaration ::SubConcept ( SubConceptDecl {
name : " Type " . to_string ( ) ,
parent_concept : " Cup " . to_string ( ) ,
kind : SubConceptKind ::Enum {
variants : vec ! [ " Small " . to_string ( ) ] ,
} ,
span : Span ::new ( 20 , 40 ) ,
} ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_sub_concept_parents ( & file , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
2026-02-14 14:17:40 +00:00
// ===== Type Invariance Tests =====
fn make_species ( name : & str , fields : Vec < Field > ) -> Species {
Species {
name : name . to_string ( ) ,
includes : vec ! [ ] ,
fields ,
span : Span ::new ( 0 , 10 ) ,
}
}
fn make_template_decl ( name : & str , species_base : Option < & str > , fields : Vec < Field > ) -> Template {
Template {
name : name . to_string ( ) ,
species_base : species_base . map ( | s | s . to_string ( ) ) ,
fields ,
strict : false ,
includes : vec ! [ ] ,
uses_behaviors : None ,
uses_schedule : None ,
span : Span ::new ( 0 , 10 ) ,
}
}
#[ test ]
fn test_type_invariance_matching_types ( ) {
let file = File {
declarations : vec ! [
Declaration ::Species ( make_species (
" Human " ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Number ( 0 ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
Declaration ::Template ( make_template_decl (
" Person " ,
Some ( " Human " ) ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Number ( 30 ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_species_type_invariance ( & file , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_type_invariance_mismatch ( ) {
let file = File {
declarations : vec ! [
Declaration ::Species ( make_species (
" Human " ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Number ( 0 ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
Declaration ::Template ( make_template_decl (
" Person " ,
Some ( " Human " ) ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Decimal ( 30.5 ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_species_type_invariance ( & file , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_type_invariance_number_to_text_error ( ) {
let file = File {
declarations : vec ! [
Declaration ::Species ( make_species (
" Human " ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Number ( 0 ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
Declaration ::Template ( make_template_decl (
" Person " ,
Some ( " Human " ) ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Text ( " old " . to_string ( ) ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_species_type_invariance ( & file , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_type_invariance_range_compatible_with_number ( ) {
let file = File {
declarations : vec ! [
Declaration ::Species ( make_species (
" Human " ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Number ( 0 ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
Declaration ::Template ( make_template_decl (
" Person " ,
Some ( " Human " ) ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Range (
Box ::new ( Value ::Number ( 18 ) ) ,
Box ::new ( Value ::Number ( 65 ) ) ,
) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ,
] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_species_type_invariance ( & file , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_type_invariance_no_species_base ( ) {
// Template without species_base should not trigger validation
let file = File {
declarations : vec ! [ Declaration ::Template ( make_template_decl (
" Person " ,
None ,
vec! [ Field {
name : " age " . to_string ( ) ,
value : Value ::Text ( " old " . to_string ( ) ) ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
) ) ] ,
} ;
let mut collector = ErrorCollector ::new ( ) ;
validate_species_type_invariance ( & file , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
2026-02-14 14:24:05 +00:00
// ===== Concept Comparison Validation Tests =====
use crate ::resolve ::types ::ConceptRegistry ;
fn make_concept_comparison_file ( ) -> File {
File {
declarations : vec ! [
Declaration ::Concept ( ConceptDecl {
name : " Cup " . to_string ( ) ,
span : Span ::new ( 0 , 10 ) ,
} ) ,
Declaration ::SubConcept ( SubConceptDecl {
name : " Type " . to_string ( ) ,
parent_concept : " Cup " . to_string ( ) ,
kind : SubConceptKind ::Enum {
variants : vec ! [
" Small " . to_string ( ) ,
" Medium " . to_string ( ) ,
" Large " . to_string ( ) ,
] ,
} ,
span : Span ::new ( 20 , 60 ) ,
} ) ,
] ,
}
}
#[ test ]
fn test_concept_comparison_valid_exhaustive ( ) {
let mut file = make_concept_comparison_file ( ) ;
file . declarations
. push ( Declaration ::ConceptComparison ( ConceptComparisonDecl {
name : " Cup " . to_string ( ) ,
variants : vec ! [
VariantPattern {
name : " Small " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Medium " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Large " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
] ,
span : Span ::new ( 0 , 100 ) ,
} ) ) ;
let registry = ConceptRegistry ::from_file ( & file ) ;
let mut collector = ErrorCollector ::new ( ) ;
validate_concept_comparison_patterns ( & file , & registry , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
#[ test ]
fn test_concept_comparison_not_exhaustive ( ) {
let mut file = make_concept_comparison_file ( ) ;
file . declarations
. push ( Declaration ::ConceptComparison ( ConceptComparisonDecl {
name : " Cup " . to_string ( ) ,
variants : vec ! [
VariantPattern {
name : " Small " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
// Missing Medium and Large
] ,
span : Span ::new ( 0 , 100 ) ,
} ) ) ;
let registry = ConceptRegistry ::from_file ( & file ) ;
let mut collector = ErrorCollector ::new ( ) ;
validate_concept_comparison_patterns ( & file , & registry , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_concept_comparison_unknown_variant ( ) {
let mut file = make_concept_comparison_file ( ) ;
file . declarations
. push ( Declaration ::ConceptComparison ( ConceptComparisonDecl {
name : " Cup " . to_string ( ) ,
variants : vec ! [
VariantPattern {
name : " Small " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Medium " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Large " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Huge " . to_string ( ) , // Not a valid variant
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
] ,
span : Span ::new ( 0 , 100 ) ,
} ) ) ;
let registry = ConceptRegistry ::from_file ( & file ) ;
let mut collector = ErrorCollector ::new ( ) ;
validate_concept_comparison_patterns ( & file , & registry , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_concept_comparison_missing_concept ( ) {
let file = File {
declarations : vec ! [ Declaration ::ConceptComparison ( ConceptComparisonDecl {
name : " NonExistent " . to_string ( ) ,
variants : vec ! [ ] ,
span : Span ::new ( 0 , 100 ) ,
} ) ] ,
} ;
let registry = ConceptRegistry ::from_file ( & file ) ;
let mut collector = ErrorCollector ::new ( ) ;
validate_concept_comparison_patterns ( & file , & registry , & mut collector ) ;
assert! ( collector . has_errors ( ) ) ;
}
#[ test ]
fn test_concept_comparison_with_field_conditions ( ) {
let mut file = make_concept_comparison_file ( ) ;
file . declarations
. push ( Declaration ::ConceptComparison ( ConceptComparisonDecl {
name : " Cup " . to_string ( ) ,
variants : vec ! [
VariantPattern {
name : " Small " . to_string ( ) ,
conditions : vec ! [ FieldCondition {
field_name : " material " . to_string ( ) ,
condition : Condition ::Is ( vec! [ " Medium " . to_string ( ) ] ) , // Valid variant ref
span : Span ::new ( 0 , 10 ) ,
} ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Medium " . to_string ( ) ,
conditions : vec ! [ FieldCondition {
field_name : " material " . to_string ( ) ,
condition : Condition ::Any ,
span : Span ::new ( 0 , 10 ) ,
} ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
VariantPattern {
name : " Large " . to_string ( ) ,
conditions : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ,
] ,
span : Span ::new ( 0 , 100 ) ,
} ) ) ;
let registry = ConceptRegistry ::from_file ( & file ) ;
let mut collector = ErrorCollector ::new ( ) ;
validate_concept_comparison_patterns ( & file , & registry , & mut collector ) ;
assert! ( ! collector . has_errors ( ) ) ;
}
2026-02-08 13:24:35 +00:00
}