Один и тот же PR, два разных ревью

Один разработчик из моего круга поставил Claude Code в CI как ревьюера кода. “Пусть Claude просто проверяет PR”, - сказал он мне. - “Он ловит то, что я пропускаю”. Я попросил его прогнать один и тот же PR через Claude дважды.

В первом ревью было написано, что обработка ошибок выглядит исчерпывающе. Во втором ревью та же самая обработка ошибок была признана недостаточной, и Claude предложил три изменения. Он пять минут смотрел в экран, пытаясь понять, какой именно Claude прав.

Не был прав ни один. И не был неправ ни один. LLM выдавал вероятностные догадки, а не применял детерминированные правила. В этом и состоит фундаментальная проблема использования AI для verification: verification требует двух свойств, которых у LLM нет. Determinism - одинаковый вход всегда даёт одинаковый выход. Completeness - все режимы отказа ловятся всегда.

Проблема детерминизма

Запустите один и тот же код через Claude Code два раза, и вы можете получить два разных ревью. Temperature, размер context window, формулировка prompt - всё это вносит вариативность. Для brainstorming вариативность полезна. Для verification это баг. Нельзя строить надёжный софт на вероятностных quality gates.

У детерминированных проверок этой проблемы нет. tsc --noEmit всегда выдаёт одни и те же ошибки. ESLint с одной и той же конфигурацией всегда находит одни и те же нарушения. Эти инструменты применяют правила, а не вероятности. Они скучные. Поэтому они и работают.

Стек guardrails с O(1)

Вот как выглядит наш pre-commit stack. Каждая проверка детерминирована. Каждая выполняется за миллисекунды. Ни одной не важно, написал код человек или Claude Code:

# Pre-commit (< 1 second total)
TypeScript strict mode      # tsc --noEmit
ESLint boundary rules       # module isolation
Import restrictions         # dependency direction
dependency-cruiser          # graph analysis
Unit tests                  # contract verification

# CI (< 30 seconds)
Full test suite
Bundle size checks
Fresh clone launch test

Ключевая метрика - O(1) относительно размера кодовой базы. Type checking инкрементальный. Linting выполняется по файлам. dependency-cruiser линейно анализирует import graph. Ничто из этого не замедляется по мере роста вашего vibe-coded приложения. Claude Code review, наоборот, становятся медленнее и менее точными, когда кодовая база перерастает context window.

Что на самом деле ловят детерминированные проверки

На прошлой неделе dependency-cruiser поймал PR, в котором screen component импортировал реализацию сервиса напрямую, а не через barrel. PR спокойно компилировался. Тесты проходили. Cursor сгенерировал импорт автоматически. Но это нарушало наше архитектурное правило: только composition root импортирует реализации.

LLM-ревью могло бы это заметить. А могло бы и пропустить, потому что PR был большим, а импорт был спрятан в середине файла. dependency-cruiser не пропускает такое никогда, потому что он не “читает” код - он анализирует граф:

// .dependency-cruiser.cjs
// 8 lines. 100ms. Catches this every time, forever.
{
  name: 'no-deep-service-imports',
  comment: 'Only service barrels are public.',
  severity: 'error',
  from: { pathNot: '^src/services/' },
  to: {
    path: '^src/services/[^/]+/',
    pathNot: '^src/services/[^/]+/index\.ts$',
  }
}

Восемь строк. Меньше ста миллисекунд. Каждый PR. Навсегда. Claude Code review тратит токены, занимает секунды и всё равно может это пропустить. Математика тут даже не близка.

Где AI действительно уместен

Я не anti-AI. Я каждый день использую Cursor и Claude Code - для черновиков кода, исследования подходов, написания тестов и отладки. Но я не ставлю их на роль verification. Verification должно быть детерминированным. Скучным. Одинаковым каждый раз.

Guardrails должны быть невидимыми и неизбежными. Они должны ловить проблемы без того, чтобы кто-то вообще вспоминал о запуске проверок. Именно это и дают детерминированные checks: страховочную сетку, которая не спит, не устаёт и не теряет контекст по мере роста кодовой базы.

Autotomy Expo Starter Pack поставляется с TypeScript strict mode, ESLint boundary rules, конфигурацией dependency-cruiser и pre-commit hooks - всё уже настроено. Вы получаете скорость Cursor для генерации. И машинную точность для verification. Так и выглядит vibe coding без поломки production.