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:
@@ -6,7 +6,7 @@ build_report_data) into the three MCP tools consumed by the
|
||||
|
||||
* ``score_review_tool`` - objective Layer-2 metrics for changed files
|
||||
* ``dedupe_findings_tool`` - fingerprint dedup + confidence merge
|
||||
* ``generate_report_tool`` - render ``code-review-report.html``
|
||||
* ``generate_report_tool`` - render the HTML and/or Markdown review report
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,7 +16,12 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..incremental import get_changed_files, get_staged_and_unstaged
|
||||
from ..scoring import build_report_data, dedupe_findings, score_review
|
||||
from ..scoring import (
|
||||
build_report_data,
|
||||
dedupe_findings,
|
||||
render_markdown_report,
|
||||
score_review,
|
||||
)
|
||||
from ._common import _get_store, _error_response
|
||||
|
||||
try:
|
||||
@@ -38,6 +43,7 @@ def score_review_func(
|
||||
include_churn: bool = True,
|
||||
repo_root: str | None = None,
|
||||
detail_level: str = "standard",
|
||||
all_files: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Compute objective Layer-2 review metrics for changed files.
|
||||
|
||||
@@ -54,10 +60,15 @@ def score_review_func(
|
||||
repo_root: Repository root. Auto-detected if omitted.
|
||||
detail_level: Output detail level. ``minimal`` returns only grades
|
||||
and values; ``standard`` includes evidence.
|
||||
all_files: When True, score every source file in the graph,
|
||||
ignoring ``changed_files`` and the git diff. Used for
|
||||
whole-project reviews (default: False).
|
||||
"""
|
||||
store, root = _get_store(repo_root)
|
||||
try:
|
||||
if changed_files is None:
|
||||
if all_files:
|
||||
changed_files = store.get_all_files()
|
||||
elif changed_files is None:
|
||||
changed_files = get_changed_files(root, base)
|
||||
if not changed_files:
|
||||
changed_files = get_staged_and_unstaged(root)
|
||||
@@ -223,45 +234,77 @@ def generate_report_func(
|
||||
review_data: dict[str, Any],
|
||||
output_path: str | None = None,
|
||||
repo_root: str | None = None,
|
||||
format: str = "both",
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a self-contained HTML code review report.
|
||||
"""Generate code review reports (HTML and/or Markdown).
|
||||
|
||||
Injects ``review_data`` (the output of ``score_review_tool`` plus
|
||||
Renders ``review_data`` (the output of ``score_review_tool`` plus
|
||||
``dedupe_findings_tool`` results and free-form verdict/tier/scope) into
|
||||
the bundled ``report-template.html`` as ``{{REPORT_DATA}}`` and writes
|
||||
the standalone file (default ``repo_root/code-review-report.html``).
|
||||
a self-contained HTML report (``report-template.html``) and/or a
|
||||
standalone Chinese Markdown report. Both are written by default.
|
||||
|
||||
``output_path`` is treated as the basename/directory of the output:
|
||||
the extension is decided by ``format``, so ``both`` writes
|
||||
``<base>.html`` and ``<base>.md``.
|
||||
|
||||
Args:
|
||||
review_data: Review data dict (metrics, findings, verdict, tier,
|
||||
scope, files, baseline, timestamp, manual_review).
|
||||
output_path: Output file path. Defaults to
|
||||
``<repo_root>/code-review-report.html``.
|
||||
output_path: Output base path (without an extension). Defaults to
|
||||
``<repo_root>/code-review-report``.
|
||||
repo_root: Repository root. Auto-detected if omitted.
|
||||
format: Output format. ``html``, ``markdown``, or ``both``
|
||||
(default: both).
|
||||
"""
|
||||
if format not in ("html", "markdown", "both"):
|
||||
return _error_response(
|
||||
f"Invalid format {format!r}; expected 'html', 'markdown' or 'both'."
|
||||
)
|
||||
store, root = _get_store(repo_root)
|
||||
try:
|
||||
data = build_report_data(review_data)
|
||||
data["summary"] = review_data.get("summary", "")
|
||||
|
||||
template = _load_report_template()
|
||||
rendered = template.replace(
|
||||
"{{REPORT_DATA}}", json.dumps(data, ensure_ascii=False)
|
||||
)
|
||||
|
||||
if output_path:
|
||||
out = Path(output_path)
|
||||
if not out.is_absolute():
|
||||
out = root / out
|
||||
base = Path(output_path)
|
||||
if not base.is_absolute():
|
||||
base = root / base
|
||||
else:
|
||||
out = root / "code-review-report.html"
|
||||
base = root / "code-review-report"
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(rendered, encoding="utf-8")
|
||||
written: list[dict[str, Any]] = []
|
||||
if format in ("html", "both"):
|
||||
template = _load_report_template()
|
||||
rendered = template.replace(
|
||||
"{{REPORT_DATA}}", json.dumps(data, ensure_ascii=False)
|
||||
)
|
||||
html_path = base.with_suffix(".html")
|
||||
html_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
html_path.write_text(rendered, encoding="utf-8")
|
||||
written.append({
|
||||
"format": "html",
|
||||
"output_path": str(html_path),
|
||||
"size_bytes": len(rendered),
|
||||
})
|
||||
|
||||
if format in ("markdown", "both"):
|
||||
rendered_md = render_markdown_report(review_data)
|
||||
md_path = base.with_suffix(".md")
|
||||
md_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
md_path.write_text(rendered_md, encoding="utf-8")
|
||||
written.append({
|
||||
"format": "markdown",
|
||||
"output_path": str(md_path),
|
||||
"size_bytes": len(rendered_md),
|
||||
})
|
||||
|
||||
paths = [w["output_path"] for w in written]
|
||||
return {
|
||||
"status": "ok",
|
||||
"summary": f"Report written to {out}",
|
||||
"output_path": str(out),
|
||||
"report_size_bytes": len(rendered),
|
||||
"summary": f"Report written to {', '.join(paths)}",
|
||||
"output_path": paths[0] if len(paths) == 1 else paths,
|
||||
"files": written,
|
||||
"report_size_bytes": sum(w["size_bytes"] for w in written),
|
||||
}
|
||||
except Exception as exc:
|
||||
return _error_response(str(exc))
|
||||
|
||||
Reference in New Issue
Block a user