Documentation as JSON

ForkTex Engineering · September 5, 2026 · 4 min

Engineering documentation rots for a structural reason, not a cultural one.

The usual diagnosis is discipline: people did not keep it current. But look at what the format asks of you. A rule about how services handle errors is a prose paragraph. Its severity is implied by an adverb. Whether anything actually enforces it is a sentence someone wrote once and nobody has checked since. Its relationship to three other documents is a hyperlink that breaks when a file is renamed.

None of that is checkable. Prose has no schema, so nothing can tell you when a document has become false. Only a human re-reading it can, and nobody re-reads documentation they did not just write.

So we stopped writing it by hand. The source of truth is JSON, validated against typed models. The markdown everyone reads is generated from it.

Structure is what makes it checkable

A document is a record with required fields: a stable id, a title, a summary of real length, and a date. Its body is typed blocks. Rules, worked examples, links to other documents, open questions.

The unit that carries the weight is the rule:

{
  "id": "errors:one-envelope",
  "title": "One error envelope",
  "statement": "Every service serialises failures into the shared envelope.",
  "severity": "blocker",
  "enforcement": "tests/test_architecture/test_conventions.py",
  "rationale": "A client written against two error shapes handles neither."
}

enforcement is required and cannot be empty. That single constraint changes the character of the whole corpus.

A rule that nothing proves is a preference wearing a rule's clothes. Worse, an invented enforcement is more damaging than an honest weak one, because the next reader trusts it and stops checking. So "review" is a legitimate value. Plenty of real rules are enforced by a person noticing. Blank is not a legitimate value. If you cannot name what would catch a violation, you have written an opinion, and it should be marked as one.

Severity is an ordered enum rather than a label. A reader does not need to know what category a document belongs to; they need to know whether this specific rule binds them. Severity states that directly.

What we deliberately left out

Three fields that seem obviously necessary are absent.

There is no status. Status is metadata about a record's lifecycle and belongs to whatever stores records, not to the content of the document. Putting it in the document means every reader has to work out whether they are looking at something live.

There is no kind or authority. Both classified without informing. Knowing a document is "a standard" rather than "a guideline" tells a reader almost nothing actionable; knowing a specific rule is a blocker tells them everything.

The ids are handcrafted rather than derived. They used to be computed from the filename and the classification, which meant renaming a file broke every reference to it, and so did reclassifying a document, which is an editorial judgement that ought to cost nothing. An id that changes is not an id.

Markdown is an export, not a source

Generation produces flat markdown pages that people read in a browser or a terminal. They are output. Editing one is meaningless, because the next build overwrites it.

Which raises the obvious question: what stops someone editing one anyway, in a hurry, and the edit surviving?

The build regenerates every page and byte-compares it against what is committed. A hand-edited page fails the check.

That is the load-bearing mechanism in the whole design. Without it, generation is advisory: an edit works, nobody regenerates for a month, and the generated file quietly becomes a second author of the same fact. With no way to tell which of the two is right. With it, the edit fails immediately, while the person who made it is still there to redirect into the JSON.

Every generated page also carries a banner naming the command that produced it. A file that does not say it is generated will eventually be edited by someone acting in good faith.

The retrieval half

Generated markdown solves the human problem. It does not solve the machine one, and that turns out to be the more interesting half.

Feeding an agent an entire corpus is wasteful and, past a certain size, impossible. Reading everything in ours costs roughly sixty-seven thousand tokens. Most tasks need a fraction of that, and which fraction depends on the task.

So retrieval is a first-class part of the library, not something each consumer improvises:

  • Ask for the normative set and you get every rule. a few thousand tokens, unranked. Ranking here would be a mistake: dropping a blocker to save a few hundred tokens is a bad trade at any budget.
  • Ask a question and you get ranked, budgeted grounding. Length-normalised so the longest document does not win on sheer volume, with summaries injected instead of whole pages.
  • Filter by tag and the set narrows to what binds the language or subsystem you are actually in.

One property matters more than the ranking: when the budget forces something out, the response says what it dropped. A silently truncated context reads exactly like a complete one, and an agent given a partial rule set will confidently apply the subset it received. Naming the omission is the difference between a budget and a lie.

The architecture rule

The library has one structural constraint: its core never touches storage. No file paths, no open calls, no database.

Storage is a four-method interface, list keys, read, write, delete, over an opaque key. The file-backed implementation is the only one shipped, and an in-memory one is about ten lines. The test suite implements a second one specifically to prove the boundary is real rather than nominal.

The payoff is that the same corpus can live in a directory, a database, or an object store without the models knowing. The constraint that produced it was simply refusing to let a Path into the core.

What it costs

Authoring is slower. Writing a rule means deciding its severity and naming what enforces it, and both of those are decisions prose lets you avoid. That friction is the feature, but it is friction, and a corpus with two documents does not repay it.

The size where it starts paying is roughly where a corpus becomes something people cite at each other rather than read end to end. Where the questions become "does this bind me" and "what proves it", which are exactly the questions structure can answer and prose cannot.

We reached that point at about twenty documents. Under ten, a well-kept markdown directory is probably the right answer, and we would not pretend otherwise.