Classifies a defect that reached production into one of three root gap categories (test gap, process gap, tooling gap) and one of thirteen sub-patterns, applying the earliest-layer rule: the category is the earliest layer that should have caught the defect, expressed as a property of the system and never of a person. Each sub-pattern carries the typical prevention fix, so the finding names an actionable class of gap. Rejects blame language and generic prevention assets such as 'add more tests'. Does not triage or prioritize the defect, does not write the missing test, and does not run a full incident postmortem. Use when a defect has been confirmed in production and the team needs to name which class of gap let it through before deciding what to build.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
A defect is "an imperfection or deficiency in a work product where it does not meet its requirements or specifications or impairs its intended use" (ISTQB, defect). When a defect reaches production, the interesting question is not what the defect was. It is which checking activity was supposed to stop it and did not.
ISTQB has a formal term for exactly this: an escaped defect is "a defect that is not detected by a test activity that is supposed to find it" (ISTQB, escaped defect). That definition is the whole skill in one sentence. Classification means naming the test activity that was supposed to find it, then naming why that activity did not.
The three categories and thirteen sub-patterns below are a practitioner taxonomy, not a standard. ISTQB and ISO/IEC/IEEE 29119 define the vocabulary (defect, escaped defect, root cause, quality gate) but neither publishes this three-way split. Treat the categories as a working classification your team can amend, and treat the cited definitions as fixed.
The output of this classification is a root cause in the ISTQB sense: "a source of a defect such that if it is removed, the occurrence of the defect type is decreased or removed" (ISTQB, root cause). Note "the defect type", not "the defect". A correct classification is one that, acted on, prevents the next defect of the same shape.
Owns: naming the class of gap that allowed one already-confirmed production defect through, so the class is actionable.
Does not own:
| Not this | Why it is separate |
|---|---|
| Triaging the defect (severity, priority, assignment) | Triage decides fix order. Classification runs after the fix is understood and changes nothing about fix order. |
| Writing the missing test or config change | Classification names the shape of the missing asset. Building it is a coding task with its own review. |
| Running a full incident postmortem | A postmortem covers timeline, detection, mitigation, customer comms, and action items across the whole incident. This covers one question inside it: which layer should have caught it. |
| Reproducing the defect | Classification takes a confirmed, understood defect as its input. If nobody can yet say what broke, classify nothing. |
The boundary test: if the answer changes the fix, it is not this skill. If the answer changes which checking layer the team invests in next, it is.
This is a hard constraint on the output, not a tone preference.
An escape analysis that names a person produces silence in the next one. Google's SRE Book states the mechanism directly: "If a culture of finger pointing and shaming individuals or teams for doing the 'wrong' thing prevails, people will not bring issues to light for fear of punishment," and warns that blame risks "a culture in which incidents and issues are swept under the rug, leading to greater risk for the organization" (Google SRE Book, Postmortem Culture). The same chapter sets the standard a finding must meet: "For a postmortem to be truly blameless, it must focus on identifying the contributing causes of the incident without indicting any individual or team."
The productive reframing is also from that chapter: "You can't 'fix' people, but you can fix systems and processes to better support people" (Google SRE Book, Postmortem Culture). Every category below is therefore a statement about a system. "Test gap" means the suite lacked a case. It does not mean an author forgot one.
"Earliest layer that should have caught it" is a statement about the system, not about who was on duty. The word "should" attaches to the layer, never to a person. If a sentence in the finding could be rewritten with a name in it and still parse, the sentence is wrong.
Two mechanical habits enforce this in the text:
For facilitation technique, the Google SRE Workbook's postmortem chapter (Postmortem Culture: Learning from Failure) extends this material with organizational adoption patterns.
Classify by the earliest layer that should have caught the defect. The categories are mutually exclusive at root level, and earliest wins.
A defect can plausibly be attributed to several layers at once. A null-input crash might have been caught by a unit test, by an integration test, by a staging soak, or by a runtime assertion. Attributing it to all four produces four vague action items and no change. Attributing it to the earliest layer produces one cheap, fast, specific check.
Apply the rule in this order and stop at the first "yes":
Step 3 is a genuine terminal state, not a fallback for hard cases. If "could a test have caught this?" is answered "no, no test ever could", classify tooling gap and recommend observability, not a test.
Test gap and tooling gap both feel like "we needed a test". The discriminator is capability, not effort:
"Hard to write" is still a test gap. "Impossible to write with these tools" is a tooling gap.
The team had the right framework, the right CI wiring, and the right process. No test existed for this specific case.
| Sub-pattern | Typical fix |
|---|---|
| Edge case not exercised | Add a unit test for the edge case. |
| Untested input domain (empty, null, unicode) | Add boundary or property-based tests. |
| Untested output channel (HTTP error path, fallback UI) | Add a test for the alternate path. |
| Untested integration between two units | Add an integration test exercising the seam. |
Signature of this category: the fix commit changes production code, and the matching test file already exists and passes both before and after, because it never covered the input in question.
The right test existed and did not run in the gating path.
| Sub-pattern | Typical fix |
|---|---|
| Test marked skipped or quarantined | Triage it back in under a quarantine policy with a hard expiry, so quarantine cannot become permanent. |
| Test on a non-blocking CI job | Move it to the blocking workflow. |
| Test ran but its result was ignored | Audit the pipeline and ensure exit codes propagate to the gate. |
| Required env var or fixture present only in some environments | Standardize test setup across environments. |
Signature of this category: a test that would have failed against the
introducing commit is already in the repository. Check for skip, only,
xfail, Ignore, continue-on-error: true, and jobs absent from the
required-checks list before concluding test gap.
Findings in this category are the ones most at risk of drifting into blame, because a human decision is visibly upstream of every one of them (someone skipped the test, someone marked the job non-blocking). Write the standing state, not the decision: "the quarantine list has no expiry mechanism, so entries persist indefinitely" is actionable; naming who added the entry is not.
The test could not have been written with the current toolchain. It needs a different layer, a different environment, or a runtime mechanism.
| Sub-pattern | Typical fix |
|---|---|
| Race condition that appears only under real concurrency | Add chaos or load testing. |
| Memory or file-descriptor leak that manifests over hours | Runtime monitoring, not a test. |
| Browser-specific or device-specific behavior (one engine, one OS) | Expand cross-browser or cross-device coverage. |
| Production-data-only defect (shape or scale absent from fixtures) | Anomaly detection or sampling-based monitoring. |
| Configuration error (env var typo, missing secret) | Startup-time configuration validation. |
Signature of this category: writing the reproducing test requires provisioning something the repository does not have. When the honest prevention asset is an alert threshold rather than an assertion, this is the category.
These four shapes are rejected outright.
| Rejected shape | Example | Why it fails |
|---|---|---|
| Blame language | "Engineer X should have written this test." | Produces silence in the next analysis (Google SRE Book, Postmortem Culture). You cannot fix people; you can fix systems. |
| Generic prevention asset | "Add more tests." "Be more careful." "Improve code review." | Not a finding. It survives any defect unchanged, which is how you know it identifies nothing. The asset must be a named file, a named config key, or a named metric. |
| Test gap conflated with tooling gap | Filing a race condition that needs a load harness as "missing unit test". | Sends the team to write a test that cannot exist, and the same defect escapes again. |
| Missing production-state evidence | No first-observation timestamp, no time-in-production, no affected-user estimate. | These bound how much the prevention asset is worth. Without them the finding cannot be prioritized against any other work. |
Note the second row carefully: "add more tests" and "be more careful" are not findings. They are the generic-prevention-asset anti-pattern in its two most common disguises, one technical and one behavioral. "Be more careful" additionally fails the blameless constraint, because carefulness is a property of a person.
One markdown document per escaped defect, stored together under a stable
directory (for example docs/escape-defects/) with a date-prefixed,
slugified filename so the set reads as a trend. The full per-defect
template - fields table, section skeleton, and the load-bearing
Prevention asset section (empty or generic means the document is not
finished) - is in
references/escape-defect-document-template.md.
Input. Defect #1234: calculateTotal throws on an empty cart. The fix
changed src/checkout/total.ts line 23. The existing test file
src/checkout/total.test.ts covered only populated carts.
Step 2, the anchor sentence. The unit suite for calculateTotal had no
case for a cart with zero items.
Step 3, the rule. Could an ordinary unit test with the installed runner have caught this? Yes. Did one exist? No. First "yes" at step 1, so test gap. Stop. Do not also file it as a missing E2E case, even though an E2E checkout test would also have caught it. The unit level is earlier and cheaper.
Step 4, sub-pattern. Edge case not exercised.
Step 5, prevention asset. A concrete case appended to the existing file:
import { describe, it, expect } from 'vitest';
import { calculateTotal } from './total';
it('returns 0 for an empty cart (regression for #1234)', () => {
expect(calculateTotal({ items: [] })).toBe(0);
});Where it lives. src/checkout/total.test.ts.
Step 6, verification. This case fails against abc1234, the
introducing commit, and locks empty-cart behavior in against future
refactors. Confirming that the fix removes the failure is
confirmation testing: "a type of change-related testing performed after
fixing a defect to confirm that a failure caused by that defect does not
reoccur"
(ISTQB, confirmation testing).
Step 7, blame scan. No names, no pronouns, no "should have". The finding reads as a property of the suite.
## Process changes section.## Open questions carries the tradeoff the team must still decide, such
as whether to also fund a soak test in CI.