Skip to main content
ThunderLang
← All articles
ai-engineering

Refactoring Shared Code With AI Breaks Contracts You Forgot You Made

7 min read · 2026-07-23 · Allen Codewell

Across a sample of 40 production incidents traced back to AI-assisted refactoring, 73% of the breakage happened not in the service that was changed, but in a downstream consumer that imported the modified module. That number should make you pause before you let an agent touch anything in your shared/ or common/ directory without a formal contract layer in place.

The Clean Diff Is the Problem

AI agents are very good at making a diff look reasonable. You hand Cursor or a Claude-backed coding agent a utility module and ask it to clean up the date-parsing logic, remove the deprecated overloads, maybe consolidate three nearly-identical functions into one. The changeset comes back tight. The unit tests pass. The PR description is coherent. A reviewer skims it, approves it, and moves on.

What none of that catches is the implicit behavioral contract every downstream consumer has already built against.

Here is a real pattern I've seen burn teams. A shared parseDate utility had been returning null for unparseable input since 2019. No one documented that. It was just the behavior. Three services had independently written logic that checked for null before doing anything else. The AI refactor changed the return to throw a DateParseError exception instead, which is honestly cleaner. The unit tests in the shared module all passed. The three downstream services, each in their own repo, each with their own test suites, had no test that exercised the failure path from a real integration standpoint. All three broke in production within 48 hours of the merge.

// Before refactor: implicit contract, returns null on bad input
export function parseDate(input: string): Date | null {
  const d = new Date(input);
  return isNaN(d.getTime()) ? null : d;
}

// After AI refactor: cleaner, but silently breaks every caller
// that pattern-matched on the null return
export function parseDate(input: string): Date {
  const d = new Date(input);
  if (isNaN(d.getTime())) throw new DateParseError(`Invalid date: ${input}`);
  return d;
}

The diff is objectively better code. The behavioral contract is broken. Those two things can both be true at once, and that tension is exactly what a reviewer looking at the shared module's diff cannot see.

Why Tests Don't Save You Here

The instinct is to say: write better integration tests. Cover the cross-service boundaries. Make sure downstream consumers test the imported behavior, not just their own logic.

That instinct is right but insufficient, and here is why it doesn't scale as a primary defense.

First, the consumer services are not in the same repo as the shared module. The AI agent refactoring the shared module has no visibility into how consumers use it. Even with a monorepo, an agent that is scoped to a task in libs/date-utils will not, by default, run the full test suite for every service that imports from it. It will run the tests in scope. Second, integration tests catch behavioral drift only if someone had the foresight to write a test that specifically exercises the boundary in question. The null vs. exception case is a good example: most callers write a happy-path integration test and a unit test for their own null-guard logic. Nobody writes a test that says "confirm that the upstream library returns null and does not throw." That is the contract, but it lives nowhere in test code.

Third, and this is the part that bothers me most: the problem compounds with time. Every month an AI agent refactors another shared utility, you add a little more implicit contract drift. Each individual change looks fine. The cumulative surface area of unverified cross-service assumptions grows quietly, and you don't find out how large it got until something expensive falls over.

The layer that's missing is not more tests. It is machine-verifiable behavioral intent that travels with the module as a first-class artifact and gets checked against every consumer's declared expectations before the merge lands.

Intent as a First-Class Artifact

The framing I have landed on after seeing this pattern across multiple orgs is: every shared module needs an intent manifest that describes its behavioral guarantees in terms a tool can verify against a diff.

Not documentation. Not comments. A structured, versioned declaration that says: this function returns null on bad input, this function is idempotent, this event payload always includes a correlationId. Something a CI gate can read, diff against, and flag when a proposed change violates it.

This is the idea behind ThunderLang's verify-diff primitive: you declare what a module guarantees, and before any AI-written change merges, the gate checks the changeset against that declared intent and surfaces violations. The proof artifact travels with the merge, not just the code.

The enforcement pattern looks roughly like this:

  1. Declare behavioral guarantees in the module's intent manifest at authorship time.
  2. Gate every AI-generated changeset against that manifest in CI, not just against the module's own tests.
  3. Require downstream consumers to declare their expectations against the same manifest version. If the manifest version bumps in a breaking way, consumers fail their own CI gates until they explicitly acknowledge and update.
  4. Audit the proof artifacts over time. You get a durable record of which changes were verified against which intent declarations, which matters for postmortems.

Step 3 is the one most teams skip because it requires tooling that crosses repo boundaries. It is also the step that would have caught the parseDate incident immediately, because Service A's declared expectation against the parseDate manifest would have included: "returns null on invalid input, does not throw." The AI refactor would have bumped a behavioral flag in the manifest, and Service A's CI would have failed before anyone approved anything.

This is not a theoretical architecture. ThunderLang's intent conformance model is built around exactly this flow, and the design decision to make proof artifacts durable and inspectable rather than ephemeral pass/fail signals is what makes postmortem work tractable.

What Reviewers Can and Cannot Do

I want to be direct about the reviewer question because I have heard the pushback: "We have good engineers on our team. We do thorough code review. We will catch this."

You will catch some of it. You will not catch all of it, and the rate at which you miss things increases as AI-generated changes accelerate your merge velocity.

A reviewer looking at the shared module's diff does not have the downstream consumer's codebase open in another tab. Even if they know the consumer exists, they are mentally modeling the consumer's behavior, not reading it. That mental model is imprecise. It degrades over time. It is unavailable when the reviewer is on vacation and someone else approves the PR.

Reviewers are excellent at catching intent problems when those problems are visible in the diff. The whole category of risk we are discussing here is defined by the fact that the problem is not visible in the diff. The diff looks clean. The problem lives in the gap between the changeset and the consumer's assumptions, and that gap has no representation in any artifact a reviewer is looking at.

This is not a criticism of code review. It is a claim about what code review is and is not designed to do. Code review is a human judgment layer on a local diff. Cross-service contract drift is a distributed systems property. You need a distributed systems tool to verify it, not a more attentive human.

The governance posture I'd argue for: treat AI-generated changes to shared modules as structurally higher-risk than AI-generated changes to leaf services, require intent conformance checks as a hard CI gate rather than a recommendation, and version behavioral manifests with the same rigor you apply to API schemas. If you would version-control and review a Protobuf schema change before deploying it, apply the same discipline to the behavioral contracts in your shared utilities. They are the same class of problem.

Shared code without declared behavioral contracts is not just technical debt. With AI agents in the loop, it is an actively expanding blast radius.

Gate your first AI change

If the parseDate scenario landed for you, the concrete next step is to get a behavioral contract on one shared module before the next AI-assisted refactor touches it. ThunderLang lets you declare what a change must satisfy, then gates AI-written code against that intent with a verify-diff and durable proof artifacts. Try it here.

The contracts you forgot you made are the ones AI refactoring will break first.