Архитектор Full-Stack JS с годом опыта работы с ИИ в продакшене описал, как переделал TypeScript-монолит на 200 тысяч строк после того, как понял: агент не нарушает архитектуру намеренно — он просто не видит границ, которые нигде не зафиксированы машиночитаемо.
Проблема обнаружилась при ревью pull request от Claude Code. Код работал, тесты были зелёными, коллега поставил апрув. Но сервис уведомлений тянул зависимости из биллинга, профиля и аналитики одновременно. Формально чисто — по факту любое изменение требовало держать в голове граф из восемнадцати файлов. Автор назвал это «когнитивной асимметрией»: у человека есть физическое трение при добавлении каждой новой зависимости, у агента его нет. Импортировать из соседнего файла и из модуля на другом конце проекта для него одинаково дёшево. Агент оптимизирует под «работает сейчас», а не под «это можно менять через полгода».
| Правило | Инструмент | Что проверяет |
|---|---|---|
| Не больше 3 внешних зависимостей в юзкейсе | ESLint | Юзкейс делает одну вещь |
| Импорт в чужой модуль только через index.ts | ESLint + dependency-cruiser | Инкапсуляция модуля |
| Нет импортов из infrastructure в domain | dependency-cruiser | Чистота доменного слоя |
Первым шагом стал отказ от папки как синонима модуля. Каждый модуль получил единственную точку входа — файл index.ts с публичным API. Всё остальное внутри стало приватным: не «нежелательным для импорта», а физически недоступным — нарушение ловит линтер. Раньше агент писал `import { TariffEntity } from '../../billing/domain/tariff.entity'`, залезая во внутренности чужого модуля. Теперь доступен только `import { getTariff } from 'modules/billing'`. Если нужной функции в публичном API нет, агент обязан явно расширить контракт — и это изменение видно на ревью, потому что меняется index.ts, а не прячется в глубине дерева импортов.
Каждый модуль получил единственную точку входа index.ts — прямой импорт во внутренности ловит линтер.
Вторым шагом три архитектурных правила перекочевали из текстовых инструкций в dependency-cruiser и ESLint-конфиг с severity: 'error'. Правило первое: не больше трёх внешних зависимостей в одном юзкейсе — если их четыре, юзкейс, скорее всего, делает две вещи. Правило второе: импорт в чужой модуль только через публичный index.ts, не глубже. Правило третье: никаких импортов из infrastructure в domain — зависимости смотрят внутрь, к доменным правилам. Нарушение любого из трёх валит CI. Для агента это принципиально: текстовое правило он трактует в пользу задачи, красный пайплайн он чинит сам, ещё до того как код доходит до ревью.
Третьим шагом логика была разведена по слоям внутри каждого модуля. Без явных слоёв Claude Code по умолчанию кладёт бизнес-правила туда, где они ближе к запросу, — в контроллер. Расчёт скидки, проверка прав и запись в базу оказываются в одном HTTP-обработчике: работает, тест есть, но переиспользовать правило из фоновой задачи уже нельзя. После разводки domain содержит чистый TypeScript без единого импорта фреймворка — функция `subscriptionPrice` принимает пользователя и тариф, возвращает цену, тестируется одним юнит-тестом. Юзкейс в слое application оркестрирует домен и генерирует событие. Контроллер стал тонким: разбирает запрос и вызывает юзкейс. Отправка письма ушла за событие — контроллер больше ничего не знает про почту.
Подход описывает практику, которая в отрасли обсуждается всё активнее: агентные инструменты вроде Claude Code, GitHub Copilot Workspace или Cursor работают в контексте нескольких файлов, но не держат в голове всю кодовую базу. Это означает, что архитектурные ограничения должны быть исполняемыми — встроенными в инструментарий, а не описанными в документации. Dependency-cruiser для TypeScript-проектов и аналогичные инструменты статического анализа зависимостей становятся не опциональным улучшением, а необходимым слоем контроля при работе с ИИ-агентами в продакшене.
