Add 6 new analysis pages and 5 CLI reports
New web UI pages with Plotly charts: - /sources: cross-source comparison (ratings, categories by standards body) - /false-positives: profiling of 73 false positives (box plots, terms) - /trends: temporal evolution (submissions, ratings, safety ratio over time) - /complexity: draft complexity matrix (correlations, scatter plots) - /idea-analysis: idea novelty deep dive (sunburst, distribution, shared ideas) - /citations: enhanced with influence analysis and BCP dependency tabs New CLI reports (ietf report <name>): - sources, false-positives, citations, complexity, idea-analysis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -951,39 +951,124 @@ class Reporter:
|
||||
return str(path)
|
||||
|
||||
def trends_report(self) -> str:
|
||||
"""Generate category trend analysis report with monthly breakdown and growth rates."""
|
||||
"""Generate full temporal evolution report with monthly stats, ratings, safety ratio, and growth."""
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
conn = self.db.conn
|
||||
pairs = self.db.drafts_with_ratings(limit=500)
|
||||
all_drafts = self.db.list_drafts(limit=500, order_by="time ASC")
|
||||
total = len(all_drafts)
|
||||
|
||||
rating_map = {draft.name: rating for draft, rating in pairs}
|
||||
|
||||
# Monthly counts per category
|
||||
monthly: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
||||
# Monthly submission counts by source
|
||||
source_monthly: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
||||
for d in all_drafts:
|
||||
month = d.time[:7] if d.time else "unknown"
|
||||
if month != "unknown":
|
||||
src = getattr(d, "source", "ietf") or "ietf"
|
||||
source_monthly[month][src] += 1
|
||||
|
||||
# Monthly category counts
|
||||
cat_monthly: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
||||
all_cats: set[str] = set()
|
||||
for d in all_drafts:
|
||||
month = d.time[:7] if d.time else "unknown"
|
||||
r = rating_map.get(d.name)
|
||||
if r:
|
||||
if r and month != "unknown":
|
||||
for c in r.categories:
|
||||
monthly[month][c] += 1
|
||||
cat_monthly[month][c] += 1
|
||||
all_cats.add(c)
|
||||
|
||||
months = sorted(m for m in monthly.keys() if m != "unknown")
|
||||
months = sorted(m for m in set(list(source_monthly.keys()) + list(cat_monthly.keys())) if m != "unknown")
|
||||
cats = sorted(all_cats)
|
||||
|
||||
# Monthly average ratings
|
||||
rating_monthly: dict[str, dict[str, list[int]]] = defaultdict(lambda: defaultdict(list))
|
||||
for d in all_drafts:
|
||||
month = d.time[:7] if d.time else "unknown"
|
||||
r = rating_map.get(d.name)
|
||||
if r and month != "unknown":
|
||||
for dim in ("novelty", "maturity", "overlap", "momentum", "relevance"):
|
||||
rating_monthly[month][dim].append(getattr(r, dim))
|
||||
|
||||
# Safety vs capability categories
|
||||
safety_cats = {"Security", "Privacy", "Trust & Identity", "Safety", "Governance & Policy", "Ethics"}
|
||||
capability_cats = {"Agent Communication", "Agent Framework", "AI Infrastructure",
|
||||
"Model Serving", "MCP", "Orchestration", "Tool Use",
|
||||
"Prompt Engineering", "Inference", "LLM Integration"}
|
||||
|
||||
# Monthly new authors
|
||||
author_rows = conn.execute("""
|
||||
SELECT da.person_id, MIN(substr(d.time, 1, 7)) AS first_month
|
||||
FROM draft_authors da
|
||||
JOIN drafts d ON da.draft_name = d.name
|
||||
WHERE d.time IS NOT NULL AND d.time != ''
|
||||
GROUP BY da.person_id
|
||||
""").fetchall()
|
||||
new_author_monthly: dict[str, int] = defaultdict(int)
|
||||
for r in author_rows:
|
||||
if r["first_month"]:
|
||||
new_author_monthly[r["first_month"]] += 1
|
||||
|
||||
# Cumulative idea counts
|
||||
idea_rows = conn.execute("""
|
||||
SELECT substr(d.time, 1, 7) AS month, COUNT(i.id) AS cnt
|
||||
FROM ideas i
|
||||
JOIN drafts d ON i.draft_name = d.name
|
||||
WHERE d.time IS NOT NULL AND d.time != ''
|
||||
GROUP BY month ORDER BY month
|
||||
""").fetchall()
|
||||
idea_cumulative = {}
|
||||
running = 0
|
||||
for r in idea_rows:
|
||||
running += r["cnt"]
|
||||
idea_cumulative[r["month"]] = running
|
||||
|
||||
def _trend(val, prev_val):
|
||||
if prev_val is None:
|
||||
return ""
|
||||
if val > prev_val:
|
||||
return " \u2191"
|
||||
elif val < prev_val:
|
||||
return " \u2193"
|
||||
return " \u2192"
|
||||
|
||||
lines = [
|
||||
"# Category Trend Analysis",
|
||||
f"*Generated {now} — {total} drafts, {len(months)} months, {len(cats)} categories*\n",
|
||||
"# Temporal Evolution Report",
|
||||
f"*Generated {now} \u2014 {total} drafts, {len(months)} months*\n",
|
||||
]
|
||||
|
||||
# Growth summary
|
||||
# Monthly stats table
|
||||
lines.extend([
|
||||
"## Monthly Overview\n",
|
||||
"| Month | Submissions | New Authors | Cum. Ideas | Avg Novelty | Avg Maturity | Avg Relevance | Safety Ratio |",
|
||||
"|-------|------------:|------------:|-----------:|------------:|-------------:|--------------:|-------------:|",
|
||||
])
|
||||
prev_total = None
|
||||
for month in months:
|
||||
total_sub = sum(source_monthly[month].values())
|
||||
new_auth = new_author_monthly.get(month, 0)
|
||||
cum_ideas = idea_cumulative.get(month, 0)
|
||||
dims = rating_monthly.get(month, {})
|
||||
avg_n = sum(dims.get("novelty", [0])) / max(len(dims.get("novelty", [1])), 1)
|
||||
avg_m = sum(dims.get("maturity", [0])) / max(len(dims.get("maturity", [1])), 1)
|
||||
avg_r = sum(dims.get("relevance", [0])) / max(len(dims.get("relevance", [1])), 1)
|
||||
safety = sum(cat_monthly[month].get(c, 0) for c in safety_cats)
|
||||
capability = sum(cat_monthly[month].get(c, 0) for c in capability_cats)
|
||||
ratio = f"{safety / capability:.2f}" if capability > 0 else "-"
|
||||
trend = _trend(total_sub, prev_total)
|
||||
prev_total = total_sub
|
||||
lines.append(
|
||||
f"| {month} | {total_sub}{trend} | {new_auth} | {cum_ideas} | "
|
||||
f"{avg_n:.1f} | {avg_m:.1f} | {avg_r:.1f} | {ratio} |"
|
||||
)
|
||||
|
||||
# Category growth summary
|
||||
recent_months = months[-3:] if len(months) >= 3 else months
|
||||
prev_months = months[-6:-3] if len(months) >= 6 else []
|
||||
|
||||
lines.extend([
|
||||
"## Growth Summary\n",
|
||||
"\n## Category Growth Summary\n",
|
||||
"| Category | Total | Last 3mo | Prev 3mo | Growth |",
|
||||
"|----------|------:|---------:|---------:|-------:|",
|
||||
])
|
||||
@@ -991,12 +1076,12 @@ class Reporter:
|
||||
cumulative: dict[str, int] = defaultdict(int)
|
||||
for month in months:
|
||||
for cat in cats:
|
||||
cumulative[cat] += monthly[month].get(cat, 0)
|
||||
cumulative[cat] += cat_monthly[month].get(cat, 0)
|
||||
|
||||
for cat in cats:
|
||||
total_cat = cumulative[cat]
|
||||
recent = sum(monthly[m].get(cat, 0) for m in recent_months)
|
||||
prev = sum(monthly[m].get(cat, 0) for m in prev_months) if prev_months else 0
|
||||
recent = sum(cat_monthly[m].get(cat, 0) for m in recent_months)
|
||||
prev = sum(cat_monthly[m].get(cat, 0) for m in prev_months) if prev_months else 0
|
||||
if prev > 0:
|
||||
growth_str = f"{((recent - prev) / prev) * 100:+.0f}%"
|
||||
elif recent > 0:
|
||||
@@ -1005,44 +1090,41 @@ class Reporter:
|
||||
growth_str = "-"
|
||||
lines.append(f"| {cat} | {total_cat} | {recent} | {prev if prev_months else '-'} | {growth_str} |")
|
||||
|
||||
# Monthly detail table
|
||||
lines.extend(["\n## Monthly Breakdown\n"])
|
||||
header = "| Month |" + " | ".join(f" {c[:15]}" for c in cats) + " | Total |"
|
||||
sep = "|-------|" + " | ".join("---:" for _ in cats) + " | -----:|"
|
||||
lines.append(header)
|
||||
lines.append(sep)
|
||||
|
||||
for month in months:
|
||||
counts = [str(monthly[month].get(c, 0)) for c in cats]
|
||||
month_total = sum(monthly[month].values())
|
||||
lines.append(f"| {month} | " + " | ".join(counts) + f" | {month_total} |")
|
||||
|
||||
# Half-over-half comparison
|
||||
# Fastest growing categories (early vs late half)
|
||||
if len(months) >= 4:
|
||||
mid = len(months) // 2
|
||||
early = months[:mid]
|
||||
late = months[mid:]
|
||||
|
||||
lines.extend([
|
||||
"\n## Fastest Growing Categories (early vs late half)\n",
|
||||
])
|
||||
|
||||
lines.extend(["\n## Fastest Growing Categories (early vs late half)\n"])
|
||||
growth_data = []
|
||||
for cat in cats:
|
||||
e = sum(monthly[m].get(cat, 0) for m in early)
|
||||
l = sum(monthly[m].get(cat, 0) for m in late)
|
||||
e = sum(cat_monthly[m].get(cat, 0) for m in early)
|
||||
l_val = sum(cat_monthly[m].get(cat, 0) for m in late)
|
||||
if e > 0:
|
||||
pct = ((l - e) / e) * 100
|
||||
growth_data.append((cat, pct, e, l))
|
||||
elif l > 0:
|
||||
growth_data.append((cat, float("inf"), e, l))
|
||||
|
||||
pct = ((l_val - e) / e) * 100
|
||||
growth_data.append((cat, pct, e, l_val))
|
||||
elif l_val > 0:
|
||||
growth_data.append((cat, float("inf"), e, l_val))
|
||||
growth_data.sort(key=lambda x: x[1], reverse=True)
|
||||
for cat, pct, e, l in growth_data:
|
||||
for cat, pct, e, l_val in growth_data:
|
||||
if pct == float("inf"):
|
||||
lines.append(f"- **{cat}**: new (0 -> {l} drafts)")
|
||||
lines.append(f"- **{cat}**: new (0 \u2192 {l_val} drafts)")
|
||||
else:
|
||||
lines.append(f"- **{cat}**: {pct:+.0f}% ({e} -> {l} drafts)")
|
||||
lines.append(f"- **{cat}**: {pct:+.0f}% ({e} \u2192 {l_val} drafts)")
|
||||
|
||||
# Rating trends
|
||||
lines.extend(["\n## Rating Dimension Trends\n"])
|
||||
if len(months) >= 2:
|
||||
first_half = months[:len(months) // 2]
|
||||
second_half = months[len(months) // 2:]
|
||||
for dim in ("novelty", "maturity", "overlap", "momentum", "relevance"):
|
||||
early_vals = [v for m in first_half for v in rating_monthly.get(m, {}).get(dim, [])]
|
||||
late_vals = [v for m in second_half for v in rating_monthly.get(m, {}).get(dim, [])]
|
||||
early_avg = sum(early_vals) / len(early_vals) if early_vals else 0
|
||||
late_avg = sum(late_vals) / len(late_vals) if late_vals else 0
|
||||
diff = late_avg - early_avg
|
||||
arrow = "\u2191" if diff > 0.1 else ("\u2193" if diff < -0.1 else "\u2192")
|
||||
lines.append(f"- **{dim.capitalize()}**: {early_avg:.2f} \u2192 {late_avg:.2f} ({diff:+.2f}) {arrow}")
|
||||
|
||||
report = "\n".join(lines)
|
||||
path = self.output_dir / "trends.md"
|
||||
|
||||
@@ -735,6 +735,21 @@ def api_bcp_analysis():
|
||||
return jsonify(get_bcp_analysis(db()))
|
||||
|
||||
|
||||
# ── Idea Analysis ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.route("/idea-analysis")
|
||||
def idea_analysis():
|
||||
data = get_idea_analysis(db())
|
||||
return render_template("idea_analysis.html", data=data)
|
||||
|
||||
|
||||
@app.route("/api/idea-analysis")
|
||||
def api_idea_analysis():
|
||||
data = get_idea_analysis(db())
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
# ── Trends & Complexity ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -752,29 +767,12 @@ def complexity():
|
||||
|
||||
@app.route("/api/trends")
|
||||
def api_trends():
|
||||
data = get_trends_data(db())
|
||||
return jsonify(data)
|
||||
return jsonify(get_trends_data(db()))
|
||||
|
||||
|
||||
@app.route("/api/complexity")
|
||||
def api_complexity():
|
||||
data = get_complexity_data(db())
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
# ── Idea Analysis ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.route("/idea-analysis")
|
||||
def idea_analysis():
|
||||
data = get_idea_analysis(db())
|
||||
return render_template("idea_analysis.html", data=data)
|
||||
|
||||
|
||||
@app.route("/api/idea-analysis")
|
||||
def api_idea_analysis():
|
||||
data = get_idea_analysis(db())
|
||||
return jsonify(data)
|
||||
return jsonify(get_complexity_data(db()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
1017
src/webui/data.py
1017
src/webui/data.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user