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 one of the products was documentation. Not documentation as an afterthought, documentation as the thing people would actually come for, because the existing material for the platform we were building on was thin and out of date and everyone working on it knew it.
The writing went badly for a while, and when I finally sat down to work out why, the answer was not that writing is hard. My note from the time names one specific waste: I was spending too much time crafting perfect sentences that did not matter, because they were probably going to change anyway. Underneath that, the actual diagnosis, which is that I was mixing all the writing phases into one suboptimal pass that tried to do everything at once, including producing the final draft.
That is a mechanical problem rather than a discipline problem, and mechanical problems have mechanical fixes.
Writing in passes
The fix I wrote down is a compiler.
A compiler does not read your source once and emit a binary. It runs a sequence of passes, and each pass has one job and assumes the previous one finished. Parsing does not worry about optimisation. Optimisation does not worry about register allocation. The staging is what lets each stage be simple.
So the document pipeline became four stages, and the useful column is the second one, because each stage is 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. |
Written out like that, the constraint is visible: nothing in the first three rows says anything about quality, and nothing in the last row says anything about coverage.
The part I still use is what the note says about the last stage. Editing is where you revise the content and let your perfectionism play out, because that is the only stage where perfectionism is useful, and everywhere else it gets in the way and holds you back.
Perfectionism does not get banned. It gets scheduled. That reframe is the whole value of the pipeline, because the instinct to make a sentence perfect is not a character flaw to be suppressed, it is a useful capability firing at the wrong time. Give it a stage of its own and it stops sabotaging the three stages where the job is coverage rather than quality.
Phase separation at scale
The pipeline came out of a bigger version of the same idea, which I had written down four months earlier about design rather than prose.
I was in Korea for a couple of months with a plan to get the language project out the door, and I was stuck. The note is honest about where: I understood how to get everything accomplished and how to explain it to people, and 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.
The three phases I set myself were a brain dump of no less than thirty minutes with no time spent organising, then organisation, described as linking together disparate concepts and adding layers to those contexts, then presentation.
I did not want to work this way, and the note has me arguing with myself about it. What I wrote was that I was not comfortable with the method because I had always had small projects without collaboration where I could win all three steps simultaneously, and that doing that on a project of this size was untenable and would not produce clarity.
Read that as a claim about scale rather than about temperament. Phase separation is what a project costs once it outgrows one head. On something small you can hold the dump, the structure, and the prose in mind at the same time and nobody notices you are doing three things at once. Past some size you cannot, and the failure does not announce itself as overload, it announces itself as sentences that will not come out right.
Use-cases instead of examples
One line from a journal that July has outlasted everything else here: 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. The difference matters because a reader with a problem cannot search a list of examples for the one that matches, since 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
For contracts specifically, the plan was to write behaviour once in a form that a person and a test suite could both read, which meant behaviour-driven development and, after weighing a couple of options, Cucumber with its Gherkin syntax.
The reason I recorded is not the usual one. The note says to opt for behaviour-driven development because it focuses on simple communication, rather than because it improves coverage or catches more bugs. Which follows, if you think about who a contract specification is for. The people with the strongest opinion about what a contract should do are usually the ones who cannot read the Rust that implements it, so a specification written only in the implementation language is a specification the team can only check against itself.
Mechanical documentation of contracts
The second half of the work is generation, and it only works 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, and if it wants something else to happen it returns a list of messages describing what should be done, and then it terminates, and the chain carries those out afterwards. That rule exists for safety, because a contract that cannot be re-entered partway through cannot be attacked partway through.
The side effect is what I wrote down. 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.
Which means a handler is a straight line. There is no point in the middle where control leaves for somewhere unpredictable and comes back with the world changed. A straight line can be divided into steps, steps can be marked with comments, and comments in known positions can be parsed. So the plan was to 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. That is the sort of consequence you only find 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 for exactly the same reason.
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 are doing work there that ordinary prose cannot. A @warning marks a consequence
the caller needs before they call, in this case that the operation lets one user reach another
user's funds. A @constraint states a condition on a value, which is a fact a document can print,
a test can check, and a reader can rely on.
The argument for keeping this next to the code rather than in a separate document is stated directly in the note: generation saves time, it makes format and style consistent without anyone policing them, and it keeps things in sync because a change is made once. That last one is the only argument that really matters. Documentation kept beside code diverges from it, always, and the divergence is silent, and the reader who trusts the stale half is worse off than the reader who had nothing.
The annotation vocabulary is where this got to. What the note leaves open is the rendering: it has a heading reading "This should translate into:" followed by a TODO, which is where the output format would have gone. Choosing the annotations is the design decision, and choosing how they print is the part that depends on where you print them.
Decision records
In September I extended the same instinct from documents to the company, and it is the smallest idea here and the one that actually stuck.
What I wrote was that 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. And then the mechanism for spreading it: that 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. A policy that everyone must write decision records produces decision records written to satisfy a policy. Someone visibly doing it, where the output is useful enough that reading it is a favour to you, produces the thing itself.
I have a worklog in the repository I am writing this in, four years later, and it is the same artifact.