Imported from natsuume/natsuume-cc-marketplace (
plugins/repo-analytics/skills/leadtime/SKILL.md). Install upstream withnpx skills add natsuume/natsuume-cc-marketplace --skill leadtime. Copyright stays with the author.
/repo-analytics:leadtime — リードタイム分析
GitHub issue/PR のタイムラインを収集し、生存バイアス (打ち切り censoring) とサイズ交絡 (PR の大きさ) を統制したリードタイム推移レポートを Artifact として発行する。
まず、この SKILL.md を含む skills/leadtime/ の 2 階層上を <plugin-root> として解決する。通常の Skill 実行では hook 用の ${CLAUDE_PLUGIN_ROOT} が設定される保証はないため、SKILL.md の実パスを正本にする。以降 <plugin-root>/skills/leadtime/scripts/... はこの解決結果を指す。
1. 引数の解釈
- 対象: 省略時はカレントの git リポジトリ (origin remote から
owner/repoを解決)。ディレクトリパスが与えられた場合は配下の git リポジトリを再帰探索する。owner/repoのカンマ区切りリストも受け付ける。 since=YYYY-MM-DD(省略可)。省略時は全期間を対象にする。- remote が GitHub でない、remote が存在しない、owner/repo の識別子が charset 不正 (明示指定エントリを含む)、またはパスに危険文字を含む (次項) リポジトリはスキップし、スキップ件数と理由をレポート・ターミナルサマリの双方に明記する。
- コマンド template への置換値の共通規律: 本 skill の bash コマンド例は、エージェントがコマンド文字列へ値を文字列置換して実行する。パス値 (対象ディレクトリ・再帰探索で発見した checkout パス) に
"・$・バッククォート・\・改行のいずれかが含まれる場合、その値をコマンドに使用せず fail-closed で扱う: 対象ディレクトリ自体なら中断してユーザーに報告し、発見した checkout パスなら{"repo": "<パス文字列>", "reason": "unsafe_path"}をskippedReposに追記してスキップする (これらの文字はディレクトリ名として合法だが、双引用符付き template への文字列置換では quoting を破って任意コマンド実行に到達しうるため)。owner/name (手順 4・5) と default branch 名 (第 6 章) には別途の charset 検証を適用しており、この規律はパス値を対象とする。<work>/<plugin-root>は harness / エージェント自身が解決・生成する値であり、provenance が操作者と harness の信頼境界内に閉じる (第三者が内容を制御しうる経路が無い) ため、この規律 — 第三者制御でありうる値への adversarial-input 検査 — の対象外とする。 - すべてのターゲット (カレントリポジトリ・再帰探索で発見した checkout・明示指定の owner/repo エントリ) は、クエリ実行前に owner/repo の収集キーへ正規化して重複排除する。キーの比較は case-insensitive で行い、同一リポジトリは 1 回だけ収集する (worktree や clone が複数あっても二重集計しない)。この重複排除は 2 段階の契約である。第 1 段はここで述べる、収集キー (owner/repo の入力文字列) を小文字化して比較する case-insensitive dedup である。第 2 段はセクション 3 の収集ループ冒頭で、API が解決した canonical 名 (nameWithOwner) を基準に行う dedup であり、リネーム・移管によって収集キー上は別名に見えるが実体が同一リポジトリであるケースを捕捉する。JSONL レコードに書く repo 値はこの正規化キー (第 1 段のキー) ではなく、API が返す canonical な nameWithOwner を使う。各収集キー (owner/repo) に、解決に使ったローカル checkout パスを optional として保持する。owner/repo 直接指定と発見済み checkout が同一リポジトリに重複した場合も checkout の関連付けを失わない。同一リポジトリに複数の checkout がある場合は最初に発見したものを代表として選ぶ。保持した checkout はセクション 6 のリポジトリイベント抽出で使う。
手順
-
作業ディレクトリを次のとおり定義する。
<work-root>= セッション scratchpad 配下のrepo-analytics-leadtime/(<scratchpad>/repo-analytics-leadtime/。<scratchpad>はセッションの scratchpad ディレクトリ)。<work>=<work-root>/<一意な実行 ID>/(例:date -u +%Y%m%dT%H%M%SZ等で採番) を 新規作成 する (既存ディレクトリの再利用・mkdir -pによる黙認は禁止。ディレクトリ作成が既存パスと衝突したら別の実行 ID を採番し、必ず新規作成できたディレクトリを<work>として使う)。収集診断 (collection-diagnostics.json) を含むこの実行のすべての中間ファイルは<work>配下にのみ書く:issues.jsonl/prs.jsonl/patterns.json/boundaries.json/collection-diagnostics.json/result.json。collection-diagnostics.jsonを{"skippedRepos": [], "webSearchSkipped": false, "repoEventCollection": [], "prSnapshotRefresh": []}で初期化する (Write ツール)。続けてissues.jsonl/prs.jsonlを空 (0 バイト) で先行作成する (Write ツールで空内容を書き込む)。収集キーが 0 件でも第 5 章の集計が入力欠落 (exit 2) にならず、skip 件数を明記した空レポート経路が成立する (第 3 章での書き込みは追記>>のため、この先行作成と矛盾しない)。リトライ時は新しい<work>を作成してこのセクションからやり直し、過去の実行 (別の<work>) の部分成果物を再利用しない。 -
呼び出し引数の文字列を分割し、
since=YYYY-MM-DDに一致するトークンを期間指定として取り出す (複数あれば最後の値を採用し、その旨を記録する)。残りのトークンを対象指定として扱う。対象指定・期間指定のいずれも無ければ対象は「カレントディレクトリの git リポジトリ」、期間は「全期間」とみなす。 -
対象指定が既存ディレクトリのパスであれば手順 3 の再帰探索、それ以外 (存在しないパス、またはカンマを含む文字列) であれば
owner/repoのカンマ区切りリストとして手順 4 に進む。 -
ディレクトリパスが与えられた場合、まず与えられたパスを絶対パスへ解決する (解決できない場合は中断してユーザーに報告する)。
findは第 1 引数が-で始まる文字列だと探索パスではなく式 (action) として解釈する — 例えば-deleteという名前のディレクトリをそのまま渡すと、GNU find は starting point 未指定として.を走査し、式の左から右への評価により-nameの絞り込み前に削除が実行されうる。この経路を閉じるため、findには必ず解決済みの絶対パスだけを渡す。そのうえで配下の git リポジトリを次のように再帰探索する (探索深さの上限で暴走を防ぐ)。find -P "<absolute-dir>" -maxdepth 4 \ \( -name $'*\n*' -exec printf '%s\n' REPO_ANALYTICS_UNSAFE_NEWLINE_NAME \; -quit \) \ -o -name .git -print改行を名前に含むエントリの検査を同一走査に組み込んでいる:
findの改行区切り出力は、名前に改行を含むディレクトリ配下のパスを複数行に分断し、2 行目以降を別の (ツリー外の) パスと誤認させうる。pre-order 走査では改行入りの祖先エントリが配下の.gitより先に評価されるため、sentinel 行REPO_ANALYTICS_UNSAFE_NEWLINE_NAMEが出力に含まれる場合、またはコマンドが非 0 で終了した場合は、出力全体を破棄して中断しユーザーに報告する (fail-closed)。symlink を辿らない既定動作を固定するため-Pを明示しており、このコマンドに-L/-Hを追加してはならない。なお、-maxdepthは POSIX の規定外だが、本 Skill の対象環境である GNU find (Linux / WSL2) と macOS の/usr/bin/findはともにサポートしている (-print0/-quitも同様)。sentinel が無く正常終了した場合、ヒットした各
.git(ディレクトリまたは worktree の gitdir ファイル) の親ディレクトリを checkout パスとして扱う。 -
各 checkout パス (カレントディレクトリを含む) について origin remote から owner/repo を解決する。
git -C "<path>" remote get-url origin- コマンドが非 0 で終了する (remote 未設定) 場合: 「remote が存在しない」としてスキップし、
{"repo": "<path>", "reason": "no_remote"}を<work>/collection-diagnostics.jsonのskippedReposに追記する。 - URL がホスト
github.comの SSH 形式 (git@github.com:owner/repo.git) の場合、:以降を取り出し末尾の.gitを除去してowner/repoを得る。 - URL がホスト
github.comの HTTPS /ssh://形式 (https://github.com/owner/repo(.git)?/ssh://git@github.com/owner/repo(.git)?) の場合、パス部分からowner/repoを取り出し末尾の.gitを除去する。 - 上記いずれの形式にも一致しない、またはホストが
github.comでない場合: 「remote が GitHub でない」としてスキップし、{"repo": "<解決できた範囲の文字列>", "reason": "non_github_remote"}を同じくskippedReposに追記する。 - 上記で
owner/repoを切り出せた場合、続けてownerが^[A-Za-z0-9-]+$、repo(name) が^[A-Za-z0-9._-]+$にそれぞれ一致することを確認する (GitHub の owner / repository 命名規則に基づく保守的 charset)。分析対象ディレクトリ配下の.git/configは非信頼データであり、切り出した owner/name は本 skill のコマンド template へ文字列置換されるため、第 6 章の default branch 名と同様に保守的 charset で検証してから使う (注入の余地を fail-closed で排除する)。いずれかが charset に一致しない場合: 「識別子が不正」としてスキップし、{"repo": "<解決できた範囲の文字列>", "reason": "invalid_identifier"}を同じくskippedReposに追記する。
- コマンドが非 0 で終了する (remote 未設定) 場合: 「remote が存在しない」としてスキップし、
-
明示指定の
owner/repoエントリ (カンマ区切りリストの各要素、前後の空白を trim) は、/でownerとrepo(name) に分解したうえで、手順 4 と同じ charset 検証 (ownerは^[A-Za-z0-9-]+$、name は^[A-Za-z0-9._-]+$) を通してから収集キー候補として採用する (この段階では GitHub への到達性を検証しない。文字種のみを検証する。存在しない owner/repo は第 3 章のgh api graphql --hostname github.com実行時にエラーとして判明し、第 2 章の fail-closed 規則に従って扱う)。charset に一致しないエントリは「識別子が不正」としてスキップし、{"repo": "<エントリ文字列>", "reason": "invalid_identifier"}をskippedReposに追記する。 -
手順 4・5 で得た収集キー候補全体を、
owner/repoを小文字化した文字列をキーとして重複排除する (case-insensitive)。重複した場合、由来 (再帰探索 / 明示指定) を問わず 1 回だけ収集対象に残す。重複排除後のキー一覧が第 3 章のデータ収集ループの対象になる。
2. 前提確認 (fail-closed)
command -v jqとjq --versionで jq の有無とバージョンを確認する。不在、または 1.5 未満の場合は GitHub API 呼び出し前に fail-closed で中断し、必要バージョン (jq 1.5+) を報告する。gh auth status --hostname github.comで github.com の認証状態を確認する。- 未認証、または後続の GraphQL クエリがエラーを返した場合は、部分データのまま分析を進めず中断し、原因をユーザーに報告する。
手順
-
データ収集 (第 3 章) を始める前に、必ず次のコマンドで jq の有無とバージョンを確認する。
command -v jq jq --versioncommand -v jqが非 0 の exit code で終了する (jq が見つからない)、またはjq --versionが報告するバージョンが 1.5 未満の場合、これ以降の手順に進まず、ここで作業を中断する。中断時にユーザーへ報告する内容:- jq が不在、またはバージョンが 1.5 未満であったこと (コマンドの出力を含める)
- 必要バージョン (jq 1.5+) であること、および対応方法 (jq のインストール・更新)
- この時点で発生した副作用は無い (gh の read-only query すら未実行) こと
-
jq の前提を満たしていれば、続けて必ず次のコマンドで認証状態を確認する。
gh auth status --hostname github.com -
上記コマンドが非 0 の exit code で終了する、または出力が未認証を示す場合 (例:
You are not logged into any GitHub hosts)、これ以降の手順に進まず、ここで作業を中断する。中断時にユーザーへ報告する内容:gh auth status --hostname github.comが未認証を示したこと (コマンドの出力を含める)- 対応方法 (
gh auth login --hostname github.comを実行してから再実行する) - この時点で発生した副作用は無い (gh の read-only query すら未実行) こと
-
認証済みであれば第 3 章のデータ収集に進む。第 3 章以降で個々の
gh api graphql --hostname github.com呼び出しがエラー (非 0 exit code、または応答 JSON にerrors配列を含む) を返した場合も同じ fail-closed 規則を適用する — 取得済みの部分データ (JSONL や中間ファイル) を集計・可視化には使わず、収集が完了していたリポジトリ数・失敗したリポジトリと owner/repo・エラーメッセージをユーザーに報告して中断する。
3. データ収集
<plugin-root>/skills/leadtime/scripts/配下の GraphQL テンプレート 5 本 (fetch-issues.graphql/fetch-prs.graphql/fetch-issue-timeline.graphql/fetch-pr-closing-issues.graphql/fetch-pr-snapshot.graphql) をgh api graphql --hostname github.comで実行し、--jqで 1 行 1 レコードの JSONL に整形してセッションの scratchpad に保存する (プロジェクト内には作成しない)。- 各行の repo フィールドには API が返す canonical な nameWithOwner を使う (ユーザ入力の owner/repo 文字列を使わない。closer や closingIssuesReferences が返す nameWithOwner と join キーのケーシングを一致させるため)。
- 変数の型に応じて
-f(--raw-field、型変換なし) と-F(--field、true/false/null/数値に見える値を JSON 型へ変換し@をファイル読み込みとして解釈する) を使い分ける: 文字列変数 (owner/name) は-fで渡す (-Fだと2026のような repo 名が数値へ変換され GraphQLString!と型不一致になるため)。数値変数 (issue / PR 番号を受け取る3テンプレートの$number: Int!) とクエリファイル展開 (query=@<file>、-fだと@がリテラル送信されてしまう) は-Fで渡す。例:gh api graphql --hostname github.com --paginate -f owner="<owner>" -f name="<name>" -F query=@<file>。 issues.jsonlの各行でtimelineItems.totalCount > len(nodes)の issue は、fetch-issue-timeline.graphqlで当該 issue の timeline を先頭から全ページ取得し、一覧クエリ由来の timelineItems を丸ごと置き換える (部分結果とのマージはページ重複を生むため行わない)。置換後の timelineItems は totalCount と全 nodes を保持し、totalCount == len(nodes) を満たす形に再構成する。fetch-prs.graphqlは OPEN + MERGED の PR を収集する (merged PR のみではない)。prs.jsonlの各行でtimelineItems.totalCount > len(nodes)の PR は timeline 取得が不完全である。PR 側には追加ページングテンプレートを用意しない (ready/draft の 2 イベント種に絞った totalCount が 100 を超える PR は実運用上ほぼ発生しない) ため、該当 PR は集計スクリプトが除外しexclusions.prTimelineOverflowに列挙する。除外件数はレポートの「測定上の限界」に明記する。prs.jsonlの各行でclosingIssuesReferences.totalCount > len(nodes)の PR はfetch-pr-closing-issues.graphqlで当該 PR の closingIssuesReferences を先頭から全ページ取得し、一覧クエリ由来の closingIssuesReferences を丸ごと置き換える (部分結果とのマージはページ重複を生むため行わない)。置換後の closingIssuesReferences は totalCount と全 nodes を保持し、totalCount == len(nodes) を満たす形に再構成する (集計スクリプトは不完全な行を入力エラーとして中断する)。- 収集段階の診断 (第 1 章のリポジトリスキップ件数・理由、stale PR snapshot の再取得結果、第 6 章のリポジトリイベント収集結果・WebSearch 省略の有無等) は、判明した時点で scratchpad の固定 shape JSON に追記して記録する。
prSnapshotRefreshの各要素は{prRepo, pr, status, reason}(status:refreshed|failed、成功時のreasonはnull) とする。第 9 章のターミナルサマリと Artifact レポートは、この記録された値をそのまま参照し独自に再集計しない。
手順
-
第 1 章で新規作成済みの
<work>(<work-root>/<一意な実行 ID>/) をそのまま使う。 -
第 1 章で重複排除した収集キー (owner/repo) それぞれについて、次の canonical 名解決 (第 2 段の重複排除) を先に行ったうえで a〜d を順に実行する。
canonical 名の解決と収集時 dedup (第 2 段)
リポジトリごとの収集開始時、issues / PR の取得より前に、canonical 名を独立に解決する。
gh api "repos/<owner>/<name>" --hostname github.com --jq .full_name(issue / PR が 0 件のリポジトリでも取得できる)。小文字化した canonical 名を「canonical → (ターゲット, checkout パス)」の対応表と突合する。
- (i) 既出なら
duplicate_canonicalとして収集診断skippedRepos({"repo": "<canonical の nameWithOwner>", "reason": "duplicate_canonical"}) に記録し、そのターゲットの issues / PR 収集をスキップして次の収集キーに進む (この収集キーについては a〜d を実行しない)。既存エントリに checkout パスが無く今回のターゲットが checkout を持つ場合は、既存エントリへ checkout を補完する。 - (ii) 新規なら対応表に登録し、以降の収集 (a〜d) はこの canonical の owner/name を使う。
issues.jsonl/prs.jsonlへの追記は、この canonical dedup 判定の後にのみ行う (判定前に書き込まない)。a. issues 収集
gh api graphql --hostname github.com --paginate \ -f owner="<owner>" -f name="<name>" \ -F query=@"<plugin-root>/skills/leadtime/scripts/fetch-issues.graphql" \ --jq '.data.repository as $r | $r.issues.nodes[] | . + {repo: $r.nameWithOwner}' \ >> "<work>/issues.jsonl"b. prs 収集
gh api graphql --hostname github.com --paginate \ -f owner="<owner>" -f name="<name>" \ -F query=@"<plugin-root>/skills/leadtime/scripts/fetch-prs.graphql" \ --jq '.data.repository as $r | $r.pullRequests.nodes[] | . + {repo: $r.nameWithOwner}' \ >> "<work>/prs.jsonl"いずれも
--jqでrepository.nameWithOwner(canonical 形) を各行のrepoに注入し、1 行 1 レコードの JSONL として追記する (ユーザ入力の owner/repo 文字列は使わない)。手順 c・d で使う
<owner>/<name>/<owner>/<name>は、この収集キー (手順 1 の重複排除後のキー。caller 指定や再帰探索由来の casing をそのまま保持しうる) ではなく、a・b でissues.jsonl/prs.jsonlに書き込み済みの canonicalrepo値 (nameWithOwner) を/で分解して得た owner/name を使う。overflow 検知のselect(.repo == ...)、再取得 API 呼び出しの-f owner=... -f name=...、再取得後の置換対象特定 (.repo == ... and .number == ...) はすべてこの canonical 値で統一し、収集キーの casing を使わない (同一リポジトリでも収集キーと canonical の casing が食い違いうるため、caller 指定の casing のまま突合すると一致しない場合がある)。c. issue timeline overflow の検知と置換
jq -c 'select(.repo == "<owner>/<name>" and (.timelineItems.totalCount > (.timelineItems.nodes | length)))' "<work>/issues.jsonl"該当した各 issue (
repo,numberの組) について、fetch-issue-timeline.graphqlで先頭ページから全ページを取得し直す。gh api graphql --hostname github.com --paginate \ -f owner="<owner>" -f name="<name>" -F number=<issue_number> \ -F query=@"<plugin-root>/skills/leadtime/scripts/fetch-issue-timeline.graphql" \ --jq '.data.repository.issue.timelineItems' \ > "<work>/_overflow-issue-timeline.pages.jsonl"このコマンドが非 0 で終了した場合 (途中ページの失敗を含む)、置換データを作らず、第 2 章と同じ fail-closed 規則でユーザーに報告して中断する。
成功した場合、ページ単位の出力を 1 つの
{totalCount, nodes}に集約する。jq -s '{totalCount: .[0].totalCount, nodes: (map(.nodes) | add)}' \ "<work>/_overflow-issue-timeline.pages.jsonl" \ > "<work>/_overflow-issue-timeline.json"得られた
{totalCount, nodes}でtotalCount == (nodes | length)になっていることを確認する。一致しない場合は置換に進まず (部分結果とのマージや部分置換は行わない)、第 2 章と同じ fail-closed 規則でユーザーに報告して中断する。一致した場合、issues.jsonl中の該当行のtimelineItemsを丸ごと置き換える。置換コマンドは--argjson(コマンドライン引数として展開するため ARG_MAX の上限にかかりうる) ではなく--slurpfile(ファイルから直接読み込むため ARG_MAX の制約を受けない) でファイルベースに渡す。jq -c --slurpfile repl "<work>/_overflow-issue-timeline.json" \ 'if .repo == "<owner>/<name>" and .number == <issue_number> then .timelineItems = $repl[0] else . end' \ "<work>/issues.jsonl" > "<work>/issues.jsonl.tmp" && mv "<work>/issues.jsonl.tmp" "<work>/issues.jsonl"d. PR closingIssuesReferences overflow の検知と置換
prs.jsonlに対して同様に検知する。jq -c 'select(.repo == "<owner>/<name>" and (.closingIssuesReferences.totalCount > (.closingIssuesReferences.nodes | length)))' "<work>/prs.jsonl"該当した各 PR (
repo,numberの組) について、fetch-pr-closing-issues.graphqlで先頭ページから全ページを取得し直す。gh api graphql --hostname github.com --paginate \ -f owner="<owner>" -f name="<name>" -F number=<pr_number> \ -F query=@"<plugin-root>/skills/leadtime/scripts/fetch-pr-closing-issues.graphql" \ --jq '.data.repository.pullRequest.closingIssuesReferences' \ > "<work>/_overflow-pr-closing-issues.pages.jsonl"このコマンドが非 0 で終了した場合 (途中ページの失敗を含む)、issue timeline の置換と同じ理由で置換データを作らず、第 2 章と同じ fail-closed 規則でユーザーに報告して中断する。
成功した場合、issue timeline と同様にページ単位の出力を 1 つの
{totalCount, nodes}に集約する。jq -s '{totalCount: .[0].totalCount, nodes: (map(.nodes) | add)}' \ "<work>/_overflow-pr-closing-issues.pages.jsonl" \ > "<work>/_overflow-pr-closing-issues.json"得られた
{totalCount, nodes}でtotalCount == (nodes | length)になっていることを確認する。一致しない場合は置換に進まず (部分結果とのマージや部分置換は行わない)、issue timeline の置換と同じ理由で第 2 章と同じ fail-closed 規則でユーザーに報告して中断する。一致した場合、prs.jsonl中の該当行のclosingIssuesReferencesを丸ごと置き換える。issue timeline の置換と同じ理由で--argjsonではなく--slurpfileを使う。jq -c --slurpfile repl "<work>/_overflow-pr-closing-issues.json" \ 'if .repo == "<owner>/<name>" and .number == <pr_number> then .closingIssuesReferences = $repl[0] else . end' \ "<work>/prs.jsonl" > "<work>/prs.jsonl.tmp" && mv "<work>/prs.jsonl.tmp" "<work>/prs.jsonl"PR 側の
timelineItems(ReadyForReviewEvent/ConvertToDraftEvent) は overflow しても追加ページングテンプレートが無いため置換せず、既存の bullet のとおり集計スクリプトの除外に委ねる。 - (i) 既出なら
-
全収集キーについて a〜d と timeline 置換が完了した後、merged closer PR の stale snapshot を検知・再取得する。
merged closer PR の stale snapshot 検知・再取得
issues.jsonlの各 issue についてtimelineItems.nodes配列内で最後に現れるClosedEventを採用し、その closer がmerged == trueのPullRequestであるものだけを調べる。prs.jsonlに同じ(repo, number)の snapshot が存在し、かつstate != "MERGED"なら stale 候補とする。cross-repo close を正しく突合するため、PR のキーには issue 側 repo ではなく closer のrepository.nameWithOwnerを使う。同じ PR が複数 issue を close していても再取得は1回にする。jq -c --slurpfile prs "<work>/prs.jsonl" ' . as $issue | ([.timelineItems.nodes[] | select(.__typename == "ClosedEvent")] | last) as $closed | select($closed.closer.__typename == "PullRequest" and $closed.closer.merged == true) | $closed.closer.repository.nameWithOwner as $prRepo | $closed.closer.number as $pr | $prs[] | select(.repo == $prRepo and .number == $pr) | select(.state != "MERGED") | {issueRepo: $issue.repo, issue: $issue.number, prRepo: $prRepo, pr: $pr, snapshotState: .state, snapshotIsDraft: .isDraft} ' "<work>/issues.jsonl" \ | jq -s -c 'unique_by([.prRepo, .pr])[]' \ > "<work>/_stale-pr-snapshots.jsonl"各 stale 候補の canonical
<prRepo>を owner/name に分解し、fetch-pr-snapshot.graphqlで一覧 query と同一 field の完全 snapshot を取得する。gh api graphql --hostname github.com \ -f owner="<owner>" -f name="<name>" -F number=<pr_number> \ -F query=@"<plugin-root>/skills/leadtime/scripts/fetch-pr-snapshot.graphql" \ --jq '.data.repository as $r | if ($r == null or $r.pullRequest == null) then empty else $r.pullRequest + {repo: $r.nameWithOwner} end' \ > "<work>/_stale-pr-snapshot.json"コマンドが成功し、出力がちょうど1個の object で、
repo/numberが候補キーと一致し、一覧 query と同じ必須 field を持ち、state == "MERGED"であることをjq -eで確認する。この時点ではまだprs.jsonlを変更しない。取得した一時 snapshot について
timelineItems.totalCount > (nodes | length)を再判定し、該当すれば追加取得せずexclusions.prTimelineOverflowに委ねる。closingIssuesReferences.totalCount > (nodes | length)も再判定し、該当すれば手順 d と同じfetch-pr-closing-issues.graphqlによる全ページ再取得・完全置換を一時 snapshot 上で再適用する。closingIssuesReferences の再取得・集約・件数検証がすべて成功するまで正本prs.jsonlは変更しない。これらの overflow 判定を省略して refreshed row を集計へ渡してはならない。snapshot 検証と必要な overflow 再取得がすべて成功した後に限り、
prs.jsonlの該当行を部分 merge ではなく完成した snapshot 全体で置換する。jq -c --slurpfile repl "<work>/_stale-pr-snapshot.json" \ 'if .repo == "<owner>/<name>" and .number == <pr_number> then $repl[0] else . end' \ "<work>/prs.jsonl" > "<work>/prs.jsonl.tmp" \ && mv "<work>/prs.jsonl.tmp" "<work>/prs.jsonl"snapshot 取得・検証、または closingIssuesReferences overflow の再取得・検証に失敗した場合は
prs.jsonlの行を一切変更せず、prSnapshotRefreshに{prRepo, pr, "status": "failed", reason}を追記する。reasonはapi_error/snapshot_missing/snapshot_invalid/snapshot_still_stale/overflow_refresh_failedのいずれかとする。該当 PR は再取得前の snapshot のまま既存の compute guard (exclusions.mergedCloserPrNotQualifying) に委ね、取得失敗を Artifact とターミナルサマリの「測定上の限界」で開示する (部分的な応答や推測値で補完しない)。置換と overflow 再適用に成功した場合は{prRepo, pr, "status": "refreshed", "reason": null}を追記する。この targeted refresh の API 失敗だけは旧 snapshot + 診断へ縮退し、第 2 章の全収集中断規則の例外とする。 -
全リポジトリの収集が終わったら、
date -u +%Y-%m-%dT%H:%M:%SZ等で収集完了時刻 (UTC ISO8601) を記録する。この値を第 5 章の--as-ofに渡す。 -
収集中に判明した診断 (スキップしたリポジトリの最終件数など) を
<work>/collection-diagnostics.jsonへ反映する (Write ツールで上書き)。第 9 章のターミナルサマリと Artifact レポートはこのファイルの値をそのまま参照し、独自に再集計しない。
4. claim 判定パターン (正本)
以下の JSON block が claim 判定パターンの唯一の定義箇所である。実行時にこの block をそのまま scratchpad のファイルに書き出し、compute_leadtime.py の --claim-patterns-file に渡す。compute_leadtime.py 自身はパターンの既定値を一切持たない。
{
"inProgressLabel": "ai:in-progress",
"strict": [
{"id": "lock-claim", "regex": "^🔒 ai:claim branch="},
{"id": "legacy-backtick-claim", "regex": "^Claim: `claim-{issue_number}-"}
],
"loose": [
{"id": "loose-ai-claim", "regex": "ai:claim"},
{"id": "loose-claim-prefix", "regex": "claim-{issue_number}-"}
]
}
- 各 regex は Python
re構文、re.MULTILINEで comment body に適用する (行頭^は各行頭に一致)。 - プレースホルダ
{issue_number}は、判定対象 issue の番号をre.escapeした文字列に置換してから compile する (同一 issue 番号の一致判定を実現する)。 - strict にも loose にも一致しない comment は無視する。loose のみ一致し strict に一致しない comment は「取りこぼし候補」として issue 参照 (repo, issue number, comment createdAt) を記録する。
patterns.json への書き出し: 上記 JSON block を一言一句そのまま (プレースホルダ {issue_number} を含む) Write ツールで <work>/patterns.json に保存する。実行のたびに新規作成 (既存ファイルがあれば上書き) してよい。
5. 集計の実行
<plugin-root>/skills/leadtime/scripts/compute_leadtime.py を次の CLI 契約で実行する。
python3 compute_leadtime.py \
--issues "<issues.jsonl のパス>" \
--prs "<prs.jsonl のパス>" \
--claim-patterns-file "<patterns.json のパス>" \
--as-of <ISO8601 UTC 例 2026-07-17T04:00:00Z> \
[--since <YYYY-MM-DD>] \
[--boundaries-file "<boundaries.json のパス>"]
--issues/--prs/--claim-patterns-file/--as-ofは必須。--as-ofにはデータ収集完了時刻 (UTC) を渡す。- stdout に結果 JSON (
schemaVersionを含む) のみを出力する。診断メッセージはすべて stderr に出る。 - exit code:
0= 成功 (空データ含む)。2= 入力エラー (ファイル不存在・JSONL parse 失敗・必須フィールド欠落・--as-of/--sinceの形式不正・--boundaries-fileの検証失敗 (ファイル不存在・JSON parse 失敗・形状不正・atの ISO8601/UTC 不正または naive 時刻・id/labelの欠落または空文字列・idの重複))。3= claim patterns file の契約違反 (欠落キー・regex compile 失敗)。0/2/3 いずれでも部分データで黙って続行しない (fail-closed)。 - ターミナルサマリで提示する数値は、この stdout JSON の決定的な投影とする。Claude はここで得た JSON の数値を再計算・改変・丸め直ししない (中央値・件数などはすべて JSON の値をそのまま転記する)。
手順
-
第 4 章で作成した
<work>/patterns.jsonと、第 3 章で完成させた<work>/issues.jsonl/<work>/prs.jsonl、および第 3 章手順 3 で記録した収集完了時刻を用意する。 -
初回実行 (この時点では第 6 章のイベント注釈がまだ無いため
--boundaries-fileは付けない)。python3 "<plugin-root>/skills/leadtime/scripts/compute_leadtime.py" \ --issues "<work>/issues.jsonl" \ --prs "<work>/prs.jsonl" \ --claim-patterns-file "<work>/patterns.json" \ --as-of <収集完了時刻> \ [--since <第 1 章で取り出した since>] \ > "<work>/result.json" -
exit code に応じて次のように対応する。
0: 成功 (対象 0 件の空データを含む)。<work>/result.jsonを後続 (第 6〜9 章) の入力として使い続行する。2: 入力エラー。stderr の診断メッセージを確認し、issues.jsonl/prs.jsonlの欠落フィールドや overflow 置換漏れ (第 3 章手順 1c/1d)、--as-of/--sinceの形式、--boundaries-file(再実行時) の形状を点検して修正し、再実行する。原因を特定・修正できない場合は部分データのまま先へ進まず、第 2 章と同じ fail-closed 規則でユーザーに報告して中断する。3:patterns.jsonの契約違反。第 4 章の JSON block と一言一句一致しているか (キー欠落・regex 不正) を確認し、修正して再実行する。修正できない場合は同様に中断してユーザーに報告する。
-
第 6 章でイベント注釈 (
boundaries.json) を作成したら、--boundaries-file <work>/boundaries.jsonを追加して同じコマンドを再実行し、<work>/result.jsonを上書きする。以降の第 7〜9 章はこの (boundaries 込みの) 最終版result.jsonを正本として使う (intervalStatsは boundaries 無指定だと常に[]になるため、区間統計を含むレポートにはこの再実行が必須)。イベント注釈が 1 件も収集できなかった場合 (第 6 章参照) は再実行を省略し、初回のresult.jsonをそのまま最終版として扱う。
6. イベント注釈の収集
- (a) リポジトリイベント:
git logから plugin 新設・feat!(破壊的変更)・CI workflow 追加などの候補を抽出する。 - (b) モデル・ツールイベント: WebSearch で Anthropic / OpenAI の主要イベント (特定モデル名をハードコードしない) を検索し、ローカル痕跡 (
~/.codex/config.tomlの更新日、plugin cache の更新日) で適用日を補正する。 - WebSearch が使えない場合は注釈収集を省略して続行し、省略した旨を Artifact レポートとターミナルサマリの両方に明記する。収集した注釈は
boundaries.json(契約:[{"id": ..., "label": ..., "at": ISO8601}]) に整形し、--boundaries-fileを付けて集計を再実行する。
手順
(a) リポジトリイベントの抽出
対象リポジトリの default branch 上のコミットのみを対象にする。全祖先走査ではなく first-parent 走査を用い、first-parent 上で変更を取り込んだコミット (merge commit を含む) の committer date (%cI、ISO8601) を、default branch への統合時刻の近似値として採用する (git は push・反映の時刻自体を保持しないため、厳密な反映時刻ではない)。merge commit topology では、フィーチャーブランチ上のコミットの %cI が実際の統合時刻より過去になるため、全祖先走査では boundary が実際より過去の時刻に配置されてしまう (backdate される)。フィーチャーブランチ上の元コミット日時 %aI は使わない。ローカル checkout の作業ツリーが実際に default branch を指しているとは限らないため、git log を無条件に実行せず、次の手順で repo/ref を明示的に束縛してから実行する。git fetch は使わない (ローカル git メタデータへの書き込みであり、本 skill の副作用契約「gh read-only query + scratchpad 書き込みのみ」に反するため)。
-
read-only の GitHub API で対象リポジトリの default branch 名と tip の commit OID を取得する。branch 名を URL パスに埋め込む REST 呼び出し (
repos/<owner>/<name>/branches/<branch>形) は、URL エンコードの考慮漏れと command injection の余地を構造的に残すため使わない。branch 名をパスに埋め込まない 1 回の parameterized GraphQL 呼び出し (他の収集クエリと同じ variables 方式) で取得する。gh api graphql --hostname github.com \ -f owner="<owner>" -f name="<name>" \ -f query='query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { defaultBranchRef { name target { oid } } } }' \ --jq '.data.repository.defaultBranchRef'出力の
nameを default branch 名、target.oidを tip の commit OID として以降の手順で使う。コマンドが非 0 で終了する、またはdefaultBranchRefがnull(空リポジトリ等で default branch が存在しない) の場合、このリポジトリのイベント抽出をスキップし、repoEventCollectionに{"repo": "<owner>/<name>", "status": "default_ref_unavailable"}を追記して以降に進まない (この status は手順 3 で述べるローカル追跡 ref の未確認とも共用し、「default branch の ref を解決・確認できない (remote 側・local 側とも)」を表す)。取得できた default branch 名は、以降の手順のコマンド template に文字列置換で埋め込む前に、次の保守的な charset で検証する。
printf '%s' "<default-branch>" | grep -Eq '^[A-Za-z0-9._/-]+$'非 0 (charset 不一致)、または branch 名に
..が含まれる場合、このリポジトリのイベント抽出をスキップし、repoEventCollectionに{"repo": "<owner>/<name>", "status": "default_branch_unsupported"}を追記して以降に進まない (fail-closed)。git refname は;$バッククォート等の shell metacharacter を合法に含みうるが、本 skill の手順はコマンド template への文字列置換で実行されるため、保守的な charset に一致する branch 名のみを扱い、それ以外は注入の余地を残さないよう fail-closed で除外する。..は refname に合法には現れないが、現れた場合 git の範囲指定として解釈されうるため同様に除外する。 -
第 1 章で保持したこのリポジトリの checkout パスを確認する。checkout が無い (第 1 章手順 3 の再帰探索で発見されず、明示指定 owner/repo にも対応する checkout が紐付いていない) 場合、このリポジトリのイベント抽出をスキップし、
<work>/collection-diagnostics.jsonのrepoEventCollectionに{"repo": "<owner>/<name>", "status": "no_checkout"}を追記して手順 3 以降に進まない。 -
checkout があれば、ローカルの追跡 ref を確認する。
git -C "<checkout>" rev-parse --verify "refs/remotes/origin/<default-branch>"コマンドが非 0 で終了する (追跡 ref が無い) 場合、このリポジトリのイベント抽出をスキップし、
repoEventCollectionに{"repo": "<owner>/<name>", "status": "default_ref_unavailable"}を追記して手順 4 に進まない。 -
手順 1 で取得した tip OID と手順 3 で得たローカル ref の OID を比較する。
-
一致する場合、その ref (
refs/remotes/origin/<default-branch>) を明示する (<ref>はこの ref を指す)。git logのコマンド群を実行する前に、shallow clone かどうかを確認する。git -C "<checkout>" rev-parse --is-shallow-repository-
出力が
trueの場合: shallow clone は古い履歴を silent に欠落させ boundary イベントが消えるため、このリポジトリのイベント抽出をスキップし、repoEventCollectionに{"repo": "<owner>/<name>", "status": "shallow_history"}を追記して以降のコマンド群に進まない。 -
コマンドが非 0 で終了する、または出力が
true/falseのいずれでもない場合: 履歴の完全性を確認できないため同様にスキップし、repoEventCollectionに{"repo": "<owner>/<name>", "status": "shallow_check_failed"}を追記して以降のコマンド群に進まない (fail-closed)。 -
出力が
falseの場合: そのまま続行し、次のコマンド群を実行する。 -
plugin / 機能の新設 (初回追加) の検出例:
git -C "<checkout>" log "<ref>" --first-parent --diff-filter=A --format='%H|%cI|%s' -- 'plugins/*/.claude-plugin/plugin.json' -
破壊的変更 (Conventional Commits の
!:記法) の検出例:git -C "<checkout>" log "<ref>" --first-parent --format='%H|%cI|%s' | grep -E '^[0-9a-f]+\|[^|]+\|[a-z]+(\([^)]+\))?!:' -
CI workflow 追加の検出例:
git -C "<checkout>" log "<ref>" --first-parent --diff-filter=A --format='%H|%cI|%s' -- '.github/workflows/*.yml' '.github/workflows/*.yaml'
いずれのコマンドも
<ref>に--first-parentを付与する (--first-parent時は merge commit の diff が first parent との比較になるため、--diff-filter=Aでファイル初回追加を merge commit でも検出できる)。ただし非 squash の merge topology では、フィーチャーブランチ内にのみ存在するfeat!:等の subject が first-parent 走査に現れず検出漏れになりうる。boundary 注釈はヒューリスティックであり、検出漏れ (保守的な欠落) より誤った過去時刻での誤配置の方が区間集計に有害という設計判断で first-parent を採る。各ヒットの
%cIをイベント時刻として採用し、コミットメッセージ (%s) や変更ファイルからラベルを組み立てる (例: 「plugin repo-analytics 新設」)。完了後、repoEventCollectionに{"repo": "<owner>/<name>", "status": "collected"}を追記する。 -
-
不一致 (stale) の場合、このリポジトリのイベント抽出をスキップし、
repoEventCollectionに{"repo": "<owner>/<name>", "status": "default_ref_stale"}を追記する。
-
いずれかの理由でスキップした対象については、「この対象はローカル checkout が存在しない・ref が確認できない・default branch 名が対応 charset 外である (解決不能な場合を含む)・shallow clone で履歴が欠落している・shallow 判定に失敗した、のいずれかの理由により、当該リポジトリに由来するリポジトリイベント注釈を含まない」という注記を Artifact レポート (第 7・8 章) とターミナルサマリ (第 9 章) に明記する。
(b) モデル・ツールイベントの収集
-
WebSearch で対象期間の Anthropic / OpenAI の主要イベントを検索する。特定モデル名をクエリにハードコードせず、次のようなクエリ雛形を対象期間 (収集対象タスクの
firstStartAtの最小値〜--as-of) に差し替えて使う。Anthropic model releases <期間>OpenAI model releases <期間>Claude Code CLI major update <期間>
検索結果から判明した候補イベント (リリース日・GA 日・デフォルトモデル変更等) を一旦リストアップする。
-
ローカル痕跡の mtime を取得する。locale 依存の表記 (
%y/%Sm等) は環境によって曜日・月名の表記が変わり比較を誤らせるため使わず、epoch 秒で取得してから ISO8601 (UTC) に変換する。~/.codex/config.tomlの mtime (epoch 秒): Linux はstat -c '%Y' ~/.codex/config.toml、macOS はstat -f '%m' ~/.codex/config.toml。~/.claude/plugins/cache/~/.codex/plugins/cache配下の mtime (epoch 秒): 各ディレクトリに対して同様にstat(上記いずれかの形式) を実行する。- epoch 秒から ISO8601 (UTC) への変換: Linux は
date -u -d @<epoch 秒> +%Y-%m-%dT%H:%M:%SZ、macOS はdate -u -r <epoch 秒> +%Y-%m-%dT%H:%M:%SZ。
-
mtime の採用規則。次の (1)〜(3) をこの順に適用する。
- ローカル痕跡が対象イベントと意味的に結び付くこと (該当モデル・ツールの設定/キャッシュであること) を確認する。無関係な設定変更等、関連性を確認できない mtime は候補にしない。
リリース日時 <= mtime <= --as-ofの場合に限り、その mtime を推定適用日としてboundaries.jsonのatに採用する。labelには「(適用日は推定、根拠: <ローカル痕跡のパス>)」等、推定であることを明記する。- リリース日より前・
--as-ofより後・関連性を確認できない mtime は採用しない。ローカル痕跡が全く得られない場合も同様に扱う。これらの場合はリリース日をそのままatに採用し、labelに「(ローカル適用日不明、リリース日で代用)」と明記する。
-
WebSearch が使えない場合は本節 (b) の収集をまるごと省略して続行する。
<work>/collection-diagnostics.jsonのwebSearchSkippedをtrueに更新し (Write ツールで上書き)、省略した旨を Artifact レポート (第 7・8 章) とターミナルサマリ (第 9 章) の両方に明記する。 -
(a)(b) で集めたイベントを
boundaries.jsonの契約 ([{"id": str, "label": str, "at": ISO8601}]) に整形し、Write ツールで<work>/boundaries.jsonに書き出す。idは重複しない短い識別子 (例:repo-plugin-repo-analytics-added,model-anthropic-202607) を付ける。1 件も収集できなかった場合 (a も b も候補が無い、または b を丸ごと省略しかつ a も 0 件) はboundaries.jsonを作成せず、第 5 章手順 4 の再実行を省略する。
7. 可視化と Artifact レポート
- チャートを作成する前に dataviz skill を、Artifact を発行する前に artifact-design skill を必ずロードし、palette validator を実行する。
- 含めるチャート: 散布図 (対数軸・打ち切りを◇マーカー・イベント境界の縦線)、区間分解 (start→PR作成→ready→merge) のグループ棒、サイズ帯×週のヒートマップ、draft 経由率の週次系列、イベント年表、区間統計テーブル、全件テーブル (折りたたみ)。
- 散布図の縦軸は対数スケールを既定とし、描画する y 値が 0 以下のレコードは軸下端の「≤0」専用バンドに別マーカー (×) で表示する (対数変換から除外するのであって、データから除外しない)。× の凡例は「対数軸に直接配置できない値」と定義する。negativeInterval だが y 値が正のレコードは正しい数値位置に置く。値は clamp せず JSON の値を表示し、hover と keyboard focus の双方で実値と理由に到達可能にする。全件テーブルにも同じ値とフラグを残す。
- 週次・区間の中央値を提示するすべてのチャート・テーブルに「完了タスクのみの記述的中央値 (censor 非調整)」の注記を付け、n と censoredN を隣接表示する。ターミナルサマリや結論文で同じ中央値を引用する場合も同じ限定を省略しない。
- 全件テーブル・hover・PR リンクは常に
(prRepo, pr)の組を使う (pr番号単独で表示・リンクしない。cross-repo close では issue の repo と PR の repo が異なるため、pr番号だけでは対象 PR を一意に特定できない)。 - ライト/ダーク両テーマに対応し、外部ライブラリを使用しない。
実行順序
-
dataviz skill をロードする (チャートを 1 つでも作成する前に必須)。
-
第 5 章で確定した
<work>/result.json(boundaries 込みの最終版) を基にチャート設計を行う。以下の対応表に従い、各チャートが読む JSON キーを確認する。チャート 読む JSON キー 散布図 (対数軸・打ち切り◇・イベント縦線) mainSeries+censored(縦線は result.json のboundariesが非空であれば併用)区間分解 (start→PR作成→ready→merge) のグループ棒 weeklyCohorts[].phaseMediansサイズ帯×週のヒートマップ weeklyCohorts[].bySizeBanddraft 経由率の週次系列 weeklyCohorts[].viaDraftRateイベント年表 result.json の boundaries区間統計テーブル intervalStats全件テーブル (折りたたみ) mainSeries/censored/auxiliarySeries -
artifact-design skill をロードする (Artifact を発行する前に必須)。
-
HTML を作成する (自己完結、外部ライブラリ不使用、CSP 制約に従う)。既存の契約 (対数軸・非正値バンド・記述的中央値の注記・ライト/ダーク両対応) をすべて満たすこと。
-
palette validator を実行し、使用した配色が検証を通ることを確認する。
-
Artifact ツールで発行し、発行 URL を控える (第 9 章のターミナルサマリで報告するため)。
8. 解釈の規律
- 中央値の底上げ・テールの伸び・停滞 (n の減少) を区別し、要因を分離できない箇所は「識別不能」と明示する。
- レポートには「測定上の限界」節を必須とし、壁時計時間ベースであること・比較可能な coverage 期間・各集計の n の小ささ・推定 (censoring・intervalStats 等) を用いた箇所を列挙する。
- 改善余地は断定した対策ではなく「データが指す介入点」としてユーザーに提示し、意思決定はユーザーに委ねる。
- 完了根拠 (qualifying completion) は MERGED の PR、または現在 OPEN・non-draft の PR の ready 到達である (merge そのものではない)。merge されず close された PR は破棄された試行として完了根拠にしない。ready 到達済み・未 merge のタスクは mainSeries に completionBasis = "ready_unmerged" として含まれる。PR timeline overflow により completion は証明できるが ready 時刻だけが不明な issue は
exclusions.prReadyTimeUnknownに分離し、打ち切り (censored) は completion が証明される PR がリンクされていない着手済み open issue に限定する。 - 週次 cohort の中央値は完了タスクのみから計算される記述的統計である (censor 非調整)。censored のみの週も median null + censoredN で行が出るため、censoredN が大きい週の中央値は必ず censoredN と併せて解釈する。
- 収集範囲外のリポジトリの merged PR で close された issue は
auxiliarySeries.externalMergedCloseとして分離集計され、mainSeriesにもcensoredにも入らない (ready 到達時刻を計測できないため)。
「測定上の限界」チェックリスト
レポートの「測定上の限界」節には、次の項目を毎回すべて含める (該当が無い項目も「該当なし」と明記し、省略しない)。
- 壁時計時間ベースの計測であること (作業時間・稼働時間ではない旨)
- 比較可能な coverage 期間 (
markerCoverageの repo × 月別coverageを基に、着手マーカーが安定して観測できている期間の範囲) - 各集計の n の小ささ (
weeklyCohorts[].n/intervalStats[].nが小さい週・区間の明示。n = 0の週は空欄・線の途切れとして扱い隠さない) - 推定を用いた箇所: 打ち切り (
censoredのelapsedHoursLowerBoundは下限値であること) と、第 6 章で補正・代用したイベント適用日 (推定である旨のラベルをそのまま引用する) -
exclusions.timelineOverflow/exclusions.prTimelineOverflow/exclusions.prReadyTimeUnknownの件数 (prReadyTimeUnknownは completion 済みだが ready 時刻不明のため censored から除外した対応関係) -
exclusions.mergedCloserPrNotQualifyingの件数 (merged closer とされた PR の収集 snapshot が非 qualifying だったため除外された件数) -
dataQuality各値 (negativeIntervalCount/redraftPrCount/notStartedClosedIssues/multipleReadyPrIssues) の件数 -
markerCoverage中のunknownTimeline(timeline 不完全で観測不能だった件数) -
claimDetection.looseOnlyIssuesの件数 (取りこぼし候補) - stale merged closer PR snapshot の再取得成功・失敗件数と失敗理由 (収集診断の
prSnapshotRefresh) - リポジトリイベント注釈をスキップした対象とその理由 (収集診断の
repoEventCollection)
個別の実行時挙動への対応
repos[].closedIssues == 0のリポジトリでも、OPEN issue が ready 到達済みの qualifying PR を持てばmainSeriesにcompletionBasis == "ready_unmerged"として編入されうる (mainSeries への編入は issue のstateを問わないため、closedIssues の値だけでは mainSeries 対象外と断定できない)。レポート・サマリで「ready 済み・未 merge」のタスクがあると記載してよいかどうかは、mainSeriesにcompletionBasis == "ready_unmerged"のレコードが 1 件以上存在するかで判定する (実メンバーシップ判定。派生カウンタrepos[].openReadyPrsによる判定は廃止する —openReadyPrsは issue との紐付けを問わない repo 単位の PR 集計であり、mainSeries への編入有無と一致しない)。該当レコードが無いにもかかわらずclosedIssues == 0の repo は、repos[].mergedPrs/repos[].openReadyPrs(merged PR 系の補助指標) のみで報告し、主系列 (mainSeries) の対象外である旨を明記する。エラーとして扱わない。markerCoverage[].coverage == 0.0(該当 repo × 月に着手マーカーが 1 件も無い) はエラーにせず「marker coverage 0%」としてそのまま報告する。coverage == null(closedIssues == 0、観測不能) とは区別して報告する。- draft を経ていない PR は
resolve_readyの契約により ready 時刻 = PRcreatedAtとしてcompute_leadtime.pyが解決済みである。SKILL.md 側で追加の判定は行わず、result.jsonの値をそのまま使う。 - 再 draft 化された PR (
redraftCount > 0) はdataQuality.redraftPrCountの件数をそのまま「測定上の限界」チェックリストで開示する (上記チェックリスト参照)。 - reopen された issue は
resolve_close_linkageの契約により最終ClosedEventを採用済みである。SKILL.md 側で追加の判定は行わない。
9. ターミナルサマリ
- 結論、主要数値、測定上の限界、発行した Artifact の URL を簡潔に報告する。
- 数値の根拠は二源泉に分ける: 集計数値 (中央値・件数等) は compute_leadtime.py の stdout JSON の決定的投影とする。収集段階の診断 (remote が GitHub でない等でスキップしたリポジトリ数と理由、WebSearch 省略の有無) は収集手順中に scratchpad へ固定 shape (JSON) で記録した値を用い、Artifact とターミナルの双方が同じ記録を参照する。
サマリの構成テンプレート
- 結論 (1〜3 文): 主指標 (着手→ready) の推移をひとことで要約する。
- 主要数値: 代表的な週次・区間の中央値と n を
result.jsonの値そのまま転記する (第 5 章の「決定的な投影」規則に従う)。 - 測定上の限界: 第 8 章のチェックリストの要点を凝縮して記載する (省略しない項目は Artifact レポート側と揃える)。
- 発行した Artifact の URL。
この 4 点の順序でターミナルへ簡潔に報告する。