Initial Architecture

Ports next to use cases, dependencies pointing inward, and a core that imports the standard library and nothing else.

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/ and infrastructure/ — adapters: the HTTP handlers, the SQLite repositories, the OpenAI gateway, the extractors, the stores. They depend on core/, never the reverse.
  • cmd/ and internal/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.

AI Insights: The Diagram Is Not the Architecture
Source Code: MPL-2.0 · Content: CC BY-SA 4.0 · Credits & AI Disclosure