ai-research-survey

Systematic scan of agentic development research. What's signal, what's noise.
git clone https://git.shiptheloop.com/ai-research-survey.git
Log | Files | Refs

commit 63a3d148e5733c645300c806f4ffdafd4d344f71
parent 64fcaa825f5ab1a80912f80abc8e771b5b0a3a81
Author: Brian Graham <brian@buildingbetterteams.de>
Date:   Sun, 22 Mar 2026 18:38:54 +0100

Add pipeline progress bar and show all registry papers in browser

- Dashboard shows segmented progress bar: v2 scanned, v1 rescan,
  queued, no PDF — updates automatically with each deploy.
- Papers browser lists all 2,687 registry entries. Unscanned papers
  show "--" for score/archetype and are not clickable.
- Pipeline stats added to dashboard.json (registry_total, v2_scanned,
  v1_needs_rescan, has_text_no_scan, no_text, excluded).

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

Diffstat:
Mexplorer/src/data.ts | 14++++++++++++--
Mexplorer/src/style.css | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexplorer/src/views/dashboard.ts | 34+++++++++++++++++++++++++++++++++-
Mexplorer/src/views/paper-detail.ts | 2+-
Mexplorer/src/views/papers.ts | 13++++++++-----
Mexplorer/tests/explorer.spec.ts | 22+++++++++++++++-------
Mscripts/build-explorer-data.py | 44++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 174 insertions(+), 16 deletions(-)

diff --git a/explorer/src/data.ts b/explorer/src/data.ts @@ -23,13 +23,22 @@ export interface PaperIndex { year: number; venue: string; tags: string[]; - score: number; - archetype: string; + score: number | null; + archetype: string | null; games: string[]; arxiv_id: string; doi: string; } +export interface Pipeline { + registry_total: number; + v2_scanned: number; + v1_needs_rescan: number; + has_text_no_scan: number; + no_text: number; + excluded: number; +} + // Full detail for per-paper pages (papers/{slug}.json) export interface PaperDetail extends PaperIndex { category_scores: Record<string, number>; @@ -58,6 +67,7 @@ export interface Dashboard { game_pcts: Record<string, number>; archetype_counts: Record<string, number>; tag_counts: Record<string, number>; + pipeline: Pipeline; } export interface TensionClaim { diff --git a/explorer/src/style.css b/explorer/src/style.css @@ -425,5 +425,66 @@ td.score { .archetype.Mixed { background: rgba(139, 143, 163, 0.2); color: var(--text-dim); } .archetype.Minimal { background: rgba(240, 101, 101, 0.2); color: var(--red); } +/* Pipeline progress bar */ +.pipeline-bar { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; +} +.pipeline-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 0.5rem; +} +.pipeline-title { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.pipeline-stat { + font-family: var(--font); + font-size: 0.8rem; + color: var(--text); +} +.pipeline-track { + display: flex; + height: 12px; + border-radius: 6px; + overflow: hidden; + background: var(--border); +} +.pipeline-seg { + height: 100%; + transition: width 0.3s; +} +.pipeline-seg.scanned { background: var(--green); } +.pipeline-seg.v1 { background: var(--accent); } +.pipeline-seg.queued { background: var(--yellow); } +.pipeline-seg.notext { background: var(--gray); } +.pipeline-legend { + display: flex; + gap: 1.25rem; + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--text-dim); +} +.pipeline-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 4px; + vertical-align: middle; +} +.pipeline-dot.scanned { background: var(--green); } +.pipeline-dot.v1 { background: var(--accent); } +.pipeline-dot.queued { background: var(--yellow); } +.pipeline-dot.notext { background: var(--gray); } + /* Year trend chart */ .trend-chart { margin-top: 0.5rem; } diff --git a/explorer/src/views/dashboard.ts b/explorer/src/views/dashboard.ts @@ -1,14 +1,46 @@ -import { loadDashboard, type Dashboard } from '../data'; +import { loadDashboard, type Dashboard, type Pipeline } from '../data'; import { renderHistogram } from '../components/histogram'; import { renderBarChart } from '../components/bar-chart'; +function renderProgressBar(p: Pipeline): string { + const total = p.registry_total; + const scannedPct = (p.v2_scanned / total * 100).toFixed(1); + const v1Pct = (p.v1_needs_rescan / total * 100).toFixed(1); + const textPct = (p.has_text_no_scan / total * 100).toFixed(1); + const noPct = (p.no_text / total * 100).toFixed(1); + + return `<div class="pipeline-bar"> + <div class="pipeline-header"> + <span class="pipeline-title">Survey Progress</span> + <span class="pipeline-stat">${p.v2_scanned} of ${total} papers scanned (${scannedPct}%)</span> + </div> + <div class="pipeline-track"> + <div class="pipeline-seg scanned" style="width:${scannedPct}%" title="V2 scanned: ${p.v2_scanned}"></div> + <div class="pipeline-seg v1" style="width:${v1Pct}%" title="V1 needs rescan: ${p.v1_needs_rescan}"></div> + <div class="pipeline-seg queued" style="width:${textPct}%" title="Text ready, awaiting scan: ${p.has_text_no_scan}"></div> + <div class="pipeline-seg notext" style="width:${noPct}%" title="No text available: ${p.no_text}"></div> + </div> + <div class="pipeline-legend"> + <span><span class="pipeline-dot scanned"></span>Scanned (${p.v2_scanned})</span> + <span><span class="pipeline-dot v1"></span>V1 rescan (${p.v1_needs_rescan})</span> + <span><span class="pipeline-dot queued"></span>Queued (${p.has_text_no_scan})</span> + <span><span class="pipeline-dot notext"></span>No PDF (${p.no_text})</span> + </div> + </div>`; +} + export async function renderDashboard(app: HTMLElement) { app.innerHTML = '<div class="spinner"></div>'; const agg = await loadDashboard(); const topGame = Object.entries(agg.game_pcts).sort((a, b) => b[1] - a[1])[0]; + const p = agg.pipeline; + const scanPct = Math.round(p.v2_scanned / p.registry_total * 100); + app.innerHTML = ` + ${renderProgressBar(p)} + <div class="cards"> <div class="card"><div class="label">Papers Analyzed</div><div class="value">${agg.n}</div></div> <div class="card"><div class="label">Median Score</div><div class="value yellow">${agg.median}%</div></div> diff --git a/explorer/src/views/paper-detail.ts b/explorer/src/views/paper-detail.ts @@ -81,7 +81,7 @@ export async function renderPaperDetail(app: HTMLElement, slug: string) { <div class="paper-header"> <h2>${paper.title}</h2> <div class="paper-meta"> - <span style="color:${scoreColor(paper.score)};font-family:var(--font);font-weight:700">${paper.score}%</span> + <span style="color:${scoreColor(paper.score!)};font-family:var(--font);font-weight:700">${paper.score}%</span> <span>${paper.year || ''}</span> <span>${paper.venue || ''}</span> <span class="archetype ${paper.archetype}">${paper.archetype}</span> diff --git a/explorer/src/views/papers.ts b/explorer/src/views/papers.ts @@ -15,7 +15,7 @@ export async function renderPapers(app: HTMLElement) { const [papers, dashboard] = await Promise.all([loadPapersIndex(), loadDashboard()]); app.innerHTML = ''; - const archetypes = [...new Set(papers.map(p => p.archetype))].sort(); + const archetypes = [...new Set(papers.map(p => p.archetype).filter((a): a is string => a != null))].sort(); const tags = Object.keys(dashboard.tag_counts).sort(); let filtered = [...papers]; @@ -26,7 +26,8 @@ export async function renderPapers(app: HTMLElement) { if (state.year && p.year !== parseInt(state.year)) return false; if (state.archetype && p.archetype !== state.archetype) return false; if (state.tag && !p.tags.includes(state.tag)) return false; - if (p.score < state.minScore || p.score > state.maxScore) return false; + if (p.score != null && (p.score < state.minScore || p.score > state.maxScore)) return false; + if (p.score == null && state.minScore > 0) return false; return true; }); updateFilterCount(filtersEl, filtered.length, papers.length); @@ -42,14 +43,16 @@ export async function renderPapers(app: HTMLElement) { const columns: Column<PaperIndex>[] = [ { key: 'title', label: 'Title', render: p => p.title.length > 70 ? p.title.slice(0, 67) + '...' : p.title, sortValue: p => p.title }, { key: 'year', label: 'Year', render: p => String(p.year || ''), sortValue: p => p.year || 0 }, - { key: 'score', label: 'Score', render: p => `<span style="color:${scoreColor(p.score)}">${p.score}%</span>`, sortValue: p => p.score }, + { key: 'score', label: 'Score', render: p => p.score != null ? `<span style="color:${scoreColor(p.score)}">${p.score}%</span>` : '<span style="color:var(--gray)">--</span>', sortValue: p => p.score ?? -1 }, { key: 'venue', label: 'Venue', render: p => (p.venue || '').length > 20 ? (p.venue || '').slice(0, 17) + '...' : (p.venue || ''), sortValue: p => p.venue || '' }, - { key: 'archetype', label: 'Type', render: p => `<span class="archetype ${p.archetype}">${p.archetype}</span>`, sortValue: p => p.archetype }, + { key: 'archetype', label: 'Type', render: p => p.archetype ? `<span class="archetype ${p.archetype}">${p.archetype}</span>` : '<span style="color:var(--gray)">--</span>', sortValue: p => p.archetype || '' }, ]; function renderTable() { tableContainer.innerHTML = ''; - const el = renderSortableTable(filtered, columns, p => navigate(`/paper/${p.id}`)); + const el = renderSortableTable(filtered, columns, p => { + if (p.score != null) navigate(`/paper/${p.id}`); + }); tableContainer.appendChild(el); } diff --git a/explorer/tests/explorer.spec.ts b/explorer/tests/explorer.spec.ts @@ -6,8 +6,8 @@ test.describe('Dashboard', () => { await expect(page.locator('.card .value').first()).toBeVisible({ timeout: 10000 }); const cards = page.locator('.card'); await expect(cards).toHaveCount(4); - await expect(cards.nth(0).locator('.value')).toHaveText('467'); - await expect(cards.nth(1).locator('.value')).toHaveText('50%'); + await expect(cards.nth(0).locator('.value')).toHaveText('741'); + await expect(cards.nth(1).locator('.value')).toHaveText('48%'); }); test('shows spinner then content', async ({ page }) => { @@ -37,6 +37,13 @@ test.describe('Dashboard', () => { await expect(page.locator('.trend-chart')).toBeVisible(); }); + test('shows pipeline progress bar', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.pipeline-bar')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.pipeline-stat')).toContainText('of'); + await expect(page.locator('.pipeline-seg.scanned')).toBeVisible(); + }); + test('shows named games', async ({ page }) => { await page.goto('/'); await expect(page.locator('.card .value').first()).toBeVisible({ timeout: 10000 }); @@ -49,8 +56,8 @@ test.describe('Papers Browser', () => { test('shows paper table', async ({ page }) => { await page.goto('/#/papers'); await expect(page.locator('table tbody tr').first()).toBeVisible({ timeout: 10000 }); - expect(await page.locator('table tbody tr').count()).toBe(467); - await expect(page.locator('#f-count')).toHaveText('467 / 467'); + expect(await page.locator('table tbody tr').count()).toBe(2687); + await expect(page.locator('#f-count')).toHaveText('2687 / 2687'); }); test('text search filters papers', async ({ page }) => { @@ -59,7 +66,7 @@ test.describe('Papers Browser', () => { await page.fill('#f-search', 'metr'); const count = await page.locator('table tbody tr').count(); expect(count).toBeGreaterThan(0); - expect(count).toBeLessThan(467); + expect(count).toBeLessThan(2687); }); test('archetype filter works', async ({ page }) => { @@ -68,7 +75,7 @@ test.describe('Papers Browser', () => { await page.selectOption('#f-archetype', 'Complete'); const count = await page.locator('table tbody tr').count(); expect(count).toBeGreaterThan(0); - expect(count).toBeLessThan(467); + expect(count).toBeLessThan(2687); }); test('clicking row navigates to paper detail', async ({ page }) => { @@ -83,7 +90,8 @@ test.describe('Papers Browser', () => { await expect(page.locator('table tbody tr').first()).toBeVisible({ timeout: 10000 }); await page.locator('thead th', { hasText: 'Score' }).click(); const firstScore = await page.locator('table tbody tr').first().locator('td.score').textContent(); - expect(parseFloat(firstScore!)).toBeLessThan(30); + // First entry is "--" (unscanned) or a low score + expect(firstScore!.trim()).toMatch(/^--|^\d/); }); }); diff --git a/scripts/build-explorer-data.py b/scripts/build-explorer-data.py @@ -331,6 +331,29 @@ def build(): game_pcts = {g: round(c / total_papers * 100, 1) for g, c in game_counts.items()} repro_count = sum(1 for p in papers_full if p["category_scores"].get("artifacts", 0) == 100) + # --- Registry pipeline stats --- + reg_total = len(registry) + reg_status = Counter(e.get("status", "unknown") for e in registry.values()) + has_text = sum(1 for e in registry.values() + if (PAPERS_DIR / e["id"] / "paper.txt").exists()) + v1_count = 0 + for scan_path in PAPERS_DIR.glob("*/scan.json"): + pid = scan_path.parent.name + if pid not in {p["id"] for p in papers_full}: + with open(scan_path) as f: + s = json.load(f) + if s.get("scan_version", 1) < 2: + v1_count += 1 + + pipeline = { + "registry_total": reg_total, + "v2_scanned": total_papers, + "v1_needs_rescan": v1_count, + "has_text_no_scan": has_text - total_papers - v1_count, + "no_text": reg_total - has_text, + "excluded": reg_status.get("excluded", 0), + } + dashboard = { "n": total_papers, "median": round(median, 1), @@ -342,6 +365,7 @@ def build(): "game_pcts": game_pcts, "archetype_counts": dict(archetype_counts), "tag_counts": dict(tag_counts), + "pipeline": pipeline, } # --- Citation network --- @@ -377,6 +401,26 @@ def build(): papers_detail_dir = OUTPUT_DIR / "papers" papers_detail_dir.mkdir(parents=True, exist_ok=True) + # Add unscanned registry entries to papers-index (score=null, no detail page) + scanned_ids = {p["id"] for p in papers_index} + for entry in registry.values(): + if entry["id"] in scanned_ids: + continue + if entry.get("status") == "excluded": + continue + papers_index.append({ + "id": entry["id"], + "title": entry.get("title", entry["id"]), + "year": entry.get("year"), + "venue": entry.get("venue", ""), + "tags": entry.get("tags", []), + "score": None, + "archetype": None, + "games": [], + "arxiv_id": entry.get("arxiv_id", ""), + "doi": entry.get("doi", ""), + }) + write_json(OUTPUT_DIR / "dashboard.json", dashboard) write_json(OUTPUT_DIR / "papers-index.json", papers_index) write_json(OUTPUT_DIR / "network.json", network)

Impressum · Datenschutz