review_pipeline.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Step 3 审查结果处理。
  5. 读取 reviewer agent 的原始输出 JSON,解析为 ReviewResult,
  6. 生成 metrics 用于 index.db 沉淀。
  7. """
  8. from __future__ import annotations
  9. import argparse
  10. import json
  11. import sys
  12. from pathlib import Path
  13. from typing import Any, Dict, List
  14. from runtime_compat import enable_windows_utf8_stdio
  15. def _ensure_scripts_path() -> None:
  16. scripts_dir = Path(__file__).resolve().parent
  17. if str(scripts_dir) not in sys.path:
  18. sys.path.insert(0, str(scripts_dir))
  19. _ensure_scripts_path()
  20. from data_modules.review_author_view import render_review_author_view
  21. from data_modules.review_schema import append_ai_flavor_anti_patterns, parse_review_output
  22. def _resolve_report_path(project_root: Path, report_file: str) -> Path:
  23. root = project_root.expanduser().resolve()
  24. report_path = Path(report_file).expanduser()
  25. if not report_path.is_absolute():
  26. report_path = root / report_path
  27. report_path = report_path.resolve()
  28. try:
  29. report_path.relative_to(root)
  30. except ValueError as exc:
  31. raise ValueError("report-file 必须位于 project_root 目录内") from exc
  32. return report_path
  33. def _format_issue(issue: Dict[str, Any], index: int) -> List[str]:
  34. description = str(issue.get("description") or "未填写问题描述")
  35. severity = str(issue.get("severity") or "medium")
  36. category = str(issue.get("category") or "other")
  37. location = str(issue.get("location") or "未标注位置")
  38. evidence = str(issue.get("evidence") or "未提供证据")
  39. fix_hint = str(issue.get("fix_hint") or "未提供修复方向")
  40. blocking = "是" if issue.get("blocking") else "否"
  41. return [
  42. f"{index}. **{description}**",
  43. f" - 严重级别:{severity}",
  44. f" - 分类:{category}",
  45. f" - 位置:{location}",
  46. f" - 阻断:{blocking}",
  47. f" - 证据:{evidence}",
  48. f" - 修复方向:{fix_hint}",
  49. ]
  50. def render_review_report(payload: Dict[str, Any]) -> str:
  51. result = payload["review_result"]
  52. metrics = payload["metrics"]
  53. issues = list(result.get("issues", []))
  54. blocking_issues = [issue for issue in issues if issue.get("blocking")]
  55. non_blocking_issues = [issue for issue in issues if not issue.get("blocking")]
  56. severity_counts = metrics.get("severity_counts", {})
  57. lines: List[str] = [
  58. f"# 第{payload['chapter']}章审查报告",
  59. "",
  60. render_review_author_view(payload).rstrip(),
  61. "",
  62. "## 总览",
  63. "",
  64. f"- 问题数:{result.get('issues_count', 0)}",
  65. f"- 阻断数:{result.get('blocking_count', 0)}",
  66. f"- 结论:{'需修复后重审' if result.get('has_blocking') else '无阻断问题'}",
  67. ]
  68. summary = str(result.get("summary") or "").strip()
  69. if summary:
  70. lines.append(f"- 摘要:{summary}")
  71. if severity_counts:
  72. ordered = [
  73. f"{level}={severity_counts.get(level, 0)}"
  74. for level in ("critical", "high", "medium", "low")
  75. ]
  76. lines.append(f"- 严重级别统计:{', '.join(ordered)}")
  77. lines.extend(["", "## 阻断问题", ""])
  78. if blocking_issues:
  79. for index, issue in enumerate(blocking_issues, start=1):
  80. lines.extend(_format_issue(issue, index))
  81. lines.append("")
  82. else:
  83. lines.append("无。")
  84. lines.append("")
  85. lines.extend(["## 其他问题", ""])
  86. if non_blocking_issues:
  87. for index, issue in enumerate(non_blocking_issues, start=1):
  88. lines.extend(_format_issue(issue, index))
  89. lines.append("")
  90. else:
  91. lines.append("无。")
  92. lines.append("")
  93. lines.extend(["## 修复方向", ""])
  94. if issues:
  95. ordered_issues = [*blocking_issues, *non_blocking_issues]
  96. for index, issue in enumerate(ordered_issues, start=1):
  97. description = str(issue.get("description") or "未填写问题描述")
  98. fix_hint = str(issue.get("fix_hint") or "未提供修复方向")
  99. lines.append(f"{index}. {description}:{fix_hint}")
  100. else:
  101. lines.append("暂无需要修复的问题。")
  102. return "\n".join(lines).rstrip() + "\n"
  103. def write_review_report(project_root: Path, report_file: str, payload: Dict[str, Any]) -> Path:
  104. report_path = _resolve_report_path(project_root, report_file)
  105. report_path.parent.mkdir(parents=True, exist_ok=True)
  106. report_path.write_text(render_review_report(payload), encoding="utf-8")
  107. return report_path
  108. def _build_review_metrics_record(metrics: Dict[str, Any]):
  109. from data_modules.index_manager import ReviewMetrics
  110. return ReviewMetrics(
  111. start_chapter=int(metrics["start_chapter"]),
  112. end_chapter=int(metrics["end_chapter"]),
  113. overall_score=float(metrics.get("overall_score", 0.0)),
  114. dimension_scores=dict(metrics.get("dimension_scores", {})),
  115. severity_counts=dict(metrics.get("severity_counts", {})),
  116. critical_issues=list(metrics.get("critical_issues", [])),
  117. report_file=str(metrics.get("report_file", "")),
  118. notes=str(metrics.get("notes", "")),
  119. )
  120. def build_review_artifacts(
  121. project_root: Path,
  122. chapter: int,
  123. review_results_path: Path,
  124. report_file: str = "",
  125. ) -> Dict[str, Any]:
  126. raw = json.loads(review_results_path.read_text(encoding="utf-8"))
  127. result = parse_review_output(chapter=chapter, raw=raw)
  128. anti_patterns_added = append_ai_flavor_anti_patterns(project_root, result)
  129. metrics = result.to_metrics_dict(report_file=report_file)
  130. normalized_review = result.to_dict()
  131. review_results_path.parent.mkdir(parents=True, exist_ok=True)
  132. review_results_path.write_text(
  133. json.dumps(normalized_review, ensure_ascii=False, indent=2),
  134. encoding="utf-8",
  135. )
  136. return {
  137. "chapter": chapter,
  138. "review_result": normalized_review,
  139. "metrics": metrics,
  140. "anti_patterns_added": anti_patterns_added,
  141. }
  142. def main() -> None:
  143. parser = argparse.ArgumentParser(description="Review pipeline v6")
  144. parser.add_argument("--project-root", required=True)
  145. parser.add_argument("--chapter", type=int, required=True)
  146. parser.add_argument("--review-results", required=True)
  147. parser.add_argument("--metrics-out", default="")
  148. parser.add_argument("--report-file", default="")
  149. parser.add_argument("--save-metrics", action="store_true",
  150. help="直接写入 index.db,省去单独调用 save-review-metrics")
  151. args = parser.parse_args()
  152. project_root = Path(args.project_root)
  153. review_results_path = Path(args.review_results)
  154. payload = build_review_artifacts(
  155. project_root=project_root,
  156. chapter=args.chapter,
  157. review_results_path=review_results_path,
  158. report_file=args.report_file,
  159. )
  160. if args.metrics_out:
  161. out_path = Path(args.metrics_out)
  162. out_path.parent.mkdir(parents=True, exist_ok=True)
  163. out_path.write_text(
  164. json.dumps(payload["metrics"], ensure_ascii=False, indent=2),
  165. encoding="utf-8",
  166. )
  167. if args.report_file:
  168. write_review_report(
  169. project_root=project_root,
  170. report_file=args.report_file,
  171. payload=payload,
  172. )
  173. if args.save_metrics:
  174. from data_modules.config import DataModulesConfig
  175. from data_modules.index_manager import IndexManager
  176. config = DataModulesConfig.from_project_root(project_root)
  177. manager = IndexManager(config)
  178. manager.save_review_metrics(_build_review_metrics_record(payload["metrics"]))
  179. print(json.dumps(payload, ensure_ascii=False, indent=2))
  180. if __name__ == "__main__":
  181. if sys.platform == "win32":
  182. enable_windows_utf8_stdio()
  183. main()