> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 报告组件一览

> 报告文件里能摆的全部官方双面组件：每个组件回答什么问题、怎么调用、网页面长什么样、终端字符输出长什么样。

报告文件里的每个组件都是双面的：网页面是 React 渲染，终端面是字符渲染，两面吃同一份算好的数据，走哪扇门由宿主决定（见[自定义报告](/docs/zh/tutorials/custom-reports)）。每个组件要么直接写省略取数的写法（省略 `input`/`data` 时用当前 Scope 自动取数），要么显式传入自己算好的数据——两种写法产出完全相同，选哪种只看你要不要在取数和渲染之间插入自己的 JavaScript。本页逐个列出官方组件：它展示哪一层数据、在报告里怎么调用、终端输出长什么样。

## 名称与用途

英文术语描述组件形态，API 名是代码里的导出名。默认组合件比较当前 Scope；范围摘要概括整批结果；实体列表逐项展示 experiment、评估用例或 Attempt；指标图形把指定维度聚合成值。

| 分类   | 中文名        | English               | API                    | 主展示单位                                           |
| ---- | ---------- | --------------------- | ---------------------- | ----------------------------------------------- |
| 组合   | 实验比较       | Experiment comparison | `ExperimentComparison` | 当前 Scope 的摘要、散点与实验列表                            |
| 汇总   | 范围摘要       | Scope summary         | `ScopeSummary`         | 一批结果：时间窗、数量、两级判定计票、端到端通过率与总成本                   |
| 实体列表 | 实验列表       | Experiment list       | `ExperimentList`       | 每项一个 experiment；展开到该 experiment 的评估用例           |
| 实体列表 | 评估用例列表     | Eval list             | `EvalList`             | 每项一个 experiment × 评估用例；展开到该评估用例的 Attempt        |
| 实体列表 | Attempt 列表 | Attempt list          | `AttemptList`          | 每项一个 Attempt；显示断言、错误、Judge 评语和证据                |
| 实体列表 | 失败列表       | Failure list          | `FailureList`          | `AttemptList` 的成品过滤：只列 failed / errored，按开始时间倒序 |
| 指标图形 | 指标表        | Metric table          | `MetricTable`          | 一个可配置行维度；每格是一个聚合指标值                             |
| 指标图形 | 指标矩阵       | Metric matrix         | `MetricMatrix`         | 两个可配置维度的交叉格；每格是一个聚合指标值                          |
| 指标图形 | 分组条形图      | Grouped bar chart     | `MetricBars`           | 两个可配置维度形成分组和系列；每根条是一个聚合指标值                      |
| 指标图形 | 成绩单        | Scoreboard            | `Scoreboard`           | 每行一个可配置维度值；按固定题集算总分和分科得分                        |
| 指标图形 | 指标散点图      | Metric scatter plot   | `MetricScatter`        | 每点一个可配置维度值，通常是 experiment；坐标是两个聚合指标值            |
| 指标图形 | 指标趋势图      | Metric line chart     | `MetricLine`           | 每点一个 experiment；横轴是数值配置变量，纵轴是聚合指标值              |
| 指标图形 | 成对差异表      | Paired delta table    | `DeltaTable`           | 每行一对 experiment 或结果快照；格内是指标值及差值                 |

`Row`、`Col`、`Grid`、`Section`、`Stat`、`Text`、`Style`、`Table`、`Tabs` 和 `Tab` 是十个排版原语，不计算结果，因此不算一种分析图。它们的中文统称依次是行、列、网格、分节、摘要项、文本、样式、表格、标签页和标签；代码里始终使用 API 名。

所有组件共守同一套契约，下面不再逐个重复：

* **诚实渲染。** 缺数据渲染 `—` 不补 0；覆盖不全的格子带 `12/15` 角标；截断如实标注剩余数量与原始 artifact 路径。
* **排序与方向随指标的 `better`。** higher 的指标降序、lower 的升序，「好」的一头恒在上、在右上。
* **深链到 Attempt 详情。** 网页面的格子、点和条目深链到 Attempt 详情页；终端面用同一 Attempt 定位符交给 `niceeval show @<id>`。
* **终端输出形成反馈闭环。** 每个 Attempt 有一个以 `@` 开头的短定位符，例如 `@1k2m9qrs`。它唯一指向 experiment、结果快照、评估用例和 Attempt。`✓` / `✗` / `!` / `–` 是 passed / failed / errored / skipped。列表不用字母缩写编码证据可用性——定位符本身就是证据入口；打开 attempt（`niceeval show @<定位符>`）后再列出实际可用的证据命令。执行步骤统一包含消息、thinking、tool call/result 和 Skill load；OTel 只给这些步骤补时间，不另开一份输出。
* **两面同口径，网页面不依赖 JS 也完整。** 排序在计算时由 `sort` 定死，终端和网页看到同一份基准顺序；下钻是普通链接，展开折叠用 `<details>`。网页面另有一层浏览操作：点表头就地重排、榜单行过滤、图表点位悬停看数值——只影响眼前的视图，不改数据口径，刷新即回基准顺序；浏览器禁用 JS 时这些操作消失，内容一样不少（悬停信息退化为图内提示）。

## 两种写法，同一份数据

每个组件的 props 分两种写法，选哪种都行：

```tsx theme={null}
// 省略 input/data：用宿主注入的当前 Scope 自动取数
<MetricTable rows="agent" columns={[endToEndPassRate, costUSD]} filter />

// 自己先算好数据再传：中间可以插入任意 JavaScript 加工
<MetricTable data={await metricTableData(scope, { rows: "agent", columns: [endToEndPassRate, costUSD] })} filter />
```

两种写法产出完全相同；同一个组件同时给出 `data` 和取数选项会报错，两者二选一。所有计算函数的第一个参数都是 Scope（或手工挑的结果快照数组）；产出的数据都是普通可序列化 JSON，可以先存下来、传给别的进程，或者原样喂给对应组件的 `data` prop。`niceeval/report/react` 入口的同名组件只收 `data`，不做取数——那一层是纯 React 渲染，见[自定义报告](/docs/zh/tutorials/custom-reports)。

`evals` 是数据获取阶段唯一的过滤选项：评估用例 id 前缀，与 CLI 位置参数同语义，在聚合**之前**收窄题集。实体列表（`ExperimentList` / `EvalList` / `AttemptList`）不设这个选项——它们逐实体成行，取数后用普通数组 `.filter()` 收窄，效果和任何专门选项完全一样。

## 排版原语

`Row` / `Col` / `Section` / `Text` 负责摆版面，一次摆放两个面各自成立：`Col` 纵向堆叠；`Row` 网页横排、终端字符分栏（宽度不够自动降级纵向）；`Section` 是带标题的块；`Text` 是说明文字。另有 `<Style>{css}</Style>` 给自定义组件带样式：网页面吐 `<style>` 标签、终端面渲染为空——静态导出不打包用户代码，`className` 引用的 CSS 靠它随树走。

```tsx theme={null}
<Col>
  <Section title="考试成绩单">
    <Row>
      <Scoreboard data={board} />
      <Text>algebra 权重 ×2，满分 100。</Text>
    </Row>
  </Section>
</Col>
```

```text theme={null}
考试成绩单
  agent  总分       algebra  geometry │ algebra 权重 ×2，
  bub    86.5/100   45/50    41.5/50  │ 满分 100。
  codex  78.0/100   40/50    38/50    │
```

## 表格（`Table`）

第六个排版原语：官方组件摆不出的表，用它摆。它不计算任何东西——列由你定，格子是你算好的显示值，它只负责把两个面都排整齐。

```tsx theme={null}
<Table
  columns={[
    { key: "eval", header: "题目" },
    { key: "pass", header: "通过率", align: "right" },
    { key: "cost", header: "成本", align: "right" },
  ]}
  rows={[
    {
      key: "记忆/写缓存",
      locator: "@160iuj3h",
      cells: { eval: "记忆/写缓存", pass: "87%", cost: "$0.09" },
    },
    {
      key: "浏览/表单填写",
      locator: "@1qrdcfq8",
      cells: { eval: "浏览/表单填写", pass: null, cost: null },
    },
  ]}
/>
```

```text theme={null}
题目            通过率    成本   attempt
记忆/写缓存        87%   $0.09   @160iuj3h
浏览/表单填写        —       —   @1qrdcfq8
```

列宽按**终端显示宽度**算，中文和全角字符记 2 列——中文题目名、中文 agent 名都不会把表撕歪。`align: "right"` 让数字列右对齐，小数点自然对齐。格子是 `null` 就渲染 `—`，不补 0。行上带 `locator` 就多出一列 attempt：网页面链到 Attempt 详情，终端面列出定位符，直接喂给 `niceeval show <定位符>`。

表比终端宽时先压最宽的左对齐列（按显示宽度折行），右对齐列不折行——数字折行读不了；压到下限仍放不下，就从右侧丢列，并在表下如实报丢了几列，不静默截断。

某一列的格子只想显示前几行时，给这一列加 `maxLines`：超出的行丢弃，最后一行按显示宽度收口成 `…`；表头不受这个限制。网页面不消费这个字段——格子的高度由你自己的样式决定。

指标表、指标矩阵、成绩单和成对差异表的终端面就建在 `Table` 上，所以你的表和官方的表用的是同一把尺子。表格之外的形态要自己排字符时，用[自定义报告](/docs/zh/tutorials/custom-reports)「换形态」一节里那套文本排版函数。

## 摘要格（`Grid` / `Stat`）

一批 label-value 的速览数字（耗时、成本、参与率这类）用 `Grid` 摆格子、`Stat` 摆每格的内容，两面都自动排版，不用自己写 CSS 或对齐字符：

```tsx theme={null}
<Grid columns={3} variant="boxed">
  <Stat label="总耗时" value="12m 30s" />
  <Stat label="总成本" value="$1.86" />
  <Stat label="通过率" value="87%" detail="26 / 30" tone="positive" />
</Grid>
```

```text theme={null}
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 总耗时       │ │ 总成本       │ │ 通过率       │
│ 12m 30s     │ │ $1.86       │ │ 87%         │
│             │ │             │ │ 26 / 30     │
└─────────────┘ └─────────────┘ └─────────────┘
```

`columns` 是网页宽屏下最多摆几列，必须是正整数；容器变窄或终端变窄时两个面各自减列，不丢任何一格。`variant="boxed"` 给每一格描边，默认 `"plain"` 无框；`density="compact"` 收紧格内留白，字号也跟着降一档，不改变内容。

`Stat.value` 收 `LocalizedText`、`number` 或 `null`：`null` 显示 `—`，数字 `0` 照常显示成 `0`，不会被当成缺数据。`detail` 是主值下面的一行小字，省略就不留空行。`tone`（`neutral` / `positive` / `negative` / `warning`）是你对这个数字的语义判断，只给主值上色，组件不会替你从正负号或阈值猜。

`Grid` 只管排版，不读 Scope、不聚合指标——`Stat.value` 必须是你已经算好的显示值。需要保留覆盖率角标（`12/15`）和证据引用时，继续用指标表这类数据组件；只有真正自由的摘要卡片才用 `Grid` / `Stat`。一格里要放两个 `Stat` 时，用 `Col` 把它们包起来，`Grid` 仍然只把这个 `Col` 算作一格。

`Section` 也多一个可选的 `meta`：标题行右侧的短元信息，比如「6/6 完成」。网页面和标题同一行右对齐，终端面空间不够时换到下一行，缩进两格。

## 实验比较（`ExperimentComparison`）

`niceeval show` / `view` 不传 `--report` 时使用的默认组合件。它把同一份 Scope 显式传给 `ScopeSummary`、成本 × 端到端通过率的 `MetricScatter` 和 `ExperimentList` 三个组件，自己不产出数据，也不合并三者的结果：

```tsx theme={null}
<ExperimentComparison />
```

每个 experiment 的评估用例集合来自快照记录的 `selectedEvalIds`。网页面与终端面都显示完整 Scope；需要子集时用 `--exp` 收窄，或在自定义报告里对 Scope 先 `.filter()` 再传给 `input`。

实验列表行的显示名不需要额外配置：experiment id 共享公共目录前缀时（比如都在 `compare/` 下），行标签自动缩成各自的最短唯一后缀，不显示重复的前缀；末段撞名的 id 会自动加长到能互相区分为止。完整 id 始终是排序、过滤和折叠展开用的身份键，只是显示文字变短了。

组卡使用 `Pass rate / 通过率`、`Experiments / 实验`、`Evals / Eval`、`Attempts / Attempt`、`Eval results / Eval 结果`、`Total cost / 总成本` 这套字段标签，不在标签里重复“数”“次”或“计票”。时间显示为本地化到分钟的 `Last run / 最近运行` 或 `Run range / 运行范围`，不直接暴露 ISO 字符串；成本数据覆盖不全时写明“`63/72 次有成本数据`”，不显示没有上下文的 `63/72`。六项 KPI 在宽卡片保持同一行，空间不足时按三项或两项一组换行，避免总成本单独掉到下一行。

下面的 `MetricScatter`、`ScopeSummary` 和 `ExperimentList` 同样忠实消费调用方传入的数据，不推导隐藏范围。它等价于把三个组件按下面这样手工摆放——想自定义顺序或搭配其它组件时，照这个形状写：

```tsx theme={null}
export const MyComparison = defineComponent((_props, ctx) => (
  <Col>
    <ScopeSummary input={ctx.scope} />
    <MetricScatter input={ctx.scope} points="experiment" x={costUSD} y={endToEndPassRate} />
    <ExperimentList input={ctx.scope} filter />
  </Col>
));
```

## 范围摘要（`ScopeSummary`）

每张报告开头「这批数据是什么」：几个配置、几道题、通过分布、总成本、什么时候跑的。评估用例的身份键是 `experimentId + evalId`：同一个评估用例在不同 experiment 中运行时算两个独立评估用例，数量和判定计票都按这个身份算。

```tsx theme={null}
<ScopeSummary />                    // 当前 Scope 的摘要，Eval 级计票（默认）
<ScopeSummary votes="attempt" />    // 同一份数据，改看 Attempt 原始计票
```

数据本身恒携带两级计票，`votes` 只决定显示哪一级：

* `votes="eval"`（默认）：每个 experiment × 评估用例先按「任一轮 passed 即 passed，否则 failed > errored > skipped」折成最终判定后计票，回答「多少道题最终通过」。
* `votes="attempt"`：Attempt 原始计票，不折叠，回答「实际跑的每一轮各是什么结果」。

两级计票与端到端通过率互不换算：通过率来自官方两级指标口径，不是从计票现场重新加总。选择警告不在这份数据里——警告的呈现件是 `ScopeWarnings` 组件，同一份事实不在页面上出现两次。

收窄范围时在自定义组件里显式传 `input`：

```tsx theme={null}
const CompareSummary = defineComponent((_props, ctx) => (
  <ScopeSummary input={ctx.scope.filter((s) => s.experimentId.startsWith("compare/"))} />
));
```

## 实验列表（`ExperimentList`）

每项固定代表一个 experiment。主行显示 experiment id、agent、model、flags、评估用例判定构成、通过率、Tokens、成本和耗时。默认 `ExperimentComparison` 把当前 Scope 的全部条目交给它；组件本身不猜边界。

行标签默认缩成 experiment id 在当前列表里的最短唯一后缀——末段唯一就只显示末段，撞名的 id 各自向前多取一段直到能区分为止（与 `MetricScatter` 散点的点标签同一算法）。排序、过滤和折叠展开始终用完整 id，不受显示名影响。中文副行用“`8 个 Eval`”而不是“`8 道题`”。

```tsx theme={null}
<ExperimentList filter />
```

要按前缀、agent 或状态过滤，先取数再用普通数组 `.filter()`：

```tsx theme={null}
export const ProdExperiments = defineComponent(async (_props, ctx) => {
  const items = await experimentListData(ctx.scope);
  return <ExperimentList data={items.filter((x) => x.experimentId.startsWith("prod/"))} filter />;
});
```

`niceeval show` 先输出 experiment 比较表，再按 experiment 展开评估用例 / Attempt 父子表。评估用例父行给题级平均值，Attempt 子行给这一轮的失败摘要和定位符：

```text theme={null}
Experiment      Model     Agent   Avg. time   Pass rate   Results                 Tokens   Cost
bub-gpt-5.4     gpt-5.4   bub     41.0s       50%         1 passed · 1 failed     42k      $0.08

bub-gpt-5.4
Status      Eval / Attempt       Result                                  Duration    Cost
✓ passed    algebra/quadratic                                             18.0s avg   $0.02 avg
  ✓         └─ @12f9k3aq         —                                       18.0s       $0.02
✗ failed    weather/brooklyn                                              42.0s avg   $0.04 avg
  ✗         ├─ @1k2m9qrs         calledTool("get_weather") · no calls    41.0s       $0.04
  ✗         └─ @1nx4dpqr         calledTool("get_weather") · no calls    43.0s       $0.04
```

定位符由 `experimentId + snapshot.startedAt + evalId + attempt 序号` 的不可变身份确定，复制或发布结果后保持不变。宿主在当前结果根解析定位符；不存在或发生冲突时直接报错，不回退到“最新一次”。`@` 前缀让它与评估用例 ID 前缀选择器无歧义。

`experimentListData(scope)` 返回普通的 `ExperimentListItem[]`，顺序按 experiment id 稳定排列。要只看某个 agent、目录前缀或运行状态，直接过滤数组。组件不提供另一套查询语法。网页面的文本搜索只是临时浏览操作，不改变传入的条目；终端面始终输出完整的传入数组。

## 评估用例列表（`EvalList`）

每项固定代表一个 `experimentId + evalId`，因为同一个评估用例跑在两个 experiment 上是两条不同结果。主行显示判定、Attempt 数、聚合分数、平均成本和平均耗时；展开后显示这个评估用例的 Attempt 列表，由每个 Attempt 行显示该轮自己的失败原因。评估用例主行不挑某一轮的失败原因冒充题级结论。

```tsx theme={null}
export const FailingEvals = defineComponent(async (_props, ctx) => {
  const items = await evalListData(ctx.scope);
  return <EvalList data={items.filter((item) => item.verdict !== "passed")} />;
});
```

`niceeval show` 每个 `experiment × Eval` 输出一项，Attempt 仍用同一套紧凑 ID 和证据位。每项只给一条模板：

```text theme={null}
weather/brooklyn · compare/bub-gpt-5.4 · failed
  score 0.20 · 3 attempts · 41s avg · $0.04 avg
  @1k2m9qrs✗ · gate calledTool("get_weather") — tool was never called
  @1nx4dpqr✗ · gate calledTool("get_weather") — tool was never called
  @19vq2jex✗ · gate calledTool("get_weather") — tool was never called

fixtures/button · compare/bub-gpt-5.4 · errored
  score — · 1 attempt · 2m 00s · $0.09
  @1c3h6twx! · command timed out after 120s

inspect: niceeval show @<id> [--source|--execution|--diff]
```

`evalListData(scope)` 返回普通的 `EvalListItem[]`。按评估用例 id 前缀、experiment、判定或分数过滤都使用数组 `.filter()`；组件只负责渲染传入的条目。

## Attempt 列表（`AttemptList`）

每项固定代表一个 Attempt，显示 experiment、评估用例、Attempt 序号、判定、耗时、成本、失败断言、结构化 error 的一层摘要、Judge 评语和证据链接。diagnostics、cause 和 stack 留给定位符下钻详情，避免比较列表被基础设施日志撑开。它既能列失败证据，也能列通过样本，不把判定过滤写死在组件名里。

```tsx theme={null}
export const RecentFailures = defineComponent(async (_props, ctx) => {
  const all = await attemptListData(ctx.scope);
  const failed = all.filter((x) => x.verdict === "failed" || x.verdict === "errored");
  return <AttemptList data={failed.slice(0, 20)} total={failed.length} />;
});
```

`niceeval show` 每项完整输出一个 Attempt，不折叠到评估用例汇总。这里已经是叶子层，只在末尾给一条与该 Attempt 可用证据对应的模板：

```text theme={null}
✗ @1k2m9qrs · weather/brooklyn · compare/bub-gpt-5.4 · 41s · $0.04
  gate calledTool("get_weather") · failed
    tool was never called
  soft judge("回答基于实时数据") · 0.2/1
    reply invents a temperature

! @1c3h6twx · fixtures/button · compare/bub-gpt-5.4 · 2m 00s · $0.09
  command timed out after 120s
inspect: niceeval show @<id> [--source|--execution|--diff]

(3 more not shown · showing 20 of 23)
```

要展示哪些 Attempt，过滤返回的 `AttemptListItem[]`。截断数量也由报告作者在数组上用 `.slice(0, 20)` 表达，截断时把原始数量交给组件的 `total`，组件据此显示“还有 n 项未展示”，不静默截断。

## 失败列表（`FailureList`）

「现在有哪些失败要处理」是每份报告都要的固定区块，工具箱直接提供成品组合件，不用每次都重写同一段取数过滤。它和上面 `AttemptList` 的手工写法完全等价，只是把最常见的一种过滤打包好了：收 `verdict` 为 failed 或 errored 的 Attempt，按开始时间倒序（最近的失败在前），截断到 `limit`（默认 20）。

```tsx theme={null}
<FailureList limit={30} />
```

要按 agent、成本或其它口径筛选，回到手写 `AttemptList` 的写法自己加工数组；`FailureList` 只覆盖这一种最常见的问题。

## 指标表（`MetricTable`）

一行一个维度值，一列一个指标，回答「谁整体更好」。行维度、指标列、排序全部可换，自定义指标（`defineMetric`）与内置指标同列。

```tsx theme={null}
<MetricTable
  rows="agent"
  columns={[endToEndPassRate, examScore, costUSD, durationMs]}
  sort={endToEndPassRate}
  evals="coding/"
  filter
/>
```

```text theme={null}
agent   pass rate   examScore   cost
bub     87%         0.91        $0.42
codex   80% 12/15   0.86        $0.51
```

两面都按 `sort` 预排，基准顺序一致——要固定换一种排序，改一行重跑。`12/15` 角标表示该格 15 个 attempt 里只有 12 个测得了这个指标。`sort` 必须是 `columns` 中同一个 Metric 实例且声明了 `better`，否则报错；省略时按行 key 字典序，避免为方向不明的指标猜顺序。`filter` 只给网页面加行过滤框，不改变数据或终端输出。

`rows: "experiment"` 时每行自动带 agent 与 model 列——结果里现成的元信息，不用配置。`MetricTable` 不展开实体层级：要看 experiment、评估用例或 Attempt 的固定诊断字段，用上面三个实体列表；要自由换维度和指标列，用指标表。

## 指标矩阵（`MetricMatrix`）

行 × 列两个维度、格子里一个指标，回答「哪道题谁挂了」。稀疏渲染：没有样本的格子空着，不编数。

```tsx theme={null}
<MetricMatrix rows="eval" columns="agent" cell={endToEndPassRate} />
```

```text theme={null}
eval                bub    codex
algebra/quadratic   100%   100%
algebra/matrix      100%   67%
geometry/area       50%    —

next: niceeval show geometry/area
```

网页面点格子深链到该格的 attempt；终端面在表下印下钻命令。

## 分组条形图（`MetricBars`）

同一份矩阵数据的另一种摆法：按组并排比大小，回答「每个科目上谁领先、差多少」。组维度一组条、系列维度一根条、条长是指标值——benchmark 发布图（Terminal-Bench、BrowseComp 各一组，每个 agent 一根柱）就是这个形状。

```tsx theme={null}
<MetricBars
  rows="evalGroup"        // 一组条 = 一个科目/benchmark
  columns="agent"         // 一根条 = 一个 agent
  cell={endToEndPassRate}
/>
```

```text theme={null}
algebra
  bub     ██████████████████░░  91.9%
  codex   █████████████████░░░  88.0%
geometry
  bub     ██████████░░░░░░░░░░  50.0%
  codex   —
```

网页面是竖向分组柱：柱顶标数值，系列颜色与其它组件的稳定配色一致，图例自动生成。终端面横向条形，字符宽度即刻度，同组内按值排序（方向随 `better`）。`better: "lower"` 的指标（成本、耗时）条形反向填充，短条恒为「好」。`MetricMatrix` 和 `MetricBars` 写同一份取数选项时，两个组件不会重复计算——只要 `input` 与选项相同，底层数据只算一次。

## 成绩单（`Scoreboard`）

总分 + 分科小计，回答「这套题它能得几分」。逐题分值制：权重按评估用例 id 前缀配置，分母对所有被打分者恒定，没跑到的题挣 0 分并如实报 `missing`。

```tsx theme={null}
<Scoreboard
  rows="agent"
  questions={[
    "security/sql-injection",
    "security/path-traversal",
    "correctness/retry",
  ]}
  weights={{ "security/": 3, "correctness/": 2 }}
  fullMarks={100}
  score={examScore}
/>
```

```text theme={null}
agent   total      algebra        geometry
bub     86.5/100   45/50          41.5/50
codex   71.0/100   40/50          31/50 (1 missing)
```

`questions` 是显式固定题集，不从已观测的 Attempt 并集猜——所有配置都没跑到的题仍然留在分母里按 0 分计。分数为 `null`（跑了但测不了）与完全没跑到的题分开计数（分别是 `unscorable` 和 `unrun`），成绩单能回答「这 0 分是没去考还是考了判不了」。`score` 默认是 `examScore`，每道题必须产出 `[0, 1]`；总分是 `fullMarks × earned / possible`。

## 指标散点图（`MetricScatter`）

每个点一个配置、两个指标各占一轴，回答「又好又便宜的是谁」。`series` 把同 agent 不同档位的点连成线；`better` 驱动轴向——`lower` 的轴反向画，「好」的角落恒在右上。

```tsx theme={null}
<MetricScatter points="experiment" series="agent" x={costUSD} y={endToEndPassRate} />

// 同族变体连线：同 line 值的实验一色成线，connect 连出基线 → 变体的位移
<MetricScatter points="experiment" series={label("line")} connect x={costUSD} y={endToEndPassRate} />
```

`MetricScatter` 直接消费传入的 Scope，宿主渲染前替你算好数据；默认 `ExperimentComparison` 也把当前 Scope 原样交给它。要嵌预先算好的数据，改传 `data`。

```text theme={null}
pass ↑                                    （好 → 右上）
 90%│                                  A
    │                     B
 75%│                          C
 60%│      D
    └──────────────────────────────────────→ cost（轴反向：越右越省）
     $0.60       $0.45       $0.30      $0.15

A bub-high   B bub-medium   C codex-high   D codex-low
```

网页面点带悬停提示（值与 `samples/total`，禁用 JS 时退化为图内提示）、同系列连线、点击深链下钻。终端面用字母标点，图例列在图下；x 或 y 缺数据的点两个面都不画，注脚如实报「n 个点缺数据」；点太密排不下时降级为坐标表，不硬挤。画得出来的点是 0 个时，两个面都明说这两个指标没有可用数据，不留一片空白；1 个点也照常出图。维度槽也收自定义维度和 `flag()`（experiment 声明的变量），怎么选见[自定义报告](/docs/zh/tutorials/custom-reports)的「换分组」一节。

## 指标趋势图（`MetricLine`）

x 是有序变量、每个系列一条线，回答「变量拧大，分数怎么走」——并行 agent 数 × 模拟延迟 × 得分这类 scaling 图就是它。与 `MetricScatter` 的分工：散点图的两轴都是测出来的指标（找优势前沿），趋势图的 x 是你配置的变量（看趋势）。变量在 experiment 的 flags 里声明，报告用 `flag()` 或数值型 flag 助手直接引用，不从 experiment 命名里解析。

```tsx theme={null}
const budget = numericFlag("budget", { label: "Token budget", unit: "tokens" });
<MetricLine x={budget} series="agent" y={endToEndPassRate} />
```

```text theme={null}
pass ↑
 80%│                   C···C
    │            C···C            B
 60%│       C         B···B
    │         B···B                        A
 40%│       A···A
    │    A
 20%│
    └───────────────────────────────────────────→ Simulated latency
      1 min       5 min       10 min      20 min

A 1 agents   B 4 agents   C 16 agents
```

每个点是一个 experiment 的聚合，与其它组件同一套指标引擎；同系列的点按 x 排序连线。网页面每个点可 hover、可深链下钻。终端面同系列共用一个字母、沿 x 排布，趋势肉眼可读；y 缺数据的点不画、注脚报数，点太密降级为坐标表。

## 成对差异表（`DeltaTable`）

每行一对配置、每列一个指标，格子里 A、B、Δ 三个值，回答「这个开关值不值」「这次修复翻转了什么」。涨跌好坏由 `better` 判定，任一侧缺数据 Δ 显示为缺，不硬算。

```tsx theme={null}
// 字面形态：逐对声明，label 自定义
<DeltaTable
  by="experiment"
  pairs={[{ label: "memory", a: "compare/baseline", b: "compare/with-memory" }]}
  metrics={[endToEndPassRate, costUSD, durationMs]}
/>

// 派生形态：按 flag 机械配对，加实验不用改报告
<DeltaTable
  by="experiment"
  pairs={pairsByFlag("memory")}
  metrics={[endToEndPassRate, costUSD, durationMs]}
/>
```

```text theme={null}
pair    pass rate              cost
bub     87% → 93%   +6%        $0.42 → $0.45   +$0.03
codex   80% → 80%   ±0         $0.51 → —       —
```

「这次 vs 上次」（同一配置的两个结果快照）也是它：`pairs` 的 `a` / `b` 除 experiment id 外也收快照键 `<experimentId> @ <startedAt>`——手挑的快照数组（比如某个实验的最新一次和上一次）配这种写法。`pairsByFlag(name)` 按一个 flag 机械导出全部 A/B 对：实验矩阵是「同配置开关某个 flag」时，配对关系本来就是 experiment 配置的推论，手抄 id 字面量等于把配置复写进报告，加实验后报告会静默缺行。

## 官方组件之外

以上摆法都表达不了时，用 `defineComponent` 写自己的双面组件——网页怎么渲染、终端字符怎么排，两个面你都说了算，写法见[自定义报告](/docs/zh/tutorials/custom-reports)的「换形态」一节。
