Rendered from docs/how-to/keep-derived-files-from-conflicting-in-parallel-pull-requests.md in the Headwater
corpus. Every document on this half of the site is typed by the taxonomy
the descriptor names: corpus.json.
Keep derived files from conflicting in parallel pull requests
Audience: an adopter of Headwater whose repository merges more than one pull request in a day.
headwater generate writes derived files, and each pull request that edits the corpus writes them again. A forge such as GitHub merges a pull request with no merge driver and reads no merge attribute. So when two open pull requests both change one derived file at the same lines, the second one shows a conflict. A pull request with a conflict runs no CI. This guide sorts your derived files into the ones you commit and the ones you compute.
Before you start
- You have adopted Headwater, and
headwater generate --checkpasses on your default branch. - You know which build step or deploy reads each file that
headwater generatewrites.headwater derivedlists the files. - To add a document on a second shelf with
headwater new, that shelf's kind needs an identifier. In the standard package,decisionhas one afterheadwater init.specificationdoes not, andheadwater new specificationrefuses until your overlay declareskinds.specification.identifierand its scheme. The refusal prints the two lines to add underadd:in.headwater/overlay.yml. Paste them, runheadwater taxonomy resolve, and runheadwater new specificationagain (#1264).
Steps
- Commit a file that a person reads on the forge. A shelf index, a register,
.headwater/taxonomy.lockand.headwater/corpus.jsonare in this group. Theverified_revisionstamps in your documents are in it too, because a person writes them. A shelf index holds one row for each document, so two branches that add documents at different places in it merge as text. - Compute a file that only a build step reads. The graph export is the usual case. It holds the whole graph, so it moves on nearly every edit, and a committed copy conflicts on most pairs of pull requests. Remove each
graph_exportentry from theadd_to: projections:block of.headwater/overlay.yml, and runheadwater taxonomy resolve. An entry that states afilter, atombstoneor a profile other thandefaultexists only on a declaration. Keep that entry, addcommitted: falseto it, and runheadwater taxonomy resolve. Thenheadwater generatedoes not write the file, and neither--checkrequires it. - Add the path of the export to
.gitignore, and delete the committed copy. For example, add.headwater/export.jsonand rungit rm --cached .headwater/export.json. - Compute the export in the step that reads it. Run
headwater export --format json > <path>in that step, before the reader starts. With nograph_exportdeclared, the one profile isdefault, so the command needs no--profile. For an entry that you kept withcommitted: false, runheadwater export --profile <name>in that step instead. It writes the file to the output path that the entry declares. - Run
headwater generate, and commit the result. The descriptor and the lock move once, because they record the projections that you declare. - Make sure that
.gitattributeshas the line.headwater/capture-cost.jsonl merge=union. Each run ofheadwater newadds one reading at the end of that file. So two branches that each add a document both change its last line, and git reports a conflict.unionkeeps the lines of both sides, and that result is correct for this file because no reading depends on another.headwater init --gitwrites this line, and the same line for.headwater/adoption.jsonl. It writes neither where a line of yours already names the file. If you ranheadwater init --gitwith an earlier release, run it again, or add the line by hand.
How to know it worked
headwater generate --check,headwater taxonomy resolve --checkandgit ls-files .headwater/export.jsonall agree: the first two exit 0 and the last prints nothing.- In a local merge or rebase, two branches that add documents to two different shelves with
headwater newmerge with no conflict. This includes a branch that edits a governed file and records a newverified_revision. - A forge merge of the same two pull requests can still conflict on
.headwater/capture-cost.jsonl. Nobody has measured whether GitHub appliesmerge=union. It did not apply-mergewhen this repository measured that attribute on 2026-09-24. - One conflict stays, on a shelf whose identifiers are numbers. Two branches that each run
headwater new decisionboth take the next number. The merge conflicts on the shelf index and on the claim file of that number under.headwater/ids/, which is by design (HW-DR-0054).headwater generatealone does not resolve it, because two documents then hold one identifier andheadwater check --strictexits 1.
If two branches took the same number
Do these steps on the branch that lands second. They are the same whether you merge the other branch into yours or rebase yours onto it.
- Take the claim file from the branch that landed first, by its name:
git checkout origin/main -- .headwater/ids/<scheme>/<identifier>. Do not use--oursor--theirs. A rebase swaps what those two names mean, and--theirsin a rebase writes your own claim over the landed one. That claim then names a file that is not on the tree, and neitherheadwater check --strictnorheadwater generate --checkreports it. - Give your document the next free number. Rename its file to the new number, and change its
idto match. - Run
headwater check --fix. It writes the claim file for the new number. - Run
headwater generate, and commit the result, or rungit rebase --continuein a rebase.headwater check --strictthen exits 0. - Read the claim file of the landed number. It names the landed document, not yours.