Files
storybook/docs/SBIR-v0.2.0-SPEC.md
Sienna Meridian Satterwhite 47fafdc2bf feat(lang): complete extends to modifies keyword migration
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.
2026-02-16 22:55:04 +00:00

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

  1. Introduction
  2. File Format Overview
  3. Section 1: Header
  4. Section 2: String Table
  5. Section 3: Types
  6. Section 4: Characters
  7. Section 5: Templates
  8. Section 6: Species
  9. Section 7: Behaviors
  10. Section 8: Schedules
  11. Section 9: Institutions
  12. Section 10: Relationships
  13. Section 11: Locations
  14. Section 12: Life Arcs
  15. Section 13: Enums
  16. 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:

  1. Resource Linking System - Characters and institutions can link to behaviors and schedules with conditions and priorities
  2. Year-Long Schedule System - Schedules support temporal patterns (day-specific, seasonal, recurrence)
  3. 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:

  1. Length/count field (u32) tells you "how much data is coming"
  2. Data bytes - read exactly that amount
  3. 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

  1. Fixed-size types (u8, u32, f64, etc.) take their exact size - no padding
  2. Variable-size types always start with a length prefix (u32)
  3. Option starts with a discriminant byte (0=None, 1=Some)
  4. Enums/discriminated unions start with a discriminant byte
  5. 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
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:

  1. Filter links where condition evaluates to true (or is None)
  2. Sort by priority (descending)
  3. Return highest priority link
  4. If tie, use declaration order
ScheduleLink:
  schedule_id: u32                       // Index into SCHEDULES section
  condition: Option<Expression>          // Optional activation condition
  is_default: bool                       // Default schedule (fallback)

Selection Algorithm:

  1. Iterate schedule_links in order
  2. Skip default links initially
  3. Return first link where condition is true (or None)
  4. 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:

  • name is now required (was optional)
  • behavior_ref replaces activity: 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:

  1. Resolve character's schedule via ScheduleLink (conditional selection)
  2. Merge inherited schedules (parent → child, depth-first)
  3. Evaluate patterns for current day/season
  4. Overlay patterns on base blocks (later patterns override earlier ones)
  5. Produce final 24-hour schedule
  6. 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, Saturday
  • Season: Spring, Summer, Fall, Winter
  • Month: 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

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