Featured image of post Three places a document can live

Three places a document can live

A spec, a how-to and a README walk into a repo. Only one of them should still be there in six months.

I opened a repo I hadn’t looked at properly in a couple of months (the CI/CD components, as it happens, though it could honestly have been any of them) and found twenty-two specs sitting in it. All in main. All in a folder next to the code. All looking, to anyone walking past, about as current as the code they were sitting next to.

And not one of them was wrong, which took me a minute to get my head round.

Nothing in that folder was a lie

As far as I could tell, all of them were true. True on the day it was written, describing a decision that really was made, and it’ll go on being true about that day forever. 0006 says we’re adding an OpenTofu provider cache and why, and we did, and there it is, cheerfully cached away.

The trouble isn’t accuracy. A file in main makes a claim just by being there, and the claim is this is how things are. Code makes that claim honestly, because if it stops being true the build goes red and someone fixes it. A spec from May makes the very same claim… with nothing at all holding it to account.

So you end up with a folder that ages like milk while looking like the fridge.

I hadn’t sorted any of this out because, until fairly recently, it had never once been a problem. When you write four specs a year, where you put them is a matter of taste and you can hold the lot in your head anyway (I could, at least, and I’m not sure that’s a boast).

Then the documents started arriving on their own

Agent sessions write specs. They write reports, and spike write-ups, and decision records, and they do it across a dozen projects at once, continuously, at a rate no human team of my size would ever produce, let alone a team of one man and a kettle. And that is good, I asked for it. Spec-first is the whole working method and I’m not about to complain that it worked.

But “put it wherever seems sensible” is a policy that survives about as long as one person is doing the putting. Multiply it out across ninety-odd repositories and several agents who have each, quite reasonably, formed their own view of what seems sensible, and what you’ve built is an estate you can’t navigate, one folder at a time, without ever making a decision about it.

Automation was what made me write the filing system down. I’m not sure that reflects especially well on me, but it’s what happened.

The question that sorts everything

There are three places a document can live in a project like mine. In the repository, in the forge wiki, or on the public docs microsite. Most teams pick one and shove the lot in it, and then argue about it later, usually in a thread with no winner (I’ve been in that thread, more than once, on both sides). I went looking for the rule that would sort mine and it came down to one question.

Does this document describe a moment, or the present state?

That one question sorts everything I have.

A spec describes a moment. Someone decided something, on a date, for reasons that made sense with the information available that morning. It is supposed to be frozen. Reading it later is reading history, and history that quietly updates itself isn’t history.

A how-to describes the present. If the command changes then the how-to is wrong the moment it changes, and it needs to change in the same breath. Reports go with the specs, obviously enough. And spikes turned out to be the case that settled it for me, because a spike is about the purest point-in-time document there is: we didn’t know whether this would work, we spent a day finding out, here’s the answer, and there’s no reason for anyone to go back and update it. If the answer changes you don’t edit the spike… you run another one.

What stays in the repo, and why the merge request decides it

I very nearly moved everything. Sat down, worked out how much would go, and then changed my mind on the development docs specifically, because they’re too valuable where they are.

The reason is the merge request.

A development doc that lives in the repository moves with the code. It’s in the same diff, under the same eyes, and, crucially, it can be rejected alongside the change it describes. “This is fine but you haven’t updated the how-to” is a sentence you can say in a review, and it works because both things are in front of you.

Take that doc out of the repo and you have severed it from the only mechanism that was keeping it true. I’ve never seen a merge request rejected because a wiki page had gone stale, and I’m not convinced anyone reads the wiki page at all. So the axis I’d have guessed at, if you’d asked me cold (internal versus external, or long versus short), isn’t the one that matters. What matters is whether the document needs to be able to fail its own review.

The bit left behind

What stayed behind, in the folder where the specs used to be, is a register. One page, every spec on it with its number and title and status and a link out to the wiki, and three sentences at the top explaining why none of them are here any more.

It was kinda an afterthought, and I suspect it’s the most useful page of the lot. A folder that simply vanishes is a mystery to whoever turns up next, and whoever turns up next is very often me, four months later, with no memory of any of this and a strong opinion about whoever deleted the specs. A pointer costs almost nothing and answers the question before anybody has to ask it.

Two small things fell out of the move that I liked. The specs lost their dated filenames and picked up canonical numbers instead. That sounds like tidying, but the date belonged to the moment and the number belongs to the decision, and once you’re linking to a spec from six other places you want the stable one. And the microsite got better by subtraction: a public docs site is a worse public docs site for having superseded internal decisions in it, and I don’t recall anyone ever asking to read our May thinking about token inputs.

And then the thing that isn’t a document

Three places for documents. There’s a fourth surface, though, and it holds the thing that isn’t one.

A spike leaves code behind. That’s rather the point of it: you didn’t know whether something would work, so you wrote the smallest ugly thing that would tell you, and now you know. The write-up goes to the wiki with the other point-in-time material, no argument. But the code that produced the answer has to go somewhere too, and for a while I did what I suspect most people do, which is leave it on a spike/whatever branch and feel organised about it.

That works for about a week. Then it becomes one more entry in the drift of stale branches nobody quite dares delete, and every branch sweep from then on either destroys it or has to be taught a special case for it. Neither is a good outcome for a thing whose entire job is to still be there in a year when someone doubts the verdict.

So spike code goes in a forge snippet, which sits outside the branch namespace altogether and cannot be swept up by accident.

Two details that are easy to get wrong and worth the sixty seconds. Use a project snippet rather than a personal one, because on GitLab only project snippets take comments, which means a colleague can argue with the verdict on the artefact itself instead of in a thread that will outlive its own link. And set the visibility explicitly, every single time. glab snippet create happens to default to private, but the API documents no default at all, so anything not going through the CLI had better say. A GitHub gist made without --public is “secret”, which sounds reassuring and means unlisted rather than access-controlled: anyone holding the URL can read the lot.

That gives you a chain that reaches the code. The spec records the decision, the report records the evidence, and the snippet holds the thing that produced it. There’s nothing left in the working tree and nothing rotting on a branch.

(There is, I suppose, a fifth place a document can live, which is inside the binary itself. But that’s a different argument and I’ve made it already.)

Worth remembering

Does this describe a moment, or the present state? Moments go where they can be left alone. The present goes where it can be reviewed.

I only got round to writing any of that down when the documents started arriving faster than I could file them. Now I look at it, that’s just what a spec would have said about itself…

Built with Hugo · Theme Stack designed by Jimmy