seonglae's picture
Interactive research article
9e2b9b8
Raw History Blame Contribute Delete
138 kB
import { useState, useEffect, useMemo, useCallback, useRef, lazy, Suspense, Fragment } from 'react'
import {
BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer, Cell, Legend,
} from 'recharts'
import { cn, fmt, fmtInt, pct, compression } from './lib/utils'
const LandingView = lazy(() => import('./LandingView'))
// ---------- Types ----------
interface Baseline {
method: string
label: string
trainFitness: number
testFitness: number
states: number | null
transitions: number | null
randomAcceptRate: number | null
permutedAcceptRate: number | null
pmPrecision: number | null
category: string
note: string
}
interface TraceStep {
idx: number
role: string
activity: string
fromState: string
toState: string | null
consumed: boolean
}
interface Trace {
id: string
split: 'train' | 'test'
success: boolean | null
fullFitness: number | null
successFitness: number | null
steps: TraceStep[]
stateSequence: string[]
fitness: number
consumed: number
total: number
firstFailIdx: number
stateVisits: Record<string, number>
}
interface Dataset {
id: string
meta: {
name: string
shortName: string
venue: string
traces: number
trainSize: number
testSize: number
activities: number
description: string
}
baselines: Baseline[]
traces: Trace[]
convergence: {
stateConvergenceAt: number
transitionConvergenceAt: number
fitnessConvergenceAt: number
curve: Array<{ n: number; states: number; transitions: number; fitness: number }>
}
downstream: {
fitnessProfile: {
mean: number; std: number; min: number; max: number
anomalyCount: number; anomalies: string[]
histogram: Array<{ min: number; max: number; count: number }>
} | null
behavioralProfiling: {
stateVisitDistribution: Array<{ state: string; count: number; fraction: number }>
transitionFrequency: Array<{ transition: string; count: number; fraction: number }>
deadTransitions: string[]
transitionCoverage: number
avgUniqueStatesPerTrace: number
} | null
structuralAnalysis: {
density: number
hubs: Array<{ id: string; outDegree: number; inDegree: number }>
sinks: string[]
sources: string[]
selfLoops: string[]
nonTrivialSCCs: string[][]
} | null
differentialFSM: {
successOnlyTransitions: string[]
failureOnlyTransitions: string[]
sharedTransitions: string[]
} | null
failurePrediction: Record<string, unknown> | null
mistakeLocalization: Record<string, unknown> | null
neuralProbe: {
seq_mlp: { cv: number; std: number; holdout: number }
fsm_mlp: { cv: number; std: number; holdout: number }
seq_gru: { cv: number; std: number; holdout: number }
fsm_gru: { cv: number; std: number; holdout: number }
seq_transf: { cv: number; std: number; holdout: number }
fsm_transf: { cv: number; std: number; holdout: number }
} | null
workflowMemory: {
n: number
noMemory: { accuracy: number; correct: number }
awm: { accuracy: number; correct: number }
fsm: { accuracy: number; correct: number }
fsmGain: number
} | null
crossModel: {
models: Array<{ name: string; shortName: string; traces: number; successRate: number }>
matrices: {
fitness: number[][]
auroc: number[][]
stateCount: number[][]
}
summary: {
diagonalAUROC: { mean: number; std: number }
offDiagonalAUROC: { mean: number; std: number }
aurocTransferGap: number
}
} | null
monitor: {
trainSize: number; testSize: number; succSize: number; failSize: number
configs: Array<{
config: string; gamma: number; window: number
monitoringCurve: Array<{ point: number; threshold: number; f1: number; precision: number; recall: number }>
}>
baselines: Record<string, { auroc: number; curve?: Array<{ point: number; f1: number }> }>
} | null
counterfactual: {
nSuccess: number; nFailure: number
decisionPoints: Array<{ state: string; branching: number; targets: string[] }>
pathDiversity: { uniqueSuccPaths: number; uniqueFailPaths: number; overlap: number }
dpDivergence: Array<{ state: string; jsd: number }>
editDistance: { meanInternal?: number; meanCross?: number } | null
} | null
}
multiseed: {
numSeeds: number
ourFSM: { testFitness: { mean: number; std: number }; states: { mean: number; std: number } }
rpni: { testFitness: { mean: number; std: number }; states: { mean: number; std: number } }
awm: { testFitness: { mean: number; std: number } }
} | null
graph: {
states: Array<{ id: string }>
transitions: Array<{ source: string; target: string; subject: string }>
}
}
type ViewId = 'landing' | 'overview' | 'baselines' | 'convergence' | 'graph' | 'traces' | 'downstream' | 'failure' | 'memory' | 'crossmodel' | 'monitor' | 'counterfactual' | 'about'
// All datasets (passed into sub-views that need cross-dataset comparisons)
type AllDatasets = Dataset[]
// ---------- Palette (warm, no green/purple) ----------
const PALETTE = {
rust: '#c4553a',
slate: '#3d5a80',
ochre: '#c49a3a',
charcoal: '#2d2926',
clay: '#a66e4e',
iron: '#5c6b73',
teal: '#2a8f82',
}
const METHOD_COLORS: Record<string, string> = {
stateGraph: PALETTE.rust,
rpni: PALETTE.slate,
heuristic: PALETTE.ochre,
inductive: PALETTE.clay,
alpha: PALETTE.iron,
awm: PALETTE.charcoal,
awm_all: '#7a7068',
}
function methodLabel(b: Baseline): string {
if (b.method === 'stateGraph') return 'Agent State Graph'
return b.label
}
// ---------- Chart theme ----------
const CHART_GRID = '#d5cfc5'
const CHART_TICK = { fill: '#8a8478', fontSize: 11, fontFamily: 'IBM Plex Mono, monospace' }
const CHART_AXIS = { stroke: '#d5cfc5' }
const CHART_TOOLTIP = {
contentStyle: {
background: '#f5f0e8',
border: '1px solid #d5cfc5',
borderRadius: 0,
fontSize: 12,
fontFamily: 'IBM Plex Mono, monospace',
boxShadow: '2px 2px 0 rgba(0,0,0,0.04)',
},
}
// ---------- URL State ----------
const VALID_TABS: ViewId[] = ['landing', 'overview', 'baselines', 'convergence', 'graph', 'traces', 'downstream', 'failure', 'memory', 'crossmodel', 'monitor', 'counterfactual', 'about']
function readParams(): { dataset?: string; tab?: ViewId; trace?: string; method?: string } {
const p = new URLSearchParams(window.location.search)
const tab = p.get('tab') as ViewId | null
return {
dataset: p.get('dataset') || undefined,
tab: tab && VALID_TABS.includes(tab) ? tab : undefined,
trace: p.get('trace') || undefined,
method: p.get('method') || undefined,
}
}
function pushParams(params: Record<string, string>) {
const p = new URLSearchParams(window.location.search)
for (const [k, v] of Object.entries(params)) p.set(k, v)
const url = `${window.location.pathname}?${p.toString()}`
window.history.pushState(null, '', url)
}
// ---------- Main App ----------
export default function App() {
const [datasets, setDatasets] = useState<Dataset[]>([])
const [activeDs, setActiveDs] = useState(0)
const [view, setView] = useState<ViewId>(() => {
const tab = new URLSearchParams(window.location.search).get('tab') as ViewId | null
return tab && VALID_TABS.includes(tab) ? tab : 'landing'
})
const [selectedTrace, setSelectedTrace] = useState<string | null>(null)
const [selectedMethod, setSelectedMethod] = useState<string | null>(null)
const [loadProgress, setLoadProgress] = useState(0)
// On load: fetch slim index first (~3MB), then load per-dataset shards in parallel.
// Falls back to monolithic data.json if index isn't published yet.
// Paths are base-relative (import.meta.env.BASE_URL) so the dashboard works both at
// the dev root and when served under /article/asg/browser/ in production.
useEffect(() => {
const BASE = import.meta.env.BASE_URL
const loadShards = async (): Promise<Dataset[]> => {
const idxRes = await fetch(`${BASE}index.json`)
if (!idxRes.ok) throw new Error('no-index')
const index = await idxRes.json() as Array<{ id: string }>
const shards = await Promise.all(index.map(async (entry, i) => {
const r = await fetch(`${BASE}datasets/${entry.id}.json`)
const ds = await r.json() as Dataset
setLoadProgress(Math.round(((i + 1) / index.length) * 100))
return ds
}))
return shards
}
const loadMonolith = async (): Promise<Dataset[]> => {
const r = await fetch(`${BASE}data.json`)
setLoadProgress(50)
const data = await r.json() as Dataset[]
setLoadProgress(100)
return data
}
loadShards()
.catch(() => loadMonolith())
.then(data => {
setDatasets(data)
setLoadProgress(100)
const { dataset, tab, trace, method } = readParams()
if (dataset) {
const idx = data.findIndex(d => d.id === dataset)
if (idx >= 0) setActiveDs(idx)
}
if (tab) setView(tab)
if (trace) setSelectedTrace(trace)
if (method) setSelectedMethod(method)
})
}, [])
// Sync URL when state changes
const navigate = useCallback((dsIndex: number, tab: ViewId, data?: Dataset[]) => {
const ds = (data || datasets)[dsIndex]
if (ds) pushParams({ dataset: ds.id, tab })
}, [datasets])
const handleSetDataset = useCallback((i: number) => {
setActiveDs(i)
setSelectedTrace(null)
setSelectedMethod(null)
// Keep current tab when switching datasets
setView(prev => {
navigate(i, prev)
return prev
})
}, [navigate])
const handleSetView = useCallback((v: ViewId) => {
setView(v)
setSelectedTrace(null)
setSelectedMethod(null)
const p = new URLSearchParams(window.location.search)
p.set('tab', v)
p.delete('trace')
p.delete('method')
window.history.pushState(null, '', `${window.location.pathname}?${p.toString()}`)
}, [])
const handleSelectTrace = useCallback((traceId: string | null) => {
setSelectedTrace(traceId)
const p = new URLSearchParams(window.location.search)
if (traceId) p.set('trace', traceId); else p.delete('trace')
window.history.pushState(null, '', `${window.location.pathname}?${p.toString()}`)
}, [])
const handleSelectMethod = useCallback((method: string | null) => {
setSelectedMethod(method)
const p = new URLSearchParams(window.location.search)
if (method) p.set('method', method); else p.delete('method')
window.history.pushState(null, '', `${window.location.pathname}?${p.toString()}`)
}, [])
// Handle browser back/forward
useEffect(() => {
const onPop = () => {
const { dataset, tab, trace, method } = readParams()
if (dataset && datasets.length) {
const idx = datasets.findIndex(d => d.id === dataset)
if (idx >= 0) setActiveDs(idx)
}
if (tab) setView(tab)
setSelectedTrace(trace || null)
setSelectedMethod(method || null)
}
window.addEventListener('popstate', onPop)
return () => window.removeEventListener('popstate', onPop)
}, [datasets])
if (!datasets.length) {
return (
<div className="flex flex-col items-center justify-center h-screen gap-3">
<div className="text-ink-3 text-sm serif italic">Loading...</div>
{loadProgress > 0 && (
<div className="w-48">
<div className="h-1 bg-rule rounded-full overflow-hidden">
<div className="h-full bg-rust transition-all duration-200" style={{ width: `${loadProgress}%` }} />
</div>
</div>
)}
</div>
)
}
const ds = datasets[activeDs]
const dashboardTabs: [ViewId, string][] = [
['overview', 'Overview'],
['baselines', 'Baselines'],
['convergence', 'Convergence'],
['graph', 'Graph'],
['traces', 'Traces'],
['downstream', 'Downstream'],
['failure', 'Failure'],
['memory', 'Memory'],
['crossmodel', 'Cross-Model'],
['monitor', 'Monitor'],
['counterfactual', 'Counterfactual'],
['about', 'About'],
]
// Landing page: full-screen, no header/nav
if (view === 'landing') {
return (
<Suspense fallback={
<div className="flex flex-col items-center justify-center h-screen gap-3">
<div className="text-ink-3 text-sm serif italic">Loading...</div>
<div className="w-48">
<div className="h-1 bg-rule rounded-full overflow-hidden">
<div className="h-full bg-rust rounded-full" style={{ width: '100%', animation: 'pulse 1.5s ease-in-out infinite' }} />
</div>
</div>
</div>
}>
<LandingView
ds={ds}
datasets={datasets}
onSelectDataset={handleSetDataset}
onEnterDashboard={() => handleSetView('overview')}
/>
</Suspense>
)
}
return (
<div className="min-h-screen">
{/* Masthead */}
<header className="thick-rule">
<div className="max-w-[1200px] mx-auto px-4 sm:px-8 pt-6 pb-5">
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<button onClick={() => handleSetView('landing')} className="hover:opacity-70 transition-opacity">
<h1 className="serif text-[28px] leading-none tracking-tight">
Agent State Graph
</h1>
</button>
<p className="text-ink-3 text-[12px] mt-1.5 tracking-wide uppercase">
Experiment Results
</p>
</div>
<div className="flex items-center gap-0 border border-rule overflow-x-auto min-w-0">
{datasets.map((d, i) => (
<button
key={d.id}
onClick={() => handleSetDataset(i)}
className={cn(
'px-4 py-1.5 text-[12px] tracking-wide transition-colors whitespace-nowrap shrink-0',
'border-r border-rule last:border-r-0',
activeDs === i
? 'bg-ink text-paper font-medium'
: 'text-ink-2 hover:bg-paper-warm'
)}
>
{d.meta.shortName}
</button>
))}
</div>
</div>
</div>
</header>
{/* Nav */}
<nav className="ruled bg-paper-warm/50 overflow-x-auto">
<div className="max-w-[1200px] mx-auto px-4 sm:px-8 flex gap-0 min-w-0">
{dashboardTabs.map(([id, label]) => (
<button
key={id}
onClick={() => handleSetView(id)}
className={cn(
'px-5 py-2.5 text-[12px] uppercase tracking-widest transition-colors relative whitespace-nowrap shrink-0',
view === id
? 'text-ink font-medium'
: 'text-ink-3 hover:text-ink-2'
)}
>
{label}
{view === id && (
<span className="absolute bottom-0 left-5 right-5 h-[2px] bg-rust" />
)}
</button>
))}
</div>
</nav>
{/* Content */}
<main className="max-w-[1200px] mx-auto px-4 sm:px-8 py-6 sm:py-8">
{view === 'overview' && <OverviewView ds={ds} datasets={datasets} onSelectDataset={handleSetDataset} />}
{view === 'baselines' && <BaselinesView ds={ds} datasets={datasets} selectedMethod={selectedMethod} onSelectMethod={handleSelectMethod} />}
{view === 'convergence' && <ConvergenceView ds={ds} datasets={datasets} />}
{view === 'graph' && <GraphView ds={ds} />}
{view === 'traces' && <TracesView ds={ds} selectedTrace={selectedTrace} onSelectTrace={handleSelectTrace} />}
{view === 'downstream' && <DownstreamView ds={ds} datasets={datasets} />}
{view === 'failure' && <FailureView ds={ds} datasets={datasets} />}
{view === 'memory' && <MemoryView ds={ds} datasets={datasets} />}
{view === 'crossmodel' && <CrossModelView ds={ds} datasets={datasets} />}
{view === 'monitor' && <MonitorView ds={ds} datasets={datasets} />}
{view === 'counterfactual' && <CounterfactualView ds={ds} datasets={datasets} />}
{view === 'about' && <AboutView datasets={datasets} />}
</main>
{/* Footer rule */}
<footer className="max-w-[1200px] mx-auto px-4 sm:px-8 pb-12">
<div className="ruled pt-4 text-[11px] text-ink-4 flex justify-between">
<span>Agent State Graph / ICML 2026 AIWILD Workshop</span>
<span className="mono">{new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'short' })}</span>
</div>
</footer>
</div>
)
}
// ---------- Overview ----------
function MiniGraph({ states, transitions }: { states: { id: string }[]; transitions: { source: string; target: string; subject: string }[] }) {
const initialPos = useMemo(() => computeLayout(states, transitions), [states, transitions])
const [pos, setPos] = useState(initialPos)
useEffect(() => setPos(initialPos), [initialPos])
const edgeSet = useMemo(() => {
const s = new Set<string>()
for (const t of transitions) s.add(`${t.source}|${t.target}`)
return s
}, [transitions])
// Zoom + pan
const [zoom, setZoom] = useState(1)
const [pan, setPan] = useState({ x: 0, y: 0 })
const containerDrag = useRef(false)
const containerLast = useRef({ x: 0, y: 0 })
// Node drag
const svgRef = useRef<SVGSVGElement>(null)
const dragNode = useRef<string | null>(null)
const toSVG = useCallback((clientX: number, clientY: number) => {
const svg = svgRef.current
if (!svg) return { x: 0, y: 0 }
const pt = svg.createSVGPoint()
pt.x = clientX; pt.y = clientY
const ctm = svg.getScreenCTM()
if (!ctm) return { x: 0, y: 0 }
const svgPt = pt.matrixTransform(ctm.inverse())
return { x: svgPt.x, y: svgPt.y }
}, [])
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault()
setZoom(z => Math.max(0.3, Math.min(5, z * (e.deltaY > 0 ? 0.9 : 1.1))))
}, [])
const handlePointerDown = useCallback((e: React.PointerEvent) => {
containerDrag.current = true
containerLast.current = { x: e.clientX, y: e.clientY }
}, [])
const handlePointerMove = useCallback((e: React.PointerEvent) => {
if (dragNode.current) {
const { x, y } = toSVG(e.clientX, e.clientY)
setPos(prev => ({ ...prev, [dragNode.current!]: { x, y } }))
return
}
if (!containerDrag.current) return
const dx = e.clientX - containerLast.current.x
const dy = e.clientY - containerLast.current.y
containerLast.current = { x: e.clientX, y: e.clientY }
setPan(p => ({ x: p.x + dx, y: p.y + dy }))
}, [toSVG])
const handlePointerUp = useCallback(() => {
containerDrag.current = false
dragNode.current = null
}, [])
const handleNodeDown = useCallback((e: React.PointerEvent, id: string) => {
e.stopPropagation()
dragNode.current = id
;(e.target as Element).setPointerCapture(e.pointerId)
}, [])
const handleDoubleClick = useCallback(() => {
setZoom(1); setPan({ x: 0, y: 0 })
}, [])
return (
<div className="mt-4 bg-paper-warm border border-rule p-4 overflow-hidden"
onWheel={handleWheel} onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerUp}
onDoubleClick={handleDoubleClick}
style={{ cursor: 'grab', touchAction: 'none' }}>
<div style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: 'center center' }}>
<svg ref={svgRef} viewBox="-20 -10 640 430" className="w-full" style={{ maxHeight: '360px' }}>
<defs>
<marker id="mg-a" viewBox="0 0 10 7" refX="0" refY="3.5" markerWidth="10" markerHeight="7" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L10,3.5 L0,7" fill="#b5afa5" />
</marker>
</defs>
{transitions.map((t, i) => {
const from = pos[t.source], to = pos[t.target]
if (!from || !to) return null
const al=10, r=14
if (t.source === t.target) {
const loopR=30, cx1=from.x-loopR, cy1=from.y-loopR*1.5, cx2=from.x+loopR, cy2=from.y-loopR*1.5
const sa=Math.atan2(cy1-from.y,cx1-from.x), ea=Math.atan2(cy2-from.y,cx2-from.x)
const epx=from.x+r*Math.cos(ea), epy=from.y+r*Math.sin(ea)
const tdx=epx-cx2, tdy=epy-cy2, td=Math.sqrt(tdx*tdx+tdy*tdy)
return <path key={i} d={`M${from.x+r*Math.cos(sa)},${from.y+r*Math.sin(sa)} C${cx1},${cy1} ${cx2},${cy2} ${epx-tdx/td*al},${epy-tdy/td*al}`}
fill="none" stroke="#b5afa5" strokeWidth={0.8} strokeOpacity={0.5} markerEnd="url(#mg-a)" />
}
const dx = to.x-from.x, dy = to.y-from.y, d = Math.sqrt(dx*dx+dy*dy), nx=dx/d, ny=dy/d
if (edgeSet.has(`${t.target}|${t.source}`)) {
const c=20, mx=(from.x+to.x)/2-ny*c, my=(from.y+to.y)/2+nx*c
const sa=Math.atan2(my-from.y,mx-from.x), ea=Math.atan2(my-to.y,mx-to.x)
const epx=to.x+r*Math.cos(ea), epy=to.y+r*Math.sin(ea)
const tdx=epx-mx, tdy=epy-my, td=Math.sqrt(tdx*tdx+tdy*tdy)
return <path key={i} d={`M${from.x+r*Math.cos(sa)},${from.y+r*Math.sin(sa)} Q${mx},${my} ${epx-tdx/td*al},${epy-tdy/td*al}`}
fill="none" stroke="#b5afa5" strokeWidth={0.8} strokeOpacity={0.5} markerEnd="url(#mg-a)" />
}
return <line key={i} x1={from.x+nx*r} y1={from.y+ny*r} x2={to.x-nx*(r+al)} y2={to.y-ny*(r+al)}
stroke="#b5afa5" strokeWidth={0.8} strokeOpacity={0.5} markerEnd="url(#mg-a)" />
})}
{states.map(s => {
const p = pos[s.id]
if (!p) return null
const isInit = s.id === 'init'
const label = s.id.length > 14 ? s.id.slice(0,12)+'..' : s.id
return (
<g key={s.id} style={{ cursor: 'grab' }} onPointerDown={(e) => handleNodeDown(e, s.id)}>
<circle cx={p.x} cy={p.y} r={14} fill="#fffdf9" stroke={isInit ? PALETTE.rust : '#b5afa5'} strokeWidth={isInit ? 2 : 1} />
{isInit && <circle cx={p.x} cy={p.y} r={10} fill="none" stroke={PALETTE.rust} strokeWidth={0.8} />}
<text x={p.x} y={p.y+24} textAnchor="middle" fill="#8a8478" fontSize="9" fontFamily="IBM Plex Mono, monospace">{label}</text>
</g>
)
})}
</svg>
</div>
<p className="text-[11px] text-ink-3 mt-2 text-center mono">
{states.length} states, {transitions.length} transitions
</p>
</div>
)
}
function OverviewView({ ds, datasets, onSelectDataset }: { ds: Dataset; datasets: Dataset[]; onSelectDataset: (i: number) => void }) {
const ours = ds.baselines.find(b => b.category === 'ours')!
const rpni = ds.baselines.find(b => b.method === 'rpni')!
return (
<div className="space-y-8">
{/* Title block */}
<div className="anim">
<div className="flex items-baseline gap-3">
<h2 className="serif text-[22px] italic">{ds.meta.name}</h2>
<span className="mono text-[11px] text-ink-3">{ds.meta.venue}</span>
</div>
<p className="text-[13px] text-ink-2 mt-1 max-w-xl">{ds.meta.description}</p>
</div>
{/* Key figures - newspaper style */}
<div className="anim d1 grid grid-cols-2 md:grid-cols-4 gap-0 thick-rule ruled-top">
<Figure label="Traces" value={ds.meta.traces.toString()} note={`${ds.meta.trainSize} / ${ds.meta.testSize}`} />
<Figure label="Test Fitness" value={fmt(ours.testFitness)} accent />
<Figure label="States" value={ours.states?.toString() || '–'} note={`vs ${rpni.states?.toLocaleString()} RPNI`} />
<Figure label="Compression" value={compression(ours.states || 1, rpni.states || 1)} accent note="vs RPNI" />
</div>
{/* Final graph preview */}
<div className="anim d2">
<SectionRule title="Learned Automaton" />
<MiniGraph states={ds.graph.states} transitions={ds.graph.transitions} />
</div>
{/* Cross-dataset table */}
<div className="anim d3">
<SectionRule title="Across Datasets" />
<table className="w-full text-[13px] mt-3">
<thead>
<tr className="thick-rule">
<th className="text-left py-2 pr-4 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Dataset</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">N</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">|A|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Fitness</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">ASG |Q|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">RPNI |Q|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Ratio</th>
<th className="text-right py-2 pl-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Rand. Rej.</th>
</tr>
</thead>
<tbody>
{datasets.map((d, i) => {
const o = d.baselines.find(b => b.category === 'ours')!
const r = d.baselines.find(b => b.method === 'rpni')!
const isActive = d.id === ds.id
return (
<tr
key={d.id}
onClick={() => onSelectDataset(i)}
className={cn('ruled cursor-pointer transition-colors', isActive ? 'bg-rust-dim' : 'hover:bg-ochre-dim')}
>
<td className="py-2.5 pr-4 font-medium">
{d.meta.name}
{isActive && <span className="ml-2 text-[10px] text-rust italic serif">current</span>}
</td>
<td className="py-2.5 px-3 text-right mono">{fmtInt(d.meta.traces)}</td>
<td className="py-2.5 px-3 text-right mono">{o.states ? o.states - 1 : '–'}</td>
<td className="py-2.5 px-3 text-right mono text-rust font-medium">{fmt(o.testFitness)}</td>
<td className="py-2.5 px-3 text-right mono font-medium">{o.states}</td>
<td className="py-2.5 px-3 text-right mono text-ink-3">{fmtInt(r.states || 0)}</td>
<td className="py-2.5 px-3 text-right mono">{compression(o.states || 1, r.states || 1)}</td>
<td className="py-2.5 pl-3 text-right mono text-rust">
{o.randomAcceptRate !== null ? pct(1 - o.randomAcceptRate) : '–'}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{/* Bar chart */}
<div className="anim d3">
<SectionRule title="Test Fitness by Method" />
<div className="mt-4 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={240}>
<BarChart data={ds.baselines.filter(b => b.testFitness > 0).map(b => ({ ...b, displayLabel: methodLabel(b) }))} barCategoryGap="16%">
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} vertical={false} />
<XAxis dataKey="displayLabel" tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} />
<YAxis domain={[0, 1.05]} tick={CHART_TICK} axisLine={false} tickLine={false} tickFormatter={(v: number) => v.toFixed(1)} />
<Tooltip {...CHART_TOOLTIP} formatter={(v: number | undefined) => [fmt(v ?? 0, 4), 'Fitness']} />
<Bar dataKey="testFitness" radius={0}>
{ds.baselines.filter(b => b.testFitness > 0).map((b) => (
<Cell key={b.method} fill={METHOD_COLORS[b.method] || PALETTE.iron} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
)
}
// ---------- Baselines ----------
function BaselinesView({ ds, datasets, selectedMethod, onSelectMethod }: {
ds: Dataset; datasets: AllDatasets; selectedMethod: string | null; onSelectMethod: (m: string | null) => void
}) {
const sorted = useMemo(() =>
[...ds.baselines].sort((a, b) => b.testFitness - a.testFitness),
[ds.baselines]
)
const ours = ds.baselines.find(b => b.category === 'ours')
const rpni = ds.baselines.find(b => b.method === 'rpni')
// Cross-dataset data for the selected method
const methodCrossData = useMemo(() => {
if (!selectedMethod) return null
return datasets.map(d => {
const b = d.baselines.find(b => b.method === selectedMethod)
return { dataset: d.meta.shortName, datasetName: d.meta.name, baseline: b || null }
})
}, [selectedMethod, datasets])
const selBaseline = selectedMethod ? ds.baselines.find(b => b.method === selectedMethod) : null
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Baseline Comparison</h2>
<p className="text-[13px] text-ink-2 mt-1">
All methods evaluated on the same 80/20 train/test split with identical activity extraction.
{' '}<span className="text-ink-4">Click a method for cross-dataset comparison.</span>
</p>
</div>
<div className="anim d1 overflow-x-auto">
<table className="w-full text-[13px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-2 pr-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-[200px]">Method</th>
<th className="text-left py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-[90px]">Type</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Test Fit.</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">|Q|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">|delta|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Rand.</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Perm.</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">PM Prec.</th>
<th className="text-right py-2 pl-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Train Fit.</th>
</tr>
</thead>
<tbody>
{sorted.map((b) => {
const isSelected = selectedMethod === b.method
return (
<tr
key={b.method}
onClick={() => onSelectMethod(isSelected ? null : b.method)}
className={cn(
'ruled cursor-pointer transition-colors',
isSelected ? 'bg-slate-dim' : b.category === 'ours' ? 'bg-rust-dim hover:bg-ochre-dim' : 'hover:bg-ochre-dim'
)}
>
<td className="py-2.5 pr-3">
<div className="flex items-center gap-2.5">
<span
className="w-[3px] h-[14px] shrink-0"
style={{ background: METHOD_COLORS[b.method] || PALETTE.iron }}
/>
<span className={cn('font-medium', b.category === 'ours' && 'text-rust')}>
{methodLabel(b)}
</span>
{isSelected && <span className="text-[9px] text-slate serif italic ml-1">selected</span>}
</div>
</td>
<td className="py-2.5 px-3 text-[11px] text-ink-3 uppercase tracking-wider">
{b.category === 'ours' ? 'Ours' : b.category === 'process-mining' ? 'PM' : b.category}
</td>
<td className={cn(
'py-2.5 px-3 text-right mono font-medium',
b.testFitness >= 0.99 ? 'text-ink' : b.testFitness >= 0.5 ? 'text-ink-2' : 'text-rust'
)}>
{fmt(b.testFitness)}
</td>
<td className="py-2.5 px-3 text-right mono">
{b.states !== null ? fmtInt(b.states) : '–'}
</td>
<td className="py-2.5 px-3 text-right mono">
{b.transitions !== null ? fmtInt(b.transitions) : '–'}
</td>
<td className="py-2.5 px-3 text-right mono">
{b.randomAcceptRate !== null ? pct(b.randomAcceptRate) : '–'}
</td>
<td className="py-2.5 px-3 text-right mono">
{b.permutedAcceptRate !== null ? pct(b.permutedAcceptRate) : '–'}
</td>
<td className={cn(
'py-2.5 px-3 text-right mono',
b.pmPrecision !== null && b.pmPrecision < 0.5 ? 'text-rust' : ''
)}>
{b.pmPrecision !== null ? fmt(b.pmPrecision) : '–'}
</td>
<td className="py-2.5 pl-3 text-right mono text-ink-3">{fmt(b.trainFitness)}</td>
</tr>
)
})}
</tbody>
</table>
</div>
{/* Method detail panel */}
{selectedMethod && selBaseline && methodCrossData && (
<div className="anim border border-rule bg-paper-warm p-6 space-y-4">
<div className="flex items-baseline justify-between">
<div className="flex items-center gap-3">
<span className="w-[4px] h-[18px] shrink-0" style={{ background: METHOD_COLORS[selectedMethod] || PALETTE.iron }} />
<h3 className="serif text-[18px] italic">{methodLabel(selBaseline)}</h3>
<span className="text-[11px] text-ink-3 uppercase tracking-wider">{selBaseline.category}</span>
</div>
<button onClick={() => onSelectMethod(null)} className="text-[11px] text-ink-3 hover:text-ink transition-colors uppercase tracking-wider">
Close
</button>
</div>
{selBaseline.note && (
<p className="text-[12px] text-ink-3 mono">{selBaseline.note}</p>
)}
<SectionRule title="Cross-Dataset Comparison" />
<table className="w-full text-[13px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-2 pr-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Dataset</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Test Fit.</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Train Fit.</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">|Q|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">|delta|</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Rand.</th>
<th className="text-right py-2 pl-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Perm.</th>
</tr>
</thead>
<tbody>
{methodCrossData.map(({ dataset, datasetName, baseline: b }) => (
<tr key={dataset} className="ruled">
<td className="py-2 pr-3 font-medium">{datasetName}</td>
{b ? (
<>
<td className="py-2 px-3 text-right mono font-medium">{fmt(b.testFitness)}</td>
<td className="py-2 px-3 text-right mono text-ink-3">{fmt(b.trainFitness)}</td>
<td className="py-2 px-3 text-right mono">{b.states !== null ? fmtInt(b.states) : '–'}</td>
<td className="py-2 px-3 text-right mono">{b.transitions !== null ? fmtInt(b.transitions) : '–'}</td>
<td className="py-2 px-3 text-right mono">{b.randomAcceptRate !== null ? pct(b.randomAcceptRate) : '–'}</td>
<td className="py-2 pl-3 text-right mono">{b.permutedAcceptRate !== null ? pct(b.permutedAcceptRate) : '–'}</td>
</>
) : (
<td colSpan={6} className="py-2 px-3 text-ink-4 text-center">Not available</td>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Observations */}
<div className="anim d2 grid grid-cols-1 sm:grid-cols-3 gap-6">
<Observation
num="i"
title="Fitness alone is insufficient"
body={`With |A| = ${(ds.baselines.find(b => b.category === 'ours')?.states ?? 1) - 1}, most methods achieve near-perfect replay fitness, so the alphabet alone rarely discriminates on fitness.`}
/>
<Observation
num="ii"
title="Precision reveals flower models"
body="PM4Py miners show 0.20-0.49 precision despite 1.0 fitness. They accept nearly any symbol sequence."
/>
<Observation
num="iii"
title={`${compression(ours?.states || 1, rpni?.states || 1)} compression`}
body={`Our ${ours?.states}-state FSM captures equivalent behavior to RPNI's ${rpni?.states?.toLocaleString()} states with no fitness loss.`}
/>
</div>
</div>
)
}
// ---------- Convergence ----------
function ConvergenceView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
// Not every dataset has an incremental-convergence run; fall back to one that does
// so the tab always shows a real curve instead of an empty placeholder.
const hasOwn = ds.convergence.curve.length > 0
const active = hasOwn ? ds : (datasets.find(d => (d.convergence?.curve?.length ?? 0) > 0) ?? ds)
const conv = active.convergence
const curve = conv.curve.map((p: { n: number; states: number; transitions: number; fitness: number }) => ({
...p,
transPerState: p.states > 0 ? +(p.transitions / p.states).toFixed(2) : 0,
}))
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Convergence</h2>
<p className="text-[13px] text-ink-2 mt-1">
Structure stabilization as training traces increase.
</p>
{!hasOwn && active !== ds && (
<p className="text-[12px] text-ink-3 mt-2 bg-paper-warm border border-rule px-3 py-2">
{ds.meta.name} has no incremental-convergence run; showing <span className="font-medium">{active.meta.name}</span>. The paper's convergence analysis covers SWE-agent, SWE-smith, Mind2Web, and Who&amp;When.
</p>
)}
</div>
{curve.length > 0 ? (
<div className="anim d1 grid grid-cols-1 sm:grid-cols-3 gap-0 thick-rule ruled-top">
<Figure
label="State convergence"
value={`${conv.stateConvergenceAt}`}
note={`${((conv.stateConvergenceAt / active.meta.trainSize) * 100).toFixed(0)}% of train`}
/>
<Figure
label="Transition conv."
value={`${conv.transitionConvergenceAt}`}
note={`${((conv.transitionConvergenceAt / active.meta.trainSize) * 100).toFixed(0)}% of train`}
/>
<Figure
label="Fitness conv."
value={`${conv.fitnessConvergenceAt}`}
note={`${((conv.fitnessConvergenceAt / active.meta.trainSize) * 100).toFixed(0)}% of train`}
accent
/>
</div>
) : (
<div className="anim d1 bg-paper-warm border border-rule p-5 text-[13px] text-ink-2">
Incremental-convergence data is not available for this dataset. See SWE-agent,
SWE-smith, Mind2Web and Who&amp;When in the paper for the convergence analysis.
</div>
)}
{curve.length > 0 && (
<>
<div className="anim d2">
<SectionRule title="Structure Evolution" />
<div className="mt-4 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={320}>
<LineChart data={curve}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} />
<XAxis dataKey="n" tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} label={{ value: 'Training traces', position: 'insideBottom', offset: -5, style: { fontSize: 10, fill: '#8a8478' } }} />
<YAxis yAxisId="count" tick={{ ...CHART_TICK, fill: PALETTE.rust }} axisLine={false} tickLine={false} domain={['dataMin - 1', 'auto']} />
<YAxis yAxisId="fitness" orientation="right" tick={{ ...CHART_TICK, fill: PALETTE.teal }} axisLine={false} tickLine={false} domain={['dataMin - 0.005', 1.002]} tickFormatter={(v: number) => v.toFixed(3)} />
<Tooltip {...CHART_TOOLTIP} />
<Line yAxisId="count" type="stepAfter" dataKey="states" stroke={PALETTE.rust} strokeWidth={2.5} dot={{ r: 2.5, fill: PALETTE.rust, stroke: 'none' }} activeDot={{ r: 4 }} name="States" />
<Line yAxisId="count" type="stepAfter" dataKey="transPerState" stroke={PALETTE.slate} strokeWidth={2} dot={{ r: 2, fill: PALETTE.slate, stroke: 'none' }} activeDot={{ r: 4 }} name="Trans/State" strokeDasharray="6 3" />
<Line yAxisId="fitness" type="monotone" dataKey="fitness" stroke={PALETTE.teal} strokeWidth={2} dot={{ r: 2, fill: PALETTE.teal, stroke: 'none' }} activeDot={{ r: 4 }} name="Test Fitness" />
</LineChart>
</ResponsiveContainer>
<div className="flex gap-6 justify-center mt-3 text-[11px] text-ink-3">
<span className="flex items-center gap-1.5">
<span className="w-3 h-[2px] bg-rust inline-block" /> States
</span>
<span className="flex items-center gap-1.5">
<span className="w-3 h-[2px] bg-slate inline-block" /> Trans/State
</span>
<span className="flex items-center gap-1.5">
<span className="w-3 h-[2px] inline-block" style={{ background: PALETTE.teal }} /> Fitness
</span>
</div>
</div>
</div>
<div className="anim d3">
<SectionRule title="Test Fitness (detail)" />
<div className="mt-4 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={200}>
<LineChart data={curve}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} />
<XAxis dataKey="n" tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} />
<YAxis domain={['dataMin - 0.005', 1.002]} tick={CHART_TICK} axisLine={false} tickLine={false} tickFormatter={(v: number) => v.toFixed(3)} />
<Tooltip {...CHART_TOOLTIP} formatter={(v: number | undefined) => [fmt(v ?? 0, 4), 'Fitness']} />
<Line type="monotone" dataKey="fitness" stroke={PALETTE.rust} strokeWidth={2} dot={{ r: 2.5, fill: PALETTE.rust, stroke: 'none' }} activeDot={{ r: 4 }} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
</>
)}
</div>
)
}
// ---------- Graph View ----------
function GraphView({ ds }: { ds: Dataset }) {
const { states, transitions } = ds.graph
const initialPositions = useMemo(() => computeLayout(states, transitions), [states, transitions])
const [nodePositions, setNodePositions] = useState(initialPositions)
useEffect(() => setNodePositions(initialPositions), [initialPositions])
const svgRef = useRef<SVGSVGElement>(null)
const dragNode = useRef<string | null>(null)
const [zoom, setZoom] = useState(1)
const [pan, setPan] = useState({ x: 0, y: 0 })
const containerDrag = useRef(false)
const containerLast = useRef({ x: 0, y: 0 })
const toSVG = useCallback((clientX: number, clientY: number) => {
const svg = svgRef.current
if (!svg) return { x: 0, y: 0 }
const pt = svg.createSVGPoint()
pt.x = clientX; pt.y = clientY
const ctm = svg.getScreenCTM()
if (!ctm) return { x: 0, y: 0 }
const svgPt = pt.matrixTransform(ctm.inverse())
return { x: svgPt.x, y: svgPt.y }
}, [])
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault()
setZoom(z => Math.max(0.3, Math.min(5, z * (e.deltaY > 0 ? 0.9 : 1.1))))
}, [])
const handleContainerDown = useCallback((e: React.PointerEvent) => {
containerDrag.current = true
containerLast.current = { x: e.clientX, y: e.clientY }
}, [])
const handleNodePointerDown = useCallback((e: React.PointerEvent, id: string) => {
e.stopPropagation()
dragNode.current = id
;(e.target as Element).setPointerCapture(e.pointerId)
}, [])
const handlePointerMove = useCallback((e: React.PointerEvent) => {
if (dragNode.current) {
const { x, y } = toSVG(e.clientX, e.clientY)
setNodePositions(prev => ({ ...prev, [dragNode.current!]: { x, y } }))
return
}
if (!containerDrag.current) return
const dx = e.clientX - containerLast.current.x
const dy = e.clientY - containerLast.current.y
containerLast.current = { x: e.clientX, y: e.clientY }
setPan(p => ({ x: p.x + dx, y: p.y + dy }))
}, [toSVG])
const handlePointerUp = useCallback(() => { dragNode.current = null; containerDrag.current = false }, [])
const handleDoubleClick = useCallback(() => { setZoom(1); setPan({ x: 0, y: 0 }) }, [])
// Build edge index for detecting bidirectional pairs
const edgeSet = useMemo(() => {
const s = new Set<string>()
for (const t of transitions) s.add(`${t.source}|${t.target}`)
return s
}, [transitions])
// Get transition frequencies from downstream data
const transFreq = useMemo(() => {
const freq = new Map<string, number>()
const bp = ds.downstream.behavioralProfiling
if (bp) {
let maxCount = 1
for (const tf of bp.transitionFrequency) {
const parts = tf.transition.split('\u2192')
if (parts.length === 2) {
freq.set(`${parts[0]}|${parts[1]}`, tf.count)
maxCount = Math.max(maxCount, tf.count)
}
}
// Normalize to 0-1
for (const [k, v] of freq) freq.set(k, v / maxCount)
}
return freq
}, [ds.downstream.behavioralProfiling])
// State visit counts for node sizing
const stateVisits = useMemo(() => {
const visits = new Map<string, number>()
const bp = ds.downstream.behavioralProfiling
if (bp) {
let maxCount = 1
for (const sv of bp.stateVisitDistribution) {
visits.set(sv.state, sv.count)
maxCount = Math.max(maxCount, sv.count)
}
for (const [k, v] of visits) visits.set(k, v / maxCount)
}
return visits
}, [ds.downstream.behavioralProfiling])
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Learned Automaton</h2>
<p className="text-[13px] text-ink-2 mt-1">
<span className="mono">{states.length}</span> states, <span className="mono">{transitions.length}</span> transitions.
Built from {ds.meta.trainSize} training traces.
{transFreq.size > 0 && ' Edge thickness shows relative frequency.'}
</p>
</div>
<div className="anim d1">
<div className="bg-paper-warm border border-rule p-6 overflow-hidden"
onWheel={handleWheel} onPointerDown={handleContainerDown}
onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerUp}
onDoubleClick={handleDoubleClick}
style={{ cursor: 'grab', touchAction: 'none' }}>
<div style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: 'center center' }}>
<svg ref={svgRef} viewBox="-20 -10 640 430" className="w-full" style={{ maxHeight: '520px' }}>
<defs>
<marker id="arrow" viewBox="0 0 10 7" refX="0" refY="3.5" markerWidth="10" markerHeight="7" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L10,3.5 L0,7" fill="#8a8478" />
</marker>
<marker id="arrow-rust" viewBox="0 0 10 7" refX="0" refY="3.5" markerWidth="10" markerHeight="7" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L10,3.5 L0,7" fill={PALETTE.rust} />
</marker>
</defs>
{/* Edges */}
{transitions.map((t, i) => {
const from = nodePositions[t.source]
const to = nodePositions[t.target]
if (!from || !to) return null
const freq = transFreq.get(`${t.source}|${t.target}`) ?? 0.1
const sw = 0.5 + freq * 2.5 // stroke width 0.5-3
const opacity = 0.3 + freq * 0.7
const al = 10
if (t.source === t.target) {
const r = 14 + (stateVisits.get(t.source) || 0) * 6
const loopR = 35, cx1 = from.x - loopR, cy1 = from.y - loopR * 1.5
const cx2 = from.x + loopR, cy2 = from.y - loopR * 1.5
const sa = Math.atan2(cy1 - from.y, cx1 - from.x)
const ea = Math.atan2(cy2 - from.y, cx2 - from.x)
const epx = from.x + r * Math.cos(ea), epy = from.y + r * Math.sin(ea)
const tdx = epx - cx2, tdy = epy - cy2, td = Math.sqrt(tdx * tdx + tdy * tdy)
return (
<path key={i}
d={`M${from.x + r * Math.cos(sa)},${from.y + r * Math.sin(sa)} C${cx1},${cy1} ${cx2},${cy2} ${epx - tdx / td * al},${epy - tdy / td * al}`}
fill="none" stroke="#b5afa5" strokeWidth={sw} strokeOpacity={opacity} strokeLinecap="round"
markerEnd="url(#arrow)" />
)
}
const dx = to.x - from.x, dy = to.y - from.y
const dist = Math.sqrt(dx * dx + dy * dy)
const nx = dx / dist, ny = dy / dist
const isBidirectional = edgeSet.has(`${t.target}|${t.source}`)
const nodeR = 14 + (stateVisits.get(t.target) || 0) * 6
if (isBidirectional) {
const curve = 25
const mx = (from.x + to.x) / 2 - ny * curve
const my = (from.y + to.y) / 2 + nx * curve
const sa = Math.atan2(my - from.y, mx - from.x)
const ea = Math.atan2(my - to.y, mx - to.x)
const srcR = 14 + (stateVisits.get(t.source) || 0) * 6
const epx = to.x + nodeR * Math.cos(ea), epy = to.y + nodeR * Math.sin(ea)
const tdx = epx - mx, tdy = epy - my, td = Math.sqrt(tdx * tdx + tdy * tdy)
return (
<path key={i}
d={`M${from.x + srcR * Math.cos(sa)},${from.y + srcR * Math.sin(sa)} Q${mx},${my} ${epx - tdx / td * al},${epy - tdy / td * al}`}
fill="none" stroke="#b5afa5" strokeWidth={sw} strokeOpacity={opacity} strokeLinecap="round"
markerEnd="url(#arrow)" />
)
}
return (
<line key={i}
x1={from.x + nx * nodeR} y1={from.y + ny * nodeR}
x2={to.x - nx * (nodeR + al)} y2={to.y - ny * (nodeR + al)}
stroke="#b5afa5" strokeWidth={sw} strokeOpacity={opacity} strokeLinecap="round"
markerEnd="url(#arrow)" />
)
})}
{/* Nodes */}
{states.map((s) => {
const pos = nodePositions[s.id]
if (!pos) return null
const isInit = s.id === 'init'
const visitNorm = stateVisits.get(s.id) || 0
const r = isInit ? 13 : 14 + visitNorm * 6
const label = s.id.length > 20 ? s.id.slice(0, 18) + '..' : s.id
return (
<g key={s.id} style={{ cursor: 'grab' }}
onPointerDown={(e) => handleNodePointerDown(e, s.id)}>
<circle
cx={pos.x} cy={pos.y} r={r}
fill={isInit ? '#f5f0e8' : `rgba(196, 85, 58, ${0.04 + visitNorm * 0.12})`}
stroke={isInit ? PALETTE.rust : visitNorm > 0.5 ? PALETTE.rust : '#b5afa5'}
strokeWidth={isInit ? 2 : visitNorm > 0.5 ? 1.5 : 1}
/>
{isInit && (
<circle cx={pos.x} cy={pos.y} r={r - 3} fill="none" stroke={PALETTE.rust} strokeWidth={1} />
)}
<text
x={pos.x} y={pos.y + r + 12} textAnchor="middle"
fill="#4a4540" fontSize="8.5" fontFamily="IBM Plex Mono, monospace"
>
{label}
</text>
</g>
)
})}
</svg>
</div>
</div>
</div>
<div className="anim d2">
<SectionRule title="State Alphabet" />
<div className="flex flex-wrap gap-1.5 mt-3">
{states.map((s) => (
<span key={s.id} className="mono text-[11px] text-ink-2 px-2 py-0.5 rounded border border-rule bg-paper-warm whitespace-nowrap">
{s.id}
</span>
))}
</div>
</div>
{/* Transition table */}
<div className="anim d3">
<SectionRule title="Transition Table" />
<table className="w-full text-[12px] mt-3">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">From</th>
<th className="text-center py-1.5 px-1 text-[10px] text-ink-3 font-normal w-[20px]"></th>
<th className="text-left py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">To</th>
<th className="text-left py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Type</th>
</tr>
</thead>
<tbody>
{transitions.map((t, i) => (
<tr key={i} className="ruled">
<td className="py-1 pr-3 mono text-[11px]">{t.source}</td>
<td className="py-1 px-1 text-center text-ink-4">{'\u2192'}</td>
<td className="py-1 px-3 mono text-[11px]">{t.target}</td>
<td className="py-1 pl-3 text-[10px] text-ink-3">
{t.source === t.target ? 'self-loop' : t.subject || 'transition'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
// ---------- Traces View ----------
// Break a long trace ID into readable parts: model tag, persona, trailing index,
// and the remaining scenario core. Falls back gracefully for non-tau2 ids.
function parseTraceId(id: string, datasetId: string) {
let rest = id
if (rest.startsWith(datasetId + '-')) rest = rest.slice(datasetId.length + 1)
let index: string | null = null
const im = rest.match(/-(\d+)$/)
if (im) { index = im[1]; rest = rest.slice(0, rest.length - im[0].length) }
let model: string | null = null
// Only tau2-bench and OSWorld ids embed a model tag (model_scenario...); other
// datasets (sweagent repo__task, webarena-idx, uuids) have no model prefix.
if (datasetId.startsWith('tau2bench-') || datasetId === 'osworld') {
const um = rest.match(/^([^_[]+)_/)
if (um) { model = um[1].replace(/-\d{4}-\d{2}-\d{2}$/, ''); rest = rest.slice(um[0].length) }
}
let persona: string | null = null
const pm = rest.match(/\[PERSONA:([^\]]+)\]/i)
if (pm) { persona = pm[1]; rest = rest.slice(0, pm.index) + rest.slice((pm.index ?? 0) + pm[0].length) }
return { model, persona, index, rest: rest.replace(/^[-_\s]+|[-_\s]+$/g, '') }
}
function TracesView({ ds, selectedTrace, onSelectTrace }: {
ds: Dataset; selectedTrace: string | null; onSelectTrace: (id: string | null) => void
}) {
const [splitFilter, setSplitFilter] = useState<'all' | 'train' | 'test'>('all')
const [successFilter, setSuccessFilter] = useState<'all' | 'success' | 'failure'>('all')
const [search, setSearch] = useState('')
const [page, setPage] = useState(0)
const PAGE_SIZE = 50
// Reset page on filter change
useEffect(() => { setPage(0) }, [splitFilter, successFilter, search, ds.id])
const filtered = useMemo(() => {
let t = ds.traces
if (splitFilter !== 'all') t = t.filter(tr => tr.split === splitFilter)
if (successFilter === 'success') t = t.filter(tr => tr.success === true)
if (successFilter === 'failure') t = t.filter(tr => tr.success === false)
if (search) {
const q = search.toLowerCase()
t = t.filter(tr => tr.id.toLowerCase().includes(q))
}
return t
}, [ds.traces, splitFilter, successFilter, search])
const totalPages = Math.ceil(filtered.length / PAGE_SIZE)
const pageTraces = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
const trainCount = ds.traces.filter(t => t.split === 'train').length
const testCount = ds.traces.filter(t => t.split === 'test').length
const successCount = ds.traces.filter(t => t.success === true).length
const failureCount = ds.traces.filter(t => t.success === false).length
const unknownCount = ds.traces.filter(t => t.success === null).length
// Navigate to the page containing the selected trace
useEffect(() => {
if (selectedTrace) {
const idx = filtered.findIndex(t => t.id === selectedTrace)
if (idx >= 0) setPage(Math.floor(idx / PAGE_SIZE))
}
}, [selectedTrace, filtered])
return (
<div className="space-y-6">
<div className="anim">
<h2 className="serif text-[22px] italic">Traces</h2>
<p className="text-[13px] text-ink-2 mt-1">
{ds.traces.length} traces: {trainCount} train, {testCount} test.
{successCount > 0 && ` ${successCount} success, ${failureCount} failure.`}
{unknownCount > 0 && ` ${unknownCount} unlabeled.`}
{' '}<span className="text-ink-4">Click a trace for details.</span>
</p>
</div>
{/* Filters */}
<div className="anim d1 flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-0 border border-rule">
{(['all', 'train', 'test'] as const).map(v => (
<button
key={v}
onClick={() => setSplitFilter(v)}
className={cn(
'px-3 py-1 text-[11px] uppercase tracking-wider border-r border-rule last:border-r-0 transition-colors',
splitFilter === v ? 'bg-ink text-paper font-medium' : 'text-ink-3 hover:bg-paper-warm'
)}
>
{v === 'all' ? `All (${ds.traces.length})` : v === 'train' ? `Train (${trainCount})` : `Test (${testCount})`}
</button>
))}
</div>
{successCount + failureCount > 0 && (
<div className="flex items-center gap-0 border border-rule">
{(['all', 'success', 'failure'] as const).map(v => (
<button
key={v}
onClick={() => setSuccessFilter(v)}
className={cn(
'px-3 py-1 text-[11px] uppercase tracking-wider border-r border-rule last:border-r-0 transition-colors',
successFilter === v ? 'bg-ink text-paper font-medium' : 'text-ink-3 hover:bg-paper-warm'
)}
>
{v === 'all' ? 'Any' : v === 'success' ? `Pass (${successCount})` : `Fail (${failureCount})`}
</button>
))}
</div>
)}
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search trace ID..."
className="px-3 py-1 text-[12px] bg-transparent border border-rule mono text-ink placeholder:text-ink-4 outline-none focus:border-ink-3 w-[280px]"
/>
<span className="text-[11px] text-ink-3 ml-auto mono">{filtered.length} results</span>
</div>
{/* Table */}
<div className="anim d2">
<table className="w-full text-[13px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-2 pr-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-8">#</th>
<th className="text-left py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Trace ID</th>
<th className="text-center py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-[70px]">Split</th>
<th className="text-center py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-[70px]">Label</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-[90px]">Full Fit.</th>
<th className="text-right py-2 pl-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider w-[90px]">Succ. Fit.</th>
</tr>
</thead>
<tbody>
{pageTraces.map((t, i) => {
const isSelected = selectedTrace === t.id
return (
<Fragment key={`${t.id}-${page * PAGE_SIZE + i}`}>
<tr
onClick={() => onSelectTrace(isSelected ? null : t.id)}
className={cn(
'ruled cursor-pointer transition-colors',
isSelected ? 'bg-slate-dim' : 'hover:bg-ochre-dim'
)}
>
<td className="py-2 pr-3 mono text-ink-4 text-[11px]">{page * PAGE_SIZE + i + 1}</td>
<td className="py-2 px-3 text-[11px] max-w-0" title={t.id}>
{(() => {
const p = parseTraceId(t.id, ds.id)
return (
<div className="flex items-center gap-1.5 min-w-0">
{p.model && <span className="mono text-[10px] px-1.5 py-0.5 rounded border border-rule bg-paper-warm whitespace-nowrap shrink-0">{p.model}</span>}
<span className="mono text-ink-2 truncate min-w-0">{p.rest || t.id}</span>
{p.persona && <span className="mono text-[10px] px-1.5 py-0.5 rounded border border-rule text-ink-3 whitespace-nowrap shrink-0">{p.persona}</span>}
{p.index != null && <span className="mono text-ink-4 text-[10px] shrink-0">#{p.index}</span>}
{isSelected && <span className="text-[9px] text-slate serif italic shrink-0">▾</span>}
</div>
)
})()}
</td>
<td className="py-2 px-3 text-center">
<span className={cn(
'text-[10px] uppercase tracking-wider font-medium',
t.split === 'test' ? 'text-rust' : 'text-ink-3'
)}>
{t.split}
</span>
</td>
<td className="py-2 px-3 text-center">
{t.success === true && <span className="text-[10px] uppercase tracking-wider text-slate font-medium">pass</span>}
{t.success === false && <span className="text-[10px] uppercase tracking-wider text-rust font-medium">fail</span>}
{t.success === null && <span className="text-ink-4">--</span>}
</td>
<td className="py-2 px-3 text-right mono">
{t.fullFitness !== null ? fmt(t.fullFitness) : ''}
</td>
<td className="py-2 pl-3 text-right mono">
{t.successFitness !== null ? fmt(t.successFitness) : ''}
</td>
</tr>
{isSelected && (
<tr>
<td colSpan={6} className="p-0">
<div className="fold-open">
<TraceDetailPanel trace={t} onClose={() => onSelectTrace(null)} />
</div>
</td>
</tr>
)}
</Fragment>
)
})}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<button
onClick={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0}
className={cn('px-3 py-1 text-[11px] uppercase tracking-wider border border-rule transition-colors', page === 0 ? 'text-ink-4' : 'text-ink-2 hover:bg-paper-warm')}
>
Prev
</button>
<span className="text-[11px] text-ink-3 mono">
{page + 1} / {totalPages}
</span>
<button
onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className={cn('px-3 py-1 text-[11px] uppercase tracking-wider border border-rule transition-colors', page >= totalPages - 1 ? 'text-ink-4' : 'text-ink-2 hover:bg-paper-warm')}
>
Next
</button>
</div>
)}
</div>
)
}
// ---------- Trace Detail Panel ----------
function TraceDetailPanel({ trace, onClose }: {
trace: Trace; onClose: () => void
}) {
const [showAll, setShowAll] = useState(false)
const STEP_LIMIT = 40
const failSteps = trace.steps.filter(s => !s.consumed)
const uniqueStates = [...new Set(trace.stateSequence.filter(s => !s.startsWith('FAIL:')))]
const maxVisits = Math.max(1, ...Object.values(trace.stateVisits))
// Steps to display (truncated or all)
const displaySteps = showAll ? trace.steps : trace.steps.slice(0, STEP_LIMIT)
const hasMore = trace.steps.length > STEP_LIMIT
return (
<div className="anim border border-rule bg-paper-warm space-y-0">
{/* Header */}
<div className="flex items-start justify-between p-5 pb-4 ruled">
<div>
<div className="flex items-center gap-3">
<h3 className="serif text-[18px] italic">Trace Detail</h3>
<span className={cn(
'text-[10px] uppercase tracking-wider font-medium px-1.5 py-0.5 border',
trace.split === 'test' ? 'text-rust border-rust/30' : 'text-ink-3 border-rule'
)}>
{trace.split}
</span>
{trace.success === true && <span className="text-[10px] uppercase tracking-wider text-slate font-medium">pass</span>}
{trace.success === false && <span className="text-[10px] uppercase tracking-wider text-rust font-medium">fail</span>}
</div>
<p className="mono text-[11px] text-ink-3 mt-1 break-all">{trace.id}</p>
</div>
<button onClick={onClose} className="text-[11px] text-ink-3 hover:text-ink transition-colors uppercase tracking-wider shrink-0 ml-4">
Close
</button>
</div>
{/* Key metrics */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-0 ruled">
<MiniStat label="Fitness" value={fmt(trace.fitness)} accent={trace.fitness >= 0.99} />
<MiniStat label="Consumed" value={`${trace.consumed}/${trace.total}`} />
<MiniStat label="Failures" value={failSteps.length.toString()} accent={failSteps.length > 0} warn />
<MiniStat label="Unique States" value={uniqueStates.length.toString()} />
<MiniStat label="First Fail" value={trace.firstFailIdx >= 0 ? `Step ${trace.firstFailIdx}` : 'None'} />
</div>
{trace.fullFitness !== null && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0 ruled">
<MiniStat label="Full-model Fit." value={fmt(trace.fullFitness)} />
<MiniStat label="Success-model Fit." value={trace.successFitness !== null ? fmt(trace.successFitness) : '–'} />
<MiniStat
label="Delta"
value={trace.successFitness !== null && trace.fullFitness !== null
? fmt(Math.abs(trace.fullFitness - trace.successFitness), 4)
: '–'}
/>
</div>
)}
{/* State visits heatmap */}
{uniqueStates.length > 0 && (
<div className="p-5 ruled">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-3 font-medium">State Visits</div>
<div className="flex flex-wrap gap-1.5">
{uniqueStates.map(state => {
const count = trace.stateVisits[state] || 0
const intensity = count / maxVisits
return (
<div
key={state}
className="flex flex-col items-center"
title={`${state}: ${count} visit${count !== 1 ? 's' : ''}`}
>
<div
className="px-2 py-1 text-[10px] mono border border-rule"
style={{
background: intensity > 0.6
? `rgba(196, 85, 58, ${0.08 + intensity * 0.18})`
: intensity > 0.3
? `rgba(196, 154, 58, ${0.06 + intensity * 0.12})`
: `rgba(61, 90, 128, ${0.04 + intensity * 0.08})`,
}}
>
{state.length > 20 ? state.slice(0, 18) + '..' : state}
</div>
<span className="text-[9px] text-ink-4 mt-0.5 mono">{count}</span>
</div>
)
})}
</div>
</div>
)}
{/* State sequence flow */}
{trace.stateSequence.length > 0 && (
<div className="p-5 ruled overflow-x-auto">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-3 font-medium">State Sequence</div>
<div className="flex items-center gap-0 min-w-0 flex-wrap">
{trace.stateSequence.slice(0, 60).map((state, i) => {
const isFail = state.startsWith('FAIL:')
return (
<span key={i} className="flex items-center shrink-0">
<span className={cn(
'text-[10px] mono px-1.5 py-0.5',
isFail ? 'text-rust bg-rust-dim font-medium' : 'text-ink-2'
)}>
{isFail ? state.replace('FAIL:', '') : state}
</span>
{i < trace.stateSequence.length - 1 && i < 59 && (
<span className={cn('text-[10px] mx-0.5', isFail ? 'text-rust' : 'text-ink-4')}>
{isFail ? '!' : '\u2192'}
</span>
)}
</span>
)
})}
{trace.stateSequence.length > 60 && (
<span className="text-[10px] text-ink-4 ml-1 mono">+{trace.stateSequence.length - 60} more</span>
)}
</div>
</div>
)}
{/* Step-by-step FSM traversal */}
{trace.steps.length > 0 && (
<div className="p-5">
<div className="flex items-baseline justify-between mb-3">
<div className="text-[10px] text-ink-3 uppercase tracking-widest font-medium">
FSM Traversal ({trace.steps.length} steps)
</div>
{hasMore && (
<button
onClick={() => setShowAll(!showAll)}
className="text-[11px] text-ink-3 hover:text-ink transition-colors uppercase tracking-wider"
>
{showAll ? 'Show less' : `Show all ${trace.steps.length}`}
</button>
)}
</div>
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-2 text-[10px] text-ink-3 font-normal uppercase tracking-wider w-[40px]">Step</th>
<th className="text-left py-1.5 px-2 text-[10px] text-ink-3 font-normal uppercase tracking-wider w-[60px]">Role</th>
<th className="text-left py-1.5 px-2 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Activity</th>
<th className="text-left py-1.5 px-2 text-[10px] text-ink-3 font-normal uppercase tracking-wider w-[130px]">From</th>
<th className="text-center py-1.5 px-1 text-[10px] text-ink-3 font-normal w-[20px]"></th>
<th className="text-left py-1.5 px-2 text-[10px] text-ink-3 font-normal uppercase tracking-wider w-[130px]">To</th>
<th className="text-center py-1.5 pl-2 text-[10px] text-ink-3 font-normal uppercase tracking-wider w-[50px]">Status</th>
</tr>
</thead>
<tbody>
{displaySteps.map((step) => {
const isFail = !step.consumed
const isFirstFail = step.idx === trace.firstFailIdx
return (
<tr
key={step.idx}
className={cn(
'ruled',
isFirstFail ? 'bg-rust-dim' : isFail ? 'bg-rust-dim/50' : ''
)}
>
<td className="py-1.5 pr-2 mono text-[10px] text-ink-4">{step.idx}</td>
<td className="py-1.5 px-2 mono text-[10px] text-ink-3">{step.role}</td>
<td className="py-1.5 px-2 mono text-[10px] truncate max-w-0" title={step.activity}>
{step.activity}
</td>
<td className="py-1.5 px-2 mono text-[10px] truncate max-w-0" title={step.fromState}>
{step.fromState}
</td>
<td className="py-1.5 px-1 text-center text-[10px]">
{isFail
? <span className="text-rust font-bold">!</span>
: <span className="text-ink-4">{'\u2192'}</span>
}
</td>
<td className={cn(
'py-1.5 px-2 mono text-[10px] truncate max-w-0',
isFail ? 'text-rust' : ''
)} title={step.toState || 'REJECTED'}>
{step.toState || 'REJECTED'}
</td>
<td className="py-1.5 pl-2 text-center">
{isFirstFail ? (
<span className="text-[9px] text-rust font-medium uppercase tracking-wider">1st fail</span>
) : isFail ? (
<span className="text-[9px] text-rust/70 uppercase tracking-wider">fail</span>
) : (
<span className="text-[9px] text-ink-4 uppercase tracking-wider">ok</span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
{!showAll && hasMore && (
<div className="mt-2 text-center">
<button
onClick={() => setShowAll(true)}
className="text-[11px] text-ink-3 hover:text-ink transition-colors mono"
>
{trace.steps.length - STEP_LIMIT} more steps...
</button>
</div>
)}
</div>
)}
{trace.steps.length === 0 && (
<div className="p-5 text-center text-[12px] text-ink-4 italic serif">
No replay data available for this trace
</div>
)}
</div>
)
}
function MiniStat({ label, value, accent, warn }: { label: string; value: string; accent?: boolean; warn?: boolean }) {
return (
<div className="py-3 px-5 border-r border-rule last:border-r-0">
<div className="text-[9px] text-ink-4 uppercase tracking-widest">{label}</div>
<div className={cn(
'mono text-[15px] mt-0.5 font-light',
warn && accent ? 'text-rust' : accent ? 'text-rust' : 'text-ink'
)}>
{value}
</div>
</div>
)
}
// ---------- Downstream View ----------
function DownstreamView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
const down = ds.downstream
const bp = down.behavioralProfiling
const sa = down.structuralAnalysis
const fp = down.fitnessProfile
const df = down.differentialFSM
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Downstream Analysis</h2>
<p className="text-[13px] text-ink-2 mt-1">
Behavioral profiling, structural analysis, and anomaly detection from the learned FSM.
</p>
</div>
{/* Fitness profile */}
{fp && (
<div className="anim d1">
<SectionRule title="Fitness Profile" />
<div className="grid grid-cols-2 md:grid-cols-5 gap-0 thick-rule ruled-top mt-3">
<Figure label="Mean" value={fmt(fp.mean)} accent />
<Figure label="Std Dev" value={fmt(fp.std, 4)} />
<Figure label="Min" value={fmt(fp.min)} />
<Figure label="Max" value={fmt(fp.max)} />
<Figure label="Anomalies" value={fp.anomalyCount.toString()} note={`of ${ds.meta.traces}`} />
</div>
{fp.histogram.length > 0 && (
<div className="mt-4 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={200}>
<BarChart data={fp.histogram.map(h => ({ range: `${h.min.toFixed(2)}`, count: h.count }))}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} vertical={false} />
<XAxis dataKey="range" tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} />
<YAxis tick={CHART_TICK} axisLine={false} tickLine={false} />
<Tooltip {...CHART_TOOLTIP} />
<Bar dataKey="count" fill={PALETTE.rust} radius={0} />
</BarChart>
</ResponsiveContainer>
<div className="text-center text-[10px] text-ink-4 mt-1">Fitness distribution (bin lower bound)</div>
</div>
)}
{fp.anomalies.length > 0 && (
<div className="mt-3">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">Anomalous Traces</div>
<div className="flex flex-wrap gap-1.5">
{fp.anomalies.map(id => (
<span key={id} className="mono text-[10px] px-2 py-0.5 bg-rust-dim text-rust border border-rust/20">
{id.length > 40 ? id.slice(0, 38) + '..' : id}
</span>
))}
</div>
</div>
)}
</div>
)}
{/* Behavioral profiling */}
{bp && (
<div className="anim d2">
<SectionRule title="Behavioral Profiling" />
<div className="grid grid-cols-2 gap-0 thick-rule ruled-top mt-3">
<Figure label="Transition Coverage" value={pct(bp.transitionCoverage)} accent />
<Figure label="Avg Unique States/Trace" value={fmt(bp.avgUniqueStatesPerTrace, 1)} />
</div>
<div className="grid grid-cols-2 gap-6 mt-6">
{/* State visits */}
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">State Visit Distribution</div>
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">State</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Count</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Per Trace</th>
</tr>
</thead>
<tbody>
{bp.stateVisitDistribution.map(sv => (
<tr key={sv.state} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{sv.state}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{fmtInt(sv.count)}</td>
<td className="py-1.5 pl-3 text-right mono text-[11px] text-ink-3">{fmt(sv.fraction, 2)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Top transitions */}
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">Top Transitions</div>
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Transition</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Count</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Per Trace</th>
</tr>
</thead>
<tbody>
{bp.transitionFrequency.slice(0, 10).map(tf => (
<tr key={tf.transition} className="ruled">
<td className="py-1.5 pr-3 mono text-[10px]">{tf.transition}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{fmtInt(tf.count)}</td>
<td className="py-1.5 pl-3 text-right mono text-[11px] text-ink-3">{fmt(tf.fraction, 2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{bp.deadTransitions.length > 0 && (
<div className="mt-4">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">Dead Transitions (in FSM, never observed in data)</div>
<div className="flex flex-wrap gap-1.5">
{bp.deadTransitions.map(dt => (
<span key={dt} className="mono text-[10px] px-2 py-0.5 bg-ochre-dim text-ink-2 border border-ochre/20">
{dt}
</span>
))}
</div>
</div>
)}
</div>
)}
{/* Structural analysis */}
{sa && (
<div className="anim d3">
<SectionRule title="Structural Analysis" />
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0 thick-rule ruled-top mt-3">
<Figure label="Graph Density" value={fmt(sa.density)} />
<Figure label="Self-Loops" value={sa.selfLoops.length.toString()} />
<Figure label="Non-trivial SCCs" value={sa.nonTrivialSCCs.length.toString()} />
</div>
<div className="grid grid-cols-2 gap-6 mt-6">
{/* Hubs */}
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">Hub States</div>
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">State</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">In-Degree</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Out-Degree</th>
</tr>
</thead>
<tbody>
{sa.hubs.map(h => (
<tr key={h.id} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{h.id}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{h.inDegree}</td>
<td className="py-1.5 pl-3 text-right mono text-[11px]">{h.outDegree}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Sources, sinks, SCCs */}
<div className="space-y-4">
{sa.sources.length > 0 && (
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-1 font-medium">Source States (no incoming)</div>
<div className="flex flex-wrap gap-1.5">
{sa.sources.map(s => (
<span key={s} className="mono text-[10px] px-2 py-0.5 bg-slate-dim text-ink-2 border border-slate/20">{s}</span>
))}
</div>
</div>
)}
{sa.sinks.length > 0 && (
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-1 font-medium">Sink States (no outgoing)</div>
<div className="flex flex-wrap gap-1.5">
{sa.sinks.map(s => (
<span key={s} className="mono text-[10px] px-2 py-0.5 bg-rust-dim text-ink-2 border border-rust/20">{s}</span>
))}
</div>
</div>
)}
{sa.selfLoops.length > 0 && (
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-1 font-medium">Self-Loops</div>
<div className="flex flex-wrap gap-1.5">
{sa.selfLoops.map(s => (
<span key={s} className="mono text-[10px] px-2 py-0.5 bg-ochre-dim text-ink-2 border border-ochre/20">{s}</span>
))}
</div>
</div>
)}
{sa.nonTrivialSCCs.length > 0 && (
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-1 font-medium">Strongly Connected Components</div>
{sa.nonTrivialSCCs.map((scc, i) => (
<div key={i} className="flex flex-wrap gap-1 mt-1">
<span className="text-[10px] text-ink-4 serif italic mr-1">SCC {i + 1}:</span>
{scc.map(s => (
<span key={s} className="mono text-[10px] px-1.5 py-0.5 text-ink-2">{s}</span>
))}
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
{/* Differential FSM */}
{df && (
<div className="anim d4">
<SectionRule title="Differential FSM (Success vs Failure)" />
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 mt-4">
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">
Success-Only ({df.successOnlyTransitions.length})
</div>
{df.successOnlyTransitions.length > 0 ? (
<div className="flex flex-col gap-1">
{df.successOnlyTransitions.map(t => (
<span key={t} className="mono text-[10px] px-2 py-0.5 bg-slate-dim text-slate border border-slate/20">{t}</span>
))}
</div>
) : (
<span className="text-[11px] text-ink-4 italic serif">None</span>
)}
</div>
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">
Failure-Only ({df.failureOnlyTransitions.length})
</div>
{df.failureOnlyTransitions.length > 0 ? (
<div className="flex flex-col gap-1">
{df.failureOnlyTransitions.map(t => (
<span key={t} className="mono text-[10px] px-2 py-0.5 bg-rust-dim text-rust border border-rust/20">{t}</span>
))}
</div>
) : (
<span className="text-[11px] text-ink-4 italic serif">None</span>
)}
</div>
<div>
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2 font-medium">
Shared ({df.sharedTransitions.length})
</div>
{df.sharedTransitions.length > 0 ? (
<div className="flex flex-col gap-1">
{df.sharedTransitions.map(t => (
<span key={t} className="mono text-[10px] px-2 py-0.5 text-ink-3">{t}</span>
))}
</div>
) : (
<span className="text-[11px] text-ink-4 italic serif">None</span>
)}
</div>
</div>
</div>
)}
{/* Failure prediction lives in the dedicated Failure tab (deduplicated). */}
{/* Cross-dataset downstream comparison */}
<div className="anim d6">
<SectionRule title="Cross-Dataset Downstream" />
<table className="w-full text-[13px] mt-3">
<thead>
<tr className="thick-rule">
<th className="text-left py-2 pr-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Dataset</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Fitness u</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Fitness sigma</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Anomalies</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Trans. Cov.</th>
<th className="text-right py-2 px-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Density</th>
<th className="text-right py-2 pl-3 text-[11px] text-ink-3 font-normal uppercase tracking-wider">Dead Trans.</th>
</tr>
</thead>
<tbody>
{datasets.map(d => {
const fp2 = d.downstream.fitnessProfile
const bp2 = d.downstream.behavioralProfiling
const sa2 = d.downstream.structuralAnalysis
const isActive = d.id === ds.id
return (
<tr key={d.id} className={cn('ruled', isActive && 'bg-rust-dim')}>
<td className="py-2 pr-3 font-medium">
{d.meta.name}
{isActive && <span className="ml-2 text-[10px] text-rust italic serif">current</span>}
</td>
<td className="py-2 px-3 text-right mono">{fp2 ? fmt(fp2.mean) : '–'}</td>
<td className="py-2 px-3 text-right mono text-ink-3">{fp2 ? fmt(fp2.std, 4) : '–'}</td>
<td className="py-2 px-3 text-right mono">{fp2 ? fp2.anomalyCount : '–'}</td>
<td className="py-2 px-3 text-right mono">{bp2 ? pct(bp2.transitionCoverage) : '–'}</td>
<td className="py-2 px-3 text-right mono">{sa2 ? fmt(sa2.density) : '–'}</td>
<td className="py-2 pl-3 text-right mono">{bp2 ? bp2.deadTransitions.length : '–'}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}
// ---------- Failure Prediction View ----------
function FailureView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
const fp = ds.downstream.failurePrediction as {
fitnessOnly?: { auroc?: number; f1?: number; accuracy?: number; bestThreshold?: number }
featureBased?: {
holdoutAUROC?: number; holdoutAUROC_LR?: number; holdoutAUROC_GBT?: number
holdoutAUROC_RF?: number; holdoutAUROC_Ensemble?: number
bestModel?: string; numFeatures?: number; numSelectedFeatures?: number
kfoldAUROC?: { mean?: number; std?: number; k?: number; repeats?: number }
f1?: number; accuracy?: number; precision?: number; recall?: number
topFeatures?: Array<{ name: string; weight: number; absWeight: number }>
}
} | null
const np = ds.downstream.neuralProbe
// Cross-dataset AUROC comparison (fitness-only vs ensemble)
const crossData = datasets
.map(d => {
const dfp = d.downstream.failurePrediction as typeof fp
if (!dfp?.fitnessOnly?.auroc || !dfp?.featureBased?.holdoutAUROC) return null
return {
name: d.meta.shortName,
fitnessOnly: dfp.fitnessOnly.auroc,
ensemble: dfp.featureBased.holdoutAUROC_Ensemble ?? dfp.featureBased.holdoutAUROC,
cvMean: dfp.featureBased.kfoldAUROC?.mean ?? 0,
cvStd: dfp.featureBased.kfoldAUROC?.std ?? 0,
}
})
.filter((x): x is NonNullable<typeof x> => x !== null)
// Neural Seq vs FSM cross-dataset
const neuralCross = datasets
.map(d => {
const n = d.downstream.neuralProbe
if (!n) return null
return {
name: d.meta.shortName,
seq_mlp: n.seq_mlp.cv, fsm_mlp: n.fsm_mlp.cv,
seq_gru: n.seq_gru.cv, fsm_gru: n.fsm_gru.cv,
seq_transf: n.seq_transf.cv, fsm_transf: n.fsm_transf.cv,
}
})
.filter((x): x is NonNullable<typeof x> => x !== null)
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Failure Prediction</h2>
<p className="text-[13px] text-ink-2 mt-1">
Per-state FSM features predict trace-level success/failure. Compared against
a fitness-only baseline and against neural models (MLP / GRU / Transformer) trained on
sequence features vs FSM-derived features.
</p>
</div>
{/* This-dataset headline */}
{fp?.featureBased && (
<div className="anim d1">
<SectionRule title={`${ds.meta.name} — Headline`} />
<div className="grid grid-cols-2 md:grid-cols-5 gap-0 thick-rule ruled-top mt-3">
<Figure label="Fitness-Only AUROC" value={fmt(fp.fitnessOnly?.auroc ?? 0.5)} />
<Figure label="Ensemble AUROC" value={fmt(fp.featureBased.holdoutAUROC_Ensemble ?? fp.featureBased.holdoutAUROC ?? 0.5)} accent />
<Figure label="CV AUROC" value={fp.featureBased.kfoldAUROC?.mean ? `${fmt(fp.featureBased.kfoldAUROC.mean)} ±${fmt(fp.featureBased.kfoldAUROC.std ?? 0, 3)}` : '—'} />
<Figure label="Features (selected/total)" value={`${fp.featureBased.numSelectedFeatures ?? '—'}/${fp.featureBased.numFeatures ?? '—'}`} />
<Figure label="Best Model" value={fp.featureBased.bestModel ?? '—'} />
</div>
</div>
)}
{/* Cross-dataset AUROC */}
{crossData.length > 0 && (
<div className="anim d2">
<SectionRule title="Cross-Dataset AUROC (Fitness-Only vs FSM Ensemble)" />
<div className="mt-4 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={280}>
<BarChart data={crossData} barCategoryGap="20%">
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} vertical={false} />
<XAxis dataKey="name" tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} angle={-20} textAnchor="end" height={50} />
<YAxis domain={[0.4, 1.0]} tick={CHART_TICK} axisLine={false} tickLine={false} />
<Tooltip {...CHART_TOOLTIP} formatter={(v) => (typeof v === 'number' ? v.toFixed(3) : String(v))} />
<Legend wrapperStyle={{ fontSize: 11, fontFamily: 'IBM Plex Mono, monospace' }} />
<Bar dataKey="fitnessOnly" name="Fitness-Only" fill={PALETTE.slate} radius={0} />
<Bar dataKey="ensemble" name="FSM Ensemble" fill={PALETTE.rust} radius={0} />
</BarChart>
</ResponsiveContainer>
<div className="text-center text-[10px] text-ink-4 mt-1">
Holdout AUROC, 80/20 split. FSM ensemble = LR + GBT + RF on per-state features.
</div>
</div>
</div>
)}
{/* Neural probe comparison */}
{neuralCross.length > 0 && (
<div className="anim d3">
<SectionRule title="Neural Models — Sequence vs FSM Features (CV AUROC)" />
<div className="mt-3 overflow-x-auto">
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Dataset</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">MLP Seq</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">MLP FSM</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">GRU Seq</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">GRU FSM</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Transf Seq</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Transf FSM</th>
</tr>
</thead>
<tbody>
{neuralCross.map(r => {
const winCell = (seq: number, fsm: number, isFsm: boolean) => {
const fsmWins = fsm > seq
const style = isFsm && fsmWins ? { color: PALETTE.rust, fontWeight: 600 } : {}
return <td className="py-1.5 px-3 text-right mono text-[11px]" style={style}>{fmt(isFsm ? fsm : seq)}</td>
}
return (
<tr key={r.name} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{r.name}</td>
{winCell(r.seq_mlp, r.fsm_mlp, false)}
{winCell(r.seq_mlp, r.fsm_mlp, true)}
{winCell(r.seq_gru, r.fsm_gru, false)}
{winCell(r.seq_gru, r.fsm_gru, true)}
{winCell(r.seq_transf, r.fsm_transf, false)}
{winCell(r.seq_transf, r.fsm_transf, true)}
</tr>
)
})}
</tbody>
</table>
</div>
<div className="text-[10px] text-ink-4 mt-2">
10×5-fold CV AUROC. Bold rust = FSM features beat sequence features for that model.
</div>
</div>
)}
{/* Neural probe for current dataset only */}
{np && (
<div className="anim d4">
<SectionRule title={`${ds.meta.name} — Neural Probe Detail`} />
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-4">
{(['mlp', 'gru', 'transf'] as const).map(model => {
const seq = np[`seq_${model}` as const]
const fsm = np[`fsm_${model}` as const]
const delta = fsm.cv - seq.cv
return (
<div key={model} className="border border-rule bg-paper-warm p-3">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-2">{model}</div>
<div className="flex justify-between items-baseline mb-1">
<span className="mono text-[11px] text-ink-3">Seq</span>
<span className="mono text-[13px]">{fmt(seq.cv)} ±{fmt(seq.std, 3)}</span>
</div>
<div className="flex justify-between items-baseline mb-1">
<span className="mono text-[11px] text-rust">FSM</span>
<span className="mono text-[13px]" style={{ color: PALETTE.rust, fontWeight: 600 }}>
{fmt(fsm.cv)} ±{fmt(fsm.std, 3)}
</span>
</div>
<div className="ruled-top mt-2 pt-1 flex justify-between items-baseline">
<span className="text-[10px] text-ink-4 uppercase tracking-wider">Δ</span>
<span className="mono text-[12px]" style={{ color: delta >= 0 ? PALETTE.rust : PALETTE.iron }}>
{delta >= 0 ? '+' : ''}{fmt(delta * 100, 1)}pp
</span>
</div>
</div>
)
})}
</div>
</div>
)}
{/* Top features */}
{fp?.featureBased?.topFeatures && fp.featureBased.topFeatures.length > 0 && (
<div className="anim d5">
<SectionRule title={`${ds.meta.name} — Top Discriminative Features`} />
<div className="mt-3 grid grid-cols-2 gap-x-8 gap-y-1">
{fp.featureBased.topFeatures.slice(0, 12).map(f => (
<div key={f.name} className="flex justify-between items-baseline ruled py-1">
<span className="mono text-[11px] text-ink-2 truncate pr-3">{f.name}</span>
<span className="mono text-[11px]" style={{ color: f.weight >= 0 ? PALETTE.rust : PALETTE.slate }}>
{f.weight >= 0 ? '+' : ''}{fmt(f.weight, 3)}
</span>
</div>
))}
</div>
<div className="text-[10px] text-ink-4 mt-2">
LR coefficients on per-state features. Rust = failure-indicative, slate = success-indicative.
</div>
</div>
)}
{(!fp && !np) && (
<div className="anim d1 border border-rule bg-paper-warm p-6 text-center">
<div className="text-[13px] text-ink-2">No failure prediction data for {ds.meta.name}.</div>
<div className="text-[11px] text-ink-3 mt-1">This dataset lacks success/failure labels or hasn't been run through the downstream pipeline.</div>
</div>
)}
</div>
)
}
// ---------- Workflow Memory View (FSM vs AWM) ----------
function MemoryView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
const wm = ds.downstream.workflowMemory
const cross = datasets
.map(d => {
const w = d.downstream.workflowMemory
if (!w) return null
return {
name: d.meta.shortName,
noMemory: w.noMemory.accuracy,
awm: w.awm.accuracy,
fsm: w.fsm.accuracy,
gain: w.fsmGain,
n: w.n,
}
})
.filter((x): x is NonNullable<typeof x> => x !== null)
const fsmWins = cross.filter(r => r.fsm > r.awm).length
const meanGain = cross.length > 0 ? cross.reduce((s, r) => s + r.gain, 0) / cross.length : 0
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Workflow Memory (FSM vs AWM)</h2>
<p className="text-[13px] text-ink-2 mt-1">
LLM next-action prediction (gpt-4.1-mini, top-1 accuracy) with three context conditions: no memory, Agent Workflow Memory (linear workflows), and FSM-state context.
</p>
</div>
{/* Summary */}
{cross.length > 0 && (
<div className="anim d1">
<SectionRule title="Summary across datasets" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-0 thick-rule ruled-top mt-3">
<Figure label="Datasets" value={cross.length.toString()} />
<Figure label="FSM wins vs AWM" value={`${fsmWins}/${cross.length}`} accent />
<Figure label="Mean Δ (FSM − AWM)" value={`+${fmt(meanGain * 100, 1)}pp`} accent />
<Figure label="Total LLM judgments" value={fmtInt(cross.reduce((s, r) => s + r.n, 0))} />
</div>
</div>
)}
{/* Cross-dataset bar chart */}
{cross.length > 0 && (
<div className="anim d2">
<SectionRule title="Per-Dataset Accuracy" />
<div className="mt-4 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={320}>
<BarChart data={cross} barCategoryGap="18%">
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} vertical={false} />
<XAxis dataKey="name" tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} angle={-20} textAnchor="end" height={60} />
<YAxis domain={[0, 1.0]} tick={CHART_TICK} axisLine={false} tickLine={false} tickFormatter={(v) => `${Math.round(v * 100)}%`} />
<Tooltip {...CHART_TOOLTIP} formatter={(v) => (typeof v === 'number' ? `${(v * 100).toFixed(1)}%` : String(v))} />
<Legend wrapperStyle={{ fontSize: 11, fontFamily: 'IBM Plex Mono, monospace' }} />
<Bar dataKey="noMemory" name="No Memory" fill={PALETTE.iron} radius={0} />
<Bar dataKey="awm" name="AWM" fill={PALETTE.slate} radius={0} />
<Bar dataKey="fsm" name="FSM" fill={PALETTE.rust} radius={0} />
</BarChart>
</ResponsiveContainer>
<div className="text-center text-[10px] text-ink-4 mt-1">
LLM-judged top-1 next-action accuracy, full validation split per dataset.
</div>
</div>
</div>
)}
{/* Table with deltas */}
{cross.length > 0 && (
<div className="anim d3">
<SectionRule title="Detail" />
<div className="mt-3 overflow-x-auto">
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Dataset</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">N</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">No Mem</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">AWM</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">FSM</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">FSM − AWM</th>
</tr>
</thead>
<tbody>
{cross.map(r => (
<tr key={r.name} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{r.name}</td>
<td className="py-1.5 px-3 text-right mono text-[11px] text-ink-3">{fmtInt(r.n)}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{pct(r.noMemory)}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{pct(r.awm)}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]" style={{ color: PALETTE.rust, fontWeight: 600 }}>{pct(r.fsm)}</td>
<td className="py-1.5 pl-3 text-right mono text-[11px]" style={{ color: r.gain >= 0 ? PALETTE.rust : PALETTE.iron }}>
{r.gain >= 0 ? '+' : ''}{fmt(r.gain * 100, 1)}pp
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="text-[10px] text-ink-4 mt-2">
Highest gains: WebArena +15.7pp, SWE-smith +25.3pp. 6/8 statsig at p&lt;1e-8 (per paper).
</div>
</div>
)}
{!wm && (
<div className="anim d1 border border-rule bg-paper-warm p-6 text-center">
<div className="text-[13px] text-ink-2">No workflow memory data for {ds.meta.name}.</div>
<div className="text-[11px] text-ink-3 mt-1">Run <code className="mono">poc-fsm-as-memory.ts</code> with LLM judging enabled.</div>
</div>
)}
</div>
)
}
// ---------- Cross-Model FSM View ----------
function CrossModelView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
const cm = ds.downstream.crossModel
const available = datasets.filter(d => d.downstream.crossModel)
// Auto-fallback to first available if current dataset has no cross-model data
const active = cm ? ds : available[0]
const acm = active?.downstream.crossModel
if (!acm) {
return (
<div className="space-y-6">
<div className="anim">
<h2 className="serif text-[22px] italic">Cross-Model FSM Transfer</h2>
<p className="text-[13px] text-ink-2 mt-1">No cross-model data available. tau2-bench airline/retail/telecom only.</p>
</div>
</div>
)
}
const heatColor = (v: number, min: number, max: number) => {
if (max === min) return PALETTE.iron
const t = (v - min) / (max - min)
const r = Math.round(196 - t * 60)
const g = Math.round(85 - t * 20)
const b = Math.round(58 - t * 15)
return `rgb(${r},${g},${b})`
}
// Transfer matrices may arrive as a raw number[][] or as a {labels, values} object
// (the experiment JSON uses the latter); normalize to a 2-D number array.
const toMatrix = (m: unknown): number[][] =>
Array.isArray(m) ? (m as number[][]) : (((m as { values?: number[][] })?.values) ?? [])
const fitMatrix = toMatrix(acm.matrices.fitness)
const aurocMatrix = toMatrix(acm.matrices.auroc)
const fitnessFlat = fitMatrix.flat()
const aurocFlat = aurocMatrix.flat()
const fitMin = Math.min(...fitnessFlat), fitMax = Math.max(...fitnessFlat)
const aurocMin = Math.min(...aurocFlat), aurocMax = Math.max(...aurocFlat)
// Paper headline = the combined figure across ALL tau2-bench suites (36 off-diagonal
// pairs), not any single suite. Single-suite telecom (0.66/0.80) understates it.
const suiteSummaries = available.map(d => d.downstream.crossModel!.summary)
const combinedSelf = suiteSummaries.reduce((s, x) => s + x.diagonalAUROC.mean, 0) / suiteSummaries.length
const combinedCross = suiteSummaries.reduce((s, x) => s + x.offDiagonalAUROC.mean, 0) / suiteSummaries.length
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Cross-Model FSM Transfer</h2>
<p className="text-[13px] text-ink-2 mt-1">
tau2-bench traces collected across 4 LLMs. Train FSM on one model's traces, evaluate on another. Diagonal = self-test, off-diagonal = cross-model transfer.
</p>
{available.length > 1 && (
<div className="mt-3 flex gap-2">
{available.map(d => (
<span key={d.id}
className={cn('mono text-[10px] px-2 py-0.5 border',
d.id === active.id ? 'bg-rust text-paper border-rust' : 'text-ink-3 border-rule')}>
{d.meta.name}
</span>
))}
<span className="mono text-[10px] text-ink-4 ml-2">(showing: {active.meta.name})</span>
</div>
)}
</div>
{/* Combined headline across all suites (paper figure) */}
{available.length > 1 && (
<div className="anim d1">
<SectionRule title="AUROC: Self vs Cross-Model (combined, all suites)" />
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0 thick-rule ruled-top mt-3">
<Figure label="Self AUROC (combined)" value={fmt(combinedSelf)} accent />
<Figure label="Cross AUROC (combined)" value={fmt(combinedCross)} />
<Figure label="Transfer Gap" value={`−${fmt((combinedSelf - combinedCross) * 100, 1)}pp`} />
</div>
<div className="text-[10px] text-ink-4 mt-2">
Mean over the {available.length} tau2-bench suites (36 off-diagonal model pairs). Per-suite breakdown below.
</div>
</div>
)}
{/* Per-suite summary */}
<div className="anim d1">
<SectionRule title={`AUROC: Self vs Cross-Model (${active.meta.name})`} />
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0 thick-rule ruled-top mt-3">
<Figure label="Diagonal AUROC (self)" value={`${fmt(acm.summary.diagonalAUROC.mean)} ±${fmt(acm.summary.diagonalAUROC.std, 3)}`} accent />
<Figure label="Off-Diagonal AUROC (cross)" value={`${fmt(acm.summary.offDiagonalAUROC.mean)} ±${fmt(acm.summary.offDiagonalAUROC.std, 3)}`} />
<Figure label="Transfer Gap" value={`−${fmt(acm.summary.aurocTransferGap * 100, 1)}pp`} />
</div>
</div>
{/* Models */}
<div className="anim d2">
<SectionRule title="LLMs in this Suite" />
<div className="mt-3 grid grid-cols-2 md:grid-cols-4 gap-3">
{acm.models.map(m => (
<div key={m.shortName} className="border border-rule bg-paper-warm p-3">
<div className="mono text-[12px] font-medium mb-1">{m.name}</div>
<div className="text-[10px] text-ink-3 uppercase tracking-wider mb-1">Traces</div>
<div className="mono text-[13px]">{fmtInt(m.traces)}</div>
<div className="text-[10px] text-ink-3 uppercase tracking-wider mt-2 mb-1">Success</div>
<div className="mono text-[13px]">{pct(m.successRate)}</div>
</div>
))}
</div>
</div>
{/* Transfer matrix: Fitness */}
<div className="anim d3">
<SectionRule title="Fitness Transfer Matrix" />
<div className="mt-3 overflow-x-auto">
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">FSM Source ↓ / Target →</th>
{acm.models.map(m => (
<th key={m.shortName} className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">{m.shortName}</th>
))}
</tr>
</thead>
<tbody>
{fitMatrix.map((row, i) => (
<tr key={i} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{acm.models[i].shortName}</td>
{row.map((v, j) => (
<td key={j} className="py-1.5 px-3 text-right mono text-[11px]"
style={{
color: 'white',
background: heatColor(v, fitMin, fitMax),
fontWeight: i === j ? 700 : 400,
}}>
{fmt(v)}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="text-[10px] text-ink-4 mt-2">
Bold diagonal = self (train and test on same model). Darker = lower fitness.
</div>
</div>
</div>
{/* Transfer matrix: AUROC */}
<div className="anim d4">
<SectionRule title="Failure-Prediction AUROC Transfer Matrix" />
<div className="mt-3 overflow-x-auto">
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">FSM Source ↓ / Target →</th>
{acm.models.map(m => (
<th key={m.shortName} className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">{m.shortName}</th>
))}
</tr>
</thead>
<tbody>
{aurocMatrix.map((row, i) => (
<tr key={i} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{acm.models[i].shortName}</td>
{row.map((v, j) => (
<td key={j} className="py-1.5 px-3 text-right mono text-[11px]"
style={{
color: 'white',
background: heatColor(v, aurocMin, aurocMax),
fontWeight: i === j ? 700 : 400,
}}>
{fmt(v)}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="text-[10px] text-ink-4 mt-2">
Per-state failure features transfer at {fmt(acm.summary.offDiagonalAUROC.mean)} mean cross-AUROC vs {fmt(acm.summary.diagonalAUROC.mean)} self.
</div>
</div>
</div>
</div>
)
}
// ---------- Runtime Monitor View ----------
function MonitorView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
const available = datasets.filter(d => d.downstream.monitor)
const active = ds.downstream.monitor ? ds : available[0]
const m = active?.downstream.monitor
if (!m) {
return (
<div className="space-y-6">
<div className="anim">
<h2 className="serif text-[22px] italic">Runtime Monitor</h2>
<p className="text-[13px] text-ink-2 mt-1">No monitor data available. Evaluated on SWE-agent, SWE-smith, tau2-bench retail, and tau2-bench airline.</p>
</div>
</div>
)
}
// Build chart data: one row per trace-completion point, one column per config
const points = m.configs[0]?.monitoringCurve.map(c => c.point) || []
const curveData = points.map(pt => {
const row: Record<string, number> = { point: pt }
m.configs.forEach(c => {
const cp = c.monitoringCurve.find(x => x.point === pt)
row[c.config] = cp?.f1 ?? 0
})
return row
})
const finalF1 = (cfg: typeof m.configs[number]) => cfg.monitoringCurve[cfg.monitoringCurve.length - 1]?.f1 ?? 0
const bestCfg = m.configs.reduce((a, b) => (finalF1(b) > finalF1(a) ? b : a))
const earliestStrong = bestCfg.monitoringCurve.find(c => c.f1 >= 0.9)
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Runtime Monitor</h2>
<p className="text-[13px] text-ink-2 mt-1">
Online failure detection from FSM conformance during trace execution. Decay parameter γ controls how quickly past conformance is forgotten; W is a sliding window.
</p>
{available.length > 1 && (
<div className="mt-3 flex gap-2 flex-wrap">
{available.map(d => (
<span key={d.id}
className={cn('mono text-[10px] px-2 py-0.5 border',
d.id === active.id ? 'bg-rust text-paper border-rust' : 'text-ink-3 border-rule')}>
{d.meta.name}
</span>
))}
<span className="mono text-[10px] text-ink-4 ml-2">(showing: {active.meta.name})</span>
</div>
)}
</div>
{/* Summary */}
<div className="anim d1">
<SectionRule title="Summary" />
<div className="grid grid-cols-2 md:grid-cols-5 gap-0 thick-rule ruled-top mt-3">
<Figure label="Test traces" value={fmtInt(m.testSize)} note={`${m.succSize} succ / ${m.failSize} fail`} />
<Figure label="Best Config" value={bestCfg.config} />
<Figure label="Best F1 (final)" value={fmt(finalF1(bestCfg))} accent />
<Figure label="F1 ≥ 0.9 at" value={earliestStrong ? `${Math.round(earliestStrong.point * 100)}%` : '—'} note="trace completion" />
<Figure label="Flag-Everything F1" value={fmt((2 * m.failSize) / (m.failSize + m.testSize))} note="trivial baseline" />
</div>
</div>
{/* Curves */}
<div className="anim d2">
<SectionRule title="F1 vs Trace Completion" />
<div className="mt-3 bg-paper-warm border border-rule p-5">
<ResponsiveContainer width="100%" height={320}>
<LineChart data={curveData}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID} vertical={false} />
<XAxis dataKey="point" type="number" domain={[0.1, 1.0]} tick={CHART_TICK} axisLine={CHART_AXIS} tickLine={false} tickFormatter={(v) => `${Math.round(v * 100)}%`} />
<YAxis domain={[0, 1.0]} tick={CHART_TICK} axisLine={false} tickLine={false} />
<Tooltip {...CHART_TOOLTIP} formatter={(v) => (typeof v === 'number' ? v.toFixed(3) : String(v))} labelFormatter={(v) => `Completion: ${Math.round(Number(v) * 100)}%`} />
<Legend wrapperStyle={{ fontSize: 11, fontFamily: 'IBM Plex Mono, monospace' }} />
{m.configs.map((c, i) => (
<Line key={c.config} dataKey={c.config} type="monotone"
stroke={[PALETTE.rust, PALETTE.slate, PALETTE.ochre, PALETTE.teal][i % 4]}
strokeWidth={2} dot={{ r: 3 }} />
))}
</LineChart>
</ResponsiveContainer>
<div className="text-center text-[10px] text-ink-4 mt-1">
X-axis: fraction of trace observed (online setting). Y-axis: F1 at the threshold tuned for that point.
</div>
</div>
</div>
{/* Baselines */}
{Object.keys(m.baselines).length > 0 && (
<div className="anim d3">
<SectionRule title="Single-Feature Baselines (AUROC)" />
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-3">
{Object.entries(m.baselines).map(([name, b]) => (
<div key={name} className="border border-rule bg-paper-warm p-3">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-1">{name}</div>
<div className="mono text-[18px]" style={{ color: b.auroc >= 0.9 ? PALETTE.ochre : PALETTE.iron }}>
{fmt(b.auroc)}
</div>
</div>
))}
</div>
<div className="text-[10px] text-ink-4 mt-2">
selfLoopRate AUROC = 1.0 on SWE-agent is an artifact of failure traces always containing self-loops — not a meaningful signal in isolation.
</div>
</div>
)}
</div>
)
}
// ---------- Counterfactual Paths View ----------
function CounterfactualView({ ds, datasets }: { ds: Dataset; datasets: AllDatasets }) {
const available = datasets.filter(d => d.downstream.counterfactual)
const active = ds.downstream.counterfactual ? ds : available[0]
const c = active?.downstream.counterfactual
if (!c) {
return (
<div className="space-y-6">
<div className="anim">
<h2 className="serif text-[22px] italic">Counterfactual Paths</h2>
<p className="text-[13px] text-ink-2 mt-1">No counterfactual data. Available for SWE-agent, SWE-smith, tau2-bench retail, and tau2-bench airline.</p>
</div>
</div>
)
}
const pathRatio = c.pathDiversity.uniqueSuccPaths > 0
? c.pathDiversity.uniqueFailPaths / c.pathDiversity.uniqueSuccPaths
: 0
const overlapPct = (c.pathDiversity.uniqueSuccPaths + c.pathDiversity.uniqueFailPaths - c.pathDiversity.overlap) > 0
? c.pathDiversity.overlap / (c.pathDiversity.uniqueSuccPaths + c.pathDiversity.uniqueFailPaths - c.pathDiversity.overlap)
: 0
return (
<div className="space-y-8">
<div className="anim">
<h2 className="serif text-[22px] italic">Counterfactual Path Analysis</h2>
<p className="text-[13px] text-ink-2 mt-1">
Where success and failure trajectories diverge through the FSM. Decision points are states whose outgoing transition distributions differ between success and failure traces (Jensen–Shannon divergence).
</p>
{available.length > 1 && (
<div className="mt-3 flex gap-2 flex-wrap">
{available.map(d => (
<span key={d.id}
className={cn('mono text-[10px] px-2 py-0.5 border',
d.id === active.id ? 'bg-rust text-paper border-rust' : 'text-ink-3 border-rule')}>
{d.meta.name}
</span>
))}
<span className="mono text-[10px] text-ink-4 ml-2">(showing: {active.meta.name})</span>
</div>
)}
</div>
{/* Summary */}
<div className="anim d1">
<SectionRule title="Path Diversity" />
<div className="grid grid-cols-2 md:grid-cols-5 gap-0 thick-rule ruled-top mt-3">
<Figure label="Success traces" value={fmtInt(c.nSuccess)} />
<Figure label="Failure traces" value={fmtInt(c.nFailure)} />
<Figure label="Unique success paths" value={fmtInt(c.pathDiversity.uniqueSuccPaths)} />
<Figure label="Unique failure paths" value={fmtInt(c.pathDiversity.uniqueFailPaths)} accent />
<Figure label="Fail/Succ ratio" value={`${fmt(pathRatio, 1)}×`} note={`${fmt(overlapPct * 100, 1)}% overlap`} />
</div>
</div>
{/* Decision points */}
{c.dpDivergence.length > 0 && (
<div className="anim d2">
<SectionRule title="Decision Points (JSD ranked)" />
<div className="mt-3">
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">State</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">JSD</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Bar</th>
</tr>
</thead>
<tbody>
{c.dpDivergence
.slice()
.sort((a, b) => b.jsd - a.jsd)
.slice(0, 15)
.map((dp) => {
const maxJsd = Math.max(...c.dpDivergence.map(x => x.jsd), 1e-9)
return (
<tr key={dp.state} className="ruled">
<td className="py-1.5 pr-3">
<span className="mono text-[10px] text-ink-2 px-1.5 py-0.5 rounded border border-rule bg-paper-warm whitespace-nowrap">{dp.state}</span>
</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{fmt(dp.jsd, 4)}</td>
<td className="py-1.5 pl-3">
<div className="h-1.5 bg-paper-warm rounded-sm overflow-hidden" style={{ minWidth: 60 }}>
<div className="h-full" style={{ width: `${(dp.jsd / maxJsd) * 100}%`, background: PALETTE.rust }} />
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
<div className="text-[10px] text-ink-4 mt-2">
Higher JSD = stronger divergence between success and failure transition distributions at that state.
</div>
</div>
)}
{/* Branching */}
{c.decisionPoints.length > 0 && (
<div className="anim d3">
<SectionRule title="Branching Decisions" />
<div className="mt-3 space-y-2">
{c.decisionPoints
.slice()
.sort((a, b) => b.branching - a.branching)
.slice(0, 8)
.map((dp) => (
<div key={dp.state} className="border border-rule bg-paper-warm p-3">
<div className="flex justify-between items-baseline mb-1">
<span className="mono text-[12px] font-medium">{dp.state}</span>
<span className="mono text-[11px] text-ink-3">{dp.branching} branches</span>
</div>
<div className="flex flex-wrap gap-1">
{dp.targets.map(t => (
<span key={t} className="mono text-[10px] px-2 py-0.5 bg-paper border border-rule">
{t}
</span>
))}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
// ---------- About / Paper View ----------
function AboutView({ datasets }: { datasets: AllDatasets }) {
const totalTraces = datasets.reduce((s, d) => s + d.meta.traces, 0)
const compressionExtremes = datasets
.map(d => {
const ours = d.baselines.find(b => b.category === 'ours')
const rpni = d.baselines.find(b => b.method === 'rpni')
if (!ours?.states || !rpni?.states) return null
return { name: d.meta.shortName, ratio: rpni.states / ours.states }
})
.filter((x): x is { name: string; ratio: number } => x !== null)
const compMin = compressionExtremes.length ? Math.min(...compressionExtremes.map(c => c.ratio)) : 0
const compMax = compressionExtremes.length ? Math.max(...compressionExtremes.map(c => c.ratio)) : 0
return (
<div className="space-y-8 max-w-[900px]">
<div className="anim">
<h2 className="serif text-[28px] italic leading-tight">Automata from Agent Traces</h2>
<p className="text-[15px] text-ink-2 mt-2 leading-relaxed">
Failure and next-step prediction for LLM-based agents via minimal finite-state machines extracted from execution traces.
</p>
</div>
<div className="anim d1">
<SectionRule title="Abstract" />
<p className="text-[13px] text-ink-2 mt-3 leading-relaxed">
LLM-based agents execute multi-step tasks, but their behavioural structure remains opaque: long unstructured traces resist the safety auditing and runtime monitoring that deployment requires.
We extract finite-state machines (FSMs) that are provably minimal for the observed prefix language via prefix-tree construction and structural state merging.
Across {datasets.length} public datasets, the FSMs are compact (7–43 states), achieve ≥0.993 replay fitness on held-out data with zero structural variance across splits, and build in milliseconds.
The same FSM unifies workflow memory, next-step prediction, failure prediction, and runtime monitoring.
</p>
</div>
<div className="anim d2">
<SectionRule title="Headline Numbers" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-0 thick-rule ruled-top mt-3">
<Figure label="Datasets" value={datasets.length.toString()} accent />
<Figure label="Total traces" value={fmtInt(totalTraces)} />
<Figure label="Compression" value={`${fmt(compMin, 0)}–${fmt(compMax, 0)}×`} note="vs RPNI" />
<Figure label="Replay fitness" value="≥0.993" note="held-out" />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-0 thick-rule mt-0">
<Figure label="Failure AUROC" value="0.70–0.94" note="holdout" />
<Figure label="Monitor F1" value="0.904" note="SWE-agent" />
<Figure label="Workflow Memory" value="8/8 wins" note="vs AWM" />
<Figure label="Cross-Model" value="1.000" note="combined FSM" />
</div>
</div>
<div className="anim d3">
<SectionRule title="Dataset Catalogue" />
<div className="mt-3 overflow-x-auto">
<table className="w-full text-[12px]">
<thead>
<tr className="thick-rule">
<th className="text-left py-1.5 pr-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Dataset</th>
<th className="text-left py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Venue</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">Traces</th>
<th className="text-right py-1.5 px-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">|A|</th>
<th className="text-right py-1.5 pl-3 text-[10px] text-ink-3 font-normal uppercase tracking-wider">|Q|</th>
</tr>
</thead>
<tbody>
{datasets.map(d => {
const ours = d.baselines.find(b => b.category === 'ours')
return (
<tr key={d.id} className="ruled">
<td className="py-1.5 pr-3 mono text-[11px]">{d.meta.name}</td>
<td className="py-1.5 px-3 text-[11px] text-ink-3">{d.meta.venue}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{fmtInt(d.meta.traces)}</td>
<td className="py-1.5 px-3 text-right mono text-[11px]">{ours?.states ? ours.states - 1 : '—'}</td>
<td className="py-1.5 pl-3 text-right mono text-[11px]" style={{ color: PALETTE.rust, fontWeight: 600 }}>
{ours?.states ?? '—'}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
)
}
// ---------- Shared Components ----------
function Figure({ label, value, note, accent }: {
label: string; value: string; note?: string; accent?: boolean
}) {
return (
<div className="py-4 pr-6 border-r border-rule last:border-r-0 first:pl-0 pl-6">
<div className="text-[10px] text-ink-3 uppercase tracking-widest mb-1">{label}</div>
<div className={cn('text-[26px] font-light mono leading-none', accent ? 'text-rust' : 'text-ink')}>
{value}
</div>
{note && <div className="text-[11px] text-ink-4 mt-1">{note}</div>}
</div>
)
}
function SectionRule({ title }: { title: string }) {
return (
<div className="flex items-baseline gap-4 thick-rule pb-1">
<h3 className="text-[11px] text-ink-3 uppercase tracking-widest font-medium shrink-0">{title}</h3>
</div>
)
}
function Observation({ num, title, body }: { num: string; title: string; body: string }) {
return (
<div className="ruled-top pt-4">
<div className="flex items-baseline gap-2 mb-1.5">
<span className="serif italic text-rust text-[14px]">{num}.</span>
<span className="text-[13px] font-medium">{title}</span>
</div>
<p className="text-[12px] text-ink-2 leading-relaxed">{body}</p>
</div>
)
}
// ---------- Layout (force-directed simulation) ----------
function computeLayout(
states: Array<{ id: string }>,
transitions: Array<{ source: string; target: string }>
): Record<string, { x: number; y: number }> {
const n = states.length
if (n === 0) return {}
const W = 600, H = 400
const cx = W / 2, cy = H / 2
// Init: circular layout as starting point
const pos: Record<string, { x: number; y: number }> = {}
const vel: Record<string, { x: number; y: number }> = {}
const initIdx = states.findIndex(s => s.id === 'init')
const r0 = Math.min(160, n * 22)
states.forEach((s, i) => {
const idx = initIdx >= 0 ? (i - initIdx + n) % n : i
const angle = (idx / n) * 2 * Math.PI - Math.PI / 2
pos[s.id] = { x: cx + r0 * Math.cos(angle), y: cy + r0 * Math.sin(angle) }
vel[s.id] = { x: 0, y: 0 }
})
// Build adjacency for attraction
const adj = new Map<string, Set<string>>()
for (const s of states) adj.set(s.id, new Set())
for (const t of transitions) {
if (t.source !== t.target) {
adj.get(t.source)?.add(t.target)
adj.get(t.target)?.add(t.source)
}
}
// In-degree for sizing
const inDeg: Record<string, number> = {}
for (const t of transitions) inDeg[t.target] = (inDeg[t.target] || 0) + 1
// Simulate
const REPULSION = 8000
const ATTRACTION = 0.008
const DAMPING = 0.85
const ITERS = 200
for (let iter = 0; iter < ITERS; iter++) {
const temp = 1 - iter / ITERS // cooling
for (const a of states) {
let fx = 0, fy = 0
// Repulsion from all other nodes
for (const b of states) {
if (a.id === b.id) continue
const dx = pos[a.id].x - pos[b.id].x
const dy = pos[a.id].y - pos[b.id].y
const d2 = dx * dx + dy * dy + 1
const f = REPULSION / d2
fx += f * dx / Math.sqrt(d2)
fy += f * dy / Math.sqrt(d2)
}
// Attraction along edges
const neighbors = adj.get(a.id)
if (neighbors) {
for (const bId of neighbors) {
const dx = pos[bId].x - pos[a.id].x
const dy = pos[bId].y - pos[a.id].y
const d = Math.sqrt(dx * dx + dy * dy)
const idealDist = 100
const f = ATTRACTION * (d - idealDist)
fx += f * dx / (d + 0.1)
fy += f * dy / (d + 0.1)
}
}
// Center gravity
fx += (cx - pos[a.id].x) * 0.001
fy += (cy - pos[a.id].y) * 0.001
vel[a.id].x = (vel[a.id].x + fx) * DAMPING * temp
vel[a.id].y = (vel[a.id].y + fy) * DAMPING * temp
}
// Apply velocities + clamp
for (const s of states) {
pos[s.id].x = Math.max(60, Math.min(W - 60, pos[s.id].x + vel[s.id].x))
pos[s.id].y = Math.max(60, Math.min(H - 60, pos[s.id].y + vel[s.id].y))
}
}
return pos
}