FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: show per-finding ready time by qiankunli · Pull Request #125 · compforge/devloop · GitHub

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

Filter by extension

Filter by extension .json  (2) .md  (1) .py  (3) All 3 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
2 changes: 1 addition & 1 deletion devloop/.claude-plugin/plugin.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "devloop",
"version": "0.2.15",
"version": "0.2.16",
"description": "Dev-loop workflow, native-first rebuild: git/PR (GitHub + GitLab) + cwd-aware enter + Board-managed context delivery + lint/test gates. Built on native Claude Code events (CwdChanged / PostCompact / FileChanged / monitors).",
"author": {
"name": "compforge",
Expand Down
2 changes: 1 addition & 1 deletion devloop/.codex-plugin/plugin.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "devloop",
"version": "0.2.15",
"version": "0.2.16",
"description": "Dev-loop workflow: git/PR (GitHub + GitLab) + Board-managed workspace/repo context delivery + lint/test gates + execution-level hard intercepts for Claude Code and Codex.",
"author": {
"name": "compforge",
Expand Down
7 changes: 6 additions & 1 deletion devloop/docs/code-review.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ commit_flow 自动 detach 起后台 **review 引擎**(默认 [`ccr`](https://g
两级都锚不上再尝试无锚 review comment,仍不支持才进汇总。多这些层级的理由是
**可打标性**:独立 review comment 能被回复 `ccr:label=`,汇总里的 finding 只是一行文本、
没有可回复的对象,等于退出 ground truth 回收。
- **单条 finding 展示自己的 ready time**:CCR Session JSONL 是时间事实源;devloop 按
`hypothesis_id` / `origin_unit` 连接 Finding 与其 Unit ready point,在独立 thread 上展示
`ready in …`。它表示该 Unit 从形成完毕到 Finding 可交付的端到端 latency,不把并发 Execution
duration 相加;汇总 header 的 `cost` 仍表示整次 review 的总耗时。
- **打标闭环锚在 forge 上,不落本地**:finding comment 带 `ccr:fp=` 指纹,verdict 以
`ccr:label=` 回复落在其下;Forge adapter 把平台线程整理成 top-level Comment + replies,
`domain/review_feedback.py` 直接读取。指纹跟着持久对象(comment body)走,所以换机器 /
Expand Down Expand Up @@ -142,11 +146,12 @@ run_review 独占写入。`comments` 是引擎的原始评论(无优先级—
{
"status": "running | success | completed_with_warnings | completed_with_errors | skipped | error",
"reviewed_sha": "…",
"comments": [ { "path", "content", "start_line", "end_line", "suggestion_code?", "existing_code?", "thinking?" } ],
"comments": [ { "path", "content", "start_line", "end_line", "ready_ms?", "suggestion_code?", "existing_code?", "thinking?" } ],
"count": 3,
"failed": 0, // review 失败的文件数(引擎的 subtask_error warnings)——0 评论但 failed>0 = 出错而非 clean
"warnings": [ … ], // 引擎原始 warnings(每文件失败原因),供诊断
"message": "…", // 引擎的整体消息(如 "No comments generated. Looks good to me.")
"session_id": "…", // 引擎运行轨迹 identity;CCR 提供,其他引擎可为空
"reviewed_range": "…", // 审查范围:HEAD 模式是 sha,--mr 模式是 "origin/<target>..HEAD"
"mr_comment": "…", // --mr 模式:发评论到 MR 的结果("posted to MR !N" / "no open MR…" / 失败原因)
"pull_request": { // 找到开放 PR/MR 时的稳定外部 identity;否则为 null
Expand Down
59 changes: 58 additions & 1 deletion devloop/lib/review_engine.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Protocol, runtime_checkable

_REVIEW_TIMEOUT = 800 # review 自身要跑 LLM、审全量 diff,给足
Expand All @@ -41,6 +42,7 @@ class ReviewResult:
models: dict = field(default_factory=dict) # routing alias -> #responses(去重);review 级 model 身份,clean 也有
cost_sec: int = 0 # 引擎自报的 review 耗时(整秒);0 = 引擎没报
tool_version: str = "" # 引擎自报的版本;"" = 引擎没报
session_id: str = "" # 引擎运行轨迹 identity;"" = 引擎没报
message: str = ""
error: str = "" # ok=False 时的诊断(写进 review.json)

Expand Down Expand Up @@ -111,14 +113,69 @@ def review(self, repo: str, from_ref: str, to_ref: str, background: str | None,
warnings = out.get("warnings") or []
failed = sum(1 for w in warnings if isinstance(w, dict) and w.get("type") == "subtask_error")
summary = out.get("summary") or {}
comments = _attach_finding_ready_time(out.get("comments") or [], out.get("session_path") or "")
return ReviewResult(ok=True, status=out.get("status", "success"),
comments=out.get("comments") or [], warnings=warnings,
comments=comments, warnings=warnings,
failed=failed, models=summary.get("models") or {},
cost_sec=int(summary.get("elapsed_sec") or 0),
tool_version=out.get("version") or "",
session_id=out.get("session_id") or "",
message=out.get("message", ""))


def _attach_finding_ready_time(comments: list, session_path: str) -> list:
"""Join each delivered finding to its origin Unit's ready point.

CCR owns pipeline timing in Session JSONL; devloop only projects the
resulting Unit-ready → Finding latency into the forge-facing comment.
"""
ready_by_hypothesis = _finding_ready_ms(session_path)
if not ready_by_hypothesis:
return comments
enriched = []
for comment in comments:
item = dict(comment)
ready_ms = ready_by_hypothesis.get(item.get("hypothesis_id"))
if ready_ms is not None:
item["ready_ms"] = ready_ms
enriched.append(item)
return enriched


def _finding_ready_ms(session_path: str) -> dict[str, int]:
if not session_path:
return {}
units: dict[str, int] = {}
findings: list[tuple[str, str, int]] = []
try:
with Path(session_path).open(encoding="utf-8") as transcript:
for line in transcript:
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
elapsed = record.get("elapsed_ms")
if not isinstance(elapsed, (int, float)):
continue
if record.get("type") == "artifact" and record.get("artifact_kind") == "review_unit":
unit_id = (record.get("data") or {}).get("unit_id")
if unit_id:
units[str(unit_id)] = int(elapsed)
elif record.get("type") == "finding":
hypothesis_id = record.get("hypothesis_id")
origin_unit = record.get("origin_unit")
if hypothesis_id and origin_unit:
findings.append((str(hypothesis_id), str(origin_unit), int(elapsed)))
except OSError:
return {}

return {
hypothesis_id: finding_ms - units[origin_unit]
for hypothesis_id, origin_unit, finding_ms in findings
if origin_unit in units and finding_ms >= units[origin_unit]
}


class OcrEngine:
"""open-code-review(ocr)adapter。与 CcrEngine 各自独立(见上)。CLI:
`ocr review --from --to --format json --repo [--background]`、`ocr llm test`。"""
Expand Down
27 changes: 24 additions & 3 deletions devloop/scripts/run_review.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@
_MAX_COMMENT_FINDINGS = 30 # 评论里最多列几条,避免超长 MR 评论


def _ready_label(comment: dict) -> str:
ready_ms = comment.get("ready_ms")
if not isinstance(ready_ms, (int, float)) or ready_ms < 0:
return ""
if ready_ms < 1000:
return "ready in <1s"
seconds = int(ready_ms / 1000 + 0.5)
minutes, seconds = divmod(seconds, 60)
if not minutes:
return f"ready in {seconds}s"
return f"ready in {minutes}m {seconds}s"


def _head_sha(repo: str) -> str:
r = subprocess.run(["git", "-C", repo, "rev-parse", "HEAD"], capture_output=True, text=True)
return r.stdout.strip() if r.returncode == 0 else ""
Expand Down Expand Up @@ -93,7 +106,12 @@ def _post_inline(forge, pr, comments: list, sha: str = "") -> tuple[int, list]:
if not body:
fallback.append(c)
continue
head = "🤖 **devloop code-review**" + (f" · {alias}" if alias else "")
head_parts = ["🤖 **devloop code-review**"]
if alias:
head_parts.append(alias)
if ready := _ready_label(c):
head_parts.append(ready)
head = " · ".join(head_parts)
# ccr:fp footer——finding 的稳定指纹(path+content hash),把这条评论和 session
# finding / 复跑重现 / 人工标注(回复 `ccr:label=<verdict>`)join 到一起;
# 回收约定见 ccr 仓 eval/README「人工标注统一约定」。
Expand Down Expand Up @@ -163,7 +181,8 @@ def _format_comment(comments: list, failed: int, range_label: str, sha: str, mod
if s or e:
loc += f":{s}-{e}"
alias = (c.get("alias") or "").strip() # 多 model 池里哪个 model 出的(引擎 routing alias),便于对比
tag = f" ({alias})" if alias else ""
meta = [part for part in (alias, _ready_label(c)) if part]
tag = f" ({' · '.join(meta)})" if meta else ""
body = (c.get("content") or "").strip().replace("\n", " ")
fp = (c.get("fingerprint") or "").strip()
if fp:
Expand Down Expand Up @@ -384,13 +403,15 @@ def skip(msg: str) -> int:
result.cost_sec, tool_label, inline_posted))
_write(repo, branch, status=result.status, reviewed_sha=sha, comments=comments,
count=len(comments), failed=result.failed, warnings=result.warnings, message=result.message,
cost_sec=result.cost_sec, tool_version=result.tool_version, inline_posted=inline_posted,
cost_sec=result.cost_sec, tool_version=result.tool_version, session_id=result.session_id,
inline_posted=inline_posted,
deduped=deduped,
reviewed_range=range_label, mr_comment=posted, pull_request=pull_request,
generated_at=base.now())
_append_history(repo, started, status=result.status, sha=sha,
pull_request=pull_request,
count=len(comments), failed=result.failed,
session_id=result.session_id,
findings=_findings_for_history(comments, result.warnings),
range=range_label, posted=posted)
print(f"run_review: {len(comments)} comment(s), {result.failed} file(s) failed on {range_label}"
Expand Down
40 changes: 38 additions & 2 deletions devloop/tests/test_review.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,41 @@ def fake_run(cmd, **kwargs):
]]


def test_ccr_engine_joins_finding_ready_time_from_session():
"""单条 finding 的 latency 来自 Session 时间线:Unit ready → Finding。"""
import json as _json
import tempfile
from types import SimpleNamespace
re = _load_script("run_review").review_engine
original = re.subprocess.run

with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False) as transcript:
transcript.write(_json.dumps({
"type": "artifact", "artifact_kind": "review_unit", "elapsed_ms": 1200,
"data": {"unit_id": "u-1"},
}) + "\n")
transcript.write(_json.dumps({
"type": "finding", "elapsed_ms": 6725, "hypothesis_id": "h-1", "origin_unit": "u-1",
}) + "\n")
session_path = transcript.name

def fake_run(cmd, **kwargs):
return SimpleNamespace(returncode=0, stdout=_json.dumps({
"status": "success", "session_id": "s-1", "session_path": session_path,
"comments": [{"hypothesis_id": "h-1", "path": "a.go", "content": "bug"}],
}), stderr="")

re.subprocess.run = fake_run
try:
result = re.CcrEngine().review("/repo", "origin/main", "abc123", None)
finally:
re.subprocess.run = original
Path(session_path).unlink(missing_ok=True)

assert result.session_id == "s-1"
assert result.comments[0]["ready_ms"] == 5525


def test_pr_identity_projects_to_ccr_biz_id():
rr = _load_script("run_review")
assert rr._biz_id({
Expand Down Expand Up @@ -445,14 +480,15 @@ def test_post_inline_findings():
fake = _FakeForge([PullRequest(number=7, state="open")])
pr = fake.get(7)
comments = [
{"path": "a.py", "start_line": 3, "end_line": 5, "alias": "m1", "content": "bug"},
{"path": "a.py", "start_line": 3, "end_line": 5, "alias": "m1", "ready_ms": 65000, "content": "bug"},
{"path": "b.py", "content": "file-level: 缺测试"}, # 无行号 → 本就是 file-level
{"content": "no path at all"}, # 无锚可锚 → 汇总
]
n, fb = rr._post_inline(fake, pr, comments, "abc123456")
assert n == 2 and [c.get("content") for c in fb] == ["no path at all"]
assert fake.diff_posted[0][:3] == (7, "a.py", 5) # line-level:锚在 end_line
assert "m1" in fake.diff_posted[0][3] and "bug" in fake.diff_posted[0][3]
assert "m1" in fake.diff_posted[0][3] and "ready in 1m 5s" in fake.diff_posted[0][3]
assert "bug" in fake.diff_posted[0][3]
assert "ccr:history=" in fake.diff_posted[0][3]
assert fake.diff_posted[1][:3] == (7, "b.py", None) # file-level:直接锚文件
assert len(fake.diff_posted) == 2 # 行锚成功 → 不会再补一条文件锚
Expand Down

Back | FazBrowse Home | New Git URL