D
Dev SOPKnowledge Base
Search
← All topics

Code Review, QA Validation, and Pre-Release Gates

Code review checklist, automated QA gates, pre-release validation — TypeScript/lint checks, security review patterns, accessibility gates, performance budgets, and the structured fix attestation format for bug commits.

code-reviewqatestinglintingsecuritypre-release
Agent trigger phrases: code review · QA validation · pre-release gate · code review checklist · lint · type check · accessibility audit · fix attestation

Overview

Code review and QA gates exist at multiple levels: automated checks (lint, types, tests), manual review (logic, security, patterns), and visual verification (UI, accessibility). Nothing ships without passing all gates.

Automated Gate Checklist

Run before every PR merge:

# TypeScript
npx tsc --noEmit

# Lint
npm run lint

# Tests
npm test

# Build
npm run build

All four must pass. Any failure blocks the merge.

Code Review Checklist

Logic and Correctness

[ ] Function does what its name says
[ ] Edge cases handled: null, empty array, zero, negative numbers
[ ] Async errors caught (await without try/catch = unhandled rejection)
[ ] No off-by-one errors in loops
[ ] Correct comparison: === not ==
[ ] Race conditions considered for async operations

Security

[ ] No secrets or API keys in code or comments
[ ] User input validated before use (Zod at API boundary)
[ ] SQL queries parameterized (no string interpolation)
[ ] File paths validated against traversal (path.resolve, path.normalize)
[ ] Auth check before every protected operation
[ ] Rate limiting on public endpoints
[ ] RLS enabled on new Supabase tables

TypeScript Quality

[ ] No `any` types (use `unknown` + type narrowing instead)
[ ] No non-null assertions (!) without a comment explaining why it's safe
[ ] Error types are typed, not `catch (e)` with implicit `any`
[ ] Interfaces preferred over type aliases for objects
[ ] Exported types documented with JSDoc if complex

Performance

[ ] No N+1 queries (batch or join instead)
[ ] Large lists virtualized
[ ] Images have explicit dimensions
[ ] No blocking operations in React render path
[ ] No unbounded array operations (.collect() equivalent)

Maintainability

[ ] Function under 50 lines (extract if longer)
[ ] No magic numbers (use named constants)
[ ] Naming is clear without comments
[ ] Duplication extracted to shared utility
[ ] Complex logic has a comment explaining WHY (not what)

Security Review Patterns

Auth Bypass Check

// WRONG — missing auth check
export async function DELETE(request: NextRequest, { params }) {
  const { id } = await params;
  await db.from("projects").delete().eq("id", id);  // anyone can delete anything
  return new Response(null, { status: 204 });
}

// RIGHT — ownership verification
export async function DELETE(request: NextRequest, { params }) {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const { id } = await params;
  const project = await db.from("projects").select().eq("id", id).single();
  if (!project || project.user_id !== userId) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });  // don't reveal existence
  }

  await db.from("projects").delete().eq("id", id);
  return new Response(null, { status: 204 });
}

Input Validation Check

// WRONG — unvalidated input
export async function POST(request: NextRequest) {
  const { name, userId } = await request.json();
  await db.from("projects").insert({ name, userId });  // no validation
}

// RIGHT — validated at boundary
const Schema = z.object({ name: z.string().min(1).max(100) });

export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) return errorResponse("Unauthorized", 401);

  const parsed = Schema.safeParse(await request.json());
  if (!parsed.success) return errorResponse("Invalid", 422, parsed.error.flatten());

  await db.from("projects").insert({ name: parsed.data.name, user_id: userId });
}

Fix Attestation Format

Every bug fix commit must include:

fix: [short description]

FIX ATTESTATION:
- Broken: [observable symptom]
- Root cause: [actual bug]
- Changed: [files modified and what changed]
- Verified: [how confirmed it's fixed]
- Regression risk: [side effects or "none"]
- Timestamp: 2026-05-12T10:00:00Z

Example:

fix: API returns 500 when query contains special characters

FIX ATTESTATION:
- Broken: GET /api/search?q=C%2B%2B returns 500 Internal Server Error
- Root cause: Missing encodeURIComponent on user input before vector DB query
- Changed: src/app/api/search/route.ts — added input sanitization at line 23
- Verified: Added test case for special chars in test/api-search.test.ts, passes
- Regression risk: None — sanitization is additive, doesn't change valid queries
- Timestamp: 2026-05-12T10:30:00Z

QA Validation Gates

Visual Verification (UI changes)

# Screenshot before and after
npx playwright screenshot --full-page http://localhost:3000 before.png
# [make changes]
npx playwright screenshot --full-page http://localhost:3000 after.png

Required for: any UI change, new page, style updates. Screenshots attached to PR.

API Verification

# Health check
curl -s http://localhost:3000/api/health | jq .

# Auth check (should return 401)
curl -s http://localhost:3000/api/projects | jq .error

# Authenticated request
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/projects | jq .

Accessibility Gate

# Run axe accessibility check
npx @axe-core/cli http://localhost:3000

# Or in Playwright test
import { checkA11y } from "axe-playwright";
await checkA11y(page, undefined, { detailedReport: true });

Must pass: no critical or serious violations.

Delivery Report Format

Every delivery includes:

DELIVERED: [what was built]
PROOF: [screenshot paths or test output]
VALIDATION: lint: PASS | types: PASS | tests: PASS | build: PASS
VERIFICATION: CODE | VISUAL | FULL

No VALIDATION block = rejected by Carlos. No PROOF = no VISUAL or FULL verification claim.