Renames the `concept_comparison` keyword to `definition` across the entire codebase for better readability and conciseness. Changes: - Tree-sitter grammar: `concept_comparison` node → `definition` - Tree-sitter queries: highlights, outline, and indents updated - Zed extension highlights.scm updated to match - Lexer: `Token::ConceptComparison` → `Token::Definition` - Parser: `ConceptComparisonDecl` rule → `DefinitionDecl` - AST: `Declaration::ConceptComparison` → `Declaration::Definition`, `ConceptComparisonDecl` struct → `DefinitionDecl` - All Rust source files updated (validate, names, convert, references, semantic_tokens, symbols, code_actions, hover, completion) - `validate_concept_comparison_patterns` → `validate_definition_patterns` - Example file and test corpus updated - Spec docs: created SBIR-v0.3.2-SPEC.md, updated TYPE-SYSTEM.md, README.md, SBIR-CHANGELOG.md, SBIR-v0.3.1-SPEC.md
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)definition: 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
definition
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
definition - Compile-time Pattern Matching
A definition performs compile-time pattern matching over combinations of sub_concept values, mapping them to derived variant names.
Syntax:
definition 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
}
definition 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
definitionis 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)
}
definition 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 definition
- 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
}
definition 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 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 definitions
- 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