This commit completes the migration started in the previous commit, updating all remaining files: - Lexer: Changed token from Extends to Modifies - Parser: Updated lalrpop grammar rules and AST field names - AST: Renamed Schedule.extends field to modifies - Grammar: Updated tree-sitter grammar.js - Tree-sitter: Regenerated parser.c and node-types.json - Examples: Updated baker-family work schedules - Tests: Updated schedule composition tests and corpus - Docs: Updated all reference documentation and tutorials - Validation: Updated error messages and validation logic - Package: Bumped version to 0.3.1 in all package manifests All 554 tests pass.
24 KiB
Storybook Intermediate Representation (SBIR) v0.2.0 Specification
Version: 0.2.0 Status: Archived (superseded by v0.3.0) Date: February 2026
Note: This is a historical specification for SBIR v0.2.0. The current specification is SBIR v0.3.0. Type names in this document (Int, Float, String, Bool) reflect v0.2.0 terminology; v0.3.0 renamed these to Number, Decimal, Text, and Boolean respectively.
Table of Contents
- Introduction
- File Format Overview
- Section 1: Header
- Section 2: String Table
- Section 3: Types
- Section 4: Characters
- Section 5: Templates
- Section 6: Species
- Section 7: Behaviors
- Section 8: Schedules
- Section 9: Institutions
- Section 10: Relationships
- Section 11: Locations
- Section 12: Life Arcs
- Section 13: Enums
- Changelog
1. Introduction
1.1 Purpose
The Storybook Intermediate Representation (SBIR) is a binary format that represents compiled Storybook programs. It serves as the interchange format between the Storybook compiler and runtime engines.
1.2 Design Goals
- Compact: Efficient binary encoding for large story worlds
- Fast to load: Direct memory mapping when possible
- Versioned: Clear version tracking for format evolution
- Complete: Represents all semantic information from source
- Runtime-ready: Minimal post-processing required
1.3 Changes in v0.2.0
Major additions:
- Resource Linking System - Characters and institutions can link to behaviors and schedules with conditions and priorities
- Year-Long Schedule System - Schedules support temporal patterns (day-specific, seasonal, recurrence)
- Behavior Tree Enhancements - Named nodes, decorator parameters, keyword transformations
Breaking changes:
- CHARACTERS section extended with behavior_links and schedule_links
- INSTITUTIONS section extended with behavior_links and schedule_links
- SCHEDULES section redesigned with patterns and inheritance
- BEHAVIORS section extended with named nodes and parameterized decorators
2. File Format Overview
2.1 File Structure
[Header]
[String Table]
[Type Definitions]
[Characters Section]
[Templates Section]
[Species Section]
[Behaviors Section]
[Schedules Section]
[Institutions Section]
[Relationships Section]
[Locations Section]
[Life Arcs Section]
[Enums Section]
2.2 Primitive Types
| Type | Size | Description |
|---|---|---|
u8 |
1 byte | Unsigned 8-bit integer |
u16 |
2 bytes | Unsigned 16-bit integer (little-endian) |
u32 |
4 bytes | Unsigned 32-bit integer (little-endian) |
u64 |
8 bytes | Unsigned 64-bit integer (little-endian) |
i32 |
4 bytes | Signed 32-bit integer (little-endian) |
i64 |
8 bytes | Signed 64-bit integer (little-endian) |
f32 |
4 bytes | IEEE 754 single-precision float |
f64 |
8 bytes | IEEE 754 double-precision float |
bool |
1 byte | 0 = false, 1 = true |
String |
Variable | Length-prefixed UTF-8: u32 length + [u8; length] |
StringRef |
4 bytes | Index into string table (u32) |
2.3 Common Structures
Option
Optional values are encoded with a discriminant byte followed by the value if present:
u8 discriminant:
0 = None → No additional bytes, next field starts immediately
1 = Some → T data follows immediately after discriminant
Encoding:
- None case: Just 1 byte (0x00), nothing else
- Some case: 1 byte (0x01) + full T encoding
Examples:
Option<StringRef> when None:
0x00 ← 1 byte total
Option<StringRef> when Some(42):
0x01 ← discriminant (Some)
0x2A 0x00 0x00 0x00 ← StringRef = 42 (u32)
← 5 bytes total
Option<Vec<Field>> when None:
0x00 ← 1 byte total
Option<Vec<Field>> when Some([field1, field2]):
0x01 ← discriminant (Some)
0x02 0x00 0x00 0x00 ← count = 2 (u32)
[Field 1 encoding] ← First field
[Field 2 encoding] ← Second field
← 5+ bytes (depends on field sizes)
Vec
u32 count
[T; count]
2.4 Binary Format Conventions
IMPORTANT: SBIR is a packed binary format with no separators.
No Delimiter Bytes
All data is laid out sequentially in memory with no separator characters between:
- Items in a Vec
- Fields in a struct
- Sections in the file
- Values of any kind
The file is a continuous stream of bytes where each element's size determines where the next element begins.
Length-Prefix Pattern
All variable-length data uses a length-prefix pattern:
- Length/count field (u32) tells you "how much data is coming"
- Data bytes - read exactly that amount
- Next field starts immediately after (no gap, no separator)
Reading Variable-Length Vec
When T itself is variable-length, each item carries its own size information:
Example: Vec
u32 count = 3 ← "There are 3 strings"
String 1:
u32 length = 5 ← "First string is 5 bytes"
[u8; 5] = "hello" ← Read 5 bytes, next item starts immediately
String 2:
u32 length = 5 ← "Second string is 5 bytes"
[u8; 5] = "world" ← Read 5 bytes
String 3:
u32 length = 3 ← "Third string is 3 bytes"
[u8; 3] = "foo" ← Read 3 bytes
Example: Vec (complex variable-length structs)
u32 count = 2 ← "There are 2 characters"
Character 1:
u32 name_len = 6
[u8; 6] = "Martha"
u8 species_discriminant = 1 ← Option::Some
u32 species_ref = 10
u32 fields_count = 2
Field 1: (name_ref, value_discriminant, value_data)
Field 2: (name_ref, value_discriminant, value_data)
u32 template_refs_count = 1
StringRef = 15
u32 behavior_links_count = 0
u32 schedule_links_count = 0
Character 2:
(starts immediately after Character 1 ends)
u32 name_len = 5
[u8; 5] = "David"
... (continues)
Parsing Algorithm
The parser reads sequentially using each length field to know how many bytes to consume:
position = 0
while position < file_size:
read_length_or_discriminant()
read_exactly_that_many_bytes()
position += bytes_read
// Next field starts here (no seeking, no separator scanning)
Key Rules
- Fixed-size types (u8, u32, f64, etc.) take their exact size - no padding
- Variable-size types always start with a length prefix (u32)
- Option starts with a discriminant byte (0=None, 1=Some)
- Enums/discriminated unions start with a discriminant byte
- No alignment padding - all data is tightly packed
There are no:
- No newline characters (
\n) - No separators (
,,;, spaces) - No null terminators (except inside UTF-8 string data)
- No padding bytes between fields (unless explicitly specified)
- No section markers or headers (sections just follow each other)
The documentation uses newlines and indentation for readability only - the actual binary file is a continuous stream of bytes with no whitespace.
3. Section 1: Header
Magic: [u8; 4] // "SBIR" (0x53 0x42 0x49 0x52)
Version: u16 // Major version (0x0002 for v0.2.0)
MinorVersion: u16 // Minor version (0x0000)
Flags: u32 // Reserved (0x00000000)
SectionCount: u32 // Number of sections (currently 13)
Version History:
- v0.1.0: Implicit version (pre-release)
- v0.2.0: First formal versioned release
4. Section 2: String Table
The string table stores all strings used in the SBIR file.
Count: u32
Strings: [String; Count]
Usage: All StringRef types reference an index in this table.
Encoding: UTF-8 with length prefix.
Example:
Count: 3
Strings:
[0]: "Alice"
[1]: "Wonderland"
[2]: "rabbit_hole"
5. Section 3: Types
Note: This section is reserved for future type system enhancements. Currently unused.
Count: u32 // 0 in v0.2.0
Types: []
6. Section 4: Characters
6.1 Structure
Count: u32
Characters: [Character; Count]
6.2 Character Encoding
Character:
name: StringRef
species: Option<StringRef>
fields: Map<StringRef, Value>
template_refs: Vec<StringRef> // Templates this character uses
behavior_links: Vec<BehaviorLink> // NEW in v0.2.0
schedule_links: Vec<ScheduleLink> // NEW in v0.2.0
6.3 BehaviorLink (NEW in v0.2.0)
BehaviorLink:
behavior_id: u32 // Index into BEHAVIORS section
priority: u8 // 0=Low, 1=Normal, 2=High, 3=Critical
condition: Option<Expression> // Optional activation condition
is_default: bool // Default behavior (no condition)
Priority Encoding:
enum Priority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
Selection Algorithm:
- Filter links where
conditionevaluates to true (or is None) - Sort by priority (descending)
- Return highest priority link
- If tie, use declaration order
6.4 ScheduleLink (NEW in v0.2.0)
ScheduleLink:
schedule_id: u32 // Index into SCHEDULES section
condition: Option<Expression> // Optional activation condition
is_default: bool // Default schedule (fallback)
Selection Algorithm:
- Iterate schedule_links in order
- Skip default links initially
- Return first link where
conditionis true (or None) - If no match, use default link (if present)
6.5 Value Encoding
Value:
discriminant: u8
data: <depends on discriminant>
Discriminants:
0x01 = Int(i64)
0x02 = Float(f64)
0x03 = String(StringRef)
0x04 = Bool(bool)
0x05 = Range(Value, Value)
0x06 = Time(u8 hour, u8 minute, u8 second)
0x07 = Duration(u32 hours, u32 minutes, u32 seconds)
0x08 = Identifier(Vec<StringRef>) // Qualified path
0x09 = List(Vec<Value>)
0x0A = Object(Vec<Field>)
0x0B = ProseBlock(StringRef tag, String content)
0x0C = Override(...)
6.6 Expression Encoding
Expression:
discriminant: u8
data: <depends on discriminant>
Discriminants:
0x01 = IntLit(i64)
0x02 = FloatLit(f64)
0x03 = StringLit(StringRef)
0x04 = BoolLit(bool)
0x05 = Identifier(Vec<StringRef>)
0x06 = FieldAccess(Box<Expr>, StringRef)
0x07 = Comparison(Box<Expr>, CompOp, Box<Expr>)
0x08 = Logical(Box<Expr>, LogicalOp, Box<Expr>)
0x09 = Unary(UnaryOp, Box<Expr>)
0x0A = Quantifier(QuantifierKind, StringRef var, Box<Expr> collection, Box<Expr> predicate)
CompOp: u8 (0x01=Eq, 0x02=Ne, 0x03=Lt, 0x04=Le, 0x05=Gt, 0x06=Ge)
LogicalOp: u8 (0x01=And, 0x02=Or)
UnaryOp: u8 (0x01=Not, 0x02=Neg)
QuantifierKind: u8 (0x01=ForAll, 0x02=Exists)
7. Section 5: Templates
Count: u32
Templates: [Template; Count]
Template:
name: StringRef
strict: bool
includes: Vec<StringRef>
fields: Map<StringRef, Value>
8. Section 6: Species
Count: u32
Species: [Species; Count]
Species:
name: StringRef
includes: Vec<StringRef>
fields: Map<StringRef, Value>
9. Section 7: Behaviors
9.1 Structure
Count: u32
Behaviors: [Behavior; Count]
9.2 Behavior Encoding
Behavior:
name: StringRef
root: BehaviorNode
9.3 BehaviorNode Encoding
BehaviorNode:
discriminant: u8
data: <depends on discriminant>
Node Type Discriminants
0x01 = Selector
0x02 = Sequence
0x03 = Condition
0x04 = Action
0x10 = DecoratorRepeat
0x11 = DecoratorRepeatN
0x12 = DecoratorRepeatRange
0x13 = DecoratorInvert
0x14 = DecoratorRetry
0x15 = DecoratorTimeout
0x16 = DecoratorCooldown
0x17 = DecoratorGuard
0x18 = DecoratorSucceedAlways
0x19 = DecoratorFailAlways
0x20 = SubTree
Selector Node (0x01)
label: Option<StringRef> // NEW in v0.2.0
children: Vec<BehaviorNode>
Keyword Mapping: selector or choose
Sequence Node (0x02)
label: Option<StringRef> // NEW in v0.2.0
children: Vec<BehaviorNode>
Keyword Mapping: sequence or then
Condition Node (0x03)
expression: Expression
Keyword Mapping: if or when
Action Node (0x04)
name: StringRef
parameters: Vec<Field>
Keyword Mapping: No prefix (just action name)
Decorator Nodes (0x10-0x19)
DecoratorRepeat (0x10):
child: Box<BehaviorNode>
Keyword: repeat { ... }
DecoratorRepeatN (0x11):
count: u32
child: Box<BehaviorNode>
Keyword: repeat(N) { ... }
DecoratorRepeatRange (0x12):
min: u32
max: u32
child: Box<BehaviorNode>
Keyword: repeat(min..max) { ... }
DecoratorInvert (0x13):
child: Box<BehaviorNode>
Keyword: invert { ... }
DecoratorRetry (0x14):
max_attempts: u32
child: Box<BehaviorNode>
Keyword: retry(N) { ... }
DecoratorTimeout (0x15):
milliseconds: u64 // Duration in milliseconds
child: Box<BehaviorNode>
Keyword: timeout(duration) { ... }
Example: timeout(5s), timeout(30m), timeout(2h)
DecoratorCooldown (0x16):
milliseconds: u64 // Cooldown period in milliseconds
child: Box<BehaviorNode>
Keyword: cooldown(duration) { ... }
DecoratorIf (0x17):
condition: Expression
child: Box<BehaviorNode>
Keyword: if(condition) { ... }
DecoratorSucceedAlways (0x18):
child: Box<BehaviorNode>
Keyword: succeed_always { ... }
DecoratorFailAlways (0x19):
child: Box<BehaviorNode>
Keyword: fail_always { ... }
SubTree Node (0x20)
path: Vec<StringRef> // Qualified path to subtree
Keyword: include path::to::subtree
10. Section 8: Schedules
10.1 Structure
Count: u32
Schedules: [Schedule; Count]
10.2 Schedule Encoding (REDESIGNED in v0.2.0)
Schedule:
name: StringRef
parent_schedule_id: Option<u32> // Index into SCHEDULES section (for inheritance)
blocks: Vec<ScheduleBlock>
patterns: Vec<SchedulePattern> // Day-specific, seasonal, recurrence patterns
10.3 ScheduleBlock
ScheduleBlock:
name: StringRef // Required in v0.2.0
start: u16 // Minutes since midnight (0-1439)
end: u16 // Minutes since midnight (0-1439)
behavior_ref: Option<Vec<StringRef>> // Reference to behavior (qualified path)
fields: Map<StringRef, Value>
Changes from v0.1.0:
nameis now required (was optional)behavior_refreplacesactivity: String- Time is encoded as minutes since midnight
10.4 SchedulePattern (NEW in v0.2.0)
SchedulePattern:
kind: u8 // Pattern discriminant
specification: Vec<u8> // Pattern-specific data
blocks: Vec<ScheduleBlock> // Blocks to apply when pattern matches
Pattern Kind Discriminants:
0x01 = DayPattern
0x02 = SeasonPattern
0x03 = RecurrencePattern
DayPattern (0x01)
specification:
day_enum_value: StringRef // References user-defined DayOfWeek enum
Example: on Fireday → references "Fireday" from user's DayOfWeek enum
SeasonPattern (0x02)
specification:
season_enum_values: Vec<StringRef> // Multiple seasons allowed
Example: season (EarlySummer, LateSummer) → references Season enum values
RecurrencePattern (0x03)
specification:
name: StringRef // Recurrence name
spec: RecurrenceSpec
RecurrenceSpec:
RecurrenceSpec:
discriminant: u8
data: <depends on discriminant>
Discriminants:
0x01 = Every(u32 days) // every N days
0x02 = WeeklyOn(Vec<StringRef>) // weekly on [days]
0x03 = MonthlyOnDay(u8 day) // monthly on day N
0x04 = Annually(u8 month, u8 day) // annually on month/day
10.5 Runtime Schedule Evaluation
Algorithm:
- Resolve character's schedule via ScheduleLink (conditional selection)
- Merge inherited schedules (parent → child, depth-first)
- Evaluate patterns for current day/season
- Overlay patterns on base blocks (later patterns override earlier ones)
- Produce final 24-hour schedule
- Return ordered list of (time, behavior_ref) pairs
11. Section 9: Institutions
11.1 Structure (EXTENDED in v0.2.0)
Count: u32
Institutions: [Institution; Count]
Institution:
name: StringRef
fields: Map<StringRef, Value>
behavior_links: Vec<BehaviorLink> // NEW in v0.2.0
schedule_links: Vec<ScheduleLink> // NEW in v0.2.0
Note: BehaviorLink and ScheduleLink are identical to Character section (§6.3, §6.4)
12. Section 10: Relationships
Count: u32
Relationships: [Relationship; Count]
Relationship:
name: StringRef
participants: Vec<Participant>
fields: Map<StringRef, Value>
Participant:
role: Option<StringRef>
name: Vec<StringRef> // Qualified path
self_block: Option<Vec<Field>>
other_block: Option<Vec<Field>>
13. Section 11: Locations
Count: u32
Locations: [Location; Count]
Location:
name: StringRef
fields: Map<StringRef, Value>
14. Section 12: Life Arcs
Count: u32
LifeArcs: [LifeArc; Count]
LifeArc:
name: StringRef
states: Vec<ArcState>
ArcState:
name: StringRef
on_enter: Option<Vec<Field>>
transitions: Vec<Transition>
Transition:
to: StringRef
condition: Expression
15. Section 13: Enums
15.1 Structure
Count: u32
Enums: [EnumDecl; Count]
15.2 EnumDecl Encoding
EnumDecl:
name: StringRef // u32 index into string table
variants: Vec<StringRef> // Encoded as below
Variants encoding (Vec):
u32 variant_count // Number of enum variants
[StringRef; variant_count] // Array of variant names (each is u32)
15.3 Binary Example
Source:
enum SkillLevel {
Novice,
Beginner,
Intermediate,
Advanced,
Expert,
Master
}
Binary encoding:
EnumDecl:
name: StringRef = 42 // "SkillLevel" (4 bytes)
u32 variant_count = 6 // 6 variants (4 bytes)
variants:
StringRef = 43 // "Novice" (4 bytes)
StringRef = 44 // "Beginner" (4 bytes)
StringRef = 45 // "Intermediate" (4 bytes)
StringRef = 46 // "Advanced" (4 bytes)
StringRef = 47 // "Expert" (4 bytes)
StringRef = 48 // "Master" (4 bytes)
Total: 32 bytes (4 + 4 + 6*4)
15.4 Usage
Usage: Enums are used for:
- Calendar definitions (DayOfWeek, Season, Month)
- Custom enumerated values
- Pattern matching in schedules
Standard Calendar Enums (optional):
DayOfWeek: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, SaturdaySeason: Spring, Summer, Fall, WinterMonth: January, February, ..., December
16. Changelog
v0.2.0 (February 2026)
Major Features:
- Resource linking system for behaviors and schedules
- Year-long schedule patterns (day, season, recurrence)
- Schedule inheritance and composition
- Behavior tree keyword support (named nodes)
- Parameterized decorators (repeat, retry, timeout, cooldown, if)
Breaking Changes:
- CHARACTERS section: added behavior_links, schedule_links
- INSTITUTIONS section: added behavior_links, schedule_links
- SCHEDULES section: complete redesign with patterns and inheritance
- BEHAVIORS section: added named node support
Deprecations:
- None (first versioned release)
Bug Fixes:
- N/A (first formal release)
v0.1.0 (Implicit, Pre-Release)
Initial format (inferred from existing codebase):
- Basic entity storage (characters, templates, species)
- Simple schedules (time blocks with activities)
- Behavior trees (symbolic syntax)
- Relationships, locations, life arcs
- Enum support
Appendix A: Binary Encoding Examples
Example 1: Character with Resource Links
Source:
character Alice: Human {
age: 7
uses behavior: CuriousBehavior, when: self.location == Wonderland, priority: High
uses behavior: DefaultBehavior, default: true
uses schedule: AdventureSchedule, when: self.in_wonderland
uses schedule: NormalSchedule, default: true
}
Binary (conceptual):
Character:
name: StringRef(0) // "Alice"
species: Some(StringRef(1)) // "Human"
fields: [
("age", Int(7))
]
template_refs: []
behavior_links: [
BehaviorLink {
behavior_id: 3
priority: 2 // High
condition: Some(Comparison(FieldAccess(...), Eq, Identifier(...)))
is_default: false
},
BehaviorLink {
behavior_id: 5
priority: 1 // Normal
condition: None
is_default: true
}
]
schedule_links: [
ScheduleLink {
schedule_id: 1
condition: Some(FieldAccess(...))
is_default: false
},
ScheduleLink {
schedule_id: 2
condition: None
is_default: true
}
]
Example 2: Schedule with Patterns
Source:
schedule WorkWeek modifies BaseSchedule {
block morning { 08:00 - 12:00: WorkTasks }
block lunch { 12:00 - 13:00: EatLunch }
block afternoon { 13:00 - 17:00: WorkTasks }
on Friday {
override afternoon { 13:00 - 15:00: FinishWeek }
}
season (Summer) {
override morning { 07:00 - 11:00: WorkEarly }
}
}
Binary (conceptual):
Schedule:
name: StringRef(10) // "WorkWeek"
parent_schedule_id: Some(0) // BaseSchedule index
blocks: [
ScheduleBlock {
name: StringRef(11) // "morning"
start: 480 // 08:00 in minutes
end: 720 // 12:00 in minutes
behavior_ref: Some(["WorkTasks"])
fields: []
},
ScheduleBlock {
name: StringRef(12) // "lunch"
start: 720
end: 780
behavior_ref: Some(["EatLunch"])
fields: []
},
ScheduleBlock {
name: StringRef(13) // "afternoon"
start: 780
end: 1020
behavior_ref: Some(["WorkTasks"])
fields: []
}
]
patterns: [
SchedulePattern {
kind: 0x01 // DayPattern
specification: StringRef(14) // "Friday"
blocks: [
ScheduleBlock {
name: StringRef(13) // "afternoon" (override)
start: 780
end: 900
behavior_ref: Some(["FinishWeek"])
fields: []
}
]
},
SchedulePattern {
kind: 0x02 // SeasonPattern
specification: [StringRef(15)] // ["Summer"]
blocks: [
ScheduleBlock {
name: StringRef(11) // "morning" (override)
start: 420
end: 660
behavior_ref: Some(["WorkEarly"])
fields: []
}
]
}
]
Appendix B: File Size Estimates
Assumptions:
- Average character: 500 bytes
- Average behavior tree: 1 KB
- Average schedule: 800 bytes
- 1000 characters, 500 behaviors, 300 schedules
Estimated size:
- Characters: 500 KB
- Behaviors: 500 KB
- Schedules: 240 KB
- Other sections: 100 KB
- Total: ~1.34 MB
Compression: Typical compression (gzip/zstd) achieves 60-70% reduction → ~400-500 KB
Appendix C: Version History
| Version | Date | Major Changes |
|---|---|---|
| 0.1.0 | (Implicit) | Initial format |
| 0.2.0 | Feb 2026 | Resource linking, year-long schedules, behavior keywords |
END OF SPECIFICATION