← AI Chaos Guide/root causes
dependency-corruption

Dependency Graph Corruption in AI-Generated Code: The Root Cause Explained

Dependency graph corruption is a structural root cause in AI-generated codebases where the import relationships between modules degrade from a directed acyclic graph into a web of circular and cross-domain dependencies. The result: isolation becomes impossible, every change has an unpredictable blast radius, and the system cannot be safely tested, refactored, or scaled.

The defining characteristic of dependency graph corruption is that it is invisible at the file level. Each individual import looks reasonable in isolation. The problem is only visible when you map the entire import graph — and by then, the circular chains have typically been present for months, silently expanding the blast radius of every change.

This page explains the mechanism by which prompt-driven development produces dependency graph corruption, how to detect it with precision, and what structural intervention is required to restore graph integrity.


Who This Is For

Developers, architects, and technical leads working with AI-generated codebases who need a precise technical understanding of:

  • Why circular dependencies form as a structural consequence of prompt-driven development
  • How to map the dependency graph and identify corruption with concrete tooling
  • What the difference is between breaking individual circular chains (symptom treatment) and establishing directional dependency enforcement (root cause intervention)
  • How dependency graph corruption interacts with architecture drift to produce regression cascades

For the founder-facing explanation of what dependency graph corruption feels like in practice, see Fragile Systems and Regression Fear.


The Mechanism: Why Prompt-Driven Development Corrupts the Dependency Graph

Dependency graph corruption is not caused by careless developers. It is a structural consequence of how prompt-driven development resolves dependencies at the file level without awareness of the graph-level consequences.

The File-Level Resolution Problem

Prompt-driven development resolves dependencies at the file level without graph-level structural awareness. When a session generates or modifies a file, the resolution question is: "What does this file need to import to accomplish its task?" The graph-level question — "Does adding this import create a cycle?" — is structurally outside the scope of the session.

The structural constraint is the bounded context of prompt-driven development. The session sees the file being modified and the files explicitly provided in the prompt. The full import graph is not in scope. The detection that module A already imports from module B — making an import from B to A a cycle — requires graph-level analysis that no individual session performs.

The result: circular dependencies form silently, one import at a time, across multiple prompt sessions. No single session creates the problem. The corruption accumulates.

The Three Formation Patterns

Dependency graph corruption in AI-generated codebases follows three distinct formation patterns:

Pattern 1: Bidirectional coupling (most common)

Session 12: auth/login imports from user/profile (to get user data)
Session 34: user/profile imports from auth/login (to check auth status)
Result: auth ↔ user circular dependency

Each import was locally reasonable. The cycle was invisible until the graph was mapped.

Pattern 2: Transitive cycle (hardest to detect)

Session 8:  billing imports from user
Session 19: user imports from notifications
Session 41: notifications imports from billing
Result: billing → user → notifications → billing (3-node cycle)

No single import created the cycle. The cycle emerged from three independent decisions across three sessions.

Pattern 3: Shared utils overuse (most insidious)

utils/helpers.ts grows to contain:
  - auth utilities (imported by auth module)
  - billing utilities (imported by billing module)
  - user utilities (imported by user module)
  - notification utilities (imported by notifications module)

auth module imports from utils/helpers.ts
billing module imports from utils/helpers.ts
user module imports from utils/helpers.ts

Result: all modules are coupled through the shared utils file.
A change to utils/helpers.ts affects all modules simultaneously.

This is not a circular dependency in the strict sense — but it produces the same effect: a change in one place has an unpredictable blast radius across the entire codebase.

Why Corruption Accelerates Over Time

Dependency graph corruption is self-accelerating. Once circular dependencies exist, they make the graph harder to reason about — which makes it more likely that future prompt sessions introduce additional cycles.

The mechanism: a developer prompts "add feature X to module A." The AI generates code that imports from module B. The developer does not know that module B already imports from module A (through a transitive chain). The new import creates a second cycle. The graph becomes more corrupted.

By the time the team maps the dependency graph for the first time, they typically find not one cycle but five to fifteen — each one added independently, each one individually invisible at the time of creation.


Technical Depth: The Three Failure Patterns of Dependency Graph Corruption

FP006: Circular Dependencies — Direct Cycles

A circular dependency exists when module A imports from module B, and module B imports from module A (directly or transitively). The consequence:

  • Isolation is impossible — you cannot load module A without loading module B, and vice versa
  • Testing is compromised — unit tests for module A require the full module B to be initialized
  • Refactoring is high-risk — a change in module A propagates to module B, which propagates back to module A
  • Build times increase — the bundler must resolve the cycle, which adds overhead and can produce non-deterministic output

Scoring thresholds (from AI Chaos Index):

Circular chains RC02 base severity
0 0
1–2 4
3–5 6
>5 8

FP007: Cross-Domain Imports — Boundary Violations at the Graph Level

Cross-domain imports occur when a module in one business domain directly imports from a module in a different business domain — bypassing any shared interface or adapter layer.

# ❌ FORBIDDEN: billing domain importing directly from auth domain
# domains/billing/calculate_overage/service.py
from domains.auth.login.service import LoginService  # cross-domain import

The consequence: the billing domain is now coupled to the auth domain's internal implementation. A change to the auth domain's internal structure — a refactoring, a rename, a signature change — breaks the billing domain. The blast radius of auth changes now includes billing.

Cross-domain imports are the dependency graph equivalent of layer boundary violations: they couple modules that should be independent, creating hidden blast radius expansion.

FP008: Shared Utils Overuse — Implicit Global Coupling

Shared utils overuse occurs when a utility module grows to contain logic from multiple domains, and multiple domains import from it. The utility module becomes an implicit global coupling point.

// utils/helpers.ts — grown to 800 lines
export function validateEmail() { ... }      // used by auth
export function formatCurrency() { ... }     // used by billing
export function getUserDisplayName() { ... } // used by user
export function sendNotification() { ... }   // used by notifications
export function calculateDiscount() { ... }  // used by billing
export function checkPermissions() { ... }   // used by auth

Every module that imports from utils/helpers.ts is now coupled to every other module that imports from it — through the shared file. A change to calculateDiscount() requires re-testing auth, user, and notifications — even though they have no logical relationship to billing calculations.


Detection: Mapping the Dependency Graph

The following detection methodology maps directly to the AI Chaos Index scoring model for RC02.

Step 1: Circular Dependency Detection (primary signal)

# TypeScript/JavaScript — detect all circular dependencies
npx madge --circular --extensions ts,tsx,js src/ 2>/dev/null

# With detailed output showing the cycle paths
npx madge --circular --extensions ts,tsx,js --json src/ 2>/dev/null | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
cycles = [v for v in data.values() if v]
print(f'Circular dependency chains: {len(cycles)}')
for i, cycle in enumerate(cycles[:10], 1):
    print(f'  Cycle {i}: {\" → \".join(cycle)}')
"

# Python — detect circular imports
pip install pydeps --quiet 2>/dev/null
pydeps . --max-bacon=3 --show-cycles 2>/dev/null

# Alternative Python check using importlib
python3 -c "
import ast, os, sys
from pathlib import Path

def get_imports(filepath):
    try:
        tree = ast.parse(Path(filepath).read_text())
        return [n.names[0].name if isinstance(n, ast.Import)
                else n.module for n in ast.walk(tree)
                if isinstance(n, (ast.Import, ast.ImportFrom))
                and getattr(n, 'module', None)]
    except:
        return []

py_files = list(Path('.').rglob('*.py'))
print(f'Analyzed {len(py_files)} Python files')
print('Run: pydeps . --show-cycles for full cycle detection')
"

Step 2: Cross-Domain Import Detection (secondary signal)

# Detect imports crossing domain boundaries
# Assumes domain structure: domains/{domain_name}/{slice_name}/
echo "=== Cross-domain imports ==="
find . -path "*/domains/*/*.py" -not -path "*/test*" | while read f; do
  domain=$(echo "$f" | grep -oP "domains/\K[^/]+")
  grep -n "from domains\." "$f" 2>/dev/null | \
    grep -v "from domains\.$domain\." | \
    sed "s|^|$f:|"
done | head -20

# TypeScript equivalent
find src/domains -name "*.ts" -o -name "*.tsx" 2>/dev/null | while read f; do
  domain=$(echo "$f" | grep -oP "domains/\K[^/]+")
  grep -n "from.*domains/" "$f" 2>/dev/null | \
    grep -v "from.*domains/$domain/" | \
    sed "s|^|$f:|"
done | head -20

Step 3: Shared Utils Size and Import Count (tertiary signal)

# Find large utility files (shared utils overuse signal)
echo "=== Large utility/helper files ==="
find . \( -name "utils.py" -o -name "helpers.py" -o -name "utils.ts" \
  -o -name "helpers.ts" -o -name "common.ts" -o -name "shared.ts" \) \
  -not -path "*/node_modules/*" -not -path "*/__pycache__/*" | \
  xargs wc -l 2>/dev/null | sort -rn | head -10

# Count how many files import from each utility file
echo "=== Import frequency of utility files ==="
for util in $(find . \( -name "utils.py" -o -name "helpers.py" \
  -o -name "utils.ts" -o -name "helpers.ts" \) \
  -not -path "*/node_modules/*" 2>/dev/null); do
  count=$(grep -rl "$(basename $util .py)" \
    --include="*.py" --include="*.ts" --include="*.tsx" \
    . 2>/dev/null | wc -l)
  echo "$count imports: $util"
done | sort -rn | head -10

Step 4: RC02 Severity Calculation

primary_signal = circular_dependency_chain_count
secondary_signals = [
  cross_domain_imports_present,        # boolean
  deep_transitive_deps_over_5,         # boolean
  shared_utils_overuse                 # boolean
]
secondary_bonus = count(secondary_signals_present) × 0.75

RC02_severity = min(lookup(primary_signal) + secondary_bonus, 10)

Example calculation:

Codebase: 3 circular chains → base score: 6
Secondary: cross_domain(✓) + shared_utils(✓) = 2 × 0.75 = 1.5
RC02_severity = 6 + 1.5 = 7.5 → rounded: 8
RC02 contribution to ACI = 8 × 0.20 = 1.6 (out of 2.0 max)

The Graph Integrity Restoration Model

The structural intervention required to stop dependency graph corruption is directional dependency enforcement — a set of rules that govern which modules are allowed to import from which other modules, enforced automatically.

1. Dependency Direction Rules

A clean dependency graph is a directed acyclic graph (DAG): imports flow in one direction, never in cycles. The rules:

Allowed:
  UI layer → Hook layer → Service layer → Repository layer → Database
  Domain A → Shared interfaces (never Domain A → Domain B directly)
  Any module → Shared infrastructure (DB session, logging, config)

Forbidden:
  Any cycle (A → B → A, or A → B → C → A)
  Cross-domain imports (Domain A → Domain B internals)
  Shared utils containing domain-specific logic

2. Dependency Linter Configuration

// dependency-cruiser configuration (.dependency-cruiser.js)
module.exports = {
  forbidden: [
    {
      name: "no-circular",
      severity: "error",
      comment: "Circular dependencies are forbidden",
      from: {},
      to: { circular: true }
    },
    {
      name: "no-cross-domain",
      severity: "error",
      comment: "Cross-domain imports are forbidden",
      from: { path: "^src/domains/([^/]+)/" },
      to: {
        path: "^src/domains/([^/]+)/",
        pathNot: "^src/domains/$1/"  // different domain
      }
    },
    {
      name: "no-shared-utils-domain-logic",
      severity: "warn",
      comment: "Shared utils should not contain domain-specific logic",
      from: { path: "^src/shared/utils" },
      to: { path: "^src/domains/" }
    }
  ]
};
# Run dependency linter
npx depcruise --config .dependency-cruiser.js src/

# Or with madge for circular detection only
npx madge --circular --extensions ts,tsx src/ --exit-code
# Exit code 1 if circular dependencies found — use in CI/CD

3. Breaking Existing Cycles

When circular dependencies are already present, breaking them requires identifying the direction of the dependency that should be reversed or removed:

Cycle: auth/login ↔ user/profile

Analysis:
  auth/login imports user/profile to get user data → legitimate
  user/profile imports auth/login to check auth status → violation

Intervention:
  Remove the auth/login import from user/profile
  Replace with: pass auth status as a parameter, or use a shared interface
  Result: auth/login → user/profile (directional, no cycle)

Structural intervention requires identifying the dependency direction that violates the intended layer hierarchy. In a cycle A ↔ B, one direction is architecturally legitimate (consistent with the layer model) and one is a violation. The violation is removed and replaced with a parameter, a shared interface, or an event-based communication pattern.


How Dependency Graph Corruption Interacts with Architecture Drift

Dependency graph corruption and architecture drift (RC01) are distinct root causes, but they interact in a compounding pattern:

  • Architecture drift erodes layer boundaries → modules begin importing across layers
  • Cross-layer imports create the conditions for circular dependencies → dependency graph corruption begins
  • Circular dependencies make the blast radius of every change unpredictable → regression cascades
  • Regression cascades make the team afraid to refactor → architecture drift accelerates

The interaction is bidirectional: architecture drift creates the conditions for dependency graph corruption, and dependency graph corruption makes architecture drift harder to address. A codebase with both RC01 and RC02 at high severity is in a self-reinforcing degradation cycle.

This is why the AI Chaos Index scores both root causes independently and weights them separately: RC01 at 25%, RC02 at 20%. A codebase with both at maximum severity contributes 4.5 points to the ACI score from these two root causes alone — before RC03, RC04, and RC05 are scored.


Is This Happening in Your Codebase?

Get a structural assessment with your AI Chaos Index score — delivered in 24 hours.