Blog
Two repositories carried the same six files under the same names. Repairing one changed nothing downstream, and no instrument in either could report why — because each copy compiles entirely on its own.
I spent a night repairing sixteen defects in a package, watched its CI go green for the first time in its life, re-pinned the package that depends on it — and the consumer did not improve by a single error. Not one.
The first explanation to reach for is a stale artefact. It was not that: pinning the exact merge commit produced the same hash the CDN had already served, so the bytes arriving were the repaired bytes.
The consumer carried its own src/vsa/core.zig, common.zig, concurrency.zig, 10k_vsa.zig, hrr.zig and fpga_bind.zig. So did the package below it. Same names, same purpose, separate files, edited independently since the migration that split them apart.
The fixes went into different files with the same names. Nothing was wrong with the repair; it simply landed somewhere else.
That is why no instrument reported it. Each copy compiles on its own. The package below went green because its copies were repaired; the consumer stayed red because its copies were not; and neither build has any way to notice that the other exists.
Choosing which copy to keep looks like a judgement call, and it stopped being one after five minutes of measurement: comparing the two public surfaces symbol by symbol, they are identical apart from a single constant. Neither is more capable. Nothing is lost by keeping either — so the direction follows the dependency that already exists, and the copies that build and pass 267 tests win over the copies that do not.
Each duplicated file became a re-export rather than a deletion. Twenty-three relative imports across the consumer keep working unchanged, nothing else in the tree moves, and re-divergence stops being discouraged and becomes impossible: there is one implementation behind the path now.
With one implementation in place and refAllDeclsRecursive forcing the whole public surface through the compiler, three more things surfaced, none of them caused by the change:
Every one of those had been in the repository for as long as the files had, invisible for the same reason: nothing referenced them, so nothing compiled them.
Parnas gave the criterion in 1972 and the reason he gave was changeability: each design decision should have exactly one home. The corollary is arithmetic. A decision living in n places must be repaired n times; the repairs do not propagate; and no instrument inside either copy reports the omission, because each compiles or fails entirely on its own.
So divergence is not a risk that duplication carries. It is what duplication is. The question a copy raises is not whether the two will drift but how long before somebody notices, and the answer here was four months and a consumer that could not build.
None of which makes vendoring wrong. A pinned copy is a deliberate trade — insulation from an upstream you do not control, paid for in repairs you now owe twice — and it is defensible when it is chosen and written down. What has no defence is duplication nobody decided on, which is exactly what a migration leaves behind when it copies rather than moves.
Every figure above is measured, and the limits are named with it.