Files
cleveragents-core/tools/render-pr-velocity.py
T
drew 88b9373fa9 feat(auto-agents): LLM activity scraper + cost dashboard wiring
Closes the cost-tracking instrumentation gap: the telemetry console's
Cost tab read from an empty ``llm_activity`` table because nothing
in production wrote to it. The scraper walks the OpenCode session
archives that ``_opencode_worker`` already writes (including subagent
trees via the BFS-walked ``parentID`` chain) and emits one row per
assistant turn. Folded into the existing PR-State Warmer loop so it
runs on the same 30s cadence without spinning a new sidecar.

Schema (v6):
- ``llm_activity`` grows ``session_id`` / ``message_id`` / ``provider``
  / ``parent_session_id`` / ``subagent_depth`` columns
- Partial UNIQUE INDEX on ``message_id`` makes re-scrapes idempotent
- v5→v6 migration ALTER-gated on column existence (safe to re-run)

Scraper (``tools/llm_activity_scraper.py``):
- Reads ``.dispatcher-logs/sessions/*.json``, one row per assistant turn
- Folds reasoning tokens into ``tokens_out`` and cache-write into
  ``tokens_in`` (preserves raw breakdown in ``raw`` JSON for future
  cost-calc refinements)
- Normalises ``subagent_depth=0`` at top level so dashboards can
  filter ``WHERE subagent_depth > 0`` cleanly
- Batch INSERT OR IGNORE via new ``PipelineCache.upsert_llm_activity_batch``
  — one fsync per archive, not per turn

Warmer integration:
- First tick: full backfill of the archive directory
- Subsequent ticks: 1h lookback via ``since=`` filter
- Scraper failures are logged and swallowed — PR-state job stays
  load-bearing and unaffected
- ``LLM_ACTIVITY_SCRAPER_DISABLE=1`` env kill switch

Renames (mechanical, atomic):
- ``tools/_forgejo_cache.py`` → ``tools/_pipeline_cache.py``
- ``ForgejoCache`` class → ``PipelineCache``
- Both reflect the module's broader scope (Forgejo data + pipeline
  telemetry tables); on-disk filename ``forgejo.sqlite`` and
  ``FORGEJO_*`` env vars are kept for compatibility

Verified end-to-end on real archives: 435 archives → 3595 turns
ingested (2873 from subagents) across 8 models / 5 providers / 9 PRs.
Re-runs insert 0, dedup 3595.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:53:25 -04:00

1155 lines
44 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Render pr-velocity.canvas.tsx from the local Forgejo cache.
This is the canonical, LLM-free way to refresh the PR velocity canvas.
It pulls a cache delta, computes every chart/table/stat deterministically,
fills a template with narrative auto-commentary (overrideable via a TOML
notes file), and writes the result to the canvas path.
Usage
-----
# Refresh the canvas in place (syncs cache first, ~15s + ~1s render):
python3 tools/render-pr-velocity.py
# Use the existing cache without a delta sync (sub-second):
python3 tools/render-pr-velocity.py --no-sync
# Write to a different path (e.g. stdout):
python3 tools/render-pr-velocity.py --output - | less
# Override commentary / headlines via a TOML notes file:
python3 tools/render-pr-velocity.py --notes tools/pr-velocity-notes.toml
Notes file format (all keys optional)
-------------------------------------
# tools/pr-velocity-notes.toml
[notes]
hero_title = "custom headline"
hero_body = "custom paragraph"
window_stats_note = "..."
weekly_note = "..."
monthly_note = "..."
daily_note = "..."
last_48h_note = "..."
open_by_author_note = "..."
open_age_note = "..."
top_authors_note = "..."
oldest_open_note = "..."
snapshot_body = "..."
hero_tone = "success" # success | warning | danger | info
findings_cards_jsx = \"\"\"... raw JSX ...\"\"\" # override the whole cards block
Any key not present falls back to a deterministic auto-generated sentence.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sqlite3
import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
try:
import tomllib
except ModuleNotFoundError: # Python < 3.11
tomllib = None # type: ignore
from zoneinfo import ZoneInfo
TOOLS_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(TOOLS_DIR))
from _pipeline_cache import PipelineCache, DEFAULT_CACHE_PATH # noqa: E402
EDT_ZONE = ZoneInfo("America/New_York")
TEMPLATE_PATH = TOOLS_DIR / "pr-velocity.canvas.template.tsx"
DEFAULT_NOTES_PATH = TOOLS_DIR / "pr-velocity-notes.toml"
DEFAULT_OUTPUT_PATH = (
Path.home()
/ ".cursor/projects/home-drew-repos-cleveragents-core/canvases/pr-velocity.canvas.tsx"
)
# ── identity consolidation for all-time TOP_AUTHORS ─────────────────────────
# Multiple git identities map to the same human contributor.
IDENTITY_MAP = {
"Brent E. Edwards": "Brent Edwards",
"Brent Edwards": "Brent Edwards",
"khyari hamza": "Hamza Khyari",
"Hamza Khyari": "Hamza Khyari",
"CleverThis": "CleverThis (bot/HAL)",
"clever-agent": "CleverThis (bot/HAL)",
"CleverAgents Build Agent": "CleverThis (bot/HAL)",
"HAL 9000": "CleverThis (bot/HAL)",
"HAL9000": "CleverThis (bot/HAL)",
}
# ── helpers ─────────────────────────────────────────────────────────────────
def _iso(dt_str: str) -> datetime | None:
if not dt_str:
return None
try:
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
except Exception:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def _to_edt(dt: datetime) -> datetime:
return dt.astimezone(EDT_ZONE)
def _fmt_edt(dt: datetime) -> str:
return _to_edt(dt).strftime("%Y-%m-%d %H:%M %Z")
def _run_cmm(args: list[str]) -> list[dict]:
"""Invoke count-master-merges.py and parse the JSON body."""
cmd = [sys.executable, str(TOOLS_DIR / "count-master-merges.py"), "--format", "json", *args]
out = subprocess.check_output(cmd, text=True)
idx = out.find("[")
if idx < 0:
return []
return json.loads(out[idx:])
# ── data computations (all deterministic, pure functions of cache state) ───
def compute_header_stats(cache: PipelineCache, now: datetime, window_stats: dict) -> dict:
con = cache._conn
rows = con.execute("SELECT created_at FROM pulls WHERE state='open'").fetchall()
open_total = len(rows)
ages = []
for r in rows:
c = _iso(r[0])
if c:
ages.append((now - c).total_seconds() / 86400.0)
stale14 = sum(1 for a in ages if a > 14)
stale21 = sum(1 for a in ages if a > 21)
w7 = window_stats["7d"]
velocity7d = round(w7["closed"] / 7.0, 1)
# Most recently merged PR — shown in the top-row summary as a
# freshness indicator (replaced the old "Stale (>14d)" stat).
last_row = con.execute(
"SELECT merged_at, number FROM pulls "
"WHERE merged=1 AND merged_at IS NOT NULL "
"ORDER BY merged_at DESC LIMIT 1"
).fetchone()
last_merge_value = ""
last_merge_label = "Last PR merged"
last_merge_tone = "danger"
if last_row:
dt_utc = _iso(last_row[0])
if dt_utc is not None:
dt_edt = _to_edt(dt_utc)
hrs_ago = (now - dt_utc).total_seconds() / 3600.0
# Value: weekday + time for sub-24h merges, month/day for older.
# Strip any leading zero on the hour ("05:14 AM" → "5:14 AM").
if hrs_ago < 24:
last_merge_value = dt_edt.strftime("%a %I:%M %p").replace(" 0", " ")
else:
last_merge_value = (
dt_edt.strftime("%b %d %I:%M %p").replace(" 0", " ")
)
# Relative-age suffix in the label.
if hrs_ago < 1:
ago = f"{max(int(hrs_ago * 60), 0)}m ago"
elif hrs_ago < 24:
ago = f"{hrs_ago:.1f}h ago"
else:
ago = f"{hrs_ago / 24:.1f}d ago"
last_merge_label = f"Last PR merged (#{last_row[1]}, {ago})"
# Tone telegraphs pipeline freshness: <6h fine, 6-24h watch,
# ≥24h is a real merge-flow outage.
if hrs_ago < 6:
last_merge_tone = "success"
elif hrs_ago < 24:
last_merge_tone = "warning"
else:
last_merge_tone = "danger"
return {
"open_total": open_total,
"stale14": stale14,
"stale21": stale21,
"velocity7d": velocity7d,
"last48_merges": window_stats["48h"]["merged"],
"last_merge_value": last_merge_value,
"last_merge_label": last_merge_label,
"last_merge_tone": last_merge_tone,
}
def compute_window_stats(cache: PipelineCache, now: datetime) -> dict:
"""Five-metric window stats, one entry per (24h, 48h, 7d, 30d)."""
windows = [
("24 h", "24h", timedelta(hours=24)),
("48 h", "48h", timedelta(hours=48)),
("7 d", "7d", timedelta(days=7)),
("30 d", "30d", timedelta(days=30)),
]
con = cache._conn
result: dict[str, dict] = {}
for label, key, delta in windows:
start = now - delta
end = now
s_iso, e_iso = start.isoformat(), end.isoformat()
commits = con.execute(
"SELECT COUNT(*) FROM commits WHERE committer_date >= ? AND committer_date < ?",
(s_iso, e_iso),
).fetchone()[0]
opened = con.execute(
"SELECT COUNT(*) FROM pulls WHERE created_at >= ? AND created_at < ?",
(s_iso, e_iso),
).fetchone()[0]
closed = con.execute(
"SELECT COUNT(*) FROM pulls WHERE closed_at >= ? AND closed_at < ?",
(s_iso, e_iso),
).fetchone()[0]
merged = con.execute(
"SELECT COUNT(*) FROM pulls WHERE closed_at >= ? AND closed_at < ? AND merged=1",
(s_iso, e_iso),
).fetchone()[0]
closed_no = closed - merged
# Lines of code merged in the window: sum of (additions + deletions)
# over PRs merged in [start, end). additions/deletions are populated
# for any PR enriched via Forgejo's pulls/{n} detail endpoint
# (count-master-merges enriches all in-window PRs on every run, so
# 30d coverage is essentially 100%). PRs without detail contribute 0
# via COALESCE so the figure is always defined.
loc_row = con.execute(
"SELECT COALESCE(SUM(COALESCE(additions,0) + COALESCE(deletions,0)), 0) "
"FROM pulls "
"WHERE closed_at >= ? AND closed_at < ? AND merged=1",
(s_iso, e_iso),
).fetchone()
loc = int(loc_row[0]) if loc_row and loc_row[0] is not None else 0
result[key] = {
"label": label,
"commits": commits,
"opened": opened,
"closed": closed,
"merged": merged,
"closed_no_merge": closed_no,
"lines": loc,
}
return result
def compute_weekly(cache: PipelineCache, now: datetime, n_weeks: int = 11) -> list[dict]:
"""N trailing ISO weeks ending at the ISO week containing `now`."""
today_utc = now.astimezone(timezone.utc)
this_monday = today_utc - timedelta(days=today_utc.weekday())
this_monday = this_monday.replace(hour=0, minute=0, second=0, microsecond=0)
out = []
con = cache._conn
for i in range(n_weeks - 1, -1, -1):
ws = this_monday - timedelta(days=7 * i)
we = ws + timedelta(days=7)
iso_year, iso_week, _ = ws.isocalendar()
commits = con.execute(
"SELECT COUNT(*) FROM commits WHERE committer_date >= ? AND committer_date < ?",
(ws.isoformat(), we.isoformat()),
).fetchone()[0]
merged = con.execute(
"SELECT COUNT(*) FROM pulls WHERE closed_at >= ? AND closed_at < ? AND merged=1",
(ws.isoformat(), we.isoformat()),
).fetchone()[0]
push = max(commits - merged, 0)
# Label like "W17 Apr"
month_short = ws.strftime("%b")
label = f"W{iso_week:02d} {month_short}"
out.append({"week": label, "total": commits, "pr": merged, "push": push})
return out
def compute_monthly(cache: PipelineCache, n_months: int = 6) -> tuple[list[str], list[int]]:
"""Last `n_months` months with activity, by master commits."""
con = cache._conn
monthly: defaultdict[str, int] = defaultdict(int)
for (cd,) in con.execute("SELECT committer_date FROM commits"):
dt = _iso(cd)
if not dt:
continue
monthly[dt.strftime("%Y-%m")] += 1
keys = sorted(monthly)[-n_months:]
labels = []
counts = []
for k in keys:
y, m = k.split("-")
labels.append(f"{datetime(int(y), int(m), 1).strftime('%b')} '{y[-2:]}")
counts.append(monthly[k])
return labels, counts
def compute_daily(
events30: list[dict], cache: PipelineCache, now: datetime, days: int = 30
) -> list[dict]:
"""30-day daily throughput in EDT days.
``pr`` = PR merge events, bucketed by ``pulls.merged_at`` (actual merge
moment on master). ``push`` = direct-push events, bucketed by the commit's
``committer_date`` (no PR record to key off).
We do NOT use the event's ``timestamp`` field for PR events because for
rebase merges that fast-forward, that field is the branch's last-push
time (hours before the auto-merge fires). Using ``merged_at`` avoids
shifting late-evening auto-merges into the previous EDT day.
"""
now_edt = _to_edt(now).date()
daily_pr: Counter = Counter()
daily_push: Counter = Counter()
con = cache._conn
for e in events30:
if e.get("kind") == "direct-push":
dt = _iso(e.get("timestamp", ""))
if not dt:
continue
daily_push[_to_edt(dt).date()] += 1
continue
num = e.get("pr_number")
if not num:
continue
row = con.execute(
"SELECT merged_at FROM pulls WHERE number=?",
(num,),
).fetchone()
ts_str = row[0] if row and row[0] else e.get("timestamp", "")
dt = _iso(ts_str)
if not dt:
continue
daily_pr[_to_edt(dt).date()] += 1
out = []
for i in range(days - 1, -1, -1):
d = now_edt - timedelta(days=i)
out.append(
{
"day": f"{d.strftime('%b')} {d.day}",
"pr": daily_pr.get(d, 0),
"push": daily_push.get(d, 0),
}
)
return out
def compute_open_age_buckets(cache: PipelineCache, now: datetime) -> list[dict]:
con = cache._conn
rows = con.execute("SELECT created_at FROM pulls WHERE state='open'").fetchall()
buckets = {"<1d": 0, "13d": 0, "37d": 0, "714d": 0, "1421d": 0, ">21d": 0}
for (c,) in rows:
dt = _iso(c)
if not dt:
continue
age = (now - dt).total_seconds() / 86400.0
if age < 1:
buckets["<1d"] += 1
elif age < 3:
buckets["13d"] += 1
elif age < 7:
buckets["37d"] += 1
elif age < 14:
buckets["714d"] += 1
elif age < 21:
buckets["1421d"] += 1
else:
buckets[">21d"] += 1
return [{"label": k, "value": v} for k, v in buckets.items()]
def compute_top_authors(cache: PipelineCache, n: int = 9) -> list[dict]:
con = cache._conn
counts: Counter = Counter()
total = 0
for (raw,) in con.execute("SELECT raw FROM commits"):
c = json.loads(raw)
raw_name = (
(c.get("commit") or {}).get("author", {}).get("name")
or (c.get("author") or {}).get("login")
or "unknown"
)
canon = IDENTITY_MAP.get(raw_name, raw_name)
counts[canon] += 1
total += 1
out = []
for author, cnt in counts.most_common(n):
pct = 100.0 * cnt / total if total else 0.0
out.append({"author": author, "events": cnt, "pct": f"{pct:.1f}%"})
return out
def compute_open_by_author(cache: PipelineCache, top: int = 5) -> list[dict]:
con = cache._conn
rows = con.execute(
"SELECT user_login, COUNT(*) FROM pulls WHERE state='open' "
"GROUP BY user_login ORDER BY COUNT(*) DESC LIMIT ?",
(top,),
).fetchall()
return [{"author": r[0] or "unknown", "open": r[1]} for r in rows]
def compute_oldest_open(cache: PipelineCache, now: datetime, n: int = 10) -> list[dict]:
con = cache._conn
rows = con.execute(
"SELECT number, user_login, created_at, raw FROM pulls WHERE state='open' "
"ORDER BY created_at ASC LIMIT ?",
(n,),
).fetchall()
out = []
for r in rows:
dt = _iso(r[2])
age = int((now - dt).total_seconds() / 86400.0) if dt else 0
raw = json.loads(r[3])
title = raw.get("title", "")
out.append(
{
"n": r[0],
"author": r[1] or "unknown",
"ageDays": age,
"title": title,
}
)
return out
def compute_driver_telemetry(cache: PipelineCache, now: datetime) -> dict:
"""Aggregate ``merge_cycle`` over the last 7 days for the canvas.
Returns a dict with summary counters, a P50 cycle duration, and a
terminal-state breakdown. When the merge driver hasn't yet emitted any
cycles, ``cycles_count == 0`` and the canvas hides this section.
"""
end = now
start = now - timedelta(days=7)
cycles = cache.merge_cycles_in_window(start, end)
cycles_count = len(cycles)
merged_states = {"merged", "merged_in_train"}
merged_count = sum(1 for c in cycles if c.get("terminal_state") in merged_states)
released_count = cycles_count - merged_count
durations = sorted(
[
float(c["total_seconds"])
for c in cycles
if c.get("total_seconds") is not None
]
)
p50_cycle_s = int(durations[len(durations) // 2]) if durations else 0
state_counts: dict[str, int] = {}
for c in cycles:
s = c.get("terminal_state") or "unknown"
state_counts[s] = state_counts.get(s, 0) + 1
breakdown = []
for state, count in sorted(state_counts.items(), key=lambda x: -x[1]):
pct = (count * 100.0 / cycles_count) if cycles_count else 0.0
breakdown.append({"state": state, "count": count, "pct": f"{pct:.0f}%"})
if cycles_count == 0:
commentary = "No driver activity recorded yet."
else:
commentary = (
f"{merged_count} merge(s), {released_count} release(s) over "
f"{cycles_count} cycle(s); P50 cycle duration ≈ {p50_cycle_s}s."
)
return {
"cycles_count": cycles_count,
"merged_count": merged_count,
"released_count": released_count,
"p50_cycle_s": p50_cycle_s,
"terminal_state_breakdown": breakdown,
"commentary": commentary,
}
def compute_conflict_drive_telemetry(
cache: PipelineCache, now: datetime, days: int = 7
) -> dict:
"""Aggregate ``conflict_drive_cycles`` over the last ``days`` for the canvas.
Returns a dict with summary counters, a per-day cycle histogram, and an
outcome breakdown. When the conflict driver hasn't yet emitted any cycles,
``cycles_count == 0`` and the canvas hides this section.
Per plan §10.A.10, the section shows: cycles per day, resolved count,
escalated count, timeout count, push-rejected count, and breakdown by
outcome.
"""
end = now
start = now - timedelta(days=days)
rows = cache.conflict_drive_cycles_in_window(start, end)
cycles_count = len(rows)
success_outcomes = {"resolved", "resolved-no-conflict"}
resolved_count = sum(1 for r in rows if r.get("outcome") in success_outcomes)
timeout_count = sum(1 for r in rows if r.get("outcome") == "timeout")
push_rejected_count = sum(
1 for r in rows if r.get("outcome") == "lease-violation"
)
# Escalations show up as PRs whose latest cycle classified as definite
# failure that exhausted the 24h budget — operationally, the count of
# rows whose ``failure_kind == 'definite'`` is a useful upper bound; the
# exact escalation flip is captured in details_json.terminal_state but
# we keep this rendering deterministic and JSON-free.
escalated_count = sum(1 for r in rows if r.get("failure_kind") == "definite")
by_day: dict[str, int] = defaultdict(int)
for r in rows:
started_at = r.get("started_at")
if not isinstance(started_at, str):
continue
dt = _iso(started_at)
if dt is None:
continue
day_label = _to_edt(dt).strftime("%Y-%m-%d")
by_day[day_label] += 1
cycles_per_day = [
{"day": day, "count": count}
for day, count in sorted(by_day.items())
]
outcome_counts: dict[str, int] = {}
for r in rows:
o = r.get("outcome") or "unknown"
outcome_counts[o] = outcome_counts.get(o, 0) + 1
outcome_breakdown = []
for outcome, count in sorted(outcome_counts.items(), key=lambda x: -x[1]):
pct = (count * 100.0 / cycles_count) if cycles_count else 0.0
outcome_breakdown.append(
{"outcome": outcome, "count": count, "pct": f"{pct:.0f}%"}
)
if cycles_count == 0:
commentary = "No conflict-driver activity recorded yet."
else:
commentary = (
f"{resolved_count} resolved, {escalated_count} definite failure(s), "
f"{timeout_count} timeout(s), {push_rejected_count} push-rejected over "
f"{cycles_count} cycle(s) in the last {days} day(s)."
)
return {
"days": days,
"cycles_count": cycles_count,
"resolved_count": resolved_count,
"escalated_count": escalated_count,
"timeout_count": timeout_count,
"push_rejected_count": push_rejected_count,
"cycles_per_day": cycles_per_day,
"outcome_breakdown": outcome_breakdown,
"commentary": commentary,
}
def compute_two_day_merges(cache: PipelineCache, events48: list[dict]) -> list[dict]:
"""48h merge list for the table.
Time column = `pulls.merged_at` (the moment Forgejo completed the merge),
NOT the event timestamp from count-master-merges (which exposes the master
commit's `committer_date`). For rebase merges that fast-forward — i.e.
master had not advanced during the PR's life — Forgejo does NOT rewrite
`committer_date`, so the commit carries the branch's last-push time, which
can precede the actual merge by hours of CI wait. Using `merged_at` makes
the table match the "Last PR merged" top stat for the same PR, and makes
the 48h in/out boundary track the real merge event.
"""
con = cache._conn
rows = []
for e in events48:
num = e.get("pr_number")
if not num:
continue
row = con.execute(
"SELECT merged_at, merged_by_login, user_login, additions, deletions, raw "
"FROM pulls WHERE number=?",
(num,),
).fetchone()
if not row or not row[0]:
continue
ts = _iso(row[0])
if ts is None:
continue
rows.append((ts, num, row))
rows.sort(key=lambda x: x[0])
out = []
for ts, num, row in rows:
raw = json.loads(row[5])
title = raw.get("title") or ""
# Normalize "by" to a short login (HAL9000 over Forgejo committer identity)
by = row[1] or row[2] or "unknown"
# Lines committed = additions + deletions (per Forgejo PR detail).
# Both are populated for in-window PRs because count-master-merges
# enriches every PR it touches; `None` only happens if a PR was
# somehow skipped, in which case we fall back to 0 so the table
# column stays an integer.
additions = int(row[3]) if row[3] is not None else 0
deletions = int(row[4]) if row[4] is not None else 0
ts_edt = _to_edt(ts)
ts_label = ts_edt.strftime("%a %I:%M %p").replace(" 0", " ")
out.append(
{
"ts": ts_label,
"pr": num,
"by": by,
"lines": additions + deletions,
"title": title[:80],
}
)
return out
# ── month-band JSX for the daily chart ──────────────────────────────────────
def month_bands_jsx(daily: list[dict]) -> str:
"""Emit proportional-flex month labels under the daily bar chart.
Groups consecutive days by month and emits one <div style={{ flex: N, textAlign: "center" }}>
per month with a <Text tone="tertiary"> label inside.
"""
bands: list[tuple[str, int]] = []
current_month: str | None = None
current_count = 0
for d in daily:
month = d["day"].split(" ", 1)[0]
if month != current_month:
if current_month is not None:
bands.append((current_month, current_count))
current_month = month
current_count = 1
else:
current_count += 1
if current_month is not None:
bands.append((current_month, current_count))
lines = []
for month, count in bands:
lines.append(
f' <div style={{{{ flex: {count}, textAlign: "center" }}}}>\n'
f' <Text tone="tertiary" size="small" style={{{{ margin: 0 }}}}>{month}</Text>\n'
f" </div>"
)
return "\n".join(lines).lstrip()
# ── narrative auto-commentary ────────────────────────────────────────────────
def _signed(n: int) -> str:
return f"{n:+d}"
def narrate(data: dict) -> dict[str, str]:
"""Build deterministic narrative for every commentary slot from the data."""
ws = data["window_stats"]
hdr = data["header"]
w48 = ws["48h"]
w7 = ws["7d"]
w30 = ws["30d"]
# Merger breakdown from 48h merges
by_counter = Counter(m["by"] for m in data["two_day_merges"])
total_48 = sum(by_counter.values())
top_merger, top_count = (by_counter.most_common(1) or [("", 0)])[0]
top_pct = round(100.0 * top_count / total_48) if total_48 else 0
breakdown = " · ".join(f"{n}: {c}" for n, c in by_counter.most_common())
# Hero headline + tone
if top_count >= 10 and top_pct >= 70:
hero_tone = "success"
hero_title = (
f"{top_merger} merge automation active — "
f"{total_48} PRs merged in last 48h ({top_count} by {top_merger} = {top_pct}%)"
)
elif total_48 == 0:
hero_tone = "warning"
hero_title = "No PR merges in the last 48h"
else:
hero_tone = "info"
hero_title = (
f"{total_48} PRs merged in last 48h — "
f"top merger {top_merger} ({top_count})"
)
net7 = w7["opened"] - w7["closed"]
direction7 = (
"net-negative (closing faster than opening)"
if net7 < 0
else "net-positive (opening faster than closing)"
if net7 > 0
else "balanced"
)
merge_rate7 = (100.0 * w7["merged"] / w7["closed"]) if w7["closed"] else 0.0
merge_rate30 = (100.0 * w30["merged"] / w30["closed"]) if w30["closed"] else 0.0
hero_body = (
f"7-day view: {w7['merged']} PRs merged, "
f"{w7['closed_no_merge']} PRs closed without merging, "
f"{w7['commits']} commits to master, "
f"{w7['opened']} new PRs opened. "
f"48h: {w48['merged']} merges + {w48['closed_no_merge']} non-merge closes against {w48['opened']} newly opened. "
f"Backlog: {hdr['open_total']} open PRs ({hdr['stale14']} stale &gt;14d, {hdr['stale21']} stale &gt;21d)."
)
window_stats_note = (
f"7-day flow is {direction7}: {w7['opened']} opened vs {w7['closed']} closed "
f"(net {_signed(net7)}). Merge acceptance rate on closed PRs: "
f"{merge_rate7:.0f}% ({w7['merged']} / {w7['closed']}). "
f"30-day: {w30['opened']} opened vs {w30['closed']} closed "
f"(net {_signed(w30['opened'] - w30['closed'])}), {merge_rate30:.0f}% merge rate."
)
weekly = data["weekly"]
latest_wk = weekly[-1] if weekly else None
peak_wk = max(weekly, key=lambda w: w["pr"]) if weekly else None
if latest_wk and peak_wk:
weekly_note = (
f"{peak_wk['week']} is the PR-merge peak ({peak_wk['pr']} merges, "
f"{peak_wk['total']} total events). Current week {latest_wk['week']}: "
f"{latest_wk['pr']} PR merges + {latest_wk['push']} non-PR commits "
f"({latest_wk['total']} total)."
)
else:
weekly_note = "No weekly data available."
monthly_labels = data["monthly_labels"]
monthly_counts = data["monthly_counts"]
if monthly_counts:
peak_idx = monthly_counts.index(max(monthly_counts))
if peak_idx == len(monthly_counts) - 1:
monthly_note = (
f"Current month ({monthly_labels[-1]}) is the series peak at "
f"{monthly_counts[-1]:,} commits."
)
else:
monthly_note = (
f"Peak month in window: {monthly_labels[peak_idx]} with "
f"{monthly_counts[peak_idx]:,} commits. Current month: "
f"{monthly_labels[-1]}{monthly_counts[-1]:,} commits."
)
else:
monthly_note = "No monthly data available."
daily = data["daily"]
day_totals = [(d["day"], d["pr"] + d["push"], d["pr"]) for d in daily]
peak_day = max(day_totals, key=lambda t: t[1]) if day_totals else None
today = daily[-1] if daily else None
if peak_day and today:
daily_note = (
f"30-day daily throughput. Peak day: {peak_day[0]} "
f"({peak_day[1]} events, {peak_day[2]} PR merges). "
f"Today ({today['day']}): {today['pr']} PR merges, {today['push']} non-PR commits."
)
else:
daily_note = "No daily data available."
# 48h range label
if data["two_day_merges"]:
first_ts = data["two_day_merges"][0]["ts"]
last_ts = data["two_day_merges"][-1]["ts"]
last_48h_range = f"{first_ts} {last_ts} EDT"
else:
last_48h_range = "no merges in last 48h"
last_48h_note = f"{total_48} PRs merged. {breakdown}." if total_48 else "No PR merges in the last 48h."
open_by_author = data["open_by_author"]
if open_by_author:
top_open = open_by_author[0]
pct_of_backlog = round(100.0 * top_open["open"] / hdr["open_total"]) if hdr["open_total"] else 0
open_by_author_note = (
f"{top_open['author']} holds {top_open['open']} of "
f"{hdr['open_total']} open PRs ({pct_of_backlog}%)."
)
else:
open_by_author_note = "No open PRs."
buckets = {b["label"]: b["value"] for b in data["open_age_buckets"]}
mid_band = buckets.get("714d", 0) + buckets.get("1421d", 0)
mid_pct = round(100.0 * mid_band / hdr["open_total"]) if hdr["open_total"] else 0
open_age_note = (
f"{mid_band} of {hdr['open_total']} open PRs ({mid_pct}%) in the 721d band. "
f"Stale (&gt;14d): {hdr['stale14']}. Oldest cohort (&gt;21d): {hdr['stale21']}."
)
top_authors = data["top_authors"]
jeff = next((a for a in top_authors if "Freeman" in a["author"]), None)
hal = next((a for a in top_authors if "HAL" in a["author"] or "CleverThis" in a["author"]), None)
if jeff and hal:
top_authors_note = (
f"All-time master activity: Jeffrey Phillips Freeman leads with "
f"{jeff['pct']}; HAL bot identities (CleverThis, clever-agent, HAL 9000, CleverAgents Build Agent) "
f"together account for {hal['pct']}."
)
else:
top_authors_note = "All-time master commit activity by git identity, consolidated across bot and human aliases."
oldest = data["oldest_open"]
if oldest:
max_age = max(p["ageDays"] for p in oldest)
min_age = min(p["ageDays"] for p in oldest)
oldest_open_note = (
f"Top 10 oldest open PRs ({min_age}{max_age} days). "
f"{hdr['stale21']} PRs total above 21 days."
)
else:
oldest_open_note = "No open PRs."
snapshot_body = (
f"{total_48} PR merges in last 48h"
+ (f" ({top_count} by {top_merger} = {top_pct}%)" if total_48 else "")
+ f". {w48['commits']} total commits to master in 48h. "
f"Open backlog: {hdr['open_total']} PRs "
f"({hdr['stale14']} stale &gt;14d, {hdr['stale21']} stale &gt;21d). "
f"At {hdr['velocity7d']:.1f} closes/day the backlog clears in "
f"~{round(hdr['open_total'] / hdr['velocity7d'])} days absent new arrivals; "
f"net 7-day trend: {_signed(net7)}."
)
# 4 auto-cards
cards = _default_findings_cards(data, by_counter, direction7, top_merger, top_count, top_pct)
return {
"hero_tone": hero_tone,
"hero_title": hero_title,
"hero_body": hero_body,
"window_stats_note": window_stats_note,
"weekly_note": weekly_note,
"monthly_note": monthly_note,
"daily_note": daily_note,
"last_48h_range": last_48h_range,
"last_48h_note": last_48h_note,
"open_by_author_note": open_by_author_note,
"open_age_note": open_age_note,
"top_authors_note": top_authors_note,
"oldest_open_note": oldest_open_note,
"snapshot_body": snapshot_body,
"findings_cards_jsx": cards,
}
def _default_findings_cards(
data: dict,
by_counter: Counter,
direction7: str,
top_merger: str,
top_count: int,
top_pct: int,
) -> str:
hdr = data["header"]
w7 = data["window_stats"]["7d"]
w30 = data["window_stats"]["30d"]
net7 = w7["opened"] - w7["closed"]
net30 = w30["opened"] - w30["closed"]
backlog_tone = "success" if net7 < 0 else "warning"
backlog_label = "Net-negative (good)" if net7 < 0 else "Net-positive"
cards = [
(
"success",
"Active",
f"{top_merger} dominant — {top_pct}% of recent merges",
(
f"{top_merger} merged {top_count} of {sum(by_counter.values())} PRs over the past 48h. "
f"7-day totals: {w7['merged']} merges, {w7['closed_no_merge']} non-merge closes, "
f"{w7['commits']} commits. Most merges are squash or rebase — only detectable via "
f"SHA cross-reference, not commit-message patterns."
),
),
(
backlog_tone,
backlog_label,
f"7-day flow: {direction7.split(' ')[0]} ({net7:+d})",
(
f"7-day: {w7['opened']} opened vs {w7['closed']} closed (net {net7:+d}). "
f"Backlog is {hdr['open_total']} PRs. At {hdr['velocity7d']:.1f} closes/day the "
f"backlog clears in ~{round(hdr['open_total'] / hdr['velocity7d'])} days absent new arrivals. "
f"30-day trend: {net30:+d} ({w30['opened']} opened {w30['closed']} closed)."
),
),
(
"danger" if hdr["stale14"] > 100 else "warning",
"Growing" if hdr["stale14"] > 100 else "Stable",
f"Stale PRs (&gt;14d): {hdr['stale14']}, &gt;21d: {hdr['stale21']}",
(
f"{hdr['stale14']} PRs have been open longer than 14 days; "
f"{hdr['stale21']} above 21 days. "
f"Intake pressure from recent weeks continues to age into the stale bands."
),
),
(
"info",
"Tooling",
"Report auto-generated (no LLM in the loop)",
(
"This canvas was produced by tools/render-pr-velocity.py: delta-sync the local "
"SQLite cache, compute every stat/chart/table from cache SQL, fill a template "
"with deterministic narrative. Re-render with python3 tools/render-pr-velocity.py; "
"override any callout via tools/pr-velocity-notes.toml."
),
),
]
jsx_parts = []
for tone, pill_label, header_text, body_text in cards:
jsx_parts.append(
f""" <Card>
<CardHeader trailing={{<Pill tone="{tone}" active size="sm">{pill_label}</Pill>}}>
{header_text}
</CardHeader>
<CardBody>
<Text>{body_text}</Text>
</CardBody>
</Card>"""
)
return "\n\n".join(jsx_parts)
# ── template rendering ──────────────────────────────────────────────────────
def render(data: dict, notes: dict[str, str]) -> str:
template = TEMPLATE_PATH.read_text()
narrative = narrate(data)
# Overrides
for k, v in notes.items():
if k in narrative:
narrative[k] = v
now_edt_str = data["updated_at"]
generator_note = f"rendered at {datetime.now(timezone.utc).isoformat(timespec='seconds')} UTC"
subs = {
"GENERATOR_NOTE": generator_note,
"UPDATED_AT": now_edt_str,
"WINDOW_STATS_JSON": json.dumps(
[data["window_stats"][k] for k in ("24h", "48h", "7d", "30d")],
indent=2,
),
"WEEKLY_JSON": json.dumps(data["weekly"], indent=2),
"MONTHLY_LABELS_JSON": json.dumps(data["monthly_labels"]),
"MONTHLY_COUNTS_JSON": json.dumps(data["monthly_counts"]),
"DAILY_JSON": json.dumps(data["daily"], indent=2),
"OPEN_AGE_BUCKETS_JSON": json.dumps(data["open_age_buckets"], indent=2),
"TOP_AUTHORS_JSON": json.dumps(data["top_authors"], indent=2),
"OPEN_BY_AUTHOR_JSON": json.dumps(data["open_by_author"], indent=2),
"OLDEST_OPEN_JSON": json.dumps(data["oldest_open"], indent=2),
"TWO_DAY_MERGES_JSON": json.dumps(data["two_day_merges"], indent=2),
"DRIVER_TELEMETRY_JSON": json.dumps(data["driver_telemetry"], indent=2),
"CONFLICT_DRIVE_TELEMETRY_JSON": json.dumps(
data["conflict_drive_telemetry"], indent=2
),
"OPEN_TOTAL": str(data["header"]["open_total"]),
"VELOCITY_7D": f"{data['header']['velocity7d']:.1f}",
"LAST_48H_MERGES": str(data["header"]["last48_merges"]),
"LAST_MERGE_VALUE": data["header"]["last_merge_value"],
"LAST_MERGE_LABEL": data["header"]["last_merge_label"],
"LAST_MERGE_TONE": data["header"]["last_merge_tone"],
"DAILY_MONTH_BANDS_JSX": month_bands_jsx(data["daily"]),
"HERO_TONE": narrative["hero_tone"],
"HERO_TITLE": narrative["hero_title"],
"HERO_BODY": narrative["hero_body"],
"WINDOW_STATS_NOTE": narrative["window_stats_note"],
"WEEKLY_NOTE": narrative["weekly_note"],
"MONTHLY_NOTE": narrative["monthly_note"],
"DAILY_NOTE": narrative["daily_note"],
"LAST_48H_RANGE": narrative["last_48h_range"],
"LAST_48H_NOTE": narrative["last_48h_note"],
"OPEN_BY_AUTHOR_NOTE": narrative["open_by_author_note"],
"OPEN_AGE_NOTE": narrative["open_age_note"],
"TOP_AUTHORS_NOTE": narrative["top_authors_note"],
"OLDEST_OPEN_NOTE": narrative["oldest_open_note"],
"SNAPSHOT_BODY": narrative["snapshot_body"],
"FINDINGS_CARDS_JSX": narrative["findings_cards_jsx"],
}
rendered = template
for k, v in subs.items():
rendered = rendered.replace(f"{{{{{k}}}}}", v)
# Fail loud if any placeholder remains un-substituted
import re
leftover = re.findall(r"\{\{[A-Z_]+\}\}", rendered)
if leftover:
raise RuntimeError(f"Unfilled template placeholders: {sorted(set(leftover))}")
return rendered
# ── notes loading ───────────────────────────────────────────────────────────
def load_notes(path: Path) -> dict[str, str]:
if not path.exists():
return {}
if tomllib is None:
print(f"# WARN: tomllib unavailable; ignoring {path}", file=sys.stderr)
return {}
with path.open("rb") as f:
data = tomllib.load(f)
notes = data.get("notes") or {}
# Normalize keys to lower_case
return {k.lower(): str(v) for k, v in notes.items()}
# ── CLI ─────────────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
p.add_argument("--cache", default=str(DEFAULT_CACHE_PATH), help="Path to SQLite cache.")
p.add_argument("--notes", default=str(DEFAULT_NOTES_PATH), help="Path to TOML notes file (optional).")
p.add_argument(
"--output",
default=str(DEFAULT_OUTPUT_PATH),
help="Output path for the rendered .tsx. Use '-' for stdout.",
)
p.add_argument("--no-sync", action="store_true", help="Skip cache delta-sync.")
p.add_argument("--weeks", type=int, default=11, help="Trailing weeks for the weekly chart.")
p.add_argument("--months", type=int, default=6, help="Trailing months for the monthly chart.")
p.add_argument("--days", type=int, default=30, help="Trailing days for the daily chart.")
p.add_argument("--quiet", action="store_true", help="Suppress progress messages.")
return p.parse_args()
def _log(msg: str, quiet: bool) -> None:
if not quiet:
print(f"# {msg}", file=sys.stderr)
def _load_token() -> str:
"""Load GITEA_TOKEN from .devcontainer/.env or environment."""
tok = (
None # type: ignore
)
env_path = Path.cwd() / ".devcontainer" / ".env"
if env_path.exists():
for line in env_path.read_text().splitlines():
if line.startswith("GITEA_TOKEN"):
_, _, val = line.partition("=")
tok = val.strip().strip('"').strip("'")
break
import os as _os
tok = tok or _os.environ.get("GITEA_TOKEN")
if not tok:
raise RuntimeError("GITEA_TOKEN not found in .devcontainer/.env or environment")
return tok
def main() -> None:
args = parse_args()
cache_path = Path(args.cache)
_log(f"opening cache at {cache_path}", args.quiet)
cache = PipelineCache.open(cache_path)
if not args.no_sync:
_log("delta-syncing cache ...", args.quiet)
token = _load_token()
summary = cache.sync(token, full=False, backfill_details=False, progress=False)
commits = summary.get("commits", {}).get("new_commits", 0)
pulls = summary.get("pulls", {}).get("pulls_upserted", 0)
_log(f"sync: +{commits} commits, {pulls} PRs upserted", args.quiet)
now = datetime.now(timezone.utc)
updated_at_label = _fmt_edt(now).replace("EDT", "EDT").replace("EST", "EST")
# Friendlier label: "2026-04-26 07:52 EDT (Sun)"
updated_at_label = (
_to_edt(now).strftime("%Y-%m-%d %H:%M %Z (%a)")
)
_log("computing window stats (24h/48h/7d/30d)", args.quiet)
window_stats = compute_window_stats(cache, now)
_log(f"computing weekly ({args.weeks} weeks)", args.quiet)
weekly = compute_weekly(cache, now, n_weeks=args.weeks)
_log(f"computing monthly ({args.months} months)", args.quiet)
monthly_labels, monthly_counts = compute_monthly(cache, n_months=args.months)
# Close the parent cache connection before spawning count-master-merges
# subprocesses. count-master-merges opens its own SQLite connection and
# performs writes (enrich_pr → _upsert_pr); holding an idle connection in
# the parent can race with those writes and trigger "database is locked".
# Closing + reopening guarantees a single writer at a time.
cache.close()
_log(f"running count-master-merges.py --days {args.days}", args.quiet)
events_days = _run_cmm(["--days", str(args.days), "--no-sync"])
_log("running count-master-merges.py --hours 48", args.quiet)
events_48h = _run_cmm(["--hours", "48", "--no-sync"])
# Reopen the cache for the remaining computations (open_age_buckets,
# top_authors, two_day_merges, header, …).
cache = PipelineCache.open(cache_path)
_log("computing daily throughput", args.quiet)
daily = compute_daily(events_days, cache, now, days=args.days)
_log("computing open-PR stats", args.quiet)
open_age_buckets = compute_open_age_buckets(cache, now)
open_by_author = compute_open_by_author(cache, top=5)
oldest_open = compute_oldest_open(cache, now, n=10)
_log("computing top authors (all-time)", args.quiet)
top_authors = compute_top_authors(cache, n=9)
_log("computing 48h merge detail", args.quiet)
two_day_merges = compute_two_day_merges(cache, events_48h)
_log("computing driver telemetry (Tier 1B)", args.quiet)
driver_telemetry = compute_driver_telemetry(cache, now)
_log("computing conflict-drive telemetry (Tier 1.5)", args.quiet)
conflict_drive_telemetry = compute_conflict_drive_telemetry(cache, now, days=7)
header = compute_header_stats(cache, now, window_stats)
data = {
"updated_at": updated_at_label,
"window_stats": window_stats,
"weekly": weekly,
"monthly_labels": monthly_labels,
"monthly_counts": monthly_counts,
"daily": daily,
"open_age_buckets": open_age_buckets,
"top_authors": top_authors,
"open_by_author": open_by_author,
"oldest_open": oldest_open,
"two_day_merges": two_day_merges,
"driver_telemetry": driver_telemetry,
"conflict_drive_telemetry": conflict_drive_telemetry,
"header": header,
}
_log("loading notes (if any)", args.quiet)
notes = load_notes(Path(args.notes))
if notes:
_log(f" {len(notes)} note override(s) loaded", args.quiet)
_log("rendering template", args.quiet)
output = render(data, notes)
if args.output == "-":
sys.stdout.write(output)
else:
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(output)
_log(f"wrote {len(output):,} chars to {out_path}", args.quiet)
if __name__ == "__main__":
main()