Все рецепты

Архитектурные барьеры для агента (Phase 3)

Фаза 3 harness-серии: «машина важнее текста». Превращаем архитектурные правила из текста в docs/architecture.md в детерминированные проверки — границы импортов, мёртвый код, циклы, строгие типы. Кросс-стековая таблица «роль → инструмент» (dependency-cruiser / import-linter, knip / ts-prune / vulture, madge, tsc / mypy), test-integrity tripwire вместо мандата на покрытие (Goodhart), pre-commit + CI как точки форса и самописные скрипты для непокрытого. С копируемыми промптами и конфигами.

ПродвинутыйDevOps с AI25 минpre-commit, CI, dependency-cruiser, import-linter, eslint
1

Барьер ≠ конвенция: форсится только барьер

Базовый принцип harness-инженерии — «машина важнее текста». Правило, выраженное проверкой (линтер, тест, скрипт), действует. То же правило абзацем в README — нет. Phase 3 — это место, где архитектурные правила из docs/architecture.md перестают быть текстом и становятся детерминированными проверками. Различай три состояния правила. Барьер (barrier) форсится механически: pre-commit и CI не дадут зашипить код, который его нарушает — у агента просто нет пути в обход. Конвенция (convention) держится только на дисциплине агента: записана в AGENTS.md, но никто не проверяет — на дистанции дрейфует. Условный шаг (conditional) срабатывает по триггеру (worktree, ExecPlan, эскалация) и часто правильно пропускается. Вывод жёсткий: на что нельзя поставить барьер — на то нельзя положиться надолго. Phase 3 максимизирует долю правил, переведённых в барьеры.

🟥 Барьер

  • Форсится механически: pre-commit / CI
  • Нет пути в обход — агент упирается
  • Границы, типы, циклы, мёртвый код

🟦 Конвенция

  • Держится на дисциплине агента
  • Записана в AGENTS.md, но не проверяется
  • На дистанции дрейфует

🟨 Условный

  • Срабатывает по триггеру
  • worktree, ExecPlan, эскалация, /wrong
  • Часто и правильно пропускается
Прежде чем писать конфиг, перечитай Layer model в docs/architecture.md. Барьер ловит ровно то, что в нём закодировано: размытое правило в тексте даст размытую (или дырявую) проверку.
2

Кросс-стек таблица: роль → инструмент

Главное правило этой фазы — инструмент-агностичность. Не «eslint — закон вселенной», а «есть роль проверки, и под стек проекта подбирается инструмент в этой роли». Сначала определяешь роли (что должно проверяться), потом фиксируешь конкретный выбор в ADR docs/decisions/0001-tool-selection.md — дальше все фазы берут его как вход. Ниже — роли и примеры инструментов по стекам. Например, в JS/TS границы модулей и публичный API часто держат через eslint-plugin-boundaries или архитектуру FSD, но это лишь один стек — роль та же и в Python (import-linter), и в Go (depguard). Что не покрыто готовым инструментом — закрываешь самописным скриптом-проверкой (последняя строка). Не ищи «один инструмент на всё»: каждая роль — отдельный барьер.
Роль (что проверяем)JS / TSPythonДругие стеки
Границы импортов / слоёвdependency-cruiser, eslint-plugin-boundariesimport-linterdepguard (Go), ArchUnit (JVM)
Мёртвый код (dead code)knip, ts-prunevulturedeadcode (Go), dead_code lint (Rust)
Циклы зависимостейmadgeimport-linter (contracts)компилятор (Go), ArchUnit (JVM)
Вложенность и размер файловeslint (max-depth, max-lines)flake8, ruff (C901, PLR)golangci-lint, clippy
Строгие типы (strict types)tsc --strictmypy --strict, pyrightsorbet (Ruby), компилятор (Go/Rust)
Непокрытое вышеСвой скрипт-проверкаСвой скрипт-проверкаСвой скрипт-проверка
Каждую строку этой таблицы зафиксируй в ADR с обоснованием выбора. Иначе следующий агент через месяц поставит второй инструмент в ту же роль — и появятся два расходящихся барьера.
3

Хребет: строгие типы + границы импортов

Два барьера несут основную нагрузку — это хребет Phase 3. Первый: строгие типы. Для статически типизированных языков — включить strict-режим и самые строгие опции компилятора (tsc --strict, mypy --strict). Стратегия миграции из промпта 3.2: если включение строгости даёт < 50 ошибок — включи и исправь сейчас; если больше — заведи ADR с поэтапным планом и TODO с датой, не блокируй работу. Второй: границы импортов из Layer model. Конфиг должен отражать ВСЕ правила слоёв — разрешённые направления зависимостей, запрет циклов, запрет orphan-модулей (мёртвый код, не импортируемый ниоткуда), запрет импортов в обход публичного API. Что из этого применимо — зависит от стека. Важный нюанс из промпта 3.1: НЕ молча правь найденные нарушения. Записывай их в docs/quality.md как техдолг с приоритетами; чинь сразу только то, что < 30 минут и не блокирует.

Хребет Phase 3 — что должно проверяться

Strict-режим типов включён на максимум, разумный для проекта
Разрешённые направления зависимостей между слоями
Запрет циклов между модулями
Запрет orphan-модулей (мёртвый код)
Запрет импортов в обход публичного API
Нарушения занесены в docs/quality.md, а не «починены молча»
Массовый авто-фикс нарушений без записи в реестр
Строгость типов и границы дают агенту легибельность среды (agent-legibility): чем строже сигнатуры и чётче слои, тем меньше агент додумывает контекст — и тем реже ломается на ровном месте.
4

Test-integrity tripwire, а не «100% покрытия»

Самая частая ошибка на этой фазе — ввести покрытие (coverage) как мандат. Это Goodhart: как только процент становится целью, агент пишет фиктивные тесты под число, и сигнал умирает. Поэтому не ставь процент как цель. Что ставить вместо мандата — test-integrity барьер (tripwire). Это проверка по диффу в CI: агенту запрещено ослаблять, удалять, скипать или «смягчать» существующие тесты, чтобы они прошли. Удалил assert, добавил .skip, ослабил матчер, выкинул кейс — CI падает. Тесты здесь — сигнал здоровья, а не цель; покрытие — побочный эффект честной работы, а не таргет. Если очень хочешь гейт на процент — только на новый/изменённый код (≈80% — разумный дефолт, но это твой выбор, не мандат), плюс mutation testing для качества сигнала.

✅ Test-integrity tripwire

  • Нельзя удалять/скипать существующие тесты
  • Нельзя ослаблять assert и матчеры
  • Проверяется по диффу в CI
  • Тесты = сигнал здоровья

❌ Мандат на покрытие (Goodhart)

  • «100% везде» как глобальное правило
  • Процент становится целью
  • Агент пишет фиктивные тесты под число
  • Сигнал умирает, дашборд зелёный
Покрытие — это side effect честной работы, а не таргет. Один tripwire «не ослабляй тесты» защищает сигнал лучше, чем любой процент: число можно накрутить, integrity-диффом — нет.
5

Точки форса: pre-commit + CI (копируемый конфиг)

Барьер действует только там, где его нельзя обойти. Две точки форса: pre-commit (быстрый, по диффу, ловит до коммита) и CI (полный, ничего не пропустит на merge). На pre-commit вешаем форматтер, основной линтер, линтер границ и тайп-чек — но только на затронутые файлы, не на всю кодовую базу, иначе хук станет невыносимо медленным и его начнут обходить через --no-verify. CI гоняет всё целиком и делает обход невозможным. Definition of Done для Phase 3: pre-commit падает на нарушении границ, CI падает на нарушении границ, docs/quality.md содержит реестр известных нарушений с приоритетами. Ниже — копируемый промпт 3.1 для агента и генерик-конфиг pre-commit. Конкретный оркестратор (pre-commit framework, husky, lefthook) выбирается под стек на Phase 0 и фиксируется в ADR.
# ── Промпт 3.1 для агента — границы модулей / Prompt 3.1 — module boundaries ──
Phase 3. Прочитай docs/architecture.md (Layer model) и
docs/decisions/0001-tool-selection.md (роль "Линтер границ модулей").

Задача: превратить layer model в машинно-проверяемые правила,
используя инструмент, выбранный на Phase 0.

1. Сконфигурируй инструмент. Конфиг отражает ВСЕ правила Layer model:
   - разрешённые направления зависимостей между слоями
   - запрет циклов между модулями
   - запрет orphan-модулей (мёртвый код, не импортируемый ниоткуда)
   - запрет импортов в обход публичного API
   (что применимо — зависит от стека).
2. Запусти на текущей кодовой базе. Покажи отчёт.
3. НЕ правь нарушения молча. Запиши их в docs/quality.md как
   техдолг с приоритетами. Чини только то, что < 30 минут и не блокирует.
4. Подключи запуск как обязательный шаг на коммит/пуш (pre-commit
   hook / husky / lefthook — уместный под стек) и в CI.
Обнови Progress, Surprises & Discoveries, Decision Log.

# ── Генерик-конфиг pre-commit (только diff!) / generic pre-commit (diff only!) ──
repos:
  - repo: local
    hooks:
      - id: format          # форматтер / formatter
        entry: <project-formatter>
      - id: lint            # основной линтер / main linter
        entry: <project-linter>
      - id: boundaries      # границы импортов / import boundaries
        entry: <boundary-linter>            # dep-cruiser / import-linter
      - id: typecheck       # строгие типы по затронутым файлам
        entry: <type-checker --changed>     # tsc / mypy
# CI повторяет всё это на ПОЛНОЙ базе — обход через --no-verify не спасёт.
pre-commit — по диффу, CI — целиком. Если повесить всё на pre-commit полным прогоном, агент (и человек) начнут жать --no-verify, и барьер тихо превратится в конвенцию.
6

Самописные скрипты для непокрытого + что дальше

Готовые инструменты закрывают типовые роли, но у каждого проекта есть свои правила, на которые нет линтера: «в domain-слое нет импортов из infra», «в этой папке нет console.log», «у каждого публичного хендлера есть контракт-схема». Это последняя строка таблицы: что не покрыто — пишешь своим скриптом-проверкой и вешаешь в ту же связку pre-commit + CI. Скрипт — такой же барьер, если он падает с ненулевым кодом. Граница harness-инженерии честная: эти барьеры дают архитектурную целостность и поддерживаемость, но НЕ валидируют функциональную корректность — что код делает то, что нужно пользователю, всё ещё проверяет человек и продуктовые тесты. Phase 3 закрыта, когда выполнен DoD. Дальше — Phase 4 (quality.md как живой документ, где покрытие фиксируется как сигнал, а не цель) и Phase 5 — сборка мусора харнеса: автоматические проверки на дрейф по расписанию, чтобы барьеры не ржавели.
#!/usr/bin/env bash
# check-boundaries-custom.sh — барьер для правила, на которое нет готового линтера
# Пример: в domain-слое нет импортов из infra / no infra imports in the domain layer.
set -euo pipefail

# Адаптируй grep/AST-инструмент под свой стек — суть в exit code.
VIOLATIONS=$(grep -rEn "from ['\"].*infra" src/domain/ || true)

if [ -n "$VIOLATIONS" ]; then
  echo "❌ domain-слой импортирует infra (нарушение Layer model):"
  echo "$VIOLATIONS"
  exit 1            # ненулевой код → pre-commit и CI падают / pre-commit & CI fail
fi
echo "✅ boundaries: domain чист от infra-импортов"

# Что НЕ ловит ни один из этих барьеров:
#   функциональная корректность — «делает ли код то, что нужно пользователю».
#   Это остаётся на человеке и продуктовых тестах. Открытая граница harness.
Самописный скрипт — кандидат №1 на промоушн из lessons-ledger: повторяющаяся коррекция «снова импорт в обход слоя» затвердевает в проверку и больше не повторяется. Так агент чинит свою среду сам.

Результат

Архитектурные правила перестали быть текстом и стали барьерами: строгие типы и границы импортов — хребет, test-integrity tripwire защищает тесты вместо мандата на покрытие (без Goodhart), pre-commit + CI делают обход невозможным, а самописные скрипты закрывают непокрытое. Инструменты выбраны по ролям под стек и зафиксированы в ADR. DoD Phase 3 выполнен; дальше — quality.md (Phase 4) и сборка мусора харнеса (Phase 5).