Skip to content

Commit 600d870

Browse files
committed
feat: Posture state machine enum
Closed StrEnum with four members pinned by docs/concepts/posture-and-hooks.md: INTERACTIVE / AUTONOMOUS / DRY_RUN / LOCKED. Added to spine_lite.__all__ under the Phase 2 sub-option (a) decision recorded in RECEIPTS.md. Phase 3 will add the transition functions; the enum is what the manifest schema validates against in commit 3.
1 parent 111f34c commit 600d870

4 files changed

Lines changed: 93 additions & 9 deletions

File tree

docs/reference/api.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,21 @@ from spine_lite import (
4040
- PostureError
4141
- HookError
4242

43+
## Posture
44+
45+
::: spine_lite.posture
46+
options:
47+
members:
48+
- Posture
49+
4350
## Phase 2 / Phase 3 modules
4451

45-
Stub modules with phase-pinning docstrings. The reference will expand as the implementations land.
52+
Stubs and partials with phase-pinning docstrings. The reference expands as implementations land.
4653

4754
::: spine_lite.manifest
4855

4956
::: spine_lite.classifier
5057

51-
::: spine_lite.posture
52-
5358
::: spine_lite.receipt
5459

5560
::: spine_lite.hook

src/spine_lite/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""spine-lite: deterministic policy and effects runtime for LLM tool calls.
22
3-
Phase 1 surface. Subsequent phases extend ``__all__`` as the classifier,
4-
posture machine, and hook ship. See ``CLAUDE.md`` and ``docs/architecture.md``.
3+
Phase 2 surface. Subsequent phases extend ``__all__`` as the classifier and
4+
hook ship. See ``CLAUDE.md`` and ``docs/explanation/architecture.md``.
55
"""
66

77
from __future__ import annotations
@@ -16,6 +16,7 @@
1616
PostureError,
1717
SpineLiteError,
1818
)
19+
from spine_lite.posture import Posture
1920

2021
__version__ = "0.1.0a0"
2122

@@ -25,6 +26,7 @@
2526
"Effect",
2627
"HookError",
2728
"ManifestError",
29+
"Posture",
2830
"PostureError",
2931
"SpineLiteError",
3032
"__version__",

src/spine_lite/posture.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1-
"""Posture state machine (Phase 3).
1+
"""Posture state machine.
22
3-
A pure transition function over a closed ``Posture`` enum. No hidden state.
4-
Every transition is a value-in-value-out function. See
5-
``docs/architecture.md`` for the state diagram.
3+
Phase 2 ships the closed :class:`Posture` enum used by manifest validation.
4+
Phase 3 will add the transition functions (pure value-in-value-out).
65
76
Pure module: deterministic, no I/O.
87
"""
98

109
from __future__ import annotations
10+
11+
from enum import StrEnum
12+
13+
14+
class Posture(StrEnum):
15+
"""Operational posture of the runtime.
16+
17+
Drives how the runtime treats ambiguous calls. Closed enum: extending
18+
requires a project-level decision. The members and their string values
19+
are pinned by ``docs/concepts/posture-and-hooks.md``.
20+
21+
Members:
22+
INTERACTIVE: Operator at the keyboard; ambiguous calls escalate.
23+
AUTONOMOUS: No operator in the loop; ambiguous calls fail closed.
24+
DRY_RUN: Classification only; non-``READ`` effects don't fire.
25+
LOCKED: Refuse everything except explicit allow-listed read-only calls.
26+
"""
27+
28+
INTERACTIVE = "interactive"
29+
AUTONOMOUS = "autonomous"
30+
DRY_RUN = "dry_run"
31+
LOCKED = "locked"

tests/unit/test_posture.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for the closed Posture enum."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from spine_lite.posture import Posture
8+
9+
10+
def test_posture_has_exactly_four_members() -> None:
11+
assert len(Posture) == 4
12+
13+
14+
def test_posture_member_names() -> None:
15+
assert {p.name for p in Posture} == {
16+
"INTERACTIVE",
17+
"AUTONOMOUS",
18+
"DRY_RUN",
19+
"LOCKED",
20+
}
21+
22+
23+
@pytest.mark.parametrize(
24+
("member", "value"),
25+
[
26+
(Posture.INTERACTIVE, "interactive"),
27+
(Posture.AUTONOMOUS, "autonomous"),
28+
(Posture.DRY_RUN, "dry_run"),
29+
(Posture.LOCKED, "locked"),
30+
],
31+
)
32+
def test_posture_string_values_are_pinned(member: Posture, value: str) -> None:
33+
assert member.value == value
34+
assert member == value
35+
36+
37+
def test_posture_is_str_subclass() -> None:
38+
assert isinstance(Posture.INTERACTIVE, str)
39+
assert str(Posture.AUTONOMOUS) == "autonomous"
40+
41+
42+
def test_posture_unknown_value_raises() -> None:
43+
with pytest.raises(ValueError, match="not a valid Posture"):
44+
Posture("escalated")
45+
46+
47+
def test_posture_round_trip_through_value() -> None:
48+
for member in Posture:
49+
assert Posture(member.value) is member
50+
51+
52+
def test_posture_is_in_public_api() -> None:
53+
import spine_lite
54+
55+
assert "Posture" in spine_lite.__all__
56+
assert spine_lite.Posture is Posture

0 commit comments

Comments
 (0)