chore: sync local changes, add Chinese docs and opencode config
This commit is contained in:
@@ -49,9 +49,39 @@ Present every finding with severity (🔴 blocker / 🟡 major / 🔵 minor), co
|
||||
|
||||
Any 🔴 blocker → verdict `❌ FAIL`. Classify each finding as Ready / Needs Fix / Unusable.
|
||||
|
||||
## Step 7.5 - 覆盖度自检(必须执行,防"审完了"由感觉决定)
|
||||
|
||||
报告前完成三件事(机制未生效时禁止用 CLI 兜底继续审查):
|
||||
|
||||
1. **G1 深读名单完整性**:确认名单外文件是"被评估过"而非"被忽略";未被任何信号点名的文件记入 **"未深读文件清单"**,在报告中显式列出。
|
||||
2. **G2 静默抽检**:调用 `coverage_tool` 取返回的 `silent_files`(未被任何信号点名的文件),随机抽 **15%** 深读;发现 ≥1 major → 该文件升级全量深读,并同社区/同类追加抽检一轮。抽检记录附入报告(抽了几份 / 几个 major / 有无升级)。
|
||||
3. **G3 覆盖度计算**:调用 `coverage_tool(deep_read_files=<本轮实际深读文件>, gate="both+line")`,引擎自动计算**三重口径**(文件数口径 + 三件套质量口径):
|
||||
- **全库覆盖** = `coverage_pct`(已深读文件数 / 全部源文件数)
|
||||
- **高风险覆盖** = `high_risk_coverage_pct`(已深读 / 信号点名文件,文件数口径)
|
||||
- **行/单元覆盖** = `line_coverage_pct` / `unit_coverage_pct`(gate="both+line" 时的三件套质量口径)
|
||||
- **feature(单功能)审查**:改用 `gate="line+unit"`——**只做行覆盖 ≥95% + 单元完整性无缺口**,不做全库/高风险文件数覆盖检查(`coverage_pct`/`high_risk_coverage_pct` 为 `null`,报告只渲染行/单元覆盖,不渲染全库/高风险行)。`target_reached=false` → 报告顶部标 🔴 覆盖不足。
|
||||
|
||||
将覆盖度结果**完整透传**到 `review_data.coverage`(直接把 `coverage_tool` 返回值全部字段传入:coverage_pct/high_risk_coverage_pct/grade/deep_read_count/total_files/high_risk_total_files/high_risk_deep_count/deep_read_weight/total_weight/target_reached/target/uncovered_files/silent_files/note),不要手挑子集,否则计数字段渲染为 0/0 或 N/A。报告会自动渲染 `## 覆盖度` 区块。
|
||||
|
||||
**前置健康检查**:调用 `community_health_tool`,若 `needs_postprocess=true`(nodes.community_id 归属率 <90%),先 `code-review-graph postprocess` 重建社区归属再计算,否则覆盖度失真。
|
||||
|
||||
## 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"`).
|
||||
Call `generate_report_tool(review_data=<verdict, scope, metrics, findings>)` to write `code-review-report.html` and `code-review-report.md` (default `format="both"`). Pass `reviewed_files: [path, ...]` (the deep-read file array) — the report header renders it as a collapsible `<details>` list (falls back to the flat `files` string when absent).
|
||||
|
||||
**Archive naming (REQUIRED):** always pass `output_path="docs/reviews/{name}-review-{YYYY-MM-DD-HHMMSS}"` (e.g. `docs/reviews/evm-feature-review-2026-08-06-151522`). Omitting `output_path` writes to `<repo_root>/code-review-report.*` which is a naming violation.
|
||||
|
||||
## Step 8.6 - Report naming self-check (REQUIRED)
|
||||
|
||||
After generating, run the naming verifier (standalone script, independent of the CRG CLI):
|
||||
|
||||
```powershell
|
||||
powershell -File "C:\Users\Administrator\.config\opencode\skills\project-review\verify-report.ps1" -Repo <repo_root>
|
||||
```
|
||||
|
||||
- Exit 0 → pass: all reports under `docs/reviews/` carry a `-YYYY-MM-DD-HHMMSS` suffix.
|
||||
- Exit 1 → stray root `code-review-report.*` detected. Fix by re-calling `generate_report_tool` with the correct `output_path`, or run with `-Fix` to auto-archive, then re-verify.
|
||||
- Historic non-conforming names in `docs/reviews/` are warnings only — do not rename them.
|
||||
|
||||
## Output Format
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
.muted { color:var(--muted); font-size:.85rem; }
|
||||
code { background:var(--bg); padding:.1rem .3rem; border-radius:4px;
|
||||
font-size:.88em; }
|
||||
details.reviewed { margin:.5rem 0; border:1px solid var(--border);
|
||||
border-radius:6px; padding:.4rem .8rem; }
|
||||
details.reviewed summary { cursor:pointer; font-weight:600; }
|
||||
details.reviewed ul { margin:.4rem 0 0; padding-left:1.2rem; }
|
||||
details.reviewed li { margin:.15rem 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -61,6 +66,23 @@ function verdictClass(v) {
|
||||
return v.includes("PASS") ? "pass" : "fail";
|
||||
}
|
||||
|
||||
// Collapsible list of the reviewed files (native <details>/<summary>, no JS).
|
||||
// Accepts an array OR a comma-separated string (agents pass both). Falls
|
||||
// back to the flat ``files`` string when nothing structured is given.
|
||||
function renderReviewedFiles(data) {
|
||||
let arr = data.reviewed_files;
|
||||
if (typeof arr === "string") {
|
||||
arr = arr.split(",").map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (!Array.isArray(arr) || !arr.length) {
|
||||
return data.files ? `<p><b>文件:</b> ${esc(data.files)}</p>` : "";
|
||||
}
|
||||
return `<details class="reviewed">
|
||||
<summary>审查文件 (${arr.length}) <span class="muted">点击展开/收起</span></summary>
|
||||
<ul>${arr.map(f => `<li><code>${esc(f)}</code></li>`).join("")}</ul>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
const metricLabels = {
|
||||
sql_risk: "SQL 注入风险",
|
||||
exception_coverage: "异常分支覆盖",
|
||||
@@ -84,9 +106,83 @@ let html = `<h1>代码审查报告</h1>
|
||||
</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>`;
|
||||
html += renderReviewedFiles(data);
|
||||
if (data.summary) html += `<p>${esc(data.summary)}</p>`;
|
||||
|
||||
const cov = data.coverage || {};
|
||||
// The coverage section renders whenever file-count coverage OR line/unit
|
||||
// coverage was computed. gate="line+unit" (feature reviews) returns
|
||||
// coverage_pct=null, so the 全库/高风险 rows are skipped and only the
|
||||
// line/unit rows + spot-check render.
|
||||
if (cov && (cov.coverage_pct != null || cov.line_coverage_pct != null)) {
|
||||
const isLineOnly = cov.coverage_pct == null;
|
||||
const covOk = cov.target_reached;
|
||||
const covCls = covOk ? "good" : "fail";
|
||||
const covStatus = covOk ? "✅ 达标" : "🔴 覆盖不足";
|
||||
const oTarget = cov.overall_target ?? cov.target ?? "N/A";
|
||||
const hTarget = cov.high_risk_target ?? cov.target ?? "N/A";
|
||||
html += `<h2>覆盖度</h2>
|
||||
<p><span class="verdict ${covCls}">${covStatus}</span></p>`;
|
||||
if (!isLineOnly) {
|
||||
html += `<p><b>全库覆盖:</b> ${esc(cov.coverage_pct ?? "N/A")}% — 已深读 ${esc(cov.deep_read_count ?? "N/A")}/${esc(cov.total_files ?? "N/A")}(目标 ${esc(oTarget)}%)</p>
|
||||
<p><b>高风险覆盖:</b> ${esc(cov.high_risk_coverage_pct ?? "N/A")}% — 已深读 ${esc(cov.high_risk_deep_count ?? "N/A")}/${esc(cov.high_risk_total_files ?? "N/A")}(目标 ${esc(hTarget)}%)</p>`;
|
||||
}
|
||||
html += renderLineUnit(cov);
|
||||
html += renderSpotCheck(data.spot_check);
|
||||
if (!isLineOnly && (cov.uncovered_files || []).length) {
|
||||
html += `<p class="muted"><b>未深读文件:</b> ${esc(cov.uncovered_files.length)} 个(静默文件 ${esc((cov.silent_files || []).length)} 个)</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Line / unit coverage (three-piece suite items 1-2). Fail-closed: a
|
||||
// missing line/unit coverage renders "未执行 🔴" so reviews that skipped
|
||||
// gate="both+line" or the three-piece data are visible, never silent green.
|
||||
function renderLineUnit(cov) {
|
||||
const linePct = cov.line_coverage_pct;
|
||||
const unitPct = cov.unit_coverage_pct;
|
||||
const lineTarget = cov.line_target ?? 95.0;
|
||||
const unitTarget = cov.unit_target ?? 100.0;
|
||||
const lineGap = (cov.line_gap_files || []).length;
|
||||
const unitGap = (cov.unit_gap_files || []).length;
|
||||
const missing = (cov.missing_data_files || []).length;
|
||||
let s = "";
|
||||
if (linePct == null || unitPct == null) {
|
||||
s += `<p><b>行覆盖:</b> 未执行 🔴 <span class="muted">(coverage_tool 未用 gate="both+line" 或未传三件套数据)</span></p>`;
|
||||
} else {
|
||||
const lineOk = linePct >= lineTarget && lineGap === 0;
|
||||
const unitOk = unitPct >= unitTarget && unitGap === 0;
|
||||
s += `<p><b>行覆盖:</b> ${esc(linePct)}% — 目标 ${esc(lineTarget)}%(缺口 ${esc(lineGap)} 文件)${lineOk ? "✅" : "🔴"}</p>`;
|
||||
s += `<p><b>单元覆盖:</b> ${esc(unitPct)}% — 目标 ${esc(unitTarget)}%(缺口 ${esc(unitGap)} 文件)${unitOk ? "✅" : "🔴"}</p>`;
|
||||
}
|
||||
if (missing) {
|
||||
s += `<p class="muted"><b>三件套数据缺失:</b> ${esc(missing)} 个文件(缺 read_ranges/语义单元,已按 fail-closed 计为缺口)</p>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Anti-fake spot check (three-piece suite item 3). Fail-closed: a
|
||||
// missing/incomplete spot_check renders "未执行 🔴" so reviews that
|
||||
// skipped the sampled re-read are visible instead of silently green.
|
||||
function renderSpotCheck(spot) {
|
||||
if (!spot) {
|
||||
return `<p><b>防伪抽验:</b> 未执行 🔴 <span class="muted">(主代理未回读任何语义单元;Step 5.5 应执行每组抽 2 文件 × 2-3 单元并落盘 spot_check)</span></p>`;
|
||||
}
|
||||
const groups = spot.groups_sampled;
|
||||
const files = spot.files_sampled;
|
||||
const units = spot.units_sampled;
|
||||
const fake = spot.fake_read_found || 0;
|
||||
const rereread = spot.groups_rereread || [];
|
||||
if (!units) {
|
||||
return `<p><b>防伪抽验:</b> 未执行 🔴 <span class="muted">(spot_check 已上报但单元数为 0)</span></p>`;
|
||||
}
|
||||
const mark = (fake || rereread.length) ? "🔴 发现假读" : "✅";
|
||||
let s = `<p><b>防伪抽验:</b> 抽样 ${esc(files)} 文件 / ${esc(units)} 单元 / ${esc(groups)} 组,假读 ${esc(fake)} ${mark}</p>`;
|
||||
if (rereread.length) {
|
||||
s += `<p class="muted">因假读重读组:${esc(rereread.join(", "))}</p>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const metrics = data.metrics || {};
|
||||
const mkeys = Object.keys(metrics);
|
||||
if (mkeys.length) {
|
||||
|
||||
@@ -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` and `code-review-report.md` (default `format="both"`). 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"`). Pass `reviewed_files: [path, ...]` (changed/reviewed files) so the report header lists them in a collapsible `<details>` list. Also present the text report inline.
|
||||
|
||||
## Step 9 - Persistence (optional)
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
.muted { color:var(--muted); font-size:.85rem; }
|
||||
code { background:var(--bg); padding:.1rem .3rem; border-radius:4px;
|
||||
font-size:.88em; }
|
||||
details.reviewed { margin:.5rem 0; border:1px solid var(--border);
|
||||
border-radius:6px; padding:.4rem .8rem; }
|
||||
details.reviewed summary { cursor:pointer; font-weight:600; }
|
||||
details.reviewed ul { margin:.4rem 0 0; padding-left:1.2rem; }
|
||||
details.reviewed li { margin:.15rem 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -61,6 +66,23 @@ function verdictClass(v) {
|
||||
return v.includes("PASS") ? "pass" : "fail";
|
||||
}
|
||||
|
||||
// Collapsible list of the reviewed files (native <details>/<summary>, no JS).
|
||||
// Accepts an array OR a comma-separated string (agents pass both). Falls
|
||||
// back to the flat ``files`` string when nothing structured is given.
|
||||
function renderReviewedFiles(data) {
|
||||
let arr = data.reviewed_files;
|
||||
if (typeof arr === "string") {
|
||||
arr = arr.split(",").map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (!Array.isArray(arr) || !arr.length) {
|
||||
return data.files ? `<p><b>文件:</b> ${esc(data.files)}</p>` : "";
|
||||
}
|
||||
return `<details class="reviewed">
|
||||
<summary>审查文件 (${arr.length}) <span class="muted">点击展开/收起</span></summary>
|
||||
<ul>${arr.map(f => `<li><code>${esc(f)}</code></li>`).join("")}</ul>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
const metricLabels = {
|
||||
sql_risk: "SQL 注入风险",
|
||||
exception_coverage: "异常分支覆盖",
|
||||
@@ -84,9 +106,83 @@ let html = `<h1>代码审查报告</h1>
|
||||
</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>`;
|
||||
html += renderReviewedFiles(data);
|
||||
if (data.summary) html += `<p>${esc(data.summary)}</p>`;
|
||||
|
||||
const cov = data.coverage || {};
|
||||
// The coverage section renders whenever file-count coverage OR line/unit
|
||||
// coverage was computed. gate="line+unit" (feature reviews) returns
|
||||
// coverage_pct=null, so the 全库/高风险 rows are skipped and only the
|
||||
// line/unit rows + spot-check render.
|
||||
if (cov && (cov.coverage_pct != null || cov.line_coverage_pct != null)) {
|
||||
const isLineOnly = cov.coverage_pct == null;
|
||||
const covOk = cov.target_reached;
|
||||
const covCls = covOk ? "good" : "fail";
|
||||
const covStatus = covOk ? "✅ 达标" : "🔴 覆盖不足";
|
||||
const oTarget = cov.overall_target ?? cov.target ?? "N/A";
|
||||
const hTarget = cov.high_risk_target ?? cov.target ?? "N/A";
|
||||
html += `<h2>覆盖度</h2>
|
||||
<p><span class="verdict ${covCls}">${covStatus}</span></p>`;
|
||||
if (!isLineOnly) {
|
||||
html += `<p><b>全库覆盖:</b> ${esc(cov.coverage_pct ?? "N/A")}% — 已深读 ${esc(cov.deep_read_count ?? "N/A")}/${esc(cov.total_files ?? "N/A")}(目标 ${esc(oTarget)}%)</p>
|
||||
<p><b>高风险覆盖:</b> ${esc(cov.high_risk_coverage_pct ?? "N/A")}% — 已深读 ${esc(cov.high_risk_deep_count ?? "N/A")}/${esc(cov.high_risk_total_files ?? "N/A")}(目标 ${esc(hTarget)}%)</p>`;
|
||||
}
|
||||
html += renderLineUnit(cov);
|
||||
html += renderSpotCheck(data.spot_check);
|
||||
if (!isLineOnly && (cov.uncovered_files || []).length) {
|
||||
html += `<p class="muted"><b>未深读文件:</b> ${esc(cov.uncovered_files.length)} 个(静默文件 ${esc((cov.silent_files || []).length)} 个)</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Line / unit coverage (three-piece suite items 1-2). Fail-closed: a
|
||||
// missing line/unit coverage renders "未执行 🔴" so reviews that skipped
|
||||
// gate="both+line" or the three-piece data are visible, never silent green.
|
||||
function renderLineUnit(cov) {
|
||||
const linePct = cov.line_coverage_pct;
|
||||
const unitPct = cov.unit_coverage_pct;
|
||||
const lineTarget = cov.line_target ?? 95.0;
|
||||
const unitTarget = cov.unit_target ?? 100.0;
|
||||
const lineGap = (cov.line_gap_files || []).length;
|
||||
const unitGap = (cov.unit_gap_files || []).length;
|
||||
const missing = (cov.missing_data_files || []).length;
|
||||
let s = "";
|
||||
if (linePct == null || unitPct == null) {
|
||||
s += `<p><b>行覆盖:</b> 未执行 🔴 <span class="muted">(coverage_tool 未用 gate="both+line" 或未传三件套数据)</span></p>`;
|
||||
} else {
|
||||
const lineOk = linePct >= lineTarget && lineGap === 0;
|
||||
const unitOk = unitPct >= unitTarget && unitGap === 0;
|
||||
s += `<p><b>行覆盖:</b> ${esc(linePct)}% — 目标 ${esc(lineTarget)}%(缺口 ${esc(lineGap)} 文件)${lineOk ? "✅" : "🔴"}</p>`;
|
||||
s += `<p><b>单元覆盖:</b> ${esc(unitPct)}% — 目标 ${esc(unitTarget)}%(缺口 ${esc(unitGap)} 文件)${unitOk ? "✅" : "🔴"}</p>`;
|
||||
}
|
||||
if (missing) {
|
||||
s += `<p class="muted"><b>三件套数据缺失:</b> ${esc(missing)} 个文件(缺 read_ranges/语义单元,已按 fail-closed 计为缺口)</p>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Anti-fake spot check (three-piece suite item 3). Fail-closed: a
|
||||
// missing/incomplete spot_check renders "未执行 🔴" so reviews that
|
||||
// skipped the sampled re-read are visible instead of silently green.
|
||||
function renderSpotCheck(spot) {
|
||||
if (!spot) {
|
||||
return `<p><b>防伪抽验:</b> 未执行 🔴 <span class="muted">(主代理未回读任何语义单元;Step 5.5 应执行每组抽 2 文件 × 2-3 单元并落盘 spot_check)</span></p>`;
|
||||
}
|
||||
const groups = spot.groups_sampled;
|
||||
const files = spot.files_sampled;
|
||||
const units = spot.units_sampled;
|
||||
const fake = spot.fake_read_found || 0;
|
||||
const rereread = spot.groups_rereread || [];
|
||||
if (!units) {
|
||||
return `<p><b>防伪抽验:</b> 未执行 🔴 <span class="muted">(spot_check 已上报但单元数为 0)</span></p>`;
|
||||
}
|
||||
const mark = (fake || rereread.length) ? "🔴 发现假读" : "✅";
|
||||
let s = `<p><b>防伪抽验:</b> 抽样 ${esc(files)} 文件 / ${esc(units)} 单元 / ${esc(groups)} 组,假读 ${esc(fake)} ${mark}</p>`;
|
||||
if (rereread.length) {
|
||||
s += `<p class="muted">因假读重读组:${esc(rereread.join(", "))}</p>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const metrics = data.metrics || {};
|
||||
const mkeys = Object.keys(metrics);
|
||||
if (mkeys.length) {
|
||||
|
||||
Reference in New Issue
Block a user