All entries
#engineering#migration#tooling

A Convention Without Enforcement Is Just a Wish

Building the codemods and facade layer for a very large framework migration, and watching an agreed-upon convention decay to roughly 7% compliance because nothing in CI was checking.

A migration across thousands of files isn't one problem. It's two.

Change every call site. Then keep them changed, while dozens of people go on committing to the same codebase, most of them working on features rather than your migration.

I worked the tooling side of one of these. The codemods, the compatibility layer, the checks meant to stop it unravelling. The team did the conversion. The part worth writing about is what I got wrong.

The facade

The idea was a thin passthrough layer. Components import a wrapper with the same shape instead of importing the underlying UI library directly, so migration happens in one place rather than thousands.

Getting there was a codemod problem. Rewriting a very large number of call sites mechanically, with a parse gate so a run producing unparseable output failed loudly instead of committing damage. Batches ran from a few hundred files to a few thousand.

Most of the effort went into making runs idempotent. Apply it twice, nothing changes the second time. A codemod you can't safely re-run is one you can only use once, and you always need it more than once.

The measured prep was worth it too. Before touching anything, a dry run counts what a change will hit: how many files, how many call sites, what fraction have the awkward shape needing hands. Finding that 43% of one component's usages had event bindings requiring different handling turns "this'll take a while" into a number. Numbers are what let you sequence the work honestly.

Where it went wrong

Once the facade existed the rule was simple. Don't import the underlying library directly. Go through the wrapper.

Everyone agreed. It was documented. It was in review guidance.

Compliance decayed to roughly 7%.

Nobody's fault. People were shipping features under deadline in a codebase where the old pattern still worked fine and matched every surrounding file. The convention lived in a document and in the memory of whoever happened to review your PR. It was competing against the path of least resistance and it lost, which is what conventions do.

My retro note was blunter than I'd usually write: no CI enforcement is just a wish.

What actually holds

A lint rule that fails the build when the old pattern appears, plus a verification gate running component checks, visual regression against captured baselines, and end-to-end tests.

The lint rule matters most, and its value isn't catching mistakes. It moves the convention from something people have to remember into something the toolchain knows. A new engineer doesn't need telling. Someone under deadline doesn't choose between the rule and shipping. The reviewer doesn't have to spot it among four hundred changed lines, which they won't reliably do, because nobody does.

Visual regression earns its keep differently. When the whole component library is being swapped underneath, the failure you fear isn't a crash. Those surface immediately. It's the button four pixels off, or the modal that lost its padding, on a screen nobody opened during testing. Comparing rendered screenshots catches what behaviour tests never see, provided you freeze animations first so baselines don't flap.

Three parts, and everyone skips the third

Every long-lived engineering change needs a mechanism that makes the new way possible, a migration that moves existing code onto it, and an enforcement that stops new code drifting back.

Teams do the first two and skip the third constantly, because once the migration lands it feels finished. The codebase is consistent on the day you stop. Then entropy resumes at the rate of your commit volume, and eighteen months later someone proposes a migration to fix the inconsistency.

The third is usually the cheapest of the three. A lint rule is an afternoon. It gets skipped not because it's expensive but because it arrives exactly when everyone is most tired of the project and most convinced the hard part is behind them.

The tooling worked. The convention it existed to support didn't hold, and that had nothing to do with the tooling's quality or the team's discipline. It didn't hold because I shipped a rule depending on humans remembering it, in a codebase where forgetting was easier and still produced working software.

Seven percent is a humbling number to write down. It's also why the first thing I ask about any new convention now is what fails if someone ignores it. If the answer is that a reviewer might mention it, that's not a convention. It's a preference with good marketing.