@mutagent/evaluator 0.1.0-alpha.4 → 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 (38) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +6 -5
  2. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +76 -12
  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/references/operation-inventory.md +1 -0
  9. package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
  10. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  11. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
  12. package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +6 -3
  13. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
  14. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
  15. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +277 -0
  16. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
  17. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +39 -0
  18. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
  19. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
  20. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
  21. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
  22. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
  23. package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
  24. package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
  25. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
  26. package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
  27. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
  28. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
  29. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +9 -91
  30. package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
  31. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +14 -4
  32. package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
  33. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
  34. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
  35. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
  36. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
  37. package/bin/mutagent-cli.mjs +659 -8
  38. package/package.json +2 -2
@@ -83,6 +83,33 @@ export function renderBuildEvalsProgressCard(p: BuildEvalsProgress): string {
83
83
  ].join("\n");
84
84
  }
85
85
 
86
+ // ── EVAL-1 — "what this stage will produce" notice (up-front, at stage start) ─
87
+ export interface StageProducesNotice {
88
+ subject: string;
89
+ /** the command about to run (e.g. "*build-dataset" | "*build-evals" | "*eval"). */
90
+ command: string;
91
+ /** the named deliverables this stage emits (dataset · criteria · which files). */
92
+ produces: string[];
93
+ }
94
+
95
+ /**
96
+ * EVAL-1 (1) — render the up-front notice that names WHAT this stage will produce,
97
+ * so a build never runs silently with the operator unaware a dataset/criteria are
98
+ * coming. Emitted at Step 0, BEFORE the build. PURE. NOT an approval gate — a
99
+ * notification only (operator constraint).
100
+ */
101
+ export function renderStageProducesNotice(n: StageProducesNotice): string {
102
+ const lines = [
103
+ TOP,
104
+ row(`▷ ${n.command} — ${n.subject}`),
105
+ MID,
106
+ row("this stage will produce:"),
107
+ ];
108
+ for (const p of n.produces) lines.push(row(` • ${p}`));
109
+ lines.push(BOT);
110
+ return lines.join("\n");
111
+ }
112
+
86
113
  // ── F22 — dataset entity card (verbose, after *build-dataset) ─────────────────
87
114
  export interface DatasetEntity {
88
115
  subject: string;
@@ -99,6 +126,12 @@ export function renderDatasetEntityCard(e: DatasetEntity): string {
99
126
  MID,
100
127
  row(`categories (${e.categories.length}) · ${e.totalItems} items total`),
101
128
  ];
129
+ // EVAL-1 — a build that produced NOTHING must WARN, never render a blank card that
130
+ // reads as a silent success (the dogfood symptom: a build that reported and a build
131
+ // that stayed silent looked identical).
132
+ if (e.categories.length === 0 || e.totalItems === 0) {
133
+ lines.push(row("⚠ EMPTY — this build produced no dataset items (nothing to evaluate)."));
134
+ }
102
135
  for (const c of e.categories) {
103
136
  lines.push(row(` • ${c.id}: ${c.items} items (edge: ${c.edgeItems})`));
104
137
  }
@@ -129,6 +162,10 @@ export function renderEvalsEntityCard(e: EvalsEntity): string {
129
162
  row(`output sink: ${e.outputSink}`),
130
163
  row(`criteria (${e.criteria.length}):`),
131
164
  ];
165
+ // EVAL-1 — an eval suite with zero criteria is a silent no-op build; WARN loudly.
166
+ if (e.criteria.length === 0) {
167
+ lines.push(row("⚠ EMPTY — this build produced no criteria (the suite judges nothing)."));
168
+ }
132
169
  for (const c of e.criteria) {
133
170
  lines.push(row(` • ${c.id} [${c.severity}·${c.kind}]`));
134
171
  }
@@ -69,6 +69,14 @@ import { assessEmitCompleteness, type EmitCompleteness } from "./emit-completene
69
69
  import type { Scorecard } from "./evaluate.ts";
70
70
  import type { SourceMap } from "./source-map.ts";
71
71
  import { routeFailures, type FailureRef, type HandoverBundle } from "./route-failures.ts";
72
+ // EV-3 — the step<->criterion side-by-side render fns (`cvRefs` + `critiqueBlock`
73
+ // + `sideBySide`, the last carrying its local `refExaminesStep` + `covEntry`) are
74
+ // EXTRACTED VERBATIM into report-fragments.ts and spliced back into the client
75
+ // script IN PLACE below, so this report's emitted HTML is byte-identical to
76
+ // before the extraction. The review UI (`build-review-ui.ts`) imports the SAME
77
+ // constant to render the identical view. Byte-identity is guarded by this file's
78
+ // render tests + a before/after HTML diff.
79
+ import { SIDE_BY_SIDE_FRAGMENT_JS } from "./report-fragments.ts";
72
80
 
73
81
  const HERE = dirname(fileURLToPath(import.meta.url));
74
82
  const BRAND_DIR = join(HERE, "..", "assets", "brand");
@@ -3319,97 +3327,7 @@ function render(){var qi=document.getElementById('q');if(!qi)return;var q=(qi.va
3319
3327
  // critique-before-verdict + grounding refs, read off the verdict file's verdicts[].
3320
3328
  // Shared by BOTH the side-by-side drill (walk traces) and the per-trajectory
3321
3329
  // scorecard drill (no-walk traces) so NO judged trace renders an empty panel.
3322
- function cvRefs(refs){if(!refs||!refs.length)return '';
3323
- return '<div class="cvrefs">'+refs.map(function(rf){var s=(typeof rf==='string')?rf:[rf.obs,rf.path,rf.value].filter(Boolean).join(' · ');return s?'<span class="cvref">'+esc(s)+'</span>':'';}).join('')+'</div>';}
3324
- function critiqueBlock(cvs){if(!cvs||!cvs.length)return '';
3325
- var order={fail:0,uncertain:1,indeterminate:1,pass:2,na:3};
3326
- var rows=cvs.slice().sort(function(a,b){return (order[a.result]==null?9:order[a.result])-(order[b.result]==null?9:order[b.result]);}).map(function(v){
3327
- var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
3328
- var conf=(v.confidence!=null)?'<span class="cvconf">conf '+esc(v.confidence)+(v.confidenceBand?' · '+esc(v.confidenceBand):'')+'</span>':'';
3329
- var crit=v.critique?'<div class="cvcrit">'+esc(v.critique)+'</div>':'<div class="cvcrit dim">— no critique recorded for this criterion</div>';
3330
- return '<div class="cvrow '+esc(res)+'"><div class="cvh"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="cvid">'+esc(v.criterionId||'')+'</span>'+conf+'</div>'+crit+cvRefs(v.refs)+'</div>';}).join('');
3331
- return '<div class="ctx cvblock"><div class="ctx-h">◇ how the judge reasoned — per-criterion verdict · critique-before-verdict · grounding refs ('+cvs.length+')</div><div class="cvbody">'+rows+'</div></div>';}
3332
- function sideBySide(d){var ctx=d.context||{};
3333
- // Gap A — the §2 "input + scenario" cell renders the RAW triggering INPUT (the
3334
- // thing that fired the agent) ABOVE the judge's scenario LABEL. Long inputs
3335
- // collapse into a <details> (lean, no JS) so the cell stays compact; short ones
3336
- // render inline (clamped). ABSENT input ⇒ the cell shows the scenario alone (or
3337
- // "—"). Font stays at the --fs-2xs (11px) floor + brand mono.
3338
- var inputCell=function(raw,scen){
3339
- var sc=scen?'<div class="iscn">scenario · '+esc(scen)+'</div>':'';
3340
- if(!raw)return (scen?'<div class="ival">'+esc(scen)+'</div>':'—');
3341
- var long=String(raw).length>180;
3342
- var body=long
3343
- ? '<details class="iexp"><summary>raw input · '+String(raw).length+' chars</summary><pre class="iraw">'+esc(raw)+'</pre></details>'
3344
- : '<pre class="iraw clamp">'+esc(raw)+'</pre>';
3345
- return body+sc;};
3346
- var refStr=function(rf){if(!rf)return '';if(typeof rf==='string')return rf;return [rf.obs,rf.path,rf.value].filter(Boolean).join(':');};
3347
- // UI-4 — surface the judge's REASONING (why the verdict + why the exit-states were
3348
- // concluded) from the EXISTING judge walk: an ordered why-chain band + the decide/bind
3349
- // text inlined where it explains a conclusion. No emit-contract change — read-only over
3350
- // the judge_steps already on the verdict file.
3351
- var KORD={gather:0,context:1,examine:2,detect:3,bind:4,ground:5,critique:6,decide:7,verify:8};
3352
- var allJs=(d.judgeSteps||[]).slice();
3353
- var stepText=function(kind){return allJs.filter(function(s){return s.kind===kind;}).map(function(s){return s.text||'';}).filter(Boolean).join(' · ');};
3354
- var decideWhy=stepText('decide')||stepText('critique');
3355
- var stateWhy=[stepText('bind'),stepText('gather')].filter(Boolean).join(' · ');
3356
- var chainSteps=allJs.slice().sort(function(a,b){var x=KORD[a.kind];var y=KORD[b.kind];return (x==null?9:x)-(y==null?9:y);});
3357
- var whyChain=chainSteps.length?('<div class="ctx whychain"><div class="ctx-h">◇ judge reasoning — full why-chain (gather → bind → ground → decide)</div><div class="wc-body">'+chainSteps.map(function(s){var rs=refStr(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+(rs?' <span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('')+'</div></div>'):'';
3358
- var verdictWhy=decideWhy?('<div class="band whyverdict"><div class="bh" style="color:var(--primary-soft)">◇ why this verdict</div><div class="wv-t">'+esc(decideWhy)+'</div></div>'):'';
3359
- // -- §2 judge lane = per-step EVAL COVERAGE --
3360
- // The judge lane (.step-r) used to filter judgeSteps by anchor === a.n -- but
3361
- // judges never emit an anchor, so EVERY step rendered a bare dash. Instead we
3362
- // map each agent step -> the per-criterion verdicts (d.criterionVerdicts) whose
3363
- // grounding refs EXAMINED that step: a precise ref.obs === step.obs match plus
3364
- // the tool-name fallback (ref.path === 'name' && ref.value === step.tool).
3365
- // Each examining criterion renders one compact entry -- result + CODE/JUDGE tag
3366
- // + criterionId + the judge reasoning (critique). A step no criterion references
3367
- // says 'not examined by any eval' (honest), never a bare dash. Any judge step
3368
- // that DOES carry a real anchor is still honored (future-proof).
3369
- var cvAll=(d.criterionVerdicts||[]);
3370
- var refExaminesStep=function(rf,a){
3371
- if(!rf||typeof rf==='string')return false;
3372
- if(a.obs&&rf.obs&&String(rf.obs)===String(a.obs))return true;
3373
- if(rf.path==='name'&&rf.value!=null&&a.tool&&String(rf.value)===String(a.tool))return true;
3374
- return false;};
3375
- var covEntry=function(v,a){
3376
- var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
3377
- // the router carries TWO vocabularies: matrix-derived ('deterministic') and
3378
- // mined ('code-based'); 'hybrid' is shared. A '[code-eval ...]'-prefixed critique
3379
- // is the deterministic-logic fallback when the method is unset.
3380
- var method=((typeof C!=='undefined'&&C[v.criterionId])||{}).m||'';
3381
- var isCode=method==='deterministic'||method==='code-based'||method==='hybrid'||String(v.critique||'').trim().indexOf('[code-eval')===0;
3382
- var tag=method==='hybrid'?'HYBRID':(isCode?'CODE':'JUDGE');
3383
- var matched=(v.refs||[]).filter(function(rf){return typeof rf!=='string'&&refExaminesStep(rf,a);});
3384
- var crit=v.critique?esc(v.critique):'<span class="dim">— no critique recorded</span>';
3385
- return '<div class="jcov '+esc(res)+'"><div class="jcov-h"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="jm '+(isCode?'code':'judge')+'" title="'+(isCode?'deterministically checked by a code-eval (not LLM-judged)':'reasoned by the LLM judge')+'">'+esc(tag)+'</span><span class="jcid">'+esc(v.criterionId||'')+'</span></div><div class="jcrit">'+crit+'</div>'+cvRefs(matched)+'</div>';};
3386
- var rowsHtml='';(d.agentSteps||[]).forEach(function(a){
3387
- // future-proof: a judge step that carries a REAL anchor still renders.
3388
- var js=(d.judgeSteps||[]).filter(function(s){return s.anchor!=null&&String(s.anchor)===String(a.n)&&s.kind!=='context';});
3389
- var anchoredHtml=js.map(function(s){var rs=refStr(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+' '+(rs?'<span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('');
3390
- var examiners=cvAll.filter(function(v){return (v.refs||[]).some(function(rf){return refExaminesStep(rf,a);});});
3391
- var covHtml=examiners.map(function(v){return covEntry(v,a);}).join('');
3392
- var jhtml=anchoredHtml+covHtml;
3393
- if(!jhtml)jhtml='<div class="jstep noexam"><span class="t">— not examined by any eval</span></div>';
3394
- rowsHtml+='<div class="step-l"><div class="evb"><div class="top"><span class="tool">'+esc(a.tool||'')+'</span><span class="st '+esc(a.status||'')+'">'+esc(a.status||'')+'</span></div><div class="det">'+esc(a.detail||'')+'</div></div></div><div class="node '+esc(a.status||'')+'"><div class="ln"></div><div class="n">'+esc(a.n)+'</div><div class="ln"></div></div><div class="step-r"><div class="evb r">'+jhtml+'</div></div>';});
3395
- var h=d.health||{};
3396
- var sp=d.subjectProfile;
3397
- var spHtml=sp?'<div class="ctx"><div class="ctx-h">◇ judge · subject profile (M1) · '+esc(sp.provenance||'')+'</div><div class="ctx-g"><div class="ctx-c"><div class="l">identity</div><div class="v">'+esc(sp.identity||'—')+'</div></div><div class="ctx-c"><div class="l">purpose</div><div class="v">'+esc(sp.purpose||'—')+'</div></div><div class="ctx-c"><div class="l">scope · harness</div><div class="v">'+esc((sp.scope||'—'))+' · harness: '+esc(sp.harness||'—')+'</div></div></div></div>':'';
3398
- var u=d.understanding;
3399
- var uHtml=u?'<div class="band"><div class="bh" style="color:var(--cyan)">◇ node-0 GATHER — understanding (M2)</div><div class="re-rephrase" style="margin-top:4px">“'+esc(u.rephrase||'')+'”</div></div>':'';
3400
- var et=d.expectedTrajectory||[];
3401
- var etHtml=et.length?'<div class="band"><div class="bh" style="color:var(--primary-soft)">◇ node-0.5 EXPECTED-TRAJECTORY (M3) — how it SHOULD have acted</div><ol class="re-ul" style="margin-top:4px">'+et.map(function(s,i){return '<li><b>'+esc(s.step||i+1)+'.</b> '+esc(s.expected||'')+(s.rationale?' <span class=re-rat>— '+esc(s.rationale)+'</span>':'')+'</li>';}).join('')+'</ol></div>':'';
3402
- return '<div class="drillbox"><div style="display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:6px"><b class="mono">'+esc(d.traceId)+'</b><span class="chip">'+esc(d.route||'all')+'</span><span class="verd '+(d.verdict=='FAIL'?'fail':d.verdict=='PASS'?'pass':'inc')+'">'+esc(d.verdict)+'</span></div>'+
3403
- resRouting(d.res||'judge-walk')+
3404
- spHtml+
3405
- verdictWhy+
3406
- critiqueBlock(d.criterionVerdicts)+
3407
- '<div class="ctx"><div class="ctx-h">◇ judge · gather context</div><div class="ctx-g"><div class="ctx-c"><div class="l">harness</div><div class="v">'+esc(ctx.harness||'—')+'</div></div><div class="ctx-c"><div class="l">input + scenario</div><div class="v">'+inputCell(d.input,ctx.scenario)+'</div></div><div class="ctx-c"><div class="l">exit states</div><div class="v">'+esc(ctx.exitStates||'—')+(stateWhy?'<div class="why-note"><span class="ref">why concluded:</span> '+esc(stateWhy)+'</div>':'')+'</div></div></div></div>'+
3408
- whyChain+
3409
- uHtml+etHtml+
3410
- '<div class="lanehdr"><div class="a">target agent — what it did</div><div class="x">step</div><div class="j">judge — eval coverage (which criteria examined this step)</div></div>'+
3411
- '<div class="grid2">'+rowsHtml+'<div class="band loc"><div class="bh">↯ localize (root, not symptom)</div><div style="font-size:var(--fs-sm);margin-top:4px">'+esc(d.localize||'—')+'</div></div></div>'+
3412
- '<div class="health"><div class="hc"><div class="l">context</div><div class="v good">'+(h.contextGathered?'✓':'—')+'</div></div><div class="hc"><div class="l">grounded</div><div class="v good">'+esc(h.grounded||0)+'</div></div><div class="hc"><div class="l">assumed</div><div class="v '+((h.assumed||0)>0?'warn':'good')+'">'+esc(h.assumed||0)+'</div></div><div class="hc"><div class="l">root vs symptom</div><div class="v good">'+(h.stoppedAtSymptom?'symptom':'✓ root')+'</div></div></div></div>';}
3330
+ ${SIDE_BY_SIDE_FRAGMENT_JS}
3413
3331
  function scorecard(r){var order={fail:0,uncertain:1,indeterminate:1,pass:2,na:3};
3414
3332
  var cells=Object.keys(r.c).sort(function(a,b){return (order[r.c[a]]||9)-(order[r.c[b]]||9);}).map(function(k){var v=r.c[k];var disp=v=='uncertain'?'indeterminate':v;
3415
3333
  return '<div class="scc '+esc(v)+'"><span class="nm" title="'+esc((C[k]||{}).n||k)+'">'+esc(k)+'</span><span style="margin-left:auto;color:var(--muted)">'+esc(disp)+'</span></div>';}).join('');
@@ -0,0 +1,173 @@
1
+ /**
2
+ * scripts/report-fragments.ts — EV-3 shared client-JS render fragments.
3
+ * ---------------------------------------------------------------------------
4
+ * The step<->criterion side-by-side render functions, extracted VERBATIM from
5
+ * `render-eval-report.ts` so BOTH the eval report AND the `*review` UI
6
+ * (`build-review-ui.ts`) emit the identical browser-side renderer.
7
+ *
8
+ * `SIDE_BY_SIDE_FRAGMENT_JS` is the BYTE-IDENTICAL block (`cvRefs` +
9
+ * `critiqueBlock` + `sideBySide` — the latter carrying its local
10
+ * `refExaminesStep` + `covEntry`). render-eval-report splices it into its
11
+ * client script IN PLACE, so the report's emitted HTML is unchanged (guarded by
12
+ * the render-eval-report byte-identity tests + a before/after HTML diff).
13
+ *
14
+ * The fragment is provider-of-record for the render logic; its runtime deps
15
+ * (`esc`, `resRouting`, `resBadge`, and the globals `C` / `RES`) are
16
+ * supplied by whichever host script embeds it (render-eval-report defines them
17
+ * above the splice; the review UI defines compatible copies — see
18
+ * `REVIEW_SIDE_BY_SIDE_DEPS_JS`).
19
+ *
20
+ * `STEP_ROW_FRAGMENT_JS` is the review-UI VIRTUALIZATION mirror: standalone
21
+ * `refExaminesStepR` / `covEntryR` / `stepRowR` that emit ONE step triplet
22
+ * with markup IDENTICAL to `sideBySide`'s per-step output — so the review UI
23
+ * can window a 1,686-step card without materializing the whole DOM. A drift
24
+ * guard (`tests/report-fragments.test.ts`) evaluates both paths and asserts the
25
+ * per-step HTML matches, so the mirror can never silently diverge from the
26
+ * frozen fragment.
27
+ */
28
+
29
+ /**
30
+ * BYTE-IDENTICAL extraction of the eval report's `cvRefs` + `critiqueBlock` +
31
+ * `sideBySide` client functions (`sideBySide` contains its local
32
+ * `refExaminesStep` + `covEntry`). Spliced verbatim by render-eval-report and
33
+ * reused by the review UI. DO NOT EDIT — regenerate from render-eval-report if
34
+ * that block ever changes, and re-verify byte-identity.
35
+ */
36
+ export const SIDE_BY_SIDE_FRAGMENT_JS = `function cvRefs(refs){if(!refs||!refs.length)return '';
37
+ return '<div class="cvrefs">'+refs.map(function(rf){var s=(typeof rf==='string')?rf:[rf.obs,rf.path,rf.value].filter(Boolean).join(' · ');return s?'<span class="cvref">'+esc(s)+'</span>':'';}).join('')+'</div>';}
38
+ function critiqueBlock(cvs){if(!cvs||!cvs.length)return '';
39
+ var order={fail:0,uncertain:1,indeterminate:1,pass:2,na:3};
40
+ var rows=cvs.slice().sort(function(a,b){return (order[a.result]==null?9:order[a.result])-(order[b.result]==null?9:order[b.result]);}).map(function(v){
41
+ var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
42
+ var conf=(v.confidence!=null)?'<span class="cvconf">conf '+esc(v.confidence)+(v.confidenceBand?' · '+esc(v.confidenceBand):'')+'</span>':'';
43
+ var crit=v.critique?'<div class="cvcrit">'+esc(v.critique)+'</div>':'<div class="cvcrit dim">— no critique recorded for this criterion</div>';
44
+ return '<div class="cvrow '+esc(res)+'"><div class="cvh"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="cvid">'+esc(v.criterionId||'')+'</span>'+conf+'</div>'+crit+cvRefs(v.refs)+'</div>';}).join('');
45
+ return '<div class="ctx cvblock"><div class="ctx-h">◇ how the judge reasoned — per-criterion verdict · critique-before-verdict · grounding refs ('+cvs.length+')</div><div class="cvbody">'+rows+'</div></div>';}
46
+ function sideBySide(d){var ctx=d.context||{};
47
+ // Gap A — the §2 "input + scenario" cell renders the RAW triggering INPUT (the
48
+ // thing that fired the agent) ABOVE the judge's scenario LABEL. Long inputs
49
+ // collapse into a <details> (lean, no JS) so the cell stays compact; short ones
50
+ // render inline (clamped). ABSENT input ⇒ the cell shows the scenario alone (or
51
+ // "—"). Font stays at the --fs-2xs (11px) floor + brand mono.
52
+ var inputCell=function(raw,scen){
53
+ var sc=scen?'<div class="iscn">scenario · '+esc(scen)+'</div>':'';
54
+ if(!raw)return (scen?'<div class="ival">'+esc(scen)+'</div>':'—');
55
+ var long=String(raw).length>180;
56
+ var body=long
57
+ ? '<details class="iexp"><summary>raw input · '+String(raw).length+' chars</summary><pre class="iraw">'+esc(raw)+'</pre></details>'
58
+ : '<pre class="iraw clamp">'+esc(raw)+'</pre>';
59
+ return body+sc;};
60
+ var refStr=function(rf){if(!rf)return '';if(typeof rf==='string')return rf;return [rf.obs,rf.path,rf.value].filter(Boolean).join(':');};
61
+ // UI-4 — surface the judge's REASONING (why the verdict + why the exit-states were
62
+ // concluded) from the EXISTING judge walk: an ordered why-chain band + the decide/bind
63
+ // text inlined where it explains a conclusion. No emit-contract change — read-only over
64
+ // the judge_steps already on the verdict file.
65
+ var KORD={gather:0,context:1,examine:2,detect:3,bind:4,ground:5,critique:6,decide:7,verify:8};
66
+ var allJs=(d.judgeSteps||[]).slice();
67
+ var stepText=function(kind){return allJs.filter(function(s){return s.kind===kind;}).map(function(s){return s.text||'';}).filter(Boolean).join(' · ');};
68
+ var decideWhy=stepText('decide')||stepText('critique');
69
+ var stateWhy=[stepText('bind'),stepText('gather')].filter(Boolean).join(' · ');
70
+ var chainSteps=allJs.slice().sort(function(a,b){var x=KORD[a.kind];var y=KORD[b.kind];return (x==null?9:x)-(y==null?9:y);});
71
+ var whyChain=chainSteps.length?('<div class="ctx whychain"><div class="ctx-h">◇ judge reasoning — full why-chain (gather → bind → ground → decide)</div><div class="wc-body">'+chainSteps.map(function(s){var rs=refStr(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+(rs?' <span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('')+'</div></div>'):'';
72
+ var verdictWhy=decideWhy?('<div class="band whyverdict"><div class="bh" style="color:var(--primary-soft)">◇ why this verdict</div><div class="wv-t">'+esc(decideWhy)+'</div></div>'):'';
73
+ // -- §2 judge lane = per-step EVAL COVERAGE --
74
+ // The judge lane (.step-r) used to filter judgeSteps by anchor === a.n -- but
75
+ // judges never emit an anchor, so EVERY step rendered a bare dash. Instead we
76
+ // map each agent step -> the per-criterion verdicts (d.criterionVerdicts) whose
77
+ // grounding refs EXAMINED that step: a precise ref.obs === step.obs match plus
78
+ // the tool-name fallback (ref.path === 'name' && ref.value === step.tool).
79
+ // Each examining criterion renders one compact entry -- result + CODE/JUDGE tag
80
+ // + criterionId + the judge reasoning (critique). A step no criterion references
81
+ // says 'not examined by any eval' (honest), never a bare dash. Any judge step
82
+ // that DOES carry a real anchor is still honored (future-proof).
83
+ var cvAll=(d.criterionVerdicts||[]);
84
+ var refExaminesStep=function(rf,a){
85
+ if(!rf||typeof rf==='string')return false;
86
+ if(a.obs&&rf.obs&&String(rf.obs)===String(a.obs))return true;
87
+ if(rf.path==='name'&&rf.value!=null&&a.tool&&String(rf.value)===String(a.tool))return true;
88
+ return false;};
89
+ var covEntry=function(v,a){
90
+ var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
91
+ // the router carries TWO vocabularies: matrix-derived ('deterministic') and
92
+ // mined ('code-based'); 'hybrid' is shared. A '[code-eval ...]'-prefixed critique
93
+ // is the deterministic-logic fallback when the method is unset.
94
+ var method=((typeof C!=='undefined'&&C[v.criterionId])||{}).m||'';
95
+ var isCode=method==='deterministic'||method==='code-based'||method==='hybrid'||String(v.critique||'').trim().indexOf('[code-eval')===0;
96
+ var tag=method==='hybrid'?'HYBRID':(isCode?'CODE':'JUDGE');
97
+ var matched=(v.refs||[]).filter(function(rf){return typeof rf!=='string'&&refExaminesStep(rf,a);});
98
+ var crit=v.critique?esc(v.critique):'<span class="dim">— no critique recorded</span>';
99
+ return '<div class="jcov '+esc(res)+'"><div class="jcov-h"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="jm '+(isCode?'code':'judge')+'" title="'+(isCode?'deterministically checked by a code-eval (not LLM-judged)':'reasoned by the LLM judge')+'">'+esc(tag)+'</span><span class="jcid">'+esc(v.criterionId||'')+'</span></div><div class="jcrit">'+crit+'</div>'+cvRefs(matched)+'</div>';};
100
+ var rowsHtml='';(d.agentSteps||[]).forEach(function(a){
101
+ // future-proof: a judge step that carries a REAL anchor still renders.
102
+ var js=(d.judgeSteps||[]).filter(function(s){return s.anchor!=null&&String(s.anchor)===String(a.n)&&s.kind!=='context';});
103
+ var anchoredHtml=js.map(function(s){var rs=refStr(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+' '+(rs?'<span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('');
104
+ var examiners=cvAll.filter(function(v){return (v.refs||[]).some(function(rf){return refExaminesStep(rf,a);});});
105
+ var covHtml=examiners.map(function(v){return covEntry(v,a);}).join('');
106
+ var jhtml=anchoredHtml+covHtml;
107
+ if(!jhtml)jhtml='<div class="jstep noexam"><span class="t">— not examined by any eval</span></div>';
108
+ rowsHtml+='<div class="step-l"><div class="evb"><div class="top"><span class="tool">'+esc(a.tool||'')+'</span><span class="st '+esc(a.status||'')+'">'+esc(a.status||'')+'</span></div><div class="det">'+esc(a.detail||'')+'</div></div></div><div class="node '+esc(a.status||'')+'"><div class="ln"></div><div class="n">'+esc(a.n)+'</div><div class="ln"></div></div><div class="step-r"><div class="evb r">'+jhtml+'</div></div>';});
109
+ var h=d.health||{};
110
+ var sp=d.subjectProfile;
111
+ var spHtml=sp?'<div class="ctx"><div class="ctx-h">◇ judge · subject profile (M1) · '+esc(sp.provenance||'')+'</div><div class="ctx-g"><div class="ctx-c"><div class="l">identity</div><div class="v">'+esc(sp.identity||'—')+'</div></div><div class="ctx-c"><div class="l">purpose</div><div class="v">'+esc(sp.purpose||'—')+'</div></div><div class="ctx-c"><div class="l">scope · harness</div><div class="v">'+esc((sp.scope||'—'))+' · harness: '+esc(sp.harness||'—')+'</div></div></div></div>':'';
112
+ var u=d.understanding;
113
+ var uHtml=u?'<div class="band"><div class="bh" style="color:var(--cyan)">◇ node-0 GATHER — understanding (M2)</div><div class="re-rephrase" style="margin-top:4px">“'+esc(u.rephrase||'')+'”</div></div>':'';
114
+ var et=d.expectedTrajectory||[];
115
+ var etHtml=et.length?'<div class="band"><div class="bh" style="color:var(--primary-soft)">◇ node-0.5 EXPECTED-TRAJECTORY (M3) — how it SHOULD have acted</div><ol class="re-ul" style="margin-top:4px">'+et.map(function(s,i){return '<li><b>'+esc(s.step||i+1)+'.</b> '+esc(s.expected||'')+(s.rationale?' <span class=re-rat>— '+esc(s.rationale)+'</span>':'')+'</li>';}).join('')+'</ol></div>':'';
116
+ return '<div class="drillbox"><div style="display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:6px"><b class="mono">'+esc(d.traceId)+'</b><span class="chip">'+esc(d.route||'all')+'</span><span class="verd '+(d.verdict=='FAIL'?'fail':d.verdict=='PASS'?'pass':'inc')+'">'+esc(d.verdict)+'</span></div>'+
117
+ resRouting(d.res||'judge-walk')+
118
+ spHtml+
119
+ verdictWhy+
120
+ critiqueBlock(d.criterionVerdicts)+
121
+ '<div class="ctx"><div class="ctx-h">◇ judge · gather context</div><div class="ctx-g"><div class="ctx-c"><div class="l">harness</div><div class="v">'+esc(ctx.harness||'—')+'</div></div><div class="ctx-c"><div class="l">input + scenario</div><div class="v">'+inputCell(d.input,ctx.scenario)+'</div></div><div class="ctx-c"><div class="l">exit states</div><div class="v">'+esc(ctx.exitStates||'—')+(stateWhy?'<div class="why-note"><span class="ref">why concluded:</span> '+esc(stateWhy)+'</div>':'')+'</div></div></div></div>'+
122
+ whyChain+
123
+ uHtml+etHtml+
124
+ '<div class="lanehdr"><div class="a">target agent — what it did</div><div class="x">step</div><div class="j">judge — eval coverage (which criteria examined this step)</div></div>'+
125
+ '<div class="grid2">'+rowsHtml+'<div class="band loc"><div class="bh">↯ localize (root, not symptom)</div><div style="font-size:var(--fs-sm);margin-top:4px">'+esc(d.localize||'—')+'</div></div></div>'+
126
+ '<div class="health"><div class="hc"><div class="l">context</div><div class="v good">'+(h.contextGathered?'✓':'—')+'</div></div><div class="hc"><div class="l">grounded</div><div class="v good">'+esc(h.grounded||0)+'</div></div><div class="hc"><div class="l">assumed</div><div class="v '+((h.assumed||0)>0?'warn':'good')+'">'+esc(h.assumed||0)+'</div></div><div class="hc"><div class="l">root vs symptom</div><div class="v good">'+(h.stoppedAtSymptom?'symptom':'✓ root')+'</div></div></div></div>';}`;
127
+
128
+ /**
129
+ * The runtime deps the extracted `sideBySide` needs — `esc` / `resBadge` /
130
+ * `resRouting` — copied BYTE-IDENTICALLY from render-eval-report's client script
131
+ * so the reused `sideBySide` behaves identically wherever it is embedded. The
132
+ * review UI embeds these (render-eval-report already defines them above its own
133
+ * splice). `RES` may be an empty map in the review UI — `resRouting`/`resBadge`
134
+ * degrade to '' when a resolution key is absent.
135
+ */
136
+ export const REVIEW_SIDE_BY_SIDE_DEPS_JS = `function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
137
+ function resBadge(res){var m=RES[res];if(!m)return '';return '<span class="resbadge '+m.cls+'" title="'+esc(m.routing)+'">'+esc(m.badge)+'</span>';}
138
+ function resRouting(res){var m=RES[res];if(!m)return '';return '<div class="routing '+m.cls+'"><span class="routing-k">resolution</span><span class="routing-b '+m.cls+'">'+esc(m.badge)+'</span><span class="routing-v">'+esc(m.routing)+'</span></div>';}`;
139
+
140
+ /**
141
+ * The review-UI VIRTUALIZATION mirror. `stepRowR` emits ONE step triplet
142
+ * (`.step-l` + `.node` + `.step-r`) whose markup is CHARACTER-IDENTICAL to the
143
+ * per-step output of the frozen `sideBySide` (compare render-eval-report's
144
+ * `rowsHtml` loop). This lets the review UI window a 1,686-step card — rendering
145
+ * only the visible slice — instead of materializing the whole DOM (the EV-3
146
+ * wall). `refExaminesStepR` / `covEntryR` mirror `sideBySide`'s local
147
+ * `refExaminesStep` / `covEntry`. Depends on `esc` + `cvRefs` (from the deps +
148
+ * the frozen fragment). The drift guard in `tests/report-fragments.test.ts`
149
+ * evaluates both paths and asserts equality, so this mirror can never diverge.
150
+ */
151
+ export const STEP_ROW_FRAGMENT_JS = `function refStrR(rf){if(!rf)return '';if(typeof rf==='string')return rf;return [rf.obs,rf.path,rf.value].filter(Boolean).join(':');}
152
+ function refExaminesStepR(rf,a){
153
+ if(!rf||typeof rf==='string')return false;
154
+ if(a.obs&&rf.obs&&String(rf.obs)===String(a.obs))return true;
155
+ if(rf.path==='name'&&rf.value!=null&&a.tool&&String(rf.value)===String(a.tool))return true;
156
+ return false;}
157
+ function covEntryR(v,a){
158
+ var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
159
+ var method=((typeof C!=='undefined'&&C&&C[v.criterionId])||{}).m||'';
160
+ var isCode=method==='deterministic'||method==='code-based'||method==='hybrid'||String(v.critique||'').trim().indexOf('[code-eval')===0;
161
+ var tag=method==='hybrid'?'HYBRID':(isCode?'CODE':'JUDGE');
162
+ var matched=(v.refs||[]).filter(function(rf){return typeof rf!=='string'&&refExaminesStepR(rf,a);});
163
+ var crit=v.critique?esc(v.critique):'<span class="dim">— no critique recorded</span>';
164
+ return '<div class="jcov '+esc(res)+'"><div class="jcov-h"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="jm '+(isCode?'code':'judge')+'" title="'+(isCode?'deterministically checked by a code-eval (not LLM-judged)':'reasoned by the LLM judge')+'">'+esc(tag)+'</span><span class="jcid">'+esc(v.criterionId||'')+'</span></div><div class="jcrit">'+crit+'</div>'+cvRefs(matched)+'</div>';}
165
+ function stepRowInnerR(a,cvAll,judgeSteps){
166
+ var js=(judgeSteps||[]).filter(function(s){return s.anchor!=null&&String(s.anchor)===String(a.n)&&s.kind!=='context';});
167
+ var anchoredHtml=js.map(function(s){var rs=refStrR(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+' '+(rs?'<span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('');
168
+ var examiners=(cvAll||[]).filter(function(v){return (v.refs||[]).some(function(rf){return refExaminesStepR(rf,a);});});
169
+ var covHtml=examiners.map(function(v){return covEntryR(v,a);}).join('');
170
+ var jhtml=anchoredHtml+covHtml;
171
+ if(!jhtml)jhtml='<div class="jstep noexam"><span class="t">— not examined by any eval</span></div>';
172
+ return '<div class="step-l"><div class="evb"><div class="top"><span class="tool">'+esc(a.tool||'')+'</span><span class="st '+esc(a.status||'')+'">'+esc(a.status||'')+'</span></div><div class="det">'+esc(a.detail||'')+'</div></div></div><div class="node '+esc(a.status||'')+'"><div class="ln"></div><div class="n">'+esc(a.n)+'</div><div class="ln"></div></div><div class="step-r"><div class="evb r">'+jhtml+'</div></div>';}
173
+ function stepExaminedByR(a,cvAll){return (cvAll||[]).some(function(v){return (v.refs||[]).some(function(rf){return refExaminesStepR(rf,a);});});}`;
@@ -35,12 +35,18 @@ export const AdlStage = {
35
35
  Build: "build",
36
36
  Evaluate: "evaluate",
37
37
  Diagnose: "diagnose",
38
- Improve: "improve",
38
+ Optimize: "optimize",
39
39
  Audit: "audit",
40
40
  } as const;
41
41
  export type AdlStageValue = (typeof AdlStage)[keyof typeof AdlStage];
42
42
 
43
- export const SubjectKind = { Skill: "skill", Agent: "agent" } as const;
43
+ // Mirrors the orchestrator handover-contract SubjectKind. The CANONICAL inter-stage
44
+ // subject vocabulary is `agent | skill | code` (matches config-schema TargetSubject).
45
+ // `code` (Wave-2 follow-up) is ADDITIVE — every existing skill/agent handoff still
46
+ // validates; it only lets a `code` subject route through *evaluate's failure handoff
47
+ // the SAME way it flows through the orchestrator handover-contract (the two are kept
48
+ // in lock-step by design; the evaluator re-implements the shape, never imports it).
49
+ export const SubjectKind = { Skill: "skill", Agent: "agent", Code: "code" } as const;
44
50
  export type SubjectKindValue = (typeof SubjectKind)[keyof typeof SubjectKind];
45
51
 
46
52
  export const ArtifactKind = {
@@ -67,7 +73,11 @@ export type EscalationPolicyValue =
67
73
  // ── TypeBox schemas (closed objects — auditable boundary) ───────────────────
68
74
  const SubjectSchema = Type.Object(
69
75
  {
70
- kind: Type.Union([Type.Literal(SubjectKind.Skill), Type.Literal(SubjectKind.Agent)]),
76
+ kind: Type.Union([
77
+ Type.Literal(SubjectKind.Skill),
78
+ Type.Literal(SubjectKind.Agent),
79
+ Type.Literal(SubjectKind.Code),
80
+ ]),
71
81
  name: Type.String({ minLength: 1 }),
72
82
  path: Type.String({ minLength: 1 }),
73
83
  },
@@ -132,7 +142,7 @@ const HandoverBundleSchema = Type.Object(
132
142
  Type.Literal(AdlStage.Build),
133
143
  Type.Literal(AdlStage.Evaluate),
134
144
  Type.Literal(AdlStage.Diagnose),
135
- Type.Literal(AdlStage.Improve),
145
+ Type.Literal(AdlStage.Optimize),
136
146
  Type.Literal(AdlStage.Audit),
137
147
  ]),
138
148
  subject: SubjectSchema,
@@ -32,6 +32,7 @@ import {
32
32
  type EvalMatrix,
33
33
  type RowResultValue,
34
34
  type ScorecardCriterion,
35
+ CheckMethod,
35
36
  RowResult,
36
37
  Track,
37
38
  trackForCheckMethod,
@@ -84,14 +85,96 @@ function isNonEmpty(value: unknown): boolean {
84
85
  return true;
85
86
  }
86
87
 
88
+ /** A STRUCTURED, non-empty value: a non-empty object or array. A present-but-
89
+ * scalar/empty artifact is NOT structured — the typebox-schema re-exec fails it
90
+ * (a hollow presence-pass under the old proxy). PURE. */
91
+ function isStructuredNonEmpty(value: unknown): boolean {
92
+ if (Array.isArray(value)) return value.length > 0;
93
+ if (value !== null && typeof value === "object") return Object.keys(value).length > 0;
94
+ return false;
95
+ }
96
+
97
+ /** Scan a produced artifact for an EXPLICIT gate-failure marker: a falsy pass
98
+ * flag (`pass`/`ok`/`passed` === false) or a failing `status` stamp
99
+ * (`fail`/`failed`/`error`). Conservative + shallow so it stays deterministic
100
+ * and does not false-fail on unrelated substrings. PURE. */
101
+ function hasFailureMarker(value: unknown): boolean {
102
+ if (value === null || typeof value !== "object") return false;
103
+ const obj = value as Record<string, unknown>;
104
+ for (const flag of ["pass", "ok", "passed"] as const) {
105
+ if (obj[flag] === false) return true;
106
+ }
107
+ const status = obj["status"];
108
+ if (typeof status === "string") {
109
+ const s = status.toLowerCase();
110
+ if (s === "fail" || s === "failed" || s === "error") return true;
111
+ }
112
+ return false;
113
+ }
114
+
115
+ /**
116
+ * Item #4 — the REAL live re-execution of a deterministic row. Replaces the
117
+ * presence-proxy (`the artifact exists → mark it covered`) with an ACTUAL
118
+ * checkMethod-keyed predicate over the artifact's LIVE content:
119
+ * - typebox-schema : the produced artifact must be a STRUCTURED, non-empty
120
+ * object/array — a present-but-malformed scalar now FAILS
121
+ * instead of hollow-passing (real shape conformance).
122
+ * - gate : the evidence must be present AND carry NO explicit
123
+ * failure marker (fail-loud gate — `hasFailureMarker`).
124
+ * - deterministic-script: the produced artifact must be present + non-empty
125
+ * (the script actually emitted its output).
126
+ * Returns pass/fail when the criterion maps to a live bundle artifact; returns
127
+ * `null` when NO artifact answers it offline — the caller then records the
128
+ * documented seam-skip. A genuinely un-runnable row would require executing the
129
+ * SUBJECT'S OWN script, which the judge-only cell never does (EV-051) and which
130
+ * dispatches NO sub-agents (PR-ORCH-01). PURE + deterministic (no clock/random/
131
+ * network) ⇒ C-PIN byte-identical reruns.
132
+ */
133
+ export function reexecDeterministicCheck(
134
+ criterion: Criterion,
135
+ bundle: RunBundle,
136
+ ): RowResultValue | null {
137
+ const artifactKey = evidenceArtifactFor(criterion, bundle);
138
+ if (artifactKey == null) return null; // no live artifact → not re-executable here
139
+ const value = bundle.data[artifactKey];
140
+ // HONESTY on the depth of each re-exec (do NOT mistake these for deep checks):
141
+ // - typebox-schema : a real SHAPE upgrade over presence (rejects a present-but-
142
+ // scalar), but it is NOT a full TypeBox `Value.Check` against
143
+ // the produced artifact's declared schema — it asserts
144
+ // "structured + non-empty", not field-level conformance.
145
+ // - gate : `hasFailureMarker` is TOP-LEVEL-ONLY (scans the object's own
146
+ // pass/ok/passed/status keys) — it does NOT recurse into nested
147
+ // gate evidence, so a failure buried in a child object is missed.
148
+ // - deterministic-script : semantically ≡ the OLD presence proxy (present +
149
+ // non-empty). It is re-run against LIVE data (so it is a real
150
+ // re-execution, not a cached verdict), but carries no deeper
151
+ // predicate than presence. Deepening any of these to a true
152
+ // re-run of the subject's own script is the documented seam
153
+ // (out of scope for a judge-only / no-sub-agent cell).
154
+ switch (criterion.checkMethod) {
155
+ case CheckMethod.TypeboxSchema:
156
+ return isStructuredNonEmpty(value) ? RowResult.Pass : RowResult.Fail;
157
+ case CheckMethod.Gate:
158
+ return isNonEmpty(value) && !hasFailureMarker(value) ? RowResult.Pass : RowResult.Fail;
159
+ case CheckMethod.DeterministicScript:
160
+ default:
161
+ return isNonEmpty(value) ? RowResult.Pass : RowResult.Fail;
162
+ }
163
+ }
164
+
87
165
  export interface EvaluateOptions {
88
166
  /**
89
- * When false (default), criteria that need a LIVE re-exec of the subject's own
90
- * script (beyond bundle presence/shape) return `skip` with a documented seam
91
- * note instead of false-passing. A future integration may set this true and
92
- * provide a re-exec callback.
167
+ * Item #4 the LIVE RE-EXEC executor. When supplied it RUNS the real
168
+ * deterministic check against live bundle data (see `reexecDeterministicCheck`,
169
+ * the default) and its pass/fail return is authoritative replacing the
170
+ * presence-proxy that marked a row covered from mere artifact existence. A
171
+ * `null` return means "this executor cannot answer this row" ⇒ the legacy
172
+ * (byte-stable) path runs. ABSENT ⇒ no re-exec wired: fully back-compatible,
173
+ * byte-identical to before (the presence proxy + documented seam-skip). The
174
+ * executor is PURE + dispatches NO sub-agents (PR-ORCH-01) + never fixes the
175
+ * subject (EV-051).
93
176
  */
94
- liveReexec?: (criterion: Criterion) => RowResultValue | null;
177
+ liveReexec?: (criterion: Criterion, bundle: RunBundle) => RowResultValue | null;
95
178
  }
96
179
 
97
180
  /**
@@ -104,21 +187,31 @@ export function evaluateDeterministic(
104
187
  opts: EvaluateOptions = {},
105
188
  ): ScorecardCriterion {
106
189
  const track = Track.Deterministic;
190
+
191
+ // Item #4 — LIVE RE-EXEC FIRST. When an executor is wired it RUNS the real
192
+ // deterministic check against live data; a pass/fail is authoritative and
193
+ // REPLACES the presence-proxy (no more "an artifact exists → covered"). A null
194
+ // return means the executor could not answer this row ⇒ fall through to the
195
+ // (byte-stable) legacy path below.
196
+ const live = opts.liveReexec?.(criterion, bundle) ?? null;
197
+ if (live === RowResult.Pass || live === RowResult.Fail || live === RowResult.Incomplete) {
198
+ return {
199
+ dimension: criterion.dimension,
200
+ severity: criterion.severity,
201
+ checkMethod: criterion.checkMethod,
202
+ track,
203
+ result: live,
204
+ detail: `live re-exec (${criterion.checkMethod}): real deterministic check over the produced artifact → ${live}`,
205
+ };
206
+ }
207
+
107
208
  const artifactKey = evidenceArtifactFor(criterion, bundle);
108
209
 
109
210
  if (artifactKey == null) {
110
- // No bundle artifact answers this row offline -> live-reexec seam.
111
- const live = opts.liveReexec?.(criterion) ?? null;
112
- if (live) {
113
- return {
114
- dimension: criterion.dimension,
115
- severity: criterion.severity,
116
- checkMethod: criterion.checkMethod,
117
- track,
118
- result: live,
119
- detail: "evaluated via live-reexec callback",
120
- };
121
- }
211
+ // No bundle artifact answers this row offline -> documented seam-skip. NOT
212
+ // silent: the coverage-honesty warning (computeCoverage) surfaces it, and
213
+ // fully executing it would require running the subject's OWN script — which
214
+ // the judge-only cell never does (EV-051) and which dispatches no sub-agents.
122
215
  return {
123
216
  dimension: criterion.dimension,
124
217
  severity: criterion.severity,
@@ -130,6 +223,9 @@ export function evaluateDeterministic(
130
223
  };
131
224
  }
132
225
 
226
+ // LEGACY presence proxy — reached ONLY when no live-reexec executor is wired
227
+ // (kept for byte-identity with pre-Item-#4 callers). With the default executor
228
+ // wired (audit-run.ts) this branch is never taken for a mapped row.
133
229
  const present = isNonEmpty(bundle.data[artifactKey]);
134
230
  return {
135
231
  dimension: criterion.dimension,
@@ -138,7 +234,7 @@ export function evaluateDeterministic(
138
234
  track,
139
235
  result: present ? RowResult.Pass : RowResult.Fail,
140
236
  detail: present
141
- ? `evidence artifact '${artifactKey}' present + non-empty`
237
+ ? `evidence artifact '${artifactKey}' present + non-empty (presence proxy — no live re-exec wired)`
142
238
  : `evidence artifact '${artifactKey}' MISSING/empty in bundle (fail-loud)`,
143
239
  };
144
240
  }