The tests were using line: 2 but the character declarations were on line: 1 (due to the leading newline in the raw string literal). This caused the cursor position to be outside the character span, making the code actions fail to trigger. Fixed by changing line: 2 to line: 1 in both test_convert_species_to_template and test_convert_template_to_species.
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
- Language is pure - no mutation, no state changes, only declarations
- Templates are record types - universal structural type definitions
- Entities are typed values - characters, institutions, locations are instances
- Behaviors are functional - control flow and pattern matching
- Actions are the boundary - where language meets runtime
Core Declarations
template: Record type definitions (structural types)character/institution/location: Typed value instancesconcept: Base type declarations for pattern matchingsub_concept: Enumerated and typed subtypes (tagged union members)concept_comparison: Compile-time pattern matching over subtype combinationsaction: 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:
species→species_type,creature_typelocation→location_ref,placecharacter→character_ref,persontemplate→template_name,template_refbehavior→behavior_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
.sbfiles (typicallyactions.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:TypeCup.Size→ parent:Cup, name:SizeCupcake.Flavor→ parent:Cupcake, name:Flavor
Why dot notation?
- ✅ Prevents accidental naming collisions (
Cupcakewon't matchCup) - ✅ 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.Smallis a distinct variant of theCupunionCupType.Glassis another distinct variant of theCupunion- 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 typeType 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_comparisonis 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:
anyis 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 anykeyword is parsed asValue::Anyin 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:
- Check character fields - use if present
- Check template fields - use if present and not in character
- Check species fields - use if not overridden
- 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:
- 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
}
- 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)
- 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:
- After action execution, check ALL life arcs for the affected entity
- For each life arc, evaluate current state's
whencondition - If condition is FALSE, current state is invalid
- Check state's transitions in order
- Take first transition whose condition is TRUE
- Update entity's life_arcs to new state
- 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:
- What state is she in? (Human.Adult, Baker.Master)
- Where is she? (Bakery)
- 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 tostrength: 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:
selfhas 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:
- Entry condition: Must be true to transition INTO this state
- 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 extends 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