|
293 | 293 | r.toolErrors > 0 |
294 | 294 | ? el('span', { class: 'tag error' }, `${r.toolErrors}!`) |
295 | 295 | : null; |
| 296 | + const adoptBadge = |
| 297 | + r.adoptionScore != null |
| 298 | + ? el( |
| 299 | + 'span', |
| 300 | + { |
| 301 | + class: 'tag adopt', |
| 302 | + title: 'metric-tool adoption (not part of combined score)', |
| 303 | + }, |
| 304 | + `adopt ${pct(r.adoptionScore)}`, |
| 305 | + ) |
| 306 | + : null; |
296 | 307 | const termBadge = |
297 | 308 | r.termination && r.termination !== 'final_answer' |
298 | 309 | ? el('span', { class: 'tag' }, r.termination) |
|
310 | 321 | ' ', |
311 | 322 | el('span', { class: 'muted small' }, `${r.toolCalls ?? '?'} calls`), |
312 | 323 | ), |
313 | | - el('span', {}, termBadge, ' ', errBadge, ' ', chip), |
| 324 | + el('span', {}, termBadge, ' ', adoptBadge, ' ', errBadge, ' ', chip), |
314 | 325 | ); |
315 | 326 | mNode.appendChild(node); |
316 | 327 | } |
|
350 | 361 | 'combined', |
351 | 362 | grade?.combinedScore != null ? grade.combinedScore.toFixed(3) : '—', |
352 | 363 | ], |
| 364 | + // Adoption is only present when the scenario rubric defines transcript |
| 365 | + // checks; omit the field entirely otherwise to avoid a dangling '—'. |
| 366 | + ...(grade?.adoption |
| 367 | + ? [['adoption', pct(grade.adoption.score)]] |
| 368 | + : []), |
353 | 369 | ]; |
354 | 370 | for (const [k, v] of fields) { |
355 | 371 | hdr.appendChild( |
|
638 | 654 | pane.appendChild(checks); |
639 | 655 | } |
640 | 656 |
|
| 657 | + // Adoption checks (transcript-aware tool usage). Reported alongside the |
| 658 | + // outcome score but intentionally excluded from the combined score. |
| 659 | + if (g.adoption?.hits) { |
| 660 | + const checks = el('div', { class: 'grade-section' }); |
| 661 | + checks.appendChild( |
| 662 | + el( |
| 663 | + 'h3', |
| 664 | + {}, |
| 665 | + 'Adoption (tool usage)', |
| 666 | + el('span', { class: 'score' }, fmtScore(g.adoption.score)), |
| 667 | + ), |
| 668 | + ); |
| 669 | + for (const h of g.adoption.hits) { |
| 670 | + const good = !!h.satisfied; |
| 671 | + checks.appendChild( |
| 672 | + el( |
| 673 | + 'div', |
| 674 | + { class: 'check' + (h.negative ? ' negative' : '') }, |
| 675 | + el( |
| 676 | + 'div', |
| 677 | + { class: 'marker ' + (good ? 'good' : 'bad') }, |
| 678 | + good ? '✓' : '✗', |
| 679 | + ), |
| 680 | + el('div', { class: 'id' }, h.id), |
| 681 | + el( |
| 682 | + 'div', |
| 683 | + { class: 'weight' }, |
| 684 | + `w${h.weight}${h.matched != null ? ` · matched=${h.matched}` : ''}`, |
| 685 | + ), |
| 686 | + ), |
| 687 | + ); |
| 688 | + } |
| 689 | + checks.appendChild( |
| 690 | + el( |
| 691 | + 'div', |
| 692 | + { class: 'muted small', style: 'margin-top:0.6rem; font-size:11px' }, |
| 693 | + 'Regexes run against the serialized tool-call transcript (tool names + args). Adoption is reported but NOT part of the combined score.', |
| 694 | + ), |
| 695 | + ); |
| 696 | + pane.appendChild(checks); |
| 697 | + } |
| 698 | + |
641 | 699 | // Judge scores |
642 | 700 | if (g.judge?.scores) { |
643 | 701 | const judge = el('div', { class: 'grade-section' }); |
|
857 | 915 | const section = el('div', { class: 'summary-section scenario-breakdown' }); |
858 | 916 | section.appendChild(el('h2', {}, scenario.scenario)); |
859 | 917 |
|
| 918 | + // Forward-compat: the aggregate/summary layer (reports/aggregate.ts) does |
| 919 | + // not yet emit adoption stats (that's HDX-4785). Only surface the adoption |
| 920 | + // metric row + per-check table when the summary actually carries the data, |
| 921 | + // so the viewer lights up automatically once aggregation lands. |
| 922 | + const hasAdoption = Object.values(scenario.cells || {}).some( |
| 923 | + (c) => c?.adoption != null, |
| 924 | + ); |
| 925 | + |
860 | 926 | // Metrics table |
861 | 927 | const metrics = [ |
862 | 928 | { |
|
873 | 939 | dKey: 'programmaticScore', |
874 | 940 | dFmt: signedPct, |
875 | 941 | }, |
| 942 | + ...(hasAdoption |
| 943 | + ? [ |
| 944 | + { |
| 945 | + label: 'Adoption (tool use)', |
| 946 | + get: (c) => c?.adoption?.mean, |
| 947 | + fmt: pct, |
| 948 | + dKey: 'adoptionScore', |
| 949 | + dFmt: signedPct, |
| 950 | + }, |
| 951 | + ] |
| 952 | + : []), |
876 | 953 | { |
877 | 954 | label: 'Judge (weighted)', |
878 | 955 | get: (c) => c?.judge?.weightedMean, |
|
1049 | 1126 | ); |
1050 | 1127 | } |
1051 | 1128 |
|
| 1129 | + // Adoption per-check (forward-compatible: only when the summary carries |
| 1130 | + // adoption.perCheck, populated once HDX-4785 extends aggregate.ts). |
| 1131 | + const allAdoptionChecks = new Set(); |
| 1132 | + for (const cell of Object.values(scenario.cells || {})) { |
| 1133 | + if (cell?.adoption?.perCheck) { |
| 1134 | + for (const id of Object.keys(cell.adoption.perCheck)) |
| 1135 | + allAdoptionChecks.add(id); |
| 1136 | + } |
| 1137 | + } |
| 1138 | + if (allAdoptionChecks.size > 0) { |
| 1139 | + const aHeader = [el('th', {}, 'Adoption check')]; |
| 1140 | + for (const m of mcpOrder) aHeader.push(el('th', {}, m)); |
| 1141 | + const aRows = [...allAdoptionChecks].sort().map((id) => { |
| 1142 | + const cells = [el('td', { class: 'metric-label' }, id)]; |
| 1143 | + for (const m of mcpOrder) { |
| 1144 | + const v = scenario.cells?.[m]?.adoption?.perCheck?.[id]; |
| 1145 | + const cls = |
| 1146 | + v != null |
| 1147 | + ? v >= 0.99 |
| 1148 | + ? 'check-pass' |
| 1149 | + : v >= 0.5 |
| 1150 | + ? 'check-partial' |
| 1151 | + : 'check-fail' |
| 1152 | + : ''; |
| 1153 | + cells.push( |
| 1154 | + el('td', { class: `heatmap-cell ${cls}` }, v != null ? pct(v) : '—'), |
| 1155 | + ); |
| 1156 | + } |
| 1157 | + return el('tr', {}, ...cells); |
| 1158 | + }); |
| 1159 | + section.appendChild( |
| 1160 | + el( |
| 1161 | + 'div', |
| 1162 | + { class: 'sub-table' }, |
| 1163 | + el('h3', {}, 'Adoption per-check (usage rate)'), |
| 1164 | + el( |
| 1165 | + 'table', |
| 1166 | + { class: 'summary-table heatmap' }, |
| 1167 | + el('thead', {}, el('tr', {}, ...aHeader)), |
| 1168 | + el('tbody', {}, ...aRows), |
| 1169 | + ), |
| 1170 | + ), |
| 1171 | + ); |
| 1172 | + } |
| 1173 | + |
1052 | 1174 | // Side-by-side run comparison |
1053 | 1175 | const scenarioCells = state.cells.filter((c) => c.scenario === scenario.scenario); |
1054 | 1176 | if (scenarioCells.length > 0) { |
|
1075 | 1197 | r.toolErrors > 0 |
1076 | 1198 | ? el('span', { class: 'tag error small' }, `${r.toolErrors}err`) |
1077 | 1199 | : null; |
| 1200 | + const adoptBit = |
| 1201 | + r.adoptionScore != null |
| 1202 | + ? el( |
| 1203 | + 'span', |
| 1204 | + { |
| 1205 | + class: 'tag adopt small', |
| 1206 | + title: 'metric-tool adoption', |
| 1207 | + }, |
| 1208 | + `adopt ${pct(r.adoptionScore)}`, |
| 1209 | + ) |
| 1210 | + : null; |
1078 | 1211 | const termBit = |
1079 | 1212 | r.termination !== 'final_answer' |
1080 | 1213 | ? el('span', { class: 'tag small' }, r.termination) |
|
1090 | 1223 | ' ', |
1091 | 1224 | el('span', { class: 'muted small' }, `${r.toolCalls ?? '?'}c ${fmtMs(r.durationMs)}`), |
1092 | 1225 | termBit ? el('span', {}, ' ', termBit) : null, |
| 1226 | + adoptBit ? el('span', {}, ' ', adoptBit) : null, |
1093 | 1227 | errBit ? el('span', {}, ' ', errBit) : null, |
1094 | 1228 | ), |
1095 | 1229 | ); |
|
0 commit comments