An AI-generated pull request lands in your queue: a new notifications preferences panel, ~700 lines, tests green, description immaculate. Five defects are seeded in it. You have a 45-minute review budget and the frontend provenance checklist — pass requires finding and writing up every critical seed.
Tomás reviews for a team where half the PRs are now agent-authored. They arrive polished: conventional commits, thorough descriptions, passing CI. The polish is the hazard — plausibility and correctness have never been less correlated, and the traditional reviewer heuristic ("messy PR, look closely; clean PR, skim") now selects for exactly the wrong PRs to skim. This lab trains the replacement heuristic: review AI PRs by provenance class — the specific ways generated frontend code goes wrong — on a fixed time budget, the way you actually work.
~60 minutes (45-minute timed review + 15 minutes write-up and key check) · Verify stage (maps to module M5, playbook PB-28) · $0 — no tools required beyond a timer; optionally your assistant as a second reader after your own pass.
No repo needed — this is a review exercise. Save the checklist and the diff excerpts below. The PR description you received:
PR #482 — Add notification preferences panel
Adds NotificationPrefs panel: fetches current prefs, renders grouped
toggles, saves on change with optimistic update. Includes loading
state, unit tests, and follows the design system.
✅ 14 tests passing · ✅ typecheck · ✅ lint
The frontend provenance checklist, checklist.md — review by class, not line-by-line:
1. HALLUCINATED API — props/components/endpoints that don't exist
2. STATE COVERAGE — loading / error / empty all present and reachable?
3. SEMANTIC HTML & A11Y — interactive elements real? labeled? focusable?
4. DESIGN-SYSTEM DRIFT — raw values where tokens/primitives exist
5. RENDER & EFFECT HYGIENE — effect deps, loops, unstable references
6. TESTS TEST SOMETHING — do the green tests assert behavior?
Key excerpts from the diff (the panel, its data hook, and a test). The design system context you know as a reviewer: Button accepts variant: "primary" | "secondary" | "danger"; tokens are CSS variables (--color-*); Switch and Banner primitives exist.
src/features/notifications/useNotificationPrefs.ts:
import { useEffect, useState } from "react";
import { api } from "../../lib/api";
export type Prefs = Record<string, boolean>;
export function useNotificationPrefs(userId: string, options = { revalidate: true }) {
const [prefs, setPrefs] = useState<Prefs | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
api.get(`/users/${userId}/notification-prefs`).then((res) => {
if (!cancelled) {
setPrefs(res.data);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId, options]);
const update = (key: string, value: boolean) => {
setPrefs((p) => ({ ...p, [key]: value }));
api.put(`/users/${userId}/notification-prefs`, { [key]: value });
};
return { prefs, loading, update };
}
src/features/notifications/NotificationPrefs.tsx:
import { Button } from "../../ds/Button";
import { useNotificationPrefs } from "./useNotificationPrefs";
const GROUPS = [
{ title: "Account", keys: ["security_alerts", "billing"] },
{ title: "Activity", keys: ["mentions", "replies", "digest"] },
];
export function NotificationPrefs({ userId }: { userId: string }) {
const { prefs, loading, update } = useNotificationPrefs(userId);
if (loading) return <p>Loading preferences…</p>;
return (
<section aria-label="Notification preferences">
{GROUPS.map((g) => (
<div key={g.title}>
<h3 style={{ color: "#334155", borderBottom: "1px solid #CBD5E1" }}>{g.title}</h3>
{g.keys.map((k) => (
<div
key={k}
onClick={() => update(k, !prefs?.[k])}
style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", cursor: "pointer" }}
>
<span>{k.replace("_", " ")}</span>
<span>{prefs?.[k] ? "On" : "Off"}</span>
</div>
))}
</div>
))}
<Button intent="danger" onClick={() => GROUPS.flatMap((g) => g.keys).forEach((k) => update(k, false))}>
Turn all off
</Button>
</section>
);
}
src/features/notifications/NotificationPrefs.test.tsx (representative excerpt of the 14):
it("renders groups after load", async () => {
mockApi.get.mockResolvedValue({ data: { mentions: true } });
render(<NotificationPrefs userId="u1" />);
expect(await screen.findByText("Account")).toBeInTheDocument();
expect(screen.getByText("Activity")).toBeInTheDocument();
});
it("turn all off button exists", async () => {
mockApi.get.mockResolvedValue({ data: {} });
render(<NotificationPrefs userId="u1" />);
expect(await screen.findByText("Turn all off")).toBeInTheDocument();
});
review.md with the six checklist classes as headings.useEffect closely: simulate two renders in your head — what is referentially stable across renders and what is not, and what does that do to the effect?The PR description says "includes loading state" — singular. AI-generated PRs describe what they did, never what they omitted, and reviewers anchor on descriptions. Review the diff, not the narration.
review.md contains findings organized under the six provenance classes, each with quoted line, mechanism, severity, and requested fix.AI-generated PRs fail in characteristic classes — hallucinated APIs, missing unhappy-path states, semantic-free interactivity, token drift, effect pathologies — so review by class with a time budget, treat description claims as assertions to verify, and never let green tests stand in for the states they were never written to see.