{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"reading-a-diff-properly","title":"Reading a diff properly","url":"https://addaly.com/learn/building-apps-with-ai/reading-a-diff-properly","position":20,"of":87,"minutes":8,"answer":"Read the removed lines before the added ones and start with git diff --stat, because unexpected files and swapped conditions are visible before you read any code.","keyIdea":"Read the removed lines before the added ones and start with git diff --stat, because unexpected files and swapped conditions are visible before you read any code."},"body":{"format":"markdown","text":"## The unit of review is the change\n\nNobody reads a codebase. People read changes. Once you accept that, reviewing generated code stops being impossible and becomes a fifteen-minute habit.\n\n```bash\ngit diff              # changes not yet staged\ngit diff --staged     # changes staged for the next commit\ngit diff --stat       # just the file names and how much moved\ngit diff HEAD~1       # everything since the previous commit\n```\n\nYour editor shows the same thing with colours and side-by-side panes. Use whichever you will actually look at.\n\n## The anatomy\n\n```diff\ndiff --git a/lib/queries.ts b/lib/queries.ts\n@@ -42,7 +42,11 @@ export async function getBookings(userId: string) {\n   const rows = await db\n     .select()\n     .from(bookings)\n-    .where(eq(bookings.userId, userId))\n+    .where(eq(bookings.status, 'confirmed'))\n     .orderBy(desc(bookings.startsAt))\n \n   return rows\n }\n```\n\nRead it in this order.\n\n**The file name.** Is this a file you expected to change?\n\n**The `@@` line.** Line 42, and the function name after it. That is context you get for free.\n\n**The minus lines.** What was removed. This is the half people skip and it is where the damage lives.\n\n**The plus lines.** What replaced it.\n\nIn that example a filter by user was replaced by a filter by status. Every user now sees every confirmed booking in the system. The addition looks perfectly reasonable on its own; only the removal reveals the fault. This exact shape (a `where` clause swapped rather than extended) is one of the most common serious defects in AI-modified data code.\n\n## Start with --stat, always\n\nBefore reading a single line, look at the shape:\n\n```bash\ngit diff --stat\n components/BookingRow.tsx      |  24 ++++++++\n app/api/bookings/route.ts      |  12 ++--\n lib/dates.ts                   | 118 ++++++++--------------\n package.json                   |   3 +-\n```\n\nFour questions, ten seconds:\n\n- Are there files here I did not expect? `lib/dates.ts` was not part of the request.\n- Is anything much bigger than the task? 118 lines for a cancel button is not a cancel button.\n- Did `package.json` change? A new dependency arrived without being mentioned.\n- Did any lock file, migration or config change? Those are the ones with consequences beyond this change.\n\nMost problems are visible at this level, before you read any code at all.\n\n## Five patterns to catch on sight\n\n**A condition removed.** Any deleted `if`, `where`, or `&&` deserves an explanation. Conditions are usually there because something went wrong once.\n\n**An `await` that disappeared.** Removing `await` makes code look synchronous and finish before the work does. It often appears to work, because the operation completes a few milliseconds later.\n\n**A catch block that swallows.** `catch (e) {}` or `catch (e) { console.log(e) }` turns a failure into silence, and silence looks like success.\n\n**A comparison flipped.** `>=` becoming `>`, or `!==` becoming `!=`. One character, and the booking that ends exactly at 14:30 now blocks the one starting at 14:30.\n\n**A hardcoded value replacing a variable.** A model that could not work out where a value came from will sometimes write the value it saw in your example.\n\n> Figure: git diff --stat for a cancel button. Bars (lines changed): lib/dates.ts 118; components/BookingRow.tsx 24; app/api/bookings/route.ts 12; package.json 3. Ten seconds, before a line of code is read. The third row was not part of the request and is five times the size of the thing you asked for. Ask why before going further.\n\n## Read the whole file when the diff will not tell you\n\nA diff shows what changed, not what the surrounding code assumed. If a change touches something that matters (money, permissions, deletion) open the file and read the function whole. Ten extra lines of context frequently reverse your reading of a change.\n\nThis is the honest limitation of diff review, and it is why \"the diff looked fine\" is not the same as \"the code is correct\".\n\n## Make small commits so diffs stay readable\n\nA diff across one task is reviewable. A diff across an afternoon is not, and will be skimmed regardless of intention. Every argument for small commits in the git block is an argument about this: you are protecting your own ability to review.\n\nAnd if a diff has become unreadable, you have a legitimate move available. Revert it, ask for the same work in three steps, and read three small diffs instead of one large one that you would have approved without understanding."},"check":{"question":"In a diff, a line filtering bookings by user id is removed and a line filtering by status is added. Why is the removal the more important half?","options":[{"text":"Removed lines are harder for the model to justify, so they signal lower confidence.","correct":false,"tempting":"Reads intent from the diff rather than consequence; the model is equally confident about both halves."},{"text":"Because losing the user filter exposes every user's data, while the added filter reads as a sensible improvement on its own.","correct":true},{"text":"Because removals are more likely to break the build than additions.","correct":false,"tempting":"Reaches for a compile-time symptom; this change compiles cleanly, which is exactly why it survives to production."},{"text":"Because git applies removals before additions when merging.","correct":false,"tempting":"Invents a mechanical ordering that has no bearing on what the resulting code does."}]},"prev":{"slug":"reading-code-you-did-not-write","title":"Reading code you did not write","url":"https://addaly.com/learn/building-apps-with-ai/reading-code-you-did-not-write"},"next":{"slug":"the-shape-of-a-project","title":"The shape of a project you did not lay out","url":"https://addaly.com/learn/building-apps-with-ai/the-shape-of-a-project"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Reading a diff properly', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/reading-a-diff-properly (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Reading a diff properly","courseTitle":"Building Apps with AI","module":{"index":3,"slug":"reading-and-checking","title":"Reading and checking what came back"},"answer":null,"keyIdea":"Read the removed lines before the added ones and start with git diff --stat, because unexpected files and swapped conditions are visible before you read any code.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"bars","unit":"lines changed","title":"git diff --stat for a cancel button","series":[{"label":"lib/dates.ts","value":118,"highlight":true},{"label":"components/BookingRow.tsx","value":24},{"label":"app/api/bookings/route.ts","value":12},{"label":"package.json","value":3}],"caption":"Ten seconds, before a line of code is read. The third row was not part of the request and is five times the size of the thing you asked for. Ask why before going further."},"alt":"Figure: git diff --stat for a cancel button. Bars (lines changed): lib/dates.ts 118; components/BookingRow.tsx 24; app/api/bookings/route.ts 12; package.json 3. Ten seconds, before a line of code is read. The third row was not part of the request and is five times the size of the thing you asked for. Ask why before going further."}},"blocks":[{"t":"h2","id":"the-unit-of-review-is-the-change","runs":[{"text":"The unit of review is the change"}]},{"t":"p","runs":[{"text":"Nobody reads a codebase. People read changes. Once you accept that, reviewing generated code stops being impossible and becomes a fifteen-minute habit."}]},{"t":"code","lang":"bash","label":"Bash","text":"git diff              # changes not yet staged\ngit diff --staged     # changes staged for the next commit\ngit diff --stat       # just the file names and how much moved\ngit diff HEAD~1       # everything since the previous commit"},{"t":"p","runs":[{"text":"Your editor shows the same thing with colours and side-by-side panes. Use whichever you will actually look at."}]},{"t":"h2","id":"the-anatomy","runs":[{"text":"The anatomy"}]},{"t":"code","lang":"diff","label":"Diff","text":"diff --git a/lib/queries.ts b/lib/queries.ts\n@@ -42,7 +42,11 @@ export async function getBookings(userId: string) {\n   const rows = await db\n     .select()\n     .from(bookings)\n-    .where(eq(bookings.userId, userId))\n+    .where(eq(bookings.status, 'confirmed'))\n     .orderBy(desc(bookings.startsAt))\n \n   return rows\n }"},{"t":"p","runs":[{"text":"Read it in this order."}]},{"t":"p","runs":[{"text":"The file name.","b":true},{"text":" Is this a file you expected to change?"}]},{"t":"p","runs":[{"text":"The ","b":true},{"text":"@@","b":true,"code":true},{"text":" line.","b":true},{"text":" Line 42, and the function name after it. That is context you get for free."}]},{"t":"p","runs":[{"text":"The minus lines.","b":true},{"text":" What was removed. This is the half people skip and it is where the damage lives."}]},{"t":"p","runs":[{"text":"The plus lines.","b":true},{"text":" What replaced it."}]},{"t":"p","runs":[{"text":"In that example a filter by user was replaced by a filter by status. Every user now sees every confirmed booking in the system. The addition looks perfectly reasonable on its own; only the removal reveals the fault. This exact shape (a "},{"text":"where","code":true},{"text":" clause swapped rather than extended) is one of the most common serious defects in AI-modified data code."}]},{"t":"h2","id":"start-with-stat-always","runs":[{"text":"Start with --stat, always"}]},{"t":"p","runs":[{"text":"Before reading a single line, look at the shape:"}]},{"t":"code","lang":"bash","label":"Bash","text":"git diff --stat\n components/BookingRow.tsx      |  24 ++++++++\n app/api/bookings/route.ts      |  12 ++--\n lib/dates.ts                   | 118 ++++++++--------------\n package.json                   |   3 +-"},{"t":"p","runs":[{"text":"Four questions, ten seconds:"}]},{"t":"ul","items":[[{"text":"Are there files here I did not expect? "},{"text":"lib/dates.ts","code":true},{"text":" was not part of the request."}],[{"text":"Is anything much bigger than the task? 118 lines for a cancel button is not a cancel button."}],[{"text":"Did "},{"text":"package.json","code":true},{"text":" change? A new dependency arrived without being mentioned."}],[{"text":"Did any lock file, migration or config change? Those are the ones with consequences beyond this change."}]]},{"t":"p","runs":[{"text":"Most problems are visible at this level, before you read any code at all."}]},{"t":"h2","id":"five-patterns-to-catch-on-sight","runs":[{"text":"Five patterns to catch on sight"}]},{"t":"p","runs":[{"text":"A condition removed.","b":true},{"text":" Any deleted "},{"text":"if","code":true},{"text":", "},{"text":"where","code":true},{"text":", or "},{"text":"&&","code":true},{"text":" deserves an explanation. Conditions are usually there because something went wrong once."}]},{"t":"p","runs":[{"text":"An ","b":true},{"text":"await","b":true,"code":true},{"text":" that disappeared.","b":true},{"text":" Removing "},{"text":"await","code":true},{"text":" makes code look synchronous and finish before the work does. It often appears to work, because the operation completes a few milliseconds later."}]},{"t":"p","runs":[{"text":"A catch block that swallows.","b":true},{"text":" "},{"text":"catch (e) {}","code":true},{"text":" or "},{"text":"catch (e) { console.log(e) }","code":true},{"text":" turns a failure into silence, and silence looks like success."}]},{"t":"p","runs":[{"text":"A comparison flipped.","b":true},{"text":" "},{"text":">=","code":true},{"text":" becoming "},{"text":">","code":true},{"text":", or "},{"text":"!==","code":true},{"text":" becoming "},{"text":"!=","code":true},{"text":". One character, and the booking that ends exactly at 14:30 now blocks the one starting at 14:30."}]},{"t":"p","runs":[{"text":"A hardcoded value replacing a variable.","b":true},{"text":" A model that could not work out where a value came from will sometimes write the value it saw in your example."}]},{"t":"h2","id":"read-the-whole-file-when-the-diff-will-not-tell-you","runs":[{"text":"Read the whole file when the diff will not tell you"}]},{"t":"p","runs":[{"text":"A diff shows what changed, not what the surrounding code assumed. If a change touches something that matters (money, permissions, deletion) open the file and read the function whole. Ten extra lines of context frequently reverse your reading of a change."}]},{"t":"p","runs":[{"text":"This is the honest limitation of diff review, and it is why “the diff looked fine” is not the same as “the code is correct”."}]},{"t":"h2","id":"make-small-commits-so-diffs-stay-readable","runs":[{"text":"Make small commits so diffs stay readable"}]},{"t":"p","runs":[{"text":"A diff across one task is reviewable. A diff across an afternoon is not, and will be skimmed regardless of intention. Every argument for small commits in the git block is an argument about this: you are protecting your own ability to review."}]},{"t":"p","runs":[{"text":"And if a diff has become unreadable, you have a legitimate move available. Revert it, ask for the same work in three steps, and read three small diffs instead of one large one that you would have approved without understanding."}]}],"check":{"question":[{"text":"In a diff, a line filtering bookings by user id is removed and a line filtering by status is added. Why is the removal the more important half?"}],"options":[{"text":[{"text":"Removed lines are harder for the model to justify, so they signal lower confidence."}],"correct":false,"tempting":[{"text":"Reads intent from the diff rather than consequence; the model is equally confident about both halves."}]},{"text":[{"text":"Because losing the user filter exposes every user’s data, while the added filter reads as a sensible improvement on its own."}],"correct":true,"tempting":null},{"text":[{"text":"Because removals are more likely to break the build than additions."}],"correct":false,"tempting":[{"text":"Reaches for a compile-time symptom; this change compiles cleanly, which is exactly why it survives to production."}]},{"text":[{"text":"Because git applies removals before additions when merging."}],"correct":false,"tempting":[{"text":"Invents a mechanical ordering that has no bearing on what the resulting code does."}]}],"order":[2,3,1,0],"why":null},"prev":{"slug":"reading-code-you-did-not-write","title":"Reading code you did not write","minutes":9},"next":{"slug":"the-shape-of-a-project","title":"The shape of a project you did not lay out","minutes":8}}}