
AIに状態遷移の抜けを洗わせる|ステートマシンの穴の見つけ方
「キャンセルしたはずの注文が、発送されてるんですけど」
問い合わせを受けて履歴をたどると、確かに流れはこうでした。支払いが済み、倉庫で出荷準備が始まり、その数分後にお客さんがキャンセルボタンを押し、それでも出荷処理はそのまま進んでいた。
コードを読んでも、どこも間違っていません。キャンセル処理は「キャンセル済み」にする。出荷処理は「出荷準備中」なら発送する。それぞれは、仕様書に書かれたとおりに動いています。
「出荷準備中の注文がキャンセルされたら、どうするか」だけが、どこにも決められていなかったのです。
この手の穴は、正常な流れを追っているあいだは見えません。仕様書のフロー図は、たいてい「うまくいく道」を1本、上から下へ描いています。穴があるのは、その道から横に外れたところ——ある状態のときに、想定していない操作が来たときです。
この記事では、その「横に外れたところ」を状態遷移の表にして、AIに抜けを洗わせる方法を整理します。状態遷移とは、注文や申請のようなデータが「受付→処理中→完了」のように状態を移っていく決まりのこと。それを決まったルールでだけ動くように作ったものを、ステートマシンと呼びます。
結論:順番は3つです。①並べる——状態と、それを動かす操作(イベント)を、それぞれ漏れなく一覧にする。フロー図ではなく「状態×操作」の表にする。②埋める——表のすべてのマスに「どこへ移る/何もしない/エラーにする」のどれかを書く。空欄をAIに洗わせ、人が決める。③弾く——決めていない遷移を、コードとデータベースの側で止める。同時に来たときも含めて。
覚え方は 並べる → 埋める → 弾く。①がないと②のマスが作れず、③がないと決めた表がコードに届きません。
今日ぜんぶやる必要はありません。①の「状態を一覧にする」だけでも、AIに渡せる材料ができて、相談の中身が変わります。
何が起きているのか——穴は「道」ではなく「マス」にある

打ち手の前に、なぜ状態遷移の抜けが生まれやすいのかを整理します。原因の側を先に見ておくと、AIに何を渡せばいいかが見えてきます。
1つ目は、仕様書やフロー図が「道」で描かれていること。受付→支払い済み→出荷準備中→発送済み→完了。この1本の矢印は分かりやすい反面、それぞれの状態で「ほかの操作が来たら」がほとんど描かれません。状態が5つ、操作が5つなら、組み合わせは25マス。図に描かれているのは、そのうち4〜5本の矢印だけ、ということがよくあります。
2つ目は、状態を表す値が、コードのあちこちで直接書き換えられていること。order.status = "CANCELED" がキャンセル処理にも、管理画面にも、夜間バッチにも書かれている。どこから、どの状態へ移れるのかが、1か所に集まっていないので、全体を見渡す手段がありません。
3つ目は、フラグで状態を表していること。is_paid、is_shipped、is_canceled のような真偽値が並んでいると、「支払い済みで、発送済みで、キャンセル済み」のような、あり得ない組み合わせも表せてしまいます。フラグが3つなら組み合わせは8通り。そのうち意味があるのは半分くらい、ということも珍しくありません。
4つ目は、AIに頼むと、正常な道だけの実装が返ってきやすいこと。「注文のステータス管理を書いて」と頼めば、きれいな1本道のコードが出てきます。AIが手を抜いているのではなく、「横から来る操作」を前提として渡していないだけです(AIにコードを書かせる前に渡す前提と制約|伝え方の型)。
5つ目は、人と時間が操作に絡むこと。利用者のキャンセル、担当者の差し戻し、決済サービスからの通知、期限切れのバッチ。それぞれ別の人・別の仕組みが、別のタイミングで同じデータを動かします。遷移の表が頭の中にしかないと、誰かの操作が別の誰かの前提を崩します。
6つ目は、AIが「業務として許されるか」までは決められないこと。出荷準備中のキャンセルを受けるか断るかは、倉庫の運用、返金の手順、お客さんへの約束で決まります。AIは選択肢を並べることは得意ですが、どれを選ぶかは業務の判断です。
裏を返すと、これは腕前の問題ではなく、見る形の問題です。道ではなくマスで見ると、空欄は勝手に目に入ってきます。そしてマスを洗い出すのは、AIがいちばん得意な作業のひとつです。
具体例——つい、こうなりがちという形
どれも「まず正常な流れを動かそう」という自然な順番から生まれます。心当たりのある行があれば、そこだけ見れば十分です。
- 出荷準備中にキャンセルが来る:キャンセル処理は通り、出荷処理も「準備中だったから」と進む。両方とも成功して、結果だけが矛盾します。
- 完了済みの申請を、差し戻せてしまう:差し戻しボタンが、状態を見ずに表示されている。画面で隠していても、APIを直接呼べば通ります。
- 決済の通知が2回届く、または順番が逆に届く:「支払い失敗」のあとに「支払い成功」が届き、失敗の状態で上書きされる。外部からの通知は、順番も回数も保証されないことがあります(AIに外部API連携の設計を相談する|タイムアウトと障害の詰め方)。
- 期限切れバッチが、処理中のデータを期限切れにする:夜間バッチは「期限を過ぎた未完了のもの」を一律に閉じる。ちょうど担当者が処理している最中のものまで巻き込みます。
- フラグの組み合わせが矛盾している:
is_canceledもis_shippedも真。どちらを信じて画面に出せばいいのか、誰にも分かりません。 - 「保留」「確認中」のような状態を、あとから足した:新しい状態から、どこへ移れるかが決まっていない。抜け出す道がない状態に、データが溜まっていきます。
- キャンセルされたものを、もう一度キャンセルできる:返金処理が2回走る。同じ操作が繰り返し届いたときの扱いは、冪等性の話と地続きです(AIに冪等性の設計を相談する|リトライで二重実行させない型)。
- 管理画面からは、どの状態へも書き換えられる:運用のための「何でもできる画面」が、表にない遷移の抜け道になっています。
影響——矛盾したデータは、あとから説明が要る
急いで全部直さないと危ない、という話ではありません。ただ、出たときの後始末が重い種類の問題なので、書いておきます。
- お客さんとのやり取りが発生する:キャンセルしたのに届いた、返金されたのに請求が来た。システムの矛盾が、そのまま人の手間になります。
- データの修正を人がやることになる:どの状態に戻すのが正しいのか、履歴を見て判断する必要があります。直し方を間違えると、別の矛盾が生まれます。
- 再現しにくい:操作の順番とタイミングがそろったときにだけ起きるので、「本当にそんな順で押したのか」の確認から始まります。
- 集計が合わなくなる:売上、在庫、件数のレポートが、状態の矛盾したデータをどちらに数えるかで食い違います。
- 新しい機能を足すたびに怖くなる:どこから状態が変わるか分からないので、影響範囲が読めず、変更のたびに確認が膨らみます。
少し安心できる話をすると、この領域は打ち手がはっきりしています。作りを全部入れ替える必要はありません。状態を一覧にして、表の空欄を埋めるところから始められますし、その作業はAIがかなり手伝ってくれます。
明日からやること(並べる・埋める・弾く)
上から順に、小さく始められます。今日は①だけで構いません。
1. 並べる——状態と操作を、それぞれ一覧にする
ここを飛ばして「抜けを探して」と頼んでも、AIの答えは一般論になります。材料さえそろえば、残りは一気に進みます。
- 対象を1つに絞る。注文、申請、問い合わせチケットなど、状態が変わるデータを1種類だけ選びます。全部まとめてやろうとしないのがコツです。
- 状態を一覧にする。コードの定数、データベースに実際に入っている値、画面に出ている表示名を突き合わせます。実データに、定義にない値が入っていないかも一緒に見ておくと安心です。
- 状態を動かす操作を一覧にする。利用者の操作、担当者の操作、外部からの通知、バッチ、管理画面。「誰が・何をきっかけに」まで書くと、次の段階で抜けが見えやすくなります。
- 状態を書き換えている場所を、コードから洗い出す。ここはAIに頼みやすい作業です。「このリポジトリで
statusを書き換えている箇所をすべて挙げて、どの状態からどの状態へ変えているか表にして」と頼むと、今の実装がどう動いているかの表ができます。 - フラグの組み合わせで表しているなら、組み合わせを全部書き出す。意味のある組み合わせだけを状態として名前をつけると、それだけで整理が進みます。
2. 埋める——表のすべてのマスに、答えを書く
縦に状態、横に操作を並べた表を作り、すべてのマスに次の3つのどれかを書きます。
- 移る:「出荷準備中」で「キャンセル」が来たら「キャンセル依頼中」へ、のように、移り先を書く。
- 何もしない:同じ操作が2回来たときなど、受け取って、何も変えずに成功として返す。
- エラーにする:「完了」で「差し戻し」が来たら断る。断るときに、利用者や呼び出し元へ何を返すかも書いておきます。
ここでAIがよく効きます。表を渡して「空欄のマスと、決めた内容が怪しいマスを挙げて」と頼むと、人が見落としがちな組み合わせを、ほとんど機械的に拾ってくれます。
- 空欄は、AIに埋めさせない。AIに出してもらうのは「このマスが空いています」「こういう選択肢があります」まで。どれを選ぶかは、業務を知っている人と決めます。倉庫、経理、カスタマーサポートに聞くべきマスも出てきます。
- 行き止まりの状態がないか見る。完了やキャンセルのような終わりの状態以外で、どこへも移れない状態は、データが溜まる場所になります。
- たどり着けない状態がないか見る。どこからも移ってこない状態は、定義だけ残った名残か、遷移の書き漏れです。
- 時間で動く遷移を書く。「48時間支払いがなければ期限切れ」のような遷移は、人の操作と同時に来ることがあります。同時に来たら、どちらを優先するかまで書いておきます。
- 表ができたら、図にもしておく。表は漏れを探すのに向き、図は全体をつかむのに向きます。PlantUMLやMermaidの状態遷移図なら、AIに表から起こしてもらえます(AIにシーケンス図・構成図を書かせる|PlantUMLで起こす型)。
3. 弾く——決めていない遷移を、コードとデータベースで止める
表を決めても、コードに届かなければ効きません。決めた表以外の動きを、仕組みで止めるところまでやって、ひとまとまりです。
- 状態を変える入口を1か所に集める。
statusを直接書き換えず、「この状態からこの操作ならこの状態へ」を判定する関数やクラスを1つ通すようにします。表とコードを1対1で見比べられるようになるのがいちばんの効き目です。 - 遷移の表を、コードの中にデータとして持つ。条件分岐を積み上げるより、表そのものをコードに置くほうが、レビューで見比べやすくなります。ライブラリを使う方法もありますが、まずは素朴な対応表で十分なことが多いです。
- 更新するときに「今の状態」を条件に入れる。「状態が出荷準備中のときだけキャンセル依頼中にする」を1文のUPDATEで書き、更新件数が0なら、別の操作が先に状態を変えたとして扱います。同時に操作が来たときの取りこぼしを防げます(AIにトランザクション境界と排他制御を相談する|整合性の詰め方)。
- 管理画面にも、同じ入口を通させる。どうしても例外的な書き換えが必要なら、理由を残す専用の操作として分け、誰がいつ変えたか記録します。
- 状態が変わった記録を残す。いつ、誰が、何の操作で、どこからどこへ。矛盾が起きたとき、履歴があれば半日の調査が数分で済みます(AIにログ出力の不足を指摘させる|障害調査で困らない設計)。
- 表のマスを、そのままテストにする。すべてのマスについて「この状態でこの操作が来たら、こうなる」を1件ずつ書きます。表があれば、テストの観点はもうそろっています。ここもAIに表から書かせやすいところです(AIにテストケースを洗い出させる|観点の与え方)。
AIに相談するときのプロンプト(コピーして使えます)
前提を渡さないと、AIは正常な1本道を返してきます。表の形で出すこと、空欄を勝手に埋めないことを、先に伝えるのがコツです。
ある業務データの状態遷移について、抜けと矛盾を洗い出す手伝いをお願いします。
①下に示す状態と操作をもとに、縦に状態・横に操作を並べた表を作ってください。
②私が決めた遷移を表に書き入れ、決まっていないマスは「未定」と書いてください。未定のマスを推測で埋めないでください。
③「未定」のマスごとに、考えられる選択肢(移る/何もしない/エラーにする)を挙げ、それぞれ業務上どんな影響が出るかを一言添えてください。
④行き止まりの状態(終わりの状態以外で、どこへも移れないもの)と、たどり着けない状態があれば指摘してください。
⑤同時に来うる操作の組み合わせ(利用者の操作とバッチ、外部通知と担当者の操作など)を挙げ、どちらが先に来ても矛盾しないか点検してください。
⑥同じ操作が2回来たとき、順番が逆に来たときの扱いが決まっていないマスを挙げてください。
なお、業務として許されるかどうかは判断しなくて構いません。選択肢と影響だけ並べてください。
対象のデータ:( ) 状態の一覧:( ) 操作の一覧(誰が・何をきっかけに):( ) 決まっている遷移:( ) 終わりの状態:( )
②の一文が効きます。「未定のまま残して」と先に伝えるだけで、AIがもっともらしく埋めてしまうのを防げます。空欄が空欄のまま見えることが、この作業のいちばん大事なところです。
出てきた表は、次の3点だけ自分の目で確かめます。(1)自分が渡していない状態や操作が、いつのまにか足されていないか、(2)「未定」と書かせたマスに、推測の移り先が入っていないか、(3)外部通知やバッチのように、人以外の操作が表に入っているか。この3点は、表の形が整っていても抜けていることがあります(設計レビューの見落としをAIに洗わせる|プロンプトの型)。
状態遷移の抜けを塞ぐチェックリスト(コピーして使えます)
全部に○が要るわけではありません。気になった行だけで十分です。
① 並べる
- 対象のデータを1種類に絞ったか
- 状態の一覧を、定数・実データ・画面の表示名で突き合わせたか
- 実データに、定義にない状態の値が入っていないか確かめたか
- 状態を動かす操作を「誰が・何をきっかけに」まで一覧にしたか
- 外部からの通知・バッチ・管理画面も、操作として数えたか
- 状態を書き換えているコードの箇所を洗い出したか
② 埋める
- 「状態×操作」の表を作ったか
- すべてのマスに「移る/何もしない/エラーにする」を書いたか
- 空欄の決定を、AIではなく業務を知っている人と決めたか
- 行き止まりの状態・たどり着けない状態がないか見たか
- 同じ操作が2回来たとき・順番が逆に来たときの扱いを書いたか
- 時間で動く遷移と人の操作が同時に来たときの優先を決めたか
③ 弾く
- 状態を変える入口を1か所に集めたか
- 更新するときに「今の状態」を条件に入れ、更新件数0を扱っているか
- 管理画面も同じ入口を通るか、例外の書き換えに理由を残しているか
- 状態が変わった記録(いつ・誰が・何で・どこからどこへ)を残しているか
- 表のマスごとのテストを書いたか
この記事のまとめ
状態遷移の抜けは、腕前の問題ではなく見る形の問題です。フロー図の1本道ではなく、「状態×操作」のマスで見ると、決めていなかった組み合わせが空欄として見えてきます。
やることは3つ。①並べる——状態と操作を、それぞれ漏れなく一覧にする。②埋める——すべてのマスに「移る/何もしない/エラーにする」を書き、空欄はAIに洗わせて人が決める。③弾く——状態を変える入口を1か所に集め、「今の状態」を条件にして更新し、決めていない遷移を止める。
AIに相談するときは、「未定のマスを推測で埋めないで」と先に一言添える。それだけで、穴が穴のまま見えるようになります。

キャンセルされた注文が発送されていたと知ったとき、まず自分の書いた処理を疑ったと思います。実際には、どの処理も指示どおりに動いていて、決められていないマスが1つあっただけでした。
これは、気をつけて防ぐ種類のものではありません。表にして見える形にする種類のものです。そして表は、状態を一覧にするところから作れます。
今日は、あなたのシステムで状態が変わるデータをひとつ選んで、状態の名前を縦に並べてみる。それだけで、次の「どうしてこうなったんですか」に、落ち着いて答えられる日が近づきます。
よければ、こちらも
- 同時更新の止め方はAIにトランザクション境界と排他制御を相談する|整合性の詰め方
- 同じ操作が2回来たときはAIに冪等性の設計を相談する|リトライで二重実行させない型
- 外部からの通知の扱いはAIに外部API連携の設計を相談する|タイムアウトと障害の詰め方
- 図に起こすならAIにシーケンス図・構成図を書かせる|PlantUMLで起こす型
- 表をテストにするならAIにテストケースを洗い出させる|観点の与え方
- 異常系の洗い出しは境界値・異常系をAIに考えさせて漏れを減らす手順
- 見落としを洗うなら設計レビューの見落としをAIに洗わせる|プロンプトの型