Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
13 changes: 9 additions & 4 deletions docs/PROJECT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 截断

这里只回答:**做到哪里、还差什么、下一步做什么**。

Expand Down Expand Up @@ -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。
Expand Down Expand Up @@ -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 和重型连接器继续后置。
Expand Down Expand Up @@ -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 功能代码验证:

Expand Down
143 changes: 143 additions & 0 deletions src/web/github_pr_review_recording.py
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions src/web/github_pr_review_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = []
Expand All @@ -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 {}
Expand Down
Loading
Loading