DEV Community

Finley Zhou
Finley Zhou

Posted on

I Inherited 60,000 Lines of Undocumented C++. I Read It With an AI and Assumed Every Answer Was a Lie.

Three months ago a colleague left, and I inherited his project: roughly 60,000 lines of C++17, no design docs, comments that mostly said // TODO: clean this up, and a build system only he understood. My manager's ask was simple: "Be able to modify it safely by end of quarter."

I did not have a quarter to read it line by line. I also did not trust any LLM enough to let it change the code. So I used one for the only job where a wrong answer is cheap: reading. This post is the workflow that came out of it — a method I now call the hypothesis ledger — plus the places where it fell apart.

Why reading is the opposite risk profile of generating

Every horror story about AI-generated code shares one shape: plausible output, merged without verification, explosion later. Reading inverts the economics. When a model explains a function to me and gets it wrong, the blast radius is my own misunderstanding — which the compiler, a debugger, or ten minutes of grep will correct before it costs anyone anything.

That asymmetry is the entire pitch: use the model where its errors are detectable and cheap, and keep it away from anything that lands in the repo unverified. This is not a story about adopting AI-written code. It is a story about using AI as a very fast, frequently wrong tour guide.

The artifact: a hypothesis ledger

The core rule: nothing the model says about the codebase counts as knowledge. It counts as a hypothesis until verified by a tool that cannot be charmed. I keep a plain Markdown file open while exploring:

| # | Hypothesis (from model) | Source | Verification method | Status |
|---|------------------------|--------|--------------------|--------|
| 1 | `RingBuffer::push` overwrites oldest entry when full | LLM reading buffer.cpp | Write probe main.cpp, push 6 items into cap-5 buffer | CONFIRMED |
| 2 | `OrderBook` is only mutated from the IO thread | LLM summarizing call graph | `grep -rn "order_book\." src/ | grep -v io_thread` + tsan build | REJECTED — also mutated in metrics.cpp:214 |
| 3 | Retry backoff is exponential, base 100ms | LLM reading retry.h | Read source myself | PARTIAL — capped at 2s, model missed the clamp |
Enter fullscreen mode Exit fullscreen mode

Three columns do the real work. Verification method forces me to decide how I would check before I believe anything. Status keeps a permanent record of how wrong the model was, which calibrates how much rope to give it next session. After three weeks my ledger showed roughly one in four substantive claims was wrong or incomplete — useful, but nowhere near trustworthy. Exactly the right tool for navigation; exactly the wrong tool for authority.

The interrogation protocol that worked

Vague questions got vague, confident nonsense. What worked was a fixed protocol:

  1. Paste narrow slices, not files. One class, one function, one header. Large pastes degraded answer quality noticeably, and I have no interest in finding the context window limit by accident.
  2. Ask for structure, not opinion. "List every field this constructor touches and where each is later read" beats "explain this class." Structural claims are cheap to verify with grep.
  3. Ask for the call graph, then check it. "What calls flush(), and what does it call?" Then verify with ctags -R . and grep, or compile with -Wunused and see what the linker actually keeps.
  4. Demand the model cite line numbers. A claim with a line number is a falsifiable claim. A claim without one is a bedtime story.

For verification I leaned on tools that predate all of this: gdb watchpoints to test "this field only changes during shutdown," a throwaway main.cpp linking the suspect object file to test behavior claims, and a ThreadSanitizer build for anything about threads. The model never once mentioned TSan when summarizing concurrency code. The tools did not care about the summary either way.

Where the free access actually mattered

This workflow is chatty. A single afternoon of code spelunking is easily 40–60 exchanges: paste a slice, get a structural claim, verify, follow up. On a metered API I would have rationed questions and been worse at the job — the temptation to accept the first plausible answer grows with every dollar.

I ran these sessions through MonkeyCode, which currently offers free model access and a free server option, so the cost of asking one more clarifying question was zero. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Two honest caveats: I am making no claims about which models it serves, its limits, or how long the free tier lasts — verify that the day you set up, not from this article. And the ledger method is deliberately provider-agnostic; it is a Markdown file and a habit, so swapping backends changes nothing about the workflow.

The failure catalog (the part that made me careful)

Keeping the ledger meant I also accumulated a catalog of how it failed, which turned out to be the most valuable artifact of the quarter:

  • Confident temporal claims. "This cache is populated at startup" — actually populated lazily on first request. Models infer order from code layout, not execution.
  • Missing the second writer. Repeatedly found one mutation site and asserted exclusivity. grep disagreed three separate times.
  • Smoothing over ugliness. When the code did something bizarre (a deliberate memory leak with a comment in a different file explaining why), the model described what the code should do. The weird parts of legacy code are usually load-bearing, and those are exactly the parts it normalized away.
  • Version drift in its head. It described std::shared_mutex semantics from a standard the project did not use. Verify language-level claims against the build flags, not the model's memory.

Notice what is absent from this list: syntax errors, broken code, anything a compiler catches. Reading-side failures are semantic and social — wrong stories told fluently.

What to ask vs. what to never ask

Task Model suitable? Why
"What calls this function?" Yes, then verify with grep/ctags Cheap falsifiable claim
"Summarize this class's invariants" Yes, as hypothesis only Invariants are exactly what it invents
"Is this thread-safe?" No — TSan and code review only Its confidence here is uncorrelated with correctness
"Why was this written this way?" No — it cannot know, but will answer anyway Intent lives in commit history and people, not syntax
"Draft a probe program to test hypothesis X" Yes, with review Small, throwaway, compiler-checked

Limitations, honestly

The ledger is discipline, and discipline does not scale forever. By week six I was spending real time maintaining it, and on a team of five the per-person ledger would need to become a shared document with conventions. It also assumes you can verify — this worked because the project built and ran locally. For code you cannot execute (hardware-dependent firmware, proprietary dependencies), the verification column gets thin and the whole method weakens. Finally, none of this transfers to writing: the moment the model's output goes into the repo, you are back in the risk profile this article explicitly avoided, and you need review gates I have deliberately not discussed here.

Who should not bother

If the codebase is small enough to read in a weekend, read it in a weekend — the ledger is overhead. If you already have the original author available, buy them lunch instead; thirty minutes with a human beats a week of hypothesis testing. And if your employer's policy forbids pasting proprietary code into external services, that constraint settles the question before the workflow starts.

The actual lesson

By end of quarter I could modify the project safely, and the model deserves maybe a third of the credit — the rest goes to gdb, TSan, and the ledger that refused to believe it. That ratio feels like the honest version of AI-assisted legacy work: the model accelerates the asking, and everything else you already owned does the knowing. If you are staring down an inherited codebase right now, start a Markdown table before you paste a single line — the column where you record how wrong it was will teach you faster than any of its answers.

Top comments (1)

Collapse
 
matthew_faithfull profile image
Matthew Faithfull

A good job and a decent way to go about it.
Six months into my first job I inherited 300,000 lines of undocumented Windows C++. We were dealing with an average of two new genuine bugs a week. I say we, but I was the sole C++ dev. It took 18 months to get that to 0.5 per month, while adding a compiler for the database schema and making the backend and the presentation UI scriptable. That freed up enough time to start the larger task of a web front end for the product. Another 18 months to get that done. I left burned out but also unafraid of almost any code base. Next time you won't even blink. You'll know you've got it.