main
py 134 lines 3.92 KB
Raw
1 #!/usr/bin/env python3
2 from __future__ import annotations
3
4 import argparse
5 import sys
6 from dataclasses import dataclass
7 from pathlib import Path
8
9 ROOT = Path(__file__).resolve().parent.parent
10 if str(ROOT) not in sys.path:
11 sys.path.insert(0, str(ROOT))
12
13 from scripts import analysis_gate # noqa: E402
14
15 DEFAULT_ANALYZED_DIR = ROOT / "data" / "analyzed"
16
17
18 @dataclass(frozen=True)
19 class QualityEntry:
20 week: str
21 score: int
22 path: Path
23
24
25 def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
26 parser = argparse.ArgumentParser(
27 description="Build a quality trend report from analyzed summaries."
28 )
29 parser.add_argument(
30 "--analyzed-dir",
31 type=Path,
32 default=DEFAULT_ANALYZED_DIR,
33 help="Directory containing analyzed weekly summaries.",
34 )
35 parser.add_argument(
36 "--output",
37 type=Path,
38 help="Optional path to write the markdown report. Defaults to stdout.",
39 )
40 return parser.parse_args(argv)
41
42
43 def load_quality_entries(analyzed_dir: Path) -> list[QualityEntry]:
44 if not analyzed_dir.exists():
45 return []
46
47 entries: list[QualityEntry] = []
48 for path in sorted(analyzed_dir.glob("*-summary.md")):
49 frontmatter, _ = analysis_gate.extract_frontmatter(path.read_text(encoding="utf-8"))
50 week = frontmatter.get("week")
51 score = frontmatter.get("quality_score")
52 if isinstance(week, str) and isinstance(score, int):
53 entries.append(QualityEntry(week=week, score=score, path=path))
54 return sorted(entries, key=lambda entry: entry.week)
55
56
57 def classify_trend(entries: list[QualityEntry]) -> str:
58 if len(entries) < 2:
59 return "insufficient history"
60
61 change = entries[-1].score - entries[0].score
62 if change >= 5:
63 return "improving"
64 if change <= -5:
65 return "declining"
66 return "stable"
67
68
69 def build_quality_report(analyzed_dir: Path) -> str:
70 from scripts.sanitize_repo_content import _escape_untrusted_boundaries
71
72 entries = load_quality_entries(analyzed_dir)
73 lines = ["# Quality Trend Report", ""]
74
75 if not entries:
76 lines.extend(
77 [
78 "No analyzed summaries with a parseable `quality_score` were found.",
79 "",
80 "## Weekly Scores",
81 "",
82 "_No data available._",
83 ]
84 )
85 return "\n".join(lines) + "\n"
86
87 average_score = sum(entry.score for entry in entries) / len(entries)
88 trend = classify_trend(entries)
89 best = max(entries, key=lambda entry: entry.score)
90 worst = min(entries, key=lambda entry: entry.score)
91 latest = entries[-1]
92
93 lines.extend(
94 [
95 f"- Summaries analyzed: {len(entries)}",
96 f"- Average quality score: {average_score:.1f}",
97 f"- Trend: {trend}",
98 f"- Latest week: {latest.week} ({latest.score})",
99 f"- Best week: {best.week} ({best.score})",
100 f"- Lowest week: {worst.week} ({worst.score})",
101 "",
102 "## Weekly Scores",
103 "",
104 "| Week | Quality Score |",
105 "| --- | ---: |",
106 ]
107 )
108 for entry in entries:
109 lines.append(f"| {entry.week} | {entry.score} |")
110
111 lines.extend(
112 [
113 "",
114 "## Interpretation",
115 "",
116 f"Quality is currently **{trend}** based on the available summaries. Use this trend as a calibration aid, not as a substitute for reviewing the underlying Signal/Noise/Gaps calls.",
117 ]
118 )
119 return _escape_untrusted_boundaries("\n".join(lines) + "\n")
120
121
122 def main(argv: list[str] | None = None) -> int:
123 args = parse_args(argv)
124 report = build_quality_report(args.analyzed_dir)
125 if args.output:
126 args.output.parent.mkdir(parents=True, exist_ok=True)
127 args.output.write_text(report, encoding="utf-8")
128 else:
129 print(report, end="")
130 return 0
131
132
133 if __name__ == "__main__":
134 raise SystemExit(main())