Architecture roadmap — five specs
Date: 2026-08-21 Status: superseded in part — all five slots resolved 2026-08-22. See the amendment immediately below before reading anything else here.
Amendment, 2026-08-22 — how each of the five resolved
This roadmap was written before any of it was measured. Two of its five specs were changed by that measurement and one was refuted outright. Read this table first; the original text below is kept because its reasoning is still worth reading, but three of its five sections no longer describe the plan.
| # | Original | Resolution |
|---|---|---|
| 1 | Onion refactor | Superseded by 2026-08-21-onion-refactor-design.md. Two reviews found a repository whose method count tracks its call-site count is a rename, not an abstraction. What shipped is the tool-surface half: 2026-08-21-tool-surface-design.md. |
| 2 | Citations domain | Written and implemented (80c1a4f) as 2026-08-22-citations-domain-design.md. Scope stands; its stated second purpose ("proof that spec 1's boundaries hold") is void, since there is no spec 1. |
| 3 | Tracker adapters | Written and implemented (984cc20) as 2026-08-22-tracker-adapters-design.md. Scope stands unchanged — it was the one spec with no dependency on the rest. |
| 4 | Swarm + swarm.toml |
REFUTED. 2026-08-22-swarm-refutation.md. Measured 7.3/15 against 8/15 for doing nothing, at twice the latency. This roadmap's own "measure before building" gate was honoured and the swarm failed it. |
| 5 | Agent-config emitters | Rescoped and implemented (d29c604) as 2026-08-22-config-emitters-design.md. Generates from AGENT_SURFACES rather than the swarm.toml that will not exist. |
The dependency table below is therefore stale: specs 2, 3 and 5 have no dependencies, and spec 4 is closed. Nothing in this roadmap is blocked on anything else in it.
What the measurement actually delivered is in
2026-08-22-agent-surfaces-design.md: four per-domain agents — spec 4's
deliverable — reached by session-level tool restriction rather than by runtime
model routing.
Kind: roadmap. Names the specs, their interfaces, and their order. Each numbered section below becomes its own design doc; none is designed here.
Problem
Agents get confused by this repo in three distinct ways, and the three have been wearing one costume.
Tool-choice confusion. mcp_server.py exposes 36 tools in one flat
namespace. A calling agent sees runs_compare, kg_path, sym_integrate and
digest as peers with equal claim on any question. Nothing in the surface says
runs_scan precedes runs_compare, or that sym_* has nothing to do with the
feed. The agent is choosing from 36 undifferentiated options every turn.
Coding-agent confusion. mcp_server.py is 1454 lines: 36 tools, 36 _impl
functions, 18 raw SQL strings of its own, and 11 deferred
from attestation import ... statements inside function bodies. Deferred
imports are the module confessing to import cycles it is routing around. An
agent asked to change one tool must hold the whole file to find it.
No data layer. Eleven modules speak sqlite3 directly; 94 raw SQL strings
are spread across the tree. db.py is connection management and migrations,
not a repository. That mcp_server.py — the outermost layer — writes its own
SQL is the clearest statement of the problem: presentation reaches to disk.
Secondary tangles found in the same survey, recorded so they are not rediscovered later:
ledger.py(637) +ledger_adapters/generic.py(554): scan, artifact-shape detection, parse, compare and caveat logic in two files.install.py(614): installer, doctor, cron scheduler, Ollama puller, MCP wiring and skill copier in one module.cli.py,server.py,mcp_server.pyare three parallel presentations that each re-derive their calls.server.pyimportsrank_itemsdirectly;cli.pygoes over HTTP to the same logic. No shared service layer, so the three drift.rank.pyexports_PROFILE_VEC_CACHEand_db_identitytomcp_server.py. Private names crossing a module boundary is a missing interface.
The shape being moved toward
┌─ presentation ────────────────────────────────┐
│ cli.py server.py mcp/<domain>.py │ thin adapters, no SQL
├─ services ────────────────────────────────────┤
│ feed knowledge provenance symbolic cite │ one facade per domain
├─ domain ──────────────────────────────────────┤
│ rank kg ledger claims corpus features embed │ pure logic, no sqlite3
├─ ports ───────────────────────────────────────┤
│ repository protocols · chat/embed ports │ interfaces only
└─ infrastructure ──────────────────────────────┘
sqlite repos · Ollama clients · feedparser · bib/zotero readers
Dependencies point inward only. Domain logic names a protocol, never a
sqlite3.Connection.
Sequence and rationale
Order is set by dependency, not by appetite. Specs 2–5 all call into the service facades that spec 1 creates; building any of them first means building it against the current tangle and redoing the work.
| # | Spec | Depends on | Size |
|---|---|---|---|
| 1 | Onion refactor | — | Large, stageable |
| 2 | Citations domain | 1 | Medium |
| 3 | Experiment-tracker adapters | — (adapter seam exists) | Small |
| 4 | Swarm + swarm.toml |
1 | Large |
| 5 | Agent-config emitters | 4 | Small |
Spec 3 is independent and may be pulled forward at any time; it is listed third because it is small, not because it blocks anything.
1. Onion refactor
Fixes: tool-choice confusion, coding-agent confusion, no data layer.
Scope. Introduce ports as Protocol classes; implement SQLite repositories
behind them; move every raw SQL string out of domain and presentation modules
into infrastructure; add one service facade per domain; split mcp_server.py
into mcp/feed.py, mcp/knowledge.py, mcp/provenance.py, mcp/symbolic.py
with a thin mcp/__init__.py registering all four; namespace the tools as
feed.*, kg.*, runs.*, sym.*.
Why namespacing addresses tool-choice confusion. The same 36 tools, grouped into 4 domains of 6–10, turns one 36-way choice into a 4-way choice followed by a smaller one. The hierarchy the swarm was reached for is obtained here without spawning a process.
Staging. Each stage lands green and independently:
1. Ports + repository protocols, no callers changed.
2. SQLite repositories implementing them; db.py keeps migrations only.
3. Domain modules migrated off sqlite3 one at a time.
4. Service facades; cli.py and server.py retargeted onto them.
5. mcp_server.py split; tools namespaced. Old names alias for one release.
Open questions for the spec. Whether resolve_db_path precedence moves
into infrastructure or stays a module function; whether repositories are
per-table or per-aggregate; how _PROFILE_VEC_CACHE is expressed once rank.py
no longer owns a connection.
Success criteria. grep -rn sqlite3 src/attestation --include=*.py matches
only infrastructure. No module over ~400 lines. No deferred imports inside
function bodies. Full pre-commit gate green throughout.
2. Citations domain
Fixes: nothing broken; adds a domain. Doubles as the proof that spec 1's boundaries hold — if adding a domain is hard, spec 1 was wrong.
Scope. Four readers behind one CitationPort:
- Zotero — local SQLite / local HTTP at 127.0.0.1:23119. Offline. The
natural pair for the reading knowledge graph.
- BibTeX / BibLaTeX — parse and write .bib on disk. Offline. Pairs with
claims.py: a claim can cite a bib key.
- arXiv / CrossRef / Semantic Scholar — metadata lookup by DOI or arXiv ID.
Network. See the exception below.
- Pandoc / CSL — render citations into formatted output. Offline; a
formatting concern, kept at the presentation edge rather than in the domain.
Offline-guarantee exception. CLAUDE.md states "Local models via Ollama;
nothing leaves the machine." The third reader breaks that, so it is opt-in
behind a flag, default off, and CLAUDE.md gains an explicit note naming
which tools reach the network. The resolver records per-record whether metadata
came from disk or the wire, so a citation's own provenance is inspectable. A
stated guarantee that quietly stops holding is worse than one with a documented
exception.
Open questions. Whether citations become KG nodes or a sibling store;
whether claims.py gains a cite-key verdict; cache policy for network lookups.
3. Experiment-tracker adapters
Fixes: coverage. Small and independent.
Scope. New adapters reading local artifacts already on disk —
wandb/run-*/files/ and mlruns/ — through the existing
ledger_adapters seam. Prefer teaching generic these conventions over adding
named adapters, per that package's own docstring.
Explicitly out of scope. Calling W&B or MLflow servers, and writing runs
back into either. ledger.py opens by declaring itself "deliberately NOT an
experiment tracker… requires no change to how anything is run," with adoption
cost named as the design constraint. Reading local artifact directories honours
that; wrapping training or logging into a tracker inverts it. If bidirectional
sync is ever wanted, it needs its own spec arguing against this docstring, not
a quiet extension of this one.
Open questions. Whether mlruns/ metric-per-file layout fits generic's
conventions or genuinely defeats them; how tracker-declared metric directions
map onto the ledger's refusal to rank undeclared ones.
4. Swarm + swarm.toml
Fixes: internal orchestration.
Scope. Per-domain agents (ingest, rerank, knowledge, provenance, citations)
with a user-interaction agent as supervisor, defined in a swarm.toml that sits
alongside feeds.toml and is read by attestation itself. Repo-native and not
locked to any one agent runtime.
The determinism constraint — load-bearing. This project's value rests on the ledger being auditable and ranking being deterministic. Agents messaging each other produce nondeterministic call sequences. The constraint that keeps both true:
Agents may orchestrate only — choose which service calls to make and in what order. Every service call is pure Python. The supervisor writes the full call trace to the ledger, so any run stays reconstructable.
The swarm is a planner over an auditable substrate. No agent computes a rank, a metric, or a verdict; agents decide which deterministic function runs next.
Measure before building. Spec 1 alone may substantially resolve the confusion this spec exists to fix. Namespaced groups plus a split server address tool-choice and coding-agent confusion directly. Re-check whether the confusion persists after spec 1 lands before committing to this one — it is the expensive spec and the one that spends determinism.
Open questions. Message contract shape; whether agents run in-process or as subprocesses; failure and timeout semantics; how a partial swarm run is recorded in the ledger.
5. Agent-config emitters
Fixes: portability.
Scope. Generate runtime-specific agent definitions from swarm.toml —
.claude/agents/*.md with frontmatter for Claude Code, and whatever other
runtime is wanted. One schema, many consumers, so the definitions cannot drift.
Emitters are generated artifacts, never hand-edited.
Open questions. Whether emission is a build step, a CLI subcommand, or part
of install.py; how to detect a hand-edited emitted file.
Decisions recorded
- Subagents: yes, deliberately. Raised the determinism objection; the decision was made to build real subagents anyway. Spec 4 constrains them to orchestration so the audit trail survives.
- Experiment tracking: local artifacts only, per
ledger.py's stated non-goal. - Hermes configs: repo-native, not locked to Claude Code; emitters in spec 5.
- Citations: all four readers, with the network one opt-in and the offline-guarantee exception documented rather than silent.
What this roadmap does not do
It designs nothing. Each section names a problem, a scope, and its open questions so that its own brainstorm starts from a written boundary instead of a blank page. Spec 1 is the recommended next brainstorm; spec 3 is the cheapest if a quick win is wanted first.