From 12cad7a8f0d8bce70df58908aa9c0ca9faa1bb0e Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Thu, 9 Jul 2026 06:52:09 +0900 Subject: [PATCH 1/2] =?UTF-8?q?improve(backend):=20=E3=82=B9=E3=82=AD?= =?UTF-8?q?=E3=83=AB=E6=8E=A8=E8=AB=96=E3=81=AE=E3=82=AD=E3=83=A3=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=82=92=E5=AE=9F=E3=83=87=E3=83=BC=E3=82=BF=E3=81=A7?= =?UTF-8?q?=E3=83=81=E3=83=A5=E3=83=BC=E3=83=8B=E3=83=B3=E3=82=B0=EF=BC=88?= =?UTF-8?q?ADR-0016=20#478=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 連携本人の実データに収集コードを当てて打ち切り発生状況とキャップ反実仮想を 計測し、閾値を見直す。 - verify(D6): ソースキャップをグローバル 30 件 → エコシステム別 50 件へ変更。 monorepo で辞書順先頭のエコシステムが枠を使い切り、direct 依存 26 件中 17 件が昇格漏れしていた(順序戦略の変更は浅い順に勝てないことを確認済み) - IaC(D10): 件数キャップを 30 → 60 へ引き上げ。30 では infra/modules/ 配下の resource 20 種中 11 種を取りこぼしていた(50 で全量一致、余裕込みで 60) - manifest(D9)と各深さ上限は実データで打ち切りゼロ・寄与ゼロを確認し据え置き - ADR-0016 に計測結果ベースの判断根拠と改訂履歴を追記 - エコシステム別キャップの回帰テスト 2 件を追加 スキーマ・API 契約は不変(migration / codegen 不要)。 Co-Authored-By: Claude Fable 5 --- .../services/intelligence/github_collector.py | 29 +++++++++++--- .../tests/test_github_collector_extended.py | 39 +++++++++++++++++++ docs/adr/0016-github-skill-inference.md | 6 ++- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/backend/app/services/intelligence/github_collector.py b/backend/app/services/intelligence/github_collector.py index 03044c81..5c663294 100644 --- a/backend/app/services/intelligence/github_collector.py +++ b/backend/app/services/intelligence/github_collector.py @@ -55,14 +55,21 @@ _MANIFEST_MAX_DEPTH = 4 # D9(c): 1 リポあたり fetch する manifest 件数上限。 _MANIFEST_MAX_COUNT = 20 -# D6: verify で 1 リポあたり fetch するソースファイル件数上限(import サンプリングの打ち切り)。 -_SOURCE_MAX_COUNT = 30 +# D6: verify で 1 リポ・1 エコシステムあたり fetch するソースファイル件数上限 +# (import サンプリングの打ち切り)。実データ計測(#478)で、グローバルキャップだと +# monorepo で先頭のエコシステムが残りを押し出して昇格漏れが増えることが分かったため、 +# エコシステム別に適用する。50 は同計測で単一エコシステムのリポが全量走査に収まり、 +# monorepo でも中規模までの昇格を回収できた値(それ以降はロングテールで頭打ち)。 +_SOURCE_MAX_COUNT_PER_ECOSYSTEM = 50 # D6: verify 対象ソースのセグメント数上限(浅い側を優先サンプリング)。 +# 実データ計測(#478)で深さ 7 以上のソースは昇格に寄与しなかったため据え置き。 _SOURCE_MAX_DEPTH = 6 # D10: IaC(.tf)探索のセグメント数上限。infra/modules/... へ分散するため manifest より深め。 _INFRA_MAX_DEPTH = 6 # D10: 1 リポあたり fetch する IaC ファイル件数上限(浅い側を優先サンプリング)。 -_INFRA_MAX_COUNT = 30 +# 実データ計測(#478)で 30 だと modules 配下の resource(provider 3 種・resource 11 種)を +# 取りこぼした。同計測では 50 で全量一致したため、余裕を持たせて 60 とする。 +_INFRA_MAX_COUNT = 60 # このモジュールの公開 API。``GitHubUserNotFoundError`` は github_link_service が # ``from .github_collector import GitHubUserNotFoundError`` で参照するため再エクスポートする。 @@ -166,9 +173,19 @@ async def _collect_repo_signals( scanner = scanner_for_extension(path) if scanner is not None and scanner.ecosystem in direct_ecosystems: path_scanners[path] = scanner - selected_sources, source_dropped = _select_shallow( - list(path_scanners), _SOURCE_MAX_DEPTH, _SOURCE_MAX_COUNT - ) + # キャップはエコシステム別に適用する(#478)。浅い順の全体キャップだと monorepo で + # 辞書順先頭のサブツリー(例: backend/)が枠を使い切り、他エコシステムのソースが + # 1 件も走査されず昇格漏れになるため、候補をエコシステムごとに分けて絞る。 + paths_by_ecosystem: Dict[str, List[str]] = {} + for path, scanner in path_scanners.items(): + paths_by_ecosystem.setdefault(scanner.ecosystem, []).append(path) + selected_sources: List[str] = [] + for eco_paths in paths_by_ecosystem.values(): + eco_selected, eco_dropped = _select_shallow( + eco_paths, _SOURCE_MAX_DEPTH, _SOURCE_MAX_COUNT_PER_ECOSYSTEM + ) + selected_sources.extend(eco_selected) + source_dropped = source_dropped or eco_dropped for path in selected_sources: scanner = path_scanners[path] content = await fetch_repo_file(client, owner, repo, path) diff --git a/backend/tests/test_github_collector_extended.py b/backend/tests/test_github_collector_extended.py index 11902652..2850a12e 100644 --- a/backend/tests/test_github_collector_extended.py +++ b/backend/tests/test_github_collector_extended.py @@ -418,6 +418,45 @@ def test_verify_skips_source_without_direct_deps(self): assert "main.go" not in fetched assert "go" not in imported + def test_verify_source_cap_is_applied_per_ecosystem(self, monkeypatch): + """source の件数キャップはエコシステム別に効くこと(#478)。 + + グローバルキャップだと辞書順で先行するエコシステムが枠を使い切り、 + 他エコシステムのソースが 1 件も走査されなくなる(実データで確認した昇格漏れ)。 + """ + monkeypatch.setattr(github_collector, "_SOURCE_MAX_COUNT_PER_ECOSYSTEM", 1) + _decls, _imported, partial, fetched = self._patched_collect( + # pypi(requirements.txt)と npm(package.json)の両方に direct 宣言がある + # monorepo。ソースは backend/(.py)が辞書順で web/(.ts)に先行する。 + [ + "requirements.txt", + "package.json", + "backend/a.py", + "backend/b.py", + "web/a.ts", + "web/b.ts", + ], + False, + ) + # 各エコシステムから浅い順で 1 件ずつ走査される(npm が押し出されない) + assert "backend/a.py" in fetched + assert "web/a.ts" in fetched + assert "backend/b.py" not in fetched + assert "web/b.ts" not in fetched + # どちらのエコシステムも打ち切りが発生しているので partial + assert partial is True + + def test_verify_source_count_cap_marks_partial(self, monkeypatch): + """source の件数キャップ打ち切りを partial として伝播すること(D6 / D9 d)。""" + monkeypatch.setattr(github_collector, "_SOURCE_MAX_COUNT_PER_ECOSYSTEM", 1) + _decls, _imported, partial, fetched = self._patched_collect( + ["requirements.txt", "app.py", "sub/deep.py"], False + ) + # 浅い順で app.py のみ走査し、打ち切りを partial にする + assert "app.py" in fetched + assert "sub/deep.py" not in fetched + assert partial is True + # ── IaC(.tf)探索(D10)──────────────────────────────────────────────── def test_detects_tf_and_attaches_source_path(self): diff --git a/docs/adr/0016-github-skill-inference.md b/docs/adr/0016-github-skill-inference.md index a8f5731b..f210962a 100644 --- a/docs/adr/0016-github-skill-inference.md +++ b/docs/adr/0016-github-skill-inference.md @@ -160,8 +160,9 @@ Layer 1 を単一型にせず、`LanguageSkill` と `PackageSkill` に型分割 ## 将来の移行条件 - **verify ステージ**: D6 として実装済み(2026-06)。recursive Trees API の 1 コールを manifest 探索と共有し、direct 宣言のエコシステムの source だけを浅い順・件数キャップでサンプリング → import 解析。辞書レスの保守的照合(`-`→`_` 変換・go 接頭辞一致・npm 完全一致)で direct 宣言を `actual_import` へ**昇格のみ**(降格なし)。 - - **import 名乖離の補正(#477 / 2026-07)**: 機械変換で当たらない配布名≠import名の乖離(PyYAML→yaml・Pillow→PIL・beautifulsoup4→bs4 等)を、pypi のみ内部マスタ(`skills/resources/pypi_import_aliases.json`)経由で補正する。**D3(辞書を持たない)とのテンション**は `linguist_master.json` と同じモデルで解消: マスタは wheel の `top_level.txt` 由来の機械的事実であり taxonomy 判断を含まない/連携ホットパスで外部を叩かず実行時は読むだけ(D4)/`top_level.txt` からのオフライン再生成を想定した暫定キュレーションで、既知の乖離 package を初期集合とする。`google.*` のような汎用名前空間へ畳まれる package(protobuf 等)は false positive を避け意図的に除外。マスタ未収録の乖離は引き続き false negative として受容(昇格漏れのみ・過剰昇格なし)。残課題は初期集合の実データ拡張・サンプリング閾値の実データチューニング。 -- **monorepo 対応**: D9 で採用済み(recursive Trees API + パスセグメント除外 + 深さ/件数キャップ + keep-all)。残課題は除外定義の高度化(Linguist `vendor.yml` 流用)・キャップ閾値の実データチューニング・規模シグナルの導入。 + - **import 名乖離の補正(#477 / 2026-07)**: 機械変換で当たらない配布名≠import名の乖離(PyYAML→yaml・Pillow→PIL・beautifulsoup4→bs4 等)を、pypi のみ内部マスタ(`skills/resources/pypi_import_aliases.json`)経由で補正する。**D3(辞書を持たない)とのテンション**は `linguist_master.json` と同じモデルで解消: マスタは wheel の `top_level.txt` 由来の機械的事実であり taxonomy 判断を含まない/連携ホットパスで外部を叩かず実行時は読むだけ(D4)/`top_level.txt` からのオフライン再生成を想定した暫定キュレーションで、既知の乖離 package を初期集合とする。`google.*` のような汎用名前空間へ畳まれる package(protobuf 等)は false positive を避け意図的に除外。マスタ未収録の乖離は引き続き false negative として受容(昇格漏れのみ・過剰昇格なし)。残課題は初期集合の実データ拡張。 + - **サンプリング閾値の実データチューニング(#478 / 2026-07)**: 連携実データで計測した結果、グローバル 30 件キャップは monorepo(npm+pypi 同居・候補 514 ファイル)で辞書順先頭のサブツリーが枠を使い切り、全量なら昇格する direct 依存 26 件中 17 件を取りこぼしていた。順序戦略の変更(ディレクトリ分散・ランダム)は浅い順に勝てず(import が深い数ファイルに局在するため)、**キャップをエコシステム別 50 件に変更**(単一エコシステムのリポは全量走査に収まり、monorepo の押し出しが解消。50 超はロングテールで頭打ち)。深さ上限 6 は深さ 7 以上のソースが昇格に寄与しなかったため据え置き。残る昇格漏れ(26 件中 9 件・全量走査には約 500 ファイル必要)はコスト対効果からロングテールとして受容(false negative のみ・declare 証跡は残る)。 +- **monorepo 対応**: D9 で採用済み(recursive Trees API + パスセグメント除外 + 深さ/件数キャップ + keep-all)。キャップ閾値の実データチューニングは #478(2026-07)で実施: manifest キャップ(深さ 4 / 件数 20)は実データ最大が 4 件・深さ 3 で打ち切りゼロのため据え置き。IaC キャップは 30 件だと `infra/modules/` 配下の resource を取りこぼした(検出 20 種中 11 種が漏れ。50 件で全量一致)ため **60 件へ引き上げ**(深さ 6 は据え置き)。残課題は除外定義の高度化(Linguist `vendor.yml` 流用)・規模シグナルの導入。 - **IaC からのインフラリソース検出**: 機械検出は **D10 で採用・実装済み**(2026-07。Terraform/OpenTofu・provider+service 粒度・kind=infra・signal_source=infra_declared・D9 探索流用・正規表現 parser)。残課題は表示名の HITL 畳み込み・動的 module 解決・Tier2 IaC・resource 出現回数の量的シグナル。 - **private リポジトリの扱い**: Layer 3 経由で人間が深さを補完する。生データは持ち込まない前提を維持する。 - **deps.dev エンリッチ**: 横断名寄せの範囲・実行タイミング。 @@ -217,6 +218,7 @@ Layer 1 を単一型にせず、`LanguageSkill` と `PackageSkill` に型分割 ## 改訂履歴 +- **2026-07**: D6/D9/D10 の**キャップ・サンプリング閾値を実データでチューニング(#478)**。連携本人の実データに収集コードを当てて打ち切り発生状況とキャップ反実仮想(値を変えた場合の検出数推移)を計測した。verify のソースキャップをグローバル 30 件 → **エコシステム別 50 件**へ変更(monorepo で先頭エコシステムが枠を使い切る押し出しを解消。順序戦略の変更は浅い順に勝てないことを確認)。IaC キャップを 30 → **60 件**へ引き上げ(30 では resource 20 種中 11 種を取りこぼし、50 で全量一致)。manifest キャップ(深さ 4 / 件数 20)と各深さ上限は実データで打ち切りゼロ・寄与ゼロを確認し据え置き。スキーマ・API 契約は不変。3 層モデル・D1〜D10 は不変。 - **2026-07**: verify(D6)の **import 名乖離の取りこぼしを低減(#477)**。pypi のみ、機械変換で当たらない配布名≠import名の乖離を内部マスタ(`skills/resources/pypi_import_aliases.json`)で補正。D3 テンションは `linguist_master.json` と同じ「ホットパス外で生成する暫定キュレーション・実行時は読むだけ」モデルで解消。既知の乖離 package を初期集合とし、`google.*` 等の汎用名前空間は false positive 回避のため除外。未収録は引き続き false negative 受容(過剰昇格なし)。schema / API 契約は不変(migration 不要)。3 層モデル・D1〜D10 は不変。 - **2026-07**: 「将来課題」だった IaC からのインフラリソース検出を **D10 として採用・実装**(Terraform/OpenTofu の `.tf` を対象に provider+service を抽出、kind=`infra` / signal_source=`infra_declared`、static resource ブロック限定、正規表現 parser で依存なし、D9 探索流用で `.terraform` 除外を追加、canonical=raw type で keep-all)。表示名の human-in-the-loop 畳み込み・動的 module 解決・Tier2 IaC・出現回数の量的シグナルは残課題。kind / signal_source / ecosystem は既存カラムの値域内のため migration 不要。3 層モデル・D1〜D9 は不変。 - **2026-06**: 当初「代替案」で延期していた monorepo サブツリー探索を **D9 として採用**(recursive Trees API + パスセグメント除外 + 深さ/件数キャップ + keep-all + manifest パス永続化)。3 層モデル・D1〜D8 は不変。当初は別 ADR 案だったが、0016 の核を維持する refine であり 1 箇所の追補に留まるため、本 ADR への統合とした。 From 5a1315121093390501c5e09393754acf80f2f0b8 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Thu, 9 Jul 2026 07:08:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?improve(backend):=20source=20fetch=20?= =?UTF-8?q?=E4=BB=B6=E6=95=B0=E3=82=92=20DEBUG=20=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=81=AB=E6=AE=8B=E3=81=99=EF=BC=88#478=20=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E5=8F=8D=E6=98=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit エコシステム別キャップで 1 リポあたりの source fetch 件数が増えうるため、 GitHub API 消費量を監視できるよう fetch 件数(tree_blobs / manifest / source / infra)を DEBUG ログに残す。既存の module logger を再利用し、レート制限・ バックオフは api_client 側の warning ログをそのまま流用する(重複させない)。 Co-Authored-By: Claude Opus 4.8 --- .../app/services/intelligence/github_collector.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/app/services/intelligence/github_collector.py b/backend/app/services/intelligence/github_collector.py index 5c663294..922db4ee 100644 --- a/backend/app/services/intelligence/github_collector.py +++ b/backend/app/services/intelligence/github_collector.py @@ -166,6 +166,7 @@ async def _collect_repo_signals( } imported_symbols: Dict[str, set] = {} source_dropped = False + selected_sources: List[str] = [] if direct_ecosystems: # path → scanner を一度だけ引き、後段の取得ループで再計算しない。 path_scanners = {} @@ -179,7 +180,6 @@ async def _collect_repo_signals( paths_by_ecosystem: Dict[str, List[str]] = {} for path, scanner in path_scanners.items(): paths_by_ecosystem.setdefault(scanner.ecosystem, []).append(path) - selected_sources: List[str] = [] for eco_paths in paths_by_ecosystem.values(): eco_selected, eco_dropped = _select_shallow( eco_paths, _SOURCE_MAX_DEPTH, _SOURCE_MAX_COUNT_PER_ECOSYSTEM @@ -215,6 +215,18 @@ async def _collect_repo_signals( partial = bool(truncated or manifest_dropped or source_dropped) infra_partial = bool(truncated or infra_dropped) + # 取得ボリュームの可観測性(#478): エコシステム別キャップで source fetch 件数が + # 増えうるため、GitHub API 消費量を監視できるよう 1 リポあたりの fetch 件数を残す。 + # レート制限・バックオフ自体は api_client 側で warning ログ化済み(ここでは重複させない)。 + logger.debug( + "ツリー走査 fetch 件数: %s/%s (tree_blobs=%d, manifest=%d, source=%d, infra=%d)", + owner, + repo, + len(tree_paths), + len(selected_manifests), + len(selected_sources), + len(selected_infra), + ) if partial or infra_partial: logger.warning( "ツリー走査が部分的: %s/%s (truncated=%s, manifest_dropped=%s, "