Boundary Enforcement: How to Prevent AI Code Regression
Boundary enforcement is a structural technique for AI-generated codebases that makes architectural violations impossible to merge. It replaces manual code review — which catches logic errors but not cumulative structural drift — with automated tooling that detects cross-layer imports, circular dependencies, and naming violations at the point of commit.
The technique addresses the root cause of most structural problems in AI-generated codebases: the absence of enforced rules about which modules can import from which other modules, which layer can contain which type of logic, and which naming conventions apply in which context. Without enforcement, prompt-driven development produces architecture drift as a structural inevitability. With enforcement, the drift stops accumulating — regardless of how many prompt sessions are run, which AI tool is used, or how fast the team ships.
This page explains the three components of boundary enforcement, how to implement each one, and what the expected structural outcome looks like.
What Boundary Enforcement Solves
Boundary enforcement directly addresses the structural failure modes that emerge in AI-generated codebases past Day 30:
- Architecture drift — each prompt session places logic in the most convenient location rather than the architecturally correct one; over time, layer boundaries dissolve
- Circular dependencies — prompt sessions resolve imports at the file level without graph-level awareness; cycles form silently across sessions
- Naming inconsistency — each session uses whatever naming convention is natural for the immediate task; conventions fragment across the codebase
- Regeneration losses — prompt-driven regeneration overwrites custom logic in files that have no protected regions
Boundary enforcement does not fix these problems retroactively. It stops them from accumulating in new development — and, when combined with a stabilization sprint, prevents them from recurring after the existing violations are addressed.
The Three Components
Boundary enforcement consists of three components that operate at different points in the development workflow.
Component 1: Layer Definition
A documented, explicit layer architecture that defines which layers exist, which imports are allowed between layers, and which types of logic belong in which layer.
Layer stack (example — adapt to your architecture):
UI Layer → Display only. No business rules, no data access.
API/Handler Layer → Transport only. Input validation, response shaping.
Service Layer → All domain operations. Business rules, calculations.
Repository Layer → All data access. Queries, mutations, caching.
Infrastructure → DB sessions, HTTP clients, logging, config.
Allowed imports (directional only):
UI → API/Handler → Service → Repository → Infrastructure
Forbidden:
Any upward import (Repository → Service is forbidden)
Any cross-domain import (billing → auth internals is forbidden)
Any cycle (A → B → A is forbidden)
The layer definition is a document — typically a ARCHITECTURE.md or a section in the project README. Without it, the boundary linter has nothing to enforce against.
Component 2: Boundary Linter
An automated tool that detects violations of the layer definition and fails the build when they are present.
TypeScript: dependency-cruiser
// .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 — use shared interfaces",
from: { path: "^src/domains/([^/]+)/" },
to: {
path: "^src/domains/([^/]+)/",
pathNot: "^src/domains/$1/"
}
},
{
name: "no-db-in-handlers",
severity: "error",
comment: "Route handlers must not import from data layer directly",
from: { path: "^src/(routes|handlers|controllers)/" },
to: { path: "^src/(models|db|database|prisma|repositories)/" }
},
{
name: "no-business-in-ui",
severity: "error",
comment: "UI components must not import from service or domain layer",
from: { path: "^src/components/" },
to: { path: "^src/(services|domains|repositories)/" }
}
]
};
# Run the boundary linter
npx depcruise --config .dependency-cruiser.js src/
# Check circular dependencies only (faster)
npx madge --circular --extensions ts,tsx src/ --exit-code
Python: custom import checker
# scripts/check_boundaries.py
"""
Enforces domain boundary rules for Python codebases.
Run in CI/CD before tests.
"""
import ast
import sys
from pathlib import Path
FORBIDDEN_PATTERNS = [
# (from_pattern, to_pattern, rule_name)
("domains/billing", "domains/auth", "no-billing-imports-auth"),
("domains/billing", "domains/user", "no-billing-imports-user"),
("routes", "repositories", "no-routes-direct-db"),
("routes", "models", "no-routes-direct-models"),
]
violations = []
for py_file in Path(".").rglob("*.py"):
file_str = str(py_file)
try:
tree = ast.parse(py_file.read_text())
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
module = getattr(node, "module", "") or ""
module_path = module.replace(".", "/")
for from_pat, to_pat, rule in FORBIDDEN_PATTERNS:
if from_pat in file_str and to_pat in module_path:
violations.append(
f"[{rule}] {file_str} imports from {module}"
)
except Exception:
pass
if violations:
print(f"❌ Boundary violations found: {len(violations)}")
for v in violations:
print(f" {v}")
sys.exit(1)
print(f"✓ Boundary check passed — 0 violations")
Component 3: CI/CD Enforcement
The boundary linter runs on every commit, before merge. A commit that introduces a violation fails the CI pipeline and cannot be merged.
# .github/workflows/ci.yml — boundary enforcement step
- name: Boundary linter
run: |
npx depcruise --config .dependency-cruiser.js src/
# or for Python:
# python scripts/check_boundaries.py
# Fails the build if any boundary violation is detected
# This runs before tests — fast feedback, fail early
Combined with GitHub branch protection (Require status checks to pass before merging), this makes it structurally impossible to merge a PR that introduces a boundary violation — regardless of how the violation was introduced.
Code Preservation Markers
Boundary enforcement addresses import-level violations. Code preservation markers address a different problem: prompt-driven regeneration that overwrites custom logic within a file.
Preservation markers define protected regions that must not be modified by regeneration:
# domains/billing/calculate_overage/service.py
class CalculateOverageService:
def __init__(self, repo: CalculateOverageRepository) -> None:
self.repo = repo
# === BEGIN USER CODE ===
# PROTECTED: custom business logic — do not regenerate
def execute(self, request: CalculateOverageRequest) -> CalculateOverageResponse:
credits = self.repo.get_credits(request.user_id)
# Custom rule: legacy DB stores credits as strings
overage = max(0, int(credits) - 100) * 0.005
return CalculateOverageResponse(amount=round(overage, 2))
# === END USER CODE ===
// src/domains/billing/calculate-overage/service.ts
export class CalculateOverageService {
constructor(private repo: CalculateOverageRepository) {}
// === BEGIN USER CODE ===
// PROTECTED: custom business logic — do not regenerate
execute(request: CalculateOverageRequest): CalculateOverageResponse {
const credits = this.repo.getCredits(request.userId)
// Custom rule: legacy DB stores credits as strings
const overage = Math.max(0, parseInt(credits) - 100) * 0.005
return { amount: Math.round(overage * 100) / 100 }
}
// === END USER CODE ===
}
The markers are enforced in two ways:
- AI governance —
.cursorrulesor equivalent instructs the AI tool to never regenerate content between the markers - CI/CD check — a preservation marker check script verifies that protected regions have not been modified in the current commit
What Changes After Boundary Enforcement Is Established
Before boundary enforcement, every prompt session can introduce architectural violations that accumulate silently. After boundary enforcement is established:
- New circular dependencies cannot merge — the boundary linter catches them before the PR is approved
- New cross-layer imports cannot merge — the linter detects the violation and fails the build
- Custom logic in protected regions cannot be overwritten — the preservation marker check catches regeneration losses before deployment
- Architecture drift stops accumulating — the structural state of the codebase is enforced on every commit
The existing violations — circular dependencies, layer violations, oversized files — are not fixed by establishing boundary enforcement. They require a separate remediation sprint. But after boundary enforcement is in place, the remediation work is durable: new violations cannot be introduced, so the structural improvements persist.
Implementation Sequence
The recommended implementation sequence for establishing boundary enforcement in an existing codebase:
Step 1 (1–2 hours): Define the layer architecture
→ Document the intended layer stack in ARCHITECTURE.md
→ Define which imports are allowed and which are forbidden
Step 2 (2–3 hours): Configure the boundary linter
→ Install dependency-cruiser (TypeScript) or create check_boundaries.py (Python)
→ Configure rules based on the layer definition
→ Run against the codebase to establish the current violation baseline
Step 3 (1–2 days): Fix the baseline violations
→ Address the violations surfaced by the linter
→ Priority: circular dependencies first, then cross-layer imports
→ Do not add new features during this step
Step 4 (1–2 hours): Add to CI/CD
→ Add the boundary linter as a CI/CD step
→ Configure branch protection to require the check to pass
→ Add preservation markers to files with custom logic
Step 5 (ongoing): Enforce
→ Every PR now runs the boundary check automatically
→ Violations are caught before merge, not after deployment