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.