「nullでも落ちないようにしました」。AIから返った修正は、たった一行。実際にnullを渡してもエラーが出なくなったので、そのまま取り込みたくなります。
ところが、今回の提案例はエラーを消す代わりに、一般ユーザーまで許可します。直してほしかった入力で成功しても、守るべき拒否条件が残っているとは限りません。 差分が小さいほど安心、とは言えない例です。
単純化した認可関数を使って、修正対象と維持すべき動作を同じ入力表で比較します。ゴールは、AIの説明を読んで終わらず、条件式を自分の言葉に戻し、取り込めない修正を具体的な入力で示せることです。
JavaScriptの関数とGit差分を読める方が対象です。2026年11月11日にmacOS・Node.js v26.8.1で掲載コードを再実行し、以下の出力を確認しました。題材は架空データを使うローカルの関数で、本番用の認証・認可システムではありません。
修正の成功条件には「変わってはいけないこと」も入れる
元の仕様は、roleがadminの利用者だけを許可することです。問題は、userがnullだとuser.roleを読めず例外になる点です。
ここで修正依頼を「例外を消す」だけにすると、仕様の半分が抜けます。今回必要なのは、次の状態です。
| 入力 | 期待する結果 | 役割 |
|---|---|---|
{role:'admin'} |
true | 許可していた人は引き続き許可する |
{role:'member'} |
false | 拒否していた人は引き続き拒否する |
{} |
false | 権限情報がなければ許可しない |
| null / undefined | false | 今回直したい例外を、拒否として扱う |
nullの行だけが通っても、この表全体を満たしたことにはなりません。変更レビューでは、修正したい症状の隣に、壊してはいけない動作を並べます。
条件式を日本語に戻すと、意味の変化が見える
元のコードと提案例の差分は、次の一行です。
- return user.role === 'admin';
+ return Boolean(user);
元の式は「roleがadminと一致するか」です。提案例は「userがtruthyな値か」へ変わっています。JavaScriptでは空のオブジェクトもtruthyなので、memberも権限情報のないオブジェクトもtrueになります。
例外が消えた理由は、nullからプロパティを読まなくなったからです。しかし同時に、roleを見ること自体をやめています。「nullへ対応した」という説明だけでは、この二つ目の変更は見えません。
修正版のuser?.role === 'admin'は、userがnullかundefinedならroleの取得を止め、結果のundefinedとadminを比較してfalseになります。今回の仕様では、許可条件を残したまま欠損へ対応できます。
元の例外を再現し、提案と修正版を同じ入力で比べる
新しい練習用フォルダに、次をreview.mjsとして保存し、node review.mjsを実行します。追加パッケージや外部アカウントは不要です。
import assert from 'node:assert/strict';
const before = user => user.role === 'admin';
const proposed = user => Boolean(user);
const fixed = user => user?.role === 'admin';
assert.throws(() => before(null), TypeError);
for (const [label,input,expected] of [
['admin',{role:'admin'},true], ['member',{role:'member'},false],
['empty',{},false], ['null',null,false], ['undefined',undefined,false]
]) {
console.log(`${label}: proposed=${proposed(input)}, fixed=${fixed(input)}, expected=${expected}`);
assert.equal(fixed(input),expected);
}
assert.equal(proposed({role:'member'}),true);
console.log('PASS: null failure reproduced; proposed authorization regression detected; fixed 5 cases');
実結果です。
$ node review.mjs
admin: proposed=true, fixed=true, expected=true
member: proposed=true, fixed=false, expected=false
empty: proposed=true, fixed=false, expected=false
null: proposed=false, fixed=false, expected=false
undefined: proposed=false, fixed=false, expected=false
PASS: null failure reproduced; proposed authorization regression detected; fixed 5 cases
assertのどれかが期待と食い違うと、AssertionErrorが表示されて0以外の終了コードで止まります。最後のPASS行まで出れば、すべての検査を通過しています。
注目するのは、memberとemptyの行です。提案例はtrue、修正版と期待値はfalseです。nullの例外を再現できたことだけでなく、「修正により以前の拒否が壊れた」ことを観察できます。
最後のPASSは、危険な提案例を採用してよいという意味ではありません。元の不具合を再現し、提案例の回帰を検出し、修正版の5ケースを確認した、という教材の検査結果です。
自分の差分では、変わった条件に入力をぶつける
レビューする変更が大きくても、最初に一つの条件式へ絞れます。変更前は何を判定していたか、変更後は何を判定するかを一文ずつ書き、二つの答えが異なる入力を探します。
今回なら「roleがadmin」と「オブジェクトが存在する」が違うので、memberや空オブジェクトが候補になります。上限チェックなら、上限ちょうどと超過した入力。未入力対応なら、空文字と0などが候補です。入力の選び方が、変更の意味から導けます。
Gitではgit status --shortで対象ファイルを確認し、未ステージはgit diff --、ステージ済みはgit diff --cached --で読みます。未追跡の新規ファイル(statusで??の行)は通常のdiffに出ないため、直接開いて確認します。
git diff --checkは行末空白やコンフリクトマーカーの検出には役立ちますが、今回の認可条件の変化は見つけません(今回の一行差分に対しても終了コード0で何も報告しません)。形式チェックと、条件を読んで実行する検査は役割が異なります。
期待と違ったときは、テストを緩めず変更目的へ戻る
| 観察した結果 | 判断と次の行動 |
|---|---|
| nullは直ったがmemberがtrue | 拒否条件の回帰。修正をそのまま取り込まず、許可条件を維持する変更へ戻す |
| adminまでfalse | 権限値の表記や入力構造が想定と合っているか確認する。許可範囲を広げてごまかさない |
| 練習コードは通るが実APIで壊れる | 関数に渡るデータの出所・型・呼び出し場所を調べる |
| AIの変更に関係ない設定や依存が混ざる | null対応に必要な理由を確認し、目的に必要な差分へ絞る |
この表は実装へ適用する際の調査順です。実APIの接続や認証基盤を、今回のローカル実験で検証したわけではありません。
正しい条件式でも、信頼する値の出所が違えば守れない
本例のroleは、信頼できるサーバー側の利用者情報から得たものという前提です。ブラウザから送られたrole:'admin'をそのまま信じるなら、関数の比較が正しくても認可を守れません。実サービスでは、本人確認とサーバー側の権限情報に基づく判定が必要です。
今回持ち帰りたいのは、修正版の一行だけでなく、レビューの見方です。直したい失敗を一つ、維持したい許可と拒否を一つずつ書く。 この三つを同じ入力表で見ると、修正の説明に隠れた意味の変化を具体的に指摘できます。
