niceeval 0.4.6 → 0.5.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 (174) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +6 -6
  2. package/docs-site/zh/concepts/experiment.mdx +7 -7
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/agent-feedback-loop.mdx +64 -21
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +243 -0
  8. package/docs-site/zh/guides/experiments.mdx +3 -3
  9. package/docs-site/zh/guides/report-components.mdx +252 -0
  10. package/docs-site/zh/guides/reporters.mdx +1 -1
  11. package/docs-site/zh/guides/results-data.mdx +224 -0
  12. package/docs-site/zh/guides/runner.mdx +1 -1
  13. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  14. package/docs-site/zh/guides/viewing-results.mdx +147 -77
  15. package/docs-site/zh/guides/write-experiment.mdx +9 -9
  16. package/docs-site/zh/guides/write-send.mdx +6 -6
  17. package/docs-site/zh/index.mdx +1 -1
  18. package/docs-site/zh/introduction.mdx +1 -1
  19. package/docs-site/zh/reference/cli.mdx +11 -3
  20. package/docs-site/zh/reference/define-agent.mdx +5 -5
  21. package/docs-site/zh/reference/define-config.mdx +1 -1
  22. package/docs-site/zh/reference/define-eval.mdx +3 -3
  23. package/package.json +3 -2
  24. package/src/agents/ai-sdk.test.ts +1 -1
  25. package/src/agents/ai-sdk.ts +2 -2
  26. package/src/agents/claude-code.ts +1 -1
  27. package/src/agents/codex.ts +2 -2
  28. package/src/agents/streaming.test.ts +1 -1
  29. package/src/agents/types.ts +4 -4
  30. package/src/agents/ui-message-stream.test.ts +1 -1
  31. package/src/cli.ts +120 -18
  32. package/src/context/context.test.ts +1 -1
  33. package/src/context/context.ts +3 -3
  34. package/src/context/session.ts +2 -2
  35. package/src/context/types.ts +2 -2
  36. package/src/i18n/en.ts +29 -7
  37. package/src/i18n/zh-CN.ts +24 -2
  38. package/src/report/aggregate.ts +79 -50
  39. package/src/report/components.tsx +190 -0
  40. package/src/report/compute.ts +149 -84
  41. package/src/report/default-report.tsx +222 -0
  42. package/src/report/dual-face.test.tsx +527 -0
  43. package/src/report/format.ts +29 -0
  44. package/src/report/index.ts +79 -24
  45. package/src/report/load.ts +67 -0
  46. package/src/report/metrics.ts +2 -1
  47. package/src/report/param.ts +18 -0
  48. package/src/report/primitives.tsx +117 -0
  49. package/src/report/react/CaseList.tsx +2 -2
  50. package/src/report/react/DeltaTable.tsx +2 -2
  51. package/src/report/react/MetricBars.tsx +109 -0
  52. package/src/report/react/MetricLine.tsx +200 -0
  53. package/src/report/react/MetricMatrix.tsx +1 -1
  54. package/src/report/react/MetricScatter.tsx +1 -1
  55. package/src/report/react/MetricTable.tsx +1 -1
  56. package/src/report/react/RunOverview.tsx +4 -4
  57. package/src/report/react/Scoreboard.tsx +3 -3
  58. package/src/report/react/cell.tsx +1 -1
  59. package/src/report/react/fixtures.ts +98 -36
  60. package/src/report/react/format.ts +3 -28
  61. package/src/report/react/index.tsx +41 -24
  62. package/src/report/react/render.test.tsx +8 -8
  63. package/src/report/react/styles.css +77 -0
  64. package/src/report/report.test.ts +248 -128
  65. package/src/report/report.ts +69 -0
  66. package/src/report/text/faces.ts +356 -0
  67. package/src/report/text/layout.ts +125 -0
  68. package/src/report/text/plot.ts +127 -0
  69. package/src/report/tree.ts +220 -0
  70. package/src/report/types.ts +75 -27
  71. package/src/report/web.ts +40 -0
  72. package/src/results/copy.ts +95 -41
  73. package/src/results/format.ts +12 -11
  74. package/src/results/index.ts +34 -17
  75. package/src/results/open.ts +201 -81
  76. package/src/results/results.test.ts +595 -191
  77. package/src/results/select.ts +107 -55
  78. package/src/results/types.ts +137 -45
  79. package/src/results/writer.ts +258 -0
  80. package/src/runner/attempt.ts +3 -3
  81. package/src/runner/fingerprint.ts +1 -1
  82. package/src/runner/reporters/artifacts.ts +38 -78
  83. package/src/runner/reporters/braintrust.test.ts +2 -2
  84. package/src/runner/reporters/braintrust.ts +3 -3
  85. package/src/runner/run.ts +1 -1
  86. package/src/runner/types.ts +18 -12
  87. package/src/show/compose.ts +215 -0
  88. package/src/show/index.ts +263 -0
  89. package/src/show/render.ts +456 -0
  90. package/src/show/show.test.ts +505 -0
  91. package/src/view/app/App.tsx +85 -24
  92. package/src/view/app/components/CostScoreChart.tsx +29 -10
  93. package/src/view/app/components/ExperimentTable.tsx +27 -9
  94. package/src/view/app/components/GroupSelector.tsx +1 -1
  95. package/src/view/app/i18n.ts +18 -3
  96. package/src/view/app/lib/attempt-route.test.ts +9 -9
  97. package/src/view/app/lib/attempt-route.ts +5 -5
  98. package/src/view/app/lib/outcome.ts +1 -3
  99. package/src/view/app/lib/rows.ts +95 -15
  100. package/src/view/app/main.tsx +18 -6
  101. package/src/view/app/pages/RunsPage.tsx +4 -7
  102. package/src/view/app/pages/TracesPage.tsx +3 -8
  103. package/src/view/app/types.ts +50 -6
  104. package/src/view/client-dist/app.css +1 -1
  105. package/src/view/client-dist/app.js +20 -20
  106. package/src/view/data.test.ts +254 -0
  107. package/src/view/data.ts +336 -0
  108. package/src/view/index.ts +64 -10
  109. package/src/view/server.ts +31 -9
  110. package/src/view/shared/types.ts +47 -41
  111. package/src/view/styles.css +5 -0
  112. package/src/view/template.html +1 -0
  113. package/src/view/view-report.test.ts +308 -0
  114. package/docs-site/.mintignore +0 -13
  115. package/docs-site/AGENTS.md +0 -46
  116. package/docs-site/concepts/adapter.mdx +0 -287
  117. package/docs-site/concepts/assert.mdx +0 -243
  118. package/docs-site/concepts/drive.mdx +0 -118
  119. package/docs-site/concepts/evals.mdx +0 -108
  120. package/docs-site/concepts/experiment.mdx +0 -37
  121. package/docs-site/concepts/hitl.mdx +0 -83
  122. package/docs-site/concepts/judge.mdx +0 -129
  123. package/docs-site/concepts/overview.mdx +0 -98
  124. package/docs-site/concepts/tier.mdx +0 -42
  125. package/docs-site/docs-ref/00-index.md +0 -23
  126. package/docs-site/docs-ref/01-page-types.md +0 -43
  127. package/docs-site/docs-ref/03-style-rules.md +0 -45
  128. package/docs-site/docs-ref/04-code-examples.md +0 -37
  129. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  130. package/docs-site/docs-ref/06-checklists.md +0 -53
  131. package/docs-site/docs.json +0 -256
  132. package/docs-site/example/ai-agent-application.mdx +0 -148
  133. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  134. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  135. package/docs-site/example/showcase.mdx +0 -39
  136. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  137. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  138. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  139. package/docs-site/example/tier1-langgraph.mdx +0 -119
  140. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  141. package/docs-site/favicon.svg +0 -5
  142. package/docs-site/github-diff.css +0 -135
  143. package/docs-site/github-diff.js +0 -11
  144. package/docs-site/guides/authoring.mdx +0 -129
  145. package/docs-site/guides/ci-integration.mdx +0 -97
  146. package/docs-site/guides/connect-otel.mdx +0 -209
  147. package/docs-site/guides/connect-your-agent.mdx +0 -221
  148. package/docs-site/guides/dataset-fanout.mdx +0 -98
  149. package/docs-site/guides/experiments.mdx +0 -71
  150. package/docs-site/guides/fixtures.mdx +0 -147
  151. package/docs-site/guides/reporters.mdx +0 -113
  152. package/docs-site/guides/runner.mdx +0 -79
  153. package/docs-site/guides/sandbox-agent.mdx +0 -138
  154. package/docs-site/guides/sandbox-backends.mdx +0 -64
  155. package/docs-site/guides/scoring-guide.mdx +0 -92
  156. package/docs-site/guides/viewing-results.mdx +0 -195
  157. package/docs-site/guides/write-experiment.mdx +0 -81
  158. package/docs-site/guides/write-send.mdx +0 -347
  159. package/docs-site/index.mdx +0 -181
  160. package/docs-site/introduction.mdx +0 -141
  161. package/docs-site/quickstart.mdx +0 -136
  162. package/docs-site/reference/builtin-agents.mdx +0 -161
  163. package/docs-site/reference/capabilities.mdx +0 -76
  164. package/docs-site/reference/cli.mdx +0 -120
  165. package/docs-site/reference/define-agent.mdx +0 -168
  166. package/docs-site/reference/define-config.mdx +0 -41
  167. package/docs-site/reference/define-eval.mdx +0 -160
  168. package/docs-site/reference/events.mdx +0 -131
  169. package/docs-site/reference/expect.mdx +0 -112
  170. package/docs-site/tracker.js +0 -8
  171. package/src/report/react/data.ts +0 -17
  172. package/src/view/aggregate.ts +0 -103
  173. package/src/view/loader.test.ts +0 -61
  174. package/src/view/loader.ts +0 -257
@@ -4,75 +4,77 @@
4
4
  // 和重试策略纠缠在一起(eval A=[1]、eval B=[0,0,0] 平铺 = 0.25,两级宏平均 = 0.5)。
5
5
  // 自定义维度把同一道题的 attempt 分进不同组时,第一级折叠发生在各组内部。
6
6
 
7
- import type { EvalResult } from "../types.ts";
8
- import type { AttemptHandle, SnapshotHandle } from "../results/types.ts";
7
+ import {
8
+ dedupeAttempts,
9
+ type AttemptHandle,
10
+ type Selection,
11
+ type SelectionWarning,
12
+ type Snapshot,
13
+ } from "../results/index.ts";
9
14
  import type {
10
15
  Aggregator,
11
16
  AttemptRef,
12
- Dimension,
17
+ DimensionInput,
13
18
  Metric,
14
19
  MetricCell,
15
20
  MetricColumn,
21
+ ParamRef,
16
22
  } from "./types.ts";
17
23
  import { formatMetricValue } from "./format.ts";
18
24
 
19
25
  // 复合键分隔符:NUL 不会出现在 eval id / experimentId / ISO 时间里,拼接键不会串味
20
26
  const KEY_SEP = "\u0000";
21
27
 
28
+ /** param 未声明时的组名:不猜,如实归一组。 */
29
+ export const PARAM_UNSET = "(unset)";
30
+
31
+ /** 计算函数的第一参:选集(warnings 随行)或手工挑的快照数组(没有挑选过程,自然无警告)。 */
32
+ export type SnapshotsInput = Selection | Snapshot[];
33
+
34
+ export function resolveInput(input: SnapshotsInput): {
35
+ snapshots: Snapshot[];
36
+ warnings: SelectionWarning[];
37
+ } {
38
+ if (Array.isArray(input)) return { snapshots: input, warnings: [] };
39
+ return { snapshots: input.snapshots, warnings: input.warnings };
40
+ }
41
+
22
42
  /** 展平后的一条样本:attempt + 它所属的快照(维度解析与题级折叠都需要快照身份)。 */
23
43
  export interface Item {
24
- snapshot: SnapshotHandle;
44
+ snapshot: Snapshot;
25
45
  attempt: AttemptHandle;
26
46
  }
27
47
 
28
48
  export function experimentIdOf(item: Item): string {
29
- return item.attempt.result.experimentId ?? item.snapshot.experimentId;
49
+ return item.attempt.experimentId || item.snapshot.experimentId;
30
50
  }
31
51
 
32
- /** 快照身份:与 view Compare 同口径的 (experimentId, startedAt) */
33
- function snapshotIdentity(snapshot: SnapshotHandle): string {
34
- return `${snapshot.experimentId}${KEY_SEP}${snapshot.startedAt}`;
52
+ export function evalIdOf(item: Item): string {
53
+ return item.attempt.evalId || item.attempt.result.id;
35
54
  }
36
55
 
37
- /** 身份键 (experimentId, evalId, attempt, startedAt);startedAt 缺失 → null(宁可不去重也不误删)。 */
38
- function identityKey(snapshot: SnapshotHandle, result: EvalResult): string | null {
39
- if (!result.startedAt) return null;
40
- return [
41
- result.experimentId ?? snapshot.experimentId,
42
- result.id,
43
- result.attempt,
44
- result.startedAt,
45
- ].join(KEY_SEP);
56
+ /** 快照键:与 view Compare 同口径的 "<experimentId> @ <startedAt>"。 */
57
+ export function snapshotKeyOf(snapshot: Snapshot): string {
58
+ return `${snapshot.experimentId} @ ${snapshot.startedAt}`;
46
59
  }
47
60
 
48
61
  /**
49
- * 跨快照聚合前按身份键去重:--resume 会把上一轮已通过的结果原样合入新 runsummary,
50
- * 同一 attempt 因此存在于多份落盘。重复时保留最新 run(按 run.summary.startedAt)里的那份。
51
- *
52
- * TODO(results-lib):这是「去重是消费方义务」的本地实现(docs/results-lib.md「身份键与去重」);
53
- * writer 给合入结果打标之后,按 niceeval/results 的规则收编,本函数随 handles.ts 一起换掉。
62
+ * 展平 + 聚合前去重(niceeval/resultsdedupeAttempts,身份键
63
+ * (experimentId, evalId, attempt, startedAt))。missing-startedAt 的警告不透出:
64
+ * 官方产出永不缺 startedAt,缺失只可能来自 legacy 落盘,「不去重、如实保留重复」即终稿。
54
65
  */
55
- export function dedupeAttempts(snapshots: SnapshotHandle[]): Item[] {
56
- // 第一遍:按身份键选赢家(run.startedAt ISO 字符串,字典序即时间序;并列保留后出现的)
57
- const winner = new Map<string, { attempt: AttemptHandle; runStartedAt: string }>();
66
+ export function collectItems(snapshots: Snapshot[]): Item[] {
67
+ const snapshotByAttempt = new Map<AttemptHandle, Snapshot>();
68
+ const flattened: AttemptHandle[] = [];
58
69
  for (const snapshot of snapshots) {
59
70
  for (const attempt of snapshot.attempts) {
60
- const key = identityKey(snapshot, attempt.result);
61
- if (key === null) continue;
62
- const runStartedAt = snapshot.run.summary.startedAt ?? "";
63
- const prev = winner.get(key);
64
- if (!prev || runStartedAt >= prev.runStartedAt) winner.set(key, { attempt, runStartedAt });
71
+ // 同一 handle 对象出现在两个快照里时以首次归属为准(手工挑重叠快照的极端场景)
72
+ if (!snapshotByAttempt.has(attempt)) snapshotByAttempt.set(attempt, snapshot);
73
+ flattened.push(attempt);
65
74
  }
66
75
  }
67
- // 第二遍:按原始遍历顺序输出,只放行赢家;无 startedAt 的一律放行(不去重)
68
- const items: Item[] = [];
69
- for (const snapshot of snapshots) {
70
- for (const attempt of snapshot.attempts) {
71
- const key = identityKey(snapshot, attempt.result);
72
- if (key === null || winner.get(key)?.attempt === attempt) items.push({ snapshot, attempt });
73
- }
74
- }
75
- return items;
76
+ const { attempts } = dedupeAttempts(flattened);
77
+ return attempts.map((attempt) => ({ attempt, snapshot: snapshotByAttempt.get(attempt)! }));
76
78
  }
77
79
 
78
80
  /**
@@ -88,12 +90,12 @@ export function evalPrefixPredicate(evals?: string | string[]): (id: string) =>
88
90
  export function filterItems(items: Item[], evals?: string | string[]): Item[] {
89
91
  if (evals === undefined) return items;
90
92
  const match = evalPrefixPredicate(evals);
91
- return items.filter((item) => match(item.attempt.result.id));
93
+ return items.filter((item) => match(evalIdOf(item)));
92
94
  }
93
95
 
94
96
  // ───────────────────────── 维度 ─────────────────────────
95
97
 
96
- export function dimensionName(dimension: Dimension): string {
98
+ export function dimensionName(dimension: DimensionInput): string {
97
99
  return typeof dimension === "string" ? dimension : dimension.name;
98
100
  }
99
101
 
@@ -103,8 +105,35 @@ export function evalGroupOf(id: string): string {
103
105
  return slash === -1 ? id : id.slice(0, slash);
104
106
  }
105
107
 
106
- export function dimensionKey(dimension: Dimension, item: Item): string {
107
- if (typeof dimension !== "string") return dimension.of(item.attempt);
108
+ function isParamRef(dimension: DimensionInput): dimension is ParamRef {
109
+ return typeof dimension === "object" && "kind" in dimension && dimension.kind === "param";
110
+ }
111
+
112
+ /** experiment 声明的 params(经 runner 原样透传进持久化字段 ExperimentRunInfo.params)。 */
113
+ function paramsOf(attempt: AttemptHandle): Record<string, unknown> | undefined {
114
+ return attempt.result.experiment?.params;
115
+ }
116
+
117
+ /** param 声明值 → 组标签:label 函数优先,其余 String();未声明 → PARAM_UNSET。 */
118
+ export function paramGroupKey(ref: ParamRef, item: Item): string {
119
+ const value = paramsOf(item.attempt)?.[ref.name];
120
+ if (value === undefined) return PARAM_UNSET;
121
+ // 持久化字段是 Record<string, unknown>;声明侧的合法值就是这三种标量
122
+ if (typeof ref.label === "function") return ref.label(value as string | number | boolean);
123
+ return String(value);
124
+ }
125
+
126
+ /** param 作轴:要求数值;未声明或非数值 → null(点不画,注脚报数)。 */
127
+ export function paramAxisValue(ref: ParamRef, item: Item): number | null {
128
+ const value = paramsOf(item.attempt)?.[ref.name];
129
+ return typeof value === "number" ? value : null;
130
+ }
131
+
132
+ export function dimensionKey(dimension: DimensionInput, item: Item): string {
133
+ if (typeof dimension !== "string") {
134
+ if (isParamRef(dimension)) return paramGroupKey(dimension, item);
135
+ return dimension.of(item.attempt);
136
+ }
108
137
  const result = item.attempt.result;
109
138
  switch (dimension) {
110
139
  case "agent":
@@ -114,11 +143,11 @@ export function dimensionKey(dimension: Dimension, item: Item): string {
114
143
  case "experiment":
115
144
  return experimentIdOf(item);
116
145
  case "eval":
117
- return result.id;
146
+ return evalIdOf(item);
118
147
  case "evalGroup":
119
- return evalGroupOf(result.id);
148
+ return evalGroupOf(evalIdOf(item));
120
149
  case "snapshot":
121
- return `${item.snapshot.experimentId} @ ${item.snapshot.startedAt}`;
150
+ return snapshotKeyOf(item.snapshot);
122
151
  default: {
123
152
  // 穷尽检查:新增内置维度而漏改这里时编译期报错
124
153
  const exhausted: never = dimension;
@@ -128,7 +157,7 @@ export function dimensionKey(dimension: Dimension, item: Item): string {
128
157
  }
129
158
 
130
159
  /** 按维度分组,保持首次出现顺序(无 sort 时表格行序即此序)。 */
131
- export function groupItems(items: Item[], dimension: Dimension): Map<string, Item[]> {
160
+ export function groupItems(items: Item[], dimension: DimensionInput): Map<string, Item[]> {
132
161
  const groups = new Map<string, Item[]>();
133
162
  for (const item of items) {
134
163
  const key = dimensionKey(dimension, item);
@@ -180,8 +209,8 @@ export async function computeCell(metric: Metric, items: Item[]): Promise<Metric
180
209
  const value = await evaluateMetric(metric, item.attempt);
181
210
  if (value === null) continue;
182
211
  samples += 1;
183
- refs.push(item.attempt.ref); // 证据引用由 niceeval/results 的句柄直供,不反查下标
184
- const bucketKey = `${item.attempt.result.id}${KEY_SEP}${snapshotIdentity(item.snapshot)}`;
212
+ refs.push(item.attempt.ref); // 证据引用由句柄直供,不反查下标
213
+ const bucketKey = `${evalIdOf(item)}${KEY_SEP}${snapshotKeyOf(item.snapshot)}`;
185
214
  const bucket = buckets.get(bucketKey);
186
215
  if (bucket) bucket.push(value);
187
216
  else buckets.set(bucketKey, [value]);
@@ -202,7 +231,7 @@ export function toColumn(metric: Metric): MetricColumn {
202
231
  };
203
232
  }
204
233
 
205
- export function assertUniqueMetricNames(metrics: Metric[], where: string): void {
234
+ export function assertUniqueMetricNames(metrics: readonly Metric[], where: string): void {
206
235
  const seen = new Set<string>();
207
236
  for (const metric of metrics) {
208
237
  if (seen.has(metric.name)) {
@@ -0,0 +1,190 @@
1
+ // 官方双面组件的装配点:web 面(./react/ 的纯 React 组件)+ text 面(./text/faces.ts)
2
+ // + 挂在组件上的 data 计算函数。faces 两键必填 —— 配对是结构义务,不是配对表;
3
+ // MetricBars.data 就是 MetricMatrix.data(同一份矩阵数据的另一种摆法),别名显式化。
4
+ //
5
+ // 官方组件在宿主里自动接上证据室:web 面的 attemptHref 缺省取 ctx.attemptHref
6
+ // (宿主注入的证据室深链);显式传 prop 可覆盖(嵌进自己应用时自定去处)。
7
+
8
+ import { defineComponent, isHostWebContextActive } from "./tree.ts";
9
+ import type { AttemptRef } from "../results/index.ts";
10
+ import type {
11
+ CaseListData,
12
+ DeltaData,
13
+ LineData,
14
+ MatrixData,
15
+ OverviewData,
16
+ ScatterData,
17
+ ScoreboardData,
18
+ TableData,
19
+ } from "./types.ts";
20
+ import {
21
+ caseListData,
22
+ deltaData,
23
+ lineData,
24
+ matrixData,
25
+ overviewData,
26
+ scatterData,
27
+ scoreboardData,
28
+ tableData,
29
+ } from "./compute.ts";
30
+ import {
31
+ barsText,
32
+ caseListText,
33
+ deltaText,
34
+ lineText,
35
+ matrixText,
36
+ overviewText,
37
+ scatterText,
38
+ scoreboardText,
39
+ tableText,
40
+ } from "./text/faces.ts";
41
+ import { RunOverview as RunOverviewWeb } from "./react/RunOverview.tsx";
42
+ import { MetricTable as MetricTableWeb } from "./react/MetricTable.tsx";
43
+ import { MetricMatrix as MetricMatrixWeb } from "./react/MetricMatrix.tsx";
44
+ import { MetricBars as MetricBarsWeb } from "./react/MetricBars.tsx";
45
+ import { Scoreboard as ScoreboardWeb } from "./react/Scoreboard.tsx";
46
+ import { MetricScatter as MetricScatterWeb } from "./react/MetricScatter.tsx";
47
+ import { MetricLine as MetricLineWeb } from "./react/MetricLine.tsx";
48
+ import { DeltaTable as DeltaTableWeb } from "./react/DeltaTable.tsx";
49
+ import { CaseList as CaseListWeb } from "./react/CaseList.tsx";
50
+
51
+ // ───────────────────────── props ─────────────────────────
52
+
53
+ export interface RunOverviewProps {
54
+ data: OverviewData;
55
+ className?: string;
56
+ }
57
+
58
+ export interface MetricTableProps {
59
+ data: TableData;
60
+ /** 传了,格子可点、下钻去处你定;不传,宿主里走证据室深链,宿主外纯展示。 */
61
+ attemptHref?: (ref: AttemptRef) => string;
62
+ className?: string;
63
+ }
64
+
65
+ export interface MetricMatrixProps {
66
+ data: MatrixData;
67
+ attemptHref?: (ref: AttemptRef) => string;
68
+ className?: string;
69
+ }
70
+
71
+ export interface ScoreboardProps {
72
+ data: ScoreboardData;
73
+ className?: string;
74
+ }
75
+
76
+ export interface MetricScatterProps {
77
+ data: ScatterData;
78
+ /** 点一个点 → 该配置的下钻页。 */
79
+ pointHref?: (row: ScatterData["rows"][number]) => string;
80
+ className?: string;
81
+ }
82
+
83
+ export interface MetricLineProps {
84
+ data: LineData;
85
+ pointHref?: (row: LineData["rows"][number]) => string;
86
+ className?: string;
87
+ }
88
+
89
+ export interface DeltaTableProps {
90
+ data: DeltaData;
91
+ className?: string;
92
+ }
93
+
94
+ export interface CaseListProps {
95
+ data: CaseListData;
96
+ attemptHref?: (ref: AttemptRef) => string;
97
+ className?: string;
98
+ }
99
+
100
+ // ───────────────────────── 装配 ─────────────────────────
101
+
102
+ /** 页头 KPI 条:何时跑的、几个配置、几道题、通过率、总成本;选集的警告随行显示在条内。 */
103
+ export const RunOverview = Object.assign(
104
+ defineComponent<RunOverviewProps>({
105
+ web: (props) => <RunOverviewWeb {...props} />,
106
+ text: ({ data }) => overviewText(data),
107
+ }),
108
+ { data: overviewData },
109
+ );
110
+ RunOverview.displayName = "RunOverview";
111
+
112
+ /** 榜单:一行一个维度值、一列一个指标,回答「谁整体更好」。 */
113
+ export const MetricTable = Object.assign(
114
+ defineComponent<MetricTableProps>({
115
+ web: (props, ctx) => <MetricTableWeb {...props} attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)} />,
116
+ text: ({ data }) => tableText(data),
117
+ }),
118
+ { data: tableData },
119
+ );
120
+ MetricTable.displayName = "MetricTable";
121
+
122
+ /** 逐题格子:行 × 列两个维度、格子里一个指标,回答「哪道题谁挂了」。 */
123
+ export const MetricMatrix = Object.assign(
124
+ defineComponent<MetricMatrixProps>({
125
+ web: (props, ctx) => <MetricMatrixWeb {...props} attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)} />,
126
+ text: ({ data }) => matrixText(data),
127
+ }),
128
+ { data: matrixData },
129
+ );
130
+ MetricMatrix.displayName = "MetricMatrix";
131
+
132
+ /** 分组条形:同一份矩阵数据的另一种摆法;MetricBars.data 就是 MetricMatrix.data 的别名。 */
133
+ export const MetricBars = Object.assign(
134
+ defineComponent<MetricMatrixProps>({
135
+ web: (props, ctx) => <MetricBarsWeb {...props} attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)} />,
136
+ text: ({ data }) => barsText(data),
137
+ }),
138
+ { data: matrixData },
139
+ );
140
+ MetricBars.displayName = "MetricBars";
141
+
142
+ /** 考试成绩单:总分 + 分科小计,固定分母、missing 如实报。 */
143
+ export const Scoreboard = Object.assign(
144
+ defineComponent<ScoreboardProps>({
145
+ web: (props) => <ScoreboardWeb {...props} />,
146
+ text: ({ data }) => scoreboardText(data),
147
+ }),
148
+ { data: scoreboardData },
149
+ );
150
+ Scoreboard.displayName = "Scoreboard";
151
+
152
+ /** 质量 × 成本 frontier:每个点一个配置、两个指标各占一轴,「好」的角落恒在右上。 */
153
+ export const MetricScatter = Object.assign(
154
+ defineComponent<MetricScatterProps>({
155
+ web: (props) => <MetricScatterWeb {...props} />,
156
+ text: ({ data }, ctx) => scatterText(data, ctx),
157
+ }),
158
+ { data: scatterData },
159
+ );
160
+ MetricScatter.displayName = "MetricScatter";
161
+
162
+ /** 趋势线:x 是 experiment 声明的 param(param()),同系列按 x 排序连线。 */
163
+ export const MetricLine = Object.assign(
164
+ defineComponent<MetricLineProps>({
165
+ web: (props) => <MetricLineWeb {...props} />,
166
+ text: ({ data }, ctx) => lineText(data, ctx),
167
+ }),
168
+ { data: lineData },
169
+ );
170
+ MetricLine.displayName = "MetricLine";
171
+
172
+ /** 成对对比:每行一对配置、格子里 A、B、Δ 三个值,涨跌好坏由 better 判定。 */
173
+ export const DeltaTable = Object.assign(
174
+ defineComponent<DeltaTableProps>({
175
+ web: (props) => <DeltaTableWeb {...props} />,
176
+ text: ({ data }) => deltaText(data),
177
+ }),
178
+ { data: deltaData },
179
+ );
180
+ DeltaTable.displayName = "DeltaTable";
181
+
182
+ /** 失败案例清单:榜单回答「多少」,它回答「为什么」;truncated 如实报剩余。 */
183
+ export const CaseList = Object.assign(
184
+ defineComponent<CaseListProps>({
185
+ web: (props, ctx) => <CaseListWeb {...props} attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)} />,
186
+ text: (props, ctx) => caseListText(props.data, ctx),
187
+ }),
188
+ { data: caseListData },
189
+ );
190
+ CaseList.displayName = "CaseList";