Build an LLM reviewer for infrastructure PRs with an ops-specific rubric (blast radius, cost, security, availability), run it against six sample diffs with known seeded issues, and measure its precision and recall like you would any detector.
Stack Overflow 2025 found 59% of developers reject AI code review — usually because someone deployed it as a gate. This lab teaches the placement that works: advisory comments, measured signal-to-noise, human merge authority untouched.
~90 minutes hands-on · Advanced tier (maps to module M2). $0–$2: any free-tier chat model works via the manual path; an API key makes it scriptable.
Create reviewbot/ with rubric.md, reviewer.py, score.py, and prs/pr-1.diff … prs/pr-6.diff.
# rubric.md — Infra PR review rubric (advisory)
You are reviewing an infrastructure diff. Evaluate ONLY these dimensions:
1. SECURITY: exposure widened (CIDRs, ports, IAM actions/resources, encryption off)?
2. BLAST RADIUS: does this remove a safety net (prevent_destroy, deletion
protection, backups) or touch state shared by other systems?
3. COST: step-change in instance size/count/storage class? Estimate direction.
4. AVAILABILITY: reduced redundancy, tightened health checks, riskier rollout?
Rules: cite the exact diff line for every finding; severity critical/warning/nit;
if the change is safe say so in one line; you are advisory — never demand.
# reviewer.py
#!/usr/bin/env python3
import json, os, sys, urllib.request
diff = open(sys.argv[1]).read()
rubric = open("rubric.md").read()
prompt = (rubric + "\n\n--- DIFF UNDER REVIEW ---\n" + diff +
"\n--- END DIFF ---\nRespond with a JSON list of findings: "
'[{"severity":"critical|warning|nit","category":"security|cost|blast-radius|availability|other",'
'"line":"<quoted diff line>","finding":"<one sentence>","suggestion":"<one sentence>"}] '
"Return [] if the change is safe. Advisory tone; never invent lines not present in the diff.")
key = os.environ.get("OPENAI_API_KEY")
if not key: # $0 path: paste output into any chat UI
print(prompt); sys.exit(0)
base = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
req = urllib.request.Request(base + "/chat/completions",
data=json.dumps({"model": model, "messages": [{"role": "user", "content": prompt}]}).encode(),
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["choices"][0]["message"]["content"])
The six diffs. Five contain one seeded issue each; one is clean (your false-positive probe). Save each fenced block as prs/pr-N.diff.
--- a/network.tf (pr-1: "open db access for analytics team")
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
- cidr_blocks = ["10.42.0.0/16"]
+ cidr_blocks = ["0.0.0.0/0"]
}
--- a/compute.tf (pr-2: "bump worker size for month-end load")
resource "aws_instance" "batch" {
- instance_type = "t3.medium"
+ instance_type = "m5.8xlarge"
count = 6
}
--- a/rds.tf (pr-3: "cleanup: remove noisy lifecycle block")
resource "aws_db_instance" "orders" {
- deletion_protection = true
- lifecycle {
- prevent_destroy = true
- }
+ deletion_protection = false
}
--- a/tags.tf (pr-4: "add cost-allocation tags")
resource "aws_instance" "app" {
tags = {
Name = "app"
+ CostCenter = "retail-372"
+ Owner = "platform-team"
}
}
--- a/iam.tf (pr-5: "unblock the ETL job permissions")
resource "aws_iam_role_policy" "etl" {
policy = jsonencode({
Statement = [{
- Action = ["s3:GetObject"]
- Resource = ["arn:aws:s3:::etl-input/*"]
+ Action = ["*"]
+ Resource = ["*"]
Effect = "Allow"
}]
})
}
--- a/asg.tf (pr-6: "faster deploys: tighten health checks")
resource "aws_autoscaling_group" "web" {
- min_size = 2
+ min_size = 1
- health_check_grace_period = 300
+ health_check_grace_period = 30
}
python3 reviewer.py prs/pr-1.diff. No API key? It prints the full prompt — paste into any free chat model and save the JSON reply as results/pr-1.json. Repeat for all six.findings.csv with rows pr,category,verdict — one row per bot finding, verdict tp (matches a real problem) or fp (noise, invented line, or wrong category). Be strict: a security finding on pr-4's tags is an fp even if phrased plausibly.truth.csv: five rows — pr-1,security · pr-2,cost · pr-3,blast-radius · pr-5,security · pr-6,availability (pr-4 is clean).# score.py
#!/usr/bin/env python3
import csv
truth = {(r["pr"], r["category"]) for r in csv.DictReader(open("truth.csv"))}
tp_set, fp = set(), 0
for r in csv.DictReader(open("findings.csv")):
if r["verdict"] == "tp" and (r["pr"], r["category"]) in truth:
tp_set.add((r["pr"], r["category"]))
else:
fp += 1
tp, fn = len(tp_set), len(truth - tp_set)
prec = tp / (tp + fp) if tp + fp else 0.0
rec = tp / (tp + fn) if tp + fn else 0.0
print(f"TP={tp} FP={fp} FN={fn} precision={prec:.2f} recall={rec:.2f}")
print("missed:", sorted(truth - tp_set) or "none")
A review bot is a detector, and detectors are judged by precision/recall — not by how impressive their best comment looks. A bot with 40% precision trains humans to ignore it, which is worse than no bot.
Human merge authority is non-negotiable. The bot comments; a person merges. Coinbase-style mandatory human review of AI output is the industry norm for infra.
python3 score.py runs and prints precision, recall, and the missed list.reviewbot/ repo with rubric versions, results, findings.csv, truth.csv, score log, and the placement memo. Self-grading rubric (pass = 8/10):
OPENAI_BASE_URL; any OpenAI-compatible endpoint works (Groq, Together, local Ollama at http://localhost:11434/v1).Client infra diffs are among the most sensitive artifacts you touch — they map the estate. Run the bot only through client-approved tenancy (Bedrock/Azure OpenAI) or fully local (Ollama + an instruct model; expect lower recall, re-measure — never assume). Redaction alternative: review module/variable diffs with account IDs, CIDRs and bucket names templated out. Your precision/recall log is the client-facing evidence that the bot earns its comment space.
Seeded issues:
count = 6 is roughly a 40x step in instance spend, and the count multiplier is what weak reviews miss.terraform destroy of production data possible.Action:* on Resource:* is full-account admin.