結論から言うと、同じ
return Noneが、正当なフォールバックにも、バグを隠す握り潰しにもなります。 そして両者を分けるのは、コードの構文ではなく「その関数の役割・呼び出し側の期待・仕様」という意図です。だから構文パターンを見る静的検出は“候補”までは出せても、正当か握り潰しかの最終判定はできない——最後は人が要る。
これは私が「AIは失敗を黙って握り潰すはず。その混入率を数えよう」とローカルLLMに書かせ、仮説ごと崩れた末にたどり着いた話です。だから本記事の中身は「法則の証明」ではなく、実測で自分の事前予想が反証された記録——測ったら考えが変わった、という話です(崩れる過程は全部開示:§8)。対象: Python / TypeScript、生成はすべて無料・ローカル。主実験は 1.5B の120本。7B の30本は §12 の補足確認で、リポジトリの公開物は計150生成です。
30秒でわかる話(専門用語なし)
AIにコードを書かせて、「失敗を黙って握り潰す(エラーが起きたのに、何事もなかったように空の答えを返す)」コードが何本混ざるか数えようとしました。予想は外れました。
- 小さなAIは、多くの場合そもそも“失敗したときの処理”を書きません(Pythonで多数)。握り潰す以前の段階でした。
- 機械が「握り潰しだ」と拾ったコードを人間が全部開くと、誤検出か、仕様どおりの正しい書き方ばかりでした。
そこで見えた本当の問題はこれです。「失敗したら空を返す」という同じ書き方が、正しい処理のことも、バグ隠しのこともある。 しかも、コメントで「失敗したら空を返す」と書いてあっても、それが妥当な設計とは限りません。見分けるには「この関数はそもそも何をすべきか」を読むしかなく、そこは構文パターンを見るだけでは決められません(最後は人が仕様と文脈を読む必要があります)。だから、AIコードの品質チェックは機械で“怪しい候補”まで挙げ、最後は人が意図を確かめる——という二段構えが要る、という話です。
実務の一文:CIに入れるなら、緑は「合格」ではなく「人が見るべき候補がゼロ」という通知として扱う。それが本記事の結論です。
この下は、それを実測データで示す技術記事です。結論はここまでで伝わっています。
TL;DR
観察対象=ローカルOSS小型モデル(Qwen2.5-Coder 1.5B、補足で7B)× 少数タスク × N回生成の実測。一般法則ではなくこの標本・この条件での観察です(射程は§12)。
-
CIに入れても不思議でない naive 検出器を当てたら、候補4本は人手裁定で真陽性0(全て偽陽性か、仕様が明記された正当なフォールバック)。ただしこの“0”は「AIは握り潰さない」ではありません——検出器は構造上
ifガード型を最初から見ておらず、握り潰しっぽい形が実際に住む場所(fetch_json×4・§9)を素通りします(=真陽性0はスコープの産物)。失敗経路100本(Python50・TS50)。 -
この標本では「握り潰す」より「派手に落ちる」方が多かった。Python の失敗経路50本は
raiseで loud に失敗14本/例外を素通り21本が主で、握り潰し候補はごく少数。TS は 33本(66%)が try/catch+log/再送出。——ただしこれは AIRA の集団レベル所見(fail-soft がAI由来で1.80×)や“静かに失敗する”パターンの存在を否定しません(後者はむしろ §9 のfetch_jsonが裏づける)。崩れたのは「生成物の大半が握り潰しだろう」という素朴な期待だけです(言語差は交絡ありの“観察”=§6)。 -
本記事の核(新理論の主張ではなく、実物で確かめた一点)=“握り潰しか否か”は構文では決まらない。
except: return Noneもelse: return Noneも、正当な契約にも握り潰しにもなる。決め手は docstring・コメント・外部仕様・呼び出し側の期待という意図で、しかもコメントがあっても妥当とは限らない(後述のreturn 0/fetch→Noneは文書化済みでも危うい)。構文パターンだけの静的検出は候補を surface できても、最終判定=意図の裁定はできない(§9)。 - これは先行研究 AIRA の線引きと一致する。AIRA は握り潰し系(C03: Broad Exception Suppression)を決定的に検出し、AI由来コードで人手由来の約1.80倍多いと報告する一方、論文自身が**「フラグされたパターンが欠陥とは限らない・一部の fail-soft は文脈上意図的・是正の前に必ず人手レビューが要る」と明記**している。機械は候補を出す(sensitivity)、人が欠陥か裁定する(adjudication)——本記事はその残余を小さなコーパスで具体化した(§10)。
実測サマリ(Qwen2.5-Coder 1.5B・temp0.7・seed0–9・失敗経路タスク各10生成・Windows/CPU)
失敗経路タスク=各言語50本(純計算の対照20本は別掲)。分類は「失敗時にどう振る舞うか」を、Python は AST、TypeScript は正規表現で一次判定し、全120本の分類ラベルを目視確認、握り潰し候補と境界例はコード本文・docstring・コメントまで開いて人手裁定(分類器出力そのままではない)。
| try/except の使い方(失敗経路・n=50/言語) | Python | TypeScript |
|---|---|---|
| try/except を使い、log か再送出(proper) | 9 | 33 (66%) |
| try/except 内で default 返し(握り潰し“候補”) | 2 | 0 |
| try/except を使わない | 39 (78%) | 17 (34%) |
⚠ 「try/except を使わない 39本」は“無処理”ではない(全数分割):14本は
if/elseでraise(loud に失敗)、4本はifガードでreturn None(すべてfetch_jsonの非200時=私の try/except 対象の検出器が最初から見ていない fallback)、21本は bare(失敗経路を書かず素通り)。“使わない率”を“握り潰し率/無処理率”と読み替えないこと(§6)。
| 検出器の before → after(人手確認) | 件数 |
|---|---|
| naive Semgrep のヒット(try/except 対象・before) | 4(Python2・TS2) |
| うち偽陽性 | 2(TS:関数外の“使用例”の空 catch) |
| うち文書化された正当フォールバック | 2(Python:docstring で契約明記) |
| try/except 内の“意図非開示の握り潰し” | 0 |
(注)検出器のスコープ外= if ガード型の default 返し |
別途存在(§6・§9) |
1. なぜこれを測ったのか/どこが空白か(白地)
AI生成コードの品質を扱う日本語の発信は、大きく2つに偏っています。
- (a) 定性的なレビュー指南:「握り潰しを見抜け」という“目視のコツ”。例:エラーを『握りつぶす』コードの見抜き方(@nrEngineer)、結城浩さんのフォールバックについて、警告を握り潰していた実体験(Zenn/eno49)。鋭いが属人的な“勘”の共有に留まる。
- (b) セキュリティ寄りの自動チェック:無料CLIで30秒セキュリティチェック(@pythonista0328) など。CLI+CIゲートはあるが、対象は秘密漏洩・脆弱性で、握り潰しに絞って“そもそも自動判定できるのか”を実測した記事は無い。
本記事が埋めるのは、「握り潰しは静的検出で自動判定できるのか?」を、無料・ローカル・再現可能なコーパスで実際に測り、“候補までしか出せない”ことを実物specimenで示すという空白です。あわせて、これは先行研究 AIRA が「決定的に候補を出しつつ、是正の前に人手レビューが要る」とした残余を、誰でも clone&run で追試できる形にしたものでもあります。
射程の宣言:「AIは危険だ」でも「静的解析は無力だ」でもありません。機械で当たりを付け、最後は人が意図を締める——その検証規律の“最後は人”の部分を実測で裏づける話です。
2. サイレント欠陥とは(CWE-703)
「失敗(例外・エラー)を、ログも再送出もせずに正常値へすり替えて飲み込む」コードを、本記事では**サイレント欠陥(握り潰しフォールバック)**と呼びます。分類上は CWE-703(Improper Check or Handling of Exceptional Conditions) に対応します(より特異的には CWE-1069 空 catch や CWE-390 も近い)。例外アンチパターンを体系化した研究(de Pádua & Shang, arXiv:1704.00778。対象は Java/C# で、Unhandled・Catch Generic 等を含む)もこの領域を扱っています。
厄介なのは、これが lint を素通りしやすいこと。だから当初、私は「意味を見る自作ルールで握り潰しを数えれば、lint の盲点を数字で突ける」と考えました。その仮説が、この記事の出発点であり——そして§8で崩れます。
3. 先行研究 AIRA と本企画の立ち位置(preprint・因果不主張)
AIRA(arXiv:2604.17587v1, 2026-04-19, W. M. Parris, preprint) は、決定的(parser-backed)な15チェックの静的解析エンジンで(LLM補強は任意モード。論文いわく "parser-backed deterministic analysis — not LLM-assisted classification")、AI由来コード955件と人手由来コード955件(=著者性による対照群であって人手評価ではない)を比較し、高深刻度所見が 0.435 vs 0.242=1.80×、なかでも握り潰し系 C03: Broad Exception Suppression が最多(Study 3 で 263 vs 185)と報告します。
ここが本企画の接続点です。AIRA は握り潰し“候補”の検出は決定的にやってのける(むしろ論文は、LLM評価器を当てるとこの種の抑制を決定的スキャナ比 44:1 で見逃す、と警告する=“意味判定に頼ると却って見落とす”)。にもかかわらず AIRA 自身が「重大度はヒューリスティック、フラグされたパターンが文脈上の欠陥とは限らない、是正の前に必ず人手レビューが要る」と明記しています(これが本記事の主たる論拠=原典に逐語で存在)。さらに AIRA は一部チェック(C07/C12)を——repo規模の意味比較が自動化できないという別の理由で——人手レビュー専用に固定してすらいます(=補強)。つまり AIRA が“候補は決定的に出せる/だが欠陥か否かの裁定は人手に残した”その残余こそ、本記事が具体specimenで示す対象です。
- 区別を明示:AIRA が決定的に出せるのは 構造的パターン(broad な except、suppression の形)=sensitivity。本記事が「機械には無理」と言うのは その候補が正当か握り潰しかの意味判定=adjudication。別レイヤなので AIRA と矛盾しません。
- ツール(著者非依存)と実証(AI由来で1.80×)は分けて引用。AI起因の増分や理論化は AIRA に譲り、本記事では主張しません(因果不主張)。射程は「無料・ローカルOSS小型生成の自コーパス実測」。
- ただし AIRA は単著・査読前の preprintであり、本記事は外部の文脈(1.80×・C03最多・免責文)をそこに借りています。AIRA 自身、AI起因を「Reward-Shaped Failure Hypothesis」という仮説と述べている=だから本記事は数値を引用しても因果は主張しない、という立場です。
4. なぜ標準 lint では足りないのか(当初の手法設計)
標準 lint の守備範囲(一次確認済み):
-
Python / ruff:
S110(try-except-pass)・S112は空握り潰しを検出するがデフォルト無効でselect/extend-select明示が要る。except Exception: return []のような型を合わせた握り潰しは素通りしやすい。 -
TypeScript / ESLint:
no-emptyは空ブロックのみが対象で、catch (e) { return null; }は空でないのですり抜ける。しかもno-emptyはコメントだけの catch も許容します(=「コメントがあるから意図的」という扱いは lint 文化にもある。だが、そのコメントが妥当なエラー処理かは別問題——§7・§9)。
そこで「意味パターン」を見る Semgrep ルールを書きました(naive 版・本記事の before):
rules:
- id: py-swallow-return-default
languages: [python]
severity: ERROR
message: "Exception caught and a default value returned without logging/re-raise (silent fallback)."
patterns:
- pattern: |
try:
...
except $E:
return $R
- metavariable-pattern:
metavariable: $R
patterns:
- pattern-either:
- pattern: None
- pattern: "[]"
- pattern: "{}"
- pattern: "0"
- pattern: "False"
- pattern: '""'
- id: py-except-pass
languages: [python]
severity: ERROR
message: "Exception swallowed with pass (silent failure)."
patterns:
- pattern: |
try:
...
except $E:
pass
(上はリポジトリの実ルールをそのまま掲載。TypeScript 版も同型で catch { return null }・空 catch 等を対象に同梱しています。§7 の TS 検出はこのルールの結果です。)
この時点の私の仮説は「このルールで数えれば握り潰しの混入率が出る」。ここから実測が始まり、仮説が崩れます。
5. 実験設計(統制条件・二層再現)
-
タスク:失敗経路が必然の業務I/O を Python/TS 各5本(設定読み込み・HTTP取得・環境変数・数値パース・ファイル先頭行)+純計算の対照を各1本(平均・合計=try が不要なタスク)。計12タスクを
tasks/tasks.jsonに凍結。プロンプトは中立(「エラー処理を書け」等の誘導をしない)。生成は各タスク N=10(seed 0–9・temperature 0.7)。120本=失敗経路100本(各言語50)+対照20本。 - タスク設計の開示(“出やすさ”で選んでいない):予備実験で、「JSON を読んで返すだけ」のような素直な仕事ではモデルはそもそも try/except を書かず、握り潰しも起きないと分かりました。つまり“握り潰し率”はタスク設計でいくらでも動きます。だから失敗が必然の I/O に寄せましたが、それは握り潰しを増やすためではなく(結果はむしろ§7の通りほぼ0)、失敗が起こり得る土俵を用意した上で、純計算の対照(try 不要)で「土俵の外では何も出ない」ことまで測るためです——数値を演出しない仕組みとして開示します。実際、純計算の対照20本は try/except 0・握り潰し候補0(失敗が必然でない土俵では何も出ない)を数字でも確認しました。
- 生成元:Ollama(MIT・無料・CPU可)上の Qwen2.5-Coder 1.5B(Apache-2.0)。7B は §12 の頑健性脚注で参照。
-
分類の非対称を先に開示:Python は
ast(構文木)で厳密に、TypeScript は正規表現で一次分類しています。TS 側は完全な TS パーサではないため、言語間の絶対比較には測定器バイアスが乗りうる(§12)。だから分類器出力をそのままGTにせず、全120本の分類ラベルを目視確認し、握り潰し候補と境界例はコード本文・docstring・コメントまで開いて人手裁定しました。 -
二層再現:生成(非決定・著者が1回)は
raw/に全件無選別で凍結。読者が再現するのは決定的な解析側(同じ120本に同じ分類器・ルールを当てれば同じ分布と候補数が出る)。ただし**“正当か握り潰しか”の最終ラベルは著者の人手裁定**でgt.csvに透明化しており、そこは読者が機械的に再導出できるものではありません(それ自体が本記事のテーゼ)。 - 決定性の実測(M0):temperature 0・スレッド数固定なら同一 seed 2回で byte 完全一致(sha 一致を実測)。分布を出すには temp>0 が要ると確認の上で temp0.7 を採用。
6. 実測①:多くは「握り潰し」ではなく“素通り”か“loud な失敗”だった
失敗経路タスク(各言語 n=50)の try/except の使い方:
| try/except+log/再送出 | try/except で default返し | try/except 不使用 | |
|---|---|---|---|
| Python | 9 | 2(候補) | 39 (78%) |
| TypeScript | 33 (66%) | 0 | 17 (34%) |
ここで**“try/except 不使用”を“無処理”と読むのは誤り**です。Python の39本を AST で全数分割すると:
-
14本は
if/elseでraise(例:環境変数が無ければraise ValueError(...))=むしろ loud に失敗。皮肉なことに、後述§9で「握り潰していない良い例」として挙げるget_api_tokenも、try/exceptを使わない書き方なので、この“不使用39本”に含まれています。 -
4本は
ifガードでreturn None(すべてfetch_json:非200ならreturn None)=default 返しだが、try/except を対象にした私の検出器は最初から見ていない(スコープの穴。§7・§9)。※§9(B) はこの4本の代表で、1例の特殊事情ではなくタスク内クラスタです。 - 21本は bare(失敗経路を書かず、呼ばれ方次第でそのまま例外が伝播)。
- (検算:14 + 4 + 21 = 39)
一方 TypeScript は66%が try/catch を書き、その中身は console.error(...)+throw(再送出)が大半=適切な処理で、握り潰していません。
言語差(Python は try/except を避け、TS は書く)は**“発見”ではなく“観察”**として提示します。プロンプト・言語イディオム(TS は async/await+try/catch が定型)・**分類器の非対称(Python=AST/TS=regex)**という交絡があり、絶対比較には向きません。本記事の背骨は言語差ではなく§9です。
つまり当初の仮説「AI は失敗を N% 握り潰す」は、この標本では“握り潰し”が支配的な現象ではなかったという形で崩れました。
7. 実測②:検出した“握り潰し”は、全部が偽陽性か正当だった(+検出器のスコープの穴)
naive Semgrep(try/except と空 catch を対象)は120本中 4本を挙げました(Python2・TS2)。人手で全部開いた結果:
TS の2件(ts_load_config)=偽陽性。 検出対象の関数本体は手本のような処理でした:
async function loadConfig(path: string): Promise<any> {
try {
const data = await fs.promises.readFile(path, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error(`Error reading or parsing the file at ${path}:`, error);
throw error; // ← ログして再送出。握り潰していない(proper)
}
}
// 以下は「使用例」としてモデルが付け足したコード
(async () => {
try { const config = await loadConfig('./config.json'); }
catch (error) { /* Handle any errors... ← コメントだけの catch */ }
})();
naive ルールが叩いたのは末尾の**“使用例”ブロックの catch**でした。原因は2つ重なっています——(a) テスト対象の関数の外の demo コードを拾った/(b) コメントだけの catch を「空」と誤認した(Semgrep の AST はコメントを無視)。文脈を見れば偽陽性です。
Python の2件(py_parse_int)=文書化された正当フォールバック。 docstring に「変換不可なら None を返す」と契約が明記されています(もう1件はコメントで同旨)。
結果:try/except 内の“意図非開示の握り潰し”は 0。 ——ですが、ここで正直に言うべき検出器自身の穴があります。私のルールは try/except を対象にしており、if ガード型の default 返し(fetch_json が非200で return None=§6の4本)を最初から見ていません。
用語を分けておきます:
-
default 返し:失敗時に
None/0/[]等を返す形(構文で拾える“候補”)。 - 握り潰し:default 返しのうち、呼び出し側に失敗を区別させない危険なもの。
- 正当フォールバック:役割・仕様上、その default 返しが妥当なもの。
つまりコーパスに「default 返し」は存在します(fetch_json の4本)。ただしそれを即「握り潰し」とは呼びません——握り潰しか正当フォールバックかは、関数の役割・呼び出し側契約・仕様を読んで初めて裁定できます(§9)。だから本記事の「握り潰し 0」は正確には**「try/except 検出器のスコープ内で、意図非開示の握り潰しが 0」であって、「コーパスに問題含みのフォールバックが 0」という意味ではありません**。
8. 転回:仮説が崩れた過程(誤→正)
「測ってから語る」ため、崩れた順に開示します。
- 誤①「AI は握り潰しを混ぜる、ルールで数えれば混入率が出る」→ 正:測ったら握り潰しは支配的でなく、素通り・loud な失敗・(TSでは)proper な処理が主だった。
- 誤②「検出器が挙げた4件=握り潰しの実数」→ 正:4件は全部が偽陽性か文書化済みの正当。数字を盛らず 0 と書く。
-
誤③「偽陽性は自分のルールが下手なだけ、精緻化すれば消える」→ 正:精緻化で TS の偽陽性は消せたが、
return None/return 0が正当か握り潰しかは、文書化の有無でも決まらないと気づいた(§9)。ここでテーゼが「混入率の計測」から「握り潰しの裁定は構文では閉じない」へ張り替わった。加えて、自分の検出器がifガード型を最初から見ていないというスコープの穴にも気づいた(§7)。 -
実装の落とし穴(cp932):Semgrep が TS ルールの YAML を Windows のデフォルト cp932 で読み、message に紛れた全角ダッシュ「—」で
UnicodeDecodeError(exit 2)。→ ルールを ASCII 化+実行時PYTHONUTF8=1を規律に(このルール自身の非ASCIIも公開前に潰した)。 - この記事の執筆そのものが、テーゼの実演になりました。 下調べは AI を使って進めたのですが、公開前に主張を一次ソース(論文本体)へ当て直したところ、私は当初「先行研究 AIRA は"人手評価"で握り潰しを判定した」と書いていた——これが事実の逆でした。AIRA は決定的な静的スキャナで、握り潰し系を"自動で"検出すると主張し、むしろ意味判定(LLM 評価器)に頼ると見落とすと報告しています。もっともらしい要約を鵜呑みにしていたら、"検証規律"を掲げる記事が引用論文の方法論を読み違えたまま公開するところでした。一次に当てて撤回・修正しています(正しい接続は§3・§10)。「候補は機械が出せる、でも最終判断は人が原典・意図に当たる」——この記事の主張を、この記事を書く過程で自分が踏みました。
9. 最小反例:同じ return None を分けるのは、構文の外にある
握り潰し“候補”の典型は「失敗時にデフォルト値を返す」です。では次の2つ、どちらが握り潰しでしょうか。
# (A) 数値パース:変換不可なら None(契約として docstring に明記)
def parse_int_field(data, key):
"""Returns the int, or None if it cannot be converted."""
try:
return int(data[key])
except ValueError:
return None # 役割上、妥当なフォールバック
# (B) HTTP取得:非200なら None(コメントで一応そう書いてある)
def fetch_json(url):
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
return None # 404 も 500 もネットワーク断も、区別なく None に潰れる
どちらも「失敗したら return None」で、返す部分の構文はほぼ同型です。しかし (A) は関数の役割(変換の可否を返す)に照らして妥当な契約、(B) は呼び出し側が「データが空」なのか「取得に失敗した」のかを永久に区別できない——同じ return None でも意味がまるで違う。しかも (B) にはコメントがある。つまり “文書化されているか”では決まらない(文書化された return 0/return None が、下流の計算や分岐を静かに汚すことはいくらでもある)。
この (B) こそ、本記事の一番強い実物です。 fetch_json は非200を一様に None へ潰す“文書化されていても危うい default 返し”で、コーパスでは4本のクラスタ(偶発でない)として現れました。そして try/except を見る naive 検出器はこれを構造的に取り逃します。=「静かに失敗するコードは実在する(AIRA が集団レベルで示した傾向と整合)/だが自動検出は、それが是か非かの最終判定は下せない」——正当例 (A) と危うい実物 (B) の対が、本記事のテーゼそのものです。
対照的に、握り潰していない書き方は意図が構文の中で閉じています:
def get_api_token():
if 'API_TOKEN' in os.environ:
return os.getenv('API_TOKEN')
raise ValueError("The API_TOKEN environment variable is not set.") # loud に失敗
要点:構文パターン(except: return None / else: return None)は候補を surface できる。だが、それが正当な契約か握り潰しかを分ける情報——関数の役割・呼び出し側の期待・仕様・(あれば)文書の“正しさ”——は、そのパターンの外にある。 だから構文中心の静的検出だけでは最終判定できない。「原理的に決定不能」とまでは言いません(より賢い解析=型・データフロー・呼び出し側解析なら候補の精度は上げられる)。ただし、「この関数がこの文脈で何を返すべきか」という仕様は、解析器そのものの外から来ます。だから解析が賢くなるほど候補は絞れても、その候補を仕様と突き合わせる最後の一歩は残る。言えるのは、構文パターンマッチ単体では意図の裁定はできず、候補までしか出せない、ということです。
10. だから機械は候補まで、人が裁定する(AIRA の線引きと一致)
§9 の帰結は、先行研究 AIRA の設計とちょうど一致します。AIRA は握り潰し系(C03)を決定的に検出し、AI由来コードで1.80倍多いと示す=候補を出すのは機械の得意分野。しかし AIRA 自身が「フラグ=欠陥ではない・一部の fail-soft は文脈上意図的・是正前に人手レビューが要る」と明記し、握り潰しの“欠陥か否か”の裁定を人手に残しています。
実務的な二段構え:
- 機械にできること(sensitivity):失敗時に default を返す“候補”や try/except の使い方の分布を自動で出し、CI ゲートで通知する。
- 機械にできないこと(adjudication):候補が「役割上妥当な契約」か「バグを隠す握り潰し」かの最終判定。これは関数の役割・呼び出し側・仕様を読む人間の仕事。
だから「自動チェックが緑=安全」では閉じません。候補までは自動化し、最後の意図判定は人が締める——実測から出た結論です。
11. 再現手順(clone&run)+ CI への組み込み
git clone https://github.com/axiom-pro/ai-silent-defect-scanner && cd ai-silent-defect-scanner
# 機械的に再現できる部分:同じ120本に分類器・ルールを当てる → §6の分布・§7の候補数
PYTHONUTF8=1 python scripts/classify_split.py # 失敗経路の分布(§6の表・n=50/言語)
PYTHONUTF8=1 python scripts/scan_and_count.py # naive Semgrep の候補(§7 before)
PYTHONUTF8=1 python scripts/build_gt.py # 人手裁定層 results/gt.csv を再生成
make scan-mine DIR=/path/to/your/repo # 自分のリポの分布・候補(最終判定は人)
-
機械再現=分布と候補数まで。“正当か握り潰しか”の最終ラベルは
results/gt.csvに著者の人手裁定として透明化しており、そこは読者が機械的に再導出するものではありません(=§9 の主張そのもの)。 -
raw/=著者が生成した監査対象コード(固定同梱)。PROVENANCEにモデル・量子化・seed・温度・スレッド数・OS・日時。生成をやり直すと分布は変わる(LLM 非決定性の論点)。 - CI では
semgrep scan --errorで「候補あり」を exit 1 にできますが、それは“人が見るべき候補”の通知であって合否ではない(§10)。
12. 限界の開示(射程)
- モデル1種・小型(1.5B)中心・少数タスク。挙動はモデル・量子化・タスク設計に強く依存。射程は「無料枠スケールのローカルOSS小型生成の自コーパス」で、一般化はしません。
- 統計的独立性の注記:同一プロンプトからの N=10 生成は互いに相関する擬似反復で、集団一般化の独立単位はタスク(各言語5)であって生成(50)ではありません。表の「n=50」は“生成数”であって“独立標本数”ではなく、集団レベルの精度はその見かけより低い。本記事のテーゼは比率でなく §9 の実物(A/B・fetch_json)に載るので結論は左右しませんが、数表を母集団推定として読まないでください。
-
検出器のスコープと「握り潰し0」の射程:naive ルールも3分類器(Python)も try/except を主対象にしており、
ifガード型(§7 のfetch_json)は取りこぼします。だから本記事の「握り潰し 0」は**「try/except スコープ内で意図非開示の握り潰しが0」の意味で、「コーパスに問題含みフォールバックが0」ではありません**(§9 のとおり、開示があっても妥当とは限らない)。 -
分類器の非対称:Python は
ast、TypeScript は正規表現(ネストした波括弧を含む catch を取りこぼしうる)。全数を人手GTでバックストップしていますが、TS の絶対比較は割り引いて読んでください。 -
頑健性脚注(7B):同じ失敗経路タスクを Qwen2.5-Coder 7B で各3生成(Python15・TS15の少数)した範囲では、Python は try/except 不使用 10/15・proper3・候補2、TS は不使用 8/15・proper7・候補0。候補2件は再び parse_int で docstring 明記=正当でした(うち1件は失敗時に
return 0を返す例で、docstring 明記でも下流の計算を静かに汚し得る=§9 で触れた“文書化済みでも危うい”の実例)。N=3 の少数確認なので「頑健」と断定はしません(少なくともこの範囲では§9の主張は反証されなかった、という程度)。分母は 1.5B が50本・7B が15本で異なります。7B は失敗経路のみ計30本(=主実験120本+7B30本で全150生成)。7B の分類はresults/footnote_7b.csv・results/classification_summary.txtに同梱しています。 -
裁定者の穴(検出器の穴と対に自己開示):候補の「正当/握り潰し」の最終裁定は単一評価者(筆者・COIあり)が行い、rater間一致は取っていません。使った判定 rubric=①失敗時に default を返すか ②その挙動が docstring/コメント/仕様で意図として開示されているか ③関数の役割に照らし、呼び出し側が区別すべき失敗情報を消していないか(②③を人が判断)。境界例(parse_int=正当/fetch_json=開示済みでも危うい)は判定理由付きで
gt.csvに公開。第二評価者・rater間一致の取得は今後の課題=**「最後は人」という本記事の急所を、検出器の穴と同じく自白しておきます**。 -
COI(利益相反):筆者は AIコード監査サービスの出品者で、「最後は人手が要る」は事業上の立場に有利になり得る結論です。だからこそタスク凍結・raw 全件無選別・人手裁定を
gt.csvで公開・偽陽性と検出器の穴を自分から開示という形で、読者が検証できるようにしています。
まとめ:緑を「合格」でなく「まだ反証されていないだけ」と読む
「AI は失敗を握り潰す率が高い」と思って測り始めたら、握り潰しは支配的ではなく、検出できた候補は偽陽性か正当でした。そして本当の発見は、同じ return None が正当にも握り潰しにもなり、その違いは構文の外(役割・呼び出し側・仕様)にあるから、構文パターンだけでは最終判定できないこと。機械は候補まで、人が意図を裁定する。だから lint 緑は「合格」ではなく「まだ反証されていないだけ」 です。
本記事が積極的に主張するのはこの1点=「構文パターン単体では正当/握り潰しの裁定はできない」だけです。「原理的に決定不能」「言語差は本質的」「AI起因で増える」は主張していません(測っていない、または言い切れないから)。
計測コード・全150生成・人手GT(gt.csv)はリポジトリに同梱。落とし穴ログは本文§8。数値はすべて実測値で、未測定のものは「測っていない」と明記しています。
リポジトリ:https://github.com/axiom-pro/ai-silent-defect-scanner
筆者について(COI 開示つき)
tauridev(ソフトウェア開発/AIコード監査)。Rust/Tauri+React/TypeScript でローカルファーストの Windows アプリを開発しつつ、「AI に本番品質を出させ、AI の誤りに気づく検証規律」を専門にしています。
なお筆者は AIコード監査サービスを提供しており、本記事の結論(最後は人が意図を読む必要がある)は筆者の事業上の立場にも有利になり得ます。COI として明記した上で、生成物・分類器・人手GT・偽陽性・検出器の穴まで公開し、読者が検証できる形にしています。本記事の §7 でやった作業——検出器が挙げた候補を1本ずつ開き、docstring と呼び出し文脈から「正当なフォールバックか、隠れたバグか」を判定する——が、まさに機械には代われず人手で行う監査そのものです。
- 🔍 AIで作ったアプリの不具合を監査・修正 → ココナラ
- プロフィール:coconala.com/users/6153961 / 公式:getaxiom.dev
