487 lines
12 KiB
Markdown
487 lines
12 KiB
Markdown
|
|
# Template Merge Implementation Notes
|
||
|
|
**Author:** Resource Linking Architect
|
||
|
|
**Date:** 2026-02-12
|
||
|
|
**Purpose:** Implementation planning for Phase 3 (Resolution)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Core Merge Algorithm
|
||
|
|
|
||
|
|
### Data Structures
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// In resolve/merge.rs
|
||
|
|
|
||
|
|
/// Tracks behavior links during merge to detect overrides
|
||
|
|
struct BehaviorMergeContext {
|
||
|
|
seen_behaviors: HashMap<String, BehaviorLink>, // behavior_name -> link
|
||
|
|
result: Vec<BehaviorLink>,
|
||
|
|
warnings: Vec<MergeWarning>,
|
||
|
|
}
|
||
|
|
|
||
|
|
enum MergeWarning {
|
||
|
|
DefaultConflict {
|
||
|
|
character_behavior: String,
|
||
|
|
template_behavior: String,
|
||
|
|
},
|
||
|
|
PriorityConflict {
|
||
|
|
behavior: String,
|
||
|
|
character_priority: Priority,
|
||
|
|
template_priority: Priority,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Merge Function Signature
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub fn merge_behavior_links(
|
||
|
|
character_links: Vec<BehaviorLink>,
|
||
|
|
template_links: Vec<BehaviorLink>,
|
||
|
|
character_name: &str,
|
||
|
|
template_name: &str,
|
||
|
|
) -> Result<(Vec<ResolvedBehaviorLink>, Vec<MergeWarning>), MergeError> {
|
||
|
|
// Implementation
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step-by-Step Algorithm
|
||
|
|
|
||
|
|
```rust
|
||
|
|
fn merge_behavior_links(
|
||
|
|
character_links: Vec<BehaviorLink>,
|
||
|
|
template_links: Vec<BehaviorLink>,
|
||
|
|
character_name: &str,
|
||
|
|
template_name: &str,
|
||
|
|
) -> Result<(Vec<ResolvedBehaviorLink>, Vec<MergeWarning>), MergeError> {
|
||
|
|
|
||
|
|
let mut ctx = BehaviorMergeContext {
|
||
|
|
seen_behaviors: HashMap::new(),
|
||
|
|
result: Vec::new(),
|
||
|
|
warnings: Vec::new(),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Step 1: Add all character links (highest precedence)
|
||
|
|
for link in character_links {
|
||
|
|
let behavior_name = link.tree.join("::");
|
||
|
|
|
||
|
|
// Check for duplicate behavior in character itself
|
||
|
|
if ctx.seen_behaviors.contains_key(&behavior_name) {
|
||
|
|
return Err(MergeError::DuplicateBehavior {
|
||
|
|
behavior: behavior_name,
|
||
|
|
source: character_name.to_string(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.seen_behaviors.insert(behavior_name.clone(), link.clone());
|
||
|
|
ctx.result.push(link);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 2: Add template links that aren't overridden
|
||
|
|
for link in template_links {
|
||
|
|
let behavior_name = link.tree.join("::");
|
||
|
|
|
||
|
|
if let Some(char_link) = ctx.seen_behaviors.get(&behavior_name) {
|
||
|
|
// Character overrides this behavior - check for conflicts
|
||
|
|
|
||
|
|
// Check priority conflict
|
||
|
|
if let (Some(char_pri), Some(tmpl_pri)) = (&char_link.priority, &link.priority) {
|
||
|
|
if char_pri != tmpl_pri {
|
||
|
|
ctx.warnings.push(MergeWarning::PriorityConflict {
|
||
|
|
behavior: behavior_name.clone(),
|
||
|
|
character_priority: char_pri.clone(),
|
||
|
|
template_priority: tmpl_pri.clone(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Skip template link (already have character's version)
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// No override, add template link
|
||
|
|
ctx.result.push(link);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 3: Check for default conflicts
|
||
|
|
let char_defaults: Vec<_> = ctx.result.iter()
|
||
|
|
.filter(|link| link.is_default)
|
||
|
|
.take(2) // Only need to check if >1
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
if char_defaults.len() > 1 {
|
||
|
|
ctx.warnings.push(MergeWarning::DefaultConflict {
|
||
|
|
// ... warning details
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok((ctx.result, ctx.warnings))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Multi-Level Template Merge
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub fn resolve_template_hierarchy(
|
||
|
|
character: &Character,
|
||
|
|
templates: &HashMap<String, Template>,
|
||
|
|
) -> Result<(Vec<BehaviorLink>, Vec<ScheduleLink>), MergeError> {
|
||
|
|
|
||
|
|
// Step 1: Build template chain (deepest first)
|
||
|
|
let mut template_chain = Vec::new();
|
||
|
|
let mut current_template_name = character.template.clone();
|
||
|
|
|
||
|
|
while let Some(tmpl_name) = current_template_name {
|
||
|
|
let template = templates.get(&tmpl_name)
|
||
|
|
.ok_or(MergeError::UnresolvedTemplate { name: tmpl_name.clone() })?;
|
||
|
|
|
||
|
|
template_chain.push(template);
|
||
|
|
current_template_name = template.parent_template.clone();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 2: Merge from deepest template up to character
|
||
|
|
template_chain.reverse(); // Now [deepest, ..., direct parent]
|
||
|
|
|
||
|
|
let mut merged_behaviors = Vec::new();
|
||
|
|
let mut merged_schedules = Vec::new();
|
||
|
|
|
||
|
|
for template in template_chain {
|
||
|
|
let (behaviors, warnings) = merge_behavior_links(
|
||
|
|
merged_behaviors,
|
||
|
|
template.behavior_links.clone(),
|
||
|
|
character.name,
|
||
|
|
&template.name,
|
||
|
|
)?;
|
||
|
|
merged_behaviors = behaviors;
|
||
|
|
// TODO: emit warnings
|
||
|
|
|
||
|
|
// Same for schedules...
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 3: Merge character on top
|
||
|
|
let (final_behaviors, warnings) = merge_behavior_links(
|
||
|
|
character.behavior_links.clone(),
|
||
|
|
merged_behaviors,
|
||
|
|
character.name,
|
||
|
|
"character",
|
||
|
|
)?;
|
||
|
|
|
||
|
|
Ok((final_behaviors, merged_schedules))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Complex Merge Scenarios to Test
|
||
|
|
|
||
|
|
### Scenario 1: Simple Override
|
||
|
|
```storybook
|
||
|
|
template Worker {
|
||
|
|
uses behaviors: [A, B, C]
|
||
|
|
}
|
||
|
|
|
||
|
|
character Martha from Worker {
|
||
|
|
uses behaviors: [B, D] // Override B, add D
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expected: [B(Martha), D, A, C]
|
||
|
|
// A and C come from Worker, B is Martha's version
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scenario 2: Multi-Level with Overrides
|
||
|
|
```storybook
|
||
|
|
template Base {
|
||
|
|
uses behaviors: [A, B]
|
||
|
|
}
|
||
|
|
|
||
|
|
template Mid from Base {
|
||
|
|
uses behaviors: [B, C] // Override B from Base
|
||
|
|
}
|
||
|
|
|
||
|
|
character Char from Mid {
|
||
|
|
uses behaviors: [C, D] // Override C from Mid
|
||
|
|
}
|
||
|
|
|
||
|
|
// Merge order:
|
||
|
|
// 1. Base: [A, B(Base)]
|
||
|
|
// 2. Mid merges on Base: [B(Mid), C, A]
|
||
|
|
// 3. Char merges on Mid: [C(Char), D, B(Mid), A]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scenario 3: Priority Changes Through Chain
|
||
|
|
```storybook
|
||
|
|
template Base {
|
||
|
|
uses behaviors: [{ tree: Rest, priority: high }]
|
||
|
|
}
|
||
|
|
|
||
|
|
template Mid from Base {
|
||
|
|
uses behaviors: [{ tree: Rest, priority: normal }] // Warning!
|
||
|
|
}
|
||
|
|
|
||
|
|
character Char from Mid {
|
||
|
|
uses behaviors: [{ tree: Rest, priority: low }] // Warning!
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expected: [Rest(low)]
|
||
|
|
// Warnings:
|
||
|
|
// - Mid changed Rest priority from high to normal
|
||
|
|
// - Char changed Rest priority from normal to low
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scenario 4: Condition Overrides
|
||
|
|
```storybook
|
||
|
|
template Worker {
|
||
|
|
uses behaviors: [
|
||
|
|
{ tree: Work, when: employed }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
character Martha from Worker {
|
||
|
|
uses behaviors: [
|
||
|
|
{ tree: Work, when: at_bakery } // Override condition
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expected: [Work(when: at_bakery)]
|
||
|
|
// Template's Work(when: employed) is completely replaced
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scenario 5: Default Conflicts
|
||
|
|
```storybook
|
||
|
|
template Worker {
|
||
|
|
uses behaviors: [
|
||
|
|
{ tree: Idle, default: true }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
character Martha from Worker {
|
||
|
|
uses behaviors: [
|
||
|
|
{ tree: Rest, default: true }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expected: [Rest(default), Idle(not default)]
|
||
|
|
// Warning: Both template and character define defaults
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scenario 6: Empty Array Edge Case
|
||
|
|
```storybook
|
||
|
|
template Worker {
|
||
|
|
uses behaviors: [A, B, C]
|
||
|
|
}
|
||
|
|
|
||
|
|
character Martha from Worker {
|
||
|
|
uses behaviors: [] // What does this mean?
|
||
|
|
}
|
||
|
|
|
||
|
|
// If empty = "clear all": []
|
||
|
|
// If empty = "ignore, inherit": [A, B, C]
|
||
|
|
// Waiting for Sienna's decision (Question 1)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scenario 7: Diamond Inheritance (Not Supported)
|
||
|
|
```storybook
|
||
|
|
template A { uses behaviors: [X] }
|
||
|
|
template B from A { uses behaviors: [Y] }
|
||
|
|
template C from A { uses behaviors: [Z] }
|
||
|
|
template D from B, C { ... } // ERROR: Multiple inheritance not supported
|
||
|
|
```
|
||
|
|
|
||
|
|
**Decision:** Single inheritance only (one parent template max).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Error Message Design
|
||
|
|
|
||
|
|
### Error: Duplicate Behavior in Same Context
|
||
|
|
|
||
|
|
```
|
||
|
|
error: duplicate behavior definition
|
||
|
|
┌─ characters/martha.sb:10:9
|
||
|
|
│
|
||
|
|
10 │ { tree: BakeryWork, priority: normal }
|
||
|
|
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
11 │ { tree: BakeryWork, priority: high }
|
||
|
|
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definition
|
||
|
|
│
|
||
|
|
= note: behavior 'BakeryWork' is already defined in this character
|
||
|
|
= help: remove one of the duplicate definitions or use different behaviors
|
||
|
|
```
|
||
|
|
|
||
|
|
### Warning: Priority Conflict in Template Merge
|
||
|
|
|
||
|
|
```
|
||
|
|
warning: behavior priority changed from template
|
||
|
|
┌─ characters/martha.sb:9:9
|
||
|
|
│
|
||
|
|
9 │ { tree: Rest, priority: low }
|
||
|
|
│ ^^^^^^^^^^^^^^^^
|
||
|
|
│
|
||
|
|
= note: template 'Worker' defines 'Rest' with priority 'high'
|
||
|
|
= note: character's priority (low) will override template's (high)
|
||
|
|
= help: if this is intentional, no action needed
|
||
|
|
```
|
||
|
|
|
||
|
|
### Warning: Multiple Defaults
|
||
|
|
|
||
|
|
```
|
||
|
|
warning: multiple default behaviors defined
|
||
|
|
┌─ characters/martha.sb:10:9
|
||
|
|
│
|
||
|
|
10 │ { tree: Rest, default: true }
|
||
|
|
│ ^^^^^^^^^^^^^^^
|
||
|
|
│
|
||
|
|
= note: template 'Worker' also defines a default behavior: 'Idle'
|
||
|
|
= note: character's default (Rest) will be used; template's (Idle) remains as non-default
|
||
|
|
= help: only one default is active at runtime
|
||
|
|
```
|
||
|
|
|
||
|
|
### Error: Unresolved Template
|
||
|
|
|
||
|
|
```
|
||
|
|
error: unresolved template reference
|
||
|
|
┌─ characters/martha.sb:1:25
|
||
|
|
│
|
||
|
|
1 │ character Martha from Workerr {
|
||
|
|
│ ^^^^^^^ template 'Workerr' not found
|
||
|
|
│
|
||
|
|
= help: did you mean 'Worker'? (defined in templates/worker.sb)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Warning: Condition Override
|
||
|
|
|
||
|
|
```
|
||
|
|
warning: behavior condition changed from template
|
||
|
|
┌─ characters/martha.sb:9:9
|
||
|
|
│
|
||
|
|
9 │ { tree: Work, when: at_bakery }
|
||
|
|
│ ^^^^^^^^^^^^^^^^^^^
|
||
|
|
│
|
||
|
|
= note: template 'Worker' defines 'Work' with condition 'employed'
|
||
|
|
= note: character's condition will replace template's condition
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[cfg(test)]
|
||
|
|
mod merge_tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_simple_merge() {
|
||
|
|
let char_links = vec![
|
||
|
|
behavior_link("BakeryWork", None, None),
|
||
|
|
];
|
||
|
|
let tmpl_links = vec![
|
||
|
|
behavior_link("BasicNeeds", Some(Priority::Critical), None),
|
||
|
|
behavior_link("Rest", Some(Priority::Normal), None),
|
||
|
|
];
|
||
|
|
|
||
|
|
let (result, warnings) = merge_behavior_links(
|
||
|
|
char_links, tmpl_links, "Martha", "Worker"
|
||
|
|
).unwrap();
|
||
|
|
|
||
|
|
assert_eq!(result.len(), 3);
|
||
|
|
assert_eq!(result[0].tree, "BakeryWork");
|
||
|
|
assert_eq!(result[1].tree, "BasicNeeds");
|
||
|
|
assert_eq!(result[2].tree, "Rest");
|
||
|
|
assert!(warnings.is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_override_priority() {
|
||
|
|
let char_links = vec![
|
||
|
|
behavior_link("Rest", Some(Priority::Low), None),
|
||
|
|
];
|
||
|
|
let tmpl_links = vec![
|
||
|
|
behavior_link("Rest", Some(Priority::High), None),
|
||
|
|
];
|
||
|
|
|
||
|
|
let (result, warnings) = merge_behavior_links(
|
||
|
|
char_links, tmpl_links, "Martha", "Worker"
|
||
|
|
).unwrap();
|
||
|
|
|
||
|
|
assert_eq!(result.len(), 1);
|
||
|
|
assert_eq!(result[0].priority, Some(Priority::Low));
|
||
|
|
assert_eq!(warnings.len(), 1);
|
||
|
|
assert!(matches!(warnings[0], MergeWarning::PriorityConflict { .. }));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_multi_level_merge() {
|
||
|
|
// Base -> Mid -> Char
|
||
|
|
// Test that merging works through chain
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_default_conflict() {
|
||
|
|
// Both template and character define defaults
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_empty_array() {
|
||
|
|
// Waiting for Sienna's decision on semantics
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Integration Tests
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[test]
|
||
|
|
fn test_resolve_alice_wonderland() {
|
||
|
|
// Load Alice example with WonderlandCreature template
|
||
|
|
// Verify merged behaviors are correct
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_three_level_inheritance() {
|
||
|
|
// Mortal -> Worker -> Baker -> Martha
|
||
|
|
// Verify all behaviors present and priorities correct
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Performance Considerations
|
||
|
|
|
||
|
|
### Merge Complexity
|
||
|
|
|
||
|
|
- Single template: O(n + m) where n=char links, m=template links
|
||
|
|
- k-level templates: O(k * (n + m))
|
||
|
|
- Typical case: k=1-3, n+m < 20, negligible overhead
|
||
|
|
|
||
|
|
### Optimization Opportunities
|
||
|
|
|
||
|
|
1. **Cache merged templates**: If Template X is used by multiple characters, cache its fully-merged result
|
||
|
|
2. **Early termination**: If character defines no links, skip merge entirely
|
||
|
|
3. **Lazy merging**: Only merge when links are actually accessed at runtime
|
||
|
|
|
||
|
|
**Decision:** Start with simple O(k*n) implementation. Optimize only if profiling shows bottleneck.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Edge Cases Checklist
|
||
|
|
|
||
|
|
- [x] Character overrides all template behaviors
|
||
|
|
- [x] Character overrides some template behaviors
|
||
|
|
- [x] Character adds new behaviors to template
|
||
|
|
- [x] Multi-level template chain
|
||
|
|
- [x] Priority conflicts through chain
|
||
|
|
- [x] Condition overrides
|
||
|
|
- [x] Multiple defaults
|
||
|
|
- [ ] Empty array semantics (awaiting Sienna)
|
||
|
|
- [x] Diamond inheritance (not supported, single parent only)
|
||
|
|
- [x] Circular template references (should be caught in validation)
|
||
|
|
- [x] Template references non-existent parent (error)
|
||
|
|
- [x] Behavior name resolution fails (error)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Status:** Implementation notes complete, awaiting Checkpoint 2 approval to begin coding.
|