# BranchStore (git-KV) as the coarse/durable state substrate

## Status

accepted (2026-06-02)

## Context

spt-core's durable state is spread across four mechanisms, each with its own
crash-recovery code: **SQLite** (`spool.db`, registry persistence), **flat files
with tmp-write + atomic-rename** (`info.json`, `status` — KNOWN-HAZARDS 5.2), a
**hand-rolled fsync-append journal** (the B5 broker effect anchor), and a
**separate git repository** for the Psyche context (`a-<id>` / `p-<project>`
branches + a custom precedence merge driver). So spt-core has *already* chosen
git-as-store for its most important state (the mind); it just hasn't named the
pattern or reused it.

Studying a sister project (BigscreenVR/attractor) and its reference Rust impl
(StrongDM's fabro, `fabro-checkpoint`) surfaced that pattern as a first-class
abstraction: a **git branch treated as a versioned key/value filesystem** —
`ensure_branch()`, one-commit-per-write, reads = tree lookups at the branch tip,
history = `git log`, recovery = read the ref tip, refs kept porcelain-compatible
under `refs/heads/<tool>/...`. The question this ADR answers: should that be a
shared internal substrate in spt-core, and where is its boundary against SQLite
and fsync-append?

## Decision

Adopt **`spt-store::BranchStore`** — a git branch used as a versioned key/value
filesystem — as the substrate for **coarse, durable, audited** state. It lives at
the bottom of the stack (`spt-store`) so context sync, the registry, and daemon
checkpointing share one abstraction rather than each re-inventing persistence.

**BranchStore is for:**
- the **context repo** — generalize the existing `a-`/`p-` design into the shared store;
- the **subnet registry** — snapshot + cross-node distribution, reusing the precedence merge driver + P2P-git sync instead of a separate replication scheme (M4);
- **daemon/brain coarse checkpoint** — the rehydrate anchor (ADR-0004): commit = checkpoint, ref tip = resume;
- **audited lifecycle state** — where a state's *trajectory* (not just current value) has operational value.

**BranchStore is NOT for (these stay as they are):**
- **hot-path / high-frequency writes** — the B5 per-effect PTY journal (per-keystroke) stays **fsync-append**; per-message spool churn stays **SQLite**. A commit per write is orders of magnitude too heavy here.
- **indexed / queried state** — spool drain-by-status, `id → address` resolution stay **SQLite** (BranchStore is a KV tree, no secondary indexes). SQLite may serve as a *derived view* rebuilt from a BranchStore source where both audit and queryability are needed.

**Rules:**
- **Single-writer-per-ref**, sharded (per run / session / endpoint — `refs/heads/spt/...`), matching the existing single-writer invariants (KNOWN-HAZARDS 6.4, active-instance-authoritative). No many-writers-one-ref contention.
- **commit = checkpoint, ref tip = resume** — the atomic ref update means a crash mid-write never advances the ref, so recovery is "read the tip." This **dissolves the torn-write hazard class** (the tmp-write + atomic-rename dance, 5.2) for the state BranchStore covers.
- **Atomic multi-key commits** — perch + registry + cursor in one all-or-nothing commit.
- **Merge-native sync** — reuse the context precedence merge driver + `--allow-unrelated-histories` for any BranchStore replicated cross-node.
- **GUI consequence** — state browsing becomes *git rendering*: raw inspection is free via standard git tooling; the product GUI (R-FRONT-1 panes) is a memformat-aware renderer over git, inheriting history / diff / time-travel rather than building a versioning backend.

## Consequences

- New requirement **REQ-STORE-1** (`required_stages = []` until its milestone). Implementation lands **M4** (context realized via BranchStore; registry persistence + distribution) and optionally **M3c** (brain checkpoint). **No retroactive churn** — the B5 fsync journal and the SQLite spool stay exactly as built.
- The torn-write hazard class (5.2) is dissolved for BranchStore-covered state; the SQLite/fsync paths keep their existing guards.
- **De-risk before locking the abstraction:** spike a `BranchStore` over gitoxide/libgit2 and measure commit-per-write latency, to validate the coarse/hot boundary empirically (a natural first attractor build-harness pilot).
- Builds on the context-as-git design (STORAGE.md) and ADR-0004 (brain rehydrate). attractor / fabro are **study references only** — license-check before porting any fabro code.
