TL;DR
- AI 利用率・生成行数・生成トークン量を KPI に置く運用を、ここでは tokenmaxxing と呼びます。問題は「質と相関しないこと」だと言われがちですが、原因はもっと手前にありました。
- 生成量 KPI は 2 種類に分かれます。 生成行数・トークン量・資産数のような累積カウンタ型は定義上単調で、悪化を表現できる値域を持ちません。一方、利用率のような比率型は分母があるので下がりえます。危ないのは前者を成果指標に置いたときで、達成が観測と無関係に決まります。
- その傾向を 1 リポジトリで確かめました。 ディレクトリごとの月次「追加行 − 削除行」を集計したところ、判定を確定できた 15 対象のうち 12 が、一度も純減した月を持っていません(
社内データ)。純減が一度でもあったのは.github/reports/README.mdの 3 つだけです。残る 8 対象は観測月が 1〜2 か月しかなく、構造上そもそも純減を示す機会がないので判定を保留しました。これは累積カウンタ型の性質を示す傍証であって、証明ではありません。 - 「ガードを何本作ったか」も生成量指標です。ただし当初の仮説(本数と検知能力が相関しない)は、実測すると外れました。登録済みガード 12 本は全数が違反入力で落ちるテストを持っており(各テストの異常系を目視確認)、「作っただけ」ではありません。それでも失敗台帳 14 件のうち 11 件は
origin: harness、つまりガード自身の欠陥で、記事を検査するガードの守備範囲の外でした(社内データ)。相関しないのではなく、測っている対象が違ったのです。 - 検証済み成果の側を比率で測るときは、分母を先に固定しないと桁が変わります。 公開判断を記録した
gate.mdを持つ記事は 221 本中 8 本(3.6%)ですが、その契約が導入されて以降に追加した記事に限れば 9 本中 8 本(89%)です。同じ事実で 2 桁違います。 - 第 1 節に、自分のリポジトリで「単調増加している対象」を洗い出す 106 行のシェルスクリプトを置きました。終了コードは 0(純減あり・保留も skip もなし)/ 1(単調増加を検出)/ 2(設定ミス・検査不能)に分かれ、対象 0 件も、一部のパスだけ空振りした場合も、観測月が足りず判定を保留した場合も、合格と同じ形では返しません。 shallow clone も
exit 2で止めます(月次集計が創作になるため)。 - ただしこのスクリプトが見ているのは git の追加行と削除行だけです。内容の良し悪しも、検証済みかどうかも測りません。第 6 節にこの測り方で解けないことを並べました。
はじめに:KPI を決める前に、その指標が下がりうるかを確かめる
AI コーディングを本格導入すると、まず数字が欲しくなります。導入効果を説明する必要があるからです。手元にある数字はだいたい次のようなものです。
| 指標 | 型 |
|---|---|
| AI が生成したコード行数、生成トークン量 | 累積カウンタ |
| AI 経由で作成した PR 数(累計) | 累積カウンタ |
| 蓄積したプロンプト資産・スキル・エージェント定義の数 | 累積カウンタ |
| AI ツールの週次アクティブ利用率 | 比率(分母 = 対象者数) |
どれも取得が簡単で、増えると気持ちがいい数字です。そしてどれも生成量の指標です。ただし性質は同じではありません。上 3 つは累積カウンタで、値域に「減る」が入っていません。一番下の利用率は分母を持つので下がりえます。
この記事が危ないと言うのは、累積カウンタ型を成果指標に置いたときです。 利用率にも問題はありますが、それは別の理由(成果を検証しなくても上がる)で、後述の FAQ で分けて扱います。
このリポジトリでも同じことをしていました。記事は 221 本(6a59050 時点)になり、エージェント定義とスキルは .agents/ 配下に積み上がり、品質ガードのスクリプトも増えました。「AI で記事制作を回す仕組みが育っている」と説明できる状態です。
なお、このリポジトリは非公開です。 以下に出てくる commit SHA は記録の固定用で、読者が直接たどれるものではありません。数値はすべてコマンドを併記しているので、手元のリポジトリで同じコマンドを走らせて確かめる形で読んでください。
ただ、直近の作業でこの説明が怪しくなる観測が続きました。Harness Engineering にも技術的負債がある では、エージェント資産が積み上がる一方で削られていないことが出ました。AIの推論を自動化へ昇格させるループの穴 では、実装済みの判定器が 8 種中 6 種で沈黙していたことが出ました(稼働初日の時点では 5 種で、その日のうちに 6 種になっています)。どちらも「作った数」は増え続けていたのに、です。
そこで、生成量の指標と検証済み成果の指標を、同じリポジトリで並べて測ってみます。 この記事は「組織はこう測るべきだ」という提言ではありません。1 リポジトリで実際に測った結果と、測ろうとして分かった落とし穴の記録です。
KPI フレームそのものの選び方は既に別記事で扱っています。DORA や SPACE のような既存フレームは、いずれも本記事が問題にしている累積カウンタ型を成果指標に据えていません(DORA は変更失敗率やリードタイム、SPACE は満足度を含む多次元)。この 3 つの使い分けは DORA vs SPACE vs DevEx、速度・品質・学習の 3 軸設計は AI開発KPIと学習ループ、フェーズごとの定規の選び方は 開発生産性指標の歩き方 にあります。本記事はその手前、「置こうとしている指標が、そもそも下がりうるのか」を実測で確かめる話に絞ります。
1. 累積カウンタ型は実際に単調増加していた(傍証)
累積カウンタ型が定義上単調なのは、値域を見れば分かります。ここで測りたいのは別のことです。**「実際の運用でも減っていないのか」**を、手元で確かめられる形にします。
代理として、ディレクトリごとに月ごとの「追加行 − 削除行」を出し、純減した月が一度でもあるかを見ます。純減が一度も無いなら、その対象の累積行数は単調増加です。行数は「資産数」そのものではありませんが、.agents/ のようにファイル追加が資産追加とほぼ一致する領域では代理になります。
これは証明ではなく傍証です。 1 リポジトリの結果であり、若いリポジトリなら単調でも不思議はありません(第 2 節冒頭・第 6 節)。
測るスクリプト
任意の git リポジトリで動きます。monotonic_kpi_probe.sh として保存してください。
#!/usr/bin/env bash
# monotonic_kpi_probe.sh
# 指定パス配下の「月ごとの追加行 - 削除行」を集計し、純減した月が一度もない
# = 生成量が単調増加している対象を検出する。
#
# 見ているのは行数の増減だけで、内容の良し悪しも検証済みかどうかも測らない。
#
# 完全な履歴が要る。shallow clone では月次の集計が創作になるので実行しない。
#
# 終了コード:
# 0 = 正常(判定したすべての対象に純減した月があり、skip / 判定保留も 0 件)
# 1 = 検知(純減が一度もない対象があった)
# 2 = 設定ミス・検査不能(引数なし / git 不在 / リポジトリ外 / shallow clone /
# 判定できた対象 0 件 / 判定できなかった対象や判定を保留した対象が残ったまま
# 「正常」になりそうなとき)
#
# 1 と 2 が同時に成り立つときは 1 を返す。どちらも非 0 なので、
# 検査できなかった対象があるのに 0 で通ることはない。
set -euo pipefail
# 変更のあった月がこれ以下の対象は判定を保留する。
# 観測月が 1 か月なら純減月を持ちようがなく、「純減 0」は測定結果ではなく
# 母数の少なさから決まってしまうため。2 という値に統計的な根拠は無い(目安)。
LOW_N_MONTHS=2
if [ "$#" -eq 0 ]; then
echo "usage: $0 <path> [<path> ...]" >&2
echo " 例: $0 .agents scripts content" >&2
exit 2
fi
command -v git >/dev/null 2>&1 || { echo "設定ミス: git が見つかりません" >&2; exit 2; }
command -v awk >/dev/null 2>&1 || { echo "設定ミス: awk が見つかりません" >&2; exit 2; }
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
echo "設定ミス: git リポジトリの中で実行してください" >&2; exit 2; }
# shallow clone だと、切り捨てられた履歴の内容が接ぎ木コミットの月へ丸ごと計上され、
# 実在しない純減月が出たり本当の純減月が消えたりする。両方向に誤るので実行しない。
# `--is-shallow-repository` は git 2.15 以降。それ未満は shallow ファイルの有無で見る。
if [ "$(git rev-parse --is-shallow-repository 2>/dev/null)" = "true" ] ||
[ -f "$(git rev-parse --git-dir)/shallow" ]; then
echo "設定ミス: shallow clone です。月次集計が実際の履歴と一致しません" >&2
echo " git fetch --unshallow するか、CI なら fetch-depth: 0 で取得してください" >&2
exit 2
fi
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
checked=0 # 月次データを取得できた対象の数
monotonic=0 # そのうち純減した月が 0 件だった対象の数
lown=0 # 観測月が少なく判定を保留した対象の数
skipped=0 # 履歴を取れず判定できなかった対象の数
for target in "$@"; do
raw="$WORK/raw"
if ! git log --numstat --pretty=format:'C%ad' --date=format:'%Y-%m' -- "$target" > "$raw" 2>"$WORK/err"; then
echo "SKIP $target: git log が失敗しました($(tr -d '\n' < "$WORK/err"))。判定していません" >&2
skipped=$((skipped + 1))
continue
fi
months="$WORK/months"
# numstat はタブ区切り。既定 FS(空白)で割ると、空白やリネーム表記 `{a => b}` を
# 含むパスの行が NF > 3 になって黙って集計から落ちる。
awk -F'\t' '/^C/ { m = substr($0, 2); next }
NF == 3 && $1 != "-" { add[m] += $1; del[m] += $2 }
END { for (k in add) printf "%s %d %d\n", k, add[k], del[k] }' "$raw" | sort > "$months"
n=$(awk 'END { print NR + 0 }' "$months")
if [ "$n" -eq 0 ]; then
echo "SKIP $target: 行数を数えられるコミットが 0 件でした。「単調増加していない」ではありません" >&2
skipped=$((skipped + 1))
continue
fi
checked=$((checked + 1))
neg=$(awk '{ if ($2 - $3 < 0) c++ } END { print c + 0 }' "$months")
echo "--- $target : 変更のあった月 ${n} / 純減した月 ${neg}"
awk '{ printf " %s add=%-7d del=%-7d net=%+d\n", $1, $2, $3, $2 - $3 }' "$months"
if [ "$n" -le "$LOW_N_MONTHS" ]; then
echo " LOW-N: 変更のあった月が ${n} か月しかなく、純減を示す機会がありません。判定を保留します"
lown=$((lown + 1))
elif [ "$neg" -eq 0 ]; then
echo " NG: 一度も純減していません。この対象を KPI に置くと達成が保証されます"
monotonic=$((monotonic + 1))
else
echo " OK: 純減した月が ${neg} 件あります"
fi
done
echo
if [ "$checked" -eq 0 ]; then
echo "判定できた対象: 0 件(skip ${skipped} 件)。これは「合格」ではありません。パス指定を見直してください" >&2
exit 2
fi
echo "集計: 月次を取れた対象 ${checked} 件 / うち判定確定 $((checked - lown)) 件(単調増加 ${monotonic} 件)/ LOW-N 保留 ${lown} 件 / skip ${skipped} 件"
if [ "$monotonic" -gt 0 ]; then
exit 1
fi
if [ "$skipped" -gt 0 ] || [ "$lown" -gt 0 ]; then
echo "判定できなかった対象が $((skipped + lown)) 件あります(skip ${skipped} / LOW-N ${lown})。残りが正常でも「正常」とは報告しません" >&2
exit 2
fi
exit 0
設計上、気をつけた点を先に書きます。読者の任意のリポジトリで走るスクリプトなので、「何も検査できなかった」を「合格」と同じ形で返さないことを優先しました。
| 起きうる事故 | この実装での扱い |
|---|---|
| 指定パスが履歴に存在せず 0 件 | SKIP を stderr に出し、判定した対象が 0 件なら exit 2 |
| 行数を数えられないパス(バイナリのみ等)を指定した | 同上。public/ を渡すと実際に skip されます |
| 複数パス指定で 1 つだけ空振り | 対象ごとに個別判定して集計し、残りが正常でも exit 0 にしない(exit 2) |
| リネーム表記や空白を含むパスの行 | awk -F'\t' で明示的にタブ区切りにする |
| 履歴が切り詰められている(shallow clone) | 前提チェックで検出して exit 2。集計しない |
| 観測月が少なく、純減する機会が無い | LOW-N として判定を保留し、単調増加にも正常にも数えない |
git log 自体が失敗 | SKIP として checked に数えない(「純減あり」に丸めない) |
| 一時ファイルの衝突 | mktemp -d + trap で毎回破棄 |
| 終了コードの混線 | 設定ミス・検査不能 = 2 / 検知 = 1 / 正常 = 0 に分離 |
3〜6 行目は最初の版に無く、独立レビューで 1 つずつ見つかりました。 4 つとも「未検査・検査不能を、検査済みと同じ形で返す」型です。
- 初版は「判定できた対象が 1 つでもあれば
exit 2にしない」実装で、./monotonic_kpi_probe.sh .github typo-path-hereのようにパスをタイプミスすると、SKIPは stderr に出るのに 終了コードは 0 でした。呼び出し側がif ./monotonic_kpi_probe.sh ...; thenと書いていれば、タイプミスは沈黙して合格になります。 - 初版の
awkは既定の区切り(空白)を使っていました。git log --numstatはタブ区切りで、パスに空白やリネーム表記{旧 => 新}が入るとNFが 3 を超え、その行がSKIPにも集計にも現れないまま消えていました。このリポジトリのcontent/posts配下にも、記事を単一ファイルからディレクトリへ移したときのリネーム行が実際に 2 行あり、合計 5 行の追加と 2 行の削除が集計から落ちていました。 - shallow clone の検査は前提チェックに一切ありませんでした。後述のとおり符号が反転した月次テーブルを、警告なしの確定判定として返していました。
LOW-Nの分離も無く、観測月 1 か月の対象を「一度も純減していません」と断定して単調増加に数えていました。純減する機会が無い対象について「純減 0」と言っても、測定ではありません。
4 つとも本記事が第 3〜4 節で扱っているのと同じ形です。自分で書いた 100 行のスクリプトで 4 回やっていました。 記事の主張が正しいかどうかとは別に、この型がどれだけ踏まれやすいかの証拠にはなっています。
このリポジトリでの実行結果
トップレベルの主要な対象をまとめて渡しました。
$ ./monotonic_kpi_probe.sh .agents .claude .github content doc docs drafts plans \
queue references reports reviews scripts spec specs src tests short-videos \
AGENTS.md README.md TODO.md AGENT_LEARNINGS.md package.json public
--- .agents : 変更のあった月 7 / 純減した月 0
2026-02 add=26144 del=432 net=+25712
2026-03 add=5059 del=1243 net=+3816
2026-04 add=939 del=43 net=+896
2026-05 add=1113 del=46 net=+1067
2026-06 add=407 del=129 net=+278
2026-07 add=164 del=0 net=+164
2026-09 add=1504 del=345 net=+1159
NG: 一度も純減していません。この対象を KPI に置くと達成が保証されます
...(対象ごとの月次行は省略)
--- .github : 変更のあった月 6 / 純減した月 1
OK: 純減した月が 1 件あります
--- drafts : 変更のあった月 1 / 純減した月 0
LOW-N: 変更のあった月が 1 か月しかなく、純減を示す機会がありません。判定を保留します
--- reports : 変更のあった月 5 / 純減した月 1
OK: 純減した月が 1 件あります
--- README.md : 変更のあった月 6 / 純減した月 1
OK: 純減した月が 1 件あります
SKIP public: 行数を数えられるコミットが 0 件でした。「単調増加していない」ではありません
集計: 月次を取れた対象 23 件 / うち判定確定 15 件(単調増加 12 件)/ LOW-N 保留 8 件 / skip 1 件
$ echo $?
1
判定を確定できた 15 対象のうち 12 が、一度も純減した月を持ちません(社内データ)。記事本体(content)、エージェント資産(.agents)、スクリプト(scripts)、テスト(tests)、仕様(spec)、いずれも純減ゼロです。
純減が一度でもあったのは 3 つだけでした。.github は 2026-09 に add=93 / del=151 で net=-58、reports と README.md も 1 か月ずつ。いずれもワークフローの整理や生成物の再生成で減った月です。
残る 8 対象(drafts / references / specs / short-videos / docs / plans / reviews / TODO.md)は LOW-N で保留しました。変更のあった月が 1〜2 か月しかない対象は、純減月を持ちようがありません。 ここで「純減 0 だから単調増加」と数えると、測定結果ではなく母数の少なさから決まった値を、指標として出すことになります。第 3 節で捨てた「収録基準から 0 が決まる数字」と同じ形です。初版のスクリプトは実際にこの 8 件を単調増加に数えていて(当時の表示は 23 中 20)、レビューで指摘されて分離しました。
public が skip されている理由は、当初「.gitignore 配下だから」と書いていました。これは間違いでした。 実測すると public/ は force-add で 620 ファイルが追跡されており、履歴も 40 コミットあります。
git ls-files public | wc -l # => 620
git log --numstat --format= -- public | wc -l # => 742(履歴行は存在する)
git log --numstat --format= -- public | grep -cv '^-' || true # => 0(行数を数えられる行は 0)
真因は .gitignore ではなく、numstat が全行バイナリ(- / -)を返すことです。スクリプトの NF == 3 && $1 != "-" がこれを落としています。反例もあります。doc/Working は .gitignore 配下ですが、追跡済みのテキストがあるので skip されず月次が出ます(観測月が 2 か月なので判定は LOW-N で保留になります)。
つまり skip される条件は「.gitignore に入っているか」ではなく、「履歴に一度もコミットされていない」か「numstat が行数を返さない」のどちらかです。ここを「純減が無い」と数えてしまうと、検査できなかったものを結果に混ぜることになります。SKIP を stderr に出し、checked に数えないのはそのためです。
「常に落ちるスクリプト」ではないことの確認
指摘条件が非対称だと、最悪ケースで鳴らない、あるいは常に鳴るスクリプトになります。両方向を確かめました。
$ ./monotonic_kpi_probe.sh .github ; echo "exit=$?"
--- .github : 変更のあった月 6 / 純減した月 1
2025-08 add=95 del=5 net=+90
2025-09 add=455 del=101 net=+354
2026-02 add=435 del=263 net=+172
2026-03 add=327 del=59 net=+268
2026-04 add=13 del=1 net=+12
2026-09 add=93 del=151 net=-58
OK: 純減した月が 1 件あります
集計: 月次を取れた対象 1 件 / うち判定確定 1 件(単調増加 0 件)/ LOW-N 保留 0 件 / skip 0 件
exit=0
$ ./monotonic_kpi_probe.sh no/such/path ; echo "exit=$?"
SKIP no/such/path: 行数を数えられるコミットが 0 件でした。「単調増加していない」ではありません
判定できた対象: 0 件(skip 1 件)。これは「合格」ではありません。パス指定を見直してください
exit=2
$ ./monotonic_kpi_probe.sh ; echo "exit=$?"
usage: ./monotonic_kpi_probe.sh <path> [<path> ...]
例: ./monotonic_kpi_probe.sh .agents scripts content
exit=2
$ ./monotonic_kpi_probe.sh .github typo-path-here ; echo "exit=$?"
--- .github : 変更のあった月 6 / 純減した月 1
...(月次行は省略)
OK: 純減した月が 1 件あります
SKIP typo-path-here: 行数を数えられるコミットが 0 件でした。「単調増加していない」ではありません
集計: 月次を取れた対象 1 件 / うち判定確定 1 件(単調増加 0 件)/ LOW-N 保留 0 件 / skip 1 件
判定できなかった対象が 1 件あります(skip 1 / LOW-N 0)。残りが正常でも「正常」とは報告しません
exit=2
$ git clone --depth 2 file://$PWD /tmp/shallow-demo && cd /tmp/shallow-demo
$ ./monotonic_kpi_probe.sh .agents ; echo "exit=$?"
設定ミス: shallow clone です。月次集計が実際の履歴と一致しません
git fetch --unshallow するか、CI なら fetch-depth: 0 で取得してください
exit=2
exit 0 / exit 1 / exit 2 のすべてに到達する入力を確認しました(社内データ)。なお exit 1(検知)と exit 2(検査不能)が同時に成り立つときは 1 を返します。どちらも非 0 なので、検査できなかった対象があるのに 0 で通ることはありません。
最後の shallow clone の分岐は、レビューで見つかった 3 つ目の空振りです。actions/checkout は既定が fetch-depth: 1 なので、CI に置いた読者が最初に踏みます。 前提チェックを足す前に検証用リポジトリで測ったところ、こうなっていました。
| 取得方法 | 出力 | 終了コード |
|---|---|---|
| 全履歴 | 2026-03 net=-100 を含む 4 か月。OK: 純減した月が 1 件 | 0 |
--depth 1 | 1 か月だけ。net=+500(接ぎ木コミットの全ツリー)。NG | 1 |
--depth 2 | 2026-03 が net=+400 と表示(真値は -100)。NG | 1 |
符号が反転しています。 切り捨てられた履歴の内容が接ぎ木コミットの月へ丸ごと計上されるため、実在しない純増月が生まれ、本当の純減月が消えます。しかも skipped は 0 件で SKIP 行も出ないので、「検査できた」形で確定判定が返ります。 月次テーブルそのものが創作でした。--is-shallow-repository を見るだけで塞げます。
2. なぜ累積カウンタ型が問題なのか
「増え続けている」こと自体は悪ではありません。 育てている最中のリポジトリなら当然です。第 1 節の 12 / 15 も、まずは「このリポジトリがまだ若い」という説明がつきます。問題は、それを KPI に置いたときに起きます。
累積カウンタ型の指標には、次の性質があります。
| 性質 | 帰結 | 比率型(利用率など)では |
|---|---|---|
| 悪化を表現できる値域が無い | 目標未達が原則として発生しない | 該当しない(下がりうる) |
| 達成が観測ではなく作業量で決まる | 何も検証しなくても数字は伸びる | 該当する(使えば上がる) |
| 減らす行為が指標上はマイナスに見える | 不要になった資産を削除する動機が消える | 該当しない |
| 分子だけで完結する | 「何本のうち何本か」を問われない | 該当しない(分母がある) |
比率型と共有しているのは 2 行目だけです。累積カウンタ型は 4 つすべてを満たすので、成果指標としての壊れ方が一段深くなります。
3 番目が特に効きます。.agents/ が変更のあった 7 か月すべてで純増しているのは(連続する 7 か月ではなく、変更のあった月が 7 つという意味です)、削除が指標上マイナスに見えるからでもあります。使われなくなったスキル定義を消すと「資産数」は減ります。消さずに置いておけば増え続けます。どちらが健全かは中身を見ないと分かりませんが、指標は中身を見ません。
指標を目標にした瞬間に指標として壊れる、というのは Goodhart の法則としてよく知られた話です。ただし累積カウンタ型の場合、壊れ方はもう少し単純です。測定行為の前に達成が決まっているので、そもそも測定が判断材料になりません。
開発者生産性の測定をめぐる議論でも、同じ形の批判が繰り返されています。McKinsey が開発者生産性は測定できると主張したのに対し、Kent Beck らはアウトプット側の代理指標を積み上げても成果には接続しないと反論しました。AI 導入後の KPI で起きているのは、この議論の代理指標がさらに測りやすい方向へずれた状態です。行数よりトークン量のほうが取得が簡単で、そのぶん成果から遠い。
なお、アウトプットが増えても価値が伸びない構造そのものは AIコード増でも価値が伸びない理由 で扱っています。本記事はその現象を指標の性質側から見ています。
3. 「作った数」は、検出実績とは別のものを数えている
もうひとつの生成量指標を測ります。品質ガードの本数です。
このリポジトリには品質ガードの契約(spec/guard_self_check.md)があり、対象ガードの一覧表(R-2)に 12 本が登録されています。表の行数は 13 ですが、うち 1 行は「R-6(未検査を PASS に丸めない)」という規約への参照で、スクリプトではありません。当初の仮説はこうでした。
「ガードを N 本作った」という数は、実際の検知能力とまったく相関しないはずだ。
直近の観測がこの仮説を支持しているように見えたからです。実装済みの昇格判定が 8 種中 6 種で沈黙していた、という ai-inference-promotion-loop の実測がありました。
実測したら、外れました。
外れた側:ガードは「作っただけ」ではなかった
R-2 の登録表に載っている 12 本を 1 本ずつ、テストファイルから参照されているかで確認しました。
# このリポジトリ専用(ガード名と tests/ のレイアウトを決め打ちしている)。
# 他のリポジトリでは、自分のガード一覧に置き換えないと空の結果が返る
for f in update_readmes.py validate_frontmatter.py audit_markdown_rules.py \
validate_skills.mjs audit_agent_indexes.mjs validate_claim_ledger.py \
check_risk_gate_drift.py check_article_working_artifacts.py \
audit_article_pipeline.py validate_code_references.py safety.sh \
check_failure_ledger.py; do
hits=$(grep -rl "$f" tests/ 2>/dev/null | tr '\n' ',' || true)
echo "$f -> [$hits]"
done
12 本すべてにテストがありました(社内データ)。空振りは 1 本もありません。
ただし、この grep が示すのは「テストファイル中にファイル名が出現する」ところまでです。正常系しか書かれていないテストでも、コメントや import 一覧に名前があるだけでもヒットします。「作った数」と「動く数」を区別しろと言っている節が、その区別の根拠に名前の一致を使うわけにはいきません。
そこで、各テストの異常系ケースを 1 本ずつ読んで確認しました(自動判定はしていません)。12 本のうち 10 本を覆う tests/test_guard_self_check.py は 1,038 行あり、ガードごとに正常入力で通ることと違反入力で落ちることの両方を検証しています(同契約の R-1「二面検証」)。validate_frontmatter.py だけで必須キー欠落・キー重複・frontmatter 不在・series のブロック配列など 10 ケースが並んでいます。残る 2 本(check_risk_gate_drift.py / check_failure_ledger.py)は tests/test_article_risk_gate.py と tests/test_article_failure_ledger.py が同じ形で覆っています。
つまり「本数を増やしただけで中身が無い」という当初の見立ては、このリポジトリには当てはまりませんでした。仮説を実データに合わせて捨てます。
当たった側:すり抜けた失敗の 11 / 14 はガードの守備範囲の外にあった
一方で、失敗の観測台帳(spec/article_failure_ledger.md)を見ると別の姿が出ます。
# このリポジトリ専用(台帳の Markdown 表レイアウトを決め打ちしている)
grep -c '^| L-' spec/article_failure_ledger.md
# => 14
awk -F'|' '/^\| L-/ { gsub(/ /, "", $8); print $8 }' spec/article_failure_ledger.md \
| sort | uniq -c
# 9 fixed
# 4 open
# 1 promoted
awk -F'|' '/^\| L-/ { gsub(/ /, "", $4); print $4 }' spec/article_failure_ledger.md \
| sort | uniq -c
# 2 D-12
# 1 D-6
# 11 harness
# evidence 列($7)だけを見る。行全体を grep すると notes 等に「CI」が混じって数が変わる
awk -F'|' '/^\| L-/ && $7 ~ /CI|Actions|workflow run/ { c++ } END { print c + 0 }' \
spec/article_failure_ledger.md
# => 0
効いているのは origin の内訳です。観測 14 件のうち 11 件が harness、つまりガード・契約・手順そのものの不具合でした(残りは記事制作フローの工程番号で、D-12=PR 作成が 2 件、D-6=独立レビューが 1 件)。
3 つ目の grep の結果(CI failure を根拠に挙げた行が 0 件)も出ますが、これは指標として使えません。 台帳の収録基準がそもそも「Harness をすり抜けた失敗」だからです(spec/article_retrospective_loop.md)。CI が公開前に捕まえた事象は、定義上ここに載りません。0 になることが収録基準から決まっている数字なので、ガードの検出能力については何も語っていません。反証もできません。
これは本記事が第 2 節で批判した「達成が観測と無関係に決まる数字」と同じ形です。分母の話をしているつもりで、より手前の選択バイアスを踏むところでした。以降の議論では使いません。
相関しないのではなく、対象が違った
この 2 つを並べると、結論が変わります。
| 数えたもの | 値 | 何を測っているか |
|---|---|---|
| 登録済みガード本数 | 12 | 作った数 |
| 違反入力で落ちることを目視確認したガード | 12 / 12 | 作ったものが動く数 |
| 台帳の観測件数 | 14 | すり抜けた失敗の数 |
うち origin: harness | 11 | すり抜けたうち、ガードの守備範囲の外だった数 |
| うち CI failure を根拠に挙げた行 | 0 | 何も測っていない(台帳の収録基準上、ここに CI failure が載ることはほぼない) |
ガードは正しく動いていました。ただしガードが検査しているのは「記事の違反」であって、「ガード自身の欠陥」ではありません。 すり抜けた 14 件のうち 11 件はガード・契約・手順そのものの不具合で、記事を検査するガードの守備範囲の外側にありました。
だから「本数と検知能力が相関しない」は言い過ぎで、正しくは 「本数はカバー範囲を教えてくれない」 です。12 本という数字からは、何を検査していて何を検査していないかが読めません。分母が無い数字だからです。
4. 検証済み成果の比率は、分母を決めるまで意味を持たない
では検証済み成果の側を比率で測ればいいのかというと、ここに別の罠がありました。
このリポジトリでは、記事の公開判断を記事ディレクトリ直下の gate.md に記録する契約があります(spec/article_risk_gate.md)。リスク判定、Gate-1(執筆に入ってよいかを決める企画段階のゲート)の可否、その理由が 1 ファイルに残ります。まさに「検証済み成果」を数えるための記録です。
数えてみます。作業ツリーではなく ref から読みます。未コミットの下書きで数字が動かないようにするためです。以下はこのリポジトリ固有のパスを決め打ちしているので、他のリポジトリではパスを読み替えてください。
trap を対話シェルに直接貼ると、あとで別の用途に WORK を使ったときにシェル終了時まで残って発火します。サブシェルで囲って閉じ込めます。
# このリポジトリ専用(記事が content/posts の下に index.md で置かれている前提)
(
BASE=6a59050 # 本記事を追加する前の origin/main
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
git ls-tree -r --name-only "$BASE" content/posts > "$WORK/tree.txt"
echo "記事: $(grep -c '/index\.md$' "$WORK/tree.txt" || true)"
echo "gate.md: $(grep -c '/gate\.md$' "$WORK/tree.txt" || true)"
)
# 記事: 221
# gate.md: 8
221 本中 8 本、3.6%。 数字だけ見ると壊滅的です。
ところが gate.md の契約が入ったのは 2026-09-06 です。それ以降に追加された記事だけを分母にすると、こうなります。ここで言う「追加」にはリネームで生まれたものも含めます(既存記事を単一ファイルからディレクトリへ移した場合など)。git 2.9 以降は diff.renames が既定で有効なので、--diff-filter=A だけだとリネーム由来の記事が R に分類されて数から落ちます。--since は既定で committer date を見るため、第 1 節の author date とは時計が違う点にも注意してください。
git log --since=2026-09-06 --diff-filter=AR --name-only --format= 6a59050 -- content/posts \
| { grep '/index\.md$' || true; } | sort -u | wc -l
# => 9
この基準コミットでは --diff-filter=A でも同じ 9 になります(当該期間に R が 0 件のため)。たまたま一致しているだけなので、定義に合うほうを書いています。
9 本中 8 本、89%。 残る 1 本は coding-agent-model-migration で、Gate-1 導入前に企画された記事である旨がコミット 097a70b で明記されています。
ただし 89% 側も手放しでは使えません。 契約導入が 2026-09-06、基準コミットが 09-09 なので、この分母は 3 日ぶん・n = 9 しかありません。1 本増減するだけで 10 ポイント以上動きます。分母を絞れば「制度の年齢」の影響は消えますが、代わりに標本が小さくなる、というトレードオフがそのまま出ています。
3.6% と 89% は同じ事実です。違うのは分母の定義だけです。
| 分母の取り方 | 値 | この数字が実際に測っているもの |
|---|---|---|
| 全記事 221 本 | 3.6% | 契約が導入されてからの経過時間 |
| 契約導入後の記事 9 本 | 89% | 契約の遵守率 |
前者を「検証済み成果の KPI」として報告すると、改善しようがない数字を追いかけることになります。 過去 213 本に遡って gate.md を書けば数字は上がりますが、それは判断の記録ではなく後付けの作文です。生成量 KPI を避けたつもりで、別の意味の無い数字を作っただけになります。
比率を検証済み成果の指標にするなら、分子の定義より先に分母の定義を書く必要がありました。具体的には「いつからの母集団か」「対象外をどう除外するか」の 2 つです。今回は後者も必要でした(導入前企画の 1 本)。
5. このリポジトリで実際に使っている 4 つの指標(記録)
以上を踏まえて、ここで実際に見ている指標を書き出します。組織一般への推奨ではなく、1 リポジトリの運用記録です。前提が違えば当然変わります。
| 指標 | 定義(分母つき) | なぜ生成量指標ではないか |
|---|---|---|
| Gate-1 記録率 | 契約導入日以降に追加した記事のうち gate.md を持つ本数 | 分母が固定されており、記録を省けば下がる |
| すり抜け件数 | 台帳の status: open 件数(6a59050 時点で 4 件) | 直せば減り、見つかれば増える。両方向に動く |
| 二面検証カバー率 | 登録済みガードのうち、違反入力で落ちることを目視確認した本数(6a59050 時点で 12 / 12) | 未検証のガードを足すと下がる |
| レビュー巡回数 | 公開までに独立 Checker を回した回数(6a59050 時点の 8 本で 1〜3 巡、多くは 2 巡) | 増えるのは手戻りの増加であって成果ではない |
共通しているのは 3 点です。
- 下がりうること。 悪化を表現できない指標は、悪化したことも教えません。
- 分母が固定されていること。 「いつからの母集団か」を先に決めます。
- 未検査を合格に数えないこと。 二面検証カバー率で「テストが無いガード」を分子から外すのはもちろんですが、ガードそのものを登録し忘れれば分母からも消えます。だから登録表(
spec/guard_self_check.mdの R-2)と実ファイルの突合が別途必要になります。
3 番目は自動化との相性が悪く、現状は人手です。ここは弱点として残っています。
レビュー巡回数について補足します。数え方を先に決めておかないと、この指標自体が第 4 節の罠にはまります。実際、初稿ではここに分母を書き忘れて数字がずれ、レビューで指摘されました。定義はこうです。
- 分母: 基準コミット
6a59050時点でgate.mdを持つ 8 本(本記事自身は含めない。進行中の値は確定していないため) - 1 巡の数え方:
gate.mdのreview_3p節に「N 巡目」「N 巡実施」「N 回実施」と明記された独立 Checker の実施回数。当該記事自身のレビューを指す記載に限る(指摘表の中で他記事の巡数に言及している行は数えない)。反映後の「再判定」は、独立 Checker を再起動したと明記されていないので数えない
この定義で 1 巡が 2 本、2 巡が 5 本、3 巡が 1 本でした(社内データ)。これは品質の高さではなく手戻りの量なので、減るほうが良い指標です。生成量指標と逆向きに読む必要があります。
蓄積した知識に有効期限と検証記録を持たせる話は AI Agentの運用メモリ設計、トークン消費そのものの最適化は Vibe Codingのトークン経済学 にあります。
6. この測り方で解けないこと
正直に範囲を切っておきます。
- 第 1 節のスクリプトは行数しか見ません。 内容が良いか、検証済みかは一切測りません。1 行のリファクタで 500 行消せば「純減あり」になりますが、それは健全さの証明ではありません。あくまで「その対象が単調増加しているか」の判定だけです。
- 月次の粒度に依存します。 同じ月に大きな追加と削除が両方あると相殺されます。粒度を週次にすれば結果は変わります。また
--date=format:は author date を見ています。rebase や squash merge を多用するリポジトリでは commit date と月がずれます(%adを%cdに変えると変わります)。git log --numstatは既定でマージコミットの差分を出しません(-mや--first-parentを付けると変わります)。 - 12 / 15 の分母は「判定を確定できた対象」です。 月次を取れた 23 対象のうち、観測月 1〜2 か月の 8 件は
LOW-Nとして分母から外しています(12 + 3 + 8 = 23)。閾値 2 か月に統計的な根拠はなく、「純減する機会が一度はあった」と言える最小値という程度の目安です。閾値を上げれば分母はさらに減ります。 - 完全な履歴が必要です。 shallow clone では月次集計が実際の履歴と一致せず、符号まで反転します。スクリプトは
exit 2で止めますが、止められるのは shallow だと自己申告されている場合だけです。部分クローン(--filter=blob:none)は別の壊れ方をしうるので検証していません。 - 判定しているのは厳密には「単調非減少」です。
net == 0の月は「純減なし」に数えます。ディレクトリ内のリネームだけを繰り返した対象は、増えていなくてもNG側に出ます。 - バイナリと未コミットのパスは測れません。 画像・ビルド成果物のように numstat が行数を返さないパスは skip されます。
.gitignoreかどうかは関係ありません(前述のとおりdoc/Workingは gitignore 配下でも月次が出ます)。skip を「合格」と読まないことだけを保証しています。 - 1 リポジトリ・2025-08-11 の初コミット以降の記録です。 15 中 12 という数字が他のリポジトリでも出るとは限りません。読者の環境で走らせて確かめる前提で書いています。
- CI failure 0 件は指標として使っていません。 台帳の収録基準(すり抜けた失敗のみ)から 0 になることが決まっており、ガードの検出能力を示しません。第 3 節の再掲ですが、混同しやすいので繰り返します。
- 登録漏れのガードは分母に入りません。 二面検証カバー率 12 / 12 の分母は登録表であって、リポジトリ内の全スクリプトではありません。登録表自体の網羅性は、現状この指標では担保できていません。
FAQ
tokenmaxxing とは何ですか
AI の利用量そのもの(トークン消費量、生成行数、AI 経由 PR 数など)を目標に置いてしまい、量が増えれば成果が出ているとみなす運用を指します。本記事では「量を増やすこと自体が目的化した状態」の呼び名として使っています。特定ベンダーの用語ではありません。
AI 利用率を KPI にするのはなぜ危険なのですか
利用率は比率型なので、本記事が主に扱う累積カウンタ型とは別物です。分母(対象者数)があるため下がりえますし、導入初期に「まだ使われていない」を検出する用途では有効です。危険なのは、それを成果指標として据え置く場合で、理由は第 2 節の表の 2 行目だけ(成果を検証しなくても上がる)です。累積カウンタ型のように「悪化を表現できない」わけではありません。導入の進捗指標と成果指標を分けておくのが実務的です。
生成量の指標をすべて捨てるべきですか
捨てる必要はありません。本記事の主張は「生成量指標を成果の代わりに使わない」であって、「取得するな」ではありません。生成量は活動量として有用で、実際このリポジトリでも記事本数は追っています。ただし成果として報告しない、という線を引いています。
単調増加の判定スクリプト(第 1 節)は他のリポジトリでも動きますか
git と awk があれば動きます。--date=format: を使うため git 2.6 以降、shallow 判定に --is-shallow-repository を使うため git 2.15 以降が快適です(2.15 未満は shallow ファイルの有無で代替します)。完全な履歴が必要で、shallow clone では exit 2 で止まります。actions/checkout は既定が fetch-depth: 1 なので、CI で使うなら fetch-depth: 0 を指定してください。判定できた対象が 0 件のときも、一部のパスだけ空振りしたときも、観測月が足りず判定を保留したときも exit 2 を返します。残りが正常でも exit 0 にはしません。
検証済み成果の指標は何から始めればよいですか
このリポジトリでは「すり抜け件数」から始めました。1 行 = 1 観測の台帳を作り、公開前に捕まえた失敗ではなく、すり抜けた失敗だけを記録するという定義にしています。分母を決める必要がなく、両方向に動くためです。
ただしこの指標にも穴があります。 記録をやめれば単調に 0 へ向かい、「本当に 0 件」と「誰も書かなかった」が区別できません。第 3 節で CI failure 0 件を捨てたのと同じ形の落とし穴です。運用側では、0 件だったときに**「観測失敗 0 件」と理由を明記する**ことを必須にして塞いでいます(この記事の gate.md にもその表があります)。定義の詳細は spec/article_retrospective_loop.md に置いています。
まとめ
- 生成量 KPI の問題は「質と相関しないこと」より前に、累積カウンタ型は定義上単調で、悪化を表現できないことにあります。比率型(利用率など)は分母を持つので下がりえます。混ぜて論じないでください。
- その傾向の傍証として、このリポジトリでは判定を確定できた 15 対象のうち 12 が一度も純減していません。観測月 1〜2 か月の 8 件は、純減する機会が無いので判定から外しています。1 リポジトリの行数差分なので、証明ではありません。
- 「ガードを何本作ったか」は本数としては健全でした(登録済み 12 本すべてが二面検証済み)。それでもすり抜けた 14 件のうち 11 件は
origin: harnessで、ガードの守備範囲の外でした。本数はカバー範囲を教えてくれません。 - 検証済み成果を比率で測るなら、分子より先に分母を書く必要があります。同じ事実が 3.6% にも 89% にもなります。
- まず自分のリポジトリで第 1 節のスクリプトを走らせて、
exit 1が返る対象を数えてみてください。そこに並んだ名前が、いま KPI に置いてはいけない候補です。
