Точное разделение, у меня был ровно этот случай: миграция 200К строк JS в TS, greenfield по технологии, но brownfield по бизнес-правилам, которые нигде не были записаны. Спасли golden-master тесты на самые страшные модули, снять снапшот поведения до того как подпускать агента, а не полагаться на существующее покрытие, которого почти не было. Агент с одинаковой уверенностью предложит и правильный рефакторинг, и тот, что тихо сломает старый костыль.
Точная механика, у меня плагин собран похоже, только два слоя вместо трёх: правила плюс скиллы, references отдельно не выносил, зря, судя по вашему описанию. Больнее всего оказалась не структура слоёв, а формулировка description у скилла: если два скилла описаны слишком похоже, агент иногда подгружает не тот, и это не всегда заметно, оба ведь relevant. Как валидируете пересекающиеся description, кроме ручного прогона на реальных задачах?
Спасибо за развёрнутый ответ, разложили ровно так как я и предполагал: policies как ортогональная граница, а не седьмой вариант. У нас в итоге разделение получилось похожим, canAccess как чистая функция в domain, а привязка к ролям и подключение в UI уже в application. Если добавите change request на права в стенд, было бы интересно посмотреть как это ляжет на карго-вариант, кажется там утечка будет самой явной.
Хорошая идея с CI_JOB_URL в вебхук, у нас похожая штука но без Телеграма, шлём в Slack. Разница в том что мы кладём ссылку не в чат, а прямо в статус корневого пайплайна через комментарий к MR, чтобы не листать чаты в поисках последнего сообщения. Возьму на вооружение вариант с прямым вебхуком, для быстрых уведомлений это проще того что делаем мы.
Согласен на все сто, граница-проверка единственное что реально работает. Когда вводили eslint-boundaries на существующий проект, первый прогон выдал больше 300 нарушений разом. Пришлось неделю держать правило на warn вместо error и чинить пачками по слайсам, иначе синьоры бы просто выключили линтер в первый же день. feature-public-api из dependency-cruiser выглядит строже, у меня eslint-boundaries иногда пропускает динамические импорты, надо проверить.
У нас в проде NestJS, там reflect-metadata и decorators, и один раз получили сюрприз: перевели сборку на esbuild ради скорости, а decorators metadata сломались, потому что esbuild эмитит их не так как tsc. Две недели дебага, прежде чем поняли что дело не в коде. Ваш подход с explicit inject через обычные объекты от этого класса проблем избавляет полностью, ценой ручного описания зависимостей.
Впечатляет, что взяли реальные метрики через dependency-cruiser, а не спор на прозе. У нас был похожий эксперимент: сравнивали Clean и FSD pre-sliced на модуле корзины, и права доступа в обоих случаях текли не туда, потому что ни одна архитектура явно не описывает где жить авторизации на уровне UI. Завели отдельный слой policies/ поперёк обеих схем. У вас в стенде такие сквозные штуки разложены по шести вариантам, или это отдельная категория?
Похожая схема была на GitHub Actions, не Gitlab, но принцип тот же: корневой workflow дёргает reusable workflows как дочерние. Главная боль оказалась не в архитектуре, а в дебаге: когда падает деплой на третьем уровне вложенности, разработчик видит красный крестик в корневом пайплайне и скачет по вкладкам в поисках реального лога. Добавили в каждый дочерний пайплайн финальный джоб, который на fail пишет прямую ссылку на лог в статус корневого, экономит минут десять на инцидент.
У нас похожая история, только на NestJS: повторяющуюся сверку договоров свели в DI-собираемый пайплайн вместо агента, который сам решал что делать. Инъекция через конструктор дала то же самое, что у вас: подменить модель на моковую в тестах без единой правки бизнес-логики. Экономика тоже совпала: разовые задачи отдаём агенту, а как только процесс становится регулярным, переводим в такой пайплайн, токены на повторный reasoning того не стоят.
У нас на Next.js похожая история, только не пять кнопок, а три хука useCredits с разной логикой инвалидации кэша, потому что агент каждый раз писал новый вместо того чтобы найти старый. Слайсы по фиче спасают, но без физической границы (index.ts с ограниченным экспортом плюс eslint-boundaries) агент всё равно рано или поздно тянет импорт из внутренностей соседнего слайса, просто потому что так короче.
Согласен, но добавлю нюанс. У джуна с ИИ на руках риск не в том, что он не напишет MVP, а что не заметит, когда стоит остановиться и спросить сеньора. Ошибка architecture-уровня, которую сеньор ловит на автомате, у джуна улетает в прод, потому что код выглядит рабочим и тесты зелёные.
Plan-режим и architect.md реально помогают, но у меня они работают только вместе с физическими границами в коде. Агент план распишет аккуратно, а потом в процессе реализации всё равно тянется через приватный экспорт в соседний модуль, если ничего не мешает физически. architect.md держит намерение, eslint-boundaries держит факт. Без второго первое живёт максимум пару спринтов, потом файл устаревает и агент по нему уже не сверяется.
У нас похожая история была, не семь месяцев, а два инцидента среди ночи за полгода. Ревью кода агента откладывали, потому что тесты зелёные и в проде тихо. Дырка вылезла на стыке модулей, где юнит-тесты не покрывали side-effect. С тех пор ревью каждого PR от агента обязательное, без исключений даже если зелёный CI.
Согласен, разделение это ключ. У нас после перехода на модули с одним index.ts агент почти перестал видеть весь проект целиком, для большинства задач хватает CLAUDE.md конкретного модуля плюс сам модуль, это два-три файла контекста вместо двухсот тысяч строк. Раздутый контекст правда тупиковый путь, мы пробовали закидывать весь проект пару раз в начале, агент путался и трогал не те файлы. Сейчас по опыту изоляция на уровне архитектуры работает лучше, чем любой промпт, который просит агента не трогать чужое.
Есть смысл, но не до конца. Write-only работает, пока агент под рукой и есть время на промпт. У нас за полгода два раза прилетал инцидент среди ночи, код пришлось читать руками, без времени звать агента и формулировать задачу словами. Тогда граница спасает, а write-only нет, если внутри всё равно нужно разобраться за 10 минут. Поэтому границы держу жёсткие, а сам код всё равно пишу читаемым, на случай что его будет читать человек, а не только следующий промпт.
Это бэкенд на NestJS, браузерного бандла тут нет, так что для api-модулей index.ts не бьёт по lazy loading. А вот на фронте (next.js) с тем же паттерном один раз словили похожую боль, страница подтягивала лишние 40-50kb JS, потому что компоненты реэкспортировались через общий index.ts модуля. Ушли от этого через точечный dynamic import на уровне страницы, а барьер index.ts оставили только для бэкенд-модулей.
Да, именно физический уровень. У нас границы вынесены в отдельные пакеты, импорт из чужого внутреннего модуля просто не напишется, eslint-boundaries рубит на корню. Пока контракт можно обойти по-человечески, агент рано или поздно это сделает, причём в самый неудобный момент. Когда обойти физически нельзя, выбора у него не остаётся.
Дело не в том, влезает проект в контекст или нет. Даже когда модель видит все 200к разом, она лезет через границу: чинит баг, дёрнув приватную функцию из соседнего модуля, потому что так короче. Архитектура нужна не чтобы уместить в окно, а чтобы агент не обходил контракт даже когда видит весь код. Продумать заранее согласен, но тут legacy, который уже есть, с нуля переписать не вариант.
Сайд-эффекты на стыках это как раз больное место. У нас агент не видел, что репозиторий дёргает вебхук, пока это не было явно в типе порта. Помогли две вещи: эффект стал частью контракта (порт описывает что он триггерит, а не только что возвращает), и интеграционные тесты ровно на стыках модулей, которые агент обязан прогнать. Дрейф ловится там, а не в юнит-тестах.
Точное разделение, у меня был ровно этот случай: миграция 200К строк JS в TS, greenfield по технологии, но brownfield по бизнес-правилам, которые нигде не были записаны. Спасли golden-master тесты на самые страшные модули, снять снапшот поведения до того как подпускать агента, а не полагаться на существующее покрытие, которого почти не было. Агент с одинаковой уверенностью предложит и правильный рефакторинг, и тот, что тихо сломает старый костыль.
Точная механика, у меня плагин собран похоже, только два слоя вместо трёх: правила плюс скиллы, references отдельно не выносил, зря, судя по вашему описанию. Больнее всего оказалась не структура слоёв, а формулировка description у скилла: если два скилла описаны слишком похоже, агент иногда подгружает не тот, и это не всегда заметно, оба ведь relevant. Как валидируете пересекающиеся description, кроме ручного прогона на реальных задачах?
Спасибо за развёрнутый ответ, разложили ровно так как я и предполагал: policies как ортогональная граница, а не седьмой вариант. У нас в итоге разделение получилось похожим, canAccess как чистая функция в domain, а привязка к ролям и подключение в UI уже в application. Если добавите change request на права в стенд, было бы интересно посмотреть как это ляжет на карго-вариант, кажется там утечка будет самой явной.
Хорошая идея с CI_JOB_URL в вебхук, у нас похожая штука но без Телеграма, шлём в Slack. Разница в том что мы кладём ссылку не в чат, а прямо в статус корневого пайплайна через комментарий к MR, чтобы не листать чаты в поисках последнего сообщения. Возьму на вооружение вариант с прямым вебхуком, для быстрых уведомлений это проще того что делаем мы.
Согласен на все сто, граница-проверка единственное что реально работает. Когда вводили eslint-boundaries на существующий проект, первый прогон выдал больше 300 нарушений разом. Пришлось неделю держать правило на warn вместо error и чинить пачками по слайсам, иначе синьоры бы просто выключили линтер в первый же день. feature-public-api из dependency-cruiser выглядит строже, у меня eslint-boundaries иногда пропускает динамические импорты, надо проверить.
У нас в проде NestJS, там reflect-metadata и decorators, и один раз получили сюрприз: перевели сборку на esbuild ради скорости, а decorators metadata сломались, потому что esbuild эмитит их не так как tsc. Две недели дебага, прежде чем поняли что дело не в коде. Ваш подход с explicit inject через обычные объекты от этого класса проблем избавляет полностью, ценой ручного описания зависимостей.
Впечатляет, что взяли реальные метрики через dependency-cruiser, а не спор на прозе. У нас был похожий эксперимент: сравнивали Clean и FSD pre-sliced на модуле корзины, и права доступа в обоих случаях текли не туда, потому что ни одна архитектура явно не описывает где жить авторизации на уровне UI. Завели отдельный слой policies/ поперёк обеих схем. У вас в стенде такие сквозные штуки разложены по шести вариантам, или это отдельная категория?
Похожая схема была на GitHub Actions, не Gitlab, но принцип тот же: корневой workflow дёргает reusable workflows как дочерние. Главная боль оказалась не в архитектуре, а в дебаге: когда падает деплой на третьем уровне вложенности, разработчик видит красный крестик в корневом пайплайне и скачет по вкладкам в поисках реального лога. Добавили в каждый дочерний пайплайн финальный джоб, который на fail пишет прямую ссылку на лог в статус корневого, экономит минут десять на инцидент.
У нас похожая история, только на NestJS: повторяющуюся сверку договоров свели в DI-собираемый пайплайн вместо агента, который сам решал что делать. Инъекция через конструктор дала то же самое, что у вас: подменить модель на моковую в тестах без единой правки бизнес-логики. Экономика тоже совпала: разовые задачи отдаём агенту, а как только процесс становится регулярным, переводим в такой пайплайн, токены на повторный reasoning того не стоят.
У нас на Next.js похожая история, только не пять кнопок, а три хука useCredits с разной логикой инвалидации кэша, потому что агент каждый раз писал новый вместо того чтобы найти старый. Слайсы по фиче спасают, но без физической границы (index.ts с ограниченным экспортом плюс eslint-boundaries) агент всё равно рано или поздно тянет импорт из внутренностей соседнего слайса, просто потому что так короче.
Согласен, но добавлю нюанс. У джуна с ИИ на руках риск не в том, что он не напишет MVP, а что не заметит, когда стоит остановиться и спросить сеньора. Ошибка architecture-уровня, которую сеньор ловит на автомате, у джуна улетает в прод, потому что код выглядит рабочим и тесты зелёные.
Plan-режим и architect.md реально помогают, но у меня они работают только вместе с физическими границами в коде. Агент план распишет аккуратно, а потом в процессе реализации всё равно тянется через приватный экспорт в соседний модуль, если ничего не мешает физически. architect.md держит намерение, eslint-boundaries держит факт. Без второго первое живёт максимум пару спринтов, потом файл устаревает и агент по нему уже не сверяется.
У нас похожая история была, не семь месяцев, а два инцидента среди ночи за полгода. Ревью кода агента откладывали, потому что тесты зелёные и в проде тихо. Дырка вылезла на стыке модулей, где юнит-тесты не покрывали side-effect. С тех пор ревью каждого PR от агента обязательное, без исключений даже если зелёный CI.
Согласен, разделение это ключ. У нас после перехода на модули с одним index.ts агент почти перестал видеть весь проект целиком, для большинства задач хватает CLAUDE.md конкретного модуля плюс сам модуль, это два-три файла контекста вместо двухсот тысяч строк. Раздутый контекст правда тупиковый путь, мы пробовали закидывать весь проект пару раз в начале, агент путался и трогал не те файлы. Сейчас по опыту изоляция на уровне архитектуры работает лучше, чем любой промпт, который просит агента не трогать чужое.
Есть смысл, но не до конца. Write-only работает, пока агент под рукой и есть время на промпт. У нас за полгода два раза прилетал инцидент среди ночи, код пришлось читать руками, без времени звать агента и формулировать задачу словами. Тогда граница спасает, а write-only нет, если внутри всё равно нужно разобраться за 10 минут. Поэтому границы держу жёсткие, а сам код всё равно пишу читаемым, на случай что его будет читать человек, а не только следующий промпт.
Это бэкенд на NestJS, браузерного бандла тут нет, так что для api-модулей index.ts не бьёт по lazy loading. А вот на фронте (next.js) с тем же паттерном один раз словили похожую боль, страница подтягивала лишние 40-50kb JS, потому что компоненты реэкспортировались через общий index.ts модуля. Ушли от этого через точечный dynamic import на уровне страницы, а барьер index.ts оставили только для бэкенд-модулей.
Спасибо. Самое рабочее из всего, пожалуй, запрет на импорт из чужих внутренних модулей, остальное уже надстройка над этим.
Да, именно физический уровень. У нас границы вынесены в отдельные пакеты, импорт из чужого внутреннего модуля просто не напишется, eslint-boundaries рубит на корню. Пока контракт можно обойти по-человечески, агент рано или поздно это сделает, причём в самый неудобный момент. Когда обойти физически нельзя, выбора у него не остаётся.
Дело не в том, влезает проект в контекст или нет. Даже когда модель видит все 200к разом, она лезет через границу: чинит баг, дёрнув приватную функцию из соседнего модуля, потому что так короче. Архитектура нужна не чтобы уместить в окно, а чтобы агент не обходил контракт даже когда видит весь код. Продумать заранее согласен, но тут legacy, который уже есть, с нуля переписать не вариант.
Сайд-эффекты на стыках это как раз больное место. У нас агент не видел, что репозиторий дёргает вебхук, пока это не было явно в типе порта. Помогли две вещи: эффект стал частью контракта (порт описывает что он триггерит, а не только что возвращает), и интеграционные тесты ровно на стыках модулей, которые агент обязан прогнать. Дрейф ловится там, а не в юнит-тестах.