Skip to content

Instantly share code, notes, and snippets.

@beatzxbt
Created December 22, 2025 17:49
Show Gist options
  • Select an option

  • Save beatzxbt/354722c13ed3d70a5447833a991113c7 to your computer and use it in GitHub Desktop.

Select an option

Save beatzxbt/354722c13ed3d70a5447833a991113c7 to your computer and use it in GitHub Desktop.
Prompt Example (Opus 4.5 - No Thinking)

Aviation system state management

Exported on 12/22/2025 at 17:46:13 GMT from Cursor (2.2.43)


User

Im having a bit of a mess with handling and managing all the various components' states in this system.

We have Airport/Aircraft states as the absolute bottom layer, then Flight/Route states on top of these, then Fleets/Schedules as the absolute top (being purely a state-changing machine).

How am i supposed to say which level changes which other level's state? eg, a Route gets assigned by the Scheduler. It then needs to generate the flights and assign an airplane, which changes:

  • the route state, by giving it an airplane and telling it to generate its scheduled flights
    • then the airplane state must indicate that its assigned, where it is
    • the each individual flights states indicating assigned
  • the fleet state, which on its own accord, indicates the airplane is assigned

All that route specific information needs to be accessible FROM the Route struct as that is what the Scheduler will need to deal with if a disruption is propagated. This disruption is meant to occur on the lowest levels (airplane, or airport) and propagate up to the Route getting more and more detailed so the Scheduler can deal with it in a proper manner.

What do you think about all these issues?


Cursor

This is a really important architectural question. Let me explore your current codebase to understand how these components are structured before I can give you meaningful advice on state management patterns.

Let me also check how the simulation handles state and events, as well as the disruption recovery logic:

Let me check the fleet model more carefully - I noticed it references AircraftState but also the AircraftState is defined in aircraft/base.py:

Now I have a complete picture of your architecture. This is a fascinating and common challenge in simulation/scheduling systems. Let me analyze the issues and propose a solution.


User

I like this. A few additional things before you build:

I want you to use more specific types for the IDs, most of which we have already defined. For example, rather than a dict[str, AircraftState], we would have dict[AircraftRegistration, AircraftState].

This would be a cleaner design in my opinion.

Also, could you add a 'Design Overview' section in the README that contains the diagram you've made within your plan for our proposed architecture? Its very clean and i'd like it to stay for users after this chat is closed.


Cursor

Good points! Let me check the existing identifier types so I can incorporate them properly:

I see the existing types. I notice we have:

  • AircraftRegistration for individual aircraft (e.g., "5Y-KZA")
  • AircraftICAO for aircraft types (e.g., "B788")
  • AirportICAO for airports
  • FlightNumber for flights

One consideration: FlightNumber alone isn't unique across dates (KQ100 operates daily). We may need a FlightId type that combines flight number + date. Let me update the plan with the typed IDs and the README section:

Now I'll update the plan with the typed IDs and add the README documentation task:

---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId and FlightId typed identifiers to identifiers.py
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. ID-Based References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft_reg: str | None = None  # Just the registration ID

2. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[str, AircraftState]   # reg -> state
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[str, FlightState]       # flight_id -> state
    route_states: dict[str, RouteState]         # route_id -> state
    
    def get_aircraft(self, reg: str) -> AircraftState: ...
    def get_flights_for_route(self, route_id: str) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: str) -> list[FlightState]: ...

3. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route
route_state.assign_aircraft(ctx, aircraft_reg="5Y-KZA")

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: str) -> None:
    self.assigned_aircraft_reg = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: str
    affected_flights: list[str]      # Populated as it propagates
    affected_routes: list[str]       # Populated as it propagates
    suggested_actions: list[RecoveryAction]

4. Clear State Ownership

| Layer | Owns | References (by ID) ||-------|------|-------------------|| AircraftState | status, available_from | current_airport_icao, current_flight_id || AirportState | status, delay_minutes | - || FlightState | status, actual times | aircraft_reg, origin_icao, dest_icao || RouteState | status, assignment | aircraft_reg, flight_ids[] || Schedule | route collection | - |---

Files to Modify

  1. models/aircraft/base.py: Change AircraftState to use current_location_icao: AirportICAO | None and current_flight_id: str | None instead of object refs.
  2. models/flight.py: Change FlightState to use assigned_aircraft_reg: str | None.
  3. models/route.py: Change RouteState to use assigned_aircraft_reg: str | None and assigned_flight_ids: tuple[str, ...].
  4. models/fleet.py: Simplify to just manage aircraft states by registration.
  5. New simulation/context.py: Central registry with lookup methods.
  6. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context
disruption = DisruptionContext(
    source_type="aircraft",
    source_id="5Y-KZA",
    delay_minutes=120,
)

# 3. Propagate UP through layers
for flight_id in ctx.get_flights_for_aircraft("5Y-KZA"):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context
scheduler.handle_disruption(disruption)
# -> Has full visibility: which aircraft, which flights, which routes

Benefits

  1. No circular references - state structs are simple, no import cycles

```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId and FlightId typed identifiers to identifiers.py
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifiers

Add to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'HKJK-EGLL-HKJK')."""
    code: str

class FlightId(Struct, frozen=True):
    """Unique flight identifier combining flight number and date."""
    flight_number: FlightNumber
    departure_date: date

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightId, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

3. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route
route_state.assign_aircraft(ctx, aircraft_reg="5Y-KZA")

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: str) -> None:
    self.assigned_aircraft_reg = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: str
    affected_flights: list[str]      # Populated as it propagates
    affected_routes: list[str]       # Populated as it propagates
    suggested_actions: list[RecoveryAction]

4. Clear State Ownership

| Layer | Owns | References (by ID) ||-------|------|-------------------|| AircraftState | status, available_from | current_airport_icao, current_flight_id || AirportState | status, delay_minutes | - || FlightState | status, actual times | aircraft_reg, origin_icao, dest_icao || RouteState | status, assignment | aircraft_reg, flight_ids[] || Schedule | route collection | - |---

Files to Modify

  1. models/aircraft/base.py: Change AircraftState to use current_location_icao: AirportICAO | None and current_flight_id: str | None instead of object refs.
  2. models/flight.py: Change FlightState to use assigned_aircraft_reg: str | None.
  3. models/route.py: Change RouteState to use assigned_aircraft_reg: str | None and assigned_flight_ids: tuple[str, ...].
  4. models/fleet.py: Simplify to just manage aircraft states by registration.
  5. New simulation/context.py: Central registry with lookup methods.
  6. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context
disruption = DisruptionContext(
    source_type="aircraft",
    source_id="5Y-KZA",
    delay_minutes=120,
)

# 3. Propagate UP through layers
for flight_id in ctx.get_flights_for_aircraft("5Y-KZA"):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context
scheduler.handle_disruption(disruption)
# -> Has full visibility: which aircraft, which flights, which routes

Benefits

  1. No circular references - state structs are simple, no import cycles

```markdown
#### 4. Bidirectional Flow: Commands Down, Events Up

**Commands flow DOWN** (Scheduler -> Route -> Flight -> Aircraft):

```python
# Scheduler assigns route
route_state.assign_aircraft(ctx, aircraft_reg=AircraftRegistration(...))

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]      # Populated as it propagates
    affected_routes: list[RouteId]        # Populated as it propagates
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

Layer Owns References (by typed ID)
AircraftState status, available_from AirportICAO, FlightId
AirportState status, delay_minutes -
FlightState status, actual times AircraftRegistration, AirportICAO, RouteId
RouteState status, assignment AircraftRegistration, tuple[FlightId, ...]
Schedule route collection -


```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId and FlightId typed identifiers to identifiers.py
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifiers

Add to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'HKJK-EGLL-HKJK')."""
    code: str

class FlightId(Struct, frozen=True):
    """Unique flight identifier combining flight number and date."""
    flight_number: FlightNumber
    departure_date: date

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightId, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

Layer Owns References (typed)
AircraftState status, available_from AirportICAO, FlightId
AirportState status, delay_minutes -
FlightState status, actual times AircraftRegistration, AirportICAO, RouteId
RouteState status, assignment AircraftRegistration, tuple[FlightId, ...]
Schedule route collection -

Files to Modify

  1. models/identifiers.py: Add RouteId and FlightId typed identifiers.
  2. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightId | None.
  3. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  4. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightId, ...].
  5. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  6. New simulation/context.py: Central registry with typed lookup methods.
  7. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes
  2. No circular references - state structs are simple, no import cycles

Let me fix the table formatting in the plan:

```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId and FlightId typed identifiers to identifiers.py
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifiers

Add to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'HKJK-EGLL-HKJK')."""
    code: str

class FlightId(Struct, frozen=True):
    """Unique flight identifier combining flight number and date."""
    flight_number: FlightNumber
    departure_date: date

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightId, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

Layer Owns References (typed)
AircraftState status, available_from AirportICAO, FlightId
AirportState status, delay_minutes -
FlightState status, actual times AircraftRegistration, AirportICAO, RouteId
RouteState status, assignment AircraftRegistration, tuple[FlightId, ...]
Schedule route collection -

Files to Modify

  1. models/identifiers.py: Add RouteId and FlightId typed identifiers.
  2. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightId | None.
  3. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  4. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightId, ...].
  5. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  6. New simulation/context.py: Central registry with typed lookup methods.
  7. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes
  2. No circular references - state structs are simple, no import cycles

```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId and FlightId typed identifiers to identifiers.py
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifiers

Add to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'HKJK-EGLL-HKJK')."""
    code: str

class FlightId(Struct, frozen=True):
    """Unique flight identifier combining flight number and date."""
    flight_number: FlightNumber
    departure_date: date

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightId, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

Layer Owns References (typed)
AircraftState status, available_from AirportICAO, FlightId
AirportState status, delay_minutes -
FlightState status, actual times AircraftRegistration, AirportICAO, RouteId
RouteState status, assignment AircraftRegistration, tuple[FlightId, ...]
Schedule route collection -

Files to Modify

  1. models/identifiers.py: Add RouteId and FlightId typed identifiers.
  2. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightId | None.
  3. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  4. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightId, ...].
  5. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  6. New simulation/context.py: Central registry with typed lookup methods.
  7. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes
  2. No circular references - state structs are simple, no import cycles
  3. Single source of truth - SimulationContext is the authority for all lookups
  4. Clear data flow - commands down, events up
  5. Testable - each layer can be tested in isolation with mock IDs
  6. Serializable - ID-based state can be easily saved/restored

I've updated the plan with your requirements:

**Key changes:**

1. **Typed identifiers throughout** - All references now use existing types like `AircraftRegistration`, `AirportICAO`, plus two new types:
   - `RouteId` - for route lookups (e.g., "HKJK-EGLL-HKJK")
   - `FlightId` - combines `FlightNumber` + `date` for unique flight identification

2. **`SimulationContext` with typed dicts:**
   ```python
   aircraft_states: dict[AircraftRegistration, AircraftState]
   airport_states: dict[AirportICAO, AirportState]
   flight_states: dict[FlightId, FlightState]
   route_states: dict[RouteId, RouteState]
  1. Added README documentation task - The architecture diagram will be added to a new "Design Overview" section in the README.

Updated task order:

  1. Add RouteId and FlightId to identifiers.py
  2. Create SimulationContext with typed lookups
  3. Refactor AircraftState → typed ID refs
  4. Refactor FlightState → typed ID refs
  5. Refactor RouteState → typed ID refs + flight generation
  6. Simplify FleetState to dict[AircraftRegistration, AircraftState]
  7. Create DisruptionContext for upward propagation
  8. Update RecoveryEngine
  9. Update tests
  10. Add Design Overview to README

Ready to begin when you confirm!

   aircraft_states: dict[AircraftRegistration, AircraftState]
   airport_states: dict[AirportICAO, AirportState]
   flight_states: dict[FlightId, FlightState]
   route_states: dict[RouteId, RouteState]

User

Im not 100% sure including the date within the FlightId is an ideal solution. You mentioned KQ100's situation of running multiple times a day.

This should never happen

Lets give the example of a standard Nairobi <> Mombasa route. This may be scheduled for a 8am/11am/2pm/6pm/10pm roundtrip. The flights for the 8am one will be, lets say, KQ100 (departure from NBO) and KQ101 (departure from MSA). The flights for the 11am MUST NOT be the same flight numbers even though they operate identical airport pairs! The 11am flights may subsequently be KQ102/103, and the 2pm ones KQ104/105 and so on.

This must be enforced WHEN the JSON is first loaded in as that sets the full universe of Routes and all possible flight numbers will be known.

Which section of the code do you think this should be enforced?


Cursor

Excellent clarification! You're absolutely right - if flight numbers are unique per day by design, then FlightNumber alone is sufficient as the identifier, and we don't need a composite FlightId. The uniqueness constraint should be enforced at load time.

Let me check where the JSON loading happens to understand the current flow:

Now I understand the full picture. Looking at the code:

  1. load_routes() in io/loaders.py is where all routes are loaded
  2. Each route has flight numbers in its legs (e.g., KQ100 outbound, KQ101 inbound)
  3. The validation for global flight number uniqueness should happen here, after all routes are parsed

The ideal place is in load_routes() - after parsing all routes but before returning them, we check that no FlightNumber appears more than once across all route legs.

This means:

  • No FlightId type needed - FlightNumber alone is the unique identifier
  • Validation at load time in load_routes() ensures the constraint from day one

Let me update the plan to reflect this:

---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifiers

Add to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'HKJK-EGLL-HKJK')."""
    code: str

class FlightId(Struct, frozen=True):
    """Unique flight identifier combining flight number and date."""
    flight_number: FlightNumber
    departure_date: date

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightId, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

| Layer | Owns | References (typed) |

|-------|------|-------------------|

| AircraftState | status, available_from | AirportICAO, FlightId |

| AirportState | status, delay_minutes | - |

| FlightState | status, actual times | AircraftRegistration, AirportICAO, RouteId |

| RouteState | status, assignment | AircraftRegistration, tuple[FlightId, ...] |

| Schedule | route collection | - |


Files to Modify

  1. models/identifiers.py: Add RouteId and FlightId typed identifiers.
  2. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightId | None.
  3. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  4. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightId, ...].
  5. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  6. New simulation/context.py: Central registry with typed lookup methods.
  7. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes

  2. No circular references - state structs are simple, no import cycles

  3. Single source of truth - SimulationContext is the authority

  4. Single source of truth - SimulationContext is the authority for all lookups

  5. Clear data flow - commands down, events up

  6. Testable - each layer can be tested in isolation with mock IDs


```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifier + Uniqueness Validation

Add RouteId to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'NBO-LHR')."""
    code: str

FlightNumber is already unique - each flight number (e.g., KQ100) only operates once per day by design. Different time slots use different flight numbers (KQ100, KQ102, KQ104, etc.).

Add validation in io/loaders.py load_routes():

# After parsing all routes, validate flight number uniqueness
seen_flight_numbers: set[FlightNumber] = set()
for route in routes:
    for leg in route.legs:
        if leg.flight_number in seen_flight_numbers:
            raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
        seen_flight_numbers.add(leg.flight_number)

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightId, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

| Layer | Owns | References (typed) |

|-------|------|-------------------|

| AircraftState | status, available_from | AirportICAO, FlightId |

| AirportState | status, delay_minutes | - |

| FlightState | status, actual times | AircraftRegistration, AirportICAO, RouteId |

| RouteState | status, assignment | AircraftRegistration, tuple[FlightId, ...] |

| Schedule | route collection | - |


Files to Modify

  1. models/identifiers.py: Add RouteId and FlightId typed identifiers.
  2. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightId | None.
  3. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  4. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightId, ...].
  5. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  6. New simulation/context.py: Central registry with typed lookup methods.
  7. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes

  2. No circular references - state structs are simple, no import cycles

  3. Single source of truth - SimulationContext is the authority

  4. Single source of truth - SimulationContext is the authority for all lookups

  5. Clear data flow - commands down, events up

  6. Testable - each layer can be tested in isolation with mock IDs


```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifier + Uniqueness Validation

Add RouteId to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'NBO-LHR')."""
    code: str

FlightNumber is already unique - each flight number (e.g., KQ100) only operates once per day by design. Different time slots use different flight numbers (KQ100, KQ102, KQ104, etc.).

Add validation in io/loaders.py load_routes():

# After parsing all routes, validate flight number uniqueness
seen_flight_numbers: set[FlightNumber] = set()
for route in routes:
    for leg in route.legs:
        if leg.flight_number in seen_flight_numbers:
            raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
        seen_flight_numbers.add(leg.flight_number)

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightNumber, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightId
    affected_flights: list[FlightId]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

| Layer | Owns | References (typed) |

|-------|------|-------------------|

| AircraftState | status, available_from | AirportICAO, FlightId |

| AirportState | status, delay_minutes | - |

| FlightState | status, actual times | AircraftRegistration, AirportICAO, RouteId |

| RouteState | status, assignment | AircraftRegistration, tuple[FlightId, ...] |

| Schedule | route collection | - |


Files to Modify

  1. models/identifiers.py: Add RouteId and FlightId typed identifiers.
  2. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightId | None.
  3. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  4. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightId, ...].
  5. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  6. New simulation/context.py: Central registry with typed lookup methods.
  7. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes

  2. No circular references - state structs are simple, no import cycles

  3. Single source of truth - SimulationContext is the authority

  4. Single source of truth - SimulationContext is the authority for all lookups

  5. Clear data flow - commands down, events up

  6. Testable - each layer can be tested in isolation with mock IDs


```markdown
#### 5. Clear State Ownership

| Layer | Owns | References (typed) |
|-------|------|-------------------|
| `AircraftState` | status, available_from | `AirportICAO`, `FlightNumber` |
| `AirportState` | status, delay_minutes | - |
| `FlightState` | status, actual times | `AircraftRegistration`, `AirportICAO`, `RouteId` |
| `RouteState` | status, assignment | `AircraftRegistration`, `tuple[FlightNumber, ...]` |
| `Schedule` | route collection | - |
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifier + Uniqueness Validation

Add RouteId to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'NBO-LHR')."""
    code: str

FlightNumber is already unique - each flight number (e.g., KQ100) only operates once per day by design. Different time slots use different flight numbers (KQ100, KQ102, KQ104, etc.).

Add validation in io/loaders.py load_routes():

# After parsing all routes, validate flight number uniqueness
seen_flight_numbers: set[FlightNumber] = set()
for route in routes:
    for leg in route.legs:
        if leg.flight_number in seen_flight_numbers:
            raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
        seen_flight_numbers.add(leg.flight_number)

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightNumber, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightNumber
    affected_flights: list[FlightNumber]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

Layer Owns References (typed)
AircraftState status, available_from AirportICAO, FlightNumber
AirportState status, delay_minutes -
FlightState status, actual times AircraftRegistration, AirportICAO, RouteId
RouteState status, assignment AircraftRegistration, tuple[FlightNumber, ...]
Schedule route collection -

Files to Modify

  1. models/identifiers.py: Add RouteId typed identifier.
  2. io/loaders.py: Add flight number uniqueness validation in load_routes().
  3. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightNumber | None.
  4. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  5. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightNumber, ...].
  6. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  7. New simulation/context.py: Central registry with typed lookup methods.
  8. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_id in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_id)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_id)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes

  2. No circular references - state structs are simple, no import cycles

  3. Single source of truth - SimulationContext is the authority

  4. Single source of truth - SimulationContext is the authority for all lookups

  5. Clear data flow - commands down, events up

  6. Testable - each layer can be tested in isolation with mock IDs


```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifier + Uniqueness Validation

Add RouteId to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'NBO-LHR')."""
    code: str

FlightNumber is already unique - each flight number (e.g., KQ100) only operates once per day by design. Different time slots use different flight numbers (KQ100, KQ102, KQ104, etc.).

Add validation in io/loaders.py load_routes():

# After parsing all routes, validate flight number uniqueness
seen_flight_numbers: set[FlightNumber] = set()
for route in routes:
    for leg in route.legs:
        if leg.flight_number in seen_flight_numbers:
            raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
        seen_flight_numbers.add(leg.flight_number)

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    aircraft_states: dict[AircraftRegistration, AircraftState]
    airport_states: dict[AirportICAO, AirportState]
    flight_states: dict[FlightNumber, FlightState]
    route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightNumber
    affected_flights: list[FlightNumber]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

| Layer | Owns | References (typed) |

|-------|------|-------------------|

| AircraftState | status, available_from | AirportICAO, FlightNumber |

| AirportState | status, delay_minutes | - |

| FlightState | status, actual times | AircraftRegistration, AirportICAO, RouteId |

| RouteState | status, assignment | AircraftRegistration, tuple[FlightNumber, ...] |

| Schedule | route collection | - |


Files to Modify

  1. models/identifiers.py: Add RouteId typed identifier.
  2. io/loaders.py: Add flight number uniqueness validation in load_routes().
  3. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightNumber | None.
  4. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  5. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightNumber, ...].
  6. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  7. New simulation/context.py: Central registry with typed lookup methods.
  8. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_number in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_number)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_number)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes
  2. No circular references - state structs are simple, no import cycles
  3. Single source of truth - SimulationContext is the authority for all lookups
  4. Clear data flow - commands down, events up
  5. Testable - each layer can be tested in isolation with mock IDs
  6. Serializable - ID-based state can be easily saved/restored

I've updated the plan with your feedback. Here's the summary of changes:

**Key decisions:**

1. **No `FlightId` type needed** - `FlightNumber` is the unique identifier since each flight number only operates once per day by design

2. **Uniqueness validation in `load_routes()`** - When routes are loaded from JSON, we validate that no `FlightNumber` appears more than once across all route legs. This catches config errors like defining KQ100 in two different routes.

3. **Validation code location**: [`io/loaders.py`](src/flightforgex/io/loaders.py) in the `load_routes()` function, after parsing all routes:
   ```python
   seen_flight_numbers: set[FlightNumber] = set()
   for route in routes:
       for leg in route.legs:
           if leg.flight_number in seen_flight_numbers:
               raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
           seen_flight_numbers.add(leg.flight_number)

Updated typed lookups:

  • dict[AircraftRegistration, AircraftState]
  • dict[AirportICAO, AirportState]
  • dict[FlightNumber, FlightState]
  • dict[RouteId, RouteState]

Updated task list:

  1. Add RouteId to identifiers.py
  2. Add flight number uniqueness validation in load_routes()
  3. Create SimulationContext
  4. Refactor state structs to use typed ID references
  5. Create DisruptionContext
  6. Update tests
  7. Add Design Overview to README

Ready to begin implementation when you confirm!

   seen_flight_numbers: set[FlightNumber] = set()
   for route in routes:
       for leg in route.legs:
           if leg.flight_number in seen_flight_numbers:
               raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
           seen_flight_numbers.add(leg.flight_number)

User

Could we add another layer of safety for all these duplication checks? I'd like the SimulationContext also verify that no duplicate values are added in.


Cursor

Absolutely - defense in depth is a great pattern. The SimulationContext should reject duplicates at runtime even if the loader already validated them. Let me update the plan:

---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext as central registry for all state lookups
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifier + Uniqueness Validation

Add RouteId to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'NBO-LHR')."""
    code: str

FlightNumber is already unique - each flight number (e.g., KQ100) only operates once per day by design. Different time slots use different flight numbers (KQ100, KQ102, KQ104, etc.).

Add validation in io/loaders.py load_routes():

# After parsing all routes, validate flight number uniqueness
seen_flight_numbers: set[FlightNumber] = set()
for route in routes:
    for leg in route.legs:
        if leg.flight_number in seen_flight_numbers:
            raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
        seen_flight_numbers.add(leg.flight_number)

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    _aircraft_states: dict[AircraftRegistration, AircraftState]
    _airport_states: dict[AirportICAO, AirportState]
    _flight_states: dict[FlightNumber, FlightState]
    _route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

Runtime duplicate prevention - all register_* methods reject duplicates:

def register_aircraft(self, state: AircraftState) -> None:
    """Register an aircraft state. Raises if registration already exists."""
    reg = state.aircraft.registration
    if reg in self._aircraft_states:
        raise ValueError(f"Duplicate aircraft registration: {reg}")
    self._aircraft_states[reg] = state

def register_flight(self, state: FlightState) -> None:
    """Register a flight state. Raises if flight number already exists."""
    fn = state.flight.flight_number
    if fn in self._flight_states:
        raise ValueError(f"Duplicate flight number: {fn}")
    self._flight_states[fn] = state

def register_route(self, state: RouteState) -> None:
    """Register a route state. Raises if route ID already exists."""
    rid = state.route_id
    if rid in self._route_states:
        raise ValueError(f"Duplicate route ID: {rid}")
    self._route_states[rid] = state

This provides two layers of validation:

  1. Load-time in load_routes() - catches config errors early
  2. Runtime in SimulationContext - catches programming errors

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightNumber
    affected_flights: list[FlightNumber]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

| Layer | Owns | References (typed) |

|-------|------|-------------------|

| AircraftState | status, available_from | AirportICAO, FlightNumber |

| AirportState | status, delay_minutes | - |

| FlightState | status, actual times | AircraftRegistration, AirportICAO, RouteId |

| RouteState | status, assignment | AircraftRegistration, tuple[FlightNumber, ...] |

| Schedule | route collection | - |


Files to Modify

  1. models/identifiers.py: Add RouteId typed identifier.
  2. io/loaders.py: Add flight number uniqueness validation in load_routes().
  3. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightNumber | None.
  4. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  5. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightNumber, ...].
  6. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  7. New simulation/context.py: Central registry with typed lookup methods.
  8. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_number in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_number)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_number)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes
  2. No circular references - state structs are simple, no import cycles
  3. Single source of truth - SimulationContext is the authority for all lookups
  4. Clear data flow - commands down, events up
  5. Testable - each layer can be tested in isolation with mock IDs
  6. Serializable - ID-based state can be easily saved/restored

```markdown
---
name: Layered State Management
overview: Restructure state management to use ID-based references with a central registry, clear ownership boundaries, and bidirectional event/command flow for clean propagation of changes and disruptions.
todos:
  - id: add-identifiers
    content: Add RouteId typed identifier to identifiers.py (FlightNumber is already unique)
    status: pending
  - id: enforce-unique-flight-numbers
    content: Add validation in load_routes() to reject duplicate flight numbers across all routes
    status: pending
  - id: create-context
    content: Create SimulationContext with typed registries and duplicate-rejection on register
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-aircraft-state
    content: Change AircraftState to use typed ID references instead of object refs
    status: pending
    dependencies:
      - add-identifiers
  - id: refactor-flight-state
    content: Change FlightState to use typed ID references
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: refactor-route-state
    content: Change RouteState to use typed ID references and implement flight generation
    status: pending
    dependencies:
      - refactor-flight-state
  - id: refactor-fleet-state
    content: Simplify FleetState to pure aircraft registry with typed keys
    status: pending
    dependencies:
      - refactor-aircraft-state
  - id: create-disruption-context
    content: Create DisruptionContext for upward event propagation
    status: pending
  - id: refactor-recovery
    content: Update RecoveryEngine to use DisruptionContext
    status: pending
    dependencies:
      - create-disruption-context
  - id: update-tests
    content: Update tests to work with new ID-based architecture
    status: pending
  - id: update-readme
    content: Add Design Overview section with architecture diagram to README
    status: pending
---

# Layered State Management Architecture

## Current Problems

Your current architecture has several entangled issues:

1. **Circular object references**: `AircraftState` holds `FlightState`, `FlightState` holds `AircraftState`, `RouteState` holds both. This creates a web where the same object instance is mutated from multiple places.
2. **Unclear ownership**: When `FlightState.set_status_scheduled()` calls `aircraft_state.set_status()`, who "owns" that change? The flight? The aircraft? The fleet?
3. **Duplicated state**: The same `AircraftState` instance might exist in `FleetState.states`, `RouteState.assigned_aircraft`, AND `FlightState.assigned_aircraft` - mutation in one place affects all, but it's invisible.
4. **Incomplete route-flight linkage**: `RouteState.assign_aircraft_and_generate_flights()` doesn't actually generate flights - it just tries to set status.
```mermaid
graph TD
    subgraph current [Current - Circular References]
        FS[FlightState] -->|holds| AS1[AircraftState]
        AS1 -->|holds| FS
        RS[RouteState] -->|holds| AS1
        RS -->|holds| FS
        Fleet[FleetState] -->|holds| AS1
    end

Proposed Architecture: ID-Based References + Central Registry

Core Principle: Own State, Reference by ID

Each layer owns its own state struct. Cross-layer references use IDs (not object references). A central SimulationContext provides lookup.

graph TB
    subgraph layer3 [Layer 3: Orchestration]
        Schedule
        Scheduler
    end
    
    subgraph layer2 [Layer 2: Operations]
        RouteState
        FlightState
    end
    
    subgraph layer1 [Layer 1: Resources]
        AircraftState
        AirportState
    end
    
    subgraph registry [Central Registry]
        SimContext[SimulationContext]
    end
    
    Schedule -->|commands| RouteState
    RouteState -->|commands| FlightState
    FlightState -->|commands| AircraftState
    
    AircraftState -.->|disruption events| FlightState
    FlightState -.->|disruption events| RouteState
    RouteState -.->|disruption events| Schedule
    
    SimContext -->|lookup| AircraftState
    SimContext -->|lookup| AirportState
    SimContext -->|lookup| FlightState
    SimContext -->|lookup| RouteState
Loading

Key Changes

1. New Typed Identifier + Uniqueness Validation

Add RouteId to models/identifiers.py:

class RouteId(Struct, frozen=True):
    """Route identifier (e.g., 'NBO-LHR')."""
    code: str

FlightNumber is already unique - each flight number (e.g., KQ100) only operates once per day by design. Different time slots use different flight numbers (KQ100, KQ102, KQ104, etc.).

Add validation in io/loaders.py load_routes():

# After parsing all routes, validate flight number uniqueness
seen_flight_numbers: set[FlightNumber] = set()
for route in routes:
    for leg in route.legs:
        if leg.flight_number in seen_flight_numbers:
            raise ValueError(f"Duplicate flight number {leg.flight_number} across routes")
        seen_flight_numbers.add(leg.flight_number)

2. Typed ID References (No Direct Object Holding)

# BEFORE: Direct reference creates coupling
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftState | None = None  # Holds object

# AFTER: Typed ID reference, lookup via context
class FlightState(Struct, kw_only=True):
    assigned_aircraft: AircraftRegistration | None = None  # Typed ID only

3. Central SimulationContext Registry

Create a new file simulation/context.py that holds all state registries with typed keys:

class SimulationContext:
    """Central registry for all simulation state."""
    
    _aircraft_states: dict[AircraftRegistration, AircraftState]
    _airport_states: dict[AirportICAO, AirportState]
    _flight_states: dict[FlightNumber, FlightState]
    _route_states: dict[RouteId, RouteState]
    
    def get_aircraft(self, reg: AircraftRegistration) -> AircraftState: ...
    def get_flights_for_route(self, route_id: RouteId) -> list[FlightState]: ...
    def get_flights_for_aircraft(self, reg: AircraftRegistration) -> list[FlightState]: ...

Runtime duplicate prevention - all register_* methods reject duplicates:

def register_aircraft(self, state: AircraftState) -> None:
    """Register an aircraft state. Raises if registration already exists."""
    reg = state.aircraft.registration
    if reg in self._aircraft_states:
        raise ValueError(f"Duplicate aircraft registration: {reg}")
    self._aircraft_states[reg] = state

def register_flight(self, state: FlightState) -> None:
    """Register a flight state. Raises if flight number already exists."""
    fn = state.flight.flight_number
    if fn in self._flight_states:
        raise ValueError(f"Duplicate flight number: {fn}")
    self._flight_states[fn] = state

def register_route(self, state: RouteState) -> None:
    """Register a route state. Raises if route ID already exists."""
    rid = state.route_id
    if rid in self._route_states:
        raise ValueError(f"Duplicate route ID: {rid}")
    self._route_states[rid] = state

This provides two layers of validation:

  1. Load-time in load_routes() - catches config errors early
  2. Runtime in SimulationContext - catches programming errors

4. Bidirectional Flow: Commands Down, Events Up

Commands flow DOWN (Scheduler -> Route -> Flight -> Aircraft):

# Scheduler assigns route with typed registration
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
route_state.assign_aircraft(ctx, aircraft_reg=reg)

# RouteState generates flights and assigns them
def assign_aircraft(self, ctx: SimulationContext, aircraft_reg: AircraftRegistration) -> None:
    self.assigned_aircraft = aircraft_reg
    for leg in self.route.legs:
        flight_state = ctx.create_flight_from_leg(leg, aircraft_reg)
        flight_state.assign_aircraft(ctx, aircraft_reg)

Disruption events flow UP (Aircraft/Airport -> Flight -> Route -> Scheduler):

@dataclass
class DisruptionContext:
    """Rich context for disruption propagation."""
    source_type: Literal["aircraft", "airport", "flight"]
    source_id: AircraftRegistration | AirportICAO | FlightNumber
    affected_flights: list[FlightNumber]
    affected_routes: list[RouteId]
    suggested_actions: list[RecoveryAction]

5. Clear State Ownership

| Layer | Owns | References (typed) |

|-------|------|-------------------|

| AircraftState | status, available_from | AirportICAO, FlightNumber |

| AirportState | status, delay_minutes | - |

| FlightState | status, actual times | AircraftRegistration, AirportICAO, RouteId |

| RouteState | status, assignment | AircraftRegistration, tuple[FlightNumber, ...] |

| Schedule | route collection | - |


Files to Modify

  1. models/identifiers.py: Add RouteId typed identifier.
  2. io/loaders.py: Add flight number uniqueness validation in load_routes().
  3. models/aircraft/base.py: Change AircraftState to use current_location: AirportICAO | None and current_flight: FlightNumber | None.
  4. models/flight.py: Change FlightState to use assigned_aircraft: AircraftRegistration | None and add route_id: RouteId | None.
  5. models/route.py: Change RouteState to use assigned_aircraft: AircraftRegistration | None and assigned_flights: tuple[FlightNumber, ...].
  6. models/fleet.py: Simplify to dict[AircraftRegistration, AircraftState].
  7. New simulation/context.py: Central registry with typed lookup methods.
  8. disruptions/recovery.py: Refactor to use DisruptionContext for upward propagation.

Disruption Propagation Example

When an aircraft has a mechanical issue:

# 1. Aircraft layer detects issue
aircraft_state.set_unavailable(until=delay_end)

# 2. Create disruption context with typed ID
reg = AircraftRegistration(registration="5Y-KZA", operator="KQA")
disruption = DisruptionContext(
    source_type="aircraft",
    source_id=reg,
    delay_minutes=120,
)

# 3. Propagate UP through layers using typed lookups
for flight_number in ctx.get_flights_for_aircraft(reg):
    flight_state = ctx.get_flight(flight_number)
    if flight_state.is_affected_by(disruption):
        disruption.affected_flights.append(flight_number)
        disruption.affected_routes.append(flight_state.route_id)

# 4. Scheduler receives rich context with fully typed references
scheduler.handle_disruption(disruption)

Benefits

  1. Type safety - typed IDs prevent mixing aircraft registrations with airport codes
  2. No circular references - state structs are simple, no import cycles
  3. Single source of truth - SimulationContext is the authority for all lookups
  4. Clear data flow - commands down, events up
  5. Testable - each layer can be tested in isolation with mock IDs
  6. Serializable - ID-based state can be easily saved/restored

Done. The plan now includes **two layers of duplicate validation**:

1. **Load-time** (`load_routes()`) - catches config errors immediately when JSON is parsed
2. **Runtime** (`SimulationContext.register_*()`) - catches programming errors during simulation

The `SimulationContext` uses private `_aircraft_states`, `_flight_states`, etc. dicts, with public `register_*` methods that reject duplicates:

```python
def register_flight(self, state: FlightState) -> None:
    fn = state.flight.flight_number
    if fn in self._flight_states:
        raise ValueError(f"Duplicate flight number: {fn}")
    self._flight_states[fn] = state

This ensures that even if someone bypasses the loader or constructs states programmatically, the registry still enforces uniqueness.

Ready to begin when you confirm!

def register_flight(self, state: FlightState) -> None:
    fn = state.flight.flight_number
    if fn in self._flight_states:
        raise ValueError(f"Duplicate flight number: {fn}")
    self._flight_states[fn] = state

User

Implement the plan as specified, it is attached for your reference. Do NOT edit the plan file itself.

To-do's from the plan have already been created. Do not create them again. Mark them as in_progress as you work, starting with the first one. Don't stop until you have completed all the to-dos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment