|
|
@@ -0,0 +1,240 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+quality_trend_report.py - 生成章节质量趋势报告(离线)
|
|
|
+
|
|
|
+数据来源:
|
|
|
+- index.db.review_metrics
|
|
|
+- index.db.writing_checklist_scores
|
|
|
+"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import argparse
|
|
|
+from datetime import datetime
|
|
|
+from pathlib import Path
|
|
|
+from typing import Any, Dict, List
|
|
|
+
|
|
|
+from runtime_compat import enable_windows_utf8_stdio
|
|
|
+
|
|
|
+try:
|
|
|
+ from project_locator import resolve_project_root
|
|
|
+except ImportError: # pragma: no cover
|
|
|
+ from scripts.project_locator import resolve_project_root
|
|
|
+
|
|
|
+try:
|
|
|
+ from data_modules.config import DataModulesConfig
|
|
|
+ from data_modules.index_manager import IndexManager
|
|
|
+except ImportError: # pragma: no cover
|
|
|
+ from scripts.data_modules.config import DataModulesConfig
|
|
|
+ from scripts.data_modules.index_manager import IndexManager
|
|
|
+
|
|
|
+
|
|
|
+def _to_float(value: Any, default: float = 0.0) -> float:
|
|
|
+ try:
|
|
|
+ return float(value)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return default
|
|
|
+
|
|
|
+
|
|
|
+def _to_int(value: Any, default: int = 0) -> int:
|
|
|
+ try:
|
|
|
+ return int(value)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return default
|
|
|
+
|
|
|
+
|
|
|
+def _percent(value: float) -> str:
|
|
|
+ return f"{value * 100:.1f}%"
|
|
|
+
|
|
|
+
|
|
|
+def _build_review_rows(records: List[Dict[str, Any]]) -> List[str]:
|
|
|
+ if not records:
|
|
|
+ return ["| - | - | - | - | - | - |", "| - | - | - | - | - | - |"]
|
|
|
+
|
|
|
+ rows: List[str] = []
|
|
|
+ sorted_records = sorted(
|
|
|
+ records,
|
|
|
+ key=lambda x: (_to_int(x.get("end_chapter")), _to_int(x.get("start_chapter"))),
|
|
|
+ )
|
|
|
+ for row in sorted_records:
|
|
|
+ severities = row.get("severity_counts") or {}
|
|
|
+ critical = _to_int(severities.get("critical"))
|
|
|
+ high = _to_int(severities.get("high"))
|
|
|
+ medium = _to_int(severities.get("medium"))
|
|
|
+ low = _to_int(severities.get("low"))
|
|
|
+ range_text = f"{_to_int(row.get('start_chapter'))}-{_to_int(row.get('end_chapter'))}"
|
|
|
+ score = _to_float(row.get("overall_score"))
|
|
|
+ rows.append(
|
|
|
+ f"| {range_text} | {score:.1f} | {critical} | {high} | {medium} | {low} |"
|
|
|
+ )
|
|
|
+ return rows
|
|
|
+
|
|
|
+
|
|
|
+def _build_checklist_rows(records: List[Dict[str, Any]]) -> List[str]:
|
|
|
+ if not records:
|
|
|
+ return ["| - | - | - | - |"]
|
|
|
+
|
|
|
+ rows: List[str] = []
|
|
|
+ sorted_records = sorted(records, key=lambda x: _to_int(x.get("chapter")))
|
|
|
+ for row in sorted_records:
|
|
|
+ chapter = _to_int(row.get("chapter"))
|
|
|
+ score = _to_float(row.get("score"))
|
|
|
+ completion = _to_float(row.get("completion_rate"))
|
|
|
+ required_items = _to_int(row.get("required_items"))
|
|
|
+ completed_required = _to_int(row.get("completed_required"))
|
|
|
+ if required_items > 0:
|
|
|
+ required_rate = completed_required / required_items
|
|
|
+ else:
|
|
|
+ required_rate = 1.0
|
|
|
+ rows.append(
|
|
|
+ f"| {chapter} | {score:.1f} | {_percent(completion)} | {_percent(required_rate)} |"
|
|
|
+ )
|
|
|
+ return rows
|
|
|
+
|
|
|
+
|
|
|
+def _build_risk_flags(
|
|
|
+ review_trend: Dict[str, Any],
|
|
|
+ checklist_trend: Dict[str, Any],
|
|
|
+) -> List[str]:
|
|
|
+ flags: List[str] = []
|
|
|
+
|
|
|
+ overall_avg = _to_float(review_trend.get("overall_avg"))
|
|
|
+ if overall_avg < 75 and review_trend.get("count", 0) > 0:
|
|
|
+ flags.append(f"审查均分偏低({overall_avg:.1f}),建议优先回看低分区间。")
|
|
|
+
|
|
|
+ severity_totals = review_trend.get("severity_totals") or {}
|
|
|
+ critical_total = _to_int(severity_totals.get("critical"))
|
|
|
+ high_total = _to_int(severity_totals.get("high"))
|
|
|
+ if critical_total > 0:
|
|
|
+ flags.append(f"存在 {critical_total} 个 critical 问题,建议设为最高修复优先级。")
|
|
|
+ elif high_total >= 5:
|
|
|
+ flags.append(f"high 问题累计 {high_total} 个,建议做批量修复专项。")
|
|
|
+
|
|
|
+ score_avg = _to_float(checklist_trend.get("score_avg"))
|
|
|
+ if checklist_trend.get("count", 0) > 0 and score_avg < 80:
|
|
|
+ flags.append(f"写作清单平均分偏低({score_avg:.1f}),建议加强执行清单落地。")
|
|
|
+
|
|
|
+ completion_avg = _to_float(checklist_trend.get("completion_avg"))
|
|
|
+ if checklist_trend.get("count", 0) > 0 and completion_avg < 0.7:
|
|
|
+ flags.append(f"写作清单完成率仅 {_percent(completion_avg)},建议减少每章可选项数量。")
|
|
|
+
|
|
|
+ if not flags:
|
|
|
+ flags.append("近期质量指标整体稳定,暂无高优先级风险。")
|
|
|
+
|
|
|
+ return flags
|
|
|
+
|
|
|
+
|
|
|
+def build_quality_report(
|
|
|
+ project_root: Path,
|
|
|
+ manager: IndexManager,
|
|
|
+ *,
|
|
|
+ limit: int,
|
|
|
+) -> str:
|
|
|
+ review_records = manager.get_recent_review_metrics(limit=limit)
|
|
|
+ review_trend = manager.get_review_trend_stats(last_n=limit)
|
|
|
+ checklist_records = manager.get_recent_writing_checklist_scores(limit=limit)
|
|
|
+ checklist_trend = manager.get_writing_checklist_score_trend(last_n=limit)
|
|
|
+
|
|
|
+ now_text = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
+ overall_avg = _to_float(review_trend.get("overall_avg"))
|
|
|
+ review_count = _to_int(review_trend.get("count"))
|
|
|
+ checklist_count = _to_int(checklist_trend.get("count"))
|
|
|
+ checklist_score_avg = _to_float(checklist_trend.get("score_avg"))
|
|
|
+ checklist_completion_avg = _to_float(checklist_trend.get("completion_avg"))
|
|
|
+
|
|
|
+ dimension_avg = review_trend.get("dimension_avg") or {}
|
|
|
+ severity_totals = review_trend.get("severity_totals") or {}
|
|
|
+ risk_flags = _build_risk_flags(review_trend, checklist_trend)
|
|
|
+
|
|
|
+ lines: List[str] = []
|
|
|
+ lines.append("# 质量趋势报告")
|
|
|
+ lines.append("")
|
|
|
+ lines.append(f"- 生成时间: {now_text}")
|
|
|
+ lines.append(f"- 项目路径: `{project_root}`")
|
|
|
+ lines.append(f"- 统计窗口: 最近 {limit} 条记录")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("## 总览")
|
|
|
+ lines.append("")
|
|
|
+ lines.append(f"- 审查记录数: {review_count}")
|
|
|
+ lines.append(f"- 审查均分: {overall_avg:.1f}")
|
|
|
+ lines.append(f"- 清单评分记录数: {checklist_count}")
|
|
|
+ lines.append(f"- 清单平均分: {checklist_score_avg:.1f}")
|
|
|
+ lines.append(f"- 清单平均完成率: {_percent(checklist_completion_avg)}")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.append("## 审查区间趋势")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("| 区间 | 总分 | Critical | High | Medium | Low |")
|
|
|
+ lines.append("|---|---:|---:|---:|---:|---:|")
|
|
|
+ lines.extend(_build_review_rows(review_records))
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.append("## 维度均分")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("| 维度 | 平均分 |")
|
|
|
+ lines.append("|---|---:|")
|
|
|
+ if dimension_avg:
|
|
|
+ for key in sorted(dimension_avg.keys()):
|
|
|
+ lines.append(f"| {key} | {_to_float(dimension_avg.get(key)):.1f} |")
|
|
|
+ else:
|
|
|
+ lines.append("| - | - |")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.append("## 严重级别汇总")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("| 等级 | 数量 |")
|
|
|
+ lines.append("|---|---:|")
|
|
|
+ for level in ("critical", "high", "medium", "low"):
|
|
|
+ lines.append(f"| {level} | {_to_int(severity_totals.get(level))} |")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.append("## 写作清单趋势")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("| 章节 | 分数 | 完成率 | 必做完成率 |")
|
|
|
+ lines.append("|---:|---:|---:|---:|")
|
|
|
+ lines.extend(_build_checklist_rows(checklist_records))
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.append("## 风险提示")
|
|
|
+ lines.append("")
|
|
|
+ for item in risk_flags:
|
|
|
+ lines.append(f"- {item}")
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
+
|
|
|
+def main() -> None:
|
|
|
+ parser = argparse.ArgumentParser(description="生成离线质量趋势报告(基于 index.db)")
|
|
|
+ parser.add_argument("--project-root", type=str, help="项目根目录(可选,不传则自动探测)")
|
|
|
+ parser.add_argument("--limit", type=int, default=20, help="统计最近 N 条记录(默认 20)")
|
|
|
+ parser.add_argument("--output", type=str, help="输出文件路径(默认 .webnovel/reports/quality-trend.md)")
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ if args.project_root:
|
|
|
+ project_root = Path(args.project_root).expanduser().resolve()
|
|
|
+ else:
|
|
|
+ project_root = resolve_project_root()
|
|
|
+
|
|
|
+ cfg = DataModulesConfig.from_project_root(project_root)
|
|
|
+ manager = IndexManager(cfg)
|
|
|
+
|
|
|
+ limit = max(1, int(args.limit))
|
|
|
+ output_path = (
|
|
|
+ Path(args.output).expanduser().resolve()
|
|
|
+ if args.output
|
|
|
+ else (cfg.webnovel_dir / "reports" / "quality-trend.md")
|
|
|
+ )
|
|
|
+ output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+
|
|
|
+ report = build_quality_report(project_root, manager, limit=limit)
|
|
|
+ output_path.write_text(report, encoding="utf-8")
|
|
|
+ print(f"✅ 已生成质量趋势报告: {output_path}")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ import sys
|
|
|
+ if sys.platform == "win32":
|
|
|
+ enable_windows_utf8_stdio()
|
|
|
+ main()
|