Human-in-the-Loop Code Review

Written by the author, AI-editedAI-generated, author-verified How this module was made

Vibe Coding versus AI-Augmented Development

Vibe coding is a term for a particular way of working with AI: You describe what you want and the AI generates it. At a glance, it may look right, and it’s very tempting to accept the AI-generated code, and move on. Not many may read the AI generated code differences, line by line. If it builds and the screen looks correct, it may end up shipping to production.

AI-augmented development looks similar from the outside. You’re still prompting an AI to generate the code, but there’s one crucial difference — a human is still in the loop, deciding whether what came from the AI, is good or not. That loop doesn’t close when the AI finishes generating. It closes when you, the human finishes reviewing.

That’s what this module is about. The AI can clearly write the code, within minutes, sometimes even very confidently, but can you tell the difference between the code that works and the code you’d actually want in your codebase?

The Review Checklist

A useful review checklist doesn’t need to be exotic. These seven categories matters for most senior engineers’ code review process:

  • Correctness: Does it do what it’s supposed to do, including edge cases?
  • Architecture: Is it structured in a way that will hold up as the project grows?
  • Performance: Does anything take more resources than it needs to?
  • Security: Is there any exposure, even in code that looks harmless, but is not?
  • Accessibility: Can every single person seamlessly use what was built?
  • Testing: Are there any tests to verify the code will keep working, even in the future?
  • Maintainability: Can the next engineer, or your future self, understand this effortlessly?

You’ll put this checklist to work on a real app — Tokens Dashboard, the starter app that you used in the earlier modules. It visualizes a team’s AI token spend across three screens, and you’ll keep working on this same project here. It looks clean and well organized, but like most code generated by AI, it’s not perfect. Using the checklist, you’ll find out where.

Open this lesson’s Starter project. Build and run.

Tokens Dashboard Summary screen showing insight cards for cost by model, top contributor, and ticket to merge trends
Tokens Dashboard Summary screen showing insight cards for cost by model, top contributor, and ticket to merge trends

On the Token’s Dashboard Summary screen, click the first line item to open the Cost by model screen.

Cost by Model detail screen showing a donut chart and ranked list of spend by AI model
Cost by Model detail screen showing a donut chart and ranked list of spend by AI model

Then, go back to the summary screen and then click the second line item to open the Tickets vs. Outcomes screen.

Tokens vs Outcomes detail screen showing a scatter plot of tokens consumed against merged PRs per developer
Tokens vs Outcomes detail screen showing a scatter plot of tokens consumed against merged PRs per developer

Once again, go back to the summary screen and click the third line item to open the Ticket to Merge screen.

Ticket to Merge detail screen showing a line chart comparing average days from ticket to merge for heavy and light AI users
Ticket to Merge detail screen showing a line chart comparing average days from ticket to merge for heavy and light AI users

You just got a feel of the starter project and confirmed that it built cleanly. Next, you’ll go through this review checklist and find out if there are any existing issues in the starter project.

Correctness

First, you’ll look at a correctness issue in Models/DashboardStartDate.swift. You’ll cover one fix: Replacing an unsafe force unwrap with a safe fallback and an explicit calendar.

Open Models/DashboardStartDate.swift. Every screen in this app anchors its mock data to a single fixed date, which is a smart pattern: It keeps the app’s numbers consistent no matter when you run it. But the today property resolves a DateComponents value into a Date using the .current calendar, then force unwraps the result.

DateComponents has a .date property that resolves into an Optional<Date>. That’s not an accident: Date components can fail to resolve into a real date, and force unwrapping that result means if it ever does fail, the app crashes before any of your own code even runs. For a static let, that’s about as early and as silent a crash as you can get.

Replace the enum with the code below:

enum DashboardStartDate {
  static let today: Date = {
    // 1
    let calendar = Calendar(identifier: .gregorian)
    let components = DateComponents(
      year: 2026,
      month: 5,
      day: 1)
    // 2
    return calendar.date(from: components)
      ?? Date(timeIntervalSince1970: 1_777_593_600)
  }()
}
  1. Pins the calendar to .gregorian explicitly. The original code used .current, which follows whatever calendar the device is set to, so on a device using the Hebrew, Islamic, or Japanese calendar, year: 2026, month: 5, day: 1 doesn’t mean what you’d expect.
  2. Falls back to a safe, hardcoded date (May 1, 2026) instead of force unwrapping, so a failed date resolution can’t crash the app.

Build and run. You’ll see the Token’s Dashboard Summary screen. The date at the top line reads May 1, 2026.

Tokens Dashboard Summary screen with the date line reading May 1, 2026
Tokens Dashboard Summary screen with the date line reading May 1, 2026

Architecture

Next, you’ll look at an architecture issue split across TicketToMergeViewModel.swift and SummaryViewModel.swift. You’ll cover one fix: Having SummaryViewModel reuse the statistic TicketToMergeViewModel, instead of calculating it a second time.

Now compare ViewModels/TicketToMergeViewModel.swift and ViewModels/SummaryViewModel.swift. TicketToMergeViewModel already computes the heavy AI users are getting faster statistic as two properties, heavyAIImprovement and latestGapDays: How much faster, and by how many days they’re ahead of light users.

But SummaryViewModel, which needs the same statistic for its headline card, doesn’t reuse any of that. Inside its own init, it recomputes the same improvement fraction from scratch, independently.

Right now, both versions agree, but that’s the problem, not the reassurance. There are now two separate implementations of the same calculation. If someone fixes a rounding issue or changes the definition of improvement in one, the other quietly keeps the old answer, and nothing will tell you they’ve drifted apart.

The fix is straightforward: Have SummaryViewModel take a TicketToMergeViewModel as a dependency and read its already computed properties, instead of recalculating them.

In SummaryViewModel.swift, replace the code of the init parameter list with the code below:

init(
  costStore: ModelCostStore = ModelCostStore(),
  outcomeStore:
    DeveloperOutcomeStore = DeveloperOutcomeStore(),
  ticketToMergeViewModel:
    TicketToMergeViewModel = TicketToMergeViewModel()) {

This swaps the ticketToMergeStore: TicketToMergeStore parameter for ticketToMergeViewModel: TicketToMergeViewModel, so SummaryViewModel now depends on the view model instead of the raw store.

Then, replace the code that builds the Ticket to Merge insight with the code below:

// 1
let gapLine = """
  , now \(TicketToMergeViewModel.days(
      ticketToMergeViewModel.latestGapDays)
  ) days ahead of light AI users
"""
built.append(Insight(
// 2
  headline: """
    Heavy AI users now merge in \(TicketToMergeViewModel.days(
      ticketToMergeViewModel.latestAIHeavyDays)) days — \(
        KPIFormat.percent(ticketToMergeViewModel.heavyAIImprovement)) faster.
  """,
  detail: """
    Ticket to merge days for heavy AI users has been improving\(gapLine).
    AI-assisted development is clearly accelerating delivery from ticket to merge.
  """,
  destination: .ticketToMerge))
  1. Builds the days ahead phrase straight from ticketToMergeViewModel.latestGapDays, already computed once inside TicketToMergeViewModel.
  2. Builds the headline from ticketToMergeViewModel.latestAIHeavyDays and .heavyAIImprovement, the same two properties TicketToMergeViewModel already exposes.

One source of truth, one place to fix if the math ever needs to change.

Build and run, then click the third ticket to merge line item in the Summary screen. Verify everything is working as expected: The ticket to merge insight should still match the numbers shown on the Ticket to Merge screen.

Ticket to Merge detail screen showing a line chart comparing average days from ticket to merge for heavy and light AI users
Ticket to Merge detail screen showing a line chart comparing average days from ticket to merge for heavy and light AI users

Maintainability

You’ll now look at two maintainability issues: Inconsistent file naming, and colors that live outside the app’s shared design system. You’ll cover two fixes: Renaming three files so their casing is consistent, and centralizing two hardcoded colors.

This one doesn’t require reading any logic at all, just the file names. Look at what’s sitting side by side in this project:

  • Models/CostbyModel.swift
  • Views/CostbyModelView.swift
  • ViewModels/CostByModelViewModel.swift

Two of these use a lowercase b in Costby. One uses an uppercase B. It’s a small thing, and it doesn’t affect how the app runs, but naming inconsistencies like this are exactly the kind of thing that slows down a future contributor searching for the right file, and it’s a very common pattern in code generated by AI, since nothing enforces consistency across separate generations unless someone catches it. The fix is a straightforward rename, bringing all three in line with CostByModel.

Build and run. Then, click the first line item to open the Cost by Model screen. Check everything is working as expected: Renaming a file shouldn’t change how anything looks or behaves.

Cost by Model detail screen showing a donut chart and ranked list of spend by AI model
Cost by Model detail screen showing a donut chart and ranked list of spend by AI model

The second issue doesn’t show up in a file list, so it takes a closer look at the detail screens themselves. Open App/AppColors.swift. Its own comment describes the app’s design language as a single indigo accent, and ColorStyles.shades backs that up: Five opacity steps of the same indigo, used consistently by the Cost by Model screen’s donut chart.

But TicketToMergeView.swift hardcodes Color.green as its second chart color, and TokensOutcomesView.swift hardcodes Color.orange for its needs coaching highlight. Neither color is defined anywhere near ColorStyles, and having two different secondary colors is its own inconsistency: Both are marking the same kind of thing, a cohort or a person that’s behind and could use more attention, so they should be the same color.

Between the two, orange is the one worth keeping. Green usually reads as good or on track, which is the wrong signal for a cohort that’s slower or a developer who needs coaching. Orange reads as a flag, which is exactly what both charts are using it for. So the fix standardizes on orange and drops green entirely.

Add one property to ColorStyles, right after shades:

static let attentionAccent = Color.orange

This leaves one shared highlight color instead of two disconnected ones, used consistently wherever a cohort or a person needs attention.

Then replace the code of the chart color range in TicketToMergeView.swift with the code below:

.chartForegroundStyleScale(
  domain: ticketToMergeViewModel.cohortNames,
  range: [Color.indigo, ColorStyles.attentionAccent])

And replace the code of the highlight color in TokensOutcomesView.swift with the code below:

.foregroundStyle(
  point.needsCoaching ?
  ColorStyles.attentionAccent :
  Color.indigo)

It was already using a bare Color.orange. You pointed it at the shared ColorStyles.attentionAccent instead.

Same fix as the file names, just a different flavor: This doesn’t invent a new brand color, it picks the one that already matched the app’s intent and gives it a single, named home, so the next person doesn’t have to go searching for a bare Color.orange to know it’s intentional.

Build and run. Click the second line item to open Tokens vs. Outcomes screen. Check this screen works as before.

Tokens vs Outcomes detail screen showing a scatter plot of tokens consumed against merged PRs per developer
Tokens vs Outcomes detail screen showing a scatter plot of tokens consumed against merged PRs per developer

Then, click the third line item to open the Ticket to Merge screen. Verify that the light AI users cohort line is now orange instead of green, matching the highlight color already used on the Tokens vs. Outcomes screen.

Ticket to Merge detail screen showing a line chart comparing average days from ticket to merge for heavy and light AI users
Ticket to Merge detail screen showing a line chart comparing average days from ticket to merge for heavy and light AI users

Performance and Security

Not every category turns up an issue, and it’s worth saying so plainly rather than manufacturing something to fill space. On a careful pass:

Performance: Nothing notable. The app currently works entirely with small, static, in memory mock data. There’s no expensive computation and no live data source to worry about yet.

Security: Nothing notable. This is a local, read only dashboard, with no networking, no user entered data, no persisted secrets. There’s nothing here to exploit.

That’s a legitimate outcome of a review, not a failure to find something.

Accessibility

A quick pass here is genuinely encouraging. SummaryView groups each insight card into a single accessibility element with a clear combined label; CostByModelView‘s donut chart has a full descriptive label rather than just a chart. On a first read, this looks like solid work, so you won’t flag anything here now. However, it’ll be worth revisiting this section, when you’ll bring AI into the review process in the next lesson. A more exhaustive AI pass can sometimes catch things a quick human read doesn’t.

Testing

There isn’t a test target in this starter project yet. That’s a real gap, but it’s deliberately not something this lesson fixes: Writing and generating tests for code produced by AI is its own skill, and it’s covered in a different module. Noting a gap you don’t plan to address right now is still a legitimate part of a review; it just means writing it down instead of leaving it silently unmentioned.

You have now applied a structured seven-category review checklist to a real AI-generated app manually, without any AI assistance, and found genuine issues that neither the compiler nor the tests would have caught.

See forum comments
Download course materials from Github
Previous: Introduction Next: Conclusion