You wire automated code review into a 5,000-engineer monorepo: a pile of linters, static analyzers, plus an LLM reviewer, all posting comments as bots on every diff. About 20,000 diffs/day, ~8 comments each — 160,000 comments daily. Week one everyone reads them. Week three everyone has learned one motion: scroll past every bot comment. The tool is still running, the metrics (comment count, coverage) still look great — but it's dead, because it has no signal, only noise.
This isn't fixable with "more rules." It's a signal-detection problem: binary classification of real bugs (signal) vs irrelevant noise, where the receiver's attention (engineers) is a scarce, system-wide shared resource. Once the false-positive rate is high enough to erode trust, all of the tool's recall is voided — a true positive nobody reads equals a miss.
graph TD
D["Diff / PR
this change"]
subgraph GEN["① Generation · multi-source signal"]
L["Linter / Formatter
deterministic · cheap"]
SA["Static analyzer
Infer-class · inter-procedural"]
LLM["LLM Reviewer
semantic · costly · hallucinates"]
end
T["② Triage layer
severity × confidence gating
nitpick filter · dedup"]
R["③ Shown in review
blocking / collapsed / dropped"]
H["Engineer
NOT USEFUL button"]
FB["④ Feedback loop
per-analyzer effective FP rate
over budget → auto-downgrade/disable"]
D --> L & SA & LLM
L & SA & LLM --> T --> R --> H
H -.score.-> FB
FB -.thresholds.-> T
FB -.disable.-> GEN
classDef src fill:#1a2530,stroke:#64c8ff,color:#e8eef5
classDef mid fill:#1a1a30,stroke:#ffb450,color:#e8eef5
classDef out fill:#0e2030,stroke:#5eead4,color:#e8eef5
class L,SA,LLM src
class T,FB mid
class R,H out
Let the generation layer be prolific; all the value is in the triage layer and feedback loop — they trade recall for precision and make "which rules deserve to exist" a data-driven decision
Principle: treat each comment as one binary decision falling into a confusion matrix — TP, FP, FN, TN. Tool authors love the technical FP definition ("the rule logically holds"); but to the user, a false positive is any report they didn't want to see — even if logically correct, if it's irrelevant and not worth fixing, it's noise. Google therefore defines an "effective false positive": the developer took no positive action after seeing it. This shift in vantage point is the foundation of the whole topic.
# Effective FP rate: derived from USER signal, not rule logic
def effective_fp_rate(analyzer):
shown = analyzer.comments_shown # comments displayed
# "positive action" = changed code / clicked useful / marked will-fix
acted = count(c for c in shown if c.got_positive_action)
return 1 - acted / max(shown, 1) # share nobody acted on = effective FP
# FP budget (Google's rule-of-thumb threshold)
FP_BUDGET = 0.10
def healthy(analyzer):
return effective_fp_rate(analyzer) < FP_BUDGET
Principle: the generation layer can be prolific, but not every true positive earns the right to interrupt a human. The triage layer scores each comment on two axes — severity (null deref vs variable naming) and confidence (how sure the analyzer is) — then decides its fate: high severity × high confidence → blocking; medium → collapsed suggestion; low → dropped. Style nitpicks (formatting, import order) shouldn't reach human eyes at all — hand them to a formatter's autofix, and reserve human attention for correctness and design.
# Triage: decouple "is it real" from "is it worth interrupting a human"
def triage(c):
if c.category == "style" and c.autofixable:
apply_autofix(c); return DROP # nitpick → machine fixes, don't ask
score = c.severity * c.confidence
if score >= BLOCK_TH: return BLOCKING # must resolve before merge
if score >= SHOW_TH: return COLLAPSED # expandable suggestion
return DROP # rather miss than make noise
# Same issue hit by multiple sources → dedup, keep the most actionable one
comments = dedup(comments, key=lambda c:(c.file, c.line, c.bug_class))
Principle: half a report's value is its content, half is when it appears. The same bug report shown at diff review (author just wrote it, full context in head) gets fixed at a high rate; dumped as an offline batch scan — a list of thousands handed to the team — the fix rate approaches zero. Nobody wants to touch code someone wrote three months ago that's still running. Corollary: report only what's newly introduced by this change, never the standing backlog, or the first onboarding buries everyone under thousands of legacy findings.
Principle: triage thresholds can't be set by gut. Maintain an effective-FP-rate curve for each analyzer/rule (derived from NOT USEFUL clicks and whether code was actually changed), and manage it like an SLO error budget: a rule that stays over budget is auto-downgraded (blocking → collapsed) or disabled. This turns "which rules deserve to exist" from a political debate into a data verdict, and forces rule authors to own their signal quality.
# FP-budget gate: govern rule lifecycle like an SLO
def govern(rule):
if rule.shown < MIN_SAMPLES: return "canary" # too few samples, don't sentence yet
fp = effective_fp_rate(rule)
budget = BUDGET_BY_SEVERITY[rule.severity] # security is more tolerant
if fp > budget * 1.5: return "disable" # badly over budget → retire
if fp > budget: return "downgrade" # over budget → non-blocking
return "active"
In signal detection, precision and recall are two ends of one trade-off curve. Cranking the decision threshold sky-high does make every shown comment a true positive (precision → 100%, FP → 0), but the cost is that many real bugs fall below the threshold and get dropped — misses (FN) spike.
Zero FP usually means the system is so conservative it only reports things that are trivially correct — often the low-value stuff a formatter/compiler already catches. Genuinely valuable reports — those needing inter-procedural reasoning, carrying some uncertainty — come precisely with a nonzero FP rate. So a healthy system targets "effective FP stable within budget (e.g. <10%)", not 0. See a rule with 0% FP but very low trigger volume, and the question to ask is: what real problem has it ever caught?
90% per-comment accuracy = 1 in 10 comments is a false positive. If the bot posts 160,000 comments/day, that's 16,000 false positives/day in engineers' faces. Humans don't experience the system by "per-comment probability" — they experience it by absolute noise volume.
Worse is the base rate: genuinely severe bugs are rare across diffs (maybe a fraction of a percent). When the base rate of positives is very low, even accurate per-comment decisions leave the true-positive share of the report pool diluted by FPs to a low level — you sift 20 bot comments to hit 1 worth fixing, and quickly learn to skip them all. This is exactly Day 51's base-rate fallacy: at low base rates, measurement must look at report-level signal-to-noise, not per-comment accuracy. The cure is a triage layer that slashes shown volume and surfaces only high-confidence, high-severity items.
Goodhart's law: when a measure becomes a target, it stops being a good measure. A growth target on "comment count" makes engineers/rule-authors lower the bar and post more to hit the number — manufacturing noise, opposite to the system's real purpose (protect attention, catch real bugs). "Useful comment count" is slightly better but still an activity metric, and "useful" is easy to game.
Track outcome metrics: ① adoption rate (share where the author actually changed code); ② escaped defects — bugs that could have been caught but reached production (measures recall); ③ effective FP rate staying within budget (measures precision); ④ higher-level: review cycle time, defect-density trend. Ideally constrain a pair of antagonistic metrics simultaneously (adoption ↑ AND escaped defects ↓); optimizing either alone gets gamed.
Blocking (no merge until resolved) is the strongest firepower triage can open, and the most dangerous. Its net-win conditions are strict: high severity (a real incident is costly, e.g. security/data corruption) × extremely high confidence (near-impossible to misfire) × a clear autofix or fix path. When all three hold, blocking stops real incidents.
It turns disaster when confidence is insufficient but you block anyway — sentencing correct code to death once, jamming an urgent release, is enough to make the team demand a global override backdoor, after which blocking is toothless. The pragmatic pattern: the vast majority of reports are non-blocking suggestions; only a handful of rules, canaried long-term with a proven near-zero FP rate, get promoted to blocking; and always keep an audited manual override channel so emergencies have an exit rather than forcing people to bypass everything.
Isomorphic: both do "binary classification amid rare signal + heavy noise, with finite receiver attention." Transferable principles — ① severity-tiered routing (page vs ticket ↔ block vs collapse); ② FP budget as alert-fatigue governance, retiring over-budget alerts/rules; ③ suppression and dedup (many triggers from one root cause merged into one); ④ grade on outcome metrics (MTTR, escaped defects) not activity metrics (alert count, comment count).
Where it breaks: ① timeliness — alerts demand second-level response and getting humans out of the fast loop (Day 41); code review is asynchronous, human-in-loop, and can be slow. ② receiver — alerts target an on-call individual's fatigue; review targets the whole engineering org's shared trust, so one FP propagates wider and recovers slower. ③ reversibility — an alert FP is transient noise; a review FP that blocks a merge directly slows delivery, a more concrete negative externality. So a code-review system's precision bar is often stricter than typical monitoring.