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:
AircraftRegistrationfor individual aircraft (e.g., "5Y-KZA")AircraftICAOfor aircraft types (e.g., "B788")AirportICAOfor airportsFlightNumberfor 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
endEach 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
# 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 IDCreate 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]: ...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]| 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 | - |---
models/aircraft/base.py: ChangeAircraftStateto usecurrent_location_icao: AirportICAO | Noneandcurrent_flight_id: str | Noneinstead of object refs.models/flight.py: ChangeFlightStateto useassigned_aircraft_reg: str | None.models/route.py: ChangeRouteStateto useassigned_aircraft_reg: str | Noneandassigned_flight_ids: tuple[str, ...].models/fleet.py: Simplify to just manage aircraft states by registration.- New
simulation/context.py: Central registry with lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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- 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
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
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# 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 onlyCreate 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]: ...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]| 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 | - |---
models/aircraft/base.py: ChangeAircraftStateto usecurrent_location_icao: AirportICAO | Noneandcurrent_flight_id: str | Noneinstead of object refs.models/flight.py: ChangeFlightStateto useassigned_aircraft_reg: str | None.models/route.py: ChangeRouteStateto useassigned_aircraft_reg: str | Noneandassigned_flight_ids: tuple[str, ...].models/fleet.py: Simplify to just manage aircraft states by registration.- New
simulation/context.py: Central registry with lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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- 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]| 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
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
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# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdandFlightIdtyped identifiers.models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightId | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightId, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)- Type safety - typed IDs prevent mixing aircraft registrations with airport codes
- 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
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
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# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdandFlightIdtyped identifiers.models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightId | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightId, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)- Type safety - typed IDs prevent mixing aircraft registrations with airport codes
- 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
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
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# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdandFlightIdtyped identifiers.models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightId | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightId, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)- Type safety - typed IDs prevent mixing aircraft registrations with airport codes
- No circular references - state structs are simple, no import cycles
- Single source of truth -
SimulationContextis the authority for all lookups - Clear data flow - commands down, events up
- Testable - each layer can be tested in isolation with mock IDs
- 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]
- Added README documentation task - The architecture diagram will be added to a new "Design Overview" section in the README.
Updated task order:
- Add
RouteIdandFlightIdtoidentifiers.py - Create
SimulationContextwith typed lookups - Refactor
AircraftState→ typed ID refs - Refactor
FlightState→ typed ID refs - Refactor
RouteState→ typed ID refs + flight generation - Simplify
FleetStatetodict[AircraftRegistration, AircraftState] - Create
DisruptionContextfor upward propagation - Update
RecoveryEngine - Update tests
- 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:
load_routes()inio/loaders.pyis where all routes are loaded- Each route has flight numbers in its legs (e.g., KQ100 outbound, KQ101 inbound)
- 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
FlightIdtype needed -FlightNumberalone 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
endEach 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
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# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdandFlightIdtyped identifiers.models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightId | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightId, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)-
Type safety - typed IDs prevent mixing aircraft registrations with airport codes
-
No circular references - state structs are simple, no import cycles
-
Single source of truth -
SimulationContextis the authority -
Single source of truth -
SimulationContextis the authority for all lookups -
Clear data flow - commands down, events up
-
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
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
Add RouteId to models/identifiers.py:
class RouteId(Struct, frozen=True):
"""Route identifier (e.g., 'NBO-LHR')."""
code: strFlightNumber 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)# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdandFlightIdtyped identifiers.models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightId | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightId, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)-
Type safety - typed IDs prevent mixing aircraft registrations with airport codes
-
No circular references - state structs are simple, no import cycles
-
Single source of truth -
SimulationContextis the authority -
Single source of truth -
SimulationContextis the authority for all lookups -
Clear data flow - commands down, events up
-
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
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
Add RouteId to models/identifiers.py:
class RouteId(Struct, frozen=True):
"""Route identifier (e.g., 'NBO-LHR')."""
code: strFlightNumber 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)# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdandFlightIdtyped identifiers.models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightId | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightId, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)-
Type safety - typed IDs prevent mixing aircraft registrations with airport codes
-
No circular references - state structs are simple, no import cycles
-
Single source of truth -
SimulationContextis the authority -
Single source of truth -
SimulationContextis the authority for all lookups -
Clear data flow - commands down, events up
-
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
endEach 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
Add RouteId to models/identifiers.py:
class RouteId(Struct, frozen=True):
"""Route identifier (e.g., 'NBO-LHR')."""
code: strFlightNumber 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)# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdtyped identifier.io/loaders.py: Add flight number uniqueness validation inload_routes().models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightNumber | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightNumber, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)-
Type safety - typed IDs prevent mixing aircraft registrations with airport codes
-
No circular references - state structs are simple, no import cycles
-
Single source of truth -
SimulationContextis the authority -
Single source of truth -
SimulationContextis the authority for all lookups -
Clear data flow - commands down, events up
-
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
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
Add RouteId to models/identifiers.py:
class RouteId(Struct, frozen=True):
"""Route identifier (e.g., 'NBO-LHR')."""
code: strFlightNumber 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)# 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 onlyCreate 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]: ...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]| 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 | - |
models/identifiers.py: AddRouteIdtyped identifier.io/loaders.py: Add flight number uniqueness validation inload_routes().models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightNumber | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightNumber, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)- Type safety - typed IDs prevent mixing aircraft registrations with airport codes
- No circular references - state structs are simple, no import cycles
- Single source of truth -
SimulationContextis the authority for all lookups - Clear data flow - commands down, events up
- Testable - each layer can be tested in isolation with mock IDs
- 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:
- Add
RouteIdtoidentifiers.py - Add flight number uniqueness validation in
load_routes() - Create
SimulationContext - Refactor state structs to use typed ID references
- Create
DisruptionContext - Update tests
- 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
endEach 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
Add RouteId to models/identifiers.py:
class RouteId(Struct, frozen=True):
"""Route identifier (e.g., 'NBO-LHR')."""
code: strFlightNumber 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)# 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 onlyCreate 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] = stateThis provides two layers of validation:
- Load-time in
load_routes()- catches config errors early - Runtime in
SimulationContext- catches programming errors
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]| 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 | - |
models/identifiers.py: AddRouteIdtyped identifier.io/loaders.py: Add flight number uniqueness validation inload_routes().models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightNumber | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightNumber, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)- Type safety - typed IDs prevent mixing aircraft registrations with airport codes
- No circular references - state structs are simple, no import cycles
- Single source of truth -
SimulationContextis the authority for all lookups - Clear data flow - commands down, events up
- Testable - each layer can be tested in isolation with mock IDs
- 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
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
Add RouteId to models/identifiers.py:
class RouteId(Struct, frozen=True):
"""Route identifier (e.g., 'NBO-LHR')."""
code: strFlightNumber 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)# 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 onlyCreate 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] = stateThis provides two layers of validation:
- Load-time in
load_routes()- catches config errors early - Runtime in
SimulationContext- catches programming errors
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]| 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 | - |
models/identifiers.py: AddRouteIdtyped identifier.io/loaders.py: Add flight number uniqueness validation inload_routes().models/aircraft/base.py: ChangeAircraftStateto usecurrent_location: AirportICAO | Noneandcurrent_flight: FlightNumber | None.models/flight.py: ChangeFlightStateto useassigned_aircraft: AircraftRegistration | Noneand addroute_id: RouteId | None.models/route.py: ChangeRouteStateto useassigned_aircraft: AircraftRegistration | Noneandassigned_flights: tuple[FlightNumber, ...].models/fleet.py: Simplify todict[AircraftRegistration, AircraftState].- New
simulation/context.py: Central registry with typed lookup methods. disruptions/recovery.py: Refactor to useDisruptionContextfor upward propagation.
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)- Type safety - typed IDs prevent mixing aircraft registrations with airport codes
- No circular references - state structs are simple, no import cycles
- Single source of truth -
SimulationContextis the authority for all lookups - Clear data flow - commands down, events up
- Testable - each layer can be tested in isolation with mock IDs
- 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] = stateUser
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.