---
title: "The Swift Defect Catalog: 24 Bug Classes, Each Wired to a Gate"
description: "We mined every fixed Swift bug in ToolPiper into 24 recurring classes across 196 incidents, then required each class to carry a check that fails the build."
date: 2026-08-15
author: "Ben Racicot"
tags: ["Architecture", "Swift", "Code Quality", "Testing", "macOS", "Apple Silicon"]
type: "paper"
canonical: "https://modelpiper.com/blog/swift-defect-catalog-machine-enforcement"
---

# The Swift Defect Catalog: 24 Bug Classes, Each Wired to a Gate

> We mined every fixed Swift bug in ToolPiper into 24 recurring classes across 196 incidents, then required each class to carry a check that fails the build.

## TL;DR

ToolPiper's Swift codebase carries a catalog of 24 recurring defect classes covering 196 attributed incidents, and every class must name a live check that reddens the build. A class whose enforcement column is empty is an open work item, not documentation. This paper covers how the classes were mined, the three enforcement tiers they map onto, and the two failure modes that made the catalog necessary in the first place.

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.

## FAQ

### How many defect classes does the catalog contain?

24 classes covering 196 attributed incidents. The classes were mined on 2026-07-01 from the full bug corpus, which yielded 148 fixed or filed instances and attributed 126 of them across the 16 classes recognized at that point. The catalog has grown as burn-down work surfaced shapes the first pass did not see.

### What stops the catalog from going stale?

The rule that every fix classifies into a class, and that a fix revealing an unlisted class must add the class and land its check in the same change. Curation is a side effect of fixing bugs rather than a separate job somebody has to be assigned.

### Why allow a ratchet instead of requiring zero?

Because a hard zero on a shape with hundreds of pre-existing instances is a standing red that nobody can clear, and a check people learn to skip is worse than no check. A ratchet carries a numeric floor, fails when the count rises, arms itself on the day it lands, and tightens as the population drops.

### Are all 24 classes machine-enforced?

Every class names a live enforcement, but not all of them are automatic. Several name the code review lens because their shape is a judgment about intent with no matchable form, and a pattern that tried to catch them would produce a worklist too mixed to triage. Each class states which side of that line it is on rather than leaving it implied.

### Does this prevent new kinds of bugs?

No. The catalog is backward-looking by construction and a genuinely novel shape passes every gate. The claim is narrower: a shape the codebase has already paid for cannot silently return.
