軽量化の効果計測(基盤変更の outcome 指標)
このドキュメントは、開発ワークフロー軽量化の効果を Issue 単位で計測する集計 tool の手順と契約を定める。
集計方法の契約
基盤変更が実利用を改善しているかは、実装 Issue ごとに次の outcome 指標で確認する。同じ期間と Issue 選定規則を使い、変更前後の傾向を比較する。
以下は集計方法の契約である。issue-session-binding.v1 の対応表と集計 script は cmd/foundation-metrics(wrapper: scripts/foundation-outcome-metrics.sh)として実装済みで、この文書の手順に従う。finding yield はこの tool の対象外で、従来どおり multi-review-evidence.json から手動集計する。
| 指標 | 取得方法 |
|---|---|
$/issue |
Issue の作成時刻から実装 PR の merge 時刻までを対象に、~/.claude/projects と ~/.codex/sessions / ~/.codex/archived_sessions の session を issue-session-binding.v1 の対応表で Issue に紐付ける。Claude は message ごとの usage を合算し、Codex は session 最後の累積 total_token_usage から fork 時の複製分を差し引いて使う。model 別の input / output / cache read / cache write を、集計時点の単価表 snapshot で USD に換算する。 |
required_action 回数/issue |
対応表で紐付けた session の tool 実行結果を行単位で数える。出力テキストに含まれる JSON の next_required_action field が proceed 以外の値を返した行だけを数える。同じ session file を重複して読み込まない。 |
manual_recovery 回数/issue |
対応表で紐付けた session の tool 実行結果を行単位で数える。出力テキストに含まれる JSON の値が manual_recovery の行だけを数える。同じ session file を重複して読み込まない。 |
| finding yield(severity 別) | Issue の multi-review-evidence.json を参照する。分母は summary.review_calibration.reviewer_jobs、分子は entries の entry_type: "finding" を finding_id で重複排除した件数とし、severity ごとに割る。critical / high / medium / low を分けて記録する。 |
issue-session-binding.v1 では候補 session を bound または理由付きの excluded に分類する。未分類、複数 Issue への重複帰属、usage 欠落、token が非空な単価未対応 model がある場合は集計値を確定しない。
これらの指標は観測と判断材料にだけ使い、閾値による自動凍結 trigger は設けない。悪化が見えた場合は、ユーザーが原因を確認し、必要なら後続 Issue を起票する。
基盤コミット比率は参考情報として必要な場合だけ、従来の script で確認する。この値だけで基盤変更を止めない。互換性のため出力に残る freeze_recommended も legacy の参考値であり、凍結 trigger として使わない。
scripts/foundation-commit-ratio.sh --since 2026-01-01 --format json
対象と非対象
対象の指標は 3 系統に絞る。
required_action回数 / issuemanual_recovery出現数 / issue- provider / model 別 token を換算した
$/issue
finding yield(severity 別)はこの tool の対象ではない。multi-review-evidence.json を入力とする別集計として扱う。
tool の構成
集計本体は Go command cmd/foundation-metrics で、scripts/foundation-outcome-metrics.sh は同じ引数をそのまま渡す薄い wrapper である。親 Issue コメントには wrapper の実行コマンドを残し、再実行できる状態を保つ。
subcommand は 4 つに分かれる。
| subcommand | 役割 | 外部依存 |
|---|---|---|
windows |
Issue の対象期間(作成時刻〜実装 PR の merge 時刻)を GitHub から解決する | gh |
candidates |
window に重なる候補 session を hint 付きで列挙する | なし(session ログのみ) |
aggregate |
人手分類 binding から Issue 単位の集計 JSON を出す | なし |
compare |
baseline と変更後の中央値を比較し成否を判定する | なし |
aggregate と compare は GitHub に触れず、binding file と session ログだけで決定論的に動く。同じ入力からは同じ数値が再現できる。
探索範囲
session ログの既定の探索 root は、Claude が ~/.claude/projects、Codex が ~/.codex/sessions と ~/.codex/archived_sessions である。Codex は実装本体を回した長い rollout を archived 側へ移すため、archived を外すと bound session と cost が大きく欠落したまま status は success になり、欠落を出力から判別できない。
--codex-root を指定した場合は、その root だけを対象にし archived を暗黙に追加しない。実際に探索した Codex 側の root は aggregate の stdout JSON の generated_from.codex_roots で確認する。互換のため残る generated_from.codex_root は先頭 root だけを指す。
issue-session-binding.v1
session の Issue 帰属は人手分類の binding file を source of truth とする。auto 推定は candidates の hint として提示するだけで、帰属は確定しない。
hint は Claude が最初の gitBranch、Codex が session_meta 由来の空白区切り key=value である。Codex の rollout は 1 thread につき 1 file なので、同じ worktree で走った root thread と subagent thread を cwd だけでは見分けられない。
| key | 内容 |
|---|---|
cwd |
thread の作業ディレクトリ |
root_thread |
root thread の id(session_meta.session_id)。同じ値を持つ file は 1 つの root thread に属する |
thread |
thread の種別。session_meta.thread_source の値(user / automation / subagent / realtime_voice)で、これを持たない古い rollout では session_meta.id と session_id の一致から root / subagent を導く |
agent_path |
subagent thread の agent path |
各 thread の total_token_usage は独立に積み上がり、root thread の累積は subagent 分を含まない。thread が subagent の file は root thread の重複記録ではないので、root と同じ Issue へ bound にする。
ただし subagent thread の file は、先頭に親 thread の token 履歴の複製を含む。この複製を消費から除く規則は「指標の算定規則」に置く。
binding file の構造は次のとおり。
{
"version": "issue-session-binding.v1",
"issues": [
{ "issue": 1801, "repo": "owner/repo", "window": { "start": "2026-07-01T00:00:00Z", "end": "2026-07-02T12:00:00Z" } }
],
"sessions": [
{ "path": "/abs/session.jsonl", "provider": "claude", "classification": "bound", "issue": 1801 },
{ "path": "/abs/other.jsonl", "provider": "codex", "classification": "excluded", "exclusion_reason": "別 repo の作業" }
]
}
issues[].window は windows subcommand が解決した対象期間を貼る。sessions[] は候補 session を 1 件ずつ分類する。bound は帰属先 issue を必須にし、excluded は exclusion_reason を必須にする。
運用手順
集計は次の順で進める。
windows --repo <owner/repo> --issue <N> ...で対象 Issue の window を解決し、出力のissuesを binding file に貼る。candidates --binding <file>で window に重なる候補 session を列挙する。hint を参考に、各 session をboundかexcludedに分類してsessionsを埋める。aggregate --binding <file>で Issue 単位の集計を出す。未確定が残る場合は non-zero で停止するので、binding を直して再実行する。- baseline 3 件と変更後 3 件を同じ binding に含め、
compare --baseline <a,b,c> --post <d,e,f>で中央値比較と成否判定を出す。
停止条件
aggregate は次のいずれかが残る限り、比較も成否判定も出さず未確定の一覧と理由を返して non-zero で停止する。
- 未分類の候補(候補だが binding に無い)
- binding にだけ在る未知の path(候補でない)
- 重複した path
- usage 欠落(bound session に token 記録が無い)
- 単価未対応 model(bound session の model が単価表に無く、token が非空)
停止は「誤った数値を確定させない」ための gate である。未確定を解消してから再実行する。
指標の算定規則
required_action と manual_recovery は、bound session の tool 実行結果を行単位で数える。対象は Claude が tool_result block、Codex が function_call_output などの tool 出力記録で、prompt や agent message の言及は数えない。同じ session file は重複して読まない。
数える条件は、対象行の出力テキストに含まれる JSON の field と値で決める。raw 行の文字列一致では、指標定義を書いた契約 doc の diff や Issue 本文の引用まで摩擦として数えてしまう。
required_actionはnext_required_actionfield の値がproceed以外の非空文字列である行を数えるmanual_recoveryはmanual_recoveryが JSON の値として現れた行を数える。field 名との一致、および文中の語との一致は数えない- shell tool の出力のように stdout JSON が JSON 文字列の中へ入れ子になっている場合も、入れ子をたどって同じ条件で判定する
- markdown の fenced code block に埋め込まれた JSON は数えない。契約 doc は gate 出力と同形の JSON を例として持つが、gate の stdout が fence に囲まれることはない
- 1 行が各指標へ寄与する上限は 1 とする
JSON として decode できない gate 出力は、fence 除去後のテキストへの行単位照合でも数える。Codex は tool 出力を記録するとき { と } だけの行を落とすため、gate の stdout JSON がこの形でしか残らないことがある。Claude は 1 record 内の複数 tool_result を連結してから渡すため、decode できる JSON と括弧が欠落した JSON が同じテキストに混ざる。
行単位照合の対象は 1 行に収まった "key": "value" の member 形に限り、コロンの直後を無条件に値として読まない。値の位置を構造から取らないと、Go の struct tag json:"manual_recovery" を値と誤判定する。member 形が数える条件は JSON 走査が数える条件の部分集合なので、両方を通しても同じ行が二重に寄与することはない。
token は provider / model 別に集約する。Claude は message ごとの usage(input / cache_read / cache_write / output)を合算し、同一 message id の重複記録は二重計上しない。Codex は session 最後の累積 total_token_usage から次の複製分を差し引いた値を使い、cached 分を除いた input と cached input(cache read)に分ける。Codex は cache 書き込みの課金概念が無いため cache write は 0 とする。
Codex の subagent thread は fork 時に親 thread の token 履歴を複製して記録するため、file 末尾の累積値には自分が消費していない分が含まれる。複製は先頭でまとめて書かれ、実 API 呼び出しがその間隔で連続することはないので、先頭の token_count から 1 秒以内に収まる prefix を複製とみなす。その prefix 末尾の累積値を baseline とし、最終累積値から field ごとに差し引く。差が負になる field は 0 に丸める。
判定条件を先頭記録との同一ミリ秒一致にすると、先頭 1 件だけ 1 ミリ秒ずれる複製を取り逃す。1 秒の窓はこの取り逃しを防ぐために置いている。
差し引きの対象は、先頭 session_meta の thread が subagent の file だけである。fork するのは subagent thread なので、root thread と automation thread は先頭の token_count が 1 秒以内に続いた場合も複製とみなさず、従来どおり file 末尾の累積値をそのまま使う。
subagent thread でも prefix が 1 件しかない file は複製を持たないものとして扱い、file 末尾の累積値をそのまま使う。
$/issue は tool に固定した単価表 snapshot で USD 換算する。snapshot は出典・適用日・通貨を含み、出力にそのまま埋め込む。既定 snapshot は internal/foundationmetrics/pricing_default.json にあり、請求と厳密に一致させたい場合は --pricing <file> で authoritative な snapshot に差し替える。
単価の無い model は $ を捏造しない。token が非空な該当 model は provider/model 形式で undetermined.unpriced_models に列挙され、aggregate は non-zero で停止する。
token が全て 0 の model は単価が無くても $0 が確定するため停止させない。Issue 単位の cost_usd.unpriced_models に観測として残るだけで、undetermined.unpriced_models には入らない。Claude Code が出す <synthetic> の record がこれに当たる。
成否判定
compare は 3 指標すべてで変更後の中央値が baseline の中央値を下回れば success とする。baseline と変更後の Issue は各グループ内で重複せず、両グループが互いに素であることを要求する。比較対象の Issue に token が非空な単価未対応 model があると cost が過小評価されるため、その場合は成否を出さず error で停止する。baseline の中央値が 0 の件数指標は、変更後も 0 なら成功とする。1 指標でも条件を満たさない場合は not_met を返す。not_met の時は親 Issue を閉じず、原因分析の後続 Issue を起票してから別の実装 Issue 3 件で再計測する。