# Ulpi Io Browse Bugfix

> <EXTREMELY-IMPORTANT>

- Skill: `tomevault-io/ulpi-io-browse-bugfix` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/ulpi-io-browse-bugfix`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/ulpi-io-browse-bugfix/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/ulpi-io-browse-bugfix

---


<EXTREMELY-IMPORTANT>
Before fixing ANY bug, you **ABSOLUTELY MUST**:

1. Write a failing test that reproduces the bug BEFORE writing any fix code
2. Identify the root cause — not just the symptom
3. Understand all callers and dependents of the code you will change
4. Run existing tests to establish a passing baseline
5. Plan the minimal fix that addresses the root cause without side effects
6. 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.
</EXTREMELY-IMPORTANT>

# Bugfix

## MANDATORY FIRST RESPONSE PROTOCOL

Before fixing ANY bug, you **MUST** complete this checklist:

1. ☐ Parse the bug source (find-bugs output, user report, or observed failure)
2. ☐ Read the relevant code thoroughly (not just the flagged line — the full function, its callers, and its tests)
3. ☐ Write a failing test that reproduces the exact bug (Red phase)
4. ☐ Identify the root cause by tracing data flow from entry to failure point
5. ☐ Map the blast radius (callers, importers, dependents, type consumers)
6. ☐ Run existing tests to establish a passing baseline (the new reproducer should be the only new failure)
7. ☐ Plan the minimal fix (smallest change that addresses root cause)
8. ☐ 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-bugs` or 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-simplify` for 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-bugs` report 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

1. **Always load the base reference** — for Node.js projects, always read `nodejs-typescript.md`; for Go projects, always read `golang.md`; for Rust projects, always read `rust.md`
2. **Layer framework-specific reference on top** — if using Express, read BOTH `nodejs-typescript.md` AND `expressjs.md`; if using Axum, read BOTH `rust.md` AND `rust-axum.md`
3. **React Native includes React** — read both `react.md` and `react-native.md`
4. **Next.js/Remix include React** — read both `react.md` and the framework file
5. **Go frameworks layer on Go** — read both `golang.md` and the framework file (e.g., `go-gin.md`)
6. **Rust frameworks layer on Rust** — read both `rust.md` and the framework file (e.g., `rust-axum.md`)
7. **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":

1. Extract the specified findings
2. Sort by severity (Critical first)
3. Group findings that touch the same file or function (fix together to avoid edit conflicts)
4. Build a dependency graph: does fixing #1 change code that #3 depends on?
5. Identify which **fix playbook** (Step 5) applies to each finding

### From User Report

If the user describes a bug in their own words:

1. Extract the **symptom** (what goes wrong — error message, wrong output, crash, hang)
2. Extract the **trigger** (what action causes it — specific input, API call, sequence of events)
3. Extract the **expected behavior** (what should happen instead)
4. Extract the **location** (file, function, endpoint — if provided)
5. 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):

1. Capture the **exact error output** (full stack trace, not just the message)
2. Identify the **failing test or build step** (which command, which assertion)
3. Parse the **stack trace** — read bottom-to-top for the call chain, identify the frame in project code (skip node_modules/framework frames)
4. Trace to the **source file and line** from the stack
5. 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

```bash
# 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:

```typescript
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

```bash
# 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:**

1. **Check the environment** — Does the bug require specific config, env vars, or database state?
2. **Check the input** — Is the triggering input more specific than you assumed? Try exact values from the report.
3. **Check for intermittency** — Is this a race condition that only happens under concurrency? (See Race Condition Playbook)
4. **Check the version** — Has the code changed since the bug was reported? Run `git log` on the file.
5. **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:

1. Read the buggy function in its entirety (use the Read tool)
2. Read the function's callers — who calls this and with what arguments?
3. Read the function's dependencies — what does it call and what does it expect back?
4. Read the type definitions for all parameters and return values
5. Read the test file — what scenarios ARE covered? What's missing?

```bash
# 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:

```bash
# 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?

```bash
# 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:

1. **One-sentence test:** Can you explain the root cause in one sentence? If not, keep digging.
2. **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.
3. **Simplicity test:** Is there a simpler explanation? Prefer the simplest root cause that explains all symptoms (Occam's razor).
4. **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:

```bash
# 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:

```bash
# 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

1. Edit files using the Edit tool (not sed/awk)
2. Apply changes one logical unit at a time
3. After each edit, verify the file is syntactically valid
4. If the fix cascades (changing a type requires updating callers), follow the chain completely
5. Follow existing code style (indentation, naming, patterns)
6. Fix ONLY what the bug requires — nothing more
7. Follow project conventions from CLAUDE.md:
   - ESM only (`import`/`export`, no `require`)
   - Bare imports (`from "@ulpi/contracts"`, no `/dist/` paths)
   - `execFileSync` with arg arrays (never `execSync` with template strings)

### After applying each change, run the reproducer test:

```bash
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:**

```typescript
// ✗ 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:**

```typescript
// ✗ WRONG — shell interpolation
execSync(`git log --author="${author}"`);

// ✓ CORRECT — arg array, no shell
execFileSync("git", ["log", `--author=${author}`]);
```

**For path traversal:**

```typescript
// ✗ 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:**

```typescript
// ✗ 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`/`{!! !!}`/`| safe` avoided?

---

### §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:**

```typescript
// ✗ 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):**

```typescript
// ✗ 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:**

```typescript
// ✗ 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 `return` or 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:**

```typescript
// ✗ 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):**

```typescript
// ✓ 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:**

```typescript
// ✓ 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:**

```typescript
// ✗ 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:**

```typescript
// ✗ 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:**

```typescript
// ✗ 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 `async` function call `await`-ed? (Search for calls without `await`)
- [ ] Are `.forEach(async ...)` patterns replaced with `for...of` or `Promise.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:**

```typescript
// ✗ 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
```

```typescript
// ✗ 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 `as` casts replaced with runtime validation? (Search for `as` in 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):**

```typescript
// ✗ 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();
  }
}
```

```typescript
// ✗ 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 `finally` block (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:**

1. **State what the correct behavior is** (from the bug report, spec, or user)
2. **Read the current code** and identify WHERE it diverges from correct behavior
3. **Fix the divergence** — don't rewrite the whole function

```typescript
// ✗ 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:**

```typescript
// ✓ 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

```bash
# 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)
