ambiguous expectations, and gives reviewers a baseline for validation.
Phase 2: Automated Gating
Human reviewers should never validate formatting, type safety, or repetitive security patterns. These belong to CI/CD pipelines. Configure pre-commit hooks and CI stages to enforce linting, static analysis, dependency scanning, and test execution. Only when these gates pass should a PR enter human review. This reduces noise and keeps discussions focused on design and logic.
Phase 3: Structured Human Review
Reviewers evaluate four dimensions in strict order:
- Correctness: Does the logic match requirements? Are edge cases and failure modes explicitly handled?
- Security: Are external inputs validated at boundaries? Are authentication, authorization, and data leakage risks addressed?
- Performance: Are blocking operations isolated? Are timeouts and retry policies configured? How does this behave under scaled load?
- Maintainability: Is control flow explicit? Are names descriptive? Does the implementation follow existing patterns without unnecessary abstraction?
Phase 4: Feedback Calibration
Comments must be actionable, specific, and decoupled from author identity. Frame suggestions as architectural alternatives, not personal preferences. Use problem-first language: "When X occurs, Y breaks. Consider Z to maintain invariants." This keeps discussions technical and reduces defensive reactions.
Phase 5: Merge Execution
Merge only when intent is validated, automated gates pass, and all security/performance checkpoints are satisfied. Avoid partial approvals that accumulate hidden technical debt. If a PR requires multiple revision cycles, evaluate whether the scope was too broad or the initial intent was unclear.
Code Example: Intent-Validated Event Processor
The following TypeScript implementation demonstrates a review-friendly architecture. It uses explicit result types, separates validation from execution, and enforces clear boundaries. This structure makes correctness, security, and performance checks trivial for reviewers.
import { z } from 'zod';
// Explicit schema for input validation at system boundary
const EventPayloadSchema = z.object({
correlationId: z.string().uuid(),
timestamp: z.number().int().positive(),
payload: z.record(z.unknown()),
});
type ValidatedEvent = z.infer<typeof EventPayloadSchema>;
// Explicit result type to avoid exception-based control flow
type ProcessResult =
| { success: true; processedId: string }
| { success: false; reason: 'INVALID_INPUT' | 'UPSTREAM_TIMEOUT' | 'DUPLICATE' };
interface EventProcessor {
validate(raw: unknown): ProcessResult;
execute(event: ValidatedEvent): Promise<ProcessResult>;
}
class StreamProcessor implements EventProcessor {
private readonly idempotencyStore: Map<string, boolean>;
constructor() {
this.idempotencyStore = new Map();
}
validate(raw: unknown): ProcessResult {
const parseResult = EventPayloadSchema.safeParse(raw);
if (!parseResult.success) {
return { success: false, reason: 'INVALID_INPUT' };
}
return { success: true, processedId: parseResult.data.correlationId };
}
async execute(event: ValidatedEvent): Promise<ProcessResult> {
// Idempotency check prevents duplicate processing
if (this.idempotencyStore.has(event.correlationId)) {
return { success: false, reason: 'DUPLICATE' };
}
try {
// Explicit timeout prevents resource exhaustion under load
const response = await fetch('/api/ingest', {
method: 'POST',
body: JSON.stringify(event.payload),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
return { success: false, reason: 'UPSTREAM_TIMEOUT' };
}
this.idempotencyStore.set(event.correlationId, true);
return { success: true, processedId: event.correlationId };
} catch {
return { success: false, reason: 'UPSTREAM_TIMEOUT' };
}
}
}
Architecture Decisions & Rationale
- Explicit Result Types: Replacing thrown exceptions with union types (
ProcessResult) forces reviewers to verify every failure path. Exceptions hide control flow; explicit results make it visible and testable.
- Schema-First Validation: Using
zod at the boundary ensures security and correctness are checked before business logic executes. Reviewers can verify input constraints without reading implementation details.
- Idempotency Enforcement: Duplicates are caught at the processor level, not the network layer. This simplifies retry logic and prevents downstream state corruption.
- AbortSignal Timeout: Hardcoding timeouts prevents resource exhaustion under load. Reviewers can immediately spot scalability risks when timeouts are explicit rather than implicit.
- Interface Segregation: The
EventProcessor interface separates validation from execution, enabling mock testing and future substitution without modifying core logic.
Pitfall Guide
1. The Style Police Trap
- Explanation: Reviewers spend cycles debating naming conventions, brace placement, or import ordering. This inflates cycle time and masks architectural issues.
- Fix: Offload all formatting to Prettier/ESLint. Configure CI to reject style-only comments. Reserve human review for logic, security, and design.
2. Context Collapse
- Explanation: Reviewing a PR in isolation without understanding the broader system leads to false positives and missed integration risks.
- Fix: Require architecture diagrams or sequence flows for changes touching multiple services. Use
CODEOWNERS strategically, but mandate cross-team review for boundary-spanning changes.
3. The Approval Illusion
- Explanation: Reviewers approve PRs to meet velocity targets, assuming follow-up work will address gaps. This accumulates hidden technical debt.
- Fix: Tie merge eligibility to explicit criteria: automated gates pass, intent matches implementation, and all security/performance checkpoints are satisfied. Never approve with unresolved architectural concerns.
- Explanation: Feedback framed as criticism triggers ego responses, derailing technical discussion into interpersonal friction.
- Fix: Adopt the "problem-first" template: "When X occurs, Y breaks. Consider Z to maintain invariants." Focus on system behavior, not author choices.
5. Over-Automation Blindness
- Explanation: Relying entirely on static analysis creates a false sense of security. Linters catch syntax, not business logic flaws or race conditions.
- Fix: Pair automated scans with targeted property-based tests. Use mutation testing to verify test suite effectiveness. Treat CI as a filter, not a guarantee.
6. Scope Creep in Pull Requests
- Explanation: Mixing feature work, refactors, and bug fixes in a single PR makes review impossible and rollback dangerous.
- Fix: Enforce the "one logical change per PR" rule. Use feature flags for incremental delivery. If a PR exceeds 400 lines, split it before review begins.
7. Silent Failure Assumption
- Explanation: Assuming network calls, database queries, or third-party APIs will succeed leads to unhandled exceptions in production.
- Fix: Require explicit timeout, retry, and fallback configurations for all external dependencies. Reviewers must verify that failure paths are documented and tested.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Hotfix to production | Single-reviewer, automated gates only, immediate merge | Speed outweighs thoroughness; rollback strategy mitigates risk | Low (short-term), High (if rollback fails) |
| Greenfield feature | Two-reviewer, intent validation, security scan, performance baseline | Prevents architectural drift and security debt early | Medium (initial), Low (long-term) |
| Legacy refactor | Incremental PRs, property-based tests, cross-team review | Reduces regression risk and distributes knowledge | High (initial), Very Low (long-term) |
| Third-party integration | Security-focused review, contract testing, timeout enforcement | External dependencies introduce unpredictable failure modes | Medium, High (if integration breaks) |
Configuration Template
# .github/workflows/pr-review-gates.yml
name: PR Review Gates
on:
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run type checks
run: npx tsc --noEmit
- name: Run linter & formatter
run: npx eslint . --max-warnings=0 && npx prettier --check .
- name: Execute test suite
run: npx vitest run --coverage
- name: Security scan
run: npx audit-ci --critical
// .husky/pre-commit
#!/bin/sh
npx lint-staged
npx tsc --noEmit
# PR Review Checklist Template
## Intent Validation
- [ ] Does the implementation match the stated goal?
- [ ] Are excluded behaviors explicitly documented?
## Correctness & Security
- [ ] Are all external inputs validated at the boundary?
- [ ] Are failure modes handled explicitly (no silent catches)?
- [ ] Are timeouts and retries configured for network calls?
## Performance & Scale
- [ ] Will this behave predictably under 10x load?
- [ ] Are blocking operations isolated from async contexts?
## Maintainability
- [ ] Is control flow explicit (no hidden exceptions)?
- [ ] Are names descriptive and consistent with existing patterns?
Quick Start Guide
- Install tooling: Run
npm install -D husky lint-staged prettier eslint typescript vitest to establish automated gates.
- Initialize hooks: Execute
npx husky init and add type-checking and linting to the pre-commit script.
- Configure CI: Copy the GitHub Actions workflow into
.github/workflows/ to block merges until gates pass.
- Adopt the checklist: Paste the PR Review Checklist Template into your repository's
.github/PULL_REQUEST_TEMPLATE.md.
- Train the team: Run a 30-minute session demonstrating intent-driven review, explicit result types, and the new automation pipeline.