tools/chain_fuzz.py。単体テスト・敵対的検証に続く 3 番目の層で、狙う不具合は
「単体では全部通るのに、op を鎖にしたときだけ現れるもの」です。
型契約の嘘、tuple/list 梱包の不一致、NaN の漏出、想定外の例外種、拡大系の 指数増殖 —— どれも単体テストの視野の外にあります。
# 拡散: 2000 本の連鎖をランダムに張って署名を集める
py -3.11 tools/chain_fuzz.py --chains 2000 --length 8 --seed 1 --out out/chain_fuzz.jsonl
# 収束: 各発見を「その署名を出す最小の op 列」まで削る
py -3.11 tools/chain_fuzz.py --minimize out/chain_fuzz.jsonl
py -3.11 tools/chain_fuzz.py --minimize out/chain_fuzz.jsonl --only compute_fpfh
# 再走: 最小再現をそのまま実行(デバッガを当てる入口)
py -3.11 tools/chain_fuzz.py --replay 5000312 --script random_dropout,compute_fpfh
# カバレッジ内訳: どの op が走り、どの op が一度も走らなかったかを書き出す
py -3.11 tools/chain_fuzz.py --chains 1500 --coverage-out out/coverage.json
# 狙い撃ち拡散の強さ(0=一様、既定 0.5)
py -3.11 tools/chain_fuzz.py --chains 1500 --explore 0.7
| 種別 | 意味 | 扱い |
|---|---|---|
CONTRACT |
文書化された ValueError |
白。fail-closed が仕事をした |
SUSPECT |
それ以外の例外(TypeError/IndexError/…) | 契約の穴。入口検証を足す |
TYPEMISS |
目録の宣言 out 型と実際の返りが違う | 型の嘘。adapter か宣言を直す |
NONFINITE |
有限入力から NaN/Inf が無言で出た | 毒の漏出。ただし契約かを先に疑う |
GROWTH |
産物が pool 上限超(拡大系の指数増殖) | 記録して捨てる(無言で切らない) |
SLOW |
1 op が閾値超(既定 10s) | 性能スメル |
OPTIONAL |
optional 依存の ImportError | 白(記録しない) |
NONFINITE は「非有限を出した op」を責める前に 出所と契約を追うこと。実例:
sdf_subtract の inf は esdf の「全自由なら +inf」という文書化済み契約を
min/max 代数が正確に伝播しただけで無実、一方で同family の sdf_smooth_union
だけが算術(inf−inf)で全 NaN になる本物のバグでした。契約として正しい非有限は
NONFINITE_BY_CONTRACT に 理由つきで登録します。
各 op の宣言(in 型 → out 型)に従ってプールから引数を引き、産物を戻します。
型語彙は目録と共有(voxel/points/signal/matrix/table/pairs/roots…)。
TYPE_CHECKS が各語彙の判定関数を持ち、宣言と実際の返りの一致を機械検証します。
連鎖 i は seed * 1_000_003 + i で回ります。共有 rng だと「i 番目だけを
後から再走する」ことができず、最小化が成り立ちません。
候補抽選(どの op を次に引くか)は連鎖 rng、引数抽選は
(連鎖 seed, op 名, その op の出現回数) から導いた別の乱数源を使います。
これを分けないと、無関係な op を 1 つ落としただけで以降の抽選が全部ずれ、
最小化の再走が原理的に再現しません。実測: 分離前は再現 48/65(74%)、
分離後は 58/58(100%)。同じ理由でプールの型選択も sorted(pool) に固定
しています(dict の挿入順は op を落とすと変わるため)。
MAX_POOL_BYTES(128MB): 拡大系連鎖の指数増殖を止める。超過は GROWTH として
記録(silent cap 禁止)。big-input: を print。万一のストールでもログだけで
犯人が判る(実測でこれが 2 度役に立った)。--minimize で各署名を最小 op 列へ| 波 | 署名数 | 主な発見 |
|---|---|---|
| 第 3 波 | — | 型の嘘 22 件、RESULT_ADAPTERS+call() を一級機能化 |
| wave-4 | 103 | TYPEMISS 7 op、SUSPECT 9 種、第 6 家系「小さい入力→巨大な内部割当」(TPS 12GB / PSF 64GB / CPD) |
| wave-5 | 4 | float32 桁あふれの無言 NaN、sdf_smooth_union の inf→NaN、入口契約の穴 2 系統 |
| wave-6 | 非 CONTRACT 0 | 収束(白 63 種のみ) |
| math 追加 | 2 | mat_svd/mat_eigh の型の嘘を初走行で即検出 |
| wave-7 | 172(NONFINITE 2) | 引数抽選の乱数を位置独立にした = 探索経路が変わり新しい発見。poly_eval の float64 オーバーフロー無言 inf、chamfer_distance の空点群無言 NaN(→ metrics3d の 5 兄弟を一掃) |
| optics 追加 | 18(全 CONTRACT) | 光学 18 op を登録(opsoptics、新語彙 jones/stokes)。非 CONTRACT 0 — バグは登録前の敵対監査で 6 件取り切っていた(文字列が長さとして通る / MTF の無言 NaN / determinant の無言 inf / 高次 Zernike 基底の桁落ち / 108GB の内部確保 / 未報告の求積誤差)。行列の形が決まっている abcd_trace/jones_apply/mueller_apply は一様抽選だと実経路を一度も通らないため(実測 800 連鎖で 0 回)、OP_ARG_BUILDERS で半分は妥当な行列を合成する |
| ライトフィールド + 光子計数 追加(2026-09-01) | 40 | 新 34 op を登録。非 CONTRACT 0(バグは登録前の敵対監査で 8 件取り切り済み)。この波の主眼は op でなくファザー自身の穴 3 件(下記) |
| 未実行の掘り起こし(2026-09-01) | 62 | 「必須引数を束縛できず黙ってスキップ」を解消したところ、これまで一度も実行されていなかった 3d op から非 CONTRACT 5 件が出た(型宣言と実装の食い違い) |
この層自体が嘘をつきうる、という実測の記録です。4 件とも「バグが無い」と 「そもそも実行されていない」を区別できない形をしていました。
lf_from_mla の既定 angular=(5,5) は 32x32 を割り切れず 1200 連鎖で 0 回)。
→ OP_PARAM_HINTS(op 名で狙い撃ち)だけ既定値も上書きできるようにした。
名前レベルの PARAM_HINTS を既定値に効かせると既存 op の挙動が一斉に変わる
ので、そちらは据え置き。--coverage-out と族ごとの内訳。出した瞬間に
photon 族 10/17 =「fail-closed が効きすぎて 7 op が一度も実行されない」が判明。K、姿勢 R/t、RANSAC 閾値 thresh、voxel 化の bounds/res など。
→ ヒントを追加。カバレッジ 321 → 336、そして隠れていた非 CONTRACT が 5 件。型の到達可能性は不動点で解ける(初期プールの型から、入力が揃う op の出力型を
足していく)。実測では 434 op 中 refine_peak_newton 1 件だけが score 型の
生産者不在で構造的に到達不能だった → 種を追加。tests/test_chain_fuzz_minimize.py
がこの不変量を固定している。
候補が数百ある中で特定の op が長さ 6 の枠に入る確率は低い。そこで連鎖ごとに
目標 op を 1 つ決めて寄せる方式を入れた(目標は連鎖固有 seed から引くので
--minimize/--replay の前提を壊さない)。同一コード・1500 連鎖・長さ 6 の実測:
| explore | カバレッジ | 署名数 |
|---|---|---|
| 0.0(一様) | 336/434 | 55 |
| 0.3 | 340/434 | 68 |
| 0.7 | 341/434 | 62 |
狙い撃ちの寄与は +5 op です。321 → 336 の伸びは上記 4 番(引数ヒント)の効果で あって狙い撃ちの効果ではないので、混ぜて報告しないこと。なお小規模では狙い撃ち の方が悪くなります(60 連鎖 × 長さ 5 で 67 op vs 一様 167 op)—— 1 連鎖の幅を捨てて 希少 op への到達を買う取引なので、集計値の大小は走行規模で反転します。
先に試した「まだプールに無い型を産む op を優先する」型空間バイアスは効きません でした(1500 連鎖で 321 → 322)。プールが最初から全生成器型で埋まっているため、 優先対象がすぐ尽きるからです。効かなかった案も消さずに残しておきます。
wave-7 の 2 件はどちらも --minimize が 5→2 / 4→2 op に削り、その最小再現を
手で実行して初めて原因が確定しました(前者は「長い信号を係数に取り違え」、
後者は「上流の outlier 除去が雲を空にした」)。削り込みが無ければ 8 op の
連鎖から原因を読み解く必要があった — 収束フェーズの実利はここにあります。
catalog() に目録を追加。型語彙が
増えるなら TYPE_CHECKS と make_generators() にも足すこと。OP_ARG_BUILDERS に
「半分は妥当な値を合成・半分は pool から敵対入力」の builder を足すこと
(先例: abcd_matrix / wavefront_stats / abcd_trace / jones_apply /
mueller_apply)。NONFINITE_BY_CONTRACT に載せる
(先例: mat_cond、光学の depth_of_field / gaussian_beam)。RESULT_ADAPTERS を登録して call() が宣言型を返す形に
します(先例: mat_svd → {"U","s","Vt"})。tests/test_chain_fuzz_minimize.py(収束フェーズの契約)と
tests/test_chain_type_contracts.py(宣言型の一致)にあります。