generated from coulomb/repo-seed
The analysis half of the weekly coding retrospection. retro/build.py: windowed detect+measure -> top-3 improvement suggestions per repo (cross-flavor first, recommendations pulled from the Pattern Catalog) + fleet snapshot. retro/publish.py: publishes the report to the hub as the coding_retro read model (event_type= coding_retro progress event) + local JSON/md, graceful degrade. retro entrypoint with --window-days/--publish/--json. Live verify over real sessions surfaced per-repo suggestions with catalog recommendations. 13 new tests; suite 152/152. Consumed by activity-core ACTIVITY-WP-0008 (Weekly Coding Retrospection, Sat 19:00). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""Publish the weekly retro (AGENTIC-WP-0010 T02).
|
|
|
|
The retro is published to the State Hub as a **read model** — a progress event of
|
|
``event_type=coding_retro`` whose ``detail`` carries the structured report. This is
|
|
exactly how ``daily-triage-report`` surfaces, and it is what activity-core's
|
|
``coding_retro`` resolver (ACTIVITY-WP-0008) reads. A local JSON + markdown report
|
|
is always written; the hub publish is best-effort and **degrades gracefully** when
|
|
the hub is unreachable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import urllib.request
|
|
from typing import Callable, Optional
|
|
|
|
DEFAULT_HUB = "http://127.0.0.1:8000"
|
|
|
|
|
|
def render_markdown(report: dict) -> str:
|
|
w = report.get("window", {})
|
|
lines = [
|
|
f"# Weekly Coding Retro ({w.get('since', '')[:10]} → {w.get('until', '')[:10]})",
|
|
f"_{report.get('n_sessions', 0)} real sessions · generated {report.get('generated_at', '')}_",
|
|
"",
|
|
"## Top improvement suggestions (cross-flavor first, ≤3 per repo)",
|
|
]
|
|
if not report.get("suggestions"):
|
|
lines.append("- (no recurring problems above threshold this week)")
|
|
for s in report.get("suggestions", []):
|
|
flag = " [CROSS-FLAVOR]" if s.get("cross_flavor") else ""
|
|
lines.append(f"- **{s['repo']}** ({s['priority']}, score={s['score']}){flag}: "
|
|
f"{s['title']} — {s['recommendation']}")
|
|
m = report.get("measure", {})
|
|
lines += ["", "## Fleet snapshot",
|
|
f"- infra-overhead median: {m.get('infra_overhead_share_median')}",
|
|
f"- error rate: {m.get('error_rate')} · schema-thrash: {m.get('schema_thrash_sessions')}",
|
|
f"- success rate: {m.get('success_rate')} · tokens p50: {m.get('tokens_p50')}"]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def write_local(report: dict, json_path: str, md_path: Optional[str] = None) -> None:
|
|
os.makedirs(os.path.dirname(json_path) or ".", exist_ok=True)
|
|
with open(json_path, "w", encoding="utf-8") as fh:
|
|
json.dump(report, fh, indent=2, sort_keys=True)
|
|
fh.write("\n")
|
|
if md_path:
|
|
with open(md_path, "w", encoding="utf-8") as fh:
|
|
fh.write(render_markdown(report))
|
|
fh.write("\n")
|
|
|
|
|
|
def _http_post(url: str, payload: dict) -> None:
|
|
req = urllib.request.Request(url, data=json.dumps(payload).encode(),
|
|
headers={"Content-Type": "application/json"}, method="POST")
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
r.read()
|
|
|
|
|
|
def publish_to_hub(report: dict, *, base_url: str = DEFAULT_HUB,
|
|
poster: Optional[Callable[[str, dict], None]] = None) -> bool:
|
|
"""POST the retro as an event_type=coding_retro progress event. Best-effort."""
|
|
poster = poster or _http_post
|
|
n = report.get("n_sessions", 0)
|
|
k = len(report.get("suggestions", []))
|
|
payload = {
|
|
"event_type": "coding_retro",
|
|
"author": "helix-forge",
|
|
"summary": f"Weekly coding retro: {k} ranked suggestions across "
|
|
f"{report.get('window', {}).get('days', 7)} days ({n} sessions).",
|
|
"detail": report,
|
|
}
|
|
try:
|
|
poster(f"{base_url.rstrip('/')}/progress/", payload)
|
|
return True
|
|
except Exception:
|
|
return False
|