From b02bc7dfc0c34e95341b946388a4d4dfa81aab8d Mon Sep 17 00:00:00 2001 From: 2002yy <15135142681@163.com> Date: Sat, 18 Jul 2026 00:52:13 +0800 Subject: [PATCH 1/2] add real multi-repo GitHub replays --- docs/PROJECT_STATUS.md | 13 +- src/web/github_pr_review_recording.py | 143 ++++ src/web/github_pr_review_replay.py | 23 + .../github_replay/contexts/flask-pr3709.json | 786 ++++++++++++++++++ .../github_replay/contexts/gson-pr705.json | 392 +++++++++ .../github_replay/contexts/vite-pr144.json | 203 +++++ tests/fixtures/github_replay/manifest.json | 71 +- tests/test_github_pr_review_recording.py | 134 +++ tests/test_github_pr_review_replay.py | 99 ++- tools/record_github_replay.py | 58 ++ 10 files changed, 1898 insertions(+), 24 deletions(-) create mode 100644 src/web/github_pr_review_recording.py create mode 100644 tests/fixtures/github_replay/contexts/flask-pr3709.json create mode 100644 tests/fixtures/github_replay/contexts/gson-pr705.json create mode 100644 tests/fixtures/github_replay/contexts/vite-pr144.json create mode 100644 tests/test_github_pr_review_recording.py create mode 100644 tools/record_github_replay.py diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 47b1fe0..2c5068a 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -2,8 +2,8 @@ > **唯一进度入口** > 更新:2026-07-17 -> 当前能力基线:PR #45 已合并,核心学习产品 G1–G8、聊天 ResearchRun 恢复/取消/EvidenceTrail 闭环及 G10-C2 cross-fork Provider 取证已进入 `main` -> 当前代码切片:G10-C3a replay harness 第一小批已落地 immutable manifest、确定性 CLI/汇总和 curated seed 来源隔离;下一步扩充真实 Provider 多仓库 corpus +> 当前能力基线:PR #46 已合并,核心学习产品 G1–G8、聊天 ResearchRun 恢复/取消/EvidenceTrail 闭环、G10-C2 cross-fork Provider 取证及 G10-C3a replay harness 已进入 `main` +> 当前代码切片:G10-C3a 第二小批已录制 Flask/Vite/Gson 真实 Provider replay,当前 4 仓库/5 case;下一步继续扩到 6 仓库/24–30 case 并补失败 CI、rename/delete 与 Provider 截断 这里只回答:**做到哪里、还差什么、下一步做什么**。 @@ -347,6 +347,10 @@ PR #30 已完成主链收口: 1. **G10-C3a 真实 replay harness**:至少 6 个公开仓库、24–30 个 immutable case,覆盖 Python/TypeScript/Java、普通 PR、cross-fork、rename/delete、CI 失败和 Provider 截断;分别报告 symbol mapping 与 CI association precision/recall/F1、coverage、partial rate、请求数、延迟和缓存命中率。 - **第一小批已完成**:新增 schema v1 manifest、immutable base/head SHA 校验、context 路径边界、唯一 case ID、语言/场景/provenance 元数据、确定性 CLI,以及 symbol/CI 微观与宏观指标、Provider status/partial/request/latency/cache 汇总。 - PR #28/#30 已迁移为 2 个 `curated_unit_seed`;报告固定显示 `provider_replay_cases=0`,不会把人工单元样例伪装成真实 Provider replay。下一批仍需采集跨仓库、跨语言真实 context,达到代表性目标前不启用质量门禁。 + - **第二小批已完成录制与本地基线**:新增生产路径录制器,只保存 immutable source、symbol/CI 映射、change-impact label candidates、Provider 预算与覆盖元数据,不保存评论正文、完整源码或令牌;manifest 会校验 recording 的 repository/PR/base/head 与声明完全一致。 + - Flask #3709、Vite #144、Gson #705 形成 3 个真实 Provider replay,覆盖 Python/TypeScript/Java、2 个 cross-fork、历史 review line 丢失、unresolved thread、removed target 和 ambiguous mapping。gold label 由 GitHub diff hunk 与完整 immutable head 文件独立复核,不复制 Provider 预测。 + - 当前 corpus 为 4 仓库/5 case,其中 3 个 Provider replay 全部 partial、合计 30 次请求、真实 replay 平均约 192.3 秒;聚合 symbol mapping precision 0.25 / recall 0.1667 / F1 0.20,三个真实 case 自身均为 0 命中。CI association 1.0 仍主要来自 curated seed,真实 replay 没有失败 CI,不能宣称 CI 质量已验证。 + - 当前结果只作为失败基线:仍缺至少 2 个仓库、19–25 个 case,以及普通非 fork、rename/delete、失败 CI、Provider 截断和缓存复放;达到目标前不设置不可回退阈值,也不进入 RAG-K1。 2. **G10-C3b Provider 证据补齐**:release、artifact metadata/按需下载,以及按 run attempt -> job -> step -> 时间窗口读取日志;所有新结果继续携带 repository、commit SHA、provider status 和 stop reason。 3. **G10-D0 只读 RepositoryWorkspaceRun**:受控临时目录、immutable checkout、Docker sandbox 优先、显式不安全的 process fallback、取消/恢复/过期清理和资源预算;只允许 list/read/search/diff。 4. **G10-D1 确定性命令执行**:仓库配置映射为结构化 `CommandSpec`,只开放声明过的 test/lint/build;保存 stdout/stderr、exit code、timeout、耗时和 artifact,不接受模型拼接任意 shell。 @@ -379,8 +383,8 @@ PR #30 已完成主链收口: ### 统一下一阶段顺序 -1. **已完成**:PR #45 已以 merge commit `972e94e` 合并,当前工作分支从该 `main` 基线创建。 -2. **本切片推进**:G10-C3a replay harness 基础设施和 curated seed 已完成;继续以小批次补真实多仓库 Provider replay,达到 6 仓库/24–30 case 前只记录基线、不宣称代表性质量。 +1. **已完成**:PR #45 与 PR #46 已合并;G10-C3a 基础 harness、curated seed 隔离及首批真实跨语言 Provider replay 已落地。 +2. **本切片推进**:当前 4 仓库/5 case;继续以小批次补真实多仓库 Provider replay,达到 6 仓库/24–30 case 前只记录基线、不宣称代表性质量。 3. G10-C3a 达到最低 corpus 覆盖后,回到核心学习产品,连续完成 RAG-K1 retrieval + answer faithfulness 基线和 RAG-K2 结构化摄取。 4. 根据 K1 数据决定是否先做 RAG-K3,或补 G10-C3b release/artifact/日志定位。 5. 只有上述只读质量门禁稳定后,才进入 G10-D0;G10-D2 可写代理、私有仓库自动执行、GraphRAG 和重型连接器继续后置。 @@ -408,6 +412,7 @@ PR #30 已完成主链收口: - 当前 Provider 分页/跨 owner 切片:review thread comments 嵌套 cursor、共享预算耗尽、fork head checks 优先与 base fallback 均有回归;相关 review/provider 专项 15 passed,Ruff 增量通过。 - 当前 cross-fork change-impact 切片:PR review context 不再返回 unsupported,而是复用 commit-pinned PR comparison 生成 base/head 双仓库源码图;双仓库 snapshot 路由、repository 归属、同 SHA 不同 fork 缓存隔离与 review-context 接线均有回归。聚焦测试 11 passed,GitHub 专项 94 passed,全量 pytest 777 passed,Ruff 全量通过;expanded mypy 当前 126 个既有错误,低于 127 基线且无新增。 - 当前 G10-C3a replay harness 第一小批:PR #28/#30 的 immutable SHA 与 curated context 已进入 schema v1 manifest;CLI 两次输出字节级一致,明确报告 1 个仓库、2 个 seed、0 个 Provider replay。原有独立 golden JSON 已删除,manifest 成为唯一 label 真值;评测/replay 聚焦 7 passed,GitHub 专项 97 passed,全量 pytest 781 passed,Ruff 全量通过,新增模块目标 mypy 通过。因 C 盘空间不足,全量 pytest 的临时目录改到 D 盘后通过。 +- 当前 G10-C3a 第二小批:录制器/manifest/replay/evaluation 聚焦 12 passed,GitHub 专项 97 passed,全量 pytest 786 passed,前端 49 files/172 tests 与生产构建通过,Ruff 全量与 package/secret scan 通过,新录制模块与 CLI 使用 `--follow-imports=skip` 目标 mypy 通过;真实基线为 4 仓库/5 case、3 个 Provider replay、partial rate 0.6、symbol F1 0.20。远端 CI 门禁待本分支发布后确认。 PR #31 功能代码验证: diff --git a/src/web/github_pr_review_recording.py b/src/web/github_pr_review_recording.py new file mode 100644 index 0000000..9cbfb3f --- /dev/null +++ b/src/web/github_pr_review_recording.py @@ -0,0 +1,143 @@ +"""Record compact, immutable GitHub PR review-context provider replays.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from time import perf_counter +from typing import Any + +from src.web.github_pr_review_context import GitHubPRReviewContextService + + +def _dict(value: Any) -> dict[str, Any]: + return dict(value) if isinstance(value, dict) else {} + + +def _items(value: Any) -> list[dict[str, Any]]: + if not isinstance(value, list): + return [] + return [dict(item) for item in value if isinstance(item, dict)] + + +def _review_item(item: dict[str, Any]) -> dict[str, Any]: + return { + "kind": str(item.get("kind") or ""), + "id": str(item.get("id") or ""), + "path": str(item.get("path") or ""), + "line": int(item.get("line") or item.get("original_line") or 0), + "is_resolved": item.get("is_resolved"), + "mapping": _dict(item.get("mapping")), + "hunk_mapping": _dict(item.get("hunk_mapping")), + } + + +def _ci_association(item: dict[str, Any]) -> dict[str, Any]: + check = _dict(item.get("check")) + return { + "check": { + "id": str(check.get("id") or ""), + "name": str(check.get("name") or ""), + "conclusion": str(check.get("conclusion") or ""), + "url": str(check.get("url") or ""), + }, + "association": _dict(item.get("association")), + } + + +def _label_candidates(result: dict[str, Any]) -> list[dict[str, Any]]: + impact = _dict(_dict(result.get("source_evidence")).get("change_impact")) + candidates: list[dict[str, Any]] = [] + seen: set[tuple[str, str]] = set() + for change in _items(impact.get("changes")): + for side in ("old", "new"): + for symbol in _items(change.get(side)): + identity = _dict(symbol.get("identity")) + evidence = _dict(symbol.get("evidence")) + symbol_id = str(identity.get("id") or "") + key = (side, symbol_id) + if not symbol_id or key in seen: + continue + seen.add(key) + candidates.append( + { + "side": side, + "change_type": str(change.get("type") or ""), + "id": symbol_id, + "qualified_name": str(symbol.get("qualified_name") or ""), + "kind": str(symbol.get("kind") or ""), + "language": str(symbol.get("language") or ""), + "path": str(evidence.get("path") or ""), + "start_line": int(evidence.get("start_line") or 0), + "end_line": int(evidence.get("end_line") or 0), + } + ) + return candidates + + +def compact_recording( + result: dict[str, Any], + *, + elapsed_ms: float, + recorded_at: str, +) -> dict[str, Any]: + """Strip provider output to replay evidence without comment/source bodies.""" + + if result.get("ok") is not True: + raise ValueError(str(result.get("error") or "github_provider_replay_failed")) + base = _dict(result.get("base")) + head = _dict(result.get("head")) + base_sha = str(base.get("commit_sha") or "") + head_sha = str(head.get("commit_sha") or "") + if len(base_sha) != 40 or len(head_sha) != 40: + raise ValueError("GitHub provider replay requires immutable base/head SHAs") + request_budget = _dict(result.get("provider_request_budget")) + return { + "provider_status": str(result.get("provider_status") or "unknown"), + "replay_metadata": { + "recorded_at": recorded_at, + "provider_requests": int(request_budget.get("used_requests") or 0), + "elapsed_ms": round(max(0.0, elapsed_ms), 3), + "cache_hit": bool(result.get("cache_hit", False)), + }, + "source": { + "kind": "github_provider", + "repository": str(result.get("repository") or ""), + "pull_request": int(result.get("number") or 0), + "url": str(result.get("url") or ""), + "base_sha": base_sha, + "head_sha": head_sha, + "cross_repository": bool(result.get("cross_repository", False)), + }, + "review_items": [_review_item(item) for item in _items(result.get("review_items"))], + "ci_associations": [ + _ci_association(item) for item in _items(result.get("ci_associations")) + ], + "label_candidates": _label_candidates(result), + "evidence_coverage": _dict(result.get("evidence_coverage")), + "summary": _dict(result.get("summary")), + "uncertainties": _items(result.get("uncertainties")), + "provider_request_budget": request_budget, + "truncated": bool(result.get("truncated", False)), + } + + +def record_github_replay_context( + service: GitHubPRReviewContextService, + repo_url: str, + pull_request: int, + *, + max_provider_requests: int = 32, + max_pages_per_collection: int = 10, +) -> dict[str, Any]: + """Invoke the production provider path and return a compact replay payload.""" + + started = perf_counter() + result = service.build( + repo_url, + pull_request, + max_provider_requests=max_provider_requests, + max_pages_per_collection=max_pages_per_collection, + ) + elapsed_ms = (perf_counter() - started) * 1000 + recorded_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat() + return compact_recording(result, elapsed_ms=elapsed_ms, recorded_at=recorded_at) diff --git a/src/web/github_pr_review_replay.py b/src/web/github_pr_review_replay.py index 69c0830..aaef05c 100644 --- a/src/web/github_pr_review_replay.py +++ b/src/web/github_pr_review_replay.py @@ -165,6 +165,28 @@ def _mean(values: list[float]) -> float: return round(sum(values) / len(values), 3) if values else 0.0 +def _validate_recording_source(case: GitHubReplayCase, context: dict[str, Any]) -> None: + if not case.provider_replay: + return + source = context.get("source") + if not isinstance(source, dict) or source.get("kind") != "github_provider": + raise ValueError(f"Provider replay requires recorded GitHub source: {case.case_id}") + expected = { + "repository": case.repository, + "pull_request": case.pull_request, + "base_sha": case.base_sha, + "head_sha": case.head_sha, + } + mismatches = [field for field, value in expected.items() if source.get(field) != value] + if mismatches: + raise ValueError( + f"Provider replay source mismatch for {case.case_id}: {', '.join(mismatches)}" + ) + metadata = context.get("replay_metadata") + if not isinstance(metadata, dict) or not str(metadata.get("recorded_at") or ""): + raise ValueError(f"Provider replay requires recorded_at: {case.case_id}") + + def evaluate_github_replay_manifest(path: str | Path) -> dict[str, Any]: manifest = load_github_replay_manifest(path) case_results: list[dict[str, Any]] = [] @@ -177,6 +199,7 @@ def evaluate_github_replay_manifest(path: str | Path) -> dict[str, Any]: context = _load_json(case.context_path) if not isinstance(context, dict): raise ValueError(f"GitHub replay context must be an object: {case.case_id}") + _validate_recording_source(case, context) metrics = evaluate_pr_review_context(context, case.expected) replay_metadata = context.get("replay_metadata") metadata = dict(replay_metadata) if isinstance(replay_metadata, dict) else {} diff --git a/tests/fixtures/github_replay/contexts/flask-pr3709.json b/tests/fixtures/github_replay/contexts/flask-pr3709.json new file mode 100644 index 0000000..e5d56ff --- /dev/null +++ b/tests/fixtures/github_replay/contexts/flask-pr3709.json @@ -0,0 +1,786 @@ +{ + "ci_associations": [], + "evidence_coverage": { + "changed_file_impact_coverage": 0.8, + "failed_job_association_coverage": 1.0, + "immutable_ref_coverage": 1.0, + "review_location_hunk_coverage": 0.0, + "review_location_symbol_coverage": 1.0, + "score": 0.76, + "status": "partial", + "unresolved_thread_symbol_coverage": 1.0 + }, + "label_candidates": [ + { + "change_type": "modified", + "end_line": 303, + "id": "symbol_27636b7e9521e5bc32ed52bb", + "kind": "class", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint", + "side": "old", + "start_line": 79 + }, + { + "change_type": "modified", + "end_line": 296, + "id": "symbol_12e7796f919c3d7fd603f7f7", + "kind": "class", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint", + "side": "new", + "start_line": 77 + }, + { + "change_type": "modified", + "end_line": 76, + "id": "symbol_449a4e1b5a7b931770afad49", + "kind": "class", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "BlueprintSetupState", + "side": "old", + "start_line": 10 + }, + { + "change_type": "modified", + "end_line": 74, + "id": "symbol_0762ee290e1b37648ea6c81b", + "kind": "class", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "BlueprintSetupState", + "side": "new", + "start_line": 8 + }, + { + "change_type": "modified", + "end_line": 280, + "id": "symbol_625fdcf30f70d5cd493182bf", + "kind": "class", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "Flask", + "side": "old", + "start_line": 87 + }, + { + "change_type": "modified", + "end_line": 285, + "id": "symbol_8086bcecdc259087a43e7628", + "kind": "class", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "Flask", + "side": "new", + "start_line": 62 + }, + { + "change_type": "added", + "end_line": 297, + "id": "symbol_d7d9771189392411d866e6ca", + "kind": "class", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold", + "side": "new", + "start_line": 34 + }, + { + "change_type": "removed", + "end_line": 84, + "id": "symbol_df98aec0f7ceaf88b588b792", + "kind": "function", + "language": "python", + "path": "src/flask/helpers.py", + "qualified_name": "_endpoint_from_view_func", + "side": "old", + "start_line": 79 + }, + { + "change_type": "modified", + "end_line": 63, + "id": "symbol_3c2872c4efc8a190630c6c7a", + "kind": "function", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "_make_timedelta", + "side": "old", + "start_line": 60 + }, + { + "change_type": "modified", + "end_line": 59, + "id": "symbol_9c59f664a1f6bd2c1c17a8e9", + "kind": "function", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "_make_timedelta", + "side": "new", + "start_line": 56 + }, + { + "change_type": "removed", + "end_line": 298, + "id": "symbol_06137ab688ba0dde4ff95e7e", + "kind": "function", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.endpoint.decorator", + "side": "old", + "start_line": 293 + }, + { + "change_type": "removed", + "end_line": 295, + "id": "symbol_401d20ea91d9f695a394927b", + "kind": "function", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.endpoint.decorator.register_endpoint", + "side": "old", + "start_line": 294 + }, + { + "change_type": "added", + "end_line": 254, + "id": "symbol_16914166dd6de904ab6d0c44", + "kind": "function", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.register.merge_dict_lists", + "side": "new", + "start_line": 248 + }, + { + "change_type": "added", + "end_line": 262, + "id": "symbol_5ed075cb161d97004def3019", + "kind": "function", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.register.merge_dict_nested", + "side": "new", + "start_line": 256 + }, + { + "change_type": "removed", + "end_line": 269, + "id": "symbol_68f23b98276fee030da19031", + "kind": "function", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.route.decorator", + "side": "old", + "start_line": 266 + }, + { + "change_type": "modified", + "end_line": 76, + "id": "symbol_52ba664cc3f5f5d2a5766b92", + "kind": "function", + "language": "python", + "path": "src/flask/helpers.py", + "qualified_name": "get_load_dotenv", + "side": "old", + "start_line": 64 + }, + { + "change_type": "modified", + "end_line": 76, + "id": "symbol_86f36944852416438044bab1", + "kind": "function", + "language": "python", + "path": "src/flask/helpers.py", + "qualified_name": "get_load_dotenv", + "side": "new", + "start_line": 64 + }, + { + "change_type": "added", + "end_line": 206, + "id": "symbol_653748f2575973e9299772cf", + "kind": "function", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.endpoint.decorator", + "side": "new", + "start_line": 204 + }, + { + "change_type": "added", + "end_line": 178, + "id": "symbol_fee0dca1bf4fa15bb21a3a3a", + "kind": "function", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.route.decorator", + "side": "new", + "start_line": 175 + }, + { + "change_type": "moved", + "end_line": 84, + "id": "symbol_06d0d4801195c7022b34913b", + "kind": "function", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "setupmethod", + "side": "old", + "start_line": 66 + }, + { + "change_type": "moved", + "end_line": 31, + "id": "symbol_59e6781ad6ac2de94dcc4cf1", + "kind": "function", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "setupmethod", + "side": "new", + "start_line": 13 + }, + { + "change_type": "moved", + "end_line": 82, + "id": "symbol_9f04267ed11a021b2ce25cee", + "kind": "function", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "setupmethod.wrapper_func", + "side": "old", + "start_line": 71 + }, + { + "change_type": "moved", + "end_line": 29, + "id": "symbol_86de80c6e0d79f632993e9eb", + "kind": "function", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "setupmethod.wrapper_func", + "side": "new", + "start_line": 18 + }, + { + "change_type": "modified", + "end_line": 159, + "id": "symbol_6e6497804350314df705e2b8", + "kind": "function", + "language": "python", + "path": "src/flask/helpers.py", + "qualified_name": "stream_with_context", + "side": "old", + "start_line": 87 + }, + { + "change_type": "modified", + "end_line": 151, + "id": "symbol_fdfa71a566e6792aefca2061", + "kind": "function", + "language": "python", + "path": "src/flask/helpers.py", + "qualified_name": "stream_with_context", + "side": "new", + "start_line": 79 + }, + { + "change_type": "modified", + "end_line": 182, + "id": "symbol_a4b9b214a7d93f34079718d9", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.__init__", + "side": "old", + "start_line": 157 + }, + { + "change_type": "modified", + "end_line": 182, + "id": "symbol_be9c2e87daf195760bad49ec", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.__init__", + "side": "new", + "start_line": 155 + }, + { + "change_type": "added", + "end_line": 185, + "id": "symbol_7d71407a5c8cf7990718f6b3", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint._is_setup_finished", + "side": "new", + "start_line": 184 + }, + { + "change_type": "modified", + "end_line": 283, + "id": "symbol_91da6583ce3b6e8816db6415", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.add_url_rule", + "side": "old", + "start_line": 273 + }, + { + "change_type": "modified", + "end_line": 296, + "id": "symbol_693db363789a760156fba4c0", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.add_url_rule", + "side": "new", + "start_line": 294 + }, + { + "change_type": "removed", + "end_line": 303, + "id": "symbol_9a389137b9fcab7c717c36a5", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.app_template_filter", + "side": "old", + "start_line": 302 + }, + { + "change_type": "removed", + "end_line": 300, + "id": "symbol_65b0626b8813e3016cd3f2b3", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.endpoint", + "side": "old", + "start_line": 285 + }, + { + "change_type": "modified", + "end_line": 200, + "id": "symbol_ae2dd77d296eeea6c568508c", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.record", + "side": "old", + "start_line": 184 + }, + { + "change_type": "modified", + "end_line": 203, + "id": "symbol_898787ba3d0a8ee0f6b4afde", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.record", + "side": "new", + "start_line": 187 + }, + { + "change_type": "modified", + "end_line": 259, + "id": "symbol_ad9a14cd116f56c4132b1d7f", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.register", + "side": "old", + "start_line": 222 + }, + { + "change_type": "modified", + "end_line": 292, + "id": "symbol_dee65d4a469e240032e1b5d4", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.register", + "side": "new", + "start_line": 225 + }, + { + "change_type": "removed", + "end_line": 271, + "id": "symbol_6262db14ef4eba3c2bc9b073", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint.route", + "side": "old", + "start_line": 261 + }, + { + "change_type": "modified", + "end_line": 76, + "id": "symbol_812fabedb003d1d9b6449b81", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "BlueprintSetupState.add_url_rule", + "side": "old", + "start_line": 54 + }, + { + "change_type": "modified", + "end_line": 74, + "id": "symbol_d2a4511b5ea84a5bb292f1f2", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "BlueprintSetupState.add_url_rule", + "side": "new", + "start_line": 52 + }, + { + "change_type": "added", + "end_line": 145, + "id": "symbol_6e4678527b0447a12133e5f2", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.__init__", + "side": "new", + "start_line": 58 + }, + { + "change_type": "added", + "end_line": 148, + "id": "symbol_27410495762552e364dd69c9", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold._is_setup_finished", + "side": "new", + "start_line": 147 + }, + { + "change_type": "added", + "end_line": 191, + "id": "symbol_7e2bf0860a49d758b68da08b", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.add_url_rule", + "side": "new", + "start_line": 183 + }, + { + "change_type": "added", + "end_line": 236, + "id": "symbol_f4105f9f7f88fb9f04b2930a", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.after_request", + "side": "new", + "start_line": 225 + }, + { + "change_type": "added", + "end_line": 222, + "id": "symbol_716aa98a683497af2087e9ae", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.before_request", + "side": "new", + "start_line": 211 + }, + { + "change_type": "added", + "end_line": 281, + "id": "symbol_3404f4f60ef1d09384ff5ad3", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.context_processor", + "side": "new", + "start_line": 278 + }, + { + "change_type": "added", + "end_line": 208, + "id": "symbol_2629def86888d86842173937", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.endpoint", + "side": "new", + "start_line": 193 + }, + { + "change_type": "added", + "end_line": 180, + "id": "symbol_0ca89847d70f743f202811bb", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.route", + "side": "new", + "start_line": 150 + }, + { + "change_type": "added", + "end_line": 275, + "id": "symbol_139ea0f69bdfc1d0a5646143", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.teardown_request", + "side": "new", + "start_line": 239 + }, + { + "change_type": "added", + "end_line": 297, + "id": "symbol_6c3b76825ff1c187622086cd", + "kind": "method", + "language": "python", + "path": "src/flask/scaffold.py", + "qualified_name": "Scaffold.url_value_preprocessor", + "side": "new", + "start_line": 284 + } + ], + "provider_request_budget": { + "exhausted": false, + "exhausted_operations": [], + "max_pages_per_collection": 10, + "max_requests": 40, + "operations": { + "check_runs:MLH-Fellowship/flask": 1, + "pull_request": 1, + "pull_request_base_commit:pallets/flask": 1, + "pull_request_files": 1, + "pull_request_head_commit:MLH-Fellowship/flask": 1, + "pull_request_issue_comments": 1, + "pull_request_review_comments": 1, + "pull_request_review_threads": 1, + "pull_request_reviews": 1, + "workflow_runs:MLH-Fellowship/flask": 1 + }, + "remaining_requests": 30, + "scope": "pull_request_rest_graphql_calls", + "used_requests": 10 + }, + "provider_status": "partial", + "replay_metadata": { + "cache_hit": false, + "elapsed_ms": 57440.343, + "provider_requests": 10, + "recorded_at": "2026-07-17T16:15:58+00:00" + }, + "review_items": [ + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg5MjU3MDY1OnYy", + "is_resolved": true, + "kind": "review_thread", + "line": 0, + "mapping": { + "change_id": "change_71315cdac98ffa515d724843", + "change_type": "modified", + "confidence": "medium", + "reason": "unique_narrowest_containing_symbol", + "signature_changed": false, + "status": "mapped", + "symbol": { + "evidence": { + "end_line": 59, + "file_sha": "f4d35658095243974aaaff9e43f971ff5640f158", + "kind": "definition", + "path": "src/flask/app.py", + "ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "repository": "MLH-Fellowship/flask", + "start_line": 56, + "symbol": "_make_timedelta", + "tree_sha": "90d8639ffd2e26409a034b8d59bd9393d889c376" + }, + "identity": { + "commit_sha": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "file_sha": "f4d35658095243974aaaff9e43f971ff5640f158", + "id": "symbol_9c59f664a1f6bd2c1c17a8e9", + "kind": "function", + "language": "python", + "path": "src/flask/app.py", + "qualified_name": "_make_timedelta", + "ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "repository": "MLH-Fellowship/flask", + "requested_ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "signature": "def _make_timedelta(value): if not isinstance(value, timedelta): return timedelta(seconds=value) return value", + "tree_sha": "90d8639ffd2e26409a034b8d59bd9393d889c376" + }, + "kind": "function", + "language": "python", + "name": "_make_timedelta", + "qualified_name": "_make_timedelta" + } + }, + "path": "src/flask/app.py" + }, + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg5MjU3MDk2OnYy", + "is_resolved": true, + "kind": "review_thread", + "line": 0, + "mapping": { + "change_id": "change_766347865dc7ad8961d1a2b8", + "change_type": "added", + "confidence": "medium", + "reason": "unique_narrowest_containing_symbol", + "signature_changed": false, + "status": "mapped", + "symbol": { + "evidence": { + "end_line": 185, + "file_sha": "2e030861a1ce73b3662051a91b0d6b56b17a7635", + "kind": "definition", + "path": "src/flask/blueprints.py", + "ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "repository": "MLH-Fellowship/flask", + "start_line": 184, + "symbol": "Blueprint._is_setup_finished", + "tree_sha": "90d8639ffd2e26409a034b8d59bd9393d889c376" + }, + "identity": { + "commit_sha": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "file_sha": "2e030861a1ce73b3662051a91b0d6b56b17a7635", + "id": "symbol_7d71407a5c8cf7990718f6b3", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint._is_setup_finished", + "ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "repository": "MLH-Fellowship/flask", + "requested_ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "signature": "def _is_setup_finished(self): return self.warn_on_modifications and self._got_registered_once", + "tree_sha": "90d8639ffd2e26409a034b8d59bd9393d889c376" + }, + "kind": "method", + "language": "python", + "name": "_is_setup_finished", + "qualified_name": "Blueprint._is_setup_finished" + } + }, + "path": "src/flask/blueprints.py" + }, + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg5NTMwNDU2OnYy", + "is_resolved": true, + "kind": "review_thread", + "line": 0, + "mapping": { + "change_id": "change_766347865dc7ad8961d1a2b8", + "change_type": "added", + "confidence": "medium", + "reason": "unique_narrowest_containing_symbol", + "signature_changed": false, + "status": "mapped", + "symbol": { + "evidence": { + "end_line": 185, + "file_sha": "2e030861a1ce73b3662051a91b0d6b56b17a7635", + "kind": "definition", + "path": "src/flask/blueprints.py", + "ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "repository": "MLH-Fellowship/flask", + "start_line": 184, + "symbol": "Blueprint._is_setup_finished", + "tree_sha": "90d8639ffd2e26409a034b8d59bd9393d889c376" + }, + "identity": { + "commit_sha": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "file_sha": "2e030861a1ce73b3662051a91b0d6b56b17a7635", + "id": "symbol_7d71407a5c8cf7990718f6b3", + "kind": "method", + "language": "python", + "path": "src/flask/blueprints.py", + "qualified_name": "Blueprint._is_setup_finished", + "ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "repository": "MLH-Fellowship/flask", + "requested_ref": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "signature": "def _is_setup_finished(self): return self.warn_on_modifications and self._got_registered_once", + "tree_sha": "90d8639ffd2e26409a034b8d59bd9393d889c376" + }, + "kind": "method", + "language": "python", + "name": "_is_setup_finished", + "qualified_name": "Blueprint._is_setup_finished" + } + }, + "path": "src/flask/blueprints.py" + } + ], + "source": { + "base_sha": "216d97c21abe9674fe1dd6364b64051b0d09d0b0", + "cross_repository": true, + "head_sha": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "kind": "github_provider", + "pull_request": 3709, + "repository": "pallets/flask", + "url": "https://github.com/pallets/flask/pull/3709" + }, + "summary": { + "affected_test_count": 6, + "associated_failed_job_count": 0, + "changed_file_count": 5, + "failed_check_count": 0, + "failed_job_count": 0, + "impact_file_count": 4, + "mapped_review_hunk_count": 0, + "mapped_review_item_count": 3, + "mapped_unresolved_review_thread_count": 0, + "review_item_count": 3, + "review_submission_count": 3, + "symbol_change_count": 36, + "uncertainty_count": 3, + "unresolved_review_thread_count": 0 + }, + "truncated": false, + "uncertainties": [ + { + "kind": "review_hunk_unmapped", + "path": "src/flask/app.py", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg5MjU3MDY1OnYy", + "review_kind": "review_thread" + }, + { + "kind": "review_hunk_unmapped", + "path": "src/flask/blueprints.py", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg5MjU3MDk2OnYy", + "review_kind": "review_thread" + }, + { + "kind": "review_hunk_unmapped", + "path": "src/flask/blueprints.py", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg5NTMwNDU2OnYy", + "review_kind": "review_thread" + } + ] +} diff --git a/tests/fixtures/github_replay/contexts/gson-pr705.json b/tests/fixtures/github_replay/contexts/gson-pr705.json new file mode 100644 index 0000000..9a4da57 --- /dev/null +++ b/tests/fixtures/github_replay/contexts/gson-pr705.json @@ -0,0 +1,392 @@ +{ + "ci_associations": [], + "evidence_coverage": { + "changed_file_impact_coverage": 1.0, + "failed_job_association_coverage": 1.0, + "immutable_ref_coverage": 1.0, + "review_location_hunk_coverage": 0.0, + "review_location_symbol_coverage": 0.0, + "score": 0.6, + "status": "partial", + "unresolved_thread_symbol_coverage": 0.0 + }, + "label_candidates": [ + { + "change_type": "modified", + "end_line": 78, + "id": "symbol_f27f1dbdb88beaf4312a12ad", + "kind": "class", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber", + "side": "old", + "start_line": 26 + }, + { + "change_type": "modified", + "end_line": 96, + "id": "symbol_12feb965d5a32cbf0362b60b", + "kind": "class", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber", + "side": "new", + "start_line": 26 + }, + { + "change_type": "added", + "end_line": 32, + "id": "symbol_6dd53d869f67ad8f199c26cf", + "kind": "class", + "language": "java", + "path": "gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java", + "qualified_name": "LazilyParsedNumberTest", + "side": "new", + "start_line": 20 + }, + { + "change_type": "modified", + "end_line": 31, + "id": "symbol_50652d31dd80179af19131a2", + "kind": "constructor", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber.LazilyParsedNumber", + "side": "old", + "start_line": 29 + }, + { + "change_type": "modified", + "end_line": 32, + "id": "symbol_d68cebb87b0d2eb348d742a4", + "kind": "constructor", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber.LazilyParsedNumber", + "side": "new", + "start_line": 30 + }, + { + "change_type": "added", + "end_line": 95, + "id": "symbol_dcad3f835941a141a13f93a8", + "kind": "method", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber.equals", + "side": "new", + "start_line": 85 + }, + { + "change_type": "added", + "end_line": 83, + "id": "symbol_ad0654760139046a71227cf9", + "kind": "method", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber.hashCode", + "side": "new", + "start_line": 80 + }, + { + "change_type": "modified", + "end_line": 77, + "id": "symbol_de8a99491d0bf88ef3f29480", + "kind": "method", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber.writeReplace", + "side": "old", + "start_line": 75 + }, + { + "change_type": "modified", + "end_line": 78, + "id": "symbol_84a03756b35009f16fc1b678", + "kind": "method", + "language": "java", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "qualified_name": "LazilyParsedNumber.writeReplace", + "side": "new", + "start_line": 76 + }, + { + "change_type": "added", + "end_line": 31, + "id": "symbol_015d58a1a7b76c50bdad69e0", + "kind": "method", + "language": "java", + "path": "gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java", + "qualified_name": "LazilyParsedNumberTest.testEquals", + "side": "new", + "start_line": 27 + }, + { + "change_type": "added", + "end_line": 25, + "id": "symbol_c5b243b01f95e24599ee7bfd", + "kind": "method", + "language": "java", + "path": "gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java", + "qualified_name": "LazilyParsedNumberTest.testHashCode", + "side": "new", + "start_line": 21 + } + ], + "provider_request_budget": { + "exhausted": false, + "exhausted_operations": [], + "max_pages_per_collection": 10, + "max_requests": 40, + "operations": { + "check_runs:google/gson": 1, + "pull_request": 1, + "pull_request_base_commit:google/gson": 1, + "pull_request_files": 1, + "pull_request_head_commit:google/gson": 1, + "pull_request_issue_comments": 1, + "pull_request_review_comments": 1, + "pull_request_review_threads": 1, + "pull_request_reviews": 1, + "workflow_runs:google/gson": 1 + }, + "remaining_requests": 30, + "scope": "pull_request_rest_graphql_calls", + "used_requests": 10 + }, + "provider_status": "partial", + "replay_metadata": { + "cache_hit": false, + "elapsed_ms": 257453.389, + "provider_requests": 10, + "recorded_at": "2026-07-17T16:24:56+00:00" + }, + "review_items": [ + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDA6djI=", + "is_resolved": false, + "kind": "review_thread", + "line": 0, + "mapping": { + "candidate_count": 2, + "candidates": [ + { + "change_id": "change_c498af2a306ae880245c26ba", + "evidence": { + "end_line": 32, + "file_sha": "3669af7b58dc466e91ebd8f8845fc267ee3d4004", + "kind": "definition", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "ref": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "repository": "google/gson", + "start_line": 30, + "symbol": "LazilyParsedNumber.LazilyParsedNumber", + "tree_sha": "d2ac677cf04bd129576231d155a07a90a41f5edd" + }, + "kind": "constructor", + "qualified_name": "LazilyParsedNumber.LazilyParsedNumber" + }, + { + "change_id": "change_b554faffef1e1847374a115b", + "evidence": { + "end_line": 78, + "file_sha": "3669af7b58dc466e91ebd8f8845fc267ee3d4004", + "kind": "definition", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "ref": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "repository": "google/gson", + "start_line": 76, + "symbol": "LazilyParsedNumber.writeReplace", + "tree_sha": "d2ac677cf04bd129576231d155a07a90a41f5edd" + }, + "kind": "method", + "qualified_name": "LazilyParsedNumber.writeReplace" + } + ], + "confidence": "low", + "reason": "multiple_symbols_share_review_location", + "status": "ambiguous" + }, + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java" + }, + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDI6djI=", + "is_resolved": false, + "kind": "review_thread", + "line": 0, + "mapping": { + "candidate_count": 2, + "candidates": [ + { + "change_id": "change_c498af2a306ae880245c26ba", + "evidence": { + "end_line": 32, + "file_sha": "3669af7b58dc466e91ebd8f8845fc267ee3d4004", + "kind": "definition", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "ref": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "repository": "google/gson", + "start_line": 30, + "symbol": "LazilyParsedNumber.LazilyParsedNumber", + "tree_sha": "d2ac677cf04bd129576231d155a07a90a41f5edd" + }, + "kind": "constructor", + "qualified_name": "LazilyParsedNumber.LazilyParsedNumber" + }, + { + "change_id": "change_b554faffef1e1847374a115b", + "evidence": { + "end_line": 78, + "file_sha": "3669af7b58dc466e91ebd8f8845fc267ee3d4004", + "kind": "definition", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "ref": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "repository": "google/gson", + "start_line": 76, + "symbol": "LazilyParsedNumber.writeReplace", + "tree_sha": "d2ac677cf04bd129576231d155a07a90a41f5edd" + }, + "kind": "method", + "qualified_name": "LazilyParsedNumber.writeReplace" + } + ], + "confidence": "low", + "reason": "multiple_symbols_share_review_location", + "status": "ambiguous" + }, + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java" + }, + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDQ6djI=", + "is_resolved": false, + "kind": "review_thread", + "line": 0, + "mapping": { + "candidate_count": 2, + "candidates": [ + { + "change_id": "change_c498af2a306ae880245c26ba", + "evidence": { + "end_line": 32, + "file_sha": "3669af7b58dc466e91ebd8f8845fc267ee3d4004", + "kind": "definition", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "ref": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "repository": "google/gson", + "start_line": 30, + "symbol": "LazilyParsedNumber.LazilyParsedNumber", + "tree_sha": "d2ac677cf04bd129576231d155a07a90a41f5edd" + }, + "kind": "constructor", + "qualified_name": "LazilyParsedNumber.LazilyParsedNumber" + }, + { + "change_id": "change_b554faffef1e1847374a115b", + "evidence": { + "end_line": 78, + "file_sha": "3669af7b58dc466e91ebd8f8845fc267ee3d4004", + "kind": "definition", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "ref": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "repository": "google/gson", + "start_line": 76, + "symbol": "LazilyParsedNumber.writeReplace", + "tree_sha": "d2ac677cf04bd129576231d155a07a90a41f5edd" + }, + "kind": "method", + "qualified_name": "LazilyParsedNumber.writeReplace" + } + ], + "confidence": "low", + "reason": "multiple_symbols_share_review_location", + "status": "ambiguous" + }, + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java" + } + ], + "source": { + "base_sha": "93605e7145c989d4f38ae347c9f31d605efcf434", + "cross_repository": false, + "head_sha": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "kind": "github_provider", + "pull_request": 705, + "repository": "google/gson", + "url": "https://github.com/google/gson/pull/705" + }, + "summary": { + "affected_test_count": 17, + "associated_failed_job_count": 0, + "changed_file_count": 2, + "failed_check_count": 0, + "failed_job_count": 0, + "impact_file_count": 2, + "mapped_review_hunk_count": 0, + "mapped_review_item_count": 0, + "mapped_unresolved_review_thread_count": 0, + "review_item_count": 3, + "review_submission_count": 0, + "symbol_change_count": 8, + "uncertainty_count": 6, + "unresolved_review_thread_count": 3 + }, + "truncated": false, + "uncertainties": [ + { + "kind": "review_location_ambiguous", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "reason": "multiple_symbols_share_review_location", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDA6djI=", + "review_kind": "review_thread" + }, + { + "kind": "review_hunk_unmapped", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDA6djI=", + "review_kind": "review_thread" + }, + { + "kind": "review_location_ambiguous", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "reason": "multiple_symbols_share_review_location", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDI6djI=", + "review_kind": "review_thread" + }, + { + "kind": "review_hunk_unmapped", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDI6djI=", + "review_kind": "review_thread" + }, + { + "kind": "review_location_ambiguous", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "reason": "multiple_symbols_share_review_location", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDQ6djI=", + "review_kind": "review_thread" + }, + { + "kind": "review_hunk_unmapped", + "path": "gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjg2MDI0MDQ6djI=", + "review_kind": "review_thread" + } + ] +} diff --git a/tests/fixtures/github_replay/contexts/vite-pr144.json b/tests/fixtures/github_replay/contexts/vite-pr144.json new file mode 100644 index 0000000..5554d5a --- /dev/null +++ b/tests/fixtures/github_replay/contexts/vite-pr144.json @@ -0,0 +1,203 @@ +{ + "ci_associations": [], + "evidence_coverage": { + "changed_file_impact_coverage": 1.0, + "failed_job_association_coverage": 1.0, + "immutable_ref_coverage": 1.0, + "review_location_hunk_coverage": 0.0, + "review_location_symbol_coverage": 1.0, + "score": 0.8, + "status": "partial", + "unresolved_thread_symbol_coverage": 1.0 + }, + "label_candidates": [ + { + "change_type": "modified", + "end_line": 156, + "id": "symbol_f2fbc6fa23d33b00baabb56d", + "kind": "interface", + "language": "typescript", + "path": "src/node/config.ts", + "qualified_name": "BuildConfig", + "side": "old", + "start_line": 79 + }, + { + "change_type": "modified", + "end_line": 162, + "id": "symbol_07361df5d1e77802af3c203d", + "kind": "interface", + "language": "typescript", + "path": "src/node/config.ts", + "qualified_name": "BuildConfig", + "side": "new", + "start_line": 80 + }, + { + "change_type": "modified", + "end_line": 163, + "id": "symbol_acd3ff5ffab3ba45572d1df8", + "kind": "interface", + "language": "typescript", + "path": "src/node/config.ts", + "qualified_name": "UserConfig", + "side": "old", + "start_line": 158 + }, + { + "change_type": "modified", + "end_line": 169, + "id": "symbol_492e8a46c3a791af94ce5719", + "kind": "interface", + "language": "typescript", + "path": "src/node/config.ts", + "qualified_name": "UserConfig", + "side": "new", + "start_line": 164 + } + ], + "provider_request_budget": { + "exhausted": false, + "exhausted_operations": [], + "max_pages_per_collection": 10, + "max_requests": 40, + "operations": { + "check_runs:sventschui/vite": 1, + "pull_request": 1, + "pull_request_base_commit:vitejs/vite": 1, + "pull_request_files": 1, + "pull_request_head_commit:sventschui/vite": 1, + "pull_request_issue_comments": 1, + "pull_request_review_comments": 1, + "pull_request_review_threads": 1, + "pull_request_reviews": 1, + "workflow_runs:sventschui/vite": 1 + }, + "remaining_requests": 30, + "scope": "pull_request_rest_graphql_calls", + "used_requests": 10 + }, + "provider_status": "partial", + "replay_metadata": { + "cache_hit": false, + "elapsed_ms": 262120.711, + "provider_requests": 10, + "recorded_at": "2026-07-17T16:25:01+00:00" + }, + "review_items": [ + { + "hunk_mapping": { + "confidence": "low", + "reason": "review_path_or_line_missing", + "status": "unmapped" + }, + "id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjY0ODI3NDEzOnYy", + "is_resolved": true, + "kind": "review_thread", + "line": 0, + "mapping": { + "change_id": "change_88a098da93e65f2f93d003e8", + "change_type": "modified", + "confidence": "medium", + "reason": "unique_narrowest_containing_symbol", + "signature_changed": false, + "status": "mapped", + "symbol": { + "evidence": { + "end_line": 169, + "file_sha": "648ccea3d2776b557ef6b57364cfb0ff0ab4967b", + "kind": "definition", + "path": "src/node/config.ts", + "ref": "90c22918b5b00fc27779d8d844d78935fe0fa836", + "repository": "sventschui/vite", + "start_line": 164, + "symbol": "UserConfig", + "tree_sha": "b3c17181790d389707c2c6151d55e1671b32fa6b" + }, + "identity": { + "commit_sha": "90c22918b5b00fc27779d8d844d78935fe0fa836", + "file_sha": "648ccea3d2776b557ef6b57364cfb0ff0ab4967b", + "id": "symbol_492e8a46c3a791af94ce5719", + "kind": "interface", + "language": "typescript", + "path": "src/node/config.ts", + "qualified_name": "UserConfig", + "ref": "90c22918b5b00fc27779d8d844d78935fe0fa836", + "repository": "sventschui/vite", + "requested_ref": "90c22918b5b00fc27779d8d844d78935fe0fa836", + "signature": "interface UserConfig extends BuildConfig, Pick", + "tree_sha": "b3c17181790d389707c2c6151d55e1671b32fa6b" + }, + "kind": "interface", + "language": "typescript", + "name": "UserConfig", + "qualified_name": "UserConfig" + } + }, + "path": "src/node/config.ts" + } + ], + "source": { + "base_sha": "57697491f3c528941b9746bd4f43563e195d95a4", + "cross_repository": true, + "head_sha": "90c22918b5b00fc27779d8d844d78935fe0fa836", + "kind": "github_provider", + "pull_request": 144, + "repository": "vitejs/vite", + "url": "https://github.com/vitejs/vite/pull/144" + }, + "summary": { + "affected_test_count": 0, + "associated_failed_job_count": 0, + "changed_file_count": 3, + "failed_check_count": 0, + "failed_job_count": 0, + "impact_file_count": 3, + "mapped_review_hunk_count": 0, + "mapped_review_item_count": 1, + "mapped_unresolved_review_thread_count": 0, + "review_item_count": 1, + "review_submission_count": 1, + "symbol_change_count": 2, + "uncertainty_count": 5, + "unresolved_review_thread_count": 0 + }, + "truncated": false, + "uncertainties": [ + { + "detail": { + "kind": "missing_base_file", + "path": "src/node/build/buildPluginHtml.ts" + }, + "kind": "change_impact_uncertainty" + }, + { + "detail": { + "kind": "missing_head_file", + "path": "src/node/build/buildPluginHtml.ts" + }, + "kind": "change_impact_uncertainty" + }, + { + "detail": { + "kind": "missing_base_file", + "path": "src/node/build/index.ts" + }, + "kind": "change_impact_uncertainty" + }, + { + "detail": { + "kind": "missing_head_file", + "path": "src/node/build/index.ts" + }, + "kind": "change_impact_uncertainty" + }, + { + "kind": "review_hunk_unmapped", + "path": "src/node/config.ts", + "reason": "review_path_or_line_missing", + "review_id": "MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMjY0ODI3NDEzOnYy", + "review_kind": "review_thread" + } + ] +} diff --git a/tests/fixtures/github_replay/manifest.json b/tests/fixtures/github_replay/manifest.json index 1a1a010..ebbdc01 100644 --- a/tests/fixtures/github_replay/manifest.json +++ b/tests/fixtures/github_replay/manifest.json @@ -1,5 +1,5 @@ { - "corpus_id": "study-agent-g10-c3a-seed-v1", + "corpus_id": "study-agent-g10-c3a-replay-v2", "schema_version": 1, "cases": [ { @@ -36,6 +36,75 @@ "review_symbol_ids": [], "ci_test_paths": ["tests/test_github_change_impact.py"] } + }, + { + "case_id": "pallets-flask_pr3709-historical-review-lines", + "repository": "pallets/flask", + "pull_request": 3709, + "base_sha": "216d97c21abe9674fe1dd6364b64051b0d09d0b0", + "head_sha": "b146a13f633b0e2e26e3b8383b3db0feb563bc83", + "language": "python", + "scenarios": [ + "provider-replay", + "cross-fork", + "historical-review-line-missing", + "mapping-false-positive" + ], + "context_path": "contexts/flask-pr3709.json", + "provenance": "github_provider_recording", + "provider_replay": true, + "labels": { + "review_symbol_ids": [ + "symbol_8f40a3b30bfb7a567e8bd92c", + "symbol_be9c2e87daf195760bad49ec" + ], + "ci_test_paths": [] + } + }, + { + "case_id": "vite-pr144-build-config-review", + "repository": "vitejs/vite", + "pull_request": 144, + "base_sha": "57697491f3c528941b9746bd4f43563e195d95a4", + "head_sha": "90c22918b5b00fc27779d8d844d78935fe0fa836", + "language": "typescript", + "scenarios": [ + "provider-replay", + "cross-fork", + "historical-review-line-missing", + "mapping-false-positive" + ], + "context_path": "contexts/vite-pr144.json", + "provenance": "github_provider_recording", + "provider_replay": true, + "labels": { + "review_symbol_ids": ["symbol_07361df5d1e77802af3c203d"], + "ci_test_paths": [] + } + }, + { + "case_id": "gson-pr705-removed-review-target", + "repository": "google/gson", + "pull_request": 705, + "base_sha": "93605e7145c989d4f38ae347c9f31d605efcf434", + "head_sha": "96b2ada79aeaa44e4c572b345563d0ee90dde5b1", + "language": "java", + "scenarios": [ + "provider-replay", + "unresolved-review-thread", + "removed-review-target", + "ambiguous-mapping" + ], + "context_path": "contexts/gson-pr705.json", + "provenance": "github_provider_recording", + "provider_replay": true, + "labels": { + "review_symbol_ids": [ + "symbol_ad0654760139046a71227cf9", + "symbol_dcad3f835941a141a13f93a8" + ], + "ci_test_paths": [] + } } ] } diff --git a/tests/test_github_pr_review_recording.py b/tests/test_github_pr_review_recording.py new file mode 100644 index 0000000..9fc078d --- /dev/null +++ b/tests/test_github_pr_review_recording.py @@ -0,0 +1,134 @@ +from src.web.github_pr_review_recording import compact_recording +from tools.record_github_replay import _repository_url + + +def test_repository_url_accepts_owner_name_or_full_url(): + assert _repository_url("pallets/flask") == "https://github.com/pallets/flask" + assert ( + _repository_url("https://github.com/pallets/flask") + == "https://github.com/pallets/flask" + ) + + +def test_compact_recording_keeps_replay_evidence_without_provider_bodies(): + payload = compact_recording( + { + "ok": True, + "provider_status": "complete", + "repository": "example/repo", + "number": 7, + "url": "https://github.com/example/repo/pull/7", + "base": {"commit_sha": "a" * 40}, + "head": {"commit_sha": "b" * 40}, + "review_items": [ + { + "kind": "review_thread", + "id": "thread-1", + "path": "src/app.py", + "line": 12, + "body": "provider comment body must not be recorded", + "mapping": {"status": "mapped", "symbol": {"identity": {"id": "s1"}}}, + "hunk_mapping": {"status": "mapped"}, + } + ], + "ci_associations": [ + { + "check": { + "id": 10, + "name": "tests", + "conclusion": "failure", + "url": "https://example.test/check/10", + "logs": "must not be recorded", + }, + "association": {"status": "associated", "tests": ["tests/test_app.py"]}, + } + ], + "source_evidence": {"large": "must not be recorded"}, + "provider_request_budget": {"used_requests": 9}, + "cache_hit": False, + }, + elapsed_ms=12.3456, + recorded_at="2026-07-17T00:00:00+00:00", + ) + + assert payload["source"]["base_sha"] == "a" * 40 + assert payload["replay_metadata"] == { + "recorded_at": "2026-07-17T00:00:00+00:00", + "provider_requests": 9, + "elapsed_ms": 12.346, + "cache_hit": False, + } + assert payload["review_items"][0]["mapping"]["symbol"]["identity"]["id"] == "s1" + assert payload["ci_associations"][0]["association"]["tests"] == ["tests/test_app.py"] + assert "body" not in payload["review_items"][0] + assert "logs" not in payload["ci_associations"][0]["check"] + assert "source_evidence" not in payload + + +def test_compact_recording_keeps_bounded_change_symbol_candidates_for_labeling(): + payload = compact_recording( + { + "ok": True, + "repository": "example/repo", + "number": 7, + "base": {"commit_sha": "a" * 40}, + "head": {"commit_sha": "b" * 40}, + "source_evidence": { + "change_impact": { + "changes": [ + { + "type": "modified", + "old": [], + "new": [ + { + "qualified_name": "Service.run", + "kind": "method", + "language": "python", + "identity": {"id": "symbol-run"}, + "evidence": { + "path": "src/service.py", + "start_line": 10, + "end_line": 20, + }, + } + ], + } + ] + }, + "large": "not retained", + }, + }, + elapsed_ms=1, + recorded_at="2026-07-17T00:00:00+00:00", + ) + + assert payload["label_candidates"] == [ + { + "side": "new", + "change_type": "modified", + "id": "symbol-run", + "qualified_name": "Service.run", + "kind": "method", + "language": "python", + "path": "src/service.py", + "start_line": 10, + "end_line": 20, + } + ] + + +def test_compact_recording_rejects_non_immutable_refs(): + try: + compact_recording( + { + "ok": True, + "base": {"commit_sha": "main"}, + "head": {"commit_sha": "b" * 40}, + }, + elapsed_ms=0, + recorded_at="2026-07-17T00:00:00+00:00", + ) + except ValueError as exc: + assert "immutable" in str(exc) + else: + raise AssertionError("expected immutable SHA validation failure") diff --git a/tests/test_github_pr_review_replay.py b/tests/test_github_pr_review_replay.py index ef1c7a7..4d93d06 100644 --- a/tests/test_github_pr_review_replay.py +++ b/tests/test_github_pr_review_replay.py @@ -17,45 +17,57 @@ _MANIFEST = _FIXTURE_DIR / "manifest.json" -def test_manifest_loads_immutable_seed_cases_without_claiming_provider_replay(): +def test_manifest_loads_immutable_seed_and_real_provider_cases(): manifest = load_github_replay_manifest(_MANIFEST) - assert manifest.corpus_id == "study-agent-g10-c3a-seed-v1" - assert len(manifest.cases) == 2 + assert manifest.corpus_id == "study-agent-g10-c3a-replay-v2" + assert len(manifest.cases) == 5 assert all(len(case.base_sha) == 40 and len(case.head_sha) == 40 for case in manifest.cases) - assert all(case.provenance == "curated_unit_seed" for case in manifest.cases) - assert not any(case.provider_replay for case in manifest.cases) + assert sum(case.provenance == "curated_unit_seed" for case in manifest.cases) == 2 + assert sum(case.provider_replay for case in manifest.cases) == 3 def test_manifest_summary_reports_quality_coverage_and_provider_limits(): summary = evaluate_github_replay_manifest(_MANIFEST) assert summary["coverage"] == { - "cases": 2, - "repositories": 1, - "repository_names": ["2002yy/study-agent"], - "languages": ["python"], + "cases": 5, + "repositories": 4, + "repository_names": [ + "2002yy/study-agent", + "google/gson", + "pallets/flask", + "vitejs/vite", + ], + "languages": ["java", "python", "typescript"], "scenarios": [ + "ambiguous-mapping", "change-impact-test", + "cross-fork", "empty-review-label", "failed-ci-test", + "historical-review-line-missing", + "mapping-false-positive", + "provider-replay", + "removed-review-target", "review-symbol", + "unresolved-review-thread", ], - "provider_replay_cases": 0, + "provider_replay_cases": 3, "curated_seed_cases": 2, } assert summary["provider"] == { - "status_counts": {"curated": 2}, - "partial_rate": 0.0, - "mean_requests": 0.0, - "mean_elapsed_ms": 0.0, + "status_counts": {"curated": 2, "partial": 3}, + "partial_rate": 0.6, + "mean_requests": 6.0, + "mean_elapsed_ms": 115402.889, "cache_hit_rate": 0.0, } assert summary["metrics"]["macro"] == { - "precision": 1.0, - "recall": 1.0, - "f1": 1.0, - "mean_case_f1": 1.0, + "precision": 0.625, + "recall": 0.5834, + "f1": 0.6, + "mean_case_f1": 0.7, } @@ -96,7 +108,7 @@ def test_replay_cli_writes_deterministic_json(tmp_path: Path): assert completed.returncode == 0, completed.stderr first = output.read_text(encoding="utf-8") - assert json.loads(first)["coverage"]["provider_replay_cases"] == 0 + assert json.loads(first)["coverage"]["provider_replay_cases"] == 3 subprocess.run( [ sys.executable, @@ -108,3 +120,52 @@ def test_replay_cli_writes_deterministic_json(tmp_path: Path): check=True, ) assert output.read_text(encoding="utf-8") == first + + +def test_provider_replay_rejects_manifest_source_mismatch(tmp_path: Path): + context = tmp_path / "context.json" + context.write_text( + json.dumps( + { + "source": { + "kind": "github_provider", + "repository": "wrong/repo", + "pull_request": 1, + "base_sha": "a" * 40, + "head_sha": "b" * 40, + }, + "replay_metadata": {"recorded_at": "2026-07-17T00:00:00+00:00"}, + "review_items": [], + "ci_associations": [], + } + ), + encoding="utf-8", + ) + manifest = tmp_path / "manifest.json" + manifest.write_text( + json.dumps( + { + "corpus_id": "test", + "schema_version": 1, + "cases": [ + { + "case_id": "case-1", + "repository": "right/repo", + "pull_request": 1, + "base_sha": "a" * 40, + "head_sha": "b" * 40, + "language": "python", + "scenarios": ["provider-replay"], + "context_path": "context.json", + "provenance": "github_provider_recording", + "provider_replay": True, + "labels": {"review_symbol_ids": [], "ci_test_paths": []}, + } + ], + } + ), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="source mismatch"): + evaluate_github_replay_manifest(manifest) diff --git a/tools/record_github_replay.py b/tools/record_github_replay.py new file mode 100644 index 0000000..9123137 --- /dev/null +++ b/tools/record_github_replay.py @@ -0,0 +1,58 @@ +"""Record one compact GitHub provider replay through the production service path.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from src.application.runtime_repository import ( + get_github_change_impact_service, + get_github_work_item_service, +) +from src.web.github_pr_review_context import GitHubPRReviewContextService +from src.web.github_pr_review_recording import record_github_replay_context + + +def _repository_url(value: str) -> str: + resolved = value.strip() + if "://" not in resolved and resolved.count("/") == 1: + return f"https://github.com/{resolved}" + return resolved + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("repository", help="GitHub owner/name or repository URL") + parser.add_argument("pull_request", type=int) + parser.add_argument("--output", type=Path) + parser.add_argument("--max-provider-requests", type=int, default=32) + parser.add_argument("--max-pages-per-collection", type=int, default=10) + args = parser.parse_args() + + service = GitHubPRReviewContextService( + get_github_work_item_service(), + get_github_change_impact_service(), + ) + result = record_github_replay_context( + service, + _repository_url(args.repository), + args.pull_request, + max_provider_requests=max(1, min(args.max_provider_requests, 128)), + max_pages_per_collection=max(1, min(args.max_pages_per_collection, 50)), + ) + rendered = json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered, encoding="utf-8") + else: + sys.stdout.write(rendered) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 61310a2b95d10a1f631f5a968bb4686efe4d06a4 Mon Sep 17 00:00:00 2001 From: 2002yy <15135142681@163.com> Date: Sat, 18 Jul 2026 01:05:17 +0800 Subject: [PATCH 2/2] allow public replay identifiers in secret scan --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 371a625..8d891c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: pip install detect-secrets 2>/dev/null detect-secrets scan --disable-plugin KeywordDetector \ --exclude-files '.*\.(pyc|jpg|png|zip)$' \ - --exclude-lines '"(base_sha|head_sha)": "[0-9a-f]{40}"' \ + --exclude-lines '(base_sha|head_sha|commit_sha|file_sha|tree_sha|ref)[^A-Za-z0-9]+[0-9a-f]{40}|id[^A-Za-z0-9]+MDIzOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFk[A-Za-z0-9+/=]+' \ .github README.md docs src tests tools config templates roles changelog assets frontend .env.example \ > detect-secrets-report.json 2>&1 python - <<'PY'