タグ: 設計レビュー

  • AIの設計案を別のセッションで実装させる前に、原仕様にない制約を見分け、引き継ぎ票で分けて渡す

    AIの設計案を別のセッションで実装させる前に、原仕様にない制約を見分け、引き継ぎ票で分けて渡す

    AIのあるセッションで設計案を作り、実装は新しいセッションや別のAIに任せたい。そのとき受け手が読むのは、承認済みの仕様と、設計者が足した提案が混ざった1枚の設計案です。原仕様にない一文が承認済みの要件と同じ顔で並んでいると、受け手はそれを要件として実装します。

    Codexに在庫予約機能の設計案を作らせ、その設計案を原仕様と一緒に別のセッションへ渡して実装させたところ、まさにそれが起きました。設計案は8つの要件すべてに触れ、boolが数量として通ってしまうリスクも正しく指摘していましたが、その対策の一文で、原仕様の「正のint」を「組み込みintだけ」に狭めていました。受け手には「設計案と原仕様が食い違ったら原仕様を優先」と添えましたが、実装はこの制約をそのまま取り込み、テストにはすべて合格しました。今回、原仕様が何も書いていない点への制約の追加は、この優先の一文では防げませんでした。

    この記事では、設計案を原仕様の要件ごとに照らして原仕様にない制約を見分ける手順と、原仕様・設計提案・未決事項を欄で分けて渡す引き継ぎ票、受け手に要件とテストの対応を返させる受領確認を、この実例で示します。読み終えたら、自分の設計案から、実装の前に渡せる引き継ぎ票を作れます。

    設計案の一文が、実装に入るまで

    題材は、Pythonの在庫予約関数 reserve(stock, requests) の追加です。原仕様(SPEC.md)は8つの要件で、数量については次のように書いています(R3)。

    R3: quantity must be a positive int; reject bool, zero, negative, float with ValueError.

    Codex CLIの新しいセッションに、原仕様・既存コード・15件のテストを渡し、ファイルを変えずに設計案だけを作らせました(2026年9月21日、1回)。設計案の「要件」と「変更案」には、次の2行がありました。

    quantity は bool を除く正の組み込み int のみ許可する。

    type(quantity) is int and quantity > 0 を検証する。

    同じ設計案の「具体的な失敗リスク」には、「isinstance(quantity, int) だけでは True を受理してしまう。」とあります。Pythonの bool は int のサブクラスなので、この指摘は正しく、R3の「boolを拒否」に沿っています。ただ、対策として書かれた type(quantity) is int は、bool以外のintのサブクラス(intを継承したクラスの値)まで拒否します。R3はそこまで書いていません。

    この設計案を PLAN.md として原仕様と一緒に置き、別の新しいセッションに実装させました(同じ日、1回)。依頼文には次の一文を入れています。

    Read PLAN.md as an advisory design. SPEC.md is authoritative if they differ.

    受け取った実装の数量の判定は、設計案のとおりでした。

    if type(quantity) is not int or quantity <= 0:

    この実装は、事前に渡した15件のテストと、実装後に足した5件のテストにすべて合格しました。完了報告は「数量の厳密な検証(bool、0、負数、浮動小数を拒否)」と書くだけで、原仕様より狭めたことには触れていません。

    原仕様だけを渡して実装させた別の実装と、intのサブクラスの値を渡して比べました。class Quantity(int) で作った Quantity(1) などを、在庫5の商品への1件の予約として、保存しておいた2つの実装に渡した結果です(Python 3.13)。

    渡した数量 原仕様だけで実装した版 設計案を添えて実装した版
    Quantity(1)(intのサブクラス) 受け入れる(在庫5→4) ValueError で拒否する
    1 受け入れる 受け入れる
    True 拒否する 拒否する
    1.0 拒否する 拒否する

    どちらの実装も同じ20件のテストにすべて合格しているので、テストの結果だけを見ると差はありません。原仕様だけで実装した版の点検の手順は、Codexの合格報告を受け入れる前の点検の記事で扱っています。

    原仕様はintのサブクラスの扱いを明記していないため、設計案の「組み込みintだけ」は、矛盾ではなく仕様の具体化として取り込まれたと解釈できます。今回確かめたのは、優先順位の一文だけでは追加制約の採用を防げなかったことです。そこで、原仕様にある要件と設計者の提案を欄で分け、追加制約を承認するかどうかも実装前に決める方法を提案します。

    設計案の一文が、原仕様の空白を通って実装に入った流れと、引き継ぎ票で原仕様と設計提案を分ける位置を示す図
    今回の実装では、原仕様にない制約の追加を、優先の一文だけでは止められませんでした。

    手順1:設計案を、原仕様の要件ごとに照らす

    渡す前に、設計案の要件や方針の一文ずつを、原仕様の要件番号と照らします。照らした結果には、次の3つの印のどれかを付けます。

    印 当てはまる記述 引き継ぎ票での扱い
    同じ 原仕様の要件を言い換えただけ 「原仕様」欄に要件番号で書く
    狭い・広い 原仕様の要件より条件を絞る、または緩める 「設計提案」欄に移し、承認するまで要件にしない
    原仕様にない 原仕様のどの要件にも当たらない方針 「設計提案」欄に移す。決める人がいなければ「未決事項」へ

    今回の設計案に印を付けると、たとえば次のようになります。

    設計案の記述 照らした原仕様 印と扱い
    入力を変更せず、常に新しい在庫辞書を返す R1 同じ:原仕様欄にR1と書く
    全リクエストを検証後に在庫を反映し、欠品・在庫不足・重複矛盾時は ValueError R4・R6 同じ:原仕様欄にR4・R6と書く
    bool を除く正の組み込み int のみ許可する R3(正のint、boolを拒否) 狭い:設計提案欄へ。intのサブクラスの扱いを未決事項にする
    エラーメッセージやエラー優先順位は仕様外。テストで文言へ依存させない なし 原仕様にない:設計提案欄へ(原仕様と矛盾しないので、承認すれば採用できる)

    設計案が8つの要件すべてに触れていても、この照合は省けません。網羅しているかと、頼んでいない条件を足していないかは、別の問いだからです。今回の「狭い」の一文は、正しいリスク指摘のすぐ隣にありました。指摘が的確なほど、その対策の一文も要件のように読めてしまいます。

    印を付けるときに迷ったら、2つの書き方で結果が割れる値を実際に渡してみます。次のコードを int_boundary.py として保存し、python3 int_boundary.py で実行すると、intのサブクラスも受け入れる判定(正のintで、boolを除く)と設計案の判定(組み込みintだけ)の違いが出ます。

    class Quantity(int):
        pass
    
    for value in [True, 1, Quantity(1)]:
        positive_int = isinstance(value, int) and not isinstance(value, bool) and value > 0
        builtin_only = type(value) is int and value > 0
        print(type(value).__name__, positive_int, builtin_only)
    bool False False
    int True True
    Quantity True False

    3行目の Quantity だけ結果が割れます。割れた値があれば、その記述は「同じ」ではなく「狭い・広い」です。

    手順2:引き継ぎ票に、原仕様・設計提案・未決事項を分けて書く

    照らした結果を、次の引き継ぎ票に書き写します。受け手が読む資料はこの票と、原仕様・テスト・対象のコードです。設計案は添えてもかまいませんが、判断の根拠は票の欄で示します。

    目的:誰のどの問題を解決するか
    対象:リポジトリ、基準コミット、作業ブランチ
    原仕様:承認済みの要件を番号つきで(R1〜)。設計案で言い換えた文ではなく原文を書く
    設計提案:原仕様より狭い・広い記述と、原仕様にない方針。それぞれ「承認済み」か「未承認」かを書く
    未決事項:決める人、決まるまで実装で触れない部分
    調査済み:読んだファイル、関数、確かめた挙動
    変更範囲:変更してよい場所、維持する公開API
    検査:実行コマンド、合格の条件、既に分かっている失敗
    禁止事項:依存の追加、データ移行、外部送信など
    復旧:変更を戻せる単位と、戻せない副作用
    受領確認:要件ごとに確かめるテストと、採用する設計提案を返してから実装する

    今回の例なら、設計提案と未決事項の欄は次のようになります。

    設計提案:
    - 数量は組み込みintだけを受け入れる(R3より狭い。未承認)
    - エラーの文言と優先順位はテストで固定しない(原仕様にない。承認済み)
    未決事項:
    - intのサブクラスの数量を受け入れるか(決める人:[呼び出し側の担当])
      決まるまで、数量判定の実装と、その判定を含む機能の取り込みを保留する。独立して進められる部分だけ実装する

    未決事項に「決まるまでの扱い」を書いておくのは、受け手が空白を自分で埋めないためです。今回の実装は、空白を設計案の一文で埋めていました。

    手順3:受け手に、要件とテストの対応を返させる

    実装を始める前に、受け手に次の2つを返してもらいます。

    実装の前に、次の2つを返してください。
    1. 原仕様の要件番号ごとに、それを確かめるテスト名(なければ「なし」)
    2. 引き継ぎ票の設計提案のうち、実装に取り入れるものと、その理由
    返した内容に不明点や矛盾があれば、実装を始めずに質問してください。

    1で「なし」が返ってきた要件は、テストが守っていない要件です。今回なら、R3に対して返るのは test_05〜test_08(bool・0・負数・float)で、intのサブクラスを確かめるテストはありません。そこが、引き継ぎで受け手の判断に任されている場所です。2で「未承認」の提案を取り入れると返ってきたら、実装の前に止めて決めます。

    返答が正しくても、実装がそのとおりになるとは限りません。実装を受け取ったら、差分と、未決事項に当たる値を渡した結果で確かめます。確かめ方は合格報告を受け入れる前の点検の記事の手順4と同じです。

    自分の案件で書き換える箇所と、渡す前の確認

    引き継ぎ票の各欄は、自分の案件で次のように書き換えます。

    • 原仕様:設計案の要約ではなく、承認済みの要件の原文を番号つきで貼る。

    • 設計提案:手順1で「狭い・広い」「原仕様にない」の印を付けた記述をすべて移し、承認の状態を書く。

    • 未決事項:決める人と、決まるまでの実装の扱いを書く。

    • 検査:実行するコマンドと、合格の条件(テスト件数、変更してよいファイル)を書く。

    渡す前に、次の3つを確かめます。

    1. 設計案の要件・方針の文に、すべて印が付いている。

    2. 「狭い・広い」「原仕様にない」の記述が、原仕様欄に混ざっていない。

    3. 未決事項のそれぞれに、決める人か、決まるまでの扱いが書いてある。

    3つがそろえば、受け手は設計案を「承認済みの要件」と「提案」に分けて読めます。今回の type(quantity) is int の一文も、設計提案の欄に「未承認」と書き、実装前に質問するよう受領確認で求めます。この引き継ぎ票で実装し直す検証は行っていないため、採用後も差分と境界値で確かめます。

    出典と確認日