TL;DR
- 筆者の公開先リポジトリ(非公開)のアセット検査ガードに、除外リストがありました。エントリは1 件だけ、理由は「画像生成が終わるまでの一時的な除外」。追加から 132 日(約 4.3 か月)残ったのち、2026-09-10 の早朝に削除されています。本記事はその 132 日間の記録です。
- 一時的だったはずの理由は、追加から 16 日後に消えていました。除外を入れた時点で欠けていたアセットは 3 点(OGP 画像・サムネイル・ショート動画)で、その 3 点が同じ日にまとめて揃っています。残る 116 日は、免除する理由が無いまま免除され続けた期間です。
- 除外の粒度は slug 全体でした。入れた時点では、欠けていたのも 3 点すべてなので粒度と意図は一致していました。 ずれたのは 16 日後です。前提が消えたあとの 116 日間、その記事はどのアセットが欠けても検出されない状態でした。今回の 1 件では、免除を入れる契機は運用の中にあり、外す契機は仕組みのどこにもありませんでした。
- この観測を受けて、本リポジトリで新しく作った title 重複ガード
scripts/check_title_uniqueness.pyでは免除の器そのものを作らない設計を採りました。既存 224 記事の違反が 0 組だったので、baseline を持つ必要がありませんでした。 - 「免除が無い」を固定するほうが難しいという結果になりました。最初のテストは「免除リストらしい識別子名がソースに現れない」という文字列一致で、frontmatter の値で除外する書き方は素通りします。使い捨てのコピーに 5 通りの免除を仕込んで測ったところ、トークン走査が捕まえたのは 1 通り、振る舞いテストが 2 通り、走査件数の突合が 1 通り、残る 1 通りは 30 テストすべてを通過しました。
- 通過した 1 通りが何だったかまで測りました。**「走査件数を減らさず」「除外キーがテストの fixture に現れない値」**という 2 条件が揃ったときだけです。この 2 条件のどちらかを崩す最小の追加策も、実行して確かめてあります。
- 免除ゼロは達成できていません。本リポジトリでもラベル 1 枚で CI ジョブごと skip する経路は残っています。この記事が言えるのは「slug 単位の恒久免除は作らずに済んだ」までです。
はじめに:免除は、入れた瞬間から誰の担当でもなくなる
CI ガードを入れると、必ず「今は通せない対象」が出ます。移行途中の既存違反、外部要因で揃っていない生成物、期限つきの例外。そこで免除リスト(skip list / allowlist / baseline)を足すのは、実務としてはまったく自然な判断です。
問題は、そのあとです。今回の 1 件では、免除を入れる動機はその日その場にありました。CI が赤いと merge できないからです。一方で外す動機は、実測した 132 日間、システムのどこにも発生していませんでした。一般に「免除リストは腐る」と言うにはこの観測 1 件では足りませんが、どういう構造なら腐りうるかは 1 件からでも分解できます。それを第 3 節でやります。
この記事は一般論ではありません。2026-09-10 時点で、筆者が管理する 2 つのリポジトリの実ファイルと履歴を読んで数えた記録です。基準となるコミットは本リポジトリの a184564(origin/main、記事 224 本)です。
どこまで読者が検証できるかを先に書きます。2 つのリポジトリはどちらも非公開で、読者がファイルを取得して追試することはできません。 第 2〜3 節は公開先リポジトリの挙動と数値だけを書いています。第 4〜6 節は本リポジトリ(こちらも非公開)のガードとテストが根拠ですが、掲載したコード・assert・件数はそのまま載せているので、同型のガードを自分のリポジトリに置けば同じ手順を再現できます。持ち帰る価値があるのは後半です。
近接する 3 本の既存記事とは、扱う対象が違います。
| 記事 | 扱っているもの |
|---|---|
| 効いていないガードを見つける7つの型 | ガード本体が発火しない型(D1〜D7) |
| AI Harnessの技術的負債と削る基準 | 足した Harness が削られずに溜まる非対称性 |
| AIの推論を自動化へ昇格させるループの穴 | 昇格に気づく側の判定器が抑制ロジックで沈黙する |
| 本記事 | ガードに付けた例外が、外す契機を持たないまま残る |
3 本目とは結論の方向が重なります(あちらは「抑制そのものをやめる」に着地しています)。本記事はそれを前提として引いたうえで、「では免除を作らない設計にしたとき、その不在をどう回帰で固定するのか」から先を書きます。そこに穴が空いていた、というのが本記事の中身です。

用語の注意: 本記事の「免除リスト / allowlist」は検査を免除する側の意味です。権限設計で使う「allowlist(許可するものだけを列挙する)」とは価値が逆になります。同じ語が本ブログの MCP の権限設計 系の記事では逆向きに使われているので、混同しないでください。
1. 測定条件と基準時点
| 項目 | 値 |
|---|---|
| 本リポジトリ | 非公開。origin/main = a184564(2026-09-10、content/posts 配下の index.md は 224 件) |
| 公開先リポジトリ | 非公開。Next.js 構成。読み取りのみで、1 文字も変更していない |
| 測定日 | 2026-09-10 |
| 実験環境 | /private/tmp 配下に作った使い捨てコピー。ガード本体・テスト・package.json・CI workflow・全 index.md を複製したもの |
以降「実測」と付した値は、すべてこの条件で筆者が数えたものです。n は 1 です。「免除リストは一般に腐る」という主張は、この観測 1 件では支えられません。この記事が言えるのは「外れなかった例が 1 件あり、その原因は仕組みの側に解除の契機が無いことだった」までです。
ガード本体の docstring は、既存違反 0 組の実測をガード追加時点のコミットで記録しています。本記事の 224 件・0 組は
a184564で測り直した値で、記事数は同じ 224 本です。
2. 観測:エントリは 132 日残り、うち 116 日は理由が無かった
公開先リポジトリには、記事のアセット(OGP 画像・サムネイル・ショート動画)が揃っているかを検査する CI ガードがあります。そこに、検査対象から外す slug の集合が置かれていました。
このエントリは 2026-09-10 の早朝、本記事の執筆と並行して走った記事取り込みの中で削除され、除外リストは空になりました。 以下はすべて削除直前までの 132 日間の記録で、現在の状態ではありません。筆者は当初これを「今も残っている」と書いており、独立レビューの実測で誤りが確定して直しています(第 8 節に経緯を書きました)。
削除直前の状態は次のとおりです。日数は 2026-09-10 を基準にしています。
| 測定 | 値 |
|---|---|
| 除外リストのエントリ数(削除直前) | 1 件 |
| エントリに添えられた理由 | 「公開予定日までに画像生成が終わらないブロッカー。生成完了後に本リストから削除する」 |
| 追加から削除までの経過 | 132 日(約 4.3 か月) |
| 除外を入れた時点で欠けていたアセット | 3 種すべて(OGP 画像 / サムネイル / ショート動画) |
| 削除直前に実在したアセット | 3 種すべて(いずれもリポジトリに追跡されている) |
| アセットが揃うまで | 除外を追加してから 16 日 |
| 理由が消えてからの経過 | 116 日(約 3.8 か月) |
免除の前提は 16 日で消えていました。 そのあと 116 日、エントリだけが残りました。コメントには「生成完了後に本リストから削除する」と、やるべきことがはっきり書かれていました。書いてあっても実行されないのが、AI Harness の技術的負債 が「足すのに根拠は要らず、削るのに根拠が要る」と呼んだ非対称性です。
ここで測っているのは「誰かがサボった」ではありません。削除を促す入力が、システムのどこにも発生していないことです。CI は緑でした。エントリが有効かどうかを見る検査は 1 本もありません。除外されている記事のアセットが揃った日にも、CI は何も言いませんでした。
第 3 の「緑」
効いていないガードを見つける7つの型 は、CI の緑を 2 通りに割りました。「違反が無いから緑」と「検査が動いていないから緑」です。今回の観測は、そのどちらでもない 3 つ目でした。
免除された対象の前提がすでに消えているのに、免除だけが残っているから緑。
検査は正しく動いています。違反もありません。ただ、検査していない対象が 1 件あり、その 1 件を検査しない理由はもう存在していません。画面上、この状態は完全な合格と区別できません。
粒度の問題は「入れた時」ではなく「残った時」に出た
もう 1 つ、実装を読んで分かったことがあります。除外は slug をキーに、その記事を検査対象から丸ごと落とす形でした。
入れた時点では、これは正しい粒度でした。 除外に添えられたコメント自身が「画像 3 点(OGP / サムネイル / 動画)が repo 未配置」と書いており、実際に 3 点とも欠けていました。記事全体を検査から外すことと、待っている対象が記事のアセット全部であることは一致しています。
ずれたのは 16 日後です。3 点が揃った瞬間、この除外は「3 点を待つための除外」から「この記事のアセット欠落を何も検出しない設定」に変わりました。そこから 116 日、仮に揃ったアセットが壊れても消えても、この記事だけは誰も気づけません。
粒度の広さは、入れる瞬間には害として現れません。 害が出るのは前提が消えたあとで、そのときには誰も見ていない。第 7 節で粒度を要件に入れているのは、入れる時の正しさが残る時の正しさを保証しないからです。
3. なぜ免除(skip list / allowlist / baseline)は外れないのか
今回の 1 件から、外れなかった構造を 3 つに分けます。いずれも「人の注意」では直せません。
| # | 構造 | 何が足りないか |
|---|---|---|
| 1 | 免除は CI を緑にする方向にしか作用しない | 免除が不要になったことを CI が赤で知らせる経路が無い |
| 2 | 解除条件がコメントに書かれている | 機械が読める場所に無いので、条件の充足を誰も判定できない |
| 3 | 検査件数は出るが、全件と突合されない | 1 件減っていることを読み手が判定できない |
3 番目は誤診しやすいところです。このガードは合格時に「OK: N 件の公開記事で…」と検査した件数を印字しています。除外された記事はこの N に入らないので、1 件除外されていれば数字は 1 つ小さくなります。つまり情報は出ているのに、N を記事の全件数と突合する検査がどこにも無いため、読み手には 1 件減っていることが判定できません。欠けているのは「件数を出すこと」ではなく「出した件数を突合すること」です。この区別は第 7 節の要件表にそのまま効きます。
4. 設計判断:免除の器そのものを作らない
同じ時期に、本リポジトリで新しいガードを 1 本作りました。scripts/check_title_uniqueness.py です。記事の title が完全一致・正規化後一致で重複していないかを検査します(過去に title も date も同一の記事が約 7 か月公開され続けた観測があり、その再発を止めるためのものです)。
この設計で最初に決めたのは、免除リストを持たないことでした。判断の根拠は 2 つあります。
- 前節の観測。免除を持てば、それが stale になる経路が生まれます。持たなければ生まれません。
- 実測で持つ必要が無かった。 既存 224 記事に対して、完全一致の重複も正規化後一致の重複も 0 組でした。初日から緑なので、baseline を積む理由がありません。
2 番目が要点です。本リポジトリの spec/article_retrospective_loop.md には「充足可能性(対 E5)」という検査項目があり、構造的に満たせない要求を契約に書かないことを求めています(.gitignore 配下の成果物を要求して自分で詰まった事故が起点です)。既存違反が大量にあるところへ免除なしのガードを入れれば、初日から赤で、回避が常態化します。免除を作らずに済むかどうかは思想ではなく、先に数えて決めることです。
将来 title が衝突したときの解法も、免除ではなく改題に決めました。前例があります。統合で退役させる側の記事を改題した PR がすでにあり、免除なしで解決できることが実証されています。ガードのエラーメッセージにも、改題後に pnpm status で content/posts/README.md を再生成しないと別の CI が落ちる、というところまで書いてあります。
反例に触れておきます。 本ブログの AI生成の巨大PRをStacked PRへ分解する では、dangling reference 検査を入れるときに「既存の負債で初回から red になるので、まず現状を baseline として記録し、新規の増加だけを落とす運用から始める」と書いています。これは本記事の推奨と食い違って見えます。 食い違いではありません。あちらは「既存違反があるので免除なしでは充足不能」に該当し、免除が入口の条件になっている例です。分かれ目は「免除が減っていく設計になっているか」で、第 7 節の要件はまさにそこを問います。
5. 「無いこと」を固定するのは、「あること」を固定するより難しい
免除を作らないと決めたら、次は将来誰も足さないことを保証する番です。ここで一度外しました。
最初のテストはこう書いていました。
def test_guard_has_no_exemption_list(self):
source = GUARD.read_text(encoding="utf-8")
code = source.split('"""', 2)[-1]
pattern = re.compile(
r"waiver|skip_?slug|exempt|allow_?list|ignore_?slug|baseline|grandfather",
re.IGNORECASE,
)
found = pattern.findall(code)
self.assertEqual(found, [], f"免除リストらしき定義がある: {found}")
「免除リストらしい識別子名がソースに現れないこと」を、文字列一致で固定しています。独立レビューで、これが素通りする書き方が指摘されました。免除を入れる人は SKIP_SLUGS という名前で入れるとは限りません。
一番自然な免除の入れ方は、識別子ではなく条件分岐です。
# 実験では下記に draft / noindex / redirect_from を加えた 5 条件で測った
if fm.get("status") == "archived" or fm.get("canonical"):
return None, None # 統合済み・非公開の記事は一意性の対象外とする
これは免除リストです。しかも、レビューで見ても「妥当な仕様」に見えます。上のトークン走査は 1 文字も引っかかりません。
直し方は、名前ではなく振る舞いで固定することでした。「実在しうる除外条件を全部載せた記事が、それでも重複として落ちること」を要求します。
def test_no_frontmatter_field_exempts_a_duplicate(self):
extra = (
'status: "archived"\n'
"draft: true\n"
"noindex: true\n"
'canonical: "/notionnext-blog/a"\n'
'redirect_from: ["/old"]\n'
'series: ["standalone"]\n'
)
# ... 既存記事と同じ title を持ち、上の値を全部持つ記事を置く
self.assertEqual(proc.returncode, 1, "frontmatter の値で重複が免除されている")
もう 1 本、走査件数を実データの全件と突合するテストも入っています。
expected = len(list((REPO_ROOT / "content" / "posts").rglob("index.md")))
self.assertIn(f"検査対象: {expected} 件の index.md", proc.stdout)
これは走査が静かに縮む退行を捕まえるために書かれたものですが、この assert の定義から、走査対象を 1 件でも減らす免除は必ず落ちます。実験して初めて分かったことではなく、定義からの帰結です。次節では、この帰結が及ぶ範囲と及ばない範囲を分けます。
トークン走査は消していません。補助として残しています。ただし保証の本体は振る舞い側です。
6. 実験:使い捨てのコピーに 5 通りの免除を仕込む
「免除を入れると落ちる」を、自分で確かめました。リポジトリのファイルは変更していません。/private/tmp 配下に、ガード本体・テスト・package.json・CI workflow・全 224 件の index.md を複製した使い捨てのツリーを作り、そこだけを書き換えています。
| # | 免除の入れ方 | 落ちたテスト | 件数 | 種別 |
|---|---|---|---|---|
| A | 名前つきの除外集合(TITLE_CHECK_SKIP_SLUGS)を作るが、空のまま置く | test_guard_has_no_exemption_list | 1 | 演繹 |
| A' | 同上。実在する slug を 1 件入れる | test_guard_has_no_exemption_list / test_wired_form_covers_the_whole_corpus | 2 | 演繹 |
| B | frontmatter の値(status / draft / noindex / canonical / redirect_from)で走査から除く | test_no_frontmatter_field_exempts_a_duplicate / test_canonical_does_not_exempt_a_duplicate / test_wired_form_covers_the_whole_corpus | 3 | 演繹 |
| C | 外部ファイル(content/.title-exceptions)を読んで走査から除く。ファイルは置かない | なし | 0 | 演繹 |
| C' | 同上。実在する slug を 1 行書き込む | test_wired_form_covers_the_whole_corpus | 1 | 演繹 |
| D | 重複の判定側で除外。キーは外部ファイル内の実在 slug(走査件数は減らない) | なし | 0 | 実験 |
| E | 同じく判定側で除外。ただしキーを frontmatter の値(canonical / noindex / draft)にする | test_no_frontmatter_field_exempts_a_duplicate / test_canonical_does_not_exempt_a_duplicate | 2 | 実験 |
「種別」列は、テストの定義から結果が導ける行(演繹)と、走らせるまで分からなかった行(実験)を分けたものです。新しい情報を持つのは D と E の 2 行だけで、A〜C' は前節の assert から機械的に出ます。ここを混ぜて「5 通り試したら発見があった」と書くのは、測っていないものを測ったことにする書き方なので分けました。
読み取れることを 4 つに分けます。
(1)トークン走査は 5 通りのうち 1 通りしか捕まえません。 A / A' では発火しますが、B・C・D・E では 1 件も引っかかりません。名前で否定命題を固定しようとした最初の実装が、実際に穴だらけだったことが確認できます。
(2)走査から除く形は、frontmatter でも外部ファイルでも件数突合で落ちます。 B を実コーパスに当てると検査件数が 224 → 204 に落ちます。frontmatter に canonical を持つ既存記事が 20 本あるためです(noindex / redirect_from / status: archived で除外される記事は 0 本、draft: true も 0 本でした。draft: false を持つ記事は 2 本ありますが、これは除外されません)。「妥当な仕様」に見える条件分岐が、実際には記事の 8.9% を黙って検査対象から外していました。
(3)器だけでは件数突合が発火せず、使われた瞬間に発火します。 C は免除の器だけを入れた状態で、30 テストすべてを通過しました。しかしファイルに実在 slug を 1 行書いた C' では、検査件数が 224 → 223 に落ちて即座に赤になります。A と A' の差も同じで、空の器では件数突合は動かず、1 件入れた途端に落ちます(A は識別子名がある分、空でもトークン走査だけは発火します)。件数突合は「免除が実際に使われたか」を測る検査で、「器があるか」は測りません。 前節の assert の定義からそうなります。
(4)通過した D と、通過しなかった E の差が答えでした。 D は 30 テストすべてを通過しました。実在 slug を書き込んでも検査件数は 224 のままで、exit 0 です。一方 E は、同じ判定側での除外なのに、frontmatter をキーにした途端 2 本のテストで落ちます。
つまり「判定側での除外は原理的に検出できない」のではありません。D が通ったのは、次の 2 条件が両方揃ったからです。
- 走査件数を減らさない(件数突合テストが発火しない)
- 除外キーが、テストの fixture に現れない値(fixture の slug は
a/b/c/dup/old/brokenで、実在 slug と衝突しない)
E は 2 を崩したので落ちました。したがって最小の追加策は、抽象的な「あらゆる書き方を貫通させる」ではなく、fixture 側に実在 slug と実在 frontmatter 値を使った重複ケースを 1 本足すことです。それだけで D の条件 2 が崩れます。
現時点で D は残った穴です。 塞いだとは書けないので、書きません。対策は本記事とは別の PR で追います。
7. 持ち帰る手順
自分のリポジトリに同型のガードを入れるときに使える順序に落とします。
第一選択:免除機構を作らない
-
判定ロジックを書いたら、CI へ配線する前に実データへ当てて件数を出す。 ガードを書く前に検査は走らせられないので、順序は「判定ロジック → 実データで件数 → 配線」です。0 件なら免除は要りません。
-
数えるとき、
sort | uniq -dを使わない。犯人はsortではなくuniqです。 ここは筆者が実際に踏み、しかも原因を取り違えたまま初稿を書きました。macOS 既定の BSDuniqはロケールの照合順序で行を比較するため、en_US.UTF-8では別々の日本語 title が同一行と数えられます。224 記事に対する実測は次のとおりです。
sortにだけLC_ALL=Cを付けると、偽陽性はむしろ増えます。組み合わせ 重複と報告された組数 既定 sort/ 既定uniq12 LC_ALL=Cを両方に付ける0 sortにだけ付ける24(悪化) uniqにだけ付ける0 sortのせいだと思ってLC_ALL=C sort | uniq -dに直すと、偽陽性は 12 から 24 に増えます。並び順だけが C ロケールになり、比較するuniqは元のままなので、隣り合う行の組み合わせが変わって誤検出が増えるためです。24 組という値は「既存違反が多いから baseline が要る」という、この記事が警告しているのと逆の設計判断へ、直す前より強く誘導します。確実なのは、ワンライナーで数えるのをやめてこれから書くガードの正規化関数そのものに数えさせることです。
bash# 本リポジトリでの実際の数え方(ガード自身に数えさせる) python3 scripts/check_title_uniqueness.py content/posts --require-target # どうしてもシェルで数えるなら、パイプライン全体にロケールを効かせる LC_ALL=C sh -c "grep -h -m1 '^title:' content/posts/*/index.md | sort | uniq -d" -
0 件でないなら、免除の前に対象を直せないかを見る。 件数が数本なら、直すほうが安いことがよくあります。本リポジトリの解法は改題でした。
-
それでも通せないなら、要求そのものを疑う。 全件を直せない要求は、構造的に充足不能な契約かもしれません。免除で通すより、検査の定義を狭めるほうが正しい場合があります。
免除を作る場合の要件
作ると決めたなら、次の 4 つを同じ PR で入れることを勧めます。今回の観測では、後から足されることはありませんでした(132 日間ゼロ)。
| 要件 | 具体形 | 検証方法 |
|---|---|---|
| 前提が消えたら落ちる | 各エントリに機械可読な解除条件を持たせ、条件が満たされたエントリが 1 件でもあれば非 0 で落とす | 条件を満たしたエントリを 1 件仕込んで、非 0 になることを実行して確認 |
| 空が正常だと分かる | 出力に必ず 除外: N 件 を印字する。N > 0 なら警告を出す。ログを読むだけで棚卸しできる状態にする | 0 件時と 1 件時の出力を比べ、区別できることを確認 |
| 使われたら痕跡が残る | 検査対象の件数を全件と突合する。免除で 1 件でも減れば落ちる(実験 C') | 実在の対象を 1 件免除して、非 0 になることを実行して確認 |
| 粒度を意図に合わせる | 免除するのは「待っている 1 項目」だけ。対象全体を検査から外さない | 免除中の対象に別の違反を仕込み、それは落ちることを確認 |
1 つ目の「前提が消えたら落ちる」は、既存のツールにお手本があります。以下 4 件は各ツールの公式ドキュメントに基づく記述ですが、本記事の執筆環境は外向き通信が遮断されており、ドキュメントを取得して実地に確認してはいません。 中心主張ではなく補強として読んでください。そのあとが筆者の推奨です。
- TypeScript の
@ts-expect-error: 次の行にエラーが無いとエラーになります。エラーが直った時点で、抑制コメント自身が赤くなります(公式リリースノート)。@ts-ignoreにはこの性質がありません。 - ESLint の未使用 disable 報告:
--report-unused-disable-directivesを有効にすると、不要になったeslint-disableコメントが報告されます(公式 CLI ドキュメント)。v9 以降は設定側のlinterOptions.reportUnusedDisableDirectivesが推奨経路です。 - Ruff の
RUF100: 不要になった# noqaを検出します(公式ルールドキュメント)。 - mypy の
--warn-unused-ignores: 不要になった# type: ignoreを報告します(公式ドキュメント)。
ここからは筆者の意見です。 4 つとも「免除の前提が消えたことを、免除自身が赤で知らせる」という同じ形をしています。自分で免除リストを設計するなら、この挙動を目標にするのが妥当だと考えます。今回観測した除外リストに欠けていたのは、まさにこの性質でした。
既存の免除リストの棚卸し
すでに免除リストがあるなら、次の順で見ます。今回のようにエントリ 1 件なら数分でした。
- エントリを列挙し、各エントリの追加日を
git log -Sで出す - 各エントリについて、追加理由に書かれた条件が今も成立しているかを実ファイルで確認する
- 成立していないものを削除し、削除しても CI が緑であることを確認する
- 同じ PR で、2 の判定を自動化する
4 が本題です。1〜3 だけをやると、その日は綺麗になり、次の免除がまた同じ経路をたどります。
8. この記事が言えないこと
- 本記事の中核事例は、執筆中に解決しました。 除外リストのエントリは 2026-09-10 の早朝に削除され、リストは空になっています。筆者は初稿でこれを「今もある」と書いていました。 原因は、公開先リポジトリの作業ツリー(別ブランチにチェックアウトされたまま)を読み、ref を明示して現在の内容を読み直さなかったことです。独立レビューの実測で確定し、本節を含む 4 箇所を直しました。この記事は現存する問題の報告ではなく、解決済みの 132 日間の構造分析です。
- 免除ゼロは達成できていません。 本リポジトリの title ガードが無くせたのは slug 単位の恒久免除までです。この検査を回す CI ジョブは
skip-quality-gate/skip-ciラベルで丸ごと skip できるので、ラベル 1 枚による PR 粒度の免除は残ります。専用ジョブを持たないガードに共通する性質で、本リポジトリのspec/guard_self_check.mdにも明記してあります。 - 実験 D は塞げていません。 「免除の不在をテストで固定した」と書けるのは、走査件数を減らす形か、除外キーが fixture に現れる形の範囲までです。最小の追加策は第 6 節に書きましたが、本記事では実装していません。最後の砦はレビューです。
- 免除を持つべき場面はあります。 既存違反があるところへ検査を入れる場合、免除(baseline)は入口の条件になります。本記事の主張は「作るな」ではなく「作るなら第 7 節の 4 要件を同じ PR で入れろ」です。
期限つきの成果物が残る話は、対象が違うだけで AIエージェントの運用メモリを設計する とほぼ同じ構造をしています。あちらは知識エントリの last_verified が 31 件中 6 件にしか付いておらず、90 日基準で生きているのは 3 件だった、という実測です。期限フィールドを置くだけでは足りず、それを読んで落ちる検査を 1 本置くところまでが設計という結論は、免除リストにもそのまま当てはまります。
FAQ
免除リストを一切持たないガードは現実的ですか
対象の実測値しだいです。本リポジトリの title ガードは、既存 224 記事の違反が 0 組だったので持たずに済みました。既存違反が多数あるなら、免除なしのガードは初日から赤で回避が常態化します。判定ロジックを書いたら配線の前に実データへ当てて件数を出し、0 件なら免除を作らない、という順序で決めてください。
既存違反を数えるとき、注意することはありますか
sort | uniq -d で数えないでください。原因は sort ではなく uniq です。 macOS 既定の BSD uniq はロケールの照合順序で行を比較するため、別々の日本語文字列が同一と数えられます。筆者の実測(224 記事)では既定で 12 組の偽陽性が出ました。ここで sort のせいだと思って LC_ALL=C sort | uniq -d に直すと偽陽性は 24 組に増えます。LC_ALL=C をパイプライン全体(少なくとも uniq)に効かせれば 0 組です。この誤測定は「baseline が要る」という逆の設計判断に直結します。
免除を「一時的」と書いておけば十分ではないですか
今回の観測では不十分でした。コメントには「生成完了後に本リストから削除する」と明記されていましたが、前提が消えてから 116 日残りました。文章による意図表明は、それを読んで判定する機械が無いかぎり実行されません。
免除の不在は、どうやってテストで固定しますか
識別子名の文字列一致では固定できません。実測では 5 通りの免除のうち 1 通りしか捕まえませんでした。実在しうる除外条件を全部載せた対象がそれでも落ちることを要求する振る舞いベースのテストにし、加えて検査対象の件数を全件と突合してください。ただし、走査件数を減らさず、かつ除外キーが fixture に現れない値の場合は捕まえられません。fixture に実在 slug を使った重複ケースを 1 本足すのが最小の追加策です。
この記事の数値は今も同じですか
同じにはなりません。公開先リポジトリの除外リストは、本記事の執筆中(2026-09-10 早朝)に削除されてすでに空です。 記事数も増えます。基準時点は本リポジトリ a184564、測定日は 2026-09-10 です。免除リストのエントリを列挙して追加日を出し、追加理由の条件が今も成立しているかを実ファイルで確認する、という数字ではなく手順のほうを持ち帰ってください。
