stateforge 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. stateforge-0.1.0/.github/workflows/ci.yml +41 -0
  2. stateforge-0.1.0/.github/workflows/release.yml +41 -0
  3. stateforge-0.1.0/.planning/PROJECT.md +72 -0
  4. stateforge-0.1.0/.planning/REQUIREMENTS.md +153 -0
  5. stateforge-0.1.0/.planning/ROADMAP.md +93 -0
  6. stateforge-0.1.0/.planning/STATE.md +118 -0
  7. stateforge-0.1.0/.planning/config.json +13 -0
  8. stateforge-0.1.0/.planning/phases/01-core-foundation/01-01-PLAN.md +167 -0
  9. stateforge-0.1.0/.planning/phases/01-core-foundation/01-01-SUMMARY.md +137 -0
  10. stateforge-0.1.0/.planning/phases/01-core-foundation/01-02-PLAN.md +194 -0
  11. stateforge-0.1.0/.planning/phases/01-core-foundation/01-02-SUMMARY.md +139 -0
  12. stateforge-0.1.0/.planning/phases/01-core-foundation/01-03-PLAN.md +345 -0
  13. stateforge-0.1.0/.planning/phases/01-core-foundation/01-03-SUMMARY.md +164 -0
  14. stateforge-0.1.0/.planning/phases/01-core-foundation/01-CONTEXT.md +81 -0
  15. stateforge-0.1.0/.planning/phases/01-core-foundation/01-RESEARCH.md +713 -0
  16. stateforge-0.1.0/.planning/phases/01-core-foundation/01-UAT.md +56 -0
  17. stateforge-0.1.0/.planning/phases/01-core-foundation/01-VALIDATION.md +89 -0
  18. stateforge-0.1.0/.planning/phases/01-core-foundation/01-VERIFICATION.md +180 -0
  19. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-01-PLAN.md +265 -0
  20. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-01-SUMMARY.md +145 -0
  21. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-02-PLAN.md +281 -0
  22. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-02-SUMMARY.md +155 -0
  23. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-03-PLAN.md +318 -0
  24. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-03-SUMMARY.md +152 -0
  25. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-CONTEXT.md +104 -0
  26. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-RESEARCH.md +653 -0
  27. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-UAT.md +65 -0
  28. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-VALIDATION.md +80 -0
  29. stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-VERIFICATION.md +122 -0
  30. stateforge-0.1.0/.planning/phases/03-validation/03-01-PLAN.md +248 -0
  31. stateforge-0.1.0/.planning/phases/03-validation/03-01-SUMMARY.md +151 -0
  32. stateforge-0.1.0/.planning/phases/03-validation/03-CONTEXT.md +88 -0
  33. stateforge-0.1.0/.planning/phases/03-validation/03-RESEARCH.md +573 -0
  34. stateforge-0.1.0/.planning/phases/03-validation/03-UAT.md +45 -0
  35. stateforge-0.1.0/.planning/phases/03-validation/03-VALIDATION.md +79 -0
  36. stateforge-0.1.0/.planning/phases/03-validation/03-VERIFICATION.md +101 -0
  37. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-01-PLAN.md +189 -0
  38. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-01-SUMMARY.md +144 -0
  39. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-02-PLAN.md +206 -0
  40. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-02-SUMMARY.md +111 -0
  41. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-CONTEXT.md +85 -0
  42. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-RESEARCH.md +501 -0
  43. stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-VALIDATION.md +77 -0
  44. stateforge-0.1.0/.planning/research/ARCHITECTURE.md +345 -0
  45. stateforge-0.1.0/.planning/research/FEATURES.md +210 -0
  46. stateforge-0.1.0/.planning/research/PITFALLS.md +297 -0
  47. stateforge-0.1.0/.planning/research/STACK.md +221 -0
  48. stateforge-0.1.0/.planning/research/SUMMARY.md +195 -0
  49. stateforge-0.1.0/.pre-commit-config.yaml +36 -0
  50. stateforge-0.1.0/LICENSE +193 -0
  51. stateforge-0.1.0/PKG-INFO +94 -0
  52. stateforge-0.1.0/README.md +70 -0
  53. stateforge-0.1.0/examples/order_workflow.py +168 -0
  54. stateforge-0.1.0/pyproject.toml +86 -0
  55. stateforge-0.1.0/src/stateforge/__init__.py +14 -0
  56. stateforge-0.1.0/src/stateforge/_core.py +326 -0
  57. stateforge-0.1.0/src/stateforge/_decorators.py +67 -0
  58. stateforge-0.1.0/src/stateforge/_errors.py +79 -0
  59. stateforge-0.1.0/src/stateforge/_registry.py +209 -0
  60. stateforge-0.1.0/src/stateforge/_sentinel.py +12 -0
  61. stateforge-0.1.0/src/stateforge/errors.py +15 -0
  62. stateforge-0.1.0/src/stateforge/py.typed +0 -0
  63. stateforge-0.1.0/tests/__init__.py +0 -0
  64. stateforge-0.1.0/tests/conftest.py +56 -0
  65. stateforge-0.1.0/tests/test_decorators.py +115 -0
  66. stateforge-0.1.0/tests/test_errors.py +96 -0
  67. stateforge-0.1.0/tests/test_registry.py +329 -0
  68. stateforge-0.1.0/tests/test_state_machine.py +1091 -0
  69. stateforge-0.1.0/tests/test_version.py +42 -0
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ ci:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ fail-fast: false
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: astral-sh/setup-uv@v7
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ enable-cache: true
24
+
25
+ - name: Install dependencies
26
+ run: uv sync --locked --dev
27
+
28
+ - name: Lint (ruff)
29
+ run: uv run ruff check .
30
+
31
+ - name: mypy strict (src)
32
+ run: uv run mypy --strict src/
33
+
34
+ - name: mypy strict (examples)
35
+ run: uv run mypy --strict examples/
36
+
37
+ - name: pyright strict
38
+ run: uv run pyright
39
+
40
+ - name: pytest with coverage
41
+ run: uv run pytest
@@ -0,0 +1,41 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ environment:
12
+ name: pypi
13
+ permissions:
14
+ id-token: write
15
+ contents: read
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: astral-sh/setup-uv@v7
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: Build
24
+ run: uv build
25
+
26
+ - name: Smoke test
27
+ run: |
28
+ WHL=$(ls dist/*.whl)
29
+ uv run --isolated --no-project --with "$WHL" python -c "
30
+ import stateforge, pathlib, importlib.metadata
31
+ pkg_path = pathlib.Path(stateforge.__file__).parent
32
+ assert (pkg_path / 'py.typed').exists(), 'py.typed missing from installed wheel'
33
+ ver = importlib.metadata.version('stateforge')
34
+ assert ver, 'version empty'
35
+ requires = importlib.metadata.distribution('stateforge').metadata.get_all('Requires-Dist') or []
36
+ assert requires == [], f'unexpected runtime deps: {requires}'
37
+ print(f'Smoke test passed: version={ver}, py.typed=present, deps={requires}')
38
+ "
39
+
40
+ - name: Publish to PyPI
41
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,72 @@
1
+ # stateforge
2
+
3
+ ## What This Is
4
+
5
+ A type-safe, async-native Python state machine library where your type hints are your state machine. Invalid transitions are caught by mypy/pyright at development time, not at runtime. Async is the default execution model — not bolted on. Designed for Python developers building async applications who want state machine correctness enforced by the type system.
6
+
7
+ ## Core Value
8
+
9
+ Type hints define and enforce the state machine — if it type-checks, it's a valid state machine; if it doesn't, you find out before running a single line.
10
+
11
+ ## Requirements
12
+
13
+ ### Validated
14
+
15
+ (None yet — ship to validate)
16
+
17
+ ### Active
18
+
19
+ - [ ] Define states and events as plain Python enums
20
+ - [ ] `StateMachine[S, E]` generic base class parameterized by state and event enums
21
+ - [ ] `@transition` decorator declaring `(from_, on, to)` edges on async methods
22
+ - [ ] `send(event)` as primary interface for triggering transitions
23
+ - [ ] Guards: async callables that gate transitions, raising `GuardRejectedError` on rejection
24
+ - [ ] Multiple source states via list in `from_`
25
+ - [ ] Wildcard source state via `ANY` sentinel
26
+ - [ ] Typed context as optional third generic parameter `StateMachine[S, E, C]`
27
+ - [ ] Event payloads passed as kwargs to `send()` and received as typed method parameters
28
+ - [ ] Lifecycle hooks: `on_enter_state`, `on_exit_state`, `on_transition`
29
+ - [ ] Fail-fast validation at class definition time (duplicate transitions, invalid states/events, etc.)
30
+ - [ ] Clear error hierarchy: `MachineDefinitionError`, `InvalidTransitionError`, `GuardRejectedError`
31
+ - [ ] PEP 561 `py.typed` marker for downstream type checking
32
+ - [ ] Zero third-party dependencies for core library
33
+ - [ ] PyPI distribution with `pyproject.toml`
34
+ - [ ] Validated by building a non-trivial example project that exercises the full API
35
+
36
+ ### Out of Scope
37
+
38
+ - Sync `send_sync()` variant — users use `asyncio.run()` at boundary; add later if demand is clear
39
+ - Hierarchical/nested states — significant complexity, defer to future version
40
+ - Parallel/orthogonal states — defer to future version
41
+ - Per-state typed `send()` (mypy plugin) — stretch goal, not blocking v1
42
+ - Observability integrations (OpenTelemetry, structlog) — future optional extras
43
+ - Docs site — README with examples is sufficient for v1
44
+ - Metaclass-based registration — use `__init_subclass__` only
45
+
46
+ ## Context
47
+
48
+ - Inspired by XState (design philosophy), Boost.SML (types-as-constraints mindset), and C++20 qlibs/sml (function signatures as DSL)
49
+ - Python's typing ecosystem (3.10+) is mature enough to express state machine constraints via generics and decorators
50
+ - No existing Python state machine library prioritizes static type safety as the core value proposition
51
+ - Key design constraint: `@transition` decorated methods must remain normal methods — callable directly in tests without going through `send()`
52
+
53
+ ## Constraints
54
+
55
+ - **Python version**: 3.10+ — enables `X | Y` union syntax and modern typing features
56
+ - **Dependencies**: Zero for core — observability/integrations are optional extras
57
+ - **Async-only**: All transition methods and hooks are async; no sync API
58
+ - **No metaclasses**: `__init_subclass__` for transition collection — simpler, more debuggable
59
+ - **Deterministic FSM**: No two transitions may share the same `(from_, on)` pair
60
+
61
+ ## Key Decisions
62
+
63
+ | Decision | Rationale | Outcome |
64
+ |----------|-----------|---------|
65
+ | Async-only, no sync variant | Simplicity; async is the native model; sync users can use asyncio.run() | — Pending |
66
+ | `__init_subclass__` over metaclass | Simpler, more debuggable, doesn't surprise users | — Pending |
67
+ | Decorated methods are still callable methods | Enables direct testing without send(); no magic wrapping | — Pending |
68
+ | Guards raise by default (not silently ignore) | Fail-fast philosophy; silent failures hide bugs | — Pending |
69
+ | Plain enums, no magic base classes | Mypy understands them natively; minimal learning curve | — Pending |
70
+
71
+ ---
72
+ *Last updated: 2026-03-13 after initialization*
@@ -0,0 +1,153 @@
1
+ # Requirements: stateforge
2
+
3
+ **Defined:** 2026-03-13
4
+ **Core Value:** Type hints define and enforce the state machine — if it type-checks, it's a valid state machine
5
+
6
+ ## v1 Requirements
7
+
8
+ Requirements for initial release. Each maps to roadmap phases.
9
+
10
+ ### Core Machine
11
+
12
+ - [x] **CORE-01**: User can define states as plain Python enum members
13
+ - [x] **CORE-02**: User can define events as plain Python enum members
14
+ - [x] **CORE-03**: User can create a state machine by subclassing `StateMachine[S, E]` with state and event enum type parameters
15
+ - [x] **CORE-04**: User can declare transitions via `@transition(from_=State, on=Event, to=State)` on async methods
16
+ - [x] **CORE-05**: User can trigger transitions via `await machine.send(event)`
17
+ - [x] **CORE-06**: Machine raises `InvalidTransitionError` when no transition matches `(current_state, event)`
18
+ - [x] **CORE-07**: Machine validates all transitions at class definition time, raising `MachineDefinitionError` for invalid state/event references or duplicate `(from_, on)` pairs
19
+ - [x] **CORE-08**: `send()` is typed as `async def send(self, event: E, **kwargs) -> None` so mypy/pyright catches wrong event enum usage
20
+ - [x] **CORE-09**: `@transition` decorated methods remain directly callable as normal async methods (no wrapping)
21
+ - [x] **CORE-10**: `initial_state` class attribute sets the starting state and is validated as a member of the state enum
22
+
23
+ ### Guards
24
+
25
+ - [x] **GUARD-01**: User can attach an async guard callable to a transition via `guard=` parameter
26
+ - [x] **GUARD-02**: Guard receives the machine instance for context inspection
27
+ - [x] **GUARD-03**: Machine raises `GuardRejectedError` (subclass of `InvalidTransitionError`) when guard returns `False`
28
+
29
+ ### Transitions
30
+
31
+ - [x] **TRANS-01**: User can specify multiple source states via `from_=[State.A, State.B]` list
32
+ - [x] **TRANS-02**: User can specify wildcard source state via `from_=ANY` for transitions valid from any state
33
+ - [x] **TRANS-03**: User can pass typed kwargs to `send(event, key=value)` that are forwarded to the transition handler
34
+ - [x] **TRANS-04**: Transition handler parameters are optional — unmatched kwargs are silently ignored
35
+
36
+ ### Context
37
+
38
+ - [x] **CTX-01**: User can define typed context via `StateMachine[S, E, C]` with a third generic parameter
39
+ - [x] **CTX-02**: Context is accessible as `self.context` within transition methods and guards
40
+ - [x] **CTX-03**: Context type parameter is optional — omitting it defaults context to `None`
41
+
42
+ ### Lifecycle Hooks
43
+
44
+ - [x] **HOOK-01**: User can override `on_enter_state(state)` to run logic when any state is entered
45
+ - [x] **HOOK-02**: User can override `on_exit_state(state)` to run logic when any state is exited
46
+ - [x] **HOOK-03**: User can override `on_transition(from_, event, to)` to run logic after any successful transition
47
+
48
+ ### Errors
49
+
50
+ - [x] **ERR-01**: `StateforgeError` is the base exception for all library errors
51
+ - [x] **ERR-02**: `MachineDefinitionError` is raised at class definition time for invalid machine configurations
52
+ - [x] **ERR-03**: `InvalidTransitionError` is raised at runtime when no transition matches
53
+ - [x] **ERR-04**: `GuardRejectedError` is a subclass of `InvalidTransitionError` raised when a guard rejects
54
+
55
+ ### Packaging
56
+
57
+ - [x] **PKG-01**: Library is distributed as a PyPI package installable via `pip install stateforge`
58
+ - [x] **PKG-02**: Package has zero runtime dependencies
59
+ - [x] **PKG-03**: Package includes `py.typed` PEP 561 marker for downstream type checking
60
+ - [x] **PKG-04**: Package uses `pyproject.toml` for build configuration
61
+ - [x] **PKG-05**: Package supports Python 3.10+
62
+
63
+ ### Validation
64
+
65
+ - [x] **VAL-01**: A non-trivial example project exercises the full API (states, events, transitions, guards, context, hooks, payloads)
66
+ - [x] **VAL-02**: Example project type-checks cleanly under both mypy strict and pyright strict
67
+ - [x] **VAL-03**: Test suite covers all core functionality with pytest + pytest-asyncio
68
+
69
+ ## v2 Requirements
70
+
71
+ Deferred to future release. Tracked but not in current roadmap.
72
+
73
+ ### Sync Support
74
+
75
+ - **SYNC-01**: Optional `send_sync()` wrapper for sync-only codebases
76
+
77
+ ### Visualization
78
+
79
+ - **VIZ-01**: Optional `stateforge[viz]` extra for Graphviz/Mermaid diagram export
80
+ - **VIZ-02**: Machine introspection API exposing transition graph programmatically
81
+
82
+ ### Observability
83
+
84
+ - **OBS-01**: Optional `stateforge[otel]` extra for OpenTelemetry spans per transition
85
+ - **OBS-02**: Optional `stateforge[structlog]` extra for structured logging integration
86
+
87
+ ## Out of Scope
88
+
89
+ Explicitly excluded. Documented to prevent scope creep.
90
+
91
+ | Feature | Reason |
92
+ |---------|--------|
93
+ | Hierarchical/nested states | Massively increases complexity; breaks plain generic type model; defer to v2+ |
94
+ | Parallel/orthogonal states | Requires different execution model; `current_state` no longer single value |
95
+ | String-based state/event names | Defeats static type safety goal; enums only |
96
+ | Dynamic `add_transition()` at runtime | Cannot be statically typed; makes `StateMachine[S, E]` meaningless |
97
+ | Auto-generated trigger methods (`machine.melt()`) | The exact feature that breaks static analysis in `transitions` (issue #658) |
98
+ | Metaclass-based registration | Harder to debug, interacts poorly with other metaclasses; use `__init_subclass__` |
99
+ | State persistence/serialization | Out of scope for core FSM library; user serializes `state.value` |
100
+ | Framework integrations (Django, FastAPI) | Contradicts zero-dependency core; future optional extras |
101
+ | SCXML import/export | Only relevant for compound/parallel states which are out of scope |
102
+ | Per-state typed `send()` (mypy plugin) | Stretch goal for v2+; not blocking v1 |
103
+
104
+ ## Traceability
105
+
106
+ Which phases cover which requirements. Updated during roadmap creation.
107
+
108
+ | Requirement | Phase | Status |
109
+ |-------------|-------|--------|
110
+ | CORE-01 | Phase 1 | Complete |
111
+ | CORE-02 | Phase 1 | Complete |
112
+ | CORE-03 | Phase 1 | Complete |
113
+ | CORE-04 | Phase 1 | Complete |
114
+ | CORE-05 | Phase 1 | Complete |
115
+ | CORE-06 | Phase 1 | Complete |
116
+ | CORE-07 | Phase 1 | Complete |
117
+ | CORE-08 | Phase 1 | Complete |
118
+ | CORE-09 | Phase 1 | Complete |
119
+ | CORE-10 | Phase 1 | Complete |
120
+ | ERR-01 | Phase 1 | Complete |
121
+ | ERR-02 | Phase 1 | Complete |
122
+ | ERR-03 | Phase 1 | Complete |
123
+ | ERR-04 | Phase 1 | Complete |
124
+ | GUARD-01 | Phase 2 | Complete |
125
+ | GUARD-02 | Phase 2 | Complete |
126
+ | GUARD-03 | Phase 2 | Complete |
127
+ | TRANS-01 | Phase 2 | Complete |
128
+ | TRANS-02 | Phase 2 | Complete |
129
+ | TRANS-03 | Phase 2 | Complete |
130
+ | TRANS-04 | Phase 2 | Complete |
131
+ | CTX-01 | Phase 2 | Complete |
132
+ | CTX-02 | Phase 2 | Complete |
133
+ | CTX-03 | Phase 2 | Complete |
134
+ | HOOK-01 | Phase 2 | Complete |
135
+ | HOOK-02 | Phase 2 | Complete |
136
+ | HOOK-03 | Phase 2 | Complete |
137
+ | VAL-01 | Phase 3 | Complete |
138
+ | VAL-02 | Phase 3 | Complete |
139
+ | VAL-03 | Phase 3 | Complete |
140
+ | PKG-01 | Phase 4 | Complete |
141
+ | PKG-02 | Phase 4 | Complete |
142
+ | PKG-03 | Phase 4 | Complete |
143
+ | PKG-04 | Phase 4 | Complete |
144
+ | PKG-05 | Phase 4 | Complete |
145
+
146
+ **Coverage:**
147
+ - v1 requirements: 35 total
148
+ - Mapped to phases: 35
149
+ - Unmapped: 0
150
+
151
+ ---
152
+ *Requirements defined: 2026-03-13*
153
+ *Last updated: 2026-03-13 after roadmap creation*
@@ -0,0 +1,93 @@
1
+ # Roadmap: stateforge
2
+
3
+ ## Overview
4
+
5
+ stateforge ships in four phases that follow the natural build order of the library. Phase 1 establishes the core generic base and error hierarchy — all critical pitfalls are concentrated here. Phase 2 layers on every feature extension (guards, multi-source transitions, typed context, lifecycle hooks, event payloads) once the foundation is proven sound. Phase 3 validates the complete implementation with a non-trivial example project and CI pipeline running both type checkers. Phase 4 packages and publishes to PyPI, completing the v1 release.
6
+
7
+ ## Phases
8
+
9
+ **Phase Numbering:**
10
+ - Integer phases (1, 2, 3): Planned milestone work
11
+ - Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
12
+
13
+ Decimal phases appear between their surrounding integers in numeric order.
14
+
15
+ - [x] **Phase 1: Core Foundation** - Working `StateMachine[S, E]` base with async `send()`, `@transition` decorator, fail-fast validation, and full error hierarchy (completed 2026-03-16)
16
+ - [x] **Phase 2: Full Feature Parity** - Guards, typed context, multi-source transitions, `ANY` sentinel, event payloads, and lifecycle hooks (completed 2026-03-16)
17
+ - [x] **Phase 3: Validation** - Non-trivial example project, test suite with 100% coverage, and CI running both mypy strict and pyright strict (completed 2026-03-16)
18
+ - [x] **Phase 4: Packaging and Release** - PyPI package with `py.typed`, `pyproject.toml`, zero-dependency verification, and 0.1.0 release (completed 2026-03-16)
19
+
20
+ ## Phase Details
21
+
22
+ ### Phase 1: Core Foundation
23
+ **Goal**: Developers can define and run a type-safe async state machine using `StateMachine[S, E]` with enum states and events, `@transition` decorated methods, and `send()`
24
+ **Depends on**: Nothing (first phase)
25
+ **Requirements**: CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, ERR-01, ERR-02, ERR-03, ERR-04
26
+ **Success Criteria** (what must be TRUE):
27
+ 1. Developer can subclass `StateMachine[S, E]` with plain Python enums and declare transitions via `@transition(from_=..., on=..., to=...)` on async methods
28
+ 2. `await machine.send(event)` drives the machine to the next state; `InvalidTransitionError` is raised for unmatched `(state, event)` pairs
29
+ 3. Duplicate or invalid transitions raise `MachineDefinitionError` at class definition time — before any instance is created
30
+ 4. `@transition` decorated methods remain directly callable as ordinary async methods with their original signatures intact
31
+ 5. mypy and pyright reject `machine.send(WrongEventEnum.value)` at type-check time without any runtime check
32
+ **Plans:** 3/3 plans complete
33
+
34
+ Plans:
35
+ - [x] 01-01-PLAN.md — Project scaffold, toolchain setup (uv, ruff, mypy, pyright, pytest-asyncio), src/ layout
36
+ - [x] 01-02-PLAN.md — Error hierarchy (_errors.py), @transition decorator (_decorators.py), TransitionRegistry (_registry.py)
37
+ - [x] 01-03-PLAN.md — StateMachine[S, E] base class (_core.py) with __init_subclass__ validation, send(), initial state, __init__.py public surface
38
+
39
+ ### Phase 2: Full Feature Parity
40
+ **Goal**: Developers can use the complete stateforge API — guards, typed context, `ANY` sentinel, multi-source transitions, event payload kwargs, and all lifecycle hooks
41
+ **Depends on**: Phase 1
42
+ **Requirements**: GUARD-01, GUARD-02, GUARD-03, TRANS-01, TRANS-02, TRANS-03, TRANS-04, CTX-01, CTX-02, CTX-03, HOOK-01, HOOK-02, HOOK-03
43
+ **Success Criteria** (what must be TRUE):
44
+ 1. Developer can attach an async guard to a transition; the machine raises `GuardRejectedError` when the guard returns `False`
45
+ 2. Developer can write `StateMachine[S, E, C]` with a typed context accessible as `self.context` inside transition methods and guards
46
+ 3. Developer can use `from_=[State.A, State.B]` or `from_=ANY` to declare transitions valid from multiple or all states
47
+ 4. Developer can pass typed kwargs to `send(event, key=value)` that the transition handler receives; unmatched kwargs are ignored without error
48
+ 5. Developer can override `on_enter_state`, `on_exit_state`, and `on_transition` to run logic during the async transition chain
49
+ **Plans:** 3/3 plans complete
50
+
51
+ Plans:
52
+ - [ ] 02-01-PLAN.md — Guards (guard= on @transition, GuardRejectedError.guard_name), ANY sentinel module, multi-source from_ expansion in registry
53
+ - [ ] 02-02-PLAN.md — Typed context third generic parameter (StateMachine[S, E, C]), frozen dataclass enforcement, kwargs filtering for handlers
54
+ - [ ] 02-03-PLAN.md — Lifecycle hooks (on_enter_state, on_exit_state, on_transition), full 6-step send() chain integrating all features
55
+
56
+ ### Phase 3: Validation
57
+ **Goal**: The complete API is exercised by a non-trivial example that type-checks cleanly under both mypy strict and pyright strict, and a pytest suite achieves 100% coverage
58
+ **Depends on**: Phase 2
59
+ **Requirements**: VAL-01, VAL-02, VAL-03
60
+ **Success Criteria** (what must be TRUE):
61
+ 1. A non-trivial example machine (e.g., order workflow) exercises states, events, guards, context, hooks, and payload kwargs and runs correctly end-to-end
62
+ 2. `mypy --strict` and `pyright --strict` both pass on the example and the library source with zero errors
63
+ 3. `pytest` with `pytest-asyncio` achieves 100% line coverage across all library modules
64
+ **Plans:** 1/1 plans complete
65
+
66
+ Plans:
67
+ - [ ] 03-01-PLAN.md — Example order workflow, pyproject.toml config (branch coverage + pyright include), branch coverage regression test, GitHub Actions CI pipeline, uat_phase2.py cleanup
68
+
69
+ ### Phase 4: Packaging and Release
70
+ **Goal**: `pip install stateforge` works, the installed package carries `py.typed`, has zero runtime dependencies, and version 0.1.0 is published on PyPI
71
+ **Depends on**: Phase 3
72
+ **Requirements**: PKG-01, PKG-02, PKG-03, PKG-04, PKG-05
73
+ **Success Criteria** (what must be TRUE):
74
+ 1. `pip install stateforge` succeeds on Python 3.10, 3.11, 3.12, and 3.13 with no additional dependencies installed
75
+ 2. The installed package contains a `py.typed` marker and downstream type checkers (mypy, pyright) infer types from stateforge without any `# type: ignore` workarounds
76
+ 3. `pip show stateforge` shows zero `Requires` entries
77
+ **Plans:** 2/2 plans complete
78
+
79
+ Plans:
80
+ - [x] 04-01-PLAN.md — Package metadata (pyproject.toml enrichment, LICENSE, __version__, README)
81
+ - [x] 04-02-PLAN.md — Release workflow (GitHub Actions tag-triggered build, smoke test, OIDC publish to PyPI)
82
+
83
+ ## Progress
84
+
85
+ **Execution Order:**
86
+ Phases execute in numeric order: 1 → 2 → 3 → 4
87
+
88
+ | Phase | Plans Complete | Status | Completed |
89
+ |-------|----------------|--------|-----------|
90
+ | 1. Core Foundation | 3/3 | Complete | 2026-03-16 |
91
+ | 2. Full Feature Parity | 3/3 | Complete | 2026-03-16 |
92
+ | 3. Validation | 1/1 | Complete | 2026-03-16 |
93
+ | 4. Packaging and Release | 2/2 | Complete | 2026-03-16 |
@@ -0,0 +1,118 @@
1
+ ---
2
+ gsd_state_version: 1.0
3
+ milestone: v1.0
4
+ milestone_name: milestone
5
+ status: completed
6
+ stopped_at: Completed 04-packaging-and-release 04-02-PLAN.md
7
+ last_updated: "2026-03-16T17:29:23Z"
8
+ last_activity: 2026-03-16 — Plan 04-02 complete (GitHub Actions release workflow, OIDC trusted publisher)
9
+ progress:
10
+ total_phases: 4
11
+ completed_phases: 4
12
+ total_plans: 9
13
+ completed_plans: 9
14
+ percent: 100
15
+ ---
16
+
17
+ # Project State
18
+
19
+ ## Project Reference
20
+
21
+ See: .planning/PROJECT.md (updated 2026-03-13)
22
+
23
+ **Core value:** Type hints define and enforce the state machine — if it type-checks, it's a valid state machine
24
+ **Current focus:** Phase 2 — Full Feature Parity
25
+
26
+ ## Current Position
27
+
28
+ Phase: 4 of 4 (Packaging and Release)
29
+ Plan: 2 of 2 in current phase — PHASE COMPLETE
30
+ Status: All 4 phases complete — PROJECT COMPLETE
31
+ Last activity: 2026-03-16 — Plan 04-02 complete (GitHub Actions release workflow, OIDC trusted publisher)
32
+
33
+ Progress: [██████████] 100%
34
+
35
+ ## Performance Metrics
36
+
37
+ **Velocity:**
38
+ - Total plans completed: 0
39
+ - Average duration: -
40
+ - Total execution time: 0 hours
41
+
42
+ **By Phase:**
43
+
44
+ | Phase | Plans | Total | Avg/Plan |
45
+ |-------|-------|-------|----------|
46
+ | - | - | - | - |
47
+
48
+ **Recent Trend:**
49
+ - Last 5 plans: none yet
50
+ - Trend: -
51
+
52
+ *Updated after each plan completion*
53
+
54
+ | Phase | Duration | Tasks | Files |
55
+ |-------|----------|-------|-------|
56
+ | Phase 01-core-foundation P01 | 2min | 2 tasks | 5 files |
57
+ | Phase 01-core-foundation P02 | 4min | 2 tasks | 6 files |
58
+ | Phase 01-core-foundation P03 | 3min | 2 tasks | 6 files |
59
+ | Phase 02-full-feature-parity P01 | 7min | 2 tasks | 8 files |
60
+ | Phase 02-full-feature-parity P02 | 45 | 2 tasks | 3 files |
61
+ | Phase 02-full-feature-parity P03 | 25min | 2 tasks | 2 files |
62
+ | Phase 03-validation P01 | 9min | 2 tasks | 4 files |
63
+ | Phase 04-packaging-and-release P01 | 4min | 2 tasks | 5 files |
64
+ | Phase 04-packaging-and-release P02 | 5min | 2 tasks | 1 file |
65
+
66
+ ## Accumulated Context
67
+
68
+ ### Decisions
69
+
70
+ Decisions are logged in PROJECT.md Key Decisions table.
71
+ Recent decisions affecting current work:
72
+
73
+ - All transitions: `@transition` must NOT wrap the function — attach metadata as attribute, return original callable unchanged
74
+ - All transitions: Async-only; no sync API; guards must be `async def`
75
+ - Phase 1 risk: `asyncio.Lock` reentrancy deadlock — use `_processing: bool` flag instead of a lock
76
+ - Phase 1 risk: `__init_subclass__` fires on base class — guard with `if cls is StateMachine: return`
77
+ - Phase 1–2 open: `__aenter__`/`__aexit__` vs deferred-hook pattern for initial state activation — decide in Phase 1 before hooks in Phase 2
78
+ - [Phase 01-core-foundation]: hatchling chosen as build backend for maximum compatibility
79
+ - [Phase 01-core-foundation]: both mypy and pyright run in strict mode for dual type checker enforcement
80
+ - [Phase 01-core-foundation]: ruff rules E/F/I/UP/ANN for errors, imports, modernization, annotations
81
+ - [Phase 01-core-foundation]: noqa: ANN401 on Any-typed params for generic enum-agnostic APIs in _errors.py, _decorators.py, _registry.py
82
+ - [Phase 01-core-foundation]: TransitionRegistry.build() returns (registry, errors) tuple — caller raises MachineDefinitionError if non-empty
83
+ - [Phase 01-core-foundation]: inspect.iscoroutinefunction replaces deprecated asyncio.iscoroutinefunction for Python 3.14+ compatibility
84
+ - [Phase 01-core-foundation]: _processing: bool flag for reentrancy protection (not asyncio.Lock) to avoid deadlock on re-entrant send()
85
+ - [Phase 01-core-foundation]: stateforge.errors public re-export module for stable import surface independent of internal _errors.py
86
+ - [Phase 01-core-foundation]: __init_subclass__ skips TypeVar type args to allow abstract intermediate classes without triggering validation
87
+ - [Phase 02-full-feature-parity P01]: ANY sentinel in _sentinel.py (not __init__.py) to break circular import between _decorators.py and _registry.py
88
+ - [Phase 02-full-feature-parity P01]: guard stored as tuple[Callable...] (not list) in frozen dataclass TransitionMeta/TransitionEntry for hashability
89
+ - [Phase 02-full-feature-parity P01]: lookup_entry() added alongside lookup() for backward compat; Plan 03 send() uses lookup_entry()
90
+ - [Phase 02-full-feature-parity P01]: from_=list expanded at build() time (not runtime) to keep lookup O(1)
91
+ - [Phase 02-full-feature-parity]: __class_getitem__ normalization only on StateMachine (not subclasses) to preserve abstract intermediate class support
92
+ - [Phase 02-full-feature-parity]: ty rules added to pyproject.toml to suppress invalid-type-arguments (2-arg form intentional) and unused-type-ignore-comment (mypy-only ignores)
93
+ - [Phase 02-full-feature-parity]: Tasks 1 and 2 committed together due to 100% coverage compliance requirement for _filter_kwargs var-keyword path
94
+ - [Phase 02-full-feature-parity P03]: getattr(guard_fn, '__name__', '') instead of guard_fn.__name__ — Callable[..., Any] has no __name__ per ty's type system; getattr is more robust for non-function callables
95
+ - [Phase 02-full-feature-parity P03]: No type: ignore[assignment] needed on self._context = new_context — mypy infers correctly after the non-None check
96
+ - [Phase 03-validation]: Guard checks machine.context.amount at guard time; initial context must have amount > 0 for guard to pass
97
+ - [Phase 03-validation]: examples/ not added to mypy_path (search path only); checked explicitly with uv run mypy --strict examples/
98
+ - [Phase 03-validation]: types.GenericAlias patching of __orig_bases__ works to cover _core.py:54 false branch via synthetic test
99
+ - [Phase 04-packaging-and-release]: importlib.metadata.version() for __version__ — single source of truth stays in pyproject.toml
100
+ - [Phase 04-packaging-and-release]: PackageNotFoundError fallback to 'unknown'; test_version.py re-imports module via sys.modules.pop() + patch to cover 100% branch
101
+ - [Phase 04-packaging-and-release P02]: OIDC trusted publisher (no API tokens) — --trusted-publishing always fails hard if misconfigured
102
+ - [Phase 04-packaging-and-release P02]: WHL=$(ls dist/*.whl) pattern avoids shell glob expansion issues in --with argument
103
+ - [Phase 04-packaging-and-release P02]: No GitHub Release creation — PyPI publish only, per user decision
104
+
105
+ ### Pending Todos
106
+
107
+ None yet.
108
+
109
+ ### Blockers/Concerns
110
+
111
+ - Phase 1: TypeVar variance on `Generic[S, E, C]` — RESOLVED: all three invariant; mypy strict + pyright strict both pass
112
+ - Phase 1–2: Whether v1 accepts sync guards (needs `inspect.iscoroutinefunction` check) or requires all guards to be `async def` — unresolved per research
113
+
114
+ ## Session Continuity
115
+
116
+ Last session: 2026-03-16T17:29:23Z
117
+ Stopped at: Completed 04-packaging-and-release 04-02-PLAN.md
118
+ Resume file: None
@@ -0,0 +1,13 @@
1
+ {
2
+ "mode": "yolo",
3
+ "granularity": "coarse",
4
+ "parallelization": true,
5
+ "commit_docs": true,
6
+ "model_profile": "balanced",
7
+ "workflow": {
8
+ "research": true,
9
+ "plan_check": true,
10
+ "verifier": true,
11
+ "nyquist_validation": true
12
+ }
13
+ }