statespec
Declarative lifecycle policy for entities whose status moves through reviewable steps.
A spec is plain data — states, named transitions (roles + guards), invariants — and the
same artifact is human-readable (Mermaid / plain-English table), machine-checkable
(validate), enforced through a single interpreter (apply / fire), and identified
by content digests (identity) so that behavioural change is mechanically
distinguishable from wording change.
statespec.core— the engine:StateSpec,Transition,Invariant,apply,fire,validate.statespec.expr— closed guard/invariant expression grammar (compare + boolean), with a versionedOpaqueescape hatch whose body is hashed into the policy identity.statespec.identity—canonical(),semantic_digest/presentation_digest,change_kind,diff, andpolicy_record()(the versioned.policy.jsonenvelope — seedocs/policy-artifact-contract.md, the authoritative copy of the contract).statespec.render— Mermaid, tables, and the generated lifecycle doc.
Pure Python, stdlib-only, no dependencies.
Quick start
pip install statespec
from statespec import StateSpec, Transition, apply, validate
from statespec.expr import field
spec = StateSpec(
name="review", title="Review",
states={"draft": "Being written", "submitted": "Awaiting review",
"approved": "Done"},
fields={"amount": "int"},
initial="draft", terminal=frozenset({"approved"}),
transitions=(
Transition("submit", ("draft",), "submitted",
roles=frozenset({"author"})),
Transition("approve", ("submitted",), "approved",
roles=frozenset({"reviewer"}),
guard=field("amount").le(100)),
),
)
assert validate(spec, known_roles=frozenset({"author", "reviewer"})) == []
apply(spec, "approve", "submitted", frozenset({"reviewer"}), {"amount": 40})
# -> "approved". Wrong role -> PermissionDenied; amount 500 -> GuardRejected;
# fire() returns the same decision plus structured evidence per condition.
Every claim above is exercised by the test suite: the refusal taxonomy and
its precedence in tests/test_core.py, the expression grammar and
typechecking in tests/test_expr.py, digests/diff in tests/test_identity.py,
and canonicalisation byte-stability against a golden artifact in
tests/test_contract_golden.py.
Boundaries (what this does not do)
statespec sits above concurrency and persistence. It decides whether a
transition is legal; it does not make the write atomic or durable. It will
not stop a race between two concurrent transitions (pair it with optimistic
locking or a version column at the persistence layer), a forgotten lock, an
out-of-band mutation that sets status around the interpreter, or a
cross-system inconsistency. The "single enforcement path" property holds
only if every status write in the application routes through apply/fire
— enforce that with a repo-level convention or CI gate, not by trust. The
digests identify policy change; they do not verify that deployed code
matches the artifact — that check belongs to CI or a control plane.
Provenance
Extracted from ConceptPending/baseplate
branch example/state-machine at tag statespec-v0.3.0 (commit 2aca24e, 2026-06-10),
where the kernel was developed and frozen. The only changes at extraction were the import
rename app.statespec → statespec and doc-path adjustments. This repository is the
kernel's authoritative home from statespec-v0.4.0 onward; the in-tree copy in baseplate
remains frozen at v0.3.0 as part of the reference example.
Versioning and freeze discipline
Kernel tags form their own series (statespec-vX.Y.Z), separate from any consuming
product's versions. The canonicalisation and digest rules are a frozen contract:
any change to the policy-artifact envelope bumps schema_version and the kernel tag
together, and is recorded in docs/policy-artifact-contract.md's changelog. Changes
that alter existing digests are breaking and require explicit re-binding by consumers
(e.g. approval planes). Exceptions to the freeze: genuine bugs, security issues, or
contract defects.
Known consumers: baseplate (reference example, v0.3.0), baseplate-control
(approval plane; vendors this kernel byte-identically and verifies digests independently).
Development
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
.venv/bin/ruff check src tests
Release files for statespec 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| statespec-0.4.1.tar.gz | 34.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| statespec-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.8 kB
Release files / statespec-0.4.1.tar.gz
| Download URL | statespec-0.4.1.tar.gz |
|---|---|
| Size | 34.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4055cfb4577f324ddfc7aa3c7d6619b2aef88ba8533bc284a21f138d68804a01
|
|
BLAKE2b-256 checksum How to use checksums |
c2e397bb84eaa7955ba4e904db0e869dbd66b6b495ee0fb5e31f536a330346b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / statespec-0.4.1-py3-none-any.whl
| Download URL | statespec-0.4.1-py3-none-any.whl |
|---|---|
| Size | 20.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d85cd920e95d29cfe5ff9d73b571c7b120eba1363308d1ab03a4205c8607fa6e
|
|
BLAKE2b-256 checksum How to use checksums |
b5a5e6324f5c87367cb82730470357a987281f424d39ab413d539e71fb121f57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|