11/02 2026
470
Series: "Writing High-Quality Code with AI: AI Werewolf Development Log" | This installment covers versions v1.5 → v1.8 | Time: Evening of October 22, 2026
Preface
The previous three articles covered "how to get AI to do things right." This one tackles something more insidious: AI will rot right along with your bad structure.
Introduction: I Asked AI to Review My Own Code, and It Handed Me a Bill
That day I didn't write a single new feature. I did one thing only: I asked AI to read through all the code I had written and produce a review report.
When it finished, it gave me a three-tier list: 3 P0 blockers, 6 P1 conformance issues, and 11 P2 extensibility issues, plus three scores at the end—conformance 6.5, encapsulation 7.5, extensibility 6.0.
Two of those lines I stared at for a long time:
engine.ts at 724 lines (self-imposed limit: 350). Role abilities scattered across 15 if statements; adding one role requires changes in 5–8 places.
This is exactly what makes technical debt so troublesome: it's on the books, but the books aren't in front of you. You don't feel it until one day you realize that "changing a single line of prompt copy" means reading three hundred lines first.
The Problem: AI Didn't Get Dumber—It Just Can't Read Everything
Three cascading effects, each harder to spot than the last:
1. If it can't read everything, it still writes anyway. A model can only see a limited amount of code at once; it can't see what lies beyond that, but it won't say "I didn't see the rest"—it fills in with the most plausible-looking pattern. 2. The same logic gets scattered across multiple places. Change three spots, miss two; the program doesn't throw an error—its behavior just quietly changes. 3. No tests as a safety net. At the time there were 0 frontend tests, so the only option was to "play a round and see"—one round takes eight minutes, so you can't verify many times a day.
These three aren't parallel—they're sequential: can't read everything → misses changes → and nothing catches the missed changes.
Root Cause: Technical Debt and the Context Window
Let's look at the books first. Every number in the image below comes from that review report—not from my after-the-fact estimates:

Figure: The books before refactoring. The leftmost number is the most striking—a single file at 724 lines, while its own limit is 350; the most critical is the "5–8 places" on the right, which determines the workload for the next version.
What "15 if statements" actually looks like
The number 15 isn't an estimate—the report breaks it down in detail:
P2-1: engine.ts 12 places, ai-player.ts 3 places with role === 'xxx'. Role abilities rely on if branches scattered everywhere; adding one role requires changes in 5–8 places and is easy to miss (for example, forgetting to add the gameMates initialization).
Just looking at numbers still gives no sense of it. Digging out the night() function from back then, it looked like this:

Figure: Werewolf, Seer, Witch—three segments with identical structure. Adding a role means copying another segment into this function.
The skeleton of all three code segments is exactly the same: issue the judge's cue → find this role → human goes through waitHuman, AI goes through AIPlayer → record thoughts. This wasn't copy-pasted; it naturally grew into this shape each time a role was added by "writing it like the previous segment." AI won't proactively say "this should be abstracted here"—it completes in the existing style of your code, and the existing style of your code is to keep copying.
This is the source of that line "adding one role requires changes in 5–8 places": one place in the night flow, one in the daytime judgment, one in the win condition, one in snapshot trimming, one in frontend display...
Why long files are especially fatal for AI
Figure: In a 724-line file, a single conversation can only fit the middle segment. It didn't read checkEnd at the end of the file, let alone consider that there might be another copy in another file.
724 lines, about 26,000 characters, roughly eight thousand to ten thousand tokens. That doesn't sound like much, but a conversation also needs room for requirements, history, and its own output—so the space actually left for "reading code" is often only a fraction.
So what it reads is a fragment. The style within that fragment becomes its entire worldview.
Key insight: AI does not lower its confidence because it "didn't see" something. This is the biggest difference between it and humans. Humans hesitate; it doesn't.
An actionable standard for judgment
The 350 limit wasn't made up by the review report—it's the project's own capability constraint, written in AGENTS.md:
Small files, single responsibility, keep each file ideally ≤ 350 lines; if exceeded, extract modules (constants, types, pure functions).
We wrote the rule ourselves, and we violated it ourselves—this is probably the most typical form of technical debt. Besides line count, three other rough rules were added afterward: split any function over 50 lines; extract any logic that appears in more than 2 places; if changing one place requires changing three files at once, the boundaries are drawn wrong.
Checking is fast: wc -l src*.ts | sort -n | tail -10, see which one's the fattest.
Before refactoring, the fattest part of this project was the engine at 724 lines; now it's 1014 lines—but that's because it now carries 5 more roles, spectating, and instrumentation, not because rules are still piled inside (this curve will be discussed below).
Solution: How to Write Refactoring Prompts (Core of This Article)
I think the most valuable part isn't "what to split it into," but how I told AI about it.
Because the easiest way for refactoring to go wrong is: AI casually changes the logic too. You ask it to move house, and it renovates along the way.
This is the template I've used many times and later solidified—you can copy it directly:
Background:
1. The Werewolf project has iterated to v1.7, engine.ts is already over 700 lines, and every new role requires changes in several places.
2. The project root has AGENTS.md, which defines our own constraints such as per-file line limits, constant management, and layering.
Goal:
1. First produce a refactoring assessment; do not change code directly.
Process:
1. Conformance: whether file length meets the requirements in AGENTS.md; whether there is hardcoding and magic numbers; whether constants are managed uniformly.
2. Encapsulation: whether the presentation layer, routing layer, data layer, and service layer are properly separated.
3. Organization and naming: whether the directory structure, file names, and method names follow industry best practices.
Requirements:
1. Grade by P0/P1/P2, and for each item give files, line numbers, and occurrence counts—no adjectives.
2. List anything uncertain and ask me; do not decide on your own.
Output location: docs/CODE-REVIEW-2026-10-22.md
What it looks like after splitting
The final result is the current structure: game (rules), ai (decision-making), server (HTTP), cloud (storage), none of which knows the details of the others.
The most valuable abstraction was moving "role night behavior" out of the engine body—the engine exposes a deliberately narrowed set of capabilities (read view, issue judge's cue, set guard/werewolf kill), and each role implements only one act():
export interface RoleBehavior {
role: RoleKey;
openCue: string; // "Guard, please open your eyes..."
closeCue: string; // "Guard, please close your eyes"
act(host: NightHost, actor: Player): Promise;
}
The engine is only responsible for "iterating in order + issuing cues + resolving." Adding a new role equals creating a new object + registering it, with zero changes to the engine body.
Effect: Changes land where they should
The result of that refactoring day can be summarized in one sentence: engine went from 808 lines → 723 lines, with 191 new lines in behaviors.ts, and all 87 test cases green.
Although the overall line count didn't drop much, the registry system kept behavior completely unchanged—registering a new role now only adds one line:
Adding a role = create a new behavior object + register it in the table, engine body untouched
export const NIGHT_BEHAVIORS: RoleBehavior[] = [
wolfBehavior, seerBehavior, witchBehavior, hunterBehavior, guardBehavior,
];
Figure: Game setup configuration page (v1.8, run from the commit at that time, switched to a 12-player game). Each of the 9 roles has its own card and its own threshold, and the Dreamweaver is still locked as "in development"—adding one card doesn't affect the others.
But this curve later rebounded
If I only wrote the result above, this article would just be whitewashing. The real data looks like this:

Figure: Engine body (brass) and night behavior registry (green). On the refactoring day the engine dropped by 85 lines, then rose all the way to 1014 lines.
So we should pay down technical debt regularly after iterating on a portion of features. (This time, in order to complete the work submission as quickly as possible, no further effort was put into refactoring afterward.)
Summary
1. Technical debt must go on the books; don't rely on feelings: 724 / 350, 15 if statements, 5–8 places to change—once these three numbers are laid out, "whether to refactor" is no longer an aesthetic question.
2. Letting AI review its own output exposes problems better than letting it keep writing; require it to give files, line numbers, and counts—no adjectives.
3. The core of refactoring prompts is two prohibitions: assess the code's conformance, encapsulation, and extensibility.
4. Is it transferable? Any project that lets AI maintain code long-term needs regular refactoring and optimization. Especially code where "every feature addition requires changing the core file"—that's digging a pit for your future self.
Other articles in this series: 48-Hour Development in Practice: WorkBuddy Builds an AI Werewolf Game | JavaScript and Node.js: Why Frontend Games Can Be Tampered With | SDD Specification-Driven Development Tutorial—The More Features You Add, the More AI Starts to "Make Things Up"