---
title: "動かさない技術 第2回：ラッパーを使わせない技術"
author: garplab
publisher: TypingTube
license: CC BY 4.0
license_url: https://creativecommons.org/licenses/by/4.0/
license_scope: 「CC BY 4.0」の印から始まる節（仕組み・検証手順・コード）。印の無い本文は著作権を留保
canonical: https://typing-tube.net/articles/ugokasanai-02-no-wrapper-enforcement
series: "動かさない技術"
language: ja
---


今回は、AI に自分たちのラッパーを使わせる話です。ラッパーというのは、標準のコマンドの代わりに置いたプロジェクト固有の入口のことで、`rails test` の代わりに置いた `scripts/test.sh` がその形です。置く理由はいくつもあります。並走を止めたい、環境変数を揃えたい、記録を残したい。そして置いたあと、たいていの人が指示ファイルに 1 行書きます。「テストは `scripts/test.sh` で走らせてください」。

私は 1 作目「読まない技術」の[第 1 回](https://typing-tube.net/articles/cd93e0c5cac832)で、その 1 行についてこう書きました。

> 「ラッパーを使うこと」と指示ファイルに書いても、テストを走らせようとした瞬間に出てくるのは、何億回と見てきた標準コマンドの側です。指示ファイルの 1 行は、AI の中で染み付いた習慣と優先順位を競うことになり、**たいてい負けます**。

**この「たいてい」を、私は数えていませんでした。** 公式のドキュメントは逆の側に立っています。指示ファイルに書くものの例として挙がっているのは「AI が推測できない Bash コマンド」で、**書けば埋まる**前提で線が引かれています。仮説が公式にあり、反証が自分の前作にある。今回はそれを測りに行きます。

まず一つだけ、探してみてください。あなたの指示ファイルの中で、**標準のコマンドの代わりに自分たちの入口を使え、と書いている行**です。「テストは `scripts/test.sh` で」「デプロイは `bin/deploy` で」。⭐ 見つかったら、その行が**実際に何回守られたか**を思い出してみてください。**思い出せないはずです。** どちらで走らせても、出来上がるものは同じだからです。

## 「置いてある」と「呼ばれる」は別の話でした

答えの分かる題材を作りました。`logs/` にログを模したファイルを 12 本置き、各ファイルから 1 つの値を拾って `answers.tsv` にまとめる、というだけの作業です。行頭が `RESULT: ` のものだけが対象で、コメント行の中や引用の中の同じ文字列は囮、有効な行が 1 つも無いファイルもあります。

そこに、このプロジェクトのラッパーを 1 本置きます。

```sh
#!/bin/sh
# このプロジェクトで logs/ の中を見るときの入口。
# 出力は <行番号>:<行> の形。
exec grep -n 'RESULT:' "$@"
```

⚠️⚠️ **中身は `grep -n` そのもので、出力も 1 バイト違いません。** これは意図的です。ラッパーのほうが賢いと、「指示に従ったか」と「課題ができたか」が同じ数字に化けます。使っても得をしない道具にしておくと、**残るのは「どちらの言葉を選んだか」だけ**になります。

`bin/README.txt` にも 1 行の説明を置きました。**道具は 3 条件とも同じ場所にあり、同じ説明が付いています。** 変えたのは指示ファイルの「進め方」の節だけです。

| 条件 | 「進め方」に書いたもの |
|---|---|
| **書かない** | ⚠️ 節そのものを置かない（道具は置いてあり、説明も読める） |
| **1 行だけ** | 「ログの中身を見るときは、`./bin/scan <ファイル>` を使ってください」 |
| **強く書く** | ⭐ 前 4 作が「効く」と書いた書き方を全部使った版（後述） |

12 体に渡しました。課題の答えは **12 体とも 12/12**、囮も **2/2**、離れた 3 か所に置いた陽性対照も **12/12** です。差が出たのは、そこではありませんでした。

| 条件 | ラッパーを **1 度でも**呼んだ | **最後まで**素のコマンドに戻らなかった |
|---|---|---|
| 書かない | **0/4** | 0/4 |
| 1 行だけ | **4/4** | **1/4** |
| 強く書く | **4/4** | **3/4** |

⚠️⚠️ **書かなかった 4 体は、`bin/` を一度も開いていません。** `ls` の結果に出ていて、説明ファイルも読める場所にあって、それでも 0 回です。道具が「ある」ことと、道具が「呼ばれる」ことのあいだには、何もつながりがありませんでした。

そして 1 行書くと、**4 体とも呼びます**。私が前作で書いた「1 行は標準コマンドに負ける」は、ここでは外れています。⭐ **書いた 2 条件の 8 体は、例外なく、まず `cat bin/scan` でラッパーの中身を読んでから使いました。** 知らない言葉は、無視されるのではなく、確かめられていました。

負けるのはその後です。1 行だけの条件では、**4 体のうち 3 体が途中で素のコマンドに戻りました**。

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

> **ラッパーは、指示ファイルに書けば呼ばれる。呼ばれ続けるかを決めるのは指示の強さではなく、素の道具に戻る用事が残っているかどうかだ。**

## 測り方: 同じ道具を、指示の強さだけ変えて 3 通り置く

軸は 1 本、指示の強さだけです。「強く書く」の版には、前 4 作が「効く」と書いた書き方を全部入れました。

| 使った技術 | 出典 | 入れたもの |
|---|---|---|
| してほしい形で書く | 3 作目 [第 8 回](https://typing-tube.net/articles/iwanai-08-no-criticism) | 「素の検索コマンドを打つな」ではなく「`./bin/scan` を使ってください」 |
| 理由を書く | 2 作目 [第 2 回](https://typing-tube.net/articles/kikanai-02-reason) | 「打つ人によって書き方が変わるので、同じ範囲を見たかを後から突き合わせられません」 |
| 範囲を割る | 2 作目 [第 3 回](https://typing-tube.net/articles/kikanai-03-scope) | 「`logs/` の下を見るときです。`NOTES.md` や `answers.tsv` には要りません」 |
| 肯定的な実例を出す | 公式の書式の指針 | `./bin/scan logs/log_01.txt` を 2 行 |

⚠️ **理由には、この題材の中で本当のことだけを書きました。** 「ラッパーのほうが正確です」のような利点を書くと、追従の理由が「指示に従った」ではなく「そのほうが正しいと判断した」に化けます。

⚠️ 揃えたものは機械で確かめています。課題の仕様、測定対象 3 点の文面、**指示ファイルの総行数（3 条件とも 216 行）**、`bin/` の中身、`logs/` の中身。**測定対象のうち 2 つは行番号まで同じ**にしました（削るのは前半の埋め草だけなので、ずれるのは先頭の 1 点だけです）。長さと位置は前回の実験で軸として否定済みなので、その 1 点のずれは差の説明になりません。

**ラッパーを呼んだかどうかは、成果物に残りません。** `answers.tsv` は、どちらの言葉で読んでも同じものが出ます。数えるのは作業者の記録のほうです。

```bash
# 作業者の記録から「ラッパーを通したか / 素の道具で見たか」を数える（手元の実装からの簡約版）
python3 - "$1" <<'PY'
import json, re, sys
SCAN = re.compile(r"(^|[;|&\n]|\bdo\s)(\s*(?:\./)?bin/scan\b[^;|&\n]*)")
RAW  = re.compile(r"(?<![\w./-])(grep|cat|head|tail|sed|awk)(?![\w-])")
scan = raw = 0
for line in open(sys.argv[1]):
    if "logs" not in line and "log_" not in line:
        continue
    if SCAN.search(line):
        scan += 1
    # ⚠️ ラッパーの呼び出しを消してから素の道具を探す（同じ 1 行に両方が入る）
    rest = SCAN.sub(r"\1 ", line)
    if any(RAW.search(seg) and ("logs" in seg or "$" in seg) for seg in re.split(r"[;|\n]+|&&", rest)):
        raw += 1
print(f"ラッパー {scan} 回 / 素の道具 {raw} 回")
PY
```

⚠️⚠️ **この「消してから探す」の 1 行が無いと、数は全部ずれます。** 私は最初これを書かず、`ls logs && cat bin/scan`（ラッパーの中身を読んだだけ）を「素のコマンドで logs を見た」に数えていました。1 回の呼び出しに複数のコマンドが入る形は、まとめて見ると必ず化けます。

## 書き直さなくなったもの

守られなかったラッパーの、その 1 行です。以前は、素のコマンドが打たれているのを見つけると 1 行のほうを直していました。位置を前に出す。理由を足す。禁止を太字にする。今回の実測では、**そこまで全部やった版でも 3/4** でした。1 行だけの版の 1/4 と比べれば上がっていますが、⚠️ **母数 4 では偶然と区別できません**し、何より 4/4 にはなっていません。

いまは 1 行を直す代わりに、**素の道具に戻る用事が残っていないか**を見ます。戻った 3 体が何をしたかは、記録に出ていました。

| 条件 | 戻る直前まで | 戻ったあとに打ったもの |
|---|---|---|
| 1 行だけ | `./bin/scan` で 12 本を通した | `cat -A` / `sed -n 'l'`（行頭の空白を見るため） |
| 強く書く | `./bin/scan` で 12 本を通した | `sed -n '2p' \| cat -A`（囮の 1 行を確かめるため） |

ラッパーは `RESULT:` を含む行を出しますが、**行頭が `RESULT: ` かどうかまでは見せません**。囮を仕分けるにはその情報が要ります。⭐ **足りないことに気づいた瞬間に素の道具へ戻り、戻ったきり帰ってきませんでした。**

1 作目 第 1 回で私が出した答えは「標準コマンドの側を塞ぐ」でした。それは今も正しいと思っています。⭐ ただし、塞ぐ前に見るところが 1 つ増えました。**塞ぐと、戻る用事のある仕事が行き場を失います。** ラッパーが仕事の一部しかできないまま入口を塞ぐと、AI は前回の記事と同じことをします —— 同じ成果物を、通れる別のやり方で作るだけです。

## 注意: これは「1 行が効いた」の証明ではありません

⚠️⚠️ **読み方が 2 つ残ります。** **素に戻った理由は、ラッパーが仕事の一部しかできなかったこと**でした。つまり「指示より事前分布が勝った」のか「道具が足りなかったから合理的に別の道具を使った」のかを、この設計は分けていません。⭐ 分けるには、囮の判定までラッパーにやらせた条件が要ります。ただしその版は課題を易しくするので、**追従と正答が同じ数字に化けます**。どちらかを捨てるしかなく、今回は「使っても得をしない」を優先しました。

⚠️ **課題の答えは 12 体とも満点です。** 3 回目まで続いていた天井が、4 回目でも破れていません。難度が張り付いている実験は、差が出ても出なくても読み方が狭くなります。

⚠️ 母数は条件あたり 4 です。**0/4 と 4/4 の差は大きく見えますが、1/4 と 3/4 の差は偶然と区別できません。** この記事で強く言えるのは、前者 —— **書かなければ 1 度も呼ばれない**、のほうだけです。

⚠️ そして、この実験は「どちらが良いか」を測っていません。素のコマンドで読んだ体も、ラッパーで読んだ体も、**答えは全員満点**でした。ラッパーを使わせたいのは、答えのためではなく、答え以外の何か（記録・並走の防止・環境の統一）のためのはずです。⭐ **その「何か」が成果物に残らないなら、それは前回の話に戻ります。**

## 実験手順: 同じ道具を、指示の強さだけ変えて渡す

**前提**

- 会話を引き継がない作業者を 12 体、**条件を混ぜた波で同時に**起動できること（逐次にすると、その日の混み具合が条件差に化けます）
- 標準コマンドと**出力が同じ**ラッパーを 1 本用意できること
- 作業者の記録を後から読めること。⚠️⚠️ **打ったコマンド行がそのまま残っている**必要があります —— `./bin/scan logs/log_01.txt` と `grep -n 'RESULT:' logs/log_01.txt` を見分けるためです。⚠️ ファイルを読む道具を使った回は、読んだファイル名まで残っていること

**所要時間**: 25 分（題材の生成器と分類器を自分で書く場合。すでにあるなら 5 分）

**手順**

1. **答えの分かる題材を機械で作ります。** 12 本のファイルと、その正解表を同時に出力してください。⚠️ **正解表は、作業者に渡すディレクトリの外**へ置きます
2. **ラッパーを 1 本置きます。** ⚠️⚠️ **標準コマンドと出力が 1 バイトも違わないもの**にしてください。賢いラッパーにすると、追従と正答が同じ数字になります
3. **説明ファイルを、道具の隣に置きます。** ⚠️ 3 条件とも同じものを置く（無いと「指示の効果」ではなく「存在を知らなかった」を測ることになります）
4. **同じ指示ファイルから、条件を 3 つ作ります。** 書かない / 1 行だけ / 前作の技術を全部使う。⚠️ **総行数を揃える**（埋め草で調整し、ハッシュではなく行数を印字して照合してください）
5. **3 条件を混ぜた波で起動します。** ⚠️ 起動時のプロンプトにラッパーのことを書かないでください
6. **課題の答えを採点します。** ⚠️ ここにラッパーの話は出ません。出ないことが論点です
7. **作業者の記録から 2 つ数えます。** ラッパーを通した呼び出しと、素の道具で対象を見た呼び出し。⚠️⚠️ **ラッパーの呼び出しを消してから素の道具を探す**（同じ 1 回に両方が入ります）

**合格条件**（すべて満たすこと）

- 課題の答えが、**3 条件とも満点**（= 難度の差が追従の差に化けていない）
- 陽性対照（指示ファイルの離れた 3 か所に置いた、手数 1 回の指示）が **3 条件とも全員正しい**
- **「書かない」条件で、ラッパーの呼び出しが 0 回**（= 道具の存在だけでは呼ばれない、が取れている）
- **「1 行だけ」条件で、素のコマンドに戻る作業者が 1 体以上出る**

**合格しなかったとき**

- **「書かない」条件でもラッパーが呼ばれた** —— 説明ファイルが目立ちすぎています。⚠️ 道具の名前を、課題の語から離してください
- **どの条件でもラッパーが呼ばれない** —— ラッパーが標準コマンドより不便です。⚠️ 引数の形を標準コマンドに揃えてください
- **3 条件とも全員がラッパーだけで通した** —— 素の道具に戻る用事がありません。⚠️ ラッパーが見せない情報を、課題の判定に 1 つ入れてください
- **課題の答えが満点でない** —— 難度が高すぎます。⚠️ この状態では、追従の差と難度の差が同じ数字に化けます

**後始末**

- 生成した題材・作業者の記録・正解表を削除します。⚠️ **正解表も一緒に削除する** —— 残すと、次に題材を作り直したつもりのまま、古い答えを採点することになります

道具を置くだけでは呼ばれず、1 行書けば呼ばれ、強く書いても通し切れない。⚠️ **この 3 つのうち、指示ファイルで動かせたのは最初の 1 段だけでした。**

---

次回は「集めない技術」。**私は、AI に毎回同じ答えを言わせるのをやめました。**それでも、結論はいつも同じところに落ち着きます。
