検査の型

AIに書き換えさせた結果を検査するコードの書き方|数えるだけの検査は通ってしまう

AIに一括で書き換えさせたあと、「壊れていないか」をどう確かめていますか。100ファイルの書き換え自体は失敗しません。難しいのは、その結果が意図どおりかを人間が判断できなくなるところです。検査コードを書くことになりますが、その検査コード自体がよく間違います。実際に出した間違いを、直し方とセットで並べます。

このサイトはAnthropicの公式サイトではありません。Claude Codeの機能・料金・提供範囲は変わる可能性があるため、重要な判断の前には公式のドキュメントと自分のアカウント設定を確認してください。

先に結論:検査を書く前に見る表

私たちが実際に出した誤判定を、8つの型に整理しました。検査コードを書く前に、自分の検査がどれかに当てはまっていないか照合してください。

やりがちな検査何が起きるか直し方
タグの絶対数を数える既存の不均衡を拾って偽陽性変更前後の差分を比べる
特定の要素の中だけ探す外にあるものを「無い」と判定0件が出たら範囲を疑う
否定形で判定する取得失敗も合格になる取得成功を先に確認し、三値で返す
意図した変更だけ数える副作用に気づけない意図しない変更も数える
数件だけ見る条件付きの失敗を取りこぼす全数、または失敗条件を列挙して全部見る
1回だけ見るキャッシュの揺れで誤判定複数回見て、駄目なら復旧
対象リストを各所に書き写す漏れた対象で同じ欠陥が再発し、検査は毎回「正常」リストを1ファイルにして全スクリプトが読む
編集開始時の取得を前提にアップ他の作業の変更を黙って上書きアップ直前に再取得して一致を確認

全部に共通しているのは、検査が「通った」ことと、作業が「正しかった」ことは別だということです。検査が何を見ていないかを言えるようにしておくと、報告の精度が上がります。以下、それぞれ実際に踏んだときの状況と直し方を説明します。

絶対数で見ると偽陽性が出る

HTMLを書き換えたあと、構造が壊れていないかを見るために開始タグと終了タグの数を数えました。

const open  = (html.match(/<div\b/g) || []).length;
const close = (html.match(/<\/div>/g) || []).length;
if (open !== close) console.log('壊れている: ' + url);

これを流すと大量に「壊れている」が出ました。ところが調べると、変更前から数が合っていませんでした。閉じ忘れのまま何年も動いているページがあったわけです。ブラウザは補正して表示するので、誰も困っていませんでした。

つまりこの検査は、自分の変更とは無関係な既存の状態を拾っていました。見るべきなのは絶対数ではなく変更によって差が変わったかどうかです。

const diff = h => (h.match(/<div\b/g) || []).length - (h.match(/<\/div>/g) || []).length;
if (diff(before) !== diff(after)) console.log('開閉差が変わった: ' + url);

こう書き直すと、既存の不均衡は無視され、自分が壊した場合だけが出ます。検査の対象は「正しい状態」ではなく「自分の変更」です。

探す範囲が狭いと「無い」が出る

全ページのh1を点検したとき、「h1が空のページが多数ある」という結果が出ました。報告する直前に確認したら、間違っていました。

原因は、<main> の中だけを対象に探していたことです。そのサイトのh1は <main> の外側にありました。範囲を全体に広げて測り直すと、1,018ページでh1の欠落はゼロでした。

ここから決めたのは、「0件」や「全滅」という結果が出たら、まず測り方を疑うということです。実運用しているサイトで全ページに同じ欠陥がある、というのは起こりにくい話です。極端な数字は、たいてい検査側の問題です。

否定形の判定は取得失敗も通す

あるドメインがサーバーに登録済みかどうかを、初期画面の文言で判定しました。

const ok = !/webserver is functioning normally/.test(res.body);

応答が取れずbodyが空文字のときも、この式は true になります。結果、未登録のドメインが「登録済み」と表示されました。

否定形で書いた判定は、対象が存在しないときにも通ります。書くなら取得に成功したことを先に確かめてから中身を見る順です。

if (res.status !== 200 || res.body.length === 0) return '取得できなかった';
return /webserver is functioning normally/.test(res.body) ? '未登録' : '登録済み';

「取得できなかった」を、合格でも不合格でもない第三の値として返すのが大事です。二値に押し込むとどちらかに誤って混ざります。

書き換えが副作用を作っていないか

リンクの張り替えをしたときに、ページが自分自身へのリンクを持つ状態を作ってしまいました。統合先のURLへ一括で付け替えたので、統合先のページ自身にも自分宛てのリンクが残ったわけです。

検出は簡単で、書き換え後に「リンク先が自分のパスと同じもの」を数えます。消すときに引っかかったのは次の2点でした。

  • アンカーだけ消すと <li></li> が残る。空の項目が並びます。<li> ごと消す処理を先に当てて、それから素のアンカーを消しました
  • href が最初の属性とは限らない。<a class="..." href="..."> の形があるので、属性の順番を決め打ちした正規表現だと取りこぼします

一括処理では、「意図した変更が入ったか」と同じくらい「意図しない変更が入っていないか」を測る必要があります。前者だけ見ていると、副作用に気づけません。

標本検査で「ok」は証明にならない

56ドメインに同じ設定を入れたあと、数件だけ確認して「問題なし」と判断したことがあります。あとで全数を測ったら、いくつか漏れていました。

数件で確認したくなるのは、全数だと時間がかかるからです。ただ、一括処理の失敗はたいてい特定の条件を持つファイルだけで起きます。ファイルの構造が違う、既に別の設定が入っている、といった理由です。無作為に数件選ぶと、その条件を持つファイルに当たらない確率のほうが高いわけです。

全数が現実的でないほど多い場合は、失敗しそうな条件を先に挙げて、その条件に当てはまるものを全部見るようにしています。「一番古いファイル」「一番大きいファイル」「別の設定が既に入っているファイル」といった具合です。

反映が揺れる環境では複数回見る

サーバーにキャッシュが挟まっていると、設定を変えた直後は301と200が行ったり来たりします。1回だけ見て「301になっていない、失敗だ」と判断すると、実際には成功しているのに戻す、ということが起きます。

対処として、確認は間隔をあけて複数回、期待する結果が出たらそこで打ち切る、という形にしました。逆に、期待する結果が最後まで出なかったときだけ復旧処理に進みます。

let r = null;
for (let i = 0; i < 4; i++) {
  await sleep(1500);
  r = await check(host);
  if (r.ok) break;
}
if (!r.ok) await rollback(host);   // 4回見て駄目なら戻す

「複数回見る」と「復旧する」をセットにしておくと、揺れで誤判定しても被害が出ません。

対象リストの手書き写しは、必ずズレる(8月16日追記)

検査コードそのものが正しくても、検査する対象の一覧が間違っていると意味がありません。2026年8月16日に、これが2件同時に見つかりました。

定期検査のスクリプトが複数あり、それぞれが対象サイトの一覧を自分のファイルの中に直接持っていました。運用するサイトが10から17に増えたとき、更新されたのは一部だけでした。結果はこうです。

  • 5サイトが、どの定期検査にも入っていなかった。5サイトとも、検査対象のサイトでは前日に直したのと同じ欠陥を抱えたままでした。うち1サイトは143URL中14本しか検索結果に出ていませんでした
  • ある欠陥を横展開検査するために作ったスクリプトの対象に、その欠陥が最初に見つかったサイト自身が入っていなかった。7本・139表示ぶんの取りこぼしが、検査されないまま残っていました

2つめは笑い話のようですが、手で書き写している限り必ず起きます。書き写した瞬間はどのコピーも正しく、ズレるのはリストが変わった未来だからです。コピーが3か所あれば、変更のたびに3か所すべてを思い出して直せる人はいません。しかも検査自体は毎回「正常」で完走するので、漏れていることに気づく仕組みがどこにもありません。

対策は素朴で、対象リストを1ファイルにして、全スクリプトがそれを読み込む形にしました。

// sites.js — 対象リストの唯一の正
module.exports = { SITES: [
  { host: 'example-a.jp', gsc: true },
  { host: 'example-b.jp', gsc: false },  // APIの権限が無い対象
]};

// 各スクリプト側は、これを読むだけ
const { SITES } = require('./sites.js');

もうひとつ工夫があります。検査によっては使えない対象(APIの権限が無いサイトなど)があるため、除外の理由をリスト側の属性として持たせました。スクリプト側の条件分岐で間引くと、それがまた手書きの分岐になって同じズレ方をします。「なぜ外れているか」がリストのファイルに書いてある状態にしておくと、権限を足したときに1か所直すだけで全部の検査に反映されます。

アップ直前に、もう一度取り直す

本番のファイルを書き換えるとき、編集を始めた時点で取得した内容を前提にしてはいけません。取得からアップロードまでの間に、別の作業が同じファイルを書き換えていることがあります。気づかず上書きすると、他人の変更——あるいは過去の自分の変更——が黙って消えます。実際に上書き事故を4件起こしてから、手順を固定しました。

  1. 編集の直前に取得する(1回目)。これをそのまま退避としても使う
  2. 手元で書き換えて、検査を通す
  3. アップロードの直前にもう一度取得する(2回目)
  4. 1回目と2回目を突き合わせ、1バイトでも違っていたら中止する。違っている=誰かが触った、なので、最新版の上に自分の編集をやり直す

突き合わせに差分の中身を見る必要はありません。全体が一致するかどうかだけで足ります。中止はエラーではなく、正常な検出結果として扱います。この4段を挟むようにしてから、上書き事故は起きていません。

時間はかかりそうに見えますが、増えるのは取得1回だけです。上書き事故を1回起こしたときの復旧コスト——何が消えたかの特定から始まります——に比べれば、比較になりません。

よくある質問

テストを書くのと何が違いますか?

考え方は同じです。違うのは対象が自分で書いたコードではなく、既に何年も動いている他人の生成物だという点です。あるべき状態が分からないので、「正しさ」ではなく「変更による差」を見ることになります。

検査コードもAIに書かせていいですか?

書かせています。ただし検査コードこそ、何を見ていて何を見ていないかを人間が理解しておく必要があります。ここに挙げた間違いは全部、コードとしては正しく動いていました。

全数検査は時間がかかりませんか?

かかります。ただし1,000ページ程度なら数分です。作業本体より検査に時間がかかることは普通にあり、それでいいと思っています。

次に読むページ