niceeval 0.8.1 → 0.9.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 (140) hide show
  1. package/INDEX.md +77 -45
  2. package/dist/agents/types.d.ts +28 -6
  3. package/dist/i18n/en.d.ts +2 -0
  4. package/dist/i18n/zh-CN.d.ts +3 -1
  5. package/dist/report/built-in/index.d.ts +3 -2
  6. package/dist/report/built-in/index.js +7 -8
  7. package/dist/report/built-in/standard.d.ts +1 -0
  8. package/dist/report/built-in/standard.js +30 -0
  9. package/dist/report/components.d.ts +69 -2
  10. package/dist/report/components.js +152 -3
  11. package/dist/report/compute.d.ts +28 -1
  12. package/dist/report/compute.js +123 -0
  13. package/dist/report/index.d.ts +4 -4
  14. package/dist/report/index.js +3 -2
  15. package/dist/report/locale.d.ts +39 -1
  16. package/dist/report/locale.js +69 -0
  17. package/dist/report/react/AttemptList.d.ts +3 -1
  18. package/dist/report/react/AttemptList.js +3 -3
  19. package/dist/report/react/CopyFixPrompt.d.ts +12 -0
  20. package/dist/report/react/CopyFixPrompt.js +12 -0
  21. package/dist/report/react/HeroCard.d.ts +13 -0
  22. package/dist/report/react/HeroCard.js +35 -0
  23. package/dist/report/react/PoweredBy.d.ts +5 -0
  24. package/dist/report/react/PoweredBy.js +7 -0
  25. package/dist/report/react/ScopeWarnings.d.ts +12 -0
  26. package/dist/report/react/ScopeWarnings.js +18 -0
  27. package/dist/report/react/TraceWaterfall.d.ts +14 -0
  28. package/dist/report/react/TraceWaterfall.js +22 -0
  29. package/dist/report/react/index.d.ts +6 -1
  30. package/dist/report/react/index.js +6 -0
  31. package/dist/report/report.d.ts +22 -6
  32. package/dist/report/report.js +68 -55
  33. package/dist/report/scope-warnings.d.ts +28 -0
  34. package/dist/report/scope-warnings.js +101 -0
  35. package/dist/report/text/faces.d.ts +19 -1
  36. package/dist/report/text/faces.js +61 -0
  37. package/dist/report/tree.d.ts +1 -1
  38. package/dist/report/tree.js +7 -2
  39. package/dist/report/types.d.ts +45 -0
  40. package/dist/report/web.d.ts +5 -4
  41. package/dist/report/web.js +7 -22
  42. package/dist/results/select.d.ts +17 -4
  43. package/dist/results/select.js +76 -11
  44. package/dist/results/types.d.ts +26 -0
  45. package/dist/runner/fingerprint.d.ts +3 -3
  46. package/dist/runner/sandbox-selection.d.ts +12 -0
  47. package/dist/runner/types.d.ts +18 -6
  48. package/dist/sandbox/types.d.ts +12 -0
  49. package/dist/shared/aggregate.d.ts +1 -1
  50. package/dist/shared/aggregate.js +1 -1
  51. package/docs-site/zh/explanation/evals.mdx +2 -1
  52. package/docs-site/zh/explanation/experiment.mdx +2 -0
  53. package/docs-site/zh/how-to/custom-reports.mdx +23 -7
  54. package/docs-site/zh/how-to/experiments.mdx +2 -2
  55. package/docs-site/zh/how-to/publish-report.mdx +11 -4
  56. package/docs-site/zh/how-to/viewing-results.mdx +17 -8
  57. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  58. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  59. package/docs-site/zh/reference/cli.mdx +3 -3
  60. package/docs-site/zh/reference/define-eval.mdx +8 -0
  61. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  62. package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
  63. package/package.json +2 -1
  64. package/src/agents/bub.ts +13 -1
  65. package/src/agents/claude-code.test.ts +43 -1
  66. package/src/agents/claude-code.ts +32 -14
  67. package/src/agents/codex.test.ts +168 -1
  68. package/src/agents/codex.ts +51 -15
  69. package/src/agents/mcp.ts +31 -0
  70. package/src/agents/post-setup.ts +33 -0
  71. package/src/agents/types.ts +28 -7
  72. package/src/cli.ts +15 -14
  73. package/src/define.ts +3 -0
  74. package/src/i18n/en.ts +8 -5
  75. package/src/i18n/zh-CN.ts +8 -4
  76. package/src/index.ts +1 -0
  77. package/src/report/built-in/index.tsx +8 -7
  78. package/src/report/built-in/standard.tsx +59 -0
  79. package/src/report/components.tsx +218 -2
  80. package/src/report/compute.ts +138 -1
  81. package/src/report/dual-render.test.tsx +141 -14
  82. package/src/report/index.ts +20 -0
  83. package/src/report/locale.ts +83 -1
  84. package/src/report/react/AttemptList.tsx +13 -1
  85. package/src/report/react/CopyFixPrompt.tsx +37 -0
  86. package/src/report/react/HeroCard.tsx +59 -0
  87. package/src/report/react/PoweredBy.tsx +20 -0
  88. package/src/report/react/ScopeWarnings.tsx +74 -0
  89. package/src/report/react/TraceWaterfall.tsx +78 -0
  90. package/src/report/react/enhance.js +14 -0
  91. package/src/report/react/index.tsx +11 -0
  92. package/src/report/react/styles.css +193 -7
  93. package/src/report/report.ts +99 -64
  94. package/src/report/scope-warnings.ts +155 -0
  95. package/src/report/site-components.test.tsx +526 -0
  96. package/src/report/text/faces.ts +66 -0
  97. package/src/report/tree.ts +10 -3
  98. package/src/report/types.ts +51 -0
  99. package/src/report/web.ts +7 -40
  100. package/src/results/host-equivalence.test.ts +6 -2
  101. package/src/results/open.ts +5 -4
  102. package/src/results/results.test.ts +78 -1
  103. package/src/results/select.ts +80 -12
  104. package/src/results/types.ts +27 -0
  105. package/src/runner/attempt.ts +10 -9
  106. package/src/runner/discover.test.ts +9 -1
  107. package/src/runner/discover.ts +3 -3
  108. package/src/runner/fingerprint.ts +9 -4
  109. package/src/runner/ledger.test.ts +30 -1
  110. package/src/runner/ledger.ts +26 -4
  111. package/src/runner/run.ts +5 -1
  112. package/src/runner/sandbox-selection.test.ts +131 -0
  113. package/src/runner/sandbox-selection.ts +110 -0
  114. package/src/runner/types.ts +19 -2
  115. package/src/sandbox/types.ts +6 -0
  116. package/src/shared/aggregate.ts +1 -1
  117. package/src/show/index.ts +19 -12
  118. package/src/show/render.ts +12 -12
  119. package/src/show/report-host.test.ts +34 -17
  120. package/src/show/report-host.ts +6 -5
  121. package/src/show/show.test.ts +141 -4
  122. package/src/view/app/App.test.tsx +79 -17
  123. package/src/view/app/App.tsx +25 -74
  124. package/src/view/app/components/CopyControls.tsx +4 -42
  125. package/src/view/app/i18n.ts +5 -227
  126. package/src/view/app/lib/rows.ts +3 -21
  127. package/src/view/app/shared.ts +1 -3
  128. package/src/view/app/types.ts +2 -2
  129. package/src/view/client-dist/app.css +1 -1
  130. package/src/view/client-dist/app.js +20 -20
  131. package/src/view/data.ts +21 -10
  132. package/src/view/index.ts +2 -12
  133. package/src/view/server.ts +4 -4
  134. package/src/view/shared/types.ts +11 -6
  135. package/src/view/styles.css +9 -252
  136. package/src/view/view-report.test.ts +99 -33
  137. package/src/view/app/components/LazyArtifact.tsx +0 -51
  138. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  139. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  140. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -31,19 +31,23 @@ import {
31
31
  import { renderReportToStaticHtml } from "./web.ts";
32
32
  import {
33
33
  AttemptList,
34
+ CopyFixPrompt,
34
35
  ExperimentComparison,
35
36
  ExperimentList,
36
37
  FailureList,
38
+ Hero,
37
39
  MetricBars,
38
40
  MetricMatrix,
39
41
  MetricScatter,
40
42
  MetricTable,
41
43
  ScopeSummary,
44
+ ScopeWarnings,
45
+ TraceWaterfall,
42
46
  } from "./components.tsx";
43
47
  import { Col, Row, Section, Style, Tab, Table, Tabs, Text } from "./primitives.tsx";
44
48
  import { attemptListData, metricScatterData } from "./compute.ts";
45
49
  import { costUSD, defineMetric, endToEndPassRate } from "./metrics.ts";
46
- import builtInReport from "./built-in/index.tsx";
50
+ import builtInReport, { standard } from "./built-in/index.tsx";
47
51
 
48
52
  // ───────────────────────── fake 数据 ─────────────────────────
49
53
 
@@ -437,7 +441,7 @@ describe("ReportNode 形状与非法节点", () => {
437
441
  // ───────────────────────── 双面同源 ─────────────────────────
438
442
 
439
443
  describe("text/web 双面同源", () => {
440
- it("双面显示同一份解析终值、覆盖率与 warning;宿主在报告树外统一显示 warning", async () => {
444
+ it("双面显示同一份解析终值、覆盖率与 warning;警告的呈现件是页内 ScopeWarnings 组件,宿主无树外通道", async () => {
441
445
  const s = snap({ experimentId: "dual/a", results: [res("q1", "passed"), res("q2", "skipped")] });
442
446
  const warning: ScopeWarning = {
443
447
  kind: "unfinished-snapshot",
@@ -448,7 +452,12 @@ describe("text/web 双面同源", () => {
448
452
  command: "niceeval exp dual/a",
449
453
  };
450
454
  const scope = scopeOf([s], [warning]);
451
- const definition = defineReport(<ScopeSummary />);
455
+ const definition = defineReport(
456
+ <Col>
457
+ <ScopeWarnings />
458
+ <ScopeSummary />
459
+ </Col>,
460
+ );
452
461
  const ctx = { scope, results: resultsOf([s]) };
453
462
  const text = await renderReportToText(definition, ctx);
454
463
  const html = await renderReportToStaticHtml(definition, ctx);
@@ -460,6 +469,10 @@ describe("text/web 双面同源", () => {
460
469
  // web 面把警告的 command 渲染为可复制命令
461
470
  expect(html).toContain("niceeval exp dual/a");
462
471
  expect(html).toContain("nre-warning-command");
472
+ // 宿主不再前置树外警告块:没有 ScopeWarnings 的树两面都不出现警告
473
+ const bare = defineReport(<ScopeSummary />);
474
+ expect(await renderReportToText(bare, ctx)).not.toContain("snapshot is incomplete");
475
+ expect(await renderReportToStaticHtml(bare, ctx)).not.toContain("nre-warning");
463
476
  });
464
477
 
465
478
  it("web 排序/过滤只改变浏览状态:有无 filter prop 数值与行集合相同", async () => {
@@ -505,6 +518,9 @@ describe("text/web 双面同源", () => {
505
518
  });
506
519
  });
507
520
 
521
+ // ScopeWarnings 的聚合、排序、折叠与下一步行为在 site-components.test.tsx(组件版单源);
522
+ // 宿主不再有树外警告前置块,这里不重复宿主版。
523
+
508
524
  // ───────────────────────── FailureList ─────────────────────────
509
525
 
510
526
  describe("FailureList", () => {
@@ -710,9 +726,11 @@ describe("defineReport 装载规范化", () => {
710
726
  expect(fromTree.pages[0]!.title).toEqual(fromContent.pages[0]!.title);
711
727
  });
712
728
 
713
- it("content 与 pages 同时声明或都省略,装载报错且文案含 <ExperimentComparison /> 下一步", () => {
714
- expect(() => defineReport({ content: <ScopeSummary />, pages: [] } as never)).toThrow(/both "content" and "pages"/);
715
- expect(() => defineReport({ title: "X" } as never)).toThrow(/<ExperimentComparison \/>/);
729
+ it("content 与 pages 同时声明或都省略,装载报错且文案给出 extends: standard 下一步", () => {
730
+ expect(() => defineReport({ content: <ScopeSummary />, pages: [] } as never)).toThrow(
731
+ /"content" and "pages" declare exactly one/,
732
+ );
733
+ expect(() => defineReport({ title: "X" } as never)).toThrow(/niceeval\/report\/built-in/);
716
734
  });
717
735
 
718
736
  it("defineReport 产物不是 ReportNode:页里放产物装载报错;树校验同样拒绝", () => {
@@ -805,20 +823,72 @@ describe("defineReport 装载规范化", () => {
805
823
  // ───────────────────────── 内建报告 ─────────────────────────
806
824
 
807
825
  describe("内建报告", () => {
808
- it("niceeval/report/built-in 默认导出是普通 defineReport 产物,与用户写的 defineReport(<ExperimentComparison />) 等价", async () => {
826
+ it("三页普通 defineReport:页 id、页名与逐页组件构成和 built-in.md 全文一致", () => {
809
827
  expect(builtInReport.kind).toBe("report");
810
- expect(builtInReport.pages).toHaveLength(1);
828
+ expect(builtInReport.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
829
+ expect(builtInReport.pages.map((p) => p.title)).toEqual([
830
+ { en: "Report", "zh-CN": "报告" },
831
+ "Attempts",
832
+ { en: "Traces", "zh-CN": "追踪" },
833
+ ]);
834
+ // 逐页组件构成:每页一个 Col,children 按 built-in.md 全文的声明序,全部是公开组件。
835
+ const childTypes = (content: unknown) => {
836
+ const col = content as { type: unknown; props: { children: Array<{ type: unknown; props: Record<string, unknown> }> } };
837
+ expect(col.type).toBe(Col);
838
+ return col.props.children;
839
+ };
840
+ const [reportPage, attemptsPage, tracesPage] = builtInReport.pages;
841
+ expect(childTypes(reportPage!.content).map((c) => c.type)).toEqual([
842
+ Hero,
843
+ ScopeWarnings,
844
+ CopyFixPrompt,
845
+ ExperimentComparison,
846
+ ]);
847
+ const attemptsChildren = childTypes(attemptsPage!.content);
848
+ expect(attemptsChildren.map((c) => c.type)).toEqual([Hero, ScopeWarnings, AttemptList]);
849
+ expect(attemptsChildren[2]!.props.filter).toBe(true);
850
+ expect(childTypes(tracesPage!.content).map((c) => c.type)).toEqual([Hero, ScopeWarnings, TraceWaterfall]);
851
+ });
811
852
 
853
+ it("与 --report 同内容文件完全等价:同一棵页树经同一条管线渲染出逐字节相同的两面", async () => {
812
854
  const s1 = snap({ experimentId: "compare/a", agent: "bub", results: [res("q", "passed")] });
813
855
  const s2 = snap({ experimentId: "compare/b", agent: "codex", results: [res("q", "failed")] });
814
856
  const scope = scopeOf([s1, s2]);
815
857
  const ctx = { scope, results: resultsOf([s1, s2]) };
816
- const builtin = await renderReportToText(builtInReport, ctx, { width: 120 });
817
- const user = await renderReportToText(defineReport(<ExperimentComparison />), ctx, { width: 120 });
818
- expect(builtin).toBe(user);
858
+ // 用户按 built-in.md 全文自己写同内容的 defineReport( import 内建入口)
859
+ const user = defineReport({
860
+ pages: [
861
+ {
862
+ id: "report",
863
+ title: { en: "Report", "zh-CN": "报告" },
864
+ content: (
865
+ <Col>
866
+ <Hero />
867
+ <ScopeWarnings />
868
+ <CopyFixPrompt />
869
+ <ExperimentComparison />
870
+ </Col>
871
+ ),
872
+ },
873
+ { id: "attempts", title: "Attempts", content: <Col><Hero /><ScopeWarnings /><AttemptList filter /></Col> },
874
+ {
875
+ id: "traces",
876
+ title: { en: "Traces", "zh-CN": "追踪" },
877
+ content: <Col><Hero /><ScopeWarnings /><TraceWaterfall /></Col>,
878
+ },
879
+ ],
880
+ });
881
+ for (const pageId of ["report", "attempts", "traces"]) {
882
+ expect(await renderReportToText(builtInReport, ctx, { width: 120, pageId })).toBe(
883
+ await renderReportToText(user, ctx, { width: 120, pageId }),
884
+ );
885
+ expect(await renderReportToStaticHtml(builtInReport, ctx, { pageId })).toBe(
886
+ await renderReportToStaticHtml(user, ctx, { pageId }),
887
+ );
888
+ }
819
889
  });
820
890
 
821
- it("text 面多组时只输出组索引与单组查看命令;单组时输出散点与实验列表", async () => {
891
+ it("首页 text 面多组时只输出组索引与单组查看命令;单组时输出散点与实验列表;页首是 Hero 的标题与 meta 行", async () => {
822
892
  const g1 = snap({ experimentId: "compare/a", results: [res("q", "passed")] });
823
893
  const g2 = snap({ experimentId: "dev/b", results: [res("q", "failed")] });
824
894
  const multi = await renderReportToText(
@@ -826,8 +896,11 @@ describe("内建报告", () => {
826
896
  { scope: scopeOf([g1, g2]), results: resultsOf([g1, g2]) },
827
897
  { width: 120 },
828
898
  );
829
- expect(multi).toContain("niceeval show --experiment compare");
830
- expect(multi).toContain("niceeval show --experiment dev");
899
+ // Hero text 面:标题行(回退链终点)+ 最后运行 meta 行在页首
900
+ expect(multi.split("\n")[0]).toBe("Eval Results");
901
+ expect(multi).toContain("Last run");
902
+ expect(multi).toContain("niceeval show --exp compare");
903
+ expect(multi).toContain("niceeval show --exp dev");
831
904
  expect(multi).not.toContain("Eval / Attempt"); // 不倾倒组内 experiment 明细
832
905
 
833
906
  const priced = snap({
@@ -844,3 +917,57 @@ describe("内建报告", () => {
844
917
  expect(single).toContain("better → upper right");
845
918
  });
846
919
  });
920
+
921
+ // ───────────────────────── extends 与内建视图集合 ─────────────────────────
922
+
923
+ describe("extends 与内建视图集合", () => {
924
+ it("内建入口是视图集合:默认导出与具名导出 standard 同引用", () => {
925
+ expect(builtInReport).toBe(standard);
926
+ });
927
+
928
+ it("extends 叠外壳:页列表与 base 逐项同引用,声明整字段覆盖、未声明沿用 base", () => {
929
+ const branded = defineReport({
930
+ extends: standard,
931
+ title: "Memory Evals",
932
+ links: [{ label: "GitHub", href: "https://github.com/you/repo" }],
933
+ });
934
+ branded.pages.forEach((page, i) => expect(page).toBe(standard.pages[i]));
935
+ expect(branded.title).toBe("Memory Evals");
936
+ expect(branded.links).toEqual([{ label: "GitHub", href: "https://github.com/you/repo" }]);
937
+ expect(branded.head).toEqual([...standard.head]); // 未声明沿用 base
938
+
939
+ // 二级 extends 链:上一次折叠的产物就是下一次的 base
940
+ const chained = defineReport({ extends: branded, links: [] });
941
+ chained.pages.forEach((page, i) => expect(page).toBe(standard.pages[i]));
942
+ expect(chained.title).toBe("Memory Evals"); // 未声明沿用最近声明
943
+ expect(chained.links).toEqual([]); // 声明即整字段覆盖,不拼接
944
+
945
+ // ctx.report.title 取 extends 上声明的 title
946
+ const s = snap({ experimentId: "compare/a", results: [res("q", "passed")] });
947
+ expect(buildReportMeta(branded, scopeOf([s]), "report").title).toBe("Memory Evals");
948
+ });
949
+
950
+ it("无外壳字段的 extends 与内建逐页两面渲染逐字节相同", async () => {
951
+ const s1 = snap({ experimentId: "compare/a", agent: "bub", results: [res("q", "passed")] });
952
+ const s2 = snap({ experimentId: "compare/b", agent: "codex", results: [res("q", "failed")] });
953
+ const ctx = { scope: scopeOf([s1, s2]), results: resultsOf([s1, s2]) };
954
+ const alias = defineReport({ extends: standard });
955
+ for (const pageId of ["report", "attempts", "traces"]) {
956
+ expect(await renderReportToText(alias, ctx, { width: 120, pageId })).toBe(
957
+ await renderReportToText(builtInReport, ctx, { width: 120, pageId }),
958
+ );
959
+ expect(await renderReportToStaticHtml(alias, ctx, { pageId })).toBe(
960
+ await renderReportToStaticHtml(builtInReport, ctx, { pageId }),
961
+ );
962
+ }
963
+ });
964
+
965
+ it("extends 只收 defineReport 产物;与 content/pages 多选或全省略按完整用户反馈报错", () => {
966
+ // @ts-expect-error 非 defineReport 产物,类型层已拒绝;这里模拟无类型 JS 输入
967
+ expect(() => defineReport({ extends: {} })).toThrow(/must be a defineReport\(\.\.\.\) product/);
968
+ // @ts-expect-error extends 与 pages 互斥
969
+ expect(() => defineReport({ extends: standard, pages: standard.pages })).toThrow(/declare exactly one/);
970
+ // @ts-expect-error content / pages / extends 至少声明一个
971
+ expect(() => defineReport({ title: "x" })).toThrow(/niceeval\/report\/built-in/);
972
+ });
973
+ });
@@ -99,29 +99,39 @@ export { DEFAULT_REPORT_LOCALE, localizedTextEquals, resolveLocalizedText, resol
99
99
  export type { LocalizedText, ReportLocale } from "./locale.ts";
100
100
 
101
101
  // 官方双面组件(spec / data 双形态;配套 *Data 计算函数在下面成对导出)
102
+ // 与站点组件(Hero / HeroCard / PoweredBy / ScopeWarnings / CopyFixPrompt / TraceWaterfall)
102
103
  export {
103
104
  AttemptList,
105
+ CopyFixPrompt,
104
106
  DeltaTable,
105
107
  EvalList,
106
108
  ExperimentComparison,
107
109
  ExperimentList,
108
110
  FailureList,
111
+ Hero,
112
+ HeroCard,
109
113
  MetricBars,
110
114
  MetricLine,
111
115
  MetricMatrix,
112
116
  MetricScatter,
113
117
  MetricTable,
118
+ PoweredBy,
114
119
  Scoreboard,
115
120
  ScopeSummary,
121
+ ScopeWarnings,
122
+ TraceWaterfall,
116
123
  } from "./components.tsx";
117
124
  export type {
118
125
  AttemptListProps,
126
+ CopyFixPromptProps,
119
127
  DataProps,
120
128
  DeltaTableProps,
121
129
  EvalListProps,
122
130
  ExperimentComparisonProps,
123
131
  ExperimentListProps,
124
132
  FailureListProps,
133
+ HeroCardProps,
134
+ HeroProps,
125
135
  MetricBarsProps,
126
136
  MetricLineProps,
127
137
  MetricMatrixProps,
@@ -129,23 +139,29 @@ export type {
129
139
  MetricTableProps,
130
140
  ScoreboardProps,
131
141
  ScopeSummaryProps,
142
+ ScopeWarningsProps,
143
+ TraceWaterfallProps,
132
144
  } from "./components.tsx";
133
145
 
134
146
  // 计算函数(组件解析面的具名形式,与组件成对;spec 形态下由管线代调,data 形态与
135
147
  // 嵌入场景下由作者手工调)
136
148
  export {
137
149
  attemptListData,
150
+ copyFixPromptData,
138
151
  deltaTableData,
139
152
  evalListData,
140
153
  experimentComparisonData,
141
154
  experimentListData,
155
+ heroData,
142
156
  metricLineData,
143
157
  metricMatrixData,
144
158
  metricScatterData,
145
159
  metricTableData,
146
160
  pairsByFlag,
147
161
  scopeSummaryData,
162
+ scopeWarningsData,
148
163
  scoreboardData,
164
+ traceWaterfallData,
149
165
  } from "./compute.ts";
150
166
  export type {
151
167
  DeltaTableOptions,
@@ -162,6 +178,7 @@ export type {
162
178
  AttemptListItem,
163
179
  AttemptLocator,
164
180
  BuiltInDimension,
181
+ CopyFixPromptData,
165
182
  CustomDimension,
166
183
  DeltaData,
167
184
  DeltaPair,
@@ -174,6 +191,7 @@ export type {
174
191
  ExperimentListEvalRow,
175
192
  ExperimentListItem,
176
193
  FlagPairs,
194
+ HeroData,
177
195
  LineData,
178
196
  MatrixData,
179
197
  Metric,
@@ -190,6 +208,8 @@ export type {
190
208
  ScopeWarning,
191
209
  ScoreboardData,
192
210
  TableData,
211
+ TraceSpanSummary,
212
+ TraceWaterfallRow,
193
213
  VerdictTally,
194
214
  } from "./types.ts";
195
215
 
@@ -146,6 +146,49 @@ const en = {
146
146
  "delta.pairHeader": "pair (A → B)",
147
147
  "delta.empty": "{experiments} experiments, 0 comparable pairs",
148
148
 
149
+ /** ScopeWarnings 聚合层的 chrome:汇总行、kind 徽标、组头与明细折叠标签;message 本体不经字典。 */
150
+ "warnings.summary.experiments.one": "{n} experiment flagged",
151
+ "warnings.summary.experiments.other": "{n} experiments flagged",
152
+ "warnings.group.unreadableSnapshot.one": "{n} snapshot skipped",
153
+ "warnings.group.unreadableSnapshot.other": "{n} snapshots skipped",
154
+ "warnings.details.one": "{n} warning",
155
+ "warnings.details.other": "{n} warnings",
156
+ "warnings.badge.partialCoverage": "coverage {covered}/{total}",
157
+ "warnings.badge.staleSnapshot": "{gap} behind",
158
+ "warnings.badge.unfinishedSnapshot": "unfinished",
159
+ "warnings.gap.second.one": "{n} second",
160
+ "warnings.gap.second.other": "{n} seconds",
161
+ "warnings.gap.minute.one": "{n} minute",
162
+ "warnings.gap.minute.other": "{n} minutes",
163
+ "warnings.gap.hour.one": "{n} hour",
164
+ "warnings.gap.hour.other": "{n} hours",
165
+ "warnings.gap.day.one": "{n} day",
166
+ "warnings.gap.day.other": "{n} days",
167
+
168
+ /** Hero / HeroCard 的运行 meta(hero.noRuns 是 latestStartedAt 为 null 时的内置文案)。 */
169
+ "hero.lastRun": "Last run {time}",
170
+ "hero.noRuns": "No runs yet",
171
+ /** web 面的合成来源标注(仅 snapshots > 1 时显示)。 */
172
+ "hero.composedRuns": "composed from {n} runs",
173
+ /** text 面的合成来源标注(show 页首 meta 行,仅 snapshots > 1 时显示)。 */
174
+ "hero.composedSnapshots": "composed from {n} snapshots",
175
+
176
+ /** CopyFixPrompt 的 web 面 chrome(prompt 本身面向 agent、固定英文,不经词典)。 */
177
+ "copyFixPrompt.summary.one": "Fix prompt · {n} failure",
178
+ "copyFixPrompt.summary.other": "Fix prompt · {n} failures",
179
+ "copyFixPrompt.copy": "Copy fix prompt",
180
+
181
+ /** TraceWaterfall 的 chrome。 */
182
+ "traceWaterfall.empty": "No attempts",
183
+ "traceWaterfall.noTrace": "no trace",
184
+ "traceWaterfall.spans.one": "{n} span",
185
+ "traceWaterfall.spans.other": "{n} spans",
186
+ "traceWaterfall.failedSpans.one": "{n} failed",
187
+ "traceWaterfall.failedSpans.other": "{n} failed",
188
+
189
+ /** AttemptList 的 web 面过滤框占位符(filter 渐进增强)。 */
190
+ "attemptList.filterPlaceholder": "Filter attempts…",
191
+
149
192
  "tabs.tab": "Tab",
150
193
  } as const;
151
194
 
@@ -245,6 +288,42 @@ const zhCN: Record<ReportMessageKey, string> = {
245
288
  "delta.pairHeader": "对比 (A → B)",
246
289
  "delta.empty": "{experiments} 个实验、0 个可配对",
247
290
 
291
+ "warnings.summary.experiments.one": "{n} 个实验的数字带警告",
292
+ "warnings.summary.experiments.other": "{n} 个实验的数字带警告",
293
+ "warnings.group.unreadableSnapshot.one": "{n} 个快照被跳过",
294
+ "warnings.group.unreadableSnapshot.other": "{n} 个快照被跳过",
295
+ "warnings.details.one": "{n} 条原始警告",
296
+ "warnings.details.other": "{n} 条原始警告",
297
+ "warnings.badge.partialCoverage": "覆盖 {covered}/{total}",
298
+ "warnings.badge.staleSnapshot": "落后 {gap}",
299
+ "warnings.badge.unfinishedSnapshot": "未收尾",
300
+ "warnings.gap.second.one": "{n} 秒",
301
+ "warnings.gap.second.other": "{n} 秒",
302
+ "warnings.gap.minute.one": "{n} 分钟",
303
+ "warnings.gap.minute.other": "{n} 分钟",
304
+ "warnings.gap.hour.one": "{n} 小时",
305
+ "warnings.gap.hour.other": "{n} 小时",
306
+ "warnings.gap.day.one": "{n} 天",
307
+ "warnings.gap.day.other": "{n} 天",
308
+
309
+ "hero.lastRun": "最后运行 {time}",
310
+ "hero.noRuns": "暂无运行",
311
+ "hero.composedRuns": "由 {n} 次运行合成",
312
+ "hero.composedSnapshots": "由 {n} 份快照合成",
313
+
314
+ "copyFixPrompt.summary.one": "修复 prompt · {n} 个失败",
315
+ "copyFixPrompt.summary.other": "修复 prompt · {n} 个失败",
316
+ "copyFixPrompt.copy": "复制修复 prompt",
317
+
318
+ "traceWaterfall.empty": "没有 attempt",
319
+ "traceWaterfall.noTrace": "无 trace",
320
+ "traceWaterfall.spans.one": "{n} 个 span",
321
+ "traceWaterfall.spans.other": "{n} 个 span",
322
+ "traceWaterfall.failedSpans.one": "{n} 个失败",
323
+ "traceWaterfall.failedSpans.other": "{n} 个失败",
324
+
325
+ "attemptList.filterPlaceholder": "筛选 attempt…",
326
+
248
327
  "tabs.tab": "Tab",
249
328
  };
250
329
 
@@ -276,7 +355,10 @@ export function countText(
276
355
  | "scoreboard.unscorable"
277
356
  | "scoreboard.ignored"
278
357
  | "entityList.moreFailures"
279
- | "table.columnsHidden",
358
+ | "table.columnsHidden"
359
+ | "copyFixPrompt.summary"
360
+ | "traceWaterfall.spans"
361
+ | "traceWaterfall.failedSpans",
280
362
  n: number,
281
363
  ): string {
282
364
  return localeText(locale, `${base}.${n === 1 ? "one" : "other"}` as ReportMessageKey, { n });
@@ -53,7 +53,8 @@ export function AttemptRow({
53
53
  }): ReactElement {
54
54
  const reason = failureSummaryText(item, locale);
55
55
  return (
56
- <li className={cx("nre-attempt", `nre-attempt-${item.verdict}`)}>
56
+ // data-nre-verdict:filter 增强按 verdict 词收窄行的匹配源(✓/✗ 判定符不含词面)
57
+ <li className={cx("nre-attempt", `nre-attempt-${item.verdict}`)} data-nre-verdict={item.verdict}>
57
58
  <div className="nre-attempt-head">
58
59
  <AttemptLocatorBadge item={item} attemptHref={attemptHref} />
59
60
  <span className="nre-attempt-eval">{item.evalId}</span>
@@ -72,6 +73,7 @@ export function AttemptRow({
72
73
  export function AttemptList({
73
74
  data,
74
75
  total,
76
+ filter = false,
75
77
  attemptHref = DEFAULT_ATTEMPT_HREF,
76
78
  className,
77
79
  locale = DEFAULT_REPORT_LOCALE,
@@ -79,6 +81,8 @@ export function AttemptList({
79
81
  data: readonly AttemptListItem[];
80
82
  /** data 被 slice 时的原始数量;如实显示还剩多少条没展示。 */
81
83
  total?: number;
84
+ /** web 面加过滤输入框(按 experiment、eval、agent、verdict 或摘要文本收窄行);渐进增强,不改变数据。 */
85
+ filter?: boolean;
82
86
  attemptHref?: (locator: AttemptLocator) => string;
83
87
  className?: string;
84
88
  locale?: ReportLocale;
@@ -86,6 +90,14 @@ export function AttemptList({
86
90
  const remaining = (total ?? data.length) - data.length;
87
91
  return (
88
92
  <section className={cx("nre", "nre-attempt-list", className)}>
93
+ {filter && (
94
+ <input
95
+ className="nre-filter"
96
+ data-nre-attempt-filter=""
97
+ type="search"
98
+ placeholder={localeText(locale, "attemptList.filterPlaceholder")}
99
+ />
100
+ )}
89
101
  {data.length === 0 && <p className="nre-attempt-list-empty">{localeText(locale, "attemptList.empty")}</p>}
90
102
  <ul className="nre-attempts">
91
103
  {data.map((item) => (
@@ -0,0 +1,37 @@
1
+ // CopyFixPrompt:批量修复 prompt 的 web 面。prompt 在 resolve 阶段算好、烘进静态 HTML;
2
+ // 无 JS 时 prompt 全文在原生 <details> 折叠块里完整可读,「复制到剪贴板」是 enhance.js 的
3
+ // 增强行为(data-nre-copy),增强只加浏览行为、不改内容。failures 为 0 时零输出
4
+ // (docs/feature/reports/library/site-components.md「CopyFixPrompt」)。
5
+
6
+ import type { ReactElement } from "react";
7
+ import type { CopyFixPromptData } from "../types.ts";
8
+ import { DEFAULT_REPORT_LOCALE, countText, localeText, type ReportLocale } from "../locale.ts";
9
+ import { cx } from "./format.ts";
10
+
11
+ /**
12
+ * 批量修复 prompt(纯 web 渲染面):折叠块内完整 prompt 文本 + 复制按钮(增强层)。
13
+ * 嵌入自有 React 页面时配合 `copyFixPromptData()` 使用;failures 为 0 返回 null。
14
+ */
15
+ export function CopyFixPrompt({
16
+ data,
17
+ className,
18
+ locale = DEFAULT_REPORT_LOCALE,
19
+ }: {
20
+ data: CopyFixPromptData;
21
+ className?: string;
22
+ locale?: ReportLocale;
23
+ }): ReactElement | null {
24
+ if (data.failures === 0) return null;
25
+ return (
26
+ <details className={cx("nre", "nre-copy-fix-prompt", className)}>
27
+ <summary className="nre-copy-fix-prompt-summary">
28
+ {countText(locale, "copyFixPrompt.summary", data.failures)}
29
+ </summary>
30
+ {/* 无 JS 时按钮静默无功能(与过滤框同一约定);prompt 文本本身始终完整可读 */}
31
+ <button type="button" className="nre-copy-fix-prompt-copy" data-nre-copy={data.prompt}>
32
+ {localeText(locale, "copyFixPrompt.copy")}
33
+ </button>
34
+ <pre className="nre-copy-fix-prompt-text">{data.prompt}</pre>
35
+ </details>
36
+ );
37
+ }
@@ -0,0 +1,59 @@
1
+ // HeroCard:站点标题区的 web 面——hero 标题(h1)、运行 meta(最后运行时间按渲染 locale
2
+ // 格式化;latestStartedAt 为 null 时内置「暂无运行」文案;snapshots > 1 时标注合成来源)
3
+ // 与品牌行(等同 PoweredBy,恒含、无拆除 prop)。标题输入是站点声明与 Scope 的合成物,
4
+ // 组件只收 data 形态(docs/feature/reports/library/site-components.md「HeroCard」)。
5
+
6
+ import type { ReactElement } from "react";
7
+ import type { HeroData } from "../types.ts";
8
+ import { DEFAULT_REPORT_LOCALE, localeText, resolveLocalizedText, type LocalizedText, type ReportLocale } from "../locale.ts";
9
+ import { cx } from "./format.ts";
10
+ import { PoweredBy } from "./PoweredBy.tsx";
11
+
12
+ /** ISO 时间 → 按 locale 的「最后运行」显示(年月日 时:分);不可解析原样返回。 */
13
+ function formatLastRun(iso: string, locale: ReportLocale): string {
14
+ const date = new Date(iso);
15
+ if (Number.isNaN(date.valueOf())) return iso;
16
+ try {
17
+ return new Intl.DateTimeFormat(locale, {
18
+ year: "numeric",
19
+ month: "short",
20
+ day: "numeric",
21
+ hour: "2-digit",
22
+ minute: "2-digit",
23
+ }).format(date);
24
+ } catch {
25
+ return iso;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 站点标题区(纯 web 渲染面):`<h1>` 标题 + meta 行 + 品牌行。
31
+ * 嵌入自有 React 页面时配合 `heroData()` 使用。
32
+ */
33
+ export function HeroCard({
34
+ title,
35
+ data,
36
+ className,
37
+ locale = DEFAULT_REPORT_LOCALE,
38
+ }: {
39
+ title: LocalizedText;
40
+ data: HeroData;
41
+ className?: string;
42
+ locale?: ReportLocale;
43
+ }): ReactElement {
44
+ const meta =
45
+ data.latestStartedAt === null
46
+ ? localeText(locale, "hero.noRuns")
47
+ : [
48
+ localeText(locale, "hero.lastRun", { time: formatLastRun(data.latestStartedAt, locale) }),
49
+ ...(data.snapshots > 1 ? [localeText(locale, "hero.composedRuns", { n: data.snapshots })] : []),
50
+ ].join(" · ");
51
+ return (
52
+ <header className={cx("nre", "nre-hero", className)}>
53
+ <h1 className="nre-hero-title">{resolveLocalizedText(title, locale)}</h1>
54
+ <p className="nre-hero-meta">{meta}</p>
55
+ {/* 品牌行与 PoweredBy 同一渲染:品牌跟着组件走,不区分官方宿主与嵌入页面 */}
56
+ <PoweredBy />
57
+ </header>
58
+ );
59
+ }
@@ -0,0 +1,20 @@
1
+ // PoweredBy:唯一的品牌件的 web 面。品牌契约是「提供一个组件,不给开关」——无 props、
2
+ // 无关闭配置;不想要品牌 = 不用它(docs/feature/reports/library/site-components.md「PoweredBy」)。
3
+ // rel 只声明 noopener 以保留 Referer(默认策略只发 origin):官网统计由此得知点击来自哪个
4
+ // 报告站点;静态导出在构建期不知道自己托管在哪个域名,来源不进 URL 参数。
5
+
6
+ import type { ReactElement } from "react";
7
+
8
+ /** 品牌行的固定去处:niceeval 官网,utm 标记「来自报告的品牌行」。 */
9
+ export const POWERED_BY_HREF = "https://niceeval.com/?utm_source=report&utm_medium=powered-by";
10
+
11
+ /** 一行品牌色小字 `Powered by NiceEval`,链接官网;HeroCard 的品牌行与它同一渲染。 */
12
+ export function PoweredBy(): ReactElement {
13
+ return (
14
+ <p className="nre nre-powered-by">
15
+ <a href={POWERED_BY_HREF} target="_blank" rel="noopener">
16
+ Powered by NiceEval
17
+ </a>
18
+ </p>
19
+ );
20
+ }
@@ -0,0 +1,74 @@
1
+ // ScopeWarnings:选择警告区的 web 面。按「下一步动作」聚合(../scope-warnings.ts,
2
+ // 与 text 面共用同一聚合层):整个警告区是默认折叠的原生 <details>,<summary> 是
3
+ // 分类计数汇总行(恒可见);展开后每组组头 = 标题 + kind 徽标 + 去重后恰一条的
4
+ // 可复制命令,逐条原始 message 收进第二层 <details>(总条数 ≤ 3 默认展开)。
5
+ // 空警告集零输出,不渲染空容器
6
+ // (docs/feature/reports/library/site-components.md「ScopeWarnings」)。
7
+
8
+ import type { ReactElement } from "react";
9
+ import type { ScopeWarning } from "../types.ts";
10
+ import { DEFAULT_REPORT_LOCALE, type ReportLocale } from "../locale.ts";
11
+ import { groupScopeWarnings, warningDetailsLabel } from "../scope-warnings.ts";
12
+ import { cx } from "./format.ts";
13
+
14
+ /** 命令的可复制块:无 JS 时点击全选(user-select: all),enhance.js 在场时点击复制。 */
15
+ function CommandBlock({ command }: { command: string }): ReactElement {
16
+ return (
17
+ <code className="nre-warning-command" data-nre-copy={command}>
18
+ {command}
19
+ </code>
20
+ );
21
+ }
22
+
23
+ /**
24
+ * 选择警告区(纯 web 渲染面):按动作聚合的警告组。嵌入自有 React 页面时传
25
+ * `data={scope.warnings}`;空集返回 null,不渲染空容器。
26
+ */
27
+ export function ScopeWarnings({
28
+ data,
29
+ className,
30
+ locale = DEFAULT_REPORT_LOCALE,
31
+ }: {
32
+ data: readonly ScopeWarning[];
33
+ className?: string;
34
+ locale?: ReportLocale;
35
+ }): ReactElement | null {
36
+ if (data.length === 0) return null;
37
+ const { summary, groups, detailsOpen } = groupScopeWarnings(data, locale);
38
+ return (
39
+ <div className={cx("nre", "nre-scope-warnings", className)}>
40
+ <details className="nre-warnings">
41
+ <summary className="nre-warnings-summary">{summary}</summary>
42
+ <ul className="nre-warning-groups">
43
+ {groups.map((group, i) => (
44
+ <li key={i} className="nre-warning-group" data-category={group.category}>
45
+ <div className="nre-warning-head">
46
+ <span className="nre-warning-title">{group.title}</span>
47
+ {group.badges.map((badge, j) => (
48
+ <span key={j} className="nre-warning-badge" data-kind={badge.kind}>
49
+ {badge.text}
50
+ </span>
51
+ ))}
52
+ {group.headCommand !== null && <CommandBlock command={group.headCommand} />}
53
+ </div>
54
+ <details className="nre-warning-details" open={detailsOpen || undefined}>
55
+ <summary>{warningDetailsLabel(locale, group.warnings.length)}</summary>
56
+ <ul>
57
+ {group.warnings.map((w, j) => (
58
+ <li key={j} className="nre-warning" data-kind={w.kind}>
59
+ {w.message}
60
+ {/* 组头命令的含义是「复制即推进整组」;去重后多于一条时命令随明细逐条走 */}
61
+ {group.headCommand === null && "command" in w && w.command !== undefined && (
62
+ <CommandBlock command={w.command} />
63
+ )}
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ </details>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ </details>
72
+ </div>
73
+ );
74
+ }