@fede0089/skill-eval 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/dist/commands/functional.js +5 -8
- package/dist/commands/trigger.js +4 -7
- package/dist/core/anomalies.js +63 -0
- package/dist/core/eval-runner.js +28 -12
- package/dist/core/statistics.js +11 -8
- package/dist/core/trial-utils.js +45 -1
- package/dist/index.js +1 -1
- package/dist/reporters/html-reporter.js +970 -324
- package/dist/reporters/index.js +0 -1
- package/dist/utils/ndjson.js +19 -0
- package/package.json +1 -1
- package/dist/reporters/json-reporter.js +0 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { detectAnomalies } from '../core/anomalies.js';
|
|
5
|
+
import { hasFunctionalBaseline } from '../utils/table-renderer.js';
|
|
6
6
|
export class HtmlReporter {
|
|
7
7
|
generate(report, runDir) {
|
|
8
8
|
const htmlPath = path.join(runDir, 'report.html');
|
|
@@ -10,384 +10,1030 @@ export class HtmlReporter {
|
|
|
10
10
|
Logger.write(`\n Report: file://${htmlPath}\n`);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
.
|
|
22
|
-
.
|
|
13
|
+
function toRunTrial(trial, cohort) {
|
|
14
|
+
return {
|
|
15
|
+
id: trial.id,
|
|
16
|
+
passed: trial.trialPassed,
|
|
17
|
+
isError: trial.isError === true,
|
|
18
|
+
results: trial.assertionResults.map(r => (r.passed ? 1 : 0)),
|
|
19
|
+
reasons: trial.assertionResults.map(r => r.reason),
|
|
20
|
+
tokens: trial.tokenStats ? { totalTokens: trial.tokenStats.totalTokens } : undefined,
|
|
21
|
+
durationMs: trial.durationMs,
|
|
22
|
+
output: trial.summary?.output ?? '',
|
|
23
|
+
outputLen: trial.summary?.outputLen ?? 0,
|
|
24
|
+
toolCalls: trial.summary?.toolCalls ?? 0,
|
|
25
|
+
stopStatus: trial.summary?.stopStatus,
|
|
26
|
+
logFile: trial.summary?.logFile,
|
|
27
|
+
anomalies: detectAnomalies(trial, cohort),
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (val >= 0.5)
|
|
31
|
-
return 'amber';
|
|
32
|
-
return 'red';
|
|
30
|
+
/** Variant order, baseline first so the comparison reads left to right. */
|
|
31
|
+
function variantsOf(result, functional) {
|
|
32
|
+
const skillVersions = Object.keys(result.skillTrials);
|
|
33
|
+
return functional && (result.baselineTrials?.length ?? 0) > 0
|
|
34
|
+
? ['baseline', ...skillVersions]
|
|
35
|
+
: skillVersions;
|
|
33
36
|
}
|
|
34
|
-
function
|
|
35
|
-
return
|
|
37
|
+
function trialsFor(result, variant) {
|
|
38
|
+
return variant === 'baseline' ? result.baselineTrials : result.skillTrials[variant] ?? [];
|
|
36
39
|
}
|
|
37
|
-
|
|
38
|
-
// Metrics Grid
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
function renderDeltaCell(base, target, format, cellClass) {
|
|
41
|
-
if (base <= 0)
|
|
42
|
-
return `<span class="metric-val muted">—</span>`;
|
|
43
|
-
const delta = target - base;
|
|
44
|
-
const sign = delta >= 0 ? '+' : '';
|
|
45
|
-
const pct = Math.round((delta / base) * 100);
|
|
46
|
-
const cls = delta > 0 ? 'amber' : delta < 0 ? 'green' : '';
|
|
47
|
-
return `<span class="${cellClass} ${cls}">${sign}${pct}%</span>`;
|
|
48
|
-
}
|
|
49
|
-
function renderMetricsGrid(report) {
|
|
50
|
-
const { metrics, results } = report;
|
|
40
|
+
export function buildRunData(report) {
|
|
51
41
|
const functional = isFunctional(report);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
${timeRows}
|
|
85
|
-
</tbody>
|
|
86
|
-
</table>
|
|
87
|
-
</div>`;
|
|
42
|
+
return {
|
|
43
|
+
// Matches the run directory name, so a reviewer's exclusions stay tied to
|
|
44
|
+
// this run and not to any other report opened from the same browser.
|
|
45
|
+
runId: report.timestamp.replace(/[:.]/g, '-'),
|
|
46
|
+
command: report.command ?? (functional ? 'functional' : 'trigger'),
|
|
47
|
+
skill: report.skill_name,
|
|
48
|
+
agent: report.agent,
|
|
49
|
+
timestamp: report.timestamp,
|
|
50
|
+
tasks: report.results.map(result => {
|
|
51
|
+
const variants = variantsOf(result, functional);
|
|
52
|
+
let assertions = [];
|
|
53
|
+
for (const v of variants) {
|
|
54
|
+
const trials = trialsFor(result, v);
|
|
55
|
+
if (trials.length > 0 && trials[0].assertionResults.length > 0) {
|
|
56
|
+
assertions = trials[0].assertionResults.map(r => r.assertion);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const byVariant = {};
|
|
61
|
+
for (const v of variants) {
|
|
62
|
+
const trials = trialsFor(result, v);
|
|
63
|
+
byVariant[v] = trials.map(t => toRunTrial(t, trials));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
taskId: result.taskId,
|
|
67
|
+
prompt: result.prompt,
|
|
68
|
+
shouldTrigger: result.shouldTrigger,
|
|
69
|
+
assertions,
|
|
70
|
+
variants: byVariant,
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
88
74
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
function renderExpectationCell(trials, expIdx, variant, taskId, clickable, totalCols) {
|
|
93
|
-
const passed = trials.filter(t => t.assertionResults[expIdx]?.passed).length;
|
|
94
|
-
const total = trials.length;
|
|
95
|
-
const rate = total > 0 ? passed / total : 0;
|
|
96
|
-
const colorCls = passColorClass(rate);
|
|
97
|
-
const ratePct = formatPercent(rate);
|
|
98
|
-
const frac = `${passed} / ${total}`;
|
|
99
|
-
const detailId = `exp-detail-${taskId}-${expIdx}-${variant}`;
|
|
100
|
-
const dataAttr = clickable ? ` data-detail="${escapeHtml(detailId)}"` : '';
|
|
101
|
-
const cell = `<td class="pass-cell ${colorCls}"${dataAttr}><span class="rate">${ratePct}</span><span class="frac">${frac}</span></td>`;
|
|
102
|
-
if (!clickable)
|
|
103
|
-
return { cell, detail: '' };
|
|
104
|
-
const lines = trials.map(t => {
|
|
105
|
-
const r = t.assertionResults[expIdx];
|
|
106
|
-
const ok = !!r?.passed;
|
|
107
|
-
const reason = r?.reason ?? '';
|
|
108
|
-
const icon = ok ? '✓' : '✗';
|
|
109
|
-
const iconCls = ok ? 'pass' : 'fail';
|
|
110
|
-
return `<div class="exp-trial-line"><div class="exp-trial-icon ${iconCls}">${icon}</div><div class="exp-trial-body"><span class="trial-label">Trial ${t.id}</span><span class="trial-reason">${escapeHtml(reason)}</span></div></div>`;
|
|
111
|
-
}).join('');
|
|
112
|
-
const detail = `<tr class="exp-detail-row" id="${escapeHtml(detailId)}"><td colspan="${totalCols}"><div class="exp-detail-inner"><div class="exp-detail-header">Judge per trial · <span class="variant-pill">${escapeHtml(variant)}</span></div>${lines}</div></td></tr>`;
|
|
113
|
-
return { cell, detail };
|
|
75
|
+
function isFunctional(report) {
|
|
76
|
+
return report.command === 'functional' || hasFunctionalBaseline(report);
|
|
114
77
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
if (canonical.length === 0) {
|
|
127
|
-
return '<p class="muted">No assertions recorded.</p>';
|
|
128
|
-
}
|
|
129
|
-
const totalCols = 1 + allVersions.length;
|
|
130
|
-
const headerCells = allVersions.map(v => `<th class="variant-col">${escapeHtml(v)}</th>`).join('');
|
|
131
|
-
const rows = canonical.map((a, expIdx) => {
|
|
132
|
-
const cells = [];
|
|
133
|
-
const details = [];
|
|
134
|
-
for (const v of allVersions) {
|
|
135
|
-
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
136
|
-
const { cell, detail } = renderExpectationCell(trials, expIdx, v, result.taskId, isFunctionalEval, totalCols);
|
|
137
|
-
cells.push(cell);
|
|
138
|
-
if (detail)
|
|
139
|
-
details.push(detail);
|
|
140
|
-
}
|
|
141
|
-
return `<tr class="exp-row"><td class="exp-text">${escapeHtml(a.assertion)}</td>${cells.join('')}</tr>${details.join('')}`;
|
|
142
|
-
}).join('');
|
|
143
|
-
return `<div class="expectations-table-wrap">
|
|
144
|
-
<table class="expectations-table">
|
|
145
|
-
<thead><tr><th class="exp-col">Expectation</th>${headerCells}</tr></thead>
|
|
146
|
-
<tbody>${rows}</tbody>
|
|
147
|
-
</table>
|
|
148
|
-
</div>`;
|
|
149
|
-
}
|
|
150
|
-
function avgTrialTokens(trials) {
|
|
151
|
-
const withStats = trials.filter(t => t.tokenStats != null);
|
|
152
|
-
if (withStats.length === 0)
|
|
153
|
-
return null;
|
|
154
|
-
return Math.round(withStats.reduce((s, t) => s + t.tokenStats.totalTokens, 0) / withStats.length);
|
|
155
|
-
}
|
|
156
|
-
function avgTrialDuration(trials) {
|
|
157
|
-
const withDuration = trials.filter(t => t.durationMs != null);
|
|
158
|
-
if (withDuration.length === 0)
|
|
159
|
-
return null;
|
|
160
|
-
return Math.round(withDuration.reduce((s, t) => s + t.durationMs, 0) / withDuration.length);
|
|
161
|
-
}
|
|
162
|
-
function renderTaskMiniGrid(result, isFunctionalEval) {
|
|
163
|
-
const skillVersions = Object.keys(result.skillTrials);
|
|
164
|
-
const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
|
|
165
|
-
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
166
|
-
const successRows = `<tr>
|
|
167
|
-
<td>Success Rate</td>
|
|
168
|
-
${allVersions.map(v => {
|
|
169
|
-
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
170
|
-
const rate = trials.length ? Math.round(computeAssertionPassRate(trials) * 100) : 0;
|
|
171
|
-
return `<td><span class="metric-val-sm ${passColorClass(rate / 100)}">${rate}%</span></td>`;
|
|
172
|
-
}).join('')}
|
|
173
|
-
</tr>`;
|
|
174
|
-
const tokenRows = `<tr>
|
|
175
|
-
<td>Tokens (avg)</td>
|
|
176
|
-
${allVersions.map(v => {
|
|
177
|
-
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
178
|
-
const tokens = avgTrialTokens(trials);
|
|
179
|
-
return `<td>${tokens != null ? `<span class="metric-val-sm">${formatTokens(tokens)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>`;
|
|
180
|
-
}).join('')}
|
|
181
|
-
</tr>`;
|
|
182
|
-
const timeRows = `<tr>
|
|
183
|
-
<td>Time (avg)</td>
|
|
184
|
-
${allVersions.map(v => {
|
|
185
|
-
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
186
|
-
const ms = avgTrialDuration(trials);
|
|
187
|
-
return `<td>${ms != null ? `<span class="metric-val-sm">${formatDuration(ms)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>`;
|
|
188
|
-
}).join('')}
|
|
189
|
-
</tr>`;
|
|
190
|
-
return `<div class="metrics-grid-sm">
|
|
191
|
-
<table>
|
|
192
|
-
<thead>
|
|
193
|
-
<tr><th></th>${headerCells}</tr>
|
|
194
|
-
</thead>
|
|
195
|
-
<tbody>
|
|
196
|
-
${successRows}
|
|
197
|
-
${tokenRows}
|
|
198
|
-
${timeRows}
|
|
199
|
-
</tbody>
|
|
200
|
-
</table>
|
|
201
|
-
</div>`;
|
|
202
|
-
}
|
|
203
|
-
function renderTaskDetails(result, isFunctionalEval) {
|
|
204
|
-
const sections = [];
|
|
205
|
-
sections.push(renderTaskMiniGrid(result, isFunctionalEval));
|
|
206
|
-
sections.push(renderExpectationsTable(result, isFunctionalEval));
|
|
207
|
-
return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
|
|
78
|
+
/**
|
|
79
|
+
* Serialises the run for embedding in a script tag. Agent output is arbitrary
|
|
80
|
+
* text and routinely contains markup, so every "<" is escaped: an unescaped
|
|
81
|
+
* "</script>" in a transcript would end the tag and break the page.
|
|
82
|
+
*/
|
|
83
|
+
function serializeRunData(data) {
|
|
84
|
+
return JSON.stringify(data)
|
|
85
|
+
.replace(/</g, '\\u003c')
|
|
86
|
+
.replace(/\u2028/g, '\\u2028')
|
|
87
|
+
.replace(/\u2029/g, '\\u2029');
|
|
208
88
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const rows = results.map(result => {
|
|
217
|
-
const prompt = escapeHtml(result.prompt);
|
|
218
|
-
// Negative trigger evals assert the opposite outcome — flag them so the row is not misread.
|
|
219
|
-
const badge = result.shouldTrigger === false ? '<span class="badge-neg">no-trigger</span>' : '';
|
|
220
|
-
const detailsBtn = `<button class="details-btn" data-target="details-${result.taskId}">▶</button>`;
|
|
221
|
-
const detailsRow = `<tr class="details-row"><td colspan="3">${renderTaskDetails(result, functional)}</td></tr>`;
|
|
222
|
-
return `<tr><td>${result.taskId}</td><td class="prompt-cell">${badge}${prompt}</td><td>${detailsBtn}</td></tr>${detailsRow}`;
|
|
223
|
-
}).join('');
|
|
224
|
-
return `<div class="table-wrap"><table><thead>${headerRow}</thead><tbody>${rows}</tbody></table></div>`;
|
|
89
|
+
function escapeHtml(s) {
|
|
90
|
+
return s
|
|
91
|
+
.replace(/&/g, '&')
|
|
92
|
+
.replace(/</g, '<')
|
|
93
|
+
.replace(/>/g, '>')
|
|
94
|
+
.replace(/"/g, '"')
|
|
95
|
+
.replace(/'/g, ''');
|
|
225
96
|
}
|
|
226
97
|
// ---------------------------------------------------------------------------
|
|
227
|
-
//
|
|
98
|
+
// Document
|
|
228
99
|
// ---------------------------------------------------------------------------
|
|
229
100
|
export function generateHtml(report) {
|
|
230
|
-
const
|
|
231
|
-
const functional = isFunctional(report);
|
|
232
|
-
const evalType = functional ? 'Functional' : 'Trigger';
|
|
233
|
-
const overallScore = metrics.passAtK['local'] ?? 0;
|
|
234
|
-
const statusClass = passColorClass(overallScore);
|
|
235
|
-
const formattedDate = new Date(timestamp).toLocaleString();
|
|
101
|
+
const data = buildRunData(report);
|
|
236
102
|
return `<!DOCTYPE html>
|
|
237
103
|
<html lang="en">
|
|
238
104
|
<head>
|
|
239
105
|
<meta charset="UTF-8">
|
|
240
106
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
241
|
-
<title>Skill Eval — ${escapeHtml(skill_name)}</title>
|
|
107
|
+
<title>Skill Eval — ${escapeHtml(report.skill_name)}</title>
|
|
242
108
|
<style>
|
|
243
109
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
110
|
+
|
|
111
|
+
:root {
|
|
112
|
+
--ground: #f8fafc;
|
|
113
|
+
--surface: #ffffff;
|
|
114
|
+
--surface-sunk: #f8fafc;
|
|
115
|
+
--ink: #1e293b;
|
|
116
|
+
--ink-soft: #334155;
|
|
117
|
+
--muted: #64748b;
|
|
118
|
+
--muted-light: #94a3b8;
|
|
119
|
+
--border: #e2e8f0;
|
|
120
|
+
--border-soft: #f1f5f9;
|
|
121
|
+
--green: #16a34a;
|
|
122
|
+
--green-soft: #86efac;
|
|
123
|
+
--amber: #d97706;
|
|
124
|
+
--amber-bg: #fffbeb;
|
|
125
|
+
--amber-border: #fcd34d;
|
|
126
|
+
--amber-soft: #fcd34d;
|
|
127
|
+
--red: #dc2626;
|
|
128
|
+
--red-soft: #fca5a5;
|
|
129
|
+
--blue: #3b82f6;
|
|
130
|
+
--blue-bg: #e0f2fe;
|
|
131
|
+
--blue-ink: #0369a1;
|
|
132
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
133
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
body {
|
|
137
|
+
font-family: var(--sans);
|
|
138
|
+
background: var(--ground);
|
|
139
|
+
color: var(--ink);
|
|
140
|
+
font-size: 14px;
|
|
141
|
+
line-height: 1.5;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.container { max-width: 1000px; margin: 0 auto; padding: 20px 16px 64px; }
|
|
145
|
+
|
|
146
|
+
/* ── Header ─────────────────────────────────────────────────── */
|
|
147
|
+
.header { background: var(--ink); color: #f1f5f9; padding: 24px 28px; border-radius: 10px; margin-bottom: 20px; }
|
|
148
|
+
.header h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; letter-spacing: -0.01em; }
|
|
149
|
+
.header-meta { display: flex; gap: 24px; flex-wrap: wrap; font-size: 13px; color: var(--muted-light); }
|
|
150
|
+
.header-meta span b { color: #e2e8f0; font-weight: 600; }
|
|
151
|
+
.status-bar { height: 4px; border-radius: 2px; margin-top: 16px; background: var(--red); transition: background 0.2s; }
|
|
256
152
|
.status-bar.green { background: #22c55e; }
|
|
257
153
|
.status-bar.amber { background: #f59e0b; }
|
|
258
|
-
.status-bar.red
|
|
154
|
+
.status-bar.red { background: #ef4444; }
|
|
155
|
+
|
|
156
|
+
/* ── Exclusion bar ──────────────────────────────────────────── */
|
|
157
|
+
.excl-bar {
|
|
158
|
+
display: none; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
159
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
160
|
+
border-left: 3px solid var(--muted); border-radius: 8px;
|
|
161
|
+
padding: 12px 16px; margin-bottom: 20px;
|
|
162
|
+
}
|
|
163
|
+
.excl-bar.active { display: flex; }
|
|
164
|
+
.excl-bar.warn { border-left-color: var(--amber); background: var(--amber-bg); border-color: var(--amber-border); }
|
|
165
|
+
.excl-bar .excl-icon { font-size: 15px; color: var(--muted); }
|
|
166
|
+
.excl-bar.warn .excl-icon { color: var(--amber); }
|
|
167
|
+
.excl-headline { font-size: 13px; font-weight: 600; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
|
|
168
|
+
.excl-split { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
169
|
+
.excl-warn-msg { flex-basis: 100%; font-size: 12px; color: #92400e; line-height: 1.5; }
|
|
170
|
+
.excl-warn-msg:empty { display: none; }
|
|
171
|
+
.excl-actions { margin-left: auto; display: flex; gap: 8px; }
|
|
172
|
+
|
|
173
|
+
.btn {
|
|
174
|
+
font-family: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
|
|
175
|
+
padding: 5px 11px; border-radius: 5px; border: 1px solid var(--border);
|
|
176
|
+
background: var(--surface); color: var(--muted); cursor: pointer; white-space: nowrap;
|
|
177
|
+
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
|
178
|
+
}
|
|
179
|
+
.btn:hover { background: var(--border-soft); color: var(--ink-soft); }
|
|
180
|
+
.btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
181
|
+
.btn.primary { background: var(--ink); border-color: var(--ink); color: #f1f5f9; }
|
|
182
|
+
.btn.primary:hover { background: #0f172a; color: #fff; }
|
|
183
|
+
.btn.danger { color: var(--red); border-color: #fecaca; }
|
|
184
|
+
.btn.danger:hover { background: #fef2f2; color: #b91c1c; }
|
|
259
185
|
|
|
260
|
-
/* Metrics
|
|
261
|
-
.metrics-grid { background:
|
|
186
|
+
/* ── Metrics grid ───────────────────────────────────────────── */
|
|
187
|
+
.metrics-grid { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 20px; }
|
|
262
188
|
.metrics-grid table { width: 100%; border-collapse: collapse; }
|
|
263
|
-
.metrics-grid thead th {
|
|
189
|
+
.metrics-grid thead th {
|
|
190
|
+
background: var(--surface-sunk); font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
191
|
+
letter-spacing: 0.06em; color: var(--muted); padding: 10px 24px; text-align: right;
|
|
192
|
+
border-bottom: 2px solid var(--border); white-space: nowrap;
|
|
193
|
+
}
|
|
264
194
|
.metrics-grid thead th:first-child { text-align: left; min-width: 120px; }
|
|
265
|
-
.metrics-grid tbody td { padding: 14px 24px; border-bottom: 1px solid
|
|
195
|
+
.metrics-grid tbody td { padding: 14px 24px; border-bottom: 1px solid var(--border-soft); text-align: right; vertical-align: middle; }
|
|
266
196
|
.metrics-grid tbody tr:last-child td { border-bottom: none; }
|
|
267
|
-
.metrics-grid tbody td:first-child {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.
|
|
273
|
-
.
|
|
274
|
-
.
|
|
275
|
-
.metrics-grid-sm thead th:first-child { text-align: left; }
|
|
276
|
-
.metrics-grid-sm tbody td { padding: 8px 14px; border-bottom: 1px solid #f1f5f9; text-align: right; vertical-align: middle; }
|
|
277
|
-
.metrics-grid-sm tbody tr:last-child td { border-bottom: none; }
|
|
278
|
-
.metrics-grid-sm tbody td:first-child { text-align: left; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #94a3b8; white-space: nowrap; }
|
|
279
|
-
.metric-val-sm { font-size: 15px; font-weight: 700; line-height: 1; display: block; }
|
|
280
|
-
|
|
281
|
-
/* Color utilities */
|
|
282
|
-
.green { color: #16a34a; }
|
|
283
|
-
.amber { color: #d97706; }
|
|
284
|
-
.red { color: #dc2626; }
|
|
197
|
+
.metrics-grid tbody td:first-child {
|
|
198
|
+
text-align: left; font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
199
|
+
letter-spacing: 0.06em; color: var(--muted); white-space: nowrap;
|
|
200
|
+
}
|
|
201
|
+
.metric-val { font-size: 24px; font-weight: 700; line-height: 1; display: block; font-variant-numeric: tabular-nums; }
|
|
202
|
+
.metric-sub { font-size: 11px; color: var(--muted-light); margin-top: 5px; font-variant-numeric: tabular-nums; }
|
|
203
|
+
.metric-sub s { text-decoration-color: #cbd5e1; }
|
|
204
|
+
.metric-sub .lowconf { color: var(--amber); font-weight: 600; }
|
|
285
205
|
|
|
286
|
-
|
|
287
|
-
.
|
|
288
|
-
.
|
|
206
|
+
.green { color: var(--green); }
|
|
207
|
+
.amber { color: var(--amber); }
|
|
208
|
+
.red { color: var(--red); }
|
|
209
|
+
.muted { color: var(--muted-light); }
|
|
289
210
|
|
|
290
|
-
/*
|
|
211
|
+
/* ── Section + task table ───────────────────────────────────── */
|
|
212
|
+
.section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
|
|
213
|
+
.section-title {
|
|
214
|
+
font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em;
|
|
215
|
+
color: var(--muted); padding: 12px 16px; border-bottom: 1px solid var(--border-soft);
|
|
216
|
+
}
|
|
291
217
|
.table-wrap { overflow-x: auto; }
|
|
292
|
-
table { width: 100%; border-collapse: collapse; }
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
.details-btn
|
|
303
|
-
|
|
304
|
-
|
|
218
|
+
.task-table { width: 100%; border-collapse: collapse; }
|
|
219
|
+
.task-table > thead th {
|
|
220
|
+
background: var(--surface-sunk); font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
221
|
+
letter-spacing: 0.05em; color: var(--muted); padding: 10px 12px; text-align: left;
|
|
222
|
+
border-bottom: 1px solid var(--border);
|
|
223
|
+
}
|
|
224
|
+
.task-table > tbody > tr > td { padding: 10px 12px; border-bottom: 1px solid var(--border-soft); vertical-align: top; }
|
|
225
|
+
.task-table > tbody > tr:last-child > td { border-bottom: none; }
|
|
226
|
+
.prompt-cell { max-width: 560px; word-break: break-word; color: var(--ink-soft); }
|
|
227
|
+
|
|
228
|
+
.details-btn {
|
|
229
|
+
background: none; border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
|
|
230
|
+
padding: 2px 8px; font-size: 11px; color: var(--muted); font-family: inherit; transition: background 0.15s;
|
|
231
|
+
}
|
|
232
|
+
.details-btn:hover { background: var(--border-soft); }
|
|
233
|
+
.details-btn.open { color: var(--blue); border-color: var(--blue); }
|
|
234
|
+
.details-btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
235
|
+
.details-row > td { padding: 0 !important; background: var(--surface-sunk); }
|
|
236
|
+
.task-details { display: none; padding: 16px; }
|
|
305
237
|
.task-details.visible { display: block; }
|
|
306
238
|
|
|
307
|
-
/*
|
|
308
|
-
.
|
|
239
|
+
/* ── Subsections: Summary / Trials / Expectations ───────────── */
|
|
240
|
+
.subsection + .subsection { margin-top: 20px; }
|
|
241
|
+
.subsection-title {
|
|
242
|
+
display: flex; align-items: center; gap: 10px; margin-bottom: 9px;
|
|
243
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
|
244
|
+
}
|
|
245
|
+
.subsection-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
|
246
|
+
.subsection-title .st-count {
|
|
247
|
+
font-size: 11.5px; font-weight: 500; text-transform: none; letter-spacing: 0;
|
|
248
|
+
color: var(--muted-light); font-variant-numeric: tabular-nums;
|
|
249
|
+
}
|
|
250
|
+
.subsection-title .st-count .flagged { color: var(--amber); font-weight: 600; }
|
|
251
|
+
|
|
252
|
+
/* ── Trials table ───────────────────────────────────────────── */
|
|
253
|
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
254
|
+
.trials-table { width: 100%; border-collapse: collapse; }
|
|
255
|
+
|
|
256
|
+
.trials-table thead th {
|
|
257
|
+
background: var(--border-soft); font-size: 10px; font-weight: 700; text-transform: uppercase;
|
|
258
|
+
letter-spacing: 0.06em; color: #475569; padding: 8px 12px; text-align: left;
|
|
259
|
+
border-bottom: 1px solid var(--border); white-space: nowrap;
|
|
260
|
+
}
|
|
261
|
+
.trials-table th.num, .trials-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
262
|
+
.trials-table th.act, .trials-table td.act { text-align: right; }
|
|
263
|
+
|
|
264
|
+
.variant-row > td {
|
|
265
|
+
background: var(--surface-sunk); padding: 7px 12px;
|
|
266
|
+
border-bottom: 1px solid var(--border); border-top: 1px solid var(--border);
|
|
267
|
+
}
|
|
268
|
+
.trials-table tbody tr.variant-row:first-child > td { border-top: none; }
|
|
269
|
+
.variant-row .variant-name {
|
|
270
|
+
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
|
|
271
|
+
color: var(--blue-ink); background: var(--blue-bg); padding: 2px 8px; border-radius: 4px;
|
|
272
|
+
}
|
|
273
|
+
.variant-row .variant-meta { font-size: 11.5px; color: var(--muted); margin-left: 9px; font-variant-numeric: tabular-nums; }
|
|
274
|
+
.variant-row .variant-meta .off { color: var(--ink-soft); font-weight: 600; }
|
|
275
|
+
|
|
276
|
+
.trial-row > td { padding: 9px 12px; border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
|
|
277
|
+
.trial-row { cursor: pointer; transition: background 0.12s; }
|
|
278
|
+
.trial-row:hover > td { background: #fbfcfd; }
|
|
279
|
+
.trial-row.open > td { background: var(--blue-bg); }
|
|
280
|
+
|
|
281
|
+
.tr-toggle {
|
|
282
|
+
background: none; border: none; cursor: pointer; font-size: 9px; color: var(--muted-light);
|
|
283
|
+
font-family: inherit; padding: 2px 4px; line-height: 1;
|
|
284
|
+
}
|
|
285
|
+
.tr-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; border-radius: 3px; }
|
|
286
|
+
.trial-row.open .tr-toggle { color: var(--blue); }
|
|
287
|
+
|
|
288
|
+
.tr-name { font-size: 12.5px; font-weight: 600; color: var(--ink-soft); white-space: nowrap; }
|
|
289
|
+
.tr-score .frac { font-size: 13px; font-weight: 700; line-height: 1.15; display: block; font-variant-numeric: tabular-nums; }
|
|
290
|
+
.tr-score .bar { display: block; width: 62px; height: 3px; background: var(--border); border-radius: 2px; margin-top: 4px; overflow: hidden; }
|
|
291
|
+
.tr-score .bar i { display: block; height: 100%; border-radius: 2px; }
|
|
292
|
+
.tr-score .bar i.green { background: var(--green); }
|
|
293
|
+
.tr-score .bar i.amber { background: var(--amber-soft); }
|
|
294
|
+
.tr-score .bar i.red { background: var(--red-soft); }
|
|
295
|
+
|
|
296
|
+
.tr-flags { line-height: 1.9; }
|
|
297
|
+
.flag-chip {
|
|
298
|
+
display: inline-block; font-size: 10.5px; font-weight: 600; white-space: nowrap;
|
|
299
|
+
color: #92400e; background: var(--amber-bg); border: 1px solid var(--amber-border);
|
|
300
|
+
border-radius: 4px; padding: 1px 7px; margin-right: 5px;
|
|
301
|
+
}
|
|
302
|
+
.tr-flags .clean { font-size: 11.5px; color: var(--muted-light); }
|
|
303
|
+
.excl-note { font-size: 11.5px; color: var(--muted-light); font-style: italic; }
|
|
304
|
+
.tr-num { font-size: 12px; color: var(--muted); white-space: nowrap; }
|
|
305
|
+
|
|
306
|
+
/* Excluded state: absence, not failure — desaturate, don't recolor. */
|
|
307
|
+
.trial-row.excluded > td {
|
|
308
|
+
background: repeating-linear-gradient(135deg, #fbfcfd, #fbfcfd 6px, var(--surface-sunk) 6px, var(--surface-sunk) 12px);
|
|
309
|
+
}
|
|
310
|
+
.trial-row.excluded:hover > td { background: repeating-linear-gradient(135deg, #f8fafc, #f8fafc 6px, var(--border-soft) 6px, var(--border-soft) 12px); }
|
|
311
|
+
.trial-row.excluded .tr-name,
|
|
312
|
+
.trial-row.excluded .tr-num { color: var(--muted-light); }
|
|
313
|
+
.trial-row.excluded .tr-score .frac { color: var(--muted-light); text-decoration: line-through; text-decoration-thickness: 1.5px; }
|
|
314
|
+
.trial-row.excluded .tr-score .bar i { background: #cbd5e1; }
|
|
315
|
+
.trial-row.excluded .flag-chip { color: var(--muted); background: var(--border-soft); border-color: var(--border); }
|
|
316
|
+
.excl-chip {
|
|
317
|
+
display: inline-block; font-size: 10.5px; font-weight: 700; white-space: nowrap;
|
|
318
|
+
color: #475569; background: #e2e8f0; border-radius: 4px; padding: 1px 7px; margin-right: 5px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* ── Trial detail row ───────────────────────────────────────── */
|
|
322
|
+
.trial-detail > td { padding: 0; background: var(--surface-sunk); border-bottom: 1px solid var(--border); }
|
|
323
|
+
.trial-detail-inner { padding: 13px 16px 15px; border-left: 3px solid var(--blue); }
|
|
324
|
+
|
|
325
|
+
.anomaly-row { display: flex; gap: 9px; align-items: baseline; padding: 5px 0; }
|
|
326
|
+
.anomaly-row + .anomaly-row { border-top: 1px dashed var(--border); }
|
|
327
|
+
.anomaly-why { font-size: 12px; color: var(--muted); line-height: 1.5; }
|
|
328
|
+
.no-anomaly { font-size: 12px; color: var(--muted-light); }
|
|
329
|
+
|
|
330
|
+
.td-label {
|
|
331
|
+
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
|
|
332
|
+
color: var(--muted); margin: 14px 0 6px;
|
|
333
|
+
}
|
|
334
|
+
.output-box {
|
|
335
|
+
font-family: var(--mono); font-size: 11.5px; line-height: 1.6; color: var(--ink-soft);
|
|
336
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 5px;
|
|
337
|
+
padding: 10px 12px; max-height: 230px; overflow: auto; white-space: pre-wrap; word-break: break-word;
|
|
338
|
+
}
|
|
339
|
+
.output-box.degenerate { color: var(--red); background: #fef2f2; border-color: #fecaca; }
|
|
340
|
+
.output-trunc { font-size: 10.5px; color: var(--muted-light); margin-top: 5px; }
|
|
341
|
+
.td-stats { font-size: 11.5px; color: var(--muted); margin-top: 12px; font-variant-numeric: tabular-nums; }
|
|
342
|
+
.td-stats b { color: var(--ink-soft); font-weight: 600; }
|
|
343
|
+
.td-log { font-size: 11.5px; color: var(--muted); margin-top: 5px; }
|
|
344
|
+
.td-log a { color: var(--blue); font-family: var(--mono); font-size: 11px; }
|
|
345
|
+
|
|
346
|
+
.reason-bar {
|
|
347
|
+
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
348
|
+
margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--border);
|
|
349
|
+
font-size: 11.5px; color: var(--muted);
|
|
350
|
+
}
|
|
351
|
+
select, input[type="text"] {
|
|
352
|
+
font-family: inherit; font-size: 11.5px; color: var(--ink-soft); background: var(--surface);
|
|
353
|
+
border: 1px solid var(--border); border-radius: 5px; padding: 4px 7px;
|
|
354
|
+
}
|
|
355
|
+
select:focus-visible, input:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
|
|
356
|
+
input[type="text"] { width: 190px; }
|
|
357
|
+
|
|
358
|
+
/* ── Expectations table ─────────────────────────────────────── */
|
|
309
359
|
.expectations-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
310
|
-
.expectations-table thead th {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
360
|
+
.expectations-table thead th {
|
|
361
|
+
background: var(--border-soft); font-size: 10px; font-weight: 700; text-transform: uppercase;
|
|
362
|
+
letter-spacing: 0.06em; color: #475569; padding: 9px 14px; text-align: left;
|
|
363
|
+
border-bottom: 1px solid var(--border);
|
|
364
|
+
}
|
|
365
|
+
.expectations-table thead th.variant-col { width: 106px; text-align: center; }
|
|
366
|
+
.expectations-table tbody td { padding: 10px 14px; border-bottom: 1px solid var(--border-soft); vertical-align: middle; font-size: 12.5px; }
|
|
314
367
|
.expectations-table tbody tr.exp-row:last-of-type > td { border-bottom: none; }
|
|
315
|
-
.exp-text { color:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
.pass-cell
|
|
319
|
-
.pass-cell
|
|
320
|
-
.pass-cell.open { background:
|
|
321
|
-
.pass-cell .rate { display: block; font-size: 13px; font-weight: 700; line-height: 1.
|
|
322
|
-
.pass-cell .frac { display: block; font-size: 10px; font-weight: 600; color:
|
|
323
|
-
.pass-cell.green .rate { color:
|
|
324
|
-
.pass-cell.amber .rate { color:
|
|
325
|
-
.pass-cell.red
|
|
326
|
-
|
|
327
|
-
.exp-detail-row {
|
|
328
|
-
.exp-detail-
|
|
329
|
-
.exp-detail-
|
|
330
|
-
.exp-
|
|
331
|
-
.exp-detail-header { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #64748b; margin-bottom: 8px; }
|
|
332
|
-
.exp-detail-header .variant-pill { display: inline-block; background: #e0f2fe; color: #0369a1; padding: 1px 7px; border-radius: 4px; margin-left: 4px; font-weight: 700; }
|
|
333
|
-
.exp-trial-line { display: flex; gap: 8px; padding: 6px 0; border-top: 1px dashed #e2e8f0; }
|
|
368
|
+
.exp-text { color: var(--ink); line-height: 1.5; word-break: break-word; }
|
|
369
|
+
.exp-text code { font-family: var(--mono); font-size: 11px; background: var(--border-soft); padding: 1px 4px; border-radius: 3px; }
|
|
370
|
+
|
|
371
|
+
.pass-cell { text-align: center; border-left: 1px solid var(--border-soft); cursor: pointer; user-select: none; transition: background 0.12s; }
|
|
372
|
+
.pass-cell:hover { background: var(--border-soft); }
|
|
373
|
+
.pass-cell.open { background: var(--blue-bg); }
|
|
374
|
+
.pass-cell .rate { display: block; font-size: 13px; font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; }
|
|
375
|
+
.pass-cell .frac { display: block; font-size: 10px; font-weight: 600; color: var(--muted-light); margin-top: 2px; font-variant-numeric: tabular-nums; }
|
|
376
|
+
.pass-cell.green .rate { color: var(--green); }
|
|
377
|
+
.pass-cell.amber .rate { color: var(--amber); }
|
|
378
|
+
.pass-cell.red .rate { color: var(--red); }
|
|
379
|
+
|
|
380
|
+
.exp-detail-row > td { padding: 0; background: var(--surface-sunk); border-bottom: 1px solid var(--border-soft); }
|
|
381
|
+
.exp-detail-inner { padding: 12px 14px 14px; border-left: 3px solid var(--blue); }
|
|
382
|
+
.exp-detail-header { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 8px; }
|
|
383
|
+
.exp-trial-line { display: flex; gap: 8px; padding: 6px 0; border-top: 1px dashed var(--border); }
|
|
334
384
|
.exp-trial-line:first-of-type { border-top: none; }
|
|
335
|
-
.exp-trial-
|
|
336
|
-
.exp-trial-icon
|
|
337
|
-
.exp-trial-icon.
|
|
338
|
-
.exp-trial-
|
|
385
|
+
.exp-trial-line.is-excluded { opacity: 0.5; }
|
|
386
|
+
.exp-trial-icon { flex-shrink: 0; font-size: 13px; font-weight: 700; line-height: 1.45; width: 16px; }
|
|
387
|
+
.exp-trial-icon.pass { color: var(--green); }
|
|
388
|
+
.exp-trial-icon.fail { color: var(--red); }
|
|
389
|
+
.exp-trial-icon.off { color: var(--muted-light); }
|
|
390
|
+
.exp-trial-body { flex: 1; min-width: 0; font-size: 12px; color: var(--ink-soft); line-height: 1.5; }
|
|
339
391
|
.exp-trial-body .trial-label { font-weight: 700; color: #475569; margin-right: 4px; }
|
|
340
|
-
.exp-trial-body .trial-reason { color:
|
|
392
|
+
.exp-trial-body .trial-reason { color: var(--muted); }
|
|
393
|
+
.exp-trial-line.is-excluded .trial-reason { text-decoration: line-through; text-decoration-color: #cbd5e1; }
|
|
394
|
+
|
|
395
|
+
/* ── Mini metrics (Summary subsection) ──────────────────────── */
|
|
396
|
+
.metrics-grid-sm table { width: 100%; border-collapse: collapse; }
|
|
397
|
+
.metrics-grid-sm thead th {
|
|
398
|
+
background: var(--border-soft); font-size: 10px; font-weight: 700; text-transform: uppercase;
|
|
399
|
+
letter-spacing: 0.06em; color: #475569; padding: 8px 14px; text-align: right;
|
|
400
|
+
border-bottom: 1px solid var(--border); white-space: nowrap;
|
|
401
|
+
}
|
|
402
|
+
.metrics-grid-sm thead th:first-child { text-align: left; }
|
|
403
|
+
.metrics-grid-sm tbody td { padding: 9px 14px; border-bottom: 1px solid var(--border-soft); text-align: right; }
|
|
404
|
+
.metrics-grid-sm tbody tr:last-child td { border-bottom: none; }
|
|
405
|
+
.metrics-grid-sm tbody td:first-child {
|
|
406
|
+
text-align: left; font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
407
|
+
letter-spacing: 0.06em; color: var(--muted-light); white-space: nowrap;
|
|
408
|
+
}
|
|
409
|
+
.metric-val-sm { font-size: 15px; font-weight: 700; line-height: 1.15; display: block; font-variant-numeric: tabular-nums; }
|
|
410
|
+
.metric-sub-sm { font-size: 10px; color: var(--muted-light); margin-top: 3px; font-variant-numeric: tabular-nums; }
|
|
341
411
|
|
|
342
|
-
|
|
412
|
+
/* ── Toast ──────────────────────────────────────────────────── */
|
|
413
|
+
.toast {
|
|
414
|
+
position: fixed; bottom: 20px; left: 50%; transform: translate(-50%, 12px);
|
|
415
|
+
background: var(--ink); color: #f1f5f9; font-size: 12.5px; padding: 10px 16px;
|
|
416
|
+
border-radius: 6px; opacity: 0; pointer-events: none; transition: opacity 0.2s, transform 0.2s;
|
|
417
|
+
max-width: min(540px, calc(100vw - 32px)); text-align: center; line-height: 1.5;
|
|
418
|
+
}
|
|
419
|
+
.toast.visible { opacity: 1; transform: translate(-50%, 0); }
|
|
420
|
+
|
|
421
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
343
422
|
</style>
|
|
344
423
|
</head>
|
|
345
424
|
<body>
|
|
425
|
+
|
|
346
426
|
<div class="container">
|
|
347
427
|
|
|
348
|
-
<!-- Header -->
|
|
349
428
|
<div class="header">
|
|
350
|
-
<h1
|
|
429
|
+
<h1 id="hdr-skill">—</h1>
|
|
351
430
|
<div class="header-meta">
|
|
352
|
-
<span><b>Agent</b>
|
|
353
|
-
<span><b>Type</b>
|
|
354
|
-
<span><b>Date</b>
|
|
431
|
+
<span><b>Agent</b> <span id="hdr-agent">—</span></span>
|
|
432
|
+
<span><b>Type</b> <span id="hdr-type">—</span></span>
|
|
433
|
+
<span><b>Date</b> <span id="hdr-date">—</span></span>
|
|
355
434
|
</div>
|
|
356
|
-
<div class="status-bar
|
|
435
|
+
<div class="status-bar" id="hdr-status"></div>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<div class="excl-bar" id="excl-bar">
|
|
439
|
+
<span class="excl-icon" aria-hidden="true">⊘</span>
|
|
440
|
+
<span class="excl-headline" id="excl-headline"></span>
|
|
441
|
+
<span class="excl-split" id="excl-split"></span>
|
|
442
|
+
<span class="excl-actions">
|
|
443
|
+
<button class="btn" id="btn-reset" type="button">Clear</button>
|
|
444
|
+
<button class="btn primary" id="btn-download" type="button">Download reviewed copy</button>
|
|
445
|
+
</span>
|
|
446
|
+
<span class="excl-warn-msg" id="excl-warn"></span>
|
|
357
447
|
</div>
|
|
358
448
|
|
|
359
|
-
|
|
360
|
-
${renderMetricsGrid(report)}
|
|
449
|
+
<div class="metrics-grid" id="metrics-grid"></div>
|
|
361
450
|
|
|
362
|
-
<!-- Eval Results Table -->
|
|
363
451
|
<div class="section">
|
|
364
452
|
<div class="section-title">Eval results</div>
|
|
365
|
-
|
|
453
|
+
<div class="table-wrap">
|
|
454
|
+
<table class="task-table">
|
|
455
|
+
<thead><tr><th style="width:44px">#</th><th>Prompt</th><th style="width:80px">Details</th></tr></thead>
|
|
456
|
+
<tbody id="task-tbody"></tbody>
|
|
457
|
+
</table>
|
|
458
|
+
</div>
|
|
366
459
|
</div>
|
|
367
460
|
|
|
368
461
|
</div>
|
|
462
|
+
|
|
463
|
+
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
|
467
|
+
|
|
468
|
+
<script id="run-data" type="application/json">${serializeRunData(data)}</script>
|
|
369
469
|
<script>
|
|
370
470
|
(function () {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
471
|
+
'use strict';
|
|
472
|
+
|
|
473
|
+
var RUN = JSON.parse(document.getElementById('run-data').textContent);
|
|
474
|
+
|
|
475
|
+
/* ── Exclusion state ────────────────────────────────────────
|
|
476
|
+
key = "<taskId>:<variant>:<trialId>" → { reason, note, at }
|
|
477
|
+
In the real reporter this same shape is what gets baked into
|
|
478
|
+
the downloaded copy. Here it also round-trips to localStorage. */
|
|
479
|
+
var STORE_KEY = 'skill-eval:excl:' + RUN.runId;
|
|
480
|
+
var excluded = {};
|
|
481
|
+
|
|
482
|
+
var REASONS = [
|
|
483
|
+
['degenerate-output', 'Degenerate output'],
|
|
484
|
+
['zero-assertions', 'Zero assertions'],
|
|
485
|
+
['premature-stop', 'Premature stop'],
|
|
486
|
+
['resource-outlier', 'Resource outlier'],
|
|
487
|
+
['environment', 'Environment problem'],
|
|
488
|
+
['infrastructure', 'Infrastructure'],
|
|
489
|
+
['other', 'Other']
|
|
490
|
+
];
|
|
491
|
+
function reasonLabel(id) {
|
|
492
|
+
for (var i = 0; i < REASONS.length; i++) if (REASONS[i][0] === id) return REASONS[i][1];
|
|
493
|
+
return id;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function loadState() {
|
|
497
|
+
try {
|
|
498
|
+
var raw = localStorage.getItem(STORE_KEY);
|
|
499
|
+
if (raw) { excluded = JSON.parse(raw) || {}; return; }
|
|
500
|
+
} catch (e) { /* file:// or blocked storage — fall through to the baked state */ }
|
|
501
|
+
|
|
502
|
+
// Opened elsewhere: a reviewed copy carries the exclusions it was saved with.
|
|
503
|
+
var baked = document.getElementById('excl-data');
|
|
504
|
+
if (!baked) return;
|
|
505
|
+
try { excluded = JSON.parse(decodeURIComponent(baked.textContent)) || {}; }
|
|
506
|
+
catch (e) { /* corrupt payload — start clean rather than fail to render */ }
|
|
507
|
+
}
|
|
508
|
+
function saveState() {
|
|
509
|
+
try { localStorage.setItem(STORE_KEY, JSON.stringify(excluded)); }
|
|
510
|
+
catch (e) { /* non-fatal: the download is the durable path */ }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function key(taskId, variant, trialId) { return taskId + ':' + variant + ':' + trialId; }
|
|
514
|
+
function isExcluded(taskId, variant, trial) { return !!excluded[key(taskId, variant, trial.id)]; }
|
|
515
|
+
|
|
516
|
+
/* ── Aggregation ────────────────────────────────────────────
|
|
517
|
+
One implementation, used for the initial render and every
|
|
518
|
+
recompute. A trial leaves the denominator when it is excluded
|
|
519
|
+
or when it is an infrastructure error. */
|
|
520
|
+
function counted(taskId, variant, trials) {
|
|
521
|
+
return trials.filter(function (t) { return !t.isError && !isExcluded(taskId, variant, t); });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function rateOf(trials) {
|
|
525
|
+
var total = 0, passed = 0;
|
|
526
|
+
trials.forEach(function (t) {
|
|
527
|
+
total += t.results.length;
|
|
528
|
+
passed += t.results.reduce(function (s, v) { return s + v; }, 0);
|
|
379
529
|
});
|
|
380
|
-
|
|
530
|
+
return total ? passed / total : 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function taskRate(taskId, variant, trials) {
|
|
534
|
+
var live = counted(taskId, variant, trials);
|
|
535
|
+
return { rate: rateOf(live), n: live.length, of: trials.length };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function avgOver(trials, field) {
|
|
539
|
+
var vals = [];
|
|
540
|
+
trials.forEach(function (t) {
|
|
541
|
+
if (field === 'tokens' && t.tokens) vals.push(t.tokens.totalTokens);
|
|
542
|
+
if (field === 'duration' && t.durationMs != null) vals.push(t.durationMs);
|
|
543
|
+
});
|
|
544
|
+
if (!vals.length) return null;
|
|
545
|
+
return Math.round(vals.reduce(function (s, v) { return s + v; }, 0) / vals.length);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* Suite level: average of per-task rates, matching aggregateAssertionPassRate. */
|
|
549
|
+
function suiteRate(variant, useExclusions) {
|
|
550
|
+
var sum = 0, n = 0, live = 0, all = 0;
|
|
551
|
+
RUN.tasks.forEach(function (task) {
|
|
552
|
+
var trials = task.variants[variant];
|
|
553
|
+
if (!trials) return;
|
|
554
|
+
var relevant = trials.filter(function (t) {
|
|
555
|
+
return !t.isError && (!useExclusions || !isExcluded(task.taskId, variant, t));
|
|
556
|
+
});
|
|
557
|
+
sum += rateOf(relevant);
|
|
558
|
+
n += 1;
|
|
559
|
+
live += relevant.length;
|
|
560
|
+
all += trials.length;
|
|
561
|
+
});
|
|
562
|
+
return { rate: n ? sum / n : 0, n: live, of: all };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function suiteAvg(variant, field) {
|
|
566
|
+
var live = [];
|
|
567
|
+
RUN.tasks.forEach(function (task) {
|
|
568
|
+
counted(task.taskId, variant, task.variants[variant] || []).forEach(function (t) { live.push(t); });
|
|
569
|
+
});
|
|
570
|
+
return avgOver(live, field);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* Anomalies are computed by core/anomalies.ts and embedded per trial;
|
|
574
|
+
the page only renders them. */
|
|
575
|
+
function anomaliesOf(trial) { return trial.anomalies || []; }
|
|
576
|
+
|
|
577
|
+
function score(trial) { return trial.results.reduce(function (s, v) { return s + v; }, 0); }
|
|
578
|
+
|
|
579
|
+
/* ── Formatting ─────────────────────────────────────────────── */
|
|
580
|
+
function fmtPct(v) { return Math.round(v * 100) + '%'; }
|
|
581
|
+
function fmtTokens(n) {
|
|
582
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
583
|
+
if (n >= 1000) return Math.round(n / 1000) + 'K';
|
|
584
|
+
return String(n);
|
|
585
|
+
}
|
|
586
|
+
function fmtDuration(ms) {
|
|
587
|
+
if (ms < 1000) return ms + 'ms';
|
|
588
|
+
var s = Math.round(ms / 1000);
|
|
589
|
+
if (s < 60) return s + 's';
|
|
590
|
+
var m = Math.floor(s / 60), rem = s % 60;
|
|
591
|
+
return rem > 0 ? m + 'm ' + rem + 's' : m + 'm';
|
|
592
|
+
}
|
|
593
|
+
function colorClass(v) { return v >= 0.8 ? 'green' : v >= 0.5 ? 'amber' : 'red'; }
|
|
594
|
+
function esc(s) {
|
|
595
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
|
596
|
+
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
597
|
+
}
|
|
598
|
+
/* Backticks in assertions are the skill's own symbol names — render them as code. */
|
|
599
|
+
function escTicks(s) {
|
|
600
|
+
return esc(s).replace(/\`([^\`]+)\`/g, function (_, inner) { return '<code>' + inner + '</code>'; });
|
|
601
|
+
}
|
|
602
|
+
function cssId(s) { return String(s).replace(/[^a-zA-Z0-9_-]+/g, '-'); }
|
|
603
|
+
|
|
604
|
+
function variantsOf(task) {
|
|
605
|
+
var names = Object.keys(task.variants);
|
|
606
|
+
names.sort(function (a, b) { return a === 'baseline' ? -1 : b === 'baseline' ? 1 : 0; });
|
|
607
|
+
return names;
|
|
608
|
+
}
|
|
609
|
+
var ALL_VARIANTS = variantsOf(RUN.tasks[0]);
|
|
610
|
+
var TRIAL_COLS = 7;
|
|
611
|
+
|
|
612
|
+
/* What is open, so a recompute never collapses what is being read.
|
|
613
|
+
One trial at a time per task keeps the page from turning into a wall. */
|
|
614
|
+
var openTrial = {}; /* taskId → "variant:trialId" */
|
|
615
|
+
var openExp = {}; /* detailId → bool */
|
|
616
|
+
|
|
617
|
+
/* ── Render: header + suite metrics ─────────────────────────── */
|
|
618
|
+
function renderHeader() {
|
|
619
|
+
document.getElementById('hdr-skill').textContent = RUN.skill;
|
|
620
|
+
document.getElementById('hdr-agent').textContent = RUN.agent;
|
|
621
|
+
document.getElementById('hdr-type').textContent = RUN.command === 'functional' ? 'Functional' : 'Trigger';
|
|
622
|
+
document.getElementById('hdr-date').textContent = new Date(RUN.timestamp).toLocaleString();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function renderSuiteMetrics() {
|
|
626
|
+
var head = ALL_VARIANTS.map(function (v) { return '<th>' + esc(v) + '</th>'; }).join('');
|
|
627
|
+
|
|
628
|
+
var successCells = ALL_VARIANTS.map(function (v) {
|
|
629
|
+
var adj = suiteRate(v, true), raw = suiteRate(v, false);
|
|
630
|
+
var sub = adj.n !== raw.n
|
|
631
|
+
? '<div class="metric-sub">raw <s>' + fmtPct(raw.rate) + '</s> · n=' + adj.n + '/' + adj.of +
|
|
632
|
+
(adj.n < 3 ? ' · <span class="lowconf">low-confidence</span>' : '') + '</div>'
|
|
633
|
+
: '<div class="metric-sub">n=' + adj.n + '/' + adj.of + '</div>';
|
|
634
|
+
return '<td><span class="metric-val ' + colorClass(adj.rate) + '">' + fmtPct(adj.rate) + '</span>' + sub + '</td>';
|
|
635
|
+
}).join('');
|
|
636
|
+
|
|
637
|
+
var tokenCells = ALL_VARIANTS.map(function (v) {
|
|
638
|
+
var t = suiteAvg(v, 'tokens');
|
|
639
|
+
return '<td>' + (t != null
|
|
640
|
+
? '<span class="metric-val">' + fmtTokens(t) + '</span><div class="metric-sub">avg total</div>'
|
|
641
|
+
: '<span class="metric-val muted">—</span>') + '</td>';
|
|
642
|
+
}).join('');
|
|
381
643
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
644
|
+
var timeCells = ALL_VARIANTS.map(function (v) {
|
|
645
|
+
var d = suiteAvg(v, 'duration');
|
|
646
|
+
return '<td>' + (d != null
|
|
647
|
+
? '<span class="metric-val">' + fmtDuration(d) + '</span>'
|
|
648
|
+
: '<span class="metric-val muted">—</span>') + '</td>';
|
|
649
|
+
}).join('');
|
|
650
|
+
|
|
651
|
+
document.getElementById('metrics-grid').innerHTML =
|
|
652
|
+
'<table><thead><tr><th></th>' + head + '</tr></thead><tbody>' +
|
|
653
|
+
'<tr><td>Success Rate</td>' + successCells + '</tr>' +
|
|
654
|
+
'<tr><td>Tokens (avg)</td>' + tokenCells + '</tr>' +
|
|
655
|
+
'<tr><td>Time (avg)</td>' + timeCells + '</tr>' +
|
|
656
|
+
'</tbody></table>';
|
|
657
|
+
|
|
658
|
+
var target = ALL_VARIANTS.indexOf('local') >= 0 ? 'local' : ALL_VARIANTS[ALL_VARIANTS.length - 1];
|
|
659
|
+
document.getElementById('hdr-status').className = 'status-bar ' + colorClass(suiteRate(target, true).rate);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/* ── Render: exclusion bar ──────────────────────────────────── */
|
|
663
|
+
function renderExclusionBar() {
|
|
664
|
+
var perVariant = {}, total = 0, all = 0;
|
|
665
|
+
ALL_VARIANTS.forEach(function (v) { perVariant[v] = 0; });
|
|
666
|
+
RUN.tasks.forEach(function (task) {
|
|
667
|
+
ALL_VARIANTS.forEach(function (v) {
|
|
668
|
+
(task.variants[v] || []).forEach(function (t) {
|
|
669
|
+
all += 1;
|
|
670
|
+
if (isExcluded(task.taskId, v, t)) { perVariant[v] += 1; total += 1; }
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
var bar = document.getElementById('excl-bar');
|
|
676
|
+
if (!total) { bar.classList.remove('active', 'warn'); return; }
|
|
677
|
+
bar.classList.add('active');
|
|
678
|
+
|
|
679
|
+
document.getElementById('excl-headline').textContent =
|
|
680
|
+
total + ' of ' + all + ' trials excluded (' + Math.round((total / all) * 100) + '%)';
|
|
681
|
+
document.getElementById('excl-split').textContent =
|
|
682
|
+
ALL_VARIANTS.map(function (v) { return perVariant[v] + ' ' + v; }).join(' · ');
|
|
683
|
+
|
|
684
|
+
/* Asymmetric exclusions are the p-hacking failure mode: warn loudly. */
|
|
685
|
+
var counts = ALL_VARIANTS.map(function (v) { return perVariant[v]; });
|
|
686
|
+
var lopsided = ALL_VARIANTS.length > 1 && Math.max.apply(null, counts) !== Math.min.apply(null, counts);
|
|
687
|
+
bar.classList.toggle('warn', lopsided);
|
|
688
|
+
document.getElementById('excl-warn').textContent = lopsided
|
|
689
|
+
? 'Exclusions are not balanced across variants. Check that you applied the same criterion to each — ' +
|
|
690
|
+
'excluding more from one than another moves the delta you are measuring.'
|
|
691
|
+
: '';
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* ── Render: task rows ──────────────────────────────────────── */
|
|
695
|
+
function renderTasks() {
|
|
696
|
+
var tbody = document.getElementById('task-tbody');
|
|
697
|
+
tbody.innerHTML = RUN.tasks.map(function (task) {
|
|
698
|
+
return '<tr>' +
|
|
699
|
+
'<td>' + task.taskId + '</td>' +
|
|
700
|
+
'<td class="prompt-cell">' + esc(task.prompt) + '</td>' +
|
|
701
|
+
'<td><button class="details-btn" type="button" data-task="' + task.taskId + '" aria-expanded="false">▶</button></td>' +
|
|
702
|
+
'</tr>' +
|
|
703
|
+
'<tr class="details-row"><td colspan="3">' +
|
|
704
|
+
'<div class="task-details" id="details-' + task.taskId + '"></div></td></tr>';
|
|
705
|
+
}).join('');
|
|
706
|
+
|
|
707
|
+
tbody.querySelectorAll('.details-btn').forEach(function (btn) {
|
|
708
|
+
btn.addEventListener('click', function () {
|
|
709
|
+
var panel = document.getElementById('details-' + btn.getAttribute('data-task'));
|
|
710
|
+
var open = panel.classList.toggle('visible');
|
|
711
|
+
btn.classList.toggle('open', open);
|
|
712
|
+
btn.textContent = open ? '▼' : '▶';
|
|
713
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
714
|
+
});
|
|
389
715
|
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* ── Subsection: Summary ────────────────────────────────────── */
|
|
719
|
+
function renderSummary(task) {
|
|
720
|
+
var head = ALL_VARIANTS.map(function (v) { return '<th>' + esc(v) + '</th>'; }).join('');
|
|
721
|
+
|
|
722
|
+
var successCells = ALL_VARIANTS.map(function (v) {
|
|
723
|
+
var trials = task.variants[v] || [];
|
|
724
|
+
if (!trials.length) return '<td><span class="metric-val-sm muted">—</span></td>';
|
|
725
|
+
var adj = taskRate(task.taskId, v, trials);
|
|
726
|
+
var rawLive = trials.filter(function (t) { return !t.isError; });
|
|
727
|
+
var sub = adj.n !== rawLive.length
|
|
728
|
+
? '<div class="metric-sub-sm">raw <s>' + fmtPct(rateOf(rawLive)) + '</s> · n=' + adj.n + '/' + adj.of + '</div>'
|
|
729
|
+
: '<div class="metric-sub-sm">n=' + adj.n + '/' + adj.of + '</div>';
|
|
730
|
+
return '<td><span class="metric-val-sm ' + colorClass(adj.rate) + '">' + fmtPct(adj.rate) + '</span>' + sub + '</td>';
|
|
731
|
+
}).join('');
|
|
732
|
+
|
|
733
|
+
function avgCells(field, fmt) {
|
|
734
|
+
return ALL_VARIANTS.map(function (v) {
|
|
735
|
+
var val = avgOver(counted(task.taskId, v, task.variants[v] || []), field);
|
|
736
|
+
return '<td><span class="metric-val-sm' + (val == null ? ' muted' : '') + '">' +
|
|
737
|
+
(val == null ? '—' : fmt(val)) + '</span></td>';
|
|
738
|
+
}).join('');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return '<div class="subsection"><div class="subsection-title">Summary</div>' +
|
|
742
|
+
'<div class="panel metrics-grid-sm"><table><thead><tr><th></th>' + head + '</tr></thead><tbody>' +
|
|
743
|
+
'<tr><td>Success Rate</td>' + successCells + '</tr>' +
|
|
744
|
+
'<tr><td>Tokens (avg)</td>' + avgCells('tokens', fmtTokens) + '</tr>' +
|
|
745
|
+
'<tr><td>Time (avg)</td>' + avgCells('duration', fmtDuration) + '</tr>' +
|
|
746
|
+
'</tbody></table></div></div>';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/* ── Subsection: Trials ─────────────────────────────────────── */
|
|
750
|
+
function renderTrials(task) {
|
|
751
|
+
var totalTrials = 0, totalFlagged = 0, totalExcluded = 0;
|
|
752
|
+
var body = '';
|
|
753
|
+
|
|
754
|
+
ALL_VARIANTS.forEach(function (variant) {
|
|
755
|
+
var trials = task.variants[variant] || [];
|
|
756
|
+
if (!trials.length) return;
|
|
757
|
+
|
|
758
|
+
var flagged = 0, off = 0;
|
|
759
|
+
trials.forEach(function (t) {
|
|
760
|
+
if (anomaliesOf(t).length) flagged += 1;
|
|
761
|
+
if (isExcluded(task.taskId, variant, t)) off += 1;
|
|
762
|
+
});
|
|
763
|
+
totalTrials += trials.length; totalFlagged += flagged; totalExcluded += off;
|
|
764
|
+
|
|
765
|
+
var meta = trials.length + ' trials';
|
|
766
|
+
if (flagged) meta += ' · ' + flagged + ' flagged';
|
|
767
|
+
if (off) meta += ' · <span class="off">' + off + ' excluded</span>';
|
|
768
|
+
|
|
769
|
+
body += '<tr class="variant-row"><td colspan="' + TRIAL_COLS + '">' +
|
|
770
|
+
'<span class="variant-name">' + esc(variant) + '</span>' +
|
|
771
|
+
'<span class="variant-meta">' + meta + '</span></td></tr>';
|
|
772
|
+
|
|
773
|
+
trials.forEach(function (t) {
|
|
774
|
+
body += renderTrialRow(task, variant, t, trials);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
var count = totalTrials + ' trials';
|
|
779
|
+
if (totalFlagged) count += ' · <span class="flagged">' + totalFlagged + ' flagged</span>';
|
|
780
|
+
if (totalExcluded) count += ' · ' + totalExcluded + ' excluded';
|
|
781
|
+
|
|
782
|
+
return '<div class="subsection">' +
|
|
783
|
+
'<div class="subsection-title">Trials <span class="st-count">' + count + '</span></div>' +
|
|
784
|
+
'<div class="panel"><table class="trials-table">' +
|
|
785
|
+
'<thead><tr>' +
|
|
786
|
+
'<th style="width:30px"></th>' +
|
|
787
|
+
'<th style="width:78px">Trial</th>' +
|
|
788
|
+
'<th style="width:86px">Assertions</th>' +
|
|
789
|
+
'<th>Anomalies</th>' +
|
|
790
|
+
'<th class="num" style="width:74px">Tokens</th>' +
|
|
791
|
+
'<th class="num" style="width:70px">Time</th>' +
|
|
792
|
+
'<th class="act" style="width:104px">Status</th>' +
|
|
793
|
+
'</tr></thead><tbody>' + body + '</tbody></table></div></div>';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function renderTrialRow(task, variant, trial, cohort) {
|
|
797
|
+
var flags = anomaliesOf(trial);
|
|
798
|
+
var k = key(task.taskId, variant, trial.id);
|
|
799
|
+
var state = excluded[k];
|
|
800
|
+
var isOpen = openTrial[task.taskId] === variant + ':' + trial.id;
|
|
801
|
+
var sc = score(trial), tot = trial.results.length;
|
|
802
|
+
var rate = tot ? sc / tot : 0;
|
|
803
|
+
|
|
804
|
+
/* Once excluded, the reason replaces the flags — the anomalies stay in the detail row. */
|
|
805
|
+
var flagCells = state
|
|
806
|
+
? '<span class="excl-chip">' + esc(reasonLabel(state.reason)) + '</span>' +
|
|
807
|
+
(state.note ? '<span class="excl-note">' + esc(state.note) + '</span>' : '')
|
|
808
|
+
: (flags.length
|
|
809
|
+
? flags.map(function (f) { return '<span class="flag-chip">⚠ ' + esc(f.tag) + '</span>'; }).join('')
|
|
810
|
+
: '<span class="clean">—</span>');
|
|
811
|
+
|
|
812
|
+
var row = '<tr class="trial-row' + (state ? ' excluded' : '') + (isOpen ? ' open' : '') + '" ' +
|
|
813
|
+
'data-task="' + task.taskId + '" data-variant="' + esc(variant) + '" data-trial="' + trial.id + '">' +
|
|
814
|
+
'<td><button class="tr-toggle" type="button" aria-expanded="' + isOpen + '" ' +
|
|
815
|
+
'aria-label="Show detail for trial ' + trial.id + '">' + (isOpen ? '▼' : '▶') + '</button></td>' +
|
|
816
|
+
'<td class="tr-name">Trial ' + trial.id + '</td>' +
|
|
817
|
+
'<td class="tr-score">' +
|
|
818
|
+
'<span class="frac ' + (state ? '' : colorClass(rate)) + '">' + sc + '/' + tot + '</span>' +
|
|
819
|
+
'<span class="bar"><i class="' + colorClass(rate) + '" style="width:' + Math.round(rate * 1000) / 10 + '%"></i></span>' +
|
|
820
|
+
'</td>' +
|
|
821
|
+
'<td class="tr-flags">' + flagCells + '</td>' +
|
|
822
|
+
'<td class="num tr-num">' + (trial.tokens ? fmtTokens(trial.tokens.totalTokens) : '—') + '</td>' +
|
|
823
|
+
'<td class="num tr-num">' + (trial.durationMs != null ? fmtDuration(trial.durationMs) : '—') + '</td>' +
|
|
824
|
+
'<td class="act"><button class="btn ' + (state ? '' : 'danger') + '" type="button" ' +
|
|
825
|
+
'data-act="' + (state ? 'include' : 'exclude') + '">' +
|
|
826
|
+
(state ? 'Re-include' : '⊘ Exclude') + '</button></td>' +
|
|
827
|
+
'</tr>';
|
|
828
|
+
|
|
829
|
+
return row + (isOpen ? renderTrialDetail(task, variant, trial, flags, state) : '');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function renderTrialDetail(task, variant, trial, flags, state) {
|
|
833
|
+
var anomalyHtml = flags.length
|
|
834
|
+
? flags.map(function (f) {
|
|
835
|
+
return '<div class="anomaly-row"><span class="flag-chip">' + esc(f.tag) + '</span>' +
|
|
836
|
+
'<span class="anomaly-why">' + esc(f.reason) + '</span></div>';
|
|
837
|
+
}).join('')
|
|
838
|
+
: '<div class="no-anomaly">No anomalies detected — this reads as a legitimate attempt.</div>';
|
|
839
|
+
|
|
840
|
+
var degenerate = flags.some(function (f) { return f.tag === 'degenerate-output'; });
|
|
841
|
+
var truncated = trial.outputLen > trial.output.length;
|
|
842
|
+
|
|
843
|
+
/* The reason controls only exist once there is something to justify. */
|
|
844
|
+
var reasonBar = state
|
|
845
|
+
? '<div class="reason-bar"><span>Reason</span>' +
|
|
846
|
+
'<select data-role="reason">' +
|
|
847
|
+
REASONS.map(function (r) {
|
|
848
|
+
return '<option value="' + r[0] + '"' + (r[0] === state.reason ? ' selected' : '') + '>' +
|
|
849
|
+
r[1] + '</option>';
|
|
850
|
+
}).join('') +
|
|
851
|
+
'</select>' +
|
|
852
|
+
'<input type="text" data-role="note" placeholder="Note (optional)" value="' +
|
|
853
|
+
esc(state.note || '') + '">' +
|
|
854
|
+
'</div>'
|
|
855
|
+
: '';
|
|
856
|
+
|
|
857
|
+
return '<tr class="trial-detail"><td colspan="' + TRIAL_COLS + '"><div class="trial-detail-inner">' +
|
|
858
|
+
anomalyHtml +
|
|
859
|
+
'<div class="td-label">Final agent output</div>' +
|
|
860
|
+
'<div class="output-box' + (degenerate ? ' degenerate' : '') + '">' +
|
|
861
|
+
esc(trial.output || '(empty)') + '</div>' +
|
|
862
|
+
(truncated ? '<div class="output-trunc">Truncated to ' + trial.output.length + ' of ' +
|
|
863
|
+
trial.outputLen + ' characters.</div>' : '') +
|
|
864
|
+
'<div class="td-stats"><b>' + trial.toolCalls + '</b> tool calls · stop: <b>' +
|
|
865
|
+
esc(trial.stopStatus || '—') + '</b> · <b>' +
|
|
866
|
+
(trial.tokens ? fmtTokens(trial.tokens.totalTokens) : '—') + '</b> tokens · <b>' +
|
|
867
|
+
(trial.durationMs != null ? fmtDuration(trial.durationMs) : '—') + '</b></div>' +
|
|
868
|
+
(trial.logFile
|
|
869
|
+
? '<div class="td-log">Full transcript: <a href="' + esc(trial.logFile) + '">' +
|
|
870
|
+
esc(trial.logFile) + '</a></div>'
|
|
871
|
+
: '') +
|
|
872
|
+
reasonBar +
|
|
873
|
+
'</div></td></tr>';
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* ── Subsection: Expectations ───────────────────────────────── */
|
|
877
|
+
function renderExpectations(task) {
|
|
878
|
+
var totalCols = 1 + ALL_VARIANTS.length;
|
|
879
|
+
var head = ALL_VARIANTS.map(function (v) { return '<th class="variant-col">' + esc(v) + '</th>'; }).join('');
|
|
880
|
+
|
|
881
|
+
var rows = task.assertions.map(function (assertion, idx) {
|
|
882
|
+
var cells = '', details = '';
|
|
883
|
+
ALL_VARIANTS.forEach(function (v) {
|
|
884
|
+
var trials = task.variants[v] || [];
|
|
885
|
+
var live = counted(task.taskId, v, trials);
|
|
886
|
+
var passed = live.filter(function (t) { return t.results[idx] === 1; }).length;
|
|
887
|
+
var rate = live.length ? passed / live.length : 0;
|
|
888
|
+
var detailId = 'exp-' + task.taskId + '-' + idx + '-' + cssId(v);
|
|
889
|
+
|
|
890
|
+
cells += '<td class="pass-cell ' + colorClass(rate) + (openExp[detailId] ? ' open' : '') +
|
|
891
|
+
'" data-detail="' + detailId + '">' +
|
|
892
|
+
'<span class="rate">' + fmtPct(rate) + '</span>' +
|
|
893
|
+
'<span class="frac">' + passed + ' / ' + live.length + '</span></td>';
|
|
894
|
+
|
|
895
|
+
if (openExp[detailId]) {
|
|
896
|
+
var lines = trials.map(function (t) {
|
|
897
|
+
var off = isExcluded(task.taskId, v, t);
|
|
898
|
+
var ok = t.results[idx] === 1;
|
|
899
|
+
return '<div class="exp-trial-line' + (off ? ' is-excluded' : '') + '">' +
|
|
900
|
+
'<div class="exp-trial-icon ' + (off ? 'off' : ok ? 'pass' : 'fail') + '">' +
|
|
901
|
+
(off ? '⊘' : ok ? '✓' : '✗') + '</div>' +
|
|
902
|
+
'<div class="exp-trial-body"><span class="trial-label">Trial ' + t.id + '</span>' +
|
|
903
|
+
'<span class="trial-reason">' + esc(t.reasons[idx] || '') + '</span></div></div>';
|
|
904
|
+
}).join('');
|
|
905
|
+
details += '<tr class="exp-detail-row"><td colspan="' + totalCols + '">' +
|
|
906
|
+
'<div class="exp-detail-inner"><div class="exp-detail-header">Judge per trial · ' + esc(v) + '</div>' +
|
|
907
|
+
lines + '</div></td></tr>';
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
return '<tr class="exp-row"><td class="exp-text">' + escTicks(assertion) + '</td>' + cells + '</tr>' + details;
|
|
911
|
+
}).join('');
|
|
912
|
+
|
|
913
|
+
return '<div class="subsection">' +
|
|
914
|
+
'<div class="subsection-title">Expectations <span class="st-count">' +
|
|
915
|
+
task.assertions.length + ' expectations</span></div>' +
|
|
916
|
+
'<div class="panel"><table class="expectations-table">' +
|
|
917
|
+
'<thead><tr><th class="exp-col">Expectation</th>' + head + '</tr></thead>' +
|
|
918
|
+
'<tbody>' + rows + '</tbody></table></div></div>';
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/* ── Wiring ─────────────────────────────────────────────────── */
|
|
922
|
+
function renderTaskDetails(task) {
|
|
923
|
+
var host = document.getElementById('details-' + task.taskId);
|
|
924
|
+
if (!host) return;
|
|
925
|
+
host.innerHTML = renderSummary(task) + renderTrials(task) + renderExpectations(task);
|
|
926
|
+
wire(task, host);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function wire(task, host) {
|
|
930
|
+
host.querySelectorAll('.trial-row').forEach(function (row) {
|
|
931
|
+
var variant = row.getAttribute('data-variant');
|
|
932
|
+
var trialId = parseInt(row.getAttribute('data-trial'), 10);
|
|
933
|
+
var k = key(task.taskId, variant, trialId);
|
|
934
|
+
|
|
935
|
+
row.addEventListener('click', function (ev) {
|
|
936
|
+
if (ev.target.closest('[data-act]')) return;
|
|
937
|
+
var slot = variant + ':' + trialId;
|
|
938
|
+
openTrial[task.taskId] = openTrial[task.taskId] === slot ? null : slot;
|
|
939
|
+
render();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
var actBtn = row.querySelector('[data-act]');
|
|
943
|
+
if (!actBtn) return;
|
|
944
|
+
actBtn.addEventListener('click', function () {
|
|
945
|
+
if (actBtn.getAttribute('data-act') === 'include') {
|
|
946
|
+
delete excluded[k];
|
|
947
|
+
toast('Trial ' + trialId + " of '" + variant + "' re-included");
|
|
948
|
+
} else {
|
|
949
|
+
/* Pre-fill the reason from the strongest signal; refine it in the detail row. */
|
|
950
|
+
var trial = task.variants[variant].filter(function (t) { return t.id === trialId; })[0];
|
|
951
|
+
var flags = anomaliesOf(trial);
|
|
952
|
+
excluded[k] = {
|
|
953
|
+
reason: flags.length ? flags[0].tag : 'other',
|
|
954
|
+
note: '',
|
|
955
|
+
at: new Date().toISOString()
|
|
956
|
+
};
|
|
957
|
+
openTrial[task.taskId] = variant + ':' + trialId;
|
|
958
|
+
toast('Trial ' + trialId + " of '" + variant + "' excluded — " + reasonLabel(excluded[k].reason));
|
|
959
|
+
}
|
|
960
|
+
saveState();
|
|
961
|
+
render();
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
/* Reason + note live inside the open detail row; persist as they change. */
|
|
966
|
+
host.querySelectorAll('.trial-detail [data-role]').forEach(function (input) {
|
|
967
|
+
var row = input.closest('.trial-detail').previousElementSibling;
|
|
968
|
+
var k = key(task.taskId, row.getAttribute('data-variant'), parseInt(row.getAttribute('data-trial'), 10));
|
|
969
|
+
input.addEventListener('change', function () {
|
|
970
|
+
if (!excluded[k]) return;
|
|
971
|
+
if (input.getAttribute('data-role') === 'reason') excluded[k].reason = input.value;
|
|
972
|
+
else excluded[k].note = input.value.trim();
|
|
973
|
+
saveState();
|
|
974
|
+
render();
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
host.querySelectorAll('.pass-cell').forEach(function (cell) {
|
|
979
|
+
cell.addEventListener('click', function () {
|
|
980
|
+
var id = cell.getAttribute('data-detail');
|
|
981
|
+
openExp[id] = !openExp[id];
|
|
982
|
+
render();
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function render() {
|
|
988
|
+
renderSuiteMetrics();
|
|
989
|
+
renderExclusionBar();
|
|
990
|
+
RUN.tasks.forEach(renderTaskDetails);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
var toastTimer;
|
|
994
|
+
function toast(msg) {
|
|
995
|
+
var el = document.getElementById('toast');
|
|
996
|
+
el.textContent = msg;
|
|
997
|
+
el.classList.add('visible');
|
|
998
|
+
clearTimeout(toastTimer);
|
|
999
|
+
toastTimer = setTimeout(function () { el.classList.remove('visible'); }, 2600);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
document.getElementById('btn-reset').addEventListener('click', function () {
|
|
1003
|
+
excluded = {};
|
|
1004
|
+
saveState();
|
|
1005
|
+
render();
|
|
1006
|
+
toast('Exclusions cleared');
|
|
390
1007
|
});
|
|
1008
|
+
|
|
1009
|
+
/* Saves a copy of this page with the current exclusions baked in — the
|
|
1010
|
+
durable, shareable artifact. URI-encoded so a reviewer's note can contain
|
|
1011
|
+
any character without breaking out of the script tag. */
|
|
1012
|
+
document.getElementById('btn-download').addEventListener('click', function () {
|
|
1013
|
+
var baked = document.getElementById('excl-data');
|
|
1014
|
+
if (!baked) {
|
|
1015
|
+
baked = document.createElement('script');
|
|
1016
|
+
baked.id = 'excl-data';
|
|
1017
|
+
baked.type = 'application/json';
|
|
1018
|
+
document.body.appendChild(baked);
|
|
1019
|
+
}
|
|
1020
|
+
baked.textContent = encodeURIComponent(JSON.stringify(excluded));
|
|
1021
|
+
|
|
1022
|
+
var link = document.createElement('a');
|
|
1023
|
+
var url = URL.createObjectURL(
|
|
1024
|
+
new Blob(['<!DOCTYPE html>\\n' + document.documentElement.outerHTML], { type: 'text/html' })
|
|
1025
|
+
);
|
|
1026
|
+
link.href = url;
|
|
1027
|
+
link.download = 'report-reviewed.html';
|
|
1028
|
+
link.click();
|
|
1029
|
+
URL.revokeObjectURL(url);
|
|
1030
|
+
toast('Saved report-reviewed.html with ' + Object.keys(excluded).length + ' exclusion(s)');
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
loadState();
|
|
1034
|
+
renderHeader();
|
|
1035
|
+
renderTasks();
|
|
1036
|
+
render();
|
|
391
1037
|
}());
|
|
392
1038
|
</script>
|
|
393
1039
|
</body>
|