<!-- llms-explorer twin of https://llms-explorer.com/blog/legible-by-construction-automated-docs-indexes-logging-and-test-centric-design/ · generated 2026-09-10 -->

# Legible by Construction — Automated Docs, Indexes, Logging, and Test-Centric Design

A practitioner review arguing automated docs, doc-as-contract conventions, retrieval indexes, structured logging, and test-centric dual-moded design make a codebase legible/verifiable, grounded in mdb-context-hub.

*A technical review for practitioners. As of 2026-06-17.*

Grounding is **hybrid**: each section states the general principle and the industry evidence, then an **In practice** note shows how `mdb-context-hub` (and the wider TAM tooling) implements it. Code shapes are illustrative sketches unless attributed to a specific file.

---

## 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 600-file 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.

### In practice — `mdb-context-hub`

The repo runs a deterministic sync pipeline (`scripts/sync-skill-pack.mjs`, invoked as `npm run sync:skills`) that pulls upstream docs plus repo-local sources, normalizes them, and writes JSON registries and generated Markdown. Every generated output carries the literal banner:

```
<!-- Generated by scripts/sync-skill-pack.mjs. Do not edit by hand. -->
```

The boundary is documented as a first-class convention in `CLAUDE.md` ("Generated files stay generated. If a file already has the sync banner, update the generator instead of editing the output"), and the source roots are environment-configurable (`MDB_CASE_ASSISTANT_DOCS_DIR`, `MDB_TAM_DOCS_DIR`) so the generator is portable rather than pinned to one machine.

The staleness gate is real and it is the most instructive part. `.github/workflows/ci.yml` boots the server, calls `buildToolInventory()` against the live tool set, and diffs it against the committed `docs/tool-inventory.json`. If the counts or the tool names disagree, the build fails with "tool-inventory.json is stale." The doc cannot drift from the code without turning CI red. (Worth noting the limit of the technique: the gate covers exactly what it checks. As of this writing, `README.md` and `docs/ARCHITECTURE.md` still read "122 tools across 22 domains" while the gated `tool-inventory.json` correctly reports 123 across 23 — the prose docs drifted precisely because no gate watched them. A generated-doc strategy is only as good as the set of facts it actually gates.)

---

## 2\. Documentation as architecture

### Principle

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 "new tools must be registered in three files together," 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.

### In practice — `mdb-context-hub`

`CLAUDE.md` is not a README; it is an enforced contract. It states invariants like "new tools must be reflected in three places together — `service.ts`, `server.ts`, and `tests/mcp-server.test.ts`," "use `instrumentedRegisterTool` (never `server.registerTool` directly) so every call is logged," and "keep `package.json` and `mcp-server/src/constants.ts` versions aligned." Each of these is backed by something that breaks on violation: the tool-inventory CI gate catches an unregistered tool, the telemetry wrapper is the only registration path the server exposes, and a version mismatch surfaces in `tam_status`.

The repo also ships `docs/ARCHITECTURE.md` (the canonical C4-style diagrams the project briefing reproduces), `docs/high_signal_file_index.json` (a machine-readable orientation map), and `docs/external-calls.md` (the audited list of every outbound network call). The last is a good example of doc-as-contract: it is not "here are some calls we make," it is the authoritative register against which the operations audit runs. Add an outbound call without registering it there and you are out of compliance with a documented standard, not merely under-documented.

---

## 3\. Indexes to expand LLM coding capacity

### Principle

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 600-file 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 `ripgrep` and 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?"

### In practice — `mdb-context-hub`

The repo is, end to end, **an index built so that LLMs can do more than their raw context allows** — that is its entire reason to exist. It is worth reading as a worked catalog of index types:

- **A capability index.** `docs/tool-inventory.json` is the machine-readable list of all 123 tools across 23 domains, generated from the live server and CI-gated for freshness (§1). An agent reads it to learn what it can invoke.  
- **A semantic index over knowledge.** The 664-skill registry is embedded locally with `@huggingface/transformers` (`npm run embed:skills`), and `tam_recommend_skills` / `tam_search_all` retrieve by relevance — the embeddings-RAG approach, run entirely on-device. An `embed:check` CI step keeps the committed embeddings in sync with skill text, which is the freshness gate the embeddings approach lives or dies on.  
- **A per-file source index.** The file-analysis library (`tam_save_file_analysis`, `tam_list_file_analyses`, and the `repo-file-analyzer`) stores per-file summaries and crawl state so an agent can locate code by purpose without re-reading the tree — the repo-map idea, persisted and queryable.  
- **A bounded retrieval surface.** Even the input limits are part of the index design: queries cap at 500 characters and context reads at 20,000 (`utils.ts`), so a single retrieval can't blow the window — context budgeting enforced at the schema boundary.

The architectural claim worth stealing: the hub treats "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

### Principle

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.

### In practice — `mdb-context-hub`

This is the repo's strongest pattern. Every tool is registered through `instrumentedRegisterTool` (from `telemetry.ts`), and `CLAUDE.md` makes "never call `server.registerTool` directly" an enforced invariant — so instrumentation coverage is total by construction. The wrapper records status, duration, redacted arguments, captured console output, and suggested fixes, and persists them to `file-analysis/telemetry.jsonl`. That history is queryable in-band through `tam_get_call_history`, `tam_get_call_card`, and `tam_resync_call`, and feeds dashboard call-cards.

Two things make this more than logging. First, **redaction lives in the middleware**, so it is a property of every call rather than a discipline each tool author must observe. Second, the captured "suggested fixes" plus a dedicated `error-log` domain and the operations registry's safe/escalate remediation dispositions close a loop: a failed call produces a classified, structured record that downstream tooling (and agents) can act on. The log is not the end of the story; it is the input to the next decision.

---

## 5\. Testing

### Principle

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.

### In practice — `mdb-context-hub`

The suite (eight files under `tests/`) is split exactly along the "test the core, not the transport" line. `tests/mcp-server.test.ts` exercises the service layer directly — and it can, because `service.ts` contains no MCP types (more on why in §6). `tests/skill-pack.test.ts` covers the sync generator. The scoring and semantic-retrieval logic gets its own dedicated suites (`score-core`, `semantic`, `semantic-rerank`), which matters because retrieval quality is a behavior, not an incidental — if you claim "relevant skills surface first," that claim needs a test.

The executable-invariant idea shows up in CI rather than the suite: `embed:check` asserts the committed embeddings match the skill text, and the tool-inventory gate asserts the doc matches the live tools. Neither is a "unit test," but both are tests in the only sense that counts — a structural claim the build refuses to let you violate.

---

## 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.

### Principle

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.
```

### In practice — `mdb-context-hub` and the TAM tooling

The hub is a clean instance of the pattern. `service.ts` is the pure core — it implements every operation (search, recommend, bundle, optimize, save, analyze) and, by deliberate rule, contains **no MCP types at all**, so it is testable in isolation (which is exactly what `tests/mcp-server.test.ts` does). `server.ts` is the thin registration adapter. `index.ts` (stdio) and `http.ts` (loopback HTTP) are two transports over that one server — the same capability, dual-moded, with no second implementation to keep in sync.

The wider TAM tooling extends the same spine to a third and fourth surface: the **operations registry** (40 registered operations, invoked through `tam_ops_run` / `tam_ops_audit` / `tam_ops_remediation`) is a uniform invocation layer designed so the same operation can be driven from MCP, from an HTTP call, and from a CLI, with the dashboard as the application surface on top. That parity — one operation, many front doors — is the design goal that makes the whole system scriptable in CI, callable by an agent, and clickable by a human without three divergent implementations. The MCP transport is, in effect, the agent's CLI: the same honest, scriptable surface that makes the core testable is the one that 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 (`embed:check` 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.** The `122 vs 123 tools` drift in this very repo is the cautionary tale — the gated artifact stayed correct while the ungated prose docs rotted. 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.

---

*Companion reading in this repo: `docs/ARCHITECTURE.md` (the C4 diagrams and ADRs), `docs/external-calls.md` (the audited outbound-call register), and the project briefing at `05 - MDB Context Hub — Project Briefing.md`.*