The same ten bugs kept coming back.
Not the same bug in the same file. The same shape, in a different file, written by someone who had never seen the incident that taught us it was a shape. A force-unwrap on a dictionary lookup. A try? that turned a real failure into a silent nil. A fire-and-forget Task whose lifetime nobody owned. Each one got found, fixed, written up in a report, and then reappeared six weeks later two directories over.
That is not a discipline problem. It is a memory problem, and reports are the wrong place to store memory. A report is write-only: it records that a thing was fixed, and it has no way to reach forward into how the next file gets written.
What a defect class is
A defect class is a bug shape that this codebase has actually paid for, stated generally enough that you can recognize it before you write it, and paired with the fix idiom that the incidents converged on.
The distinction that matters is between a class and an instance. "We force-unwrapped config[key] in the model loader" is an instance. "Force-unwrapping a value whose absence is a reachable runtime state" is a class, and it covers 38 incidents in our tree across code that has nothing else in common.
Classes are only useful if they are derived from what actually happened rather than from what a style guide imagines might happen. Ours were mined on 2026-07-01 by a workflow that read the full bug corpus: two bug trackers, an engine-substrate review, a trust audit, three hardening reports, five debt cards and the canary audits. That pass found 148 fixed or filed instances and attributed 126 of them across 16 classes. A seventeenth arrived five days later out of a keychain review. The catalog now stands at 24 classes over 196 attributed incidents.
The attribution gap is the honest part. Not every instance fits a class, and the ones that do not are the raw material for the next class rather than noise to be discarded.
The rule that makes it a flywheel instead of a wiki page
A catalog of bug shapes is a nice document and changes nothing. The rule that gives it teeth is this: every class must carry a machine enforcement, and a class whose enforcement is empty is an open work item rather than documentation.
That single constraint turns the catalog from a description of the past into a gate on the future. It also makes the document self-auditing, because an empty enforcement column is visible at a glance and embarrassing.
The loop runs in four places:
- Writing. Before non-trivial Swift work, skim the table. These are the shapes we have paid for, each with the idiom the fix converged on.
- Reviewing. Review findings cite a class slug. A finding that fits no class is a candidate new class.
- Fixing. Every burn-down fix classifies into a class. A fix that reveals an unlisted class adds the class and lands its check in the same change.
- Enforcing. Every class names the live gate that catches it.
The third bullet is the one that keeps the catalog current without anyone being assigned to curate it. You cannot land a fix for a novel shape without also teaching the tree to catch it.
Three tiers, because not every shape is greppable
Enforcement closure shipped on 2026-07-02, and it did not arrive as one mechanism. Different defect shapes are legible to different instruments, and pretending otherwise produces gates that are either useless or unbearable.
Tier 1: the AST pass
A Swift lint tool walks the syntax tree and reports on structure rather than text. This is where force-unwraps, forced casts, unhandled Objective-C boundaries and fire-and-forget tasks without a stored handle get caught. Structure-aware checks can tell a force-unwrap in a test from one in shipping code, which a text search cannot.
Tier 2: the grep tier, hard and ratcheted
A shell suite runs pattern checks in two modes. Hard checks must return zero: the shape is banned outright and any new instance fails the build. Ratchet checks carry a numeric floor and fail when the count rises, which is how you gate a shape with hundreds of pre-existing instances you are not going to fix this quarter.
The ratchet is the pragmatic half of the design. A hard zero on a shape with 208 live instances is a standing red that everyone learns to route around, and a check people route around is worse than no check. A floor arms itself immediately and tightens as the population drops.
Tier 3: the review lens
Some shapes have no matchable form. A check-then-act race, a bulk rewrite where an incremental update belonged, a filter applied after the gate instead of before it: these are judgments about intent, and a regex that tried to catch them would produce a mixed worklist that nobody triages.
Those classes name the review lens as their enforcement and say so explicitly. That is not a gap being papered over. It is a deliberate statement about which shapes are machine-legible, and every class's entry says which side of that line it sits on.
Two classes worth reading in full
Keychain result collapse, 32 instances
The largest class after force-unwraps, and the one that best shows why a class beats an instance. The shape is collapsing a keychain call's three-way result into a boolean or an optional, so "the item is genuinely absent" and "the read failed" become the same value at the call site. The first is a normal state. The second means the user is about to be silently signed out or handed an empty credential.
What makes it a good class is that the check needs five separate anchors to see it. The seam appears as a direct call, as a discarded assignment, as a raw Security framework call, as a method on a binding whose declared type names the seam under some other variable name, and as a value-type wrapper where the seam's type never reaches the call site at all. Four of those five are invisible to the obvious pattern. You only learn that by writing the check and watching it miss.
Phantom enforcement, 8 instances
The class that audits the other twenty-three. A phantom enforcement is a check that looks like it enforces something and does not: a validator with no callers, a guard whose caller chain is unreachable, an identifier shadowed so the assertion reads a local rather than the thing it names.
This is the failure mode that makes a catalog dangerous rather than merely useless. A tree full of gates that pass because they cannot fail is worse than a tree with no gates, because the green is load-bearing in people's heads. The class exists to keep us honest about our own instrumentation, and its check includes a caller-chain reachability scan and a detector self-test on every run.
What this does not do
It does not prevent novel bugs. The catalog is backward-looking by construction: it encodes what we have already paid for, and a genuinely new shape passes every gate on the way in. The claim is narrower and duller than "we stopped shipping bugs." It is that a shape we have already paid for cannot silently return.
It also does not replace review. Four of the classes name the review lens as their only enforcement, and a fifth carries a lens for the half of its shape that has no greppable form. Those are the classes where a person still has to look.
And it carries maintenance cost. Every ratchet floor is a number that has to move in the right direction and be updated when it does. Every hard check needs a committed fixture proving it can still fail, because a pattern check that has quietly stopped matching anything reports the same green as one that is working.
That last point generalizes past Swift and past us, and it is the reason the catalog has a sibling document about how gates get chosen at all. A check whose verdict is the absence of matches is indistinguishable from a check that cannot match. The only way to know which one you have is to break the thing on purpose and watch the check go red.
Why publish it
Because the format transfers even though the contents do not. Your 24 classes are not our 24 classes. But the two rules that make ours work are not Swift-specific at all: mine the classes from incidents you actually had, and refuse to let a class exist without a check that reddens the build.
Most teams have the first half sitting in closed tickets already. The second half is the one that turns it into something that compounds.