niceeval 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +321 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
- package/dist/report/built-ins/experiment-comparison.js +13 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +180 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +600 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +26 -0
- package/dist/report/format.js +109 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +94 -0
- package/dist/report/locale.js +183 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +19 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +11 -0
- package/dist/report/react/ExperimentList.js +67 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +96 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +398 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +541 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +408 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +7 -7
- package/docs-site/zh/concepts/assert.mdx +19 -18
- package/docs-site/zh/concepts/evals.mdx +12 -11
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +3 -3
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +4 -2
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +61 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
- package/docs-site/zh/guides/viewing-results.mdx +200 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +4 -3
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +15 -13
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +9 -3
- package/docs-site/zh/reference/expect.mdx +13 -8
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +59 -3
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +33 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +640 -0
- package/src/report/built-ins/experiment-comparison.tsx +19 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +225 -50
- package/src/report/compute.ts +335 -54
- package/src/report/dual-render.test.tsx +1129 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +35 -2
- package/src/report/index.ts +47 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +229 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +113 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +220 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +126 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +168 -0
- package/src/report/react/fixtures.ts +169 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +21 -6
- package/src/report/react/render.test.tsx +180 -31
- package/src/report/react/styles.css +432 -213
- package/src/report/report.test.ts +398 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +344 -96
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +184 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +119 -95
- package/src/show/render.ts +462 -159
- package/src/show/show.test.ts +471 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
package/src/report/types.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// niceeval/report 的公开类型:指标(Metric)、维度(Dimension / flag())与计算函数
|
|
2
|
-
// 产物(即组件的 data props)。数据契约照 docs/reports.md
|
|
2
|
+
// 产物(即组件的 data props)。数据契约照 docs/feature/reports/library.md「数据计算与缓存边界」;
|
|
3
3
|
// 这些不是持久化格式,没有 format / schemaVersion 信封,兼容性跟随 npm 版本。
|
|
4
4
|
|
|
5
|
-
import type { AttemptHandle,
|
|
5
|
+
import type { AttemptHandle, SelectionWarning } from "../results/types.ts";
|
|
6
|
+
import type { AttemptLocator } from "../results/locator.ts";
|
|
7
|
+
import type { AssertionResult, Verdict } from "../types.ts";
|
|
8
|
+
import type { LocalizedLabel, ReportLocale } from "./locale.ts";
|
|
6
9
|
|
|
7
|
-
export type {
|
|
10
|
+
export type { SelectionWarning };
|
|
11
|
+
export type { AttemptLocator };
|
|
12
|
+
export type { LocalizedLabel, ReportLocale };
|
|
8
13
|
|
|
9
14
|
// ───────────────────────── 指标与聚合 ─────────────────────────
|
|
10
15
|
|
|
@@ -31,8 +36,11 @@ export interface MetricAggregate {
|
|
|
31
36
|
export interface Metric<Name extends string = string> {
|
|
32
37
|
/** MetricColumn.key 与列头的来源;同一次计算里重名是错误。 */
|
|
33
38
|
name: Name;
|
|
34
|
-
/**
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* 列头;省略时用 name。可以给按 locale 的字典({ en, "zh-CN" }),
|
|
41
|
+
* 渲染面按宿主 locale 解析,缺项回退 en(display 是 format 产物,不本地化)。
|
|
42
|
+
*/
|
|
43
|
+
label?: LocalizedLabel;
|
|
36
44
|
description?: string;
|
|
37
45
|
/** 渲染提示:越高越好还是越低越好(排序方向、轴向、涨跌配色用)。 */
|
|
38
46
|
better?: "higher" | "lower";
|
|
@@ -86,7 +94,8 @@ export type DimensionInput = Dimension | FlagRef;
|
|
|
86
94
|
export interface MetricColumn {
|
|
87
95
|
/** = metric.name,与 cells 的键对应。 */
|
|
88
96
|
key: string;
|
|
89
|
-
label
|
|
97
|
+
/** 数据层原样携带 metric.label(可本地化);渲染面用 resolveMetricLabel 按 locale 解析。 */
|
|
98
|
+
label: LocalizedLabel;
|
|
90
99
|
unit?: string;
|
|
91
100
|
/** 渲染提示:排序方向、轴向、涨跌配色。 */
|
|
92
101
|
better?: "higher" | "lower";
|
|
@@ -105,7 +114,39 @@ export interface MetricCell {
|
|
|
105
114
|
* 这个格子由哪些 attempt 算出 —— 回到证据的引用。必填(可空数组):
|
|
106
115
|
* 「每个数字点进去就是证据」是页面的核心承诺,可选字段会让深链静默缺失。
|
|
107
116
|
*/
|
|
108
|
-
refs:
|
|
117
|
+
refs: AttemptLocator[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 榜单行的元信息:rows: "experiment" 时随行(experiment 行天然有唯一的 agent/model 身份、
|
|
122
|
+
* eval 级折叠计票与「这行覆盖了多少题/多少次尝试/最近何时跑的」);其它维度不携带。
|
|
123
|
+
* web / text 面在 meta 在场时补 Model / Agent / Verdicts 列,`evals`/`attempts`/
|
|
124
|
+
* `lastRunAt` 则渲染成行键下的一行紧凑摘要——与 view 原生榜单同一份信息密度。
|
|
125
|
+
* `MetricTable` 只表达维度 × 指标,没有实体下钻——要展开到 experiment 的 Eval 或
|
|
126
|
+
* Eval 的 Attempt,用 `ExperimentList` / `EvalList`,这里不再有 `subRows`。
|
|
127
|
+
*/
|
|
128
|
+
export interface TableRowMeta<K extends string = string> {
|
|
129
|
+
agent?: string;
|
|
130
|
+
model?: string;
|
|
131
|
+
/** eval 级折叠计票(foldEvalVerdict 口径,与 view 榜单同一套):每题折成单一判定后计数。 */
|
|
132
|
+
verdicts?: { passed: number; failed: number; errored: number; skipped: number };
|
|
133
|
+
/**
|
|
134
|
+
* `rows: "experiment"` 专属:这一行覆盖的 eval 数(去重后,summarizeItems 口径,与
|
|
135
|
+
* `verdicts` 四项之和一致)。其它行维度(agent/eval/自定义…)没有「这一行是几道题」的
|
|
136
|
+
* 独立语义(题本身就是行),不携带这个字段。
|
|
137
|
+
*/
|
|
138
|
+
evals?: number;
|
|
139
|
+
/**
|
|
140
|
+
* `rows: "experiment"` 专属:这一行覆盖的 attempt 总数(原始计数,含多轮重试)。
|
|
141
|
+
* 大于 `evals` 说明存在多轮重试(early-exit 复测 / flaky 重跑);等于 `evals` 说明
|
|
142
|
+
* 每题只跑了一轮。同上,只在 `rows: "experiment"` 时语义成立。
|
|
143
|
+
*/
|
|
144
|
+
attempts?: number;
|
|
145
|
+
/**
|
|
146
|
+
* `rows: "experiment"` 专属:这一行覆盖范围内快照 `startedAt` 的最大值(最近一次运行
|
|
147
|
+
* 时间,ISO 8601,字符串比较即可比大小)。组内没有任何 item 时缺席。
|
|
148
|
+
*/
|
|
149
|
+
lastRunAt?: string;
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
/** 列键 K 来自 columns 元组的字面量 name:拼错列名编译不过,不是运行时 undefined。 */
|
|
@@ -113,7 +154,7 @@ export interface TableData<K extends string = string> {
|
|
|
113
154
|
/** 行维度名,如 "agent"。 */
|
|
114
155
|
dimension: string;
|
|
115
156
|
columns: MetricColumn[];
|
|
116
|
-
rows: { key: string; cells: Record<K, MetricCell> }[];
|
|
157
|
+
rows: { key: string; cells: Record<K, MetricCell>; meta?: TableRowMeta<K> }[];
|
|
117
158
|
}
|
|
118
159
|
|
|
119
160
|
export interface MatrixData {
|
|
@@ -199,20 +240,67 @@ export interface LineData {
|
|
|
199
240
|
}[];
|
|
200
241
|
}
|
|
201
242
|
|
|
243
|
+
/**
|
|
244
|
+
* 一组 experiment(如自定义报告里同一 `<Section>` 内的全部 experiment)的摘要:
|
|
245
|
+
* experiment/eval/attempt 数量、eval 级折叠计票、通过率、总成本、最后运行时间——
|
|
246
|
+
* 恢复旧 `GroupSelector` 卡片曾展示的信息密度,但通过率是官方 `MetricCell` 形态,
|
|
247
|
+
* 不是裸数字,渲染面不用另外拼格式。
|
|
248
|
+
*/
|
|
249
|
+
export interface GroupSummaryData {
|
|
250
|
+
/** 组内 experiment 数(去重后的 experimentId 个数)。 */
|
|
251
|
+
experiments: number;
|
|
252
|
+
/**
|
|
253
|
+
* 组内 eval 数,按完整身份键(experimentId + eval id)去重——多 experiment 的组里
|
|
254
|
+
* 两个 experiment 各自的同名 eval(如都叫 "algebra/a")算两道题,不会被误合并成一道。
|
|
255
|
+
*/
|
|
256
|
+
evals: number;
|
|
257
|
+
/** 组内 attempt 总数(原始计数,一轮 attempt 一票,不折叠)。 */
|
|
258
|
+
attempts: number;
|
|
259
|
+
/**
|
|
260
|
+
* eval 级折叠计票:同一 eval 的多轮 attempt 先折成一个判定(`foldEvalVerdict`,任一轮
|
|
261
|
+
* 通过则通过,否则取最严重的),再计数——与 `TableRowMeta.verdicts`、view 榜单同一口径,
|
|
262
|
+
* 不是 attempt 原始票数的直接计票。
|
|
263
|
+
*/
|
|
264
|
+
verdicts: { passed: number; failed: number; errored: number; skipped: number };
|
|
265
|
+
/**
|
|
266
|
+
* 组的通过率:eval 级折叠计票的 `passed / (passed + failed + errored)`(`skipped` 不进
|
|
267
|
+
* 分母)——这是旧 `GroupSelector` 卡片的口径,不是 `OverviewData.totals.passRate` 那种
|
|
268
|
+
* `computeCell` 两级聚合(两者服务不同问题:「这组题多少算过」vs「整体质量几分」)。
|
|
269
|
+
* 分母为 0(组内没有任何已跑的 eval)时 `value` 为 `null`,不编 0%。
|
|
270
|
+
*/
|
|
271
|
+
passRate: MetricCell;
|
|
272
|
+
/** 组内可测成本(`attemptCostUSD`)求和;一次 attempt 都没报成本 = `null`,不编 `0`。 */
|
|
273
|
+
totalCostUSD: number | null;
|
|
274
|
+
/** 组内快照 `startedAt` 的最大值(字符串比较,ISO 8601 天然可比);组内没有任何 item 时缺席。 */
|
|
275
|
+
lastRunAt?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
202
278
|
export interface OverviewData {
|
|
203
279
|
snapshots: { experimentId: string; agent: string; model?: string; startedAt: string }[];
|
|
204
280
|
totals: {
|
|
205
281
|
evals: number;
|
|
206
282
|
attempts: number;
|
|
283
|
+
/**
|
|
284
|
+
* 四个 attempt 原始判定计票(一个 attempt 一票),独立于 `passRate`:驱动页头的
|
|
285
|
+
* 判定计数展示,不是通过率公式的输入——不要从这四个数现场重算百分比。
|
|
286
|
+
*/
|
|
207
287
|
passed: number;
|
|
208
288
|
failed: number;
|
|
209
289
|
errored: number;
|
|
210
290
|
skipped: number;
|
|
291
|
+
/**
|
|
292
|
+
* 通过率的唯一官方口径:`computeCell(passRate, items)`,与 `MetricTable.data(...,
|
|
293
|
+
* columns: [passRate])` 同一台两级聚合引擎(题内折叠 perEval、跨题折叠 across,默认都是
|
|
294
|
+
* mean)——一道题内多个 attempt 部分通过,贡献的是小数份额而不是二元票。`samples`/`total`
|
|
295
|
+
* 是两级聚合口径下的 attempt 计数(`total` 含 skipped,`samples` 不含),不等于上面四个
|
|
296
|
+
* verdict 计票的任何一个之和。
|
|
297
|
+
*/
|
|
298
|
+
passRate: MetricCell;
|
|
211
299
|
/** 任一 attempt 报了成本才有;全缺 = null,不编 0。 */
|
|
212
300
|
costUSD: number | null;
|
|
213
301
|
durationMs: number;
|
|
214
302
|
};
|
|
215
|
-
/**
|
|
303
|
+
/** Selection 的警告随行(结构化,含渲染好的 message),RunOverview 直接渲染。 */
|
|
216
304
|
warnings: SelectionWarning[];
|
|
217
305
|
}
|
|
218
306
|
|
|
@@ -239,20 +327,91 @@ export interface DeltaData<K extends string = string> {
|
|
|
239
327
|
}[];
|
|
240
328
|
}
|
|
241
329
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
330
|
+
// ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
|
|
331
|
+
//
|
|
332
|
+
// 三个组件按「experiment → experimentId × eval → attempt」逐级下钻,固定展示实体事实,
|
|
333
|
+
// 没有列配置(docs/feature/reports/library.md「实体列表」)。每一级都以下一级的 `AttemptListItem[]`
|
|
334
|
+
// 收尾——同一个类型既是 `AttemptList` 自己的 items,也是 `ExperimentListEvalRow.attempts` /
|
|
335
|
+
// `EvalListItem.attempts` 的元素,报告作者可以直接把这些嵌套数组喂给 `<AttemptList items={...} />`。
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* `AttemptList` 一项 = 一个 Attempt:身份、判定、断言、error、耗时、成本和 locator。
|
|
339
|
+
* `ExperimentList` / `EvalList` 的下钻数组复用同一个类型,不是各自的精简版。
|
|
340
|
+
*/
|
|
341
|
+
export interface AttemptListItem {
|
|
342
|
+
evalId: string;
|
|
343
|
+
experimentId: string;
|
|
344
|
+
attempt: number;
|
|
345
|
+
agent: string;
|
|
346
|
+
verdict: Verdict;
|
|
347
|
+
error?: string;
|
|
348
|
+
assertions: AssertionResult[];
|
|
349
|
+
durationMs: number;
|
|
350
|
+
costUSD?: number;
|
|
351
|
+
locator: AttemptLocator;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* `ExperimentList` 一项里,一个 Eval 的展开行:折叠判定(`foldEvalVerdict`)、失败原因摘要
|
|
356
|
+
* (`error` → `skipReason` → 未通过的 gate 断言,`reasonFor` 的口径,soft 断言永不进入)、
|
|
357
|
+
* 该 Eval 内 attempt 的平均耗时/成本(两级聚合引擎在单一 eval 上退化成组内均值),以及这道题
|
|
358
|
+
* 的全部 Attempt(升序,供进一步展开到 `AttemptList`)。
|
|
359
|
+
*/
|
|
360
|
+
export interface ExperimentListEvalRow {
|
|
361
|
+
evalId: string;
|
|
362
|
+
/** 折叠判定(任一 attempt 通过则通过,否则取最严重的)。 */
|
|
363
|
+
verdict: Verdict;
|
|
364
|
+
reason?: string;
|
|
365
|
+
/** 这道题内 attempt 的平均耗时(`computeCell(durationMs, …)`,单一 eval 分组下即均值)。 */
|
|
366
|
+
duration: MetricCell;
|
|
367
|
+
/** 这道题内 attempt 的平均成本。 */
|
|
368
|
+
cost: MetricCell;
|
|
369
|
+
/** 这道题的全部 Attempt,按 attempt 序号升序。 */
|
|
370
|
+
attempts: AttemptListItem[];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* `ExperimentList.data(selection)` 的一项 = 一个 experiment:身份(experimentId/agent/model)、
|
|
375
|
+
* 声明的 flags、Eval 判定构成(`foldEvalVerdict` 计票,与 view 榜单同一口径)、官方两级聚合
|
|
376
|
+
* 汇总指标(passRate/cost/duration/tokens,直接来自 `computeCell`,不现场重算),以及展开到
|
|
377
|
+
* 这个 experiment 每道 Eval 的 `evalRows`(按 eval id 升序)。
|
|
378
|
+
*/
|
|
379
|
+
export interface ExperimentListItem {
|
|
380
|
+
experimentId: string;
|
|
381
|
+
agent: string;
|
|
382
|
+
model?: string;
|
|
383
|
+
flags?: Record<string, unknown>;
|
|
384
|
+
/** eval 级折叠计票(foldEvalVerdict 口径,与 `TableRowMeta.verdicts`、view 榜单同一套)。 */
|
|
385
|
+
verdicts: { passed: number; failed: number; errored: number; skipped: number };
|
|
386
|
+
/** 官方两级聚合口径,与 `MetricTable.data(..., columns: [passRate])` 同一台引擎。 */
|
|
387
|
+
passRate: MetricCell;
|
|
388
|
+
cost: MetricCell;
|
|
389
|
+
duration: MetricCell;
|
|
390
|
+
tokens: MetricCell;
|
|
391
|
+
/** 这个 experiment 覆盖的 eval 数(去重后,与 `verdicts` 四项之和一致)。 */
|
|
392
|
+
evals: number;
|
|
393
|
+
/** 这个 experiment 覆盖的 attempt 总数(原始计数,含多轮重试)。 */
|
|
394
|
+
attempts: number;
|
|
395
|
+
/** 所含快照中最近的 startedAt。 */
|
|
396
|
+
lastRunAt: string;
|
|
397
|
+
/** 展开到这个 experiment 的 Eval,按 eval id 升序。 */
|
|
398
|
+
evalRows: ExperimentListEvalRow[];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* `EvalList.data(selection)` 的一项 = 一个 `experimentId + evalId`(同一个 Eval 跑在两个
|
|
403
|
+
* experiment 上是两条不同结果,不合并)。判定、分数(examScore 的两级聚合)、这道题内 attempt
|
|
404
|
+
* 的平均耗时/成本,失败原因摘要(与 `ExperimentListEvalRow.reason` 同一口径),外加展开到这道题
|
|
405
|
+
* 全部 Attempt 的 `attempts`(按 attempt 序号升序)。
|
|
406
|
+
*/
|
|
407
|
+
export interface EvalListItem {
|
|
408
|
+
evalId: string;
|
|
409
|
+
experimentId: string;
|
|
410
|
+
verdict: Verdict;
|
|
411
|
+
reason?: string;
|
|
412
|
+
/** examScore 的两级聚合;单一 eval 分组下即这道题的题级分数。 */
|
|
413
|
+
score: MetricCell;
|
|
414
|
+
duration: MetricCell;
|
|
415
|
+
cost: MetricCell;
|
|
416
|
+
attempts: AttemptListItem[];
|
|
258
417
|
}
|
package/src/report/web.ts
CHANGED
|
@@ -5,36 +5,63 @@
|
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import type { ReactNode } from "react";
|
|
7
7
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// 编译模式下都可渲染;只定义一次,不覆盖宿主已有的全局。
|
|
13
|
-
const g = globalThis as { React?: unknown };
|
|
14
|
-
if (g.React === undefined) g.React = React;
|
|
15
|
-
import type { AttemptRef } from "../results/index.ts";
|
|
16
|
-
import { runWithWebContext, validateReportTree, type WebContext } from "./tree.ts";
|
|
17
|
-
import { prepareDefaultReportData, runWithDefaultReportData } from "./default-report.tsx";
|
|
8
|
+
import type { AttemptLocator } from "../results/locator.ts";
|
|
9
|
+
import type { SelectionWarning } from "../results/types.ts";
|
|
10
|
+
import { resolveReportTree, runWithWebContext, validateReportTree, type WebContext } from "./tree.ts";
|
|
11
|
+
import { DEFAULT_REPORT_LOCALE, type ReportLocale } from "./locale.ts";
|
|
18
12
|
import type { ReportContext, ReportDefinition } from "./report.ts";
|
|
19
13
|
|
|
20
14
|
export interface StaticHtmlOptions {
|
|
21
|
-
/** 证据室深链;缺省用 view 的 attempt 路由 `#/attempt
|
|
22
|
-
attemptHref?: (
|
|
15
|
+
/** 证据室深链;缺省用 view 的 attempt 路由 `#/attempt/@<locator>`(单段、不透明)。 */
|
|
16
|
+
attemptHref?: (locator: AttemptLocator) => string;
|
|
17
|
+
/** 官方组件 chrome 文案的 locale;默认 "en"。 */
|
|
18
|
+
locale?: ReportLocale;
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* 挑选警告的 HTML 形态:宿主级前置块,与 RunOverview 里的警告用同一套结构和类名
|
|
23
|
+
* (`.nre nre-report-warnings` 外壳内一个 `ul.nre-warnings` + `li.nre-warning[data-kind]`,
|
|
24
|
+
* 复用 styles.css 已有的 `.nre .nre-warnings` 样式)。经 renderToStaticMarkup 走 React,
|
|
25
|
+
* message 文本自动转义,不裸拼 HTML。裸跑 / --report 都在报告顶上如实报残缺,不静默。
|
|
26
|
+
*/
|
|
27
|
+
function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
|
|
28
|
+
return renderToStaticMarkup(
|
|
29
|
+
React.createElement(
|
|
30
|
+
"div",
|
|
31
|
+
{ className: "nre nre-report-warnings" },
|
|
32
|
+
React.createElement(
|
|
33
|
+
"ul",
|
|
34
|
+
{ className: "nre-warnings" },
|
|
35
|
+
warnings.map((w, i) =>
|
|
36
|
+
React.createElement("li", { key: i, className: "nre-warning", "data-kind": w.kind }, w.message),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* build → 渲染前解析数据组件(唯一的 await 边界)→ 树校验(与 text 宿主同一遍)→ 静态渲染
|
|
45
|
+
* web 面;Selection 有挑选警告时在报告顶部前置一块警告 HTML;报告树里的 RunOverview
|
|
46
|
+
* 已经渲染同一条时不重复。
|
|
47
|
+
*/
|
|
26
48
|
export async function renderReportToStaticHtml(
|
|
27
49
|
definition: ReportDefinition,
|
|
28
50
|
ctx: ReportContext,
|
|
29
51
|
options?: StaticHtmlOptions,
|
|
30
52
|
): Promise<string> {
|
|
31
53
|
const node = await definition.build(ctx);
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
const resolved = await resolveReportTree(node);
|
|
55
|
+
validateReportTree(resolved);
|
|
34
56
|
const webCtx: WebContext = {
|
|
35
|
-
attemptHref: options?.attemptHref ?? ((
|
|
57
|
+
attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
|
|
58
|
+
locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
|
|
36
59
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
|
|
61
|
+
const missingWarnings = ctx.selection.warnings.filter((warning) => {
|
|
62
|
+
const escapedMessage = renderToStaticMarkup(React.createElement(React.Fragment, null, warning.message));
|
|
63
|
+
return !body.includes(escapedMessage);
|
|
64
|
+
});
|
|
65
|
+
const warnings = missingWarnings.length > 0 ? renderSelectionWarningsHtml(missingWarnings) : "";
|
|
66
|
+
return warnings + body;
|
|
40
67
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// buildAnnotatedEvalSource 的单测(定稿见 docs/concepts.md「标注 Eval 源码」)。
|
|
2
|
+
// 覆盖:同一行多条断言、无 loc 断言进 unmapped、loc 指向别的文件/越界行也进 unmapped
|
|
3
|
+
// (never silently dropped 的边界情况)、空断言数组、summary 计数、哈希与归一化行为。
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import type { AssertionResult } from "../types.ts";
|
|
7
|
+
import { buildAnnotatedEvalSource } from "./annotated-source.ts";
|
|
8
|
+
import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
|
|
9
|
+
|
|
10
|
+
const SOURCE_PATH = "evals/weather.eval.ts";
|
|
11
|
+
|
|
12
|
+
function assertion(over: Partial<AssertionResult> & Pick<AssertionResult, "name">): AssertionResult {
|
|
13
|
+
return {
|
|
14
|
+
severity: "soft",
|
|
15
|
+
score: 1,
|
|
16
|
+
passed: true,
|
|
17
|
+
...over,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("buildAnnotatedEvalSource", () => {
|
|
22
|
+
it("maps multiple assertions on the same line, preserving input order", () => {
|
|
23
|
+
const content = "line 1\nline 2\nline 3\n";
|
|
24
|
+
const a1 = assertion({ name: "first", loc: { file: SOURCE_PATH, line: 2 } });
|
|
25
|
+
const a2 = assertion({ name: "second", loc: { file: SOURCE_PATH, line: 2 } });
|
|
26
|
+
const a3 = assertion({ name: "third", loc: { file: SOURCE_PATH, line: 1 } });
|
|
27
|
+
|
|
28
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, [a1, a2, a3]);
|
|
29
|
+
|
|
30
|
+
expect(model.lines).toHaveLength(3);
|
|
31
|
+
expect(model.lines[0]!.assertions.map((a) => a.name)).toEqual(["third"]);
|
|
32
|
+
expect(model.lines[1]!.assertions.map((a) => a.name)).toEqual(["first", "second"]);
|
|
33
|
+
expect(model.lines[2]!.assertions).toEqual([]);
|
|
34
|
+
expect(model.unmapped).toEqual([]);
|
|
35
|
+
expect(model.summary.mappedAssertions).toBe(3);
|
|
36
|
+
expect(model.summary.unmappedAssertions).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("buckets an assertion with no SourceLoc into unmapped, never dropping it", () => {
|
|
40
|
+
const content = "only line\n";
|
|
41
|
+
const withLoc = assertion({ name: "has-loc", loc: { file: SOURCE_PATH, line: 1 } });
|
|
42
|
+
const withoutLoc = assertion({ name: "no-loc" });
|
|
43
|
+
|
|
44
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, [withLoc, withoutLoc]);
|
|
45
|
+
|
|
46
|
+
expect(model.lines[0]!.assertions.map((a) => a.name)).toEqual(["has-loc"]);
|
|
47
|
+
expect(model.unmapped.map((a) => a.name)).toEqual(["no-loc"]);
|
|
48
|
+
expect(model.summary.totalAssertions).toBe(2);
|
|
49
|
+
expect(model.summary.mappedAssertions).toBe(1);
|
|
50
|
+
expect(model.summary.unmappedAssertions).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("buckets an assertion whose loc points at a different file into unmapped", () => {
|
|
54
|
+
const content = "a\nb\n";
|
|
55
|
+
const elsewhere = assertion({ name: "elsewhere", loc: { file: "evals/other.eval.ts", line: 1 } });
|
|
56
|
+
|
|
57
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, [elsewhere]);
|
|
58
|
+
|
|
59
|
+
expect(model.lines.every((l) => l.assertions.length === 0)).toBe(true);
|
|
60
|
+
expect(model.unmapped).toEqual([elsewhere]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("buckets an assertion whose loc.line is out of range into unmapped", () => {
|
|
64
|
+
const content = "a\nb\n"; // 2 lines
|
|
65
|
+
const tooHigh = assertion({ name: "too-high", loc: { file: SOURCE_PATH, line: 3 } });
|
|
66
|
+
const tooLow = assertion({ name: "too-low", loc: { file: SOURCE_PATH, line: 0 } });
|
|
67
|
+
|
|
68
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, [tooHigh, tooLow]);
|
|
69
|
+
|
|
70
|
+
expect(model.lines.every((l) => l.assertions.length === 0)).toBe(true);
|
|
71
|
+
expect(model.unmapped.map((a) => a.name).sort()).toEqual(["too-high", "too-low"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("handles an empty assertions array: lines are still built, everything else is zeroed", () => {
|
|
75
|
+
const content = "a\nb\nc\n";
|
|
76
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, []);
|
|
77
|
+
|
|
78
|
+
expect(model.lines).toHaveLength(3);
|
|
79
|
+
expect(model.lines.every((l) => l.assertions.length === 0)).toBe(true);
|
|
80
|
+
expect(model.unmapped).toEqual([]);
|
|
81
|
+
expect(model.summary).toEqual({
|
|
82
|
+
totalAssertions: 0,
|
|
83
|
+
mappedAssertions: 0,
|
|
84
|
+
unmappedAssertions: 0,
|
|
85
|
+
passed: 0,
|
|
86
|
+
failed: 0,
|
|
87
|
+
gate: 0,
|
|
88
|
+
soft: 0,
|
|
89
|
+
totalLines: 3,
|
|
90
|
+
annotatedLines: 0,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("computes summary counts across passed/failed and gate/soft, mapped and unmapped alike", () => {
|
|
95
|
+
const content = "line 1\nline 2\n";
|
|
96
|
+
const assertions = [
|
|
97
|
+
assertion({ name: "a", severity: "gate", passed: true, loc: { file: SOURCE_PATH, line: 1 } }),
|
|
98
|
+
assertion({ name: "b", severity: "gate", passed: false, loc: { file: SOURCE_PATH, line: 1 } }),
|
|
99
|
+
assertion({ name: "c", severity: "soft", passed: true, loc: { file: SOURCE_PATH, line: 2 } }),
|
|
100
|
+
assertion({ name: "d", severity: "soft", passed: false }), // unmapped
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, assertions);
|
|
104
|
+
|
|
105
|
+
expect(model.summary).toEqual({
|
|
106
|
+
totalAssertions: 4,
|
|
107
|
+
mappedAssertions: 3,
|
|
108
|
+
unmappedAssertions: 1,
|
|
109
|
+
passed: 2,
|
|
110
|
+
failed: 2,
|
|
111
|
+
gate: 2,
|
|
112
|
+
soft: 2,
|
|
113
|
+
totalLines: 2,
|
|
114
|
+
annotatedLines: 2,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not produce a phantom trailing blank line for source text ending in a newline", () => {
|
|
119
|
+
const content = "a\nb\n";
|
|
120
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, []);
|
|
121
|
+
expect(model.lines.map((l) => l.text)).toEqual(["a", "b"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps a real trailing blank line when the source has one (double newline)", () => {
|
|
125
|
+
const content = "a\nb\n\n";
|
|
126
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, []);
|
|
127
|
+
expect(model.lines.map((l) => l.text)).toEqual(["a", "b", ""]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("treats an empty source file as a single empty line", () => {
|
|
131
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content: "" }, []);
|
|
132
|
+
expect(model.lines).toEqual([{ line: 1, text: "", assertions: [] }]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("sourceSha256 matches hashEvalSource(normalizeEvalSource(content)) and is stable across CRLF/LF", () => {
|
|
136
|
+
const lf = "a\nb\nc\n";
|
|
137
|
+
const crlf = "a\r\nb\r\nc\r\n";
|
|
138
|
+
|
|
139
|
+
const modelLf = buildAnnotatedEvalSource({ path: SOURCE_PATH, content: lf }, []);
|
|
140
|
+
const modelCrlf = buildAnnotatedEvalSource({ path: SOURCE_PATH, content: crlf }, []);
|
|
141
|
+
|
|
142
|
+
expect(modelLf.sourceSha256).toBe(hashEvalSource(normalizeEvalSource(lf)));
|
|
143
|
+
expect(modelLf.sourceSha256).toBe(modelCrlf.sourceSha256);
|
|
144
|
+
expect(modelLf.lines.map((l) => l.text)).toEqual(modelCrlf.lines.map((l) => l.text));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// AnnotatedEvalSource:标注 Eval 源码(定稿见 docs/concepts.md「结果数据与报告」、
|
|
2
|
+
// docs/concepts.md「Attempt 证据」)。把一份 eval 源码文本与该次运行产出的
|
|
3
|
+
// AssertionResult[] 叠在一起 —— 每条断言按 SourceLoc 标回源码行,没有(或够不上)行映射的
|
|
4
|
+
// 断言进 `unmapped` 桶,不静默丢弃。这是共享 model:未来的网页 CodeView 与
|
|
5
|
+
// `show --eval` 文本 renderer 只消费这同一份数据,不各自重新分桶。
|
|
6
|
+
//
|
|
7
|
+
// 泛化自 src/view/app/lib/transcript-data.tsx 的 indexAsserts()(Map<string, Assertion[]>
|
|
8
|
+
// + noloc 兜底桶的先例)——同一套"按 loc 分桶、没有 loc 进兜底"的思路,换成源码行数组
|
|
9
|
+
// (不是 Map)存放,外加 sourceSha256 / summary 计数。indexTurns() 那部分(events → 轮次)
|
|
10
|
+
// 是 ExecutionTree 的地盘,不在这个模型里。
|
|
11
|
+
//
|
|
12
|
+
// 本模块不 import react/jsx,纯数据 + 纯函数,可以在任何 Node 语境(CLI 的 show、
|
|
13
|
+
// view 的 server 端数据准备)调用。
|
|
14
|
+
|
|
15
|
+
import type { AssertionResult } from "../types.ts";
|
|
16
|
+
import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
|
|
17
|
+
|
|
18
|
+
/** 一行源码 + 映射到这一行的全部断言(保持原始顺序;可以是空数组)。 */
|
|
19
|
+
export interface AnnotatedSourceLine {
|
|
20
|
+
/** 1-indexed 行号,与 SourceLoc.line 同一坐标系。 */
|
|
21
|
+
line: number;
|
|
22
|
+
/** 这一行的原始文本(已按 normalizeEvalSource 归一化,不含行尾换行符)。 */
|
|
23
|
+
text: string;
|
|
24
|
+
/**
|
|
25
|
+
* 映射到这一行的断言,按输入顺序(通常即执行顺序)排列。每条断言自带 severity / score /
|
|
26
|
+
* passed / detail / evidence —— 这就是"状态 / 严重度 / 分数 / detail / evidence"标回源码行
|
|
27
|
+
* 的完整信息,不需要额外的行级折叠字段:折叠成单一显示态是 renderer 的展示决定,不是这份
|
|
28
|
+
* 领域 model 的职责(与 indexAsserts() 的先例一致,它同样只分桶、不折叠)。
|
|
29
|
+
*/
|
|
30
|
+
assertions: AssertionResult[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AnnotatedEvalSourceSummary {
|
|
34
|
+
/** 参与统计的断言总数 = mappedAssertions + unmappedAssertions。 */
|
|
35
|
+
totalAssertions: number;
|
|
36
|
+
/** 成功映射到某一行源码的断言数。 */
|
|
37
|
+
mappedAssertions: number;
|
|
38
|
+
/** 落进 unmapped 桶的断言数。 */
|
|
39
|
+
unmappedAssertions: number;
|
|
40
|
+
/** 按 passed 计票(映射与未映射的断言都计入)。 */
|
|
41
|
+
passed: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
/** 按 severity 计票(映射与未映射的断言都计入)。 */
|
|
44
|
+
gate: number;
|
|
45
|
+
soft: number;
|
|
46
|
+
/** 源码总行数,等于 lines.length。 */
|
|
47
|
+
totalLines: number;
|
|
48
|
+
/** 至少挂了一条断言的行数。 */
|
|
49
|
+
annotatedLines: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AnnotatedEvalSource {
|
|
53
|
+
/** 项目相对路径,与 SourceArtifact.path / SourceLoc.file 同一约定。 */
|
|
54
|
+
sourcePath: string;
|
|
55
|
+
/** 归一化后源码文本的 SHA-256(与 captureEvalSource() 的算法一致,见 source-hash.ts)。 */
|
|
56
|
+
sourceSha256: string;
|
|
57
|
+
lines: AnnotatedSourceLine[];
|
|
58
|
+
/**
|
|
59
|
+
* 没有落到 lines 里任何一行的断言,原始顺序保留,永不丢弃。三种情况都进这里:
|
|
60
|
+
* 没有 SourceLoc(如 skip 前就出错的 eval、或调用方尚未收集到位置)、SourceLoc 指向
|
|
61
|
+
* 另一个文件(loc.file !== sourcePath —— 罕见,如断言写在共享 helper 里)、
|
|
62
|
+
* SourceLoc 的行号落在这份源码的行范围之外(源码与断言记录不同步的边界情况)。
|
|
63
|
+
* 后两种不是文档明确要求的场景,但"never silently dropped"的契约对它们同样成立 ——
|
|
64
|
+
* 这份 model 是分桶断言的唯一出口,不属于任何一行就必须出现在这里。
|
|
65
|
+
*/
|
|
66
|
+
unmapped: AssertionResult[];
|
|
67
|
+
summary: AnnotatedEvalSourceSummary;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 纯函数:给定一份源码文本(未归一化也可以,内部会 normalizeEvalSource)与一批断言,
|
|
72
|
+
* 产出标注好的 AnnotatedEvalSource。幂等 / 无 IO —— 可以在渲染路径上安全调用。
|
|
73
|
+
*
|
|
74
|
+
* 断言 → 源码行的映射规则:`a.loc` 存在、`a.loc.file === source.path`、且
|
|
75
|
+
* `1 <= a.loc.line <= lines.length` 三者同时成立才落到对应行;否则进 `unmapped`。
|
|
76
|
+
*/
|
|
77
|
+
export function buildAnnotatedEvalSource(
|
|
78
|
+
source: { path: string; content: string },
|
|
79
|
+
assertions: readonly AssertionResult[],
|
|
80
|
+
): AnnotatedEvalSource {
|
|
81
|
+
const normalized = normalizeEvalSource(source.content);
|
|
82
|
+
// 末尾的单个换行符不产出幻影空行(源码文件几乎总以换行符收尾);中间的空行原样保留为一行。
|
|
83
|
+
const body = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
84
|
+
const rawLines = body.length === 0 && normalized.length === 0 ? [""] : body.split("\n");
|
|
85
|
+
|
|
86
|
+
const lines: AnnotatedSourceLine[] = rawLines.map((text, i) => ({ line: i + 1, text, assertions: [] }));
|
|
87
|
+
|
|
88
|
+
const unmapped: AssertionResult[] = [];
|
|
89
|
+
let mappedAssertions = 0;
|
|
90
|
+
let passed = 0;
|
|
91
|
+
let failed = 0;
|
|
92
|
+
let gate = 0;
|
|
93
|
+
let soft = 0;
|
|
94
|
+
|
|
95
|
+
for (const assertion of assertions) {
|
|
96
|
+
if (assertion.passed) passed++;
|
|
97
|
+
else failed++;
|
|
98
|
+
if (assertion.severity === "gate") gate++;
|
|
99
|
+
else soft++;
|
|
100
|
+
|
|
101
|
+
const loc = assertion.loc;
|
|
102
|
+
const target = loc && loc.file === source.path && loc.line >= 1 && loc.line <= lines.length ? lines[loc.line - 1] : undefined;
|
|
103
|
+
if (target) {
|
|
104
|
+
target.assertions.push(assertion);
|
|
105
|
+
mappedAssertions++;
|
|
106
|
+
} else {
|
|
107
|
+
unmapped.push(assertion);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
sourcePath: source.path,
|
|
113
|
+
sourceSha256: hashEvalSource(normalized),
|
|
114
|
+
lines,
|
|
115
|
+
unmapped,
|
|
116
|
+
summary: {
|
|
117
|
+
totalAssertions: assertions.length,
|
|
118
|
+
mappedAssertions,
|
|
119
|
+
unmappedAssertions: unmapped.length,
|
|
120
|
+
passed,
|
|
121
|
+
failed,
|
|
122
|
+
gate,
|
|
123
|
+
soft,
|
|
124
|
+
totalLines: lines.length,
|
|
125
|
+
annotatedLines: lines.reduce((n, l) => n + (l.assertions.length > 0 ? 1 : 0), 0),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|