Platform Build — 06

ledger

Local-First Architectural Decision Record

Every serious build accumulates invisible architecture: the decisions that didn't make it into the code, only the reasoning that produced the code. The longer you build across multiple projects, the more valuable that reasoning becomes — and the faster it disappears.

Status
v0.1.0 Shipped
Built
June 2026 — single session
Stack
Python · SQLite · Anthropic Haiku · MCP
License
Apache-2.0

Cross-project decision drift

I run multiple production builds simultaneously: BoardPath, Auris Intelligence, Litt, transparent-confidence. Each one has architectural decisions embedded in it — choices about storage, retrieval, schema design, layer separation, model selection. Those decisions are in the code. The reasoning behind them is nowhere.

The specific friction that triggered this build: I couldn't answer "how have I handled OCR across projects?" without reading through the code of three separate repositories. I couldn't start a new BoardPath session and immediately know why I'd made a particular decision two weeks earlier in a different Claude Code session. Session memory in coding agents is per-agent and per-session. It doesn't answer cross-project knowledge questions. Nothing else owned that problem, so I built the thing that did.

Figure 1 — The shape of the tool
2
Architecture layers
0
Layer 1 runtime deps
6
CLI commands
4
MCP tools
The zero is the load-bearing number: Layer 1 runs on the Python standard library alone, so the deterministic half of the tool has nothing to install and nothing to break.

A hard boundary between what the architecture guarantees and what the AI judges

The central design decision in ledger is the two-layer split — and the insistence that the split be real, not just described.

Figure 2 — The two-layer boundary
Layer 2 — Intelligent Retrieval
recall · resume · keyword rank → bounded hydration → Haiku synthesis
pip install -e ".[ai]"
Layer 1 — Deterministic Core
capture · store · list · filter · show · supersede · SQLite only
Zero deps · Always works
The green edge marks the guarantee: Layer 1 is complete without Layer 2, the dependency runs one way only, and a test in the suite fails if that direction is ever reversed.

Layer 1 is complete and useful on its own. It's a structured decision store backed by SQLite, which ships with Python. No API key. No network call. No external service. You can pip install ledger and use it without ever configuring an AI integration.

Layer 2 sits on top. Natural-language recall uses a four-stage pipeline: structured filter on the decision store → keyword rank to surface the most relevant candidates → bounded hydration (top 10) into a context block → a single Anthropic Haiku synthesis call. The default model is Haiku — it's fast enough for interactive use and 3x cheaper than Sonnet for output tokens. Override with LEDGER_MODEL if you want deeper synthesis.

The boundary is enforced as a test, not as a comment: test_core_imports_no_anthropic imports the core modules and asserts that the Anthropic SDK never appears in sys.modules. If the boundary is ever accidentally crossed, the test fails. That decision got its own architecture note.

Python 3.10+ SQLite Anthropic Haiku MCP FastMCP pytest

Read-only by protocol, not by convention

The MCP server exposes four read tools: ledger_list, ledger_show, ledger_recall, ledger_resume. It exposes no write tools. Capture stays CLI-only — you log a decision deliberately, at the terminal, with intent. No connected client can write to the record.

Figure 3 — The MCP surface
ledger_list
List decisions with optional project / status / tags filters. Default: active only.
ledger_show
Full record for a decision ID, including its supersession chain.
ledger_recall
Natural-language question answered by AI synthesis over the decision store. Layer 2.
ledger_resume
Re-orientation brief for a project's active decisions. Layer 2.
Four tools, all reads — there is no write tool on this list. Capture stays at the CLI, so no connected client can add to the record on its own.

The read/write asymmetry is the philosophy enforced at the protocol boundary. Any Claude Code session can read your architectural reasoning. None can add to it without you sitting at a terminal and choosing to.

The choices that defined the shape of the tool

Decision

Zero runtime dependencies for Layer 1. Python's sqlite3 module ships with the standard library. Layer 1 has nothing to install and nothing to break.

Decision

Timestamp-based IDs for chronological natural sort. Decision IDs use the format YYYYMMDD-HHMM-XXXX — natural string sort equals chronological sort, no separate index required. Human-readable without a lookup table.

Decision

Keyword overlap for v1 ranking, stable interface for embedding swap. rank.score() uses term-set intersection. The interface signature is identical to what an embedding-based ranker will implement in v1.1 — the upgrade is a one-file swap with no changes to the pipeline or CLI.

Decision

Supersession as a graph, not deletion. Superseding a decision sets the old record's status to 'superseded' and links the new record via a self-referential foreign key. The decision history is a directed graph of how reasoning evolved — the amendment-chain pattern from BoardPath applied to your own build process.

Honest retrospective

This section is written after enough time has passed to have genuine perspective. Check back when v1.1 (embedding-based ranking) is complete.

← Persisted Memory Next: transparent-confidence →