Effects Taxonomy¶
The six classes, the precedence ordering, and the rules that make them stable.
The six classes¶
| Class | Meaning | Examples |
|---|---|---|
READ |
Pure observation. No state change anywhere the caller can see. | reading a file, listing a directory, querying without writing |
WRITE |
Persistent state change to caller-owned storage. Reversible by the caller. | writing a file, updating a row, appending a log entry |
NETWORK |
Outbound network call. Crosses a trust boundary; result is observable to a third party. | HTTP request, DNS lookup, Slack message, webhook fire |
EXECUTE |
Subprocess invocation, no fork-and-detach. Caller waits for completion. | subprocess.run, os.system, shell pipeline |
SPAWN |
Subprocess invocation that may fork-and-detach. Caller does not necessarily wait. | Popen with no wait, daemon launch, nohup, container run -d |
DESTRUCTIVE |
Irreversible state change. Recovery requires backups or external intervention. | rm -rf, git push --force, dropping a table, deleting a branch |
Precedence¶
Most restrictive at the top:
DESTRUCTIVE ← highest
SPAWN
EXECUTE
NETWORK
WRITE
READ ← lowest
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.
from spine_lite import Effect, most_restrictive
most_restrictive({Effect.READ, Effect.NETWORK, Effect.WRITE})
# <Effect.NETWORK: 'network'>
most_restrictive({Effect.DESTRUCTIVE, Effect.READ})
# <Effect.DESTRUCTIVE: 'destructive'>
The ordering is encoded once in spine_lite.PRECEDENCE. Every comparison in the runtime resolves through that constant; reordering is a one-line change with project-level sign-off.
Why these six¶
Each class draws a line that matters for safety review:
READvs.WRITE: did anything change?WRITEvs.NETWORK: did anything cross a trust boundary?NETWORKvs.EXECUTE: did we hand control to another process?EXECUTEvs.SPAWN: did the child outlive the call?SPAWNvs.DESTRUCTIVE: can we undo it?
Anything finer is a manifest concern (the which file, the which host), not a taxonomy concern (the what kind of effect).
Why closed¶
A taxonomy that grows at runtime is a taxonomy that drifts. Adding a seventh class would require:
- Updating
EffectandPRECEDENCEineffects.py. - Updating the parity tests and the porting-notes log.
- Updating every consumer that exhaustively matches on
Effect. - A migration note in
CHANGELOG.mdanddocs/explanation/porting-notes.md. - Project-level sign-off — recorded as a HALT against
CLAUDE.md.
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.
Determinism¶
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.
The effects module contains zero I/O, zero clocks, zero randomness. This is enforced by the Invariants and verified by the test suite.
See also¶
- Concepts / Overview — where the taxonomy fits in the pipeline.
- Reference / API — the auto-generated
Effect,PRECEDENCE,most_restrictivereference. - Reference / Glossary — term-by-term definitions.
- Explanation / Porting Notes — design history and the relationship to the sibling project.