|
| 1 | +# SCXML `<invoke>` Implementation Plan |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Implement SCXML `<invoke>` support (W3C spec §6.4) for spawning child state machine |
| 6 | +sessions when entering a state, with automatic cancellation on exit. |
| 7 | + |
| 8 | +**Branch:** `feat/invoke` |
| 9 | +**Base:** `develop` |
| 10 | +**Worktree:** `../python-statemachine-invoke` |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## Steps |
| 15 | + |
| 16 | +### 1. Create worktree and branch |
| 17 | +- [x] Create git worktree and branch `feat/invoke` |
| 18 | + |
| 19 | +### 2. Schema + Parser SCXML |
| 20 | +- [x] Add `InvokeDefinition` dataclass to `schema.py` |
| 21 | +- [x] Add `invocations` field to `schema.State` |
| 22 | +- [x] Add `parse_invoke()` to `parser.py` |
| 23 | +- [x] Call from `parse_state()` for `<invoke>` elements |
| 24 | +- [x] **Commit:** `feat: parse SCXML <invoke> elements` (fe19eca → rebased to 6d8067d) |
| 25 | + |
| 26 | +### 3. InvokeConfig and InvokeManager |
| 27 | +- [x] Create `statemachine/invoke.py` with: |
| 28 | + - `InvokeConfig` — static configuration |
| 29 | + - `Invocation` — runtime state |
| 30 | + - `ParentBridge` — listener on child for `#_parent` sends |
| 31 | + - `InvokeManager` — spawn/cancel/finalize lifecycle |
| 32 | +- [x] **Commit:** `feat: add InvokeManager for child session lifecycle` (76b9627 → rebased to 2b94c34) |
| 33 | + |
| 34 | +### 4. Engine integration — spawn and cancel |
| 35 | +- [x] Add `invoke_manager` and `states_to_invoke` to `BaseEngine.__init__` |
| 36 | +- [x] Track states with invocations in `_enter_states()` |
| 37 | +- [x] Cancel invocations in `_exit_states()` and `_handle_final_state()` |
| 38 | +- [x] Replace TODO in `sync.py` with actual invoke spawn code |
| 39 | +- [x] Replace TODO in `sync.py` with finalize + autoforward |
| 40 | +- [x] Mirror changes in `async_.py` |
| 41 | +- [ ] **Commit** (pending — code written, needs final test pass) |
| 42 | + |
| 43 | +### 5. Event routing (#_parent, invokeid, done.invoke) |
| 44 | +- [x] Add `invokeid` field to `TriggerData` in `event_data.py` |
| 45 | +- [x] Update `Event.put()` and `Event.build_trigger()` to accept `invokeid` |
| 46 | +- [x] Update `StateChart.send()` to accept `invokeid` |
| 47 | +- [x] Update `EventDataWrapper.__init__` to read `invokeid` from trigger_data |
| 48 | +- [x] Implement `#_parent` target in `create_send_action_callable()` |
| 49 | +- [x] Implement `#_child` target |
| 50 | +- [x] Implement `#_<invokeid>` target |
| 51 | +- [x] Fix `_eval_send_params` duplicate `machine` kwarg bug |
| 52 | +- [x] Fix `#_scxml_` vs `#_invokeid` target ordering (test496/521 regression) |
| 53 | +- [ ] **Commit** (pending — code written, needs final test pass) |
| 54 | + |
| 55 | +### 6. done_invoke_ naming convention and State(invoke=...) API |
| 56 | +- [x] Add `invoke` parameter to `State.__init__()` |
| 57 | +- [x] Add `_normalize_invoke()` static method |
| 58 | +- [x] Add `invocations` property to `InstanceState` |
| 59 | +- [x] Add `invoke` to `BaseStateKwargs` in `io/__init__.py` |
| 60 | +- [ ] Add `done_invoke_` handler in `factory.py` (not yet implemented) |
| 61 | +- [ ] **Commit** |
| 62 | + |
| 63 | +### 7. SCXML advanced invoke features |
| 64 | +- [x] `src` — load SCXML from external file |
| 65 | +- [x] `namelist` / `<param>` — pass initial data to child |
| 66 | +- [x] `autoforward` — forward all external events to child |
| 67 | +- [x] `<finalize>` — execute before processing child event |
| 68 | +- [ ] `typeexpr` — evaluate expression for type (not yet) |
| 69 | +- [ ] `srcexpr` — evaluate expression for src (not yet) |
| 70 | +- [ ] **Commit** |
| 71 | + |
| 72 | +### 8. Run W3C invoke tests and fix failures |
| 73 | +- [ ] Remove `.fail.md` for passing tests |
| 74 | +- [ ] Group failures by root cause and fix |
| 75 | +- [ ] Current status (post-rebase, post-bug-fixes): |
| 76 | + - Tests that now xpass (need .fail.md removed): test191, test207, test220, test223, test228, |
| 77 | + test232, test233, test235, test237, test241, test242, test245, test247, test338, test347, |
| 78 | + test422, test554 |
| 79 | + - Tests still expected to fail: test187, test192, test216, test226, test229, test234, test236, |
| 80 | + test240, test243, test244, test276, test530 |
| 81 | +- [ ] **Commits per group** |
| 82 | + |
| 83 | +### 9. Unit tests for Python invoke API |
| 84 | +- [ ] Create `tests/test_invoke.py` |
| 85 | +- [ ] Test basic: `State(invoke=ChildMachine)`, child terminates, parent gets `done.invoke` |
| 86 | +- [ ] Test cancellation: parent exits state, child is cancelled |
| 87 | +- [ ] Test cross-engine: sync parent + async child, async parent + sync child |
| 88 | +- [ ] Test `done_invoke_` naming convention |
| 89 | +- [ ] Test autoforward |
| 90 | +- [ ] Test multiple invocations |
| 91 | +- [ ] Use `sm_runner` fixture for sync/async coverage |
| 92 | +- [ ] **Commit** |
| 93 | + |
| 94 | +### 10. Documentation and release notes |
| 95 | +- [ ] Create `docs/invoke.md` (concept, Python API, SCXML, examples) |
| 96 | +- [ ] Add to `docs/index.md` toctree |
| 97 | +- [ ] Add section in `docs/releases/3.0.0.md` |
| 98 | +- [ ] Reference from `docs/statecharts.md` |
| 99 | +- [ ] **Commit** |
| 100 | + |
| 101 | +### 11. Final cleanup and verification |
| 102 | +- [ ] `uv run ruff check .` — all clean |
| 103 | +- [ ] `uv run ruff format .` — all clean |
| 104 | +- [ ] `uv run mypy statemachine/` — no errors |
| 105 | +- [ ] `uv run pytest -n auto` — full suite passes |
| 106 | +- [ ] Verify W3C invoke tests pass (`.fail.md` removed for passing tests) |
| 107 | +- [ ] Fix any regressions |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +## Key Files |
| 112 | + |
| 113 | +| File | Status | |
| 114 | +|------|--------| |
| 115 | +| `statemachine/invoke.py` | Committed + uncommitted changes | |
| 116 | +| `statemachine/engines/base.py` | Uncommitted changes | |
| 117 | +| `statemachine/engines/sync.py` | Uncommitted changes | |
| 118 | +| `statemachine/engines/async_.py` | Uncommitted changes | |
| 119 | +| `statemachine/event.py` | Uncommitted changes | |
| 120 | +| `statemachine/event_data.py` | Uncommitted changes | |
| 121 | +| `statemachine/state.py` | Uncommitted changes | |
| 122 | +| `statemachine/statemachine.py` | Uncommitted changes | |
| 123 | +| `statemachine/io/__init__.py` | Uncommitted changes | |
| 124 | +| `statemachine/io/scxml/actions.py` | Uncommitted changes | |
| 125 | +| `statemachine/io/scxml/processor.py` | Uncommitted changes | |
| 126 | +| `statemachine/io/scxml/schema.py` | Committed | |
| 127 | +| `statemachine/io/scxml/parser.py` | Committed | |
| 128 | + |
| 129 | +## Bugs Fixed |
| 130 | + |
| 131 | +1. **`_eval_send_params` duplicate `machine` kwarg** — function took `machine` as positional |
| 132 | + AND received it via `**kwargs`. Fixed by taking only `**kwargs` and extracting `machine` from it. |
| 133 | +2. **`#_scxml_` vs `#_invokeid` target ordering** — `#_scxml_foo` targets were caught by the |
| 134 | + generic `#_` invoke handler before reaching the `#_scxml_` → `error.communication` handler. |
| 135 | + Fixed by checking `#_scxml_` first. |
| 136 | +3. **Child `_parent_sm` not set during initial entry** — fixed by setting `_parent_sm` and |
| 137 | + `_invokeid` on the child CLASS before instantiation. |
0 commit comments