[object Object]

← back to Ventura Corridor

feat(coverage): /api/ideas/stats + accept-rate line on /coverage.html

faa570d99d35d2627a7c182df227a34ba691dc73 · 2026-05-07 22:49:49 -0700 · SteveStudio2

Sanity-check the idea-loop debate panel: are we accepting too generously
(>50% rate) or too harshly (<5%)? Current 7/55 = 12.7% sits in the
healthy zone.

Routes:
  GET /api/ideas/stats   {accepted, rejected, accept_rate, accepted_by_type{},
                          rejected_by_type{}}. Reads both jsonl files in the
                          idea-loop data dir; 60s cache.

UI:
  /coverage.html idea section   monospace stats line above the score-sorted
                                feed: "N accepted · M rejected · P%
                                accept rate · by type: …"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit faa570d99d35d2627a7c182df227a34ba691dc73
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 7 22:49:49 2026 -0700

    feat(coverage): /api/ideas/stats + accept-rate line on /coverage.html
    
    Sanity-check the idea-loop debate panel: are we accepting too generously
    (>50% rate) or too harshly (<5%)? Current 7/55 = 12.7% sits in the
    healthy zone.
    
    Routes:
      GET /api/ideas/stats   {accepted, rejected, accept_rate, accepted_by_type{},
                              rejected_by_type{}}. Reads both jsonl files in the
                              idea-loop data dir; 60s cache.
    
    UI:
      /coverage.html idea section   monospace stats line above the score-sorted
                                    feed: "N accepted · M rejected · P%
                                    accept rate · by type: …"
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/coverage.html | 10 ++++++++++
 src/server/index.ts  | 36 ++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+)

diff --git a/public/coverage.html b/public/coverage.html
index 93e7537..3f29571 100644
--- a/public/coverage.html
+++ b/public/coverage.html
@@ -87,6 +87,7 @@ main{padding:32px;max-width:1200px;margin:0 auto}
 <div class="bar-section">
   <h3>Idea-loop · accepted ideas (top by score)</h3>
   <p style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:var(--ink-mute);margin:0 0 14px">Surviving ideas from the autonomous idea-loop debate panel (qwen3 + gemma3 + deepseek-r1). Full viewer at <a href="http://127.0.0.1:9920/" target="_blank" rel="noopener" style="color:var(--metal)">:9920</a>.</p>
+  <div id="idea-stats" style="font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--ink-mute);margin-bottom:14px;text-transform:uppercase"></div>
   <div id="idea-feed"></div>
 </div>
 
@@ -160,6 +161,15 @@ async function load() {
   renderBars(d.by_vertical, document.getElementById('vert'));
   renderBars(d.by_city, document.getElementById('city'));
 
+  // Idea-loop stats line
+  try {
+    const stats = await fetch('/api/ideas/stats').then(r => r.json());
+    const pct = (stats.accept_rate * 100).toFixed(1);
+    const types = Object.entries(stats.accepted_by_type || {}).map(([k,v]) => k + ':' + v).join(' · ');
+    document.getElementById('idea-stats').textContent =
+      `${stats.accepted} accepted · ${stats.rejected} rejected · ${pct}% accept rate${types ? ' · by type: ' + types : ''}`;
+  } catch(e) {}
+
   // Idea-loop feed
   try {
     const ideas = await fetch('/api/ideas/recent').then(r => r.json());
diff --git a/src/server/index.ts b/src/server/index.ts
index 9ff6830..c1cd119 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5143,6 +5143,42 @@ app.get('/api/news/archive', async (_req, res) => {
 // Bridge to the idea-loop skill at ~/.claude/skills/idea-loop/data/ideas.jsonl.
 // Reads the JSONL on demand (not large, ~kb scale), returns last N parsed
 // entries. Pure read; idea-loop owns its own writes.
+// Idea-loop stats: accept/reject totals + breakdown by type. Useful to spot
+// "is the panel too generous" (>50% accept) or "too harsh" (<5% accept).
+app.get('/api/ideas/stats', async (_req, res) => {
+  try {
+    const fs = await import('node:fs/promises');
+    const path = await import('node:path');
+    const home = process.env.HOME || '/Users/stevestudio2';
+    const dir = path.join(home, '.claude', 'skills', 'idea-loop', 'data');
+    const readJsonl = async (file: string) => {
+      try {
+        const txt = await fs.readFile(path.join(dir, file), 'utf8');
+        return txt.split(/\r?\n/).filter(Boolean).map(l => {
+          try { return JSON.parse(l); } catch { return null; }
+        }).filter(Boolean);
+      } catch { return []; }
+    };
+    const accepted = await readJsonl('ideas.jsonl');
+    const rejected = await readJsonl('rejects.jsonl');
+    const by = (arr: any[]) => arr.reduce((m: Record<string, number>, x) => {
+      const t = String(x.type || 'other');
+      m[t] = (m[t] || 0) + 1; return m;
+    }, {});
+    const total = accepted.length + rejected.length;
+    res.set('Cache-Control', 'public, max-age=60');
+    res.json({
+      accepted: accepted.length,
+      rejected: rejected.length,
+      accept_rate: total ? +(accepted.length / total).toFixed(3) : 0,
+      accepted_by_type: by(accepted),
+      rejected_by_type: by(rejected)
+    });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 app.get('/api/ideas/recent', async (_req, res) => {
   try {
     const fs = await import('node:fs/promises');

← c9a00d6 feat(coverage): ideas_accepted count in homepage build-statu  ·  back to Ventura Corridor  ·  feat(coverage): per-type accept-rate mini-bars in idea-loop df2073d →