Skip to content

Commit d5e36c7

Browse files
committed
docs: restructure site into Diátaxis quadrants and add full content
Move existing pages under explanation/, how-to/, and reference/. Add getting-started; concepts/ (overview, effects-taxonomy, posture-and-hooks); how-to/ (use-the-api, wire-claude-code, contribute, release); reference/ (api, cli, exceptions, glossary); explanation/ (architecture refined, invariants, faq); history/ (phase-1 narrative). mkdocs nav reorganised to match. mkdocs build --strict clean.
1 parent d90e72c commit d5e36c7

24 files changed

Lines changed: 1613 additions & 128 deletions

‎docs/api.md‎

Lines changed: 0 additions & 23 deletions
This file was deleted.

‎docs/architecture.md‎

Lines changed: 0 additions & 47 deletions
This file was deleted.

‎docs/concepts/effects-taxonomy.md‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Effects Taxonomy
2+
3+
The six classes, the precedence ordering, and the rules that make them stable.
4+
5+
## The six classes
6+
7+
| Class | Meaning | Examples |
8+
|---|---|---|
9+
| `READ` | Pure observation. No state change anywhere the caller can see. | reading a file, listing a directory, querying without writing |
10+
| `WRITE` | Persistent state change to caller-owned storage. Reversible by the caller. | writing a file, updating a row, appending a log entry |
11+
| `NETWORK` | Outbound network call. Crosses a trust boundary; result is observable to a third party. | HTTP request, DNS lookup, Slack message, webhook fire |
12+
| `EXECUTE` | Subprocess invocation, no fork-and-detach. Caller waits for completion. | `subprocess.run`, `os.system`, shell pipeline |
13+
| `SPAWN` | Subprocess invocation that may fork-and-detach. Caller does not necessarily wait. | `Popen` with no `wait`, daemon launch, `nohup`, container `run -d` |
14+
| `DESTRUCTIVE` | Irreversible state change. Recovery requires backups or external intervention. | `rm -rf`, `git push --force`, dropping a table, deleting a branch |
15+
16+
## Precedence
17+
18+
Most restrictive at the top:
19+
20+
```
21+
DESTRUCTIVE ← highest
22+
SPAWN
23+
EXECUTE
24+
NETWORK
25+
WRITE
26+
READ ← lowest
27+
```
28+
29+
`spine_lite.most_restrictive(effects)` walks this ordering top-down and returns the first match. The function is total over any non-empty subset of `Effect`; it raises `ValueError` on an empty input.
30+
31+
```python
32+
from spine_lite import Effect, most_restrictive
33+
34+
most_restrictive({Effect.READ, Effect.NETWORK, Effect.WRITE})
35+
# <Effect.NETWORK: 'network'>
36+
37+
most_restrictive({Effect.DESTRUCTIVE, Effect.READ})
38+
# <Effect.DESTRUCTIVE: 'destructive'>
39+
```
40+
41+
The ordering is encoded once in [`spine_lite.PRECEDENCE`](../reference/api.md). Every comparison in the runtime resolves through that constant; reordering is a one-line change with project-level sign-off.
42+
43+
## Why these six
44+
45+
Each class draws a line that matters for safety review:
46+
47+
- `READ` vs. `WRITE`: did anything change?
48+
- `WRITE` vs. `NETWORK`: did anything cross a trust boundary?
49+
- `NETWORK` vs. `EXECUTE`: did we hand control to another process?
50+
- `EXECUTE` vs. `SPAWN`: did the child outlive the call?
51+
- `SPAWN` vs. `DESTRUCTIVE`: can we undo it?
52+
53+
Anything finer is a manifest concern (the *which* file, the *which* host), not a taxonomy concern (the *what kind of effect*).
54+
55+
## Why closed
56+
57+
A taxonomy that grows at runtime is a taxonomy that drifts. Adding a seventh class would require:
58+
59+
1. Updating `Effect` and `PRECEDENCE` in `effects.py`.
60+
2. Updating the parity tests against the TypeScript reference.
61+
3. Updating every consumer that exhaustively matches on `Effect`.
62+
4. A migration note in `CHANGELOG.md` and `docs/explanation/porting-notes.md`.
63+
5. Project-level sign-off — recorded as a HALT against [`CLAUDE.md`](https://github.com/MacFall7/spine-lite-python/blob/main/CLAUDE.md).
64+
65+
If you find yourself wanting a new class, first try modelling the new behaviour as an existing class with manifest-level qualifiers. The seventh class is almost always an existing class wearing a different hat.
66+
67+
## Determinism
68+
69+
`most_restrictive` is a pure function. Same input set → same output, every time. No hidden state, no clocks, no randomness. The runtime depends on this property — receipts hash by SHA exactly because the inputs uniquely determine the outputs.
70+
71+
The `effects` module contains zero I/O, zero clocks, zero randomness. This is enforced by the [Invariants](../explanation/invariants.md) and verified by the test suite.
72+
73+
## See also
74+
75+
- [Concepts / Overview](overview.md) — where the taxonomy fits in the pipeline.
76+
- [Reference / API](../reference/api.md#effects) — the auto-generated `Effect`, `PRECEDENCE`, `most_restrictive` reference.
77+
- [Reference / Glossary](../reference/glossary.md) — term-by-term definitions.
78+
- [Explanation / Porting Notes](../explanation/porting-notes.md) — how the Python form relates to the TypeScript reference.

‎docs/concepts/overview.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Overview
2+
3+
The mental model in one page.
4+
5+
## The pipeline
6+
7+
```
8+
tool call ──┐
9+
│
10+
manifest ───┼──► classifier ──► effect set ──► most_restrictive ──► dominant effect
11+
│ │ │
12+
posture ────┘ │ │
13+
▼ ▼
14+
posture transition ─────────────► decision
15+
│
16+
▼
17+
receipt
18+
│
19+
▼
20+
hook ──► allow / deny
21+
```
22+
23+
You give the runtime three inputs:
24+
25+
- A **tool call** — the LLM has decided to invoke a named tool with arguments.
26+
- A **manifest** — the policy document for that tool, including its declared effects and any posture constraints.
27+
- A **posture** — the current operational state (e.g., interactive vs. autonomous, dry-run vs. live).
28+
29+
The runtime returns a single output: a **decision** carrying the dominant effect, the full effect set, the new posture, and a receipt that hashes to the same value if you re-run with the same inputs.
30+
31+
The PreToolUse **hook** is just an I/O wrapper that maps stdin/stdout to and from the pure pipeline above.
32+
33+
## What's pure
34+
35+
`effects`, `manifest`, `classifier`, `posture`, and `receipt` are pure modules. No clocks, no randomness, no network, no filesystem. Same input → same output, every time. This is what makes a receipt replayable by SHA — there's no hidden state to drift.
36+
37+
`hook` and `cli` do I/O, because something has to. They're thin adapters around the pure core.
38+
39+
## What's closed
40+
41+
The effects taxonomy is closed at six classes. Adding a class is a project-level decision that requires updating the precedence ordering, the parity tests against the TypeScript reference, and the docstrings. It is not a runtime extension point.
42+
43+
This is a feature, not a limitation. A taxonomy that grows at runtime is a taxonomy that drifts. The six classes were chosen to cover every observable side effect a tool call can produce. If something looks like a seventh class, it's probably an existing class with new arguments.
44+
45+
## What's deterministic
46+
47+
Same tool call + same manifest + same posture → same decision + same receipt bytes. No "the timestamp differs" caveats; the receipt doesn't carry a timestamp. The hook layer is where wall-clock entropy enters, and only as input — it doesn't influence classification.
48+
49+
## What's offline
50+
51+
Zero network calls inside the runtime. Zero LLM calls inside the runtime. The runtime's job is to look at a tool call and decide; if you want to call a model to second-guess it, that's a separate concern that lives outside this package.
52+
53+
## Phase boundaries
54+
55+
| Phase | Modules complete | What you can do |
56+
|---|---|---|
57+
| 1 | `effects`, `exceptions`, `cli` (`version`) | Collapse effect sets, raise/catch typed errors, ship as a package. |
58+
| 2 | `+ manifest`, `classifier` | Classify a tool call against a manifest, get a `Decision`. |
59+
| 3 | `+ posture`, `receipt`, `hook`, `cli` (full) | End-to-end PreToolUse integration with Claude Code. |
60+
61+
The architecture stays the same across phases. Each phase fills in pure modules that adhere to the same purity contract.
62+
63+
## Where to go next
64+
65+
- [Effects Taxonomy](effects-taxonomy.md) — the six classes and their precedence.
66+
- [Posture and Hooks](posture-and-hooks.md) — the Phase 2/3 components in outline.
67+
- [Architecture](../explanation/architecture.md) — why the modules are split this way.
68+
- [Invariants](../explanation/invariants.md) — the rules nothing in this repo gets to break.

‎docs/concepts/posture-and-hooks.md‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Posture, Receipts, and Hooks
2+
3+
The Phase 2 and Phase 3 components in outline. Implementations land at `v0.2.0a0` and `v0.3.0a0` respectively; what's described here is the contract those phases fill in.
4+
5+
## Manifest (Phase 2)
6+
7+
A manifest is the policy document for a tool. It declares:
8+
9+
- The tool's name and signature.
10+
- The set of effects each invocation can produce.
11+
- Posture constraints — under which postures the tool may be invoked, and under which it must be refused.
12+
13+
Manifests are validated as Pydantic v2 models and round-trip the TypeScript reference fixtures byte-for-byte after JSON normalisation.
14+
15+
## Classifier (Phase 2)
16+
17+
```python
18+
def classify(tool_call: ToolCall, manifest: Manifest) -> Decision: ...
19+
```
20+
21+
Pure function. Given a tool call and a manifest, returns a `Decision` carrying:
22+
23+
- `effects: frozenset[Effect]` — every effect the call can produce.
24+
- `most_restrictive: Effect` — the dominant class.
25+
- `rationale: str` — human-readable explanation.
26+
- `posture_transition: PostureTransition | None` — what the posture machine should do next.
27+
28+
No I/O. No clocks. Same input → same output, every time.
29+
30+
## Posture state machine (Phase 3)
31+
32+
A posture is the current operational mode. Transitions are pure value-in-value-out functions; no hidden state.
33+
34+
Postures (planned, subject to refinement against the TypeScript reference):
35+
36+
| Posture | Meaning |
37+
|---|---|
38+
| `INTERACTIVE` | Operator is at the keyboard; ambiguous calls escalate to a prompt. |
39+
| `AUTONOMOUS` | No operator in the loop; ambiguous calls fail closed. |
40+
| `DRY_RUN` | Classification only; no `WRITE`/`NETWORK`/`EXECUTE`/`SPAWN`/`DESTRUCTIVE` effects fire. |
41+
| `LOCKED` | Refuse everything except explicitly allow-listed read-only calls. |
42+
43+
Transitions are total — every `(posture, decision)` pair has a defined next posture or a `PostureError`. There are no implicit transitions.
44+
45+
## Receipt (Phase 3)
46+
47+
A structured record of a single decision. Field ordering is deterministic; serialised receipts hash to identical bytes given identical inputs.
48+
49+
A receipt carries:
50+
51+
- The input tool call (as the manifest validated it).
52+
- The classified effects and dominant class.
53+
- The posture before and after.
54+
- The decision (`allow`, `deny`, `escalate`).
55+
- A content hash that uniquely identifies the receipt.
56+
57+
No timestamps. The hook layer attaches wall-clock time as out-of-band metadata; the receipt itself stays content-addressable.
58+
59+
## Hook (Phase 3)
60+
61+
A thin I/O wrapper. Reads a hook payload from stdin, runs the pipeline, writes a decision to stdout, exits 0 (allow) or non-zero (deny).
62+
63+
The hook is the only place in the package that touches `sys.stdin` or `sys.stdout`. Everything inside is pure.
64+
65+
```bash
66+
# Phase 3 — wired into Claude Code as a PreToolUse hook
67+
spine-lite hook < payload.json
68+
echo $? # 0 = allow, non-zero = deny
69+
```
70+
71+
## CLI (Phase 3)
72+
73+
| Subcommand | Status | Purpose |
74+
|---|---|---|
75+
| `version` | Phase 1 — shipped | Print the installed version. |
76+
| `validate-manifest` | Phase 2 | Validate a manifest file against the schema. |
77+
| `classify` | Phase 2 | One-shot classification of a tool call. |
78+
| `hook` | Phase 3 | stdin/stdout PreToolUse adapter for Claude Code. |
79+
80+
## See also
81+
82+
- [Concepts / Overview](overview.md) — pipeline shape.
83+
- [How-To / Wire into Claude Code](../how-to/wire-claude-code.md) — operator runbook.
84+
- [Explanation / Porting Notes](../explanation/porting-notes.md) — how this maps to the TypeScript reference.

‎docs/explanation/architecture.md‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Architecture
2+
3+
## Mission
4+
5+
Classify and decide on every LLM tool call before it runs. Pure, deterministic, byte-for-byte reproducible. Wire into Claude Code as a PreToolUse hook; carry the same semantics anywhere else a hook contract exists.
6+
7+
## The runtime in one diagram
8+
9+
```
10+
┌──────────────────────────────────────────────┐
11+
│ pure pipeline │
12+
│ │
13+
stdin payload ──► │ manifest ──► classifier ──► posture machine │ ──► stdout decision
14+
│ │ │ │ │
15+
│ ▼ ▼ ▼ │
16+
│ ManifestError Classification- Posture- │
17+
│ Error Error │
18+
│ │
19+
│ receipt (deterministic bytes) │
20+
└──────────────────────────────────────────────┘
21+
exit 0 = allow │
22+
exit n = deny ▼
23+
receipt directory (optional)
24+
```
25+
26+
The hook is a thin I/O wrapper. Everything inside is pure: no clocks, no randomness, no network, no filesystem. Same input → same output, every time.
27+
28+
## Module map
29+
30+
| Module | Responsibility | Pure | Phase |
31+
| --- | --- | --- | --- |
32+
| `effects` | Closed six-class taxonomy + precedence + `most_restrictive` | yes | 1 ✓ |
33+
| `exceptions` | Hierarchy rooted at `SpineLiteError` | yes | 1 ✓ |
34+
| `manifest` | Tool-definition schema (Pydantic v2) | yes | 2 |
35+
| `classifier` | Tool call → effect set → `Decision` | yes | 2 |
36+
| `posture` | Posture state machine | yes | 3 |
37+
| `receipt` | Structured, deterministic decision records | yes | 3 |
38+
| `hook` | Claude Code PreToolUse stdin/stdout adapter | I/O | 3 |
39+
| `cli` | Operator interface (Typer) | I/O | 1 (`version`) → 3 (full) |
40+
41+
The five pure modules contain no clocks, no randomness, no network, no filesystem. Determinism is the contract.
42+
43+
## Why the pure / I/O split
44+
45+
Determinism only holds if there's exactly one boundary where wall-clock entropy enters. Everything else has to be pure. The hook is that boundary; the CLI is that boundary; nothing else is.
46+
47+
This is what makes a receipt content-addressable. A receipt that hashed differently every time you ran it would be useless for replay; the only way to get byte-stability is to keep the hot path free of timestamps, UUIDs, and randomness. The cost is one extra layer of indirection (the hook); the benefit is replayability by SHA forever.
48+
49+
## Why a closed taxonomy
50+
51+
A taxonomy that grows at runtime is a taxonomy that drifts. The six classes were chosen to draw the lines that matter for safety review:
52+
53+
- `READ` vs. `WRITE`: did anything change?
54+
- `WRITE` vs. `NETWORK`: did anything cross a trust boundary?
55+
- `NETWORK` vs. `EXECUTE`: did we hand control to another process?
56+
- `EXECUTE` vs. `SPAWN`: did the child outlive the call?
57+
- `SPAWN` vs. `DESTRUCTIVE`: can we undo it?
58+
59+
Anything finer is a manifest concern, not a taxonomy concern. If something feels like a seventh class, it's almost always an existing class with manifest-level qualifiers — *which* file the WRITE touched, *which* host the NETWORK call hit.
60+
61+
Adding a class is permitted but expensive. The process is in [Effects Taxonomy / Why closed](../concepts/effects-taxonomy.md#why-closed) and triggers a HALT to the project lead.
62+
63+
## Phase plan
64+
65+
| Phase | Modules | Version | What you can do |
66+
|---|---|---|---|
67+
| 1 (shipped) | `effects`, `exceptions`, `cli` (`version`) | `v0.1.0a0` | Collapse effect sets, raise/catch typed errors, ship as a package. |
68+
| 2 | `+ manifest`, `classifier` | `v0.2.0a0` | Classify a tool call against a manifest, get a `Decision`. |
69+
| 3 | `+ posture`, `receipt`, `hook`, `cli` (full) | `v0.3.0a0` | End-to-end PreToolUse integration with Claude Code. |
70+
71+
The architecture stays the same across phases. Each phase fills in pure modules that adhere to the same purity contract.
72+
73+
## Reference implementation
74+
75+
The TypeScript reference lives at [MacFall7/M87-Spine-lite](https://github.com/MacFall7/M87-Spine-lite). Treat it as the spec for semantic behaviour. Where Python idiom diverges from TypeScript (typing, dataclass shape, exception names), prefer the Python form and document the call in [Porting Notes](porting-notes.md). Anything that changes observable behaviour is a divergence and needs project-level sign-off.
76+
77+
## See also
78+
79+
- [Invariants](invariants.md) — the rules nothing in this repo gets to break.
80+
- [Design Rationale](design-rationale.md) — past decisions, with reasoning.
81+
- [Porting Notes](porting-notes.md) — TypeScript-to-Python translation log.
82+
- [FAQ](faq.md) — common questions about the design.
File renamed without changes.

0 commit comments

Comments
 (0)