How we wrote and generated documentation at Terran One
Documentation was slow because I was doing every job at once. The fix was a compiler's. Separate the passes, then generate what the code can generate.
Motivation
I ran a small research lab building developer tools for smart contracts, and documentation was one of the products rather than an afterthought, and the thing people would come for, because the existing material for the platform we built on was thin and out of date and everyone working on it knew it.
The writing went badly for mechanical reasons:
- The waste. Time spent crafting perfect sentences that did not matter, because they were going to change anyway.
- The fault under it. All the writing phases mixed into one pass that tried to do everything at once, including producing the final draft.
A mechanical problem, and mechanical problems have mechanical fixes.
Writing in passes
The fix is a compiler. A compiler runs a sequence of passes, each with one job, each assuming the previous one finished, so parsing never worries about optimisation and optimisation never worries about register allocation. The staging is what lets each stage be simple.
Four stages, each defined by the single job it is allowed to do:
| Stage | Its one job |
|---|---|
| Brain dump | Empty everything out. No attempt to order it. The note calls it a storm. |
| Organisation | Arrange the pieces, drawn as a mind map. |
| Rough draft | String the organised content together so it hangs. |
| Editing | Make the sentences good. |
Nothing in the first three rows says anything about quality, and nothing in the last row says anything about coverage.
Editing is the only stage where perfectionism is useful, and everywhere else it holds you back. So perfectionism does not get banned, it gets scheduled. The instinct to make a sentence perfect is a useful capability firing at the wrong time, and giving it a stage of its own stops it sabotaging the three stages whose job is coverage.
Phase separation at scale
The pipeline came out of a bigger version of the same idea, written four months earlier about design rather than prose.
The stuck point was specific. I understood how to get everything accomplished and how to explain it to people, what remained was design-level work, and the engineering experiments I kept running were actively hindering progress because they required too much investment to answer a design question.
Three phases, same staging:
- Brain dump, no less than thirty minutes, with no time spent organising.
- Organisation, described as linking together disparate concepts and adding layers to those contexts.
- Presentation.
Phase separation is what a project costs once it outgrows one head:
- Small enough for one head. You hold the dump, the structure and the prose at once, and nobody notices you are doing three jobs.
- Past that size. The failure presents as sentences that will not come out right, not as overload.
The note argues the point against itself, which is how the scale claim got separated from the preference: small projects without collaboration let you win all three steps simultaneously, and doing that on a project of this size is untenable.
Use-cases instead of examples
The pipeline says when to do each job and nothing about what the content is made of. The principle came out of a journal entry that July: when you want to describe a complex tool, do not describe examples, describe use-cases.
- An example shows a thing the tool can do.
- A use-case names a situation the reader is in.
A reader with a problem cannot search a list of examples for the one that matches, because matching requires already understanding the tool well enough to know which example is theirs, which is the thing they came to find out.
Specifying behaviour once
A use-case is a situation stated in terms the reader already has, which raises the question of what language to state it in. The requirement: write behaviour once, in a form a person and a test suite can both read. Two candidates on the page, behaviour-driven development and RSpec, separated by an unusual criterion: simple communication, rather than better coverage or more bugs caught. That picks Cucumber with its Gherkin syntax.
The criterion is about readership, and it follows from who a contract specification is for. The people with the strongest opinion about what a contract should do usually cannot read the Rust that implements it, so a specification written only in the implementation language is one the team can only check against itself.
Mechanical documentation of contracts
Gherkin gets the specification out of Rust by hand. Getting documentation out of Rust without anyone writing it works only because of a property of the platform that has nothing to do with documentation.
Contracts on CosmWasm cannot call other contracts in the middle of their own execution. A contract handles a message, returns a list of messages describing what should happen next, and terminates; the chain carries those out afterwards. The rule exists for safety, because a contract that cannot be re-entered partway through cannot be attacked partway through.
The side effect is the useful part. Every message is handled by exactly one function, and that function is restricted to a procedural flow that can be explained in a linear fashion, precisely because that rule means contracts may only call other contracts at the end.
So a handler is a straight line, with no point in the middle where control leaves for somewhere unpredictable and comes back with the world changed. A straight line divides into steps, steps take comments, and comments in known positions parse. The plan: delineate the boundaries of the logical operations with comments, allow those steps a little hierarchy for complicated logic, and parse them out into the documentation.
A safety constraint made the code documentable by machine. You only find that by looking at what a restriction gives you rather than what it costs, and it is the same property that decided the shape of the simulator we built the same year, which had to be a scheduler rather than a debugger.
The annotation format
The other half of generation is putting the documentation where the thing being documented is. The sketch in my notes is a contract message type with the explanation attached to each field:
pub enum ExecuteMsg {
/// Increases the allowance of user at `address` by `amount`.
/// @warning this allows users to access funds from another user
IncreaseAllowance {
/// The amount by which to increase.
/// @constraint must be less than
amount: Optional<Uint128>,
address: Addr,
},
...
}Two annotations do work ordinary prose cannot:
@warningmarks a consequence the caller needs before they call, here that the operation lets one user reach another user's funds.@constraintstates a condition on a value, which is a fact a document can print, a test can check, and a reader can rely on.
Three arguments for keeping this next to the code, in ascending order of how much they matter:
- generation saves time
- it makes format and style consistent without anyone policing them
- a modification needs to be performed only once
The third is the only one that survives scrutiny, and it is narrower than it looks. An annotation beside the code is hand-written and goes stale like any other comment. What generation removes is every further copy: a reference page, a guide and an SDK document derived from one annotation cannot disagree with each other or with their source. Divergence lives in the copies.
The rendering is left open. The note has a heading reading "This should translate into:" followed by a TODO. Choosing the annotations is the design decision; choosing how they print depends on where you print them.
Decision records
Keeping a record beside the work that produced it does not stop at code. The same move applies to a company, which is where I took it that September, and it is the smallest idea here.
At the micro level it starts with a work log, or an architecture decision record, of your own design decisions, tracking your thought processes every day during work. The spreading mechanism: as the person running the company, what I do propagates to other people as an example, so if I document my thought processes and specs relentlessly, the practice might become the culture.
Propagation by example rather than by policy, and the difference is not cosmetic:
- By policy. Everyone must write decision records, which produces decision records written to satisfy a policy.
- By example. Someone visibly doing it, where the output is useful enough that reading it is a favour to you, produces the thing itself.
The repository this site is written in carries a worklog, and it is the same artifact.