Legible by Construction — Automated Docs, Indexes, Logging, and Test-Centric Design
Published
Project article; see sources and editorial standards.
A technical review for practitioners. As of 2026-06-17.
Each section states the general principle and the industry evidence behind it. Code shapes are illustrative sketches.
0. Thesis
For most of software’s history, four classes of artifact were treated as secondary to “the real code”: documentation, indexes, logs, and tests. You shipped the feature; you wrote the docs if there was time, added logging when something broke, and backfilled tests under duress. That ordering was always a mistake, but you could survive it because the only consumer of the system was a human who could read the source.
That assumption no longer holds. A large language model now reads, writes, and operates code alongside the human, and it does so under a hard constraint the human does not share: a bounded context window. An agent cannot hold a large repository in its head. It sees what you index for it, reasons over what you document for it, recovers from what you log for it, and is trusted only as far as what you test for it. The secondary artifacts have become the primary interface.
This review argues one position across six topics:
Documentation, indexes, logs, and tests are load-bearing architecture. Their job is to make a system legible (understandable without reading all of the source) and verifiable by construction (correct because of how it is built, not because someone remembered to check). Dual-moding a single core across CLI, API, and application surfaces is the structural keystone that makes legibility and verifiability cheap instead of aspirational.
The six core sections build on each other: automated docs, then docs as the contract, then the index (the agent’s view of the docs), then logs (the runtime feedback signal), then tests, then the design pattern that ties testing to dual-moding. Section 7 is the honest bill of costs; Section 8 ties the six together.
1. Automated documentation
Principle
Treat documentation as a build artifact, not a hand-maintained sibling of the code. Anything derivable from a source of truth — an API surface, a CLI’s --help, a config schema, a dependency table, an inventory of capabilities — should be generated from that source on every change, never transcribed by hand. Hand-transcription has a half-life: the moment a human copies a fact from code into prose, the two begin to diverge, and the divergence is invisible until a reader is burned by it.
The discipline that makes this work is a hard generated-vs-handwritten boundary. Every file is unambiguously one or the other. Generated files carry a banner that forbids hand-editing and name the generator. Handwritten files never contain a fact that a generated file could own. The two never mix in the same file, because a file that is “mostly generated but with a few manual tweaks” is a file that will be silently clobbered or silently stale, and you won’t know which.
The industry has converged here over two decades: OpenAPI/Swagger generating client SDKs and reference docs from a spec, TypeDoc/JSDoc/Sphinx autodoc lifting docs out of type signatures, the “docs as code” movement (treat docs like source: version them, review them, build them in CI). Diátaxis (Daniele Procida) adds the orthogonal insight that type matters — tutorials, how-tos, reference, and explanation have different jobs and should not be blended — which tells you what to generate versus what to write by hand: reference is generable, explanation rarely is.
The failure mode that matters: staleness
Automated docs do not fail by being wrong on day one. They fail by drifting. The single most valuable thing you can build is not the generator — it is the staleness gate: a CI check that fails the build when a generated doc no longer matches its source. Without that gate, “generated” is a comforting label on a file that quietly rots.
A gate is also only as good as its coverage: it guards exactly the facts it checks, and a prose document that no gate watches can drift while the gated artifact stays correct.
2. Documentation as architecture
The previous section made docs track the code. This section inverts the arrow: the best documentation constrains the code. It is not a description of the system written after the fact — it is the contract the system is built to satisfy, and it is enforced.
Three established forms of this:
- Architecture Decision Records (Michael Nygard, 2011): short, immutable, dated records of why a structural choice was made and what was rejected. An ADR is architecture-as-documentation because the next person — or agent — proposing a change reads the ADR and is bound by its reasoning until they explicitly supersede it. The decision lives in the doc, not in the tribal memory of whoever was in the room.
- The C4 model (Simon Brown) and arc42: a fixed, leveled vocabulary for describing structure (context → container → component → code) so that “the architecture” is a navigable artifact rather than a whiteboard photo.
- API-first / contract-first: the OpenAPI document, the protobuf
.proto, the GraphQL schema is written first and the implementation conforms to it. The doc is executable truth; codegen and contract tests make conformance mechanical.
What ties these together, and what is new, is the agent-facing spec. Files like CLAUDE.md, AGENTS.md, and .cursorrules are documentation that an LLM reads as operating instructions. When such a file says “every new tool must be registered in all the places that have to agree,” that sentence is not describing the system — it is a constraint on every future change, read and obeyed by both humans and agents. Documentation has become an interface with enforcement semantics.
The litmus test for whether a doc is architecture: if you change the code in a way that violates the doc, does something break? If yes, the doc is load-bearing. If no, it is commentary — useful, perhaps, but not architecture.
3. Indexes to expand LLM coding capacity
This is the section where the agent’s constraint becomes the architecture’s problem. An LLM coding agent is bounded by its context window and degrades as that window fills — the “context rot” effect where relevant facts get buried in irrelevant ones. The entire discipline of agentic coding is, at bottom, the management of what enters that window and when. An index is the data structure that answers “what is the relevant slice of this large repo for the task in front of me right now?”
The field has produced several concrete approaches, and the tension between them is the interesting part:
- Repo maps. Aider (Paul Gauthier) builds a tree-sitter parse of the repository, extracts the symbol skeleton (signatures, not bodies), and ranks symbols by a PageRank over the reference graph so the most-depended-on definitions surface first. The agent gets a compressed, navigable map of the whole repo instead of a random sample of files.
- Precomputed embeddings. Cursor and others chunk the codebase along AST boundaries, embed the chunks, sync incrementally via a Merkle tree, and retrieve by vector similarity. This is RAG applied to code.
- Agentic search. Anthropic’s Claude Code notably dropped embeddings in favor of letting the agent drive
ripgrepand file reads directly — the argument being that a good agent with good search tools out-performs a stale vector index, and avoids the indexing/sync/eviction machinery entirely.
The debate (precomputed retrieval vs. agentic grep) is not settled and probably shouldn’t be — they trade off freshness against latency, and infrastructure cost against per-query cost. The practitioner takeaway is not “pick the winner.” It is: decide, explicitly, what your agent’s index is, how it stays fresh, and what happens when it’s stale — because there is always an index, even if it’s just “the agent greps.” An unmanaged index is the one that hurts you.
A second, underrated point: indexes are not only for retrieving source. An index of capabilities — the tools, operations, and commands a system exposes — is what lets an agent act, not just read. A tool inventory is a retrieval target exactly like a repo map; it answers “what can I invoke?” instead of “what is defined?”
The architectural claim worth stealing: treat the context an agent needs as a first-class, file-backed, version-controlled, freshness-gated asset — the same way a database treats an index. Retrieval quality is a property you build and test, not a prompt you hope works.
4. Automated logging
Logging earns the word “automated” only when it is a structural property of the system, not a habit you rely on developers to remember. The two design commitments that get you there:
Instrument at the boundary, not in the body. Cross-cutting concerns — timing, status, argument capture, error classification — belong in a single wrapper that every operation passes through, not sprinkled by hand into each handler. A decorator/middleware that wraps every registered operation guarantees coverage: a new operation is instrumented because it cannot be registered any other way. Hand-placed log.info() calls guarantee the opposite — the one path nobody instrumented is the one that pages you at 3 a.m.
Logs are structured data, not prose. Emit events with fields (operation, status, duration, correlation id), not human sentences. This is the “canonical log line” / wide-event discipline (Brandur; Honeycomb’s observability model, Charity Majors) — one structured event per unit of work, queryable after the fact. Prose logs are write-only; structured logs are a dataset. And redaction is a default, not a step: the wrapper strips secrets and PII before anything hits disk, because a log pipeline that depends on every caller remembering not to log a token will eventually log a token.
The new dimension is that logs are now a feedback signal for two consumers. A human reads them to debug. An agent reads them to self-correct — a failed tool call whose structured error says “connection refused, retryable” is a recovery instruction, not just a record. Designing logs as data with explicit failure classification turns your observability layer into the substrate for automated remediation.
5. Testing
The reflexive image of testing is the test pyramid (Mike Cohn): many unit tests, fewer integration tests, fewest end-to-end. The more useful modern framing is Kent C. Dodds’s testing trophy (and Spotify’s honeycomb): weight toward the integration band, because tests that exercise real collaborations catch the bugs that matter while staying fast enough to run constantly. The shape matters less than the principle underneath both: test where the behavior lives, and test it through a stable interface.
That second clause is the one teams get wrong. Tests coupled to implementation (private methods, internal call order) break on every refactor and train the team to distrust the suite. Tests coupled to a stable contract survive refactors and actually enable them. So:
- Test the core, not the transport. Business logic should be reachable and assertable without spinning up an HTTP server, a message bus, or a UI. If asserting a rule requires booting a web framework, the rule is in the wrong place.
- Contract tests at the seams. Where two components meet (client/server, service/adapter), test the contract explicitly (the Pact model) so either side can change independently as long as the contract holds.
- Determinism and golden files. A test that flakes is worse than no test — it teaches the team to ignore red. Pin time, seed randomness, and use golden/snapshot files for large structured outputs so a diff is a decision, not a mystery.
- Make invariants executable. The strongest “test” is often not in the test suite at all — it is a CI check that asserts a structural property (the generated doc matches the code; the committed index matches the live one). These are property tests over the repository itself.
6. Test-centric design via dual-moding (CLI, API, application)
This is the keystone. Everything above gets cheaper — or stays expensive — depending on one structural decision.
Build the system as a single pure core surrounded by thin adapters. This is Ports and Adapters / Hexagonal Architecture (Alistair Cockburn, 2005), the same idea at the heart of Clean Architecture: the domain logic depends on nothing outward; the outward surfaces (a CLI, an HTTP API, an RPC transport, a GUI) depend inward on the core through well-defined ports. The core has no idea whether it was invoked from a terminal, a socket, or a button.
The payoff is usually sold as flexibility. The deeper payoff is testability and anti-drift:
- The core is unit-testable with zero I/O. No server to boot, no UI to drive. Tests call functions and assert returns. This is why §5’s “test the core not the transport” is a consequence of this design, not a separate rule.
- Each adapter is thin enough to contract-test. An adapter that only translates (parse args → call core → format output) has almost no logic to get wrong, and what little it has is testable in isolation.
- The surfaces cannot drift, because there is only one implementation. When the CLI, the API, and the app are three façades over one core, a behavior change lands in all three at once. When they are three codebases, they diverge — and the divergence is where the bugs and the “works in the API but not the CLI” tickets live.
Then comes the move that makes this test-centric: the CLI is the most honest test harness you have. A core that is fully exercisable from the command line is a core you can script, diff, fuzz, and drive in CI without any test-framework ceremony. kubectl is a thin CLI over the same API the Kubernetes dashboard uses; the Stripe CLI drives the same API as the SDKs. The Unix tradition of composable programs over text streams was test-centric design before the term existed, because a program you can pipe is a program you can assert against. If a capability is awkward to invoke from a CLI, that awkwardness is a design smell in the core, surfaced early and cheaply.
The ordering inverts the usual one. Instead of “build the app, bolt on an API, maybe add a CLI,” you build the core, expose it through the cheapest-to-test surface (the CLI) first, and treat the API and app as additional adapters. Testability stops being something you add and becomes the shape of the thing.
┌─────────────────────────┐
CLI ───────► │ │
HTTP API ───► │ pure core (domain) │ ◄─── unit tests call here directly,
MCP/RPC ────► │ no I/O, no transport │ no transport to boot
App/UI ────► │ │
└─────────────────────────┘
each adapter: parse → call core → format. thin, contract-tested, can't drift.
For a system that agents operate, an MCP server over the same core is, in effect, the agent’s CLI: the same honest, scriptable surface that makes the core testable also makes it agent-operable.
7. Costs and anti-patterns
A practitioner review that only sells the upside is marketing. The honest bill:
- Generated docs add build surface. A generator is code: it has bugs, it needs its own tests, and a broken generator blocks the build. The staleness gate that makes generated docs trustworthy is also one more thing that turns CI red. Worth it above a threshold of doc volume and churn; over-engineering for a five-page project.
- Indexes add a freshness liability. Every precomputed index (embeddings, repo maps, file analyses) is a cache, and caches go stale. If you build one, you owe a freshness gate (a CI check that the committed index matches its source is the pattern); if you can’t afford the gate, agentic grep may genuinely be the better call. “We added embeddings” without “we test that they’re current” is a latent staleness bug.
- Middleware instrumentation can over-capture. Wrapping every call is total coverage, but it is also a place to accidentally log too much, redact too little, or add latency to the hot path. The wrapper is high-impact and therefore high-blast-radius; it deserves its own tests and a clear redaction policy.
- Dual-moding has real overhead. Ports and adapters is the right default for a system with more than one consumer, but it is ceremony for a script with exactly one. The adapter layer is code to maintain, and a too-eager abstraction (a “port” with one adapter that will never have a second) is just indirection. Apply it when a second surface is real or clearly coming — not speculatively.
- The meta-risk: a gate is only as good as its coverage. A gated artifact can stay correct while the ungated prose around it rots. Every “this can’t drift, it’s automated” claim is scoped to exactly what the automation checks. Know the edges of your gates.
8. Synthesis
The six topics are not six best practices. They are one architecture seen from six angles.
A system built this way generates its reference docs and gates their freshness (1); treats its key docs as enforced contracts rather than commentary (2); maintains explicit, fresh indexes so a bounded-context agent can find what it needs (3); instruments every operation at the boundary and emits structured, classified, redacted events that humans and agents both act on (4); tests its core through stable contracts and promotes structural claims to executable CI invariants (5); and achieves all of the above cheaply because it is one pure core behind thin, dual-moded adapters (6).
The unifying property is the one in the title. Such a system is legible: a human or an agent can understand it without reading all of it, because the docs, the index, and the logs are designed to be read. And it is verifiable by construction: its correctness comes from structure (the boundary, the gate, the contract, the single core) rather than from anyone’s vigilance.
That distinction is the whole argument. Vigilance does not scale, does not survive turnover, and emphatically does not survive an agent that has never met your team’s unwritten rules. Structure does. As more of our code is read and written by systems that cannot hold the whole repository in their heads, the artifacts that make a system legible and self-verifying stop being good hygiene and become the architecture itself.