Free

3000 行を一度で本番投入——なぜ複雑な機能は plan から始めるべきか

複雑な機能を前にした反射的反応は「とりあえず試してみよう」——だが設計判断は 3 つ目のモデル、5 つ目のエッジケースに潜む。plan mode の本当の価値は、設計レベルの対話をコードの前、文字の段階で済ませることだ。実例:Pickful の社区陪審システム——3,032 行・119 spec すべてパス・一度の commit で本番投入、以後の設計戻しはゼロ。


複雑な機能を前にすると、大半の人の本能的反応は「とりあえず少し書いてみよう」だ。

問題は、複雑なシステムには特徴があることだ——設計判断は 3 つ目のモデルの中、5 つ目のエッジケースの中、8 つ目のポイントルールの中に隠れている。コードに頭から飛び込んで、そうした判断に出くわしてから手戻りするコストは、最初に文字のレベルで詰めきっておくコストの 10 倍になる。

私は Claude Code の plan mode で Pickful の TopicReview という社区陪審システムを作ったとき、この不等式の極端なバージョンを目撃した:1 コミット、3,032 行、119 spec すべてグリーン、本番投入は一度きり。その後の十数コミットはいずれも設計戻しではなく、パラメータ調整・UI の磨き込み・エッジケースの補修だけだ。システムは以降ずっと安定稼働しており、コミュニティ自治の中核になっている。

この記事では、なぜ複雑な機能は plan から始めなければならないのか、plan フェーズは実際に何をしているのか、会話がどこまで進んだらコードを書き始めてよいのか、を書く。

システムがどれだけ複雑か

TopicReview は低品質な投稿を削除するかどうかをコミュニティ投票で決める仕組みだ。一言で言えばそれだけだが、仕様は層を重ねていく:

  • 5 つの状態:open → voting → decided → appealed → closed
  • 3 種の評決:remove / warn / keep
  • 2 つの審理段階:初審は 12 人の陪審員が投票;上訴は 5 人の judge(ポイント上位 20 人から抽選)が再審
  • 多軸のポイントフロー:10 pt の仮差押え、10 pt の上訴 stake、陪審員が多数派と一致すれば +5、judge が多数派と一致すれば +10、通報者の通報が認められれば +3、上訴勝訴で stake + bonus + 仮差押え分の返還
  • 4 種のスケジュールジョブ:24h 初審投票窓口、24h 上訴受付窓口、24h 上訴審理窓口、そして remove 評決では陪審員ポイントの発行を 24h 遅延(上訴で覆る可能性があるため)
  • 並行 + ロールバック:provisional removal 中に票が反転すれば投稿を復元し仮差押えを返還;上訴で覆れば stake と bonus を返し、場合によっては仮差押えも返還、陪審員は新評決で再計算

複雑さの核心はどれか一つのルールではなく、ルール同士の相互作用にある。ルールを一つ足すたびに別のどこかでロールバックが走りうる。

plan を省くとぶつかる壁

ぶっつけで書いたとき、いちばん厄介なのは見えている事実ではなく、自分では問わなかったであろう問いだ。TopicReview のコードから逆算すると、plan を省けば必ずぶつかる壁が少なくとも四つある:

陪審員の資格ルール。見た目はただの User.jurors_and_judges.sample(12)。でも実際のルールは、投稿者を除外、通報者を除外、初審で投票済みの人も除外(同一人物が上訴で再度投票するのを防ぐ)——三条の除外が重なる。モデルを一気に書くと、一つか二つ落としやすい。

ポイントの遅延発行。remove 評決は通常、陪審員ポイントの発行を伴う。しかし上訴は remove を覆しうる——覆ると陪審員の多数派側が反転し、ポイントを新評決で再計算しなければならない。だから remove 評決は必ず24h の上訴窓口が閉じてからポイントを発行することになる。この仕様を紙に書いておかずに先にポイントを配ると、回収に回ることになる——遅延発行の 10 倍面倒だ。

スケジュールジョブのインターフェース設計CloseTopicReviewJob は見た目「review を閉じる」だけ。実際は 3 つのシナリオを扱う:

# 初審投票窓口の期限切れ
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)

先に整理しないと、最初は単一シグネチャで書いて、2 番目のシナリオを実装するときにインターフェースを全部やり直すことになる。

最も高くつく壁:provisional removal 中に上訴できるか。票が多数派 remove に達すると投稿は即座に非公開になる(provisional)。ただし review 自体はまだ voting 状態、decided になっていない。ユーザーは上訴できるのか?

  • できる → decided と appealed の間の遷移を広げる必要がある
  • できない → UX が悪い。投稿は非公開にされ、24h 待たないと抗議もできない
  • TopicReview の実際の選択:provisional 中でも上訴可。ただし先に finalize → decided を走らせてから appeal を開く

この選択は逆流して open_appeal! のパラメータ検証と状態マシンのロジック全体に影響する。途中で決めるのは半分のシステムを書き直すのと同義だ。

plan フェーズは実際に何をしているのか

Claude Code の plan mode は、コードを書くことが許されないモードだ——Claude はリポジトリを読むこと、方針を考えること、あなたと議論することはできるが、ファイル変更はあなたが plan を承認するまで hard-block される。

この機械的な制約そのものが、plan mode の核心的価値だ——設計レベルの対話を文字の段階で起こすことを強制する

plan フェーズが実際にやっていることをいくつか:

1. 状態マシンと役割の描画。5 つの状態、4 種の役割(陪審員 / judge / 投稿者 / 通報者)、各役割が各状態で何をでき何をできないか——すべて数行の markdown に収まる。数行 vs 数十ファイル、変更コストは 2 桁違う。

2. 各役割のフロー walkthrough

  • 陪審員視点:通知を受ける → review を開く → 投稿と理由を読む → 投票と reasoning → ポイントを受け取る
  • 投稿者視点:通知を受ける → 評決を見る → remove なら上訴を検討 → stake を積む → 待つ
  • Judge 視点:top-20 から選ばれた appeal を受ける → 投票 → ポイントを受け取る

歩きが止まるところで隠れた問いが即座に浮上する:「陪審員は他の陪審員の票を見られるのか?」「投稿者は provisional removal 中に何ができるか?」

3. エッジケースの尋問。plan フェーズは「通常ケース」を設計するものではなく、普段考えようとしない問いをわざわざ問うものだ:

  • 投票窓口が切れても 0 票だったらどうする?(最終選択:デフォルト keep)
  • 上訴窓口が切れても 0 票だったらどうする?(dismissed、stake は没収)
  • judge が自分で通報した post に投票できるか?(不可、同じ除外ルール)
  • 複数の陪審員が同時に finalize 条件をトリガーした場合、レースでポイントを二重発行しないか?(juror_points_awarded を冪等フィールドとして加える)

これらの問いの 95% はコードを書いているときに自然には浮かび上がらない——plan フェーズが一つずつ潰すことを強制する。

4. ポイント台帳。ポイントシステムは議論だけでは足りないほど複雑で、実際に表を描く必要がある:各 point の流れに対して、トリガー条件・金額・ロールバック経路を明記する。台帳がそろえば、すべてのエッジケース(仮差押えの返還、上訴の返還、bonus)が帳尻合わせ可能になる。

会話がどこまで進んだらコードを書き始めてよいか

硬い基準:

  • 各経路を詰まらずウォークスルーできる——review を開いてから closed に至るまでのあらゆる組み合わせ(keep / remove / warn × 上訴あり / なし × provisional / 非 provisional)を一気通貫で話せる
  • エッジケースに明確な挙動がある——「それはまた」ではなく、「0 票ならデフォルト keep」「provisional 中でも上訴可だが先に finalize」
  • 「これはまだ考えていない」がない——投げられるあらゆる問いに答えが用意されている

この水準に達したら、コードを書くことは plan を Ruby に翻訳する作業になる。

一気呵成の実行フェーズはどう見えるか

このシステムの最初の ship:

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

63 ファイル、3,032 行、119 spec グリーン。一度で本番投入

その後の十数コミットが設計の堅牢さを証明している:

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 の本当のリターンだ——実行が中断されない。「ちょっと待て、この経路はどう扱う?」が存在しない——plan に書いてある。「このエッジケース考えてなかった」が存在しない——plan フェーズで問うた。「このインターフェース再設計しなきゃ」が存在しない——plan で整理済み。

連続数時間、コードを書く間にやることは一つだけ:明確な設計をキーボードに落とし込むこと。

plan を使わないほうがよいとき

すべてのタスクに plan が必要なわけではない:

  • 単純なバグ修正——特定 + 修正 + リグレッション test、事前議論は不要
  • 機械的リファクタ——改名、関数抽出、ファイル移動。経路が一つなので plan は余分な手順
  • 明白な道が一つしかない——GET エンドポイントを一つ追加、UI トグルを一つ追加、議論の余地がない
  • 探索的プロトタイプ——自分でも何が欲しいか分かっていない。粗い版を動かすほうが考え込むより速い

plan mode の収益は書き直しコストを下げることから来る。タスクに書き直しリスクがないなら、plan はただのオーバーヘッドだ。

結び

「やる前に考えろ」は性格上のアドバイスのように聞こえる。plan mode はそうではない。

plan mode は、設計レベルの対話を 10 文字で変えられる場所で起こすためのものだ——30 ファイル変えなければならない場所ではなく。

複雑さの下では、ソフトウェア業界の古諺「words are cheap, code is expensive」を逆手に取る必要がある——「先にコードを書いて走らせてみろ」(これはコスト差が小さいときにしか成り立たない)ではなく、そのコスト差を利用して、高価な対話を安価な媒体に前倒しせよ、ということだ。

3,000 行を一度で本番投入する感覚は非常に快い。速く書いたからではなく、plan が「書くこと」を純粋に機械的な行為に変えたからだ。