# Structured Runtime Migration Plan Goal: simplify the Python to Rust ability pipeline without breaking current behavior. This plan is intentionally staged. We keep the current bytecode path working until each new boundary has parity tests and a rollback point. ## Guiding rule Do not remove an old path until the new path: 1. Produces the same behavior on a targeted test set. 2. Has a clear Rust-side consumer. 3. Has a rollback path that is one commit away. ## Phase 1: Introduce structured data as the source of truth Scope: - `engine/models/ability.py` - `engine/models/structured_instruction_ir.py` - `compiler/parser_v2.py` (legacy entry point; parser logic is now split across `parser_*` modules) - `compiler/main.py` Work: 1. Keep `Ability.compile()` behavior unchanged for now. 2. Make sure every `Effect`, `Condition`, and `Cost` carries the runtime fields we need later: - `runtime_opcode` - `runtime_value` - `runtime_attr` - `runtime_slot` - `is_optional` - `target` - `params` 3. Treat the structured IR export as the new contract for inspection and debugging. 4. Add tests that snapshot the structured representation for a few representative abilities. Pass criteria: - Existing card compilation still works. - Structured output is stable and human-readable. - No runtime behavior changes yet. Rollback: - Remove the new structured export usage and keep the old compile path only. ## Phase 2: Move one runtime behavior at a time Scope: - `engine_rust_src/src/core/logic/interpreter/handlers/flow.rs` - `engine_rust_src/src/core/logic/interpreter/mod.rs` - `engine_rust_src/src/core/logic/models.rs` Work: 1. Start with `ALL_PLAYERS` handling. 2. Let Rust interpret the target semantics directly for one opcode family. 3. Keep Python emitting the legacy expansion until Rust parity is proven. 4. Add regression tests for: - self/opponent alternation - multiple consecutive `ALL_PLAYERS` effects - target reset behavior after mixed target sequences Pass criteria: - Rust can execute the structured target behavior without relying on Python expansion. - Legacy compiled bytecode still passes old tests. Rollback: - Re-enable the Python expansion path only. ## Phase 3: Move optional flow control into Rust Scope: - `engine/models/ability.py` - `engine_rust_src/src/core/logic/interpreter/mod.rs` - `engine_rust_src/src/core/logic/interpreter/handlers/flow.rs` Work: 1. Keep Python jump generation in place until Rust handles optional flow correctly. 2. Add a Rust-side optional execution path that can: - ask for player choice - skip an instruction cleanly when declined - continue execution at the next instruction 3. Test: - optional cost accepted - optional cost declined - nested optional blocks Pass criteria: - Optional behavior matches current output. - No `JUMP_IF_FALSE` dependency is needed for the new path. Rollback: - Restore the Python jump emission and keep Rust as a passive executor. ## Phase 4: Remove the semantic shim Scope: - `engine/models/ability_ir.py` - `engine/models/ability.py` - `compiler/parser_v2.py` (legacy parser surface at the time of writing) Work: 1. Replace intermediate semantic representations with direct `Effect` / `Condition` / `Cost` construction. 2. Keep any compatibility wrappers needed by tests or exporters. 3. Remove only the parts that no longer have consumers. Pass criteria: - Parser output feeds the final dataclasses directly. - No hidden mapping layer remains in the hot path. Rollback: - Reintroduce the shim as a thin adapter if needed. ## Phase 5: Replace packed runtime_attr with named params Scope: - `engine/models/generated_packer.py` - `engine/models/ability.py` - `engine_rust_src/src/core/logic/models.rs` - `engine_rust_src/src/core/logic/interpreter/handlers/*` Work: 1. Add named param access on the Rust side. 2. Keep packed fields available until every consumer has moved. 3. Remove bit-packing only after the last Rust caller stops using it. Pass criteria: - Rust reads named params directly. - Bit-packing is no longer required for runtime correctness. Rollback: - Restore packed-field reads from the existing compatibility fields. ## Recommended first commit If we want the safest possible start, the first commit should only do this: 1. Add or tighten structured IR export coverage. 2. Add golden tests for: - optional costs - `ALL_PLAYERS` - constant-trigger condition handling 3. Leave `Ability.compile()` behavior unchanged. That gives us a foundation we can trust before we touch control flow or target expansion.