CI(継続的インテグレーション。コード変更のたびに自動でテストやチェックを走らせる仕組み)にリンクチェッカーを組み込んでいるプロジェクトは珍しくありません。READMEのリンク切れを検知する定番の仕組みですが、その「緑のチェックマーク」が実は何も保証していない場合があります。
具体例があります。67,000個のスターを持つリンク集リポジトリ design-resources-for-developers では、リンクチェッカーが全件パスしているにもかかわらず、React UI Libraries セクションの「Chakra Templates」をクリックすると、インドネシア語のスロットゲームの配当率を解説するページに飛びます。ドメインの契約が切れ、第三者が買い取り、サーバーはHTTPステータス200(正常応答)を返す。リンクチェッカーが確認するのはそれだけです。このリストでは8件がギャンブルサイトに、22件がドメイン販売ページや無関係な業者のサイトに変わっていました。
この事象はアーキテクチャ設計における「検証の対象範囲」をどう定義するか、という非機能要件の問題として読み解けます。
リンクチェッカーが検知できる範囲とできない範囲
リンクチェッカーの多くは、HTTPリクエストを送ってステータスコードを見るだけの単純な仕組みです。200番台が返れば「生きているリンク」と判定します。
これは可用性(アベイラビリティ。システムが使える状態を保っているか)の観点では正しい判定です。しかし整合性(インテグリティ。コンテンツが意図した内容と一致しているか)の観点では何も保証していません。
ドメインが失効し第三者に再取得される、いわゆるドメインハイジャックやエクスパイアドドメインの再利用は、セキュリティの文脈ではよく知られたリスクです。SPF/DKIMレコードが残ったままのドメインが迷惑メール送信に悪用される事例と構造は同じで、「かつて信頼されていた参照先が、今は別の実体になっている」点が共通しています。
検証ロジックを多段化する設計
この問題に対して、GitHub Actions(GitHubが提供するCI/CDの自動実行基盤)上で動く possessed というツールが一つの解法を示しています。既存のリンクチェッカーの後段に挿入し、チェッカーが通過したリンクだけを対象に、追加の意味検証をかける設計です。
処理は三段階に分かれます。まず embeddinggemma というモデルで、リンク先ページの内容とリンクを引用している文章の両方をベクトル化し、意味的な類似度を計算します。類似度が低い上位15%と、別ドメインへのリダイレクトやパーキングページ・ギャンブル関連語を含むものを抽出するトリアージ(優先度付けの選別)です。design-resources の例では、1,114件のパス済みリンクがこの段階で200件に絞り込まれています。
次に gemma4:e4b というモデルが、現在のライブページと Internet Archive(過去のWebページを保存している非営利アーカイブ)が持つ保存時点のスナップショットの両方を読み、リンクを引用している文脈と照合します。判定は「fine(問題なし)」「moved(移転)」「possessed(乗っ取り)」「dead(死亡)」の4種類です。
最後に qwen3.5:4b という別系統のモデルが、同じ証拠を独立に評価し二重チェックします。異なるモデルファミリーで二重判定する構成は、単一モデルの誤判定リスクを下げる設計上の工夫です。
「ツールが嘘をつかない」ための歯止め
アーキテクチャ上、特に注目すべきなのは、修正提案の正当性をコードレベルで強制している点です。判定モデルが置換先URLを提案しても、そのURLが同じセッション内でモデル自身が実際に開いたページ(アーカイブのスナップショットか、チェッカーを通過するライブURL)でない限り、システムはその提案を拒否します。拒否は「ツールエラー」としてモデルに差し戻され、再試行を促す仕組みです。
この設計は、LLM(大規模言語モデル)の出力をそのまま信用せず、外部で検証可能な事実だけを採用する「グラウンディング(根拠付け)」の典型例です。5つのリストに対する597件の判定のうち、この拒否機構が作動したのは18件で、いずれもモデルの誤りではなく実装側のバグ(URLの末尾スラッシュの不一致や、JavaScriptで描画されるページを空ページと誤認するルール)だったと報告されています。バグ修正後に大規模リストを再判定したところ、拒否は0件になっています。
自分のプロジェクトへの影響を確認する方法
この話は特定のリンク集リポジトリに限った話ではありません。READMEやドキュメントに外部リンクを多数抱えるプロジェクト全般に当てはまる、技術的負債の一種です。
確認の手順は次の通りです。
- リポジトリで使っているリンクチェッカー(lychee、markdown-link-check など)の判定基準を確認し、HTTPステータスコードのみで合否を出していないか見る
- READMEやドキュメント内の外部リンクのうち、作成から1年以上経過しているものを git log -S などでコミット履歴から洗い出す
- 該当リンクを実際に開き、ドメインの所有者が変わっていそうなページ(広告だらけ・言語が急に変わる・ギャンブルやアダルト系の内容)がないか目視で抜き取り確認する
- 外部リンクが多い場合は、possessed のように既存のチェッカーの後段に意味検証を追加するか、定期的な手動サンプリングの仕組みを運用に組み込む
CI/CDパイプラインに検証ステップを足すこと自体は難しくありません。重要なのは、「何を検証したことにして緑にしているか」を設計時に明文化しておくことです。可用性のチェックとコンテンツ整合性のチェックは別物だと認識するだけでも、ドキュメントの信頼性に対する見方が変わります。
まとめ
HTTPステータス200は「サーバーが生きている」ことしか示さず、コンテンツの正当性までは保証しません。design-resources-for-developers の事例は、この前提のズレが数年単位で見過ごされうることを具体的に示しています。
自分のプロジェクトのCI設定を開き、リンクチェッカーがどの条件で合格判定を出しているか、まず確認してみてください。外部リンクを多く抱えるドキュメントがあるなら、作成から時間が経ったリンクを優先してサンプリングし、ドメインの実態が変わっていないか見ておくと安心です。検証の粒度をどこまで引き上げるかは、プロジェクトの規模とリンクの重要度に応じて判断する問題です。