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 fdf7bf9ec122ae8104c86d45c5563560fafefd4f
parent f41b10dd2346179281c7a9ef5e809bb5cb87c2ec
Author: Brian Graham <brian@buildingbetterteams.de>
Date:   Tue, 24 Mar 2026 06:25:59 +0100

Add butterfly timeline charts to tensions view

Each tension now has a diverging bar chart with horizontal time axis:
- Blue bars extend UP for positive claims (count + mean method score)
- Gray bars extend DOWN for nuanced claims (count + mean method score)
- No color encoding for score — numbers shown directly as labels
- Hover tooltips on each bar with full details

Year field added to tension claims in build pipeline.
Replaces the previous vertical color-encoded butterfly prototype.

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

Diffstat:
Mexplorer/src/data.ts | 1+
Mexplorer/src/views/tensions.ts | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/build-explorer-data.py | 6+++---
3 files changed, 97 insertions(+), 3 deletions(-)

diff --git a/explorer/src/data.ts b/explorer/src/data.ts @@ -120,6 +120,7 @@ export interface TensionClaim { claim: string; supported: string; score: number; + year: number | null; } export interface Tensions { diff --git a/explorer/src/views/tensions.ts b/explorer/src/views/tensions.ts @@ -30,6 +30,13 @@ function scoreColor(s: number): string { return 'var(--green)'; } +function scoreHex(s: number): string { + if (s < 35) return '#d04040'; + if (s < 45) return '#c08030'; + if (s < 55) return '#a0a040'; + return '#40a060'; +} + export async function renderTensions(app: HTMLElement) { app.innerHTML = '<div class="spinner"></div>'; const tensions = await loadTensions(); @@ -42,6 +49,9 @@ export async function renderTensions(app: HTMLElement) { return `<div class="tension-group section"> <h2>${meta.title}</h2> <div class="tension-stat">Positive claims: ${sides.positive.length} (mean score ${posMean}%) \u00b7 Nuanced claims: ${sides.nuanced.length} (mean score ${nuaMean}%)</div> + + ${renderButterfly(sides.positive, sides.nuanced, meta)} + <div class="tension-columns"> <div class="tension-col"> <h4>${meta.positive}</h4> @@ -56,6 +66,89 @@ export async function renderTensions(app: HTMLElement) { }).join(''); } +function renderButterfly(positive: TensionClaim[], nuanced: TensionClaim[], meta: { positive: string; nuanced: string }): string { + // Group by year + const posByYear = new Map<number, TensionClaim[]>(); + const nuaByYear = new Map<number, TensionClaim[]>(); + + for (const c of positive) { + if (!c.year || c.year < 2022) continue; + if (!posByYear.has(c.year)) posByYear.set(c.year, []); + posByYear.get(c.year)!.push(c); + } + for (const c of nuanced) { + if (!c.year || c.year < 2022) continue; + if (!nuaByYear.has(c.year)) nuaByYear.set(c.year, []); + nuaByYear.get(c.year)!.push(c); + } + + const sortedYears = [...new Set([...posByYear.keys(), ...nuaByYear.keys()])].sort(); + if (sortedYears.length < 2) return ''; + + let maxCount = 1; + for (const y of sortedYears) { + maxCount = Math.max(maxCount, (posByYear.get(y) || []).length, (nuaByYear.get(y) || []).length); + } + + // Horizontal time axis, bars going up (positive) and down (nuanced) from center line + const w = 600, h = 280; + const pad = { l: 40, r: 20, t: 30, b: 30 }; + const chartW = w - pad.l - pad.r; + const chartH = h - pad.t - pad.b; + const midY = pad.t + chartH / 2; + const barMaxH = chartH / 2 - 15; + const colW = chartW / sortedYears.length; + + let svg = ''; + + // Zero line + svg += `<line x1="${pad.l}" y1="${midY}" x2="${w - pad.r}" y2="${midY}" stroke="var(--border)" stroke-width="1"/>`; + + // Side labels + svg += `<text x="${pad.l - 4}" y="${midY - barMaxH / 2}" text-anchor="end" font-size="10" fill="var(--text-dim)" transform="rotate(-90, ${pad.l - 4}, ${midY - barMaxH / 2})">${meta.positive} \u2191</text>`; + svg += `<text x="${pad.l - 4}" y="${midY + barMaxH / 2}" text-anchor="end" font-size="10" fill="var(--text-dim)" transform="rotate(-90, ${pad.l - 4}, ${midY + barMaxH / 2})">${meta.nuanced} \u2193</text>`; + + for (let i = 0; i < sortedYears.length; i++) { + const y = sortedYears[i]; + const cx = pad.l + i * colW + colW / 2; + const barW = colW * 0.65; + const posC = posByYear.get(y) || []; + const nuaC = nuaByYear.get(y) || []; + const posCount = posC.length; + const nuaCount = nuaC.length; + const posMean = posC.length ? Math.round(posC.reduce((s, c) => s + c.score, 0) / posC.length) : 0; + const nuaMean = nuaC.length ? Math.round(nuaC.reduce((s, c) => s + c.score, 0) / nuaC.length) : 0; + + // Year label + svg += `<text x="${cx}" y="${h - 8}" text-anchor="middle" font-size="11" fill="var(--text)">${y}</text>`; + + // Positive bar (goes UP from center) + if (posCount > 0) { + const barH = (posCount / maxCount) * barMaxH; + svg += `<rect x="${cx - barW / 2}" y="${midY - barH - 1}" width="${barW}" height="${barH}" rx="2" fill="var(--accent)" opacity="0.7"> + <title>${y} ${meta.positive}: ${posCount} claims, mean score ${posMean}%</title> + </rect>`; + svg += `<text x="${cx}" y="${midY - barH - 4}" text-anchor="middle" font-size="9" fill="var(--text-dim)">${posCount}</text>`; + svg += `<text x="${cx}" y="${midY - barH - 14}" text-anchor="middle" font-size="9" font-weight="600" fill="var(--text)">${posMean}%</text>`; + } + + // Nuanced bar (goes DOWN from center) + if (nuaCount > 0) { + const barH = (nuaCount / maxCount) * barMaxH; + svg += `<rect x="${cx - barW / 2}" y="${midY + 1}" width="${barW}" height="${barH}" rx="2" fill="var(--text-dim)" opacity="0.5"> + <title>${y} ${meta.nuanced}: ${nuaCount} claims, mean score ${nuaMean}%</title> + </rect>`; + svg += `<text x="${cx}" y="${midY + barH + 12}" text-anchor="middle" font-size="9" fill="var(--text-dim)">${nuaCount}</text>`; + svg += `<text x="${cx}" y="${midY + barH + 22}" text-anchor="middle" font-size="9" font-weight="600" fill="var(--text)">${nuaMean}%</text>`; + } + } + + return `<div style="margin:1rem 0"> + <p style="font-size:0.78rem;color:var(--text-dim);margin-bottom:0.5rem">Bar height = claim count. Numbers show count and mean methodology score. Blue bars up = positive claims, gray bars down = nuanced claims.</p> + <svg viewBox="0 0 ${w} ${h}" style="width:100%;max-width:${w}px">${svg}</svg> + </div>`; +} + function renderClaims(claims: TensionClaim[]): string { if (!claims.length) return '<p style="color:var(--text-dim);font-size:0.82rem">No claims in this category.</p>'; const sorted = [...claims].sort((a, b) => b.score - a.score).slice(0, 20); diff --git a/scripts/build-explorer-data.py b/scripts/build-explorer-data.py @@ -369,19 +369,19 @@ def build(): bucket = "positive" if any(k in ct for k in ["faster", "speedup", "improves", "increases", "gain"]) else "nuanced" tensions["productivity"][bucket].append({ "paper_id": paper_id, "claim": claim["claim"], - "supported": claim.get("supported", ""), "score": score_pct, + "supported": claim.get("supported", ""), "score": score_pct, "year": year, }) if any(k in ct for k in ["benchmark", "evaluation", "leaderboard", "swe-bench"]): bucket = "positive" if any(k in ct for k in ["state-of-the-art", "outperforms", "achieves", "best"]) else "nuanced" tensions["benchmarks"][bucket].append({ "paper_id": paper_id, "claim": claim["claim"], - "supported": claim.get("supported", ""), "score": score_pct, + "supported": claim.get("supported", ""), "score": score_pct, "year": year, }) if any(k in ct for k in ["agent", "autonomous", "multi-agent"]): bucket = "positive" if any(k in ct for k in ["solves", "achieves", "succeeds", "capable", "outperforms"]) else "nuanced" tensions["agents"][bucket].append({ "paper_id": paper_id, "claim": claim["claim"], - "supported": claim.get("supported", ""), "score": score_pct, + "supported": claim.get("supported", ""), "score": score_pct, "year": year, }) cat_scores_pct = {k: round(v * 100, 1) for k, v in cat_scores.items()}

Impressum · Datenschutz