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:
2026-03-08 20:35:32 +01:00
parent 8515e46d5d
commit dec8667193
8 changed files with 1517 additions and 63 deletions

View File

@@ -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"

View File

@@ -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__":

File diff suppressed because it is too large Load Diff