この記事は、連載「AIの意見を聞かない技術」の第 3 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、禁止のルールそのものの話です。禁止事項とは、開発のときに許さないと決めたことを書き並べたルールで、AI にも自分にも守らせるものです。禁止ルールは作ります。作らなくなったのは、判断に迷うたびに足す新しい一本です。前回、その禁止に理由を添えました。それでも、何が禁止に入るかが決まらない禁止が、私の手元には残っていました。決まっていなかったのは言葉ではなく範囲のほうで、範囲が決まっているかどうかは、境界のケースを一つ作るまで見えません。

まず一つだけ、試してみてください。

禁止事項を 1 つ選んで、「これは禁止に入るのかどうか、自分でも一瞬迷う」というケースを 1 つ作ってみます。

作れたでしょうか。

作れたなら、次の問いです。AI がそのケースを踏んだとき、どちらに判定するかを、あなたは答えられますか。

CC BY 4.0

「色リテラルを書かない」と決めて 0 にしたのに、254 件残っていた

私の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——には、「色はテーマの変数で書く。色リテラルを書かない」という禁止があります。明るいものから暗いものまで 10 のテーマを切り替えられるので、#fff と直に書いた文字は、どれかのテーマで必ず背景に沈みます。理由もはっきりした、譲れない類のものです。

4 月に私は pre-commit hook を置いて、1 週間で 1,775 件を 0 にしました。「達成済」と設計書に書いて、終わったつもりでした。

8 月に、hook が見ていなかった場所を初めて数えました。254 件ありました。HTML テンプレートの style="color:#fff" と、JavaScript の el.style.color = "#fff" です。CSS ファイルには 1 件も無く、その隣に 254 件が並んでいました。文字色を直書きしていた箇所を 10 テーマで測ると、白い面に白い文字が出ているトースト(コントラスト比 1.00)が、10 テーマ中 5 つで見つかりました。

同じ月に AI が書いた計画書には、こうありました。

CSS ファイルに色リテラルを増やさないよう、inline style で流す

禁止の名前を挙げたうえで、hook が数えていない側へ迂回路が通っていました。

文面は正しく書いてありました。「色リテラルを書かない」と書いてある。決まっていなかったのは、style 属性の中の色が「CSS の色」に入るかどうかです。私も、AI も、hook も、そこを決めていませんでした。

「どっちの判定軸で数えるのか」が決まっていなかった

もう一つ、範囲の決まっていない禁止があります。このサービスの「歌詞タイピングと単語帳の実績・ミッション・統計は完全に分離する」です。難易度もプレイコストも違うので、混ぜると片方の実績価値が壊れる。理由もはっきりしています。

9 月に 3 つ目の面——記事——が増えたとき、答えられない問いが出ました。記事から入って単語帳をプレイしたユーザーは、どっちの判定軸で数えるのか。

流入元で数えるなら記事軸です。行為で数えるなら単語帳軸です。どちらの読み方も「相互乗り入れ禁止」に反していません。禁止は正しく書いてあるのに、どちらに入るかが決まらない。

決めた線引きは 1 行です。

判定軸は行為で決まる(打った = 単語帳 / 読んだ = 記事)のであって、どこから来たかでは決まらない。流入元は導線クリックとして記事軸が別に数える。

理解したことは一文にできます。

理由を書いても、何が禁止に入るかは決まらない。決まるのは、境界のケースを一つ作って、そのどちらに入るかを書いたときです。

なぜ、境界のケースを書くまで決まらないのか

計画書に「CSS ファイルに色リテラルを増やさないよう」と書いた AI の手元には、2 つのものがありました。禁止の一行と、hook が出す件数です。一行のほうに範囲は書いてありません。件数のほうは、app/assets/stylesheets の下の CSS ファイルだけを数えていました。

禁止の一行は毎回届きます。Claude Code のドキュメントは、指示ファイルもメモリも文脈であって、強制される設定ではないと書いています。届いた一行が、いま触っている style 属性に当たるかどうかは、そのつど判定されます。

結論の一行しか無ければ、足りない分は AI が学習で持っているほうから来ます。色リテラルのほうには、もっと近くに材料がありました。hook が数えているのは CSS ファイルだけで、その件数は 0 のままです。数えている範囲が、そのまま禁止の範囲として読まれました。禁止の文面が範囲を持っていないとき、範囲を持っているものが手元にあれば、そちらが範囲になります。

判定軸のほうは、材料が一つも無い側です。「完全に分離する」という文面は、2 つの面の間について決めたものです。3 つ目の面が増えたとき、その文面は新しい組について何も言いません。人が読んでも決まらないものは、AI が読んでも決まりません。

Anthropic がスキルの書き方について出しているガイダンスは、指示の具体度を、その操作の壊れやすさと振れ幅に合わせよと書いています。判断が要る開けた野原には散文の目安を、一つの手順しか安全でない狭い橋には正確なコマンドを。禁止の「結論」は判断が要るので散文でよく、「適用範囲」は狭い橋です。同じ 1 項目の中で、書き方を変えていいということです。同じページは、例について「望ましい書き方と詳しさの水準は、説明だけよりも、例のほうが Claude に明確に伝わる」と続けます。だから適用範囲は、説明ではなくケースで書きます。

仕組み: 適用範囲を「判定軸」で書き、境界のケースを 1 行ずつ置く

前回の 1 枚には、結論と理由の下に「適用範囲」の見出しがあります。今回置くのは、その中身です。書くのは、迷ったときにどちらに入るかだけです。

### NG13 歌詞タイピング・単語帳・記事の実績・ミッション・統計の相互乗り入れ禁止(3 軸)

**適用範囲**:

- 単語帳のプレイを歌詞側の集計テーブルに計上しない(単語帳は独立系統)
- ⚠️ 記事から入った単語帳プレイは**単語帳軸**に計上する(記事軸ではない)。
  軸は行為で決まる(打った = 単語帳 / 読んだ = 記事)のであって、どこから来たかでは決まらない
- ⚠️ 記事軸に実績・ミッションは当面作らない。持つのはアクセス分析だけ。
  ★ 作りたくなったときは**この行を書き換えてから**作る(黙って足さない)

禁止の文言は、ここで繰り返しません。結論をもう一度書いても、範囲は増えないからです。明らかに入る場面と、明らかに外れる場面は、適用範囲に並びません。そこは範囲が書いていなくても判定が同じです。残るのは、自分が一瞬迷ったケースだけになります。

3 行目の ★ は解除の手順です。第 1 回の測定で、2 体目が通ったのは、この形の一行でした。

そして、判定軸は後から増えます。この禁止は 2 つの判定軸で始まって、3 つの判定軸になりました。増えるたびに境界のケースが新しく生まれるので、判定軸を増やす作業と境界を 1 行足す作業はセットです。

境界の行が効くのは、そのあとの判定です。すでに散らばっているものは、境界を決めても 1 件も減りません。境界の行は、どちらに入るかを決めると同時に、こちら側なら通ると教えることでもあります。

CC BY 4.0 はここまで

相談されなくなったもの

「これは禁止に入りますか」です。以前はその都度こちらが判定していました。AI が聞いてくることもあれば、聞かずに越えていることもあり、後者に気づくのは実装が終わった後です。境界の行を書いてからは、迷いどころが先に文字になっているので、判定を求められません。

CC BY 4.0

注意: 判定軸を増やすと、組み合わせが増える

2 つの判定軸なら境界は 1 本ですが、3 つの判定軸なら 3 本です。判定軸を足すコストは、判定軸の数ではなく組み合わせの数で効きます。私の場合、3 軸目を足した日にいちばん時間を使ったのは、記事軸の定義ではなく「記事 × 単語帳」の境界を決めることでした。

検証手順: 境界を、両方向から破る

前提

  • 禁止に「適用範囲」を書き、境界の行が 1 つ以上あること
  • その境界を見ている自動テストがあり、今は通っていること
  • 作業中の変更が無い状態(git status が空)から始めること

所要時間: 20 分

境界には向きが 2 つあります。片方だけ確かめると、半分しか見ていない自動テストを、両方見ていると思い込みます。

手順

  1. あなたが、境界の行を 1 つ選びます(本文の例なら「記事から入って単語帳をプレイしたユーザーを、どちらの判定軸で数えるか」)
  2. あなたが、一方の向きに破ります。境界が「行為で数える」と決めているなら、流入元で数える実装をわざと 1 行書きます。自動テストを走らせ、結果を控えます
  3. あなたが、その 1 行を戻し、逆の向きに破ります。今度は反対側から越える実装を書きます。自動テストを走らせ、結果を控えます

合格条件(すべて満たすこと)

  • 手順 2 で、自動テストが落ちる
  • 手順 2 の失敗のメッセージに、どちらの向きで破ったかが出ている
  • 手順 3 でも、自動テストが落ちる

合格しなかったとき

  • 手順 3 で落ちない —— その自動テストは片側しか見ていません。見つけたのがこの検証の収穫です。逆向きの表明を 1 つ足します
  • どちらでも落ちない —— 自動テストが、その境界を走査の対象にしていません

後始末

  • 手順 3 で書いた 1 行を戻し、自動テストが合格に戻ることと、git diff が空になることを確かめます

片方向だけの自動テストは、境界の半分しか見ていません。私の環境でこの禁止を見ているテストに「どちらの向きにも漏れない」と書いてあるのは、片側だけ塞いで安心した過去があるからです。


CC BY 4.0 はここまで

次回は「サブエージェントの入力だけは、確かめろ」。サブエージェントの入力だけは、確かめないととんでもないことになります。

連載「AIの意見を聞かない技術」