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:
@@ -0,0 +1,42 @@
|
||||
---
|
||||
description: Whole-project or single-feature code review (not diff-based) using graph-wide analysis and objective scoring.
|
||||
agent: build
|
||||
---
|
||||
|
||||
# Project Review
|
||||
|
||||
Review the entire codebase or a single feature/module, independent of the git diff. The scope is driven by your instruction.
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
**Token optimization:** Before starting, call `get_docs_section_tool(section_name="project-review")` for the optimized workflow.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Parse the scope** from the user instruction:
|
||||
- "对项目代码进行全面审查" / "全面审查" / "整个项目" → `scope=whole-project`
|
||||
- "审查 <功能/模块> 的代码" (e.g. payment, auth) → `scope=feature`, target=<keyword>
|
||||
|
||||
2. **Ensure the graph is current** by calling `build_or_update_graph_tool()`.
|
||||
|
||||
3. **Map the architecture** by calling `get_architecture_overview_tool(detail_level="minimal")` and `list_communities_tool(detail_level="minimal")`.
|
||||
|
||||
4. **Scan high-risk areas** (whole-project): `get_knowledge_gaps_tool()`, `get_hub_nodes_tool()`, `get_bridge_nodes_tool()`, `find_large_functions_tool()`, `get_surprising_connections_tool()`.
|
||||
|
||||
5. **Score objectively**:
|
||||
- whole-project: `score_review_tool(all_files=True)` — every source file in the graph
|
||||
- feature: `semantic_search_nodes_tool(query=<target>)` + `query_graph_tool(pattern="children_of", target=<target>)` to locate files, then `score_review_tool(changed_files=<files>)` + `get_impact_radius_tool(changed_files=<files>)`
|
||||
|
||||
6. **Review the code** (Layer 1 chain decomposition): eight categories + gstack CRITICAL sub-pass. Produce findings with severity (blocker/major/minor), confidence (1-10), file:line, and proposed fix.
|
||||
|
||||
7. **Merge findings** by calling `dedupe_findings_tool(findings=<your findings>)`.
|
||||
|
||||
8. **Generate the report** by calling `generate_report_tool(review_data=<verdict, scope, metrics, merged findings>)` — writes `code-review-report.html` and `code-review-report.md` (default `format="both"`).
|
||||
|
||||
9. **Report** the verdict (✅ PASS / ❌ FAIL), severity counts, each issue with confidence + fix, and manual-review items.
|
||||
|
||||
## Important Rules
|
||||
|
||||
- **READ-ONLY.** This workflow never modifies code, commits, or pushes. Every finding waits for a manual fix decision.
|
||||
- **Any blocker → verdict ❌ FAIL**, regardless of other scores.
|
||||
- This is **not** a diff review. For diff-based review use `/code-review-graph-unified-review`.
|
||||
@@ -27,7 +27,7 @@ $ARGUMENTS
|
||||
|
||||
7. **Merge findings** by calling `dedupe_findings_tool(findings=<your findings>)` — fingerprint dedup, multi-source confidence boost, PR quality score.
|
||||
|
||||
8. **Generate the report** by calling `generate_report_tool(review_data=<verdict, tier, scope, metrics, merged findings>)` — writes `code-review-report.html`.
|
||||
8. **Generate the report** by calling `generate_report_tool(review_data=<verdict, tier, scope, metrics, merged findings>)` — writes `code-review-report.html` and `code-review-report.md` (default `format="both"`).
|
||||
|
||||
9. **Report** the verdict (✅ PASS / ❌ FAIL), severity counts, each issue with confidence + fix, and manual-review items.
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
`dedupe_findings_tool` (fingerprint dedup, multi-source confidence boost,
|
||||
PR quality score), and `generate_report_tool` (standalone
|
||||
`code-review-report.html`). See `code_review_graph/scoring.py`.
|
||||
- Added the **project-review** workflow for whole-project or single-feature
|
||||
code review independent of the git diff. Scope is parsed from the user
|
||||
instruction (全面/整个项目 → whole-project; otherwise feature + target
|
||||
keyword). `score_review_tool` gained an `all_files` parameter that scores
|
||||
every source file in the graph. Ships as a skill, an MCP prompt
|
||||
(`project_review`), and the `/code-review-graph:project-review` command.
|
||||
- Added a Chinese Markdown review report alongside the HTML one;
|
||||
`generate_report_tool` defaults to `format="both"` writing
|
||||
`code-review-report.html` + `code-review-report.md`.
|
||||
|
||||
- Added a Voyage AI embedding provider (`--provider voyage`, key from
|
||||
`VOYAGE_API_KEY`, opt-in request throttling via
|
||||
|
||||
@@ -19,7 +19,7 @@ When using code-review-graph MCP tools, follow these rules:
|
||||
- `custom_languages.py` — Config-driven custom language support (`.code-review-graph/languages.toml`, see docs/CUSTOM_LANGUAGES.md)
|
||||
- `graph.py` — SQLite-backed graph store (nodes, edges, weighted-score impact analysis)
|
||||
- `tools/` — 31 MCP tool implementations split by domain
|
||||
- `main.py` — FastMCP server entry point, registers 31 tools + 6 prompts
|
||||
- `main.py` — FastMCP server entry point, registers 31 tools + 7 prompts
|
||||
- `incremental.py` — Git-based change detection, file watching
|
||||
- `embeddings.py` — Optional vector embeddings (local sentence-transformers, OpenAI-compatible endpoints, Google Gemini, MiniMax)
|
||||
- `visualization.py` — D3.js interactive HTML graph generator
|
||||
@@ -30,7 +30,7 @@ When using code-review-graph MCP tools, follow these rules:
|
||||
- `changes.py` — Risk-scored change impact analysis (detect-changes)
|
||||
- `refactor.py` — Rename preview, dead code detection, refactoring suggestions
|
||||
- `hints.py` — Review hint generation
|
||||
- `prompts.py` — 6 MCP prompt templates (review_changes, architecture_map, debug_issue, onboard_developer, pre_merge_check, unified_review)
|
||||
- `prompts.py` — 7 MCP prompt templates (review_changes, architecture_map, debug_issue, onboard_developer, pre_merge_check, unified_review, project_review)
|
||||
- `wiki.py` — Markdown wiki generation from community structure
|
||||
- `skills.py` — Multi-platform install/config generation and shipped skill metadata
|
||||
- `registry.py` — Multi-repo registry helpers
|
||||
|
||||
+2
-2
@@ -253,8 +253,8 @@ code-review-graph serve # MCP सर्वर शुरू करे
|
||||
| `list_repos_tool` | रजिस्टर्ड रिपॉज़िटरीज़ की सूची |
|
||||
| `cross_repo_search_tool` | सभी रजिस्टर्ड रिपॉज़िटरीज़ में सर्च करें |
|
||||
|
||||
**MCP प्रॉम्प्ट्स** (6 वर्कफ़्लो टेम्प्लेट):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`
|
||||
**MCP प्रॉम्प्ट्स** (7 वर्कफ़्लो टेम्प्लेट):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`, `project_review`
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+2
-2
@@ -255,8 +255,8 @@ code-review-graph serve # MCPサーバーの起動
|
||||
| `list_repos_tool` | 登録済みリポジトリの一覧 |
|
||||
| `cross_repo_search_tool` | 全登録リポジトリを横断検索 |
|
||||
|
||||
**MCPプロンプト**(6つのワークフローテンプレート):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`
|
||||
**MCPプロンプト**(7つのワークフローテンプレート):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`, `project_review`
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+2
-2
@@ -255,8 +255,8 @@ code-review-graph serve # MCP 서버 시작
|
||||
| `list_repos_tool` | 등록된 저장소 목록 |
|
||||
| `cross_repo_search_tool` | 등록된 모든 저장소에서 검색 |
|
||||
|
||||
**MCP 프롬프트** (6개 워크플로 템플릿):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`
|
||||
**MCP 프롬프트** (7개 워크플로 템플릿):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`, `project_review`
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ The benchmark also runs an honest **co-change mode**: the predictor is seeded wi
|
||||
| **Wiki generation** | Auto-generate markdown wiki from community structure |
|
||||
| **Multi-repo registry** | Register multiple repos, search across all of them |
|
||||
| **Multi-repo daemon** | `crg-daemon` watches multiple repos as child processes, with health checks and auto-restart |
|
||||
| **MCP prompts** | 6 workflow templates: review, architecture, debug, onboard, pre-merge, unified-review |
|
||||
| **MCP prompts** | 7 workflow templates: review, architecture, debug, onboard, pre-merge, unified-review, project-review |
|
||||
| **Full-text search** | FTS5-powered hybrid search combining keyword and vector similarity |
|
||||
| **Local storage** | SQLite file in `.code-review-graph/`. Core graph storage needs no external database or cloud service. |
|
||||
| **Watch mode** | Continuous graph updates as you work |
|
||||
@@ -501,10 +501,10 @@ Your AI assistant uses these automatically once the graph is built.
|
||||
| `cross_repo_search_tool` | Search across all registered repositories |
|
||||
| `score_review_tool` | Objective Layer-2 review metrics (good/warn/fail grades) |
|
||||
| `dedupe_findings_tool` | Fingerprint dedup + multi-source confidence merge |
|
||||
| `generate_report_tool` | Render the standalone HTML code review report |
|
||||
| `generate_report_tool` | Render the HTML and/or Markdown review report |
|
||||
|
||||
**MCP Prompts** (6 workflow templates):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`
|
||||
**MCP Prompts** (7 workflow templates):
|
||||
`review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check`, `unified_review`, `project_review`
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+2
-2
@@ -253,8 +253,8 @@ code-review-graph serve # 启动 MCP 服务器
|
||||
| `list_repos_tool` | 列出已注册的仓库 |
|
||||
| `cross_repo_search_tool` | 跨所有注册仓库搜索 |
|
||||
|
||||
**MCP 提示模板**(6 种工作流模板):
|
||||
`review_changes`、`architecture_map`、`debug_issue`、`onboard_developer`、`pre_merge_check`、`unified_review`
|
||||
**MCP 提示模板**(7 种工作流模板):
|
||||
`review_changes`、`architecture_map`、`debug_issue`、`onboard_developer`、`pre_merge_check`、`unified_review`、`project_review`
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -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>
|
||||
Tier: <code>${esc(data.tier || "standard")}</code>
|
||||
Scope: <code>${esc(data.scope || "change-level")}</code>
|
||||
${data.baseline ? ` Baseline: <code>${esc(data.baseline)}</code>` : ""}
|
||||
<span class="verdict ${verdictClass(data.verdict)}">${esc(data.verdict || "无结论")}</span>
|
||||
档位: <code>${esc(data.tier || "standard")}</code>
|
||||
范围: <code>${esc(data.scope || "change-level")}</code>
|
||||
${data.baseline ? ` 基线: <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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -210,8 +211,90 @@ def unified_review_prompt(
|
||||
"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'
|
||||
'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}")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
if output_path:
|
||||
base = Path(output_path)
|
||||
if not base.is_absolute():
|
||||
base = root / base
|
||||
else:
|
||||
base = root / "code-review-report"
|
||||
|
||||
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 output_path:
|
||||
out = Path(output_path)
|
||||
if not out.is_absolute():
|
||||
out = root / out
|
||||
else:
|
||||
out = root / "code-review-report.html"
|
||||
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),
|
||||
})
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(rendered, encoding="utf-8")
|
||||
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))
|
||||
|
||||
+26
-5
@@ -28,6 +28,13 @@ Three-layer unified code review (CRG graph context + ai-code-review scoring + gs
|
||||
- Fingerprint merge + quality score via `dedupe_findings_tool`
|
||||
- Standalone HTML report via `generate_report_tool`
|
||||
|
||||
### `/code-review-graph:project-review`
|
||||
Whole-project or single-feature code review (not diff-based).
|
||||
- Scope parsed from user instruction: 全面/整个项目 → whole-project; otherwise feature + target
|
||||
- Whole-project: `score_review_tool(all_files=True)` scores every source file
|
||||
- Feature: `semantic_search_nodes` + `query_graph(children_of)` locate the code
|
||||
- Read-only: every finding waits for a manual fix decision
|
||||
|
||||
## MCP Tools
|
||||
|
||||
### Core Tools
|
||||
@@ -259,11 +266,14 @@ findings to the appendix, and computes `PR quality score = max(0, 10 -
|
||||
#### `generate_report_tool`
|
||||
```
|
||||
review_data: dict # metrics + findings + verdict + tier + scope
|
||||
output_path: str | None # Default: <repo_root>/code-review-report.html
|
||||
repo_root: str | None
|
||||
output_path: str | None # Base name without extension
|
||||
repo_root: str | None # Default: <repo_root>/code-review-report
|
||||
format: str = "both" # "html" | "markdown" | "both"
|
||||
```
|
||||
Renders the standalone HTML code review report (self-contained, no external
|
||||
dependencies) from the bundled `report-template.html`.
|
||||
Renders the standalone HTML review report (self-contained, no external
|
||||
dependencies) and/or the Chinese Markdown report from the bundled
|
||||
`report-template.html`. Default `format="both"` writes
|
||||
`code-review-report.html` + `code-review-report.md`.
|
||||
|
||||
#### `refactor_tool`
|
||||
```
|
||||
@@ -310,7 +320,7 @@ kind: str | None
|
||||
limit: int = 20
|
||||
```
|
||||
|
||||
## MCP Prompts (6 workflow templates)
|
||||
## MCP Prompts (7 workflow templates)
|
||||
|
||||
### `review_changes`
|
||||
Pre-commit review workflow using detect_changes, affected_flows, and test gaps.
|
||||
@@ -346,6 +356,17 @@ base: str = "HEAD~1"
|
||||
tier: str = "standard" # fast | standard | strict
|
||||
```
|
||||
|
||||
### `project_review`
|
||||
Whole-project or single-feature code review (not diff-based). Reviews
|
||||
the entire codebase or a single feature/module using graph-wide analysis
|
||||
and objective scoring. READ-ONLY: every finding waits for a manual fix
|
||||
decision. Scope is parsed from the user instruction (全面/整个项目 →
|
||||
whole-project; otherwise feature + target keyword).
|
||||
```
|
||||
scope: str = "whole-project" # whole-project | feature
|
||||
target: str = "" # feature/module keyword when scope="feature"
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
- [USAGE.md](USAGE.md) -- How to install and use
|
||||
- [FAQ.md](FAQ.md) -- How it compares to LSP, RAG, grep, and similar tools; when not to use it
|
||||
- [FEATURES.md](FEATURES.md) -- What's included, changelog
|
||||
- [COMMANDS.md](COMMANDS.md) -- All 31 MCP tools, 6 MCP prompts, skills, and CLI commands
|
||||
- [COMMANDS.md](COMMANDS.md) -- All 31 MCP tools, 7 MCP prompts, skills, and CLI commands
|
||||
- [GITHUB_ACTION.md](GITHUB_ACTION.md) -- Risk-scored PR review comments via GitHub Actions
|
||||
- [CUSTOM_LANGUAGES.md](CUSTOM_LANGUAGES.md) -- Bring your own language via `.code-review-graph/languages.toml`
|
||||
- [LLM-OPTIMIZED-REFERENCE.md](../code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md) -- Token-optimized reference for MCP-capable AI coding agents
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ MCP Server (stdio or localhost HTTP) │ │
|
||||
│ │ │ │
|
||||
│ │ 31 MCP Tools + 6 MCP Prompts │ │
|
||||
│ │ 31 MCP Tools + 7 MCP Prompts │ │
|
||||
│ │ ├── Core: build, impact, query, review, │ │
|
||||
│ │ │ search, traverse, embed, stats, docs │ │
|
||||
│ │ ├── Flows: list, get, affected │ │
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: project-review
|
||||
description: Whole-project or single-feature code review (not diff-based) using graph-wide analysis and objective scoring
|
||||
---
|
||||
|
||||
# Project Review
|
||||
|
||||
Review the entire codebase or a single feature/module, independent of the git diff. Two scopes, driven by the user's instruction:
|
||||
|
||||
- **whole-project**: "对项目代码进行全面审查", "全面审查", "整个项目" → review every source file in the graph.
|
||||
- **feature**: "审查 <功能/模块> 的代码" (e.g. payment, auth) → review only the code related to the target.
|
||||
|
||||
**This skill is READ-ONLY.** Every finding is presented to the user for a manual fix decision. Never apply code changes, commit, or push.
|
||||
|
||||
## Token Efficiency Rules
|
||||
- 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.
|
||||
|
||||
## Step 0 - Parse the scope
|
||||
|
||||
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.
|
||||
|
||||
## Step 1 - Graph ready
|
||||
|
||||
1. Call `build_or_update_graph_tool()` to ensure the graph is current.
|
||||
2. Call `get_minimal_context_tool(task="project review")` for stats and community overview.
|
||||
|
||||
## Step 2 - Architecture map
|
||||
|
||||
Call `get_architecture_overview_tool(detail_level="minimal")` and `list_communities_tool(detail_level="minimal")` to map the module structure.
|
||||
|
||||
## Step 3 - High-risk scan (whole-project)
|
||||
|
||||
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.
|
||||
|
||||
## Step 4 - Objective scoring
|
||||
|
||||
- whole-project: `score_review_tool(all_files=True)` scores every source file in the graph.
|
||||
- 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.
|
||||
|
||||
## Step 5 - Chain decomposition
|
||||
|
||||
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 ✅ / ⚠️ / —.
|
||||
|
||||
## Step 6 - Manual adjudication (READ-ONLY)
|
||||
|
||||
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.**
|
||||
|
||||
## Step 7 - Acceptance gate
|
||||
|
||||
Any 🔴 blocker → verdict `❌ FAIL`. Classify each finding as Ready / Needs Fix / Unusable.
|
||||
|
||||
## Step 8 - Report
|
||||
|
||||
Call `generate_report_tool(review_data=<verdict, scope, metrics, findings>)` to write `code-review-report.html` and `code-review-report.md` (default `format="both"`).
|
||||
|
||||
## Output Format
|
||||
|
||||
`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.
|
||||
|
||||
## Token Efficiency Rules
|
||||
- ALWAYS start with `get_minimal_context(task="project review")` before any other graph tool.
|
||||
- Use `detail_level="minimal"` on all calls. Only escalate to `"standard"` when minimal is insufficient.
|
||||
- Target: complete a project review in ≤12 tool calls and ≤1800 total output tokens.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Unified Review — Common Mistakes
|
||||
|
||||
- **Skipping graph context** — always run `get_minimal_context` first; CRG
|
||||
context is what makes the review token-efficient and blast-radius aware.
|
||||
- **Rushing to fix** — this skill is READ-ONLY. Present findings, wait for
|
||||
user decision. Never apply fixes, commit, or push.
|
||||
- **Ignoring tier** — read `.code-review.yaml`. `fast` skips Layer 2/3;
|
||||
`strict` requires per-item confirmation for every blocker/major.
|
||||
- **Judging metrics without evidence** — `score_review_tool` outputs are
|
||||
heuristics. Cite the evidence, and let the LLM confirm SQL/exception/vuln
|
||||
findings before presenting them as facts.
|
||||
- **Missing manual-review modules** — payment, order, inventory, permission,
|
||||
distributed-lock, data-migration always require a manual review checklist.
|
||||
- **Forgetting enum completeness reads OUTSIDE the diff** — grep sibling
|
||||
values, then read each consumer; in-diff review alone is insufficient.
|
||||
- **Batch-skipping blockers** — 🔴 blockers cannot be batch-skipped; each
|
||||
needs an explicit user decision.
|
||||
- **Not producing the report** — always call `generate_report_tool` at the
|
||||
end and present the text report inline.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Data Migration — Manual Review Checklist
|
||||
|
||||
High-risk module: schema/data migration requires human confirmation.
|
||||
|
||||
- [ ] Migration is idempotent and re-runnable
|
||||
- [ ] Forward and rollback paths both defined and tested
|
||||
- [ ] Backfill is batched / resumable for large tables
|
||||
- [ ] Data type / precision changes do not silently truncate
|
||||
- [ ] Nullability and default changes safe for existing rows
|
||||
- [ ] Migration ordering across shards / replicas is consistent
|
||||
- [ ] Application deploys compatibly with both old and new schema (expand/contract)
|
||||
- [ ] Irreversible operations are flagged with a documented reason
|
||||
@@ -0,0 +1,11 @@
|
||||
# Distributed Lock — Manual Review Checklist
|
||||
|
||||
High-risk module: distributed-lock changes require architecture confirmation.
|
||||
|
||||
- [ ] Lock has a TTL / expiry — no permanent deadlock after crash
|
||||
- [ ] Lock release is atomic and ownership-checked (compare-and-delete)
|
||||
- [ ] Lock scope is correct (key includes the right entity identifiers)
|
||||
- [ ] Renewal / watchdog exists for long critical sections
|
||||
- [ ] Locking order is consistent across paths (no lock-ordering deadlock)
|
||||
- [ ] Fencing tokens / version check prevents stale-holder writes
|
||||
- [ ] Fail-open vs fail-closed behavior is intentional and documented
|
||||
@@ -0,0 +1,11 @@
|
||||
# Inventory Module — Manual Review Checklist
|
||||
|
||||
High-risk module: stock/inventory changes require human confirmation.
|
||||
|
||||
- [ ] Stock decrement is atomic (conditional UPDATE, not read-then-write)
|
||||
- [ ] Oversell prevented: `UPDATE ... SET qty = qty - ? WHERE qty >= ?`
|
||||
- [ ] Reservation vs. deduction semantics are consistent
|
||||
- [ ] Concurrent orders cannot both reserve the last unit
|
||||
- [ ] Restock/return increments handled correctly
|
||||
- [ ] Inventory events are idempotent (retry-safe)
|
||||
- [ ] Async stock updates propagate to downstream (warehouse, carts) safely
|
||||
@@ -0,0 +1,12 @@
|
||||
# Order Module — Manual Review Checklist
|
||||
|
||||
High-risk module: order lifecycle changes require human confirmation.
|
||||
|
||||
- [ ] State machine transitions are atomic (`WHERE status = ?` updates)
|
||||
- [ ] Cancellation / timeout / expiry paths complete all side effects
|
||||
- [ ] Order idempotency key prevents duplicate order creation
|
||||
- [ ] Price/lock snapshot captured at order time, not at payment time
|
||||
- [ ] Partial fulfillment / split-shipment handled
|
||||
- [ ] Negative or inconsistent totals impossible
|
||||
- [ ] Concurrent edits (cart + order) do not corrupt state
|
||||
- [ ] Audit trail: every status change logged with reason
|
||||
@@ -0,0 +1,14 @@
|
||||
# Payment Module — Manual Review Checklist
|
||||
|
||||
High-risk module: payment changes require human confirmation for every
|
||||
blocker/major fix.
|
||||
|
||||
- [ ] Callback idempotency: a duplicated webhook/callback does not double-charge
|
||||
- [ ] Amounts stored as fixed-point (integers/cents), never floats
|
||||
- [ ] Currency codes and precision handled correctly
|
||||
- [ ] Provider signature / HMAC verification on callbacks
|
||||
- [ ] Refund logic: correct reversal, no double-refund
|
||||
- [ ] Failure path: payment timeout, declined, retry semantics
|
||||
- [ ] Transaction boundary spans charge + order-state update
|
||||
- [ ] Sensitive data (PAN, tokens) never logged or masked on output
|
||||
- [ ] Ledger/journal entries are append-only and auditable
|
||||
@@ -0,0 +1,11 @@
|
||||
# Permission Module — Manual Review Checklist
|
||||
|
||||
High-risk module: authorization changes require product/human confirmation.
|
||||
|
||||
- [ ] Every endpoint/action enforces the intended permission — no default-allow
|
||||
- [ ] Role hierarchy / scoping (tenant, org, user) is consistent
|
||||
- [ ] Object-level permissions checked on read AND write
|
||||
- [ ] Deny-before-allow ordering is safe
|
||||
- [ ] Permission checks cannot be bypassed via IDs, query params, or bulk ops
|
||||
- [ ] New permission/role values handled by all consumers (enum completeness)
|
||||
- [ ] Sensitive actions audited with actor + target
|
||||
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>代码审查报告</title>
|
||||
<style>
|
||||
:root { --border:#d0d7de; --bg:#f6f8fa; --fg:#1f2328; --muted:#57606a;
|
||||
--good:#1a7f37; --warn:#9a6700; --fail:#cf222e; --na:#57606a;
|
||||
--blocker:#cf222e; --major:#9a6700; --minor:#57606a;
|
||||
--critical:#cf222e; --informational:#0969da; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
margin:0; padding:2rem 1rem; color:var(--fg); background:#fff; line-height:1.55; }
|
||||
.wrap { max-width:960px; margin:0 auto; }
|
||||
h1 { font-size:1.5rem; margin:0 0 .25rem; }
|
||||
h2 { font-size:1.1rem; margin:1.5rem 0 .5rem; padding-bottom:.25rem;
|
||||
border-bottom:1px solid var(--border); }
|
||||
.meta { color:var(--muted); font-size:.9rem; margin-bottom:1.5rem; }
|
||||
.verdict { display:inline-block; padding:.25rem .75rem; border-radius:20px;
|
||||
font-weight:700; font-size:.95rem; }
|
||||
.verdict.pass { background:#dafbe1; color:var(--good); }
|
||||
.verdict.fail { background:#ffebe9; color:var(--fail); }
|
||||
table { border-collapse:collapse; width:100%; margin:.75rem 0; }
|
||||
th,td { border:1px solid var(--border); padding:.45rem .6rem; text-align:left;
|
||||
font-size:.9rem; vertical-align:top; }
|
||||
th { background:var(--bg); }
|
||||
.grade.good { color:var(--good); font-weight:600; }
|
||||
.grade.warn { color:var(--warn); font-weight:600; }
|
||||
.grade.fail { color:var(--fail); font-weight:600; }
|
||||
.grade.na { color:var(--na); }
|
||||
.issue { margin:.6rem 0; padding:.65rem .8rem; border:1px solid var(--border);
|
||||
border-radius:6px; background:#fff; }
|
||||
.issue .tag { display:inline-block; padding:.1rem .5rem; border-radius:10px;
|
||||
font-size:.75rem; font-weight:700; color:#fff; margin-right:.5rem; }
|
||||
.tag.blocker, .tag.critical { background:var(--blocker); }
|
||||
.tag.major, .tag.warn { background:var(--major); }
|
||||
.tag.minor, .tag.informational { background:var(--minor); }
|
||||
.issue .cat { font-weight:600; }
|
||||
.issue .loc { color:var(--muted); font-size:.85rem; margin-top:.2rem; }
|
||||
.issue .fix { margin-top:.35rem; font-size:.88rem; background:var(--bg);
|
||||
padding:.4rem .6rem; border-radius:4px; }
|
||||
.muted { color:var(--muted); font-size:.85rem; }
|
||||
code { background:var(--bg); padding:.1rem .3rem; border-radius:4px;
|
||||
font-size:.88em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap" id="app"></div>
|
||||
<script>
|
||||
const data = {{REPORT_DATA}};
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? "").replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
})[c]);
|
||||
}
|
||||
|
||||
function verdictClass(v) {
|
||||
v = String(v || "").toUpperCase();
|
||||
return v.includes("PASS") ? "pass" : "fail";
|
||||
}
|
||||
|
||||
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 || "无结论")}</span>
|
||||
档位: <code>${esc(data.tier || "standard")}</code>
|
||||
范围: <code>${esc(data.scope || "change-level")}</code>
|
||||
${data.baseline ? ` 基线: <code>${esc(data.baseline)}</code>` : ""}
|
||||
</div>`;
|
||||
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>客观指标</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(label)}</td>
|
||||
<td>${esc(m.value ?? "N/A")}</td>
|
||||
<td class="grade ${esc(g)}">${esc(gradeText)}</td>
|
||||
<td class="muted">${esc(m.note || "")}</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += `</table>`;
|
||||
}
|
||||
|
||||
const issues = data.issues || [];
|
||||
html += `<h2>问题清单 (${issues.length})</h2>`;
|
||||
if (!issues.length) {
|
||||
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(sevLabel)}</span>
|
||||
<span class="cat">${esc(i.category)}</span>
|
||||
${esc(i.message || "")}
|
||||
${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>修复建议:</b> ${esc(i.fix)}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const manual = data.manual_review || [];
|
||||
if (manual.length) {
|
||||
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 判断的指标:</b> ${judged.map(esc).join(", ")}</p>`;
|
||||
}
|
||||
|
||||
document.getElementById("app").innerHTML = html;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
# Unified Review — Generic Checklist
|
||||
|
||||
Reference for the Layer-1 chain decomposition and gstack CRITICAL sub-pass.
|
||||
Load the language-specific checklist when available (`java-spring.md`,
|
||||
`python-django.md`, `python-fastapi.md`, `node-express.md`, `go-gin.md`,
|
||||
`csharp-dotnet.md`, `rust.md`, `php-laravel.md`, `ruby-rails.md`); otherwise
|
||||
use this generic list.
|
||||
|
||||
## Layer 1 — eight categories
|
||||
|
||||
For each changed area mark ✅ Clean / ⚠️ Issues Found / — N/A.
|
||||
|
||||
1. **Interface** — parameter validation, response conventions, HTTP status
|
||||
codes, rate limiting, API versioning, protocol correctness
|
||||
2. **Business** — logic aligns with requirements, state machine correctness,
|
||||
idempotency design, distributed locks
|
||||
3. **Data** — SQL injection, query performance, index usage, transaction
|
||||
boundaries, cache invalidation
|
||||
4. **Utility** — input validity, no side effects, error return values,
|
||||
date/time timezone handling
|
||||
5. **Error handling** — exception classification, fallback logic, error
|
||||
message sanitization, retry with backoff
|
||||
6. **Security** — AuthN/AuthZ, sensitive data masking, permission control,
|
||||
CSRF/XSS prevention
|
||||
7. **Performance** — N+1 queries, caching strategy, connection pooling,
|
||||
batch operations, blocking in async paths
|
||||
8. **Observability** — structured logging with correlation IDs, metrics,
|
||||
health checks
|
||||
|
||||
## gstack CRITICAL sub-pass (highest severity)
|
||||
|
||||
### SQL & Data Safety
|
||||
- String interpolation in SQL — use parameterized queries
|
||||
- TOCTOU check-then-set — use atomic `WHERE` + update
|
||||
- Bypassing model validations for direct DB writes
|
||||
- N+1 queries — missing eager loading
|
||||
|
||||
### Race Conditions & Concurrency
|
||||
- Read-check-write without uniqueness constraint / duplicate-key retry
|
||||
- find-or-create without a unique DB index
|
||||
- Status transitions not atomic (`WHERE old_status = ? UPDATE ...`)
|
||||
- Unsafe HTML rendering on user-controlled data
|
||||
|
||||
### LLM Output Trust Boundary
|
||||
- LLM-generated values (emails, URLs, names) written to DB without format
|
||||
validation
|
||||
- Structured tool output accepted without type/shape checks
|
||||
- LLM-generated URLs fetched without an allowlist (SSRF)
|
||||
- LLM output stored in knowledge bases without sanitization (stored prompt
|
||||
injection)
|
||||
|
||||
### Shell Injection
|
||||
- `subprocess` with `shell=True` AND interpolated command strings
|
||||
- `os.system()` with variable interpolation
|
||||
- `eval()`/`exec()` on LLM-generated code without sandboxing
|
||||
|
||||
### Enum & Value Completeness
|
||||
- New enum/status/tier values: read (not just grep) every consumer that
|
||||
switches/filters/displays the value
|
||||
- Check allowlists and `case`/`if-elsif` chains for fall-through
|
||||
|
||||
## Suppressions — do NOT flag
|
||||
- Harmless redundancy that aids readability
|
||||
- "Add a comment explaining a threshold" — thresholds drift
|
||||
- Consistency-only changes
|
||||
- Anything already addressed in the diff
|
||||
|
||||
## Severity
|
||||
- 🔴 **blocker** — must fix before merge (injection, secrets, missing
|
||||
transaction, auth bypass) → verdict FAIL
|
||||
- 🟡 **major** — should fix before merge (missing validation, missing
|
||||
fallback, N+1, unmasked data)
|
||||
- 🔵 **minor** — can optimize later (naming, duplicate code, comments)
|
||||
|
||||
## Confidence calibration
|
||||
- 9-10 verified by reading specific code
|
||||
- 7-8 high-confidence pattern match
|
||||
- 5-6 medium — show with caveat
|
||||
- 3-4 low — move to appendix
|
||||
- 1-2 speculation — suppress unless severity would be P0
|
||||
|
||||
Every finding: `[SEVERITY] (confidence: N/10) file:line — problem → fix`.
|
||||
@@ -0,0 +1,13 @@
|
||||
# API Contract Specialist
|
||||
|
||||
Focus: API and interface contract changes in the diff.
|
||||
|
||||
- [ ] Breaking changes to public endpoints (paths, params, response shape)
|
||||
- [ ] Versioning compatibility (deprecations, fallbacks)
|
||||
- [ ] Request/response validation matches the schema
|
||||
- [ ] Error response shape is consistent
|
||||
- [ ] Authentication/authorization behavior unchanged for existing consumers
|
||||
- [ ] Renamed/moved functions: all callers updated
|
||||
|
||||
Output JSON lines:
|
||||
`{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"api-contract","summary":"...","fix":"...","source":"api-contract"}`
|
||||
@@ -0,0 +1,15 @@
|
||||
# Data Migration Specialist
|
||||
|
||||
Focus: database schema and data migrations in the diff.
|
||||
|
||||
- [ ] Migration idempotent and re-runnable
|
||||
- [ ] Forward + rollback paths defined
|
||||
- [ ] Backfill batched / resumable
|
||||
- [ ] Type/precision changes do not truncate data
|
||||
- [ ] Nullability/default changes safe for existing rows
|
||||
- [ ] Application deploy compatible with old + new schema (expand/contract)
|
||||
|
||||
Insurance specialist — always runs, even when silent.
|
||||
|
||||
Output JSON lines:
|
||||
`{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"data-migration","summary":"...","fix":"...","source":"data-migration"}`
|
||||
@@ -0,0 +1,16 @@
|
||||
# Maintainability Specialist
|
||||
|
||||
Focus: code quality and maintainability issues.
|
||||
|
||||
- [ ] Dead code / unreachable branches / unused variables
|
||||
- [ ] Magic numbers → named constants
|
||||
- [ ] Overcomplicated abstractions (indirection without payoff)
|
||||
- [ ] Copy-paste blocks that should be shared (only when it aids clarity)
|
||||
- [ ] Functions too large / doing too much
|
||||
- [ ] Stale comments contradicting the code
|
||||
|
||||
Suppress: harmless redundancy that aids readability, comment-on-threshold
|
||||
requests, consistency-only changes.
|
||||
|
||||
Output JSON lines:
|
||||
`{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"maintainability","summary":"...","fix":"...","source":"maintainability"}`
|
||||
@@ -0,0 +1,14 @@
|
||||
# Performance Specialist
|
||||
|
||||
Focus: performance and resource efficiency in the diff.
|
||||
|
||||
- [ ] N+1 queries — missing eager loading
|
||||
- [ ] Unindexed lookups in hot loops
|
||||
- [ ] O(n×m) lookups in views/loops
|
||||
- [ ] Blocking calls in async paths (sync subprocess, requests, sleep)
|
||||
- [ ] Connection pool exhaustion, unbounded retries
|
||||
- [ ] Bundle/asset size regressions (frontend)
|
||||
- [ ] Redundant recomputation / missing caching
|
||||
|
||||
Output JSON lines:
|
||||
`{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"performance","summary":"...","fix":"...","source":"performance"}`
|
||||
@@ -0,0 +1,18 @@
|
||||
# Red Team Specialist (conditional)
|
||||
|
||||
Focus: find what the primary and specialist reviewers MISSED. Only dispatched
|
||||
when the diff is large (>200 lines) or a specialist found a critical issue.
|
||||
|
||||
Think like an attacker and a chaos engineer:
|
||||
|
||||
- [ ] Cross-cutting concerns the specialist checklists do not cover
|
||||
- [ ] Integration boundary failures (service-to-service, module-to-module)
|
||||
- [ ] Failure modes: what breaks in production under load, restart, partial
|
||||
failure
|
||||
- [ ] Silent data corruption paths (wrong results without errors)
|
||||
- [ ] Error handling that swallows failures
|
||||
- [ ] Trust boundary violations
|
||||
- [ ] Race conditions and edge cases the primary review missed
|
||||
|
||||
Be adversarial. No compliments — just the problems. Tag findings with
|
||||
`"source":"red-team"`. Output `NO FINDINGS` when nothing new is found.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Security Specialist
|
||||
|
||||
Focus: security vulnerabilities in the diff.
|
||||
|
||||
- [ ] SQL injection (string interpolation, parameterized queries)
|
||||
- [ ] AuthN/AuthZ bypasses, missing permission checks
|
||||
- [ ] XSS (unsafe HTML rendering on user data)
|
||||
- [ ] Sensitive data exposure / missing masking in logs and responses
|
||||
- [ ] SSRF (fetching user/LLM-controlled URLs without allowlist)
|
||||
- [ ] Command injection (`shell=True` + interpolation)
|
||||
- [ ] Hardcoded secrets / credentials
|
||||
- [ ] CSRF / missing rate limiting on auth endpoints
|
||||
|
||||
Insurance specialist — always runs, even when silent.
|
||||
|
||||
Output JSON lines:
|
||||
`{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"security","summary":"...","fix":"...","source":"security"}`
|
||||
@@ -0,0 +1,14 @@
|
||||
# Testing Specialist
|
||||
|
||||
Focus: test coverage gaps and tests that would catch the issues found.
|
||||
|
||||
- [ ] Every changed function has at least a happy-path test
|
||||
- [ ] Negative/error paths tested (invalid input, failure branches)
|
||||
- [ ] Edge cases mirror the happy-path structure
|
||||
- [ ] If the fix for a finding can be caught by a test, propose a minimal
|
||||
`test_stub` (framework-detected: jest/vitest/rspec/pytest/go-test)
|
||||
- [ ] Integration coverage for critical flows (DB, external calls)
|
||||
- [ ] No assertion-only tests that pass trivially
|
||||
|
||||
Output JSON lines:
|
||||
`{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"testing","summary":"...","fix":"...","test_stub":"...","source":"testing"}`
|
||||
@@ -53,7 +53,7 @@ Any 🔴 blocker → verdict `❌ FAIL` regardless of other scores. Classify eac
|
||||
|
||||
## Step 8 - Report
|
||||
|
||||
Call `generate_report_tool(review_data=<collected verdict, metrics, findings, tier, scope>)` to write `code-review-report.html`. Also present the text report inline.
|
||||
Call `generate_report_tool(review_data=<collected verdict, metrics, findings, tier, scope>)` to write `code-review-report.html` and `code-review-report.md` (default `format="both"`). Also present the text report inline.
|
||||
|
||||
## Step 9 - Persistence (optional)
|
||||
|
||||
|
||||
@@ -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>
|
||||
Tier: <code>${esc(data.tier || "standard")}</code>
|
||||
Scope: <code>${esc(data.scope || "change-level")}</code>
|
||||
${data.baseline ? ` Baseline: <code>${esc(data.baseline)}</code>` : ""}
|
||||
<span class="verdict ${verdictClass(data.verdict)}">${esc(data.verdict || "无结论")}</span>
|
||||
档位: <code>${esc(data.tier || "standard")}</code>
|
||||
范围: <code>${esc(data.scope || "change-level")}</code>
|
||||
${data.baseline ? ` 基线: <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;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Tests for the project-review workflow (whole-project / feature scope)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from fastmcp.prompts.prompt import Message # noqa: E402
|
||||
|
||||
from code_review_graph.prompts import project_review_prompt # noqa: E402
|
||||
|
||||
|
||||
def _text(msg: Message) -> str:
|
||||
return msg.content.text
|
||||
|
||||
|
||||
class TestProjectReviewPromptRendering:
|
||||
def test_whole_project_renders_full_workflow(self):
|
||||
text = _text(project_review_prompt()[0])
|
||||
# Header + shared steps 1-4 must all be present.
|
||||
assert "## Project Review Workflow" in text
|
||||
assert "scope=whole-project" in text
|
||||
assert "1. Call" in text
|
||||
assert "4. Locate high-risk" in text
|
||||
# whole-project branch uses all_files=True.
|
||||
assert "all_files=True" in text
|
||||
assert "get_knowledge_gaps" in text
|
||||
|
||||
def test_feature_renders_feature_branch(self):
|
||||
text = _text(project_review_prompt(scope="feature", target="checkout")[0])
|
||||
assert "scope=feature" in text
|
||||
assert "target=checkout" in text
|
||||
# Shared steps 1-4 must still be present (no truncation).
|
||||
assert "4. Locate high-risk" in text
|
||||
# Feature branch: semantic search + impact radius, not all_files.
|
||||
assert "semantic_search_nodes" in text
|
||||
assert "get_impact_radius" in text
|
||||
assert "all_files=True" not in text
|
||||
|
||||
def test_default_scope_is_whole_project(self):
|
||||
text = _text(project_review_prompt()[0])
|
||||
assert "scope=whole-project" in text
|
||||
|
||||
def test_read_only_present(self):
|
||||
text = _text(project_review_prompt()[0])
|
||||
assert "READ-ONLY" in text
|
||||
|
||||
def test_both_branches_have_preamble(self):
|
||||
assert "get_minimal_context" in _text(project_review_prompt()[0])
|
||||
assert "get_minimal_context" in _text(
|
||||
project_review_prompt(scope="feature", target="auth")[0]
|
||||
)
|
||||
|
||||
def test_both_branches_end_with_report_step(self):
|
||||
whole = _text(project_review_prompt()[0])
|
||||
feat = _text(project_review_prompt(scope="feature", target="x")[0])
|
||||
assert "generate_report" in whole
|
||||
assert "generate_report" in feat
|
||||
@@ -7,6 +7,7 @@ from code_review_graph.prompts import (
|
||||
debug_issue_prompt,
|
||||
onboard_developer_prompt,
|
||||
pre_merge_check_prompt,
|
||||
project_review_prompt,
|
||||
review_changes_prompt,
|
||||
unified_review_prompt,
|
||||
)
|
||||
@@ -214,6 +215,58 @@ class TestUnifiedReviewPrompt:
|
||||
assert "FAIL" in _text(result[0])
|
||||
|
||||
|
||||
class TestProjectReviewPrompt:
|
||||
def test_returns_list_with_messages(self):
|
||||
result = project_review_prompt()
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_message_has_role_and_content(self):
|
||||
result = project_review_prompt()
|
||||
for msg in result:
|
||||
assert isinstance(msg, Message)
|
||||
assert msg.role == "user"
|
||||
assert _text(msg)
|
||||
|
||||
def test_default_whole_project_scope(self):
|
||||
result = project_review_prompt()
|
||||
text = _text(result[0])
|
||||
assert "whole-project" in text
|
||||
assert "all_files=True" in text
|
||||
|
||||
def test_feature_scope_with_target(self):
|
||||
result = project_review_prompt(scope="feature", target="payment")
|
||||
text = _text(result[0])
|
||||
assert "scope=feature" in text
|
||||
assert "target=payment" in text
|
||||
|
||||
def test_mentions_architecture_scan(self):
|
||||
result = project_review_prompt()
|
||||
text = _text(result[0])
|
||||
assert "get_architecture_overview" in text
|
||||
assert "list_communities" in text
|
||||
|
||||
def test_mentions_high_risk_scan(self):
|
||||
result = project_review_prompt()
|
||||
text = _text(result[0])
|
||||
assert "get_knowledge_gaps" in text
|
||||
assert "get_hub_nodes" in text
|
||||
|
||||
def test_feature_mentions_semantic_search(self):
|
||||
result = project_review_prompt(scope="feature", target="auth")
|
||||
text = _text(result[0])
|
||||
assert "semantic_search_nodes" in text
|
||||
assert "get_impact_radius" in text
|
||||
|
||||
def test_mentions_read_only(self):
|
||||
result = project_review_prompt()
|
||||
assert "READ-ONLY" in _text(result[0])
|
||||
|
||||
def test_mentions_generate_report(self):
|
||||
result = project_review_prompt()
|
||||
assert "generate_report" in _text(result[0])
|
||||
|
||||
|
||||
class TestTokenEfficiencyPreamble:
|
||||
"""All prompts should include the token efficiency preamble."""
|
||||
|
||||
@@ -241,3 +294,7 @@ class TestTokenEfficiencyPreamble:
|
||||
def test_unified_review_has_preamble(self):
|
||||
result = unified_review_prompt()
|
||||
assert "get_minimal_context" in _text(result[0])
|
||||
|
||||
def test_project_review_has_preamble(self):
|
||||
result = project_review_prompt()
|
||||
assert "get_minimal_context" in _text(result[0])
|
||||
|
||||
+111
-19
@@ -1,4 +1,4 @@
|
||||
"""Tests for the unified-review HTML report tool."""
|
||||
"""Tests for the unified-review HTML/Markdown report tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -9,13 +9,14 @@ import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from code_review_graph.scoring import build_report_data # noqa: E402
|
||||
from code_review_graph.scoring import ( # noqa: E402
|
||||
build_report_data,
|
||||
render_markdown_report,
|
||||
)
|
||||
from code_review_graph.tools.scoring_tools import ( # noqa: E402
|
||||
_load_report_template,
|
||||
generate_report_func,
|
||||
)
|
||||
|
||||
|
||||
def _review_data() -> dict:
|
||||
return {
|
||||
"scope": "change-level",
|
||||
@@ -24,6 +25,8 @@ def _review_data() -> dict:
|
||||
"files": "app.py, main.py",
|
||||
"baseline": "generic",
|
||||
"verdict": "\u2705 PASS",
|
||||
"quality_score": 7.0,
|
||||
"counts": {"critical": 1, "informational": 2},
|
||||
"metrics": {
|
||||
"sql_risk": {"value": 0, "grade": "good", "note": "heuristic"},
|
||||
"exception_coverage": {"value": 33.33, "grade": "warn", "note": "heuristic"},
|
||||
@@ -44,6 +47,11 @@ def _review_data() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _mkroot(tmp_path: Path) -> Path:
|
||||
Path(tmp_path, ".code-review-graph").mkdir(exist_ok=True)
|
||||
return tmp_path
|
||||
|
||||
|
||||
class TestBuildReportData:
|
||||
def test_normalises_review_data(self):
|
||||
data = build_report_data(_review_data())
|
||||
@@ -55,6 +63,11 @@ class TestBuildReportData:
|
||||
assert data["issues"][0]["location"] == "src/app.py:42"
|
||||
assert "requirement_coverage" in data["llm_judged"]
|
||||
|
||||
def test_quality_score_and_counts_carried(self):
|
||||
data = build_report_data(_review_data())
|
||||
assert data["quality_score"] == 7.0
|
||||
assert data["counts"] == {"critical": 1, "informational": 2}
|
||||
|
||||
def test_empty_findings(self):
|
||||
rd = _review_data()
|
||||
rd["findings"] = []
|
||||
@@ -76,29 +89,108 @@ class TestLoadTemplate:
|
||||
assert template.startswith("<!DOCTYPE html>")
|
||||
|
||||
|
||||
class TestRenderMarkdownReport:
|
||||
def test_renders_chinese_structure(self):
|
||||
md = render_markdown_report(_review_data())
|
||||
assert "# 代码审查报告" in md
|
||||
assert "结论" in md
|
||||
assert "PR 质量分" in md and "7.0/10" in md
|
||||
assert "客观指标" in md
|
||||
assert "SQL 注入风险" in md
|
||||
assert "异常分支覆盖" in md
|
||||
assert "问题清单" in md
|
||||
assert "🟡 主要" in md
|
||||
assert "置信度 7/10" in md
|
||||
assert "修复建议" in md
|
||||
assert "需要人工审查" in md
|
||||
assert "Payment callback idempotency" in md
|
||||
assert "需 LLM 判断的指标" in md
|
||||
|
||||
def test_grade_localisation(self):
|
||||
rd = _review_data()
|
||||
rd["metrics"] = {
|
||||
"sql_risk": {"value": 0, "grade": "good"},
|
||||
"vulnerability_risk": {"value": 2, "grade": "fail"},
|
||||
"high_risk_density": {"value": None, "grade": "na"},
|
||||
}
|
||||
md = render_markdown_report(rd)
|
||||
assert "良好" in md
|
||||
assert "不合格" in md
|
||||
assert "不适用" in md
|
||||
|
||||
def test_empty_report(self):
|
||||
md = render_markdown_report({})
|
||||
assert "# 代码审查报告" in md
|
||||
assert "未发现问题" in md
|
||||
|
||||
|
||||
class TestGenerateReport:
|
||||
def test_writes_self_contained_html(self, tmp_path):
|
||||
Path(tmp_path, ".code-review-graph").mkdir(exist_ok=True)
|
||||
out = tmp_path / "sub" / "report.html"
|
||||
def test_both_writes_html_and_md(self, tmp_path):
|
||||
root = _mkroot(tmp_path)
|
||||
result = generate_report_func(
|
||||
_review_data(),
|
||||
output_path=str(out),
|
||||
repo_root=str(tmp_path),
|
||||
output_path="sub/report",
|
||||
repo_root=str(root),
|
||||
)
|
||||
assert result["status"] == "ok"
|
||||
assert Path(result["output_path"]).is_file()
|
||||
html = Path(result["output_path"]).read_text(encoding="utf-8")
|
||||
assert "{{REPORT_DATA}}" not in html
|
||||
assert "\u2705 PASS" in html
|
||||
assert "N+1" in html
|
||||
assert isinstance(result["output_path"], list)
|
||||
assert len(result["files"]) == 2
|
||||
html = root / "sub" / "report.html"
|
||||
md = root / "sub" / "report.md"
|
||||
assert html.is_file()
|
||||
assert md.is_file()
|
||||
html_text = html.read_text(encoding="utf-8")
|
||||
md_text = md.read_text(encoding="utf-8")
|
||||
assert "{{REPORT_DATA}}" not in html_text
|
||||
assert "代码审查报告" in html_text
|
||||
assert "\u2705 PASS" in html_text
|
||||
assert "N+1" in html_text
|
||||
assert "# 代码审查报告" in md_text
|
||||
|
||||
def test_markdown_only(self, tmp_path):
|
||||
root = _mkroot(tmp_path)
|
||||
result = generate_report_func(
|
||||
_review_data(),
|
||||
output_path="md-only",
|
||||
repo_root=str(root),
|
||||
format="markdown",
|
||||
)
|
||||
assert result["status"] == "ok"
|
||||
assert len(result["files"]) == 1
|
||||
assert str(result["output_path"]).endswith("md-only.md")
|
||||
assert (root / "md-only.md").is_file()
|
||||
assert not (root / "md-only.html").exists()
|
||||
|
||||
def test_html_only(self, tmp_path):
|
||||
root = _mkroot(tmp_path)
|
||||
result = generate_report_func(
|
||||
_review_data(),
|
||||
output_path="html-only",
|
||||
repo_root=str(root),
|
||||
format="html",
|
||||
)
|
||||
assert result["status"] == "ok"
|
||||
assert len(result["files"]) == 1
|
||||
assert str(result["output_path"]).endswith("html-only.html")
|
||||
assert (root / "html-only.html").is_file()
|
||||
assert not (root / "html-only.md").exists()
|
||||
|
||||
def test_default_output_path_is_repo_root(self, tmp_path):
|
||||
# repo_root must look like a project root: create .code-review-graph.
|
||||
Path(tmp_path, ".code-review-graph").mkdir(exist_ok=True)
|
||||
result = generate_report_func(_review_data(), repo_root=str(tmp_path))
|
||||
root = _mkroot(tmp_path)
|
||||
result = generate_report_func(_review_data(), repo_root=str(root))
|
||||
assert result["status"] == "ok"
|
||||
assert result["output_path"].endswith("code-review-report.html")
|
||||
assert Path(result["output_path"]).is_file()
|
||||
assert (root / "code-review-report.html").is_file()
|
||||
assert (root / "code-review-report.md").is_file()
|
||||
|
||||
def test_invalid_format_errors(self, tmp_path):
|
||||
root = _mkroot(tmp_path)
|
||||
result = generate_report_func(
|
||||
_review_data(),
|
||||
repo_root=str(root),
|
||||
format="xml",
|
||||
)
|
||||
assert result["status"] == "error"
|
||||
assert "xml" in result["error"]
|
||||
|
||||
def test_handles_missing_repo_root(self, tmp_path):
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@@ -118,12 +118,13 @@ class TestGenerateSkills:
|
||||
assert result.is_dir()
|
||||
assert result == tmp_path / ".claude" / "skills"
|
||||
|
||||
def test_creates_five_skill_subdirs(self, tmp_path):
|
||||
def test_creates_six_skill_subdirs(self, tmp_path):
|
||||
skills_dir = generate_skills(tmp_path)
|
||||
subdirs = sorted(f.name for f in skills_dir.iterdir() if f.is_dir())
|
||||
assert subdirs == [
|
||||
"debug-issue",
|
||||
"explore-codebase",
|
||||
"project-review",
|
||||
"refactor-safely",
|
||||
"review-changes",
|
||||
"unified-review",
|
||||
@@ -153,6 +154,7 @@ class TestGenerateSkills:
|
||||
for skill_name in (
|
||||
"debug-issue",
|
||||
"explore-codebase",
|
||||
"project-review",
|
||||
"refactor-safely",
|
||||
"review-changes",
|
||||
"unified-review",
|
||||
@@ -169,7 +171,7 @@ class TestGenerateSkills:
|
||||
result = generate_skills(tmp_path, skills_dir=custom)
|
||||
assert result == custom
|
||||
assert result.is_dir()
|
||||
assert len(list(result.iterdir())) == 5
|
||||
assert len(list(result.iterdir())) == 6
|
||||
|
||||
def test_skill_content_includes_get_minimal_context(self, tmp_path):
|
||||
"""Every skill template must reference get_minimal_context."""
|
||||
@@ -194,7 +196,7 @@ class TestGenerateSkills:
|
||||
generate_skills(tmp_path)
|
||||
generate_skills(tmp_path)
|
||||
skills_dir = tmp_path / ".claude" / "skills"
|
||||
assert len(list(skills_dir.iterdir())) == 5
|
||||
assert len(list(skills_dir.iterdir())) == 6
|
||||
|
||||
|
||||
class TestGenerateHooksConfig:
|
||||
@@ -859,8 +861,10 @@ class TestCodeBuddyPlatform:
|
||||
assert {path.name for path in skills_root.iterdir()} == {
|
||||
"debug-issue",
|
||||
"explore-codebase",
|
||||
"project-review",
|
||||
"refactor-safely",
|
||||
"review-changes",
|
||||
"unified-review",
|
||||
}
|
||||
for skill_dir in skills_root.iterdir():
|
||||
content = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
|
||||
|
||||
@@ -31,6 +31,19 @@ class TestToolRegistration:
|
||||
):
|
||||
assert hasattr(m, name), f"{name} not exposed by main module"
|
||||
|
||||
def test_project_review_prompt_exposed(self):
|
||||
import code_review_graph.main as m
|
||||
|
||||
assert hasattr(m, "project_review"), "project_review prompt not exposed"
|
||||
|
||||
def test_score_review_has_all_files_param(self):
|
||||
import inspect
|
||||
|
||||
import code_review_graph.main as m
|
||||
|
||||
sig = inspect.signature(m.score_review_tool)
|
||||
assert "all_files" in sig.parameters
|
||||
|
||||
|
||||
class TestDocsSections:
|
||||
def test_unified_review_section(self):
|
||||
@@ -61,7 +74,23 @@ class TestGenerateSkills:
|
||||
assert "get_minimal_context" in content
|
||||
assert "detail_level" in content
|
||||
|
||||
def test_project_review_generated(self, tmp_path):
|
||||
from code_review_graph.skills import generate_skills
|
||||
|
||||
skills_dir = generate_skills(tmp_path)
|
||||
skill_file = skills_dir / "project-review" / "SKILL.md"
|
||||
assert skill_file.is_file()
|
||||
content = skill_file.read_text(encoding="utf-8")
|
||||
assert "all_files=True" in content
|
||||
assert "get_minimal_context" in content
|
||||
assert "detail_level" in content
|
||||
|
||||
def test_uninstall_knows_unified_review(self):
|
||||
from code_review_graph.uninstall import _generated_skill_slugs
|
||||
|
||||
assert "unified-review" in _generated_skill_slugs()
|
||||
|
||||
def test_uninstall_knows_project_review(self):
|
||||
from code_review_graph.uninstall import _generated_skill_slugs
|
||||
|
||||
assert "project-review" in _generated_skill_slugs()
|
||||
|
||||
Reference in New Issue
Block a user