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
+37 -19
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Code Review Report</title>
<title>代码审查报告</title>
<style>
:root { --border:#d0d7de; --bg:#f6f8fa; --fg:#1f2328; --muted:#57606a;
--good:#1a7f37; --warn:#9a6700; --fail:#cf222e; --na:#57606a;
@@ -61,28 +61,45 @@ function verdictClass(v) {
return v.includes("PASS") ? "pass" : "fail";
}
let html = `<h1>Code Review Report</h1>
const metricLabels = {
sql_risk: "SQL 注入风险",
exception_coverage: "异常分支覆盖",
redundancy_rate: "代码冗余率",
high_risk_density: "高风险场景密度",
vulnerability_risk: "漏洞风险",
};
const gradeLabels = { good: "良好", warn: "警告", fail: "不合格", na: "不适用" };
const sevLabels = {
blocker: "🔴 阻塞", critical: "🔴 严重",
major: "🟡 主要", warn: "🟡 主要",
minor: "🔵 次要", informational: "🔵 次要",
};
let html = `<h1>代码审查报告</h1>
<div class="meta">
<span class="verdict ${verdictClass(data.verdict)}">${esc(data.verdict || "NO VERDICT")}</span>
&nbsp; Tier: <code>${esc(data.tier || "standard")}</code>
&nbsp; Scope: <code>${esc(data.scope || "change-level")}</code>
${data.baseline ? `&nbsp; Baseline: <code>${esc(data.baseline)}</code>` : ""}
<span class="verdict ${verdictClass(data.verdict)}">${esc(data.verdict || "无结论")}</span>
&nbsp; 档位: <code>${esc(data.tier || "standard")}</code>
&nbsp; 范围: <code>${esc(data.scope || "change-level")}</code>
${data.baseline ? `&nbsp; 基线: <code>${esc(data.baseline)}</code>` : ""}
</div>`;
if (data.timestamp) html += `<p class="muted">Generated ${esc(data.timestamp)}</p>`;
if (data.files) html += `<p><b>Files:</b> ${esc(data.files)}</p>`;
if (data.quality_score != null) html += `<p><b>PR 质量分:</b> ${esc(data.quality_score)}/10</p>`;
if (data.timestamp) html += `<p class="muted">生成时间: ${esc(data.timestamp)}</p>`;
if (data.files) html += `<p><b>文件:</b> ${esc(data.files)}</p>`;
if (data.summary) html += `<p>${esc(data.summary)}</p>`;
const metrics = data.metrics || {};
const mkeys = Object.keys(metrics);
if (mkeys.length) {
html += `<h2>Objective Metrics</h2><table><tr><th>Metric</th><th>Value</th><th>Grade</th><th>Notes</th></tr>`;
html += `<h2>客观指标</h2><table><tr><th>指标</th><th>数值</th><th>评级</th><th>说明</th></tr>`;
for (const k of mkeys) {
const m = metrics[k] || {};
const g = m.grade || "na";
const label = metricLabels[k] || k;
const gradeText = gradeLabels[g] || g;
html += `<tr>
<td>${esc(k)}</td>
<td>${esc(label)}</td>
<td>${esc(m.value ?? "N/A")}</td>
<td class="grade ${esc(g)}">${esc(g)}</td>
<td class="grade ${esc(g)}">${esc(gradeText)}</td>
<td class="muted">${esc(m.note || "")}</td>
</tr>`;
}
@@ -90,32 +107,33 @@ if (mkeys.length) {
}
const issues = data.issues || [];
html += `<h2>Issues (${issues.length})</h2>`;
html += `<h2>问题清单 (${issues.length})</h2>`;
if (!issues.length) {
html += `<p class="muted">No issues found.</p>`;
html += `<p class="muted">未发现问题。</p>`;
}
for (const i of issues) {
const sev = (i.severity || "minor").toLowerCase();
const sevLabel = sevLabels[sev] || i.severity;
html += `<div class="issue">
<span class="tag ${esc(sev)}">${esc(i.severity)}</span>
<span class="tag ${esc(sev)}">${esc(sevLabel)}</span>
<span class="cat">${esc(i.category)}</span>
${esc(i.message || "")}
${i.confidence ? `<span class="muted">(confidence ${esc(i.confidence)})</span>` : ""}
${i.confidence != null ? `<span class="muted">(置信度 ${esc(i.confidence)}/10)</span>` : ""}
${i.location ? `<div class="loc">→ ${esc(i.location)}</div>` : ""}
${i.fix ? `<div class="fix"><b>Fix:</b> ${esc(i.fix)}</div>` : ""}
${i.fix ? `<div class="fix"><b>修复建议:</b> ${esc(i.fix)}</div>` : ""}
</div>`;
}
const manual = data.manual_review || [];
if (manual.length) {
html += `<h2>Manual Review Required</h2><ul>`;
html += `<h2>需要人工审查</h2><ul>`;
for (const m of manual) html += `<li>${esc(m)}</li>`;
html += `</ul>`;
}
const judged = data.llm_judged || [];
if (judged.length) {
html += `<p class="muted"><b>LLM-judged:</b> ${judged.map(esc).join(", ")}</p>`;
html += `<p class="muted"><b>LLM 判断的指标:</b> ${judged.map(esc).join(", ")}</p>`;
}
document.getElementById("app").innerHTML = html;
@@ -25,18 +25,22 @@ Never include full files unless explicitly asked.
</section>
<section name="unified-review">
Full three-layer review: 1) get_minimal_context_tool + build_or_update_graph_tool + get_review_context_tool + detect_changes_tool for graph context; 2) Layer-1 chain decomposition across 8 categories + gstack CRITICAL sub-pass; 3) score_review_tool for objective Layer-2 metrics; 4) specialist subagents (diff >= 50 lines); 5) dedupe_findings_tool to merge; 6) READ-ONLY manual adjudication per severity; 7) generate_report_tool to write code-review-report.html. Read .code-review.yaml for tier (fast/standard/strict). Target: <=8 tool calls, <=1200 tokens.
Full three-layer review: 1) get_minimal_context_tool + build_or_update_graph_tool + get_review_context_tool + detect_changes_tool for graph context; 2) Layer-1 chain decomposition across 8 categories + gstack CRITICAL sub-pass; 3) score_review_tool for objective Layer-2 metrics; 4) specialist subagents (diff >= 50 lines); 5) dedupe_findings_tool to merge; 6) READ-ONLY manual adjudication per severity; 7) generate_report_tool to write code-review-report.html + code-review-report.md. Read .code-review.yaml for tier (fast/standard/strict). Target: <=8 tool calls, <=1200 tokens.
</section>
<section name="project-review">
Whole-project or feature review (not diff-based): 1) get_minimal_context_tool + build_or_update_graph_tool; 2) get_architecture_overview_tool + list_communities_tool for the module map; 3) get_knowledge_gaps_tool + get_hub_nodes_tool + get_bridge_nodes_tool + find_large_functions_tool + get_surprising_connections_tool for high-risk areas; 4) whole-project: score_review_tool(all_files=True); feature: semantic_search_nodes_tool + query_graph_tool(children_of) to locate files, then score_review_tool(changed_files) + get_impact_radius_tool; 5) dedupe_findings_tool; 6) READ-ONLY adjudication; 7) generate_report_tool (format=both). Parse scope from the user instruction (全面/整个项目 -> whole-project, else feature + target). Target: <=12 tool calls, <=1800 tokens.
</section>
<section name="score-review">
score_review_tool returns objective metrics (sql_risk, exception_coverage, redundancy_rate, high_risk_density, vulnerability_risk) with good/warn/fail grades + llm_judged list. dedupe_findings_tool merges findings by path:line:category fingerprint, boosts multi-source confidence (+1 cap 10), computes PR quality score. generate_report_tool writes the standalone HTML report.
score_review_tool returns objective metrics (sql_risk, exception_coverage, redundancy_rate, high_risk_density, vulnerability_risk) with good/warn/fail grades + llm_judged list. Pass all_files=True to score every source file (whole-project review). dedupe_findings_tool merges findings by path:line:category fingerprint, boosts multi-source confidence (+1 cap 10), computes PR quality score. generate_report_tool writes the HTML and/or Markdown review report (format=both by default).
</section>
<section name="commands">
Core MCP tools: get_minimal_context_tool, detect_changes_tool, get_review_context_tool, get_impact_radius_tool, query_graph_tool, semantic_search_nodes_tool, get_architecture_overview_tool, get_affected_flows_tool, list_flows_tool, list_communities_tool, refactor_tool, build_or_update_graph_tool, run_postprocess_tool, embed_graph_tool, list_graph_stats_tool, get_docs_section_tool
Unified-review MCP tools: score_review_tool, dedupe_findings_tool, generate_report_tool
MCP prompts (6): review_changes, architecture_map, debug_issue, onboard_developer, pre_merge_check, unified_review
Skills: build-graph, debug-issue, explore-codebase, refactor-safely, review-changes, review-delta, review-pr, unified-review
MCP prompts (7): review_changes, architecture_map, debug_issue, onboard_developer, pre_merge_check, unified_review, project_review
Skills: build-graph, debug-issue, explore-codebase, refactor-safely, review-changes, review-delta, review-pr, unified-review, project-review
CLI: code-review-graph [install|init|build|update|status|watch|visualize|serve|mcp|wiki|detect-changes|postprocess|embed|register|unregister|repos|eval|daemon]
Token efficiency: Prefer detail_level="minimal" where available. Always call get_minimal_context_tool first. Some review/context tools return compact estimated context_savings metadata.
</section>
+34 -8
View File
@@ -27,6 +27,7 @@ from .prompts import (
debug_issue_prompt,
onboard_developer_prompt,
pre_merge_check_prompt,
project_review_prompt,
review_changes_prompt,
unified_review_prompt,
)
@@ -697,6 +698,7 @@ async def score_review_tool(
include_churn: bool = True,
repo_root: Optional[str] = None,
detail_level: str = "standard",
all_files: bool = False,
) -> dict:
"""Compute objective Layer-2 review metrics for changed files.
@@ -717,6 +719,9 @@ async def score_review_tool(
repo_root: Repository root path. Auto-detected if omitted.
detail_level: "standard" for full output, "minimal" for
token-efficient summary. Default: standard.
all_files: When True, score every source file in the graph,
ignoring ``changed_files`` and the git diff. Used for
whole-project reviews (default: False).
"""
root = _resolve_repo_root(repo_root)
@@ -724,7 +729,7 @@ async def score_review_tool(
return with_provenance(score_review_func(
changed_files=changed_files, base=base,
include_churn=include_churn, repo_root=root,
detail_level=detail_level,
detail_level=detail_level, all_files=all_files,
), root)
return await asyncio.to_thread(_run)
@@ -763,30 +768,34 @@ async def generate_report_tool(
review_data: dict,
output_path: Optional[str] = None,
repo_root: Optional[str] = None,
format: str = "both",
) -> dict:
"""Generate a self-contained HTML code review report.
"""Generate code review reports (HTML and/or Markdown).
Injects ``review_data`` (output of ``score_review_tool`` plus
``dedupe_findings_tool`` results and free-form verdict/tier/scope) into
the bundled ``report-template.html`` as ``{{REPORT_DATA}}`` and writes
the standalone file (default ``repo_root/code-review-report.html``).
the bundled ``report-template.html`` as ``{{REPORT_DATA}}`` (HTML) and/or
renders a standalone Chinese Markdown report. Both are written by
default. ``output_path`` is the base name without an extension.
Offloaded to a thread via ``asyncio.to_thread`` — rendering loads the
template asset and writes the output file.
template asset and writes the output file(s).
Args:
review_data: Review data dict (metrics, findings, verdict, tier,
scope, files, baseline, timestamp, manual_review).
output_path: Output file path. Defaults to
``<repo_root>/code-review-report.html``.
output_path: Output base path (no extension). Defaults to
``<repo_root>/code-review-report``.
repo_root: Repository root path. Auto-detected if omitted.
format: Output format. ``html``, ``markdown``, or ``both``
(default: both).
"""
root = _resolve_repo_root(repo_root)
def _run() -> dict:
return with_provenance(generate_report_func(
review_data=review_data, output_path=output_path,
repo_root=root,
repo_root=root, format=format,
), root)
return await asyncio.to_thread(_run)
@@ -1140,6 +1149,23 @@ def unified_review(base: str = "HEAD~1", tier: str = "standard") -> list[dict]:
return unified_review_prompt(base=base, tier=tier)
@mcp.prompt()
def project_review(scope: str = "whole-project", target: str = "") -> list[dict]:
"""Whole-project or single-feature code review (not diff-based).
Reviews the entire codebase (whole-project) or a single feature/module
(feature) using graph-wide analysis and objective scoring, independent
of the git diff. READ-ONLY: every finding waits for a manual fix
decision.
Args:
scope: Review scope. ``whole-project`` reviews every source file;
``feature`` reviews only the target's code.
target: Feature/module/function keyword when scope="feature".
"""
return project_review_prompt(scope=scope, target=target)
def _apply_tool_filter(tools: str | None = None) -> None:
"""Remove tools not listed in the allow-list.
+89 -6
View File
@@ -1,6 +1,6 @@
"""MCP prompt templates for Code Review Graph.
Provides 6 pre-built prompt workflows, all enforcing token-efficient
Provides 7 pre-built prompt workflows, all enforcing token-efficient
detail_level="minimal" first patterns with get_minimal_context entry point.
1. review_changes - pre-commit review using detect_changes + affected_flows
@@ -9,6 +9,7 @@ detail_level="minimal" first patterns with get_minimal_context entry point.
4. onboard_developer - new dev orientation using stats, architecture, flows
5. pre_merge_check - PR readiness with risk scoring, test gaps, dead code
6. unified_review - three-layer review: graph context + scoring + dedupe + report
7. project_review - whole-project or single-feature review (not diff-based)
"""
from __future__ import annotations
@@ -209,9 +210,91 @@ def unified_review_prompt(
'6. Call `dedupe_findings(findings=<your findings>)` to merge '
"by fingerprint, boost multi-source confidence and compute the "
"PR quality score.\n"
'7. Call `generate_report(review_data=<verdict, tier, scope, '
'metrics, merged findings>)` to write code-review-report.html.\n'
"8. Output: verdict (✅ PASS / ❌ FAIL), severity counts, each "
"issue with confidence + fix, and the manual-review items. "
"Any blocker → verdict ❌ FAIL."
'7. Call `generate_report(review_data=<verdict, tier, scope, '
'metrics, merged findings>)` to write code-review-report.html '
"and code-review-report.md (format=\"both\").\n"
"8. Output: verdict (✅ PASS / ❌ FAIL), severity counts, each "
"issue with confidence + fix, and the manual-review items. "
"Any blocker → verdict ❌ FAIL."
)
def project_review_prompt(
scope: str = "whole-project",
target: str = "",
) -> list[Message]:
"""Whole-project or single-feature code review workflow (not diff-based).
Reviews the entire codebase (``scope="whole-project"``) or a single
feature/module/function (``scope="feature"`` with ``target``), using
graph-wide analysis and objective scoring independent of the git diff.
READ-ONLY: every finding waits for a manual fix decision.
Args:
scope: ``whole-project`` reviews every source file in the graph;
``feature`` reviews only the code related to ``target``.
target: Feature/module/function keyword when ``scope="feature"``
(e.g. "payment", "auth", "checkout flow").
"""
scope_note = (
"whole-project scope: score every source file with "
"`score_review(all_files=True)`."
if scope == "whole-project"
else (
"feature scope: locate the target's files with semantic search "
"and graph queries, then score only those files."
)
)
common = (
f"## Project Review Workflow (scope={scope}, target={target})\n"
f"{scope_note}\n"
"**READ-ONLY.** Present every finding for a manual fix decision. "
"Never modify code, commit, or push.\n"
'1. Call `get_minimal_context(task="project review")` for graph '
"stats and community overview.\n"
'2. Call `build_or_update_graph()` to ensure the graph is current.\n'
'3. Map the architecture: `get_architecture_overview('
'detail_level="minimal")` and `list_communities('
'detail_level="minimal")`.\n'
'4. Locate high-risk areas: `get_knowledge_gaps()`, '
'`get_hub_nodes()`, `get_bridge_nodes()`, `find_large_functions()` '
"and `get_surprising_connections()`.\n"
)
if scope == "whole-project":
workflow = (
'5. Score every source file: `score_review(all_files=True)`.\n'
"6. For the top-risk communities, drill in with "
'`get_community(include_members=True)` and '
'`query_graph(pattern="children_of", target=<community>)`.\n'
"7. Produce findings with severity (blocker/major/minor), "
"confidence (1-10), file:line and a proposed fix.\n"
"8. Call `dedupe_findings(findings=<your findings>)` to merge "
"and compute the PR quality score.\n"
'9. Call `generate_report(review_data=<verdict, scope, '
'metrics, merged findings>)` (format="both").\n'
"10. Output the verdict (✅ PASS / ❌ FAIL), severity counts, "
"each issue, and the manual-review items.\n"
)
else:
workflow = (
'5. Locate the feature code: `semantic_search_nodes(query='
'<target>)` and `query_graph(pattern="children_of", '
'target=<target>)`; collect the related files.\n'
"6. Map the blast radius: `get_impact_radius(changed_files="
"<collected files>)`.\n"
'7. Score the feature: `score_review(changed_files=<files + '
'impacted files>)`.\n'
"8. Review the feature code (Layer 1 chain decomposition) and "
"produce findings with severity, confidence, file:line and a "
"proposed fix.\n"
"9. Call `dedupe_findings(findings=<your findings>)` to merge "
"and compute the PR quality score.\n"
'10. Call `generate_report(review_data=<verdict, scope, '
'metrics, merged findings>)` (format="both").\n'
"11. Output the verdict (✅ PASS / ❌ FAIL), severity counts, "
"each issue, and the manual-review items.\n"
)
return _user(f"{_TOKEN_EFFICIENCY_PREAMBLE}\n{common}{workflow}")
+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"
+85 -2
View File
@@ -809,8 +809,8 @@ _SKILLS: dict[str, dict[str, str]] = {
"### Step 8 - Report\n"
"Call `generate_report_tool(review_data=<collected verdict, "
"metrics, findings, tier, scope>)` to write "
"`code-review-report.html`. Also present the text report "
"inline.\n\n"
"`code-review-report.html` and `code-review-report.md` (default "
"`format=\"both\"`). Also present the text report inline.\n\n"
"### Step 9 - Persistence (optional)\n"
"If the `gstack-review-log` binary is available, record the "
"review outcome (status, counts, quality score, per-finding "
@@ -830,6 +830,89 @@ _SKILLS: dict[str, dict[str, str]] = {
"≤1200 total output tokens."
),
},
"project-review.md": {
"name": "project-review",
"description": (
"Whole-project or single-feature code review (not diff-based) "
"using graph-wide analysis and objective scoring"
),
"body": (
"## Project Review\n\n"
"Review the entire codebase or a single feature/module, "
"independent of the git diff. Two scopes, driven by the "
"user's instruction:\n"
"- **whole-project**: \"对项目代码进行全面审查\", \"全面审查\", "
"\"整个项目\" → review every source file in the graph.\n"
"- **feature**: \"审查 <功能/模块> 的代码\" (e.g. payment, "
"auth) → review only the code related to the target.\n\n"
"**This skill is READ-ONLY.** Every finding is presented to the "
"user for a manual fix decision. Never apply code changes, "
"commit, or push.\n\n"
"### Token Efficiency Rules\n"
'- ALWAYS start with `get_minimal_context(task="project review")`. '
'Use `detail_level="minimal"` on all calls; escalate to '
'"standard" only when a metric or finding needs evidence.\n\n'
"### Step 0 - Parse the scope\n"
"Read the user's instruction and set scope: whole-project "
"(contains 全面/整个项目/所有/all) or feature + target "
"(extract the feature/module keyword). Declare both in the "
"report header.\n\n"
"### Step 1 - Graph ready\n"
"1. Call `build_or_update_graph_tool()` to ensure the graph is "
"current.\n"
"2. Call `get_minimal_context_tool(task=\"project review\")` "
"for stats and community overview.\n\n"
"### Step 2 - Architecture map\n"
"Call `get_architecture_overview_tool(detail_level=\"minimal\")` "
"and `list_communities_tool(detail_level=\"minimal\")` to map "
"the module structure.\n\n"
"### Step 3 - High-risk scan (whole-project)\n"
"Call `get_knowledge_gaps_tool()`, `get_hub_nodes_tool()`, "
"`get_bridge_nodes_tool()`, `find_large_functions_tool()` and "
"`get_surprising_connections_tool()` to locate hotspots, "
"chokepoints, untested areas and odd coupling.\n\n"
"### Step 4 - Objective scoring\n"
"- whole-project: `score_review_tool(all_files=True)` scores "
"every source file in the graph.\n"
"- feature: locate the target files with "
"`semantic_search_nodes_tool(query=<target>)` and "
"`query_graph_tool(pattern=\"children_of\", target=<target>)`, "
"then `score_review_tool(changed_files=<files>)` and "
"`get_impact_radius_tool(changed_files=<files>)` for the "
"blast radius.\n\n"
"### Step 5 - Chain decomposition\n"
"Inspect the scored code across eight categories (interface, "
"business, data, utility, error handling, security, "
"performance, observability) and apply the gstack CRITICAL "
"sub-pass (SQL & Data Safety, Race Conditions, LLM Output "
"Trust Boundary, Shell Injection, Enum Completeness). Mark "
"each ✅ / ⚠️ / —.\n\n"
"### Step 6 - Manual adjudication (READ-ONLY)\n"
"Present every finding with severity (🔴 blocker / 🟡 major / "
"🔵 minor), confidence (1-10), file:line and a proposed fix. "
"Group by severity and ask the user per batch: fix / skip / "
"self-fix. 🔴 blockers cannot be batch-skipped. **Do not "
"modify code.**\n\n"
"### Step 7 - Acceptance gate\n"
"Any 🔴 blocker → verdict `❌ FAIL`. Classify each finding as "
"Ready / Needs Fix / Unusable.\n\n"
"### Step 8 - Report\n"
"Call `generate_report_tool(review_data=<verdict, scope, "
"metrics, findings>)` to write `code-review-report.html` and "
"`code-review-report.md` (default `format=\"both\"`).\n\n"
"### Output Format\n"
"`Project Review: N issues (X blocker, Y major, Z minor) — "
"verdict: ✅ PASS / ❌ FAIL`. List each issue with severity, "
"confidence, file:line, problem, and proposed fix.\n\n"
"## Token Efficiency Rules\n"
'- ALWAYS start with `get_minimal_context(task="project review")` '
"before any other graph tool.\n"
'- Use `detail_level="minimal"` on all calls. Only escalate to '
'"standard" when minimal is insufficient.\n'
"- Target: complete a project review in ≤12 tool calls and "
"≤1800 total output tokens."
),
},
"debug-issue.md": {
"name": "debug-issue",
"description": "Systematically debug issues using graph-powered code navigation",
+1 -1
View File
@@ -31,7 +31,7 @@ Exposes 31 tools:
28. traverse_graph - BFS/DFS traversal from best-matching node
29. score_review - objective Layer-2 review metrics for changed files
30. dedupe_findings - fingerprint dedup + confidence merge for findings
31. generate_report - render the standalone HTML code review report
31. generate_report - render the HTML and/or Markdown review report
"""
from __future__ import annotations
+5 -4
View File
@@ -115,8 +115,9 @@ def get_docs_section(
Args:
section_name: Exact section name. One of: usage, review-delta,
review-pr, unified-review, score-review, commands,
legal, watch, embeddings, languages, troubleshooting.
review-pr, unified-review, project-review, score-review,
commands, legal, watch, embeddings, languages,
troubleshooting.
repo_root: Repository root path. Auto-detected from current
directory if omitted.
@@ -178,8 +179,8 @@ def get_docs_section(
available = [
"usage", "review-delta", "review-pr", "unified-review",
"score-review", "commands", "legal", "watch", "embeddings",
"languages", "troubleshooting",
"project-review", "score-review", "commands", "legal", "watch",
"embeddings", "languages", "troubleshooting",
]
return {
"status": "not_found",
+66 -23
View File
@@ -6,7 +6,7 @@ build_report_data) into the three MCP tools consumed by the
* ``score_review_tool`` - objective Layer-2 metrics for changed files
* ``dedupe_findings_tool`` - fingerprint dedup + confidence merge
* ``generate_report_tool`` - render ``code-review-report.html``
* ``generate_report_tool`` - render the HTML and/or Markdown review report
"""
from __future__ import annotations
@@ -16,7 +16,12 @@ from pathlib import Path
from typing import Any
from ..incremental import get_changed_files, get_staged_and_unstaged
from ..scoring import build_report_data, dedupe_findings, score_review
from ..scoring import (
build_report_data,
dedupe_findings,
render_markdown_report,
score_review,
)
from ._common import _get_store, _error_response
try:
@@ -38,6 +43,7 @@ def score_review_func(
include_churn: bool = True,
repo_root: str | None = None,
detail_level: str = "standard",
all_files: bool = False,
) -> dict[str, Any]:
"""Compute objective Layer-2 review metrics for changed files.
@@ -54,10 +60,15 @@ def score_review_func(
repo_root: Repository root. Auto-detected if omitted.
detail_level: Output detail level. ``minimal`` returns only grades
and values; ``standard`` includes evidence.
all_files: When True, score every source file in the graph,
ignoring ``changed_files`` and the git diff. Used for
whole-project reviews (default: False).
"""
store, root = _get_store(repo_root)
try:
if changed_files is None:
if all_files:
changed_files = store.get_all_files()
elif changed_files is None:
changed_files = get_changed_files(root, base)
if not changed_files:
changed_files = get_staged_and_unstaged(root)
@@ -223,45 +234,77 @@ def generate_report_func(
review_data: dict[str, Any],
output_path: str | None = None,
repo_root: str | None = None,
format: str = "both",
) -> dict[str, Any]:
"""Generate a self-contained HTML code review report.
"""Generate code review reports (HTML and/or Markdown).
Injects ``review_data`` (the output of ``score_review_tool`` plus
Renders ``review_data`` (the output of ``score_review_tool`` plus
``dedupe_findings_tool`` results and free-form verdict/tier/scope) into
the bundled ``report-template.html`` as ``{{REPORT_DATA}}`` and writes
the standalone file (default ``repo_root/code-review-report.html``).
a self-contained HTML report (``report-template.html``) and/or a
standalone Chinese Markdown report. Both are written by default.
``output_path`` is treated as the basename/directory of the output:
the extension is decided by ``format``, so ``both`` writes
``<base>.html`` and ``<base>.md``.
Args:
review_data: Review data dict (metrics, findings, verdict, tier,
scope, files, baseline, timestamp, manual_review).
output_path: Output file path. Defaults to
``<repo_root>/code-review-report.html``.
output_path: Output base path (without an extension). Defaults to
``<repo_root>/code-review-report``.
repo_root: Repository root. Auto-detected if omitted.
format: Output format. ``html``, ``markdown``, or ``both``
(default: both).
"""
if format not in ("html", "markdown", "both"):
return _error_response(
f"Invalid format {format!r}; expected 'html', 'markdown' or 'both'."
)
store, root = _get_store(repo_root)
try:
data = build_report_data(review_data)
data["summary"] = review_data.get("summary", "")
template = _load_report_template()
rendered = template.replace(
"{{REPORT_DATA}}", json.dumps(data, ensure_ascii=False)
)
if output_path:
out = Path(output_path)
if not out.is_absolute():
out = root / out
base = Path(output_path)
if not base.is_absolute():
base = root / base
else:
out = root / "code-review-report.html"
base = root / "code-review-report"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(rendered, encoding="utf-8")
written: list[dict[str, Any]] = []
if format in ("html", "both"):
template = _load_report_template()
rendered = template.replace(
"{{REPORT_DATA}}", json.dumps(data, ensure_ascii=False)
)
html_path = base.with_suffix(".html")
html_path.parent.mkdir(parents=True, exist_ok=True)
html_path.write_text(rendered, encoding="utf-8")
written.append({
"format": "html",
"output_path": str(html_path),
"size_bytes": len(rendered),
})
if format in ("markdown", "both"):
rendered_md = render_markdown_report(review_data)
md_path = base.with_suffix(".md")
md_path.parent.mkdir(parents=True, exist_ok=True)
md_path.write_text(rendered_md, encoding="utf-8")
written.append({
"format": "markdown",
"output_path": str(md_path),
"size_bytes": len(rendered_md),
})
paths = [w["output_path"] for w in written]
return {
"status": "ok",
"summary": f"Report written to {out}",
"output_path": str(out),
"report_size_bytes": len(rendered),
"summary": f"Report written to {', '.join(paths)}",
"output_path": paths[0] if len(paths) == 1 else paths,
"files": written,
"report_size_bytes": sum(w["size_bytes"] for w in written),
}
except Exception as exc:
return _error_response(str(exc))