@fede0089/skill-eval 1.2.2 → 1.4.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 +4 -3
- package/dist/commands/functional.js +58 -52
- package/dist/commands/trigger.js +1 -0
- package/dist/index.js +3 -1
- package/dist/reporters/html-reporter.js +103 -63
- package/dist/utils/table-renderer.js +13 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ A CLI tool for evaluating Agent Skills locally. Tests whether your skill trigger
|
|
|
6
6
|
|
|
7
7
|
Skills are instructions that change how the agent behaves. But a single successful run isn't enough to trust one — agents are non-deterministic, and a good isolated result can be exactly that: an isolated case.
|
|
8
8
|
|
|
9
|
-
Skill evals let you turn that intuition into evidence: run several comparable tasks with
|
|
9
|
+
Skill evals let you turn that intuition into evidence: run several comparable tasks with the skill, measure them against the same criteria, and optionally compare against a baseline or historical skill branches.
|
|
10
10
|
|
|
11
11
|
## How it works
|
|
12
12
|
|
|
13
|
-
For each eval prompt, skill-eval spins up parallel agent processes
|
|
13
|
+
For each eval prompt, skill-eval spins up parallel agent processes with the current skill installed by default. You can optionally add the no-skill baseline or historical skill branches for side-by-side comparison. Each agent runs headlessly and produces a transcript. An LLM judge then grades each transcript against your expectations. Results are aggregated into **pass@k** metrics, giving you a clear view of how the skill behaves in isolation or relative to comparison targets.
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
eval prompt
|
|
@@ -66,7 +66,7 @@ npm link # makes `skill-eval` available globally
|
|
|
66
66
|
# Checks that the skill is triggered (invoked) for each prompt
|
|
67
67
|
skill-eval trigger --workspace <path> --skill <path> [options] [agent]
|
|
68
68
|
|
|
69
|
-
# Checks that the skill produces correct output
|
|
69
|
+
# Checks that the skill produces correct output (skill-only by default)
|
|
70
70
|
skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -81,6 +81,7 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
|
81
81
|
| `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
|
|
82
82
|
| `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
|
|
83
83
|
| `--compare-ref [refs...]` | no | — | Git references to compare against |
|
|
84
|
+
| `--compare-baseline` | no | `false` | Also run the no-skill baseline alongside the skill |
|
|
84
85
|
| `-v, --debug` | no | `false` | Enable verbose debug logging |
|
|
85
86
|
| `[agent]` | no | `gemini-cli` | Agent backend to use |
|
|
86
87
|
|
|
@@ -13,7 +13,7 @@ import { renderFunctionalTable, renderRunHeader } from '../utils/table-renderer.
|
|
|
13
13
|
import { JsonReporter } from '../reporters/index.js';
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import { git } from '../utils/git.js';
|
|
16
|
-
export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = []) {
|
|
16
|
+
export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = [], compareBaseline = false) {
|
|
17
17
|
if (!injectedSuite)
|
|
18
18
|
preflight(agent, workspace, skillPath);
|
|
19
19
|
const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
|
|
@@ -63,10 +63,10 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
63
63
|
}));
|
|
64
64
|
}
|
|
65
65
|
// 3. Baseline Runner
|
|
66
|
-
const withoutSkillRunner = new EvalRunner({
|
|
66
|
+
const withoutSkillRunner = compareBaseline ? new EvalRunner({
|
|
67
67
|
agent, workspace, skillPath, skillName: skill_name, runDir, isBaseline: true, debug, timeoutMs,
|
|
68
68
|
variant: 'baseline'
|
|
69
|
-
});
|
|
69
|
+
}) : undefined;
|
|
70
70
|
const taskResults = [];
|
|
71
71
|
let withSkillTasksAllPassedCount = 0;
|
|
72
72
|
let baselineTasksAllPassedCount = 0;
|
|
@@ -76,10 +76,12 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
76
76
|
let barrier = Promise.resolve();
|
|
77
77
|
// Subtask labels
|
|
78
78
|
const skillVersions = Array.from(variantRunners.keys());
|
|
79
|
-
const subtaskLabels =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
const subtaskLabels = compareBaseline
|
|
80
|
+
? [
|
|
81
|
+
...Array.from({ length: numTrials }, (_, i) => `Without Skill ${i + 1}`),
|
|
82
|
+
...skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`))
|
|
83
|
+
]
|
|
84
|
+
: skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`));
|
|
83
85
|
try {
|
|
84
86
|
renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
|
|
85
87
|
Logger.write(`──────────────────────────────────────────────────\n`);
|
|
@@ -98,34 +100,36 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
98
100
|
await thisBarrier;
|
|
99
101
|
// 1. Without Skill trials
|
|
100
102
|
const baselineTrialPromises = [];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
if (withoutSkillRunner) {
|
|
104
|
+
for (let idx = 0; idx < numTrials; idx++) {
|
|
105
|
+
const trialId = idx + 1;
|
|
106
|
+
const trialCtx = multi?.getTrialCtx(trialId) ?? uiCtx;
|
|
107
|
+
const release = await pool.acquire();
|
|
108
|
+
const p = withRetry((attempt) => withoutSkillRunner.runFunctionalTask(task, i, trialId, trialCtx, attempt)
|
|
109
|
+
.catch((error) => ({
|
|
110
|
+
id: trialId,
|
|
111
|
+
transcript: { error: error instanceof Error ? error.message : String(error) },
|
|
112
|
+
assertionResults: [{ assertion: 'Without Skill Execution', passed: false, reason: String(error) }],
|
|
113
|
+
trialPassed: false,
|
|
114
|
+
isError: true
|
|
115
|
+
})), 2, 1000, (nextAttempt, lastTrial) => {
|
|
116
|
+
const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
|
|
117
|
+
trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
|
|
118
|
+
}).then(trial => {
|
|
119
|
+
if (multi) {
|
|
120
|
+
const reason = trial.assertionResults.find(r => !r.passed)?.reason;
|
|
121
|
+
const passedCount = trial.assertionResults.filter(r => r.passed).length;
|
|
122
|
+
const totalCount = trial.assertionResults.length;
|
|
123
|
+
multi.markTrialComplete(trialId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
|
|
124
|
+
}
|
|
125
|
+
return trial;
|
|
126
|
+
}).finally(release);
|
|
127
|
+
baselineTrialPromises.push(p);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
// 2. Skill variants trials
|
|
127
131
|
const variantTrialsPromises = {};
|
|
128
|
-
let currentSubtaskIdx = numTrials;
|
|
132
|
+
let currentSubtaskIdx = compareBaseline ? numTrials : 0;
|
|
129
133
|
for (const [version, runner] of variantRunners.entries()) {
|
|
130
134
|
variantTrialsPromises[version] = [];
|
|
131
135
|
for (let idx = 0; idx < numTrials; idx++) {
|
|
@@ -158,19 +162,17 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
158
162
|
}
|
|
159
163
|
// All slots for this prompt have been acquired — signal the next prompt.
|
|
160
164
|
resolveBarrier();
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
...Object.values(variantTrialsPromises).map(ps => Promise.all(ps))
|
|
164
|
-
]);
|
|
165
|
+
const variantsTrials = await Promise.all(Object.values(variantTrialsPromises).map(ps => Promise.all(ps)));
|
|
166
|
+
const woTrials = withoutSkillRunner ? await Promise.all(baselineTrialPromises) : [];
|
|
165
167
|
const woPassedCount = woTrials.filter(t => t.trialPassed).length;
|
|
166
|
-
if (woPassedCount === woTrials.length)
|
|
168
|
+
if (withoutSkillRunner && woPassedCount === woTrials.length)
|
|
167
169
|
baselineTasksAllPassedCount++;
|
|
168
170
|
const taskSkillTrials = {};
|
|
169
171
|
const variantNames = Object.keys(variantTrialsPromises);
|
|
170
172
|
let localAllPassed = true;
|
|
171
173
|
for (let vIdx = 0; vIdx < variantNames.length; vIdx++) {
|
|
172
174
|
const vName = variantNames[vIdx];
|
|
173
|
-
const vTrials =
|
|
175
|
+
const vTrials = variantsTrials[vIdx];
|
|
174
176
|
taskSkillTrials[vName] = vTrials.map(t => ({ ...t, transcript: undefined }));
|
|
175
177
|
if (vName === 'local') {
|
|
176
178
|
const passedCount = vTrials.filter(t => t.trialPassed).length;
|
|
@@ -185,7 +187,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
185
187
|
const taskResult = {
|
|
186
188
|
taskId: task.id,
|
|
187
189
|
prompt: task.prompt,
|
|
188
|
-
baselineTrials: woTrials.map(t => ({ ...t, transcript: undefined })),
|
|
190
|
+
baselineTrials: withoutSkillRunner ? woTrials.map(t => ({ ...t, transcript: undefined })) : [],
|
|
189
191
|
skillTrials: taskSkillTrials
|
|
190
192
|
};
|
|
191
193
|
taskResults.push(taskResult);
|
|
@@ -199,25 +201,28 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
199
201
|
await ui.run(tasks.length);
|
|
200
202
|
// ==== REPORTING ====
|
|
201
203
|
const allSkillVersions = taskResults.length > 0 ? Object.keys(taskResults[0].skillTrials) : ['local'];
|
|
202
|
-
const
|
|
204
|
+
const hasBaseline = compareBaseline;
|
|
205
|
+
const allVersions = hasBaseline ? ['baseline', ...allSkillVersions] : allSkillVersions;
|
|
203
206
|
const scores = {};
|
|
204
207
|
const passAtK = {};
|
|
205
208
|
const assertionPassRate = {};
|
|
206
209
|
const tokenStats = {};
|
|
207
210
|
const durationStats = {};
|
|
208
211
|
// 1. Baseline Metrics
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
212
|
+
if (hasBaseline) {
|
|
213
|
+
const { passAtK: woPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.baselineTrials);
|
|
214
|
+
const woAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.baselineTrials);
|
|
215
|
+
const woPercentage = Math.round(woAssertionPassRate * 100);
|
|
216
|
+
scores['baseline'] = `${woPercentage}%`;
|
|
217
|
+
passAtK['baseline'] = Math.round(woPassAtK * 1000) / 1000;
|
|
218
|
+
assertionPassRate['baseline'] = Math.round(woAssertionPassRate * 1000) / 1000;
|
|
219
|
+
const woTokens = aggregateTokenStats(taskResults.flatMap(r => r.baselineTrials));
|
|
220
|
+
if (woTokens)
|
|
221
|
+
tokenStats['baseline'] = woTokens;
|
|
222
|
+
const woDuration = aggregateDurationStats(taskResults.flatMap(r => r.baselineTrials));
|
|
223
|
+
if (woDuration)
|
|
224
|
+
durationStats['baseline'] = woDuration;
|
|
225
|
+
}
|
|
221
226
|
// 2. Skill Variants Metrics
|
|
222
227
|
for (const version of allSkillVersions) {
|
|
223
228
|
const { passAtK: wiPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.skillTrials[version] || []);
|
|
@@ -235,6 +240,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
235
240
|
}
|
|
236
241
|
const report = {
|
|
237
242
|
timestamp: startTime.toISOString(),
|
|
243
|
+
command: 'functional',
|
|
238
244
|
skill_name,
|
|
239
245
|
agent,
|
|
240
246
|
metrics: {
|
package/dist/commands/trigger.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ program
|
|
|
61
61
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
62
62
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
63
63
|
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
64
|
+
.option('--compare-baseline', 'Also run the no-skill baseline alongside the skill')
|
|
64
65
|
.action((agent, options) => {
|
|
65
66
|
const workspace = path.resolve(options.workspace);
|
|
66
67
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
@@ -69,7 +70,8 @@ program
|
|
|
69
70
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
70
71
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
71
72
|
const compareRefs = options.compareRef || [];
|
|
72
|
-
|
|
73
|
+
const compareBaseline = !!options.compareBaseline;
|
|
74
|
+
functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs, compareBaseline).catch(errorHandler);
|
|
73
75
|
});
|
|
74
76
|
const isMain = process.argv[1] && (() => {
|
|
75
77
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import { formatTokens, formatDuration } from '../utils/table-renderer.js';
|
|
4
|
+
import { formatTokens, formatDuration, hasFunctionalBaseline } from '../utils/table-renderer.js';
|
|
5
5
|
import { computeAssertionPassRate } from '../core/statistics.js';
|
|
6
6
|
export class HtmlReporter {
|
|
7
7
|
generate(report, runDir) {
|
|
@@ -32,7 +32,7 @@ function passColorClass(val) {
|
|
|
32
32
|
return 'red';
|
|
33
33
|
}
|
|
34
34
|
function isFunctional(report) {
|
|
35
|
-
return report.
|
|
35
|
+
return report.command === 'functional' || hasFunctionalBaseline(report);
|
|
36
36
|
}
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
// Metrics Grid
|
|
@@ -50,7 +50,7 @@ function renderMetricsGrid(report) {
|
|
|
50
50
|
const { metrics, results } = report;
|
|
51
51
|
const functional = isFunctional(report);
|
|
52
52
|
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
53
|
-
const allVersions = functional ? ['baseline', ...skillVersions] : skillVersions;
|
|
53
|
+
const allVersions = functional && hasFunctionalBaseline(report) ? ['baseline', ...skillVersions] : skillVersions;
|
|
54
54
|
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
55
55
|
const successRows = `<tr>
|
|
56
56
|
<td>Success Rate</td>
|
|
@@ -87,38 +87,64 @@ function renderMetricsGrid(report) {
|
|
|
87
87
|
</div>`;
|
|
88
88
|
}
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
|
-
//
|
|
90
|
+
// Expectations × Variants table (per-task drill-down)
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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>`;
|
|
105
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 };
|
|
106
114
|
}
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
function renderExpectationsTable(result, isFunctionalEval) {
|
|
116
|
+
const skillVersions = Object.keys(result.skillTrials);
|
|
117
|
+
const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
|
|
118
|
+
let canonical = [];
|
|
119
|
+
for (const v of allVersions) {
|
|
120
|
+
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
121
|
+
if (trials && trials.length > 0 && trials[0].assertionResults.length > 0) {
|
|
122
|
+
canonical = trials[0].assertionResults;
|
|
123
|
+
break;
|
|
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>
|
|
122
148
|
</div>`;
|
|
123
149
|
}
|
|
124
150
|
function avgTrialTokens(trials) {
|
|
@@ -135,7 +161,7 @@ function avgTrialDuration(trials) {
|
|
|
135
161
|
}
|
|
136
162
|
function renderTaskMiniGrid(result, isFunctionalEval) {
|
|
137
163
|
const skillVersions = Object.keys(result.skillTrials);
|
|
138
|
-
const allVersions = isFunctionalEval ? ['baseline', ...skillVersions] : skillVersions;
|
|
164
|
+
const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
|
|
139
165
|
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
140
166
|
const successRows = `<tr>
|
|
141
167
|
<td>Success Rate</td>
|
|
@@ -177,14 +203,7 @@ function renderTaskMiniGrid(result, isFunctionalEval) {
|
|
|
177
203
|
function renderTaskDetails(result, isFunctionalEval) {
|
|
178
204
|
const sections = [];
|
|
179
205
|
sections.push(renderTaskMiniGrid(result, isFunctionalEval));
|
|
180
|
-
|
|
181
|
-
sections.push('<div class="trial-group-label">Baseline</div>');
|
|
182
|
-
sections.push(...result.baselineTrials.map(t => renderTrial(t)));
|
|
183
|
-
}
|
|
184
|
-
for (const version of Object.keys(result.skillTrials)) {
|
|
185
|
-
sections.push(`<div class="trial-group-label">${version}</div>`);
|
|
186
|
-
sections.push(...result.skillTrials[version].map(t => renderTrial(t)));
|
|
187
|
-
}
|
|
206
|
+
sections.push(renderExpectationsTable(result, isFunctionalEval));
|
|
188
207
|
return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
|
|
189
208
|
}
|
|
190
209
|
// ---------------------------------------------------------------------------
|
|
@@ -281,31 +300,42 @@ tr:last-child td { border-bottom: none; }
|
|
|
281
300
|
.details-row > td { padding: 0; background: #f8fafc; }
|
|
282
301
|
.task-details { display: none; padding: 12px 16px; }
|
|
283
302
|
.task-details.visible { display: block; }
|
|
284
|
-
.trial-group-label { font-size: 11px; font-weight: 600; text-transform: uppercase; color: #94a3b8; letter-spacing: 0.05em; margin: 8px 0 4px; }
|
|
285
303
|
|
|
286
|
-
/*
|
|
287
|
-
.
|
|
288
|
-
.
|
|
289
|
-
.
|
|
290
|
-
.
|
|
291
|
-
.
|
|
292
|
-
.
|
|
293
|
-
.
|
|
304
|
+
/* Expectations × Variants table */
|
|
305
|
+
.expectations-table-wrap { border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden; background: #fff; }
|
|
306
|
+
.expectations-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
307
|
+
.expectations-table thead th { background: #f1f5f9; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #475569; padding: 10px 14px; text-align: left; border-bottom: 1px solid #e2e8f0; }
|
|
308
|
+
.expectations-table thead th.exp-col { width: auto; }
|
|
309
|
+
.expectations-table thead th.variant-col { width: 110px; text-align: center; }
|
|
310
|
+
.expectations-table tbody td { padding: 10px 14px; border-bottom: 1px solid #f1f5f9; vertical-align: middle; font-size: 13px; }
|
|
311
|
+
.expectations-table tbody tr.exp-row:last-of-type > td { border-bottom: none; }
|
|
312
|
+
.exp-text { color: #1e293b; line-height: 1.4; word-break: break-word; }
|
|
294
313
|
|
|
295
|
-
|
|
296
|
-
.
|
|
297
|
-
.
|
|
298
|
-
.
|
|
299
|
-
.
|
|
314
|
+
.pass-cell { text-align: center; border-left: 1px solid #f1f5f9; }
|
|
315
|
+
.pass-cell[data-detail] { cursor: pointer; user-select: none; transition: background 0.12s; }
|
|
316
|
+
.pass-cell[data-detail]:hover { background: #f1f5f9; }
|
|
317
|
+
.pass-cell.open { background: #e0f2fe; }
|
|
318
|
+
.pass-cell .rate { display: block; font-size: 13px; font-weight: 700; line-height: 1.1; }
|
|
319
|
+
.pass-cell .frac { display: block; font-size: 10px; font-weight: 600; color: #94a3b8; margin-top: 2px; letter-spacing: 0.03em; }
|
|
320
|
+
.pass-cell.green .rate { color: #16a34a; }
|
|
321
|
+
.pass-cell.amber .rate { color: #d97706; }
|
|
322
|
+
.pass-cell.red .rate { color: #dc2626; }
|
|
323
|
+
|
|
324
|
+
.exp-detail-row { display: none; }
|
|
325
|
+
.exp-detail-row.visible { display: table-row; }
|
|
326
|
+
.exp-detail-row > td { padding: 0; background: #f8fafc; border-bottom: 1px solid #f1f5f9; }
|
|
327
|
+
.exp-detail-inner { padding: 12px 14px 14px; border-left: 3px solid #3b82f6; }
|
|
328
|
+
.exp-detail-header { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #64748b; margin-bottom: 8px; }
|
|
329
|
+
.exp-detail-header .variant-pill { display: inline-block; background: #e0f2fe; color: #0369a1; padding: 1px 7px; border-radius: 4px; margin-left: 4px; font-weight: 700; }
|
|
330
|
+
.exp-trial-line { display: flex; gap: 8px; padding: 6px 0; border-top: 1px dashed #e2e8f0; }
|
|
331
|
+
.exp-trial-line:first-of-type { border-top: none; }
|
|
332
|
+
.exp-trial-icon { flex-shrink: 0; font-size: 13px; font-weight: 700; line-height: 1.4; width: 16px; }
|
|
333
|
+
.exp-trial-icon.pass { color: #16a34a; }
|
|
334
|
+
.exp-trial-icon.fail { color: #dc2626; }
|
|
335
|
+
.exp-trial-body { flex: 1; min-width: 0; font-size: 12px; color: #334155; line-height: 1.45; }
|
|
336
|
+
.exp-trial-body .trial-label { font-weight: 700; color: #475569; margin-right: 4px; }
|
|
337
|
+
.exp-trial-body .trial-reason { color: #64748b; }
|
|
300
338
|
|
|
301
|
-
/* Assertions */
|
|
302
|
-
.assertion { display: flex; gap: 8px; }
|
|
303
|
-
.assert-icon { flex-shrink: 0; font-size: 14px; margin-top: 1px; }
|
|
304
|
-
.assert-pass .assert-icon { color: #16a34a; }
|
|
305
|
-
.assert-fail .assert-icon { color: #dc2626; }
|
|
306
|
-
.assert-body { flex: 1; min-width: 0; }
|
|
307
|
-
.assert-text { font-size: 13px; color: #1e293b; word-break: break-word; }
|
|
308
|
-
.assert-reason { font-size: 12px; color: #64748b; margin-top: 2px; word-break: break-word; }
|
|
309
339
|
.muted { color: #94a3b8; font-size: 13px; }
|
|
310
340
|
</style>
|
|
311
341
|
</head>
|
|
@@ -345,6 +375,16 @@ tr:last-child td { border-bottom: none; }
|
|
|
345
375
|
btn.textContent = isOpen ? '▶' : '▼';
|
|
346
376
|
});
|
|
347
377
|
});
|
|
378
|
+
|
|
379
|
+
document.querySelectorAll('.pass-cell[data-detail]').forEach(function (cell) {
|
|
380
|
+
cell.addEventListener('click', function () {
|
|
381
|
+
const detailRow = document.getElementById(cell.getAttribute('data-detail'));
|
|
382
|
+
if (!detailRow) return;
|
|
383
|
+
const isOpen = detailRow.classList.contains('visible');
|
|
384
|
+
detailRow.classList.toggle('visible', !isOpen);
|
|
385
|
+
cell.classList.toggle('open', !isOpen);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
348
388
|
}());
|
|
349
389
|
</script>
|
|
350
390
|
</body>
|
|
@@ -51,6 +51,12 @@ export function formatTokens(n) {
|
|
|
51
51
|
return `${Math.round(n / 1_000)}K`;
|
|
52
52
|
return `${n}`;
|
|
53
53
|
}
|
|
54
|
+
export function hasFunctionalBaseline(report) {
|
|
55
|
+
if (report.results.some(result => (result.baselineTrials?.length ?? 0) > 0)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return report.metrics.passAtK['baseline'] !== undefined || report.metrics.assertionPassRate['baseline'] !== undefined;
|
|
59
|
+
}
|
|
54
60
|
function formatTokenStatsLine(stats) {
|
|
55
61
|
const total = formatTokens(stats.avgTotal);
|
|
56
62
|
const input = formatTokens(stats.avgInput);
|
|
@@ -189,7 +195,8 @@ export function renderFunctionalTable(report) {
|
|
|
189
195
|
const { results, metrics } = report;
|
|
190
196
|
// Identify all versions present (baseline + skill versions)
|
|
191
197
|
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
192
|
-
const
|
|
198
|
+
const hasBaseline = hasFunctionalBaseline(report);
|
|
199
|
+
const allVersions = hasBaseline ? ['baseline', ...skillVersions] : skillVersions;
|
|
193
200
|
const header = ['ID', 'Prompt'];
|
|
194
201
|
for (const version of allVersions) {
|
|
195
202
|
header.push(version);
|
|
@@ -201,9 +208,11 @@ export function renderFunctionalTable(report) {
|
|
|
201
208
|
const row = [result.taskId.toString(), promptSnippet];
|
|
202
209
|
// Baseline
|
|
203
210
|
const woTrials = result.baselineTrials || [];
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
if (hasBaseline) {
|
|
212
|
+
if (woTrials.some(t => t.isError) && !woTrials.every(t => t.isError))
|
|
213
|
+
hasPartialErrors = true;
|
|
214
|
+
row.push(formatAssertionRate(woTrials));
|
|
215
|
+
}
|
|
207
216
|
// Skills
|
|
208
217
|
for (const version of skillVersions) {
|
|
209
218
|
const wiTrials = result.skillTrials[version] || [];
|