YONAPO RESEARCH
研究記録
MedDSL(仮称)

RESEARCH / RECORD

P2-a の追加作業:拡張ホストの分離・防護例・宣言の固定

原文 docs/research/22-p2-a-hardening.md

18 KB

GitHubで見る

版 be280c0

この文書の目次 5項目

実施日:2026-10-19。契約版:p0-draft-2。状態:作成者(AI-001)の単独・内部実施の結果。独立確認前。P2-a の結果 21 の「P2-b の前に判断すること」3 点を、利用者の指示(「拡張ホストを分離する」「防護例のない義務のうち拒否例を作れるものを作る」「宣言を固定対象に加える」)で実施した記録です。実装の挙動と契約(p0-draft-2)は変えていません。

判断 実施した内容 結果の要約
拡張ホストを分離する handover.py(1 モジュールに全操作)を、ホストと操作に分け、最上位の振り分けを登録表にした 分離の前後で、22,052 件の出力が一致。1 操作の変更が列挙する拡張が 6 件から 1〜2 件に
防護例のない義務に拒否例を作る examples/p2/ に防護例 35 件を追加し、24 件の欠落のうち 21 件を置き換えた 欠落は 3 件(拒否する入力を構成できないもの)。実装のゲートを壊した 27 件の変異のすべてを防護例が検出(うち 10 件は既存テストが見逃した)
宣言を固定対象に加える 宣言と、宣言が根拠として引く実体を、共通 manifest(lock)に加えた 固定対象 170 → 248 ファイル(宣言 11、宣言が引く防護例・検査・オラクルなど 62、新しいモジュール 5)。例外は監査スクリプト 1 件

1. 拡張ホストの分離

変更

変更前 変更後
handover.py(371 行)に、ヘッダーの検査・実体の確認・鎖・改名・欠落の診断・再生の点検・登録の振り分け handover.py(75 行)はヘッダーの検査・実体の確認・登録表(OPERATIONS)による振り分けだけ
- handover_common.py(25 行):P0 の固定値、鎖の項目、操作の登録の形(Operation)、P0 要求の組み立て
- chain.py(鎖と改名)、absence.py(欠落の診断)、replay.py(再生の点検):それぞれ登録(Operation)を持つ
reference.py・projection.py は handover.py から呼ばれる 同じく自分の Operation を持ち、ホストの登録表に載る
__init__.py に evaluate(3 つの族への振り分けとP0 の既定)と多数の再 export host.py:SURFACES(schema_version → 処理・結果の schema・profile)と evaluate。__init__.py は evaluate だけを再 export

関数の本体は、handover.py の行範囲をそのまま移した(転記による誤りを避けるため、手で書き直していない)。ヘッダーの検査は、操作ごとの項目と P0 の確認の要否を、各操作の Operation から読むようにしただけで、検査の順序・エラーの code・path は変えていない。

行わなかったこと:予測・事象・引き継ぎの 3 つの族のヘッダー検査の共通化。エラーの path や code が族ごとに違う(予算の不正に path が付くか、native 仕様の不一致の path)ので、1 つにまとめると結果が変わる。それはリファクタリングではなく契約の変更になるため、今回の範囲外とした。

挙動が変わらないことの確認

検査 結果
全例の出力・改変入力・予算の全値掃引(固定の seed。131 例×(1+改変 150)+予算の掃引)の出力 22,052 件が分離の前後で完全に一致(certifier だけは、実装のファイルの束の hash なので除いて比較)。出力の種類は 95(status と最初の理由コードの組)、例外 0
既存の開発テスト(231)・追加監査(8) 無変更で通過
レビュー系列(52) 1 箇所だけ変更:差し替え先を med_dsl.handover._compare から med_dsl.chain._compare へ
P2 系列 無効化の方法を変更(§下)
P1 の 20 条件・80 JSON・417 必須パス/P0 221 必須パス 通過(lock を再固定)

比較の仕組みは、全例を評価した結果を正規化して hash にし、ファイルとして保存するだけの補助(作業記録 refactor_digest.py)。分離の前に基準を取り、分離の後に同じ入力で比べた。入力が同じであること(seed を例ごとの名前から決める)と、基準が再現すること(2 回の実行で同一)は確認した。

再検証範囲の精度

affected() に、登録の辺の規則を入れた(実行契約 20 の §5)。ホストは登録のために操作のモジュールを import するが、これは依存の向きの逆で、辿るとどの操作の変更も全操作を列挙する。

変更するモジュール P2-a の最初の実装(handover.py 1 モジュール) 分離後
absence.py、replay.py、projection.py、reference.py (同じモジュール内)6 件 1 件(その拡張)
chain.py 6 件 2 件(鎖と、鎖の認定を読む再生)
handover.py、handover_common.py 6 件 6 件(ホストと全操作。ホストの変更は全操作に及ぶ)
events.py 7 件 7 件(事象化と、それに依存する引き継ぎの全体)
host.py、__init__.py 10 件 10 件(共有の入口の pin)
forecast.py 1 件 1 件

非干渉(X03)の無効化の方法

登録表が関数の参照を持つようになったので、モジュールの属性を差し替えるだけでは無効にならない。拡張を不活性にする方法を、宣言した実装モジュールが定義する全関数を、他のモジュールが import した束縛も含めて差し替え、ホストの登録表(host.SURFACES、handover.OPERATIONS)の該当の項目も差し替える形に変えた。これは、以前の「拡張ごとに差し替える関数の表」より厳しい:宣言したモジュールの関数のうち、宣言に現れない依存先から呼ばれるものがあれば、非干渉の破れとして見つかる。結果は以前と同じ(8 拡張を無効にしたとき、その拡張と依存先に属さない例の結果は変わらず、属する例は変わる)。宣言の誤り(p1-replay が鎖を読むことの書き落とし)が検出されることも、新しい方法で再確認した。

限界

  • 登録の辺を辿らないので、ホストが操作の関数を実際に使う逆向きの読み取りは、列挙では見えない。非干渉の検査(無効化)が見るが、ホスト自身が持つ例がなければ見えない。
  • __init__.py が再 export していた名前(PROFILE、Problem など)を削った。リポジトリ内に使う箇所はなかった(使われていたのは evaluate だけ)が、外部の利用者がいればここは契約の変更になる。
  • examples/review/ の gate(例:handover.py:176)の行番号は分離前の位置。固定した例を書き換えないため、対応表を examples/review/README.md に追記した。

2. 防護例の追加

対象

P2-a で防護例のなかった義務 24 件のうち、拒否・境界の入力を構成できる 21 件に防護例を付けた。新しい例 35 件を examples/p2/ に置き(台帳つき。別の分母)、事象化の R3-4 だけは既存の例(p1-b/shared-key:同じキーの 2 つの主張が別々に保持される)を引いた。

義務の種類 付けた防護例
予算の境界(K-RES、8 拡張) 各拡張の既存の例を、予算だけ変えた対:完了に要る最小の steps n の n−1 で止まる/n で完了する(上限を厳しくしすぎない)。参考意味には式の深さの上限(16)の境界も
停止(K-STOP) 定義:予算 0(追加が空でも)/鎖:未知の能力で認定しない/ホスト:未登録の操作・consumer の操作・profile/参考意味:有限領域の探査は未対応・式の深さの上限
不変条件(K-INV) 定義:台帳を受け付けない/鎖:確認のない AI 候補が支持にならない(未決のまま両計算が一致)/ホスト:打切りで認定票・部分の報告を出さない
鎖の範囲(COMP-2) 範囲外の問い(事象の同一性)を旧の問いの認定に含めない
予測(R2-2) 任意の package・報告専用の述語以外を受け付けない
参考意味(R3-4、R3-5、R3-6) 新理論が文を一つ追加するとき被覆を真にも偽にもしない/台帳を受け付けない/宣言した語彙にない述語を持ち込めない

欠落として残る 3 件(拒否する入力を構成できない。理由は宣言の guard_gap):定義の R1-3(旧モデルの拡張は構文の十分条件 R1-1・R1-2・R1-4 で保証され、独自の拒否例がない)、事象化の R3-3(被覆は手作業の導出で、反例の検査は参考意味が担う)、鎖の COMP-3(この鎖に損失のある段がない)。

作り方

期待結果は、ゲートの意味から手で指定し、処理系の出力から生成していない(例外は予算の境界の n で、無制限の実行の steps_used を測った)。鎖の入力は、変更した履歴から端点・範囲の宣言を作り直す必要があるため、実装の事象化・改名・端点の計算を入力の構成にだけ使った。35 件のうち最初の 33 件は、最初の実行で期待が全件一致した。一致しただけでは検出力の証拠にならないので、次の変異検査を行った(残りの 2 件は変異検査の結果で追加した)。

検出力(変異検査)

防護例を付けたゲートごとに実装を壊し(27 件)、意図した例が失敗するかを調べた。あわせて、既存の開発テスト(231)とレビュー系列(52)が同じ変異を検出するかも記録した。変異は、予算の課金の削除(9)、ゲートの条件の緩和・削除(11)、認定・採用の条件の変更(4)、予算の定義の変更(2)、停止時の応答の形(1)で、作成者が防護例の対象に合わせて選んだもの(網羅的な変異演算子の集合ではない)。P0 実行体のファイルを変異させるときは、変異後の hash に再固定した。対照(変異なし)は失敗 0。

結果 件数
意図した防護例が失敗した(検出) 27 / 27
既存の開発テスト・レビュー系列も検出した 17
既存のテストが見逃し、防護例だけが検出した 10(欠落の診断・改名・射影・参考意味の課金、式の深さの下側の境界、有限探査の未対応、ホストの操作・consumer の操作・profile の検査、参考意味の語彙の検査)

検査で分かったこと:

  1. 定義の初期の課金が、最初の例では守られていなかった。 「予算 0 は、追加が空でも未評価」という初期の課金(definitions.py の最初の budget.tick())を外しても、追加が空でない例は後の課金で止まるので、予算 0 の例が失敗しなかった。追加が空の例(予算 0 と 1 の対)を足して検出できるようにした。
  2. 等価な変異が一つあった。 鎖の certified = complete and not outside の and not outside を外しても、どの検査も失敗しない。範囲外の問いがあると事象化の比較の状態が partial になり、complete(全比較が matched)が偽になるので、not outside は冗長(多重の防御)。代わりに範囲外の問いの集計(outside)を壊す変異で、chain-out-of-scope-query が検出することを確認した。
  3. 重複した防御がある。 予算 0 を「無制限」とみなす変異(Budget.tick)でも、鎖は止まる。内側の P0 の問い合わせ(query.py)が、予算 0 を未評価として止める独立のゲートを持つため。そのため鎖の予算 0 の例は、停止時の応答の形(incomplete: ["request"]、認定票なし)を壊す変異で検出できることを確認した。個々のゲートが単独で守られているとは限らない。
  4. 検査の環境の誤りを一度出した。 変異した作業ツリーの複製が git のリポジトリでなく、既存テストの一つが git rev-parse で失敗するため、「既存テストで検出」の列が全変異で誤って検出になった。対照の変異を加えて気付き、環境を直して全変異をやり直した。結果は上の表の値。

限界

  • 変異は作成者が選んだ 27 件で、防護例が検出できない退行が他にないことは示していない。
  • 防護例の期待は作成者が手で指定した主張で、独立の確認は受けていない。
  • 防護例のない義務 3 件の理由(「拒否する入力を構成できない」)は、構成できないことの証明ではなく、作成者が試みた範囲の判断。

3. 宣言の固定

固定したもの

共通 manifest(lock、examples/p1-evaluation/manifest.json)の対象に、次を加えた(評価器 evaluate-p1-d.py の EXTENSION_PATTERNS)。固定対象は 170 → 248 ファイル(新しい実装モジュール 5 を含む)。

追加 ファイル数
extensions/*.json(宣言 10 と索引) 11
examples/p2/*.json(防護例 35 と台帳) 36
examples/review/*.json(宣言が引くレビュー系列の例と台帳) 15
tests/review/*.py(宣言が引く検査と独立オラクル) 7
tests/p2/*.py、tests/audits/*.py 2、1
scripts/research/check-extensions.py 1
実装モジュール(host.py、handover_common.py、chain.py、absence.py、replay.py) 5

範囲の決め方:宣言だけを固定しても、宣言が根拠として引く防護例・検査・オラクルが差し替えられれば、宣言の主張は黙って崩れる。そこで「宣言と、宣言が引くものすべて」を固定対象にし、それをテストで保証した(test_the_lock_pins_the_declarations_and_everything_they_cite:実装・仕様・契約・pin・評価の例・検査・オラクル・防護例のうち、固定対象に載っていないものは、監査スクリプト 1 件だけ)。

例外:scripts/research/audit-preservation.py は載せない。固定対象の sha256 を定数に持つので、載せると自己参照の循環になる(test_the_audit_script_is_outside_the_lock_because_it_holds_the_lock_hash が、この理由が今も成り立つことを検査する)。固定対象の hash が変われば、固定対象に載る tests/audits が検出する。

分母は変えていない:P1 の 20 条件・80 JSON・417 必須パス、P0 の 22 条件・37 JSON・221 必須パスは同じ。レビュー系列と防護例は、宣言が根拠として引く実体として載るだけで、評価の分母(SUITES)には入らない。「レビュー系列は固定対象に載らない」という書き方だったテスト(tests/review/test_review_negatives.py)は、意図(分母に入らない)に合わせて「分母は 4 系列だけ」に直した。

効果と限界

  • 宣言、防護例、検査、オラクルのどれを変えても、固定対象の再固定(明示的な --create-snapshot)が要り、評価器の preflight が不一致を拒否する。
  • 固定対象は同じリポジトリのファイルなので、改ざんを防ぐものではなく、変更を見落とさないための関門。外側の錨は、固定対象の hash を持つ監査スクリプトの定数だけで、外部への記録(独立の確認者による固定)は未実施。
  • certifier(認定票に付く実装の束の hash)は、従来どおり src/med_dsl/ の *.py と *.json だけから計算する(24 ファイル)。宣言や例は含めない。

4. 検証

検査 Python 3.14.4 Python 3.11.17
開発テスト 231 231
レビュー系列 52 52
追加監査 8 8
P2 系列 52(P2-a の 39 から追加:無効化の復元 1、登録の辺 3、防護例 7、固定 2) 52
拡張の登録 10 件登録・拒否 0・防護例のない義務 3 同左
共通の評価(CLI) P1 20 条件・80 JSON・417 必須パス、P0 221 必須パス、束 51a44343… 同左
保存の監査 拡張 600/単項 256/存在規則 32/範囲 1 (3.14 のみ)
挙動の一致(分離の前後) 22,052 件が一致 22,052 件が一致
extensions.py の変異(42) 42/42 検出(P2-a の 37 に、登録の辺・推移的な依存の 5 を追加) -
防護例の変異(27+等価 1+対照 1) 27/27 検出 -

extensions.py の変異検査では、登録の辺の規則を入れたとき、依存の推移閉包を壊す変異(X41)が最初は見逃された。人工の木のテストに「ホストが import するモジュールの拡張が、別の拡張を経由してホストに依存する」場合が無かったため。テストを足して検出した。

5. 未決と次

  • 3 つの族のヘッダー検査の共通化は、挙動(エラーの path・code)を変えるので行っていない。必要なら契約の変更として扱う。
  • 防護例のない義務 3 件(R1-3、R3-3、COMP-3)は、拒否する入力を構成できないと判断したもの。独立の確認で覆り得る。
  • 防護例の変異検査は、作成者が選んだ 27 件。変異演算子の網羅的な集合による検査は未実施。
  • 独立確認は未実施(宣言・防護例・期待結果・欠落の理由は、すべて作成者の主張)。
  • 次は P2-b(パイロット:概念の分割)。実装に入る前に、利用者の確認を取る。

P2-a

同じ区分の文書