@crewhaus/hangar-ui 0.5.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 +309 -0
- package/assets/hangar.css +1726 -0
- package/assets/index.html +21 -0
- package/assets/js/api.js +368 -0
- package/assets/js/app.js +734 -0
- package/assets/js/dom.js +228 -0
- package/assets/js/keys.js +159 -0
- package/assets/js/markdown.js +131 -0
- package/assets/js/notify.js +152 -0
- package/assets/js/omnibox.js +307 -0
- package/assets/js/router.js +280 -0
- package/assets/js/routes.js +793 -0
- package/assets/js/shapes.js +36 -0
- package/assets/js/supervision.js +1334 -0
- package/assets/js/util.js +426 -0
- package/assets/js/views/activity.js +140 -0
- package/assets/js/views/approvals.js +155 -0
- package/assets/js/views/channels.js +455 -0
- package/assets/js/views/control.js +253 -0
- package/assets/js/views/costs.js +103 -0
- package/assets/js/views/creds.js +610 -0
- package/assets/js/views/data.js +456 -0
- package/assets/js/views/deploy.js +106 -0
- package/assets/js/views/evals-lab.js +1029 -0
- package/assets/js/views/evals.js +342 -0
- package/assets/js/views/feedback.js +555 -0
- package/assets/js/views/health.js +176 -0
- package/assets/js/views/inbox.js +103 -0
- package/assets/js/views/inspect.js +751 -0
- package/assets/js/views/jobs.js +110 -0
- package/assets/js/views/library.js +605 -0
- package/assets/js/views/memory-fabric.js +2026 -0
- package/assets/js/views/memory.js +228 -0
- package/assets/js/views/onboarding.js +253 -0
- package/assets/js/views/overview.js +196 -0
- package/assets/js/views/panes.js +132 -0
- package/assets/js/views/proc.js +280 -0
- package/assets/js/views/review.js +164 -0
- package/assets/js/views/runs.js +452 -0
- package/assets/js/views/runtime.js +450 -0
- package/assets/js/views/schedulers.js +133 -0
- package/assets/js/views/security.js +925 -0
- package/assets/js/views/sessions.js +265 -0
- package/assets/js/views/settings.js +489 -0
- package/assets/js/views/spec-edit.js +1289 -0
- package/assets/js/views/spec.js +66 -0
- package/assets/js/views/thredz.js +1853 -0
- package/assets/js/views/token.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +145 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evals — the M3 half of the Evals tab: the typed launcher, matrix runs, CI
|
|
3
|
+
* suites, trends, the sample-size planner, judge calibration, grader
|
|
4
|
+
* quality, redteam, coverage, the drift sentinel, voice replays, the
|
|
5
|
+
* optimizer, the flywheel, experiments, and the annotation → distill join.
|
|
6
|
+
*
|
|
7
|
+
* The M1/M2 surface (`views/evals.js`: run history, drill-down, baselines)
|
|
8
|
+
* stays where it is and is rendered above this.
|
|
9
|
+
*
|
|
10
|
+
* Rules this screen must not break, because each one turns a measurement
|
|
11
|
+
* into a false alarm:
|
|
12
|
+
* - a PARTIAL run renders deflated and is never charted as a regression;
|
|
13
|
+
* - a REPLAYED run is badged not-live;
|
|
14
|
+
* - trends split agent spend from judge spend — the judge frequently costs
|
|
15
|
+
* more than the thing it is judging;
|
|
16
|
+
* - a matrix cell's crash class decides whether Retry is even offered: a
|
|
17
|
+
* billing failure is NOT retryable, however much it looks like a 429;
|
|
18
|
+
* - spending the locked test split is a typed, visible gesture.
|
|
19
|
+
*
|
|
20
|
+
* Every node is built with el()/text() — markup strings are banned app-wide,
|
|
21
|
+
* so nothing a grader, a model or a provider wrote can inject into this page.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { api } from "../api.js";
|
|
25
|
+
import { asOf, clear, collapsible, dot, el, emptyState, jsonPre, skeleton, toast } from "../dom.js";
|
|
26
|
+
import { fmtCount, fmtPct, fmtRelativeTime, fmtUsd } from "../util.js";
|
|
27
|
+
|
|
28
|
+
export async function renderEvalsLab(root, ctx) {
|
|
29
|
+
clear(root).appendChild(skeleton(4));
|
|
30
|
+
const [
|
|
31
|
+
trends,
|
|
32
|
+
matrix,
|
|
33
|
+
suites,
|
|
34
|
+
judge,
|
|
35
|
+
graders,
|
|
36
|
+
redteam,
|
|
37
|
+
coverage,
|
|
38
|
+
sentinel,
|
|
39
|
+
voice,
|
|
40
|
+
optimize,
|
|
41
|
+
flywheel,
|
|
42
|
+
experiments,
|
|
43
|
+
annotations,
|
|
44
|
+
] = await Promise.all(
|
|
45
|
+
[
|
|
46
|
+
api.evalTrends,
|
|
47
|
+
api.evalMatrix,
|
|
48
|
+
api.evalSuites,
|
|
49
|
+
api.judgeCalibration,
|
|
50
|
+
api.graderCards,
|
|
51
|
+
api.redteam,
|
|
52
|
+
api.evalCoverage,
|
|
53
|
+
api.sentinel,
|
|
54
|
+
api.voiceEvals,
|
|
55
|
+
api.optimizer,
|
|
56
|
+
api.flywheel,
|
|
57
|
+
api.experiments,
|
|
58
|
+
api.annotations,
|
|
59
|
+
].map((fn) => fn({ id: ctx.id }).catch((err) => ({ ok: false, status: 0, body: { err } }))),
|
|
60
|
+
);
|
|
61
|
+
clear(root);
|
|
62
|
+
const reload = () => renderEvalsLab(root, ctx);
|
|
63
|
+
root.appendChild(launcherCard(ctx, flywheel, reload));
|
|
64
|
+
root.appendChild(trendsCard(trends));
|
|
65
|
+
root.appendChild(matrixCard(ctx, matrix));
|
|
66
|
+
root.appendChild(suitesCard(ctx, suites, reload));
|
|
67
|
+
root.appendChild(plannerCard(ctx));
|
|
68
|
+
root.appendChild(judgeCard(ctx, judge, reload));
|
|
69
|
+
root.appendChild(gradersCard(ctx, graders, reload));
|
|
70
|
+
root.appendChild(redteamCard(ctx, redteam, reload));
|
|
71
|
+
root.appendChild(coverageCard(ctx, coverage, reload));
|
|
72
|
+
root.appendChild(sentinelCard(ctx, sentinel, reload));
|
|
73
|
+
if (payload(voice).applicable === true) root.appendChild(voiceCard(voice));
|
|
74
|
+
root.appendChild(optimizerCard(ctx, optimize, reload));
|
|
75
|
+
root.appendChild(flywheelCard(ctx, flywheel, reload));
|
|
76
|
+
root.appendChild(experimentsCard(ctx, experiments, reload));
|
|
77
|
+
root.appendChild(annotationsCard(annotations));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// shared bits
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/** The `{ok, status, body}` envelope's payload, tolerant of a failed call. */
|
|
85
|
+
function payload(res) {
|
|
86
|
+
return res && typeof res === "object" && res.body && typeof res.body === "object" ? res.body : {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A card whose body is only drawn when the server said something IS there;
|
|
90
|
+
* otherwise the standard empty state, which always names the verb. */
|
|
91
|
+
function panel(title, res, draw, extraHead) {
|
|
92
|
+
const data = payload(res);
|
|
93
|
+
const card = el("div", { class: "card" }, [
|
|
94
|
+
el("h3", { class: "card-title" }, [el("span", { text: title }), extraHead ?? null]),
|
|
95
|
+
]);
|
|
96
|
+
if (res && res.ok === false && res.status !== 0 && data.present === undefined) {
|
|
97
|
+
card.appendChild(el("p", { class: "muted", text: `unavailable (HTTP ${String(res.status)})` }));
|
|
98
|
+
return card;
|
|
99
|
+
}
|
|
100
|
+
if (data.present === false) {
|
|
101
|
+
card.appendChild(emptyState(String(data.note ?? "Nothing yet"), data.verb ?? null));
|
|
102
|
+
return card;
|
|
103
|
+
}
|
|
104
|
+
if (typeof data.note === "string" && data.note !== "") {
|
|
105
|
+
card.appendChild(el("p", { class: "muted", text: data.note }));
|
|
106
|
+
}
|
|
107
|
+
draw(card, data);
|
|
108
|
+
return card;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function table(headers, rows) {
|
|
112
|
+
return el("div", { class: "table-scroll" }, [
|
|
113
|
+
el("table", { class: "fleet" }, [
|
|
114
|
+
el(
|
|
115
|
+
"thead",
|
|
116
|
+
null,
|
|
117
|
+
el(
|
|
118
|
+
"tr",
|
|
119
|
+
null,
|
|
120
|
+
headers.map((h) => el("th", { text: h })),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
el("tbody", null, rows),
|
|
124
|
+
]),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stat(label, value) {
|
|
129
|
+
return el("div", { class: "mini" }, [
|
|
130
|
+
el("div", { class: "mini-num", text: value }),
|
|
131
|
+
el("div", { class: "mini-label", text: label }),
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function field(label, node) {
|
|
136
|
+
return el("label", { class: "field" }, [el("span", { class: "kv-k", text: label }), node]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function input(name, attrs) {
|
|
140
|
+
return el("input", { class: "input", name, ...(attrs ?? {}) });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readNumber(form, name) {
|
|
144
|
+
const raw = form.elements[name]?.value?.trim();
|
|
145
|
+
if (!raw) return undefined;
|
|
146
|
+
const n = Number(raw);
|
|
147
|
+
return Number.isFinite(n) ? n : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readText(form, name) {
|
|
151
|
+
const raw = form.elements[name]?.value?.trim();
|
|
152
|
+
return raw ? raw : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Post an M3 write and report BOTH outcomes — no write ever fails silently,
|
|
156
|
+
* and a refusal is rendered as the payload it is, not swallowed. */
|
|
157
|
+
async function submit(fn, params, body, okMessage, reload) {
|
|
158
|
+
const res = await fn(params, body).catch((err) => ({
|
|
159
|
+
ok: false,
|
|
160
|
+
status: 0,
|
|
161
|
+
body: { error: String(err) },
|
|
162
|
+
}));
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
toast(refusalText(res), "error");
|
|
165
|
+
return res;
|
|
166
|
+
}
|
|
167
|
+
toast(okMessage, "info");
|
|
168
|
+
if (reload) reload();
|
|
169
|
+
return res;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function refusalText(res) {
|
|
173
|
+
const body = payload(res);
|
|
174
|
+
const raw = typeof body.error === "string" ? body.error : "";
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(raw);
|
|
177
|
+
if (parsed && typeof parsed === "object" && typeof parsed.error === "string") {
|
|
178
|
+
return `${parsed.error}${parsed.route ? ` — use ${parsed.route}` : ""}`;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// not a structured refusal — fall through to the raw message
|
|
182
|
+
}
|
|
183
|
+
return raw || `HTTP ${String(res.status)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// the launcher
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Conventional paths default from the harness dir, so the common launch is
|
|
192
|
+
* one click. `--allow-test-split` is deliberately NOT a checkbox beside the
|
|
193
|
+
* others: it spends a held-out split, the spend is counted as a burn, and the
|
|
194
|
+
* server refuses it unless the release confirmation rides along.
|
|
195
|
+
*/
|
|
196
|
+
function launcherCard(ctx, flywheelRes, reload) {
|
|
197
|
+
const form = el("form", { class: "row-editor" }, [
|
|
198
|
+
field("dataset", input("dataset", { placeholder: "eval/dataset.jsonl (default)" })),
|
|
199
|
+
field("graders", input("graders", { placeholder: "eval/graders.yaml (default)" })),
|
|
200
|
+
field("repeats", input("repeats", { type: "number", min: "1", placeholder: "1" })),
|
|
201
|
+
field("seed", input("seed", { type: "number", min: "1", placeholder: "—" })),
|
|
202
|
+
field("budget $", input("budgetUsd", { type: "number", step: "0.01", placeholder: "—" })),
|
|
203
|
+
field("models (comma separated)", input("models", { placeholder: "matrix run" })),
|
|
204
|
+
]);
|
|
205
|
+
const gate = el("input", { type: "checkbox", name: "gate" });
|
|
206
|
+
const holdout = el("input", { type: "checkbox", name: "allowTestSplit" });
|
|
207
|
+
const confirmField = el("div", { class: "gated" }, [
|
|
208
|
+
el("span", {
|
|
209
|
+
class: "gated-why",
|
|
210
|
+
text: 'spending the held-out split is a release gesture — type "release" to confirm',
|
|
211
|
+
}),
|
|
212
|
+
input("releaseConfirm", { placeholder: "release" }),
|
|
213
|
+
]);
|
|
214
|
+
confirmField.hidden = true;
|
|
215
|
+
holdout.addEventListener("change", () => {
|
|
216
|
+
confirmField.hidden = !holdout.checked;
|
|
217
|
+
});
|
|
218
|
+
form.appendChild(el("label", { class: "check" }, [gate, el("span", { text: " gate this run" })]));
|
|
219
|
+
form.appendChild(
|
|
220
|
+
el("label", { class: "check" }, [
|
|
221
|
+
holdout,
|
|
222
|
+
el("span", { text: " spend the locked test split (recorded as a burn)" }),
|
|
223
|
+
]),
|
|
224
|
+
);
|
|
225
|
+
form.appendChild(confirmField);
|
|
226
|
+
|
|
227
|
+
const run = el("button", { class: "btn btn-primary", type: "submit", text: "Run eval" });
|
|
228
|
+
form.appendChild(el("div", { class: "editor-actions" }, [run]));
|
|
229
|
+
form.addEventListener("submit", async (event) => {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
const models = readText(form, "models");
|
|
232
|
+
const body = {
|
|
233
|
+
...(readText(form, "dataset") ? { dataset: readText(form, "dataset") } : {}),
|
|
234
|
+
...(readText(form, "graders") ? { graders: readText(form, "graders") } : {}),
|
|
235
|
+
...(readNumber(form, "repeats") ? { repeats: readNumber(form, "repeats") } : {}),
|
|
236
|
+
...(readNumber(form, "seed") ? { seed: readNumber(form, "seed") } : {}),
|
|
237
|
+
...(readNumber(form, "budgetUsd") ? { budgetUsd: readNumber(form, "budgetUsd") } : {}),
|
|
238
|
+
...(models
|
|
239
|
+
? {
|
|
240
|
+
models: models
|
|
241
|
+
.split(",")
|
|
242
|
+
.map((m) => m.trim())
|
|
243
|
+
.filter(Boolean),
|
|
244
|
+
}
|
|
245
|
+
: {}),
|
|
246
|
+
...(gate.checked ? { gate: true } : {}),
|
|
247
|
+
...(holdout.checked ? { allowTestSplit: true } : {}),
|
|
248
|
+
...(holdout.checked && readText(form, "releaseConfirm") === "release"
|
|
249
|
+
? { releaseConfirm: true }
|
|
250
|
+
: {}),
|
|
251
|
+
};
|
|
252
|
+
run.disabled = true;
|
|
253
|
+
const res = await submit(api.evalLaunch, { id: ctx.id }, body, "Eval queued", null);
|
|
254
|
+
run.disabled = false;
|
|
255
|
+
if (res.ok) {
|
|
256
|
+
const warnings = Array.isArray(payload(res).warnings) ? payload(res).warnings : [];
|
|
257
|
+
for (const w of warnings) toast(String(w), "info");
|
|
258
|
+
if (reload) reload();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const card = el("div", { class: "card" }, [
|
|
263
|
+
el("h3", { class: "card-title" }, [el("span", { text: "Launch an eval" })]),
|
|
264
|
+
el("p", {
|
|
265
|
+
class: "muted",
|
|
266
|
+
text: "Conventional paths default from the harness directory; progress streams on the run feed.",
|
|
267
|
+
}),
|
|
268
|
+
form,
|
|
269
|
+
]);
|
|
270
|
+
// The dataset-precedence trap belongs here, where a launch is composed: a
|
|
271
|
+
// conventional eval/dataset.jsonl SILENTLY wins over registry:<spec>-ratings.
|
|
272
|
+
const precedence = payload(flywheelRes).datasetPrecedence;
|
|
273
|
+
if (precedence && precedence.shadowing === true) {
|
|
274
|
+
card.appendChild(
|
|
275
|
+
el("p", { class: "gated-why" }, [dot("warn", "dataset precedence"), text(precedence.note)]),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return card;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// trends
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function trendsCard(res) {
|
|
286
|
+
return panel("Trends", res, (card, data) => {
|
|
287
|
+
const spend = data.spend ?? {};
|
|
288
|
+
card.appendChild(
|
|
289
|
+
el("div", { class: "mini-row" }, [
|
|
290
|
+
stat("agent spend", fmtUsd(spend.agentUsd ?? null)),
|
|
291
|
+
stat("judge spend", fmtUsd(spend.judgeUsd ?? null)),
|
|
292
|
+
stat("judge share", typeof spend.judgeShare === "number" ? fmtPct(spend.judgeShare) : "—"),
|
|
293
|
+
]),
|
|
294
|
+
);
|
|
295
|
+
card.appendChild(
|
|
296
|
+
el("p", {
|
|
297
|
+
class: "muted",
|
|
298
|
+
text: "Agent and judge spend are reported apart — one blended number hides which of the two is worth cutting.",
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
for (const series of data.series ?? []) {
|
|
302
|
+
const rows = (series.points ?? []).map((p) =>
|
|
303
|
+
el("tr", { class: p.partial === true ? "folded" : null }, [
|
|
304
|
+
el("td", { class: "mono", text: String(p.runId ?? "") }),
|
|
305
|
+
el("td", { text: fmtRelativeTime(p.ts, Date.now()) }),
|
|
306
|
+
el("td", null, [dot(p.partial === true ? "unknown" : "ok", fmtPct(p.passRate))]),
|
|
307
|
+
el("td", { class: "num", text: fmtScore(p.meanScore) }),
|
|
308
|
+
el("td", { class: "num", text: fmtUsd(p.agentCostUsd) }),
|
|
309
|
+
el("td", { class: "num", text: fmtUsd(p.judgeCostUsd) }),
|
|
310
|
+
el("td", { class: "cell-caps" }, [
|
|
311
|
+
p.partial === true
|
|
312
|
+
? el("span", {
|
|
313
|
+
class: "chip chip-warn",
|
|
314
|
+
title: "ended early — its figures read deflated and never move the trend",
|
|
315
|
+
text: "partial",
|
|
316
|
+
})
|
|
317
|
+
: null,
|
|
318
|
+
p.replayed === true
|
|
319
|
+
? el("span", {
|
|
320
|
+
class: "chip",
|
|
321
|
+
title:
|
|
322
|
+
"every tool result came from a cassette — not a measurement of the live system",
|
|
323
|
+
text: "not live",
|
|
324
|
+
})
|
|
325
|
+
: null,
|
|
326
|
+
p.pinned === true ? el("span", { class: "chip chip-group", text: "baseline" }) : null,
|
|
327
|
+
]),
|
|
328
|
+
]),
|
|
329
|
+
);
|
|
330
|
+
card.appendChild(
|
|
331
|
+
collapsible(
|
|
332
|
+
[
|
|
333
|
+
el("span", { class: "mono", text: `${series.specName} / ${series.datasetName}` }),
|
|
334
|
+
el("span", {
|
|
335
|
+
class: "muted",
|
|
336
|
+
text: ` ${fmtCount(series.measuredCount)} measured${
|
|
337
|
+
series.partialCount ? ` · ${String(series.partialCount)} partial` : ""
|
|
338
|
+
}${
|
|
339
|
+
typeof series.deltaPp === "number"
|
|
340
|
+
? ` · ${series.deltaPp >= 0 ? "+" : ""}${series.deltaPp.toFixed(1)}pp`
|
|
341
|
+
: " · no trend yet"
|
|
342
|
+
}`,
|
|
343
|
+
}),
|
|
344
|
+
],
|
|
345
|
+
[table(["Run", "When", "Pass", "Mean", "Agent $", "Judge $", "Flags"], rows)],
|
|
346
|
+
true,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function fmtScore(v) {
|
|
354
|
+
return typeof v === "number" && Number.isFinite(v) ? v.toFixed(2) : "—";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// matrix
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Matrix cells live outside run history. The crash CLASS is the actionable
|
|
363
|
+
* part: billing and systemic failures get their remedy, never a Retry button
|
|
364
|
+
* — re-running a quota exhaustion just spends another minute proving it.
|
|
365
|
+
*/
|
|
366
|
+
function matrixCard(ctx, res) {
|
|
367
|
+
return panel("Model matrix", res, (card, data) => {
|
|
368
|
+
const rows = (data.cells ?? []).map((cell) => {
|
|
369
|
+
const crash = cell.crash;
|
|
370
|
+
return el("tr", null, [
|
|
371
|
+
el("td", { class: "mono", text: String(cell.model ?? "") }),
|
|
372
|
+
el("td", null, [
|
|
373
|
+
crash
|
|
374
|
+
? dot(crash.retryable ? "warn" : "bad", crash.kind)
|
|
375
|
+
: dot("ok", fmtPct(cell.passRate)),
|
|
376
|
+
]),
|
|
377
|
+
el("td", { class: "num", text: fmtScore(cell.meanScore) }),
|
|
378
|
+
el("td", { class: "num", text: fmtCount(cell.sampleCount) }),
|
|
379
|
+
el("td", { class: "num", text: fmtUsd(cell.costPer1kSamplesUsd) }),
|
|
380
|
+
el("td", { class: "muted", text: crash ? String(crash.remedy) : "" }),
|
|
381
|
+
el("td", { class: "cell-actions" }, [
|
|
382
|
+
crash?.retryable
|
|
383
|
+
? retryCellBtn(ctx, cell)
|
|
384
|
+
: crash
|
|
385
|
+
? el("span", {
|
|
386
|
+
class: "chip chip-warn",
|
|
387
|
+
title: "re-running cannot change this outcome",
|
|
388
|
+
text: "not retryable",
|
|
389
|
+
})
|
|
390
|
+
: null,
|
|
391
|
+
]),
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
card.appendChild(
|
|
395
|
+
table(["Model", "Verdict", "Mean", "Samples", "Est $/1k", "Remedy", ""], rows),
|
|
396
|
+
);
|
|
397
|
+
for (const root of data.roots ?? []) {
|
|
398
|
+
card.appendChild(
|
|
399
|
+
el("p", { class: "muted" }, [
|
|
400
|
+
el("span", { class: "mono", text: String(root.root) }),
|
|
401
|
+
asOf(root.generatedAt) ?? null,
|
|
402
|
+
]),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function retryCellBtn(ctx, cell) {
|
|
409
|
+
const btn = el("button", { class: "btn btn-ghost", type: "button", text: "Re-run cell" });
|
|
410
|
+
btn.addEventListener("click", async () => {
|
|
411
|
+
btn.disabled = true;
|
|
412
|
+
await submit(
|
|
413
|
+
api.evalLaunch,
|
|
414
|
+
{ id: ctx.id },
|
|
415
|
+
{ models: [String(cell.model)] },
|
|
416
|
+
"Cell re-queued",
|
|
417
|
+
null,
|
|
418
|
+
);
|
|
419
|
+
btn.disabled = false;
|
|
420
|
+
});
|
|
421
|
+
return btn;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// CI suites
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
function suitesCard(ctx, res, reload) {
|
|
429
|
+
const runner = el("form", { class: "row-editor" }, [
|
|
430
|
+
field("suite", input("suite", { placeholder: "eval/suite.yaml", required: true })),
|
|
431
|
+
field(
|
|
432
|
+
"tier",
|
|
433
|
+
el(
|
|
434
|
+
"select",
|
|
435
|
+
{ class: "input", name: "tier" },
|
|
436
|
+
["fast", "nightly", "release"].map((t) => el("option", { value: t, text: t })),
|
|
437
|
+
),
|
|
438
|
+
),
|
|
439
|
+
]);
|
|
440
|
+
const go = el("button", { class: "btn", type: "submit", text: "Run suite" });
|
|
441
|
+
runner.appendChild(el("div", { class: "editor-actions" }, [go]));
|
|
442
|
+
runner.addEventListener("submit", async (event) => {
|
|
443
|
+
event.preventDefault();
|
|
444
|
+
go.disabled = true;
|
|
445
|
+
await submit(
|
|
446
|
+
api.evalSuiteRun,
|
|
447
|
+
{ id: ctx.id },
|
|
448
|
+
{ suite: readText(runner, "suite"), tier: runner.elements["tier"].value },
|
|
449
|
+
"Suite queued",
|
|
450
|
+
reload,
|
|
451
|
+
);
|
|
452
|
+
go.disabled = false;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const card = panel("CI suites", res, (node, data) => {
|
|
456
|
+
for (const suite of data.suites ?? []) {
|
|
457
|
+
const rows = (suite.entries ?? []).map((entry) =>
|
|
458
|
+
el("tr", null, [
|
|
459
|
+
el("td", { class: "mono", text: String(entry.name) }),
|
|
460
|
+
el("td", null, [dot(entry.passed ? "ok" : "bad", entry.passed ? "pass" : "fail")]),
|
|
461
|
+
el("td", { class: "num", text: fmtPct(entry.passRate) }),
|
|
462
|
+
el("td", { class: "num", text: fmtScore(entry.meanScore) }),
|
|
463
|
+
el("td", {
|
|
464
|
+
class: "muted",
|
|
465
|
+
text: [
|
|
466
|
+
entry.minPassRate != null ? `min pass ${fmtPct(entry.minPassRate)}` : "",
|
|
467
|
+
entry.minMeanScore != null ? `min score ${fmtScore(entry.minMeanScore)}` : "",
|
|
468
|
+
]
|
|
469
|
+
.filter(Boolean)
|
|
470
|
+
.join(" · "),
|
|
471
|
+
}),
|
|
472
|
+
el("td", { class: "cell-caps" }, [
|
|
473
|
+
entry.partial === true
|
|
474
|
+
? el("span", {
|
|
475
|
+
class: "chip chip-warn",
|
|
476
|
+
title: "a partial entry always fails — its aborted samples counted as errors",
|
|
477
|
+
text: "partial ⇒ fail",
|
|
478
|
+
})
|
|
479
|
+
: null,
|
|
480
|
+
]),
|
|
481
|
+
]),
|
|
482
|
+
);
|
|
483
|
+
node.appendChild(
|
|
484
|
+
collapsible(
|
|
485
|
+
[
|
|
486
|
+
dot(suite.passed ? "ok" : "bad", `${suite.tier} tier`),
|
|
487
|
+
el("span", { class: "muted", text: ` ${String(suite.dir)}` }),
|
|
488
|
+
],
|
|
489
|
+
[table(["Entry", "Verdict", "Pass", "Mean", "Floors", ""], rows)],
|
|
490
|
+
suite.passed !== true,
|
|
491
|
+
),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
card.appendChild(runner);
|
|
496
|
+
return card;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// the sample-size planner
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
/** Pure offline arithmetic — nothing runs and nothing is written, which is
|
|
504
|
+
* exactly why it can be asked BEFORE spending anything. */
|
|
505
|
+
function plannerCard(ctx) {
|
|
506
|
+
const out = el("div", { class: "mini-row" });
|
|
507
|
+
const form = el("form", { class: "row-editor" }, [
|
|
508
|
+
field(
|
|
509
|
+
"delta to detect",
|
|
510
|
+
input("targetDelta", { type: "number", step: "0.01", value: "0.05", required: true }),
|
|
511
|
+
),
|
|
512
|
+
field(
|
|
513
|
+
"measured base rate",
|
|
514
|
+
input("baseRate", { type: "number", step: "0.01", placeholder: "0.5" }),
|
|
515
|
+
),
|
|
516
|
+
]);
|
|
517
|
+
const go = el("button", { class: "btn", type: "submit", text: "Plan" });
|
|
518
|
+
form.appendChild(el("div", { class: "editor-actions" }, [go]));
|
|
519
|
+
form.addEventListener("submit", async (event) => {
|
|
520
|
+
event.preventDefault();
|
|
521
|
+
const body = {
|
|
522
|
+
targetDelta: readNumber(form, "targetDelta"),
|
|
523
|
+
...(readNumber(form, "baseRate") !== undefined
|
|
524
|
+
? { baseRate: readNumber(form, "baseRate") }
|
|
525
|
+
: {}),
|
|
526
|
+
};
|
|
527
|
+
const res = await api
|
|
528
|
+
.evalPlan({ id: ctx.id }, body)
|
|
529
|
+
.catch((err) => ({ ok: false, status: 0, body: { error: String(err) } }));
|
|
530
|
+
clear(out);
|
|
531
|
+
if (!res.ok) {
|
|
532
|
+
toast(refusalText(res), "error");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const data = payload(res);
|
|
536
|
+
out.appendChild(stat("samples needed", fmtCount(data.samples)));
|
|
537
|
+
out.appendChild(stat("confidence", fmtPct(data.confidence)));
|
|
538
|
+
out.appendChild(stat("formula", String(data.formula ?? "")));
|
|
539
|
+
if (data.caveat) out.appendChild(el("p", { class: "muted", text: String(data.caveat) }));
|
|
540
|
+
});
|
|
541
|
+
return el("div", { class: "card" }, [
|
|
542
|
+
el("h3", { class: "card-title" }, [el("span", { text: "Sample-size planner" })]),
|
|
543
|
+
el("p", {
|
|
544
|
+
class: "muted",
|
|
545
|
+
text: "How many samples it takes to detect a delta. Offline arithmetic — no run, no cost.",
|
|
546
|
+
}),
|
|
547
|
+
form,
|
|
548
|
+
out,
|
|
549
|
+
]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// judge calibration + graders
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
function judgeCard(ctx, res, reload) {
|
|
557
|
+
const card = panel("Judge calibration", res, (node, data) => {
|
|
558
|
+
node.appendChild(
|
|
559
|
+
table(
|
|
560
|
+
["Spec", "Cut", "Judge model", "Calibrated"],
|
|
561
|
+
(data.specs ?? []).map((s) =>
|
|
562
|
+
el("tr", null, [
|
|
563
|
+
el("td", { class: "mono", text: String(s.spec) }),
|
|
564
|
+
el("td", { class: "num", text: fmtScore(s.cut) }),
|
|
565
|
+
el("td", { class: "mono", text: String(s.judgeModel ?? "—") }),
|
|
566
|
+
el("td", { text: fmtRelativeTime(s.calibratedAt, Date.now()) }),
|
|
567
|
+
]),
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
const preview = el("button", { class: "btn btn-ghost", type: "button", text: "Preview ROC cut" });
|
|
573
|
+
preview.addEventListener("click", async () => {
|
|
574
|
+
preview.disabled = true;
|
|
575
|
+
await submit(api.judgeCalibrate, { id: ctx.id }, {}, "Calibration preview queued", reload);
|
|
576
|
+
preview.disabled = false;
|
|
577
|
+
});
|
|
578
|
+
const apply = el("button", {
|
|
579
|
+
class: "btn btn-danger",
|
|
580
|
+
type: "button",
|
|
581
|
+
text: "Calibrate + apply",
|
|
582
|
+
});
|
|
583
|
+
apply.addEventListener("click", async () => {
|
|
584
|
+
if (
|
|
585
|
+
!window.confirm("Applying a calibration changes how every LATER run is scored. Continue?")
|
|
586
|
+
) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
apply.disabled = true;
|
|
590
|
+
await submit(
|
|
591
|
+
api.judgeCalibrate,
|
|
592
|
+
{ id: ctx.id },
|
|
593
|
+
{ apply: true, confirm: true },
|
|
594
|
+
"Calibration queued",
|
|
595
|
+
reload,
|
|
596
|
+
);
|
|
597
|
+
apply.disabled = false;
|
|
598
|
+
});
|
|
599
|
+
card.appendChild(el("div", { class: "editor-actions" }, [preview, apply]));
|
|
600
|
+
return card;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function gradersCard(ctx, res, reload) {
|
|
604
|
+
const card = panel("Grader quality", res, (node, data) => {
|
|
605
|
+
node.appendChild(
|
|
606
|
+
table(
|
|
607
|
+
["Kind", "Name", "Judge model", "Runs", "Datasets"],
|
|
608
|
+
(data.graders ?? []).map((g) =>
|
|
609
|
+
el("tr", null, [
|
|
610
|
+
el("td", { text: String(g.kind) }),
|
|
611
|
+
el("td", { class: "mono", text: String(g.name ?? g.gradersHash ?? "") }),
|
|
612
|
+
el("td", { class: "mono", text: String(g.judgeModel ?? "—") }),
|
|
613
|
+
el("td", { class: "num", text: fmtCount(g.runs) }),
|
|
614
|
+
el("td", { class: "muted", text: (g.datasets ?? []).join(", ") }),
|
|
615
|
+
]),
|
|
616
|
+
),
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
if (data.gradersFile) {
|
|
620
|
+
node.appendChild(el("p", { class: "muted mono", text: String(data.gradersFile) }));
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
const suggest = el("button", { class: "btn btn-ghost", type: "button", text: "Suggest graders" });
|
|
624
|
+
suggest.addEventListener("click", async () => {
|
|
625
|
+
suggest.disabled = true;
|
|
626
|
+
await submit(api.gradersSuggest, { id: ctx.id }, {}, "Grader draft queued (advisory)", reload);
|
|
627
|
+
suggest.disabled = false;
|
|
628
|
+
});
|
|
629
|
+
const meta = el("button", { class: "btn btn-ghost", type: "button", text: "Meta-eval graders" });
|
|
630
|
+
meta.addEventListener("click", async () => {
|
|
631
|
+
meta.disabled = true;
|
|
632
|
+
await submit(api.gradersTest, { id: ctx.id }, { golden: true }, "Meta-eval queued", reload);
|
|
633
|
+
meta.disabled = false;
|
|
634
|
+
});
|
|
635
|
+
card.appendChild(el("div", { class: "editor-actions" }, [suggest, meta]));
|
|
636
|
+
return card;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// redteam, coverage, sentinel, voice
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
function redteamCard(ctx, res, reload) {
|
|
644
|
+
const card = panel("Redteam", res, (node, data) => {
|
|
645
|
+
node.appendChild(
|
|
646
|
+
el("div", { class: "mini-row" }, [
|
|
647
|
+
stat(
|
|
648
|
+
"attack success",
|
|
649
|
+
typeof data.latestAttackSuccessRate === "number"
|
|
650
|
+
? fmtPct(data.latestAttackSuccessRate)
|
|
651
|
+
: "—",
|
|
652
|
+
),
|
|
653
|
+
stat("attack dataset", data.dataset ? String(data.dataset.name) : "—"),
|
|
654
|
+
]),
|
|
655
|
+
);
|
|
656
|
+
node.appendChild(
|
|
657
|
+
table(
|
|
658
|
+
["Run", "When", "Attack success", "Samples"],
|
|
659
|
+
(data.runs ?? []).map((r) =>
|
|
660
|
+
el("tr", { class: r.partial ? "folded" : null }, [
|
|
661
|
+
el("td", { class: "mono", text: String(r.runId) }),
|
|
662
|
+
el("td", { text: fmtRelativeTime(r.ts, Date.now()) }),
|
|
663
|
+
el("td", null, [
|
|
664
|
+
dot(r.attackSuccessRate > 0 ? "bad" : "ok", fmtPct(r.attackSuccessRate)),
|
|
665
|
+
]),
|
|
666
|
+
el("td", { class: "num", text: fmtCount(r.sampleCount) }),
|
|
667
|
+
]),
|
|
668
|
+
),
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
const generate = el("button", {
|
|
673
|
+
class: "btn btn-ghost",
|
|
674
|
+
type: "button",
|
|
675
|
+
text: "Generate attacks",
|
|
676
|
+
});
|
|
677
|
+
generate.addEventListener("click", async () => {
|
|
678
|
+
generate.disabled = true;
|
|
679
|
+
await submit(api.redteamGenerate, { id: ctx.id }, {}, "Attack generation queued", reload);
|
|
680
|
+
generate.disabled = false;
|
|
681
|
+
});
|
|
682
|
+
card.appendChild(el("div", { class: "editor-actions" }, [generate]));
|
|
683
|
+
return card;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Coverage gaps hand off to `dataset mine` — the sessions that DEMONSTRATE
|
|
687
|
+
* a gap are what make "draft samples from these" a real hand-off. */
|
|
688
|
+
function coverageCard(ctx, res, reload) {
|
|
689
|
+
return panel("Coverage gaps", res, (node, data) => {
|
|
690
|
+
node.appendChild(
|
|
691
|
+
table(
|
|
692
|
+
["Behavior", "Kind", "Seen", "Sessions"],
|
|
693
|
+
(data.gaps ?? []).map((gap) =>
|
|
694
|
+
el("tr", null, [
|
|
695
|
+
el("td", { text: String(gap.behavior) }),
|
|
696
|
+
el("td", { class: "muted", text: String(gap.kind ?? "") }),
|
|
697
|
+
el("td", { class: "num", text: fmtCount(gap.frequency) }),
|
|
698
|
+
el("td", { class: "mono muted", text: (gap.sessions ?? []).slice(0, 3).join(" ") }),
|
|
699
|
+
]),
|
|
700
|
+
),
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
const mine = el("button", {
|
|
704
|
+
class: "btn btn-ghost",
|
|
705
|
+
type: "button",
|
|
706
|
+
text: "Draft samples from these sessions",
|
|
707
|
+
});
|
|
708
|
+
mine.addEventListener("click", async () => {
|
|
709
|
+
mine.disabled = true;
|
|
710
|
+
await submit(
|
|
711
|
+
api.datasetMine,
|
|
712
|
+
{ id: ctx.id },
|
|
713
|
+
{ name: `${String(ctx.detail?.entry?.specName ?? "mined")}-mined`, dryRun: true },
|
|
714
|
+
"Mining preview ready — open the Datasets tab",
|
|
715
|
+
reload,
|
|
716
|
+
);
|
|
717
|
+
mine.disabled = false;
|
|
718
|
+
});
|
|
719
|
+
node.appendChild(el("div", { class: "editor-actions" }, [mine]));
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Sentinel attribution is CONDITIONAL: a flip is provider drift only when
|
|
725
|
+
* spec, dataset, graders and judge all still match the frozen baseline. When
|
|
726
|
+
* one moved, the panel names it instead of blaming the provider.
|
|
727
|
+
*/
|
|
728
|
+
function sentinelCard(ctx, res, reload) {
|
|
729
|
+
const card = panel("Provider-drift sentinel", res, (node, data) => {
|
|
730
|
+
const attribution = data.attribution ?? {};
|
|
731
|
+
node.appendChild(
|
|
732
|
+
el("p", null, [
|
|
733
|
+
dot(
|
|
734
|
+
attribution.providerDrift ? "warn" : "unknown",
|
|
735
|
+
attribution.providerDrift ? "attributable to provider drift" : "not attributable",
|
|
736
|
+
),
|
|
737
|
+
]),
|
|
738
|
+
);
|
|
739
|
+
node.appendChild(el("p", { class: "muted", text: String(attribution.reason ?? "") }));
|
|
740
|
+
if ((attribution.mismatches ?? []).length > 0) {
|
|
741
|
+
node.appendChild(
|
|
742
|
+
el(
|
|
743
|
+
"ul",
|
|
744
|
+
{ class: "check-list" },
|
|
745
|
+
attribution.mismatches.map((m) => el("li", { text: String(m) })),
|
|
746
|
+
),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (data.baseline) node.appendChild(jsonPre(data.baseline));
|
|
750
|
+
});
|
|
751
|
+
const run = el("button", { class: "btn btn-ghost", type: "button", text: "Run sentinel" });
|
|
752
|
+
run.addEventListener("click", async () => {
|
|
753
|
+
run.disabled = true;
|
|
754
|
+
await submit(api.sentinelRun, { id: ctx.id }, {}, "Sentinel queued", reload);
|
|
755
|
+
run.disabled = false;
|
|
756
|
+
});
|
|
757
|
+
card.appendChild(el("div", { class: "editor-actions" }, [run]));
|
|
758
|
+
return card;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function voiceCard(res) {
|
|
762
|
+
return panel("Voice replays", res, (node, data) => {
|
|
763
|
+
node.appendChild(
|
|
764
|
+
el("div", { class: "mini-row" }, [stat("replays on disk", fmtCount(data.replays))]),
|
|
765
|
+
);
|
|
766
|
+
if (data.report) node.appendChild(jsonPre(data.report));
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
// optimizer, flywheel, experiments, annotations
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
function optimizerCard(ctx, res, reload) {
|
|
775
|
+
const card = panel("Optimizer", res, (node, data) => {
|
|
776
|
+
node.appendChild(el("p", { class: "muted", text: String(data.acceptance ?? "") }));
|
|
777
|
+
node.appendChild(
|
|
778
|
+
table(
|
|
779
|
+
["Run", "When", "Artifacts", ""],
|
|
780
|
+
(data.runs ?? []).map((r) =>
|
|
781
|
+
el("tr", null, [
|
|
782
|
+
el("td", { class: "mono", text: String(r.optRunId) }),
|
|
783
|
+
el("td", { text: fmtRelativeTime(r.at, Date.now()) }),
|
|
784
|
+
el("td", { class: "cell-caps" }, [
|
|
785
|
+
r.hasDecisions ? el("span", { class: "chip", text: "decisions" }) : null,
|
|
786
|
+
r.hasPatchedYaml ? el("span", { class: "chip", text: "patched.yaml" }) : null,
|
|
787
|
+
]),
|
|
788
|
+
el("td", { class: "cell-actions" }, [artifactsBtn(ctx, r.optRunId)]),
|
|
789
|
+
]),
|
|
790
|
+
),
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
const form = el("form", { class: "row-editor" }, [
|
|
795
|
+
field(
|
|
796
|
+
"mutator",
|
|
797
|
+
el(
|
|
798
|
+
"select",
|
|
799
|
+
{ class: "input", name: "mutator" },
|
|
800
|
+
["rule-based", "claude", "meta-harness"].map((m) => el("option", { value: m, text: m })),
|
|
801
|
+
),
|
|
802
|
+
),
|
|
803
|
+
field("budget $", input("budgetUsd", { type: "number", step: "0.01", placeholder: "—" })),
|
|
804
|
+
]);
|
|
805
|
+
const go = el("button", { class: "btn", type: "submit", text: "Optimize" });
|
|
806
|
+
form.appendChild(el("div", { class: "editor-actions" }, [go]));
|
|
807
|
+
form.addEventListener("submit", async (event) => {
|
|
808
|
+
event.preventDefault();
|
|
809
|
+
go.disabled = true;
|
|
810
|
+
await submit(
|
|
811
|
+
api.optimizerRun,
|
|
812
|
+
{ id: ctx.id },
|
|
813
|
+
{
|
|
814
|
+
mutator: form.elements["mutator"].value,
|
|
815
|
+
...(readNumber(form, "budgetUsd") ? { budgetUsd: readNumber(form, "budgetUsd") } : {}),
|
|
816
|
+
},
|
|
817
|
+
"Optimizer queued",
|
|
818
|
+
reload,
|
|
819
|
+
);
|
|
820
|
+
go.disabled = false;
|
|
821
|
+
});
|
|
822
|
+
card.appendChild(form);
|
|
823
|
+
return card;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function artifactsBtn(ctx, optRunId) {
|
|
827
|
+
const btn = el("button", { class: "btn btn-ghost", type: "button", text: "Artifacts" });
|
|
828
|
+
const holder = el("div");
|
|
829
|
+
btn.addEventListener("click", async () => {
|
|
830
|
+
btn.disabled = true;
|
|
831
|
+
const res = await api
|
|
832
|
+
.optimizerArtifacts({ id: ctx.id, optRunId })
|
|
833
|
+
.catch((err) => ({ ok: false, status: 0, body: { error: String(err) } }));
|
|
834
|
+
btn.disabled = false;
|
|
835
|
+
clear(holder);
|
|
836
|
+
if (!res.ok) {
|
|
837
|
+
toast(refusalText(res), "error");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const data = payload(res);
|
|
841
|
+
holder.appendChild(
|
|
842
|
+
table(
|
|
843
|
+
["File", "Kind", "Bytes"],
|
|
844
|
+
(data.files ?? []).map((f) =>
|
|
845
|
+
el("tr", null, [
|
|
846
|
+
el("td", { class: "mono", text: String(f.name) }),
|
|
847
|
+
el("td", { text: String(f.kind) }),
|
|
848
|
+
el("td", { class: "num", text: fmtCount(f.size) }),
|
|
849
|
+
]),
|
|
850
|
+
),
|
|
851
|
+
),
|
|
852
|
+
);
|
|
853
|
+
if (data.decisions) holder.appendChild(jsonPre(data.decisions));
|
|
854
|
+
});
|
|
855
|
+
return el("div", { class: "job-action" }, [btn, holder]);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function flywheelCard(ctx, res, reload) {
|
|
859
|
+
const card = panel("Flywheel", res, (node, data) => {
|
|
860
|
+
const precedence = data.datasetPrecedence ?? {};
|
|
861
|
+
node.appendChild(
|
|
862
|
+
el("p", null, [
|
|
863
|
+
dot(
|
|
864
|
+
precedence.shadowing ? "warn" : "ok",
|
|
865
|
+
precedence.shadowing ? "dataset shadowed" : "dataset precedence clear",
|
|
866
|
+
),
|
|
867
|
+
]),
|
|
868
|
+
);
|
|
869
|
+
node.appendChild(el("p", { class: "muted", text: String(precedence.note ?? "") }));
|
|
870
|
+
if ((data.workflows ?? []).length > 0) {
|
|
871
|
+
node.appendChild(
|
|
872
|
+
el(
|
|
873
|
+
"ul",
|
|
874
|
+
{ class: "check-list" },
|
|
875
|
+
data.workflows.map((w) => el("li", { class: "mono", text: String(w) })),
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
for (const action of ["run", "init"]) {
|
|
881
|
+
const btn = el("button", {
|
|
882
|
+
class: action === "run" ? "btn" : "btn btn-ghost",
|
|
883
|
+
type: "button",
|
|
884
|
+
text: action === "run" ? "Run the loop" : "Scaffold workflows",
|
|
885
|
+
});
|
|
886
|
+
btn.addEventListener("click", async () => {
|
|
887
|
+
btn.disabled = true;
|
|
888
|
+
await submit(
|
|
889
|
+
api.flywheelRun,
|
|
890
|
+
{ id: ctx.id },
|
|
891
|
+
{ action },
|
|
892
|
+
action === "run" ? "Flywheel queued" : "Scaffolding queued",
|
|
893
|
+
reload,
|
|
894
|
+
);
|
|
895
|
+
btn.disabled = false;
|
|
896
|
+
});
|
|
897
|
+
card.appendChild(btn);
|
|
898
|
+
}
|
|
899
|
+
return card;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function experimentsCard(ctx, res, reload) {
|
|
903
|
+
const card = panel("Experiments", res, (node, data) => {
|
|
904
|
+
node.appendChild(el("p", { class: "muted", text: String(data.boundary ?? "") }));
|
|
905
|
+
for (const experiment of data.experiments ?? []) {
|
|
906
|
+
const rows = (experiment.variants ?? []).map((v) =>
|
|
907
|
+
el("tr", null, [
|
|
908
|
+
el("td", { class: "mono", text: String(v.version) }),
|
|
909
|
+
el("td", { class: "num", text: fmtCount(v.n) }),
|
|
910
|
+
el("td", null, [dot(v.n > 0 ? "ok" : "unknown", fmtPct(v.successRate))]),
|
|
911
|
+
el("td", {
|
|
912
|
+
class: "num",
|
|
913
|
+
text: Array.isArray(v.ci95) ? `${fmtPct(v.ci95[0])}–${fmtPct(v.ci95[1])}` : "—",
|
|
914
|
+
}),
|
|
915
|
+
el("td", {
|
|
916
|
+
class: "num",
|
|
917
|
+
text:
|
|
918
|
+
typeof v.successRateDelta === "number"
|
|
919
|
+
? `${v.successRateDelta >= 0 ? "+" : ""}${fmtPct(v.successRateDelta)}`
|
|
920
|
+
: "—",
|
|
921
|
+
}),
|
|
922
|
+
el("td", { class: "muted", text: (v.sources ?? []).join(", ") }),
|
|
923
|
+
]),
|
|
924
|
+
);
|
|
925
|
+
node.appendChild(
|
|
926
|
+
collapsible(
|
|
927
|
+
[
|
|
928
|
+
el("span", { class: "mono", text: String(experiment.name) }),
|
|
929
|
+
el("span", {
|
|
930
|
+
class: "muted",
|
|
931
|
+
text: ` ${fmtCount(experiment.totalObservations)} observations`,
|
|
932
|
+
}),
|
|
933
|
+
],
|
|
934
|
+
[table(["Version", "n", "Success", "Wilson 95%", "Δ vs control", "Sources"], rows)],
|
|
935
|
+
true,
|
|
936
|
+
),
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
const form = el("form", { class: "row-editor" }, [
|
|
941
|
+
field("experiment", input("name", { placeholder: "name", required: true })),
|
|
942
|
+
field("version", input("version", { placeholder: "1.2.3" })),
|
|
943
|
+
field(
|
|
944
|
+
"outcome",
|
|
945
|
+
el(
|
|
946
|
+
"select",
|
|
947
|
+
{ class: "input", name: "outcome" },
|
|
948
|
+
["pass", "fail"].map((o) => el("option", { value: o, text: o })),
|
|
949
|
+
),
|
|
950
|
+
),
|
|
951
|
+
]);
|
|
952
|
+
const go = el("button", { class: "btn btn-ghost", type: "submit", text: "Record outcome" });
|
|
953
|
+
form.appendChild(el("div", { class: "editor-actions" }, [go]));
|
|
954
|
+
form.addEventListener("submit", async (event) => {
|
|
955
|
+
event.preventDefault();
|
|
956
|
+
go.disabled = true;
|
|
957
|
+
const res = await submit(
|
|
958
|
+
api.experimentRecord,
|
|
959
|
+
{ id: ctx.id },
|
|
960
|
+
{
|
|
961
|
+
action: "record",
|
|
962
|
+
name: readText(form, "name"),
|
|
963
|
+
version: readText(form, "version"),
|
|
964
|
+
outcome: form.elements["outcome"].value,
|
|
965
|
+
},
|
|
966
|
+
"Outcome recorded",
|
|
967
|
+
reload,
|
|
968
|
+
);
|
|
969
|
+
if (res.ok && payload(res).submitted === false) {
|
|
970
|
+
toast(String(payload(res).note ?? "incomplete"), "error");
|
|
971
|
+
}
|
|
972
|
+
go.disabled = false;
|
|
973
|
+
});
|
|
974
|
+
card.appendChild(form);
|
|
975
|
+
return card;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* F-7, rendered honestly. Eval-sample annotations are durable and visible,
|
|
980
|
+
* and mostly NOT training-data eligible: a sample's transcript lives under
|
|
981
|
+
* the run directory, and distill joins ratings to turns by sweeping the
|
|
982
|
+
* sessions root. The panel states the real counts and the upstream fix
|
|
983
|
+
* rather than showing a pipeline that does not run.
|
|
984
|
+
*/
|
|
985
|
+
function annotationsCard(res) {
|
|
986
|
+
return panel("Annotations → distill", res, (node, data) => {
|
|
987
|
+
const join = data.join ?? {};
|
|
988
|
+
node.appendChild(
|
|
989
|
+
el("div", { class: "mini-row" }, [
|
|
990
|
+
stat("annotations", fmtCount(join.total)),
|
|
991
|
+
stat("reach distill", fmtCount(join.resolvable)),
|
|
992
|
+
stat("blocked", fmtCount(join.unresolvable)),
|
|
993
|
+
]),
|
|
994
|
+
);
|
|
995
|
+
node.appendChild(
|
|
996
|
+
el("p", null, [
|
|
997
|
+
dot(
|
|
998
|
+
join.unresolvable > 0 ? "warn" : "ok",
|
|
999
|
+
join.unresolvable > 0 ? "join incomplete" : "join resolves",
|
|
1000
|
+
),
|
|
1001
|
+
]),
|
|
1002
|
+
);
|
|
1003
|
+
node.appendChild(el("p", { class: "muted", text: String(join.reason ?? "") }));
|
|
1004
|
+
if (join.upstreamFix) {
|
|
1005
|
+
node.appendChild(el("p", { class: "gated-why", text: String(join.upstreamFix) }));
|
|
1006
|
+
}
|
|
1007
|
+
node.appendChild(
|
|
1008
|
+
el(
|
|
1009
|
+
"ul",
|
|
1010
|
+
{ class: "check-list" },
|
|
1011
|
+
(join.sinks ?? []).map((s) => el("li", { class: "mono", text: String(s) })),
|
|
1012
|
+
),
|
|
1013
|
+
);
|
|
1014
|
+
node.appendChild(
|
|
1015
|
+
table(
|
|
1016
|
+
["Run", "Sample", "Verdict", "Note", "State"],
|
|
1017
|
+
(data.annotations ?? []).map((a) =>
|
|
1018
|
+
el("tr", null, [
|
|
1019
|
+
el("td", { class: "mono", text: String(a.runId) }),
|
|
1020
|
+
el("td", { class: "mono", text: String(a.sampleId) }),
|
|
1021
|
+
el("td", null, [dot(a.verdict === "pass" ? "ok" : "bad", String(a.verdict ?? "?"))]),
|
|
1022
|
+
el("td", { class: "muted", text: String(a.note ?? "") }),
|
|
1023
|
+
el("td", { class: "muted", text: String(a.join?.state ?? "") }),
|
|
1024
|
+
]),
|
|
1025
|
+
),
|
|
1026
|
+
),
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
}
|