じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

AI時代のソフトウェア要求 - Software Requirements Essentialsの読書感想文

はじめに

私たちは、明確なゴールを決めずにコードを書き始めていた。

書いて、動かして、また直した。

AIエージェントを使う今、それは過去の話ではない。

コードから考え始めることには、もっともな理由があります。動くものがあれば話は具体的になり、間違いにも早く気づけます。AIエージェントは、このやり方をさらにやりやすくしました。曖昧な依頼でも何かを返し、修正を頼めば次の案を出します。

その一方で、完成したコードから「なぜ作るのか」「どの損失を避けるのか」「誰が判断するのか」を復元する仕事は残ります。生成が速いほど、曖昧さも速く実装へ広がります。要求工学は、作る機能だけでなく、作る理由、判断する人、受け入れられる結果、変更時に見直す範囲を扱います。コードを書く速度では埋まらない部分を考えるため、その本へ戻りました。

まず『ソフトウェア要求 第3版』で要求工学の全体を見る

最初に置きたいのは、Karl WiegersとJoy Beattyによる『Software Requirements, 3rd Edition』です。要求を集めて文書にする方法だけでなく、要求の開発と管理を、プロジェクトの開始から変更まで一続きで扱います。現在の実装判断がどの論点に属するのかを探す時、この広さが役立ちます。

原書は2013年刊の672ページ、32章です。要求開発に加えて、アジャイル、既存システムの機能追加と置き換え、パッケージ導入、外部委託、業務プロセス自動化、分析系システム、組み込み・リアルタイムシステムといったプロジェクト別の章があります。後半では、変更管理、追跡、要求管理ツール、要求プロセスの改善、リスク管理まで扱います。

www.microsoftpressstore.com

日本語版『ソフトウェア要求 第3版』は、渡部洋子訳、宗雅彦監修で2014年に日経BPから刊行されています。

ci.nii.ac.jp

第3版の価値は、成果物のひな型を網羅していることだけではありません。同じ要求活動でも、新規開発、既存システムの置き換え、外部委託、組み込みシステムでは、先に確かめるリスクが違うと示します。データ要求なら発生源、型、一意性、保持、移行を確認し、レビューなら要求を別の表現へ変換して食い違いを探します。要求という一語の下に隠れていた作業を、実際に質問できる粒度まで下ろす本です。

第3版の広さを、そのまま1つの開発手順として適用すると重すぎます。新規開発と既存システムの置き換えでは、最初に潰す不確実性が違うからです。全章を順番に実行するのではなく、今の変更がどの失敗を持ち込み、その失敗を扱う章がどこにあるかを探します。網羅性は作業を増やす理由ではなく、見落とした論点へ戻るために使います。

2013年の本なので、紹介されるツールや当時のアジャイル開発をめぐる言い回しには時代を感じる箇所があります。しかし、要求の出所を辿り、例外を正常系と同じ時期に考える判断は古くなっていません。異なるモデルの照合と、変更の波及先を調べる作業も同じです。AIエージェントがコードとテストを同時に生成する今は、要求、期待値、仮定を誰が確認するかという形で読み直せます。

私は、日本語版を最初から順番に消化する読み方は選びません。まず目次と全体構造から、今の変更がデータ、品質属性、変更管理、プロジェクト固有の論点のどこに属するかを探します。品質属性とは、性能や安全性、変更しやすさのように、機能がどの程度うまく働くべきかを示す条件です。送金の入力形式を変えるならデータ要求、複数インスタンス化するなら品質属性とリアルタイムシステム、既存サービスを置き換えるなら機能追加・置き換えプロジェクトの章へ戻ります。読み終えることではなく、次の判断を具体化するために使います。

その全体像を20項目へ絞った『Software Requirements Essentials』

Karl WiegersとCandase Hokansonによる『Software Requirements Essentials』は、第3版が扱う広い要求工学を背景にした本です。そこから中核となる20のプラクティスを取り出しています。特定の開発手法を導入する本ではありません。問題、目標、範囲、利用場面、データ、品質、変更という順に、ソフトウェアを作る前後で確認する仕事を並べています。

www.pearson.com

『Essentials』は、第3版を置き換える改訂版ではありません。20項目を入口として使い、詳しい質問、成果物、例外、失敗例、プロジェクトごとの差が必要になったら第3版へ戻る関係です。企画資料でも、小さい本で中核となる道具を素早く掴み、大きい本で詳細、例、判断材料を得る構成が意図されています。

https://www.processimpact.com/files/Proposal%20for%20Software%20Requirements%20Essentials.pdfwww.processimpact.com

20項目を単独で読むと、どれも当たり前に見えます。問題を理解する。判断者を決める。用語を揃える。変更を管理する。それでも実案件では、納期が近づくほど、この当たり前が実装の外へ押し出されます。本書の価値は意外な技法ではなく、最初の問題設定から最後の変更管理まで、途中で省くと何が壊れるかを順序として見せることにあります。

読んで最も強く残ったのは、20項目の多さではなく順序でした。解決策より先に問題を置き、実装より先に成功の観測方法と判断者を置く。この順序があると、AIエージェントへ渡す指示は機能一覧から判断材料へ変わります。要求工学を分厚い文書の作成とみなすより、何を人が決め、何を機械で止めるかを配置する仕事と考える方が、現在の開発に合います。

20項目は、第3版の各論へ入る索引としても読めます。#1から#5はビジネス要求と判断者、#6から#9は利用場面、イベント、データ、品質属性を扱います。#10から#18は分析、モデル、試作、優先順位、記述、整理、用語、レビューへ進みます。#19と#20は変更管理と追跡へ続きます。

著者本人の説明を短く聞くなら、Karl WiegersのYouTubeチャンネルも勧めます。更新は止まっていますが、「The One-Minute Analyst」として20のプラクティスを#1から#20まで約1分ずつ説明した動画が揃っています。書籍の代わりになる深さはありません。それでも、各項目で著者が最初に置く問いと強調点を掴んでから本文へ戻る入口として使えます。

www.youtube.com

要望、要求、要件を同じものとして扱わない

『Software Requirements Essentials』という題名の中心は、EssentialsだけでなくRequirementsにもあります。本書は20項目へ進む前に、要求という言葉を定義します。システムが示す振る舞い、備える性質、開発上の制約だけでは足りず、その起点にステークホルダーのニーズを置きます。要求を機能一覧に狭めると、なぜ作るのかと、誰の何を満たすのかが抜けるからです。

本書が要求と呼ぶのは、1種類の文章ではありません。プロジェクトへ取り組む理由を示すビジネス要求、ユーザーが達成したいタスクを示すユーザー要求、それらを満たすためにプロダクトが備える能力や特性を示すソリューション要求があります。さらに、機能、品質属性、データ、外部との接続、ビジネスルール、制約も含みます。現場でフィーチャー、ユースケース、ユーザーストーリーのどれを使っていても、正しいソリューションを作るために必要な知識の総体を、本書は要求として扱います。

要望は要求の起源であって、そのまま実装項目ではない

要望は軽く扱ってよい情報ではありません。現在の不満、期待する結果、欲しい機能、避けたい損失が最初に言葉になったものです。ただし、そこには解決したい問題と、思いついた解決策が混ざっています。「再送ボタンが欲しい」という要望だけでは、利用者が同じ送金をもう一度実行したいのか、応答を受け取れなかった送金の結果を安全に確かめたいのかが分かりません。

前者として実装すれば、ボタンを押すたびに新しい送金を作るかもしれません。後者なら必要なのは、同じ依頼を識別し、すでに反映済みなら残高を変えずに結果を返すことです。表面上は同じボタンでも、必要な状態、失敗時の振る舞い、保持すべきデータが変わります。要望を聞いた直後に解決策へ変換すると、この分岐を決めないままコードへ埋め込んでしまいます。

本書のプラクティス#6が、「何が欲しいか」より「そのソリューションを使って何をする必要があるか」を重く見るのはこのためです。要望を退けるのではありません。要望を、利用者のタスク、起きている問題、期待する結果へ戻してから、必要な機能を導きます。採用しなかった要望にも、見落としていたニーズや運用上の痛みが残ることがあります。

要望は実装の入口ではなく、判断の入口です。

要求は、要望を詳しく書き直したものではない

要求は、要望の文章を丁寧にして強制力を持たせたものではありません。出所と理由を確かめ、他のステークホルダーの利害と照合し、対象範囲、例外、優先度、実現可能性、受け入れ基準を判断した結果です。すべての要望を要求として採用するわけではなく、1つの要望から複数の要求が導かれることもあります。

送金の例なら、「結果が分からない時に安全に再送したい」というユーザー要求の背後に、二重反映による損失と問い合わせを減らすビジネス上の理由があります。そこから、同じ要求IDと同じ内容なら残高を変えずにReplayedを返す、同じIDで内容が違えば拒否する、というソリューション要求を導きます。さらに、要求IDをどこへ、どれだけの期間保持するかというデータ要求が生まれます。再起動後や複数インスタンス間でも守るなら、メモリ内の実装では足りません。

ここで重要なのは、抽象的なニーズから具体的な機能へ一方向に降りれば終わるわけではないことです。具体化した結果が本当に元のニーズを満たすかを確かめ、実現費用や新しい危険が分かれば、範囲や優先度を戻して考えます。本書が要求開発を、引き出し、分析、仕様化、妥当性確認の反復として扱う理由です。

要件という言葉は、指している粒度を決めてから使う

日本語の現場では、requirementを「要求」と呼ぶ場合も「要件」と呼ぶ場合もあります。本記事では本書に合わせ、ビジネス上の理由から実装条件までを含む総称に「要求」を使います。一方、実装や受け入れの判断に使えるところまで絞った条件を「要件」と呼ぶ現場もあります。この使い分けは便利ですが、どこでも通用する固定的な段階ではありません。

要望、要求、要件を三つの箱へきれいに仕分けること自体が目的ではありません。「再送できるようにしてほしい」という文が、利用者から届いた未分析の要望なのか、採用済みのユーザー要求なのか、Replayedを返す実装条件なのかを見分けられることが重要です。同じ一文でも状態が違えば、AIエージェントが変更してよい範囲と、人へ確認すべき内容が変わります。

「要件定義済み」というラベルも、それだけでは判断材料になりません。誰のニーズに由来し、なぜ採用され、何をもって満たしたと判断し、どの前提が変われば見直すのかまで辿れて初めて、実装へ使えます。呼び方を揃えるのは、未判断の要望を決定済みの条件に見せないためです。

AI時代には、要望が要件の顔をして実装される

AIエージェントは、「再送ボタンが欲しい」という一文から、画面、API、状態更新、テストまで一度に作れます。足りない受け入れ基準やエラー処理も補い、成果物同士を整合させられます。これは速さであると同時に、要望から要求を導く途中の判断を、もっともらしい推測で閉じられるということです。

プロンプトには、要望、要求、実装方法、制約が混在します。詳しく書かれたプロンプトが要求とは限らず、短い利用者の発言が要求の重要な起源になる場合もあります。プロンプトはAIへ情報を渡す形式を示しますが、その情報が誰の判断を経て、どの状態にあるかまでは示しません。

AI時代に危険なのは要望が曖昧なことより、判断を経ていない要望が、完成した要件の顔で実装されることです。

AI時代の要求は、人間の意図と自律的な実行のあいだに境界を引く

要望から要求を導き、実装へ使える条件まで具体化する仕事は、AI以前から変わりません。AIエージェントによって変わるのは文面ではなく、そこで置いた判断が、人の再解釈を介さずどこまで機械の実行へ移されるかです。要求とは、人間の意図を漏れなく書き下す文書ではなく、機械が進めてよい範囲と、進めずに人へ戻す範囲を分ける営みだと私は考えます。

人間の意図には、最初から決め切れない部分があります。関係者の利害は揃わず、将来の環境も分からず、起きてほしくない事象をすべて列挙できません。要求作業によって、この曖昧さが消えるわけではありません。その曖昧さを、決めたこと、まだ決めていないこと、誰が決めることかへ分けます。未決の判断を実装済みの事実として扱わないためです。

要求作業は曖昧さを消す仕事ではなく、曖昧さの持ち主を明らかにする仕事です。

変わらないのは、意味を人間の判断へ接地する仕事

AIエージェントは、利用履歴から目標を推測し、もっともらしい受け入れ条件を提案できます。それでも、その推測を採用してよい理由までは作れません。誰の損失を避けるのか、対立する価値のどちらを優先するのか、残った危険を誰が引き受けるのかは、成果物の整合性からは導けないからです。

口座間送金で「不正を減らす」と書いても、正当な送金を止める誤検知と、不正な送金を通す見逃しのどちらを重く見るかは決まりません。AIは過去の傾向から閾値を提案できますが、その閾値で事業を続ける判断まで正当化できるわけではありません。要求の意味は文章の詳しさではなく、判断する人と、その根拠へ接続されて初めて定まります。

型、テスト、形式仕様は、決めた規則から実装が外れた時に止められます。ここで人間の仕事が消えるのではありません。「実装は意図に合っているか」という大きな問いが、「この短い仕様は意図を表しているか」という問いへ圧縮されます。要求工学に残るのは、この圧縮された問いを現実へ接地する仕事です。

変わったのは、要求が説明から実行の入力になったこと

以前の要求文と実装のあいだには、人間の解釈がありました。曖昧な箇所があれば実装者が立ち止まり、質問し、都合の悪い条件に気づく余地がありました。もちろん、人間も誤解します。それでも、要求が実装へ届く途中には、読み直される摩擦がありました。

今は、1つの仕様から計画、コード、テスト、説明文まで続けて生成できます。要求が実行へ近づいたことで、意図を反映する速度は上がりました。同時に、最初の誤解が複数の成果物へ一貫して広がる速度も上がりました。コードとテストが同じ誤解から作られれば、両者はきれいに一致し、すべてが緑のまま間違った場所へ到達できます。

ここで要求する内容は、望ましい出力だけでは足りません。AIエージェントが読めるデータ、変更できる範囲、呼び出せる外部機能、費やしてよい時間や費用も対象になります。出力の条件が「何を得たいか」を示すのに対し、委任の境界は「そのために何をしてよいか」を示します。自律性を与えるほど、この2つを分けて書く必要があります。

AI時代の要求は、AIへ正解を教える文章ではなく、AIが正解を決めてよい範囲を決める文章です。

完了条件だけでなく、介入条件を要求する

人間の実装者なら、情報が足りなければ質問するだろうと期待できます。AIエージェントは、情報不足を仮の値で埋め、検査が通るまで変更を続けることもできます。この能力は探索では役立ちますが、推測が権限を伴う操作へ届く場面では危険になります。

そこで、何をもって完了とするかに加えて、どの状態なら処理を中断するかを決めます。判断に必要な情報が欠けた時、複数の評価結果が食い違った時、許可された範囲を越える変更が必要になった時、費用や試行回数が上限へ達した時に、誰へ何を添えて戻すかまで含めます。分からないこと自体ではなく、分からないまま進めたことを失敗として扱います。

要求は、生成を始める文章であると同時に、生成を止める文章でもあります。AI時代には、完成像を詳しく描くことと同じくらい、どこで人間が介入するかを設計する必要があります。停止条件は外側から加える安全装置ではなく、要求そのものです。

実行できることと、人間が問い直せることを両立させる

要求を構造化し、IDを付け、受け入れ条件を機械で評価できるようにすれば、AIエージェントは作業を進めやすくなります。しかし、機械にとって実行しやすい仕様が、人間にとって読みやすいとは限りません。生成されたコード、テスト、説明が増え続ければ、レビュー担当者は判断の根拠を復元できないまま、整った成果物へ同意するだけになります。

ここで不足するのは、さらに強い生成器ではありません。一定時間の中で、別の人が要求から変更理由、証拠、残った前提まで辿り、異議を唱えられる能力です。私はこれをレビューの速さだけではなく、チームが持つレビュー帯域として捉えます。生成量がこの帯域を超えた時、レビュー待ちが増えるとは限りません。確認できない成果物を「おそらく正しい」と受け入れることで、待ち時間だけが消える場合もあります。

AIが速くしたのは実装だけではありません。理解していない成果物を、それでも承認できてしまう速度も上げました。

後で扱う「要求IDから公開APIと証拠まで15分以内に辿れる」という目標は、このレビュー帯域を測るために置きます。コード量やテスト数を数えるのではなく、初見のレビュー担当者が判断理由を再構成できるかを実際に測ります。実行可能であることと、問い直し可能であることの両方を満たして初めて、要求を機械へ近づけた利得が残ります。

要求を、一度書く文書から観測で更新する制御面へ変える

仕様が生成器を動かせても、それだけでは開発が望ましい状態へ近づいているか分かりません。送金処理なら、すべてのテストが通っても、本番の二重反映や誤った宛先への送金が減ったとは限りません。必要なのは、望ましい状態と現在の状態の差を観測し、その結果を次の変更へ戻す流れです。私は以前、この構造をループエンジニアリングとして考えました。

syu-m-5151.hatenablog.com

この見方では、要求はループの開始時に置く目標だけではありません。受け入れ条件は差を測る物差しになり、変更管理は観測結果を次の判断へ戻す経路になります。判断者と対象範囲は、いつ続け、いつ止め、いつ目標そのものを見直すかを決めます。ここでいう制御面とは、目標、観測方法、介入条件、更新する人を1つにつなぐ層です。

ただし、ループが回ることと、正しい場所へ近づくことは別です。コードとテストが同じ誤解から生成されれば、検査結果は緑のまま、意図とは違う実装へ近づき続けます。AIへ完了判定まで任せるなら、評価基準の欠落も反復されます。さらに、評価方法の決定者、停止に使う証拠、目標を見直す変化も決めます。この条件群はループの外から与える付属情報ではなく、ループの結果を意味あるものにする成立条件です。

同じAIでも、生成工程と提供機能では要求が違う

Rustで送金処理を実装するなら、AIをどこで使うかも分けて考えます。AIエージェントにコードを生成させるだけなら、提供する送金機能の中にAIは入りません。一方、送金処理そのものへ不正検知モデルを組み込むなら、要求する内容が変わります。AIという言葉だけで扱えば、生成工程の問題と提供機能の問題が混ざります。

不正検知モデルを加えるなら、「不正な送金を検出する」だけでは判断できません。見逃しと誤検知のどちらに大きな損失があるのか。判断の閾値を誰が変えるのか。担当者が判定を覆せるのか。入力の傾向が変わった時に、誰が止めて見直すのか。出力を一意に決められなくても、評価条件と許容する誤りは決められます。運用中に何を観測し、どの変化を介入のきっかけにするかも要求です。

AIエージェントをコード生成にだけ使うなら、不正検知モデルの論点は持ち込みません。実行時の構成要素ではないものに、モデルの性能低下や判定の公平性を要求しても判断には使えないからです。AI時代という理由だけで、あらゆるAI固有の論点を加える必要はありません。対象外を決めずに項目を増やせば、詳しさと引き換えに、何の判断へ使う要求なのかが見えなくなります。

私にとって、AI時代の要求とは、解く問題、受け入れる結果、判断する人、委任する範囲、確認する証拠、介入する条件を結んだものです。文章の長さや形式ではなく、次の変更が前提から外れた時に止められるかで確かめます。仕様が実装を直接動かせるようになったからこそ、仕様そのものも、検証結果の意味を決めるレビュー対象になります。

口座間送金では、未認可の操作、失敗時に片方の残高だけが変わる不整合、再送による二重反映を同時に扱います。Rustで実装するなら、20項目をこの題材へ当て、要求を型やテストへどう移すか、機械で確認した後もどの判断が人へ残るかを見ます。すべての機能へ全項目と全検証技法を適用するわけではありません。

機能の要求はFR、品質属性はQR、業務上の規則はBR、検証が頼る前提はAというIDで表します。たとえばFR-3は3番目の機能要求、A-1は1番目の前提です。IDは重要度を示す番号ではありません。要求からコードと証拠まで辿るための目印です。

20項目を辿ると、型やテストで確認する範囲と、確認のために置いた前提が分かれていきます。AI時代の要求工学では、人間が決めたことを要求IDで識別し、実装が外れた時に止める証拠を用意します。その過程で、機械が確認していない前提も見えてきます。

最初の5項目で、コードを書く順番が変わった

本書の最初の5項目は、要求文の書き方へ進みません。問題、ビジネス目標、対象範囲、ステークホルダー、判断者を先に扱います。私は以前、要求を実装する機能の一覧として読みがちでした。この5項目をRustの送金処理へ当てるなら、実装案より先に、避ける損失と判断の持ち主を決める必要があります。

#1 解決策へ収束する前に、問題を理解する

Rustで実装するなら、型、Rust向けモデル検査器のKani、予想外の入力を自動生成するファジングを足せば送金処理を固められると考えがちです。しかし、なぜ検証を増やすのかを繰り返し掘ると、レビュー担当者が関数本体から判断理由を毎回復元する問題に行き着きます。そこで、改善したい開発上の問題を「要求から証拠までを辿れず、実装から判断理由を復元している」と置きます。

github.com

ただし、これは送金処理そのものが防ぐべき問題ではありません。開発工程には追跡できない問題があり、送金機能には避けたい損失があります。この2つを混ぜると、追跡しやすいコードを作っただけで、送金の危険も減らしたように見えてしまいます。

送金で避けたいのは、未認可の操作、失敗時に片方の残高だけが変わる不整合、通信再試行による二重反映です。「Rustで送金APIを作る」「Kaniで証明する」は、これらへ対処する解決策の候補にすぎません。確認するのは技法の強さではなく、開発上の問題と送金上の損失のどちらに効き、何を扱わないかです。問題を「安全な送金処理がない」とだけ置くと、何をもって安全とするかが未定のまま、採用した道具が扱いやすい危険だけを選ぶことになります。

問題を掘るときは、「この案が答えだとすれば、元の問いは何か」と逆算します。二重反映への対策として要求IDを導入するなら、問いは「同じ業務上の依頼をどう識別するか」です。ここで要求IDをクライアントが自由に再利用できる設計にすると、今度は別内容の送金を同一依頼とみなす問題が生まれます。対策を置くたびに、何を防ぎ、どの新しい失敗を持ち込むかを見ます。

根本原因を1つに決めつけないことも重要です。二重反映は、通信再試行、処理結果の消失、DBへの書き込み順、クライアント側のタイムアウト設計など、複数の原因から起こり得ます。完了一覧をメモリに置くなら、扱えるのは一部だけです。「二重反映を解決する」ではなく、「同一プロセスが稼働している間の同一要求ID再送を扱う」と書く方が正確です。

変更を読むときは、その変更が未認可送金、部分更新、二重反映のどれを減らすのかを確かめます。どれにも結び付かない機能は、まだ言語化されていない問題を示すか、不要な実装かのどちらかです。反対に、問題として挙げた3つの失敗のどれにも証拠がないなら、実装量が増えていても問題は解けていません。問題文を、追加と削除の判断基準として使います。

#2 ビジネス目標を定義する

Rustで実装するなら、未認可送金、失敗時の一部更新、二重反映をリリース前に検出することを目標にします。もう1つは、要求IDから公開APIと証拠まで15分以内に辿れることです。前者は自動テストで確認できますが、後者は代表レビュー担当者に実際の変更を読んでもらう必要があります。測り方が違う目標を、同じ品質という言葉でまとめません。

目標を書いたら、作業がその達成に寄与するかも確認します。たとえばファジングの実行時間を延ばしても、要求IDから証拠まで辿る時間は短くなりません。成功指標を分けると、技法を増やすこと自体が目的になるのを防げます。

目標には、基準値、期限、測定方法を含めます。「レビューを速くする」では、改善したか判断できません。要求から証拠まで辿る基準値が未測定なら、最初に代表レビュー担当者へ未見の変更を読んでもらいます。開始から要求、公開API、対応テスト、残る前提を説明できるまでの時間を計ります。15分はその試行に対する目標であり、測定前から達成済みとは書きません。

絶対値と相対値も区別します。「従来より50%速い」は、従来値が分からなければ評価できません。「未認可送金を0件にする」も、本番で未検出の件数を直接測れないなら、そのまま成功指標にはできません。Rustで実装するなら、既知の未認可経路を受け入れテストで拒否し、公開APIから認可を飛ばした実行をコンパイル不能にすることを代理指標にします。代理指標が目的そのものではないことは残します。

直接測れる指標と、代理指標がずれる場面も確認します。検証手段を追加しても、意図した危険を捕捉できるとは限りません。コードカバレッジが高くても、期待値が誤っていれば要求は満たせません。指標をレビューするときは、数値が上がるだけで本来の損失が減らない実装を作れないかを考えます。

また、部分的な解決と最終的な解決に別の期限を置きます。メモリ上の完了一覧を使うなら、単一プロセス内の再送制御は先に確認できます。一方、再起動後や複数インスタンス間の二重反映には、DBを含む設計が必要です。前者の達成を後者の達成として報告しないため、目標には対象範囲も含めます。

#3 ソリューションの境界を定義する

Rustで送金処理を実装するなら、入力解析、認可、残高更新、再送制御を対象にします。本人確認、認証トークン、永続DB、分散トランザクション、HTTP、監査ログは含めません。ActorIdを比較しただけで、認証まで実装したと扱わないためです。

対象外は、後で忘れてよい項目ではありません。HTTPをつないだら入力上限とエラー応答を再確認し、DBをつないだら原子性と再送制御を見直します。外部の認証基盤や永続層との接点を明示し、どの変更を境に現在の設計判断が使えなくなるかを残します。

境界は、単なる「やらないこと一覧」ではなく、責任とデータの受け渡しを決めます。入力側からは、認証済みとされるActorIdと未信頼の送金バイト列を受け取ります。出力側へは、分類されたエラーかTransferReceiptを返します。外部の認証基盤がどの識別子を発行し、誰がActorIdへ変換し、失効をどう伝えるかは本実装の外です。この接点が曖昧なら、内部の所有者比較が正しくても、別人の識別子を渡されて未認可送金が成立します。

コンテキスト図を文章で置き換えるなら、「利用者または呼び出し元 → 入力解析 → 台帳 → 応答」と、その周囲に認証基盤、永続層、監査基盤があります。ここで見るのは矢印の名前です。送金要求だけでなく、認証結果、時刻、失効通知、処理結果、監査情報のどれが境界を越えるのかを挙げます。名前を付けられない矢印は、後から暗黙の依存になりやすい箇所です。

さらに、直接接続しない関係者まで含む広い環境も見ます。SREは完了一覧の増加を監視し、問い合わせ担当者は失敗理由を利用者へ説明し、会計側は残高変化の追跡を必要とします。コードから見えない業務変更を含めないと、ソフトウェアだけ成功しても運用全体で失敗します。

境界を見直すきっかけを具体化します。HTTP層の追加、DB永続化、複数プロセス化、非同期キューの導入、要求IDの有効期限追加、監査ログ出力の追加が該当します。このいずれかが入った変更では、現在のテストが緑でも、A-1からA-4の前提とFR-3、FR-4を再評価します。境界が変われば、同じコードでも保証できる範囲が変わるからです。

#4 ステークホルダーを特定し、特徴づける

Rustで送金処理を実装するなら、送金利用者に加えて、プロダクト責任者、セキュリティ責任者、SRE、開発者、テスト担当者、レビュー担当者を挙げます。利用者は二重引き落とし、SREは性能と復旧、セキュリティ責任者は改ざんと資源枯渇を見ます。同じ送金処理でも確認する失敗が違うため、利用者という一語で済ませません。

役割名を並べた後は、誰が要求、制約、依存関係、リスクを提示できるかまで確認します。代表者に十分な知識があっても、優先順位を決める権限まで持つとは限りません。意見を聞く相手と、判断を下す相手を分けておかないと、レビューで同じ論点が繰り返し差し戻されます。

ステークホルダーは、利用者だけではありません。処理に入力を渡す認証基盤の担当者、結果を保存するDBの担当者、障害時に復旧する当番、規制や社内方針を確認する担当者も含みます。直接操作しなくても対象になる立場があります。データを提供する人、結果を受け取る人、変更の影響を負う人、実装を止められる人です。エンドユーザーへの聞き取りだけでは、保持期間や監査、復旧の要求は出てこないことがあります。

利用者も一種類にまとめません。送金を始める利用者、問い合わせを受ける担当者、不正利用を調べる担当者では、頻度、権限、失敗時に必要な情報が違います。送金利用者には「安全に再試行できる」が重要でも、問い合わせ担当者には「同一再送とID衝突を区別できる」が重要です。ある利用者クラスの都合だけでエラーを一種類に潰すと、別の担当者が調査できなくなります。

代表者を選ぶときは、肩書だけでなく、知識、利用可能な時間、現場からの信頼、判断に必要な権限を確認します。詳しいがレビュー時間を取れない人を唯一の窓口にすると、要求が止まります。逆に、会議へ出られても実際の例外処理を知らない人だけでは、正常系に偏ります。必要なら、現場の説明者と最終判断者を別に置きます。

レビューでは、各要求で3つの立場を辿ります。情報を出した人、影響を受ける人、確認する人です。誰にも結び付かない要求は、価値の根拠が弱いと判断します。誰も確認しない対象外項目は、境界の外へ捨てたのではなく、単なる担当漏れです。ステークホルダー分析は名簿作りではなく、見落としの発生源を探す作業です。

#5 権限を持つ意思決定者を特定する

業務ルールと優先順位はプロダクト責任者、受け入れるセキュリティリスクはセキュリティ責任者、検証方法は技術責任者が決めます。実案件では役割名に加え、個人名と判断日も記録します。AIエージェントは選択肢と矛盾を出せますが、この判断を引き受ける立場にはありません。

決定者だけでなく、決め方と決定後の伝達も必要です。送金上限の変更を多数決で決めるのか、プロダクト責任者が決めるのか。品質ゲートを外す例外を誰が止められるのか。決定理由を要求IDへ残し、影響を受ける人へ伝えるところまでを扱います。

先に決めるべき論点は、衝突する要求、優先順位、品質属性のトレードオフ、リリース範囲、変更の採否です。たとえば再送履歴を長く保持すると二重反映を防ぎやすくなりますが、メモリ消費は増えます。どちらを優先するかは、実装担当者がコードを書きながら暗黙に決めてよい話ではありません。保持期間と許容メモリを定める人を特定します。

「チームで決める」と書く時は、そのチームの結論を覆せる人がいるかを確認します。後からプロダクト責任者が単独で覆せるなら、チームは助言者であり、最終判断者ではありません。この違いを曖昧にすると、合意したはずの要求が実装後に差し戻されます。現実の権限の流れを記録します。

決め方も衝突が起きる前に選びます。全員一致、異論を残した合意、多数決、責任者判断、専門家への委任では、決定に必要な時間と受け入れられる異論が違います。セキュリティ上の例外は多数決に向かず、性能目標の測定方法は技術責任者への委任が向きます。論点ごとに使い分けます。

決定記録には、採用した案、日付、判断者、理由を残します。退けた案と見直す条件も必要です。たとえば完了一覧をメモリに置くなら、単一プロセスだけを対象とする理由を記し、DB接続時に見直します。理由がなければ、次の変更者は制約なのか偶然なのかを判別できません。AIエージェントへ渡す入力としても、結論だけより見直し条件まである方が安全です。

次の4項目で、会話をイベントと観測値へ変える

要求の聞き取りという言葉からは、利用者の要望を集める会議を想像します。本書の#6から#9が求めるのは、要望の一覧ではありません。利用者が達成したいことを、イベント、応答、データ、測定可能な品質属性へ変換します。Rustで送金処理を実装するなら、正常系だけでなく、再送、要求IDの衝突、入力上限、処理時間まで同じ要求集合へ入れます。

#6 ユーザーがソリューションで何を行う必要があるかを理解する

「送金できる」だけでは足りません。通信失敗後に安全に再送し、失敗理由を区別し、資金が動いていないことを確認するところまでが利用者の仕事です。正常系の動詞だけを拾うと、二重反映や失敗時の一部更新が要求から落ちます。

通常の流れに加えて、代替経路と例外を先に挙げます。残高不足、存在しない口座、別主体による操作、同じ要求の再送、同じIDを使った別内容の送信です。コードレビューでは正常系の行数より、各例外が分類されたエラーを返し、台帳を変更しないことを確認します。

質問も「どんな機能が欲しいですか」ではなく、「この処理を使って何を完了したいですか」から始めます。利用者の目標は送金ボタンを押すことではなく、指定した相手へ一度だけ資金を移し、結果が分からないときにも安全に確認または再試行することです。機能名だけを聞くと、送信画面は作れても、タイムアウト後の行動や失敗の説明が抜けます。

通常の流れでは、入力を受け、認可し、残高を確認して両口座へ反映した後に結果を返します。代替の成功経路では、通信失敗後に同一要求を再送したとき、残高を変えずに既存結果を返します。例外経路は、入力を作れない状態と送金元を操作できない状態を含みます。残高計算が成立しない状態や、要求IDの意味が衝突する状態もあります。成功と失敗を分けるだけでは、安全な再送という別の成功経路を表せません。

利用者クラスごとに目標も違います。一般利用者は自分の送金結果を知りたい。問い合わせ担当者はAppliedReplayedを区別し、二重反映されていないと説明したい。不正調査の担当者はUnauthorizedSourceRequestIdConflictを追いたい。公開APIが単なるResult<(), Error>なら、処理側では十分でも、後続の業務で必要な区別を失う可能性があります。

ユーザーストーリーやユースケースは、短ければよいわけではありません。実装とテストに必要なだけ、きっかけ、事前条件、成功後の状態、代替経路、例外を補います。一方、画面の色やボタン位置のような設計案を早く固定しません。Rustのライブラリとして実装するなら、UIを決めずに公開APIと観測結果を定め、利用者の目標と実装方法を切り分けます。

レビューでは、各ユーザー目標から必要な機能が導けるか、逆に各公開機能がどの目標へ寄与するかを往復します。どの目標にも結び付かない機能は削除候補です。機能があるのに目標を完了できない箇所は要求の欠落です。この往復は、AIエージェントがもっともらしい補助機能を追加したときのスコープ逸脱も見つけやすくします。

#7 イベントと応答を特定する

Rustで送金処理を実装するなら、初回実行、同一内容の再送、要求IDを使った別内容の送信、不正入力を分けます。初回はApplied、同一再送はReplayed、IDだけ同じならRequestIdConflictです。戻り値だけでなく、各イベントがどの状態を変え、どの状態を変えないかまで書きます。

書籍ではイベントをビジネス、信号、時間の3種類から探します。入力受信と再送を実装しても、完了済み要求IDをいつ失効させるかという時間イベントが残ります。この観点を加えると、メモリ上の完了一覧が増え続ける問題と、失効後の再送をどう扱うかが未決だと分かります。

ビジネスイベントは、呼び出し元が送金を依頼する、結果が不明なため再送する、といった外部からのサービス要求です。信号イベントは、DB書き込みの完了通知、キューからのメッセージ、認証失効通知のように、別システムや装置から届きます。時間イベントは、要求IDの有効期限、処理タイムアウト、日次の照合です。対話型APIだけを見ていると、信号と時間のイベントが要求から落ちやすくなります。

各イベントには、現在状態と応答を組み合わせます。同じ送金要求でも、未処理ならApplied、同一内容を処理済みならReplayed、同じIDで別内容を処理済みならRequestIdConflictです。「送金要求を受けたら実行する」という一文では、処理済み状態による違いを表せません。イベント名だけでなく、事前状態、応答、次状態、変更してはいけない状態を記録します。

実装上は、completedの照合が状態とイベントの交点です。

if let Some(completed) = self.ledger.completed.get(&request_id) {
    return if *completed == self.transfer {
        Ok(TransferReceipt {
            request_id,
            disposition: Disposition::Replayed,
        })
    } else {
        Err(TransferError::RequestIdConflict)
    };
}

この分岐を読むときは、戻り値だけでなく、どちらの経路にも残高更新とcompletedへの追加がないことを確認します。イベント応答をコードへ対応させると、「応答なし」が意図した無視なのか、実装漏れなのかも議論できます。

時間イベントを加えると、新しい判断が必要になります。完了一覧から要求IDを削除した直後に再送されたら、新規送金として再反映するのか、期限切れとして拒否するのか。処理途中でタイムアウトしたら、呼び出し元へ何を返すのか。時刻を持たない実装では、この問いへ答えられません。要求IDの期限を扱わないことを明記し、時刻を導入する変更時の確認項目にします。

要求IDの失効を対象外にした場合も、想定イベントの一覧と状態遷移を並べると、その欠落を意図して残したのか見分けられます。片方にしかない項目、到達できない状態、出口のない状態、どのイベントからも生じない応答は、要求の誤りを示すことがあります。テストはイベントと状態の組み合わせから導き、正常な一経路だけで網羅したとは扱いません。

#8 データの概念と関係を評価する

Rustで実装するなら、ActorIdAccountIdRequestIdは、内部表現が同じu64でも交換できない型にします。送金額は0を許さず、口座残高は0を許します。1つの要求IDは、1つの送金内容にだけ結びつきます。コードでは非公開フィールドと生成関数で表します。

pub struct ActorId(NonZeroU64);
pub struct AccountId(NonZeroU64);
pub struct RequestId(NonZeroU64);
pub struct TransferAmount(NonZeroU64);

impl TransferAmount {
    pub const fn new(value: u64) -> Option<Self> {
        match NonZeroU64::new(value) {
            Some(value) if value.get() <= MAX_TRANSFER_AMOUNT => Some(Self(value)),
            Some(_) | None => None,
        }
    }
}

同じ整数型を使って関数名や変数名だけで区別すると、AIエージェントが引数の順序を間違えてもコンパイルできます。別の型にすれば、ActorIdAccountIdとして渡す変更はコンパイルできません。一方、型が分かれていても、その操作主体が認証済みかは分からないため、認証は上流の前提として残します。

入力解析では、値の変換と処理順を確認します。Rustで実装するなら、入力長をUTF-8変換や数値解析より先に検査します。

if input.len() > MAX_TRANSFER_INPUT_BYTES {
    return Err(ParseTransferError::InputTooLong);
}

let input = std::str::from_utf8(input).map_err(|_| ParseTransferError::NotUtf8)?;
let mut fields = input.split(',');

この順序なら、上限を超えた非UTF-8入力をInputTooLongで止められます。その後に4フィールドを各型へ変換し、ParsedTransfer::newで送金元と送金先が異なることを確認します。レビューでは正常入力だけでなく、処理量を制限する検査が解析より前にあるか、不正値を保持した状態を外部から直接作れないかを見ます。

データを見るときは型だけで終えません。入力の最大長、数値の範囲、口座間の関係、外部から受け取る値との不一致を確認します。MAX_TRANSFER_INPUT_BYTESがHTTP層の上限と一致するか、DBへ移したときに要求IDと送金内容の一意性を維持できるかは、接続先を追加した時点で再検討します。

この型設計を要求から導くには、扱うデータと関係を名詞から拾います。操作主体は複数の口座を所有でき、口座は1人の所有者と1つの残高を持ちます。送金は1つの要求ID、送金元、送金先、金額からなり、送金元と送金先は異なります。処理済み要求IDは1つの送金内容とだけ結び付きます。この関係が曖昧だと、コードではBTreeMap<RequestId, ParsedTransfer>にするのか、結果だけを保存するのかを決められません。

データごとに、作成、読み取り、更新、削除の経路も見ます。ParsedTransferは入力解析またはnewで作られ、認可と実行で読み取られますが、更新されません。口座残高は実行時だけ更新されます。完了一覧は成功時に作成され、再送時に読み取られますが、削除経路がありません。ここから、メモリ使用量と有効期限の要求が欠けていることを発見できます。

外部とのデータ不一致は、型の強さでは解決しません。HTTPの要求IDが文字列やUUIDなら、NonZeroU64へ落とす変換規則が必要です。DBの金額列が符号付き整数なら、Rustのu64と範囲が一致しません。送信元が128バイトより大きい正当なメッセージを送り得るなら、現在の上限は相互接続を壊します。双方の型、長さ、必須性、一意性、文字コード、切り捨ての有無を照合します。

データ辞書には、名前だけでなく、意味、型、範囲、単位、必須性、発生源、利用先、保持期間、機密性を置きます。たとえばamountは通貨の最小単位なのか、表示通貨は何か、小数を許すかが現在のコードだけでは分かりません。整数として正しく計算できても、単位を取り違えれば意図した送金にはなりません。型で守れる制約と、文書に残る意味を分けます。

レビューでは、モデル、入力形式、コード、保存先に現れるデータ名を対応させます。コードにあるがモデルにないcompletedは、再送制御という重要な状態です。要求にある監査時刻を保存せず、計算にも使っていないなら未実装です。複数の表現の不一致を、どれかに合わせて機械的に直すのではなく、どの意味が正しいか判断する材料にします。

#9 品質属性を引き出して評価する

品質属性は、機能が何をするかではなく、どの程度うまく働くべきかを示します。「安全」「高速」「レビューしやすい」という形容だけでは検証できないため、Rustで実装するなら実行条件と観測値を次のように書きます。

  • 任意のバイト列を解析してもpanicしない。
  • 送金成功時は、u128で見た2口座の残高合計を保存する。
  • 開発機のリリースビルドで、10万件の逐次送金を2秒以内に終える。
  • レビュー担当者が、要求IDから公開APIと証拠まで15分以内に辿れる。

10万件2秒は、本番環境で達成すべきサービス目標を示しません。同じ開発機で性能退行を見つけるための条件です。15分も達成済みの値ではありません。測定条件を書くと、その数字から言えないことも明らかになります。数字を置いた後は、環境、入力分布、未測定の範囲を一緒に確認します。

品質属性は互いに独立でもありません。完了済み要求IDを長く保持すれば再送には強くなりますが、メモリ使用量は増えます。検証を増やせば反例を見つけやすくなりますが、変更から結果が戻るまでの時間は延びます。要求には目標値だけでなく、衝突したときに誰がどちらを優先するかも必要です。

性能、可用性、堅牢性、セキュリティ、保守性では、関心を持つ人と測り方が違います。利用者は応答時間を見て、SREは復旧時間と資源上限を見て、レビュー担当者は追跡に必要な時間を見ます。「高品質」という一語では、設計判断に使えません。

測定可能にするには、刺激、実行環境、対象、期待する応答、尺度を置きます。性能なら「Apple M3上のリリースビルドで、メモリ内台帳へ10万件の逐次送金を与えたとき、2秒以内に完了する」です。負荷源、環境、対象、応答、尺度がそろいます。これを「本番で高速」と読み替えてはいけません。同時アクセス、ネットワーク、DB、末尾遅延を含まないためです。

セキュリティも「安全である」ではなく、機能へ分解します。未所有口座からの送金はUnauthorizedSourceで拒否し、拒否時の残高を維持します。128バイトを超える入力は解析前に拒否します。同じ要求IDで別内容を受け取った時も拒否します。1つの品質属性から、複数の機能要求、データ制約、テストが導かれます。

測定値には分布と失敗条件も必要です。仮にベンチマークで平均89nsと出ても、極端に遅い1件やメモリ増加を隠せます。固定件数を逐次処理するだけなら、遅い側の応答を見るp95やp99、長時間実行時の増加、複数スレッドでの競合は測れません。レビューでは、提示された数値がどの悪化を平均化して隠せるかを考えます。

品質属性同士の優先順位は、実装前に決めます。完了一覧を無期限に保持する案は再送の確実性を上げますが、資源上限を満たせません。入力エラーを細かく返す案は運用調査を助けますが、外部へ内部情報を出しすぎる可能性があります。衝突を見つけたら、どちらも最大化しようとせず、許容できる不足を決定者と定量化します。

分析、モデル、試作、優先順位を実装へ接続する

そして#10から#13を読んで、分析とモデルは実装前の説明資料ではないと捉え直しました。要求同士の矛盾を見つけ、状態遷移を比較し、不確かな設計を試作し、限られた時間で何を先に確かめるかを決める道具です。Rustで送金処理を実装するなら、失敗時に片方だけを更新しない原子性、認可、再送制御を型と処理順へ落とします。モデルがコードと違う抽象度を持つ時にだけ、両者を比較する意味が生まれます。

#10 要求と要求セットを分析する

再送は成功させたい一方、同じ要求IDによる内容の変更は拒否したい。残高はu64で持ちたい一方、合計の確認ではオーバーフローさせたくない。認可後に台帳が変われば、その認可結果は古くなります。Rustで実装するなら、入力検証、認可、ID衝突、残高不足、宛先オーバーフロー、反映の順で処理します。

個々の要求が正しく見えても、集合にすると衝突します。FR-3の原子性とFR-4の再送制御を別々に読むだけでは、再送時に残高計算を再実行してよいか判断できません。分析では、重複、欠落、矛盾、実現可能性、検証可能性を確認し、エラーの優先順位まで決めます。

再送の分岐では、要求IDだけでなく解析済みの送金内容全体を比較します。同じ内容なら状態を変えずにReplayedを返し、別内容ならRequestIdConflictで止めます。

if let Some(completed) = self.ledger.completed.get(&request_id) {
    return if *completed == self.transfer {
        Ok(TransferReceipt {
            request_id,
            disposition: Disposition::Replayed,
        })
    } else {
        Err(TransferError::RequestIdConflict)
    };
}

ここで見るのは分岐条件と書き込みの有無です。同一再送の経路に残高更新がないこと、IDが同じだけの別内容を成功として返さないことを確認します。ただし完了一覧はメモリ上にしかないため、再起動後の再送制御はこのコードでは扱えません。

個々の要求は、出所、理由、例外、受け入れ条件、前提、制約、リスクまで見ます。FR-3の「失敗時は台帳を変更しない」は、残高不足だけでなく、宛先オーバーフロー、存在しない口座、要求ID衝突にも適用されるのか。FR-4の再送制御は、成功済みの要求だけでなく、失敗した要求も記録するのか。文章を一行ずつ読むと正しそうでも、例外を列挙すると未決事項が出ます。

分解の粒度も確認します。「送金を安全に処理する」は大きすぎて、見積もりと検証ができません。入力解析、認可、残高遷移、再送判定へ分けると、それぞれに異なる証拠を割り当てられます。一方、u64の各演算を別要求にするほど細かくすると、実装の言い換えになり、意図を見失います。独立して判断、変更、検証できる単位を目安にします。

要求集合では、親を持たない項目と、子を持たない目標を探します。MAX_TRANSFER_INPUT_BYTESは資源枯渇を抑える品質目標に結び付きます。Replayedは安全な再試行という利用者目標に結び付きます。反対に、監査可能性を目標に挙げたのに時刻と操作記録が要求へ降りていなければ、目標だけが浮いています。下方向と上方向の両方に辿ります。

重複も危険です。送金上限を要求文、TransferAmount、Kaniの仮定、ファジングの表明へ別々の数値で書けば、変更時に食い違います。コードではMAX_TRANSFER_AMOUNTへ寄せ、文書からその定数と根拠へ辿ります。複数箇所に同じ情報が必要な時は、どこを更新元とし、どう不一致を検出するかを決めます。

前提は事実になるまで見える形で残します。「actorは上流で認証済み」「認可から実行まで所有者は変わらない」などです。レビューでは、前提が互いに矛盾しないか、運用で本当に守れるか、期限切れになっていないかを見ます。実装が前提どおりでも、前提が現実と違えば要求集合としては失敗です。

#11 要求モデルを作成する

Rustで実装するなら、処理順を次の状態モデルで表します。ParsedTransferは実行できず、認可後のAuthorizedTransferだけがexecuteを持ちます。

&[u8] -> ParsedTransfer -> AuthorizedTransfer -> TransferReceipt
          parse             authorize             execute

残高更新は台帳から分け、外部状態を書き換えず入力から結果だけを返す純粋関数transitionで計算します。送金元の減算と送金先の加算が両方成功した後に、台帳へ新しい残高を書き込みます。

fn transition(
    source_balance: u64,
    destination_balance: u64,
    amount: TransferAmount,
) -> Result<(u64, u64), TransferError> {
    let source_balance = source_balance
        .checked_sub(amount.get())
        .ok_or(TransferError::InsufficientFunds)?;
    let destination_balance = destination_balance
        .checked_add(amount.get())
        .ok_or(TransferError::DestinationOverflow)?;
    Ok((source_balance, destination_balance))
}

let (source_balance, destination_balance) =
    transition(source.balance, destination.balance, self.transfer.amount())?;

self.ledger.accounts.insert(
    self.transfer.source(),
    Account { balance: source_balance, ..source },
);
self.ledger.accounts.insert(
    self.transfer.destination(),
    Account { balance: destination_balance, ..destination },
);

レビューではchecked_subchecked_addがあるだけで安心しません。transition(...)?より前に台帳を書き換えていないことを確認します。この順序なら、残高不足と宛先オーバーフローのどちらでも2口座を元のまま残せます。

モデルはコードを短く説明するためだけに置くものではありません。イベントと状態遷移を別の形で表し、到達できない状態、応答のないイベント、許してはいけない遷移を探します。状態モデルに再送が現れないなら、完了一覧の照合を別のモデルか要求で補う必要があります。

モデルは、理解したい問いに合わせて選びます。システムの外との接点にはコンテキスト、送金の順序には状態遷移、データの関係にはエンティティ関係、条件の組み合わせには決定表が向きます。図を増やすことが目的ではありません。新規性が高い箇所、分岐が多い箇所、損失が大きい箇所だけを、文章とは異なる見方で表します。

この型状態モデルを使うなら、未信頼バイト列から解析済み、認可済み、実行結果へ進む正の経路を表せます。しかし、残高不足で認可済みからエラーへ進む経路や、再送時に状態を変えない経路は図にありません。モデルを見て安心するのではなく、何を省略したかを書きます。省略した経路は、別のイベント応答記述とテストで補います。

純粋関数transitionへ残高計算を分けるなら、状態変更の核心を小さくできます。入力2つと金額から、新しい残高2つかエラーだけを返します。台帳、所有者、要求IDを含めないため、整数演算の性質をKaniへ渡しやすくなります。その代わり、認可や再送の正しさはこのモデルでは扱えません。モデルを単純化したときに外へ出した責任を記録します。

複数の表現を照合すると、単一表現では見えない誤りが出ます。状態モデルにAuthorizedTransferがあるのに、公開APIから直接executeできるなら型とモデルが食い違います。データモデルに処理済み要求があるのに、要件から保持期間を辿れないなら管理ルールが欠けています。モデルとコードを同じ人が同じ順序で写すだけでは誤りも複製するため、可能なら別の観点で作ります。

モデルは反復して直します。初回から完全な図を作るのではなく、分かった範囲のたたき台を置き、例外や境界をレビューします。修正しにくい作図資産を先に作り込むと、図を守るために要求を曲げることがあります。安定するまでは短いテキストや簡単な図を使い、価値が確認できたモデルだけを保守対象にします。

#12 プロトタイプを作成して評価する

Rustで実装するなら、送金計算より先に、不正な順序を防ぎながら公開APIを使えるかを確かめます。ledger.transfer(actor, parsed)?と、認可と実行を分ける形を比較できます。後者を選ぶなら、処理順を型シグネチャに出します。

pub fn authorize(
    &mut self,
    actor: ActorId,
    transfer: ParsedTransfer,
) -> Result<AuthorizedTransfer<'_>, TransferError> {
    let source = self
        .accounts
        .get(&transfer.source())
        .ok_or(TransferError::SourceAccountNotFound)?;
    if source.owner != actor {
        return Err(TransferError::UnauthorizedSource);
    }
    if !self.accounts.contains_key(&transfer.destination()) {
        return Err(TransferError::DestinationAccountNotFound);
    }

    Ok(AuthorizedTransfer {
        ledger: self,
        transfer,
    })
}

pub struct AuthorizedTransfer<'ledger> {
    ledger: &'ledger mut Ledger,
    transfer: ParsedTransfer,
}

ledger.authorize(actor, parsed)?.execute()?;

AuthorizedTransfer<'ledger>&'ledger mut Ledgerを保持するため、認可から実行までの間に同じ台帳へ別の変更を差し込めません。行数は増えますが、認可を飛ばした実行はコンパイルできません。この排他性が効くのは単一プロセスの同じLedgerだけであり、DBや複数インスタンスの同時実行制御は別に必要です。

プロトタイプには先に評価項目を置きます。正常系の行数、誤った順序がコンパイルされないこと、エラーを区別できること、レビュー時に処理順を説明できることです。最終実装へ残すことは評価基準にしません。不確実だった要求について、判断材料を得られたかで評価します。

プロトタイプは、作り始める前に問いを1つ置きます。Rustで送金処理を実装するなら、「認可済みという状態を型で表せば、誤用を減らし、APIの過度な複雑化を避けられるか」と問います。問いがなければ、動くコードを作った達成感だけが残り、何を学んだのか判断できません。性能を知りたいならベンチマーク用、UIの流れを知りたいなら画面用と、問いに応じて形を変えます。

比較対象も必要です。単一メソッド案は呼び出しが短い一方、認可と実行の境界が関数内部に隠れます。型状態案はAuthorizedTransferを持ち回るため公開型が増えますが、認可を経ないexecuteを表現できません。ここでの判断は、行数が少ない方ではなく、誤った利用をどこで止められるかと、利用者が処理順を理解できるかで行います。

コンパイル失敗例も評価に含めます。たとえばParsedTransferに直接executeを呼ぶコードや、認可後に同じLedgerをもう一度可変参照するコードがコンパイルしないことをrustdocで確認します。成功例だけでは、避けたい誤用が本当に閉じられているか分かりません。

一方、型状態案で分からないこともあります。DBトランザクションが認可から実行まで維持されるか、別プロセスで所有者が変わらないか、待機中に処理が飢餓状態にならないかは、この所有権からは言えません。プロトタイプの成功を広げすぎず、解消した不確実性と残った不確実性を分けます。

使い捨てか、そのまま育てるかも先に決めます。使い捨てなら、得た知識を要求と設計判断へ移し、コードは残しません。育てるなら、入力検証、エラー処理、文書化、テスト、保守性を最初から本番相当で扱います。Rustコードを後者として育てるなら、forbid(unsafe_code)と品質ゲートも最初から含めます。動いた試作品を品質確認なしでそのまま提供するのが最も危険です。

#13 要求に優先順位を付ける

Rustで送金処理を実装するなら、認可、原子性、再送制御、攻撃ケース、残高保存のモデル検査を必須にします。ファジング、性質テスト、性能測定は推奨とし、HTTP、DB、分散実行は対象外に置きます。形式検証を最上位に置くという意味ではありません。整数上限の誤りが資金破壊につながり、残高計算を純粋関数へ切り出せる場合にKaniを選びます。

優先度は、損失、緊急度、依存関係、検証費用で決めます。技法の格は基準にしません。入力解析が固まらなければファジングの入口も安定しません。認可と原子性が未完成のまま性能を測っても、速いが使えない実装を評価するだけです。Rustで実装するなら、実装順と検証順を分けず、先に満たす必要がある要求から並べます。

優先順位には、重要度と緊急度を分けて入れます。未認可送金の拒否は損失が大きく、最初の提供時点から必要です。再送制御も、通信失敗があり得るなら初回リリースに必要です。一方、長時間ファジングは重要でも、短い検証を通した後に別ジョブで実行できます。重要だが直ちに同期実行する必要はない項目を区別します。

「なければどうするか」を聞くと、必須度が見えます。認可がなければ送金機能そのものを提供できません。性能測定がなくてもRustライブラリとしての動作は評価できますが、性能退行を早期に検出できません。HTTPがなければネットワーク経由の振る舞いは評価できません。代替手段、遅らせた損失、永久に提供しない場合の影響を比べます。

価値だけでなく、コスト、リスク、依存関係、学習効果も見ます。transitionのKani検証は、対象が純粋関数で小さいため、低い費用で整数境界の不確実性を減らせます。分散実行のモデル検査は、対象システムと状態モデルがなければ、費用が高いうえに有効な問いを置けません。Rustで実装するなら、難しさではなく、現在の判断に寄与する順へ並べます。

優先順位は粒度の近い項目同士で比較します。「認可を実装する」と「Kaniの表明を追加する」を直接並べると、大きさが違いすぎます。まず利用者能力や危険単位で並べ、その中で実装と証拠の順序を決めます。また、すべてを最優先に分類することを認めません。最上位に置ける項目数は、利用できる時間と検証能力で制限します。

新しい要求が来るたびに、既存の順序と比較します。DB追加は単なる後続作業ではなく、原子性と再送制御の前提を変えます。この変更は、現在の性能改善より先に再検証します。優先順位は開始時の1回限りの投票ではなく、目標、リスク、依存関係が変わったときに更新する判断です。

要求文を、反例を探せる証拠へ変える

要求を読みやすく整える章は#14から#18です。実際に適用すると、一貫した形式、構造、業務ルール、用語集は、レビューで反例を探すための準備でした。用語が揃っていなければ型との対応を追えず、失敗時の状態が書かれていなければテストの期待値を決められません。要求をレビュー可能にすることと、コードを検証可能にすることは同じ方向を向いています。

#14 要求を一貫した形式で書く

Rustで送金処理を実装するなら、要求にはFR-1からFR-4、品質属性にはQR-1からQR-4、ビジネスルールにはBR-1からBR-8のIDを付けます。各要求はイベント、条件、観測できる応答の順で書き、優先度、受け入れ基準、証拠を添えます。文章の型を揃える目的は、見た目を整えることではありません。条件や応答が欠けた要求を見つけやすくするためです。

FR-3 残高を原子的に移す
WHEN  認可済みの送金を実行する
IF    送金元の残高が足り、送金先でオーバーフローしない
THEN  正確な金額を減算・加算する
ELSE  どの台帳状態も変更しない
証拠  execute、transition、受け入れテスト、性質テスト、Kani

レビューでは「適切に」「安全に」といった副詞を、入力、出力、エラー、状態変化へ分解できているかを見ます。要求の粒度も揃えます。1つの要求に認証、送金、監査、復旧まで入っていれば、どの証拠が何を確認したのか追えません。

WHENが空なら、いつ適用される要求か未定です。ELSEが空なら、失敗時の状態を決めた記録がありません。証拠が空なら、実装後に何を見て完了とするか決まっていません。書式によって現れた空欄を、そのまま議論の入口にします。

文は短くし、主体と動作を明示します。「送金は適切に検証される」では、誰が何をするか分かりません。「入力解析器は、128バイトを超える入力をUTF-8変換前に拒否する」なら、主体、条件、処理順、観測結果が読めます。受動的な表現や一般的な「システム」を避け、必要ならParsedTransfer::parseのように責任を持つ部分まで示します。

1つの要求へ独立した振る舞いを詰め込みません。接続語の「かつ」「または」「ただし」が増えたら分割を検討します。Rustで実装するなら、FR-3では成功時の二口座更新と失敗時の無変更を一緒に扱いますが、認可や再送までは含めません。分けた項目同士はIDで関係を示します。

抽象度もそろえます。「送金を実現する」という利用者能力と、「checked_addを使う」という実装方法を同じ階層へ並べません。前者をFR-1からFR-4へ分解し、実装方法は制約として本当に必要な場合だけ書きます。checked_addそのものではなく、「宛先残高がu64上限を超える時は変更せず拒否する」が要求です。実装を差し替えても意図が残る書き方にします。

要求文以外の属性は、実際に判断へ使う最小限に絞ります。Rustで実装するなら、ID、出所、理由、優先度、状態、受け入れ条件、証拠、依存関係、変更履歴を持たせます。誰も更新せず、誰も参照しない属性を増やすと、空欄を埋める作業が本体になります。必要になった時点で追加します。

レビューでは、初見の人が同じ入力から同じ結果を予測できるかを試します。複数の読み方が出たら、説明で押し切らず要求を直します。要求文が正しくても、対応するテスト名やエラー名が別の語を使っていれば追跡時に迷います。文章、型、API、テストの語彙まで一貫性の対象です。

#15 要求を構造化して整理する

Rustで実装するなら、問題と判断理由はDESIGN.md、基準版と追跡情報はREQUIREMENTS.md、型と規則はsrc/lib.rsへ置きます。反例の探索はtests/fuzz/、再実行の手順はREADME.mdへ分けます。情報を1つの文書へ集めず、判断する人が必要な入口から辿れるようにします。

確認するのはファイルの存在ではありません。変更後も現在の実装を表しているか、要求IDから型とテストへ辿れるか、初めて読むレビュー担当者が関数本体を掘る前に対象範囲を把握できるかを見ます。情報が複数箇所にある以上、更新漏れを検出する手段も必要です。

整理方法は、書き手より読み手を基準に選びます。プロダクト責任者は問題、目標、優先順位を先に読み、実装担当者は境界、型、例外を読み、テスト担当者は受け入れ条件と残る前提を読みます。全員に同じ長大な文書を最初から最後まで読ませるのではなく、必要な情報へIDとリンクで移れる構造にします。

テンプレートはチェックリストとして使い、プロジェクトへ合わせて縮めます。対象外、前提、品質属性の欄が空なら、「該当なし」と判断したのか、単に調べていないのかを区別します。一方、小さなライブラリに大規模システム向けの章立てをそのまま持ち込みません。情報を見つけやすくしない様式は削ります。

個々の要求には、位置に依存しない永続的なIDを付けます。見出し番号だけをIDにすると、項目の挿入で参照がずれます。FR-3BR-8のようなIDから、設計判断、コード、テスト、検証結果へ辿ります。削除したIDは別の意味で再利用しません。過去の議論と結果が別要求へ結び付くのを防ぐためです。

情報を複製するときは、更新元を決めます。送金上限の数値はコードの定数にあり、要求文には意味と理由があります。テストは同じ定数を参照します。文書中の例に数値を直接書く時は、定数変更時に検索できる表記へそろえます。同じ定義が三箇所にあり、少しずつ違う状態が最も危険です。

検索性と変更容易性も確認します。rg 'FR-3|BR-8'で関連箇所を見つけられるか、基準版に含まれる項目を一覧できるか、未評価の要求だけを取り出せるかを試します。高度な管理ツールを導入しても、要求の出所や例外を考えてはくれません。MarkdownとGitで足りる規模なら、まず既存の仕組みで運用します。

構造化した情報は、最新でなければ害になります。変更の品質ゲートでは、コードだけでなく、該当要求、テスト、前提、残存リスクの差分を見ます。文書の最終更新日だけで新しさを判断せず、実装の振る舞いと一致するかを具体例で確認します。古い文書が検索上位に残るなら、整理は成功していません。

#16 ビジネスルールを特定し、文書化する

Rustで実装するなら、送金額は1以上1,000,000以下、送金元と送金先は別口座、所有者だけが送金できる、失敗時は台帳を変えないと定めます。このほか、残高不足、宛先オーバーフロー、同一再送、ID再利用を含む8つのルールへBR-1からBR-8を付けます。

ルールを書いた後は、どこで強制するかを確認します。金額の範囲はTransferAmount、別口座はParsedTransfer::new、所有者はLedger::authorize、残高と再送はexecuteが担当します。ルールは要求とコードの両方に現れます。文言の一致より、同じ入力に対して同じ結果になるかをテストします。

条件の組み合わせも見落とせません。送金元の残高が足りず、送金先もオーバーフローする場合に、どちらのエラーを返すか。残高不足を先に検査するならInsufficientFundsを返します。この優先順位が業務上重要なら要求へ明記し、重要でなければ呼び出し側が特定の順序へ依存しないようにします。

ビジネスルールは、特定の画面や関数がなくても成立する業務上の決まりです。「送金額は1以上1,000,000以下」は、HTTPと窓口処理のどちらにも適用されます。その規則から、TransferAmountの生成制約、入力エラー、境界値テストが導かれます。規則そのものと、規則をソフトウェアで強制するための機能を分けます。

ルールには種類があります。用語の定義、常に成り立つ事実、許可や禁止を示す制約、条件に応じて処理を起こす規則、計算式、別の情報から導く推論です。送金額の範囲は制約、残高計算は計算、同一内容の再送ならReplayedを返すのは条件付きの処理です。種類を意識すると、自然言語、型、純粋関数、分岐のどこへ置くか判断しやすくなります。

規則の出所も残します。法令、社内方針、業務責任者の判断、外部システムの制約、既存コードからの推定では、変更できる人と確かさが違います。既存コードに1,000,000と書かれているだけなら、現行ルールと古い都合のどちらか判別できません。リバースエンジニアリングで見つけた値は、業務側へ確認するまで前提として扱います。

規則には一意のID、有効日、管理する人、変更理由を持たせます。同じBR-1を各要求へコピーせず、参照で結びます。上限が変わったときに、入力解析、Kani、ファジング、性能条件へ影響を辿るためです。複数のサービスが同じルールを使うなら、サービスごとに別の文言を持たず、共有する定義と各実装の対応を分けます。

条件の組み合わせは文章だけで追わず、決定ロジックとして列挙します。Rustで実装するなら、要求IDの処理状態と内容の一致を組み合わせて、AppliedReplayedRequestIdConflictを決めます。さらに残高不足と宛先オーバーフローが重なる場合を加えると、エラー優先順位も必要です。全組み合わせに結果があり、重複した結果定義がないかを見ます。

ルール違反時の振る舞いまで要求へ落とします。「所有者だけが送金できる」だけでは、違反をログに残すか、どのエラーを返すか、残高を変えないかが未定です。Rustで実装するならUnauthorizedSourceを返し、実行用の型を生成せず、台帳を変えない設計にします。ルールと例外処理を別々にしないことが重要です。

変更頻度が高いルールは、変更しやすい場所へ局所化します。ただし、何でも設定化するわけではありません。送金上限を設定へ移すなら、変更権限と反映時刻を決めます。範囲外設定の拒否方法と、検証結果の更新方法も必要です。変更容易性と誤設定の危険を比較します。

#17 用語集を作成する

Rustで実装するなら、操作主体と口座所有者、未信頼入力と解析済み送金、解析済みと認可済み、再送とID再利用を分けます。「証明」もKaniへ渡すモデル、仮定、性質の範囲だけを指すと定義します。システム全体が正しいという意味では使いません。

用語は詳しい人だけで決めず、コードを初めて読む人にも確認してもらいます。ActorIdを「認証済みユーザー」と読み替える人がいるなら、上流の認証を信頼しているだけだと伝わっていません。型名、要求、エラー名、文書で同じ概念を別の言葉で呼んでいないかを確認します。

用語集が必要なのは、単語を知らないからだけではありません。同じ言葉を知っている人同士が、違う意味で使う方が発見しにくいからです。「成功」を、残高が一度反映された状態と考える人も、再送で既存結果を返せた状態まで含める人もいます。そこでAppliedReplayedを分け、どちらも呼び出しとしては成功だが、状態変更の有無が違うと定義します。

各項目には、定義、同義語、使わない呼び方、単位、例を必要に応じて置きます。「操作主体」はAPIへ渡されたActorIdであり、本人確認済みであることをこのクレートが保証するわけではありません。「口座所有者」は台帳内のAccount.ownerです。「認可済み送金」は所有者と口座存在を確認し、台帳の可変参照を保持する値です。似た概念の差を、実装に対応させて書きます。

データ辞書との違いも保ちます。用語集はRequestIdの意味と「再送」「ID再利用」の区別を説明します。データ辞書はRequestIdが非ゼロのu64であること、入力の第1フィールドであること、一意性の扱いを説明します。意味と表現を1つの欄へ混ぜると、業務用語の変更とデータ形式の変更を区別できません。

候補語は、要求、会話、データモデル、型名、エラー名から拾います。同じ対象を「依頼」「要求」「リクエスト」と呼んでいないか、逆に別の対象をすべて「ユーザー」と呼んでいないかを検索します。表現を豊かにするための言い換えは、仕様では誤解の原因になります。選んだ語を一貫して使います。

用語集は、ドメインの専門家と初見の人の両方でレビューします。専門家は意味の正しさを確認し、初見の人は定義だけで区別できるかを確認します。新しいメンバーが口頭説明なしで要求とコードを結び付けられないなら、暗黙知が残っています。AIエージェントも同じ曖昧さを増幅するため、プロンプトへ長い説明を毎回書く前に共有語彙を直します。

変更時には用語集も対象です。要求IDの有効期限を導入すると、「同一再送」が期限内だけを指すのか、期限後も同じ語を使うのかが変わります。定義を変えたら、型名、エラー、テスト名、運用手順への影響を辿ります。用語集を静的な付録ではなく、概念の境界を管理する成果物として扱います。

#18 要求をレビューし、テストする

FR-3に書式、業務ルール、用語が揃っていても、期待値そのものが利用者の意図を取り違えていれば、実装前から誤りは始まっています。要求レビューと要求テストでは、読みやすさに加えて、反例を探せる形になっているかを確かめます。要求文を読んだ時点から、意図との一致、曖昧さ、例外、結果を観測できるかを確認できます。

ここで「署名された」「会議で承認された」ことを、内容の妥当性と同一視しません。文章を精査せず、進行だけへ同意できるからです。判断者の確認は必要ですが、それだけでは要求の誤りを減らせません。

レビュー担当者は、見つけたい問題に合わせて選びます。プロダクト責任者は業務上の正しさと優先順位を見ます。開発者は実現可能性と依存関係を見ます。テスト担当者は、入力、例外、観測結果が不足していないかを見ます。セキュリティ担当者は、信頼境界と悪用経路を見ます。同じ文章を全員へ回すだけでなく、各自がどの問いを持って読むかを明示します。

すべての要求へ同じ重さの会議を置きません。小さな変更は同僚による短い確認で済ませ、損失が大きい認可、原子性、再送制御は個別準備を伴うレビューにします。レビュー会議で初めて文章を読み始めると、説明の理解だけで時間を使います。事前に問題候補を記録し、会議は誤りの解決と判断へ使います。

レビューで意味を確かめた後は、要求ごとに反例を探す方法を選びます。型、例示テスト、生成した入力に対して規則を確かめる性質テスト、Kani、ファジング、性能測定、攻撃者視点のテストに一律の強弱はありません。型は処理順の飛ばし、例示テストは具体的な利用場面を担当します。性質テスト用ライブラリのProptestは値の組み合わせ、Kaniは整数全域の性質、ファジングは予想していないバイト列、攻撃者視点のテストは既知の悪用を扱います。

まず、要求から受け入れテストを書けるかを確認します。残高不足で失敗したときは、エラーだけでなく両口座が元の値を保つことまで検査します。

let mut ledger = ledger(499, 300);
let result = ledger
    .authorize(actor(1), transfer(7, 10, 20, 500))
    .expect("the owner must be authorized")
    .execute();

assert_eq!(result, Err(TransferError::InsufficientFunds));
assert_eq!(ledger.balance(account(10)), Some(499));
assert_eq!(ledger.balance(account(20)), Some(300));

このテストは具体的な送金例を確認します。Proptestでは送金元残高、送金先残高、金額を生成し、成功、残高不足、オーバーフローの各分岐と失敗時の無変更を探します。具体例と生成ケースのどちらにも、要求IDを付けて何を反証しようとしているかを残します。

例示テストは、要求を別の形で記述する作業でもあります。要求作成者がFR-3を書き、別の人が同じ利用場面からテストを作れば、2つの理解を比較できます。期待する結果へ到達する要求がなければ、テストが誤っているか、要求が欠けています。同じ人が要求文をそのままコードへ写したテストだけでは、誤解も一緒に複製する可能性があります。

テストケースは、同じ結果になるとみなせる入力のまとまりである同値クラスと、その境界値から絞ります。送金額では、下限の周辺に0と1を置きます。上限の周辺には1,000,0001,000,001を置きます。送金元残高なら、金額より1少ない、同じ、1多い値を確認します。宛先残高なら、u64::MAX - amountと、その1大きい値を確認します。範囲内の値を無作為に増やすだけより、要求の境界を直接たたく方が誤りを見つけやすくなります。

エラー経路では、戻り値と状態を一緒に確認します。InsufficientFundsが返っても、送金元だけ減っていれば失敗です。RequestIdConflictが返っても、完了一覧が上書きされていれば次の再送判定が壊れます。観測対象をエラー文字列だけにせず、残高、処理済み要求、返却された区分を含めます。

モデルもテストできます。parse -> authorize -> executeの各経路へ受け入れテストを対応させ、テストから辿れない経路を探します。未到達の経路は、テスト不足か不要な処理です。逆に、要求上は必要なテストがモデル上を辿れないなら、モデルまたは要求が不完全です。文章、モデル、テストを相互に照合します。

Kaniを使うなら、台帳全体を渡さず、純粋関数transitionだけを検証対象にします。成功条件を仮定し、u128で見た残高合計が保存されることを確認します。

let source = kani::any::<u64>();
let destination = kani::any::<u64>();
let raw_amount = kani::any::<u64>();
kani::assume((1..=MAX_TRANSFER_AMOUNT).contains(&raw_amount));
kani::assume(source >= raw_amount);
kani::assume(destination <= u64::MAX - raw_amount);

let Some(amount) = TransferAmount::new(raw_amount) else {
    unreachable!();
};
let Ok((new_source, new_destination)) = transition(source, destination, amount) else {
    unreachable!();
};

assert_eq!(new_source, source - raw_amount);
assert_eq!(new_destination, destination + raw_amount);
assert_eq!(
    u128::from(new_source) + u128::from(new_destination),
    u128::from(source) + u128::from(destination)
);

Kaniを導入するなら、公式の手順に従います。

model-checking.github.io

この検証から言えるのは、指定した仮定の下でtransitionが保存則を満たすことです。認証、台帳、DBを含む送金システム全体の正しさまでは言えません。

Kaniの表明自体もレビューします。合計保存だけなら、送金元と送金先を逆に更新しても通る可能性があります。そのため、個別の差分もassert_eq!で確認します。さらに、kani::assumeが成功経路だけを残しすぎていないかを見ます。仮定を強くすれば証明は通りやすくなりますが、現実の入力を排除していれば価値はありません。

もう1つの検証では、transitionの結果が事前条件と一致することを確認します。残高不足ならInsufficientFunds、送金元が足りて宛先が上限を超えるならDestinationOverflow、両方成立するなら新しい残高です。成功時の性質だけでなく、失敗の分類が処理順と一致することも対象にします。

ファジングは解析器へ任意のバイト列を渡します。成功した場合にも、入力長、異なる2口座、送金額の範囲を再確認します。

fuzz_target!(|input: &[u8]| {
    if let Ok(transfer) = ParsedTransfer::parse(input) {
        assert!(input.len() <= MAX_TRANSFER_INPUT_BYTES);
        assert_ne!(transfer.source(), transfer.destination());
        assert!((1..=MAX_TRANSFER_AMOUNT).contains(&transfer.amount().get()));
    }
});

ファジングを行うなら、cargo-fuzznightlyを使います。

rust-fuzz.github.io

-Zsanitizerstableでは使えないため、検証手順にはコマンドだけでなく必要なツールチェーンも含めます。

ファジングの表明も、解析器の仕様の一部です。「クラッシュしない」だけでは、長すぎる入力を誤って受理しても成功扱いになります。解析に成功した時は、入力長、口座の相違、送金額の範囲を再確認します。一方、解析後の認可と残高更新はこの対象に含まれません。ファジングの入口と本番の入口が一致しているか、前処理が迂回されていないかも確認します。

探索の種として保存する入力群であるコーパスと実行時間は、網羅性の証明ではありません。一定時間に多くの入力を試しても、意味のある4フィールド構造へ到達する割合や、特定の数値境界を通る割合は別です。構造を理解する生成器と任意バイト列のファジングを組み合わせ、それぞれが担当する探索空間を明記します。

Rustで品質ゲートをまとめるなら、次のようなスクリプトにします。

./scripts/verify.sh

スクリプトは、整形確認、Clippy、通常テスト、rustdoc、性能テスト、ベンチマーク、Kani、ファジングを順に実行します。実行順と必要なツールチェーンも検証手順の一部です。ローカルのファイル位置を公開資料の参照先にはせず、この記事では再現に必要なコマンド、バージョン、対象、結果を記録します。

実装後は./scripts/verify.shを最終差分に対して実行し、すべてのゲートが完了することを確認します。ここで見るべきなのは件数ではなく、同じ手順を再実行できることです。通常テストが通っても、未生成の全ケースが正しいとは言えません。

検証結果には、対象と除外範囲を添えます。受け入れテストは利用者から見た振る舞い、攻撃者視点のテストは既知の悪用、Proptestは生成した入力に対する性質を扱います。Kaniが扱うのはtransitionであり、ファジングが扱うのはParsedTransfer::parseです。結果を合算して「十分」と判断せず、要求ごとに担当する証拠があるかを見ます。

性能測定は、メモリ内のBTreeMapを使う逐次処理の退行を見つけるために使います。ログ、暗号化、永続化、ネットワーク待ちは含みません。本番の応答時間目標とは分け、環境が変われば基準値を取り直します。

攻撃者視点のテストでは、未認可送金、巨大入力、整数変換、要求IDの改ざん再利用を扱います。これは既知の悪用を繰り返す回帰テストです。HTTP、認証トークン、永続DB、複数インスタンスを接続した段階では、別テナントの混同、同時実行、再起動、資源枯渇、情報漏えいを独立して評価する必要があります。

レビューの完了条件は、すべてのゲートが緑であることだけではありません。各要求が必要で、意図と合い、検証可能であり、対応する証拠と残る前提を説明できることです。自動検証に通らない変更は止めますが、自動検証に通った変更は人間の判断へ進めるだけです。特に要求文、期待値、生成範囲、仮定は、検証装置が依拠する側なので目視します。

不具合が見つかったら、実装だけでなく要求とテストのどこが見逃したかを確認します。新しいテストだけを足して終えると、同じ種類の誤りが別の値で残ります。境界値の欠落なら同値クラスを見直し、状態の欠落ならイベント応答を見直し、前提違反なら境界と運用を見直します。反例を要求体系の改善へ戻すところまでをレビューに含めます。

最後の2項目が、AI時代には重くなる

品質ゲートを通し、対象と除外範囲まで書くと、20項目を終えたような気分になります。しかし、要求IDから証拠まで15分以内に辿れるかは、人に変更を読んでもらうまで評価できません。この未評価を残したまま考えると、#19と#20は締めの管理作業ではなく、検証結果をどの要求集合へ結び付け、次の変更へ渡す仕事として見えてきます。AIエージェントが実装とテストを同時に書き換えるほど、その仕事は重くなります。

#19 要求の基準版を作成する

基準版は、ある開発サイクルで実装し、テストする項目のスナップショットです。「最新版」とだけ書くのではなく、含まれる要求ID、各項目の状態、対象リリース、判断日、検証結果を特定します。開発者は何を作るか、テスト担当者は何を確認するか、ステークホルダーは何が提供範囲かを同じ集合から判断します。

Rustで実装するなら、自動検証が揃った要求集合をcandidate-0.1として区切ります。ここで固定するのは、その時点のFR、QR、BR、前提、証拠、判断者、対象範囲です。15分のレビュー目標は代表レビュー担当者が評価するまで、達成済みとは扱いません。基準版は、何を確認した結果なのかを後から再現するために使います。以後の変更を拒むためのものではありません。

基準版へ入れる前に、着手できる状態かを確認します。各要求の理由と受け入れ条件があり、重大な矛盾が解消され、依存関係が分かり、必要な判断者が合意していることが最低限です。未決事項が残る時は、隠さず状態と影響を付けます。質の低い要求を版名で固定しても、安心材料にはなりません。

区切り方には、時間を固定して入る量を選ぶ方法と、提供したい範囲を固定して必要期間を見積もる方法があります。Rustの実装範囲を先に固定する場合もあれば、実案件の短い反復で、利用可能な開発とテストの時間に合わせて優先項目を選ぶ場合もあります。時間、範囲、品質をすべて固定したまま、見積もり超過を品質低下で吸収しません。

版名より、実行された検証と要求集合の一致を確認します。コードだけが先へ進み、candidate-0.1という名前が残っているなら基準にはなりません。コミット識別子、ツールのバージョン、実行コマンド、結果、未評価項目を結び付けます。「どの差分に対する緑か」が分からない結果は再利用しません。

承認は、変更不能にする行為ではありません。その時点で把握している範囲では、実装へ進めると関係者が判断したことを示します。後から変更はできますが、変更理由、影響、採否を通して新しい基準版を作ります。この説明がないと、合意したら直せないと恐れて承認が止まるか、逆に誰でも無断で内容を変えます。

1つの時点で複数の基準版が進む場合もあります。開発中のcandidate-0.2と、テスト中のcandidate-0.1があれば、変更がどちらへ適用されるかを明示します。テスト中の版へ修正を入れたのに、開発中の版へ反映しなければ、次の提供で不具合が戻ります。版ごとの要求集合と差分を追います。

小さな変更では影響を受ける要求だけを更新できます。ただし、対象範囲、前提、主要イベント、外部システムとの接点が変わる時は要求集合全体を見直します。自動ゲートが通った時刻と未評価の項目を同じ場所へ残し、「確認済み」と「まだ判断していない」を混ぜないようにします。

#20 要求の変更を管理する

たとえば送金上限を1,000,000から増やす場合、TransferAmountの定数だけを変えても終わりません。BR-1、入力の同値クラス、Kaniの仮定、ファジングの検査、性能ワークロード、記事の記述が影響を受けます。要求IDから関連箇所を辿れるため、変更対象と再実行する検証を先に決められます。

変更は避けられません。利用者が新しい使い方を見つけ、障害から例外が分かり、外部システムが変わり、業務目標や規則も変わります。変更件数をゼロにはできません。減らすべきなのは、誰にも知られない変更、影響を調べない変更、古い説明を残す変更です。

変更依頼には、変更したい内容だけでなく、理由、期待する効果、緊急度、対象の基準版を含めます。「上限を上げてほしい」だけでは、どの利用者が何をできず、いつまでに必要か分かりません。大口送金への対応なのか、試験データを通す都合なのかで、採用判断と必要な検証が変わります。

影響分析では、要求、業務ルール、データ、公開API、実装、テスト、性能、運用、スケジュール、関係者を辿ります。ローカルな定数変更に見えても、損失上限、監視閾値、問い合わせ手順、外部システムの型へ波及します。コンテキスト図、データモデル、要求IDの参照を使い、直接の変更箇所だけでなく二次的な影響を探します。

DBを追加する変更なら、A-3とA-4が成立しなくなり、原子性、再送、同時実行の検証方法を作り直す必要があります。&mut Ledgerは同一プロセス内の排他性しか示しません。DBトランザクション、分離レベル、一意制約、再起動後の処理済み要求、複数インスタンスの競合を新しい要求へ入れます。既存テストが緑でも、前提が変われば証拠をそのまま移せません。

変更しやすい領域は、実装前から記録します。送金上限、要求IDの保持期間、外部接続先は変わる可能性があります。ただし、将来の変更を予想して無制限に設定項目や抽象化を増やしません。変更頻度、影響の大きさ、誤設定の危険を見て、データ化するかコード変更にするかを決めます。

すべての変更に重い審査会を置く必要はありません。単一チーム内の小変更なら、要求を更新し、担当者と責任者が影響を確認して、同じ差分に対して検証すれば足ります。複数システムや高い損失に関わる変更は、関係する責任者を増やします。規模に応じて手順を変えても、依頼、影響、判断、更新、伝達の段階は省きません。

判断後は、採用した変更だけでなく、見送った理由も残します。同じ依頼が後で再提出されたとき、前回の前提と何が変わったかを比較できます。見送りを無言で閉じると、依頼者は実装待ちだと考え、別経路から同じ変更を入れようとします。判断結果を影響する人へ返します。

採用時は、要求、実装、証拠、基準版を同じ変更で更新します。コードを先に直して文書を後回しにすると、その間にAIエージェントや別の開発者が古い記述を入力として使います。逆に要求だけ変えて実装が追いついていない時は、状態を「承認済み」「実装中」のように明示します。文章と現実の差を隠しません。

要求は実装前に完成させる文書ではありません。AIエージェントが実装を速めるほど、変更理由と影響範囲を短い周期で更新できることが重要になります。プロンプトを渡した時点で要求の管理が終わるのではなく、反例、運用結果、外部変更を次の基準版へ戻し続けます。

読後に残ったのは、検証が作る「保証の外側」だった

20項目をAIエージェントの開発へ当てると、本書の要約だけでは収まらない読み替えが残りました。型、テスト、モデル検査、ファジングを足せば、機械で確かめられる範囲は広がります。しかし、検証はソフトウェア全体へ「正しい」という印を付けるものではありません。何を、どの対象について、どの仮定の下で確かめたかを限定します。検証を増やすほど、その限定を読まなければ保証の意味を取り違えます。

ここで私が「保証の外側」と呼ぶのは、単にテストしていないコードではありません。検証した性質を利用者の意図と結び付ける判断、検証時の仮定、本番でも仮定が保たれるとみなす根拠、検証器そのものへの依存を含みます。機械が確認した範囲の外にありながら、その確認結果を意味あるものにしている条件です。

レビューでは、合格した実装より合格が成り立つ条件を読む

型は、表現できない状態を減らします。テストは、選んだ入力で観測した結果を示します。モデル検査は、与えたモデルと仮定の中で性質を調べます。どれも強い道具ですが、強さの単位が違います。結果を1つの「すべて緑」へ畳むと、何を言えて、何を言えていないかが見えなくなります。

保証を読む時は、性質、対象、仮定、証拠の4つを分けます。何が成り立つのか。どの関数、状態、実行経路が対象なのか。どの入力や環境条件を置いたのか。どの検証結果が支えているのか。この4つが結び付いて初めて、緑の結果へ意味を与えられます。

たとえば「同じ要求IDによる再送で残高を二度変更しない」という性質をKaniで確認したとします。その保証が及ぶのは、同一プロセス内のtransitionと、モデルへ与えた状態です。要求IDがプロセスを越えて保持されること、異なる内容へ再利用されないこと、本番の入口が必ずtransitionを通ることは、別に確認しなければなりません。証明済みという一語では、この違いが消えます。

保証の外側には、検証の失敗ではなく、何を正しさと呼ぶかという決定が残ります。

csrc.nist.gov

発想の起点にあるNISTの概念は、セキュリティ方針を実施するハードウェア、ファームウェア、ソフトウェアの保護機構を扱います。この記事では、それを日本語へ直訳して使いません。「ある保証は、何を正しいと置くことで成立しているか」という関係だけを取り出し、より広い検証の問題を「保証の外側」と表します。

保証の外側は、システムに固定された部品表ではありません。保証する性質が変われば、外側へ残る条件も変わります。残高の合計が保存されるという主張では、整数演算、状態遷移、検証器の意味が成立条件になります。本人だけが送金できるという主張では、利用者IDの発行元、認証結果の受け渡し、認可済みの値を作れる場所を確認します。同じ関数を見ていても、主張が変わればレビュー対象は変わります。

外側にある条件を別の検証へ移せる場合もあります。要求IDの一意性をデータベース制約で確かめれば、その性質は機械で止められる側へ移ります。ただし、今度はスキーマ、接続先、移行手順が成立条件になります。検証は外側を消すのではなく、縮めたり、分割したり、別の場所へ移したりします。

ここまで書くと、保証の外側をすべて読めばよいように見えます。しかし、それでは実装全体を読むレビューへ戻るだけです。優先して読むのは、今回変わった前提、壊れた時の損失が大きい条件、実環境で崩れやすい仮定、独立した証拠がない判断です。人間の注意を減らすのではなく、結果の意味を変え得る箇所へ寄せます。

レビューは緑の結果を疑う仕事ではありません。その緑が何を意味するかを確定する仕事です。

20項目から見えた成立条件は、4つの問いで探せます。意図を要求へ移す時に意味を取り違えていないか。現在の検証は必要な性質を表現できているか。要求に書き切れない動作を、型や権限制限で起こしにくくできるか。そして、検査条件を見ながらコードを生成するAIエージェントが、その死角へ誤りを残していないかです。これは部品を4種類へ仕分ける分類ではありません。保証の意味を変え得る条件を、意味、表現、制限、誤りの偏りという4方向から探す問いです。

第1層と第2層で、現在の保証が言えていないことを見つけます。第3層では、検査項目を増やす代わりに、望ましくない実装を書ける範囲を狭めます。第4層では、検査を通るまで生成を繰り返すことで、どの誤りが選び残されるかを疑います。4層を通じて問うのは、「何を確認したか」だけではありません。「その確認を意味あるものにするため、何を人が引き受けたか」まで読みます。

Rustで送金処理を実装するなら、まず保証を一文にします。「同じ要求IDと同じ内容の再送では、残高を再更新しない」のように書きます。次に、対象がtransitionだけなのか公開APIまで含むのかを確かめます。そのうえで、A-1からA-4、テストの期待値、Proptestの生成範囲、kani::assumeassert_eq!を辿ります。Rustコンパイラ、Kani、接続する認証基盤や永続層は、主張に応じて成立条件へ加えます。

Kaniでtransitionの残高保存を確認した後は、同じ式を人が全入力について追い直す価値が小さくなります。代わりに、送金の正しさを残高合計だけで表してよいか、仮定が実環境でも成立するか、検証対象の関数と本番経路が一致するかを読みます。実装変更が検証対象を迂回できる場合や、ツールの設定が変わる時は、実装本体も引き続きレビュー対象です。

第1層: 意図と仕様の意味のずれを確認する

実装が形式仕様を満たすと数学的に示せれば、標本しか見ないテストより強い保証を得られます。ここだけを見れば、証明がソフトウェアの正しさを確定するように思えます。しかし、機械が確認するのは、書かれた性質に対する実装の振る舞いです。その性質が利用者や事業の意図を捉えているかは、検証結果からは分かりません。

Rustで保存則を検査するなら、送金元と送金先の合計が変わらないことは確認できます。しかし、送金先を取り違えた処理でも、2口座の合計だけなら保存できます。そこで、送金元と送金先それぞれの差分、所有者、要求ID、失敗時の無変更を別の性質として確認します。

形式証明の価値は、意図まで自動で保証することではなく、「実装は正しいか」という巨大な問いを「仕様は意図を表しているか」という短い問いへ圧縮することにあります。

この移動は、人が判断する対象を圧縮します。transition全体より、3つの仮定と5つの表明の方が短ければ、実装の分岐を逐語的に追うより意図との対応を調べやすくなります。ただし、仕様が実装の分岐を一行ずつ言い換え、同じ複雑さを持ち始めたら利得は薄れます。レビューではコード量に加え、要求、テストの期待値、モデル検査の仮定が増え続けていないかを見ます。同じ規則を別の形式で複製せず、反証したい性質を短く表せているかが基準です。

第2層: 現在の検証が表現していない性質を探す

テストと有限入力を使うファジングが得意なのは、ある実行で起きた悪い結果を示すことです。残高が負になる、panicする、拒否時に台帳が変わる、といった性質は反例を1つ出せば破れたと分かります。この種の性質は安全性(safety)と呼ばれます。

一方、「受け付けた要求へいつか応答する」「ロックを待つ処理がいつか進む」といった活性(liveness)は、有限時間だけ観測して何も起きなかったことから、将来も起きないとは断定できません。違いは、有限の実行断片だけで違反と確定できるかです。

https://www.sciencedirect.com/science/article/pii/0020019085900560www.sciencedirect.com

ここでは有限実行を扱う現在のテストとファジングに限って考えます。

この区別を送金処理に戻すと、Kaniへ単一のtransitionだけを渡しても、再送処理が最終的に完了するか、複数インスタンス間で処理が進み続けるかは表現していません。

さらに、1回の実行だけでなく、複数の実行結果の関係として定義する性質があります。たとえば秘密値だけを変えた2回の実行で、公開出力が変わらないことを調べる情報流は、単一の実行結果だけでは判定できません。この分類はハイパープロパティ(hyperproperty)と呼ばれます。非干渉性(noninterference)や観測的決定性(observational determinism)が例です。

https://www.cs.cornell.edu/fbs/publications/1813-9480.pdfwww.cs.cornell.edu

単独の実行トレースを何本集めても、トレース間の関係を表明しなければ、この性質を直接には確認できません。

分類名を覚えることが目的ではありません。現在の検証が1回の実行だけを見るのか、時間の経過まで扱うのか、複数の実行を比較するのかを問い直せれば、緑の結果から何を言えないかが見えてきます。

分類名を付ければ、自動的に人の仕事になるわけではありません。モデル検査へ並行状態を与えれば、デッドロックや無限循環を扱える場合があり、2つの実行を関係づける検証器もあります。秘密値による応答時間の差を確認するなら、比較する2つの実行と観測する時間を検証側へ表します。一方、ログ漏えいは出力を観測できれば有限の安全性として、資源リークも上限超過として検査できる場合があります。項目名から担当を決めず、どの実行を何本比較し、どれだけの時間を含む性質かを先に確認します。

第3層: 検査する前に、書けるプログラムを狭める

情報漏洩や未認可の操作を検査項目として列挙すれば、既知の禁止事項は繰り返し確認できます。しかし、「それ以外の副作用を起こさない」という要求が排除する事象を、有限の検査項目だけで書き切ることは困難です。検査を増やすだけでは、まだ列挙していない望ましくない動作が残ります。そこで、書かれた処理を後から調べるだけでなく、誤った状態や操作を表現しにくくします。

検査が書かれたプログラムを評価するのに対し、型や権限制限は書けるプログラムの空間そのものを変えます。

Rustで実装するなら、ActorIdAccountId、非公開フィールド、AuthorizedTransferによって値や処理順の候補を狭めます。クレートの先頭には次の指定を置きます。

#![deny(missing_docs)]
#![forbid(unsafe_code)]

forbidは同じlintの許可指定による上書きを認めないため、このクレート内でunsafeを使う変更をコンパイル時に止めます。

doc.rust-lang.org

Rustで実装するなら、これは「unsafeが安全か」を変更ごとに読み解く仕事を、「unsafeを受け入れない」というクレート単位の制限へ変えます。

ただし依存クレート、コンパイラ、OSまでunsafeが消えるわけではありません。型で認可の順序を固定しても、上流が偽のActorIdを渡す可能性は残ります。制限を置いたら、何が表現不能になり、何が外側へ押し出されたかを対で確認します。権限を持つ値を生成できるモジュールの限定、外部通信を持つモジュールの分離、依存の許可リスト、unsafeを使う専用クレートの隔離も同じ考え方です。

第4層: 全検査を通る誤りの形を考える

ここまでの三層は、人間とAIエージェントのどちらが実装しても残ります。AIエージェントを使うと、さらに検証網と残存エラーの関係を疑う必要があります。公開されたテストを見ながら修正を繰り返すと、テストへ適合しても、テストが表していない要求を満たさない実装が残り得るからです。2026年のプレプリントで提案されたSpecBenchは、コーディングエージェントにおいて、公開テストと非公開テストの成績差を測るために作られたベンチマークです。

arxiv.org

これはすべての生成コードが検査を攻略するという証明ではありません。それでも、生成側が検査条件を観測できる工程では、残る誤りが検査の死角へ偏る可能性を前提にした方が安全です。

コードの生成主体が人間からAIへ変わっても、従来と同じ検証網なら同じように誤りを捕捉できる、という見方があります。しかし、検査条件を見ずに生じた誤りと、公開された検査へ適合するまで生成を繰り返した後に残る誤りを、同じ分布とみなすことはできません。後者では、検査が見つける誤りは反復の途中で除かれ、検査が見ていない誤りほど選び残されやすくなります。

AI生成コードでは、「すべて緑」を無謬性の証拠ではなく、残った誤りがどこへ偏ったかを考える出発点として読みます。

ループエンジニアリングとして見ると、この偏りは1回の生成で終わりません。検査を通るまで修正する流れでは、検査が見ていない誤りは反復のたびに選び残されます。さらに「すべての検査が緑になったら完了」を停止条件にすれば、利用者の意図から外れていても安定した到達点に見えます。検査項目だけでなく、検査結果を次の生成へどう戻し、何を完了とみなすかまでレビューします。

レビューの問いは「この関数は正しそうか」だけでなく、「現在の全検査を通しながら、どんな誤りを残せるか」になります。Rustで送金処理を実装するなら、合計を保存したまま送金先を取り違える変更を考えます。同一要求をReplayedとして返しながら残高を更新する変更も候補です。さらに、ファジング対象ではないauthorizeで状態を変える変更を試します。既存の証拠で検出できるかを確認し、検出できなければテストか制限を足します。

AI生成コードの品質保証は、成果物を検査するだけでなく、検証網そのものを敵対的に分析する仕事です。

ミューテーションテストは、演算子や分岐を機械的に変えた実装をテストが検出できるかを見る手法です。テストの強さを評価する手掛かりにはなりますが、「検査を通る誤ったプログラム」全体の大きさを測るものではありません。選んだ変異に対する感度の測定です。

discovery.ucl.ac.uk

たとえばchecked_subchecked_addへ変える、再送時の内容比較を外す、失敗後の残高表明を消す、といった変異を作ります。これらを生き残らせないかを見ると、要求とテストの対応を具体的に評価できます。生成時に見せない受け入れテストも役立ちます。別担当者が要求から作る攻撃ケースも、この偏りを減らします。

人が引き受ける成立条件を、検索できる形にする

検査の自動化を成熟度とみなすと、人手が残る箇所は順次機械へ置き換えるべきだと考えがちです。しかし、仕様が意図を捉えているかという判断まで検証装置へ移せるわけではありません。問題は人間の関与量ではなく、その注意をどこへ置くかです。

アーキテクチャは、希少な人間の注意をどこへ配るかを決める装置でもあります。

人間の判断が必要な箇所は、意味として小さいだけでなく、rgと差分で見つけられる必要があります。要求ID、仮定、許可、依存を、検索できる有限の記述として局在させます。Rustで実装するなら、前提はREQUIREMENTS.mdのA-1からA-4、モデル検査の範囲はkani::assumeassert、対象外と残存リスクはSECURITY.mdへ置きます。#![forbid(unsafe_code)]、依存ファイル、品質ゲートのスクリプトも、保証の範囲を検索できる場所です。

自動化の目的は人間を消すことではなく、人間の注意を判断が必要な差分へ集中させることです。

この配置なら、通常の機能差分とは別に、前提、仮定、依存、許可、検証コマンドが変わったかを先に見られます。保証の成立条件に関わる差分が空なら、人の注意を減らす材料にはなります。ただし、それだけで安全とは判断しません。通常のコード変更が既存の仮定を悪用したり、新しい性質を要求したりすることがあるためです。差分はレビューの通過条件にせず、深く読む場所を決める選別信号として使います。

仕様と検証コードを、新しいレビュー単位にする

仕様と検証コードは、新しいソースコードです。

要求、テスト、Kaniの仮定をAIエージェントが書けば、それらも誤り得ます。それを検証する別の仕組みを足しても、今度はその仕組みの論理、検査器、設定を信頼する必要があります。小さな証明検査器(proof checker)を用い、信頼する実装を減らす考え方はありますが、何を性質として選ぶかまで消えるわけではありません。

collaborate.princeton.edu

AIがコードを生成し、型検査、テスト、証明が通るなら、人間によるレビューは付加価値を失ったように見えます。機械が保証した性質について、実装を一行ずつ読み直す工程は縮められます。しかし、仕様と意図のずれ、検証が表現していない性質、書かれていない禁止事項は残ります。

レビューを廃止するのではなく、読む対象を実装全体から、仕様、境界の型、検証の仮定、依存の許可リストへ移します。

どこかで「この要求が利用者の意図を表す」「この仮定を運用が守る」と人が判断します。その打ち切り点がレビュー対象です。Rustで実装するなら、FR、QR、BRの文章とA-1からA-4が対象です。テストの期待値と生成器、kani::assumeassertも含みます。境界の型と依存変更も見ます。これは補助文書ではありません。可読性、複雑さ、変更容易性を確認し、意図との対応を実装コードと同じ厳しさで見ます。

一本につなぐと、検証は主張できる範囲を明確にし、その外側へ成立条件を残します。性質の分類は現在の証拠で言えないことを示し、型や権限制限は誤った実装を書ける範囲を狭めます。文書とコードの配置は、人が引き受けた判断を見つけやすくします。レビューは廃止しません。レビュー単位を実装全体から意図、仕様、仮定、実環境との接続へ寄せるのが狙いです。

本を読んでも、品質ゲートを増やしすぎない

人が引き受ける成立条件を見つけやすくしても、検証を増やすたびに、仮定、入口、性能条件、追跡情報という新しい保守対象が生まれます。すべての検証を別の専門チームへ渡し、結果が返るまで待つなら、レビュー待ちを検証待ちへ置き換えただけです。人が読む場所を絞るには、検証の追加そのものにも費用と役割を問う必要があります。

以前、『Architecture for Flow』の感想文で、機能は作った後も維持、確認、説明の負担を生むと書きました。

syu-m-5151.hatenablog.com

検証資産も同じです。数を競えば、AIによるコードの過剰生産を、品質ゲートの過剰生産へ置き換えることになります。

私は、型検査と通常テストを短いフィードバックとして返し、Kaniは危険な純粋関数に限定します。開発中に回すファジングと時間をかける検証は分け、独立したセキュリティ評価も別のジョブにします。変更を所有するチームが自分で実行でき、専門家は検証方法の改善と高リスク変更の判断を支援する形を選びます。

コメントと型シグネチャだけで判断できる範囲を狭くする

検証の役割を絞った次の問いは、その証拠を使って人間の読み方をどこまで変えられるかです。コメントと型シグネチャを入口にマージ判断する方針を採る場合も、実装本体を読まなくてよいわけではありません。次の3条件を満たす低リスクな変更に限り、本体の逐語的な確認を減らします。

  1. 変更する要求IDから、公開API、受け入れ条件、専門的な検証まで辿れる。
  2. 変更が壊し得る性質を検出するゲートを、最終差分に対して実行している。
  3. 認証、永続化、分散実行など、検証していない範囲へ変更が広がっていない。

高リスクな変更、証拠が弱い変更、対象範囲が動く変更では実装本体も読みます。独立したセキュリティ評価も外しません。レビューは残し、変更の危険度に応じて読む場所と深さを変えます。

まだ決めていないことを、見える形へ出す

読み終えて、要求工学をコード生成の前工程だけとは考えなくなりました。実装と一緒に管理する対象は、作るもの、判断者、正しさの観測方法です。変更時に再確認する範囲も含みます。本書の20項目は、最初に完成させて引き渡す文書ではなく、開発中に判断の抜けを見つける順序として使えます。

第3版と『Essentials』の関係は、大きい本を小さく要約しただけではありません。第3版は、要求工学がプロジェクト全体のどこに関わり、状況によって何を変えるかを広く示します。『Essentials』は、その広い領域を20の判断へ並べ直し、データ要求、要求分析、要求テストのように、現在の実務から見直した問いを前へ出します。私は『Essentials』で抜けを見つけ、第3版で背景と別の選択肢を調べるという往復で使います。

agnozingdays.hatenablog.com

この往復をRustの送金処理へ当てるなら、要求は文書の章立てではなく、コードと証拠を読む順序になります。RequestIdという型を見たら、一意性と保持期間を問います。Kaniの表明を見たら、証明できたことより、仮定へ押し出したことを問います。20項目を実装へつなぐとは、成果物を増やすことではなく、次に読むべき場所を決めることです。

型、テスト、Kani、ファジングは、人間の判断を置き換えません。決めた規則から実装が外れた時に止める道具です。その道具が止められない範囲も記録します。自動ゲートがすべて緑でも、レビュー担当者が15分以内に理解できるかは、まだ人に試してもらう必要があります。未評価の要求を未評価のまま残せることも、要求を書く実利です。

自分の仕事へ持ち帰る時、20項目を一度に導入する必要はありません。まず変更予定の機能について、解きたい問題、避けたい失敗、判断する人、観測できる結果、まだ確かめていない前提を1つずつ書きます。コードを書く前に全部決めるのが目的ではありません。未決事項を実装済みの事実に見せないところから始めます。

私たちは、明確なゴールを決めずにコードを書き始めていた。

これからも、コードはすぐに書き始める。

ただ、まだ決めていないことだけは、先に見える形へ出しておく。