Free

3000 строк одним коммитом: почему сложные фичи требуют сначала plan mode

Инстинкт при сложной фиче — «давай что-нибудь попробую» — но архитектурные решения прячутся в третьей модели, в пятом edge case. Настоящая ценность plan mode в том, чтобы перенести архитектурный разговор в текст до кода. Реальный кейс: система общественного жюри Pickful — 3032 строки, 119 spec зелёные, один коммит, ноль архитектурных переделок после.


Столкнувшись со сложной фичей, у большинства инстинкт один — «дай попробую что-нибудь».

Проблема в том, что у сложных систем есть особенность — архитектурные решения прячутся в третьей модели, в пятом edge case, в восьмом правиле начисления очков. Нырнуть в код и наткнуться на эти решения посередине, а потом возвращаться и менять их, обходится в 10× дороже, чем проговорить всё в тексте с самого начала.

Я использовал plan mode Claude Code, чтобы собрать TopicReview — общественную систему присяжных в Pickful, — и увидел крайний вариант этого неравенства: один коммит, 3032 строки, 119 спеков зелёных, отгружено за один раз. Ни один из десятка с лишним последующих коммитов не был архитектурной переделкой — всё это подстройка параметров, допиливание UI, заплатки для edge case. Система с тех пор работает стабильно и сейчас является центральной для того, как сообщество модерирует само себя.

Эта статья о том, почему сложные фичи требуют сначала plan mode, что на самом деле делает фаза plan и когда разговор достаточно созрел, чтобы начинать писать код.

Насколько сложна система

TopicReview — это общественное голосование, решающее, удалять ли некачественный пост. Одной фразой — но спецификация раскладывается слоями:

  • 5 состояний: open → voting → decided → appealed → closed
  • 3 вердикта: remove / warn / keep
  • 2 стадии: первичное рассмотрение 12 присяжными; апелляция — 5 судей (выбранных жребием из топ-20 пользователей по очкам)
  • Многомерный поток очков: 10 pt временной штраф, 10 pt стейк апелляции, +5 присяжным, голосовавшим за вердикт, +10 судьям, голосовавшим за вердикт, +3 жалобщику, чья жалоба подтверждена, возврат при победе в апелляции = стейк + бонус + временной штраф
  • 4 типа запланированных задач: окно первичного голосования 24 ч, окно апелляции 24 ч, окно рассмотрения апелляции 24 ч и — очки присяжным откладываются на 24 ч после вердикта remove (апелляция может его развернуть)
  • Параллельные потоки + rollback: если голоса переворачиваются во время временного удаления, пост надо восстановить и вернуть временной штраф; если апелляция переворачивает вердикт, возвращается стейк плюс бонус, возможно и временной штраф, а присяжных пересчитывают по новому вердикту

Суть сложности не в отдельных правилах — она во взаимодействии правил. Каждое новое правило может запустить rollback где-то ещё.

О какие стены ты ударяешься без plan

Когда пишешь с разгона, самые сложные проблемы — не видимые факты, а вопросы, которые тебе не пришло в голову задать. Прочитав код TopicReview задом наперёд, видно минимум четыре стены, о которые ты неизбежно ударишься, если пропустишь plan:

Правила отбора присяжных. На вид просто User.jurors_and_judges.sample(12). Но реальные правила: исключить автора поста, исключить того, кто на него пожаловался, исключить того, кто уже голосовал в первичной стадии (чтобы один и тот же человек не голосовал и в апелляции). Три слоя исключений. Пишешь модель одним махом — и пропустишь одно-два.

Отложенная выдача очков. Вердикт remove обычно запускает выдачу очков присяжным. Но апелляция может перевернуть remove — и когда переворачивает, присяжные, голосовавшие за старый вердикт, оказываются на неправильной стороне, и выдачу надо пересчитать против нового вердикта. Значит, вердикт remove должен ждать закрытия 24-часового окна апелляции, прежде чем платить присяжным. Пропустишь это правило, выдашь очки раньше — потом будешь их отзывать: в 10 раз геморройнее, чем заплатить позже.

Проектирование интерфейса запланированной задачи. CloseTopicReviewJob с виду «закрыть review». На практике он обрабатывает три ситуации:

# Истечение окна первичного голосования
CloseTopicReviewJob.set(wait_until: voting_ends_at).perform_later(review.id)
# Отложенная выдача очков присяжным после вердикта remove
CloseTopicReviewJob.set(wait: 24.hours).perform_later(review.id, award_juror_points: true)
# Истечение окна апелляции
CloseTopicReviewJob.set(wait: 24.hours).perform_later(review.id, appeal_id: appeal.id)

Без предварительного планирования напишешь первую сигнатуру, а во втором случае обнаружишь, что интерфейс надо менять.

Самая дорогая стена: можно ли подать апелляцию во время временного удаления? Когда голоса remove превышают порог, пост немедленно скрывают (временно), но review всё ещё в состоянии voting, а не decided. Может ли пользователь уже подавать апелляцию?

  • Может → надо расширять переходы между decided и appealed
  • Не может → плохой UX: пост скрыт, а пользователю надо 24 ч ждать, чтобы даже запротестовать
  • Реальный выбор TopicReview: да, апелляция разрешена во время временного удаления — но review должен сперва пройти finalize → decided, прежде чем откроется apelляция

Одно это решение расходится назад по коду и меняет проверки параметров в open_appeal! и логику конечного автомата. Принять его посередине = переписать половину системы.

Что на самом деле делает фаза plan

Plan mode Claude Code — это режим, в котором писать код запрещено: Claude может читать репо, обдумывать подходы, обсуждать с тобой, но любая модификация файлов жёстко блокируется, пока ты не одобришь план.

Это механическое ограничение и есть суть: оно заставляет разговор архитектурного уровня происходить в тексте.

Несколько вещей, которые фаза plan делает на практике:

1. Рисует конечный автомат и роли. 5 состояний, 4 роли (присяжный / судья / автор / жалобщик), что каждая роль может и не может делать в каждом состоянии — всё в нескольких строках markdown. Несколько строк vs десятки файлов. Стоимость изменения отличается на два порядка.

2. Прогоняет поток каждой роли:

  • Присяжный: получает уведомление → открывает review → читает пост и причину → голосует с reasoning → получает очки
  • Автор поста: получает уведомление → смотрит вердикт → если remove, обдумывает апелляцию → закладывает стейк → ждёт
  • Судья: получает апелляцию, вытянутую из топ-20 → голосует → получает очки

Где шаг застревает, всплывает скрытый вопрос: «видят ли присяжные голоса других присяжных?», «что может делать автор во время временного удаления?»

3. Допрашивает edge case. Фаза plan не проектирует «нормальный путь» — она специально задаёт вопросы, которые обычно не всплывают:

  • Что, если окно закрылось с 0 голосами? (Итоговый выбор: по умолчанию keep.)
  • Что, если апелляция закрылась с 0 голосами? (dismissed; стейк сгорает.)
  • Может ли судья голосовать за пост, на который сам пожаловался? (Нет — то же правило исключения.)
  • Несколько присяжных одновременно триггерят finalize — гонка удвоит выплату? (Добавить поле juror_points_awarded для идемпотентности.)

95% таких вопросов не всплывают естественно при написании кода. Фаза plan заставляет пройтись по ним по одному.

4. Бухгалтерская книга очков. Система очков достаточно сложна, чтобы обсуждения было мало — надо реально нарисовать таблицу: каждый поток очков с триггером, суммой и путём rollback. Когда книга сходится, все edge case (возврат временного штрафа, возврат апелляции, бонусы) согласуются.

Насколько надо договориться, прежде чем писать код

Жёсткий критерий:

  • Ты можешь провести любой путь без заминки — каждую комбинацию от открытия review до closed (keep / remove / warn × с апелляцией / без × временное / нет) рассказать от начала до конца
  • У каждого edge case определено поведение — не «потом посмотрим», а «0 голосов — по умолчанию keep», «временное разрешает апелляцию, но сначала finalize»
  • Нет «а, об этом я ещё не подумал» — на любой сформулированный тобой вопрос уже есть ответ

Когда планка достигнута, писать код — значит переводить план на Ruby.

Как выглядит фаза сквозного выполнения

Первый деплой системы:

d162f1e Add community moderation system with jury/judge review and appeals
  63 files changed, 3032 insertions(+)
  119 specs, 0 failures

63 файла, 3032 строки, 119 спеков зелёных. Задеплоено за один раз.

Следующие десяток с лишним коммитов подтверждают, что архитектура устояла:

Apply legacy penalty (1pt) for posts created before 2026-03-26
Fix topic review appeal bugs: window mismatch and verdict not updated
Add topic_removed status for posts removed by community review
Default to keep verdict when review expires with zero votes
Reduce provisional removal penalty to 1pt during trial period
Allow topic creator to withdraw active reviews
Add handled tab to jury dashboard, fix tabs styling

Каждый — подстройка / фикс / полировка / маленькая фича. Ни один не «вернуться и перепроектировать конечный автомат» или «модель очков надо менять». Каркас оказался полностью верным.

Это и есть настоящая отдача plan: выполнение не прерывается. Нет «подожди, а этот путь как?» — план уже покрыл. Нет «про этот edge case я не подумал» — фаза plan спросила. Нет «этот интерфейс надо переделывать» — план уже решил.

Часы написания кода, делая только одно: переводить ясный дизайн на клавиатуру.

Когда plan не нужен

Не всякая задача достойна plan:

  • Простые багфиксы — локализовать + починить + добавить regression test; предварительного обсуждения не нужно
  • Механический рефакторинг — переименовать, выделить, переместить; путь один, plan — лишний шаг
  • Есть только один очевидный путь — добавить GET-эндпоинт, UI-тогл; дебатировать нечего
  • Разведочные прототипы — ты сам не знаешь, чего хочешь; запустить что-то корявое выигрышнее, чем дальше думать

Выгода plan mode в снижении стоимости переписывания. Если в задаче нет риска переписывания, plan — просто накладные расходы.

Заключение

«Сначала подумай — потом делай» звучит как совет по характеру. Plan mode не об этом.

Plan mode — это когда разговор архитектурного уровня происходит там, где 10 слов могут его изменить, а не там, где придётся менять 30 файлов.

В условиях сложности стоит перевернуть старое отраслевое «words are cheap, code is expensive» — оно не значит «пиши код сначала, там видно будет» (это работает только при малом разрыве стоимости), оно значит использовать этот разрыв: перенести дорогой разговор вперёд, в дешёвую среду.

3000 строк одним коммитом — ощущение прекрасное. Не потому что я быстро пишу, а потому что план превратил «писать» в чисто механический акт.