テスト・レビュー代表記事全11節Verify

AIの実装を検証し、不具合を修正する

この記事で身につくこと「AIができたと言った」と「要件を満たすと確認した」を区別し、要件から期待結果を先に決めて実装を検証し、不備を修正させ、証拠を残せる。

記事内目次(28項目)
この記事の管理情報(該当工程・関連要件・成果物の閲覧と保存・実行記録)
記事ID
SAMPLE-B
該当工程
09-02 AIの実装を業務の流れで検証する(関連:05-02、05-03、08-04、09-01、09-03)
関連要件
REQ-003、REQ-004、REQ-005、REQ-006、REQ-007
到達目標
「AIができたと言った」と「要件を満たすと確認した」を区別し、要件から期待結果を先に決めて実装を検証し、不備を修正させ、証拠を残せる
主要な成果物
確認環境と検証状況
最小実験は実行確認済み(rev1、2026-09-18、macOS 26.6.2 / Python 3.9.6 / SQLite 3.51.0)。GPT による別環境(Linux / Python 3.13.5)での再実行と反例確認を受けて検査を強化。Phase 4D-R6(Progressive Disclosure)適用後、ユーザーによる実読で PASS。
未検証の範囲
同時操作、本物の本人確認、複数役割、画面経由の操作、本番保存先、対応しない接続設定での取消、差戻しの重複、性能、サポート中の Python での実行。
実行記録

正本の原稿にある管理表(記事ID・先に読むと理解しやすい記事・成果物パス・最終更新日など)は、この下の本文冒頭に「原稿の管理表」として開閉できる形で残しています。

原稿の管理表(13項目:記事ID・カテゴリ・要件ID・先に読むと理解しやすい記事・成果物パス・対象ツール・検証状況・最終更新日など)
管理項目内容
記事IDSAMPLE-B
主な中カテゴリ09-02 AIの実装を業務の流れで検証する
関連中カテゴリ05-02、05-03、08-04、09-01、09-03
関連要件IDREQ-003、REQ-004、REQ-005、REQ-006、REQ-007
先に読むと理解しやすい記事SAMPLE-A(要件と受入条件)。08-01(最小の処理を動かす)を読んでいるとコードが追いやすいが、未読でも本文で説明する
到達目標「AIができたと言った」と「要件を満たすと確認した」を区別し、要件から期待結果を先に決めて実装を検証し、不備を修正させ、証拠を残せる
3層の配置理解する=第3〜4節/作って確かめる=第5〜7節/設計を判断する=第8節
成果物パスexamples/sample-b/(コード)、evidence/sample-b/run_log.md(初版の実行記録)、evidence/rev1-2026-09-18/sample-b/run_log.md(改訂版の実行記録)、templates/verification_matrix_template.md、templates/verification_matrix_example.md
出典第11節参照
対象ツール・バージョンPython 標準ライブラリのみ(sqlite3、unittest)。外部部品は使わない。実行記録は macOS 26.6.2 / Python 3.9.6 / SQLite 3.51.0。Python 3.9 は公式サポート終了済み(2025-10-31)のため、公開教材として推奨する環境ではない。サポート中の版(3.12・3.13 など)での実行はこの記事では未確認
検証状況最小実験は実行確認済み(初版 2026-09-18、改訂版 rev1 同日)。GPTによる別環境(Linux、Python 3.13.5)での再実行と反例確認を受けて検査を強化。Phase 4D-R6(Progressive Disclosure)適用後、ユーザーによる実読でPASS。同時操作・本物の本人確認・画面・本番保存先は未検証
残課題第11節参照
最終更新日2026-09-18(改訂版 rev1)。Phase 4D-R5:非エンジニア向けに導入と用語の説明順序を改善。テスト・結果・結論は不変。2026-09-21(Final Content Audit):第6.3節の技術詳細に、掲載コードが抜粋であることの断りを1文追加。コード・テスト・結果は不変。2026-09-21(Final Content Audit:読む順路の「本文は今後制作予定」(全項目に本文がある現在は誤り)を削除。本文は不変)

この記事の会社・人物・データは架空の設定です。「AIの初稿」として示すコードは、教えるために意図的に不備を入れた説明用の失敗例であり、実際のAIの出力を観測したものではありません。コードと本文はClaude Codeが作成しました。

この記事は何の話か。テスト(検証)の話です。テストとは、AIが作ったものが本当に要件どおりに動くかを、自分で試して確かめることです。そのために、「こう操作したら、こうなるはず」という答えを先に決めておき、実際に動かして答え合わせをします。

なぜ必要か。AIは「動作確認済みです」と言いますが、それはAIが思いついた範囲を試しただけです。確かめずに使うと、権限のない人が承認できてしまう、承認の記録が残らない、といった不備が業務で見つかります。

この記事を読むとできるようになること。

  • 「AIができたと言った」と「要件どおりだと確認した」の違いを、自分の言葉で説明できる
  • 要件から「こう操作したら、こうなるはず」の表(期待結果)を先に作り、実装を動かして照合できる
  • 失敗したときに、テストではなく実装を直すようAIに指示し、直ったことを記録として残せる

読む順路。この記事は3つの深さで読めます。

  • 基本だけ理解する:第1節→第3節→第4節→第5.2節の表(何を確かめるか)→第6.1節と第6.4節の結果の日本語説明→第7節の「確かめていないこと」。コードを読まなくても、「AIができたと言った」と「確認した」の違い、テストが何を検出したか、何が未検証かが分かります。
  • 作って確かめる:上に加えて第5.1節(ファイルと実行方法)→第5.3〜5.5節→第6.2〜6.3節(修正指示とコード)→第6.5節(テストの検出力)。
  • 設計判断を深める:上に加えて第8節。

1. 今回の業務課題と完成するもの

この記事でやることは、AIが作った承認処理を、記事Aの要件から作った12の場面で試し、不備を見つけて直させ、直ったことを記録に残すことです。

記事Aで、架空企業「みらい業務サービス」の依頼・承認管理アプリの要件と受入条件を作りました。その後、AIに承認処理を実装してもらった、という設定で話を進めます。AIは「承認処理を実装しました。動作確認済みです」と言っています。

ここで問いが1つあります。「動作確認済み」とは、何を確認したことなのか。

AIが確認したのは、多くの場合「正常な承認が通ること」だけです。割当外の人が承認できてしまわないか、自分の依頼を自分で承認できてしまわないか、承認ボタンを2回押したら履歴が2件になってしまわないか、処理の途中で保存に失敗したらデータが半端な状態で残らないか。これらは、要件を知っている人が、「こうなるはず」を先に決めて試さない限り、確認されません。

確認の流れこの記事では
何を確認するのか承認処理が、正常な承認だけでなく、権限なし・自己承認・状態外・重複・途中失敗・入力不足でも要件どおりに振る舞うか
なぜ確認が必要かAIの「動作確認済み」は、AIが思いついた範囲の確認にすぎないから
何を見れば正しいと言えるか要件から先に決めた期待結果と、実際の結果(戻り値と保存内容)が一致すること
どう検証するか期待結果をテスト(自動で照合するプログラム)にして、実装に対して実行する
結果をどう判断するか全件一致なら「この12場面では要件どおり」。不一致なら実装を直す。確かめていない範囲は別に書く

この記事を読み終えると、次のものが手元にできます。

  • 要件から先に決めた期待結果と、それを実行できる形にしたテスト(原版8件+追加4件の12件)
  • 説明用の失敗例(不備入りの承認処理)に対して、テストが不備を検出した実行記録
  • AIへの修正指示の文章と、修正前後のコードの比較
  • 修正後に全テストが通った実行記録
  • この実験で確かめた範囲と、確かめていない範囲の一覧
  • 要件・テスト対応表と修正記録のひな形(空欄版と記入例)

2. 前提知識と読む順路

この記事で使う言葉を先に説明します。言葉を覚える必要はありません。「何のことを言っているか」が分かれば十分です。

テストと期待結果

一言でいうと、テストは「こう操作したら、こうなるはず」を先に決めておき、実際に動かして答え合わせをする作業です。その「こうなるはず」が期待結果です。

たとえば、「割当外の承認者が承認しようとしたら拒否される。依頼と履歴は操作前と同じまま」が1つの期待結果で、それを自動で確かめるプログラムがテストです。

これがないと、AIが「動きました」と言った結果を見て「これでよいのだろう」と思うしかなく、答案を見て模範解答を書くのと同じで、検証になりません。

システム開発では、期待結果は実装を動かす前に要件から決めます。記事Aの受入条件AC-001〜004とAC-008が、この記事の期待結果になります。

実行結果と証拠

一言でいうと、実行結果はテストを実際に動かして出た答え、証拠はその答えが本当に出たことを示す記録です。

たとえば、テストを実行すると「T2 … ok」「T3 … FAIL」のように1件ずつ結果が出ます。この出力をそのまま保存したものが証拠(実行記録)です。

これがないと、「テストは通ったはず」と「テストが通った」を区別できません。あとから第三者が確かめることもできません。

システム開発では、実行環境(OSやPythonの版)と日時を添えて記録を残し、実行していないものは「未検証」と書きます。

成功と失敗(ok / FAIL)

一言でいうと、期待結果と実行結果が一致すれば成功(ok)、違えば失敗(FAIL)です。

たとえば、不備のある実装に対してテストを実行し「FAIL」が出るのは、実験の失敗ではなく、テストが不備を見つけたという意味です。

これがないと、テストが失敗したときに、実装が悪いのか、テストが悪いのか、要件が悪いのかを区別せずに「直す」ことになります。

システム開発では、失敗したときに直してよいのは原則として実装です。実装の出力に合わせてテストを書き換えるのは、検証をやめることと同じです(第3.2節)。

トランザクション

一言でいうと、複数の保存を「全部成功するか、全部なかったことにするか」のどちらかにする仕組みです。

たとえば、承認では「状態を承認済みに変える」と「承認の履歴を1件追加する」の2つを1つのまとまりにします。

これがないと、途中で失敗したときに「承認済みなのに承認の記録がない」依頼が生まれ、誰が承認したかを説明できなくなります(記事Aの不変条件1)。

システム開発では、これをトランザクション(transaction)と呼びます。この記事では、保存先(SQLite)の設定に頼って実現するため、設定が違うと取り消せないことも扱います(第6.3節)。

コードを読むときの言葉

  • SQLite(エスキューライト)は、1つのファイルの中にデータベースを持てる軽量なデータベースです。この実験では、テストのたびにメモリ上に作って捨てます。
  • unittest(ユニットテスト)は、Pythonに最初から付いているテストの仕組みです。テストごとに「成功」「失敗」を表示してくれます。
  • SQL(エスキューエル)は、データベースに「この条件の行を更新して」「この行を追加して」と頼むための言語です。この記事では UPDATE(更新)と INSERT(追加)が出てきます。
  • 関数と引数:関数は「入力を受け取って処理をして結果を返す」ひとまとまりのコードです。引数はその入力です。今回の approve(conn, "REQ-A01", "u-suzuki") は、承認関数に「接続、依頼ID、操作者ID」の3つを渡しています。
  • assertion(アサーション)は、テストの中で「この条件が成り立つはず」と書いた検査です。成り立たなければテストが失敗します。
  • 終了コードは、コマンドが終わるときに呼び出した側へ返す数字です。0が成功、それ以外が失敗という約束です。人が画面を見なくても、成功か失敗かを他のプログラムが判断できます。
  • カレントディレクトリは、今コマンドを実行しているフォルダです。この記事のコマンドは examples/sample-b/ の中で実行する前提なので、先に cd examples/sample-b でそこへ移動します。

読む順路:要件と受入条件の作り方は記事A。コードの読み方(変数、関数、条件分岐)が初めての方は 08-01 が該当します。この記事のコードは短く、各行の意味を本文で説明します。

3. 理解する:「できた」と「確認した」の違い

3.1 誰が期待結果を決めるか

AIに実装を頼むと、AIは動くコードと「確認しました」という言葉を返します。この言葉には2つの限界があります。

  1. AIが確認した内容は、AIが思いついた範囲に限られる。要件を渡していなければ、要件にある失敗の場面は試されない。
  2. AIが実装と確認の両方を行うと、実装の思い込みが確認にも入る。「承認者の割当を確認する必要はない」と思って実装したAIは、割当外の承認を試さない。

だから、期待結果は要件から、実装とは別に決めます。記事Aの受入条件AC-001〜004とAC-008が、そのまま期待結果になります。

3.2 よくある誤解

「テストが通った=安全」。テストは、書いた期待結果の範囲だけを確かめます。この記事の12件が通っても、同時に2人が承認したときのことは分かりません。何を確かめたかと同じ重さで、何を確かめていないかを書きます。

「AIにもう一度確認させれば独立したレビューになる」。同じAIに「本当に大丈夫か確認して」と頼むと、多くの場合「大丈夫です」と返ってきます。それは同じ前提の上での再確認であって、独立した保証ではありません。要件から作ったテストを実行する、別の人が読む、が独立した確認です。

「失敗したテストは直せばよい」。テストが失敗したとき、直す対象は実装か、テストか、要件かの3通りあります。実装の出力に合わせてテストを書き換えるのは、検証をやめることと同じです。テストを直してよいのは、テストが要件を誤って写していたときだけで、そのときは要件との照合を記録します。

3.3 今回の対象:承認処理の小さな例

対象は、依頼を承認する関数 approve と、差し戻す関数 send_back です。画面もログインもありません。「誰が操作しているか」は、操作者のIDを引数で渡す代用です。これは本人確認(認証)を実装した証拠にはなりません。保存先はメモリ上のSQLiteです。

データは記事Aと同じ架空の設定です。

依頼ID依頼者件名状態割当承認者
REQ-A01佐藤会議室プロジェクタ手配提出済み鈴木
REQ-A02佐藤名刺の追加発注下書き鈴木
REQ-A03鈴木備品棚の移設提出済み鈴木(自己承認の検査用)
REQ-A04佐藤社用車の予約承認済み高橋
REQ-A05田中来客用駐車場の確保提出済み運用管理者(承認者でない人を誤って割当。役割の検査用)

利用者は、依頼者の佐藤・田中、承認者の鈴木・高橋、運用管理者の5人で、この最小実験では1人1役割です。REQ-A03とREQ-A05は、わざと異常な割当にしたデータです。処理がそれを拒否できるかを確かめるために置いています。

画面と紙の確認項目を見比べ、違いを一緒に確かめる同僚たち。架空の確認作業のイメージで、実行結果の画面ではない。

4. 判断すること

この検証で決めることは4つです。

  1. 何を確かめるか:要件REQ-003〜007から、正常・無権限(割当外)・自己承認・対象外状態・重複・途中失敗・入力不足の7種類を確かめる。改訂版では、役割(操作者が承認者か)、保存先が関係を守るか、保存先の状態条件、接続の設定の4つを加える。
  2. 期待結果をどこから取るか:記事Aの受入条件と設計書の要件から。実装からは取らない。
  3. 失敗の影響をどこまで見るか:「エラーが返る」だけでなく、「依頼と履歴の全行が操作前と同じであること」まで見る。初版は「承認履歴が増えていないこと」しか見ておらず、拒否のついでに別の履歴を消すような誤りを見逃す弱さがあった(第6.5節)。
  4. AIに任せる範囲:テストの実行、失敗の原因の説明、修正案の作成、修正後の再実行はAIに任せる。期待結果の決定と、修正が要件どおりかの最終判断は人が行う。

失敗の影響について補足します。承認処理の不備は、「承認されるべきでないものが承認される」(統制の破れ)と、「承認の証拠が残らない」(説明責任の破れ)の2つの形で業務に影響します。どちらも、画面にエラーが出たかどうかでは分かりません。保存されたデータを見る必要があります。

5. 作って確かめる

5.1 準備

examples/sample-b/ に5つのファイルがあります。

ファイル役割
common.pyテーブルの定義と、架空データの投入。テストごとにメモリ上のSQLiteを作り直す。外部キー検査を有効にする
approval_v1.py説明用の失敗例。「AIの初稿」という設定。改訂版でも中身は変えていない
approval_v2.py修正後(改訂版で役割の確認と接続設定の確認を追加)
test_approval.py要件から作ったテスト12件。原版8件(T1〜T8)と追加4件(T9〜T12)を分けて実行できる
mutation_check.pyテストの検出力の確認。故意に誤らせたコピーを一時フォルダに作り、テストが検知するかを見る

実行に必要なのはPythonの標準ライブラリだけで、追加のインストールは要りません。この記事の実行記録はPython 3.9.6で取りましたが、3.9は公式サポートが終了しています。新しく環境を用意するなら、サポート中の版(3.12や3.13など)を使ってください。ただし、サポート中の版でこのコードを実行した記録は、この記事にはありません(未確認)。

5.2 期待結果を先に決める(要件・テスト対応表)

この節でやることは、実装を動かす前に、12の場面それぞれについて「こう操作したら、こうなるはず」を表にすることです。この表がこの記事の答案用紙で、あとの実行結果はすべてこの表と照合します。

12の場面を、業務の言葉でまとめると次のとおりです。「こうなるはず」の右側が、テストが確かめる答えです。

場面何を試すかこうなるはず
正常割当承認者が提出済みの依頼を承認する/理由を書いて差し戻す承認済み(差戻し)になり、誰が・いつの記録が1件増える
権限なし割当外の人・依頼者本人・承認者でない人が承認する拒否。依頼も記録も操作前と同じまま
状態外下書きの依頼を承認する拒否。下書きのまま
重複同じ承認を2回する2回目は拒否。承認の記録は1件のまま
途中失敗状態を変えた直後に保存が失敗する承認済みにならず、記録も増えない(片方だけ残さない)
入力不足理由を書かずに差し戻す拒否
保存先の設定関係を守る設定が効いているか。取り消せない設定の接続で承認する保存先が拒否する。処理の前に拒否する

「拒否」の場面で大事なのは、エラーが返ることではなく、依頼と履歴が操作前と1行も変わっていないことまで確かめる点です。エラーが出たのに裏でデータが変わっていた、という不備を見逃さないためです。

12件(T1〜T12)それぞれの前提・操作・期待結果・保存内容で照合するものは、次の技術詳細にあります。原版8件(T1〜T8)と追加4件(T9〜T12)に分かれています。

技術詳細要件・テスト対応表(T1〜T12)

「保存内容で照合するもの」の列が、改訂版で強化した部分です。戻り値(関数が返す成功/失敗)だけでなく、保存先に残ったデータを調べます。

テストID要件種類前提操作期待結果保存内容で照合するもの
T1REQ-003, 005正常REQ-A01 提出済み鈴木が承認承認済みになる履歴の最後の1行が(承認、鈴木、固定時刻、備考なし)。既存の履歴は不変
T2REQ-003無権限(割当外)同上高橋(割当外)が承認拒否依頼と履歴の全行が操作前と同一
T3REQ-003自己承認REQ-A03(依頼者=承認者の異常データ)鈴木が承認拒否全行が操作前と同一
T4REQ-003対象外状態REQ-A02 下書き鈴木が承認拒否全行が操作前と同一
T5REQ-006重複REQ-A01鈴木が2回承認2回目は拒否1回目の後と2回目の後で全行が同一。承認履歴は1件
T6REQ-007途中失敗REQ-A01状態更新の直後に保存失敗を模擬失敗全行が操作前と同一(状態も履歴も戻っている)
T7REQ-004入力不足REQ-A01鈴木が理由なしで差戻し拒否全行が操作前と同一
T8REQ-004, 005正常REQ-A01鈴木が理由ありで差戻し差戻しになる履歴の最後の1行が(差戻し、鈴木、固定時刻、理由)。既存の履歴は不変
T9REQ-003役割REQ-A05(承認者でない運用管理者を割当)運用管理者が承認拒否全行が操作前と同一
T10(保存先の関係)関係の保護存在しない依頼ID履歴を直接追加保存先が拒否する外部キー検査が1(有効)であること
T11REQ-003, 006保存先の状態条件REQ-A02 下書き。処理側の確認を素通りさせる鈴木が承認失敗全行が操作前と同一(UPDATEの状態条件と更新件数の検査で止まる)
T12REQ-007接続設定自動確定モードの接続鈴木が承認変更前に拒否全行が操作前と同一

固定時刻:T1とT8で「時刻が期待どおり」を確かめるため、テストは処理に固定の時刻(2026-09-18T12:00:00+00:00)を差し込みます。実行のたびに変わる現在時刻では照合できないからです。

T11の意味:修正後の処理は、まず処理側で状態を確認し(「提出済みでなければ拒否」)、さらに保存先のUPDATE文にも「状態が提出済みの行だけ」という条件を付けています。二重の守りです。通常の手順では処理側で止まるため、保存先の条件が効いているかは分かりません。T11は、テストの中でだけ処理側の確認を一時的に素通りさせ、保存先の条件だけで止まるかを確かめます。これは同時操作(2人が同時に承認する状況)を再現したものではありません。同時操作は引き続き未検証です。

T12の意味:修正後の処理は「失敗したら全部取り消す」を、接続の設定に頼って実現しています。設定が違えば取り消せません。T12は、取り消せない設定の接続を渡したとき、処理が何も変えずに拒否することを確かめます。

5.3 テストのコード

テストは、期待結果を1つずつ関数にしたものです。T2を例に示します。

1件のテストは、どれも同じ3手順です。操作の前に依頼と履歴の全行を控える → 承認処理を呼ぶ → 結果(拒否か成功か)と、保存内容が控えどおりかを確かめる。この3手順が12件分あります。

初版のテストは「承認の記録が増えていないこと」しか見ておらず、「拒否はするが、ついでに別の記録を消す」ような誤りを見逃していました。全行を照合する形に直したことで、その種の誤りも検知できます(第6.5節で実演します)。

技術詳細テストのコード(T2 の例と行ごとの意味)
def test_T2_not_assigned_approver(self):
    """割当外の承認者(高橋)が承認 → 拒否。依頼・履歴の全行が不変(REQ-003)"""
    before = full_snapshot(self.conn)
    r = self.mod.approve(self.conn, "REQ-A01", "u-takahashi")
    self.assertFalse(r.ok)
    self.assertUnchanged(before)

行ごとの意味は次のとおりです。

  • 1行目:テストの名前。T2で、割当外の承認者の話だと分かるようにしてある。
  • 3行目:操作の前に、依頼と履歴の全行を控えておく。
  • 4行目:承認処理を呼ぶ。操作者は高橋(u-takahashi)。
  • 5行目:結果の ok が偽(拒否)であることを確かめる(assertion)。
  • 6行目:依頼と履歴の全行が、3行目の控えと同一であることを確かめる。

初版のT2は6行目が「承認履歴が空であること」だけでした。それだと「拒否はするが、ついでに提出の履歴を消す」ような誤りを見逃します。全行の照合に変えたことで、その種の誤りも検知できます(第6.5節で実演します)。

5.4 説明用の失敗例(AIの初稿という設定)

approval_v1.py の承認処理です。教えるために意図的に不備を入れています。改訂版でも中身は変えていません。

この初稿は、「依頼があるか」だけを確かめて状態を承認済みに変え、そのあと別の保存で記録を1件足します。誰が承認しようとしているか、依頼が提出済みかは確かめていません。正常な承認(T1)は通るので、AIが「動作確認済み」と言うのは、この意味で嘘ではありません。

入力・処理・出力・失敗時で読むと、こうなります。

  • 入力:依頼ID、操作者ID
  • 処理:依頼があるかだけ確かめ、状態を承認済みに変えて確定(commit)、そのあと履歴を1件追加して確定
  • 出力:ok=True と「承認しました」
  • 失敗時:依頼がなければ拒否。それ以外の失敗は考えていない
技術詳細失敗例のコード(approval_v1.py)
def approve(conn, request_id, actor_id, _fault=None):
    row = conn.execute("SELECT state FROM requests WHERE id = ?", (request_id,)).fetchone()
    if row is None:
        return ApprovalResult(ok=False, reason="依頼が存在しない")

    # 不備: 誰が承認しようとしているかを一切確認していない。状態も確認していない。
    conn.execute("UPDATE requests SET state = '承認済み' WHERE id = ?", (request_id,))
    conn.commit()  # 不備: ここで状態変更だけ先に確定してしまう

    if _fault is not None:
        _fault()

    conn.execute(
        "INSERT INTO history (request_id, action, actor_id, at) VALUES (?, '承認', ?, ?)",
        (request_id, actor_id, _now()),
    )
    conn.commit()
    return ApprovalResult(ok=True, reason="承認しました")

5.5 実行手順

テストは examples/sample-b/ で、失敗例(approval_v1)に対して原版8件・追加4件、修正後(approval_v2)に対して12件、最後に検出力の確認を実行します。コマンドは次の技術詳細にあります。Pythonの標準ライブラリだけで動き、追加のインストールは要りません。

技術詳細実行コマンド

先に examples/sample-b/ へ移動します(カレントディレクトリをそこにする)。

cd examples/sample-b

失敗例に対して、原版8件と追加4件を分けて実行します。

python3 test_approval.py approval_v1 --suite original
python3 test_approval.py approval_v1 --suite additional

修正後に対しても同じように実行します。--suite を省くと12件まとめて実行します。

python3 test_approval.py approval_v2

テストの検出力を確かめるには、次を実行します。故意に誤らせたコピーは一時フォルダに作られ、終了時に消えます。

python3 mutation_check.py

6. 初稿・失敗例・修正

6.1 失敗例に対する実行結果(実行確認済み)

ここで見るのは、不備のある実装に対してテストを実行したときの出力です。読み方は1行1テストで、「ok」は期待どおり、「FAIL」は期待と違った、という意味です。ここでの FAIL は、テストが不備を見つけたという良い知らせです。

2026-09-18に改訂版のテストで実行した結果の要点です。全文は evidence/rev1-2026-09-18/sample-b/run_log.md にあります(初版のテストでの記録は evidence/sample-b/run_log.md に別に残しています)。

結果は、原版8件中6件(T2〜T7)、追加4件中2件(T9、T12)が FAIL、1件(T11)はこの版に該当する関数がないためスキップでした。

これは実験が失敗したのではなく、テストが不備を検出したという成功です。失敗の中身を日本語にすると次のとおりです。

  • T2・T3・T9:拒否されるはずの承認が通っている。割当外でも、本人でも、承認者でない人でも承認できてしまう。
  • T4:下書きが承認済みになっている。
  • T5:2回目の承認も通り、承認履歴が2件になっている。
  • T6:途中で失敗したのに、状態は承認済みに変わり、履歴がない。承認済みなのに誰が承認したか分からない依頼が生まれる。これがREQ-007の言う「片方だけ完了」です。
  • T7:理由なしの差戻しが通っている。
  • T12:取り消せない設定の接続でも処理が進んでしまう。
  • T10は失敗例でも通ります。これは保存先(common.py)の設定であって、承認処理の出来とは関係がないからです。
技術詳細実行結果の全文(approval_v1)

原版8件(検査を強化したもの):

=== 検証対象: approval_v1 / スイート: original ===
test_T1_normal_approval ... ok
test_T2_not_assigned_approver ... FAIL
test_T3_self_approval ... FAIL
test_T4_wrong_state_draft ... FAIL
test_T5_duplicate_approval ... FAIL
test_T6_partial_failure_rolls_back ... FAIL
test_T7_send_back_requires_reason ... FAIL
test_T8_send_back_with_reason ... ok
結果: 実行 8 件 / 失敗 6 / エラー 0 / スキップ 0
終了コード: 1

追加4件:

=== 検証対象: approval_v1 / スイート: additional ===
test_T10_foreign_key_enforced ... ok
test_T11_sql_state_guard ... skipped 'この版には処理側の確認関数がない'
test_T12_autocommit_connection_refused ... FAIL
test_T9_assignee_without_approver_role ... FAIL
結果: 実行 4 件 / 失敗 2 / エラー 0 / スキップ 1
終了コード: 1

6.2 AIへの修正指示

失敗したテストと要件を添えて、次のように指示します。テストは変えないことを明記します。

approval_v1.py の approve と send_back を、要件 REQ-003〜007 に合わせて修正してください。
test_approval.py の T2〜T7、T9、T12 が失敗しています。テストの期待結果は要件から決めたものなので、テストの方を変えないでください。

直してほしい点:
1. 操作者の役割が「承認者」でなければ拒否する。役割の確認と、その依頼に割り当てられているかの確認は別の条件として書く。
2. 操作者が依頼に割り当てられた承認者でなければ拒否する。依頼者本人なら自己承認として拒否する。
3. 対象の状態が「提出済み」でなければ拒否する。UPDATE 文にも状態の条件を付け、更新件数が1でなければ処理を取り消す。
4. 状態の更新と履歴の追加を同じトランザクションで行い、途中で失敗したら両方取り消す。
5. 差戻しは理由が空なら拒否する。
6. 取消を保証できない接続設定(自動確定モード、未確定の処理が残っている接続)では、何も変更せずに拒否する。

修正後、python3 test_approval.py approval_v2 を実行し、その出力をそのまま貼ってください。
既存の T1 と T8 が引き続き通ることも確認してください。

指示の要点は、失敗しているテストIDを示すこと、要件IDと結びつけること、直し方の方針を示すこと、実行結果をそのまま貼らせること、既存のテストが壊れていないことも確認させること、です。

この修正指示を含む、AIと進める流れの全体と、人とAIの分担は次のとおりです。

6.3 修正後のコード

修正後の処理は、承認の前に4つを順に確かめます。接続の設定が取消できるものか、操作者の役割が承認者か、その依頼に割り当てられていて依頼者本人でないか、依頼が提出済みか。どれか1つでも違えば、何も変えずに拒否します。そのうえで、「状態を承認済みに変える」と「承認の記録を1件足す」を1つのまとまりとして保存し、途中で失敗したら両方を取り消します。

修正前後の比較です。

観点修正前(v1)修正後(v2)対応テスト
接続設定の確認なし自動確定モードや未確定処理のある接続は、変更前に拒否T12
操作者の役割なし役割が承認者でなければ拒否T9
操作者の割当・本人なし割当承認者か、依頼者本人でないかを確認T2, T3
状態の確認(処理側)なし提出済みのときだけ進むT4, T5
状態の条件(保存先)WHERE id = ?WHERE id = ? AND state = '提出済み'。更新件数が1でなければ取消T11
確定の単位状態と履歴を別々に確定with conn: で1つのトランザクションにまとめ、失敗すれば両方取消T6
取消の表示—取消後に未確定処理が残っていないかを見て、「取り消した」か「取り消せなかった」かを返すT6
差戻し理由確認なし空なら拒否T7

役割と割当は別の条件です。「鈴木は承認者である」(役割)と「REQ-A01の承認者は鈴木である」(割当)は違う話です。初版は割当だけを見ていたので、承認者でない運用管理者を誤って割当先にしたデータでは承認できてしまいました。改訂版は両方を見ます。

技術詳細修正後のコード(approval_v2.py)と補足

掲載しているのは approval_v2.py のうち承認処理に関わる部分の抜粋です。冒頭の説明文、import、時刻の差込み口(_now)、差戻し(send_back)、型注釈、行末のコメントは省いています。判定と処理の行は正本と同じで、全文は最小実験コード一式(第9節)にあります。

def _check_connection(conn):
    """この処理が前提にする接続設定かを確かめる。違えば変更前に拒否する。"""
    if getattr(conn, "autocommit", False) is True:
        return ApprovalResult(ok=False, reason="接続が自動確定モード(autocommit=True)のため処理しない")
    if conn.isolation_level is None:
        return ApprovalResult(ok=False, reason="接続が自動確定モード(isolation_level=None)のため処理しない")
    if conn.in_transaction:
        return ApprovalResult(ok=False, reason="未確定の処理が残っている接続のため処理しない")
    return None

def _check_approver(conn, request_id, actor_id):
    row = conn.execute(
        "SELECT state, requester_id, approver_id FROM requests WHERE id = ?", (request_id,)
    ).fetchone()
    if row is None:
        return None, ApprovalResult(ok=False, reason="依頼が存在しない")
    state, requester_id, approver_id = row
    role_row = conn.execute("SELECT role FROM users WHERE id = ?", (actor_id,)).fetchone()
    if role_row is None or role_row[0] != "承認者":
        return None, ApprovalResult(ok=False, reason="操作者の役割が承認者ではない")
    if actor_id != approver_id:
        return None, ApprovalResult(ok=False, reason="割り当てられた承認者ではない")
    if actor_id == requester_id:
        return None, ApprovalResult(ok=False, reason="自己承認は禁止")
    if state != "提出済み":
        return None, ApprovalResult(ok=False, reason=f"対象外の状態(現在:{state})")
    return row, None

def approve(conn, request_id, actor_id, _fault=None):
    err = _check_connection(conn)
    if err:
        return err
    row, err = _check_approver(conn, request_id, actor_id)
    if err:
        return err
    try:
        with conn:  # 中で例外が出れば取消(rollback)、出なければ確定(commit)。接続は閉じない
            cur = conn.execute(
                "UPDATE requests SET state = '承認済み' WHERE id = ? AND state = '提出済み'",
                (request_id,),
            )
            if cur.rowcount != 1:
                raise RuntimeError("操作時点で対象の状態が有効でなかった")
            if _fault is not None:
                _fault()
            conn.execute(
                "INSERT INTO history (request_id, action, actor_id, at) VALUES (?, '承認', ?, ?)",
                (request_id, actor_id, _now()),
            )
    except Exception as e:
        rolled_back = not conn.in_transaction
        return ApprovalResult(ok=False, reason=f"処理を{'取り消した' if rolled_back else '取り消せなかった'}:{e}")
    return ApprovalResult(ok=True, reason="承認しました")

with conn: について。Pythonの sqlite3 では、接続を with 文で使うと、中で例外が出なければ確定(commit)、例外が出れば取消(rollback)されます。ただし、この構文は新しいトランザクションを自分で開くわけでも、接続を閉じるわけでもありません。トランザクションは、この設定(isolation_level が None でない)のとき、最初の UPDATE や INSERT の前に自動で開かれます。接続が自動確定モード(isolation_level=None、または3.12以降の autocommit=True)だと、with conn: は何もせず、途中で失敗しても状態変更だけが残ります。だから修正版は、処理の前に接続設定を確かめ、対応しない設定では拒否します(第11節の一次資料)。

外部キー検査について。common.py は接続を作るときに PRAGMA foreign_keys = ON を実行し、実際に1になったことを確かめています。SQLiteでは、テーブル定義に REFERENCES と書いても、この設定を接続ごとに有効にしなければ関係は守られません(既定では無効です)。T10は、存在しない依頼の履歴を直接追加しようとして、保存先が拒否することを確かめます。これは「保存先が守る関係」であって、「処理が守る業務の条件」(役割、割当、状態)とは別のものです。

状態変更と履歴追加を一まとまりにする。両方成功なら確定し、途中で失敗したら両方取り消す。修正後(v2)の保存処理の説明図。

6.4 修正後の実行結果(実行確認済み)

12件すべて成功(ok)でした。正常系のT1とT8が引き続き通っているので、修正が正常な承認・差戻しを壊していないことも分かります。

技術詳細実行結果の全文(approval_v2)
=== 検証対象: approval_v2 / スイート: original ===
test_T1_normal_approval ... ok
test_T2_not_assigned_approver ... ok
test_T3_self_approval ... ok
test_T4_wrong_state_draft ... ok
test_T5_duplicate_approval ... ok
test_T6_partial_failure_rolls_back ... ok
test_T7_send_back_requires_reason ... ok
test_T8_send_back_with_reason ... ok
結果: 実行 8 件 / 失敗 0 / エラー 0 / スキップ 0
終了コード: 0

=== 検証対象: approval_v2 / スイート: additional ===
test_T10_foreign_key_enforced ... ok
test_T11_sql_state_guard ... ok
test_T12_autocommit_connection_refused ... ok
test_T9_assignee_without_approver_role ... ok
結果: 実行 4 件 / 失敗 0 / エラー 0 / スキップ 0
終了コード: 0

6.5 テストの検出力を確かめる(実行確認済み)

「テストが通った」の重さは、テストが何を見逃すかで決まります。GPTによる別環境のレビューでは、初版のテストに対して、故意に誤らせた4種類のコードがすべて通ってしまうことが示されました。改訂版では、同じ4種類を一時フォルダのコピーに加えて、強化したテストが検知するかを確かめました(mutation_check.py)。故意に誤らせたコードは正常版に混ぜず、終了時に削除されます。

結果は、4種類すべてを検知しました(初版のテストはすべて見逃していました)。どの誤りをどのテストが見つけたかは、次の技術詳細にあります。

技術詳細故意の誤変更4種と検知したテスト
故意の誤変更誤りの内容初版のテスト改訂版のテスト(検知したテスト)
sql_guard_removedUPDATEの状態条件と更新件数の検査を削除見逃し(8件成功)検知(T11)
rejection_deletes_history割当外を拒否する直前に、対象の既存履歴を削除見逃し(8件成功)検知(T2)
empty_reason_adds_history理由なし差戻しを拒否する直前に、余分な履歴を追加見逃し(8件成功)検知(T7)
sendback_wrong_actor差戻し成功時の履歴の操作者を別人、時刻を空に見逃し(8件成功)検知(T8)

「初版のテスト」の列は、GPTが別環境(Linux、Python 3.13.5)で確認した結果を、この記事の説明として引いたものです。「改訂版のテスト」の列は、この環境で実行した結果です(evidence/rev1-2026-09-18/sample-b/run_log.md)。

7. 確認結果

結論は、「修正後の実装は、この12場面では要件どおりに動く。ただし、アプリ全体や本番環境が安全だという証拠ではない」です。何を確かめ、何を確かめていないかを、次に分けて書きます。

項目内容
実行日2026-09-18(改訂版 rev1)
実行環境macOS 26.6.2、Python 3.9.6、SQLite 3.51.0。外部接続なし。メモリ上の新規SQLiteのみ
再現手順examples/sample-b/ で第5.5節のコマンド
失敗例 v1 の結果原版8件中6件失敗(T2〜T7)、追加4件中2件失敗(T9、T12)、1件スキップ(T11)。期待どおり不備を検出
修正後 v2 の結果原版8件・追加4件のすべて成功
検出力の確認故意の誤変更4種をすべて検知
実行記録evidence/rev1-2026-09-18/sample-b/run_log.md(初版の記録は evidence/sample-b/run_log.md)

この実験で確かめたこと:要件REQ-003〜007から作った12の場面について、修正前の実装が不備を持ち、修正後の実装がその場面では期待どおりに動くこと。拒否のときに保存内容が一切変わらないこと。成功のときに履歴の操作者・時刻・理由が期待どおりであること。テストが4種類の故意の誤りを検知すること。

この実験で確かめていないこと:

範囲理由どこで確かめるか
2人の承認者が同時に操作したとき1つのプロセスで順番に実行しているだけ。T11は保存先の条件が効くことの確認であって、同時操作の再現ではない09-03 で、実際の保存先を使った同時操作テスト
本物の本人確認(ログイン)操作者IDを引数で渡す代用。役割の確認(T9)は「そのIDの役割」を見るだけで、そのIDが本人かは見ていない05-03 / 08-03 で認証を実装したあと
複数の役割を持つ人この最小実験は1人1役割05-03 の権限設計で扱う
画面からの操作関数を直接呼んでいる画面実装後の通し検証
本番の保存先メモリ上のSQLite本番と分けた検証環境で
対応しない接続設定での安全な動作対応しない設定は「処理前に拒否」で対応し、その設定での取消は確かめていない必要なら別途
差戻しの重複テストを用意していない追加を検討
処理の性能測っていない09-03 の負荷確認
サポート中のPython(3.12・3.13など)での実行この環境にない。標準ライブラリのみなので動く見込みだが未実行環境を用意してから

この12件が通ったことは、承認処理のこの部分がこの12場面で期待どおりに動くことの証拠です。アプリ全体が安全であることや、本番環境で問題が起きないことの証拠ではありません。GPTによる別環境での再実行も、AIによる技術レビューであって、人間の独立監査や利用者テストの代わりにはなりません。

8. 設計を判断する

8.1 「誰が承認できるか」をどこで守るか

承認の許可判定は、3つの場所で行えます。それぞれ役割が違い、どれか1つで足りるものではありません。

場所何をするか守れること守れないこと費用・複雑さ
画面割当外の人には承認ボタンを表示しない誤操作を減らす。使いやすさ画面を経由しない操作(直接の要求、別の画面、古い画面)は止められない低い
処理(今回の _check_approver)要求ごとに役割(承認者か)・割当(その依頼の承認者か)・本人でないこと・状態を確認する画面を経由しない操作も止める。理由を返せる確認と更新の間に別の操作が割り込む隙がある中
保存先(今回の WHERE ... AND state='提出済み' と更新件数の確認、トランザクション、外部キー検査)更新時に条件を付け、条件に合わなければ更新しない。複数の変更をまとめて確定する。宣言した関係を守る割り込みや途中失敗があっても、データの整合性を守る(T11で状態条件が効くことは確認。同時操作そのものは未検証)「誰が」の判断そのものは苦手。理由を利用者に伝えにくい。接続の設定に依存する中〜高(設計が要る)

判断の目安です。

  • 画面だけで守っている実装は、要件REQ-003を満たしていません。画面は使いやすさのためのもので、統制のためのものではありません。
  • 処理側の判定は必須です。最小実験のv2はここを直しました。役割と割当を別々に確認することも含みます。
  • 保存先の条件とトランザクションは、同時操作や途中失敗があるときに効きます。利用者が数人で、同時に同じ依頼を操作することがまずないなら、処理側の判定に保存先の条件を添える程度で足りることが多いでしょう。利用者が多い、承認が業務上重要、外部から要求が来る、という条件が加わるほど、保存先での守りの比重が上がります。

8.2 重複した承認をどう扱うか

T5では、2回目の承認を「拒否」にしました。ここで冪等性(べきとうせい、idempotency)という言葉を整理しておきます。冪等性とは、同じ操作を何度繰り返しても、意図した効果が1回分と同じで重複しない性質のことです。返ってくる答え(成功か拒否か)が同じかどうかは、冪等性とは別の話です。HTTPという通信の約束でも、同じ区別がされています(第11節の一次資料)。

この意味では、今回の「2回目を拒否する」方式も、承認の効果は1回分しか起きないので冪等です。「拒否」と「冪等」は対立する概念ではありません。設計として分かれるのは、2回目に何を返すかです。

案2回目の効果2回目の返答利点負担
拒否を返す(今回)なし(冪等)拒否利用者が「二重に押した」ことに気づける。実装が単純通信の再送で自動的に2回届いた場合、利用者に不要なエラーが見える
成功を返すなし(冪等)成功再送に強い。利用者に余計なエラーを見せない「今の操作で承認したのか」「すでに承認済みだったのか」を区別する設計が要る

どちらでも、REQ-006「承認の効果と業務履歴が二重に発生しない」は守れます。違いは利用者への見せ方と、通信の再送をどう考えるかです。今回は、画面がまだなく再送の設計もしていないので、拒否を返す方式を選びました。この判断は docs/DECISIONS.md に記録しています。

「戻す」という言葉も区別しておきます。この記事のトランザクションの取消は、確定前の変更を保存先が捨てることです。記事Cで扱うコードの切戻しは、プログラムを前の版に戻すことで、データは戻りません。同じく記事Cのデータの復元は、バックアップの時点のデータを別の場所に戻すことです。3つは別の作業です。

8.3 テストの深さと費用

12件のテストは数秒で終わり、追加のインストールも要りません。これは「安いテストを、要件ごとに1つ以上持つ」という最小限の形です。ここに同時操作テスト、画面を含む通しテスト、負荷テストを足すと、環境の準備と維持の費用が上がります。

判断の目安は、失敗の影響の大きさです。承認済みなのに履歴がない依頼が生まれると、あとから誰が承認したか分からなくなり、業務上の説明ができなくなります。この影響が許容できないなら、T6のような途中失敗のテストは省けません。一方、性能のテストは、利用者が数人で1日数件の依頼なら、初期版では省く判断もあり得ます。

8.4 適用限界

この記事の方法は、処理が関数として切り出せる、保存先がテストごとに作り直せる、という条件で成り立っています。外部サービスと連携する処理や、保存先を作り直せない環境では、テスト用の代用(モック)や検証環境の分離が必要になり、06-02(開発・検証・本番の分離)と08-03(外部機能との連携)の内容が前提になります。

9. 持ち帰る成果物

成果物場所
要件・テスト対応表と修正記録(空欄版)templates/verification_matrix_template.md
同(記入例)templates/verification_matrix_example.md
AIへ渡す資料と指示(空欄版・記入例)templates/ai_request_template.md、templates/ai_request_example.md
最小実験のコードexamples/sample-b/
実行記録evidence/sample-b/run_log.md

自分の処理に使うときの手順は次のとおりです。

  1. 要件と受入条件から、正常・権限なし・状態外・重複・途中失敗・入力不足の場面を書き出す
  2. 場面ごとに、前提データ・操作・期待結果(状態と履歴の両方)を表にする
  3. 表をテストにして、実装に対して実行する
  4. 失敗したら、テストではなく実装を直すようAIに指示する。テストIDと要件IDを添える
  5. 修正後に全件を再実行し、出力をそのまま記録に残す
  6. 確かめていない範囲を表に書く

10. 次工程への受渡し

次へ渡すもの:要件・テスト対応表(修正前後の結果入り)、実行記録、確かめていない範囲の一覧、修正後のコード。

次工程での使われ方:09-03(例外・権限・同時操作・負荷)では、「確かめていない範囲」の表がそのまま次のテストの計画になります。09-04(第三者レビュー)では、対応表と実行記録がレビューの入力になります。10-01(受入確認)では、受入条件AC-001〜004、008の結果を業務担当者に見せる材料になります。

前工程へ戻る条件:T5の重複をどう扱うか(拒否か無視か)を決められないなら、02-04(例外と完成条件)に戻って受入条件を決め直します。テストを書く途中で「差戻しの重複」のように要件にない場面が見つかったら、要件に追加するかどうかを02に戻って判断します。

11. 根拠と更新情報

  • 会社・人物・データは架空の設定です。approval_v1.py は説明用の失敗例で、実際のAI出力を観測した記録ではありません。コード・テスト・本文はClaude Codeが作成しました。
  • 実行記録:evidence/rev1-2026-09-18/sample-b/run_log.md(改訂版、2026-09-18、macOS 26.6.2、Python 3.9.6、SQLite 3.51.0)。初版の記録は evidence/sample-b/run_log.md にそのまま残しています。
  • 改訂の来歴:2026-09-18にGPTによる別環境(Linux、Python 3.13.5、SQLite 3.46.1)での再実行と反例確認を受け、同日に改訂版(rev1)を作成。役割の確認、接続設定の確認、外部キー検査、テストの検査強化(全行照合・固定時刻)、追加テスト4件、変異確認を行った。人による実読は未実施。
  • Python公式ドキュメント「sqlite3 — DB-API 2.0 interface for SQLite databases」(Python 3.13版、https://docs.python.org/3.13/library/sqlite3.html 、2026-09-18確認)。参照箇所:接続をコンテキストマネージャ(with 文)として使うと、例外なしなら確定、例外ありなら取消されること。この構文は新しいトランザクションを開かず、接続も閉じないこと。autocommit が True のときは何もしないこと。isolation_level が None でないとき、INSERT/UPDATE の前に暗黙にトランザクションが開かれること。in_transaction 属性。
  • Python公式ドキュメント「sqlite3」「unittest」(Python 3.9版、2026-09-18確認)。参照箇所:with 文の確定と取消、Cursor.rowcount、unittest.main(verbosity=2)。
  • SQLite公式「SQLite Foreign Key Support」第2節(https://www.sqlite.org/foreignkeys.html 、2026-09-18確認)。参照箇所:外部キー制約は既定で無効であり、接続ごとに PRAGMA foreign_keys = ON で有効にする必要があること。PRAGMA foreign_keys で現在の値を確認できること。
  • RFC 9110「HTTP Semantics」9.2.2 Idempotent Methods(https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2 、2026-09-18確認)。参照箇所:同一の要求を複数回行ったときの意図した効果が1回のときと同じなら冪等であること。応答が同じである必要はないこと。この記事はHTTPの適合を試すものではなく、効果と返答を分ける考え方の出典として参照。
  • Python Developer's Guide「Status of Python versions」(https://devguide.python.org/versions/ 、2026-09-18確認)。参照箇所:Python 3.9 の end-of-life は 2025-10-31。
  • Claude Code公式「Best practices for Claude Code」(https://code.claude.com/docs/en/best-practices 、2026-09-18確認)。参照箇所:検証の基準を与え、証拠(テスト出力)を示させること。同じAIによる再確認より、独立した確認を重視すること。
  • 未検証事項:同時操作、本物の本人確認、複数役割、画面経由の操作、本番保存先、対応しない接続設定での取消、差戻しの重複、性能、サポート中のPythonでの実行。第7節参照。
  • 2026-09-19(Phase 4D-R5):非エンジニア向けに、冒頭の結論、第1節の確認の流れ、第2節の用語の説明順序、第5.2・6.1・7節の冒頭を改善した。テスト・コード・実行結果・結論は変更していない。改善後の実読による確認は未実施。
  • 2026-09-19(Phase 4D-R6):第5.2〜6.5節を「主本文の要約」と「技術詳細(開閉)」に分けた。コード・テスト表・実行結果・補足の文章は技術詳細の中に原文のまま残している。削除した技術内容はない。
  • 2026-09-19(Phase 4D-R6後):ユーザー実読でPASS。R5で指摘された「第5〜6節以降が重い」という評価は、R6のProgressive Disclosure適用後は解消したとユーザーが判断した。テスト・コード・実行結果・結論は変更していない。
  • 2026-09-21(Final Content Audit):第6.3節の技術詳細に掲載している修正後のコードが approval_v2.py の抜粋(import・_now・send_back・型注釈・行末コメントを省略)であることの断りを1文追加した。コード・テスト・実行結果・結論は不変。