先に結論です。本編 37 本が「置いたもの」として出した自動テストは、対象がコードでも、指示ファイルでも、記事でも、自動テスト自身でも、8 つの型に収まります。 「走査 + 下限」型は走査して下限を併置する、ラチェット型は件数を基準値で止める、メタテスト型は自動テストを自動テストが見る、変異注入型は壊して落ちるかを見る、受け入れテスト型は成果物の構造を突き合わせる、「名指し + 目印」型は名指しと目印を往復させる、「機構の裏打ち」型は依頼と機構を突き合わせる、「件数を数える」型は数えて並べる。8 つに共通する設計は 2 つだけで、「何も見ていない」と「違反が無い」を同じ合格で返さないこと(「走査 + 下限」型の下限がすべての型に入っている)と、置いた日に一度、壊して失敗を見ること(変異注入型を「走査 + 下限」型〜8 の全部に当てる)です。3 作 37 本のうち、検証手順を置いた 30 本で「壊して確かめる」形が 13 本あった理由は、ここにあります。
もう 1 つの結論は、型の外にあるものの側です。8 つの型が見られるのは、機械が真偽を決められるものだけです。 「色リテラルを書かない」は見られますが、「この経済バランスは意図どおりか」は見られません。本編が 3 作かけて言ったのは、見られるものを全部機械に渡すと、残った方針だけが指示ファイルに残って薄まらなくなる、ということでした。この章は渡す側の技術を、手元の実物 27 本と pre-commit のチェック 8 つ、hook の自動テスト 4 本から、型ごとに取り出します。
付録 C.9 は hook 自身の自動テストを書きました。この章はそれを変異注入型の 1 例として引き、hook 以外の 7 種類の対象に同じ型がどう当たるかを埋めます。
F.1 8 つの型 —— 何を見て、何で壊れるか
flowchart TB
R["守らせたいこと 1 条"] --> Q1{"機械が真偽を<br/>決められるか"}
Q1 -- "いいえ" --> M["指示ファイル / メモリに残す<br/>「機械検証: なし(理由)」を書く<br/>→ 第 2 部 第 5 回"]
Q1 -- "はい" --> Q2{"いま違反が<br/>0 件か"}
Q2 -- "はい" --> T1["型 1 走査 + 下限<br/>違反が 1 件でも出たら赤"]
Q2 -- "いいえ" --> T2["型 2 ラチェット<br/>基準値を超えたら赤"]
T1 --> Q3{"対象は何か"}
Q3 -- "自動テスト自身" --> T3["型 3 メタテスト"]
Q3 -- "成果物と原本" --> T5["型 5 受け入れテスト"]
Q3 -- "文書とコードの対応" --> T6["型 6 名指し + 印"]
Q3 -- "依頼と機構の対応" --> T7["型 7 機構の裏打ち"]
T1 & T2 & T3 & T5 & T6 & T7 --> T4["型 4 変異注入<br/>置いた日に 1 度、壊して赤を見る"]
Q1 -. "決められないが、数は数えられる" .-> T8["型 8 件数を数える<br/>合否ではなく判断材料を毎回出す"]
図の左の分岐が第 2 部 第 5 回、右の分岐が第 1 部 第 6 回、下の変異注入型が第 1 部 第 3 回と第 3 部 第 11 回です。8 つを、見るもの・壊れ方・手元の実物で並べます。
| 型 | 見るもの | 失敗になる条件 | この型が黙って壊れる形 | 手元の実物(2026-09-05) |
|---|---|---|---|---|
| 1 走査 + 下限 | ファイル群を正規表現で走査した違反 | 違反 ≥ 1 または 走査件数 < 下限 | glob のパスが変わって 0 件を走査(下限が無いと永遠に合格) | test/reference/ 27 本のうち走査型 12 本。全部に下限あり |
| 2 ラチェット | 違反の件数と基準値の差 | 現状値 > 基準値(両向きなら < でも失敗) | 基準値を上げる文化 / 走査対象の定義の隙間(CSS だけ数えて inline を見ない) | design_rules.py 8 項目 / validate_locale_parity.py / 強調の BASELINE = 7 / 本の MAX_PENDING_REVIEW = 44 |
| 3 メタテスト | 自動テストの書き方・文書の書式 | 「走査 + 下限」型の下限が無い自動テストがある / 書式が欠けている | メタテスト自身の読み先が壊れる | zero_target_guard_test.rb(初回に 1 本発見)/ experience_principles_test.rb の 6 本 |
| 4 変異注入 | 壊した実装・壊した自動テストで、合格のまま通るものがあるか | 壊したのに合格(= その行を見ていない) | 当たらない行を壊して「抜け穴だ」と騒ぐ(等価変異)/ 全部落ちたのを品質の保証に読み替える | 方針ファイル 5 条(第 1 部 第 3 回)/ hook の自動テストの 9 項目目 / 計画 214 の 7 種(1 種が素通り) |
| 5 受け入れテスト | 成果物と原本の構造(意味は見ない) | リンク・見出し・箇条書き・段落・コード・数値のどれかが原本と違う | 突合が 0 組で回る / 別経路で単体だけ通す | article_translation_parity_test.rb(7 つの突合)/ 本の生成の陽性対照 5 定数 |
| 6 名指し + 目印 | 文書が名指しした先に目印があるか、目印が既知の ID を指すか | 名指し先が無い / 目印が無い / 未知の ID | 逆方向(目印 → 名指し)が無いので、名指しを消しても合格 | 18 原則・名指し 43 ファイル・目印 75 ファイル(32 は宙) |
| 7 機構の裏打ち | 指示ファイルの実行の依頼が hook にも実在するか | 依頼があって機構が無い | 語形(「必ず実行」)で拾うので言い換えで抜ける / hook の実体でなく .sample を見る | claude_md_enforced_command_test.rb |
| 8 件数を数える | 3 階層の件数・強調の行・未実行の領域 | (合否を出さない。並べるだけ) | 数字が止まって見えるだけで格上げの経路が壊れている / 一覧が会話層に毎回積まれる | 3 階層 91 / 43 / 17(2026-09-03)/ [E2E] 一覧 23 領域 / 強調 7 行 |
⚠️ 表の 4 列目が、この章の本体です。8 つの型はどれも自分の壊れ方を 1 つ持っていて、その壊れ方は変異注入型(壊して失敗を見る)でしか見つかりません。第 3 部 第 11 回が「落ちたことのない自動テストは、まだ何も証明していない」と書いたのは、この列のことです。
F.2 「走査 + 下限」型 走査 + 下限 —— 空虚な真を塞ぐ
走査する自動テストの骨は 3 行です。集める、選ぶ、空であることを言う。
files = GLOBS.flat_map { |g| Dir.glob(Rails.root.join(g)) }
violations = files.select { |path| File.read(path).match?(PATTERN) }
assert_empty violations
この 3 行は、files が空のときに必ず通ります。 空集合のすべての要素は任意の条件を満たす(空虚な真、vacuous truth)ので、violations も空になり、assert_empty は合格を返します。Dir.glob は存在しないパスを渡されても例外を出しません。手元の Ruby 3.3.6 で確かめると、こうです。
$ docker compose run --rm --no-deps -T web ruby -e 'p Dir.glob("no_such_dir/**/*_test.rb")'
[]
ディレクトリを 1 つ改名する、app/javascript を app/frontend に移す、glob の ** を 1 つ落とす。どれも例外を出さず、自動テストは以後ずっと合格です。第 1 部 第 6 回が「落ちたことのない自動テストは、守っている自動テストと壊れている自動テストの区別が付かない」と書いたのは、この機構です。
塞ぐ 1 行が下限です。
assert_operator files.size, :>=, 1000,
"走査対象が激減している(#{files.size} 件)。GLOBS が壊れていないか"
下限の決め方 —— 定数と自己較正
手元の 12 本の走査型は、下限を 3 通りの形で持っています。
| 形 | 例 | 向くもの | 弱点 |
|---|---|---|---|
| 定数 | assert_operator files.size, :>=, 1000(memory_rule_user_name_field_test.rb)/ MIN_ENTRIES = 40(articles_registry_test.rb) | 対象が減らないもの(app 配下のファイル数・記事の本数) | 対象を正当に減らしたときに落ちる。連載ごとに当てると短い連載を足した日に落ちた(第 2 部 10 本でこれを踏んだ。記事の一覧の自動テストのコメントに記録) |
| 相対 | files.size >= 150 の代わりに、直前の commit の件数の 8 割 | 増減するもの | 基準の取得に git が要る。手元では使っていない |
| 自己較正 | 基準値ファイルと除外リストに載っている群が全部実在するか(validate_locale_parity.py) | 群の増減があるもの | 基準値ファイルが空なら何も見ない。初回導入時は定数で補う |
自己較正の形は、計画 206 §8.3 で「件数を定数で決め打ちしない」と決めたものです。locale の群は増えるので、>= 40 と書いた日から数字が腐ります。代わりに「基準値に載っている群が glob の結果に無ければ落ちる」にすると、群が増減しても書き換えが要らず、glob が壊れれば必ず落ちます。
走査の正規表現 —— 偽陽性は行の内容で除く
「走査 + 下限」型の 2 つ目の壊れ方は正規表現です。第 1 部 第 6 回の CSRF の例は method:\s*["']?(post|put|patch|delete) で非 GET を拾いますが、文字列の中の "method: post" も拾います(偽陽性)。逆に method: verb のように変数で渡すと拾えません(偽陰性)。
手元の memory_rule_user_name_field_test.rb は、除外をファイル名ではなく行の内容で決めています。user.name == "email login user" は表示ではなく比較なので、.name の直後か直前に == / != があれば許す。ファイル名で除外すると、そのファイルに後から足された本物の違反も通ります。行の内容で除外すると、除外の条件がそのまま「何を違反と呼ぶか」の定義になります。
⚠️ 「走査 + 下限」型の走査はコードブロックの中を見ません。正確には、見る・見ないを決めていないので、記事の md に書いたコードは app/** の glob に入らず、docs/** を走査する自動テストは中身をコードとして読みません。第 3 部 第 11 回のレビューで返ってきた「記事の中のコードが 5 件、コピペで動かなかった」は、この外側で起きました。この本の付録のコードも同じ外側にあります(F.12)。
F.3 ラチェット型 ラチェット —— 基準値・走査対象の定義・上げる手順
ラチェットは、いま 0 にできない違反を「増えたら落ちる」形で止める型です。機構としては「走査 + 下限」型に基準値を足しただけですが、運用の抜け穴が 3 つ増えます。基準値をどこに置くか、走査対象をどう定義するか、基準値を上げるときに何を書くか。
flowchart LR
subgraph S["毎回の比較"]
C["現状値 cur<br/>(いま数えた)"] --- B["基準値 base<br/>(ファイル / 定数)"]
end
C -->|"cur > base"| NG["赤: 増えた<br/>直すか、design-allow を付けるか、<br/>base を上げて理由を書く"]
C -->|"cur = base"| OK["緑"]
C -->|"cur < base"| Q{"両向きか"}
Q -- "片向き(design_rules.py)" --> OK2["緑。OK と印字するだけ<br/>= 焼き込みは人が --update するまで起きない"]
Q -- "両向き(validate_locale_parity.py)" --> NG2["赤: 減ったのに焼き直していない<br/>→ --update-baseline で固定"]
基準値の置き場 3 つ
| 置き場 | 実物 | 誰が更新するか | 向くもの |
|---|---|---|---|
| JSON ファイル | config/design_baseline.json(8 項目)/ config/locale_parity_baseline.json(群ごと) | スクリプトの --update | 項目が多い / 複数のスクリプトが読む |
| 自動テストの定数 | BASELINE = 7(強調)/ MAX_PENDING_REVIEW = 44(本の判断待ち) | 人がファイルを編集し、理由をコメントに書く | 項目が 1 つ / 上げる理由をコードレビューに載せたい |
| 除外リスト | config/locale_parity_excludes.yml(群ごとに reason: 必須) | 人 | 「この群はまだ 0 にしない」を理由つきで保管する |
自動テストの定数に置く形は、更新が commit の diff に必ず出ます。第 3 部 第 5 回が「上げるときの記録が本体」と書いたのはこの性質を使っていて、claude_md_emphasis_ratchet_test.rb の冒頭コメントは「基準値を上げるときは、その行を機構で守れないかを先に見て、上げた理由をここに書き足すこと」を要求しています。JSON ファイルに置くと、--update が数字だけを書き換えるので理由が残りません。手元の design ratchet はそれを pre-commit の案内文(bash scripts/design_fix.sh && git add config/design_baseline.json docs/reference/)で補っていますが、なぜ増やしたかは commit message にしか残りません。
片向きと両向き
第 1 部 第 6 回は「減ったら基準値ファイルの数字を下げて更新すれば、改善が巻き戻ることもなくなる」と書きました。手元の 2 つの実装は、この更新(基準値の焼き込み)を別の強さで持っています。
design_rules.py は片向きです。cur < base は OK ... (-N) と印字して 0 で終わります。焼き込みは人が --update を打つまで起きず、打ち忘れると次に誰かが N 件増やしても基準値の内側なので合格です。改善は巻き戻れます。
validate_locale_parity.py は両向きです。cur < base も失敗にして「焼き直して固定してください」と案内します。計画 206 §2.2 が「片側だけだと『埋めたのに固定し忘れて、あとで戻る』を止められない」と書いた形です。⚠️ 両向きは pre-commit の中で走ると、翻訳を埋めた commit が必ず一度失敗になります。案内文に --update-baseline の 1 行が無いと、この失敗は「壊れた」に見えます。
除外リストにも逆向きが入っています。admin の群は「運営者しか見ないので訳さない」と理由つきで除外されていますが、欠落が 0 になった瞬間に「除外を消してください」と落ちます。除外が要らなくなったのに残ると、その群は以後ずっと誰にも見られません。⚠️ 同じ形が article_translation_parity_test.rb の JAPANESE_AS_DATA(日本語のまま許した行の一覧)にもあります。一覧の行が記事から消えたら「この行はもう記事に無い。消すこと」と落ちる。逃げ道は、腐ると抜け穴だけが残るので、逃げ道の一覧には逆向きのラチェットを付けます。
走査対象の定義が、範囲そのもの
第 2 部 第 3 回の 254 件は、ラチェットの機構が壊れたのではなく、走査対象の定義が禁止の範囲より狭かった事故です。color_literals は app/assets/stylesheets/**/*.css だけを数えていました。禁止の文面は「色リテラルを書かない」で、AI はその文面を「CSS ファイルに」と読み、style="color:#fff" と el.style.color = "#fff" に流しました。
直しは、走査対象を足すことです。design_rules.py の RULES に inline_color_erb(app/views/**/*.erb の style 属性の中)と inline_color_js(app/javascript/**/*.js の .style.color = などの右辺)を足し、基準値を再測定して 142 と 60 で固定しました。⚠️ 足すときに決めるのは正規表現ではなく「何を色リテラルと呼ぶか」で、canvas の fillStyle やパレット定数は「面に載る色」ではないので数えない、と RULES のコメントに書いてあります。ラチェットの項目を 1 つ足すことは、禁止の適用範囲を 1 行書くことと同じ作業です。
ラチェットでしか見えないもの
第 2 部 第 5 回の 5,525 件は、1 回の判断では誰も気づけない積み上がりでした。翻訳を 1 つ足すたびに「まず ja / en を通す」が勝ち、残り 14 言語が 1 回ぶん欠ける。1 回の欠けは 14 件で、diff を見ても異常には見えません。毎回わずかに負けるものは、件数の時系列にしか現れません。 ラチェットは、その時系列の 1 点を基準値として固定し、次の 1 点との差だけを見る装置です。導入日に 4,041 件(除外の後の値)で基準値を焼き、以後は増えれば失敗、埋めれば焼き直し。ルールだけで守らせていた間に積もったものが、1 commit 単位で見えるようになりました。
下げていくラチェット
もう 1 つの向きがあります。積み残しの件数を上から止め、減らすたびに基準値を下げる運用です。上げてよいのは「新しい負債が増えた」と言えるときだけと決め、0 になったら「この自動テストと基準値の仕組みを畳むこと」まで自動テストのコメントに書いておきます。積み残しが無くなったら装置ごと消すところまで、ラチェットの一部です。
F.4 メタテスト型 メタテスト —— 自動テストを、自動テストが見る
「走査 + 下限」型の下限は「書く」と決めても、書き忘れます。手元では 2026-09-04 に claude_md_enforced_command_test.rb の抽出が 0 件のまま合格を返しました(第 3 部 第 1 回の実測。README §6d)。同じ日に置いたのが zero_target_guard_test.rb で、test/reference/*_test.rb を読み、走査しているのに下限を表明していない自動テストを挙げます。
SCANNING = /Dir\.glob|Dir\[|Rails\.root\.glob|\.glob\(/
MINIMUM = /assert_operator[^\n]*:>= | MIN_[A-Z_]*\s*= | assert_predicate[^\n]*:any\? | refute_empty | \.any\?,/x
SCANNED_MIN = 20
見ているのは書き方だけです。SCANNING に当たる本文があって MINIMUM に当たる本文が無ければ挙げる。静的なので、assert_operator files.size, :>=, 0 と書けば通ります(第 3 部 第 11 回が「4 度目の同じ死角」と呼んだもの)。それでも、0 件で合格になる形を新しく書けば、一度は目に入る。初回の実行で e2_pixel_art_not_in_core_jobs_test.rb が挙がりました。Dir.exist?(JOBS_DIR) でディレクトリの存在は見ていて、中の *.yml が 0 件でも合格でした。いまはその下に assert_operator files.size, :>=, 1 があります。
メタテストは自分にも下限を課す
メタテスト自身が Dir.glob(DIR.join("*_test.rb")) で走査しています。test/reference/ を改名すれば、メタテストは 0 本を読んで「下限の無い自動テストは無い」と合格を返します。だから SCANNED_MIN = 20 があり、コメントに「2026-09-04 時点で 25 本。読み先が壊れたらここで落ちる」とあります。2026-09-05 時点で 27 本、走査型 12 本、12 本とも下限あり(F.13 の 3 で数え方を書きます)。
⚠️ メタテストの検証手順は、メタテストの読み先を壊すことです(第 3 部 第 11 回の検証手順 3)。他の自動テストから下限を消して失敗を見るのは、メタテストが「見ている」ことの確認で、メタテストが「空回りしていない」ことの確認ではありません。両方が要ります。
文書の書式を見るメタテスト
対象が自動テストでなく文書でも、メタテスト型です。experience_principles_test.rb の 6 本は docs/reference/experience_principles.md の HTML コメントの中の YAML を ExperiencePrincipleParser で読み、artifacts と guarded_by が最低 1 つあるか(1)、名指し先が実在するか(2・3)、名指し先の中に目印があるか(4・5)、禁止パターンに当たる行が無いか(6)を見ます。第 3 部 第 9 回はこの 6 本を「名指し + 目印」型(名指し + 目印)として書きましたが、書式を要求する側はメタテスト型です。パーサは id / name / severity / artifacts / guarded_by の 5 欄を必須にし、severity は critical か recommended だけを許し、コードブロックの中の説明用サンプルは strip_code_blocks で先に落としています。⚠️ このパーサにも「ブロックが 0 個なら ParseError」があります。「走査 + 下限」型の下限を、パーサが持っている形です。
検査の道具にも単体テスト
ラチェットのスクリプト design_rules.py には scripts/tests/test_design_rules.py が 42 本あり、コメントの除去、色リテラルの正規表現、design-allow の効き方を単体で見ています。hook の 4 本は付録 C.9 です。自動テストの道具は、自動テストの外側にいます。 テストスイートが合格でも、ラチェットの正規表現が壊れていれば件数は静かに 0 に近づき、基準値の内側なので誰も気づきません。
F.5 変異注入型 変異注入 —— 壊して、合格のまま通るものを探す
第 1 部 第 3 回の方針ファイルは 5 条でした。それぞれの下に、ミューテーションテスト(変異テスト、mutation testing)の文献が置いた名前があります。
| 方針 | 文献の側の名前 | 何が起きるか |
|---|---|---|
| 1. テストが落ちるはずの行を選ぶ。当たらないはずの行を壊して通っても抜け穴ではない | 等価変異(equivalent mutant) —— 元と振る舞いが変わらない変異。検出できないのが正しい | 設計上到達しない行・結果に効かない定数を壊し、通ったのを「テストの抜け穴だ」と AI が騒いだ(本編の実例) |
2. 1 か所壊す → 走らせる → 記録 → 必ず元に戻す(git diff が空に戻るまで) | 変異体(mutant)は 1 度に 1 つ(一次変異) | 2 か所同時に壊すと、落ちたときにどちらが見られているか分からない |
| 3. 最初の 1 件は確実に落ちる変異にする。全部通ったら測定器を疑う | pseudo-tested —— 実行はされているが、欠陥を入れても検出されない形でテストされているメソッド(Niedermayr ほか 2016) | 何を壊しても合格で、テストが走っていなかった(本編の実例)。陽性対照が無いと、測定器の故障と「抜け穴なし」を区別できない |
| 4. 落ちた変異は何も保証しない。報告するのは通った変異だけ | 変異スコア(殺した変異体 / 全変異体)は網羅の指標で、AI の自作自演検出には使わない | 「5 か所壊して 5 か所落ちた」を成果に読む誤用 |
| 5. 通ったテストは、実装の写しではなく仕様から書き直す | — | 写しを直しても写しになる。テストの入力を仕様の側から取る |
AI が写しを書く機構と、AI に壊させる理由
本編の主張は「AI は実装とテストを同時に書くから、テストが実装の写しになる」でした。機構としては、同じコンテキスト(付録 A の 1 リクエスト)に実装が載った状態でテストを生成するので、実装の出力をそのまま期待値に貼る、実装と同じ計算をモックの中で繰り返す、実装が見ていない入力はテストにも現れない、という相関が生まれます。人のチームがレビューで防いでいた自作自演が、生産の標準形になります(AI の性質としての出典は付録 I)。
変異注入を専用ツール(全変異体の機械生成)ではなく AI に頼む理由は 2 つです。対象が違う——網羅の測定ではなく、写しの検出です。写しは、実装が通る経路の変異なら落とすので、変異スコアは高く出ます。見たいのは「実装が見ていない場所」で、それを一番知っているのは実装を書いた AI です。コストが違う——全変異体を回すと、等価変異の判定に人手が要り、実行時間も変異体の数に比例します。数か所の抜き取り(mutant sampling の極端な形)は網羅ではない、と本編は注意に書きました。
カバレッジは代わりにならない
「テストは通っていて、カバレッジも 83% ある」は、写しの検出には効きません。Inozemtseva と Holmes(ICSE 2014)は 5 つの Java プログラム(最大 72 万行)で 31,000 のテストスイートを生成し、行・分岐・修正条件カバレッジと、変異テストで測った欠陥検出力の相関を、スイートの大きさを統制して測りました。結論は、相関は低〜中程度で、強いカバレッジ基準にしても洞察は増えず、カバレッジは未テストの場所を見つけるには有用だが品質目標にすべきではない、です。手元の test_helper.rb は COVERAGE=1 のときだけ minimum_coverage line: 83, branch: 60 を課しますが、これは「見ていない場所」を指すためのもので、「見ている場所が写しでない」ことは言いません。
変異注入の対象は、実装だけではない
本編で「壊して確かめる」と書いた場所を数えると、対象は 4 種類ありました。
| 対象 | 壊す操作 | 本編 |
|---|---|---|
| 実装 | 境界値・条件・定数を 1 つ | 第 1 部 第 3 回 |
| 自動テスト | 下限の行を消す / 突合を 1 つ外す | 第 3 部 第 11 回 / article_translation_parity_test.rb のコメント |
| hook | 禁句の正規表現を sed で潰した写しを作る | 宣言 hook / 付録 C.9 |
| 文書 | 機構の無い依頼を足す / 実在するが目印の無いファイルを名指しする | 第 3 部 第 1・9 回 |
hook の自動テストの 9 項目目は、こう書いてあります。
# 9. 変異注入: 禁句の判定行を潰すと 5 が素通りする(=その行を見ている)
MUT="$WORK/mutant.sh"
sed 's/念のため|とりあえず|確認のため/__never__/' "$HOOK" > "$MUT"
変異体を別ファイルに作るので、方針 2 の「必ず元に戻す」が要りません。実装に対する変異注入も、可能なら同じ形(写しを作って写しを壊す)にすると、F.12 の「壊した実装が生きている時間」の問題が消えます。Ruby のテストでは Module#prepend で 1 メソッドだけ差し替える、環境変数で分岐を入れる、などが写しの代わりになります。
素通りした 1 種
計画 214 §5b は、記事の計測を実装した日に 7 種の変異を注入し、6 種が落ちて 1 種が素通りした記録です。素通りしたのは「未知の値(unknown)を捨てる」変異で、理由は自動テストが unknown を 1 度も作っていなかったこと——4 次元とも既知の階級に落ちる送信しかしていなかった。「合計が一致する」型の自動テストは、ずれる材料を混ぜないと永遠に一致します。 これは「走査 + 下限」型の空虚な真の変形で、走査対象が 0 件なのではなく、違反になりうる入力が 0 件でした。下限で塞げないので、変異注入でしか見つかりません。
F.6 受け入れテスト型 受け入れテスト —— 構造の同一性
第 3 部 第 6 回は、サブエージェントに翻訳を任せ、戻ってきた成果物を原本と構造が同じかで受け入れました。意味の正しさは人が見ます。機械が決められるのは構造までで、そこまでは機械に決めさせる。手元の article_translation_parity_test.rb の assert_structure_matches は 7 つを突き合わせます。
| 突合 | 何を比べるか | 訳で変わらない理由 |
|---|---|---|
links | ](...) の中身を順序込みで | リンク先は Zenn の URL のまま書く規約 |
heading_levels | # の数の列 | 見出しの数と階層は訳で変わらない |
bullets | - で始まる行の数 | 項目数は変わらない |
paragraphs | 空行区切りの段落数(フェンスの中は除く) | 段落を落とす・割る事故を拾う |
code_blocks | 訳さない囲み(bash / ruby / 無印)の全行。行コメントだけ落とす | コードは訳さない規約 |
translatable_blocks | 訳してよい囲み(markdown / mermaid)の行数・箇条書き数・矢印数 | 中身の文は訳すが骨格は変わらない |
numbers | 本文の数値の集合(URL の中は除く) | 実測値は訳しても変わらない |
変異注入で見つけた 2 つの抜け穴
この自動テストのコメントに、2026-09-03 の変異注入の記録が 2 つ残っています。
1 つ目、地の文だけの段落。 リンクも数値も箇条書きも持たない段落を訳から丸ごと落としても、合格のままでした。7 つのうち paragraphs 以外はその段落に何も見つけないので、落ちても差が出ません。paragraphs を足したのはこの後です。「段落を 1 つ落としたことに気づけない」ために置いた自動テストが、段落を 1 つ落として気づけなかった、とコメントにあります。
2 つ目、別経路。 訳は 2 本(コメントが書かれた 2026-09-03 も、この章を書いた 2026-09-05 も)で、どちらも markdown / mermaid の囲みを持たないので、translatable_blocks の突合は実データでは 0 組で回ります。それを固定するために単体の突合(原本と訳の文字列を自動テストの中に書いた)を置いたのですが、最初は assert_structure_matches を通さず別の関数で書いていました。実データのループから突合を 1 つ外しても、単体の側は別経路なので合格のままでした。いまは同じ関数を通し、さらに「訳してよい囲みが太っても素通りしていないか」を assert_raises(Minitest::Assertion) で見ています。⚠️ このとき差分は箇条書き・段落・数値・リンクのどれにも掛からない形(矢印を 1 本足す)にする、とコメントにあります。項目を 1 つ増やす形だと bullets に助けられて、外した突合が見えません。等価変異の逆で、他の突合に拾われない変異を選ぶ必要があります。
受け入れテストに 0 組が来る
受け入れテスト型は、成果物が来るまで突合が 0 組です。訳が 1 本も無ければ pairs = 0 で、MIN_PAIRS = 1 の下限が落とします。訳が 1 本でもあれば通りますが、その 1 本に markdown の囲みが無ければ translatable_blocks は空と空を比べています。だから「連載には訳してよい囲みを持つ記事が実在する」を別に見ています(owners.size >= 5)。原本の側に材料が実在することを固定しないと、規則ごと消しても誰も落ちません。
生成にも同じ型
この本の章は docs/articles から生成します。生成器 ZennBookBuilder は連載をまたぐ装置(次回予告・前後ナビ・「この記事は連載の第 N 回です」の枠)を落としますが、落とす規則が空回りしていないことは、原本にその装置が何個あるかで見ています。MIN_INTRO_FRAMES = 30 / MIN_TRAILING_NAVS = 36 / MIN_TEASERS = 35 / MIN_STAGE_FULL = 16 / MIN_CHAPTER_LINKS = 150。原本の装置が減ったら「落とす規則が空回りしている疑い」で落ちる。⚠️ 落とすものの数を固定するのは、落とした結果が 0 件だからです。「何も残っていない」を確かめるには、「何を落としたか」の数が要ります。
F.7 「名指し + 目印」型 名指し + 目印 —— 双方向にする設計
第 3 部 第 9 回の形は、文書(原則)がコードを名指しし、コードに目印を書かせ、両方をメタテストで突き合わせるものでした。2026-09-05 に数え直した値は本編と同じです。
| 数 | 値 |
|---|---|
| 原則 | 18 |
artifacts の参照 / 実ファイル | 53 / 43 |
guarded_by の参照 / 実ファイル | 27 / 25 |
@experience-principle の目印があるファイル | 75 |
| うち、どの原則も名指ししていないファイル | 32 |
保証は片方向です。名指ししたファイルは実在し、開いて目印を書き込んである(原則 → コード)。目印があるファイルがどの原則からも名指しされていないことは、見ていません(コード → 原則)。名指しを 1 つ消しても、目印は宙に浮いたまま合格です。
逆方向を足すとどうなるか
逆方向の自動テストは書けます。
test "every marker is named by some principle" do
named = @principles.flat_map { |p| p["artifacts"] }.to_set
marked = Dir.glob(REPO_ROOT.join("{app,lib,test,config,db}/**/*"))
.select { |f| File.file?(f) && File.read(f).include?("@experience-principle") }
.map { |f| Pathname.new(f).relative_path_from(REPO_ROOT).to_s }
assert_operator marked.size, :>=, 40, "印の走査が空回りしている"
orphan = marked - named.to_a
assert_empty orphan, "印があるのに、どの原則も名指ししていない:\n - " + orphan.join("\n - ")
end
手元でこれを走らせると 32 件が並びます。足す前に決めるのは、32 件を名指しに足すか、目印を剥がすかです。目印はコードのノイズです——原則が 18 個で 75 ファイルなら成立しますが、200 個なら目印を入れるコストが先に破綻する、と本編は書きました。逆方向を足すと、目印を書いた側に「どの原則のためか」を答えさせることになり、目印の数は名指しの数に収束します。それがこの型の望ましい形か、過剰な締め付けかは、目印を何のために書いているかで決まります。原則の設計を守るためなら前者、AI が開いた形跡を残すためだけなら後者です。手元では足していません。
書式で強制することの一般形
「名指し + 目印」型の要点は、読んだかどうか(測れない)を、読まなければ書けない書式(測れる)に置き換えたことです。同じ置き換えが、他の型にもあります。
| 測れないこと | 置き換えた書式 | 型 |
|---|---|---|
| 原則を書くときに実装を開いたか | 名指し先に目印 | 6 |
| 撮影前に安い手段で済まないかを考えたか | 宣言ファイルの Q1 / Q2(付録 C.8) | 7 の変形 |
| E2E を走らせるべきかを判断したか | E2E="<領域名>" を打つ(領域名は現物から数える) | 8 |
| 除外してよいか考えたか | 除外リストに reason: を必須にする | 2 |
書式の強制は、AI にも人にも同じ形で当たります。第 1 部 第 6 回が「チームでは言って回らなくて済むぶん、個人開発より効く」と書いたのは、書式には読み手が要らないからです。
F.8 「機構の裏打ち」型 機構の裏打ち —— 依頼と hook を突き合わせる
第 3 部 第 1 回の claude_md_enforced_command_test.rb は、指示ファイルの「必ず実行」の直後の囲みから scripts/... を拾い、.git-hooks/pre-commit.sample にその文字列が含まれるかを見ます。見ているのは 1 点で、実行を依頼するなら、その実行が機構で裏打ちされていること。裏打ちの無い依頼は読まれたときだけ効く、という第 1 部 第 6 回の線引きを、指示ファイルの側から自動テストにしたものです。
抽出の壊れ方 —— 0 件で合格になった日
requested_commands は、必ず実行 を含む行の次から最初のフェンス 1 つの中身を読みます。最初の実装は開きのフェンスで break していて、囲みの中を 1 行も読まず 0 件を返し、assert_empty unenforced は空と空で合格でした。落ちたのは assert_operator requested.size, :>=, 2 のほうで、コメントに「『0 件で合格にしない』アサーションのほうが先に落ちて気づけた」とあります。「走査 + 下限」型の下限は、「機構の裏打ち」型の抽出器にも要ります。 抽出器は走査の一種で、正規表現とフェンスの扱いが変われば空を返します。
実体ではなく、版管理された正を見る
自動テストが読むのは .git/hooks/pre-commit ではなく .git-hooks/pre-commit.sample です。前者は版管理されておらず、未設置の環境では存在しないので、読むと空振りします。後者はリポジトリに入っている正です。⚠️ その代わり、実体が正と一致しているかはこの自動テストでは見えません。手元ではそこを別の hook(require_git_hooks_installed.sh。git commit の前に hooks が設置されているかだけを見て、無ければ止める。内容差は見ない。付録 C.6)が持っています。「機構の裏打ち」型は「依頼 ↔ 正」を見て、hook が「正 ↔ 実体」を見る、2 段階です。片方だけだと、正に書いてあるのに実体が無い環境で依頼だけが残ります(付録 G が worktree と clone ごとの hooks の場所を扱います)。
語形依存
拾うのは 必ず実行 という語形だけです。「実行しておいてください」に書き換えれば抜けます。機械に決められるのはここまでで、言い換えは通る。同じ死角がラチェット型の強調(EMPHASIS = /\*\*|⚠️|最重要|絶対|必ず/)にもあります。⚠️ 語形依存を塞ぐ手は 2 つあります。語を増やす(正規表現を育てる。増えるほど偽陽性も増える)か、書式を決める(実行の依頼は必ず > **コミット前に必ず実行**: の形で書く、と規約にしてメタテスト型で書式を見る)。手元は前者のまま、語を 1 つに絞っています。
対応表の整合 —— 対応表がテスト名を写すとき
「機構の裏打ち」型の一般形は「文書が名指しした機構が実在するか」で、「名指し + 目印」型の片割れです。禁止事項の「機械検証: あり」の欄や、メモリの行動ルールと自動テストの対応表は、テストファイル名を文字列で写しています。写した名前の実在を見る自動テストが無ければ、テストを改名した日に対応表は静かに古くなります。第 2 部 第 8 回の「機械検証: なし」の項目が黙って実装から離れる、と同じことが「あり」の項目にも起きます。置くなら「走査 + 下限」型の形(名指しを正規表現で抜き、下限を併置し、実在を見る)で 1 本です。
F.9 「件数を数える」型 件数を数える —— 合否を出さず、判断材料を毎回出す
「走査 + 下限」型〜7 は失敗か合格かを返します。「件数を数える」型は返しません。数えて、並べて、人か AI が読む。本編に 3 つありました。
3 階層の件数(第 2 部 第 8 回)
気づきのメモ 91 / 行動ルール 43 / 公式の禁止 17(2026-09-03)。数えるのは数分で、ls と grep -c で足ります。
M="$HOME/.claude/projects/<project>/memory" # <…> は自分の値に
ls "$M"/*.md | wc -l # 気づきのメモ全体
ls "$M"/feedback_*.md | wc -l # 行動ルールとして分類されたもの
grep -c '^### NG' docs/reference/non_goals.md # 公式の禁止
grep -c '^| `feedback_' docs/reference/memory_rule_guards.md # 自動テストへ移した分
読み方は本編にあります。下が増えて上が止まっているなら健全、下も上も止まっていたら書き込みの経路を疑う。 「件数を数える」型に合否が無いのは、「上が増えていない」が健全にも故障にも読めるからです。合否を機械に出させると、どちらかに寄ります。⚠️ 数えるものが 3 つ並んでいることが本体で、1 つだけ数えると「増えていないから健全」に必ず寄ります。
強調の行(第 3 部 第 5 回)
claude_md_emphasis_ratchet_test.rb はラチェット型ですが、目的は「件数を数える」型です。落ちたときに現在の強調行が全部並ぶ。本編の検証手順 2 は「並んだ行を眺めて、そのうち何行に機構があるかを数える」で、これは合否ではなく点検です。7 行のうち 5 行に機構があり、機構の無い 2 行が唯一「最重要」を名乗る節でした。⚠️ その節には、いまも機構がありません(F.12)。
未実行の一覧
[E2E] 次の領域は実行していません の一覧は、走らせなかったものを走らせなかった本人の報告の外に置く装置です。機構は 2 段階あります。
収集。各テストは skip_e2e を呼び、選ばれた領域と完全一致しなければ Minitest::Skip を投げて SKIPPED_AREAS に領域名を積みます。Minitest.after_run がそれを並べます。領域名の一覧は文書に置きません——TestExecutionGuard#e2e_areas が test/**/*_test.rb を読んで E2E_AREA = "..." を現物から数えます(2026-09-05 に 23 領域。本編の 22 から 1 つ増えています)。文書に一覧を置くと二重管理になり、片方が腐る。
照合。pre-commit は、ステージした E2E のテストファイルから領域名を拾い、走らせた領域の履歴にその領域がファイルの更新時刻より後にあるかを見ます。無ければ「直した E2E を走らせていません」で止まります。⚠️ 判定できるのは「E2E のテストファイル自身をステージしたか」だけで、実装の変更から走らせるべき領域を導くことはしていません。導くには依存関係の解決が要り、それは test impact analysis が持っていて手元に無いものです。この方式が TIA と違うのは、間引いた事実を隠さないことだけです(付録 H.6 の表)。
skip と件数の行
Minitest の skip は Minitest::Skip を投げ、要約の行(N runs, N assertions, N failures, N errors, N skips)の skips に数えられ、失敗の終了コードにはなりません。skip されたテストの一覧は --verbose を付けたときだけ出ます。⚠️ つまりE2E の除外も、他の理由の skip も、同じ skips の 1 数字に混ざります。 第 1 部 第 1 回の「件数の行で異常を拾う」を使うなら、skips の平常値(E2E 除外の件数)を知っている必要があり、それが増えたら E2E 以外の skip が入ったことになります。手元の rails_test.sh は skips を分けて見ていません。一覧が [E2E] の行で別に出るので、skips の数字は読まなくてよいという設計ですが、E2E 以外の skip が混ざったときに気づく手は、いまのところ人の目だけです。
毎回出すものの条件
T3 は「常駐させてよいのは、覚えなくていいものだけ」と書きました。「件数を数える」型の出力を毎回出してよい条件を、機構の側から言い直すと 3 つです。照合だけを求める(触った領域が載っていなければ読み飛ばして終わり)。正は現物にある(一覧を人が更新しない)。出力の場所が道具の側にある(AI の報告に依存しない)。3 つを満たさないものを常駐させると、序論の「ルールは足すほど薄まる」に戻ります。23 行が毎回会話層に積まれるトークンは、この条件の対価です(付録 A・B)。
F.10 型の外側 —— 8 つで見られないもの
| 見られないもの | なぜ | 手元での扱い |
|---|---|---|
| 意味の正しさ(訳の質・帰属・経済バランス) | 機械が真偽を決められない | 受け入れテスト型は構造まで。意味は人。「機械検証: なし(理由)」を書く |
hook が settings.json に配線されているか | hook の自動テストは hook のファイルを直接呼ぶ(C.9) | C.12 の 5 で人が見る |
| ラチェットの基準値が妥当か | >= 0 も BASELINE = 999 も書式としては正しい | 上げる理由をコメントに書く運用 |
| 「必ず実行」以外の語形の依頼 | 語形で拾う | 語を 1 つに絞り、増やさない |
| 実装の変更から走らせるべきテスト | 依存関係の解決が要る | ステージしたテストファイルだけ見る([7/8] [8/8]) |
| キーはあるが訳が無い(英語のまま) | parity はキーの有無しか見ない(計画 206 §8.9) | 人 |
| 記事・付録のコードブロックの中 | 走査対象の定義に入っていない | 未(F.12) |
F.11 本編との対応
| 本編 | 置いたもの → 型 |
|---|---|
| 第 1 部 第 3・6 回 / 未実行の一覧と宣言 hook(本書の章には無い) | 変異注入の方針 5 条 → 4 / 走査 + 下限とラチェット → 1・2 / E2E の skip と未実行一覧と pre-commit の照合 → 8(+ 7)/ hook の自動テスト 9 項目 → 4(対象 = hook。C.9) |
| 第 2 部 第 3・5・8 回 | 適用範囲を判定軸で書く + 双方向の回帰 → 2(走査対象の定義)・4 / 「機械検証」の見出しと locale parity → 型の外の記録・2(両向き)/ 3 階層の件数と置かなかったものの一覧 → 8 |
| 第 3 部 第 1・5・6・9・11 回 | 「必ず実行」↔ pre-commit → 7 / 強調の件数 → 2(目的は 8)/ 訳と原本の構造 → 5 / 名指し + 目印 → 6(書式の要求は 3)/ 下限の無い走査テストを挙げる → 3 |
F.12 本編が言っていない注意
- 記事と付録のコードブロックは、どの型の走査対象にも入っていません。 第 3 部 第 11 回が数えたレビューの指摘 5 件がここで起きました。この本の付録 A〜F のコードも同じ外側にいます。塞ぐなら、
docs/articles/**/*.mdのフェンスの中を言語別に切り出して、bash はbash -n、ruby はruby -cに通す「走査 + 下限」型を 1 本置きます(F.13 の 8)。⚠️ 簡約版のコードは実物から関数を落としているので、構文は通っても未定義の呼び出しが残ります。shell_undefined_command_sweep.pyがスクリプト側で同種のことをしていますが、記事には当てていません。⚠️ この段落を書いた翌日(2026-09-06)に、その 1 本をtest/reference/article_code_fence_syntax_test.rbとして置きました。最初の走行で 4 件落ちました——付録 D の JSON が断片、付録 E と F の<session-id><project>を bash がリダイレクトと読む、第 2 部 第 6 回の Ruby に簡約の...が残る。うち F の 1 件はbash -nを通ります(M=~/…/<project>/memoryは「projectから読んで/memoryへ書く」という正しい構文です)。行末の<…>しか落ちないので、引用符の外の<…>を見る検査を別に足しました。 - 変異注入の最中は、壊した実装がその場に生きています。 第 1 部 第 3 回の方針 2「必ず元に戻す」は、並列実行するセッション・pre-commit・commit が走らないことを前提にしています。手元の並走ガード(付録 G)はテストの同時実行を止めますが、別セッションが同じ作業ツリーで
git add -Aすることは止めません。戻す手段は 2 つ(Claude Code のファイル checkpoint による rewind とgit diff/git checkout)あり、どちらで戻すかを先に決めておくこと、可能なら写しを作って写しを壊す形(F.5 の hook の例)にすることです。 - ガードは、自分自身を締め出します。 2026-09-11 に
require_full_read.py(監視対象のルール文書を「一部だけ」読む形を止める hook)の下限を、パターンの本数ではなく展開後のファイル数のつもりで置きました。16 < 20 で自分の設定が不正と判定され、⚠️ その判定を、関係のない呼び出しの前で先に見ていたので、git statusすら通らなくなりました。BashもReadも塞がっているので、自分では直せません(Writeが matcher の外にいたので復旧できました)。守りの条件を見る場所は、それが守る対象の呼び出しに限ること。もう一つ、下限には単位を書くこと —— 「20」だけでは、何を 20 数えているのかが次に読む人にも自分にも分かりません。⚠️ F.2 の「0 件で合格にしない」は正しいのですが、その下限自体が誤爆すると、守っていたはずの作業が全部止まります。逃げ道(SKIP_*)を、下限の判定より前に置くのが安全です(この段落を書いた直後に、自分の hook がそうなっていないことに気づいて直しました)。⚠️ そのとき変異注入が赤くならず、判定が「止める側」しか見ていないことも分かりました —— 陽性対照は併置しただけでは足りず、変異の合否も両側で数える必要があります。 - ラチェットの基準値ファイルは、並列実行する作業場所で競合します。
config/design_baseline.jsonを 2 つの clone で別々に--updateすると、merge で片方の数字が勝ちます。数字が勝った側の clone では、負けた側の改善が「増えた」と見えます。基準値の更新は 1 commit に分け、merge した直後にpython3 scripts/design_rules.pyを素で走らせて数字が現状と合っているかを見ます。手元でこれを機構にはしていません。 - 「落ちた変異は何も保証しない」を、もう 1 段階。 通った変異が 0 件でも、それは抜き取った数か所が見られていることしか言いません。第 1 部 第 3 回の注意は「全部落ちても品質は万全にはならない」で、その言い換えを避けるべき理由は F.5 のカバレッジの研究と同じです。数か所の変異注入の結果を、変異スコアのように報告しないこと。
- 「名指し + 目印」型の逆方向は無く、足すと設計判断が要ります。 32 ファイルの目印を名指しに足すか剥がすかは、目印を何のために書いたかで決まります(F.7)。足さないなら、「目印は名指しの証拠であって網羅ではない」を原則の文書に 1 行書いておきます。
skipsは 1 数字に混ざります(F.9)。E2E の除外以外の skip が入っても、要約の行では区別が付きません。- 一番強く書いた節に、いまも機構がありません。 第 3 部 第 5 回が「最重要」を名乗る節に機構が無いと書いたまま、この本を書いた日も変わっていません。「機構の裏打ち」型の自動テストは「必ず実行」の語形しか見ないので、この節の欠落を挙げません。
- hook の配線は自動テストの外です(C.9)。変異注入型で hook を壊して失敗を見ても、
settings.jsonのパスの打ち間違いは合格のままです。 - メタテストの
>= 0。 メタテスト型は書式しか見ないので、下限を0にすれば通ります。手元の 12 本の下限は 1 から 1000 の間(10 本がassert_operator ... :>=かMIN_*の定数、2 本がrefute_empty/.any?の形)で、0は無いことを F.13 の 3 で数えられます。
F.13 検証手順
止まるべきものが止まり、通るべきものが通ることを、型ごとに一度ずつ。⚠️ 壊す操作は写しか作業ツリーの一時変更で行い、終わったら git diff が空に戻ることを見ます。
1. 「走査 + 下限」型 —— glob を壊して、下限が先に落ちる。 走査型の自動テスト 1 本(例: memory_rule_user_name_field_test.rb)の GLOBS を app_/**/*.rb に変えて走らせます。assert_empty violations ではなく assert_operator files.size, :>=, 1000 が落ちて、メッセージに件数が出ることを見ます。戻して合格。次に下限の行を消して同じ変異を入れると、合格のまま通ることを見ます。これが空虚な真の実物です。
2. ラチェット型 —— 片向きと両向きの違いを見る。 app/assets/stylesheets/ のどこかに color: #abc; を 1 行足して python3 scripts/design_rules.py を走らせ、NG color_literals: 42 -> 43 (+1) で 1 が返ることを見ます。戻して 0。今度は既存の色リテラルを 1 つ変数に直して走らせると、OK ... (-1) と出て 0 で終わる(片向き)ことを見ます。--update を打たずに置いておけば、次に 1 つ増やしても合格です。両向きの側は config/locales/ のどれかの群で 1 キーを訳に足し、python3 scripts/validate_locale_parity.py が「減りました。焼き直して固定してください」で 1 を返すことを見ます。
3. メタテスト型 —— メタテストの読み先を壊す。 まず、手元の走査型と下限の数を自分で数えます。
python3 - <<'EOF'
import re, glob
scan = re.compile(r"Dir\.glob|Dir\[|Rails\.root\.glob|\.glob\(")
minimum = re.compile(r"assert_operator[^\n]*:>=|MIN_[A-Z_]*\s*=|assert_predicate[^\n]*:any\?|refute_empty|\.any\?,")
files = sorted(glob.glob("test/reference/*_test.rb"))
scanning = [f for f in files if scan.search(open(f, encoding="utf-8").read())]
guarded = [f for f in scanning if minimum.search(open(f, encoding="utf-8").read())]
print(len(files), "本 / 走査型", len(scanning), "/ 下限あり", len(guarded))
for f in scanning:
for m in re.finditer(r"assert_operator[^\n]*:>=,\s*(\w+)|MIN_[A-Z_]*\s*=\s*(\d+)", open(f, encoding="utf-8").read()):
print(" ", f.split("/")[-1], m.group(1) or m.group(2))
EOF
2026-09-05 の値は 27 / 12 / 12 です。次に zero_target_guard_test.rb の DIR を test/reference_ に変えて走らせ、SCANNED_MIN の行が「読み先が壊れている(0 本しか見えていない)」で落ちることを見ます。戻して合格。最後に、別の走査型から下限の行を 1 行消して走らせ、メタテストがそのファイル名を挙げることを見ます。
4. 変異注入型 —— 等価変異を 1 つ、当たる変異を 1 つ。 「走査 + 下限」型の自動テスト 1 本を選び、実装の側に 2 つの変異を入れます。1 つ目は当たらない行(到達しない分岐の定数)で、テストが通ることを見て、方針 1 のとおり「抜け穴ではない」と記録します。2 つ目は当たる行(comparison_only? の [=!]= を [=!]== にする)で、比較の行が違反として挙がる(失敗になる)ことを見ます。両方戻して合格。⚠️ 2 つ目が合格のままなら、その行は見られていません。
5. 受け入れテスト型 —— 他の突合に掛からない変異を選ぶ。 article_translation_parity_test.rb の単体の突合で、grown の作り方を「矢印を足す」から「箇条書きを 1 つ足す」に変えて走らせます。assert_raises は通ります(bullets が拾うから)。次に assert_structure_matches から translatable_blocks の 1 行を消して走らせると、箇条書き版では合格のまま、矢印版では assert_raises が「訳してよい囲みが太っても素通りしている」で落ちることを見ます。突合を消す変異が見えるのは、他の突合に拾われない差分だけです。
6. 「名指し + 目印」型 —— 逆方向が無いことを確かめる。 experience_principles.md のどれか 1 原則から artifacts の 1 行を消して走らせます。6 本とも合格のままです(目印は宙に浮くが、誰も見ていない)。戻して、今度は実在するが目印の無いファイル(例: app/models/concerns/ の 1 本)を artifacts に足すと、メタテスト 5 が「marker not found」で落ちることを見ます。F.7 の逆方向のコードを一時的に置いて走らせれば、32 件が並びます。
7. 「機構の裏打ち」型 —— 依頼と機構の両方向。 CLAUDE.md に「コミット前に必ず実行」と scripts/nothing.sh の囲みを足して走らせ、claude_md_enforced_command_test.rb が行番号つきで落ちることを見ます。戻して、今度は .git-hooks/pre-commit.sample から scripts/validate_locales.py の呼び出し行を消して走らせ、同じ自動テストが落ちることを見ます。両方戻して合格。⚠️ 3 つ目として、CLAUDE.md の「必ず実行」を「実行しておいてください」に書き換えると、合格のまま通ります(語形依存)。
8. 「走査 + 下限」型の未実装 —— 記事のコードブロックの構文。 記事の bash の囲みを切り出して bash -n に通す 1 本を、一時的に置いて走らせます(⚠️ 2026-09-06 に恒久の形で置きました。F.12 の末尾)。
python3 - <<'EOF'
import re, glob, subprocess, tempfile, os
n = bad = 0
for md in glob.glob("docs/articles/**/*.md", recursive=True):
for lang, body in re.findall(r"```(bash|sh)\n(.*?)```", open(md, encoding="utf-8").read(), re.S):
n += 1
with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as f:
f.write(body); path = f.name
r = subprocess.run(["bash", "-n", path], capture_output=True, text=True)
os.unlink(path)
if r.returncode != 0:
bad += 1; print(md, r.stderr.strip().splitlines()[0])
print("bash blocks", n, "syntax errors", bad)
EOF
n が 0 なら抽出が空回りしています(フェンスの言語名が bash でない)。第 3 部 第 11 回の「山括弧をシェルがリダイレクトと解釈する」は bash -n で拾えます。「未定義の関数を呼ぶ」は拾えません(F.12 の 1 つ目)。
9. 「件数を数える」型 —— 一覧に載る・消える・照合が止める。 E2E_AREA を持つテスト 1 本の領域名を控え、素の全量で走らせて [E2E] 次の領域は実行していません にその領域が載ることを見ます。E2E="<領域名の一部>" を付けて走らせ、一覧から消えて [E2E] 走らせた領域: に出ることを見ます。最後に、そのテストファイルにコメントを 1 行足してステージし、E2E を走らせずに git commit すると [7/8] が「直した E2E を走らせていません」で止まることを見ます。E2E を付けて走らせてから commit すると通ります。⚠️ E2E=all は落ちます(全量指定を用意していない)。
10. skips を分ける。 素の全量の要約の行の skips を控えます。test/models/ のどれかに理由の無い skip を 1 つ足して走らせ、skips が 1 増えることと、[E2E] の一覧は変わらないことを見ます。この差が、いま人の目でしか拾えないものです(F.12)。
F.14 出典(2026-09-05 確認)
- R. Niedermayr, E. Juergens, S. Wagner, "Will My Tests Tell Me If I Break This Code?" arXiv:1611.07163, 2016 —— pseudo-tested methods(欠陥を入れても検出されない形でテストされているメソッド)の定義と、単体テストでは許容範囲だがシステムテストでは許容できない比率になるという結果。arXiv の要旨で確認
- L. Inozemtseva, R. Holmes, "Coverage Is Not Strongly Correlated with Test Suite Effectiveness," ICSE 2014 —— 5 つの Java プログラム(最大 72.4 万行)で 31,000 スイートを生成し、行・分岐・修正条件カバレッジと変異スコアの相関を、スイートの大きさを統制して測定。相関は低〜中程度、強いカバレッジ基準でも洞察は増えず、カバレッジを品質目標にすべきでないと結論。著者サイトの PDF の要旨で確認
- Minitest::Assertions#skip ——
skipがMinitest::Skipを投げ、失敗の終了コードにならず、一覧は--verboseのときだけ出ること - Ruby 3.3
Dir.glob—— 公式リファレンスは無一致時の戻り値を明記していないので、手元の Ruby 3.3.6(Docker)でDir.glob("no_such_dir/**/*_test.rb") #=> []を実測 - Hooks guide / How Claude remembers your project —— 指示ファイルは文脈で強制ではない、hook は LLM の選択に頼らず必ず起きる(第 1 部 第 6 回が引いた 2 文。付録 C・D で確認済み)
- 手元の実測 ——
test/reference/*_test.rb27 本(走査型 12 本・下限あり 12 本。2026-09-05)と、本文で名指しした自動テスト・スクリプト・基準値ファイル