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.
- stateforge-0.1.0/.github/workflows/ci.yml +41 -0
- stateforge-0.1.0/.github/workflows/release.yml +41 -0
- stateforge-0.1.0/.planning/PROJECT.md +72 -0
- stateforge-0.1.0/.planning/REQUIREMENTS.md +153 -0
- stateforge-0.1.0/.planning/ROADMAP.md +93 -0
- stateforge-0.1.0/.planning/STATE.md +118 -0
- stateforge-0.1.0/.planning/config.json +13 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-01-PLAN.md +167 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-01-SUMMARY.md +137 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-02-PLAN.md +194 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-02-SUMMARY.md +139 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-03-PLAN.md +345 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-03-SUMMARY.md +164 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-CONTEXT.md +81 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-RESEARCH.md +713 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-UAT.md +56 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-VALIDATION.md +89 -0
- stateforge-0.1.0/.planning/phases/01-core-foundation/01-VERIFICATION.md +180 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-01-PLAN.md +265 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-01-SUMMARY.md +145 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-02-PLAN.md +281 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-02-SUMMARY.md +155 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-03-PLAN.md +318 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-03-SUMMARY.md +152 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-CONTEXT.md +104 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-RESEARCH.md +653 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-UAT.md +65 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-VALIDATION.md +80 -0
- stateforge-0.1.0/.planning/phases/02-full-feature-parity/02-VERIFICATION.md +122 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-01-PLAN.md +248 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-01-SUMMARY.md +151 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-CONTEXT.md +88 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-RESEARCH.md +573 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-UAT.md +45 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-VALIDATION.md +79 -0
- stateforge-0.1.0/.planning/phases/03-validation/03-VERIFICATION.md +101 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-01-PLAN.md +189 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-01-SUMMARY.md +144 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-02-PLAN.md +206 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-02-SUMMARY.md +111 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-CONTEXT.md +85 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-RESEARCH.md +501 -0
- stateforge-0.1.0/.planning/phases/04-packaging-and-release/04-VALIDATION.md +77 -0
- stateforge-0.1.0/.planning/research/ARCHITECTURE.md +345 -0
- stateforge-0.1.0/.planning/research/FEATURES.md +210 -0
- stateforge-0.1.0/.planning/research/PITFALLS.md +297 -0
- stateforge-0.1.0/.planning/research/STACK.md +221 -0
- stateforge-0.1.0/.planning/research/SUMMARY.md +195 -0
- stateforge-0.1.0/.pre-commit-config.yaml +36 -0
- stateforge-0.1.0/LICENSE +193 -0
- stateforge-0.1.0/PKG-INFO +94 -0
- stateforge-0.1.0/README.md +70 -0
- stateforge-0.1.0/examples/order_workflow.py +168 -0
- stateforge-0.1.0/pyproject.toml +86 -0
- stateforge-0.1.0/src/stateforge/__init__.py +14 -0
- stateforge-0.1.0/src/stateforge/_core.py +326 -0
- stateforge-0.1.0/src/stateforge/_decorators.py +67 -0
- stateforge-0.1.0/src/stateforge/_errors.py +79 -0
- stateforge-0.1.0/src/stateforge/_registry.py +209 -0
- stateforge-0.1.0/src/stateforge/_sentinel.py +12 -0
- stateforge-0.1.0/src/stateforge/errors.py +15 -0
- stateforge-0.1.0/src/stateforge/py.typed +0 -0
- stateforge-0.1.0/tests/__init__.py +0 -0
- stateforge-0.1.0/tests/conftest.py +56 -0
- stateforge-0.1.0/tests/test_decorators.py +115 -0
- stateforge-0.1.0/tests/test_errors.py +96 -0
- stateforge-0.1.0/tests/test_registry.py +329 -0
- stateforge-0.1.0/tests/test_state_machine.py +1091 -0
- 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
|