The $0 Bug: How We Found a Hidden Dependency with Zero Imports
Two files changed together in 75% of commits. Zero imports connected them. Static analysis said they were independent. Git history said otherwise.
We had a bug that took three hours to find. The fix was one line. The root cause was something no linter, type checker, or static analyzer could have caught: two files that were deeply coupled through shared assumptions, with zero imports between them.
This is the story of how we found it, and why we built a tool to make sure it never happens again.
The setup
Our API server has two entry points: routes.ts (for local development) and worker.ts (for Cloudflare Workers production). Both implement the same endpoints. Both format responses the same way. Neither imports the other.
One Friday, we added a role field to the API response in worker.ts. Tests passed. Deployed. Production looked fine.
Monday morning, a user reports the SDK is throwing type errors. The SDK expected the role field. The local dev server — running through routes.ts — wasn't returning it. Same API, different runtime, different response shape.
The fix was trivial: add the same field to routes.ts. The debugging wasn't trivial because nothing in the codebase pointed us to the problem. No import chain. No type shared between the files. No test that exercised both entry points with the same assertion.
What static analysis sees
We ran every tool we had:
- **TypeScript compiler**: both files type-check independently. No errors.
- **ESLint**: no unused imports, no reference issues. Clean.
- **Import graph**: `routes.ts` and `worker.ts` exist in separate dependency trees. No connection.
- **Call graph**: no function in one file calls a function in the other.
From the perspective of static analysis, these files are completely independent. You could delete one and the other would compile and run without errors.
Static analysis answers the question "what does this code reference?" It cannot answer "what does this code assume?"
What git history sees
We ran git log and counted how often these files appeared in the same commit:
Total commits touching routes.ts: 12
Commits also touching worker.ts: 9
Co-change rate: 75%
Import between them: none75%. Three out of every four times someone touched routes.ts, they also touched worker.ts. This isn't coincidence — it's a dependency. Not a code dependency. A behavioral dependency. They share an implicit contract that both files must maintain the same response format.
Git knows this. Static analysis doesn't. And no AI coding agent is running git log --name-only and computing co-change matrices before suggesting edits.
This pattern is everywhere
Once you start looking for hidden dependencies, you see them constantly:
Config and consumers. A YAML config file and the code that reads it share key names and value shapes. Change the config structure, the consumer breaks silently. No import connects them.
Database migrations and queries. A migration adds a column. A query file assumes the column exists. The migration file and query file are in different directories, different languages sometimes. No import.
API contracts and clients. The server returns a response shape. The frontend destructures it. Different repos, different deploys, same implicit contract.
Test fixtures and implementations. A test fixture assumes a function returns an array. The function changes to return a generator. The fixture still "works" but now tests something that doesn't exist.
In a codebase audit of 5 mid-size projects, we found an average of 8-12 hidden dependency pairs per project. Files that change together 40-80% of the time with zero import connections.
Why this matters for AI agents
This is the specific failure mode that makes AI coding agents unreliable for non-trivial refactoring:
- 1.Developer asks agent to refactor `routes.ts`
- 2.Agent reads `routes.ts`, understands its imports, traces its dependency tree
- 3.Agent makes the change. All imported files are updated. Types check out.
- 4.Agent doesn't touch `worker.ts` because nothing suggests they're related
- 5.Production bug ships
The agent did everything right by the standards of static analysis. But it missed the behavioral coupling because that information doesn't exist in the source code. It exists in the project's history.
Measuring coupling strength
The formula for co-change coupling is straightforward:
coupling(A, B) = commits_both(A, B) / commits_either(A, B)A strength of 0.75 means 75% of commits that touch either file touch both. Combined with import data, you get a risk classification:
High coupling + import exists = expected dependency, well-understood
High coupling + no import = HIDDEN dependency, high risk
Low coupling + import exists = loose coupling, safe to change independently
Low coupling + no import = genuinely unrelatedThe hidden dependencies (high coupling, no import) are where bugs live. They're the blind spots that experienced developers know about intuitively ("oh yeah, if you touch routes you always have to check worker too") but that are never documented and never visible to tools that only read source code.
Building temporal analysis into the workflow
After the role field incident, we built git co-change analysis into CodeCortex. When you run codecortex init, it scans your entire commit history and extracts:
Hotspots (most volatile files):
12 changes worker.ts VOLATILE
10 changes routes.ts VOLATILE
8 changes compute.ts STABILIZINGHidden dependencies: routes.ts ↔ worker.ts 75% coupling, no import ← WARNING routes.ts ↔ migrate.ts 58% coupling, no import sdk/types ↔ worker.ts 42% coupling
Bug history: worker.ts: "NULL-coalescing SQL fails on neon()" worker.ts: "role field must exist in ALL 3 API surfaces" compute.ts: "batch inserts required — 1/row too slow" ```
When an AI agent connects via MCP and prepares to edit routes.ts, it sees the coupling warning. It knows to check worker.ts. It knows the historical failure mode. The three-hour debugging session becomes a five-second lookup.
The broader point
Hidden dependencies are a fundamental limitation of static analysis. No type system, no linter, and no AST parser will ever catch them because they don't exist in the syntax tree. They exist in the behavioral patterns of how a codebase evolves over time.
Git history is the richest source of behavioral data in any software project. It records every co-change, every fix, every refactoring pattern. But almost nobody queries it systematically, and no AI coding agent has access to it unless you explicitly provide it.
The role field bug cost us three hours. In a larger organization, a hidden dependency bug in a critical path can cost days, incidents, and customer trust. The information to prevent it was sitting in git log the entire time.
We just weren't asking the right questions.