軽量化の効果計測(基盤変更の 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、分子は entriesentry_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 回数 / issue
  • manual_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 と変更後の中央値を比較し成否を判定する なし

aggregatecompare は GitHub に触れず、binding file と session ログだけで決定論的に動く。同じ入力からは同じ数値が再現できる。

探索範囲

session ログの既定の探索 root は、Claude が ~/.claude/projects、Codex が ~/.codex/sessions~/.codex/archived_sessions である。Codex は実装本体を回した長い rollout を archived 側へ移すため、archived を外すと bound session と cost が大きく欠落したまま statussuccess になり、欠落を出力から判別できない。

--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.idsession_id の一致から root / subagent を導く
agent_path subagent thread の agent path

各 thread の total_token_usage は独立に積み上がり、root thread の累積は subagent 分を含まない。threadsubagent の 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[].windowwindows subcommand が解決した対象期間を貼る。sessions[] は候補 session を 1 件ずつ分類する。bound は帰属先 issue を必須にし、excludedexclusion_reason を必須にする。

運用手順

集計は次の順で進める。

  1. windows --repo <owner/repo> --issue <N> ... で対象 Issue の window を解決し、出力の issues を binding file に貼る。
  2. candidates --binding <file> で window に重なる候補 session を列挙する。hint を参考に、各 session を boundexcluded に分類して sessions を埋める。
  3. aggregate --binding <file> で Issue 単位の集計を出す。未確定が残る場合は non-zero で停止するので、binding を直して再実行する。
  4. 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_actionmanual_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_actionnext_required_action field の値が proceed 以外の非空文字列である行を数える
  • manual_recoverymanual_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 ごとの usageinput / 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_metathreadsubagent の 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 件で再計測する。

このページは生成物です。原本は元リポジトリ側にあります。