Files
awesome-copilot/skills/quality-playbook/references/defensive_patterns.md
Andrew Stellman 74208f679b quality-playbook v1.2.0: state machine analysis and missing safeguard detection (#1238)
Add Step 5a (state machine completeness analysis) and expand Step 6
with missing safeguard detection patterns. These catch two categories
of bugs that defensive pattern analysis alone misses: unhandled states
in lifecycle/status machines, and operations that commit users to
expensive work without adequate preview or termination conditions.
2026-04-01 10:37:00 +11:00

8.7 KiB
Raw Blame History

Finding Defensive Patterns (Step 5)

Defensive code patterns are evidence of past failures or known risks. Every null guard, try/catch, normalization function, and sentinel check exists because something went wrong — or because someone anticipated it would. Your job is to find these patterns systematically and convert them into fitness-to-purpose scenarios and boundary tests.

Don't skim — grep the codebase methodically. The exact patterns depend on the project's language. Here are common defensive-code indicators grouped by what they protect against:

Null/nil guards:

Language Grep pattern
Python None, is None, is not None
Java null, Optional, Objects.requireNonNull
Scala Option, None, .getOrElse, .isEmpty
TypeScript undefined, null, ??, ?.
Go == nil, != nil, if err != nil
Rust Option, unwrap, .is_none(), ?

Exception/error handling:

Language Grep pattern
Python except, try:, raise
Java catch, throws, try {
Scala Try, catch, recover, Failure
TypeScript catch, throw, .catch(
Go if err != nil, errors.New, fmt.Errorf
Rust Result, Err(, unwrap_or, match

Internal/private helpers (often defensive):

Language Grep pattern
Python def _, __
Java/Scala private, protected
TypeScript private, # (private fields)
Go lowercase function names (unexported)
Rust pub(crate), non-pub functions

Sentinel values, fallbacks, boundary checks: Search for == 0, < 0, default, fallback, else, match, switch — these are language-agnostic.

What to Look For Beyond Grep

  • Bugs that were fixed — Git history, TODO comments, workarounds, defensive code that checks for things that "shouldn't happen"
  • Design decisions — Comments explaining "why" not just "what." Configuration that could have been hardcoded but isn't. Abstractions that exist for a reason.
  • External data quirks — Any place the code normalizes, validates, or rejects input from an external system
  • Parsing functions — Every parser (regex, string splitting, format detection) has failure modes. What happens with malformed input? Empty input? Unexpected types?
  • Boundary conditions — Zero values, empty strings, maximum ranges, first/last elements, type boundaries

Converting Findings to Scenarios

For each defensive pattern, ask: "What failure does this prevent? What input would trigger this code path?"

The answer becomes a fitness-to-purpose scenario:

### Scenario N: [Memorable Name]

**Requirement tag:** [Req: inferred — from function_name() behavior] *(use the canonical `[Req: tier — source]` format from SKILL.md Phase 1, Step 1)*

**What happened:** [The failure mode this code prevents. Reference the actual function, file, and line. Frame as a vulnerability analysis, not a fabricated incident.]

**The requirement:** [What the code must do to prevent this failure.]

**How to verify:** [A concrete test that would fail if this regressed.]

Converting Findings to Boundary Tests

Each defensive pattern also maps to a boundary test:

# Python (pytest)
def test_defensive_pattern_name(fixture):
    """[Req: inferred — from function_name() guard] guards against X."""
    # Mutate fixture to trigger the defensive code path
    # Assert the system handles it gracefully
// Java (JUnit 5)
@Test
@DisplayName("[Req: inferred — from methodName() guard] guards against X")
void testDefensivePatternName() {
    fixture.setField(null);  // Trigger defensive code path
    var result = process(fixture);
    assertNotNull(result);  // Assert graceful handling
}
// Scala (ScalaTest)
// [Req: inferred — from methodName() guard]
"defensive pattern: methodName()" should "guard against X" in {
  val input = fixture.copy(field = None)  // Trigger defensive code path
  val result = process(input)
  result should equal (defined)  // Assert graceful handling
}
// TypeScript (Jest)
test('[Req: inferred — from functionName() guard] guards against X', () => {
    const input = { ...fixture, field: null };  // Trigger defensive code path
    const result = process(input);
    expect(result).toBeDefined();  // Assert graceful handling
});
// Go (testing)
func TestDefensivePatternName(t *testing.T) {
    // [Req: inferred — from FunctionName() guard] guards against X
    t.Helper()
    fixture.Field = nil  // Trigger defensive code path
    result, err := Process(fixture)
    if err != nil {
        t.Fatalf("expected graceful handling, got error: %v", err)
    }
    // Assert the system handled it
}
// Rust (cargo test)
#[test]
fn test_defensive_pattern_name() {
    // [Req: inferred — from function_name() guard] guards against X
    let input = Fixture { field: None, ..default_fixture() };
    let result = process(&input);
    assert!(result.is_ok(), "expected graceful handling");
}

State Machine Patterns

State machines are a special category of defensive pattern. When you find status fields, lifecycle phases, or mode flags, trace the full state machine — see SKILL.md Step 5a for the complete process.

How to find state machines:

Language Grep pattern
Python status, state, phase, mode, == "running", == "pending"
Java enum.*Status, enum.*State, .getStatus(), switch.*status
Scala sealed trait.*State, case object, status match
TypeScript status:, state:, Status =, switch.*status
Go Status, State, type.*Phase, switch.*status
Rust enum.*State, enum.*Status, match.*state

For each state machine found:

  1. List every possible state value (read the enum or grep for assignments)
  2. For each handler/consumer that checks state, verify it handles ALL states
  3. Look for states you can enter but never leave (terminal state without cleanup)
  4. Look for operations that should be available in a state but are blocked by an incomplete guard

Converting state machine gaps to scenarios:

### Scenario N: [Status] blocks [operation]

**Requirement tag:** [Req: inferred — from handler() status guard]

**What happened:** The [handler] only allows [operation] when status is "[allowed_states]", but the system can enter "[missing_state]" status (e.g., due to [condition]). When this happens, the user cannot [operation] and has no workaround through the interface.

**The requirement:** [operation] must be available in all states where the user would reasonably need it, including [missing_state].

**How to verify:** Set up a [entity] in "[missing_state]" status. Attempt [operation]. Assert it succeeds or provides a clear error with a workaround.

Missing Safeguard Patterns

Search for operations that commit the user to expensive, irreversible, or long-running work without adequate preview or confirmation:

Pattern What to look for
Pre-commit information gap Operations that start batch jobs, fan-out expansions, or API calls without showing estimated cost, scope, or duration
Silent expansion Fan-out or multiplication steps where the final work count isn't known until runtime, with no warning shown
No termination condition Polling loops, watchers, or daemon processes that check for new work but never check whether all work is done
Retry without backoff Error handling that retries immediately or on a fixed interval without exponential backoff, risking rate limit floods

Converting missing safeguards to scenarios:

### Scenario N: No [safeguard] before [operation]

**Requirement tag:** [Req: inferred — from init_run()/start_watch() behavior]

**What happened:** [Operation] commits the user to [consequence] without showing [missing information]. In practice, a [example] fanned out from [small number] to [large number] units with no warning, resulting in [cost/time consequence].

**The requirement:** Before committing to [operation], display [safeguard] showing [what the user needs to see].

**How to verify:** Initiate [operation] and assert that [safeguard information] is displayed before the point of no return.

Minimum Bar

You should find at least 23 defensive patterns per source file in the core logic modules. If you find fewer, read function bodies more carefully — not just signatures and comments.

For a medium-sized project (515 source files), expect to find 1530 defensive patterns total. Each one should produce at least one boundary test. Additionally, trace at least one state machine if the project has status/state fields, and check at least one long-running operation for missing safeguards.