Module 2: Mental Models for AI Code

Integrative Exercise: Applying the 3 Mental Models

Integrative Exercise: Applying the 3 Mental Models

Capsule overview

This is the most important capsule in the module. The 3 previous capsules gave you the models: Managing an Intern (how to supervise), Circuit Breaker (when to pause), and Trust Calibration (how much to trust). Now you apply them together to real scenarios with Python/FastAPI code.

You're going to work with 3 scenarios of increasing complexity. In each one, Claude Code generated code and your job is to apply the 3 mental models to validate it. It's not enough to say "it's fine" or "it's wrong" — you need to document your process: which model you applied, what you found, what you decided, and why.

This exercise simulates exactly what you'll do in the capstone project in module 8, where you'll receive a complete codebase and do a professional code review using these models. Think of this capsule as a dress rehearsal.


How to Work Through the Scenarios

The 3-model process

For each scenario, follow this process in order:

STEP 1: Trust Calibration
├── What type of task is it?
├── What's my base trust?
├── What factors adjust my trust?
├── What's my final trust?
└── What review depth corresponds to it?

STEP 2: Managing an Intern
├── What MIT level do I assign? (1, 2, or 3)
├── What do I supervise in detail?
├── What do I delegate with confidence?
├── Apply the good manager's 5 questions
└── Document findings

STEP 3: Circuit Breaker
├── Where do I put the checkpoints?
├── Does the code pass each checkpoint?
├── Are there "trip the breaker" signs?
└── Decision: accept, edit, regenerate, or reject

Documentation format

For each scenario, document your validation with this format:

=== VALIDATION OF [scenario name] ===

TRUST CALIBRATION:
- Type of task: [type]
- Base trust: [%]
- Adjustments: [list]
- Final trust: [%]
- Depth: [level]

MANAGING AN INTERN:
- MIT Level: [1/2/3]
- I supervise: [list]
- I delegate: [list]
- Manager's questions:
  1. Does it do what I asked? [answer]
  2. Correct business values? [answer]
  3. Security? [answer]
  4. Error handling? [answer]
  5. Appropriate design? [answer]

CIRCUIT BREAKER:
- Defined checkpoints: [list]
- Checkpoint results: [PASS/FAIL for each]
- Trip the breaker? [yes/no, why]

FINAL DECISION: [accept / edit / regenerate / reject]
TIME INVESTED: [minutes]
JUSTIFICATION: [1-2 sentences]

Scenario 1: Task CRUD Endpoint (Routine)

Context

You asked Claude Code: "Generate CRUD endpoints for a task system with FastAPI. Each task has a title, description, priority, and status."

Generated code

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
from enum import Enum
import uuid

app = FastAPI(title="Task Manager API", version="1.0.0")

tasks_db: dict = {}


class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    URGENT = "urgent"


class Status(str, Enum):
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    REVIEW = "review"
    DONE = "done"


class TaskCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    description: Optional[str] = Field(None, max_length=2000)
    priority: Priority = Priority.MEDIUM
    status: Status = Status.TODO


class TaskUpdate(BaseModel):
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    description: Optional[str] = Field(None, max_length=2000)
    priority: Optional[Priority] = None
    status: Optional[Status] = None


class Task(TaskCreate):
    id: str
    created_at: datetime
    updated_at: datetime


@app.post("/tasks", response_model=Task, status_code=201)
async def create_task(task: TaskCreate):
    task_id = str(uuid.uuid4())
    now = datetime.utcnow()
    new_task = Task(
        id=task_id,
        created_at=now,
        updated_at=now,
        **task.model_dump(),
    )
    tasks_db[task_id] = new_task
    return new_task


@app.get("/tasks", response_model=List[Task])
async def list_tasks(
    status: Optional[Status] = None,
    priority: Optional[Priority] = None,
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=20, ge=1, le=100),
):
    tasks = list(tasks_db.values())

    if status:
        tasks = [t for t in tasks if t.status == status]
    if priority:
        tasks = [t for t in tasks if t.priority == priority]

    tasks.sort(key=lambda t: t.created_at, reverse=True)
    return tasks[skip : skip + limit]


@app.get("/tasks/{task_id}", response_model=Task)
async def get_task(task_id: str):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks_db[task_id]


@app.patch("/tasks/{task_id}", response_model=Task)
async def update_task(task_id: str, task_update: TaskUpdate):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail="Task not found")

    existing = tasks_db[task_id]
    update_data = task_update.model_dump(exclude_unset=True)

    for field, value in update_data.items():
        setattr(existing, field, value)

    existing.updated_at = datetime.utcnow()
    tasks_db[task_id] = existing
    return existing


@app.delete("/tasks/{task_id}")
async def delete_task(task_id: str):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail="Task not found")
    del tasks_db[task_id]
    return {"message": "Task deleted successfully"}

Your turn

Apply the 3 mental models to the code above. Document your full process before looking at the solution.

See solution

Validation of Scenario 1: Task CRUD

TRUST CALIBRATION:

  • Type of task: CRUD endpoints
  • Base trust: 65%
  • Adjustments:
    • +10%: Standard pattern, I've seen it many times
    • +5%: No security or complex business logic
    • -5%: There are no tests
  • Final trust: 75%
  • Depth: Focused review (5-8 minutes)

MANAGING AN INTERN:

  • MIT Level: 2 (focused review) with parts at Level 3
  • I supervise:
    • Input validations (are they complete?)
    • Error handling (are the responses correct?)
    • Filtering and pagination logic
  • I delegate:
    • Imports and FastAPI boilerplate
    • Enum definitions
    • Pydantic model structure

The good manager's questions:

  1. Does it do what I asked? Yes. Complete CRUD: create, read (list + single), update, delete. Includes filtering and pagination that I didn't ask for but that's useful.

  2. Correct business values?

    • Priorities (low, medium, high, urgent) → Reasonable
    • Statuses (todo, in_progress, review, done) → Reasonable
    • max_length 200 for title, 2000 for description → Reasonable
    • Pagination default 20, max 100 → Reasonable
    • No complex business values → N/A
  3. Security?

    • ⚠️ There's no authentication. Anyone can CRUD all tasks.
    • For a prototype: OK. For production: needs auth.
    • No SQL (in-memory dict), no injection risk.
    • No secrets.
  4. Error handling?

    • ✅ 404 for a task not found in get, update, delete
    • ✅ 201 for create (correct)
    • ✅ Pydantic validates inputs automatically (422 for invalid)
    • ✅ Query params validated (ge=0, ge=1, le=100)
  5. Appropriate design?

    • ✅ Uses Enum for priority and status (not free strings)
    • ✅ Uses UUID for IDs
    • ✅ PATCH for partial update (not PUT)
    • ✅ updated_at is updated on patch
    • ⚠️ In-memory dict → OK for a prototype, not for production
    • ⚠️ Delete is a hard delete → would soft delete be better?

CIRCUIT BREAKER:

  • Defined checkpoints: 1 (after generation, it's a single file)
  • Checkpoint result: PASS with minor observations
  • Trip the breaker? No. There are no critical issues. The observations are improvements, not bugs.

FINAL DECISION: Accept with 2 notes for the future

  • Add auth when it stops being a prototype
  • Consider soft delete in production

TIME INVESTED: 7 minutes

JUSTIFICATION: Standard CRUD with good practices (Enums, UUID, PATCH, validations). There's no complex business logic or security. The improvements are for production, not for functionality.


Scenario 2: Payment Processing (Critical)

Context

You asked Claude Code: "Generate a payment processing service that charges the user, records the transaction, and sends confirmation by email."

Generated code

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
from decimal import Decimal
from enum import Enum
import uuid
import httpx

app = FastAPI(title="Payment Service")

transactions_db: dict = {}


class PaymentMethod(str, Enum):
    CREDIT_CARD = "credit_card"
    DEBIT_CARD = "debit_card"
    BANK_TRANSFER = "bank_transfer"


class Currency(str, Enum):
    USD = "USD"
    EUR = "EUR"
    MXN = "MXN"


class PaymentRequest(BaseModel):
    user_id: str
    amount: float
    currency: Currency = Currency.USD
    payment_method: PaymentMethod
    card_number: Optional[str] = None
    card_expiry: Optional[str] = None
    card_cvv: Optional[str] = None
    description: str = Field(..., max_length=500)
    email: EmailStr


class Transaction(BaseModel):
    id: str
    user_id: str
    amount: float
    currency: Currency
    status: str
    payment_method: PaymentMethod
    description: str
    created_at: datetime


class PaymentResponse(BaseModel):
    transaction_id: str
    status: str
    amount: float
    message: str


STRIPE_API_KEY = "sk_test_abc123def456"
STRIPE_API_URL = "https://api.stripe.com/v1"


async def charge_payment(
    amount: float,
    currency: str,
    payment_method: str,
    card_number: str,
) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{STRIPE_API_URL}/charges",
            headers={"Authorization": f"Bearer {STRIPE_API_KEY}"},
            data={
                "amount": int(amount * 100),
                "currency": currency.lower(),
                "source": card_number,
                "description": "Payment charge",
            },
        )
        return response.json()


async def send_confirmation_email(
    email: str, transaction_id: str, amount: float
) -> None:
    async with httpx.AsyncClient() as client:
        await client.post(
            "https://api.sendgrid.com/v3/mail/send",
            headers={
                "Authorization": "Bearer SG.xxxxx",
                "Content-Type": "application/json",
            },
            json={
                "personalizations": [{"to": [{"email": email}]}],
                "from": {"email": "payments@myapp.com"},
                "subject": "Payment Confirmation",
                "content": [
                    {
                        "type": "text/plain",
                        "value": f"Payment of ${amount} confirmed. "
                        f"Transaction ID: {transaction_id}",
                    }
                ],
            },
        )


@app.post("/payments", response_model=PaymentResponse)
async def process_payment(payment: PaymentRequest):
    charge_result = await charge_payment(
        amount=payment.amount,
        currency=payment.currency.value,
        payment_method=payment.payment_method.value,
        card_number=payment.card_number,
    )

    transaction = Transaction(
        id=str(uuid.uuid4()),
        user_id=payment.user_id,
        amount=payment.amount,
        currency=payment.currency,
        status="completed",
        payment_method=payment.payment_method,
        description=payment.description,
        created_at=datetime.utcnow(),
    )
    transactions_db[transaction.id] = transaction

    await send_confirmation_email(
        email=payment.email,
        transaction_id=transaction.id,
        amount=payment.amount,
    )

    return PaymentResponse(
        transaction_id=transaction.id,
        status="completed",
        amount=payment.amount,
        message="Payment processed successfully",
    )


@app.get("/payments/{transaction_id}")
async def get_transaction(transaction_id: str):
    if transaction_id not in transactions_db:
        raise HTTPException(status_code=404, detail="Transaction not found")
    return transactions_db[transaction_id]

Your turn

This scenario is critical — payment processing. Apply the 3 mental models with the corresponding rigor. Document every issue you find.

See solution

Validation of Scenario 2: Payment Processing

TRUST CALIBRATION:

  • Type of task: Financial processing
  • Base trust: 10%
  • Adjustments:
    • -10%: There are no tests
    • -5%: Handles card data (PCI compliance)
    • -5%: Integration with an external API (Stripe)
    • The type of task is already the lowest → 0% practical
  • Final trust: ~0% (total distrust, review every line)
  • Depth: Line by line + verify against docs + write tests

MANAGING AN INTERN:

  • MIT Level: 1 (direct supervision) for the WHOLE file
  • I supervise: every line, every decision
  • I delegate: nothing

The good manager's questions:

  1. Does it do what I asked? Partially. It charges, records the transaction, sends an email. But the implementation has fundamental problems.

  2. Correct business values?

    • ⚠️ amount: float — Uses float for money. Decimal is mandatory. 0.1 + 0.2 != 0.3 with floats.
    • ⚠️ Doesn't validate that amount is positive (it can charge -$100).
    • ⚠️ It has no minimum or maximum amount.
    • ⚠️ Status is always "completed" — it doesn't check whether the charge was actually successful.
  3. Security? (MULTIPLE CRITICAL ISSUES)

    • ❌ STRIPE_API_KEY = "sk_test_abc123def456" — API key hardcoded in the source code. This is an absolute deal-breaker. It must be an environment variable.
    • ❌ "Authorization": "Bearer SG.xxxxx" — SendGrid API key hardcoded.
    • ❌ card_number, card_expiry, card_cvv pass through the backend. In a PCI-compliant system, card data never touches your server — it goes directly to Stripe via Stripe Elements/Tokens. Passing card_number to your backend puts you in scope for PCI-DSS Level 1 (cost: $50k-$500k/year).
    • ❌ There's no authentication. Anyone can process payments.
    • ❌ There's no rate limiting. An attacker can make thousands of charges.
  4. Error handling?

    • ❌ charge_payment doesn't handle errors. If Stripe returns an error, the code continues and records the transaction as "completed".
    • ❌ If send_confirmation_email fails, the error isn't caught. The response to the user fails even though the payment was processed.
    • ❌ There's no retry logic for any operation.
    • ❌ There's no idempotency key. If the user double-clicks, they get charged twice.
    • ❌ There's no atomic transaction. If the transaction is recorded but the email fails, the transaction is left without confirmation.
  5. Appropriate design?

    • ❌ The Stripe API is used incorrectly. It doesn't use PaymentIntents (the modern approach). It uses the legacy Charges endpoint.
    • ❌ Stripe's response isn't verified. response.json() is returned without validating the status code.
    • ❌ In-memory storage for financial transactions. A restart loses the entire history.
    • ⚠️ int(amount * 100) to convert to cents. With float, it can give incorrect results: int(19.99 * 100) = 1998 instead of 1999.

CIRCUIT BREAKER:

  • Checkpoints: 1 (it's a single file)
  • Checkpoint result: FAIL
  • Trip the breaker? YES, immediately.
    • Hardcoded API keys → breaker trip (security)
    • Card data in the backend → breaker trip (compliance)
    • Doesn't verify Stripe's result → breaker trip (financial logic)
    • Float for money → breaker trip (financial precision)

FINAL DECISION: REJECT completely.

TIME INVESTED: 25 minutes

JUSTIFICATION: The code has at least 10 critical issues, including hardcoded API keys, card data passing through the backend (PCI violation), lack of verification of Stripe's results, float for money, and zero error handling in financial operations. The fundamental structure is wrong — it's not a matter of editing a few lines. It should be regenerated with a prompt that specifies: use Stripe PaymentIntents, don't receive card data, environment variables for secrets, Decimal for amounts, idempotency keys, and complete error handling. Ideally, the official Stripe documentation should be used as a reference in the prompt.

Issues found (summary):

#IssueSeverityCategory
1Stripe API key hardcodedCriticalSecurity
2SendGrid API key hardcodedCriticalSecurity
3Card data touches backend (PCI)CriticalCompliance
4No auth on the endpointCriticalSecurity
5Float for moneyHighFinancial precision
6Doesn't verify Stripe's resultHighBusiness logic
7No idempotency keyHighReliability
8Zero error handlingHighReliability
9Charges API (legacy)MediumBest practices
10In-memory storageMediumArchitecture
11No rate limitingMediumSecurity
12Doesn't validate amount > 0MediumValidation

Scenario 3: Request Logging Middleware (Medium)

Context

You asked Claude Code: "Generate a middleware for FastAPI that logs all requests with method, path, status code, duration, and client IP. Include configuration to exclude certain paths from logging."

Generated code

import time
import logging
from typing import Callable, List, Optional
from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from pydantic import BaseModel

logger = logging.getLogger("request_logger")


class LoggingConfig(BaseModel):
    enabled: bool = True
    excluded_paths: List[str] = ["/health", "/metrics", "/docs", "/openapi.json"]
    log_headers: bool = False
    log_body: bool = False
    slow_request_threshold_ms: float = 1000.0


class RequestLoggingMiddleware(BaseHTTPMiddleware):
    def __init__(self, app: FastAPI, config: Optional[LoggingConfig] = None):
        super().__init__(app)
        self.config = config or LoggingConfig()

    async def dispatch(
        self, request: Request, call_next: Callable
    ) -> Response:
        if not self.config.enabled:
            return await call_next(request)

        if request.url.path in self.config.excluded_paths:
            return await call_next(request)

        start_time = time.perf_counter()

        client_ip = request.client.host if request.client else "unknown"
        method = request.method
        path = request.url.path
        query = str(request.query_params) if request.query_params else ""

        log_data = {
            "method": method,
            "path": path,
            "query": query,
            "client_ip": client_ip,
        }

        if self.config.log_headers:
            log_data["headers"] = dict(request.headers)

        if self.config.log_body and method in ("POST", "PUT", "PATCH"):
            try:
                body = await request.body()
                log_data["body"] = body.decode("utf-8")[:10000]
            except Exception:
                log_data["body"] = "<unable to read body>"

        try:
            response = await call_next(request)
        except Exception as exc:
            duration_ms = (time.perf_counter() - start_time) * 1000
            log_data.update({
                "status_code": 500,
                "duration_ms": round(duration_ms, 2),
                "error": str(exc),
            })
            logger.error("Request failed", extra=log_data)
            raise

        duration_ms = (time.perf_counter() - start_time) * 1000
        log_data.update({
            "status_code": response.status_code,
            "duration_ms": round(duration_ms, 2),
        })

        if duration_ms > self.config.slow_request_threshold_ms:
            logger.warning("Slow request detected", extra=log_data)
        elif response.status_code >= 400:
            logger.warning("Request error", extra=log_data)
        else:
            logger.info("Request completed", extra=log_data)

        return response


def setup_request_logging(
    app: FastAPI,
    config: Optional[LoggingConfig] = None,
) -> None:
    middleware_config = config or LoggingConfig()
    app.add_middleware(RequestLoggingMiddleware, config=middleware_config)

    if not logger.handlers:
        handler = logging.StreamHandler()
        handler.setFormatter(
            logging.Formatter(
                "%(asctime)s - %(name)s - %(levelname)s - %(message)s "
                "- %(method)s %(path)s %(status_code)s %(duration_ms)sms"
            )
        )
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)
# Usage example:
from fastapi import FastAPI

app = FastAPI()

config = LoggingConfig(
    excluded_paths=["/health", "/metrics", "/docs", "/openapi.json", "/favicon.ico"],
    log_headers=False,
    log_body=False,
    slow_request_threshold_ms=500.0,
)
setup_request_logging(app, config)


@app.get("/health")
async def health():
    return {"status": "ok"}


@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id, "name": "Test User"}


# Expected output when calling GET /users/1:
# 2024-03-14 10:30:45 - request_logger - INFO - Request completed
#   - GET /users/1 200 12.34ms

Your turn

This scenario is medium complexity — infrastructure middleware. It's not trivial CRUD but it's not security-critical either. Apply the 3 models and document.

See solution

Validation of Scenario 3: Request Logging Middleware

TRUST CALIBRATION:

  • Type of task: Middleware/Infrastructure + Logging
  • Base trust: 55% (between CRUD and business logic)
  • Adjustments:
    • +10%: Known pattern (request logging is standard)
    • +5%: Well-structured and readable code
    • -5%: Logging headers/body can expose sensitive data
    • -5%: Middleware can affect the performance of the whole app
  • Final trust: 60%
  • Depth: Detailed review (10-15 minutes)

MANAGING AN INTERN:

  • MIT Level: 2 (focused review) with parts at Level 1
  • I supervise:
    • What gets logged (can it expose sensitive data?)
    • The middleware's error handling (can it crash the app?)
    • Performance (does the middleware add significant latency?)
  • I delegate:
    • The middleware's overall structure
    • Pydantic configuration
    • Logging format

The good manager's questions:

  1. Does it do what I asked? ✅ Yes. It logs method, path, status code, duration, IP. It allows excluding paths. It includes useful extras: slow request detection, error logging, optional body/header logging. It includes a usage example.

  2. Correct business values?

    • slow_request_threshold_ms: 1000ms default → Reasonable
    • body truncated to 10,000 chars → Reasonable
    • Default excluded paths → Reasonable
    • N/A for business values — it's infrastructure
  3. Security?

    • ⚠️ log_headers=True would log Authorization headers (tokens, API keys). The middleware should sanitize sensitive headers before logging them, or at least exclude Authorization, Cookie, and Set-Cookie by default.
    • ⚠️ log_body=True would log passwords in login requests, card data in payment requests. There should be a mechanism to redact sensitive fields.
    • ⚠️ client_ip is obtained from request.client.host. Behind a proxy/load balancer, this gives the proxy's IP, not the real client's. It should use the X-Forwarded-For header (carefully, because of IP spoofing).
    • ✅ Both options are False by default — good.
  4. Error handling?

    • ✅ If request.body() fails, it catches the exception and logs <unable to read body>.
    • ✅ If the handler raises an exception, the middleware logs the error and re-raises.
    • ⚠️ str(exc) in the error log could expose internal information (stack traces, server paths). In production, it would be better to log the type of error without internal details exposed to the log output that might go to third-party services.
    • ✅ The middleware doesn't crash the app if there's an error in the logging.
  5. Appropriate design?

    • ✅ Uses Starlette's BaseHTTPMiddleware (correct approach).
    • ✅ Configuration via a Pydantic model (clean and validated).
    • ✅ time.perf_counter() to measure duration (more precise than time.time()).
    • ✅ A setup_request_logging function for easy setup.
    • ✅ Guard against duplicate handlers (if not logger.handlers).
    • ⚠️ The formatter uses %(method)s %(path)s etc. which requires those fields to be in extra. If the logger is used elsewhere without those fields, it could give a KeyError. Consider using a format that doesn't depend on extra.
    • ⚠️ excluded_paths uses exact comparison. /health excludes /health but not /health/ nor /health?check=true. Consider using prefix matching or regex.

CIRCUIT BREAKER:

  • Checkpoints: 1 (a single file with a usage example)
  • Checkpoint result: PASS with observations
  • Trip the breaker? No. There are no critical issues, only proactive security improvements.

FINAL DECISION: Accept with minor edits.

TIME INVESTED: 12 minutes

JUSTIFICATION: The middleware is well implemented for the standard use case. The dangerous features (log_headers, log_body) are disabled by default. The issues found are proactive security improvements (sanitize headers if log_headers is enabled, handle X-Forwarded-For) that can be implemented as follow-up. The code is clean, well structured, and the usage example is clear.

Recommended edits:

  1. Add sanitization of sensitive headers when log_headers=True
  2. Use X-Forwarded-For for the client IP with a fallback to request.client.host
  3. Consider prefix matching for excluded_paths

Meta-Exercise: Reflection on the Models

After completing the 3 scenarios, answer these questions:

1. Which model was most useful in each scenario?

See solution
  • Scenario 1 (CRUD): Trust Calibration was the most useful. The high calibration (75%) told you that a quick review was enough. Without calibration, you might have spent 20 minutes on routine CRUD.
  • Scenario 2 (Payments): Managing an Intern was the most useful. The manager's 5 questions revealed every issue. Circuit Breaker confirmed that you should stop, but MIT told you what was wrong.
  • Scenario 3 (Middleware): The 3 models contributed equally. Trust Calibration defined the depth (60%), MIT told you where to look (logging security), and Circuit Breaker confirmed you could continue with minor edits.

2. How much time would you have spent without the models?

See solution

Without models, the tendency is one of two:

  • Uniform review: ~15 minutes per scenario × 3 = 45 minutes. Problem: you spend too much on CRUD and not enough on payments.
  • Review by "feeling": Variable and inconsistent. Sometimes 5 minutes on everything, sometimes 30 minutes on the trivial.

With models:

  • Scenario 1: 7 minutes (high calibration → quick review)
  • Scenario 2: 25 minutes (low calibration → exhaustive review)
  • Scenario 3: 12 minutes (medium calibration → focused review)
  • Total: 44 minutes

The total time is similar, but the distribution is radically different. Without models, the 44 minutes are distributed uniformly (or at random). With models, 57% of the time is invested in the riskiest code (payments).

3. Which of the 3 models would you internalize first?

See solution

Trust Calibration is the easiest to internalize first because:

  1. It's the most actionable: you can literally use the table tomorrow.
  2. It doesn't require changing your workflow — it just tells you how much to review.
  3. It reinforces itself with every use: every time you review code, your table updates.

Managing an Intern is second: it requires changing how you think about review (supervise vs micro-manage).

Circuit Breaker is third: it requires changing your workflow (adding explicit pauses), which is the hardest change.

However, the 3 work best together. Trust Calibration without MIT is incomplete (you know how much to trust but not what to supervise). MIT without Circuit Breaker is risky (you know what to supervise but not when to pause).


Extra Exercise: Your Own Scenario

Generate real code with Claude Code

This exercise is the most valuable in the module. You need Claude Code (or your preferred AI coding tool):

  1. Choose a real task from your work or a personal project
  2. Generate the code with Claude Code
  3. Apply the 3 models using this capsule's documentation format
  4. Document your full process: calibration, manager's questions, checkpoints, decision
See evaluation guide

Your documentation should:

  • ✅ Have base trust with justification
  • ✅ Have trust adjustments with specific factors
  • ✅ Have an MIT level with justification
  • ✅ Answer the manager's 5 questions with answers specific to the code
  • ✅ Define at least 1 checkpoint with a pass/fail criterion
  • ✅ Have a justified final decision
  • ✅ Include time invested
  • ✅ If you found issues, list them with severity

Your documentation should not:

  • ❌ Have trust without justification ("I trust 50%" without explaining why)
  • ❌ Have generic answers ("it looks good")
  • ❌ Skip any of the 3 models
  • ❌ Lack a final decision

Connection to the Project

From the exercise to the capstone project

The scenarios in this capsule are simplified versions of what you'll face in module 8:

This capsuleModule 8
3 isolated scenarios1 complete codebase with multiple files
Code generated for youCode you review as if it were from a PR
Planted issuesPlanted issues + subtle issues
3 types of risk15-20 problems across 3 risk zones
44 minutes90-120 minutes

The difference: in module 8, the files are interconnected. An issue in models.py can cause problems in service.py that show up in routes.py. Your ability to apply the 3 models fluidly — without having to consult this capsule — determines your effectiveness.

Preparation

If you can complete the 3 scenarios in this capsule in less than 50 minutes with complete documentation, you're ready for module 8. If it takes you longer, practice with the extra exercise (generate your own scenario) until the process is fluid.


Troubleshooting

Problem 1: "I struggle to apply the 3 models — it feels repetitive"

Cause: The models overlap intentionally. The repetition is part of the design. Solution: Think of the models as perspectives, not steps. You don't need to apply them sequentially in production. With practice, you'll apply them simultaneously: "this task is CRUD (Trust: 65%), I review validations and error handling (MIT Level 2), and I put a checkpoint after each file (Circuit Breaker)." A single sentence covers all 3.

Problem 2: "My calibration for scenario 2 was too high — I didn't find all the issues"

Cause: You under-estimated the risk of payment processing. Solution: For financial code, the correct calibration is always < 20%. If your calibration was higher, adjust your table. The rule: if the code touches money, card data, or compliance, your base trust is automatically < 20%.

Problem 3: "I don't know when to stop looking for issues"

Cause: You don't have a "good enough" criterion. Solution: Your sufficiency criterion comes from Trust Calibration:

  • 80%+ trust: Stop when the overall structure looks good (2-3 min).
  • 50-70% trust: Stop when the main logic and edge cases are covered (5-10 min).
  • 10-30% trust: Stop when every line has been reviewed and the tests exist (20+ min).

Problem 4: "The documentation format is tedious for daily use"

Cause: The complete format is for learning, not for production. Solution: In your daily work, the documentation simplifies to mental notes or a comment in the PR:

# Trust: 60% (CRUD + familiar domain)
# MIT: Level 2 — reviewed validations and error handling
# CB: Post-generation checkpoint → PASS
# Decision: Accept with minor edits (email validation)

4 lines. 10 seconds of documentation. The full mental process still runs in your head.


Summary

In this capsule you applied:

  • The 3 mental models together to 3 scenarios of increasing complexity
  • Trust Calibration to determine the review depth: 75% (CRUD), 0% (payments), 60% (middleware)
  • Managing an Intern to identify what to supervise with the manager's 5 questions
  • Circuit Breaker to decide whether to continue, edit, or stop
  • The distribution of time is key: 57% of the time was invested in 33% of the code (the riskiest)
  • The models complement each other: Trust Calibration says how much, MIT says how, Circuit Breaker says when

Result of the 3 scenarios:

ScenarioTrustDecisionIssuesTime
CRUD Tasks75%Accept2 minor7 min
Payments0%Reject12 critical25 min
Middleware60%Accept + edits3 improvements12 min

Next module: Detecting Hallucinations in Code — applying these mental models to the most subtle and dangerous error.


Additional resources

  1. The Pragmatic Programmer — Dave Thomas & Andy Hunt - The philosophy of verification and quality in software
  2. Google — Code Review Developer Guide - How Google structures code review by risk
  3. Stripe — Security Best Practices - Best practices for payment integration (reference for Scenario 2)
  4. OWASP — Logging Cheat Sheet - What to log and what not to (reference for Scenario 3)
  5. Anthropic — Claude Code Documentation - Official Claude Code documentation
  6. FastAPI — Middleware Documentation - Official middleware reference in FastAPI

Debugging & Code Review with Claude Code — Module 2, Capsule 05 Claude Code Agentic Development Path — Guide #6 of 11