Skip to content

AI Applied to Legacy

Capacity is the binding constraint, so anything that multiplies it deserves a careful look. Legacy work is also unusually well suited to assistance: the task is mostly comprehension, the context is all in the repository, and the output is verifiable.

But one of the obvious applications has a failure mode that defeats its entire purpose, and it is not obvious until it has already happened.

Explaining unfamiliar code. The strongest use. A model reading a long method and producing a summary of what it appears to do is a substantial accelerator for the slowest part of legacy work, and it is cheap to verify: you are going to read the code anyway, and now you read it with a hypothesis.

Critically, this attacks the exact bottleneck the previous topic identified — the need for someone with deep context. It does not replace that person, but it raises how much a less experienced engineer can do before needing them, which is the constraint that matters.

Finding call sites and tracing data flow. Mechanical, tedious, and the kind of exhaustive sweep where a model’s patience beats a human’s. Verification is direct: the references either exist or they do not.

Proposing refactoring steps. Given a target structure, enumerating the sequence of safe intermediate moves is work a model does well, because the moves are standard and the hard part is remembering them all.

Drafting documentation of current behaviour — with the Module 9 discipline attached. The output is a hypothesis to verify and must be marked as reconstructed, never filed as a contemporaneous record.

The application that looks most valuable and carries a specific, severe failure mode.

Recall the purpose from the earlier topic. A characterization test captures what the system actually does, including behaviour nobody intended, so that you can change the code and detect any difference. Its whole value lies in being an honest record of present behaviour, bugs included. It is not a specification.

Now consider what a model does when asked to write tests for a function. It infers intent — from the function’s name, its comments, the surrounding conventions, and everything it has learned about what code like this is supposed to do. It writes tests asserting the intended behaviour.

That is the correct instinct for new code and exactly wrong here:

A characterization test that asserts intended behaviour will fail against the current system, and the natural response is to “fix” the discrepancy — which is a behaviour change, made unknowingly, with no test protecting you.

Worse, the discrepancies are precisely the interesting cases: the undocumented edge case, the workaround for one large customer, the quiet fix for a recurring problem. The generated test disagrees with reality exactly where reality holds something load-bearing that nobody remembers.

graph TD
  A["Ask for tests on<br/>legacy code"] --> B["Model infers<br/><b>intended</b> behaviour"]
  B --> C["Tests fail against<br/>the real system"]
  C --> D["<b>The discrepancy is the<br/>behaviour that matters</b><br/><i>and it looks like a bug</i>"]

  classDef start fill:#5B8FB9,stroke:#4A7A9E,stroke-width:2px,color:#ffffff
  classDef mid fill:#E8A33D,stroke:#C4832A,stroke-width:2px,color:#ffffff
  classDef bad fill:#D9695B,stroke:#B4483A,stroke-width:2px,color:#ffffff
  class A start
  class B,C mid
  class D bad

The failure is not that the tests are wrong. It is that they are wrong in the one direction that destroys their purpose.

The fix is procedural and simple. Generate characterization tests by running the code and recording what it returns, then use the model to write the test around the observed values rather than to predict them. The model is excellent at enumerating the input cases to cover and at writing the scaffolding; it must not be the source of the expected values. Any case where the generated expectation and the observed behaviour disagree is a finding to investigate, never a discrepancy to correct.

Module 7’s point, which holds throughout: fluency is uncorrelated with correctness, and legacy code is the worst possible place for a confident guess because nobody remembers enough to notice.

The pattern that makes this safe is consistent across every use above: a model is reliable here when its output is cheap to verify against something real — the code, the references, the recorded behaviour. It is unreliable whenever it is asked to supply a fact that exists only in a history nobody wrote down. Comprehension, yes. Archaeology, no.

This shapes the execution of your migration sequence: where assistance multiplies scarce context, and the explicit rule that generated expectations are never the source of truth for current behaviour.


→ One more decision, and the frontier moves under it every quarter. — Buy vs build under a moving frontier