- Write a failing test that reproduces the bug BEFORE writing any fix code
- Identify the root cause — not just the symptom
- Understand all callers and dependents of the code you will change
- Run existing tests to establish a passing baseline
- Plan the minimal fix that addresses the root cause without side effects
- After fixing, confirm the failing test now passes
Fixing without a reproducer = guessing. Fixing without diagnosis = wrong fix. Both = wasted time.
This is not optional. Every fix requires a reproducer and root-cause analysis.
Bugfix
MANDATORY FIRST RESPONSE PROTOCOL
Before fixing ANY bug, you MUST complete this checklist:
- ☐ Parse the bug source (find-bugs output, user report, or observed failure)
- ☐ Read the relevant code thoroughly (not just the flagged line — the full function, its callers, and its tests)
- ☐ Write a failing test that reproduces the exact bug (Red phase)
- ☐ Identify the root cause by tracing data flow from entry to failure point
- ☐ Map the blast radius (callers, importers, dependents, type consumers)
- ☐ Run existing tests to establish a passing baseline (the new reproducer should be the only new failure)
- ☐ Plan the minimal fix (smallest change that addresses root cause)
- ☐ Announce: "Fixing [N] bug(s): [brief description]. Root cause: [cause]. Blast radius: [scope]."
Fixing bugs WITHOUT completing this checklist = wrong fixes and regressions.
Overview
Diagnose and fix bugs with surgical precision. Every fix follows a strict red-green workflow: reproduce the bug with a failing test, diagnose the root cause, implement the minimal fix, confirm the test goes green, verify no regressions.
What this skill does:
- Parses structured findings from
/find-bugsor freeform user reports - Writes a failing test that captures the exact bug scenario BEFORE any fix code
- Traces from symptom to root cause through systematic data-flow analysis
- Applies bug-type-specific fix playbooks (security, logic, async, null, race conditions)
- Implements minimal fixes with defense-in-depth hardening where appropriate
- Verifies fixes work and introduces no regressions
- Reports root cause, fix rationale, files changed, and tests added
What this skill does NOT do:
- Refactor surrounding code (fix only — use
/code-simplifyfor cleanup) - Add features beyond what the fix requires
- Fix style or formatting issues (those are not bugs)
- Guess at fixes without understanding the root cause
- Skip the reproducer test ("it's a simple change" is never an excuse)
When to Use
- User says "fix this bug", "fix the findings", "fix finding #3", "/bugfix"
- User pastes a
/find-bugsreport and asks to fix some or all findings - User describes a bug (error message, wrong behavior, crash)
- User points to a specific line or function and says "this is broken"
- $ARGUMENTS provided as guidance (e.g.,
/bugfix fix the SQL injection in auth.ts)
Never fix proactively. Only when explicitly requested.
When NOT to Use
- No confirmed bug — if the code works correctly, there is nothing to fix
- Feature request — if the user wants new behavior, that is a feature, not a bugfix
- Style/formatting — cosmetic issues are not bugs
- Architecture redesign needed — if the fix requires rethinking the design, discuss with the user first
- Unclear requirements — if you cannot determine what "correct" means, ask before fixing
Step 0: Detect Framework and Load Reference
Gate: Framework identified and reference file read before proceeding to Step 1.
Before diagnosing any bug, identify the framework/language and read the corresponding reference file from the references/ directory alongside this skill. The reference files contain framework-specific bug patterns, idiomatic fixes, common traps, and testing patterns that are critical for accurate diagnosis and correct fixes.
Detection Rules
Identify the framework from the buggy file's imports, the project's dependency files, and the file extension:
| Signal | Framework | Reference File |
|---|---|---|
package.json has express |
Express.js | references/expressjs.md |
package.json has react + JSX/TSX files |
React | references/react.md |
package.json has react-native or expo |
React Native | references/react-native.md |
package.json has next |
Next.js | references/nextjs.md |
package.json has fastify |
Fastify | references/fastify.md |
package.json has hono |
Hono | references/hono.md |
package.json has @remix-run/react |
Remix | references/remix.md |
bun.lockb or bunfig.toml present |
Bun | references/bun.md |
composer.json has laravel/framework |
Laravel | references/laravel.md |
go.mod present |
Go | references/golang.md |
go.mod has github.com/gin-gonic/gin |
Go + Gin | references/go-gin.md |
go.mod has github.com/labstack/echo |
Go + Echo | references/go-echo.md |
go.mod has github.com/gofiber/fiber |
Go + Fiber | references/go-fiber.md |
.swift files, Package.swift |
Swift | references/swift.md |
Cargo.toml present, .rs files |
Rust | references/rust.md |
Cargo.toml has axum |
Rust + Axum | references/rust-axum.md |
Cargo.toml has actix-web |
Rust + Actix Web | references/rust-actix.md |
Cargo.toml has rocket |
Rust + Rocket | references/rust-rocket.md |
.ts/.js files (no specific framework) |
Node.js/TypeScript | references/nodejs-typescript.md |
Loading Rules
- Always load the base reference — for Node.js projects, always read
nodejs-typescript.md; for Go projects, always readgolang.md; for Rust projects, always readrust.md - Layer framework-specific reference on top — if using Express, read BOTH
nodejs-typescript.mdANDexpressjs.md; if using Axum, read BOTHrust.mdANDrust-axum.md - React Native includes React — read both
react.mdandreact-native.md - Next.js/Remix include React — read both
react.mdand the framework file - Go frameworks layer on Go — read both
golang.mdand the framework file (e.g.,go-gin.md) - Rust frameworks layer on Rust — read both
rust.mdand the framework file (e.g.,rust-axum.md) - If the framework is unknown, fall back to the language-level reference
Read the reference file(s) using the Read tool. The file path is relative to this skill's directory.
Why This Matters
Framework-specific bugs are the most common source of incorrect fixes. For example:
- Fixing a Fastify validation bug with Express-style middleware won't work (Fastify uses JSON Schema)
- Fixing a Fiber request bug without
copy()will cause memory corruption (fasthttp pools request data) - Fixing a Next.js auth bug in a page component misses that Server Actions need separate auth checks
- Fixing a Go error without checking the return value is the #1 Go bug pattern
The reference files prevent these mistakes by providing the idiomatic fix pattern for each framework.
Step 1: Parse the Bug
Gate: Bug(s) clearly identified with location, description, and category before proceeding to Step 2.
From /find-bugs Output
If the user provides structured findings from /find-bugs, parse each finding:
- Finding number — e.g., Finding #1, Finding #3
- File:Line — exact location
- Severity — Critical / High / Medium / Low
- Category — Security (injection, XSS, auth, etc.) / Logic Bug / Code Quality
- Problem — what is wrong
- Evidence — why it's real (from find-bugs verification)
- Attack vector — how it can be exploited (security issues)
- Fix suggestion — the suggested approach (use as starting point, not gospel)
If the user says "fix all critical bugs" or "fix findings 1, 3, 5":
- Extract the specified findings
- Sort by severity (Critical first)
- Group findings that touch the same file or function (fix together to avoid edit conflicts)
- Build a dependency graph: does fixing #1 change code that #3 depends on?
- Identify which fix playbook (Step 5) applies to each finding
From User Report
If the user describes a bug in their own words:
- Extract the symptom (what goes wrong — error message, wrong output, crash, hang)
- Extract the trigger (what action causes it — specific input, API call, sequence of events)
- Extract the expected behavior (what should happen instead)
- Extract the location (file, function, endpoint — if provided)
- Extract the frequency (always, intermittent, only under load — critical for race conditions)
If any of symptom, trigger, or expected behavior are missing, ask before proceeding:
To fix this bug, I need to understand:
- What goes wrong? (error message, wrong output, crash)
- What triggers it? (specific input, action, sequence)
- What should happen instead?
- How often? (always, sometimes, only under load)
- Where in the code? (file/function if known)
From Observed Failure
If you encounter a bug during other work (test failure, build error, runtime crash):
- Capture the exact error output (full stack trace, not just the message)
- Identify the failing test or build step (which command, which assertion)
- Parse the stack trace — read bottom-to-top for the call chain, identify the frame in project code (skip node_modules/framework frames)
- Trace to the source file and line from the stack
- Check
git log --oneline -5 -- <file>— is this a recent regression?
Step 2: Reproduce the Bug (Red Phase)
Gate: A failing test exists that captures the exact bug scenario before proceeding to Step 3.
This is the most important step. A fix without a reproducer is guessing. Write the test FIRST.
2.1 Find the Test File
# Tests live alongside source: src/foo.test.ts next to src/foo.ts
# Check for existing test file
Use the Glob tool to find *.test.ts or *.spec.ts files adjacent to the buggy source file. If no test file exists, create one following the project's test patterns.
2.2 Write the Failing Test
Write a test that captures the EXACT scenario from the bug report:
describe("[module/function name]", () => {
it("should [expected behavior] when [condition that triggers bug]", () => {
// Arrange: set up the exact scenario from the bug report
// - Use the specific input that triggers the bug
// - Set up the exact state that causes the failure
// Act: trigger the operation
// Assert: verify the CORRECT behavior (what SHOULD happen)
// This assertion should FAIL with the current buggy code
});
});
Naming convention: The test name should describe the correct behavior, not the bug. "should sanitize SQL in user input" not "should not have SQL injection."
2.3 Run the Test and Confirm It Fails
# Run just the new test to confirm it fails
pnpm --filter <package> test -- --reporter=verbose <test-file>
The test MUST fail. If it passes, either:
- The bug doesn't exist (re-examine the report)
- Your test doesn't capture the right scenario (rewrite it)
- The bug is in a different layer than you think (widen the search)
If you cannot reproduce the bug:
- Check the environment — Does the bug require specific config, env vars, or database state?
- Check the input — Is the triggering input more specific than you assumed? Try exact values from the report.
- Check for intermittency — Is this a race condition that only happens under concurrency? (See Race Condition Playbook)
- Check the version — Has the code changed since the bug was reported? Run
git logon the file. - Ask the user — "I cannot reproduce this bug with the current code. Can you provide the exact input/steps?"
Never proceed to fix code you cannot prove is broken. If you truly can't reproduce after these steps, report back to the user with what you tried.
2.4 Write Additional Edge Case Tests
While you're in the test file, also write tests for related edge cases the fix should handle:
- Null/undefined input (if the bug is about missing data)
- Empty collection (if the bug is about array/object handling)
- Boundary values (if the bug is about off-by-one or limits)
- Malicious input (if the bug is a security vulnerability)
These tests may pass or fail — that's fine. They document the expected behavior and prevent future regressions.
Step 3: Diagnose Root Cause
Gate: Root cause identified and distinguished from symptom before proceeding to Step 4.
Now that you have a failing test proving the bug exists, find WHY it exists.
3.1 Read the Code — Thoroughly
Read the complete function/module where the bug lives. Not just the flagged line — the full context:
- Read the buggy function in its entirety (use the Read tool)
- Read the function's callers — who calls this and with what arguments?
- Read the function's dependencies — what does it call and what does it expect back?
- Read the type definitions for all parameters and return values
- Read the test file — what scenarios ARE covered? What's missing?
# Check git history for the area — was this a recent change?
git log --oneline -10 -- <file-path>
# Who last touched the buggy lines?
git log -1 --format='%h %s (%an, %ar)' -- <file-path>
For regressions (something that used to work), use git bisect:
# Find the commit that introduced the bug
git log --oneline --all -- <file-path>
# Then read the diff of the suspicious commit
git show <commit-hash> -- <file-path>
3.2 Trace the Data Flow
Follow data from entry point to failure, mapping every transformation:
ENTRY: [where data enters] (request handler, function param, config read, DB query)
│
├── Validation: [is input validated?] [what constraints?] [can invalid data pass?]
│
├── Transformation 1: [what happens to the data?] [can this produce unexpected output?]
│
├── Transformation 2: [next step] [assumptions about input from step 1?]
│
├── ...
│
└── FAILURE POINT: [where it breaks] [what value causes the break?] [why?]
At each step ask:
- What values can this produce?
- What does the next step ASSUME about its input?
- Can the output of this step violate the assumption of the next step?
The root cause is at the FIRST point where an assumption is violated.
3.3 Classify the Bug
Identify which category the bug falls into — this determines which fix playbook to use in Step 5:
| Category | Signals | Fix Playbook |
|---|---|---|
| SQL/Command Injection | User input concatenated into query/command string | §5.1 Injection Fix |
| XSS | User input rendered in HTML without escaping | §5.2 XSS Fix |
| Auth/AuthZ Bypass | Missing or incorrect auth check, IDOR | §5.3 Auth Fix |
| Null/Undefined Error | Property access on potentially null value | §5.4 Null Safety Fix |
| Race Condition | TOCTOU, concurrent modification, double-submit | §5.5 Race Condition Fix |
| Off-by-One / Boundary | Wrong loop bound, array index, pagination | §5.6 Boundary Fix |
| Async/Promise Error | Missing await, unhandled rejection, stale closure | §5.7 Async Fix |
| Type Coercion / Cast | Wrong type assumption, implicit conversion | §5.8 Type Safety Fix |
| Resource Leak | Unclosed connection, handle, listener, timer | §5.9 Resource Leak Fix |
| Logic / Business Rule | Wrong conditional, missing state transition check | §5.10 Logic Fix |
3.4 Distinguish Root Cause from Symptom
| What You See | Likely Symptom | Dig Deeper For Root Cause |
|---|---|---|
| Null pointer exception | Missing null check | Why is the value null? Who should have provided it? |
| Wrong query results | Bad WHERE clause | Is the data model wrong? Are joins missing? Is the filter inverted? |
| Auth bypass | Missing middleware | Is the route registration wrong? Is the middleware order wrong? |
| Race condition | Missing lock | Is the design inherently concurrent? Should it be serial? |
| Type error at runtime | Wrong cast | Is the type definition wrong upstream? Is the API contract violated? |
| Timeout | Slow query | Is there an N+1 query? Missing index? Unbounded result set? |
| Memory leak | Growing collection | Who should be cleaning up? Is there a missing removeListener? |
The root cause is the first thing that goes wrong in the chain, not the last.
3.5 Map the Blast Radius
Before planning a fix, map everything the change will affect:
Search for (using the Grep tool, not bash grep):
- Direct callers — functions/methods that invoke the buggy code
- Importers — every file that imports the module
- Type consumers — code that depends on the type signatures you might change
- Tests — all tests covering the affected code paths
- Config/routes — registration of routes, middleware, plugins that reference this code
- Downstream consumers — if this is a library/package, who depends on it?
# Check cross-package dependents in monorepo
pnpm why <package-name>
Document: "Changing functionName in file.ts will affect: [list of callers], tested by: [list of test files], imported by: [list of importers]."
3.6 Confirm the Diagnosis
Before proceeding, pass these gates:
- One-sentence test: Can you explain the root cause in one sentence? If not, keep digging.
- Completeness test: Does your explanation account for ALL symptoms? If the user reports two issues and your root cause only explains one, there may be two bugs or a deeper shared cause.
- Simplicity test: Is there a simpler explanation? Prefer the simplest root cause that explains all symptoms (Occam's razor).
- Prediction test: Does your diagnosis predict what your failing test does? If your root cause is correct, you should be able to predict exactly WHY the test fails and with what values.
Step 4: Plan the Fix
Gate: Fix plan complete, minimal, and matched to the correct playbook before proceeding to Step 5.
4.1 Design the Minimal Fix
The best fix is the smallest change that addresses the root cause:
| Principle | Why |
|---|---|
| Fix at the root cause, not the symptom | Symptom fixes mask the real problem; it resurfaces later |
| Change as few files as possible | Smaller blast radius = fewer regressions |
| Change as few lines as possible | Less to review, less to go wrong |
| Don't refactor alongside the fix | Mixing fix + refactor makes review impossible |
| Don't add features alongside the fix | Scope creep obscures the fix |
| Preserve behavior for non-buggy paths | Only fix what is broken |
| Match the fix to the bug category | Use the right playbook (Step 5), not a generic patch |
4.2 Consider Edge Cases in the Fix
For the planned fix, check each:
- Empty case — does the fix handle empty arrays, null, undefined, empty strings?
- Error case — does the fix handle network failures, invalid input, timeouts?
- Concurrent access — does the fix handle parallel requests? (if relevant)
- Existing tests — will the fix break any existing assertions? (read them)
- New dependency — does the fix require importing something new? (avoid if possible)
- Performance — does the fix add a query, loop, or allocation in a hot path?
- Backwards compatibility — does the fix change a public API? (flag to user)
4.3 Plan Defense-in-Depth
After fixing the immediate bug, consider whether a structural guard would prevent the entire class of bug from recurring:
| Bug Class | Defense-in-Depth Option |
|---|---|
| Null access | TypeScript strict null checks, NonNullable<T>, assertion function |
| Injection | Parameterized query builder, tagged template validation |
| Missing auth | Route-level middleware that requires explicit opt-out |
| Type mismatch | Zod schema at the boundary, runtime validation |
| Race condition | Database transaction, optimistic locking column |
| Resource leak | using declaration (TC39 Explicit Resource Management), try/finally |
Only add defense-in-depth if it's a small, focused addition. If it requires architectural changes, note it in the report as a recommendation for future work.
4.4 Create a Rollback Checkpoint
Before making any code changes, ensure you can revert cleanly:
# Stash any unrelated uncommitted work
git stash --include-untracked --message "pre-bugfix checkpoint"
# Or if on a clean working tree, just note the current HEAD
git rev-parse HEAD
If the fix goes wrong mid-implementation, you can:
# Revert all changes since the checkpoint
git checkout -- .
# Or restore the stash
git stash pop
4.5 Present the Plan (for significant fixes)
For High/Critical severity, multi-file changes, security fixes, or fixes that touch public API — present the plan before implementing:
## Fix Plan
**Bug:** [one-sentence description]
**Root Cause:** [one-sentence explanation — WHY, not just WHAT]
**Category:** [from §3.3 classification] → using [Playbook §5.X]
**Blast Radius:** [N files affected] — [list callers/dependents]
**Reproducer Test:**
- `test-file:line` — [test name] — currently FAILING ✗
**Changes:**
1. [file:line] — [what will change and why]
2. [file:line] — [what will change and why]
**Defense-in-Depth:** [structural guard to add, or "none needed"]
**Edge Cases Considered:**
- [case] — handled by [how]
- [case] — handled by [how]
**Risk:** [Low/Medium/High] — [why]
For simple, obvious fixes (Low severity, single-line, clear root cause, no public API change), proceed directly — don't over-ceremony a typo fix.
Step 5: Implement the Fix (Green Phase)
Gate: All planned changes applied, reproducer test passes, no regressions before proceeding to Step 6.
General Implementation Rules
- Edit files using the Edit tool (not sed/awk)
- Apply changes one logical unit at a time
- After each edit, verify the file is syntactically valid
- If the fix cascades (changing a type requires updating callers), follow the chain completely
- Follow existing code style (indentation, naming, patterns)
- Fix ONLY what the bug requires — nothing more
- Follow project conventions from CLAUDE.md:
- ESM only (
import/export, norequire) - Bare imports (
from "@ulpi/contracts", no/dist/paths) execFileSyncwith arg arrays (neverexecSyncwith template strings)
- ESM only (
After applying each change, run the reproducer test:
pnpm --filter <package> test -- --reporter=verbose <test-file> -t "<test-name>"
The reproducer test should go from RED (failing) → GREEN (passing) after the fix is applied. If it's still failing, the fix is incomplete or wrong.
§5.1 Injection Fix Playbook (SQL, Command, Template, Path)
Root cause pattern: User-controlled input concatenated into a query, command, template, or path string.
Fix approach — always parameterize, never sanitize:
// ✗ WRONG — string concatenation (the bug)
const result = db.query(`SELECT * FROM users WHERE id = '${userId}'`);
// ✗ WRONG — blocklist sanitization (incomplete fix — will be bypassed)
const safeId = userId.replace(/['";\-\-]/g, "");
const result = db.query(`SELECT * FROM users WHERE id = '${safeId}'`);
// ✓ CORRECT — parameterized query (the fix)
const result = db.query("SELECT * FROM users WHERE id = $1", [userId]);
For command injection:
// ✗ WRONG — shell interpolation
execSync(`git log --author="${author}"`);
// ✓ CORRECT — arg array, no shell
execFileSync("git", ["log", `--author=${author}`]);
For path traversal:
// ✗ WRONG — direct concatenation
const filePath = path.join(uploadDir, req.params.filename);
// ✓ CORRECT — resolve and verify within boundary
const filePath = path.resolve(uploadDir, req.params.filename);
if (!filePath.startsWith(path.resolve(uploadDir))) {
throw new Error("Path traversal attempt");
}
Verification checklist for injection fixes:
- Is the input parameterized (not concatenated)?
- Does the test include a payload that would exploit the original vulnerability? (e.g.,
'; DROP TABLE users; --) - Are ALL code paths that use this input parameterized? (search for other usages)
- If using an ORM, are raw query escapes also parameterized?
§5.2 XSS Fix Playbook
Root cause pattern: User-controlled input rendered in HTML output without escaping.
Fix approach — escape at output, not input:
// ✗ WRONG — innerHTML with user data
element.innerHTML = `<p>${userComment}</p>`;
// ✓ CORRECT — textContent (auto-escapes)
const p = document.createElement('p');
p.textContent = userComment;
element.appendChild(p);
// ✓ CORRECT — React (auto-escapes by default)
return <p>{userComment}</p>;
// ✗ WRONG — React dangerouslySetInnerHTML
return <div dangerouslySetInnerHTML={{ __html: userComment }} />;
Verification checklist for XSS fixes:
- Does the test include a
<script>alert(1)</script>payload? - Is every output point for this data escaped? (search for all render sites)
- Are URL parameters also sanitized? (
javascript:protocol, data URIs) - If using a framework that auto-escapes, is
dangerouslySetInnerHTML/{!! !!}/| safeavoided?
§5.3 Auth/AuthZ Fix Playbook
Root cause pattern: Missing authentication check, missing authorization check, or direct object reference without ownership validation.
Fix approach — deny by default, check explicitly:
// ✗ WRONG — no auth check on route
router.get("/api/users/:id/settings", getSettings);
// ✓ CORRECT — auth middleware + ownership check
router.get("/api/users/:id/settings", requireAuth, async (req, res) => {
const user = await getUser(req.params.id);
if (user.id !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: "Forbidden" });
}
return res.json(await getSettings(req.params.id));
});
For IDOR (Insecure Direct Object Reference):
// ✗ WRONG — trusts the ID from the URL
const order = await db.orders.findById(req.params.orderId);
// ✓ CORRECT — scopes query to authenticated user
const order = await db.orders.findOne({
id: req.params.orderId,
userId: req.user.id, // ownership check
});
if (!order) return res.status(404).json({ error: "Not found" });
Verification checklist for auth fixes:
- Does the test verify that unauthenticated requests are rejected (401)?
- Does the test verify that unauthorized users cannot access other users' data (403)?
- Are ALL endpoints for this resource protected? (list endpoints, check each)
- Is the auth check in middleware (not repeated inline in each handler)?
- Is the ownership check scoped to the query (not a post-fetch check that leaks timing)?
§5.4 Null Safety Fix Playbook
Root cause pattern: Property access or method call on a value that can be null/undefined at runtime.
Fix approach — fix the SOURCE of null, not just the access site:
// ✗ WEAK — null guard at the symptom (masks the real issue)
const name = user?.name ?? "Unknown";
// FIRST: Ask WHY user is null
// If user SHOULD always exist (e.g., after auth middleware):
// → Fix the middleware/provider that should guarantee non-null
// If user CAN legitimately be null (e.g., optional lookup):
// → Handle the null case with appropriate business logic
// ✓ CORRECT — handle at the business logic level
const user = await findUser(id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// user is now narrowed to non-null by TypeScript
processUser(user); // no optional chaining needed
When optional chaining IS appropriate:
- Data from external APIs where schema is not guaranteed
- Optional configuration values with sensible defaults
- Display logic where missing data should show fallback UI
Verification checklist for null fixes:
- Did you fix the SOURCE of null, or just guard the access? (Prefer fixing the source)
- Does the test cover the null case with an explicit assertion?
- Is the TypeScript type accurate? (If the value can be null, the type should include
| null) - If you added a null guard, does the else branch have appropriate behavior? (Not just
returnor swallow)
§5.5 Race Condition Fix Playbook
Root cause pattern: TOCTOU (time-of-check-time-of-use), concurrent modification without synchronization, double-submit.
Fix approach — make the operation atomic:
// ✗ WRONG — TOCTOU: check then update (race window between check and update)
const balance = await getBalance(userId);
if (balance >= amount) {
await deductBalance(userId, amount); // another request could deduct between check and here
}
// ✓ CORRECT — atomic operation with transaction
await db.transaction(async (tx) => {
const { balance } = await tx.query(
"SELECT balance FROM accounts WHERE user_id = $1 FOR UPDATE", // row lock
[userId],
);
if (balance < amount) throw new InsufficientFundsError();
await tx.query(
"UPDATE accounts SET balance = balance - $1 WHERE user_id = $2",
[amount, userId],
);
});
For idempotency (double-submit):
// ✓ Use idempotency key
router.post("/api/payments", async (req, res) => {
const idempotencyKey = req.headers["idempotency-key"];
const existing = await db.payments.findByKey(idempotencyKey);
if (existing) return res.json(existing); // return cached result
// ... process payment
});
For optimistic locking:
// ✓ Use version column
const result = await db.query(
"UPDATE items SET data = $1, version = version + 1 WHERE id = $2 AND version = $3",
[newData, itemId, expectedVersion],
);
if (result.rowCount === 0) throw new ConcurrentModificationError();
Verification checklist for race condition fixes:
- Is the critical section atomic? (Single query, transaction, or mutex)
- Does the test simulate concurrent access? (Multiple simultaneous calls)
- Is the fix correct under retry? (Idempotent operations)
- Are there other code paths to the same shared state? (Check all writers)
§5.6 Boundary / Off-by-One Fix Playbook
Root cause pattern: Wrong loop bound, array index, pagination offset, slice endpoint.
Fix approach — reason about the boundaries explicitly:
// ✗ WRONG — off-by-one in pagination
const items = data.slice(page * pageSize, page * pageSize + pageSize - 1);
// ^^^ slice end is exclusive, so this misses the last item
// ✓ CORRECT
const start = page * pageSize;
const items = data.slice(start, start + pageSize); // slice end is exclusive, so this gets exactly pageSize items
Boundary reasoning template:
Value range: [min, max] (inclusive or exclusive?)
Loop: for (i = START; i COMPARISON BOUND; i INCREMENT)
- First iteration: i = ?
- Last iteration: i = ?
- Total iterations: ?
- Does this match the expected count?
Verification checklist for boundary fixes:
- Test with boundary values: 0, 1, max, max+1
- Test with empty input (length 0)
- Test with single element (length 1)
- Verify inclusive vs exclusive bounds are consistent
- Check if the same boundary logic exists elsewhere (DRY it if repeated)
§5.7 Async / Promise Fix Playbook
Root cause pattern: Missing await, unhandled rejection, stale closure, promise returned but not awaited.
Fix approach — ensure every async operation is awaited and errors are caught:
// ✗ WRONG — missing await (error is silently swallowed)
async function processItems(items: Item[]) {
items.forEach(async (item) => {
// forEach doesn't await the callback!
await processItem(item);
});
// Returns here BEFORE any items are processed
}
// ✓ CORRECT — use for...of or Promise.all
async function processItems(items: Item[]) {
// Sequential:
for (const item of items) {
await processItem(item);
}
// Or parallel:
await Promise.all(items.map((item) => processItem(item)));
}
For stale closures:
// ✗ WRONG — stale closure captures initial value
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // always uses the count from when effect was created
}, 1000);
return () => clearInterval(interval);
}, []); // empty deps = stale closure
// ✓ CORRECT — functional updater
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1); // uses current value
}, 1000);
return () => clearInterval(interval);
}, []);
Verification checklist for async fixes:
- Is every
asyncfunction callawait-ed? (Search for calls withoutawait) - Are
.forEach(async ...)patterns replaced withfor...oforPromise.all? - Are promise rejections caught? (try/catch or
.catch()) - Does the test verify the operation completes before assertions run?
- For React: are effect cleanup functions correct? Are deps arrays complete?
§5.8 Type Safety Fix Playbook
Root cause pattern: Runtime type doesn't match compile-time type, implicit coercion, wrong as cast.
Fix approach — validate at boundaries, trust within:
// ✗ WRONG — trusting external data matches TypeScript type
const data = JSON.parse(body) as UserInput; // as-cast provides zero runtime safety
// ✓ CORRECT — validate with Zod at the boundary
import { UserInputSchema } from "@ulpi/contracts";
const data = UserInputSchema.parse(JSON.parse(body)); // throws on invalid input
// ✗ WRONG — loose equality allows type coercion
if (userId == 0) { ... } // '' == 0 is true!
// ✓ CORRECT — strict equality
if (userId === 0) { ... }
Verification checklist for type fixes:
- Is external data (API, user input, DB) validated at the boundary?
- Are
ascasts replaced with runtime validation? (Search forasin the file) - Is
===used instead of==? (Search for==in the file) - Does the TypeScript type accurately reflect all possible runtime values?
§5.9 Resource Leak Fix Playbook
Root cause pattern: Unclosed file handle, database connection, event listener, timer, WebSocket.
Fix approach — ensure cleanup in all code paths (success, error, early return):
// ✗ WRONG — connection leaks on error
async function queryData() {
const conn = await pool.getConnection();
const result = await conn.query("SELECT ..."); // if this throws, conn is never released
conn.release();
return result;
}
// ✓ CORRECT — finally block ensures cleanup
async function queryData() {
const conn = await pool.getConnection();
try {
return await conn.query("SELECT ...");
} finally {
conn.release();
}
}
// ✗ WRONG — event listener never removed
element.addEventListener("resize", handler);
// ✓ CORRECT — remove in cleanup
element.addEventListener("resize", handler);
// In cleanup/dispose/useEffect return:
element.removeEventListener("resize", handler);
Verification checklist for resource leak fixes:
- Is cleanup in a
finallyblock (not just the happy path)? - Are event listeners removed in the corresponding cleanup function?
- Are timers (
setInterval,setTimeout) cleared in cleanup? - Does the test verify cleanup runs? (Mock the resource, assert
.release()/.close()called)
§5.10 Logic / Business Rule Fix Playbook
Root cause pattern: Wrong conditional, inverted check, missing state validation, incorrect algorithm.
Fix approach — reason from the specification, not the code:
- State what the correct behavior is (from the bug report, spec, or user)
- Read the current code and identify WHERE it diverges from correct behavior
- Fix the divergence — don't rewrite the whole function
// ✗ WRONG — inverted condition
if (order.status === "shipped") {
allowCancel(); // Bug: shouldn't allow cancel after shipping
}
// ✓ CORRECT
if (order.status === "pending" || order.status === "processing") {
allowCancel();
}
For state machine bugs:
// ✓ Explicit valid transitions
const VALID_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
draft: ["pending"],
pending: ["processing", "cancelled"],
processing: ["shipped", "cancelled"],
shipped: ["delivered", "returned"],
delivered: [],
cancelled: [],
returned: [],
};
function transitionOrder(order: Order, newStatus: OrderStatus) {
if (!VALID_TRANSITIONS[order.status]?.includes(newStatus)) {
throw new InvalidTransitionError(order.status, newStatus);
}
order.status = newStatus;
}
Verification checklist for logic fixes:
- Does the test cover the exact scenario from the bug report?
- Are all branches of the conditional tested? (Not just the buggy one)
- If fixing a state machine, are all valid and invalid transitions tested?
- Is the fix consistent with other similar logic in the codebase?
Step 6: Verify the Fix
Gate: All tests pass, no type errors, reproducer is green, no regressions before proceeding to Step 7.
6.1 Run Tests
# Run tests for the affected package
pnpm --filter <affected-package> test
# If changes cross package boundaries, run the full suite
pnpm test
# Run type checking
pnpm -r typecheck
6.2 Compare Against Baseline
| Metric | Baseline (Step 2) | After Fix | Verdict |
|---|---|---|---|
| Passing tests | N | N + (new tests) | Must increase |
| Failing tests | M + 1 (reproducer) | M | Reproducer now passes |
| Type errors | P | 0 | Must be zero |
6.3 Verify the Bug is
…(truncated)