@mutagent/evaluator 0.1.0-alpha.5 → 0.1.0-alpha.6

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.
Files changed (34) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +5 -5
  2. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +16 -16
  3. package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
  4. package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
  5. package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
  6. package/.claude/skills/mutagent-evaluator/references/eval-stage.md +36 -3
  7. package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
  8. package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
  9. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  10. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
  11. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
  12. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
  13. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
  14. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
  15. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +39 -0
  16. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
  17. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
  18. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
  19. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
  20. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
  21. package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
  22. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
  23. package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
  24. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
  25. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
  26. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +9 -91
  27. package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
  28. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +2 -2
  29. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
  30. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
  31. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
  32. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
  33. package/bin/mutagent-cli.mjs +9 -5
  34. package/package.json +1 -1
@@ -15,6 +15,17 @@
15
15
  * - auto-save on every action (localStorage) + a labels export;
16
16
  * - a trace counter + jump-to-id + labeled/unlabeled counts.
17
17
  *
18
+ * EV-3 — the review screen used to dump each whole session as a FLAT sibling
19
+ * list of collapsible steps (10,254 step-blocks / up to 1,686 per card in the
20
+ * real run → unreviewable). This renderer now ALSO ports the eval report's
21
+ * step<->criterion SIDE-BY-SIDE view onto review (the SAME extracted render
22
+ * functions — `report-fragments.ts`), with (a) criterion-scoped fragments (show
23
+ * only the steps a criterion examined, rendered by the REUSED `sideBySide`),
24
+ * (b) a VIRTUALIZED / windowed scroll so a 1,686-step card scrolls instead of
25
+ * walling the DOM, and (c) pre-labelled evidence steps (the steps a criterion's
26
+ * refs already identify). Activated ONLY when `opts.sideBySide` carries a
27
+ * per-trace bundle; without it the legacy flat review is unchanged.
28
+ *
18
29
  * Austerity: holds NO judge prompt, makes NO pass/fail decision (the HUMAN
19
30
  * decides). DETERMINISTIC: `renderReviewUi(traces, opts)` is byte-identical for
20
31
  * the same input — NO clock / random / network in the generator. The browser
@@ -26,6 +37,7 @@
26
37
  * (the subject profile), never hard-coded.
27
38
  */
28
39
  import type {
40
+ CriterionVerdict,
29
41
  DiscoveryAssumption,
30
42
  DiscoveryRef,
31
43
  EvalTrace,
@@ -34,6 +46,11 @@ import type {
34
46
  VerdictBlock,
35
47
  } from "./contracts/eval-types.ts";
36
48
  import { type HumanLabel } from "./contracts/validation.ts";
49
+ import {
50
+ REVIEW_SIDE_BY_SIDE_DEPS_JS,
51
+ SIDE_BY_SIDE_FRAGMENT_JS,
52
+ STEP_ROW_FRAGMENT_JS,
53
+ } from "./report-fragments.ts";
37
54
 
38
55
  /**
39
56
  * GA — the judge's adjudication for ONE trace, surfaced on the review screen so
@@ -61,6 +78,63 @@ export function esc(s: unknown): string {
61
78
  .replace(/'/g, "&#39;");
62
79
  }
63
80
 
81
+ /** JSON for a `<script>` inline injection — `<` escaped so no `</script>` breakout. */
82
+ function jsonInject(v: unknown): string {
83
+ return JSON.stringify(v ?? null).replace(/</g, "\\u003c");
84
+ }
85
+
86
+ // ── EV-3 side-by-side data model (per-trace) ─────────────────────────────────
87
+
88
+ /** One agent step in the side-by-side lane (mirrors the eval report's AgentStep). */
89
+ export interface ReviewStep {
90
+ n: number | string;
91
+ tool?: string;
92
+ status?: string;
93
+ detail?: string;
94
+ /** the observation id the criterion refs bind to (`ref.obs === step.obs`). */
95
+ obs?: string;
96
+ }
97
+
98
+ /** One judge-walk step (optional — most review bundles carry none). */
99
+ export interface ReviewJudgeStep {
100
+ kind: string;
101
+ text?: string;
102
+ ref?: unknown;
103
+ anchor?: number | null;
104
+ }
105
+
106
+ /**
107
+ * EV-3 — the per-trace SIDE-BY-SIDE bundle: the agent steps on the left, the
108
+ * per-criterion verdicts (with refs) that examined them on the right. Shaped to
109
+ * be consumed BOTH by the virtualized windowed renderer AND (criterion-scoped)
110
+ * by the REUSED `sideBySide` render fn from `report-fragments.ts`.
111
+ */
112
+ export interface ReviewSideBySide {
113
+ agentSteps: ReviewStep[];
114
+ /** the per-criterion verdicts for this trace (refs bind them to the steps). */
115
+ criterionVerdicts: CriterionVerdict[];
116
+ judgeSteps?: ReviewJudgeStep[];
117
+ context?: { harness?: string; scenario?: string; exitStates?: string };
118
+ health?: { contextGathered?: boolean; grounded?: number; assumed?: number; stoppedAtSymptom?: boolean };
119
+ verdict?: string;
120
+ route?: string;
121
+ res?: string;
122
+ localize?: string;
123
+ input?: string;
124
+ /** analyzer-identified evidence obs ids — steps to PRE-LABEL as evidence. */
125
+ evidenceObs?: string[];
126
+ // pass-through for `sideBySide`'s optional bands (rendered when present).
127
+ subjectProfile?: unknown;
128
+ understanding?: unknown;
129
+ expectedTrajectory?: unknown;
130
+ }
131
+
132
+ /** Criterion metadata for the method tag (`C[id].m` → CODE/JUDGE/HYBRID). */
133
+ export interface ReviewCriterionMeta {
134
+ n?: string;
135
+ m?: string;
136
+ }
137
+
64
138
  export interface ReviewUiOptions {
65
139
  /** subject display name for the header badge (from the subject profile). */
66
140
  subjectName?: string;
@@ -72,6 +146,14 @@ export interface ReviewUiOptions {
72
146
  * eliminate capture. OPTIONAL — absent ⇒ the legacy label-only review screen.
73
147
  */
74
148
  adjudications?: Record<string, ReviewAdjudication>;
149
+ /**
150
+ * EV-3 — the per-trace side-by-side bundle (agent steps + per-criterion
151
+ * verdicts). When present for a trace, the card renders the criterion-scoped,
152
+ * virtualized step<->criterion view. OPTIONAL — absent ⇒ legacy flat review.
153
+ */
154
+ sideBySide?: Record<string, ReviewSideBySide>;
155
+ /** criterion metadata (id → {n,m}) for the CODE/JUDGE/HYBRID method tag. */
156
+ criteriaMeta?: Record<string, ReviewCriterionMeta>;
75
157
  }
76
158
 
77
159
  // ── per-trace native-format render (server-side, deterministic) ──────────────
@@ -140,23 +222,42 @@ function renderAdjudicationPanel(adj: ReviewAdjudication): string {
140
222
  );
141
223
  }
142
224
 
225
+ /**
226
+ * EV-3 — the side-by-side MOUNT for one trace. The card carries an empty mount
227
+ * div (data-trace-id) that the client controller fills with the criterion-scoped,
228
+ * virtualized step<->criterion view. Server emits only the shell (deterministic);
229
+ * the heavy step DOM is windowed at runtime so a 1,686-step card never walls.
230
+ */
231
+ function renderSbsMount(traceId: string): string {
232
+ return (
233
+ `<div class="role role-sbs"><h4>Step ‖ Criterion — side-by-side (virtualized)</h4>` +
234
+ `<div class="sbs-mount" data-trace-id="${esc(traceId)}">` +
235
+ `<div class="sbs-boot">loading side-by-side…</div>` +
236
+ `</div></div>`
237
+ );
238
+ }
239
+
143
240
  /**
144
241
  * Render one trace as a card (native format: escaped text, collapsible tool
145
242
  * observations, color-coded role borders, full trace accessible). All cards are
146
243
  * emitted; the client shows exactly one at a time. When a GA adjudication is
147
244
  * supplied for the trace, the judge panel + verify/eliminate capture is surfaced.
245
+ * When a side-by-side bundle is supplied, the criterion-scoped virtualized
246
+ * step<->criterion view is mounted.
148
247
  */
149
248
  function renderTraceCard(
150
249
  trace: EvalTrace,
151
250
  index: number,
152
251
  subjectName: string,
153
252
  adj?: ReviewAdjudication,
253
+ hasSbs = false,
154
254
  ): string {
155
255
  const prompt = esc(trace.input?.prompt ?? "");
156
256
  const response = esc(trace.output?.response ?? "");
157
257
  const obs = trace.observations.map(renderObservation).join("");
158
258
  const toolCount = trace.observations.filter((o) => o.type === "TOOL").length;
159
259
  const panel = adj !== undefined ? renderAdjudicationPanel(adj) : "";
260
+ const sbs = hasSbs ? renderSbsMount(trace.id) : "";
160
261
  return (
161
262
  `<section class="trace-card" data-index="${index}" data-trace-id="${esc(trace.id)}" hidden>` +
162
263
  `<div class="card-head">` +
@@ -165,6 +266,7 @@ function renderTraceCard(
165
266
  `<span class="badge meta">${toolCount} tool call(s)</span>` +
166
267
  `</div>` +
167
268
  `<div class="role role-user"><h4>Input</h4><pre>${prompt}</pre></div>` +
269
+ sbs +
168
270
  `<div class="role role-tool"><h4>Trace (${trace.observations.length} step(s))</h4>${obs || "<em>no observations</em>"}</div>` +
169
271
  `<div class="role role-assistant"><h4>Output</h4><pre>${response}</pre></div>` +
170
272
  panel +
@@ -172,6 +274,58 @@ function renderTraceCard(
172
274
  );
173
275
  }
174
276
 
277
+ // ── EV-3 derive: build a per-trace side-by-side bundle from a trace + verdicts ─
278
+
279
+ /**
280
+ * Derive a `ReviewSideBySide` from a trace + its per-criterion verdicts. PURE /
281
+ * deterministic. Synthesizes agent steps from the trace observations (n · tool ·
282
+ * detail · obs) and pre-labels the evidence obs ids the verdict refs cite. When
283
+ * `agentSteps` are supplied directly (already-projected judge steps), they win.
284
+ */
285
+ export function deriveReviewSideBySide(
286
+ trace: EvalTrace,
287
+ verdicts: CriterionVerdict[],
288
+ extra: Partial<ReviewSideBySide> = {},
289
+ ): ReviewSideBySide {
290
+ const agentSteps: ReviewStep[] =
291
+ extra.agentSteps ??
292
+ trace.observations.map((o, i) => {
293
+ const obsId = (o as { id?: string }).id;
294
+ let detail = "";
295
+ try {
296
+ detail = JSON.stringify(o.output ?? o.input ?? {});
297
+ } catch {
298
+ detail = String(o.output ?? "");
299
+ }
300
+ return {
301
+ n: i + 1,
302
+ tool: o.name ?? o.type,
303
+ status: o.type === "TOOL" ? "" : String(o.type),
304
+ detail: detail.length > 240 ? detail.slice(0, 240) + "…" : detail,
305
+ obs: obsId,
306
+ };
307
+ });
308
+ // evidence obs = every obs id a verdict ref cites (analyzer-identified).
309
+ const evidence = new Set<string>(extra.evidenceObs ?? []);
310
+ for (const v of verdicts) for (const r of v.refs ?? []) if (r.obs) evidence.add(String(r.obs));
311
+ return {
312
+ agentSteps,
313
+ criterionVerdicts: verdicts,
314
+ input: extra.input ?? trace.input?.prompt,
315
+ verdict: extra.verdict,
316
+ route: extra.route,
317
+ res: extra.res,
318
+ localize: extra.localize,
319
+ context: extra.context,
320
+ health: extra.health,
321
+ judgeSteps: extra.judgeSteps,
322
+ evidenceObs: [...evidence],
323
+ subjectProfile: extra.subjectProfile,
324
+ understanding: extra.understanding,
325
+ expectedTrajectory: extra.expectedTrajectory,
326
+ };
327
+ }
328
+
175
329
  // ── the deterministic HTML document ──────────────────────────────────────────
176
330
 
177
331
  // Brand: the unified MutagenT design-system — SHARP corners (radius 0), SUBTLE
@@ -179,15 +333,15 @@ function renderTraceCard(
179
333
  // mirror @mutagent/templates/design-system/tokens.css (self-contained inline copy;
180
334
  // the review UI is a standalone HTML doc with no bundled brand-asset read).
181
335
  const STYLE = `
182
- :root{--bg:#0a0a12;--surf:#14141d;--surf-2:#1a1a25;--fg:#d6dbe6;--fg-strong:#eef1f6;--mut:#8a8698;--bd:rgba(255,255,255,.09);--bstr:rgba(255,255,255,.16);--pass:#43c39a;--fail:#e06666;--defer:#e8a64d;--user:#45b8cc;--tool:#7E47D7;--asst:#43c39a;--fs:'Space Grotesk',system-ui,-apple-system,sans-serif;--fm:'IBM Plex Mono',ui-monospace,monospace}
336
+ :root{--bg:#0a0a12;--surf:#14141d;--surf-2:#1a1a25;--fg:#d6dbe6;--fg-strong:#eef1f6;--mut:#8a8698;--dim:#6b6878;--bd:rgba(255,255,255,.09);--bstr:rgba(255,255,255,.16);--pass:#43c39a;--fail:#e06666;--defer:#e8a64d;--warn:#e8a64d;--cyan:#45b8cc;--user:#45b8cc;--tool:#7E47D7;--asst:#43c39a;--primary-soft:#a986e8;--fs:'Space Grotesk',system-ui,-apple-system,sans-serif;--fm:'IBM Plex Mono',ui-monospace,monospace;--fs-sm:12px;--fs-xs:11px;--fs-2xs:10px}
183
337
  *{box-sizing:border-box;border-radius:0}body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 var(--fs)}
184
338
  header{position:sticky;top:0;background:var(--surf);border-bottom:1px solid var(--bstr);padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;z-index:10}
185
339
  header strong{color:var(--fg-strong)}
186
- main{padding:16px;max-width:980px;margin:0 auto}
340
+ main{padding:16px;max-width:1180px;margin:0 auto}
187
341
  .badge{display:inline-block;padding:2px 8px;background:var(--surf-2);border:1px solid var(--bd);font-size:12px;font-family:var(--fm)}
188
342
  .badge.subject{background:var(--surf-2);border-color:var(--bstr)}.badge.tid{font-family:var(--fm)}
189
343
  .role{border-left:3px solid var(--bd);padding:4px 12px;margin:10px 0}
190
- .role-user{border-color:var(--user)}.role-tool{border-color:var(--tool)}.role-assistant{border-color:var(--asst)}
344
+ .role-user{border-color:var(--user)}.role-tool{border-color:var(--tool)}.role-assistant{border-color:var(--asst)}.role-sbs{border-color:var(--primary-soft)}
191
345
  .role h4{margin:4px 0;color:var(--mut);font-weight:600;text-transform:uppercase;font-size:11px;letter-spacing:.04em;font-family:var(--fm)}
192
346
  pre{white-space:pre-wrap;word-break:break-word;background:var(--surf);border:1px solid var(--bd);padding:10px;margin:4px 0;font-family:var(--fm)}
193
347
  details.obs{margin:4px 0;border:1px solid var(--bd);padding:4px 8px;background:var(--surf-2)}
@@ -207,6 +361,57 @@ input[type=text]{background:var(--surf);color:var(--fg);border:1px solid var(--b
207
361
  .ga-assumptions{margin:4px 0;padding-left:18px}
208
362
  .badge.verdict-pass{border-color:var(--pass);color:var(--pass)}.badge.verdict-fail{border-color:var(--fail);color:var(--fail)}.badge.verdict-indeterminate{border-color:var(--defer);color:var(--defer)}
209
363
  button.calib{padding:2px 8px;font-size:11px}button.calib.verify.active{background:var(--pass);color:#0a0a12}button.calib.eliminate.active{background:var(--fail);color:#0a0a12}
364
+ /* EV-3 — side-by-side (criterion-scoped, virtualized, evidence-highlighted) */
365
+ .sbs-mount{border:1px solid var(--bd);background:var(--surf-2);padding:8px}
366
+ .sbs-boot{color:var(--mut);font-family:var(--fm);font-size:12px;padding:8px}
367
+ .sbs-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
368
+ .sbs-chip{padding:3px 9px;font-size:12px;font-family:var(--fm);background:var(--surf);border:1px solid var(--bd);color:var(--fg)}
369
+ .sbs-chip.on{border-color:var(--primary-soft);color:var(--fg-strong);background:var(--surf-2)}
370
+ .lanehdr{display:grid;grid-template-columns:1fr auto 1fr;gap:8px;padding:5px 6px;border-bottom:1px solid var(--bstr);color:var(--mut);font-family:var(--fm);font-size:11px;text-transform:uppercase;letter-spacing:.03em}
371
+ .lanehdr .x{text-align:center}.lanehdr .j{text-align:left}
372
+ .sbs-scroll{height:440px;overflow:auto;position:relative;border:1px solid var(--bd);background:var(--surf)}
373
+ .sbs-sizer{position:relative;width:100%}
374
+ .sbs-window{position:absolute;left:0;right:0;top:0;will-change:transform}
375
+ .srow{display:grid;grid-template-columns:1fr auto 1fr;gap:8px;border-bottom:1px solid var(--bd);overflow:hidden;padding:2px 0}
376
+ .srow.evidence{background:rgba(126,71,215,.10);border-left:3px solid var(--primary-soft)}
377
+ .step-l,.step-r{overflow:auto;padding:4px 8px}
378
+ .step-l .evb,.step-r .evb{font-family:var(--fm);font-size:11px}
379
+ .step-l .top{display:flex;gap:6px;align-items:center}
380
+ .step-l .tool{color:var(--fg-strong)}.step-l .st{color:var(--warn)}
381
+ .step-l .det{color:var(--mut);margin-top:2px;word-break:break-word}
382
+ .node{display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:34px}
383
+ .node .n{font-family:var(--fm);font-size:11px;color:var(--mut);border:1px solid var(--bd);padding:0 5px}
384
+ .node .ln{flex:1;width:1px;background:var(--bd);min-height:6px}
385
+ .jcov{border:1px solid var(--bd);border-left:3px solid var(--bd);padding:4px 7px;margin:3px 0;background:var(--surf-2)}
386
+ .jcov.pass{border-left-color:var(--pass)}.jcov.fail{border-left-color:var(--fail)}.jcov.uncertain,.jcov.indeterminate{border-left-color:var(--warn)}
387
+ .jcov-h{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:2px}
388
+ .cvb{font-family:var(--fm);font-size:10px;text-transform:uppercase;letter-spacing:.03em;padding:0 5px;border:1px solid var(--bstr)}
389
+ .cvb.pass{color:var(--pass);border-color:var(--pass)}.cvb.fail{color:var(--fail);border-color:var(--fail)}.cvb.uncertain,.cvb.indeterminate{color:var(--warn);border-color:var(--warn)}.cvb.na{color:var(--dim)}
390
+ .jm{font-family:var(--fm);font-size:9px;padding:0 4px;border:1px solid var(--bd);color:var(--mut)}.jm.code{color:var(--cyan);border-color:var(--cyan)}
391
+ .jcid{font-family:var(--fm);font-size:11px;color:var(--fg-strong)}
392
+ .jcrit{font-size:12px;color:var(--fg);line-height:1.45}
393
+ .cvrefs{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
394
+ .cvref{font-family:var(--fm);font-size:10px;color:var(--cyan);border:1px solid var(--bd);padding:0 4px;word-break:break-all}
395
+ .jstep{font-family:var(--fm);font-size:11px;color:var(--fg);padding:2px 0}.jstep.noexam{color:var(--dim)}
396
+ .jstep .k{color:var(--primary-soft);margin-right:5px}.jstep .ref{color:var(--cyan)}
397
+ .dim{color:var(--dim)}
398
+ .sbs-meta{color:var(--mut);font-family:var(--fm);font-size:11px;margin-top:6px}
399
+ .sbs-full{margin-top:8px}.sbs-hint{color:var(--mut);font-family:var(--fm);font-size:11px;padding:6px}
400
+ .sbs-fulldet{border:1px solid var(--bd);background:var(--surf);padding:6px 8px}
401
+ .sbs-fulldet summary{color:var(--primary-soft);font-family:var(--fm);font-size:12px}
402
+ /* the reused sideBySide fragment renders a drillbox — give its bands legible defaults */
403
+ .drillbox{padding:6px 2px}.drillbox .mono{font-family:var(--fm)}
404
+ .drillbox .chip{font-family:var(--fm);font-size:11px;border:1px solid var(--bd);padding:0 6px;margin:0 4px}
405
+ .drillbox .verd{font-family:var(--fm);font-size:11px;padding:0 6px;border:1px solid var(--bstr)}.drillbox .verd.fail{color:var(--fail);border-color:var(--fail)}.drillbox .verd.pass{color:var(--pass);border-color:var(--pass)}.drillbox .verd.inc{color:var(--warn);border-color:var(--warn)}
406
+ .drillbox .ctx,.drillbox .band{border:1px solid var(--bd);padding:6px 8px;margin:6px 0;background:var(--surf-2)}
407
+ .drillbox .ctx-h,.drillbox .bh{color:var(--mut);font-family:var(--fm);font-size:11px;text-transform:uppercase;letter-spacing:.03em;margin-bottom:4px}
408
+ .drillbox .ctx-g{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}
409
+ .drillbox .ctx-c .l{color:var(--dim);font-family:var(--fm);font-size:10px;text-transform:uppercase}.drillbox .ctx-c .v{color:var(--fg)}
410
+ .drillbox .grid2{display:grid;grid-template-columns:1fr auto 1fr;gap:8px;margin-top:6px}
411
+ .drillbox .health{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px}.drillbox .hc{border:1px solid var(--bd);padding:3px 8px;font-family:var(--fm);font-size:11px}.drillbox .hc .v.good{color:var(--pass)}.drillbox .hc .v.warn{color:var(--warn)}
412
+ .cvblock .cvbody{display:flex;flex-direction:column;gap:5px}.cvrow{border:1px solid var(--bd);border-left:3px solid var(--bd);padding:5px 7px;background:var(--surf)}.cvrow.pass{border-left-color:var(--pass)}.cvrow.fail{border-left-color:var(--fail)}.cvrow.uncertain,.cvrow.indeterminate{border-left-color:var(--warn)}
413
+ .cvh{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:2px}.cvid{font-family:var(--fm);color:var(--fg-strong);font-size:11px}.cvconf{font-family:var(--fm);font-size:10px;color:var(--dim);margin-left:auto}.cvcrit{font-size:12px}.cvcrit.dim{color:var(--dim)}
414
+ .routing{display:flex;gap:6px;align-items:center;margin:4px 0;font-family:var(--fm);font-size:11px}.re-rephrase,.re-ul,.re-rat,.why-note{font-size:12px}.whychain .jstep{font-size:11px}
210
415
  `;
211
416
 
212
417
  /**
@@ -215,11 +420,16 @@ button.calib{padding:2px 8px;font-size:11px}button.calib.verify.active{backgroun
215
420
  * NO trace data (the cards are server-rendered) and NO subject specifics. The
216
421
  * `STORAGE_KEY` is parameterized by a stable subject key so multiple subjects'
217
422
  * labels don't collide in one browser.
423
+ *
424
+ * EV-3 — when side-by-side data (`SBS`) is injected, the controller ALSO mounts
425
+ * the criterion-scoped, virtualized step<->criterion view per card (lazily, on
426
+ * first show), reusing the extracted `sideBySide` fn for the scoped fragment.
218
427
  */
219
- function clientScript(storageKey: string): string {
428
+ function clientScript(storageKey: string, sbsJson: string, critJson: string): string {
220
429
  return `
221
430
  const STORAGE_KEY=${JSON.stringify(storageKey)};
222
431
  const CALIB_KEY=STORAGE_KEY+':calibration';
432
+ const SBS=${sbsJson},C=${critJson},RES={};
223
433
  const cards=[...document.querySelectorAll('.trace-card')];
224
434
  const ids=cards.map(c=>c.dataset.traceId);
225
435
  let cur=0;
@@ -232,6 +442,69 @@ function saveCalib(m){localStorage.setItem(CALIB_KEY,JSON.stringify(m))}
232
442
  let calib=loadCalib();
233
443
  let labels=loadLabels();
234
444
  let history=[];
445
+ // ── EV-3 side-by-side deps + extracted render fns + virtualization mirror ──
446
+ ${REVIEW_SIDE_BY_SIDE_DEPS_JS}
447
+ ${SIDE_BY_SIDE_FRAGMENT_JS}
448
+ ${STEP_ROW_FRAGMENT_JS}
449
+ var SBS_ROW_H=112,SBS_OVERSCAN=6;
450
+ function sbsStepsFor(d,scope){
451
+ var steps=d.agentSteps||[];
452
+ if(scope==='all')return steps;
453
+ var v=(d.criterionVerdicts||[]).filter(function(x){return x.criterionId===scope;});
454
+ return steps.filter(function(a){return stepExaminedByR(a,v);});
455
+ }
456
+ function sbsEvidence(a,d){
457
+ if(d.evidenceObs&&a.obs&&d.evidenceObs.indexOf(String(a.obs))>=0)return true;
458
+ return stepExaminedByR(a,d.criterionVerdicts||[]);
459
+ }
460
+ function sbsChipBar(d){
461
+ var steps=d.agentSteps||[];var ev=steps.filter(function(a){return sbsEvidence(a,d);}).length;
462
+ var chips='<button class="sbs-chip on" data-scope="all">All · '+steps.length+' steps · '+ev+' evidence</button>';
463
+ (d.criterionVerdicts||[]).forEach(function(v){
464
+ var n=sbsStepsFor(d,v.criterionId).length;var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
465
+ chips+='<button class="sbs-chip" data-scope="'+esc(v.criterionId)+'" title="'+esc(v.critique||'')+'"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span> '+esc(v.criterionId)+' · '+n+'</button>';
466
+ });
467
+ return '<div class="sbs-chips">'+chips+'</div>';
468
+ }
469
+ function renderSbsWindow(mount){
470
+ var d=mount._d,scope=mount._scope;var steps=sbsStepsFor(d,scope);
471
+ var scroll=mount.querySelector('.sbs-scroll'),sizer=mount.querySelector('.sbs-sizer'),win=mount.querySelector('.sbs-window');
472
+ var cvAll=scope==='all'?(d.criterionVerdicts||[]):(d.criterionVerdicts||[]).filter(function(x){return x.criterionId===scope;});
473
+ sizer.style.height=(steps.length*SBS_ROW_H)+'px';
474
+ var top=scroll.scrollTop,h=scroll.clientHeight||440;
475
+ var start=Math.max(0,Math.floor(top/SBS_ROW_H)-SBS_OVERSCAN);
476
+ var count=Math.ceil(h/SBS_ROW_H)+SBS_OVERSCAN*2;var end=Math.min(steps.length,start+count);
477
+ var html='';
478
+ for(var i=start;i<end;i++){var a=steps[i];html+='<div class="srow'+(sbsEvidence(a,d)?' evidence':'')+'" data-step="'+esc(a.n)+'" style="height:'+SBS_ROW_H+'px">'+stepRowInnerR(a,cvAll,d.judgeSteps)+'</div>';}
479
+ win.style.transform='translateY('+(start*SBS_ROW_H)+'px)';win.innerHTML=html;
480
+ var meta=mount.querySelector('.sbs-meta');
481
+ if(meta)meta.textContent=steps.length+' step(s) in scope · '+(steps.length?(end-start):0)+' in DOM (rows '+(steps.length?start+1:0)+'–'+end+') · virtualized window';
482
+ }
483
+ function renderSbsFull(mount){
484
+ var d=mount._d,scope=mount._scope,full=mount.querySelector('.sbs-full');if(!full)return;
485
+ if(scope==='all'){full.innerHTML='<div class="sbs-hint">Pick a criterion chip above to open its full side-by-side fragment (reused eval-report renderer).</div>';return;}
486
+ var v=(d.criterionVerdicts||[]).filter(function(x){return x.criterionId===scope;});
487
+ var steps=sbsStepsFor(d,scope);var dScoped={};for(var k in d)dScoped[k]=d[k];
488
+ dScoped.agentSteps=steps;dScoped.criterionVerdicts=v;dScoped.traceId=mount.getAttribute('data-trace-id');
489
+ full.innerHTML='<details class="sbs-fulldet" open><summary>criterion-scoped fragment · '+esc(scope)+' · '+steps.length+' step(s) (reused sideBySide)</summary>'+sideBySide(dScoped)+'</details>';
490
+ }
491
+ function mountSbs(mount){
492
+ if(mount._wired)return;var tid=mount.getAttribute('data-trace-id');var d=SBS[tid];
493
+ if(!d){mount.innerHTML='<div class="sbs-hint">no side-by-side data for this trace.</div>';mount._wired=true;return;}
494
+ mount._d=d;mount._scope='all';mount._wired=true;
495
+ mount.innerHTML=sbsChipBar(d)+
496
+ '<div class="lanehdr"><div class="a">agent step — what it did</div><div class="x">#</div><div class="j">criteria that examined this step</div></div>'+
497
+ '<div class="sbs-scroll"><div class="sbs-sizer"><div class="sbs-window"></div></div></div>'+
498
+ '<div class="sbs-meta"></div><div class="sbs-full"></div>';
499
+ var scroll=mount.querySelector('.sbs-scroll');
500
+ scroll.addEventListener('scroll',function(){renderSbsWindow(mount);});
501
+ mount.querySelectorAll('.sbs-chip').forEach(function(ch){ch.addEventListener('click',function(){
502
+ mount.querySelectorAll('.sbs-chip').forEach(function(x){x.classList.remove('on');});ch.classList.add('on');
503
+ mount._scope=ch.getAttribute('data-scope');scroll.scrollTop=0;renderSbsWindow(mount);renderSbsFull(mount);
504
+ });});
505
+ renderSbsWindow(mount);renderSbsFull(mount);
506
+ }
507
+ function sbsOnShow(i){var card=cards[i];if(!card)return;var m=card.querySelector('.sbs-mount');if(m)try{mountSbs(m);}catch(e){}}
235
508
  function show(i){
236
509
  if(i<0)i=0;if(i>=cards.length)i=cards.length-1;cur=i;
237
510
  cards.forEach((c,k)=>c.hidden=k!==i);
@@ -240,6 +513,7 @@ function show(i){
240
513
  document.getElementById('notes').value=rec.notes||'';
241
514
  document.getElementById('jump').value=id;
242
515
  updateCounter();
516
+ sbsOnShow(i);
243
517
  }
244
518
  function updateCounter(){
245
519
  const done=ids.filter(id=>labels[id]&&labels[id].label).length;
@@ -318,8 +592,10 @@ export function renderReviewUi(traces: EvalTrace[], opts: ReviewUiOptions = {}):
318
592
  // stable storage key so labels namespace per subject (no clock/random).
319
593
  const storageKey = `mutagent-evaluator:review:${subjectName}`;
320
594
  const adj = opts.adjudications ?? {};
595
+ const sbs = opts.sideBySide ?? {};
596
+ const criteriaMeta = opts.criteriaMeta ?? {};
321
597
  const cards = traces
322
- .map((t, i) => renderTraceCard(t, i, subjectName, adj[t.id]))
598
+ .map((t, i) => renderTraceCard(t, i, subjectName, adj[t.id], sbs[t.id] !== undefined))
323
599
  .join("");
324
600
  const empty = traces.length === 0 ? `<p><em>No traces to review.</em></p>` : "";
325
601
  return (
@@ -341,7 +617,7 @@ export function renderReviewUi(traces: EvalTrace[], opts: ReviewUiOptions = {}):
341
617
  `<span class="kbd">← → nav · 1 Pass · 2 Fail · D Defer · U Undo · ⌘S Save · ⌘⏎ Save&amp;Next</span>` +
342
618
  `</span></header>` +
343
619
  `<main>${empty}${cards}<textarea id="notes" placeholder="notes — what went wrong / right"></textarea></main>` +
344
- `<script>${clientScript(storageKey)}</script></body></html>`
620
+ `<script>${clientScript(storageKey, jsonInject(sbs), jsonInject(criteriaMeta))}</script></body></html>`
345
621
  );
346
622
  }
347
623
 
@@ -391,3 +667,41 @@ export function labelStats(traceIds: string[], labels: HumanLabel[]): LabelStats
391
667
  }
392
668
  return { total: traceIds.length, labeled, unlabeled: traceIds.length - labeled, pass, fail, defer };
393
669
  }
670
+
671
+ // ── CLI entrypoint ───────────────────────────────────────────────────────────
672
+ //
673
+ // bun scripts/build-review-ui.ts <input.json> [out.html]
674
+ // Reads { traces, subjectName?, title?, adjudications?, sideBySide?, criteriaMeta? }
675
+ // and writes the review HTML. This is the production caller the `*review`
676
+ // command's parent Bash invokes (references/build-review-interface.md).
677
+ declare const Bun: { argv: string[] } | undefined;
678
+
679
+ interface ReviewCliInput extends ReviewUiOptions {
680
+ traces: EvalTrace[];
681
+ }
682
+
683
+ async function main(): Promise<void> {
684
+ const argv = typeof Bun !== "undefined" ? Bun.argv.slice(2) : process.argv.slice(2);
685
+ const [inputPath, outArg] = argv;
686
+ if (!inputPath) {
687
+ console.error("usage: build-review-ui.ts <input.json> [out.html]");
688
+ process.exit(2);
689
+ return;
690
+ }
691
+ const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
692
+ const { dirname } = await import("node:path");
693
+ const input = JSON.parse(readFileSync(inputPath, "utf8")) as ReviewCliInput;
694
+ const { traces, ...opts } = input;
695
+ const outPath = outArg ?? "review.html";
696
+ mkdirSync(dirname(outPath) || ".", { recursive: true });
697
+ writeFileSync(outPath, renderReviewUi(traces ?? [], opts));
698
+ console.info(`review UI written: ${outPath} (${(traces ?? []).length} trace(s))`);
699
+ process.exit(0);
700
+ }
701
+
702
+ const isMain =
703
+ typeof import.meta !== "undefined" &&
704
+ (import.meta as unknown as { main?: boolean }).main === true;
705
+ if (isMain) {
706
+ void main();
707
+ }
@@ -30,9 +30,11 @@ import { execSync } from "node:child_process";
30
30
  import { readFileSync, writeFileSync } from "node:fs";
31
31
  import { join, resolve } from "node:path";
32
32
  import { parseUnitfJsonl } from "../read-unitf-traces.ts";
33
+ import { validateSiblingManifest } from "../read-manifest.ts";
33
34
  import { prepDeterminerTasks, prepJudgeTasks } from "../prep-tasks.ts";
34
35
  import { selectColdStartSuite, type ColdStartMeta } from "../cold-start-project.ts";
35
36
  import { profileSubject } from "../profile-subject.ts";
37
+ import { deriveSubjectFrame } from "../subject-profile.ts";
36
38
  import { loadProfileVocab } from "../load-profile-vocab.ts";
37
39
  import { buildMatrixPacket, writeMatrixPacket, packetFileName } from "../matrix-judge.ts";
38
40
  import { configPathFor, loadEvaluatorConfig, type SourceBinding } from "../config/load.ts";
@@ -208,7 +210,29 @@ async function main(): Promise<void> {
208
210
  if (startContext !== "") process.stdout.write(`prep: run-start context —\n${startContext}\n`);
209
211
  const pin: PinnedEnvelope = { model, temperature: 0 };
210
212
  const ndjson = loadNdjson(tracesFile, limit !== undefined ? Number.parseInt(limit, 10) : undefined);
211
- const { traces } = parseUnitfJsonl(ndjson);
213
+ const { traces, skipped, emptyPromptTraceIds } = parseUnitfJsonl(ndjson);
214
+
215
+ // EV-6 — read + validate the sibling TraceManifest (count/format/truncation) and
216
+ // WARN on mismatch, the way diagnostics does (read-unitf.ts). The evaluator used
217
+ // to read ONLY the JSONL, so a truncated/partial slice was judged as if complete.
218
+ for (const w of validateSiblingManifest(tracesFile, traces.length)) {
219
+ process.stderr.write(`prep: MANIFEST WARNING — ${w}\n`);
220
+ }
221
+
222
+ // INF-1 — never silently emit empty: surface any record whose prompt-bearing span
223
+ // still projected an empty prompt (the Claude Code 21/21-blank class). Loud on
224
+ // stderr so a projection miss can never pass unnoticed again.
225
+ if (emptyPromptTraceIds.length > 0) {
226
+ process.stderr.write(
227
+ `prep: EMPTY-PROMPT WARNING — ${emptyPromptTraceIds.length}/${traces.length} trace(s) have a ` +
228
+ `prompt-bearing span but projected an EMPTY input.prompt (a trace-read miss; the judge would ` +
229
+ `score them blind). traceIds: ${emptyPromptTraceIds.slice(0, 10).join(", ")}` +
230
+ `${emptyPromptTraceIds.length > 10 ? ", …" : ""}\n`,
231
+ );
232
+ }
233
+ if (skipped > 0) {
234
+ process.stderr.write(`prep: ${skipped} line(s) skipped (malformed / non-UniTF).\n`);
235
+ }
212
236
 
213
237
  // DEFAULT *evaluate PREP — one eval-matrix packet per trajectory (the headline cell).
214
238
  if (stage === "matrix") {
@@ -253,7 +277,13 @@ async function main(): Promise<void> {
253
277
  const coldSize = coldSizeFlag !== undefined ? Number.parseInt(coldSizeFlag, 10) : traces.length;
254
278
  const cold = selectColdStartSuite(traces, { size: coldSize });
255
279
  coldStartMeta = cold.meta;
256
- specs = prepDeterminerTasks(cold.selected, { dir: taskDir, pin, vocab });
280
+ // EV-1 the subject-aware determiner frame, derived from the FULL parsed batch
281
+ // (NOT the cold subsample) via the SHARED `deriveSubjectFrame` so it is byte-
282
+ // identical to what the Stage-B pipeline builds → the cross-stage determiner cache
283
+ // lines up. subjectName mirrors run-pipeline's `subject.name` (traces[0].name).
284
+ const determinerSubjectName = traces[0]?.name ?? "unknown-subject";
285
+ const subjectFrame = deriveSubjectFrame(determinerSubjectName, traces);
286
+ specs = prepDeterminerTasks(cold.selected, { dir: taskDir, pin, vocab, subject: subjectFrame });
257
287
  } else if (stage === "judge") {
258
288
  const criteriaFile = flag(argv, "criteria");
259
289
  const verdictDir = flag(argv, "verdict-dir");
@@ -34,7 +34,7 @@ const MR_RUBRIC_SKELETON = [
34
34
  { id: "MR-2", title: "Data-flow efficiency", reads: "pipeline trace", how: "scan for redundant/recomputed transforms, collapsible stages", family: "data-flow-efficiency", advisory: true },
35
35
  { id: "MR-3", title: "Methodology fitness given findings", reads: "run trajectory + findings", how: "did depth/breadth match the need?", family: "methodology-fitness", advisory: true },
36
36
  { id: "MR-4", title: "Sequence soundness", reads: "transcript trajectory", how: "does later work invalidate earlier work?", family: "sequence-soundness", advisory: true },
37
- { id: "MR-5", title: "Process self-feedback", reads: "all of the above", how: "ranked rearrange/improve proposals", family: "process-self-feedback", advisory: true },
37
+ { id: "MR-5", title: "Process self-feedback", reads: "all of the above", how: "ranked rearrange/optimize proposals", family: "process-self-feedback", advisory: true },
38
38
  { id: "MR-6", title: "Followed != Right", reads: "matrix (Tab-1) vs this (Tab-4)", how: "separate conformance from fitness", family: "conformance-vs-fitness", advisory: true },
39
39
  { id: "MR-7", title: "Signal/failure-mode selection", reads: "tier0 -> awareness -> deep-read chain", how: "was the primary signal grounded in the right evidence tier?", family: "signal-selection", advisory: true },
40
40
  { id: "MR-8", title: "Confidence derivation", reads: "coverageConfidence vs deep-read sample", how: "does the confidence FOLLOW from the LLM-trace evidence?", family: "confidence-derivation", advisory: true },