背景
ADR-0016 のキャップ実データチューニング(#478 / PR #483)の計測中に見つかった、fetch_repo_tree の 403 握り込みバグ。#478 の完了条件外だったため follow-up として切り出す。
fetch_repo_tree(backend/app/services/intelligence/github/api_client.py:195)は非 200 レスポンスを無条件で ([], True)(partial マーク)として返す:
resp = await client.get(f"/repos/{owner}/{repo}/git/trees/{branch}", params={"recursive": "1"})
if resp.status_code != 200:
return [], True # ← 403 (rate limit) もここで握り潰される
問題
GitHub API は rate limit でも 403 を返す(fetch_repos_raw は api_client.py:93-102 で X-RateLimit-Remaining ヘッダを見て RetryableError を raise し分けている)。ところが fetch_repo_tree は 403 を rate limit かどうか判別せず、リトライ経路(RetryableError)を通さずに「partial な走査結果」として即返してしまう。
結果として、レート制限に当たったリポはリトライされずに黙って部分スキャン扱いになり、そのリポの依存宣言・import・IaC 検出がまるごと取りこぼされる(partial フラグは立つが warning ログすら出ない)。連携全体は成功扱いのまま、スキル証跡だけが静かに欠ける。
完了条件
想定サイズ
小(1 セッション)。決定論的ロジック層の変更のため TDD 対象(.claude/rules/common/tdd.md)。schema / API 契約は不変(migration / codegen 不要)。
参考
背景
ADR-0016 のキャップ実データチューニング(#478 / PR #483)の計測中に見つかった、
fetch_repo_treeの 403 握り込みバグ。#478 の完了条件外だったため follow-up として切り出す。fetch_repo_tree(backend/app/services/intelligence/github/api_client.py:195)は非 200 レスポンスを無条件で([], True)(partial マーク)として返す:問題
GitHub API は rate limit でも 403 を返す(
fetch_repos_rawはapi_client.py:93-102でX-RateLimit-Remainingヘッダを見てRetryableErrorを raise し分けている)。ところがfetch_repo_treeは 403 を rate limit かどうか判別せず、リトライ経路(RetryableError)を通さずに「partial な走査結果」として即返してしまう。結果として、レート制限に当たったリポはリトライされずに黙って部分スキャン扱いになり、そのリポの依存宣言・import・IaC 検出がまるごと取りこぼされる(partial フラグは立つが warning ログすら出ない)。連携全体は成功扱いのまま、スキル証跡だけが静かに欠ける。
完了条件
fetch_repo_treeの 403 を rate limit(X-RateLimit-Remaining: 0/ 429)とそれ以外で判別し、rate limit ならfetch_repos_rawと同様にRetryableError(retry_after付き)を raise するlogger.warningを残す(現状の httpx.HTTPError catch は warning を出しているが、非 200 の早期 return は無言)fetch_repos_rawと重複するなら共通ヘルパへ抽出(_retry_after_from_githubが既にあるので隣接させる /.claude/rules/common/duplication.md)RetryableError、403 + 残量あり(本当の権限エラー)で partial 返却、を分けて検証想定サイズ
小(1 セッション)。決定論的ロジック層の変更のため TDD 対象(
.claude/rules/common/tdd.md)。schema / API 契約は不変(migration / codegen 不要)。参考
backend/app/services/intelligence/github/api_client.py(fetch_repo_tree/fetch_repos_raw/_retry_after_from_github)