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:
-
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.
-
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
-
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.
-
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)
-
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:
-
Does it do what I asked? Partially. It charges, records the transaction, sends an email. But the implementation has fundamental problems.
-
Correct business values?
- ⚠️
amount: float— Uses float for money.Decimalis mandatory.0.1 + 0.2 != 0.3with 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.
- ⚠️
-
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_cvvpass 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.
- ❌
-
Error handling?
- ❌
charge_paymentdoesn't handle errors. If Stripe returns an error, the code continues and records the transaction as "completed". - ❌ If
send_confirmation_emailfails, 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.
- ❌
-
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)=1998instead of1999.
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):
| # | Issue | Severity | Category |
|---|---|---|---|
| 1 | Stripe API key hardcoded | Critical | Security |
| 2 | SendGrid API key hardcoded | Critical | Security |
| 3 | Card data touches backend (PCI) | Critical | Compliance |
| 4 | No auth on the endpoint | Critical | Security |
| 5 | Float for money | High | Financial precision |
| 6 | Doesn't verify Stripe's result | High | Business logic |
| 7 | No idempotency key | High | Reliability |
| 8 | Zero error handling | High | Reliability |
| 9 | Charges API (legacy) | Medium | Best practices |
| 10 | In-memory storage | Medium | Architecture |
| 11 | No rate limiting | Medium | Security |
| 12 | Doesn't validate amount > 0 | Medium | Validation |
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:
-
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.
-
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
-
Security?
- ⚠️
log_headers=Truewould 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=Truewould log passwords in login requests, card data in payment requests. There should be a mechanism to redact sensitive fields. - ⚠️
client_ipis obtained fromrequest.client.host. Behind a proxy/load balancer, this gives the proxy's IP, not the real client's. It should use theX-Forwarded-Forheader (carefully, because of IP spoofing). - ✅ Both options are False by default — good.
- ⚠️
-
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.
- ✅ If
-
Appropriate design?
- ✅ Uses Starlette's BaseHTTPMiddleware (correct approach).
- ✅ Configuration via a Pydantic model (clean and validated).
- ✅
time.perf_counter()to measure duration (more precise thantime.time()). - ✅ A
setup_request_loggingfunction for easy setup. - ✅ Guard against duplicate handlers (
if not logger.handlers). - ⚠️ The formatter uses
%(method)s %(path)setc. which requires those fields to be inextra. 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.
/healthexcludes/healthbut 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:
- Add sanitization of sensitive headers when
log_headers=True - Use
X-Forwarded-Forfor the client IP with a fallback torequest.client.host - 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:
- It's the most actionable: you can literally use the table tomorrow.
- It doesn't require changing your workflow — it just tells you how much to review.
- 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):
- Choose a real task from your work or a personal project
- Generate the code with Claude Code
- Apply the 3 models using this capsule's documentation format
- 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 capsule | Module 8 |
|---|---|
| 3 isolated scenarios | 1 complete codebase with multiple files |
| Code generated for you | Code you review as if it were from a PR |
| Planted issues | Planted issues + subtle issues |
| 3 types of risk | 15-20 problems across 3 risk zones |
| 44 minutes | 90-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:
| Scenario | Trust | Decision | Issues | Time |
|---|---|---|---|---|
| CRUD Tasks | 75% | Accept | 2 minor | 7 min |
| Payments | 0% | Reject | 12 critical | 25 min |
| Middleware | 60% | Accept + edits | 3 improvements | 12 min |
Next module: Detecting Hallucinations in Code — applying these mental models to the most subtle and dangerous error.
Additional resources
- The Pragmatic Programmer — Dave Thomas & Andy Hunt - The philosophy of verification and quality in software
- Google — Code Review Developer Guide - How Google structures code review by risk
- Stripe — Security Best Practices - Best practices for payment integration (reference for Scenario 2)
- OWASP — Logging Cheat Sheet - What to log and what not to (reference for Scenario 3)
- Anthropic — Claude Code Documentation - Official Claude Code documentation
- 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