Skip to content

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/main meanwhile 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:

# assertion-floor.txt: rows from BOTH sides, sorted by the guard's numeric prefix

Three checks that catch the silent variant before it ships:

  1. Count, then compare — ls | wc -l against the file, in the merged tree, as its own step. Not "the merge went fine".
  2. 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.
  3. 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.
  • 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.