A pull request on the Dojo came back marked conflicting. It was the second of two slices of the same feature, a quota that stops a free cohort adding a sixth member, and while it sat open its sibling slice had merged to the trunk. So far this is the most ordinary story in software. What made it worth stopping over was where the conflict landed.
Both branches had edited participants.ts. Both had added tests to the same test file. Git merged all of it without a murmur. The single file it could not reconcile was APP-BEHAVIOUR.md, a document describing what the application does, which neither author had opened, and into which neither had typed a single character.
What the document is
APP-BEHAVIOUR.md is generated. A script walks the integration tests, reads their names, and writes them out as a list of the things the application is known to do. Each entry carries the line in the test file where the guarantee lives, so a reader can go and see it:
**write authorisation**
- Creating a participant requires an instructor session. (:423)
- Updating a participant is self-or-instructor only. (:448)
That parenthesised number is the whole problem, and it will take a moment to see why, because at first glance it looks like the most innocent thing on the page.
The arithmetic
The first slice, already merged to the trunk, had added a route for administrators to change a cohort's plan. Its tests went in near the bottom of the test file, fifty eight lines of them, sitting just above the authorisation block. So when its author regenerated the document, every entry below the insertion moved down by fifty eight. Line 423 became line 481.
The second slice, still on its branch, added the quota tests. Those went in much higher up, beside the other creation tests, sixty four lines of them. Its author regenerated too. Line 423 became line 487.
Now ask git to merge the two. It sees one line in the common ancestor and two different replacements, and it does the only honest thing available to it:
<<<<<<< HEAD
- Creating a participant requires an instructor session. (:481)
=======
- Creating a participant requires an instructor session. (:487)
>>>>>>> Enforce the members quota on member creation
Sit with that for a second, because the interesting fact is not that git could not choose. It is that both choices are wrong. In the merged world both blocks of tests exist, both insertions are above the authorisation block, and the guarantee lives at line 545. Four hundred and eighty one is what the line would be if the quota work had never happened. Four hundred and eighty seven is what it would be if the plan route had never happened. The number the reader needs is on neither side of the conflict, and no amount of care in resolving it could have produced 545, because 545 is not text that either branch contains. It is a fact about a file that only exists after the merge.
This is the moment the whole thing turns over. A conflict usually means two people disagreed and a human must decide. Here nobody disagreed. Nobody even had an opinion. Git did its job perfectly and the result was meaningless, because the thing it was asked to merge was not the truth. It was a photograph of the truth, taken from two positions, and you cannot combine two photographs by choosing one.
A generated file is a cache
We have written before, in The fix was merged, but the numbers were still wrong, that derived data is a cache and caches go stale. That post was about a database column. This is the same principle wearing different clothes, and the version control system is where it bites hardest, because git will cheerfully let you hand resolve a cache.
The rule underneath is short. You do not merge derived state. You invalidate it and derive it again. Whether a merge conflict appears is not the signal. The absence of a conflict would have been worse: had the two slices inserted their tests far enough apart, git would have merged the document cleanly, produced a file where half the line numbers pointed at the wrong tests, and nobody would have seen a marker to warn them. The conflict was a courtesy.
So the resolution was never going to be a decision between 481 and 487. It was to put the branch on top of the current trunk, and run the script:
git fetch origin
git rebase origin/main # take either side of the document
cd server && npm run docs:behaviour
The generator reads the merged test file, counts, and writes 545. It could not have written anything else. There is no judgement involved and therefore no opportunity for judgement to fail.
Three ways to stop it recurring
The narrow question is what to do about this file. The honest answer is that there are three moves, and their costs are different.
Do not commit the artefact. Generate it in the pipeline, publish it somewhere readers can find it, and let the repository hold only the tests it is derived from. This is the purest answer and it is the one we did not take, because the document earns its place in the repo. People read it in pull requests, and a diff that shows a behaviour appearing is worth seeing.
Teach git that the file is not text. A line in .gitattributes can point the file at a merge driver that resolves by taking either side, on the understanding that regeneration follows:
docs/product/APP-BEHAVIOUR.md merge=generated
This works. It also has a catch worth naming, because it is the sort of catch that quietly undoes tooling: the attribute is committed but the driver behind it is not, so every developer has to configure it once in their own clone. A convention that each person must remember to install is a convention that will be missing on somebody's machine on the day it matters.
Make the build refuse. The pipeline already runs the generator and fails when its output differs from what was committed. That check is a single line in the workflow, it does not care whether you forgot to regenerate after a rebase or after a merge or after writing a test at four in the afternoon, and it cannot be missing from anyone's machine because it does not run on anyone's machine.
We keep all three considerations in view and lean on the third, for the reason we set out in From convention to plugin. A rule that lives in a document is a rule people will forget. A rule that lives in the build is a step they cannot skip by accident. The gate does not make the conflict less likely. It makes resolving the conflict wrongly impossible to ship.
The lesson
Every merge conflict is a question git asks when it cannot see enough to answer. What this one exposed is that git can only ever see the characters in the file, and for a generated artefact the characters are downstream of a truth git has no access to. Ask it to reconcile them and you get a well formed lie.
So carry two questions into your own repository. For each file under version control, ask whether a human wrote it, or whether a program did. And for every file a program wrote, ask what happens on the day two branches regenerate it. If the answer is that somebody resolves the conflict by reading carefully and choosing the better looking side, you have a bug waiting for the afternoon when they are tired.
There is a larger thread running out of this. The two slices only collided because they were apart long enough to. That is a question about how often you integrate, and it is where this series goes next.

