chore: sync local changes, add Chinese docs and opencode config

This commit is contained in:
AuraK Developer
2026-08-31 11:37:03 +08:00
parent 307d2fd471
commit ecc55158c1
81 changed files with 7645 additions and 144 deletions
+11 -1
View File
@@ -1,6 +1,6 @@
"""MCP tool definitions for the Code Review Graph server.
Exposes 31 tools:
Exposes 33 tools:
1. build_or_update_graph - full or incremental build
2. get_impact_radius - blast radius from changed files
3. query_graph - predefined graph queries
@@ -32,6 +32,8 @@ Exposes 31 tools:
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 HTML and/or Markdown review report
32. coverage - risk-weighted deep-read coverage (project-review G3)
33. community_health - nodes.community_id attribution health check
"""
from __future__ import annotations
@@ -108,8 +110,12 @@ from .review import (
# -- scoring (unified-review) ------------------------------------------------
from .scoring_tools import (
community_health_func,
coverage_func,
dedupe_findings_func,
deep_read_plan_func,
generate_report_func,
save_coverage_index_func,
score_review_func,
)
@@ -157,6 +163,10 @@ __all__ = [
"score_review_func",
"dedupe_findings_func",
"generate_report_func",
"coverage_func",
"deep_read_plan_func",
"save_coverage_index_func",
"community_health_func",
# analysis_tools
"get_bridge_nodes_func",
"get_hub_nodes_func",
+415 -7
View File
@@ -12,18 +12,165 @@ build_report_data) into the three MCP tools consumed by the
from __future__ import annotations
import json
import os
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any
from typing import Any, Callable, Optional
from ..incremental import get_changed_files, get_staged_and_unstaged
from ..scoring import (
build_report_data,
check_community_health,
compute_coverage,
dedupe_findings,
deep_read_plan,
render_markdown_report,
score_review,
)
from ._common import _get_store, _error_response
#: Persistent cross-round deep-read index (relative path -> per-file SHA).
COVERAGE_INDEX_REL = ".code-review-graph/coverage-index.json"
def _git_head_sha(repo_root: Path) -> str | None:
"""Current git HEAD SHA (None when not a repo / git unavailable)."""
try:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
text=True,
cwd=str(repo_root),
timeout=10,
)
if result.returncode == 0:
return result.stdout.strip()
except (OSError, subprocess.SubprocessError):
pass
return None
def _git_file_sha(repo_root: Path, rel_path: str) -> str | None:
"""Blob SHA of the *current working-tree* content of ``rel_path``.
Uses ``git hash-object`` (content hash, does not touch the index) so an
uncommitted edit still counts as stale. Returns None when the file does
not exist in the working tree or git is unavailable.
"""
try:
result = subprocess.run(
["git", "hash-object", rel_path],
capture_output=True,
text=True,
cwd=str(repo_root),
timeout=10,
)
if result.returncode == 0:
return result.stdout.strip()
except (OSError, subprocess.SubprocessError):
pass
return None
def _coverage_index_path(repo_root: Path) -> Path:
return repo_root / COVERAGE_INDEX_REL
def _load_coverage_index(repo_root: Path) -> list[str]:
"""Return the relative paths whose per-file SHA still matches HEAD.
Files that changed since the last review are automatically excluded
(stale), so an incremental round never masks new code with old findings.
SHA check uses the graph's ``nodes.file_hash`` when the graph is
current, else falls back to a single batched ``git hash-object``
invocation (never one subprocess per file, see #46/#136).
"""
index_path = _coverage_index_path(repo_root)
if not index_path.is_file():
return []
try:
payload = json.loads(index_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return []
entries = payload.get("entries") or {}
if not entries:
return []
return [
rel
for rel, info in entries.items()
if _index_entry_current(repo_root, rel, info)
]
def _load_coverage_index_full(
repo_root: Path,
) -> tuple[list[str], dict[str, list[list[int]]]]:
"""Load (current rel paths, their persisted read ranges)."""
index_path = _coverage_index_path(repo_root)
if not index_path.is_file():
return [], {}
try:
payload = json.loads(index_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return [], {}
entries = payload.get("entries") or {}
if not entries:
return [], {}
current, ranges = [], {}
for rel, info in entries.items():
if _index_entry_current(repo_root, rel, info):
current.append(rel)
if info.get("ranges"):
ranges[rel] = info["ranges"]
return current, ranges
def _index_entry_current(repo_root: Path, rel: str, info: dict) -> bool:
"""True when a persisted entry's SHA matches the current file content."""
if not (repo_root / rel).is_file():
return False
expected = info.get("sha")
if not expected:
return False
# Prefer graph file_hash (single SQL fetch, no subprocess). Fall back to
# a batched git hash-object for files not in the graph.
graph_hash = _graph_file_hashes(repo_root).get(rel)
if graph_hash:
return graph_hash == expected
return _git_file_sha(repo_root, rel) == expected
_GRAPH_HASH_CACHE: dict[str, dict[str, str]] = {}
def _graph_file_hashes(repo_root: Path) -> dict[str, str]:
"""All File-node content hashes for a repo, cached (one SQL fetch)."""
key = str(repo_root)
if key in _GRAPH_HASH_CACHE:
return _GRAPH_HASH_CACHE[key]
result: dict[str, str] = {}
try:
store, root = _get_store(str(repo_root))
try:
rows = store._conn.execute(
"SELECT file_path, file_hash FROM nodes WHERE kind='File'"
).fetchall()
for r in rows:
fp = str(r["file_path"])
rel = fp.replace("\\", "/")
if rel.startswith(str(root).replace("\\", "/") + "/"):
rel = rel[len(str(root).replace("\\", "/")) + 1:].replace("/", "\\")
rel = rel.replace("\\", "/")
result[rel] = r["file_hash"] or ""
finally:
store.close()
except Exception:
result = {}
_GRAPH_HASH_CACHE[key] = result
return result
try:
from importlib.resources import files as _pkg_files # Python 3.9+
@@ -44,13 +191,14 @@ def score_review_func(
repo_root: str | None = None,
detail_level: str = "standard",
all_files: bool = False,
progress_cb: Callable[[float, Optional[str]], None] | None = None,
) -> dict[str, Any]:
"""Compute objective Layer-2 review metrics for changed files.
Runs the git-history / graph risk factors plus the heuristic metrics
(SQL risk, exception coverage, redundancy, high-risk density,
vulnerability). LLM-judged metrics are listed in ``llm_judged`` so the
calling agent knows what still needs judgement.
vulnerability). These five objective metrics form the full report
metric set; ``llm_judged`` is returned empty for compatibility.
Args:
changed_files: Files to score (auto-detected from git diff if
@@ -63,6 +211,8 @@ def score_review_func(
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).
progress_cb: Optional ``(fraction, message)`` progress callback
forwarded to the scoring engine.
"""
store, root = _get_store(repo_root)
try:
@@ -85,6 +235,7 @@ def score_review_func(
root,
changed_files,
include_churn=include_churn,
progress_cb=progress_cb,
)
if detail_level == "minimal":
@@ -154,6 +305,238 @@ def dedupe_findings_func(
return _error_response(str(exc))
# ---------------------------------------------------------------------------
# Tool: coverage
# ---------------------------------------------------------------------------
def coverage_func(
deep_read_files: list[str],
all_files: bool = True,
include_churn: bool = True,
gate: str = "high_risk",
include_prior: bool = False,
file_read_ranges: dict[str, list[list[int]]] | None = None,
file_semantic_units: dict[str, list[dict]] | None = None,
repo_root: str | None = None,
progress_cb: Callable[[float, Optional[str]], None] | None = None,
) -> dict[str, Any]:
"""Compute file-count review coverage for a set of deep-read files.
Coverage = number of deep-read files / number of all source files.
The high-risk coverage uses only the signal-flagged subset (also
file-count based). Per-file risk weights still rank the priority
deep-read list so the highest-risk files are read first. This backs
the project-review Step 7.5 coverage self-check (G3).
Args:
deep_read_files: Files the agent actually deep-read during the
review (relative or absolute paths). Required.
all_files: When True, the denominator is every source file in the
graph (default: True, whole-project semantics).
include_churn: Include git-churn as the w3 weight term.
gate: Gate mode: ``"high_risk"`` (default) / ``"overall"`` /
``"both"`` (both overall and high-risk must meet the target) /
``"both+line"`` (both + per-file line coverage >= line_target
and unit-completeness gap-free) / ``"line+unit"`` (line + unit
only, feature reviews; file-count / high-risk coverage are
skipped and returned as ``None``).
include_prior: When True, merge the cross-round coverage index
(files whose SHA is unchanged since the last review) into the
deep-read set.
file_read_ranges: Optional {rel_path: [[s,e],...]} of line ranges a
sub-agent actually read per deep-read file (for gate="both+line").
file_semantic_units: Optional {rel_path: [{range,kind,name}, ...]}
reported semantic units per deep-read file (gate="both+line").
progress_cb: Optional ``(fraction, message)`` progress callback
forwarded to the scoring engine.
Returns:
Dict with coverage_pct, grade, deep_read_count, total_files,
target_reached, target, remaining_files_to_target,
priority_deep_read_files, uncovered_files, silent_files and note.
``silent_files`` feeds the G2 spot-check sampling. For
``gate="both+line"`` / ``"line+unit"`` also returns
line_coverage_pct, unit_coverage_pct, line_gap_files,
unit_gap_files, unit_exempt_files.
"""
store, root = _get_store(repo_root)
try:
deep_read = list(deep_read_files or [])
ranges = dict(file_read_ranges or {})
units = dict(file_semantic_units or {})
if include_prior:
prior, prior_ranges = _load_coverage_index_full(root)
deep_read += prior
for rel, rr in prior_ranges.items():
ranges.setdefault(rel, rr)
return compute_coverage(
store,
root,
deep_read_files=deep_read,
include_churn=include_churn,
gate=gate,
file_read_ranges=ranges,
file_semantic_units=units,
progress_cb=progress_cb,
)
except Exception as exc:
return _error_response(str(exc))
finally:
store.close()
def deep_read_plan_func(
repo_root: str | None = None,
target_coverage: float = 85.0,
batch_size: int = 40,
include_churn: bool = True,
include_prior: bool = False,
deep_read_files: list[str] | None = None,
progress_cb: Callable[[float, Optional[str]], None] | None = None,
) -> dict[str, Any]:
"""Generate a grouped deep-read plan that closes the coverage gap.
Greedily selects the highest-risk files not yet deep-read until the
file-count overall coverage target is reached, then groups them by
parent directory (≤ ``batch_size`` per group) so each group can be
dispatched to one parallel sub-agent.
Args:
repo_root: Repository root. Auto-detected if omitted.
target_coverage: Overall coverage target percentage (default 85).
batch_size: Max files per group (default 40).
include_churn: Include git-churn as the w3 weight term.
include_prior: Exclude files already covered by the cross-round
coverage index (SHA unchanged) from the plan.
deep_read_files: Files already deep-read this round.
progress_cb: Optional ``(fraction, message)`` progress callback
forwarded to the scoring engine.
Returns:
Dict with current_coverage_pct, current/target/remaining file
counts (plus compatible weight counts), planned_files
(priority-ordered), directory groups and estimated_batches.
"""
store, root = _get_store(repo_root)
try:
prior = set(_load_coverage_index(root)) if include_prior else None
return deep_read_plan(
store,
root,
deep_read_files=deep_read_files,
target_coverage=target_coverage,
batch_size=batch_size,
include_churn=include_churn,
prior_covered=prior,
progress_cb=progress_cb,
)
except Exception as exc:
return _error_response(str(exc))
finally:
store.close()
def save_coverage_index_func(
deep_read_files: list[str],
repo_root: str | None = None,
file_read_ranges: dict[str, list[list[int]]] | None = None,
file_semantic_units: dict[str, list[dict]] | None = None,
) -> dict[str, Any]:
"""Persist the deep-read file list to the cross-round coverage index.
Records each file's per-file SHA at HEAD so a later review can tell
which previously-deep-read files are still current (SHA unchanged)
and which are stale and must be re-read.
Version 2: SHA comes from the graph's ``nodes.file_hash`` (one SQL
query, zero subprocesses) instead of one ``git hash-object`` per file.
Line ranges (``file_read_ranges``) are persisted so a later review can
reuse the line coverage of SHA-unchanged files.
Args:
deep_read_files: Files deep-read this round (relative or absolute).
repo_root: Repository root. Auto-detected if omitted.
file_read_ranges: Optional {rel_path: [[s,e],...]} line ranges to
persist per file (index v2, reused by include_prior).
file_semantic_units: Optional per-file semantic units; persisted
for informational purposes (unit completeness is recomputed
against the live graph each round, so this is advisory only).
Returns:
Dict with index_path, entry count and head_sha.
"""
store, root = _get_store(repo_root)
store.close()
file_hashes = _graph_file_hashes(root)
def _hash_for(rel: str) -> str | None:
return file_hashes.get(rel) or _git_file_sha(root, rel)
entries: dict[str, dict[str, Any]] = {}
for f in deep_read_files or []:
p = Path(f)
rel = str(p.relative_to(root)) if p.is_absolute() else str(p)
rel = rel.replace("\\", "/")
entry: dict[str, Any] = {"sha": _hash_for(rel) or _git_file_sha(root, rel)}
ranges = (file_read_ranges or {}).get(rel)
if ranges:
entry["ranges"] = ranges
units = (file_semantic_units or {}).get(rel)
if units:
entry["units"] = units
entries[rel] = entry
payload = {
"version": 2,
"last_updated": datetime.now().isoformat(timespec="seconds"),
"head_sha": _git_head_sha(root),
"entries": entries,
}
index_path = _coverage_index_path(root)
try:
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
except OSError as exc:
return _error_response(f"Failed to write coverage index: {exc}")
return {
"status": "ok",
"index_path": str(index_path),
"entries": len(entries),
"head_sha": payload["head_sha"],
}
def community_health_func(
repo_root: str | None = None,
) -> dict[str, Any]:
"""Check node->community attribution health (nodes.community_id).
Detects the desync where ``communities.size`` is correct but
``nodes.community_id`` is mostly NULL (e.g. after an incremental
rebuild). Returns ``needs_postprocess``; the review skill should run
``code-review-graph postprocess`` when True before computing coverage.
Args:
repo_root: Repository root. Auto-detected if omitted.
Returns:
Dict with total_nodes, attributed_nodes, non_file_nodes,
attribution_pct, needs_postprocess and note.
"""
store, root = _get_store(repo_root)
try:
return check_community_health(store)
except Exception as exc:
return _error_response(str(exc))
finally:
store.close()
# ---------------------------------------------------------------------------
# Tool: generate_report
# ---------------------------------------------------------------------------
@@ -206,8 +589,30 @@ const el = document.getElementById("report");
let html = `<h1>Code Review Report</h1>
<p><span class="verdict ${data.verdictClass || "fail"}">${data.verdict || "NO VERDICT"}</span>
&middot; Tier: ${data.tier || "standard"} &middot; Scope: ${data.scope || "change-level"}</p>`;
if (data.files) html += `<p><b>Files:</b> ${data.files}</p>`;
if (data.files || (data.reviewed_files && data.reviewed_files.length)) {
if (Array.isArray(data.reviewed_files) && data.reviewed_files.length) {
html += `<details><summary><b>Files (${data.reviewed_files.length})</b></summary><ul>`
+ data.reviewed_files.map(f => `<li><code>${f}</code></li>`).join("")
+ `</ul></details>`;
} else {
html += `<p><b>Files:</b> ${data.files}</p>`;
}
}
if (data.summary) html += `<p>${data.summary}</p>`;
const cov = data.coverage || {};
if (cov && (cov.coverage_pct != null || cov.line_coverage_pct != null)) {
const covOk = cov.target_reached;
let c = `<p><b>Coverage:</b> ${covOk ? "target reached" : "coverage insufficient"}`;
if (cov.coverage_pct != null) {
c += ` &middot; overall ${cov.coverage_pct}% (${cov.deep_read_count}/${cov.total_files})`
+ ` &middot; high-risk ${cov.high_risk_coverage_pct}% (${cov.high_risk_deep_count}/${cov.high_risk_total_files})`;
}
if (cov.line_coverage_pct != null) {
c += ` &middot; line ${cov.line_coverage_pct}% &middot; unit ${cov.unit_coverage_pct}%`;
}
c += `</p>`;
html += c;
}
html += `<h2>Objective Metrics</h2><table><tr><th>Metric</th><th>Value</th><th>Grade</th></tr>`;
for (const [k, m] of Object.entries(data.metrics || {})) {
html += `<tr><td>${k}</td><td>${m.value ?? "N/A"}</td>
@@ -275,9 +680,12 @@ def generate_report_func(
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)
)
# Escape "<" as "\u003c" so finding text can never close the
# surrounding <script> tag (e.g. a literal "</script>" in a
# message/fix). JSON keeps it a valid escape; the JS template
# string re-parses it as "<" before esc() HTML-escapes it.
payload = json.dumps(data, ensure_ascii=False).replace("<", "\\u003c")
rendered = template.replace("{{REPORT_DATA}}", payload)
html_path = base.with_suffix(".html")
html_path.parent.mkdir(parents=True, exist_ok=True)
html_path.write_text(rendered, encoding="utf-8")