Initial Architecture
I had the layers and the stack; what I needed was a rule for how they may talk to each other. I modelled the structure on Scriber, a sibling project, because its dependency discipline had already survived a year of feature creep.
Dependencies Point Inward, Only
Three rings:
core/— entities, the domain types, and the use cases. It imports the standard library and nothing else. No YAML, no PDF, no OCR, no HTTP, no OpenAI. This is the ring that must not rot.api/andinfrastructure/— adapters: the HTTP handlers, the SQLite repositories, the OpenAI gateway, the extractors, the stores. They depend oncore/, never the reverse.cmd/andinternal/registry— the only places where concrete implementations are wired to interfaces. Wiring happens here and nowhere else.
Two rules make the direction enforceable rather than aspirational. Interfaces live next to the use cases, never next
to the adapters—so the port is defined by what the core needs, not by what an implementation happens to offer. And
use cases receive and return DTOs, never raw entities: one partial per entity with a toEntity() method, which
keeps the wire format from leaking into the domain and vice versa.
The Two Adapters That Matter Most
The agent and the model are separated on purpose: the agent is a use case, the model is an adapter. The core
drives a bounded tool loop; the OpenAI SDK lives in infrastructure/. That is what lets the agent be tested with a fake
model, and what will let the model be swapped without touching the domain.
The same shape covers the PDF pipeline: subprocess calls to pdftotext, pdftocairo, and tesseract sit behind a port
(ToolRunner), so extraction and OCR orchestration are covered by tests with a scripted fake—no poppler needed in CI.
The Structure That Fell Out
cmd/thought-keeper (one binary, all verbs) · internal/app (lifecycle, tray, single instance, port discovery) ·
internal/registry (wiring) · internal/api (router, handlers, payloads) · internal/web (embedded PWA) · web/ (the
SPA sources) · internal/core (entities, markdown, tree, usecases, usecases/agents) · internal/infrastructure
(config, objects, atoms, wiki, markdown, extract, tree, persistence, gateways).
Supporting rules set here and never broken since: time comes only through a Clock port (so tests are deterministic
and the author’s clock is never trusted); errors are sentinel types mapped to the wire exactly once; logs are
structured (log/slog) from the first line.
Test Tiers
Fast by design: the database is :memory:, the vault is a temp directory, and small fixtures are allowed but not
stored. go test ./... covers units and integration; the invariant is that a clean checkout can run the whole suite
without network access or a single external tool.
Architecture is tested by change, not by how tidy the diagram looks. The chapter fixes two rules—interfaces next to use cases, dependencies pointing inward—and measures them against the moments a requirement moved: swapping a retrieval strategy or adding OCR landed in an adapter while the core stayed untouched. The dependency rule is also what makes the agent a use case and the model an adapter. Structure is a prediction about future changes, and the prediction is what has to be right.
