From c59c9365c9ac6357fdd3824e4d27487f1e142421 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 3 Jul 2026 00:00:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(lint):=20env=5Fkeys=20drift=20=E6=A4=9C?= =?UTF-8?q?=E7=9F=A5=E3=82=92=E6=9C=AC=E7=95=AA=E7=B5=8C=E8=B7=AF=E3=81=B8?= =?UTF-8?q?=E6=8B=A1=E5=BC=B5=20+=20AI=20=E3=83=95=E3=83=AC=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=AA=E3=83=BC=E3=81=95=E3=83=80=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=83=9C=E3=83=BC=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lint-env-keys の強化(従来は docker-compose の正方向のみ): - cloud_run main.tf の env 名(静的 block + secret locals キー)が env_keys.py に存在するかの逆方向検証を追加(本番注入経路を初めて機械検証下に) - docker-compose の逆方向検証を追加(rename/削除時の旧名残留・typo を検知) - backend/app のリテラル os.getenv("XXX") 参照禁止を機械強制 (従来は規律のみ。3 検知ともネガティブテストで fail することを確認済み) AI フレンドリーさダッシュボード: - scripts/metrics-ai-friendliness.sh を新規作成。手戻りコミット率 / CodeRabbit 指摘密度 / ルール追記数を PR 履歴から月次集計し、 docs/metrics/ai-friendliness.md(mermaid xychart)を再生成する - PR 単位の計測値は ai-friendliness-data.json にキャッシュ(差分のみ API 取得) - make metrics-ai-friendliness ターゲットと README 導線を追加 Co-Authored-By: Claude Fable 5 --- Makefile | 19 +- README.md | 1 + backend/app/core/env_keys.py | 17 +- docs/metrics/ai-friendliness-data.json | 2151 ++++++++++++++++++++++++ docs/metrics/ai-friendliness.md | 78 + scripts/lint-env-keys.sh | 68 +- scripts/metrics-ai-friendliness.sh | 206 +++ 7 files changed, 2530 insertions(+), 10 deletions(-) create mode 100644 docs/metrics/ai-friendliness-data.json create mode 100644 docs/metrics/ai-friendliness.md create mode 100755 scripts/metrics-ai-friendliness.sh diff --git a/Makefile b/Makefile index 0e845fb8..2a337d84 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ dupe-check dupe-check-html dupe-clean \ build-web build-backend deploy-web \ gen-redirects codegen-types licenses \ + metrics-ai-friendliness \ migrate migrate-create \ infra-fmt infra-fmt-check infra-validate-dev infra-validate-stg infra-validate-prod infra-validate \ clean @@ -45,7 +46,7 @@ help: @echo " typecheck-backend Backend: pyright 型チェック" @echo " lint-web Frontend: eslint" @echo " lint-web-messages Frontend: setError等にリテラル日本語が渡っていないか検知" - @echo " lint-env-keys env名/エラーコードの SSoT drift を検知 (env_keys.py↔compose, errors.py↔errorCodes.ts)" + @echo " lint-env-keys env名/エラーコードの SSoT drift を検知 (env_keys.py↔compose/cloud_run 双方向, リテラル参照禁止, errors.py↔errorCodes.ts)" @echo " lint-fix リント自動修正 (ruff + eslint)" @echo " format Prettier で整形" @echo " format-check Prettier チェック" @@ -55,6 +56,9 @@ help: @echo " dupe-check-html 同上 (HTML レポート出力後にパスを表示)" @echo " dupe-clean report/dupe/ を削除" @echo "" + @echo "計測" + @echo " metrics-ai-friendliness AI フレンドリーさ指標を GitHub 履歴から集計し docs/metrics/ を再生成" + @echo "" @echo "ビルド" @echo " build-web Vite ビルド" @echo " build-backend Docker イメージビルド" @@ -174,7 +178,8 @@ lint-web-messages: nix develop --command bash scripts/lint-web-messages.sh # env 名 / エラーコードの SSoT drift を検知。 -# env_keys.py↔docker-compose.yml、errors.py↔errorCodes.ts の集合一致を検証する。 +# env_keys.py↔docker-compose.yml(双方向)/ cloud_run main.tf(逆方向)、 +# backend のリテラル env 参照禁止、errors.py↔errorCodes.ts の集合一致を検証する。 # grep/sed/comm のみに依存(ripgrep 不要)。他 lint と揃えて nix wrap で実行する。 lint-env-keys: nix develop --command bash scripts/lint-env-keys.sh @@ -232,6 +237,16 @@ gen-redirects: codegen-types: nix develop --command bash -c "set -e; cd backend && .venv/bin/python scripts/export_openapi.py && cd ../web && node scripts/gen-types.mjs" +# ------------------------------------------------------------------ # +# 計測(AI フレンドリーさダッシュボード) +# ------------------------------------------------------------------ # + +# 手戻りコミット率・CodeRabbit 指摘密度等を GitHub 履歴から月次集計し、 +# docs/metrics/ai-friendliness.md(ダッシュボード)を再生成する。 +# gh の認証(ホスト側 keyring)を使うため nix wrap しない(依存: gh / jq / git のみ)。 +metrics-ai-friendliness: + bash scripts/metrics-ai-friendliness.sh + # ------------------------------------------------------------------ # # 使用 OSS ライセンス一覧 # ------------------------------------------------------------------ # diff --git a/README.md b/README.md index cc2a45e1..d0fd5ae0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ GitHub活動分析、ブログ連携による発信力を集計 | [docs/data-model.md](./docs/data-model.md) | Turso (libSQL) 運用・Alembic マイグレーション・データ設計 | | [docs/adr/](./docs/adr/) | アーキテクチャ判断記録(ADR) | | [docs/runbooks/](./docs/runbooks/) | 運用 Runbook | +| [docs/metrics/ai-friendliness.md](./docs/metrics/ai-friendliness.md) | AI フレンドリーさ指標の月次ダッシュボード(`make metrics-ai-friendliness` で再生成) | ## 主な機能 diff --git a/backend/app/core/env_keys.py b/backend/app/core/env_keys.py index 1f569386..8ab674ed 100644 --- a/backend/app/core/env_keys.py +++ b/backend/app/core/env_keys.py @@ -26,10 +26,19 @@ ## drift 検知(自動) -本モジュールの定数値が `docker-compose.yml` の environment にすべて存在するかは -`scripts/lint-env-keys.sh`(`make lint-env-keys` / CI の test-backend ジョブ)で機械検証する。 -rename / 追加で compose 追従を忘れると lint が落ちる。ローカル開発で意図的に注入しない -定数(起動時内部フラグ等)は同スクリプトの `COMPOSE_ALLOWLIST` に明示的に追記する。 +`scripts/lint-env-keys.sh`(`make lint-env-keys` / CI)が以下を機械検証する: + +- 本モジュールの定数値が `docker-compose.yml` の environment にすべて存在するか。 + ローカル開発で意図的に注入しない定数(起動時内部フラグ等)は同スクリプトの + `COMPOSE_ALLOWLIST` に明示的に追記する。 +- `docker-compose.yml` / `infra/modules/cloud_run/main.tf` に書かれた env 名が + すべて本モジュールに存在するか(rename / 削除時の旧名残留・typo を検知)。 + cloud_run は環境変数ごとに注入要否が異なるため逆方向のみ検証する。 +- backend/app が `os.getenv("XXX")` 等の文字列リテラルで env を参照していないか + (本モジュールの定数経由を機械強制する)。 + +`.github/workflows/ci.yml` は検証対象外(実 API を CI から呼ばないため。 +OPENAI_API_KEY のコメント参照)。`docs/api.md` の環境変数表は手動同期のまま。 ## 関連ドキュメント diff --git a/docs/metrics/ai-friendliness-data.json b/docs/metrics/ai-friendliness-data.json new file mode 100644 index 00000000..04d20cfc --- /dev/null +++ b/docs/metrics/ai-friendliness-data.json @@ -0,0 +1,2151 @@ +[ + { + "number": 1, + "merged_at": "2026-02-21T14:33:20Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 3, + "merged_at": "2026-02-21T14:54:13Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 4, + "merged_at": "2026-02-21T14:54:56Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 5, + "merged_at": "2026-02-21T20:01:51Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 6, + "merged_at": "2026-02-21T20:02:25Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 7, + "merged_at": "2026-02-21T23:15:23Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 9, + "merged_at": "2026-02-24T01:59:21Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 10, + "merged_at": "2026-02-26T09:03:03Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 11, + "merged_at": "2026-02-26T09:42:03Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 12, + "merged_at": "2026-03-07T13:00:12Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 13, + "merged_at": "2026-03-07T14:29:26Z", + "commits_total": 11, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 14, + "merged_at": "2026-03-07T13:08:17Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 15, + "merged_at": "2026-03-07T14:11:35Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 16, + "merged_at": "2026-03-07T14:23:32Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 17, + "merged_at": "2026-03-09T07:34:33Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 18, + "merged_at": "2026-03-09T07:38:40Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 19, + "merged_at": "2026-03-09T08:50:13Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 20, + "merged_at": "2026-03-09T08:55:11Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 21, + "merged_at": "2026-03-09T09:00:12Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 22, + "merged_at": "2026-03-09T09:07:01Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 24, + "merged_at": "2026-03-09T09:16:58Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 25, + "merged_at": "2026-03-09T09:17:58Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 26, + "merged_at": "2026-03-09T12:03:01Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 27, + "merged_at": "2026-03-09T12:10:01Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 28, + "merged_at": "2026-03-09T12:21:45Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 29, + "merged_at": "2026-03-09T12:26:08Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 30, + "merged_at": "2026-03-11T14:27:11Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 31, + "merged_at": "2026-03-11T14:30:42Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 32, + "merged_at": "2026-03-11T14:34:44Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 33, + "merged_at": "2026-03-11T15:26:12Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 34, + "merged_at": "2026-03-11T15:27:09Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 35, + "merged_at": "2026-03-11T15:27:38Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 36, + "merged_at": "2026-03-11T15:40:50Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 37, + "merged_at": "2026-03-11T15:41:43Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 38, + "merged_at": "2026-03-11T15:42:18Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 39, + "merged_at": "2026-03-11T15:49:06Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 40, + "merged_at": "2026-03-12T00:41:55Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 41, + "merged_at": "2026-03-12T08:25:30Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 42, + "merged_at": "2026-03-13T05:26:24Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 43, + "merged_at": "2026-03-13T09:04:37Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 44, + "merged_at": "2026-03-13T11:18:21Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 45, + "merged_at": "2026-03-13T20:24:28Z", + "commits_total": 11, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 46, + "merged_at": "2026-03-13T20:24:58Z", + "commits_total": 11, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 47, + "merged_at": "2026-03-15T04:11:39Z", + "commits_total": 11, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 48, + "merged_at": "2026-03-16T05:03:45Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 49, + "merged_at": "2026-03-17T11:24:25Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 50, + "merged_at": "2026-03-18T12:31:33Z", + "commits_total": 7, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 51, + "merged_at": "2026-03-19T06:45:15Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 52, + "merged_at": "2026-03-19T06:59:07Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 53, + "merged_at": "2026-03-19T07:00:01Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 54, + "merged_at": "2026-03-19T07:24:09Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 55, + "merged_at": "2026-03-19T07:19:16Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 56, + "merged_at": "2026-03-19T07:22:00Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 57, + "merged_at": "2026-03-19T15:13:41Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 58, + "merged_at": "2026-03-20T13:40:56Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 59, + "merged_at": "2026-03-21T07:29:25Z", + "commits_total": 39, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 60, + "merged_at": "2026-03-20T14:37:29Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 61, + "merged_at": "2026-03-20T14:58:47Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 62, + "merged_at": "2026-03-20T15:20:02Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 63, + "merged_at": "2026-03-21T07:27:16Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 64, + "merged_at": "2026-03-21T08:26:31Z", + "commits_total": 42, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 65, + "merged_at": "2026-03-21T07:43:32Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 66, + "merged_at": "2026-03-21T08:21:00Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 67, + "merged_at": "2026-03-21T07:48:57Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 68, + "merged_at": "2026-03-21T08:19:21Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 69, + "merged_at": "2026-03-21T08:29:13Z", + "commits_total": 42, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 70, + "merged_at": "2026-03-21T08:44:14Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 71, + "merged_at": "2026-03-21T08:48:30Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 72, + "merged_at": "2026-03-21T09:20:22Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 73, + "merged_at": "2026-03-21T14:39:16Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 81, + "merged_at": "2026-03-22T07:07:02Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 82, + "merged_at": "2026-03-22T08:38:45Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 84, + "merged_at": "2026-03-22T13:05:36Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 85, + "merged_at": "2026-03-22T13:18:48Z", + "commits_total": 9, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 86, + "merged_at": "2026-03-22T13:22:14Z", + "commits_total": 9, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 87, + "merged_at": "2026-03-23T10:16:48Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 88, + "merged_at": "2026-03-23T10:56:29Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 89, + "merged_at": "2026-03-23T13:05:45Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 90, + "merged_at": "2026-03-24T08:16:52Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 91, + "merged_at": "2026-03-24T08:48:30Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 92, + "merged_at": "2026-03-24T13:29:10Z", + "commits_total": 7, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 94, + "merged_at": "2026-03-25T03:57:26Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 95, + "merged_at": "2026-03-25T03:59:29Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 96, + "merged_at": "2026-03-25T04:01:37Z", + "commits_total": 7, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 99, + "merged_at": "2026-03-25T11:01:10Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 100, + "merged_at": "2026-03-25T11:48:18Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 104, + "merged_at": "2026-03-25T23:14:35Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 105, + "merged_at": "2026-03-26T11:54:07Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 110, + "merged_at": "2026-03-27T14:18:56Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 111, + "merged_at": "2026-03-27T14:32:05Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 112, + "merged_at": "2026-03-27T14:36:15Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 113, + "merged_at": "2026-03-27T15:11:36Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 114, + "merged_at": "2026-03-28T12:40:40Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 117, + "merged_at": "2026-03-28T12:49:42Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 118, + "merged_at": "2026-03-29T08:34:06Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 119, + "merged_at": "2026-03-29T09:42:13Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 122, + "merged_at": "2026-03-31T06:27:56Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 125, + "merged_at": "2026-03-31T06:26:27Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 126, + "merged_at": "2026-03-31T06:27:19Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 128, + "merged_at": "2026-03-31T06:32:15Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 129, + "merged_at": "2026-03-31T07:30:38Z", + "commits_total": 7, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 130, + "merged_at": "2026-04-01T06:53:48Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 131, + "merged_at": "2026-04-01T10:10:37Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 132, + "merged_at": "2026-04-04T09:55:06Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 133, + "merged_at": "2026-04-04T09:59:45Z", + "commits_total": 20, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 135, + "merged_at": "2026-04-04T13:00:05Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 136, + "merged_at": "2026-04-04T13:06:10Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 137, + "merged_at": "2026-04-04T13:18:06Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 138, + "merged_at": "2026-04-04T13:26:26Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 139, + "merged_at": "2026-04-04T13:38:47Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 140, + "merged_at": "2026-04-04T13:47:46Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 141, + "merged_at": "2026-04-05T01:49:12Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 142, + "merged_at": "2026-04-05T04:18:46Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 143, + "merged_at": "2026-04-05T07:38:00Z", + "commits_total": 26, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 144, + "merged_at": "2026-04-07T01:18:50Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 145, + "merged_at": "2026-04-07T01:50:20Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 146, + "merged_at": "2026-04-07T09:12:54Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 147, + "merged_at": "2026-04-07T09:17:12Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 148, + "merged_at": "2026-04-07T10:41:01Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 149, + "merged_at": "2026-04-07T10:45:32Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 150, + "merged_at": "2026-04-07T13:20:17Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 151, + "merged_at": "2026-04-08T12:19:06Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 152, + "merged_at": "2026-04-08T12:24:06Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 153, + "merged_at": "2026-04-08T12:32:49Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 154, + "merged_at": "2026-04-09T10:00:54Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 155, + "merged_at": "2026-04-09T10:01:25Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 156, + "merged_at": "2026-04-09T10:08:50Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 157, + "merged_at": "2026-04-09T12:35:43Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 160, + "merged_at": "2026-04-10T06:23:19Z", + "commits_total": 112, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 163, + "merged_at": "2026-04-11T01:33:53Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 165, + "merged_at": "2026-04-11T02:23:50Z", + "commits_total": 148, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 166, + "merged_at": "2026-04-11T09:26:20Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 167, + "merged_at": "2026-04-11T09:29:17Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 168, + "merged_at": "2026-04-11T09:36:48Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 169, + "merged_at": "2026-04-11T09:45:30Z", + "commits_total": 150, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 170, + "merged_at": "2026-04-11T12:03:35Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 171, + "merged_at": "2026-04-12T07:37:54Z", + "commits_total": 12, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 173, + "merged_at": "2026-04-12T09:42:50Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 180, + "merged_at": "2026-04-13T13:21:38Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 182, + "merged_at": "2026-04-13T13:52:42Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 183, + "merged_at": "2026-04-14T02:39:41Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 184, + "merged_at": "2026-04-14T03:12:33Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 186, + "merged_at": "2026-04-14T05:49:25Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 188, + "merged_at": "2026-04-14T06:48:54Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 189, + "merged_at": "2026-04-14T06:53:16Z", + "commits_total": 19, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 190, + "merged_at": "2026-04-14T07:08:59Z", + "commits_total": 19, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 191, + "merged_at": "2026-04-15T12:50:21Z", + "commits_total": 12, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 192, + "merged_at": "2026-04-17T11:59:10Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 193, + "merged_at": "2026-04-18T03:48:29Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 194, + "merged_at": "2026-04-18T06:22:24Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 195, + "merged_at": "2026-04-18T07:33:32Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 196, + "merged_at": "2026-04-18T08:14:34Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 197, + "merged_at": "2026-04-18T13:20:55Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 198, + "merged_at": "2026-04-18T13:54:53Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 199, + "merged_at": "2026-04-24T07:05:40Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 200, + "merged_at": "2026-04-24T07:19:32Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 201, + "merged_at": "2026-04-24T07:25:18Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 202, + "merged_at": "2026-04-24T07:22:04Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 204, + "merged_at": "2026-04-24T12:51:54Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 206, + "merged_at": "2026-04-25T10:48:38Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 207, + "merged_at": "2026-04-25T11:25:36Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 208, + "merged_at": "2026-04-25T10:47:43Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 209, + "merged_at": "2026-04-29T11:12:15Z", + "commits_total": 0, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 210, + "merged_at": "2026-04-29T22:42:36Z", + "commits_total": 22, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 211, + "merged_at": "2026-04-30T01:52:46Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 212, + "merged_at": "2026-05-01T13:54:50Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 213, + "merged_at": "2026-05-01T14:01:41Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 214, + "merged_at": "2026-05-01T14:33:29Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 215, + "merged_at": "2026-05-01T15:07:09Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 217, + "merged_at": "2026-05-01T15:33:22Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 218, + "merged_at": "2026-05-01T15:35:07Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 219, + "merged_at": "2026-05-01T22:20:36Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 220, + "merged_at": "2026-05-01T23:00:00Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 221, + "merged_at": "2026-05-02T05:47:11Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 222, + "merged_at": "2026-05-02T13:36:19Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 223, + "merged_at": "2026-05-06T12:20:45Z", + "commits_total": 7, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 224, + "merged_at": "2026-05-07T12:48:32Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 232, + "merged_at": "2026-05-11T04:26:28Z", + "commits_total": 10, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 233, + "merged_at": "2026-05-11T04:32:10Z", + "commits_total": 81, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 234, + "merged_at": "2026-05-11T04:45:44Z", + "commits_total": 81, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 238, + "merged_at": "2026-05-14T15:32:28Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 239, + "merged_at": "2026-05-14T15:48:16Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 240, + "merged_at": "2026-05-14T15:51:36Z", + "commits_total": 8, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 241, + "merged_at": "2026-05-14T16:05:22Z", + "commits_total": 8, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 242, + "merged_at": "2026-05-14T22:59:17Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 243, + "merged_at": "2026-05-15T11:36:30Z", + "commits_total": 18, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 244, + "merged_at": "2026-05-15T11:40:52Z", + "commits_total": 20, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 245, + "merged_at": "2026-05-15T23:08:43Z", + "commits_total": 24, + "commits_rework": 0, + "review_comments": 23 + }, + { + "number": 246, + "merged_at": "2026-05-15T14:34:22Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 247, + "merged_at": "2026-05-15T14:37:15Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 248, + "merged_at": "2026-05-15T15:57:43Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 249, + "merged_at": "2026-05-15T16:01:10Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 250, + "merged_at": "2026-05-15T22:50:56Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 251, + "merged_at": "2026-05-15T22:54:02Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 252, + "merged_at": "2026-05-16T01:38:47Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 253, + "merged_at": "2026-05-16T01:45:46Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 254, + "merged_at": "2026-05-17T05:32:10Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 10 + }, + { + "number": 255, + "merged_at": "2026-05-16T21:56:59Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 256, + "merged_at": "2026-05-16T22:12:42Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 257, + "merged_at": "2026-05-17T03:16:27Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 258, + "merged_at": "2026-05-17T03:30:24Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 259, + "merged_at": "2026-05-17T05:33:37Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 260, + "merged_at": "2026-05-17T05:34:49Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 261, + "merged_at": "2026-05-17T09:59:33Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 263, + "merged_at": "2026-05-17T11:07:15Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 264, + "merged_at": "2026-05-17T13:32:54Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 265, + "merged_at": "2026-05-22T13:12:00Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 22 + }, + { + "number": 266, + "merged_at": "2026-05-26T01:02:08Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 267, + "merged_at": "2026-05-26T04:01:44Z", + "commits_total": 16, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 268, + "merged_at": "2026-05-26T05:08:16Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 269, + "merged_at": "2026-05-26T05:59:21Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 270, + "merged_at": "2026-05-26T06:15:45Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 272, + "merged_at": "2026-05-26T08:35:23Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 273, + "merged_at": "2026-05-27T06:50:35Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 274, + "merged_at": "2026-05-26T09:13:50Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 275, + "merged_at": "2026-05-26T09:22:38Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 276, + "merged_at": "2026-05-28T15:34:45Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 277, + "merged_at": "2026-05-28T16:42:18Z", + "commits_total": 5, + "commits_rework": 2, + "review_comments": 3 + }, + { + "number": 278, + "merged_at": "2026-05-28T16:03:42Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 279, + "merged_at": "2026-05-28T16:30:03Z", + "commits_total": 2, + "commits_rework": 2, + "review_comments": 0 + }, + { + "number": 280, + "merged_at": "2026-05-29T01:24:26Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 281, + "merged_at": "2026-05-29T07:53:55Z", + "commits_total": 8, + "commits_rework": 0, + "review_comments": 10 + }, + { + "number": 282, + "merged_at": "2026-05-29T02:22:13Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 283, + "merged_at": "2026-05-29T07:02:28Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 284, + "merged_at": "2026-05-29T07:36:30Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 285, + "merged_at": "2026-05-29T07:40:35Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 286, + "merged_at": "2026-05-29T07:47:33Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 287, + "merged_at": "2026-05-29T08:45:40Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 288, + "merged_at": "2026-05-29T09:37:11Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 289, + "merged_at": "2026-05-29T13:45:51Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 4 + }, + { + "number": 290, + "merged_at": "2026-05-29T14:06:07Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 291, + "merged_at": "2026-05-29T14:30:48Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 292, + "merged_at": "2026-05-29T14:56:21Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 293, + "merged_at": "2026-05-31T04:29:33Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 2 + }, + { + "number": 294, + "merged_at": "2026-05-31T08:19:00Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 3 + }, + { + "number": 295, + "merged_at": "2026-05-31T08:25:27Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 296, + "merged_at": "2026-05-31T08:35:21Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 297, + "merged_at": "2026-05-31T12:36:49Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 298, + "merged_at": "2026-05-31T12:48:47Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 299, + "merged_at": "2026-06-01T03:39:48Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 300, + "merged_at": "2026-06-01T06:46:23Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 2 + }, + { + "number": 301, + "merged_at": "2026-06-01T12:35:32Z", + "commits_total": 3, + "commits_rework": 2, + "review_comments": 1 + }, + { + "number": 302, + "merged_at": "2026-06-05T02:01:46Z", + "commits_total": 5, + "commits_rework": 0, + "review_comments": 3 + }, + { + "number": 303, + "merged_at": "2026-06-05T02:15:45Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 304, + "merged_at": "2026-06-05T01:37:42Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 305, + "merged_at": "2026-06-05T05:17:52Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 306, + "merged_at": "2026-06-05T11:51:03Z", + "commits_total": 5, + "commits_rework": 2, + "review_comments": 6 + }, + { + "number": 307, + "merged_at": "2026-06-05T12:13:24Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 308, + "merged_at": "2026-06-06T06:20:52Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 4 + }, + { + "number": 309, + "merged_at": "2026-06-06T10:16:22Z", + "commits_total": 4, + "commits_rework": 1, + "review_comments": 3 + }, + { + "number": 310, + "merged_at": "2026-06-06T16:24:12Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 8 + }, + { + "number": 311, + "merged_at": "2026-06-07T18:04:53Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 3 + }, + { + "number": 312, + "merged_at": "2026-06-09T04:17:48Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 313, + "merged_at": "2026-06-09T09:36:18Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 314, + "merged_at": "2026-06-09T11:26:46Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 315, + "merged_at": "2026-06-09T12:49:43Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 316, + "merged_at": "2026-06-09T13:45:38Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 317, + "merged_at": "2026-06-10T05:44:27Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 318, + "merged_at": "2026-06-10T11:58:44Z", + "commits_total": 4, + "commits_rework": 0, + "review_comments": 2 + }, + { + "number": 319, + "merged_at": "2026-06-10T14:55:59Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 320, + "merged_at": "2026-06-10T23:10:55Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 321, + "merged_at": "2026-06-11T05:00:30Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 2 + }, + { + "number": 322, + "merged_at": "2026-06-11T12:03:42Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 4 + }, + { + "number": 323, + "merged_at": "2026-06-12T02:14:36Z", + "commits_total": 6, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 324, + "merged_at": "2026-06-12T09:08:05Z", + "commits_total": 4, + "commits_rework": 1, + "review_comments": 7 + }, + { + "number": 325, + "merged_at": "2026-06-12T16:03:39Z", + "commits_total": 10, + "commits_rework": 1, + "review_comments": 5 + }, + { + "number": 326, + "merged_at": "2026-06-12T16:37:00Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 327, + "merged_at": "2026-06-15T09:20:17Z", + "commits_total": 14, + "commits_rework": 3, + "review_comments": 11 + }, + { + "number": 328, + "merged_at": "2026-06-16T12:25:14Z", + "commits_total": 3, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 329, + "merged_at": "2026-06-18T01:09:09Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 330, + "merged_at": "2026-06-18T09:54:52Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 331, + "merged_at": "2026-06-18T14:05:04Z", + "commits_total": 4, + "commits_rework": 1, + "review_comments": 0 + }, + { + "number": 332, + "merged_at": "2026-06-19T06:11:39Z", + "commits_total": 5, + "commits_rework": 2, + "review_comments": 3 + }, + { + "number": 333, + "merged_at": "2026-06-19T11:10:56Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 334, + "merged_at": "2026-06-19T22:46:15Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 336, + "merged_at": "2026-06-19T23:47:54Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 1 + }, + { + "number": 340, + "merged_at": "2026-06-19T23:54:01Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 389, + "merged_at": "2026-06-20T06:47:36Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 391, + "merged_at": "2026-06-20T06:04:29Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 394, + "merged_at": "2026-06-20T07:13:20Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 398, + "merged_at": "2026-06-22T11:51:46Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 400, + "merged_at": "2026-06-22T13:19:32Z", + "commits_total": 4, + "commits_rework": 1, + "review_comments": 2 + }, + { + "number": 401, + "merged_at": "2026-06-24T08:19:30Z", + "commits_total": 5, + "commits_rework": 2, + "review_comments": 6 + }, + { + "number": 408, + "merged_at": "2026-06-25T05:44:44Z", + "commits_total": 5, + "commits_rework": 3, + "review_comments": 8 + }, + { + "number": 409, + "merged_at": "2026-06-26T02:11:58Z", + "commits_total": 2, + "commits_rework": 1, + "review_comments": 2 + }, + { + "number": 417, + "merged_at": "2026-06-27T06:23:38Z", + "commits_total": 2, + "commits_rework": 1, + "review_comments": 2 + }, + { + "number": 418, + "merged_at": "2026-06-27T10:36:34Z", + "commits_total": 2, + "commits_rework": 1, + "review_comments": 0 + }, + { + "number": 419, + "merged_at": "2026-06-27T12:03:19Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 421, + "merged_at": "2026-06-27T13:02:32Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 422, + "merged_at": "2026-06-28T06:32:16Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 423, + "merged_at": "2026-06-28T09:32:59Z", + "commits_total": 5, + "commits_rework": 0, + "review_comments": 4 + }, + { + "number": 424, + "merged_at": "2026-06-28T13:36:25Z", + "commits_total": 3, + "commits_rework": 1, + "review_comments": 2 + }, + { + "number": 425, + "merged_at": "2026-06-28T13:35:23Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 426, + "merged_at": "2026-06-28T13:35:53Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 427, + "merged_at": "2026-06-28T13:49:53Z", + "commits_total": 3, + "commits_rework": 1, + "review_comments": 0 + }, + { + "number": 428, + "merged_at": "2026-06-28T14:43:28Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 429, + "merged_at": "2026-06-29T00:53:18Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 432, + "merged_at": "2026-06-29T02:40:52Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 433, + "merged_at": "2026-06-29T02:50:05Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 434, + "merged_at": "2026-06-29T05:48:49Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 444, + "merged_at": "2026-06-30T12:48:59Z", + "commits_total": 2, + "commits_rework": 1, + "review_comments": 0 + }, + { + "number": 445, + "merged_at": "2026-06-30T14:02:24Z", + "commits_total": 2, + "commits_rework": 1, + "review_comments": 4 + }, + { + "number": 446, + "merged_at": "2026-06-30T22:45:58Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 447, + "merged_at": "2026-07-01T00:20:14Z", + "commits_total": 1, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 449, + "merged_at": "2026-07-02T00:15:14Z", + "commits_total": 2, + "commits_rework": 0, + "review_comments": 0 + }, + { + "number": 450, + "merged_at": "2026-07-02T13:23:47Z", + "commits_total": 3, + "commits_rework": 2, + "review_comments": 0 + }, + { + "number": 451, + "merged_at": "2026-07-02T14:07:44Z", + "commits_total": 2, + "commits_rework": 1, + "review_comments": 0 + } +] diff --git a/docs/metrics/ai-friendliness.md b/docs/metrics/ai-friendliness.md new file mode 100644 index 00000000..a84eaecd --- /dev/null +++ b/docs/metrics/ai-friendliness.md @@ -0,0 +1,78 @@ +# AI フレンドリーさダッシュボード + + + +「AI エージェントが不変条件を壊さず開発できるアーキテクチャ」(scoped rules / SSoT lint / +codegen drift 検知)の効果を、GitHub 履歴から取れる代理指標の**月次推移**で観測する。 + +- 再生成: `make metrics-ai-friendliness`(判定パターン変更後は `--full` で全再取得) +- データ正本: [`ai-friendliness-data.json`](./ai-friendliness-data.json)(PR 単位の計測値。マージ済み PR は不変のため差分のみ取得) +- 最終生成日: 2026-07-02 + +## 指標の定義 + +| 指標 | 定義 | 望ましい向き | +|---|---|---| +| 手戻りコミット率 | PR 内コミット(Merge 除く)のうち `review(...)` / `fix(ci)` 等のレビュー・CI 対応コミットの割合 | 低いほど「一発で不変条件を守れている」 | +| CodeRabbit 指摘数 / PR | PR の inline review comments のうち CodeRabbit による件数の PR 平均 | 低いほど機械レビュー前に品質が担保されている | +| ルール追記コミット数 | `.claude/rules` / `CLAUDE.md`(旧 `AGENTS.md`)に触れたコミット数 | 事故由来の追記が減り漸近するのが理想(proxy) | + +## 読み方の注意(交絡) + +- **比較が有効なのは 2026-05 以降のみ**。レビュー対応コミット規約(`review(...)`)と CodeRabbit の導入以前の月は、手戻り率・指摘数が「ゼロ」に見えるが実際は**観測不能**(規約が無く squash 前の手戻りを識別できない) +- 期間中に **LLM モデル自体が世代交代**しており、改善がアーキテクチャ起因かモデル起因かは分離できない。**因果ではなく記述統計**として読む +- 作者のプロンプト習熟・タスク難度の変動も混ざる +- 直近月は PR 数が少なく率が暴れる(小標本)。月次 PR 数と併せて読む +- ルール追記コミットは構成整理・初期整備も含む proxy であり、すべてが「事故」ではない +- Renovate 等の bot PR は母数から除外している + +## 手戻りコミット率(%) + +```mermaid +xychart-beta + title "手戻りコミット率(% / 月)" + x-axis ["2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07"] + y-axis "%" + line [0, 0, 0, 1, 15.4, 37.5] +``` + +## CodeRabbit 指摘数 / PR + +```mermaid +xychart-beta + title "CodeRabbit 指摘数 / PR(月平均)" + x-axis ["2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07"] + y-axis "件 / PR" + line [0, 0, 0, 1.1, 1.6, 0] +``` + +## 月次 PR 数(母数の文脈) + +```mermaid +xychart-beta + title "マージ済み PR 数(bot 除く / 月)" + x-axis ["2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07"] + y-axis "件" + bar [9, 92, 64, 74, 64, 4] +``` + +## ルール追記コミット数 + +```mermaid +xychart-beta + title "rules / CLAUDE.md 追記コミット数(月)" + x-axis ["2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07"] + y-axis "件" + bar [0, 4, 2, 26, 22, 2] +``` + +## 月次データ + +| 月 | PR 数 | コミット | 手戻り | 手戻り率 % | 指摘 | 指摘 / PR | ルール追記 | +|---|---|---|---|---|---|---|---| +| 2026-02 | 9 | 11 | 0 | 0 | 0 | 0 | 0 | +| 2026-03 | 92 | 346 | 0 | 0 | 0 | 0 | 4 | +| 2026-04 | 64 | 622 | 0 | 0 | 0 | 0 | 2 | +| 2026-05 | 74 | 401 | 4 | 1 | 79 | 1.1 | 26 | +| 2026-06 | 64 | 169 | 26 | 15.4 | 101 | 1.6 | 22 | +| 2026-07 | 4 | 8 | 3 | 37.5 | 0 | 0 | 2 | diff --git a/scripts/lint-env-keys.sh b/scripts/lint-env-keys.sh index 315e7f35..124c8a2e 100755 --- a/scripts/lint-env-keys.sh +++ b/scripts/lint-env-keys.sh @@ -7,14 +7,27 @@ # 複製を消せないため、複製を消す代わりに「複製が正本と一致しているか」を機械検証する。 # # 検証内容: -# (1) env 名: backend/app/core/env_keys.py の定数値(= 実 env 名)が +# (1) env 名(正方向): backend/app/core/env_keys.py の定数値(= 実 env 名)が # docker-compose.yml の environment ブロックにすべて存在するか # (ALLOWLIST で起動時内部フラグ等を除外)。rename / 削除時の同期忘れを CI で止める。 -# (2) エラーコード: backend/app/core/errors.py の ErrorCode 値集合と +# (2) env 名(逆方向): docker-compose.yml / infra/modules/cloud_run/main.tf に +# 書かれた env 名がすべて env_keys.py に存在するか。rename / 削除時に downstream へ +# 旧名が残留する drift と typo を検知する。本番注入経路(cloud_run)は環境変数ごとに +# 注入要否が異なる(ローカル専用の OLLAMA_* 等がある)ため、正方向(すべて注入 +# されているか)は検証できず逆方向のみ検証する。 +# (3) リテラル参照禁止: backend/app が os.getenv("XXX") / os.environ["XXX"] / +# os.environ.get("XXX") の文字列リテラルで env を参照していないか +# (env_keys 定数経由を機械強制する。従来は規律のみだった)。 +# (4) エラーコード: backend/app/core/errors.py の ErrorCode 値集合と # web/src/constants/errorCodes.ts の ERROR_CODES が完全一致するか。 # FE 側の型検査(Record)は FE 内で完結するため、 # BE が新コードを追加して FE 未反映の drift は型エラーにならない。それを補う。 # +# 対象外(意図的): +# - .github/workflows/ci.yml: 実 API を CI から呼ばないため(env_keys.py の +# OPENAI_API_KEY コメント参照) +# - docs/api.md の環境変数表: 手動同期のまま(doc drift は実害が小さい) +# # 正本: # - env 名: backend/app/core/env_keys.py # - エラーコード: backend/app/core/errors.py の ErrorCode @@ -26,6 +39,7 @@ cd "$(dirname "$0")/.." ENV_KEYS="backend/app/core/env_keys.py" COMPOSE="docker-compose.yml" +CLOUD_RUN_TF="infra/modules/cloud_run/main.tf" ERRORS_PY="backend/app/core/errors.py" ERROR_CODES_TS="web/src/constants/errorCodes.ts" @@ -74,7 +88,53 @@ if [ -n "$missing_in_compose" ]; then fail=1 fi -# ── (2) errors.py ErrorCode == errorCodes.ts ERROR_CODES ────────────────── +# ── (2) 逆方向: downstream の env 名 ⊆ env_keys.py ───────────────────────── +# rename / 削除時に compose / cloud_run へ旧名が残留する drift と typo を検知する。 +# +# cloud_run の env 名は 2 形式から取る: +# - 静的 env block: name = "XXX" +# - secret env の locals: XXX = "secret-name"(dynamic env の name は locals キーが正本) +cloud_run_names=$({ + grep -E 'name[[:space:]]+=[[:space:]]+"[A-Z_]+"' "$CLOUD_RUN_TF" \ + | sed -E 's/.*"([A-Z_]+)".*/\1/' + grep -E '^[[:space:]]*[A-Z_]+[[:space:]]*=[[:space:]]*"' "$CLOUD_RUN_TF" \ + | sed -E 's/^[[:space:]]*([A-Z_]+).*/\1/' +} | sort -u) + +unknown_in_compose=$(comm -23 <(printf '%s\n' "$compose_names") <(printf '%s\n' "$env_names")) +unknown_in_cloud_run=$(comm -23 <(printf '%s\n' "$cloud_run_names") <(printf '%s\n' "$env_names")) + +if [ -n "$unknown_in_compose" ] || [ -n "$unknown_in_cloud_run" ]; then + echo "ERROR: $ENV_KEYS に存在しない env 名が downstream に残っています:" >&2 + if [ -n "$unknown_in_compose" ]; then + echo " $COMPOSE:" >&2 + printf ' - %s\n' $unknown_in_compose >&2 + fi + if [ -n "$unknown_in_cloud_run" ]; then + echo " $CLOUD_RUN_TF:" >&2 + printf ' - %s\n' $unknown_in_cloud_run >&2 + fi + echo "" >&2 + echo "rename / 削除した env 名の旧名残留か typo です。downstream 側を追従してください。" >&2 + echo "" >&2 + fail=1 +fi + +# ── (3) backend のリテラル env 参照禁止(env_keys 定数経由を強制) ────────── +# env_keys.py 自身は docstring にリテラル例を含むため除外する。 +literal_refs=$(grep -rnE 'os\.(getenv|environ\.get)\([[:space:]]*"|os\.environ\[[[:space:]]*"' \ + backend/app --include='*.py' | grep -v 'app/core/env_keys\.py' || true) + +if [ -n "$literal_refs" ]; then + echo "ERROR: backend/app に文字列リテラルでの env 参照があります:" >&2 + printf '%s\n' "$literal_refs" | sed 's/^/ /' >&2 + echo "" >&2 + echo "from app.core import env_keys した上で os.getenv(env_keys.XXX) を使ってください。" >&2 + echo "" >&2 + fail=1 +fi + +# ── (4) errors.py ErrorCode == errorCodes.ts ERROR_CODES ────────────────── be_codes=$(grep -E '^[[:space:]]+[A-Z_]+[[:space:]]*=[[:space:]]*"[A-Z_]+"' "$ERRORS_PY" \ | sed -E 's/.*=[[:space:]]*"([A-Z_]+)".*/\1/' | sort -u) fe_codes=$(grep -E '^[[:space:]]+"[A-Z_]+",' "$ERROR_CODES_TS" \ @@ -103,4 +163,4 @@ if [ "$fail" -ne 0 ]; then exit 1 fi -echo "lint-env-keys: OK(env 名 / ErrorCode の SSoT drift なし)" +echo "lint-env-keys: OK(env 名 compose/cloud_run・リテラル参照・ErrorCode の SSoT drift なし)" diff --git a/scripts/metrics-ai-friendliness.sh b/scripts/metrics-ai-friendliness.sh new file mode 100755 index 00000000..99086e12 --- /dev/null +++ b/scripts/metrics-ai-friendliness.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# AI フレンドリーさの推移指標を GitHub 履歴から集計し、ダッシュボード md を再生成する。 +# +# 背景: +# 本リポジトリは「AI エージェントが不変条件を壊さず開発できるアーキテクチャ」を +# 主目的の一つとしている(scoped rules / SSoT lint / codegen drift 検知)。 +# その効果を「そう感じる」で終わらせず、PR 履歴から観測可能な代理指標の月次推移で追う。 +# +# 指標(いずれも月次・マージ済み PR ベース。bot PR は母数から除外): +# (1) 手戻りコミット率: PR 内コミットのうち REWORK_PATTERN(review(...) 等の +# レビュー/CI 対応 prefix)に一致する割合。squash merge 運用のため main の +# git log からは復元できず、GitHub API で PR ごとの生コミットを取得する。 +# (2) CodeRabbit 指摘数 / PR: PR の inline review comments のうち CodeRabbit による件数。 +# (3) ルール追記コミット数: .claude/rules / CLAUDE.md(旧 AGENTS.md)に触れたコミット数。 +# 「不変条件が壊れて再発防止ルールを足した」の代理指標(構成整理も含む proxy)。 +# +# データの正本とキャッシュ: +# - PR ごとの計測値は docs/metrics/ai-friendliness-data.json に保持する。 +# マージ済み PR は不変なので、既取得の PR は再取得しない(差分のみ API を叩く)。 +# - docs/metrics/ai-friendliness.md は本スクリプトが全体を再生成する生成物。手で編集しない。 +# - 判定パターン変更後など全再取得したい場合は --full を付ける(data JSON を作り直す)。 +# +# 依存: gh(認証済み)/ jq / git。ホスト側で実行する(nix devshell 不要)。 +set -euo pipefail + +cd "$(dirname "$0")/.." + +DATA="docs/metrics/ai-friendliness-data.json" +OUT="docs/metrics/ai-friendliness.md" + +# 手戻りコミット判定(コミットメッセージ先頭行に適用)。 +# コミット規約: review(scope): は CodeRabbit / レビュー指摘対応、fix(ci) は CI こけ対応。 +REWORK_PATTERN='^review[(:]|^fix\(ci\)|[Cc]ode[Rr]abbit' +# 集計対象外の bot author(依存更新 PR 等は AI 開発の観測対象ではない) +BOT_PATTERN='renovate|dependabot|coderabbit|github-actions' + +for cmd in gh jq git; do + command -v "$cmd" >/dev/null 2>&1 || { echo "ERROR: $cmd が必要です" >&2; exit 1; } +done + +if [ "${1:-}" = "--full" ]; then + rm -f "$DATA" +fi +mkdir -p docs/metrics +[ -f "$DATA" ] || echo "[]" > "$DATA" + +REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) + +# ── 1. マージ済み PR 一覧(bot 除外) ──────────────────────────────────── +prs=$(gh pr list --state merged --limit 1000 --json number,author,mergedAt \ + | jq --arg bot "$BOT_PATTERN" \ + '[ .[] + | select((.author.is_bot | not) and (((.author.login // "") | test($bot; "i")) | not)) + | {number, mergedAt} ]') + +# ── 2. 未取得 PR の詳細を API から取得して data JSON へ追記 ──────────────── +new_numbers=$(jq -n --argjson prs "$prs" --slurpfile known "$DATA" \ + '(($prs | map(.number)) - ($known[0] | map(.number))) | sort | .[]') + +total_new=$(printf '%s\n' "$new_numbers" | grep -c . || true) +echo "対象 PR: $(jq -n --argjson p "$prs" '$p | length') 件(新規取得: $total_new 件)" + +i=0 +for n in $new_numbers; do + i=$((i + 1)) + merged_at=$(jq -rn --argjson prs "$prs" --argjson n "$n" \ + '$prs[] | select(.number == $n) | .mergedAt') + # --paginate はページごとに JSON 配列を吐くため jq -s 'add' で結合する + msgs=$(gh api "repos/$REPO/pulls/$n/commits" --paginate \ + | jq -s '[(add // [])[] | .commit.message | split("\n")[0]]') + comments=$(gh api "repos/$REPO/pulls/$n/comments" --paginate \ + | jq -s '[(add // [])[] | select(((.user.login // "") | test("coderabbit"; "i")))] | length') + record=$(jq -n --argjson n "$n" --arg m "$merged_at" --argjson msgs "$msgs" \ + --argjson c "$comments" --arg re "$REWORK_PATTERN" ' + { + number: $n, + merged_at: $m, + commits_total: ([$msgs[] | select(startswith("Merge ") | not)] | length), + commits_rework: ([$msgs[] | select(test($re))] | length), + review_comments: $c + }') + jq --argjson r "$record" '. + [$r] | sort_by(.number)' "$DATA" > "$DATA.tmp" \ + && mv "$DATA.tmp" "$DATA" + if [ $((i % 25)) -eq 0 ]; then + echo " ... $i / $total_new" + fi +done + +# ── 3. ルール追記コミット(git log から毎回全量を再計算) ────────────────── +rule_months=$(git log --format=%as -- .claude/rules .claude/CLAUDE.md AGENTS.md \ + | cut -c1-7 | sort | uniq -c \ + | awk '{printf "{\"month\":\"%s\",\"count\":%s}\n", $2, $1}' | jq -s '.') + +# ── 4. 月次集計 ─────────────────────────────────────────────────────────── +summary=$(jq --argjson rules "$rule_months" ' + group_by(.merged_at[:7]) + | map( + { + month: .[0].merged_at[:7], + prs: length, + commits: (map(.commits_total) | add), + rework: (map(.commits_rework) | add), + comments: (map(.review_comments) | add) + } + | . + { + rework_rate: (if .commits > 0 then ((1000 * .rework / .commits | round) / 10) else 0 end), + comments_per_pr: ((10 * .comments / .prs | round) / 10) + } + | . as $row + | $row + {rule_commits: (($rules | map(select(.month == $row.month) | .count) | first) // 0)} + ) + | sort_by(.month)' "$DATA") + +# ── 5. ダッシュボード md を再生成 ───────────────────────────────────────── +# 観測開始月: レビュー対応コミット規約 / CodeRabbit が観測に現れた最初の月。 +# それ以前の月の手戻り率・指摘数は「ゼロ」ではなく「観測不能」(比較対象外)。 +observable_from=$(jq -r '[.[] | select(.rework > 0 or .comments > 0)] | first | .month // "N/A"' <<<"$summary") +months_q=$(jq -r '[.[].month | "\"" + . + "\""] | join(", ")' <<<"$summary") +rework_vals=$(jq -r '[.[].rework_rate] | join(", ")' <<<"$summary") +comment_vals=$(jq -r '[.[].comments_per_pr] | join(", ")' <<<"$summary") +pr_vals=$(jq -r '[.[].prs] | join(", ")' <<<"$summary") +rule_vals=$(jq -r '[.[].rule_commits] | join(", ")' <<<"$summary") +table_rows=$(jq -r '.[] | "| \(.month) | \(.prs) | \(.commits) | \(.rework) | \(.rework_rate) | \(.comments) | \(.comments_per_pr) | \(.rule_commits) |"' <<<"$summary") +generated_at=$(date '+%Y-%m-%d') + +cat > "$OUT" < + +「AI エージェントが不変条件を壊さず開発できるアーキテクチャ」(scoped rules / SSoT lint / +codegen drift 検知)の効果を、GitHub 履歴から取れる代理指標の**月次推移**で観測する。 + +- 再生成: \`make metrics-ai-friendliness\`(判定パターン変更後は \`--full\` で全再取得) +- データ正本: [\`ai-friendliness-data.json\`](./ai-friendliness-data.json)(PR 単位の計測値。マージ済み PR は不変のため差分のみ取得) +- 最終生成日: $generated_at + +## 指標の定義 + +| 指標 | 定義 | 望ましい向き | +|---|---|---| +| 手戻りコミット率 | PR 内コミット(Merge 除く)のうち \`review(...)\` / \`fix(ci)\` 等のレビュー・CI 対応コミットの割合 | 低いほど「一発で不変条件を守れている」 | +| CodeRabbit 指摘数 / PR | PR の inline review comments のうち CodeRabbit による件数の PR 平均 | 低いほど機械レビュー前に品質が担保されている | +| ルール追記コミット数 | \`.claude/rules\` / \`CLAUDE.md\`(旧 \`AGENTS.md\`)に触れたコミット数 | 事故由来の追記が減り漸近するのが理想(proxy) | + +## 読み方の注意(交絡) + +- **比較が有効なのは $observable_from 以降のみ**。レビュー対応コミット規約(\`review(...)\`)と CodeRabbit の導入以前の月は、手戻り率・指摘数が「ゼロ」に見えるが実際は**観測不能**(規約が無く squash 前の手戻りを識別できない) +- 期間中に **LLM モデル自体が世代交代**しており、改善がアーキテクチャ起因かモデル起因かは分離できない。**因果ではなく記述統計**として読む +- 作者のプロンプト習熟・タスク難度の変動も混ざる +- 直近月は PR 数が少なく率が暴れる(小標本)。月次 PR 数と併せて読む +- ルール追記コミットは構成整理・初期整備も含む proxy であり、すべてが「事故」ではない +- Renovate 等の bot PR は母数から除外している + +## 手戻りコミット率(%) + +\`\`\`mermaid +xychart-beta + title "手戻りコミット率(% / 月)" + x-axis [$months_q] + y-axis "%" + line [$rework_vals] +\`\`\` + +## CodeRabbit 指摘数 / PR + +\`\`\`mermaid +xychart-beta + title "CodeRabbit 指摘数 / PR(月平均)" + x-axis [$months_q] + y-axis "件 / PR" + line [$comment_vals] +\`\`\` + +## 月次 PR 数(母数の文脈) + +\`\`\`mermaid +xychart-beta + title "マージ済み PR 数(bot 除く / 月)" + x-axis [$months_q] + y-axis "件" + bar [$pr_vals] +\`\`\` + +## ルール追記コミット数 + +\`\`\`mermaid +xychart-beta + title "rules / CLAUDE.md 追記コミット数(月)" + x-axis [$months_q] + y-axis "件" + bar [$rule_vals] +\`\`\` + +## 月次データ + +| 月 | PR 数 | コミット | 手戻り | 手戻り率 % | 指摘 | 指摘 / PR | ルール追記 | +|---|---|---|---|---|---|---|---| +$table_rows +EOF + +echo "" +echo "生成完了:" +echo " - $OUT" +echo " - $DATA" +jq -r '.[-3:][] | " \(.month): 手戻り率 \(.rework_rate)% / 指摘 \(.comments_per_pr) 件/PR(PR \(.prs) 件)"' <<<"$summary"