niceeval 0.6.0 → 0.6.1

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 (87) hide show
  1. package/dist/agents/types.d.ts +5 -1
  2. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  3. package/dist/report/built-ins/experiment-comparison.js +13 -0
  4. package/dist/report/built-ins/index.d.ts +1 -1
  5. package/dist/report/built-ins/index.js +1 -1
  6. package/dist/report/components.d.ts +3 -1
  7. package/dist/report/components.js +1 -1
  8. package/dist/report/compute.js +15 -33
  9. package/dist/report/format.d.ts +0 -6
  10. package/dist/report/format.js +1 -18
  11. package/dist/report/index.d.ts +2 -2
  12. package/dist/report/index.js +1 -1
  13. package/dist/report/locale.d.ts +13 -2
  14. package/dist/report/locale.js +26 -3
  15. package/dist/report/metrics.js +1 -1
  16. package/dist/report/react/AttemptList.d.ts +2 -2
  17. package/dist/report/react/AttemptList.js +3 -4
  18. package/dist/report/react/ExperimentList.d.ts +3 -2
  19. package/dist/report/react/ExperimentList.js +59 -7
  20. package/dist/report/react/MetricScatter.js +1 -5
  21. package/dist/report/react/fixtures.js +9 -4
  22. package/dist/report/react/format.d.ts +1 -1
  23. package/dist/report/react/format.js +1 -1
  24. package/dist/report/react/index.d.ts +1 -1
  25. package/dist/report/text/faces.js +71 -33
  26. package/dist/report/types.d.ts +3 -15
  27. package/docs-site/zh/concepts/adapter.mdx +2 -2
  28. package/docs-site/zh/concepts/assert.mdx +11 -10
  29. package/docs-site/zh/concepts/evals.mdx +7 -6
  30. package/docs-site/zh/concepts/overview.mdx +1 -1
  31. package/docs-site/zh/guides/agent-feedback-loop.mdx +9 -7
  32. package/docs-site/zh/guides/fixtures.mdx +3 -1
  33. package/docs-site/zh/guides/official-adapters.mdx +1 -1
  34. package/docs-site/zh/guides/publish-report.mdx +1 -1
  35. package/docs-site/zh/guides/report-components.mdx +13 -13
  36. package/docs-site/zh/guides/results-data.mdx +28 -2
  37. package/docs-site/zh/guides/sandbox-agent.mdx +1 -0
  38. package/docs-site/zh/guides/sandbox-providers.mdx +1 -1
  39. package/docs-site/zh/guides/viewing-results.mdx +6 -5
  40. package/docs-site/zh/guides/write-send.mdx +2 -1
  41. package/docs-site/zh/reference/cli.mdx +3 -3
  42. package/docs-site/zh/reference/define-agent.mdx +3 -1
  43. package/docs-site/zh/reference/events.mdx +2 -2
  44. package/docs-site/zh/reference/expect.mdx +10 -5
  45. package/package.json +1 -1
  46. package/src/agents/types.ts +5 -1
  47. package/src/i18n/en.ts +1 -2
  48. package/src/i18n/zh-CN.ts +1 -1
  49. package/src/report/built-in-user-parity.test.tsx +44 -47
  50. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  51. package/src/report/built-ins/index.ts +1 -1
  52. package/src/report/components.tsx +3 -1
  53. package/src/report/compute.ts +12 -36
  54. package/src/report/dual-render.test.tsx +33 -29
  55. package/src/report/format.ts +1 -16
  56. package/src/report/index.ts +1 -2
  57. package/src/report/locale.ts +26 -3
  58. package/src/report/metrics.ts +1 -1
  59. package/src/report/react/AttemptList.tsx +4 -6
  60. package/src/report/react/ExperimentList.tsx +167 -55
  61. package/src/report/react/MetricScatter.tsx +1 -11
  62. package/src/report/react/enhance.js +39 -0
  63. package/src/report/react/fixtures.ts +10 -4
  64. package/src/report/react/format.ts +1 -1
  65. package/src/report/react/index.tsx +0 -1
  66. package/src/report/react/render.test.tsx +24 -8
  67. package/src/report/react/styles.css +40 -13
  68. package/src/report/report.test.ts +2 -7
  69. package/src/report/text/faces.ts +76 -36
  70. package/src/report/types.ts +3 -15
  71. package/src/results/host-equivalence.test.ts +3 -3
  72. package/src/show/index.ts +8 -15
  73. package/src/show/render.ts +7 -204
  74. package/src/show/show.test.ts +22 -23
  75. package/src/view/data.test.ts +1 -1
  76. package/src/view/data.ts +6 -6
  77. package/src/view/index.ts +1 -1
  78. package/src/view/shared/types.ts +1 -1
  79. package/src/view/view-report.test.ts +5 -5
  80. package/dist/o11y/execution-tree.d.ts +0 -103
  81. package/dist/o11y/otlp/select.d.ts +0 -22
  82. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
  83. package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
  84. package/dist/results/annotated-source.d.ts +0 -61
  85. package/dist/results/attempt-evidence.d.ts +0 -69
  86. package/dist/results/attempt-source.d.ts +0 -15
  87. package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
@@ -73,6 +73,45 @@
73
73
  }
74
74
  });
75
75
 
76
+ // ExperimentList 的 web 面以原生 details 表达「八列主行 + 展开明细」,不是 table/tbody。
77
+ // 单独按 summary 的列排序、按整条 details 文本过滤;无 JS 时保持数据侧成功率降序。
78
+ document.addEventListener("click", function (e) {
79
+ var control = closest(e.target, "[data-nre-experiment-sort]");
80
+ if (!control) return;
81
+ var board = control.closest(".nre-experiment-table");
82
+ if (!board) return;
83
+ var index = Number(control.getAttribute("data-nre-experiment-sort"));
84
+ var dir = control.classList.contains("nre-sort-asc") ? "desc" : "asc";
85
+ var controls = board.querySelectorAll("[data-nre-experiment-sort]");
86
+ for (var i = 0; i < controls.length; i++) controls[i].classList.remove("nre-sort-asc", "nre-sort-desc");
87
+ control.classList.add(dir === "asc" ? "nre-sort-asc" : "nre-sort-desc");
88
+ var entries = Array.prototype.slice.call(board.querySelectorAll(":scope > .nre-experiment-entry"));
89
+ entries.sort(function (a, b) {
90
+ var ac = a.querySelector(".nre-experiment-summary").children[index];
91
+ var bc = b.querySelector(".nre-experiment-summary").children[index];
92
+ var av = ac ? ac.getAttribute("data-sort-value") || ac.textContent.trim() : "";
93
+ var bv = bc ? bc.getAttribute("data-sort-value") || bc.textContent.trim() : "";
94
+ if (av === "" && bv === "") return 0;
95
+ if (av === "") return 1;
96
+ if (bv === "") return -1;
97
+ var an = Number(av), bn = Number(bv);
98
+ var out = !isNaN(an) && !isNaN(bn) ? an - bn : String(av).localeCompare(String(bv));
99
+ return dir === "asc" ? out : -out;
100
+ });
101
+ for (var j = 0; j < entries.length; j++) board.appendChild(entries[j]);
102
+ });
103
+
104
+ document.addEventListener("input", function (e) {
105
+ var input = closest(e.target, "input[data-nre-experiment-filter]");
106
+ if (!input) return;
107
+ var scope = input.parentElement;
108
+ var entries = scope ? scope.querySelectorAll(".nre-experiment-entry") : [];
109
+ var query = input.value.trim().toLowerCase();
110
+ for (var i = 0; i < entries.length; i++) {
111
+ entries[i].classList.toggle("nre-row-hidden", query !== "" && entries[i].textContent.toLowerCase().indexOf(query) === -1);
112
+ }
113
+ });
114
+
76
115
  // ───────────────────────── tooltip:.nre-scatter-point / .nre-line-point ─────────────────────────
77
116
  // 首次 hover 时把点内 <title> 的内容搬进 data-nre-title(避免与原生 tooltip 重影),
78
117
  // 渲染样式化 tooltip div(定位在点上方,挂在所属 figure 里)。无 JS 时 <title> 原样生效。
@@ -330,7 +330,6 @@ const failedAttempt: AttemptListItem = {
330
330
  durationMs: 32_000,
331
331
  costUSD: 0.12,
332
332
  locator: locator("@1a4a4a4"),
333
- capabilities: { eval: true, execution: true, timing: true, diff: false },
334
333
  };
335
334
 
336
335
  const erroredAttempt: AttemptListItem = {
@@ -343,7 +342,13 @@ const erroredAttempt: AttemptListItem = {
343
342
  assertions: [],
344
343
  durationMs: 4_500,
345
344
  locator: locator("@1c1c1c1"),
346
- capabilities: { eval: false, execution: true, timing: false, diff: false },
345
+ };
346
+
347
+ const failedRetryAttempt: AttemptListItem = {
348
+ ...failedAttempt,
349
+ attempt: 4,
350
+ durationMs: 35_000,
351
+ locator: locator("@1b5b5b5"),
347
352
  };
348
353
 
349
354
  export const attemptListItems: AttemptListItem[] = [failedAttempt, erroredAttempt];
@@ -376,13 +381,14 @@ export const experimentListItems: ExperimentListItem[] = [
376
381
  experimentId: "compare/bub",
377
382
  agent: "bub",
378
383
  model: "gpt-5.4",
384
+ flags: { memory: true },
379
385
  verdicts: { passed: 1, failed: 1, errored: 0, skipped: 0 },
380
386
  passRate: { value: 0.5, display: "50%", samples: 2, total: 2, refs: [] },
381
387
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 2, refs: [failedAttempt.locator] },
382
388
  duration: { value: 32_000, display: "32.0s", samples: 2, total: 2, refs: [] },
383
389
  tokens: { value: null, display: "—", samples: 0, total: 2, refs: [] },
384
390
  evals: 2,
385
- attempts: 2,
391
+ attempts: 3,
386
392
  lastRunAt: "2026-07-01T10:00:00Z",
387
393
  evalRows: [
388
394
  {
@@ -391,7 +397,7 @@ export const experimentListItems: ExperimentListItem[] = [
391
397
  reason: "roots-correct: expected x=2, got x=3",
392
398
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
393
399
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
394
- attempts: [failedAttempt],
400
+ attempts: [failedAttempt, failedRetryAttempt],
395
401
  },
396
402
  {
397
403
  evalId: "algebra/simple",
@@ -1,7 +1,7 @@
1
1
  // web 面的小工具:展示格式化统一住在计算侧的 ../format.ts(两个渲染面同一份),
2
2
  // 这里 re-export 并补 class 名拼接。MetricCell 一律自带 display,组件不重算。
3
3
 
4
- export { MISSING_TEXT, capabilityBadge, formatDurationMs, formatPercent, formatUSD, verdictMark } from "../format.ts";
4
+ export { MISSING_TEXT, formatDurationMs, formatPercent, formatUSD, verdictMark } from "../format.ts";
5
5
 
6
6
  /** 拼 class 名:过滤空值,末尾接使用者透传的 className。 */
7
7
  export function cx(...parts: (string | undefined | false)[]): string {
@@ -38,7 +38,6 @@ export type {
38
38
 
39
39
  // 数据契约类型(家在 ../types.ts,「算」与「画」两侧共用同一份)
40
40
  export type {
41
- AttemptEvidenceCapabilities,
42
41
  AttemptListItem,
43
42
  AttemptLocator,
44
43
  DeltaData,
@@ -271,7 +271,7 @@ describe("MetricScatter", () => {
271
271
  expect(empty).toContain("4 points missing data");
272
272
  });
273
273
 
274
- it("恰好 1 个可画点:比较至少要两个实验,不画孤点图", () => {
274
+ it("恰好 1 个可画点:照常画出单点", () => {
275
275
  const single = renderToStaticMarkup(
276
276
  <MetricScatter
277
277
  data={{
@@ -281,8 +281,8 @@ describe("MetricScatter", () => {
281
281
  }}
282
282
  />,
283
283
  );
284
- expect(single).not.toContain("<svg");
285
- expect(single).toContain("At least 2 experiments needed to compare");
284
+ expect(single).toContain("<svg");
285
+ expect(single).toContain("nre-scatter-point");
286
286
  });
287
287
  });
288
288
 
@@ -339,10 +339,10 @@ describe("AttemptList", () => {
339
339
  expect(html).not.toContain("more not shown");
340
340
  });
341
341
 
342
- it("每条 attempt 带 locator + 默认证据室深链 + 证据能力标记", () => {
342
+ it("每条 attempt 带 locator + 默认证据室深链,不追加字母缩写", () => {
343
343
  expect(html).toContain('href="#/attempt/@1a4a4a4"');
344
344
  expect(html).toContain('href="#/attempt/@1c1c1c1"');
345
- expect(html).toContain("[E,X,⏱]"); // failedAttempt: eval + execution + timing
345
+ expect(html).not.toMatch(/\[[EXD⏱,]+\]/);
346
346
  });
347
347
 
348
348
  it("长文本(evidence)收进 <details>,零 JS 可展开", () => {
@@ -373,18 +373,34 @@ describe("EvalList", () => {
373
373
  });
374
374
 
375
375
  describe("ExperimentList", () => {
376
- const html = renderToStaticMarkup(<ExperimentList items={experimentListItems} />);
376
+ const html = renderToStaticMarkup(<ExperimentList items={experimentListItems} filter />);
377
377
 
378
- it("主行:身份、agent/model、官方两级聚合指标", () => {
378
+ it("web 面是带过滤框与八个固定列头的 experiment 比较表", () => {
379
+ expect(html).toContain('data-nre-experiment-filter=""');
380
+ for (const header of ["Experiment", "Model", "Agent", "Avg duration", "Pass rate", "Tokens", "Est. cost", "Result"]) {
381
+ expect(html).toContain(`>${header}</button>`);
382
+ }
383
+ expect(html).toContain('data-nre-experiment-sort="4" class="nre-sort-desc"');
384
+ });
385
+
386
+ it("主行:身份、agent/model、官方两级聚合指标与判定构成", () => {
379
387
  expect(html).toContain("compare/bub");
380
388
  expect(html).toContain("compare/codex");
381
389
  expect(html).toContain("gpt-5.4");
382
390
  expect(html).toContain("50%"); // passRate.display
391
+ expect(html).toContain("1 passed / 1 failed");
392
+ expect(html).toContain("memory=true");
383
393
  });
384
394
 
385
- it("展开到 Eval:判定符 + 该题 Attempt 的 locator 徽标内联", () => {
395
+ it("展开到 Eval:同一道题的全部 Attempt 逐条显示", () => {
386
396
  expect(html).toContain("algebra/quadratic");
387
397
  expect(html).toContain('href="#/attempt/@1a4a4a4"');
398
+ expect(html).toContain('href="#/attempt/@1b5b5b5"');
399
+ expect(html.match(/algebra\/quadratic/g)).toHaveLength(1);
400
+ expect(html).toContain("nre-experiment-eval-header");
401
+ expect(html.match(/nre-experiment-attempt-row/g)).toHaveLength(3);
402
+ expect(html).toContain("├─");
403
+ expect(html).toContain("└─");
388
404
  });
389
405
 
390
406
  it("零 JS 靠原生 <details>,无 <script>", () => {
@@ -42,7 +42,7 @@
42
42
  margin: 1rem 0;
43
43
  }
44
44
 
45
- /* ---- 判定符与证据能力徽标:AttemptList / EvalList / ExperimentList 共用的最小单元 ---- */
45
+ /* ---- locator 与判定符:AttemptList / EvalList / ExperimentList 共用的最小单元 ---- */
46
46
  .nre .nre-locator { display:inline-flex; align-items:center; gap:4px; color:var(--nre-c0); font-variant-numeric:tabular-nums; text-decoration:none; white-space:nowrap; }
47
47
  .nre .nre-locator:hover { text-decoration:underline; }
48
48
  .nre .nre-locator-mark { font-weight:700; }
@@ -50,7 +50,6 @@
50
50
  .nre .nre-locator.nre-verdict-failed .nre-locator-mark,
51
51
  .nre .nre-locator.nre-verdict-errored .nre-locator-mark { color:var(--bad); }
52
52
  .nre .nre-locator.nre-verdict-skipped .nre-locator-mark { color:var(--soft); }
53
- .nre .nre-locator-caps { color:var(--muted); font-size:.85em; }
54
53
 
55
54
  /* ---- AttemptList:实体列表的叶子层,一条卡片一个 Attempt ---- */
56
55
  .nre-attempt-list .nre-attempts { list-style:none; margin:0; padding:0; }
@@ -93,25 +92,53 @@
93
92
  .nre-eval-list .nre-attempts { list-style:none; margin:0 0 12px; padding:0 0 0 20px; border-left:2px solid color-mix(in oklch,var(--nre-c0),transparent 55%); }
94
93
  .nre-eval-list .nre-eval-list-empty { border:1px dashed var(--line-strong); border-radius:8px; padding:18px; color:var(--muted); text-align:center; background:var(--panel); }
95
94
 
96
- /* ---- ExperimentList:实体列表的第一级,一条 <details> 一个 experiment ---- */
97
- .nre-experiment-list .nre-experiments { list-style:none; margin:0; padding:0; border:1px solid var(--line); border-radius:9px; overflow:hidden; background:var(--panel); }
95
+ /* ---- ExperimentList:web 面固定八列比较表,一条 <details> 一个 experiment ---- */
96
+ .nre-experiment-table { border:1px solid var(--line); border-radius:9px; overflow:hidden; background:var(--panel); }
97
+ .nre-experiment-head,
98
+ .nre-experiment-summary { display:grid; grid-template-columns:minmax(220px,1.25fr) minmax(120px,.8fr) minmax(90px,.6fr) minmax(100px,.65fr) minmax(90px,.55fr) minmax(110px,.7fr) minmax(90px,.6fr) minmax(160px,.9fr); column-gap:16px; align-items:center; }
99
+ .nre-experiment-head { padding:12px 20px; color:var(--muted); font-size:12px; font-weight:560; text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid var(--line); }
100
+ .nre-experiment-head button { appearance:none; border:0; padding:0; background:transparent; color:inherit; font:inherit; text-transform:inherit; letter-spacing:inherit; text-align:left; cursor:pointer; }
101
+ .nre-experiment-head button::after { content:"↕"; margin-left:6px; color:var(--soft); font-size:11px; }
102
+ .nre-experiment-head button.nre-sort-asc::after { content:"↑"; color:var(--text); }
103
+ .nre-experiment-head button.nre-sort-desc::after { content:"↓"; color:var(--text); }
104
+ .nre-experiment-table-wrap > .nre-filter { height:34px; min-width:260px; margin-bottom:16px; border:1px solid var(--line); border-radius:7px; padding:0 11px; background:var(--panel); color:var(--text); font:inherit; }
98
105
  .nre-experiment-list .nre-experiment-entry { border-bottom:1px solid var(--line); }
99
106
  .nre-experiment-list .nre-experiment-entry:last-child { border-bottom:0; }
100
- .nre-experiment-list .nre-experiment-summary { display:flex; flex-wrap:wrap; gap:.9em; align-items:baseline; padding:13px 20px; cursor:pointer; list-style:none; }
107
+ .nre-experiment-list .nre-experiment-summary { padding:13px 20px; cursor:pointer; list-style:none; transition:background .12s; }
101
108
  .nre-experiment-list .nre-experiment-summary::-webkit-details-marker { display:none; }
102
109
  .nre-experiment-list .nre-experiment-summary:hover { background:color-mix(in oklch,var(--panel-2),var(--panel) 25%); }
103
- .nre-experiment-list .nre-experiment-id { font-weight:650; overflow-wrap:anywhere; }
104
- .nre-experiment-list .nre-experiment-agent,
105
- .nre-experiment-list .nre-experiment-model { color:var(--muted); font-size:.9em; }
106
- .nre-experiment-list .nre-experiment-cell { font-variant-numeric:tabular-nums; }
107
- .nre-experiment-list .nre-experiment-meta-sub { flex-basis:100%; color:var(--soft); font-size:12px; }
108
- .nre-experiment-list .nre-experiment-evals { list-style:none; margin:0; padding:8px 20px 16px; display:grid; gap:6px; background:color-mix(in oklch,var(--panel-2),var(--panel) 24%); }
109
- .nre-experiment-list .nre-experiment-eval-row { display:flex; flex-wrap:wrap; gap:.6em; align-items:baseline; font-size:13px; }
110
+ .nre-experiment-list .nre-experiment-summary > span { min-width:0; }
111
+ .nre-experiment-list .nre-experiment-name { position:relative; padding-left:30px; }
112
+ .nre-experiment-list .nre-experiment-name::before { content:""; position:absolute; left:5px; top:6px; width:7px; height:7px; border-right:1.5px solid var(--muted); border-bottom:1.5px solid var(--muted); transform:rotate(-45deg); transition:transform .15s; }
113
+ .nre-experiment-list .nre-experiment-entry[open] .nre-experiment-name::before { transform:rotate(45deg); }
114
+ .nre-experiment-list .nre-experiment-id { display:block; overflow-wrap:anywhere; font-weight:650; }
115
+ .nre-experiment-list .nre-experiment-name small { display:block; margin-top:3px; color:var(--muted); font-size:12px; font-weight:400; }
116
+ .nre-experiment-list .nre-num { font-variant-numeric:tabular-nums; font-feature-settings:"tnum"; white-space:nowrap; }
117
+ .nre-experiment-list .nre-good { color:var(--good); }
118
+ .nre-experiment-list .nre-warn { color:var(--warn); }
119
+ .nre-experiment-list .nre-bad { color:var(--bad); }
120
+ .nre-experiment-list .nre-experiment-pill { display:inline-flex; min-height:22px; align-items:center; border:1px solid var(--line); border-radius:999px; padding:0 8px; color:var(--muted); font-size:12px; white-space:nowrap; }
121
+ .nre-experiment-list .nre-experiment-detail { padding:18px 30px 22px; background:color-mix(in oklch,var(--panel-2),var(--panel) 24%); border-top:1px solid var(--line); }
122
+ .nre-experiment-list .nre-experiment-flags { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 14px; color:var(--soft); font-size:12px; }
123
+ .nre-experiment-list .nre-experiment-flags b { border:1px solid color-mix(in oklch,var(--line),transparent 16%); border-radius:999px; padding:3px 8px; color:var(--text); font-weight:560; }
124
+ .nre-experiment-list .nre-experiment-evals { list-style:none; margin:0; padding:0; display:grid; gap:6px; }
125
+ .nre-experiment-list .nre-experiment-eval,
126
+ .nre-experiment-list .nre-experiment-attempts { list-style:none; margin:0; padding:0; }
127
+ .nre-experiment-list .nre-experiment-eval { padding:4px 0 9px; border-bottom:1px solid color-mix(in oklch,var(--line),transparent 28%); }
128
+ .nre-experiment-list .nre-experiment-eval:last-child { border-bottom:0; }
129
+ .nre-experiment-list .nre-experiment-eval-header,
130
+ .nre-experiment-list .nre-experiment-attempt-row { display:grid; grid-template-columns:20px minmax(220px,1fr) minmax(150px,.7fr) minmax(260px,1fr); column-gap:12px; align-items:baseline; font-size:13px; }
131
+ .nre-experiment-list .nre-experiment-eval-header { padding:7px 0 5px; }
132
+ .nre-experiment-list .nre-experiment-attempt-row { padding:5px 0 5px 20px; color:var(--muted); }
110
133
  .nre-experiment-list .nre-eval-id { font-weight:560; overflow-wrap:anywhere; }
134
+ .nre-experiment-list .nre-eval-attempt-count { color:var(--soft); font-size:12px; }
135
+ .nre-experiment-list .nre-eval-summary { color:var(--muted); font-size:.92em; overflow-wrap:anywhere; }
136
+ .nre-experiment-list .nre-attempt-branch { color:var(--line-strong); font-variant-numeric:tabular-nums; }
111
137
  .nre-experiment-list .nre-eval-attempt-badges { display:inline-flex; flex-wrap:wrap; gap:.6em; }
112
- .nre-experiment-list .nre-eval-avg,
138
+ .nre-experiment-list .nre-eval-attempt-metrics,
113
139
  .nre-experiment-list .nre-eval-reason { color:var(--muted); font-size:.92em; }
114
140
  .nre-experiment-list .nre-experiment-list-empty { border:1px dashed var(--line-strong); border-radius:8px; padding:18px; color:var(--muted); text-align:center; background:var(--panel); }
141
+ @media(max-width:1000px){.nre-experiment-table{overflow-x:auto}.nre-experiment-head,.nre-experiment-summary{min-width:1040px}.nre-experiment-detail{min-width:1040px}}
115
142
 
116
143
  /* ---- verdict 判定色(AttemptList/EvalList/ExperimentList 的 verdict 文字与判定符共用) ---- */
117
144
  .nre .nre-verdict-passed { color:var(--good); }
@@ -136,7 +136,7 @@ function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selecti
136
136
  }
137
137
 
138
138
  describe("ExperimentList.data", () => {
139
- it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带 locator 与证据能力", async () => {
139
+ it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带全部 Attempt locator", async () => {
140
140
  const s = snap({
141
141
  experimentId: "group/codex-e2b--mempal",
142
142
  agent: "codex",
@@ -181,8 +181,6 @@ describe("ExperimentList.data", () => {
181
181
  expect(evalA.verdict).toBe("passed"); // 任一轮通过 → 该题通过
182
182
  expect(evalA.attempts.map((a) => a.attempt)).toEqual([0, 1]);
183
183
  expect(evalA.attempts[0]!.locator).toMatch(/^@1[0-9a-z]{7}$/);
184
- expect(evalA.attempts[0]!.capabilities.execution).toBe(true); // attempt0 的 hasEvents:true
185
- expect(evalA.attempts[1]!.capabilities.execution).toBe(false);
186
184
  // 每次 attempt 独立编码,即使同一道题也不撞车
187
185
  expect(evalA.attempts[0]!.locator).not.toBe(evalA.attempts[1]!.locator);
188
186
 
@@ -243,7 +241,7 @@ describe("EvalList.data", () => {
243
241
  });
244
242
 
245
243
  describe("AttemptList.data", () => {
246
- it("每项一个 Attempt:身份、判定、assertions、耗时、成本、locator 与证据能力标记", async () => {
244
+ it("每项一个 Attempt:身份、判定、assertions、耗时、成本与 locator", async () => {
247
245
  const s = snap({
248
246
  experimentId: "exp/x",
249
247
  results: [
@@ -270,9 +268,6 @@ describe("AttemptList.data", () => {
270
268
  });
271
269
  expect(item.assertions).toHaveLength(1);
272
270
  expect(item.locator).toMatch(/^@1[0-9a-z]{7}$/);
273
- // 证据能力:cheap 计算,不调用完整 loadAttemptEvidence——execution 来自 hasEvents,
274
- // eval 来自 hasSources,diff 懒加载(fixture 的 diff() 恒返回 null → false)。
275
- expect(item.capabilities).toEqual({ eval: true, execution: true, timing: false, diff: false });
276
271
  });
277
272
 
278
273
  it("redact 作用于 error 与断言 detail/evidence,不作用于身份字段", async () => {
@@ -9,7 +9,6 @@ import type {
9
9
  AttemptListItem,
10
10
  DeltaData,
11
11
  EvalListItem,
12
- ExperimentListEvalRow,
13
12
  ExperimentListItem,
14
13
  GroupSummaryData,
15
14
  LineData,
@@ -24,7 +23,6 @@ import type { TextContext } from "../tree.ts";
24
23
  import type { TableColumn, TableRow } from "../primitives.tsx";
25
24
  import {
26
25
  attemptItemReason,
27
- capabilityBadge,
28
26
  formatDurationMs,
29
27
  formatMetricValue,
30
28
  formatPlainNumber,
@@ -325,11 +323,6 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
325
323
  if (drawable.length === 0) {
326
324
  return [localeText(locale, "scatter.noData", axes), ...footnotes].join("\n");
327
325
  }
328
- // 恰好 1 个可画点:成本 × 通过率的比较至少要两个实验,单点不成图。
329
- if (drawable.length === 1) {
330
- return [localeText(locale, "scatter.needTwo", axes), ...footnotes].join("\n");
331
- }
332
-
333
326
  // 点太密排不下时降级为坐标表,不硬挤
334
327
  if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
335
328
  const table = renderCoordinateTable(
@@ -451,45 +444,93 @@ export function deltaText(data: DeltaData, ctx: TextContext): string {
451
444
 
452
445
  // ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
453
446
  //
454
- // 三面共用的紧凑标记:`locator✓[E,X,⏱]`(判定符紧跟 locator,证据能力方括号紧跟判定符,
455
- // 中间不留空格)——docs-site/zh/guides/report-components.mdx「终端输出形成反馈闭环」定的形态。
447
+ // 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
456
448
  // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
457
449
  // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
458
450
  // 命令模板只在 AttemptList(叶子层)展示完整断言明细时才值得,不在中间层重复。
459
451
 
460
- function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"]; capabilities: AttemptListItem["capabilities"] }): string {
461
- return `${item.locator}${verdictMark(item.verdict)}${capabilityBadge(item.capabilities)}`;
452
+ function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"] }): string {
453
+ return `${item.locator}${verdictMark(item.verdict)}`;
462
454
  }
463
455
 
464
456
  // ── ExperimentList ──
465
457
 
466
- function experimentListEvalLine(row: ExperimentListEvalRow): string {
467
- const badges = row.attempts.map(locatorBadge).join(" ");
468
- const trailer =
469
- row.verdict === "passed"
470
- ? [formatDurationMs(row.duration.value ?? 0), row.cost.value === null ? undefined : formatUSD(row.cost.value)]
471
- .filter((s): s is string => s !== undefined)
472
- .join(" · ")
473
- : (row.reason ?? "");
474
- return ` ${verdictMark(row.verdict)} ${row.evalId} ${badges} ${trailer}`;
458
+ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext): string {
459
+ const locale = ctx.locale;
460
+ const compact = ctx.width < 100;
461
+ const columns: TableColumn[] = [
462
+ { key: "experiment", header: compact && locale === "en" ? "Exp." : localeText(locale, "experimentList.experiment") },
463
+ { key: "model", header: localeText(locale, "table.model") },
464
+ { key: "agent", header: localeText(locale, "table.agent") },
465
+ { key: "duration", header: compact && locale === "en" ? "Avg" : localeText(locale, "experimentList.avgDuration"), align: "right" },
466
+ { key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
467
+ { key: "result", header: localeText(locale, "experimentList.result") },
468
+ { key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
469
+ { key: "cost", header: compact && locale === "en" ? "Cost" : localeText(locale, "experimentList.estimatedCost"), align: "right" },
470
+ ];
471
+ const rows: TableRow[] = items.map((item) => ({
472
+ key: item.experimentId,
473
+ cells: {
474
+ experiment: item.experimentId,
475
+ model: item.model ?? localeText(locale, "experimentList.defaultModel"),
476
+ agent: item.agent,
477
+ duration: cellText(item.duration),
478
+ passRate: cellText(item.passRate),
479
+ result: verdictTallyText(item.verdicts, locale),
480
+ tokens: cellText(item.tokens),
481
+ cost: cellText(item.cost),
482
+ },
483
+ }));
484
+ const metadata = items.flatMap((item) =>
485
+ wrapDisplay(
486
+ `${item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
487
+ Math.max(8, ctx.width - 2),
488
+ ).map((line) => ` ${line}`),
489
+ );
490
+ return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
475
491
  }
476
492
 
477
- export function experimentListText(items: ExperimentListItem[], ctx: TextContext): string {
493
+ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): string {
478
494
  const locale = ctx.locale;
479
- if (items.length === 0) return localeText(locale, "attemptList.empty");
480
- const blocks = items.map((item) => {
481
- const identity = item.model ? `${item.experimentId} · ${item.agent} · ${item.model}` : `${item.experimentId} · ${item.agent}`;
482
- const summary = [
483
- `${localeText(locale, "overview.passRate")} ${cellText(item.passRate)}`,
484
- verdictTallyText(item.verdicts, locale),
485
- localeText(locale, "overview.attemptsCount", { n: item.attempts }),
486
- formatDurationMs(item.duration.value ?? 0),
487
- item.cost.value === null ? missingText(locale) : formatUSD(item.cost.value),
488
- ].join(" · ");
489
- const evalLines = item.evalRows.map(experimentListEvalLine);
490
- return [identity, ` ${summary}`, ...evalLines].join("\n");
495
+ const columns: TableColumn[] = [
496
+ { key: "status", header: localeText(locale, "experimentList.status") },
497
+ { key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
498
+ { key: "result", header: localeText(locale, "experimentList.result") },
499
+ { key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
500
+ { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
501
+ ];
502
+ const rows: TableRow[] = item.evalRows.flatMap((row) => {
503
+ const parent: TableRow = {
504
+ key: row.evalId,
505
+ cells: {
506
+ status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
507
+ entity: row.evalId,
508
+ result: "",
509
+ duration: "",
510
+ cost: "",
511
+ },
512
+ };
513
+ const attempts: TableRow[] = row.attempts.map((attempt, index) => ({
514
+ key: attempt.locator,
515
+ cells: {
516
+ status: ` ${verdictMark(attempt.verdict)}`,
517
+ entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
518
+ result: attemptItemReason(attempt) ?? MISSING_MARK,
519
+ duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
520
+ cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
521
+ },
522
+ }));
523
+ return [parent, ...attempts];
491
524
  });
492
- return blocks.join("\n\n");
525
+ const flags = item.flags && Object.keys(item.flags).length > 0
526
+ ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
527
+ : undefined;
528
+ return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)].filter(Boolean).join("\n");
529
+ }
530
+
531
+ export function experimentListText(items: ExperimentListItem[], ctx: TextContext): string {
532
+ if (items.length === 0) return localeText(ctx.locale, "attemptList.empty");
533
+ return [experimentSummaryTable(items, ctx), ...items.map((item) => experimentDetailTable(item, ctx))].join("\n\n");
493
534
  }
494
535
 
495
536
  // ── EvalList ──
@@ -518,7 +559,7 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
518
559
 
519
560
  // ── AttemptList ──
520
561
 
521
- /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本 + 证据能力,
562
+ /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本,
522
563
  * 然后逐条断言(gate 与 soft 都列,与 web 面的 AttemptRow 同一份材料)。 */
523
564
  function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: ReportLocale): string {
524
565
  const head = [
@@ -527,7 +568,6 @@ function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: Re
527
568
  item.experimentId,
528
569
  formatDurationMs(item.durationMs),
529
570
  ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
530
- ...(capabilityBadge(item.capabilities) ? [capabilityBadge(item.capabilities)] : []),
531
571
  ].join(" · ");
532
572
  const lines = [head];
533
573
  if (item.error) {
@@ -4,12 +4,11 @@
4
4
 
5
5
  import type { AttemptHandle, SelectionWarning } from "../results/types.ts";
6
6
  import type { AttemptLocator } from "../results/locator.ts";
7
- import type { AttemptEvidenceCapabilities } from "../results/attempt-evidence.ts";
8
7
  import type { AssertionResult, Verdict } from "../types.ts";
9
8
  import type { LocalizedLabel, ReportLocale } from "./locale.ts";
10
9
 
11
10
  export type { SelectionWarning };
12
- export type { AttemptLocator, AttemptEvidenceCapabilities };
11
+ export type { AttemptLocator };
13
12
  export type { LocalizedLabel, ReportLocale };
14
13
 
15
14
  // ───────────────────────── 指标与聚合 ─────────────────────────
@@ -336,10 +335,8 @@ export interface DeltaData<K extends string = string> {
336
335
  // `EvalListItem.attempts` 的元素,报告作者可以直接把这些嵌套数组喂给 `<AttemptList items={...} />`。
337
336
 
338
337
  /**
339
- * `AttemptList` 一项 = 一个 Attempt:身份、判定、断言、error、耗时、成本、locator,外加
340
- * 证据能力标记(有没有保存的 Eval 源码 / 执行事件 / OTel 计时 / diff——与 `AttemptEvidence.capabilities`
341
- * 同一个类型,不重新发明四个布尔位的含义)。`ExperimentList` / `EvalList` 的下钻数组复用同一个
342
- * 类型,不是各自的精简版。
338
+ * `AttemptList` 一项 = 一个 Attempt:身份、判定、断言、error、耗时、成本和 locator
339
+ * `ExperimentList` / `EvalList` 的下钻数组复用同一个类型,不是各自的精简版。
343
340
  */
344
341
  export interface AttemptListItem {
345
342
  evalId: string;
@@ -352,15 +349,6 @@ export interface AttemptListItem {
352
349
  durationMs: number;
353
350
  costUSD?: number;
354
351
  locator: AttemptLocator;
355
- /**
356
- * 证据能力标记:`eval`(运行时 Eval 源码已保存)、`execution`(执行事件非空)、
357
- * `timing`(执行事件 + 这次运行接入过 OTel)、`diff`(工作区有文件改动)。逐位定义与
358
- * `AttemptEvidence.capabilities` 完全一致——这里复用同一个类型,不重复声明四个布尔位的门槛。
359
- * 计算函数从 `EvalResult` 上已有的 `hasEvents` / `hasSources` / `hasTrace` 摘要位与一次
360
- * `attempt.diff()` 懒加载算出,不对每个 item 调用完整的 `loadAttemptEvidence`
361
- * (那还会额外装配 Eval 源码标注与 ExecutionTree,这里只要四个布尔位)。
362
- */
363
- capabilities: AttemptEvidenceCapabilities;
364
352
  }
365
353
 
366
354
  /**
@@ -418,7 +418,7 @@ describe("宿主接线 · show text 面与 view web 面反映同一批事实", (
418
418
  expect(face).toContain("compare/bub");
419
419
  expect(face).toContain("q2"); // 周一补齐的失败 eval,若宿主回退 results.latest() 就会消失
420
420
  }
421
- expect(text).toContain("1 passed · 1 failed");
421
+ expect(text).toContain("1 passed / 1 failed");
422
422
  expect(html).toContain("50%");
423
423
  // 现刻水位覆盖齐全 → 两面都不出 partial-coverage(text: "verdicts cover"; html: data-kind)。
424
424
  expect(hasPartialCoverageText(text)).toBe(false);
@@ -493,7 +493,7 @@ function hasPartialCoverageHtml(html: string): boolean {
493
493
  return html.includes('data-kind="partial-coverage"');
494
494
  }
495
495
 
496
- describe("默认宿主事实对照 · show 索引与 view 报告同口径", () => {
496
+ describe("默认宿主事实对照 · show / view 渲染同一报告的两面", () => {
497
497
  it("通过计数一致:同一 fixture 两面反映 3 过 1 败", async () => {
498
498
  const root = await makeRoot();
499
499
  await writeSnapshot(root, "2026-07-01T08-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-01T08:00:00.000Z" }, [
@@ -504,7 +504,7 @@ describe("默认宿主事实对照 · show 索引与 view 报告同口径", () =
504
504
  ]);
505
505
  const text = await showText(root, []);
506
506
  const html = await viewHtml(root);
507
- expect(text).toContain("3 passed · 1 failed");
507
+ expect(text).toContain("3 passed / 1 failed");
508
508
  expect(html).toContain("75%");
509
509
  for (const face of [text, html]) expect(face).toContain("compare/bub");
510
510
  // 无残缺:两面都不出 partial-coverage,布尔一致(某面偷偷补/漏警告即失配)。
package/src/show/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // 宿主组合语义:docs/feature/reports/architecture.md「Selection 是计算入口」)。
3
3
  //
4
4
  // 位置参数 = eval id 前缀,或 `@<locator>`(精确指名单个 attempt,见 results/locator.ts):
5
- // 裸跑 / 前缀 show 专用的紧凑 attempt 索引(单 eval 前缀仍进入详情)
5
+ // 裸跑 / 多 eval 前缀 默认 ExperimentComparison text 面(单 eval 前缀仍进入详情)
6
6
  // 恰好一个 eval 单 eval 详情(attempt / 断言明细,宿主本体)
7
7
  // @<locator> 精确 attempt:无证据 flag → 紧凑全景;带 flag → 对应证据切面
8
8
  // --eval / --execution / --diff[=路径] 证据切面(宿主本体):出现即走证据室,不渲染报告槽
@@ -11,7 +11,7 @@
11
11
  // --run <目录> 结果根换成该目录;--experiment Selection 只留该实验
12
12
  //
13
13
  // 数据全部走 niceeval/results 的读取面(openResults + 合成 Selection + loadAttemptEvidence),
14
- // 不自己爬目录、不自己重算证据 capability。
14
+ // 不自己爬目录;证据可用性只由 loadAttemptEvidence 在单 Attempt 页面计算。
15
15
 
16
16
  import { existsSync } from "node:fs";
17
17
  import { join, resolve } from "node:path";
@@ -24,7 +24,7 @@ import {
24
24
  MalformedLocatorError,
25
25
  } from "../results/index.ts";
26
26
  // report.ts / load.ts have no JSX of their own, but ReportDefinition / ReportLoadError must
27
- // come from the SAME module instance CostPassRateComparison (built-ins, .tsx) is built
27
+ // come from the SAME module instance ExperimentComparison (built-ins, .tsx) is built
28
28
  // against — `unique symbol` branding and `instanceof` are keyed by declaration site, so a raw
29
29
  // src copy and the compiled dist copy of "the same" ReportDefinition are, to TypeScript and to
30
30
  // `instanceof`, two different types. The package-owned report runtime ships as precompiled
@@ -32,6 +32,7 @@ import {
32
32
  // when niceeval is linked in — see tsconfig.report-build.json); show pulls all of it from
33
33
  // there, not just the .tsx-touching pieces.
34
34
  import { renderReportToText } from "../../dist/report/report.js";
35
+ import { ExperimentComparison } from "../../dist/report/built-ins/index.js";
35
36
  import { ReportLoadError, loadReportFile } from "../../dist/report/load.js";
36
37
  import { detectLocale, t } from "../i18n/index.ts";
37
38
  import { foldEvalVerdict } from "../shared/verdict.ts";
@@ -43,7 +44,6 @@ import {
43
44
  attemptIndexLine,
44
45
  attemptOverviewText,
45
46
  attemptsOfEval,
46
- cheapCapabilities,
47
47
  diffText,
48
48
  evalDetailText,
49
49
  evalHistoryText,
@@ -52,7 +52,6 @@ import {
52
52
  experimentHistoryText,
53
53
  pickDetailAttempt,
54
54
  skippedRunsText,
55
- showIndexText,
56
55
  verdictReasonLine,
57
56
  } from "./render.ts";
58
57
 
@@ -202,7 +201,7 @@ async function show(
202
201
  // 证据切面是宿主本体:出现即走证据室,不渲染报告槽(与默认报告同规则)。
203
202
  if (evidence) {
204
203
  if (matchedEvalIds.length !== 1) {
205
- // 撞多个 eval 时不止说「有几个」,直接给紧凑索引(locator + capability 字母 + 失败原因)
204
+ // 撞多个 eval 时不止说「有几个」,直接给紧凑索引(locator + 失败原因)
206
205
  // 让 agent 一步摘到 `@<locator>`,不必再跑一轮 `show <eval id>` 才知道选谁。
207
206
  const index = matchedEvalIds
208
207
  .map((evalId) => {
@@ -213,7 +212,6 @@ async function show(
213
212
  evalId,
214
213
  verdict,
215
214
  locator: rep?.locator,
216
- capabilities: rep ? cheapCapabilities(rep.result) : { eval: false, execution: false, timing: false, diff: false },
217
215
  reason: rep ? verdictReasonLine(rep.result) : undefined,
218
216
  });
219
217
  })
@@ -277,16 +275,11 @@ async function show(
277
275
  return;
278
276
  }
279
277
 
280
- // 裸 show 是终端诊断索引(docs/feature/reports/show.md),不是 web 默认分析报告的 text 面。
281
- // --report 才进入可替换报告槽;view 仍可选择 CostPassRateComparison 作为自己的默认面。
282
- // locale = CLI 界面语言(NICEEVAL_LANG / LC_* / LANG 检测):报告 chrome 文案跟随
278
+ // 裸 show 与裸 view 选择同一个普通默认 definition,这里只是渲染 text 面。
279
+ // --report 替换同一个报告槽。locale = CLI 界面语言(NICEEVAL_LANG / LC_* / LANG 检测):报告 chrome 文案跟随
283
280
  // 终端语言(docs/feature/reports/library.md「locale:渲染面的语言」);Locale 与 ReportLocale 同为
284
281
  // "en" | "zh-CN",直接传递。
285
- if (flags.report === undefined) {
286
- io.out(showIndexText(selection, io.width) + "\n");
287
- return;
288
- }
289
- const definition = await loadReportFile(cwd, flags.report);
282
+ const definition = flags.report === undefined ? ExperimentComparison : await loadReportFile(cwd, flags.report);
290
283
  // attemptCommand 留给 renderReportToText 的默认值:AttemptLocator 已经是可直接 `niceeval show
291
284
  // @<locator>` 的真实 CLI 语法,不需要再反查 eval id 拼一条近似命令(见 tree.ts 的默认实现)。
292
285
  const text = await renderReportToText(definition, { selection, results }, { width: io.width, locale: detectLocale() });