Exact-count mirrors collide silently on merge¶
Problem¶
scripts/tests/setup/guard-count.txt holds one number, asserted for exact equality against
the guards discovered on disk. It exists so a guard that stops being collected cannot hide
(SPEC-183). That design is right, and it has a merge-time failure mode that no conflict marker
will ever show you.
Measured on SPEC-193, twice in one day:
- The branch added two guards and moved the count 96 → 98.
origin/mainmeanwhile added guards of its own and moved the same file 96 → 103. Merging produced a conflict only because the two numbers differed. Taking either side would have been wrong: main's 103 ignores the branch's two, the branch's 98 ignores main's seven. The correct value, 105, exists on neither side and has to be counted from the merged tree. - The second merge repeated it at a different scale: main had moved to 105, the branch still read 105 from the first merge, and the merged tree held 107.
The dangerous variant is the one that does not conflict. If two branches each add exactly one
guard, both write N+1. Git sees the same content on both sides, merges cleanly, and the file
now under-counts by one — silently, with every guard still green until someone deletes a guard
and the number happens to match again.
The neighbouring case is assertion-floor.txt: both sides append rows, git conflicts on the
adjacent lines, and the resolution is a union — never a choice.
Solution¶
Never resolve an exact-count mirror by choosing a side. Re-derive it from the merged tree.
# after resolving every other conflict, with the merged tree on disk:
ls scripts/tests/setup/[0-9]*.sh | wc -l # what is actually there
# write that number into guard-count.txt, then verify both directions:
tail -1 scripts/tests/setup/guard-count.txt
And for the append-only neighbour, take the union in the file's own order rather than one side:
Three checks that catch the silent variant before it ships:
- Count, then compare —
ls | wc -lagainst the file, in the merged tree, as its own step. Not "the merge went fine". - Map both directions — every floor row resolves to exactly one file and every file to one row. A one-sided check passes while the other side rots.
- Re-run the guard that reads it after the merge, not only the full suite before it.
Prevention¶
- Treat any file whose comment says "exact", "bump this in the same commit" or "not a grow-only floor" as merge-hostile by construction. It is a single scalar describing a set that two branches can both grow.
- When a guard's number is a derived fact, prefer deriving it at test time over storing it — storing it is a deliberate trade (it catches a collection failure that derivation cannot), so the storage needs this merge discipline as its price.
- Say the number out loud in the merge commit, with where it came from: "recomputed from the
merged tree: 107 = main's 105 plus this branch's two". A reviewer can check that sentence; they
cannot check a silent
git checkout --theirs.
Related¶
count-reconciliation-single-source.md— the N copies / N−1 checks shape this is a merge-time instance of.- SPEC-183's own reasoning in
scripts/tests/setup/guard-count.txt, on why the file is exact rather than grow-only. - BUG-069 / guard 63 — guard prefixes have the same three denominators (refs, worktrees, indices) and race the same way; two collisions landed on SPEC-193 in one morning.