niceeval 0.8.0 → 0.9.0

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 (143) 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 +2 -0
  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 +72 -6
  10. package/dist/report/components.js +159 -10
  11. package/dist/report/compute.d.ts +31 -4
  12. package/dist/report/compute.js +131 -12
  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 +66 -53
  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.js +6 -1
  38. package/dist/report/types.d.ts +45 -10
  39. package/dist/report/web.d.ts +5 -4
  40. package/dist/report/web.js +7 -22
  41. package/dist/results/select.d.ts +15 -2
  42. package/dist/results/select.js +75 -10
  43. package/dist/results/types.d.ts +26 -11
  44. package/dist/runner/fingerprint.d.ts +3 -3
  45. package/dist/runner/sandbox-selection.d.ts +12 -0
  46. package/dist/runner/types.d.ts +18 -6
  47. package/dist/sandbox/types.d.ts +12 -0
  48. package/docs-site/zh/explanation/evals.mdx +2 -1
  49. package/docs-site/zh/explanation/experiment.mdx +2 -0
  50. package/docs-site/zh/how-to/custom-reports.mdx +22 -6
  51. package/docs-site/zh/how-to/publish-report.mdx +6 -8
  52. package/docs-site/zh/how-to/viewing-results.mdx +3 -3
  53. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  54. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  55. package/docs-site/zh/reference/cli.mdx +1 -2
  56. package/docs-site/zh/reference/define-eval.mdx +8 -0
  57. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  58. package/docs-site/zh/reference/report-components.mdx +2 -2
  59. package/docs-site/zh/reference/results-data.mdx +2 -4
  60. package/package.json +2 -1
  61. package/src/agents/bub.ts +13 -1
  62. package/src/agents/claude-code.test.ts +43 -1
  63. package/src/agents/claude-code.ts +32 -14
  64. package/src/agents/codex.test.ts +168 -1
  65. package/src/agents/codex.ts +51 -15
  66. package/src/agents/mcp.ts +31 -0
  67. package/src/agents/post-setup.ts +33 -0
  68. package/src/agents/types.ts +28 -7
  69. package/src/cli.ts +9 -11
  70. package/src/context/context.ts +11 -5
  71. package/src/define.ts +3 -0
  72. package/src/i18n/en.ts +5 -2
  73. package/src/i18n/zh-CN.ts +5 -1
  74. package/src/index.ts +1 -0
  75. package/src/report/built-in/index.tsx +8 -7
  76. package/src/report/built-in/standard.tsx +59 -0
  77. package/src/report/components.tsx +231 -17
  78. package/src/report/compute.ts +146 -21
  79. package/src/report/dual-render.test.tsx +139 -12
  80. package/src/report/index.ts +20 -1
  81. package/src/report/locale.ts +83 -1
  82. package/src/report/react/AttemptList.tsx +13 -1
  83. package/src/report/react/CopyFixPrompt.tsx +37 -0
  84. package/src/report/react/HeroCard.tsx +59 -0
  85. package/src/report/react/PoweredBy.tsx +20 -0
  86. package/src/report/react/ScopeWarnings.tsx +74 -0
  87. package/src/report/react/TraceWaterfall.tsx +78 -0
  88. package/src/report/react/enhance.js +14 -0
  89. package/src/report/react/index.tsx +11 -0
  90. package/src/report/react/styles.css +187 -7
  91. package/src/report/report.test.ts +2 -20
  92. package/src/report/report.ts +97 -62
  93. package/src/report/scope-warnings.ts +155 -0
  94. package/src/report/site-components.test.tsx +526 -0
  95. package/src/report/text/faces.ts +66 -0
  96. package/src/report/tree.ts +8 -1
  97. package/src/report/types.ts +51 -11
  98. package/src/report/web.ts +7 -40
  99. package/src/results/copy.ts +15 -78
  100. package/src/results/host-equivalence.test.ts +5 -1
  101. package/src/results/open.ts +5 -4
  102. package/src/results/publish.ts +4 -146
  103. package/src/results/results.test.ts +86 -9
  104. package/src/results/select.ts +78 -10
  105. package/src/results/types.ts +27 -7
  106. package/src/runner/attempt.ts +10 -9
  107. package/src/runner/discover.test.ts +9 -1
  108. package/src/runner/discover.ts +3 -3
  109. package/src/runner/fingerprint.ts +9 -4
  110. package/src/runner/ledger.test.ts +30 -1
  111. package/src/runner/ledger.ts +26 -4
  112. package/src/runner/run.ts +5 -1
  113. package/src/runner/sandbox-selection.test.ts +131 -0
  114. package/src/runner/sandbox-selection.ts +110 -0
  115. package/src/runner/types.ts +19 -2
  116. package/src/sandbox/types.ts +6 -0
  117. package/src/show/index.ts +17 -10
  118. package/src/show/render.ts +11 -11
  119. package/src/show/report-host.test.ts +32 -15
  120. package/src/show/report-host.ts +5 -4
  121. package/src/show/show.test.ts +140 -3
  122. package/src/view/app/App.test.tsx +78 -17
  123. package/src/view/app/App.tsx +17 -78
  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/artifact-serving.test.ts +1 -1
  130. package/src/view/client-dist/app.css +1 -1
  131. package/src/view/client-dist/app.js +20 -20
  132. package/src/view/data.ts +2 -13
  133. package/src/view/index.ts +1 -12
  134. package/src/view/server.ts +0 -2
  135. package/src/view/shared/types.ts +11 -6
  136. package/src/view/site-parity.test.ts +1 -1
  137. package/src/view/site.ts +1 -1
  138. package/src/view/styles.css +6 -266
  139. package/src/view/view-report.test.ts +64 -27
  140. package/src/view/app/components/LazyArtifact.tsx +0 -51
  141. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  142. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  143. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -21,35 +21,42 @@ import {
21
21
  type TextContext,
22
22
  type WebContext,
23
23
  } from "./tree.ts";
24
- import type { ReportLocale } from "./locale.ts";
24
+ import type { LocalizedText, ReportLocale } from "./locale.ts";
25
25
  import type { AttemptLocator } from "../results/locator.ts";
26
26
  import type {
27
27
  AttemptListItem,
28
+ CopyFixPromptData,
28
29
  DeltaData,
29
- EntityListDataOptions,
30
30
  EvalListItem,
31
31
  ExperimentComparisonData,
32
32
  ExperimentListItem,
33
+ HeroData,
33
34
  LineData,
34
35
  MatrixData,
35
36
  ReportInput,
36
37
  ScatterData,
37
38
  ScopeSummaryData,
39
+ ScopeWarning,
38
40
  ScoreboardData,
39
41
  TableData,
42
+ TraceWaterfallRow,
40
43
  } from "./types.ts";
41
44
  import {
42
45
  attemptListData,
46
+ copyFixPromptData,
43
47
  deltaTableData,
44
48
  evalListData,
45
49
  experimentComparisonData,
46
50
  experimentListData,
51
+ heroData,
47
52
  metricLineData,
48
53
  metricMatrixData,
49
54
  metricScatterData,
50
55
  metricTableData,
51
56
  scopeSummaryData,
57
+ scopeWarningsData,
52
58
  scoreboardData,
59
+ traceWaterfallData,
53
60
  type DeltaTableOptions,
54
61
  type MetricLineOptions,
55
62
  type MetricMatrixOptions,
@@ -65,18 +72,26 @@ import {
65
72
  experimentComparisonText,
66
73
  experimentListText,
67
74
  barsText,
75
+ heroCardText,
68
76
  lineText,
69
77
  matrixText,
70
78
  scatterText,
79
+ scopeWarningsText,
71
80
  scoreboardText,
72
81
  scopeSummaryText,
73
82
  tableText,
83
+ traceWaterfallText,
74
84
  } from "./text/faces.ts";
75
85
  import { ScopeSummary as ScopeSummaryWeb } from "./react/ScopeSummary.tsx";
76
86
  import { ExperimentComparisonView } from "./react/ExperimentComparison.tsx";
77
87
  import { ExperimentList as ExperimentListWeb } from "./react/ExperimentList.tsx";
78
88
  import { EvalList as EvalListWeb } from "./react/EvalList.tsx";
79
89
  import { AttemptList as AttemptListWeb } from "./react/AttemptList.tsx";
90
+ import { HeroCard as HeroCardWeb } from "./react/HeroCard.tsx";
91
+ import { PoweredBy as PoweredByWeb } from "./react/PoweredBy.tsx";
92
+ import { ScopeWarnings as ScopeWarningsWeb } from "./react/ScopeWarnings.tsx";
93
+ import { CopyFixPrompt as CopyFixPromptWeb } from "./react/CopyFixPrompt.tsx";
94
+ import { TraceWaterfall as TraceWaterfallWeb } from "./react/TraceWaterfall.tsx";
80
95
  import { MetricTable as MetricTableWeb } from "./react/MetricTable.tsx";
81
96
  import { MetricMatrix as MetricMatrixWeb } from "./react/MetricMatrix.tsx";
82
97
  import { MetricBars as MetricBarsWeb } from "./react/MetricBars.tsx";
@@ -258,6 +273,55 @@ const validateAttemptListData: Validator = (data) => {
258
273
  return null;
259
274
  };
260
275
 
276
+ const validateHeroData: Validator = (data) => {
277
+ if (!isObject(data)) return "expected an object";
278
+ if (!("latestStartedAt" in data) || (data.latestStartedAt !== null && typeof data.latestStartedAt !== "string")) {
279
+ return 'missing "latestStartedAt" (string | null)';
280
+ }
281
+ if (typeof data.snapshots !== "number") return 'missing "snapshots" (number)';
282
+ return null;
283
+ };
284
+
285
+ const validateScopeWarningsData: Validator = (data) => {
286
+ if (!Array.isArray(data)) return "expected an array of ScopeWarning";
287
+ for (const item of data as unknown[]) {
288
+ if (!isObject(item) || typeof item.kind !== "string" || typeof item.message !== "string") {
289
+ return "each warning needs { kind, message, … }";
290
+ }
291
+ }
292
+ return null;
293
+ };
294
+
295
+ const validateCopyFixPromptData: Validator = (data) => {
296
+ if (!isObject(data)) return "expected an object";
297
+ if (typeof data.prompt !== "string") return 'missing "prompt" (string)';
298
+ if (typeof data.failures !== "number") return 'missing "failures" (number)';
299
+ return null;
300
+ };
301
+
302
+ const validateTraceWaterfallData: Validator = (data) => {
303
+ if (!Array.isArray(data)) return "expected an array of TraceWaterfallRow";
304
+ for (const row of data as unknown[]) {
305
+ if (
306
+ !isObject(row) ||
307
+ typeof row.experimentId !== "string" ||
308
+ typeof row.evalId !== "string" ||
309
+ typeof row.locator !== "string" ||
310
+ !("durationMs" in row) ||
311
+ (row.durationMs !== null && typeof row.durationMs !== "number") ||
312
+ !Array.isArray(row.spans)
313
+ ) {
314
+ return "each row needs { experimentId, evalId, locator, durationMs: number | null, spans }";
315
+ }
316
+ for (const span of row.spans as unknown[]) {
317
+ if (!isObject(span) || typeof span.name !== "string" || typeof span.startOffsetMs !== "number") {
318
+ return "each span needs { name, kind, startOffsetMs, durationMs, failed }";
319
+ }
320
+ }
321
+ }
322
+ return null;
323
+ };
324
+
261
325
  // ───────────────────────── spec / data 双形态的通用装配 ─────────────────────────
262
326
 
263
327
  interface DataComponentDef<Data, Options, Presentation> {
@@ -400,7 +464,7 @@ interface EntityListChrome extends ChromeProps {
400
464
 
401
465
  export type ExperimentListProps = DataProps<
402
466
  readonly ExperimentListItem[],
403
- EntityListDataOptions,
467
+ Record<never, never>,
404
468
  EntityListChrome & {
405
469
  /** web 面在比较表前显示实验过滤框;text 面忽略。 */
406
470
  filter?: boolean;
@@ -415,14 +479,14 @@ export type ExperimentListProps = DataProps<
415
479
  /** 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。 */
416
480
  export const ExperimentList = makeDataComponent<
417
481
  readonly ExperimentListItem[],
418
- EntityListDataOptions,
482
+ Record<never, never>,
419
483
  EntityListChrome & { filter?: boolean; relativeTo?: string }
420
484
  >({
421
485
  name: "ExperimentList",
422
486
  dataFnName: "experimentListData",
423
487
  shapeName: "ExperimentListItem[]",
424
- dataFn: (input, options) => experimentListData(input, options),
425
- specKeys: ["redact"],
488
+ dataFn: (input) => experimentListData(input),
489
+ specKeys: [],
426
490
  validate: validateExperimentListData,
427
491
  web: (props, ctx) => (
428
492
  <ExperimentListWeb
@@ -437,15 +501,15 @@ export const ExperimentList = makeDataComponent<
437
501
  text: (props, ctx) => experimentListText(props.data, ctx, props.relativeTo),
438
502
  }) as unknown as ReportComponent<ExperimentListProps>;
439
503
 
440
- export type EvalListProps = DataProps<readonly EvalListItem[], EntityListDataOptions, EntityListChrome>;
504
+ export type EvalListProps = DataProps<readonly EvalListItem[], Record<never, never>, EntityListChrome>;
441
505
 
442
506
  /** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
443
- export const EvalList = makeDataComponent<readonly EvalListItem[], EntityListDataOptions, EntityListChrome>({
507
+ export const EvalList = makeDataComponent<readonly EvalListItem[], Record<never, never>, EntityListChrome>({
444
508
  name: "EvalList",
445
509
  dataFnName: "evalListData",
446
510
  shapeName: "EvalListItem[]",
447
- dataFn: (input, options) => evalListData(input, options),
448
- specKeys: ["redact"],
511
+ dataFn: (input) => evalListData(input),
512
+ specKeys: [],
449
513
  validate: validateEvalListData,
450
514
  web: (props, ctx) => (
451
515
  <EvalListWeb
@@ -460,29 +524,32 @@ export const EvalList = makeDataComponent<readonly EvalListItem[], EntityListDat
460
524
 
461
525
  export type AttemptListProps = DataProps<
462
526
  readonly AttemptListItem[],
463
- EntityListDataOptions,
527
+ Record<never, never>,
464
528
  EntityListChrome & {
465
529
  /** 过滤 / 截断前的总数;省略时等于 data 长度。 */
466
530
  total?: number;
531
+ /** web 面加过滤输入框(按 experiment、eval、agent、verdict 或摘要文本收窄行);渐进增强,不改变数据与 text 面。 */
532
+ filter?: boolean;
467
533
  }
468
534
  >;
469
535
 
470
536
  /** Attempt 列表:实体列表的叶子层,每项一次 attempt 的判定、单行摘要与 locator。 */
471
537
  export const AttemptList = makeDataComponent<
472
538
  readonly AttemptListItem[],
473
- EntityListDataOptions,
474
- EntityListChrome & { total?: number }
539
+ Record<never, never>,
540
+ EntityListChrome & { total?: number; filter?: boolean }
475
541
  >({
476
542
  name: "AttemptList",
477
543
  dataFnName: "attemptListData",
478
544
  shapeName: "AttemptListItem[]",
479
- dataFn: (input, options) => attemptListData(input, options),
480
- specKeys: ["redact"],
545
+ dataFn: (input) => attemptListData(input),
546
+ specKeys: [],
481
547
  validate: validateAttemptListData,
482
548
  web: (props, ctx) => (
483
549
  <AttemptListWeb
484
550
  data={props.data}
485
551
  total={props.total}
552
+ filter={props.filter}
486
553
  locale={props.locale ?? ctx.locale}
487
554
  attemptHref={hrefOf(props, ctx) ?? ctx.attemptHref}
488
555
  className={props.className}
@@ -498,7 +565,6 @@ export interface FailureListProps {
498
565
  limit?: number;
499
566
  /** 默认宿主注入的 Scope。 */
500
567
  input?: ReportInput;
501
- redact?: (text: string) => string;
502
568
  attemptHref?: (locator: AttemptLocator) => string;
503
569
  locale?: ReportLocale;
504
570
  className?: string;
@@ -512,7 +578,7 @@ export interface FailureListProps {
512
578
  */
513
579
  export const FailureList = defineComponent<FailureListProps>(async (props, ctx) => {
514
580
  const input = props.input ?? ctx.scope;
515
- const all = await attemptListData(input, props.redact !== undefined ? { redact: props.redact } : undefined);
581
+ const all = await attemptListData(input);
516
582
  // attempt 开始时间不在列表条目里(它不是列表展示字段);从同一 input 的读取面按 locator 对回。
517
583
  const startedAtByLocator = new Map<string, string>();
518
584
  for (const item of collectItems(resolveInput(input).snapshots)) {
@@ -539,6 +605,154 @@ export const FailureList = defineComponent<FailureListProps>(async (props, ctx)
539
605
  });
540
606
  FailureList.displayName = "FailureList";
541
607
 
608
+ // ───────────────────────── 站点组件(Hero / PoweredBy / ScopeWarnings / CopyFixPrompt / TraceWaterfall)─────────────────────────
609
+
610
+ /** `Hero` 的 props:标题缺省取 `ctx.report.title`(回退链后的站点标题)。 */
611
+ export interface HeroProps {
612
+ /** 覆盖标题;省略时取 ctx.report.title(回退链后的站点标题)。 */
613
+ title?: LocalizedText;
614
+ className?: string;
615
+ }
616
+
617
+ /** HeroCard 的 data 校验入口(它不经 makeDataComponent,数据形态是唯一形态)。 */
618
+ const assertHeroData = (data: unknown): HeroData => {
619
+ const problem = validateHeroData(data);
620
+ if (problem !== null) throw dataShapeError("HeroCard", "heroData", "HeroData", problem);
621
+ return data as HeroData;
622
+ };
623
+
624
+ /**
625
+ * `HeroCard`:Hero 的渲染件,双面组件,只收 data 形态——标题输入是站点声明与 Scope 的
626
+ * 合成物,没有单独的 spec 等价形。web 面渲染 hero 标题(h1)、按渲染 locale 格式化的运行
627
+ * meta(latestStartedAt 为 null 时内置「暂无运行」文案)与品牌行(等同 PoweredBy,恒含、
628
+ * 无拆除 prop);text 面输出标题行与 meta 行,不含品牌行
629
+ * (docs/feature/reports/library/site-components.md「HeroCard」)。
630
+ */
631
+ export const HeroCard = defineComponent<HeroCardProps>({
632
+ web: (props, ctx) => {
633
+ assertHeroData(props.data);
634
+ return <HeroCardWeb title={props.title} data={props.data} className={props.className} locale={ctx.locale} />;
635
+ },
636
+ text: (props, ctx) => {
637
+ assertHeroData(props.data);
638
+ return heroCardText(props.title, props.data, ctx);
639
+ },
640
+ });
641
+ HeroCard.displayName = "HeroCard";
642
+
643
+ /** `HeroCard` 的 props:标题 + `heroData()` 的产物,只有 data 形态。 */
644
+ export interface HeroCardProps {
645
+ title: LocalizedText;
646
+ data: HeroData;
647
+ className?: string;
648
+ }
649
+
650
+ /**
651
+ * `Hero`:页首的站点标题区——标题、最后运行时间、快照合成来源,恒含品牌行。官方组合组件,
652
+ * 与手写 `<HeroCard title={title ?? ctx.report.title} data={await heroData(ctx.scope)} />`
653
+ * 严格等价、没有私有能力;读 `ctx.report` 意味着输出跟随站点,要站点无关的标题区直接用
654
+ * `HeroCard` 显式传值(docs/feature/reports/library/site-components.md「Hero」)。
655
+ */
656
+ export const Hero = defineComponent<HeroProps>(async ({ title, className }, ctx) => (
657
+ <HeroCard title={title ?? ctx.report.title} data={await heroData(ctx.scope)} className={className} />
658
+ ));
659
+ Hero.displayName = "Hero";
660
+
661
+ /**
662
+ * `PoweredBy`:唯一的品牌件,无 props 双面组件。web 面渲染指向 niceeval 官网的一行品牌色
663
+ * 小字(`utm_source=report&utm_medium=powered-by`,`rel` 仅 `noopener` 以保留 Referer);
664
+ * text 面零输出。没有任何配置——品牌契约是「提供一个组件,不给开关」:不想要品牌就不用
665
+ * 这些组件、自己写替代组件(docs/feature/reports/library/site-components.md「PoweredBy」)。
666
+ */
667
+ export const PoweredBy = defineComponent<Record<never, never>>({
668
+ web: () => <PoweredByWeb />,
669
+ text: () => "",
670
+ });
671
+ PoweredBy.displayName = "PoweredBy";
672
+
673
+ /** `ScopeWarnings` 的 props:spec 形态取宿主 Scope 的 warnings,data 形态收 `ScopeWarning[]`。 */
674
+ export type ScopeWarningsProps = DataProps<readonly ScopeWarning[], Record<never, never>, ChromeProps>;
675
+
676
+ /**
677
+ * `ScopeWarnings`:选择警告区,警告的唯一呈现组件。把 Scope 携带的 `ScopeWarning[]`
678
+ * 按「下一步动作」聚合渲染(带 experimentId 的按实验聚合、非实验作用域按 kind 聚合;
679
+ * integrity 组在前);web 面组头带去重后的可复制命令、明细收原生 `<details>`(总条数 ≤ 3
680
+ * 默认展开),text 面同构但不折叠。空警告集与裸 `Snapshot[]` 输入两面零输出
681
+ * (docs/feature/reports/library/site-components.md「ScopeWarnings」)。
682
+ */
683
+ export const ScopeWarnings = makeDataComponent<readonly ScopeWarning[], Record<never, never>, ChromeProps>({
684
+ name: "ScopeWarnings",
685
+ dataFnName: "scopeWarningsData",
686
+ shapeName: "ScopeWarning[]",
687
+ dataFn: (input) => scopeWarningsData(input),
688
+ specKeys: [],
689
+ validate: validateScopeWarningsData,
690
+ web: (props, ctx) =>
691
+ props.data.length === 0 ? null : (
692
+ <ScopeWarningsWeb data={props.data} locale={props.locale ?? ctx.locale} className={props.className} />
693
+ ),
694
+ text: (props, ctx) => scopeWarningsText(props.data, ctx),
695
+ }) as unknown as ReportComponent<ScopeWarningsProps>;
696
+
697
+ /** `CopyFixPrompt` 的 props:spec 形态无选项,data 形态收 `copyFixPromptData()` 的产物。 */
698
+ export type CopyFixPromptProps = DataProps<CopyFixPromptData, Record<never, never>, ChromeProps>;
699
+
700
+ /**
701
+ * `CopyFixPrompt`:把当前范围的全部失败整理成一段可交给 coding agent 的修复 prompt。
702
+ * prompt 在 resolve 阶段算好、烘进静态 HTML,无 JS 时在折叠块里完整可读,「复制」是增强层
703
+ * 行为;`failures` 为 0 时两面零输出;text 面恒零输出——终端里的等价能力是 `show` 的
704
+ * attempt 下钻命令本身(docs/feature/reports/library/site-components.md「CopyFixPrompt」)。
705
+ */
706
+ export const CopyFixPrompt = makeDataComponent<CopyFixPromptData, Record<never, never>, ChromeProps>({
707
+ name: "CopyFixPrompt",
708
+ dataFnName: "copyFixPromptData",
709
+ shapeName: "CopyFixPromptData",
710
+ dataFn: (input) => copyFixPromptData(input),
711
+ specKeys: [],
712
+ validate: validateCopyFixPromptData,
713
+ web: (props, ctx) =>
714
+ props.data.failures === 0 ? null : (
715
+ <CopyFixPromptWeb data={props.data} locale={props.locale ?? ctx.locale} className={props.className} />
716
+ ),
717
+ text: () => "",
718
+ }) as unknown as ReportComponent<CopyFixPromptProps>;
719
+
720
+ /** `TraceWaterfall` 的 props:spec 形态无选项,data 形态收 `traceWaterfallData()` 的产物。 */
721
+ export type TraceWaterfallProps = DataProps<
722
+ readonly TraceWaterfallRow[],
723
+ Record<never, never>,
724
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
725
+ >;
726
+
727
+ /**
728
+ * `TraceWaterfall`:每个 attempt 一行的执行时间瀑布,用 canonical OTel 字段显示被测 agent
729
+ * 的原始 span(agent / model / tool)。web 面静态渲染顶层 span 分解条(失败 span 带失败
730
+ * 标记),行链接 attempt 详情;text 面每 attempt 一行(locator、总耗时、span 计数与失败
731
+ * 标记)+ 可复制的 `--timing` 下钻命令。trace 缺失的行照常出现并如实显示缺失;runner
732
+ * 生命周期节点不进瀑布(docs/feature/reports/library/site-components.md「TraceWaterfall」)。
733
+ */
734
+ export const TraceWaterfall = makeDataComponent<
735
+ readonly TraceWaterfallRow[],
736
+ Record<never, never>,
737
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
738
+ >({
739
+ name: "TraceWaterfall",
740
+ dataFnName: "traceWaterfallData",
741
+ shapeName: "TraceWaterfallRow[]",
742
+ dataFn: (input) => traceWaterfallData(input),
743
+ specKeys: [],
744
+ validate: validateTraceWaterfallData,
745
+ web: (props, ctx) => (
746
+ <TraceWaterfallWeb
747
+ data={props.data}
748
+ attemptHref={hrefOf(props, ctx) ?? ctx.attemptHref}
749
+ locale={props.locale ?? ctx.locale}
750
+ className={props.className}
751
+ />
752
+ ),
753
+ text: (props, ctx) => traceWaterfallText(props.data, ctx),
754
+ }) as unknown as ReportComponent<TraceWaterfallProps>;
755
+
542
756
  // ───────────────────────── 指标组件 ─────────────────────────
543
757
 
544
758
  export type MetricTableProps = DataProps<
@@ -14,16 +14,17 @@
14
14
  import type {
15
15
  AttemptListItem,
16
16
  AttemptLocator,
17
+ CopyFixPromptData,
17
18
  DeltaData,
18
19
  DeltaPair,
19
20
  DimensionInput,
20
- EntityListDataOptions,
21
21
  EvalListItem,
22
22
  ExperimentComparisonData,
23
23
  ExperimentComparisonGroupData,
24
24
  ExperimentListEvalRow,
25
25
  ExperimentListItem,
26
26
  FlagPairs,
27
+ HeroData,
27
28
  LineData,
28
29
  MatrixData,
29
30
  Metric,
@@ -32,11 +33,14 @@ import type {
32
33
  ReportInput,
33
34
  ScatterData,
34
35
  ScopeSummaryData,
36
+ ScopeWarning,
35
37
  ScoreboardData,
36
38
  TableData,
39
+ TraceSpanSummary,
40
+ TraceWaterfallRow,
37
41
  VerdictTally,
38
42
  } from "./types.ts";
39
- import type { EvalResult, JsonValue } from "../types.ts";
43
+ import type { EvalResult, JsonValue, TraceSpan } from "../types.ts";
40
44
  import type { Snapshot } from "../results/types.ts";
41
45
  import { comparabilityConfigOf, deepEqualJson } from "../results/select.ts";
42
46
  import { evalLevelStats, foldEvalVerdict } from "../shared/verdict.ts";
@@ -199,10 +203,8 @@ function failureSummaryOf(result: EvalResult): { summary: string | null; more: n
199
203
  return { summary: null, more: 0 };
200
204
  }
201
205
 
202
- const identityRedact = (text: string): string => text;
203
-
204
206
  /** AttemptList / ExperimentList / EvalList 共用的叶子构造:一个 Item → 一个 AttemptListItem。 */
205
- async function attemptListItemOf(item: Item, redact: (text: string) => string): Promise<AttemptListItem> {
207
+ async function attemptListItemOf(item: Item): Promise<AttemptListItem> {
206
208
  const result = item.attempt.result;
207
209
  const { summary, more } = failureSummaryOf(result);
208
210
  return {
@@ -211,7 +213,7 @@ async function attemptListItemOf(item: Item, redact: (text: string) => string):
211
213
  attempt: result.attempt,
212
214
  agent: result.agent,
213
215
  verdict: result.verdict,
214
- failureSummary: summary === null ? null : redact(summary),
216
+ failureSummary: summary,
215
217
  moreFailures: more,
216
218
  examScore: await computeCell(examScore, [item]),
217
219
  durationMs: result.durationMs,
@@ -221,20 +223,15 @@ async function attemptListItemOf(item: Item, redact: (text: string) => string):
221
223
  }
222
224
 
223
225
  /** `attemptListData(input)`:每个 Attempt 一项,顺序取自 Scope 展平顺序(不重排)。 */
224
- export async function attemptListData(
225
- input: ReportInput,
226
- options?: EntityListDataOptions,
227
- ): Promise<AttemptListItem[]> {
226
+ export async function attemptListData(input: ReportInput): Promise<AttemptListItem[]> {
228
227
  const { snapshots } = resolveInput(input);
229
- const redact = options?.redact ?? identityRedact;
230
228
  const items = collectItems(snapshots);
231
- return Promise.all(items.map((item) => attemptListItemOf(item, redact)));
229
+ return Promise.all(items.map((item) => attemptListItemOf(item)));
232
230
  }
233
231
 
234
232
  /** `evalListData(input)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
235
- export async function evalListData(input: ReportInput, options?: EntityListDataOptions): Promise<EvalListItem[]> {
233
+ export async function evalListData(input: ReportInput): Promise<EvalListItem[]> {
236
234
  const { snapshots } = resolveInput(input);
237
- const redact = options?.redact ?? identityRedact;
238
235
  const items = collectItems(snapshots);
239
236
  const groups = new Map<string, Item[]>();
240
237
  for (const item of items) {
@@ -247,7 +244,7 @@ export async function evalListData(input: ReportInput, options?: EntityListDataO
247
244
  for (const group of groups.values()) {
248
245
  const sorted = [...group].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
249
246
  const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
250
- const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item, redact)));
247
+ const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item)));
251
248
  out.push({
252
249
  experimentId: experimentIdOf(sorted[0]!),
253
250
  evalId: evalIdOf(sorted[0]!),
@@ -269,12 +266,8 @@ export async function evalListData(input: ReportInput, options?: EntityListDataO
269
266
  * Snapshot[] 时若同一 experiment 混入不一致的可比性配置,按完整用户反馈失败并指引——
270
267
  * 看跨配置演化用 snapshot 维度或 MetricLine,不把两套配置拼成一行冒充单一配置。
271
268
  */
272
- export async function experimentListData(
273
- input: ReportInput,
274
- options?: EntityListDataOptions,
275
- ): Promise<ExperimentListItem[]> {
269
+ export async function experimentListData(input: ReportInput): Promise<ExperimentListItem[]> {
276
270
  const { snapshots } = resolveInput(input);
277
- const redact = options?.redact ?? identityRedact;
278
271
 
279
272
  // 可比性配置单义检查:同一 experiment 的输入快照必须共享一套可比性配置。
280
273
  const configByExperiment = new Map<string, { snapshot: Snapshot; config: unknown }>();
@@ -304,7 +297,7 @@ export async function experimentListData(
304
297
  for (const [evalId, evalItems] of evalGroups) {
305
298
  const sorted = [...evalItems].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
306
299
  const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
307
- const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item, redact)));
300
+ const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item)));
308
301
  evalRows.push({
309
302
  evalId,
310
303
  verdict,
@@ -981,3 +974,135 @@ function deltaOutcome(metric: Metric, delta: number | null): "improved" | "regre
981
974
  const better = metric.better ?? "higher";
982
975
  return (delta > 0) === (better === "higher") ? "improved" : "regressed";
983
976
  }
977
+
978
+ // ───────────────────────── 站点组件的计算函数(hero / warnings / fix prompt / trace)─────────────────────────
979
+
980
+ /**
981
+ * `heroData(input)`:站点标题区的运行 meta——`latestStartedAt` 取范围内最新快照的开始时间
982
+ * (空范围为 null,不编造当前时间),`snapshots` 计贡献当前水位的快照数
983
+ * (docs/feature/reports/library/site-components.md「HeroCard」)。
984
+ */
985
+ export async function heroData(input: ReportInput): Promise<HeroData> {
986
+ const { snapshots } = resolveInput(input);
987
+ let latest: string | null = null;
988
+ for (const snapshot of snapshots) {
989
+ if (latest === null || snapshot.startedAt > latest) latest = snapshot.startedAt;
990
+ }
991
+ return { latestStartedAt: latest, snapshots: snapshots.length };
992
+ }
993
+
994
+ /**
995
+ * `scopeWarningsData(input)`:Scope 携带的挑选警告原样透出;`input` 是裸 `Snapshot[]` 时
996
+ * 没有挑选过程、没有警告,返回空数组,也如实
997
+ * (docs/feature/reports/library/site-components.md「ScopeWarnings」)。
998
+ */
999
+ export async function scopeWarningsData(input: ReportInput): Promise<readonly ScopeWarning[]> {
1000
+ return resolveInput(input).warnings;
1001
+ }
1002
+
1003
+ /**
1004
+ * `copyFixPromptData(input)`:把范围内全部失败(verdict 为 failed / errored 的 attempt)
1005
+ * 整理成一段可交给 coding agent 的修复 prompt——逐失败含 eval id、主失败摘要与 attempt
1006
+ * 下钻命令(`niceeval show @<locator>`)。prompt 面向 agent,固定英文
1007
+ * (docs/feature/reports/library/site-components.md「CopyFixPrompt」)。
1008
+ */
1009
+ export async function copyFixPromptData(input: ReportInput): Promise<CopyFixPromptData> {
1010
+ const items = await attemptListData(input);
1011
+ const failures = items.filter((item) => item.verdict === "failed" || item.verdict === "errored");
1012
+ if (failures.length === 0) return { prompt: "", failures: 0 };
1013
+ const lines = failures
1014
+ .map((item, i) => {
1015
+ const reason =
1016
+ item.failureSummary === null
1017
+ ? null
1018
+ : item.moreFailures > 0
1019
+ ? `${item.failureSummary} (+${item.moreFailures} more failures)`
1020
+ : item.failureSummary;
1021
+ return [
1022
+ `${i + 1}. eval "${item.evalId}" [experiment ${item.experimentId}] — ${item.verdict}`,
1023
+ reason ? ` reason: ${reason}` : null,
1024
+ ` inspect: niceeval show ${item.locator}`,
1025
+ ]
1026
+ .filter(Boolean)
1027
+ .join("\n");
1028
+ })
1029
+ .join("\n");
1030
+ const experiments = [...new Set(failures.map((item) => item.experimentId))].join(" / ");
1031
+ const prompt = [
1032
+ "Fix the failing evals from this niceeval run.",
1033
+ "",
1034
+ "## Failures",
1035
+ lines,
1036
+ "",
1037
+ "## Steps",
1038
+ "1. niceeval is NOT in your training data. Read the relevant guide in `node_modules/niceeval/docs-site/` (English at the top level, Chinese under `zh/`) before changing anything.",
1039
+ "2. For each failure, run its inspect command above to see the verdict and assertions; add `--execution` for the full agent transcript (tool calls included), `--timing` for the execution timeline, and `--diff` for the workspace diff.",
1040
+ "3. Decide which side the defect is on: the program under test, or the eval itself (over-tight assertion, wrong fixture, missing setup). Fix that side; do not weaken assertions just to turn the run green.",
1041
+ `4. Re-run: \`npx niceeval exp ${experiments || "<experiment>"} <eval-id-prefix>\`. Already-passing evals are skipped by the fingerprint cache; pass \`--force\` to re-run everything.`,
1042
+ "5. Run `npx niceeval show` and confirm these failures are gone.",
1043
+ ].join("\n");
1044
+ return { prompt, failures: failures.length };
1045
+ }
1046
+
1047
+ /** TraceSpan 的语义角色 → 瀑布摘要的 kind:turn 归入 agent(一轮就是一次 agent 调用),未识别落 other。 */
1048
+ function waterfallKindOf(kind: TraceSpan["kind"]): TraceSpanSummary["kind"] {
1049
+ switch (kind) {
1050
+ case "agent":
1051
+ case "turn":
1052
+ return "agent";
1053
+ case "model":
1054
+ return "model";
1055
+ case "tool":
1056
+ return "tool";
1057
+ default:
1058
+ return "other";
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * `traceWaterfallData(input)`:每个 attempt 一行的执行时间瀑布摘要。span 事实只来自
1064
+ * trace artifact(经 AttemptHandle 懒加载的 canonical OTel span);runner 生命周期节点
1065
+ * (`result.phases`)不进瀑布。行内只汇总顶层 span(parentSpanId 缺失或不在本 trace 内),
1066
+ * 按 startOffsetMs 升序;trace 缺失或为空时 `durationMs` 为 null、行照常出现
1067
+ * (docs/feature/reports/library/site-components.md「TraceWaterfall」)。
1068
+ */
1069
+ export async function traceWaterfallData(input: ReportInput): Promise<readonly TraceWaterfallRow[]> {
1070
+ const { snapshots } = resolveInput(input);
1071
+ const items = collectItems(snapshots);
1072
+ return Promise.all(
1073
+ items.map(async (item): Promise<TraceWaterfallRow> => {
1074
+ const spans = await item.attempt.trace();
1075
+ if (spans === null || spans.length === 0) {
1076
+ return {
1077
+ experimentId: experimentIdOf(item),
1078
+ evalId: evalIdOf(item),
1079
+ locator: locatorOf(item),
1080
+ durationMs: null,
1081
+ spans: [],
1082
+ };
1083
+ }
1084
+ const t0 = Math.min(...spans.map((s) => s.startMs));
1085
+ const t1 = Math.max(...spans.map((s) => s.endMs));
1086
+ const ids = new Set(spans.map((s) => s.spanId));
1087
+ const topLevel = spans.filter((s) => s.parentSpanId === undefined || !ids.has(s.parentSpanId));
1088
+ const summaries = topLevel
1089
+ .map(
1090
+ (s): TraceSpanSummary => ({
1091
+ name: s.name,
1092
+ kind: waterfallKindOf(s.kind),
1093
+ startOffsetMs: s.startMs - t0,
1094
+ durationMs: s.endMs - s.startMs,
1095
+ failed: s.status === "error",
1096
+ }),
1097
+ )
1098
+ .sort((a, b) => a.startOffsetMs - b.startOffsetMs);
1099
+ return {
1100
+ experimentId: experimentIdOf(item),
1101
+ evalId: evalIdOf(item),
1102
+ locator: locatorOf(item),
1103
+ durationMs: Math.max(0, t1 - t0),
1104
+ spans: summaries,
1105
+ };
1106
+ }),
1107
+ );
1108
+ }