feat: add project-review workflow (whole-project / single-feature review)

Adds the project-review workflow for code review independent of the git
diff. The scope is parsed from the user instruction: 全面/整个项目 ->
whole-project (score every source file), otherwise feature + target
keyword (locate the code with semantic search + graph queries).

- scoring_tools.py: score_review_func gains all_files=True to score every
  source file in the graph via store.get_all_files()
- main.py: score_review_tool gains all_files param; registers the
  project_review MCP prompt (prompts 6->7)
- prompts.py: project_review_prompt(scope, target) with whole-project and
  feature branches (fixed a precedence bug that truncated the feature text)
- skills.py + skills/project-review/: new read-only project-review skill
  with shared checklists
- .opencode/command/code-review-graph-project-review.md: slash command
- tests: test_project_review.py (prompt rendering), TestProjectReviewPrompt,
  skill count assertions 5->6, all_files wiring checks
- docs: prompts (6->7) + project-review entries across COMMANDS, CLAUDE,
  README (+localized), INDEX, architecture, LLM-OPTIMIZED-REFERENCE,
  CHANGELOG
This commit is contained in:
dev
2026-08-06 13:56:54 +08:00
parent 6f0e6f0775
commit 307d2fd471
45 changed files with 1339 additions and 131 deletions
+134
View File
@@ -624,6 +624,8 @@ def build_report_data(
"files": review_data.get("files", ""),
"baseline": review_data.get("baseline", "generic"),
"verdict": review_data.get("verdict", "❌ FAIL"),
"quality_score": review_data.get("quality_score"),
"counts": review_data.get("counts", {}),
"metrics": {},
"issues": [],
"manual_review": review_data.get("manual_review", []),
@@ -656,3 +658,135 @@ def build_report_data(
})
return data
# ---------------------------------------------------------------------------
# Markdown report rendering (Chinese)
# ---------------------------------------------------------------------------
#: Human-readable labels for the Chinese Markdown report.
_METRIC_LABELS: dict[str, str] = {
"sql_risk": "SQL 注入风险",
"exception_coverage": "异常分支覆盖",
"redundancy_rate": "代码冗余率",
"high_risk_density": "高风险场景密度",
"vulnerability_risk": "漏洞风险",
}
_GRADE_LABELS: dict[str, str] = {
"good": "良好",
"warn": "警告",
"fail": "不合格",
"na": "不适用",
}
_SEVERITY_LABELS: dict[str, str] = {
"blocker": "🔴 阻塞",
"critical": "🔴 严重",
"major": "🟡 主要",
"warn": "🟡 主要",
"minor": "🔵 次要",
"informational": "🔵 次要",
}
def render_markdown_report(review_data: dict[str, Any]) -> str:
"""Render the review data as a Chinese, standalone Markdown report.
Mirrors the HTML report content: verdict, tier, scope, objective
metrics, issues, manual-review items and LLM-judged metrics. The
output is a single Markdown document with no external dependencies.
Args:
review_data: Review data dict (see :func:`build_report_data`).
Returns:
The Markdown report text.
"""
data = build_report_data(review_data)
lines: list[str] = []
lines.append("# 代码审查报告\n")
verdict = str(data["verdict"])
verdict_line = f"- **结论**{verdict}"
tier = data.get("tier") or "standard"
scope = data.get("scope") or "change-level"
baseline = data.get("baseline") or "generic"
lines.append(verdict_line)
lines.append(
f"- **档位**{tier} · **范围**{scope} · **基线**{baseline}"
)
if data.get("timestamp"):
lines.append(f"- **生成时间**{data['timestamp']}")
if data.get("files"):
lines.append(f"- **文件**{data['files']}")
qs = data.get("quality_score")
if qs is not None:
lines.append(f"- **PR 质量分**{qs}/10")
counts = data.get("counts") or {}
if counts:
lines.append(
f"- **问题统计**{counts.get('critical', 0)} 严重 · "
f"{counts.get('informational', 0)} 次要"
)
lines.append("")
# Objective metrics
metrics = data.get("metrics") or {}
if metrics:
lines.append("## 客观指标\n")
lines.append("| 指标 | 数值 | 评级 | 说明 |")
lines.append("|---|---|---|---|")
for name, m in metrics.items():
label = _METRIC_LABELS.get(name, name)
value = m.get("value")
value_text = str(value) if value is not None else "N/A"
grade = m.get("grade") or "na"
grade_text = _GRADE_LABELS.get(grade, grade)
note = str(m.get("note", "") or "")
note = note.replace("\n", " ")
lines.append(f"| {label} | {value_text} | {grade_text} | {note} |")
lines.append("")
# Issues
issues = data.get("issues") or []
lines.append(f"## 问题清单({len(issues)}\n")
if not issues:
lines.append("未发现问题。\n")
for i, issue in enumerate(issues, start=1):
sev = str(issue.get("severity") or "minor").lower()
sev_label = _SEVERITY_LABELS.get(sev, sev)
category = str(issue.get("category") or "")
message = str(issue.get("message") or "")
location = str(issue.get("location") or "")
conf = issue.get("confidence")
fix = str(issue.get("fix") or "")
lines.append(
f"{i}. **{sev_label}** {message}"
f"{f'(置信度 {conf}/10' if conf is not None else ''}"
)
if category:
lines.append(f" - **类别**{category}")
if location:
lines.append(f" - **位置**`{location}`")
if fix:
lines.append(f" - **修复建议**{fix}")
lines.append("")
# Manual review
manual = data.get("manual_review") or []
if manual:
lines.append("## 需要人工审查\n")
for m in manual:
lines.append(f"- {m}")
lines.append("")
# LLM-judged metrics
judged = data.get("llm_judged") or []
if judged:
lines.append("## 需 LLM 判断的指标\n")
lines.append(", ".join(str(j) for j in judged))
lines.append("")
return "\n".join(lines).strip() + "\n"