Files
storybook/docs/TYPE-SYSTEM.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

44 KiB

Storybook Type System

Version: 0.3.0 Status: Draft

Overview

The Storybook type system is a declarative, pure functional DSL for defining narrative simulations. It separates type definitions and logic (language layer) from state management and mutation (runtime layer).

Core Principles

  1. Language is pure - no mutation, no state changes, only declarations
  2. Templates are record types - universal structural type definitions
  3. Entities are typed values - characters, institutions, locations are instances
  4. Behaviors are functional - control flow and pattern matching
  5. Actions are the boundary - where language meets runtime

Core Declarations

  • template: Record type definitions (structural types)
  • character/institution/location: Typed value instances
  • concept: Base type declarations for pattern matching
  • sub_concept: Enumerated and typed subtypes (tagged union members)
  • concept_comparison: Compile-time pattern matching over subtype combinations
  • action: Signature declarations for runtime-implemented operations

This system enables static validation of entity relationships, behavior conditions, and data structures while maintaining readability for narrative design.


Architecture: Language vs Runtime

Language Layer (Storybook DSL)

Pure, declarative, no mutation:

  • Defines types (templates, concepts)
  • Defines values (characters, institutions, locations)
  • Defines logic (behaviors, pattern matching)
  • Declares action signatures (interface to runtime)

Example:

template Person {
    age: Number,
    profession: Profession
}

character Martha: Person {
    age: 34,
    profession: Baker
}

action bake(baker: Baker, item: BakingItem)

behavior BakingWork {
    then {
        check_orders
        bake
    }
}

Runtime Layer (Implementation)

Stateful, manages mutation:

  • Holds character state
  • Executes actions (implements mutations)
  • Runs behavior trees
  • Updates world state over time

Example (Rust):

enum Action {
    Bake { baker: EntityId, item: ItemId },
    CheckOrders { baker: EntityId },
    PrepareIngredients { baker: EntityId, recipe: RecipeId },
}

impl Runtime {
    fn execute_action(&mut self, action: Action) {
        match action {
            Action::Bake { baker, item } => {
                let character = self.get_character_mut(baker);
                let baked_item = self.get_item(item).bake();
                character.inventory.add(baked_item);
                character.energy -= 10;
            }
            Action::CheckOrders { baker } => {
                let character = self.get_character_mut(baker);
                let orders = self.query_orders();
                character.task_queue.extend(orders);
            }
            Action::PrepareIngredients { baker, recipe } => {
                // Implementation
            }
        }
    }
}

Actions: The Boundary

Actions are declared in Storybook (signatures) but implemented in runtime (behavior).

Language sees:

/// Bakes an item in the oven
///
/// The baker must have sufficient energy and the item must be prepared.
/// Updates baker's inventory and reduces energy.
action bake(baker: Baker, item: BakingItem)

Runtime implements:

impl Runtime {
    fn bake(&mut self, baker: EntityId, item: ItemId) {
        let character = self.get_character_mut(baker);
        let baked = self.get_item(item).bake();
        character.inventory.add(baked);
        character.energy -= 10;
    }
}

Benefits:

  • Language stays pure and type-safe
  • Runtime has implementation flexibility
  • Clear contract between layers
  • Can add standard library later without changing language

Core Declarations

template - Record Type Definition

A template defines a structural record type. Templates are the universal mechanism for defining structured data in Storybook.

Syntax:

template TypeName {
    field1: Type,
    field2: Type,
    ...
}

Examples:

template Person {
    age: Number,
    species_type: Species,
    profession: Profession
}

template Building {
    owner: Person,           // Reference to Person template
    capacity: Number,
    place: Settlement        // Reference to Settlement template
}

template Settlement {
    terrain: Terrain,
    population: Number
}

template FamilialBond {
    parent: Person,          // Reference to Person template
    child: Person,           // Reference to Person template
    strength: Number
}

Purpose:

  • Define reusable record structures
  • Provide types for characters, institutions, locations, relationships
  • Enable type checking for fields and references

Field Types: Templates support both value types and reference types:

Value Types:

  • Number - integer values (e.g., age: 34)
  • Decimal - floating-point values (e.g., price: 19.99)
  • Text - text values (e.g., name: "Martha")
  • Boolean - boolean values (e.g., active: true)

Range Declarations: For procedural generation, templates can specify ranges instead of concrete values:

template Person {
    age: 18..80,        // Random age between 18 and 80
    height: 150..200,   // Random height in cm
    wealth: 100..10000  // Random wealth amount
}

When a character instantiates a template with ranges, it must provide concrete values within those ranges:

character Martha: Person {
    age: 34,      // Must be between 18 and 80
    height: 165,  // Must be between 150 and 200
    wealth: 5000  // Must be between 100 and 10000
}

Reference Types: Templates can reference concepts and other templates:

template Baker {
    profession: Profession,  // Reference to concept
    place: Settlement,       // Reference to Settlement template
    tools: BakingTools       // Reference to BakingTools template
}

Note: Template fields reference template types (Person, Settlement, Building), not declaration keywords (character, location, institution).

Reserved Keywords: Field names cannot use reserved keywords. Use suffixes or alternatives instead:

// ✗ Invalid - 'species' is a keyword
template Person {
    age: Number,
    species: Species,        // Error!
    profession: Profession
}

// ✓ Valid - use alternative names
template Person {
    age: Number,
    species_type: Species,   // OK
    profession: Profession
}

Common alternatives:

  • speciesspecies_type, creature_type
  • locationlocation_ref, place
  • charactercharacter_ref, person
  • templatetemplate_name, template_ref
  • behaviorbehavior_ref, action_tree

Template Composition: Templates can reference other templates and concepts in their fields.


Entities: Typed Value Instances

Characters, institutions, locations, and relationships are typed values - instances of template types.

Syntax:

character Name: TemplateName {
    field1: value,
    field2: value,
    ...
}

institution Name: TemplateName {
    field1: value,
    field2: value,
    ...
}

location Name: TemplateName {
    field1: value,
    field2: value,
    ...
}

relationship Name: TemplateName {
    field1: value,
    field2: value,
    ...
}

Examples:

character Martha: Person {
    age: 34,
    species_type: Human,
    profession: Baker
}

institution Bakery: Building {
    owner: Martha,
    capacity: 20,
    place: TownSquare
}

location TownSquare: Settlement {
    terrain: Plains,
    population: 500
}

relationship ParentChild: FamilialBond {
    parent: Martha,
    child: Jane,
    strength: 10
}

Type Checking:

  • Values must match their template's field types
  • Values must fall within range constraints (if specified in template)
  • All required fields must be provided
  • References must resolve to declared entities

Example:

template Person {
    age: 18..80,
    profession: Profession
}

// ✓ Valid - within constraints
character Martha: Person {
    age: 34,              // 18 <= 34 <= 80
    profession: Baker
}

// ✗ Error - age out of range
character TooYoung: Person {
    age: 12,              // 12 < 18 (violates constraint)
    profession: Child
}

State Management:

  • Entities are values at declaration time
  • State changes are managed by the runtime (outside the language)
  • Language defines initial state, runtime manages ongoing state

action - Runtime Operation Signature

An action declares the signature of a runtime-implemented operation. Actions are the interface between the pure language layer and the stateful runtime layer.

Syntax:

/// Documentation comment (required)
///
/// Multi-line docstrings explain what the action does,
/// its preconditions, and its effects.
action name(param1: Type, param2: Type, ...)

Examples:

/// Bakes an item in the oven
///
/// The baker must have sufficient energy and the item must be prepared.
/// Updates baker's inventory and reduces energy.
action bake(baker: Baker, item: BakingItem)

/// Checks pending orders and updates the baker's task list
///
/// Queries the order queue and populates the baker's work queue.
action check_orders(baker: Baker)

/// Moves a character to a new location
///
/// Updates the character's location and triggers any location-based events.
action move_to(character: Person, destination: Settlement)

Documentation Requirement:

  • Actions must have a docstring explaining their behavior
  • This helps runtime implementers understand intent
  • Serves as contract between language and runtime

Type Checking:

  • Action calls in behaviors are validated against signatures
  • Parameter types must match
  • Unknown actions are compile errors

Implementation:

  • Signatures are declared in .sb files (typically actions.sb)
  • Implementations are provided by the runtime
  • Standard library of common actions may come in future versions

Current Limitations:

  • Actions have no return values (this version)
  • Actions are statements, not expressions
  • No support for action composition

concept - Base Type Declaration

A concept declares a base type with no inherent structure. It serves as a parent for related sub_concept declarations.

Syntax:

concept TypeName

Example:

concept Cup
concept Customer
concept Vendor

Purpose:

  • Establish type namespaces for related subtypes
  • Provide type identity for values in the system
  • Enable type checking in behaviors and conditions

When to use:

  • When you need a type that will have multiple variants or aspects
  • To create type-safe enumerations through sub_concept
  • As a base for compile-time pattern matching via concept_comparison

sub_concept - Subtype Definition

A sub_concept defines members of a tagged union for a parent concept. Sub_concepts are associated with their parent through dot notation (explicit parent reference).

Syntax:

Enumerated Form (fixed set of values):

sub_concept Parent.Name {
    Variant1,
    Variant2,
    Variant3
}

Typed Form (structured with fields):

sub_concept Parent.RecordName {
    field1: TypeOrAny,
    field2: TypeOrAny
}

Parent Declaration: The parent is explicitly declared using dot notation:

  • Cup.Type → parent: Cup, name: Type
  • Cup.Size → parent: Cup, name: Size
  • Cupcake.Flavor → parent: Cupcake, name: Flavor

Why dot notation?

  • Prevents accidental naming collisions (Cupcake won't match Cup)
  • Makes parent-child relationships explicit and clear
  • Reads naturally: "Cup's Type", "Cup's Size"
  • No ambiguity - parser knows exact parent
  • Allows arbitrary sub_concept names without prefix requirements

Examples:

// Parent concepts
concept Cup;
concept Cupcake;

// Cup sub_concepts (tagged union members)
sub_concept Cup.Size {
    Small,
    Medium,
    Large
}

sub_concept Cup.Type {
    Ceramic,
    Glass,
    Plastic
}

sub_concept Cup.Color {
    Red,
    Blue,
    Green
}

// Cupcake sub_concepts (no confusion with Cup)
sub_concept Cupcake.Flavor {
    Strawberry,
    Chocolate,
    Raspberry
}

// Typed sub_concept (record-like)
sub_concept Vendor.Inventory {
    Bread: any,
    Pastries: any,
    Cakes: any,
    Cup: any
}

Type Checking Rules:

  • Field values must be identifiers (references to other concepts/sub_concepts or any)
  • Field values cannot be value types (Text, Number, Decimal, Boolean)
// ✅ VALID - identifier references
sub_concept Inventory {
    item: Product,
    container: Cup,
    quantity: any
}

// ❌ INVALID - value types not allowed
sub_concept BadInventory {
    name: "string",     // ✗ string literal
    count: 42,          // ✗ integer literal
    active: true        // ✗ boolean literal
}

Purpose:

  • Enumerated: Define a fixed set of mutually exclusive values
  • Typed: Define structured data with type-safe fields
  • Both forms create tagged union members of the parent concept

Relationship to Parent: Sub_concepts are tagged union members, not inheritance:

  • CupSize.Small is a distinct variant of the Cup union
  • CupType.Glass is another distinct variant of the Cup union
  • They coexist as different aspects/facets of the same base type

concept_comparison - Compile-time Pattern Matching

A concept_comparison performs compile-time pattern matching over combinations of sub_concept values, mapping them to derived variant names.

Syntax:

concept_comparison ComparisonName {
    VariantName1: {
        SubConceptName1: condition1,
        SubConceptName2: condition2,
        ...
    },
    VariantName2: {
        SubConceptName1: condition1,
        SubConceptName2: condition2,
        ...
    }
}

Condition Syntax:

  • any: Matches any value of the specified sub_concept type
  • Type is Value1 or Type is Value2: Matches specific enumerated values

Example:

concept Cup;

sub_concept Cup.Size {
    Small, Medium, Large
}

sub_concept Cup.Type {
    Ceramic, Glass, Plastic
}

sub_concept Cup.Color {
    Red, Blue, Green
}

concept_comparison CustomerNumbererestInCups {
    Numbererested: {
        Cup.Size: any,  // Any size
        Cup.Type: Cup.Type is Glass or Cup.Type is Plastic,  // Only Glass or Plastic
        Cup.Color: Cup.Color is Red or Cup.Color is Blue     // Only Red or Blue
    },
    NotNumbererested: {
        Cup.Size: any,
        Cup.Type: any,
        Cup.Color: any
    },
    Maybe: {
        Cup.Size: any,
        Cup.Type: any,
        Cup.Color: any
    }
}

Pattern Matching Semantics:

  • The comparison evaluates at compile-time
  • Given concrete sub_concept values, determines which variant they match
  • Used in behavior conditions to enable type-safe decision making

Usage in Behaviors:

behavior SellAtMarket {
    repeat {
        then {
            greet_customer
            show_products
            if(CustomerNumbererestInCups.Numbererested.Cup.Size is Medium or Cup.Size is Large) {
                make_sale(Cup)
            }
            thank_customer
        }
    }
}

Purpose:

  • Create derived types from combinations of existing sub_concepts
  • Enable compile-time validation of complex conditions
  • Provide type-safe pattern matching for behaviors

Mixing Enumerated and Typed Sub_concepts: Concept_comparisons can reference both enumerated and typed sub_concepts in the same pattern (support for this is being implemented).


Type Compatibility

Subtyping Rules

Sub_concepts are tagged union members of their parent concept:

CupSize.Small <: Cup
CupType.Glass <: Cup
CupColor.Red <: Cup

Substitution

Where sub_concept values can be used:

  • As arguments to actions that expect the parent concept
  • In field assignments expecting the parent type
  • In conditions and comparisons

Example:

action serve(item: Cup) {
    // Implementation
}

// Valid calls - sub_concepts substitute for parent
serve(CupSize.Small)
serve(CupType.Glass)
serve(CupColor.Red)

Type Checking

Compile-time:

  • Sub_concept field types must be identifiers (not value types)
  • Pattern matching in concept_comparison is validated
  • Type references must resolve to declared concepts

Runtime:

  • Values are tagged with their specific sub_concept variant
  • Pattern matching evaluates to variant names
  • Type information preserved for behavior execution

any Keyword

The any keyword represents any value of a specific type, not a universal type.

Semantics:

sub_concept VendorInventory {
    Bread: any,      // any value of type Bread
    Pastries: any,   // any value of type Pastries
    Cup: any         // any value of type Cup (includes all CupSize, CupType, CupColor)
}

concept_comparison Example {
    AllCups: {
        CupSize: any,    // matches Small, Medium, Large
        CupType: any,    // matches Ceramic, Glass, Plastic
        CupColor: any    // matches Red, Blue, Green
    }
}

Not a universal type:

  • any is scoped to the field's declared type
  • Does not mean "any type in the system"
  • Provides flexibility while maintaining type safety

Design Guidelines

When to use concept

  • You need a base type for multiple related variants
  • You want type-safe enumerations
  • You need to group related aspects of an entity

When to use enumerated sub_concept

  • You have a fixed set of mutually exclusive values
  • Values are symbolic/categorical (not data-bearing)
  • You need type-safe pattern matching over the values

When to use typed sub_concept

  • You need structured data with multiple fields
  • Fields reference other concepts or need flexibility (any)
  • You want record-like types within the concept system

When to use concept_comparison

  • You need to map combinations of sub_concepts to outcomes
  • Complex conditional logic benefits from compile-time validation
  • Behavior trees need type-safe decision points

Examples

Simple Enumeration

concept Season;

sub_concept Season.Name {
    Spring,
    Summer,
    Fall,
    Winter
}

Multi-faceted Type

concept Food;

sub_concept Food.Type {
    Bread,
    Pastry,
    Cake
}

sub_concept Food.Freshness {
    Fresh,
    Stale,
    Spoiled
}

concept_comparison FoodQuality {
    Excellent: {
        Food.Type: any,
        Food.Freshness: Food.Freshness is Fresh
    },
    Poor: {
        Food.Type: any,
        Food.Freshness: Food.Freshness is Stale or Food.Freshness is Spoiled
    }
}

Record Type

concept Inventory;

sub_concept Inventory.Record {
    item_type: Product,
    quantity: any,
    location: StorageArea
}

Implementation Notes

Parser

  • Sub_concept parent inference uses prefix matching on the concept name
  • Enumerated vs typed sub_concepts are disambiguated by presence of : after first identifier
  • any keyword is parsed as Value::Any in the AST

AST Representation

pub struct ConceptDecl {
    pub name: Text,
    pub span: Span,
}

pub struct SubConceptDecl {
    pub name: Text,
    pub parent_concept: Text,  // Inferred from naming convention
    pub kind: SubConceptKind,
    pub span: Span,
}

pub enum SubConceptKind {
    Enum { variants: Vec<Text> },
    Record { fields: Vec<Field> },
}

pub struct ConceptComparisonDecl {
    pub name: Text,
    pub variants: Vec<VariantPattern>,
    pub span: Span,
}

pub struct VariantPattern {
    pub name: Text,
    pub conditions: Vec<FieldCondition>,
    pub span: Span,
}

pub enum Condition {
    Any,
    Is(Vec<Text>),  // "is Value1 or Value2"
}


Species and Template Inheritance

Species as Base Types with Defaults

Species define base record types with default values that templates can extend and characters can override.

Syntax:

species SpeciesName {
    field: Type = default_value,
    ...
}

Example:

species Human {
    bipedal: Boolean = true,
    has_hair: Boolean = true,
    base_lifespan: Number = 80,
    can_use_magic: Boolean = false
}

species Elf {
    bipedal: Boolean = true,
    pointed_ears: Boolean = true,
    base_lifespan: Number = 1000,
    can_use_magic: Boolean = true
}

Template Extension

Templates extend species using : syntax and can override species defaults or add new fields.

Syntax:

template TemplateName: SpeciesName {
    field: Type = default,     // Override species field
    new_field: Type,           // Add new required field
    new_field2: Type = default // Add new field with default
}

Example:

template Person: Human {
    // Inherits: bipedal, has_hair, base_lifespan, can_use_magic (with defaults)
    age: Number,          // New required field
    name: Text            // New required field
}

template Cyborg: Human {
    // Override species defaults
    base_lifespan: Number = 200,  // Cyborgs live longer
    organic: Boolean = false,     // New field

    // Add new fields
    model: Text,                  // Required
    battery_level: Number = 100   // With default
}

Override Priority Chain

Character > Template > Species

When a field is defined at multiple levels, the most specific definition wins:

species Human {
    strength: Number = 10,
    intelligence: Number = 10,
    speed: Number = 10
}

template Warrior: Human {
    strength: Number = 15,     // Override species default
    weapon: Text               // New required field
}

character Conan: Warrior {
    strength: 20,              // Override template (which overrode species)
    weapon: "Greatsword",      // Required by template
    intelligence: 5            // Override species default
    // speed: 10 (from species, no overrides)
}

Resolution order:

  1. Check character fields - use if present
  2. Check template fields - use if present and not in character
  3. Check species fields - use if not overridden
  4. Error if no value found and field is required

Character Instantiation with Inheritance

character Martha: Person {
    // Required template fields (no defaults)
    age: 34,
    name: "Martha",

    // Optional overrides of species defaults
    bipedal: false,           // She has one leg
    can_use_magic: true,      // Exceptional human

    // Inherited from species (use defaults)
    // has_hair: true (from Human)
    // base_lifespan: 80 (from Human)
}

Benefits

Layered defaults - species → template → character No redundancy - don't repeat common defaults Composable - templates customize species for specific roles Exceptional cases - characters override when needed Clear precedence - explicit override chain


Life Arcs as Entity State

Core Concept

Life arcs represent the current state of an entity in various state machines.

An entity (character/location/institution) is not just data - it has state that changes over time. Life arcs define:

  • What states exist (Baby, Child, Adult, Elder)
  • How to transition between states (when age >= 18)
  • What behaviors are available in each state

The entity's life arc assignments ARE its current state in those state machines.

Entity State Model

character Martha: Baker {
    // Data fields
    age: 34,
    skill_level: 8,
    reputation: 85,

    // STATE - current position in life arc state machines
    life_arcs: {
        Human: Adult,        // In "Adult" state of Human state machine
        Baker: Master,       // In "Master" state of Baker state machine
        Reputation: Famous   // In "Famous" state of Reputation state machine
    }
}

Martha's state is:

  • Age progression: Adult (can Work, Train, Manage)
  • Career progression: Master (can BakingWork, TrainApprentice, InnovateTechniques)
  • Social progression: Famous (can Influence, NetworkGlobally)

Life Arcs Define State Machines

life_arc Human requires { age: Number } {
    Baby {
        when age < 2
        can use behaviors: [Cry, Sleep, Eat]
        -> Child when age >= 2
    }

    Child {
        when age >= 2 and age < 12
        can use behaviors: [Cry, Play, Learn, Eat, Sleep]
        -> Adolescent when age >= 12
    }

    Adolescent {
        when age >= 12 and age < 18
        can use behaviors: [Cry, Sleep, Play, Learn, Socialize, Work]
        -> Adult when age >= 18
    }

    Adult {
        when age >= 18 and age < 65
        can use behaviors: [Cry, Sleep, Work, Socialize, Train, Manage]
        -> Elder when age >= 65
    }

    Elder {
        when age >= 65
        can use behaviors: [Cry, Sleep, Rest, Socialize, Mentor]
    }
}

This defines:

  • States: Baby, Child, Adolescent, Adult, Elder
  • Transitions: age thresholds trigger state changes
  • Capabilities: each state grants different behaviors

State Determines Capabilities

Behavior availability = Life Arc States + Location + Template

Can Martha use TrainApprentice at Bakery?

  Check life arc states:
    ✓ Human.Adult grants ability to Train
    ✓ Baker.Master grants TrainApprentice

  Check location:
    ✓ Bakery enables TrainApprentice

  → YES - all conditions met

If Martha were Baker.Journeyman instead:

character Martha: Baker {
    skill_level: 5,
    life_arcs: {
        Human: Adult,
        Baker: Journeyman  // Different state!
    }
}

Can Martha use TrainApprentice at Bakery?

  Check life arc states:
    ✓ Human.Adult grants ability to Train
    ✗ Baker.Journeyman does NOT grant TrainApprentice

  → NO - lacks capability from life arc state

Multiple Concurrent State Machines

Entities can be in multiple state machines simultaneously:

life_arc Human requires { age: Number } {
    // Age-based progression
}

life_arc Baker requires { skill_level: Number } {
    // Skill-based progression
}

life_arc Reputation requires { fame: Number } {
    // Fame-based progression
}

character Martha: Baker {
    age: 34,
    skill_level: 8,
    fame: 85,

    life_arcs: {
        Human: Adult,           // State in age dimension
        Baker: Master,          // State in career dimension
        Reputation: Famous      // State in social dimension
    }
}

Each life arc is an independent state machine:

  • Human state machine: transitions based on age
  • Baker state machine: transitions based on skill_level
  • Reputation state machine: transitions based on fame

Available behaviors = union of all state capabilities:

  • From Human.Adult: Work, Train, Manage
  • From Baker.Master: BakingWork, TrainApprentice
  • From Reputation.Famous: Influence, NetworkGlobally

Locations Have State Too

life_arc Settlement requires { population: Number, development: Number } {
    Village {
        when population < 500
        enables behaviors: [Farming, Fishing]
        -> Town when population >= 500 and development >= 3
    }

    Town {
        when population >= 500 and population < 5000
        enables behaviors: [Trading, Crafting, Farming]
        -> City when population >= 5000 and development >= 7
    }

    City {
        when population >= 5000
        enables behaviors: [Trading, Manufacturing, Politics, Education]
    }
}

location TownSquare: SettlementTemplate {
    population: 500,
    development: 4,

    life_arcs: {
        Settlement: Town  // Current state
    }
}

TownSquare's state:

  • It's in the "Town" state (not Village, not City)
  • It enables behaviors: Trading, Crafting, Farming
  • When population reaches 5000 AND development reaches 7, it transitions to City state

Institutions Have State Too

life_arc Business requires { revenue: Number, reputation: Number } {
    Startup {
        when revenue < 10000
        enables behaviors: [Hustle, Experiment]
        -> Established when revenue >= 10000
    }

    Established {
        when revenue >= 10000 and revenue < 100000
        enables behaviors: [Operate, Expand]
        -> Corporate when revenue >= 100000
    }

    Corporate {
        when revenue >= 100000
        enables behaviors: [Operate, Expand, Franchise, Lobby]
    }
}

institution Bakery: Building {
    revenue: 15000,
    reputation: 75,

    life_arcs: {
        Business: Established  // Current state
    }
}

Bidirectional State Transitions

State machines are NOT necessarily forward-only.

Characters can progress forward, regress backward, or transition laterally based on field changes:

life_arc Baker requires { skill_level: Number, experience: Number } {
    Apprentice {
        when skill_level < 3
        can use behaviors: [Learn, AssistBaking]
        -> Journeyman when skill_level >= 3
    }

    Journeyman {
        when skill_level >= 3 and skill_level < 7
        can use behaviors: [BakingWork, ManageInventory]
        -> Master when skill_level >= 7              // Progress forward
        -> Apprentice when skill_level < 3           // Regress backward!
    }

    Master {
        when skill_level >= 7
        can use behaviors: [BakingWork, ManageInventory, TrainApprentice, InnovateTechniques]
        -> Journeyman when skill_level < 7           // Can lose mastery!
    }
}

The when clause defines:

  • State validity condition: What must be true to be in this state
  • NOT a one-time check: Re-evaluated whenever fields change

Examples of regression:

character Martha: Baker {
    skill_level: 8,
    life_arcs: { Baker: Master }
}

// Martha is Master Baker, then loses practice
action lose_practice(baker: Person) {
    baker.skill_level -= 5  // Now skill_level = 3
}

lose_practice(Martha)

// Runtime re-evaluates:
// 1. Is Martha still valid for Master? (requires skill_level >= 7)
//    3 >= 7? NO - state is invalid!
// 2. Check Master's transitions:
//    -> Journeyman when skill_level < 7
//    3 < 7? YES - transition!
// 3. Martha is now Journeyman

// Martha's available behaviors change:
// Lost: TrainApprentice, InnovateTechniques
// Kept: BakingWork, ManageInventory

Examples of recovery:

// Later, Martha practices and improves
action practice_baking(baker: Person) {
    baker.skill_level += 1
}

// Practice multiple times
practice_baking(Martha)  // skill_level = 4
practice_baking(Martha)  // skill_level = 5
practice_baking(Martha)  // skill_level = 6
practice_baking(Martha)  // skill_level = 7

// After skill_level reaches 7:
// Runtime re-evaluates:
// 1. Check Journeyman's transitions:
//    -> Master when skill_level >= 7
//    7 >= 7? YES - transition!
// 2. Martha is Master again!

State Changes = Mutations

Life arcs ARE the mutability mechanism:

  1. Actions modify field values:
action practice_baking(baker: Person) {
    // Runtime implementation
    baker.skill_level += 1
}

action lose_practice(baker: Person) {
    // Runtime implementation
    baker.skill_level -= 1
}
  1. Modified values trigger state transitions (in ANY direction):
// Progression
// Before: Martha.skill_level = 6, Baker state = Journeyman
practice_baking(Martha)
// After: Martha.skill_level = 7
// Transition: Journeyman -> Master (forward)

// Regression
// Before: Martha.skill_level = 7, Baker state = Master
lose_practice(Martha)
lose_practice(Martha)
lose_practice(Martha)
// After: Martha.skill_level = 4
// Transition: Master -> Journeyman (backward)
  1. New state grants/removes capabilities:
// After progression to Master:
// Gained: TrainApprentice, InnovateTechniques

// After regression to Journeyman:
// Lost: TrainApprentice, InnovateTechniques
// Kept: BakingWork, ManageInventory

Runtime State Re-evaluation

After ANY action that modifies fields:

impl Runtime {
    fn execute_action(&mut self, action: Action) {
        // 1. Execute action (mutates entity fields)
        let entity_id = match &action {
            Action::PracticeBaking { baker } => {
                let character = self.get_character_mut(*baker);
                character.skill_level += 1;
                *baker
            }
            Action::LosePractice { baker } => {
                let character = self.get_character_mut(*baker);
                character.skill_level -= 1;
                *baker
            }
            _ => {
                // Handle other actions
                return;
            }
        };

        // 2. Re-evaluate ALL life arc states for this entity
        self.update_life_arc_states(entity_id);
    }

    fn update_life_arc_states(&mut self, entity_id: EntityId) {
        let entity = self.get_character(entity_id);
        let life_arcs = entity.life_arcs.clone();

        for (arc_name, current_state) in &life_arcs {
            let arc = self.get_life_arc(arc_name);
            let state_def = arc.get_state(current_state);

            // Check if current state condition is still valid
            if !self.evaluate_condition(entity_id, &state_def.condition) {
                // State is invalid - find valid transition
                for transition in &state_def.transitions {
                    if self.evaluate_condition(entity_id, &transition.condition) {
                        // Transition to new state
                        let entity = self.get_character_mut(entity_id);
                        entity.life_arcs.insert(arc_name.clone(), transition.to.clone());

                        // Log transition
                        println!(
                            "Entity {} transitioned: {}.{} -> {}.{}",
                            entity.name, arc_name, current_state, arc_name, transition.to
                        );

                        break;
                    }
                }
            }
        }
    }
}

Re-evaluation algorithm:

  1. After action execution, check ALL life arcs for the affected entity
  2. For each life arc, evaluate current state's when condition
  3. If condition is FALSE, current state is invalid
  4. Check state's transitions in order
  5. Take first transition whose condition is TRUE
  6. Update entity's life_arcs to new state
  7. New state's capabilities are now available

Design principle: State follows data. When field values change, states automatically transition to match the new reality.

Mental Model

Think of entities as:

  • Data: Fields with values (age, skill_level, reputation)
  • State: Current position in life arc state machines
  • Capability: Behaviors available based on state + location + template

Life arcs are NOT metadata - they ARE the state.

When you ask "What can Martha do?", you're asking:

  1. What state is she in? (Human.Adult, Baker.Master)
  2. Where is she? (Bakery)
  3. What does her template grant? (Baker template)

The answer is the intersection of capabilities from all three layers.



Type System Soundness

Type Theory Foundations

Storybook uses a hybrid type system combining:

  • Nominal typing: Templates and entities have explicit names and identity
  • Structural typing: Life arc requirements use duck typing (any template with required fields)
  • Subtype polymorphism: Template extension creates subtype relationships
  • Algebraic types: Concepts and sub_concepts form sum types with pattern matching

Subtyping Relationship

Template extension creates subtyping:

species Human { strength: Number = 10 }
template Person: Human { age: Number }
template Baker: Person { skill_level: Number }

Type hierarchy:
  Baker <: Person <: Human

Liskov Substitution Principle:

  • Baker can be used wherever Person is expected (has all Person fields)
  • Person can be used wherever Human is expected (has all Human fields)
  • Subtyping flows from child to parent (upcast is implicit and safe)

Action parameter compatibility:

action greet(person: Person)

character Martha: Baker
greet(Martha)  // ✓ Valid - Baker <: Person (implicit upcast)

action train_baker(baker: Baker)

character Bob: Person
train_baker(Bob)  // ✗ ERROR - Person </: Baker (not a subtype)

No downcasting needed: All types are resolved at compile-time. The compiler knows the exact template type of each entity and validates action calls accordingly.

Field Resolution Through Inheritance

Override chain priority: Character > Template > Species

species Human { strength: Number = 10, speed: Number = 10 }
template Warrior: Human { strength: Number = 15 }
character Conan: Warrior { strength: 20 }

Field lookup algorithm:
  Conan.strength:
    1. Check character instance → 20 ✓ FOUND
    2. (not reached)

  Conan.speed:
    1. Check character instance → not found
    2. Check Warrior template → not found
    3. Check Human species → 10 ✓ FOUND

Type invariance through chain:

  • Field types cannot change through inheritance
  • If species defines strength: Number, template cannot override to strength: Text
  • Only values/defaults can be overridden, not types

Life Arc Type Constraints

Life arcs use structural requirements (duck typing):

life_arc Aging requires { age: Number, species: Species }

// Works with ANY template that has these fields

Compile-time validation:

template Person: Human {
    age: Number,        // ✓ has required field
    name: Text,
    // species inherited from Human ✓ has required field
}

character Martha: Person {
    life_arcs: {
        Aging: Adult  // ✓ Person has all required fields
    }
}

// Compiler checks:
// 1. Does Person have field 'age' of type Number? YES
// 2. Does Person have field 'species' of type Species? YES (inherited)
// 3. Are types exact match (not subtypes)? YES
// → VALID

Unsoundness prevention:

template Robot {
    age?: Number     // Optional field
}

life_arc Aging requires { age: Number } {
    // Requires NON-optional age
}

character R2D2: Robot {
    life_arcs: { Aging: Adult }
}

// ✗ TYPE ERROR: Aging requires age: Number (required)
//               but Robot has age?: Number (optional)
//               Required field cannot be optional in template

Rule: Life arc required fields MUST be non-optional in template.

Behavior Type Checking

Implicit self with template typing:

behavior BakingWork {
    then {
        check_orders        // Desugars to: check_orders(self)
        bake(Bread)         // Desugars to: bake(self, Bread)
    }
}

action check_orders(baker: Baker)
action bake(baker: Baker, item: Item)

Type of implicit self:

  • self has the type of the entity's template
  • Known at compile-time

Compatibility checking:

character Martha: Baker {
    uses behaviors: [BakingWork]
}

// Compiler validates:
// When Martha runs BakingWork:
//   self: Baker (Martha's template)
//   check_orders(self) → check_orders(Baker) ✓
//   bake(self, Bread) → bake(Baker, Item) ✓
// → VALID

character Bob: Person {
    uses behaviors: [BakingWork]
}

// Compiler validates:
// When Bob tries to run BakingWork:
//   self: Person (Bob's template)
//   check_orders(self) → check_orders(Person)
//   But check_orders expects Baker
//   Person </: Baker (not a subtype)
// → ERROR: Bob cannot use BakingWork

Rule: Entity can only use behaviors where all action calls are compatible with entity's template type (via exact match or subtyping).

State Validation

State conditions are contracts:

life_arc Baker requires { skill_level: Number } {
    Master {
        when skill_level >= 7  // Entry condition + runtime assertion
        can use behaviors: [InnovateTechniques]
    }
}

The when clause serves two purposes:

  1. Entry condition: Must be true to transition INTO this state
  2. Runtime assertion: Should be true while IN this state

Potential inconsistency:

character Martha: Baker {
    skill_level: 8,
    life_arcs: { Baker: Master }  // State says Master
}

action lower_skill(baker: Baker) {
    baker.skill_level -= 5  // Now skill_level = 3
}

// After action: Martha is in Master state, but skill_level < 7
// State is inconsistent with data!

Resolution - Hybrid approach:

Compile-time (Storybook): Emit warnings when actions could invalidate state conditions:

Warning: action 'lower_skill' modifies 'skill_level' which is used in
         state condition for Baker.Master (requires skill_level >= 7).
         Runtime should validate state before executing state-specific behaviors.

Runtime (Implementation): Before executing behaviors that depend on state:

impl Runtime {
    fn execute_behavior(&mut self, entity_id: EntityId, behavior: &Behavior) {
        let character = self.get_character(entity_id);

        // Validate current state conditions
        for (arc_name, current_state) in &character.life_arcs {
            let arc = self.get_life_arc(arc_name);
            let state_def = arc.get_state(current_state);

            if !self.evaluate_condition(entity_id, &state_def.condition) {
                // State is inconsistent!
                // Option A: Re-evaluate and transition to correct state
                // Option B: Log warning and continue
                // Option C: Throw error and reject behavior
                // Runtime decides policy
            }
        }

        // Execute behavior actions...
    }
}

Rule: State conditions are validated at entry (transition time) and SHOULD be validated at execution time (runtime's responsibility).

Type Checking Guarantees

Compile-time guarantees: Template subtyping is safe (Baker <: Person) Field access through inheritance is valid Life arc structural requirements are satisfied Action parameters match expected types Behaviors are compatible with entity templates Initial life arc states satisfy entry conditions

Runtime responsibilities: ⚠️ Validate state conditions before executing state-specific behaviors ⚠️ Handle state transitions when field values change ⚠️ Manage entity state consistency

The language guarantees type safety at compile-time. The runtime guarantees state consistency at execution-time.

Formal Type Rules

Subtyping (transitive):

T modifies S
─────────────── (SUB-EXTEND)
T <: S

T <: S    S <: R
─────────────── (SUB-TRANS)
T <: R

Field access:

E : T    T has field f : τ (through inheritance chain)
──────────────────────────────────────────────────── (FIELD-ACCESS)
E.f : τ

Life arc compatibility:

T has fields {f1: τ1, ..., fn: τn}  (required, non-optional)
L requires {f1: τ1, ..., fn: τn}
──────────────────────────────────────────────────── (LIFEARC-COMPAT)
T compatible with L

Behavior validity:

B calls actions {a1(Self, ...), ..., an(Self, ...)}
E : T
∀i. T <: ParamType(ai, 0)  (template is subtype of first param)
──────────────────────────────────────────────────── (BEHAVIOR-VALID)
E can use behavior B

Action call:

a : (T1, T2, ..., Tn) → Unit
E : T    T <: T1
args : (T2, ..., Tn)
──────────────────────────────────────────────────── (ACTION-CALL)
a(E, args) : Unit

Summary

The type system is sound with these properties:

  • Progress: Well-typed programs don't get stuck at compile-time
  • Type preservation: Types are maintained through inheritance and execution
  • Memory safety: No invalid field access or type confusion
  • Subtype safety: Upcasting (child → parent) is safe and implicit

The runtime must maintain state consistency by validating life arc conditions at execution time.


Future Considerations

  • Exhaustiveness checking: Ensure all variant combinations are covered in concept_comparisons
  • Default variants: Support for catch-all patterns in comparisons
  • Type inference: Automatic parent concept detection beyond prefix matching
  • Generic concepts: Parameterized types for reusable patterns
  • Constraint validation: Runtime validation of type constraints