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

Tension butterfly: single-hue gradient + quality-weighted balance line

Bars use single-hue intensity gradients instead of color categories:
- Positive claims: light-to-dark blue (pale = weak methodology)
- Nuanced claims: light-to-dark gray
Works in grayscale and for colorblind viewers.

Added dashed balance line across years: quality-weighted center of
gravity between positive and nuanced claims. Above center line means
optimism dominates (weighted by methodology), below means skepticism.
Shows how each tension's balance shifts over time.

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

Diffstat:
Mexplorer/src/views/tensions.ts | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 66 insertions(+), 20 deletions(-)

diff --git a/explorer/src/views/tensions.ts b/explorer/src/views/tensions.ts @@ -66,8 +66,24 @@ export async function renderTensions(app: HTMLElement) { }).join(''); } +// Single-hue gradient: lighter = weaker methodology, darker = stronger +function posBarColor(score: number): string { + // Blue family: light (#a0c0f0) at 20% → dark (#1a4080) at 80% + const t = Math.max(0, Math.min(1, (score - 20) / 60)); + const r = Math.round(160 - t * 134); + const g = Math.round(192 - t * 128); + const b = Math.round(240 - t * 112); + return `rgb(${r},${g},${b})`; +} + +function nuaBarColor(score: number): string { + // Gray family: light (#b0b0b0) at 20% → dark (#404040) at 80% + const t = Math.max(0, Math.min(1, (score - 20) / 60)); + const v = Math.round(176 - t * 112); + return `rgb(${v},${v},${v})`; +} + 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[]>(); @@ -90,9 +106,8 @@ function renderButterfly(positive: TensionClaim[], nuanced: TensionClaim[], meta 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 pad = { l: 15, r: 15, t: 30, b: 30 }; const chartW = w - pad.l - pad.r; const chartH = h - pad.t - pad.b; const midY = pad.t + chartH / 2; @@ -104,47 +119,78 @@ function renderButterfly(positive: TensionClaim[], nuanced: TensionClaim[], meta // 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>`; + // Side labels at edges + svg += `<text x="${pad.l + 4}" y="${pad.t + 8}" font-size="10" fill="var(--text-dim)">\u2191 ${meta.positive}</text>`; + svg += `<text x="${pad.l + 4}" y="${h - pad.b - 2}" font-size="10" fill="var(--text-dim)">\u2193 ${meta.nuanced}</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 barW = colW * 0.6; 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; + const posMean = posC.length ? posC.reduce((s, c) => s + c.score, 0) / posC.length : 0; + const nuaMean = nuaC.length ? 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) + // Positive bar (UP) — blue gradient by score 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> + svg += `<rect x="${cx - barW / 2}" y="${midY - barH - 1}" width="${barW}" height="${barH}" rx="2" fill="${posBarColor(posMean)}"> + <title>${y} ${meta.positive}: ${posCount} claims, mean score ${Math.round(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>`; + svg += `<text x="${cx}" y="${midY - barH - 4}" text-anchor="middle" font-size="9" fill="var(--text)">${posCount} \u00b7 ${Math.round(posMean)}%</text>`; } - // Nuanced bar (goes DOWN from center) + // Nuanced bar (DOWN) — gray gradient by score 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> + svg += `<rect x="${cx - barW / 2}" y="${midY + 1}" width="${barW}" height="${barH}" rx="2" fill="${nuaBarColor(nuaMean)}"> + <title>${y} ${meta.nuanced}: ${nuaCount} claims, mean score ${Math.round(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>`; + svg += `<text x="${cx}" y="${midY + barH + 12}" text-anchor="middle" font-size="9" fill="var(--text)">${nuaCount} \u00b7 ${Math.round(nuaMean)}%</text>`; + } + } + + // Quality-weighted balance line: for each year, compute + // (positive_count * positive_mean - nuanced_count * nuanced_mean) / total_count + // Positive values = optimism dominates, negative = skepticism dominates + // Scale: maps to y position between top and bottom of chart + const balancePoints: string[] = []; + for (let i = 0; i < sortedYears.length; i++) { + const y = sortedYears[i]; + const cx = pad.l + i * colW + colW / 2; + const posC = posByYear.get(y) || []; + const nuaC = nuaByYear.get(y) || []; + if (posC.length + nuaC.length === 0) continue; + + const posMean = posC.length ? posC.reduce((s, c) => s + c.score, 0) / posC.length : 0; + const nuaMean = nuaC.length ? nuaC.reduce((s, c) => s + c.score, 0) / nuaC.length : 0; + // Weighted balance: positive pushes up, nuanced pushes down, weighted by mean score + const posWeight = posC.length * posMean; + const nuaWeight = nuaC.length * nuaMean; + const totalWeight = posWeight + nuaWeight; + // Balance from -1 (all nuanced) to +1 (all positive) + const balance = totalWeight > 0 ? (posWeight - nuaWeight) / totalWeight : 0; + // Map to y: +1 → top, -1 → bottom, 0 → midY + const lineY = midY - balance * barMaxH * 0.8; + balancePoints.push(`${cx},${lineY}`); + } + + if (balancePoints.length >= 2) { + svg += `<polyline points="${balancePoints.join(' ')}" fill="none" stroke="var(--text)" stroke-width="2" stroke-dasharray="6,3" opacity="0.6"/>`; + for (const pt of balancePoints) { + const [px, py] = pt.split(','); + svg += `<circle cx="${px}" cy="${py}" r="3" fill="var(--text)" opacity="0.6"/>`; } } 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> + <p style="font-size:0.78rem;color:var(--text-dim);margin-bottom:0.5rem">Bar height = claim count. Darker = higher methodology score. Dashed line = quality-weighted balance (above center = optimism dominates, below = skepticism). A tall pale bar means many claims with weak methodology.</p> <svg viewBox="0 0 ${w} ${h}" style="width:100%;max-width:${w}px">${svg}</svg> </div>`; }

Impressum · Datenschutz