Skip to content

Defect taxonomy scan

One pass per bug class, each with the input that triggers it; the MR's green tests clear none of them.

Predict first

Before you read: predict

MR !3361 adds a regrade for one question of an ExamHall exam; touchExam tells the proctor console to reload that exam's marks. The MR's test, question q7 with three answers and key b, passes:

A proctor regrades question q12, which no candidate has answered yet.

What happens?

Code for this question
// src/grading/regrade.ts (after the MR)import type { MarksStore } from '../infra/types'; export async function regradeQuestion(store: MarksStore, questionId: string, key: string) {  const answers = await store.answersTo(questionId);  for (const a of answers) await store.setMark(a.id, a.choice === key ? 1 : 0);  await store.touchExam(answers[0].examId);  return { regraded: answers.length };}
Pick one

The 30-second version

Read the diff once for what it should do. Then sweep it again, once per bug class on a fixed list: boundaries, absent values, state, errors, resources, then the async, security and performance lenses. Each pass asks one question of every changed line and tries the input that triggers it. A finding is a failing test, not a hunch.

One pass per bug class, each with the input that triggers it; the MR's green tests clear none of them.

How it works

Step 1 of 12

Intent first. Say what the change must do, and trace one request through it (mr-entry-point-tracing), before hunting bugs.
In the kitchen
  • she reads the request form first: what is the referring doctor asking? → intent first (step 1)

Analogy: A radiologist reading a chest X-ray with a fixed search pattern

In the reading room In the review
she reads the request form first: what is the referring doctor asking? intent first (step 1)
she sweeps the film one region at a time, in the same order every time one class per pass, from a fixed list (steps 2–3)
she looks twice at the review areas that hide findings: behind the heart, the lung tops the quiet classes: empty, absent, the error path (steps 4–8)
her report names the region and what she saw there a finding names its class and its trigger (step 9)
the referral says "fall, rib pain"; she finds the broken rib and still sweeps the rest, where a small nodule sits keep going after a find (step 11)
a shadow a second view shows to be an overlapping vessel is left out of the report drop what a test clears (step 12)

In real terms: a diff hides a few classes of bug; asking each class's question of every changed line, with the input that triggers it, finds the ones a single read skims past.

Breaks down when: a film is only looked at, but code can be run. Each pass ends in an executed trigger, not a judgment of shadows, and the MR's own tests are like a film taken from one angle only (step 10).

Contrasting case: a one-line typo fix in a comment has no list, state, error path or resource; a scan has nothing to sweep, and one read is enough.

Examples, run for real

4.1 ExamHall EXH-3060: five passes on "close the exam"

EXH-3060: ExamHall's "close the exam" MR passed one careful read, and its test, three open attempts with a draft each, was green. On exam day closing ECON-201 failed, and every later try hung. The store's type promises each open attempt a draft; a candidate who opened the paper and never typed has draft: null. Run the passes on the code as merged:

// src/exams/close-exam.ts (as merged)
import type { ExamDeps } from '../infra/types';

export const CLOSE_DEFAULTS = { notify: true };

export async function closeExam(deps: ExamDeps, examId: string, opts: { notify?: boolean } = {}) {
  const o = Object.assign(CLOSE_DEFAULTS, opts);
  await deps.lock.acquire(examId);
  const open = await deps.store.openAttempts(examId);
  for (const a of open) await deps.store.submit(a.id, a.draft.answers);
  if (o.notify) await deps.mail.resultsPending(examId);
  deps.lock.release(examId);
  return { closed: open.length };
}
4.1 as merged MR's test (3 open attempts, a draft each): {"closed":3} · emails 1 · green
4.1 as merged boundaries · 0 open attempts: {"closed":0} · lock held 0 · nothing to find
4.1 as merged absent values · a candidate who never typed (draft null): fails TypeError (Cannot read properties of null (reading 'answers')) · submitted 1 of 3 · red
4.1 as merged state · close e3 with { notify: false }, then close e4 with no options: emails for e4 0 · red
4.1 as merged errors · the store refuses a submit: fails Error · emails 0 · the failure reaches the caller: nothing to find
4.1 as merged resources · close e2 a second time: pending · lock held on e2 · red
4.1 MR !3360 MR's test (3 open attempts, a draft each): {"closed":3} · emails 1 · green
4.1 MR !3360 boundaries · 0 open attempts: {"closed":0} · lock held 0 · nothing to find
4.1 MR !3360 absent values · a candidate who never typed (draft null): {"closed":3} · submitted 3 of 3 · green
4.1 MR !3360 state · close e3 with { notify: false }, then close e4 with no options: emails for e4 1 · green
4.1 MR !3360 errors · the store refuses a submit: fails Error · emails 0 · the failure reaches the caller: nothing to find
4.1 MR !3360 resources · close e2 a second time: fulfilled · lock held on nothing · green
4.1 classes red on the MR: absent, state, resources · on MR !3360: none

Two passes find nothing; three find a defect each, and the resources defect shows only once the absent value has thrown (steps 5, 6, 8). MR !3360 spreads the defaults into a new object, reads a.draft?.answers ?? {} and releases the lock in finally: every trigger is then green.

4.2 Proof domain, chat: a suspicion a test clears

A chat MR adds lastActivity for the thread list's "last message" time. The boundaries pass suspects an empty thread:

// src/messages/last-activity.ts (after the MR)
import type { Message } from '../infra/types';

export function lastActivity(messages: Message[]): number | null {
  const last = messages.at(-1);
  return last?.sentAt ?? null;
}
4.2 suspicion (boundaries): an empty thread throws on .sentAt · trigger: lastActivity([]) -> null · green, so not posted · two messages -> 250

The trigger runs and the test passes, so the suspicion is not posted (step 12); the pass moves on.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

An MR to a team chat app adds message previews. The thread list calls previewText(m) on each message the store keeps in memory, and opening a thread renders each message's text from the same store. The MR's test, a 120-character message gets an 81-character preview ending in …, passes:

Which review comment names a real defect in this MR?

Code for this question
// src/messages/preview.ts (after the MR)import type { Message } from '../infra/types'; const MAX = 80; export function previewText(message: Message): string {  const text = message.text ?? '';  if (text.length <= MAX) return text;  message.text = text.slice(0, MAX) + '…';  return message.text;}
Pick one

Question 2 of 7

An MR to a video platform's upload worker reads each uploaded file's first bytes. files.open returns a handle, and the handle's close() releases it. The MR's test, three MP4 paths give three infos, passes:

The worker runs it on ten paths; the 4th and the 10th are not MP4s.

When it returns, how many of the ten files are still open?

Code for this question
// src/uploads/probe-uploads.ts (after the MR)import type { Files, Log, VideoInfo } from '../infra/types'; export async function probeUploads(files: Files, log: Log, paths: string[]): Promise<VideoInfo[]> {  const infos: VideoInfo[] = [];  for (const path of paths) {    const handle = await files.open(path);    const head = await handle.read(8);    if (!head.startsWith('ftyp')) {      log.warn(`skipped ${path}: not an MP4`);      continue;    }    infos.push({ path, brand: head.slice(4, 8) });    await handle.close();  }  return infos;}
Pick one

Question 3 of 7

A box office's seat map offers "two seats together" from firstPair, the first two adjacent free seats in a row. The MR's test, row J with only j3 and j4 free gives ['j3', 'j4'], passes:

Row K has ten seats, k1 to k10; only k1 and k10 are free.

What does firstPair do on row K?

Code for this question
// src/seatmap/first-pair.ts (after the MR)import type { Seat } from '../infra/types'; export function firstPair(row: Seat[]): [string, string] | null {  for (let i = 0; i < row.length; i++) {    if (row[i].status === 'free' && row[i + 1].status === 'free') return [row[i].id, row[i + 1].id];  }  return null;}
Pick one

Question 4 of 7

MR !3362 lets a proctor extend one candidate's time. The reviewer's first comment, that the extension added seconds where endsAt holds milliseconds, is fixed; his test (30 minutes moves endsAt by 1,800,000) and the MR's own (an unknown attempt gets 404) pass. An error that escapes the handler is answered 500 by the route. The proctor console retries a 5xx and shows "no such attempt" on a 404:

Which further comment names a real defect?

Code for this question
// src/proctor/extend-time.ts (after the fix commit)import type { AttemptsStore, Reply } from '../infra/types'; export async function extendTime(store: AttemptsStore, attemptId: string, minutes: number): Promise<Reply> {  try {    const attempt = await store.findAttempt(attemptId);    if (!attempt) return { status: 404, body: { message: 'no such attempt' } };    const endsAt = attempt.endsAt + minutes * 60_000;    await store.setEndsAt(attemptId, endsAt);    return { status: 200, body: { endsAt } };  } catch {    return { status: 404, body: { message: 'no such attempt' } };  }}
Pick one

Question 5 of 7

The interviewer: "MR !3363 adds a bulk regrade for a whole exam. It has five planted bugs; find as many as you can." answersFor returns the list the store also serves to the proctor console, and keys holds the new key of each regraded question only:

In at most eight lines, name each defect as its class and the input that triggers it.

Code for this question
// src/grading/bulk-regrade.ts (after the MR)import type { RegradeDeps } from '../infra/types'; export async function bulkRegrade(deps: RegradeDeps, examId: string, keys: Record<string, string>) {  const lock = await deps.locks.take(`regrade:${examId}`);  const answers = await deps.store.answersFor(examId);  answers.sort((a, b) => a.candidateId.localeCompare(b.candidateId));  let regraded = 0;  for (let i = 0; i < answers.length - 1; i++) {    const a = answers[i];    const key = keys[a.questionId];    try {      await deps.store.setMark(a.id, a.choice === key ? 1 : 0);    } catch {      deps.log.warn(`mark ${a.id} failed`);    }    regraded++;  }  lock.release();  return { status: 200, body: { regraded } };}

Question 6 of 7

A payments ledger's nightly job applies the day's entries to the account balances. The MR's test, two entries to an existing account add up, passes, and tsc --strict reports nothing:

Account acc-77 opened today: it is not in balances yet, and its first entry is +2,500 cents.

What does the returned object hold for acc-77?

Code for this question
// src/ledger/apply-entries.ts (after the MR)import type { Entry } from '../infra/types'; export function applyEntries(balances: Record<string, number>, entries: Entry[]): Record<string, number> {  const next = { ...balances };  for (const e of entries) next[e.account] = next[e.account] + e.amountCents;  return next;}
Pick one

Question 7 of 7

Two versions of a chat app's loadThread. The thread screen shows "No messages yet" when it gets an empty list, and "Couldn't load messages · Retry" when the call fails:

The message store is down.

What does a member opening a thread see with each version?

Code for this question
// src/messages/load-thread-x.ts (version X)import type { Message, MessageStore } from '../infra/types'; export async function loadThread(store: MessageStore, threadId: string): Promise<Message[]> {  try {    return await store.messages(threadId);  } catch {    return [];  }}// src/messages/load-thread-y.ts (version Y)import type { Log, Message, MessageStore } from '../infra/types'; export async function loadThread(store: MessageStore, log: Log, threadId: string): Promise<Message[]> {  try {    return await store.messages(threadId);  } catch (err) {    log.error(`thread ${threadId}: ${String(err)}`);    throw err;  }}
Pick one

Go deeper

Pro tip
  1. Let a search find each pass's stopping lines. Before a pass, search the changed lines for the syntax its class lives in: catch for errors, acquire, open( and release for resources, Object.assign, .sort( and .push( for state, [0] and length - 1 for boundaries. The pass still reads every line; the hits are where it stops. When: a diff longer than a screen (steps 2, 6–8). Verify (RAN, tips.mjs):
    tip search, close-exam.ts (11 lines): errors no hit · resources l.7, l.11 · state l.6 · boundaries no hit
    tip the absent-values pass has no syntax to search for: l.9 reads a.draft.answers, found by reading every line
    Status: RAN.
Source

Every defect this lesson grades is executed; the guides support the steps and key nothing. Valid as of 2026-10-06.

  • Google Engineering Practices, What to look for in a code review (the curriculum row's first source), https://google.github.io/eng-practices/review/reviewer/looking-for.html (google-looking-for.txt). Quoted: "as the reviewer you should still be thinking about edge cases, looking for concurrency problems, trying to think like a user, and making sure that there are no bugs that you see just by reading the code." Supports steps 3–4. Quoted: "don’t scan over a human-written class, function, or block of code and assume that what’s inside of it is okay." Supports step 2. Quoted: "Will the tests actually fail when the code is broken?" Supports step 10.
  • GitLab documentation, Code Review Guidelines (the row's third source), https://docs.gitlab.com/development/code_review/ (gitlab-code-review.txt). Quoted: "Understand why the change is necessary (fixes a bug, improves the user experience, refactors the existing code)." Supports step 1. Quoted: "Using checklists improves quality in software engineering." Supports steps 2–3.
  • OWASP, Code Review Guide project page (the row's second source), https://owasp.org/www-project-code-review-guide/ (owasp-code-review-guide.txt). The saved page is the project's landing page (licence, links, project facts) and holds no review method; it keys nothing and supports no step here. The security lens belongs to mr-security-lens.

Checked against the tools and docs as of 2026-10-06.