Trimming the duplication an AI cannot see¶
An AI agent that edits your code one ticket at a time has a structural blind spot, and it is not a quality-of-model problem. The agent sees the files the ticket told it to touch, plus whatever it greps during that run. It does not carry a whole-repo model of every retry wrapper, deep merge, role-string check, formatter, registry, or superseded path you have ever written. So when the helper it needs already exists three modules over, the agent has no cheap signal to reuse it. It writes a fresh near-copy, the copy passes review in isolation, and your duplication count quietly ticks up. The same horizon limit leaves orphaned code behind after a refactor: the old path sits outside the ticket's local view, so nobody deletes it.
This is the fifth post in a thread, and the other four built a machine. Code comments written for the robot, not the human introduced the codemap: annotations and the risk-vs-complexity score. The test that knows what it is introduced the test taxonomy and its evidence: and kind: marks. Let the library carry the proof used both to delete hand-rolled code and the tests that pinned it. The refactor the annotations made safe pointed the same instruments at a 3,300-line god file and decomposed it without breaking anything. This post is about the drift those posts were circling: the duplication and dead code that an AI accelerates precisely because it cannot see the whole repo -- and the tooling that lives outside any single context window to find it, flag it the moment it lands, and trim it under a human gate.
The fix is not "tell the agent to be careful." You cannot instruction-tune away a missing input. The fix is to give the project a vantage point the per-ticket agent does not have.
The blind spot is a property of the context window, not the model¶
The duplication-and-dead-code review spec (the review-rule docs) names this directly, in a section titled "Why AI accelerates this drift." An agent working a ticket sees that ticket's ## Touches files plus what it greps. It does not hold a model of every helper that already exists. "When the needed helper already exists three modules away, the agent has no cheap signal that it should reuse it, so it can write a fresh near-copy that passes review in isolation."
Read that twice, because the second clause is the trap. The copy passes review in isolation. Every local signal says the code is fine: it is correct, it is tested, the diff is small. The defect is only visible from a vantage point no single ticket occupies -- the whole repo at once. A human senior engineer accumulates that vantage point slowly, over months of working in the codebase, as a mental index of "haven't I seen this before?" The agent starts every ticket with that index empty.
That is why the spec splits the control into two parts that both live outside the ticket: a periodic whole-repo scan, and an always-on review-time tripwire. A per-ticket agent structurally cannot see every clone or orphan, so one tool gives the project the broad scan and the other catches new drift between full passes. Neither asks the agent to remember anything. They re-derive the whole-repo picture from scratch, mechanically, every time.
Duplication is hidden blast radius¶
Here is the part that connects this review to its sibling. A duplicated block is not just redundant typing -- it is a coupling you cannot see.
When the same logic lives in N places, those N copies are semantically joined: one logical change or bug fix has to be applied to all of them, or the behavior silently diverges. But the copies have no import edge between them. So the structural fan-in signal that the blast-radius review uses to find dangerous hubs -- counting how many files import a module -- is blind to the coupling. The spec puts it sharply: duplication is "worse than a visible high-fan-in hub: the cost is real, but the dependency graph does not point at it."
So de-duplication is not a tidiness exercise. It converts hidden semantic coupling into one governed, testable-once shared home. And in doing so it creates (or increases) a visible structural fan-in on that new home -- which the blast-radius review then watches. The two reviews share one north star: shrink the blast radius of the next change. That is also why the duplication scoreboard carries a "net new shared modules" column as an explicit counterweight. A de-dup only wins when the visible shared surface it adds is smaller and better governed than the hidden duplicate set it replaces. If you trade five invisible clones for one shared helper that ten modules now import, you have to be honest that you added a real fan-in -- you just made it one the graph can finally see.
The blast-radius post (The refactor the annotations made safe) is the decomposition cut of this same north star: it shrinks blast radius by pulling a seam out of a god file. This post is the duplication cut: it shrinks blast radius by collapsing clones into one home. Same instrument, different smell.
The machine, not the lecture¶
"Don't repeat yourself" is a lecture every developer has heard and an agent cannot act on without a whole-repo input. So the project does not lecture. It builds three things that live outside the context window.
The finder: a whole-repo scan. the project scripts (run it with the project dev wrapper) is the broad pass. It runs jscpd for token-based clone detection across the Python and JS/JSX scan roots -- which catches renamed and near-miss clones that a plain text grep misses -- and a standard-library ast reachability scan for dead symbols, orphan files, unused exports, and unused dependencies. Crucially, it does not rank on raw size. It ranks each candidate by (band exceedance * codemap risk * maturity), reusing the exact the codemap subsystem annotations from the annotation post. A stable, high-risk, load-bearing clone sorts above an experimental low-risk one, because wasted surface there is more expensive to leave rotting. The finder emits a ranked ledger with enough evidence for a human or agent to file one narrow refactor ticket per accepted candidate.
The soft-check: a review-time tripwire. the project scripts (registered as lint-dedup-drift in the soft_checks bucket of the review configuration) runs on every review. When a changed file adds a clone or orphans a symbol across the warn band of the shared threshold table, it does not block the merge -- it warns and drafts a remediation ticket. This is the piece that fights the agent's blind spot in real time: the moment a new near-copy lands, the system files the cleanup against itself, before the drift compounds into a campaign.
The remediation pass: human-gated, one small fix per ticket. The finder ranks and the soft-check warns, but neither one edits production code on its own. A deliberate triage pass reads the ledger, throws out the false positives, and authors one narrowly-scoped state: draft remediation ticket per genuine candidate. A human then promotes the ones worth doing. Each promoted ticket does exactly one behavior-preserving fix, following the spec's protocol: for a de-dup, write a golden or characterization test on real fixtures that passes on both copies first, then collapse to a single source of truth with no compat shim, then run the golden test again -- equal output is the proof. For a deletion, prove the target unreachable (zero static references, no dynamic-dispatch allowlist match, manual string/registry search) and use the full suite as the equivalence oracle.
The same loop fired again after Werewolf landed. The Phase 5 pass (the ticket corpus) ran the project dev wrapper, considered the top 10 finder rows, deduped 9 rows against existing remediation tickets, and authored 1 new confirmed backlog ticket: dedup-memory-match-hidden-payload-fixtures. The important part is not that the triage pass removed code by itself -- it did not -- but that the fresh third game made the whole-repo scan find the same generated live-payload clone shape across Werewolf, Bluff, and Memory Match, while still recognizing work already owned by existing tickets.
The real before/after numbers¶
Here is what that machine actually trimmed on its first turn of the crank. The first remediation pass (dedup-first-remediation-pass) was triage only: it scanned the top 30 finder rows, kept 4 genuine candidates, discarded 14 false positives (7 of them generated server_payloads.js fixture repeats, the rest allowlist-covered helpers reached through registries and same-module callers), and authored 4 draft remediation tickets. Its own scoreboard row is all zeros on purpose -- triage removes nothing. The trims came when a human promoted those four drafts and they ran. Here is each one as merged, from its run record:
| Remediation ticket | duplicate LOC removed | clone-pairs eliminated | dead symbols removed | orphan files removed | golden / full-suite |
|---|---|---|---|---|---|
| the private source tree start request | 18 | 1 | 0 | 0 | pass/pass, suite pass |
| the relevant server module Bluff post-action tail | net -4 LOC into 1 shared helper | 1 | 0 | 0 | pass/pass, suite pass |
| the relevant AI module | 0 | 0 | 1 | 0 | n/a, suite pass |
| the relevant AI module (module) | 0 | 0 | 11 | 1 | n/a, suite pass |
The second turn added a triage row and two remediation records. One row is a real duplicate-LOC trim; the other is a generated-fixture decomposition whose run record reports file delta and golden status instead of duplicate-LOC and clone-pair counts, so I am not converting it into a number the run did not claim.
| Phase 5 row | duplicate LOC removed | clone-pairs eliminated | dead symbols removed | orphan files removed | golden / full-suite |
|---|---|---|---|---|---|
Phase 5 dedup pass triage (10 considered, 9 deduped, 1 authored, 1 confirmed) |
0 | 0 | 0 | 0 | not-applicable / not-run |
| the private source tree hidden first-flip generated fixtures | 86 | 1 | 0 | 0 | passed before and after extraction; backend pytest and full client Vitest passed |
| the private source tree role/phase fixture decomposition | not reported; file delta -590/+613 (net +23) |
not reported | not reported | not reported | pass/pass; full client Vitest passed |
The two first-pass de-duplications, the Memory Match hidden-payload de-duplication, and the single durable dead-symbol deletion held: on current main, the UnifiedHomeGameSettings start request is still one helper, the Bluff post-action tail is still one shared private method, memory_match_hidden_payloads.js is generated from shared fixture helpers, and build_basic_request is still gone. The first pass's fourth row did not stick -- and that turns out to be the most honest data point in the whole pass, so I cover it in the edges below rather than bury it in a sum. The durable duplicate/dead-code trim reported in comparable rows is therefore 3 clone-pairs eliminated (104 duplicate lines plus a Bluff post-action tail folded into one shared private helper) and 1 dead symbol removed, with zero unused dependencies and zero net new shared modules in those rows. Every de-dup carried a golden or characterization test that was green before and after the collapse, and every change kept the relevant full suite green. These are not large numbers, and that is the point: the cadence is meant to be re-run, summing durable rows into a trend line, not to land one heroic cleanup. The first pass proved the find-leads-to-fix loop closes; the Werewolf-era pass proved it can fire again on a fresh game without re-finding work already ticketed.
The honest edges¶
I want to be as honest about the limits here as the earlier posts were about theirs, because overstating this would undermine the whole reason it belongs in this thread.
Not all duplication should be removed. Some is coincidental -- two functions that look alike today but answer to different owners and will diverge tomorrow. Some is intentional decoupling. The seven generated fixture repeats the first pass discarded are duplication by design: hand-editing generated output to satisfy a clone detector would be the actual mistake. The finder surfaces candidates; it does not issue verdicts.
Dead-code detection is confidence-graded, not exact. A static reachability scan cannot see dynamic dispatch, so the spec carries a dynamic-dispatch allowlist: FastAPI route handlers, engine and personality registries, name-keyed dispatch maps, pytest fixtures, __main__ entrypoints, codemap-marked public API, string/reflection-loaded names, and React lazy imports all lower a finding's confidence or suppress it. A "dead" symbol reached only through a registry or a getattr is a runtime break waiting to happen, which is why deletion requires a manual string/config search and a human gate on top of the scan. The finder reports confidence bands, not a delete button.
The human gate reverses calls -- and that is the gate working. This is the fourth scoreboard row above. The orphan module the relevant AI module (the HonestEddie / LiarLarry / ParanoidPat rule personalities) genuinely had zero inbound production imports, so the deletion ticket merged it as 11 dead symbols and one orphan file removed. Then a human restored it. The file is back on current main, still un-imported by any production module -- kept deliberately as future-use rule personalities rather than wrongly-detected live code. The static analysis was correct that nothing reaches it; the human decision was that "nothing reaches it yet" is not the same as "delete it." That is precisely why the protocol gates deletion behind a person instead of auto-confirming a high-confidence finding: the finder measures reachability, which is a fact, but "should this be removed" is a judgment the finder cannot make. A reversed deletion is not the system failing. It is the system declining to let an approximate ranking drive an irreversible call.
Remediation is incremental and human-gated. The division of labor is deliberate: tooling ranks, the triage pass authors narrow tickets, a human promotes the ones worth doing, and each promoted ticket does one small fix. Auto-confirming would let an approximate ranking drive autonomous refactors of load-bearing code, which is exactly the failure the golden-test-first protocol exists to prevent. The point is not to make the codebase look smaller in one stroke. It is to make whole-repo cleanup repeatable enough that the clone count, the dead-symbol count, and the hidden blast radius actually trend down over time.
The annotations told the finder which clone was worth collapsing and which dead symbol was load-bearing. The golden test proved the one collapse it made was behavior-preserving. The soft-check keeps the next near-copy from landing unnoticed. None of it asks the agent to see what it structurally cannot. It gives the project the whole-repo vantage point instead, and points it at the drift the context window will keep producing.
Prior-art check pending
The individual pieces are old: token-based clone detection (jscpd and the broader CPD/PMD lineage), AST reachability and dead-code analysis (vulture, ts-prune, knip), and CI drift tripwires. What I believe is uncommon is joining a provenance-tiered risk/maturity annotation score to the clone-and-dead-code candidate ranking so the same metadata that scores a change also ranks the cleanup, paired with a review-time tripwire that drafts the remediation ticket the instant drift lands. That combination claim is unverified. Editor: either commission a short prior-art block in the spiral-detection-watchdog house style, or soften "tooling no single context window has" to "I have not seen this exact combination in the wild."
The other beats of this machine: Code comments written for the robot, not the human, which owns the risk and maturity score the finder ranks on; The test that knows what it is, which owns the evidence: mark behind every golden test; Let the library carry the proof, which shrinks the footprint by replacing hand-rolled code with a trusted library; and The refactor the annotations made safe, the sibling that shrinks blast radius by decomposing a hub where this one shrinks it by collapsing clones. Same north star, different cut.
Or: subscribe to the newsletter for more posts on building this stuff.