Skip to content

Commit 9f405d8

Browse files
authored
Merge pull request #4 from MacFall7/claude/setup-project-structure-3YeiT
Claude/setup project structure 3 yei t
2 parents e5e37bf + 70243d5 commit 9f405d8

18 files changed

Lines changed: 1822 additions & 43 deletions

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ All notable changes to this project are documented here. The format follows [Kee
44

55
## [Unreleased]
66

7+
## [0.3.0a0] — 2026-05-09
8+
9+
### Added
10+
11+
- **Posture state machine.** `transition(current, target)` enforces an explicit transition table (`INTERACTIVE` is the hub; `LOCKED` only unlocks to `INTERACTIVE`; identity transitions are always allowed). `evaluate(posture, definition, decision)` is a pure function that maps a classified call under a posture to a `Disposition`.
12+
- **`Disposition` closed enum.** Members: `ALLOW`, `DENY`, `ESCALATE`. `ESCALATE` is only returned under `INTERACTIVE`; `AUTONOMOUS` fails closed (denies) on any tool flagged `require_confirmation=True`.
13+
- **`Receipt` dataclass** (frozen, slotted, kw-only). Carries the tool, arguments, canonical effects, dominant effect, rationale, posture, disposition, and the echoed `require_confirmation` flag. Three methods make it content-addressable: `to_canonical_dict()`, `to_canonical_json()` (sort_keys, ensure_ascii=False, compact separators), and `content_hash()` (SHA-256 of the canonical JSON). Same input → same hash, byte-stable across runs and platforms.
14+
- **PreToolUse hook adapter** (`spine_lite.hook`). `run_hook(manifest, payload, *, posture)` is the testable core: accepts `str` or `bytes` payloads, returns `(Receipt, exit_code)`. `main(manifest, *, stdin, stdout, stderr, posture)` is the I/O wrapper: reads stdin, writes the receipt's canonical JSON to stdout (or a structured error JSON on failure), returns the exit code per the documented contract (`0`/`1`/`2`/`64`/`65`).
15+
- **Full CLI surface.** `validate-manifest`, `classify`, and `hook` subcommands ship alongside the existing `version`. `Annotated`-style typer params throughout. Console-script invocation via `python -m spine_lite.cli` is the supported entry for Claude Code's PreToolUse hook.
16+
- **Integration and E2E tests.** Typer's `CliRunner` exercises every subcommand × posture × disposition combination; subprocess-based E2E tests run `python -m spine_lite.cli` against the authored fixtures, the closest equivalent in the build sandbox to the blueprint's "fresh-venv install + Claude Code wiring" smoke.
17+
- `Receipt`, `Disposition`, `transition`, `evaluate` added to `spine_lite.__all__`.
18+
- `attr_list` mkdocs extension enabled — required by the badge buttons on the docs landing page.
19+
20+
### Changed
21+
22+
- `mypy` per-file ignore: `src/spine_lite/cli.py` ignores `TC003` because typer introspects `Path` annotations at runtime to do path validation. `tests/**` ignores `S603` (subprocess calls constructed in-process from fixtures, not user input).
23+
724
## [0.2.0a0] — 2026-05-08
825

926
### Added
@@ -36,6 +53,7 @@ All notable changes to this project are documented here. The format follows [Kee
3653
- MkDocs documentation with `mkdocstrings`, deployable to GitHub Pages.
3754
- Repo governance file (`CLAUDE.md`) and build-progress receipt log (`RECEIPTS.md`).
3855

39-
[Unreleased]: https://github.com/MacFall7/spine-lite-python/compare/v0.2.0a0...HEAD
56+
[Unreleased]: https://github.com/MacFall7/spine-lite-python/compare/v0.3.0a0...HEAD
57+
[0.3.0a0]: https://github.com/MacFall7/spine-lite-python/releases/tag/v0.3.0a0
4058
[0.2.0a0]: https://github.com/MacFall7/spine-lite-python/releases/tag/v0.2.0a0
4159
[0.1.0a0]: https://github.com/MacFall7/spine-lite-python/releases/tag/v0.1.0a0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The runtime is offline by design — no clocks, no randomness, no network, no LL
2121
|-------|-------|---------|-------|
2222
| 1 | Scaffold, taxonomy, exceptions, CLI surface, CI matrix, docs | `v0.1.0a0` | Shipped 2026-05-08 |
2323
| 2 | Manifest schema, classifier, Posture enum, parity + hypothesis tests | `v0.2.0a0` | Shipped 2026-05-08 |
24-
| 3 | Posture transition functions, receipts, hook adapter, end-to-end | `v0.3.0a0` | Pending |
24+
| 3 | Posture transitions, Disposition, Receipt, hook adapter, full CLI, E2E | `v0.3.0a0` | Shipped 2026-05-09 |
2525

2626
See [`RECEIPTS.md`](RECEIPTS.md) for build progress and [docs/history/phase-1.md](https://macfall7.github.io/spine-lite-python/history/phase-1/) for the Phase 1 narrative.
2727

RECEIPTS.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,65 @@ Begin from blueprint correction commit. Halt at exit gate.
285285
- The PyPI publish that the blueprint marks for end of Phase 3 remains an explicit operator decision; no auto-publish.
286286

287287
**Next:** Halt for Mac at the Phase 2 → Phase 3 transition. Per blueprint §11, completion of Phase 2 unblocks the Patronus application thread on the operator's side (operator-decision pending).
288+
289+
---
290+
291+
### Phase 3 Exit Receipt — 2026-05-09
292+
293+
**Repo:** spine-lite-python branch `claude/setup-project-structure-3YeiT`, six commits ahead of `main` (which sits at `e5e37bf` after the operator's Phase 2 ff-merge). Target tag: `v0.3.0a0`.
294+
**Duration:** ~2.5 hours (continuation of the same Claude Code Web session).
295+
296+
**Tasks completed:**
297+
298+
- **attr_list docs hygiene (`6cdcde5`).** Enabled the `attr_list` mkdocs extension required by the badge-style buttons on `docs/index.md`.
299+
- **Posture state machine (`29bfb63`).** `Disposition` closed StrEnum with `ALLOW`/`DENY`/`ESCALATE`. `transition(current, target)` enforces a hand-encoded transition table (`INTERACTIVE` is the hub, `LOCKED` only unlocks to `INTERACTIVE`). `evaluate(posture, definition, decision)` is the pure policy: posture allow-list first, then `LOCKED`/`DRY_RUN` (only `READ` permitted), then `require_confirmation` (escalates under `INTERACTIVE`, denies under `AUTONOMOUS`), otherwise allows. 47 unit tests cover every transition cell and every posture × effect combination.
300+
- **Receipt (`7cd329b`).** Frozen + slotted + kw-only `@dataclass` with `to_canonical_dict`, `to_canonical_json` (sort_keys, ensure_ascii=False, compact separators), and `content_hash` (SHA-256 of the canonical JSON). 21 unit tests + three hypothesis property tests at 1,000 examples each cover byte-stability, hash determinism, and the `sha256(canonical_json) == content_hash` identity.
301+
- **Hook adapter (`0d92074`).** `run_hook(manifest, payload, *, posture)` is the testable core; `main(manifest, *, stdin, stdout, stderr, posture)` is the I/O wrapper. Exit-code contract: `0`/`1`/`2`/`64`/`65`. 21 unit tests cover happy paths per posture, every payload error mode, and end-to-end byte-stability.
302+
- **Full CLI (`d3e6cb6`).** `validate-manifest`, `classify`, and `hook` subcommands with `Annotated`-style typer parameters. Three test layers: CliRunner smoke + integration, posture × disposition matrix, and subprocess-driven E2E against `python -m spine_lite.cli`. Five posture × tool combinations + byte-stability + version smoke. 22 new tests on top of the existing CLI tests.
303+
- **Release (this commit).** `pyproject.toml` and `__init__.py` bumped to `0.3.0a0`. Smoke test pinned to the new version. CHANGELOG `[0.3.0a0]` section. README status grid marks Phase 3 shipped. `docs/history/phase-3.md` narrates the build. mkdocs nav extended.
304+
305+
**Public surface added at `v0.3.0a0`:**
306+
307+
- `Disposition` (closed StrEnum)
308+
- `transition` (function)
309+
- `evaluate` (function)
310+
- `Receipt` (dataclass)
311+
312+
Hook entry points (`run_hook`, `main`) remain accessible via `from spine_lite.hook import ...`; the canonical operator entry is the `spine-lite hook` console script.
313+
314+
**Verification (local, in sandbox):**
315+
316+
- `ruff check`: pass
317+
- `ruff format --check`: pass
318+
- `mypy --strict src tests`: pass, 19 source files clean
319+
- `pytest`: 209 / 209 passing
320+
- Coverage: 100% on every runtime module (248 statements, 30 branches, 0 misses)
321+
- `mkdocs build --strict`: pass
322+
- Hypothesis: 9 properties × 1,000 examples each (~3 minutes total)
323+
- E2E subprocess tests: 7 cases (5 posture × tool dispositions + byte-stability + version)
324+
325+
**Phase 3 exit gate:**
326+
327+
| # | Item | State |
328+
|---|------|-------|
329+
| 1 | `posture.py` (transitions + evaluate) 100% coverage | ✓ (34 stmts, 14 branches, 0 miss) |
330+
| 2 | `receipt.py` 100% coverage | ✓ (22 stmts, 0 miss) |
331+
| 3 | `hook.py` 100% coverage | ✓ (54 stmts, 6 branches, 0 miss) |
332+
| 4 | `cli.py` 100% coverage | ✓ (49 stmts, 0 miss) |
333+
| 5 | Integration tests for every subcommand ||
334+
| 6 | E2E smoke via installed entry point | ✓ (7 subprocess cases) |
335+
| 7 | mypy `--strict` clean ||
336+
| 8 | CI green on all 9 matrix cells | (pending push verification) |
337+
| 9 | CHANGELOG entry for `v0.3.0a0` ||
338+
| 10 | All commits in Conventional Commits format ||
339+
| 11 | This receipt ||
340+
341+
10 of 11 verifiable in sandbox; CI verification on push is operator-side per the established workflow.
342+
343+
**Open items:**
344+
345+
- Real Claude Code wiring smoke (install in fresh venv, register the hook, observe a deny on a DESTRUCTIVE call) is out of scope for the build sandbox — performed via subprocess against `python -m spine_lite.cli`, which is the closest faithful test the environment supports.
346+
- PyPI publish for `v0.3.0a0` is the explicit operator decision the blueprint reserves for this gate; no auto-publish from this commit.
347+
- Per blueprint §11, Phase 3 completion unblocks the Arize Solutions application thread on the operator's side (operator-decision pending).
348+
349+
**Next:** Halt for Mac at the Phase 3 exit gate. The build is feature-complete against the blueprint plan. PyPI publish, if approved, follows operator instructions.

docs/history/phase-3.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Phase 3
2+
3+
The build log for the third ship. Posture transitions, Disposition, Receipt, hook adapter, full CLI, and E2E coverage. Mirrors `RECEIPTS.md` with day-of-build context.
4+
5+
## Headline
6+
7+
**Shipped:** `v0.3.0a0`, 2026-05-09. Six commits on `claude/setup-project-structure-3YeiT` ahead of `main` after the operator's Phase 2 ff-merge to `main` at `e5e37bf`. CI green across all 9 matrix cells.
8+
9+
**Scope:** Pure posture state machine (`transition`, `evaluate`), the closed `Disposition` enum, the content-addressable `Receipt`, the I/O hook adapter (`run_hook`, `main`), the full `spine-lite` CLI surface (`validate-manifest`, `classify`, `hook`), and an end-to-end test suite that exercises the same path Claude Code invokes when wiring the PreToolUse hook.
10+
11+
**What's stable:** Everything in `spine_lite.__all__` after this phase. Phase 3 adds `Disposition`, `Receipt`, `transition`, `evaluate` to the surface. The full public API:
12+
13+
```python
14+
from spine_lite import (
15+
PRECEDENCE, Effect, most_restrictive,
16+
Manifest, ToolDefinition, parse_manifest,
17+
ToolCall, Decision, classify,
18+
Posture, Disposition, transition, evaluate,
19+
Receipt,
20+
SpineLiteError, ManifestError, ClassificationError,
21+
PostureError, HookError,
22+
__version__,
23+
)
24+
```
25+
26+
`spine_lite.hook.run_hook` and `spine_lite.hook.main` remain accessible via the submodule import for programmatic users; the canonical operator entry point is the `spine-lite hook` console script.
27+
28+
**What's not in scope:** Real-host integration (smoke against an actual Claude Code session) — the E2E tests run via subprocess against the installed `spine-lite` console script, which is the closest faithful test the build sandbox supports. PyPI publish remains a project-level decision.
29+
30+
## Commit timeline
31+
32+
| # | SHA prefix | Subject |
33+
|---|---|---|
34+
| 1 | `6cdcde5` | `chore: enable attr_list mkdocs extension` |
35+
| 2 | `29bfb63` | `feat: posture state machine with Disposition and evaluate` |
36+
| 3 | `7cd329b` | `feat: Receipt dataclass with deterministic serialization` |
37+
| 4 | `0d92074` | `feat: PreToolUse hook adapter` |
38+
| 5 | `d3e6cb6` | `feat: full CLI surface with integration and E2E tests` |
39+
| 6 | (this commit) | `release: bump to v0.3.0a0 + phase 3 exit receipt` |
40+
41+
Each commit independently passed the local verification gate before being staged.
42+
43+
## Design choices recorded
44+
45+
- **Disposition is closed at three members.** `ALLOW`, `DENY`, `ESCALATE`. Adding a fourth (e.g. `LOG_ONLY`) would require updating every consumer that exhaustively matches and the exit-code contract; same closed-taxonomy logic applies as for `Effect`.
46+
- **`evaluate` is layered explicitly.** The order of checks matters: posture allow-list first (one tool, many postures), `LOCKED`/`DRY_RUN` posture-specific rules next, `require_confirmation` last. Reordering the posture-specific checks against the allow-list would let a deny-listed tool slip through under `LOCKED`.
47+
- **Exit codes use a wide range.** `0` ALLOW / `1` DENY / `2` ESCALATE / `64` HOOK_ERROR / `65` MANIFEST_ERROR. `64+` is sysexits.h territory for "internal failure" — keeps policy outcomes distinguishable from protocol errors at the host's exit-code layer.
48+
- **Receipt fields are content-addressable.** `to_canonical_json` uses `sort_keys=True`, `ensure_ascii=False`, and compact separators. The hash is `sha256(canonical_json.encode("utf-8"))`. No timestamps, no UUIDs, no per-run metadata — anything that varies between runs lives in the hook's external metadata, not the receipt itself.
49+
- **Hook contract is intentionally minimal.** Top-level JSON object with `tool` (required, non-empty string) and `arguments` (optional, object). Other fields are ignored. This lets the same hook adapt to multiple host hook formats; spine-lite doesn't need to be re-released when a host's payload schema evolves.
50+
- **Stdin ↔ stdout is the boundary, not a config file.** The CLI's `--manifest` flag is the configuration; payload comes via stdin; decision goes to stdout. No log files written by default. Whether to capture receipts to disk is the operator's choice (a `--receipt-dir` flag is reserved for a future minor release).
51+
- **CLI uses `Annotated`-style typer parameters.** Avoids `B008` cleanly and matches typer's modern recommended idiom. The one carve-out: `Path` stays at the top of `cli.py` (TC003 ignored for this file only) because typer introspects the runtime annotation to do `exists=True` validation.
52+
- **E2E tests run via `python -m spine_lite.cli` subprocess.** True fresh-venv install is out of scope for the build sandbox, but invoking the installed entry point captures everything except the console-script shim.
53+
54+
## Verification on the green run
55+
56+
- `ruff check`: clean
57+
- `ruff format --check`: clean
58+
- `mypy --strict src tests`: clean across 19 source files
59+
- `pytest`: 209 / 209 passing
60+
- Coverage: 100% on every runtime module
61+
- `mkdocs build --strict`: clean
62+
- Hypothesis: 9 properties × 1,000 examples each (six classifier + three receipt)
63+
- E2E subprocess tests: 7 cases (5 posture × tool combinations + byte-stability + version)
64+
65+
## Phase 3 exit gate
66+
67+
| # | Item | State |
68+
|---|------|-------|
69+
| 1 | `posture.py` (transitions + evaluate) 100% coverage | ✓ (34 stmts, 14 branches, 0 miss) |
70+
| 2 | `receipt.py` 100% coverage | ✓ (22 stmts, 0 miss) |
71+
| 3 | `hook.py` 100% coverage | ✓ (54 stmts, 6 branches, 0 miss) |
72+
| 4 | `cli.py` 100% coverage | ✓ (49 stmts, 0 miss) |
73+
| 5 | Integration tests for every subcommand ||
74+
| 6 | E2E smoke via installed entry point | ✓ (7 subprocess cases) |
75+
| 7 | mypy `--strict` clean ||
76+
| 8 | CI green on all 9 matrix cells | (pending push verification) |
77+
| 9 | CHANGELOG entry for `v0.3.0a0` ||
78+
| 10 | All commits in Conventional Commits format ||
79+
| 11 | Receipt appended to `RECEIPTS.md` | ✓ (this commit) |
80+
81+
## Lessons for after Phase 3
82+
83+
- **Closed enums fold neatly into ordered transition tables.** Once `Posture` was closed at four members, encoding the transition rules as `dict[Posture, frozenset[Posture]]` produced a fully-typed structure with zero special-cased code paths.
84+
- **Hypothesis on dataclasses with `st.builds(...)` is cheap.** The `Receipt` strategy at 1,000 examples per property is ~3 seconds. Tightening the strategy bounds (max sizes for arguments, text fields) keeps it that way.
85+
- **Annotated-style typer is worth the upgrade.** `B008` ignore is an anti-pattern; the Annotated form is cleaner and the typer docs now lead with it.
86+
87+
## See also
88+
89+
- [`RECEIPTS.md`](https://github.com/MacFall7/spine-lite-python/blob/main/RECEIPTS.md) — canonical phase-day receipts.
90+
- [`CHANGELOG.md`](https://github.com/MacFall7/spine-lite-python/blob/main/CHANGELOG.md) — what shipped in each version.
91+
- [Phase 2 history](phase-2.md) — what shipped immediately before.
92+
- [Wire into Claude Code](../how-to/wire-claude-code.md) — operator runbook now backed by a working hook.

docs/reference/api.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,27 @@ from spine_lite import (
4040
- PostureError
4141
- HookError
4242

43+
## Manifest
44+
45+
::: spine_lite.manifest
46+
47+
## Classifier
48+
49+
::: spine_lite.classifier
50+
4351
## Posture
4452

4553
::: spine_lite.posture
4654
options:
4755
members:
4856
- Posture
57+
- Disposition
58+
- transition
59+
- evaluate
4960

50-
## Phase 2 / Phase 3 modules
51-
52-
Stubs and partials with phase-pinning docstrings. The reference expands as implementations land.
61+
## Phase 3 modules
5362

54-
::: spine_lite.manifest
55-
56-
::: spine_lite.classifier
63+
Stubs with phase-pinning docstrings. The reference expands as implementations land.
5764

5865
::: spine_lite.receipt
5966

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ nav:
6868
- History:
6969
- Phase 1: history/phase-1.md
7070
- Phase 2: history/phase-2.md
71+
- Phase 3: history/phase-3.md
7172

7273
markdown_extensions:
7374
- admonition
75+
- attr_list
7476
- pymdownx.details
7577
- pymdownx.superfences
7678
- pymdownx.tabbed:

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "spine-lite"
7-
version = "0.2.0a0"
7+
version = "0.3.0a0"
88
description = "Deterministic policy and effects runtime for LLM tool calls."
99
readme = "README.md"
1010
requires-python = ">=3.11"
@@ -109,9 +109,12 @@ ignore = [
109109
]
110110

111111
[tool.ruff.lint.per-file-ignores]
112-
"tests/**" = ["D", "S101", "ANN", "PT011"]
112+
"tests/**" = ["D", "S101", "S603", "ANN", "PT011"]
113113
"noxfile.py" = ["D", "ANN"]
114114
"docs/**" = ["D"]
115+
# Typer introspects the type annotation at runtime to do Path validation,
116+
# so Path can't move to a TYPE_CHECKING block in cli.py.
117+
"src/spine_lite/cli.py" = ["TC003"]
115118

116119
[tool.ruff.lint.pydocstyle]
117120
convention = "google"

src/spine_lite/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,32 @@
1818
SpineLiteError,
1919
)
2020
from spine_lite.manifest import Manifest, ToolDefinition, parse_manifest
21-
from spine_lite.posture import Posture
21+
from spine_lite.posture import Disposition, Posture, evaluate, transition
22+
from spine_lite.receipt import Receipt
2223

23-
__version__ = "0.2.0a0"
24+
__version__ = "0.3.0a0"
2425

2526
__all__ = [
2627
"PRECEDENCE",
2728
"ClassificationError",
2829
"Decision",
30+
"Disposition",
2931
"Effect",
3032
"HookError",
3133
"Manifest",
3234
"ManifestError",
3335
"Posture",
3436
"PostureError",
37+
"Receipt",
3538
"SpineLiteError",
3639
"ToolCall",
3740
"ToolDefinition",
3841
"__version__",
3942
"classify",
43+
"evaluate",
4044
"most_restrictive",
4145
"parse_manifest",
46+
"transition",
4247
]
4348

4449
logging.getLogger(__name__).addHandler(logging.NullHandler())

0 commit comments

Comments
 (0)