DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Eleven branches, one shared file: the anchor rule that made them merge

CogniPrep is a library of simulated employer assessments. Adding a provider means building its tests, its scoring, its question banks, its public pages, and then wiring it into a dozen shared registries: the game library, the component registry, the scoring engine's state union, the cheating guide registry, the sitemap sources, the theme path list.

We added eleven providers at once, each built in its own git worktree on its own branch, in parallel. That took the library from 16 providers and 108 tests to 24 and 165.

The build work parallelises beautifully, because almost all of it is new files. The wiring does not. Eleven branches all appending to the same handful of shared files is eleven guaranteed conflicts per file, and a conflict in a registry is not a conflict a human can resolve at speed, because the two sides are structurally identical and the diff has no idea which block belongs to whom.

The shared plan document that every worker read first solved this with one rule.

The anchor rule

Every edit you make to a shared file goes at an anchor unique to you.

Each worker was assigned an existing provider as its anchor:

Worker Provider id Anchor
hirevue hirevue arctic-shores
sova sova pymetrics
mckinsey-solve mckinsey-solve shl
testgroup testgroup cappfinity
ixly ixly predictive-index
assessio assessio aon
acer acer korn-ferry
kenexa kenexa watson-glaser
test-partnership test-partnership cubiks
thomas thomas saville

In every shared list, record, union and switch statement, a worker inserts its block immediately after its anchor provider's corresponding block. Never at the end.

The end of a list is where all eleven workers would write. The middle, at eleven different well separated points, is where they can each write without the others' hunks overlapping. Git merges by context lines, so two insertions separated by a few hundred lines of unchanged registry are just two independent hunks.

Two supporting rules make it hold:

  • Never touch another provider's lines. No reordering, no renaming, no tidying, no "while I was in here".
  • Never reformat a file you did not write. This repo deliberately has no formatter, so a bulk reformat is both unnecessary and a guaranteed conflict in eleven branches at once.

One consequence you have to accept up front: display order becomes a casualty, because the provider list derives from key order and new providers now interleave with old ones. That is fixed in a single commit after the merge, by one person, in one place. Trying to control ordering per worker would reintroduce exactly the collision the rule exists to avoid.

Did it work

Mostly, and the exceptions are the interesting part.

The pilot merge of three providers produced exactly one conflict, in the file where every worker appends game ids to the same small arrays. Everything else auto merged: the game library, the component registry, the scoring engine, the employer associations, the registries. Three branches, one conflict, resolved by keeping both sides.

Then a fourth and fifth branch landed and found two more, neither of which resolves by stripping the markers:

A conflict inside a shared import { opener. Both workers put their dimension imports in the same region and the conflict fell inside a single import statement. Keeping both sides verbatim leaves the second block with no opener and the file stops parsing. The resolution is to keep both and re-add the import {.

A registry whose members are rewritten rather than appended. One registry groups providers into families, so each worker edits an existing line rather than adding one. There is no anchor that helps, because the collision is the line itself. Seven merges, seven conflicts, each resolved by merging the two member lists into one array.

That is the honest boundary of the technique. Anchoring prevents collisions in append-only structures. It does nothing for a shared line that every contributor has to rewrite. If you are planning parallel work, that is the distinction to audit for in advance: which of your shared files grow, and which get edited in place.

The rules that had nothing to do with git

The plan document was about half merge mechanics and half content integrity, and in hindsight the second half mattered more.

Pricing is derived, never hand set. Our provider unlock price is computed from the count of playable tests: five or fewer is the lower tier. Each worker had a target test count in its brief. The rule attached to it:

Never invent a test that the vendor does not sell in order to reach a price tier. A cheaper provider built on real tests is correct; a more expensive one padded with a fabricated test is a lie to a paying candidate. If your research shows the real suite is smaller than the brief assumed, build the smaller suite and say so in your report.

Derived pricing is lovely until you notice it creates an incentive to add content. Write the counter-incentive down, explicitly, before anyone starts.

Every claim carries a URL someone actually opened. No association between an employer and an assessment vendor is inferred from sector norms or from what competitors use. Item counts, time limits, whether a test is adaptive, whether wrong answers are penalised: source it or do not claim it, and where sources disagree, say so on the page rather than picking the convenient one. One worker's research killed an association we already had on the site, because the vendor case study behind it now 404s and its content dated to 2016.

No database work. No migrations, no seed scripts, no db:push. Adding a provider needs no schema change, and eleven workers each deciding to run a script against a shared database is a category of accident with no clean undo. New tests do need population statistics for percentile scoring; workers listed the ids in their report and one person seeded them after the merge.

Write findings back into the plan, not into a report

The single highest leverage thing we did was append to the plan document as each branch landed, so later workers inherited what earlier ones learned. Some of the entries:

  • A global stylesheet rule removes borders from any class containing bg-card, matching by substring. It typechecks, it tests green, and it is visible only on screen. Two workers lost time to it; the third read about it.
  • A hand maintained list keyed by provider drifted twice, each time missed by a worker with no reason to know the file existed. The fix written down was not "be careful", it was "a list that nothing derives from needs a test", and that test now fails if any provider is missing.
  • Green tests are not evidence that a game works. Every worker who actually rendered at 320px found real bugs that typechecked and passed: scroll position carrying across items so the next prompt sat above the fold, a 520px table pushing a column off screen, an action row below the fold, and one crash on page load from passing a function across the server boundary.

Go and look at the result

The eleven providers are live. The ones that close a specific market gap are the most interesting to poke at, because they are the ones we had no prior code shape for: ACER for Australia, Ixly and TestGroup for the Netherlands, Assessio for the Nordics, and McKinsey Solve, which is one employer's simulation rather than a vendor suite.

The full library is the merge result: 24 providers in one list, in an order that one commit had to go and fix afterwards.

Top comments (0)