quality_trend_report.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. quality_trend_report.py - 生成章节质量趋势报告(离线)
  5. 数据来源:
  6. - index.db.review_metrics
  7. - index.db.writing_checklist_scores
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. from datetime import datetime
  12. from pathlib import Path
  13. from typing import Any, Dict, List
  14. from runtime_compat import enable_windows_utf8_stdio
  15. try:
  16. from project_locator import resolve_project_root
  17. except ImportError: # pragma: no cover
  18. from scripts.project_locator import resolve_project_root
  19. try:
  20. from data_modules.config import DataModulesConfig
  21. from data_modules.index_manager import IndexManager
  22. except ImportError: # pragma: no cover
  23. from scripts.data_modules.config import DataModulesConfig
  24. from scripts.data_modules.index_manager import IndexManager
  25. def _to_float(value: Any, default: float = 0.0) -> float:
  26. try:
  27. return float(value)
  28. except (TypeError, ValueError):
  29. return default
  30. def _to_int(value: Any, default: int = 0) -> int:
  31. try:
  32. return int(value)
  33. except (TypeError, ValueError):
  34. return default
  35. def _percent(value: float) -> str:
  36. return f"{value * 100:.1f}%"
  37. def _build_review_rows(records: List[Dict[str, Any]]) -> List[str]:
  38. if not records:
  39. return ["| - | - | - | - | - | - |", "| - | - | - | - | - | - |"]
  40. rows: List[str] = []
  41. sorted_records = sorted(
  42. records,
  43. key=lambda x: (_to_int(x.get("end_chapter")), _to_int(x.get("start_chapter"))),
  44. )
  45. for row in sorted_records:
  46. severities = row.get("severity_counts") or {}
  47. critical = _to_int(severities.get("critical"))
  48. high = _to_int(severities.get("high"))
  49. medium = _to_int(severities.get("medium"))
  50. low = _to_int(severities.get("low"))
  51. range_text = f"{_to_int(row.get('start_chapter'))}-{_to_int(row.get('end_chapter'))}"
  52. score = _to_float(row.get("overall_score"))
  53. rows.append(
  54. f"| {range_text} | {score:.1f} | {critical} | {high} | {medium} | {low} |"
  55. )
  56. return rows
  57. def _build_checklist_rows(records: List[Dict[str, Any]]) -> List[str]:
  58. if not records:
  59. return ["| - | - | - | - |"]
  60. rows: List[str] = []
  61. sorted_records = sorted(records, key=lambda x: _to_int(x.get("chapter")))
  62. for row in sorted_records:
  63. chapter = _to_int(row.get("chapter"))
  64. score = _to_float(row.get("score"))
  65. completion = _to_float(row.get("completion_rate"))
  66. required_items = _to_int(row.get("required_items"))
  67. completed_required = _to_int(row.get("completed_required"))
  68. if required_items > 0:
  69. required_rate = completed_required / required_items
  70. else:
  71. required_rate = 1.0
  72. rows.append(
  73. f"| {chapter} | {score:.1f} | {_percent(completion)} | {_percent(required_rate)} |"
  74. )
  75. return rows
  76. def _build_risk_flags(
  77. review_trend: Dict[str, Any],
  78. checklist_trend: Dict[str, Any],
  79. ) -> List[str]:
  80. flags: List[str] = []
  81. overall_avg = _to_float(review_trend.get("overall_avg"))
  82. if overall_avg < 75 and review_trend.get("count", 0) > 0:
  83. flags.append(f"审查均分偏低({overall_avg:.1f}),建议优先回看低分区间。")
  84. severity_totals = review_trend.get("severity_totals") or {}
  85. critical_total = _to_int(severity_totals.get("critical"))
  86. high_total = _to_int(severity_totals.get("high"))
  87. if critical_total > 0:
  88. flags.append(f"存在 {critical_total} 个 critical 问题,建议设为最高修复优先级。")
  89. elif high_total >= 5:
  90. flags.append(f"high 问题累计 {high_total} 个,建议做批量修复专项。")
  91. score_avg = _to_float(checklist_trend.get("score_avg"))
  92. if checklist_trend.get("count", 0) > 0 and score_avg < 80:
  93. flags.append(f"写作清单平均分偏低({score_avg:.1f}),建议加强执行清单落地。")
  94. completion_avg = _to_float(checklist_trend.get("completion_avg"))
  95. if checklist_trend.get("count", 0) > 0 and completion_avg < 0.7:
  96. flags.append(f"写作清单完成率仅 {_percent(completion_avg)},建议减少每章可选项数量。")
  97. if not flags:
  98. flags.append("近期质量指标整体稳定,暂无高优先级风险。")
  99. return flags
  100. def build_quality_report(
  101. project_root: Path,
  102. manager: IndexManager,
  103. *,
  104. limit: int,
  105. ) -> str:
  106. review_records = manager.get_recent_review_metrics(limit=limit)
  107. review_trend = manager.get_review_trend_stats(last_n=limit)
  108. checklist_records = manager.get_recent_writing_checklist_scores(limit=limit)
  109. checklist_trend = manager.get_writing_checklist_score_trend(last_n=limit)
  110. now_text = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  111. overall_avg = _to_float(review_trend.get("overall_avg"))
  112. review_count = _to_int(review_trend.get("count"))
  113. checklist_count = _to_int(checklist_trend.get("count"))
  114. checklist_score_avg = _to_float(checklist_trend.get("score_avg"))
  115. checklist_completion_avg = _to_float(checklist_trend.get("completion_avg"))
  116. dimension_avg = review_trend.get("dimension_avg") or {}
  117. severity_totals = review_trend.get("severity_totals") or {}
  118. risk_flags = _build_risk_flags(review_trend, checklist_trend)
  119. lines: List[str] = []
  120. lines.append("# 质量趋势报告")
  121. lines.append("")
  122. lines.append(f"- 生成时间: {now_text}")
  123. lines.append(f"- 项目路径: `{project_root}`")
  124. lines.append(f"- 统计窗口: 最近 {limit} 条记录")
  125. lines.append("")
  126. lines.append("## 总览")
  127. lines.append("")
  128. lines.append(f"- 审查记录数: {review_count}")
  129. lines.append(f"- 审查均分: {overall_avg:.1f}")
  130. lines.append(f"- 清单评分记录数: {checklist_count}")
  131. lines.append(f"- 清单平均分: {checklist_score_avg:.1f}")
  132. lines.append(f"- 清单平均完成率: {_percent(checklist_completion_avg)}")
  133. lines.append("")
  134. lines.append("## 审查区间趋势")
  135. lines.append("")
  136. lines.append("| 区间 | 总分 | Critical | High | Medium | Low |")
  137. lines.append("|---|---:|---:|---:|---:|---:|")
  138. lines.extend(_build_review_rows(review_records))
  139. lines.append("")
  140. lines.append("## 维度均分")
  141. lines.append("")
  142. lines.append("| 维度 | 平均分 |")
  143. lines.append("|---|---:|")
  144. if dimension_avg:
  145. for key in sorted(dimension_avg.keys()):
  146. lines.append(f"| {key} | {_to_float(dimension_avg.get(key)):.1f} |")
  147. else:
  148. lines.append("| - | - |")
  149. lines.append("")
  150. lines.append("## 严重级别汇总")
  151. lines.append("")
  152. lines.append("| 等级 | 数量 |")
  153. lines.append("|---|---:|")
  154. for level in ("critical", "high", "medium", "low"):
  155. lines.append(f"| {level} | {_to_int(severity_totals.get(level))} |")
  156. lines.append("")
  157. lines.append("## 写作清单趋势")
  158. lines.append("")
  159. lines.append("| 章节 | 分数 | 完成率 | 必做完成率 |")
  160. lines.append("|---:|---:|---:|---:|")
  161. lines.extend(_build_checklist_rows(checklist_records))
  162. lines.append("")
  163. lines.append("## 风险提示")
  164. lines.append("")
  165. for item in risk_flags:
  166. lines.append(f"- {item}")
  167. lines.append("")
  168. return "\n".join(lines)
  169. def main() -> None:
  170. parser = argparse.ArgumentParser(description="生成离线质量趋势报告(基于 index.db)")
  171. parser.add_argument("--project-root", type=str, help="项目根目录(可选,不传则自动探测)")
  172. parser.add_argument("--limit", type=int, default=20, help="统计最近 N 条记录(默认 20)")
  173. parser.add_argument("--output", type=str, help="输出文件路径(默认 .webnovel/reports/quality-trend.md)")
  174. args = parser.parse_args()
  175. if args.project_root:
  176. # 允许传入“工作区根目录”,统一解析到真正的 book project_root
  177. project_root = resolve_project_root(args.project_root)
  178. else:
  179. project_root = resolve_project_root()
  180. cfg = DataModulesConfig.from_project_root(project_root)
  181. manager = IndexManager(cfg)
  182. limit = max(1, int(args.limit))
  183. output_path = (
  184. Path(args.output).expanduser().resolve()
  185. if args.output
  186. else (cfg.webnovel_dir / "reports" / "quality-trend.md")
  187. )
  188. output_path.parent.mkdir(parents=True, exist_ok=True)
  189. report = build_quality_report(project_root, manager, limit=limit)
  190. output_path.write_text(report, encoding="utf-8")
  191. print(f"✅ 已生成质量趋势报告: {output_path}")
  192. if __name__ == "__main__":
  193. import sys
  194. if sys.platform == "win32":
  195. enable_windows_utf8_stdio()
  196. main()