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

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

- scoring_tools.py: score_review_func gains all_files=True to score every
  source file in the graph via store.get_all_files()
- main.py: score_review_tool gains all_files param; registers the
  project_review MCP prompt (prompts 6->7)
- prompts.py: project_review_prompt(scope, target) with whole-project and
  feature branches (fixed a precedence bug that truncated the feature text)
- skills.py + skills/project-review/: new read-only project-review skill
  with shared checklists
- .opencode/command/code-review-graph-project-review.md: slash command
- tests: test_project_review.py (prompt rendering), TestProjectReviewPrompt,
  skill count assertions 5->6, all_files wiring checks
- docs: prompts (6->7) + project-review entries across COMMANDS, CLAUDE,
  README (+localized), INDEX, architecture, LLM-OPTIMIZED-REFERENCE,
  CHANGELOG
This commit is contained in:
dev
2026-08-06 13:56:54 +08:00
parent 6f0e6f0775
commit 307d2fd471
45 changed files with 1339 additions and 131 deletions
+37 -19
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Code Review Report</title>
<title>代码审查报告</title>
<style>
:root { --border:#d0d7de; --bg:#f6f8fa; --fg:#1f2328; --muted:#57606a;
--good:#1a7f37; --warn:#9a6700; --fail:#cf222e; --na:#57606a;
@@ -61,28 +61,45 @@ function verdictClass(v) {
return v.includes("PASS") ? "pass" : "fail";
}
let html = `<h1>Code Review Report</h1>
const metricLabels = {
sql_risk: "SQL 注入风险",
exception_coverage: "异常分支覆盖",
redundancy_rate: "代码冗余率",
high_risk_density: "高风险场景密度",
vulnerability_risk: "漏洞风险",
};
const gradeLabels = { good: "良好", warn: "警告", fail: "不合格", na: "不适用" };
const sevLabels = {
blocker: "🔴 阻塞", critical: "🔴 严重",
major: "🟡 主要", warn: "🟡 主要",
minor: "🔵 次要", informational: "🔵 次要",
};
let html = `<h1>代码审查报告</h1>
<div class="meta">
<span class="verdict ${verdictClass(data.verdict)}">${esc(data.verdict || "NO VERDICT")}</span>
&nbsp; Tier: <code>${esc(data.tier || "standard")}</code>
&nbsp; Scope: <code>${esc(data.scope || "change-level")}</code>
${data.baseline ? `&nbsp; Baseline: <code>${esc(data.baseline)}</code>` : ""}
<span class="verdict ${verdictClass(data.verdict)}">${esc(data.verdict || "无结论")}</span>
&nbsp; 档位: <code>${esc(data.tier || "standard")}</code>
&nbsp; 范围: <code>${esc(data.scope || "change-level")}</code>
${data.baseline ? `&nbsp; 基线: <code>${esc(data.baseline)}</code>` : ""}
</div>`;
if (data.timestamp) html += `<p class="muted">Generated ${esc(data.timestamp)}</p>`;
if (data.files) html += `<p><b>Files:</b> ${esc(data.files)}</p>`;
if (data.quality_score != null) html += `<p><b>PR 质量分:</b> ${esc(data.quality_score)}/10</p>`;
if (data.timestamp) html += `<p class="muted">生成时间: ${esc(data.timestamp)}</p>`;
if (data.files) html += `<p><b>文件:</b> ${esc(data.files)}</p>`;
if (data.summary) html += `<p>${esc(data.summary)}</p>`;
const metrics = data.metrics || {};
const mkeys = Object.keys(metrics);
if (mkeys.length) {
html += `<h2>Objective Metrics</h2><table><tr><th>Metric</th><th>Value</th><th>Grade</th><th>Notes</th></tr>`;
html += `<h2>客观指标</h2><table><tr><th>指标</th><th>数值</th><th>评级</th><th>说明</th></tr>`;
for (const k of mkeys) {
const m = metrics[k] || {};
const g = m.grade || "na";
const label = metricLabels[k] || k;
const gradeText = gradeLabels[g] || g;
html += `<tr>
<td>${esc(k)}</td>
<td>${esc(label)}</td>
<td>${esc(m.value ?? "N/A")}</td>
<td class="grade ${esc(g)}">${esc(g)}</td>
<td class="grade ${esc(g)}">${esc(gradeText)}</td>
<td class="muted">${esc(m.note || "")}</td>
</tr>`;
}
@@ -90,32 +107,33 @@ if (mkeys.length) {
}
const issues = data.issues || [];
html += `<h2>Issues (${issues.length})</h2>`;
html += `<h2>问题清单 (${issues.length})</h2>`;
if (!issues.length) {
html += `<p class="muted">No issues found.</p>`;
html += `<p class="muted">未发现问题。</p>`;
}
for (const i of issues) {
const sev = (i.severity || "minor").toLowerCase();
const sevLabel = sevLabels[sev] || i.severity;
html += `<div class="issue">
<span class="tag ${esc(sev)}">${esc(i.severity)}</span>
<span class="tag ${esc(sev)}">${esc(sevLabel)}</span>
<span class="cat">${esc(i.category)}</span>
${esc(i.message || "")}
${i.confidence ? `<span class="muted">(confidence ${esc(i.confidence)})</span>` : ""}
${i.confidence != null ? `<span class="muted">(置信度 ${esc(i.confidence)}/10)</span>` : ""}
${i.location ? `<div class="loc">→ ${esc(i.location)}</div>` : ""}
${i.fix ? `<div class="fix"><b>Fix:</b> ${esc(i.fix)}</div>` : ""}
${i.fix ? `<div class="fix"><b>修复建议:</b> ${esc(i.fix)}</div>` : ""}
</div>`;
}
const manual = data.manual_review || [];
if (manual.length) {
html += `<h2>Manual Review Required</h2><ul>`;
html += `<h2>需要人工审查</h2><ul>`;
for (const m of manual) html += `<li>${esc(m)}</li>`;
html += `</ul>`;
}
const judged = data.llm_judged || [];
if (judged.length) {
html += `<p class="muted"><b>LLM-judged:</b> ${judged.map(esc).join(", ")}</p>`;
html += `<p class="muted"><b>LLM 判断的指标:</b> ${judged.map(esc).join(", ")}</p>`;
}
document.getElementById("app").innerHTML = html;