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, перш ніж відкриється апеляція

Одне це рішення розходиться назад по коду й змінює перевірки параметрів у 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 — чи race подвоїть виплату? (Додати поле 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 рядків одним комітом — відчуття прекрасне. Не тому, що я швидко пишу, а тому, що план перетворив «писати» на чисто механічний акт.