niceeval 0.3.0 → 0.4.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.
- package/README.zh.md +11 -0
- package/docs/README.md +2 -1
- package/docs/adapters/collection.md +1 -1
- package/docs/adapters/contract.md +6 -5
- package/docs/adapters/reference/claude-code-otel-telemetry.md +1 -1
- package/docs/assertions.md +0 -1
- package/docs/capabilities-by-construction.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/concepts.md +2 -2
- package/docs/e2e-ci.md +135 -98
- package/docs/eval-authoring.md +3 -3
- package/docs/experiments.md +7 -4
- package/docs/observability.md +9 -5
- package/docs/origin-integration.md +18 -18
- package/docs/references.md +1 -1
- package/docs/reports.md +551 -0
- package/docs/results-format.md +3 -3
- package/docs/results-lib.md +191 -0
- package/docs/runner.md +1 -1
- package/docs/sandbox.md +1 -1
- package/docs/source-map.md +22 -2
- package/docs/tier-sync.md +74 -58
- package/docs/view.md +39 -5
- package/package.json +29 -3
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/claude-code.ts +18 -1
- package/src/agents/codex.ts +3 -2
- package/src/agents/index.ts +16 -0
- package/src/agents/openai-compat.test.ts +56 -0
- package/src/agents/openai-compat.ts +151 -0
- package/src/agents/types.ts +3 -1
- package/src/cli.ts +3 -2
- package/src/context/context.test.ts +78 -1
- package/src/context/context.ts +19 -11
- package/src/context/session.ts +2 -0
- package/src/context/types.ts +11 -5
- package/src/i18n/en.ts +3 -4
- package/src/i18n/zh-CN.ts +2 -3
- package/src/o11y/cost.test.ts +40 -0
- package/src/o11y/cost.ts +27 -5
- package/src/o11y/derive.ts +2 -2
- package/src/o11y/otlp/mappers/claude-code.test.ts +31 -0
- package/src/o11y/otlp/mappers/claude-code.ts +24 -0
- package/src/o11y/otlp/mappers/index.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +127 -0
- package/src/o11y/prices.json +176 -119
- package/src/o11y/types.ts +2 -1
- package/src/report/aggregate.ts +215 -0
- package/src/report/compute.ts +405 -0
- package/src/report/format.ts +47 -0
- package/src/report/index.ts +40 -0
- package/src/report/metrics.ts +96 -0
- package/src/report/react/CaseList.tsx +70 -0
- package/src/report/react/DeltaTable.tsx +79 -0
- package/src/report/react/MetricMatrix.tsx +75 -0
- package/src/report/react/MetricScatter.tsx +217 -0
- package/src/report/react/MetricTable.tsx +71 -0
- package/src/report/react/RunOverview.tsx +87 -0
- package/src/report/react/Scoreboard.tsx +87 -0
- package/src/report/react/cell.tsx +49 -0
- package/src/report/react/colors.ts +42 -0
- package/src/report/react/data.ts +17 -0
- package/src/report/react/fixtures.ts +236 -0
- package/src/report/react/format.ts +34 -0
- package/src/report/react/index.tsx +34 -0
- package/src/report/react/render.test.tsx +303 -0
- package/src/report/react/styles.css +302 -0
- package/src/report/report.test.ts +667 -0
- package/src/report/types.ts +210 -0
- package/src/results/copy.ts +200 -0
- package/src/results/format.ts +75 -0
- package/src/results/index.ts +27 -0
- package/src/results/open.ts +207 -0
- package/src/results/results.test.ts +359 -0
- package/src/results/select.ts +101 -0
- package/src/results/types.ts +98 -0
- package/src/runner/attempt.ts +3 -1
- package/src/runner/report.test.ts +111 -0
- package/src/runner/report.ts +52 -1
- package/src/runner/reporters/artifacts.ts +1 -1
- package/src/runner/reporters/braintrust.test.ts +106 -0
- package/src/runner/reporters/braintrust.ts +197 -0
- package/src/runner/reporters/index.ts +3 -1
- package/src/runner/run.ts +30 -19
- package/src/runner/types.ts +17 -0
- package/src/sandbox/types.ts +3 -3
- package/src/view/app/App.tsx +82 -17
- package/src/view/app/components/CostScoreChart.tsx +127 -0
- package/src/view/app/i18n.ts +10 -1
- package/src/view/app/lib/attempt-route.test.ts +89 -0
- package/src/view/app/lib/attempt-route.ts +52 -0
- package/src/view/app/lib/chart.ts +72 -0
- package/src/view/app/lib/outcome.ts +1 -1
- package/src/view/app/types.ts +4 -5
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/src/view/index.ts +12 -18
- package/src/view/loader.ts +14 -7
- package/src/view/shared/types.ts +14 -1
- package/src/view/styles.css +43 -0
- package/src/view/template.html +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// RunOverview:页头 KPI 条——「这批数据是什么」。
|
|
2
|
+
// 数字下面标注数据来源(几个快照、何时跑的);warnings 有内容时直接显示在条内,
|
|
3
|
+
// 诚实不靠使用者记得渲染(docs/reports.md「第一档」行为清单)。
|
|
4
|
+
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import type { OverviewData } from "./data.ts";
|
|
7
|
+
import { MISSING_TEXT, cx, formatDurationMs, formatPercent, formatUSD } from "./format.ts";
|
|
8
|
+
|
|
9
|
+
export function RunOverview({
|
|
10
|
+
data,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
data: OverviewData;
|
|
14
|
+
className?: string;
|
|
15
|
+
}): ReactElement {
|
|
16
|
+
const { totals } = data;
|
|
17
|
+
// 通过率口径与内置 passRate 指标一致:skipped → null 不进分母,errored/failed 计 0
|
|
18
|
+
const judged = totals.passed + totals.failed + totals.errored;
|
|
19
|
+
const passRate = judged > 0 ? formatPercent(totals.passed / judged) : null;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<header className={cx("nre", "nre-overview", className)}>
|
|
23
|
+
<dl className="nre-kpis">
|
|
24
|
+
<div className="nre-kpi">
|
|
25
|
+
<dt>Snapshots</dt>
|
|
26
|
+
<dd>{data.snapshots.length}</dd>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="nre-kpi">
|
|
29
|
+
<dt>Evals</dt>
|
|
30
|
+
<dd>{totals.evals}</dd>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="nre-kpi">
|
|
33
|
+
<dt>attempts</dt>
|
|
34
|
+
<dd>{totals.attempts}</dd>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="nre-kpi">
|
|
37
|
+
<dt>Pass rate</dt>
|
|
38
|
+
<dd>{passRate ?? <span className="nre-missing">{MISSING_TEXT}</span>}</dd>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="nre-kpi">
|
|
41
|
+
<dt>Total cost</dt>
|
|
42
|
+
{/* costUSD 全缺 = null:显示缺数据,不编 $0 */}
|
|
43
|
+
<dd>
|
|
44
|
+
{totals.costUSD === null ? (
|
|
45
|
+
<span className="nre-missing">{MISSING_TEXT}</span>
|
|
46
|
+
) : (
|
|
47
|
+
formatUSD(totals.costUSD)
|
|
48
|
+
)}
|
|
49
|
+
</dd>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="nre-kpi">
|
|
52
|
+
<dt>Total duration</dt>
|
|
53
|
+
<dd>{formatDurationMs(totals.durationMs)}</dd>
|
|
54
|
+
</div>
|
|
55
|
+
</dl>
|
|
56
|
+
|
|
57
|
+
<p className="nre-outcomes">
|
|
58
|
+
<span className="nre-outcome nre-outcome-passed">passed {totals.passed}</span>
|
|
59
|
+
<span className="nre-outcome nre-outcome-failed">failed {totals.failed}</span>
|
|
60
|
+
<span className="nre-outcome nre-outcome-errored">errored {totals.errored}</span>
|
|
61
|
+
<span className="nre-outcome nre-outcome-skipped">skipped {totals.skipped}</span>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
{/* 数据来源:哪些快照、何时跑的——报告的数字都从这里来 */}
|
|
65
|
+
<p className="nre-source">
|
|
66
|
+
Source: {data.snapshots.length} snapshots
|
|
67
|
+
{data.snapshots.map((s) => (
|
|
68
|
+
<span key={`${s.experimentId}@${s.startedAt}`} className="nre-source-snapshot">
|
|
69
|
+
{s.experimentId}({s.agent}
|
|
70
|
+
{s.model ? ` · ${s.model}` : ""})@ {s.startedAt}
|
|
71
|
+
</span>
|
|
72
|
+
))}
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
{/* 选择器的警告(残缺快照等)直接渲染在条内,不静默 */}
|
|
76
|
+
{data.warnings.length > 0 && (
|
|
77
|
+
<ul className="nre-warnings">
|
|
78
|
+
{data.warnings.map((w, i) => (
|
|
79
|
+
<li key={i} className="nre-warning">
|
|
80
|
+
{w}
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
)}
|
|
85
|
+
</header>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Scoreboard:考试成绩单——总分 + 分科小计。
|
|
2
|
+
// 固定分母的口径不藏:没跑的题挣 0 分但留在分母里,科目行如实报 missing;
|
|
3
|
+
// weights 是「实际生效的权重表」,渲染出来让成绩单可审计(docs/reports.md scoreboard 公式一节)。
|
|
4
|
+
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import type { ScoreboardData } from "./data.ts";
|
|
7
|
+
import { colorClassForKey } from "./colors.ts";
|
|
8
|
+
import { cx } from "./format.ts";
|
|
9
|
+
|
|
10
|
+
export function Scoreboard({
|
|
11
|
+
data,
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
data: ScoreboardData;
|
|
15
|
+
className?: string;
|
|
16
|
+
}): ReactElement {
|
|
17
|
+
// 科目列 = 各行 subjects 的并集,按首次出现顺序;固定分母下各行本应一致,这里防御性合并
|
|
18
|
+
const subjectKeys: string[] = [];
|
|
19
|
+
for (const row of data.rows) {
|
|
20
|
+
for (const subject of row.subjects) {
|
|
21
|
+
if (!subjectKeys.includes(subject.key)) subjectKeys.push(subject.key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<section className={cx("nre", "nre-scoreboard", className)}>
|
|
27
|
+
<table className="nre-scoreboard-table">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr>
|
|
30
|
+
<th scope="col" className="nre-dimension">
|
|
31
|
+
{data.of}
|
|
32
|
+
</th>
|
|
33
|
+
<th scope="col" className="nre-total-col">
|
|
34
|
+
Total<span className="nre-full-marks">/ {data.fullMarks}</span>
|
|
35
|
+
</th>
|
|
36
|
+
{subjectKeys.map((key) => (
|
|
37
|
+
<th scope="col" key={key} className="nre-subject-col">
|
|
38
|
+
{key}
|
|
39
|
+
</th>
|
|
40
|
+
))}
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{data.rows.map((row) => (
|
|
45
|
+
<tr key={row.key}>
|
|
46
|
+
{/* 被打分者(如 agent):稳定散列上色,跨块同键同色 */}
|
|
47
|
+
<th scope="row" className={cx("nre-row-key", "nre-key", colorClassForKey(row.key))}>
|
|
48
|
+
{row.key}
|
|
49
|
+
</th>
|
|
50
|
+
<td className="nre-total">{row.total.display}</td>
|
|
51
|
+
{subjectKeys.map((key) => {
|
|
52
|
+
const subject = row.subjects.find((s) => s.key === key);
|
|
53
|
+
if (!subject) return <td key={key} className="nre-td-empty" />;
|
|
54
|
+
return (
|
|
55
|
+
<td key={key} className="nre-subject">
|
|
56
|
+
<span
|
|
57
|
+
className="nre-subject-score"
|
|
58
|
+
title={`${subject.evals} evals, weighted ${subject.earned} of ${subject.possible}`}
|
|
59
|
+
>
|
|
60
|
+
{subject.earned}/{subject.possible}
|
|
61
|
+
</span>
|
|
62
|
+
{/* 固定分母的如实注脚:没跑、按 0 计的题数 */}
|
|
63
|
+
{subject.missing > 0 && (
|
|
64
|
+
<span className="nre-subject-missing">{subject.missing} {subject.missing === 1 ? "eval" : "evals"} missing, scored 0</span>
|
|
65
|
+
)}
|
|
66
|
+
</td>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</tr>
|
|
70
|
+
))}
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
{/* 实际生效的权重表:成绩单可审计 */}
|
|
74
|
+
<p className="nre-weights">
|
|
75
|
+
权重:
|
|
76
|
+
{data.weights.length === 0
|
|
77
|
+
? "all evals ×1"
|
|
78
|
+
: data.weights.map((w) => (
|
|
79
|
+
<span key={w.prefix} className="nre-weight">
|
|
80
|
+
{w.prefix} ×{w.weight}
|
|
81
|
+
</span>
|
|
82
|
+
))}
|
|
83
|
+
{data.weights.length > 0 && <span className="nre-weight-rest">others ×1</span>}
|
|
84
|
+
</p>
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// MetricCell 的统一渲染:MetricTable / MetricMatrix / DeltaTable 共用,
|
|
2
|
+
// 保证三处对同一份数据长得一致——覆盖率角标、缺数据文案、证据链接只实现一次。
|
|
3
|
+
// 纯渲染、零 hooks;交互只有普通 <a>(下钻由使用者的 attemptHref 决定去处)。
|
|
4
|
+
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import type { AttemptRef, MetricCell } from "./data.ts";
|
|
7
|
+
import { MISSING_TEXT } from "./format.ts";
|
|
8
|
+
|
|
9
|
+
export function MetricCellView({
|
|
10
|
+
cell,
|
|
11
|
+
attemptHref,
|
|
12
|
+
}: {
|
|
13
|
+
cell: MetricCell;
|
|
14
|
+
attemptHref?: (ref: AttemptRef) => string;
|
|
15
|
+
}): ReactElement {
|
|
16
|
+
// 全 null(没有任何有效样本)→ 缺数据文案,绝不画 0;total 仍如实入 title
|
|
17
|
+
if (cell.value === null) {
|
|
18
|
+
return (
|
|
19
|
+
<span className="nre-cell nre-cell-missing">
|
|
20
|
+
<span className="nre-missing" title={`0/${cell.total} attempts measurable`}>
|
|
21
|
+
{MISSING_TEXT}
|
|
22
|
+
</span>
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<span className="nre-cell">
|
|
28
|
+
<span className="nre-value" title={`${cell.samples}/${cell.total} attempts measured`}>
|
|
29
|
+
{cell.display}
|
|
30
|
+
</span>
|
|
31
|
+
{/* samples < total:有 attempt 测不了这个指标,覆盖率角标如实标出 */}
|
|
32
|
+
{cell.samples < cell.total && (
|
|
33
|
+
<sup className="nre-coverage" title={`coverage ${cell.samples}/${cell.total}: this metric is null for the remaining attempts`}>
|
|
34
|
+
{cell.samples}/{cell.total}
|
|
35
|
+
</sup>
|
|
36
|
+
)}
|
|
37
|
+
{/* refs + attemptHref:格子可点,「给我看那次 attempt」就在手边 */}
|
|
38
|
+
{attemptHref && cell.refs && cell.refs.length > 0 && (
|
|
39
|
+
<span className="nre-refs">
|
|
40
|
+
{cell.refs.map((ref, i) => (
|
|
41
|
+
<a key={`${ref.run}:${ref.result}`} className="nre-ref" href={attemptHref(ref)}>
|
|
42
|
+
#{i + 1}
|
|
43
|
+
</a>
|
|
44
|
+
))}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// 跨块配色一致(docs/reports.md 四条跨组件契约之二):
|
|
2
|
+
// 系列/维度键 → 固定调色板下标,用稳定散列而不是「按出现顺序分配」。
|
|
3
|
+
// 这样同一个 agent 在 scatter 的线、DeltaTable 的行、matrix 的列头永远同色,
|
|
4
|
+
// 不需要 Provider、不需要手工配置,甚至不需要两个组件见过同一份数据。
|
|
5
|
+
// 调色板的十六进制值以这里为准:SVG(scatter 的线和点)直接内联 hex,
|
|
6
|
+
// 保证漏加载 styles.css 时图形仍然分得清系列;styles.css 里的 --nre-c0..c7
|
|
7
|
+
// 是同一组值的拷贝(供 .nre-cN 文本类用),改色时两边一起改。
|
|
8
|
+
|
|
9
|
+
/** 固定调色板;下标即 nre-cN 的 N,与 styles.css 的 --nre-cN 逐个对应。 */
|
|
10
|
+
export const NRE_PALETTE = [
|
|
11
|
+
"#2563eb", // c0 蓝
|
|
12
|
+
"#db2777", // c1 玫红
|
|
13
|
+
"#059669", // c2 绿
|
|
14
|
+
"#d97706", // c3 橙
|
|
15
|
+
"#7c3aed", // c4 紫
|
|
16
|
+
"#0891b2", // c5 青
|
|
17
|
+
"#dc2626", // c6 红
|
|
18
|
+
"#65a30d", // c7 橄榄
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export const NRE_PALETTE_SIZE = NRE_PALETTE.length;
|
|
22
|
+
|
|
23
|
+
/** FNV-1a 32 位散列:输入相同永远得到相同下标,与运行顺序无关。 */
|
|
24
|
+
export function colorIndexForKey(key: string): number {
|
|
25
|
+
let hash = 0x811c9dc5;
|
|
26
|
+
for (let i = 0; i < key.length; i++) {
|
|
27
|
+
hash ^= key.charCodeAt(i);
|
|
28
|
+
// FNV prime 乘法,拆成移位加法保持 32 位整数运算
|
|
29
|
+
hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0;
|
|
30
|
+
}
|
|
31
|
+
return hash % NRE_PALETTE_SIZE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 键对应的稳定 class 名("nre-c3"),配 styles.css 的 .nre-cN 上色。 */
|
|
35
|
+
export function colorClassForKey(key: string): string {
|
|
36
|
+
return `nre-c${colorIndexForKey(key)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 键对应的十六进制颜色,给 SVG 内联 stroke/fill 用(不依赖 CSS 加载)。 */
|
|
40
|
+
export function colorHexForKey(key: string): string {
|
|
41
|
+
return NRE_PALETTE[colorIndexForKey(key)];
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// niceeval/report/react 的数据契约(组件 props 类型)。
|
|
2
|
+
//
|
|
3
|
+
// 契约的家在计算层(../types.ts,照 docs/reports.md「计算函数与数据契约」),这里 re-export,
|
|
4
|
+
// 「算」与「画」两侧共用同一份类型;本文件零运行时代码——组件不做任何聚合或数据操作。
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
AttemptRef,
|
|
8
|
+
MetricColumn,
|
|
9
|
+
MetricCell,
|
|
10
|
+
TableData,
|
|
11
|
+
MatrixData,
|
|
12
|
+
ScoreboardData,
|
|
13
|
+
ScatterData,
|
|
14
|
+
OverviewData,
|
|
15
|
+
DeltaData,
|
|
16
|
+
CaseListData,
|
|
17
|
+
} from "../types.ts";
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// 手工 fixture:模拟计算层(niceeval/report 的 table()/matrix()/… )的产物。
|
|
2
|
+
// 仅供 render.test.tsx 与 scripts/report-react-demo.tsx 使用,不从入口导出。
|
|
3
|
+
// 数字刻意覆盖诚实细节:coverage 角标(samples<total)、全 null 格子、
|
|
4
|
+
// 稀疏矩阵、缺数据的散点、delta 的 null 不硬算、truncated 计数。
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CaseListData,
|
|
8
|
+
DeltaData,
|
|
9
|
+
MatrixData,
|
|
10
|
+
MetricColumn,
|
|
11
|
+
OverviewData,
|
|
12
|
+
ScatterData,
|
|
13
|
+
ScoreboardData,
|
|
14
|
+
TableData,
|
|
15
|
+
} from "./data.ts";
|
|
16
|
+
|
|
17
|
+
export const passRateColumn: MetricColumn = { key: "pass-rate", label: "通过率", unit: "%", better: "higher" };
|
|
18
|
+
export const codeLinesColumn: MetricColumn = { key: "code-lines", label: "代码行数", unit: "lines", better: "lower" };
|
|
19
|
+
export const costColumn: MetricColumn = { key: "cost", label: "成本", unit: "$", better: "lower" };
|
|
20
|
+
|
|
21
|
+
export const overviewData: OverviewData = {
|
|
22
|
+
snapshots: [
|
|
23
|
+
{ experimentId: "compare/bub", agent: "bub", model: "gpt-5.4", startedAt: "2026-07-01T10:00:00Z" },
|
|
24
|
+
{ experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-01T11:30:00Z" },
|
|
25
|
+
],
|
|
26
|
+
totals: {
|
|
27
|
+
evals: 12,
|
|
28
|
+
attempts: 48,
|
|
29
|
+
passed: 36,
|
|
30
|
+
failed: 8,
|
|
31
|
+
errored: 2,
|
|
32
|
+
skipped: 2,
|
|
33
|
+
costUSD: null, // 全部 attempt 都没报成本:null,组件必须显示缺数据而不是 $0
|
|
34
|
+
durationMs: 261_000,
|
|
35
|
+
},
|
|
36
|
+
warnings: ["compare/bub 快照缺 3 个 eval 的结果"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const overviewWithCost: OverviewData = {
|
|
40
|
+
...overviewData,
|
|
41
|
+
totals: { ...overviewData.totals, costUSD: 1.234 },
|
|
42
|
+
warnings: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const tableData: TableData = {
|
|
46
|
+
dimension: "agent",
|
|
47
|
+
// 行顺序故意不按 passRate 排:组件必须按传入顺序渲染,不重排
|
|
48
|
+
columns: [passRateColumn, codeLinesColumn],
|
|
49
|
+
rows: [
|
|
50
|
+
{
|
|
51
|
+
key: "codex",
|
|
52
|
+
cells: {
|
|
53
|
+
"pass-rate": { value: 0.5, display: "50%", samples: 6, total: 6 },
|
|
54
|
+
// 全 null:一个有效样本都没有 → 缺数据文案,绝不画 0
|
|
55
|
+
"code-lines": { value: null, display: "", samples: 0, total: 6 },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "bub",
|
|
60
|
+
cells: {
|
|
61
|
+
"pass-rate": {
|
|
62
|
+
value: 0.87,
|
|
63
|
+
display: "87%",
|
|
64
|
+
samples: 6,
|
|
65
|
+
total: 6,
|
|
66
|
+
refs: [{ run: "run-a", result: 0 }],
|
|
67
|
+
},
|
|
68
|
+
// samples < total:有 attempt 测不了 → 覆盖率角标 5/6
|
|
69
|
+
"code-lines": { value: 120, display: "120 lines", samples: 5, total: 6 },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const matrixData: MatrixData = {
|
|
76
|
+
rows: "eval",
|
|
77
|
+
columns: "agent",
|
|
78
|
+
metric: passRateColumn,
|
|
79
|
+
// 稀疏:geometry/angles × codex 没有样本,数据里不出现 → 格子空着
|
|
80
|
+
cells: [
|
|
81
|
+
{
|
|
82
|
+
row: "algebra/quadratic",
|
|
83
|
+
column: "bub",
|
|
84
|
+
cell: { value: 1, display: "100%", samples: 2, total: 2, refs: [{ run: "run-b", result: 3 }, { run: "run-b", result: 7 }] },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
row: "algebra/quadratic",
|
|
88
|
+
column: "codex",
|
|
89
|
+
cell: { value: 0, display: "0%", samples: 3, total: 3 },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
row: "geometry/angles",
|
|
93
|
+
column: "bub",
|
|
94
|
+
cell: { value: 0.5, display: "50%", samples: 2, total: 2 },
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const scoreboardData: ScoreboardData = {
|
|
100
|
+
of: "agent",
|
|
101
|
+
fullMarks: 100,
|
|
102
|
+
weights: [{ prefix: "algebra/", weight: 2 }],
|
|
103
|
+
rows: [
|
|
104
|
+
{
|
|
105
|
+
key: "bub",
|
|
106
|
+
total: { value: 78.5, display: "78.5" },
|
|
107
|
+
subjects: [
|
|
108
|
+
// missing 1:一题没跑、按 0 计——固定分母的如实注脚
|
|
109
|
+
{ key: "algebra", earned: 14, possible: 16, evals: 8, missing: 1 },
|
|
110
|
+
{ key: "geometry", earned: 3, possible: 4, evals: 4, missing: 0 },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: "codex",
|
|
115
|
+
total: { value: 52, display: "52" },
|
|
116
|
+
subjects: [
|
|
117
|
+
{ key: "algebra", earned: 9, possible: 16, evals: 8, missing: 0 },
|
|
118
|
+
{ key: "geometry", earned: 1.4, possible: 4, evals: 4, missing: 2 },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const scatterData: ScatterData = {
|
|
125
|
+
points: "experiment",
|
|
126
|
+
series: "agent",
|
|
127
|
+
x: costColumn, // better: "lower" → 轴反向,便宜的一端在右
|
|
128
|
+
y: passRateColumn,
|
|
129
|
+
rows: [
|
|
130
|
+
{
|
|
131
|
+
key: "compare/bub-low",
|
|
132
|
+
series: "bub",
|
|
133
|
+
x: { value: 5, display: "$5.00", samples: 6, total: 6 },
|
|
134
|
+
y: { value: 0.5, display: "50%", samples: 6, total: 6 },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: "compare/bub-high",
|
|
138
|
+
series: "bub",
|
|
139
|
+
x: { value: 10, display: "$10.00", samples: 6, total: 6 },
|
|
140
|
+
y: { value: 0.9, display: "90%", samples: 6, total: 6 },
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "compare/codex-mid",
|
|
144
|
+
series: "codex",
|
|
145
|
+
x: { value: 7, display: "$7.00", samples: 6, total: 6 },
|
|
146
|
+
y: { value: 0.6, display: "60%", samples: 6, total: 6 },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
// x 缺数据:这个点不画,注脚报 1 个点缺数据
|
|
150
|
+
key: "compare/codex-broken",
|
|
151
|
+
series: "codex",
|
|
152
|
+
x: { value: null, display: "", samples: 0, total: 6 },
|
|
153
|
+
y: { value: 0.7, display: "70%", samples: 6, total: 6 },
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const deltaData: DeltaData = {
|
|
159
|
+
columns: [passRateColumn, costColumn],
|
|
160
|
+
rows: [
|
|
161
|
+
{
|
|
162
|
+
key: "bub",
|
|
163
|
+
a: { experimentId: "compare/bub" },
|
|
164
|
+
b: { experimentId: "compare/bub--agents-md" },
|
|
165
|
+
cells: {
|
|
166
|
+
// 通过率 +12pp:better higher → 好(绿)
|
|
167
|
+
"pass-rate": {
|
|
168
|
+
a: { value: 0.5, display: "50%", samples: 6, total: 6 },
|
|
169
|
+
b: { value: 0.62, display: "62%", samples: 6, total: 6 },
|
|
170
|
+
delta: 0.12,
|
|
171
|
+
display: "+12pp",
|
|
172
|
+
},
|
|
173
|
+
// 成本 +$0.15:better lower → 坏(红)
|
|
174
|
+
cost: {
|
|
175
|
+
a: { value: 0.2, display: "$0.20", samples: 6, total: 6 },
|
|
176
|
+
b: { value: 0.35, display: "$0.35", samples: 6, total: 6 },
|
|
177
|
+
delta: 0.15,
|
|
178
|
+
display: "+$0.15",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "codex",
|
|
184
|
+
a: { experimentId: "compare/codex" },
|
|
185
|
+
b: { experimentId: "compare/codex--agents-md" },
|
|
186
|
+
cells: {
|
|
187
|
+
"pass-rate": {
|
|
188
|
+
a: { value: 0.4, display: "40%", samples: 6, total: 6 },
|
|
189
|
+
b: { value: 0.4, display: "40%", samples: 6, total: 6 },
|
|
190
|
+
delta: 0,
|
|
191
|
+
display: "±0pp",
|
|
192
|
+
},
|
|
193
|
+
// A 侧缺数据 → delta null:显示缺,不硬算
|
|
194
|
+
cost: {
|
|
195
|
+
a: { value: null, display: "", samples: 0, total: 6 },
|
|
196
|
+
b: { value: 0.3, display: "$0.30", samples: 6, total: 6 },
|
|
197
|
+
delta: null,
|
|
198
|
+
display: "",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const caseListData: CaseListData = {
|
|
206
|
+
rows: [
|
|
207
|
+
{
|
|
208
|
+
eval: "algebra/quadratic",
|
|
209
|
+
experimentId: "compare/bub",
|
|
210
|
+
agent: "bub",
|
|
211
|
+
outcome: "failed",
|
|
212
|
+
failedAssertions: [
|
|
213
|
+
{
|
|
214
|
+
name: "roots-correct",
|
|
215
|
+
score: 0,
|
|
216
|
+
detail: "期望 x=2,得到 x=3",
|
|
217
|
+
evidence: "judge: 求根公式代入时符号写反",
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
durationMs: 32_000,
|
|
221
|
+
costUSD: 0.12,
|
|
222
|
+
ref: { run: "run-a", result: 4 },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
eval: "geometry/angles",
|
|
226
|
+
experimentId: "compare/codex",
|
|
227
|
+
agent: "codex",
|
|
228
|
+
outcome: "errored",
|
|
229
|
+
error: "TypeError: cannot read properties of undefined (reading 'foo')",
|
|
230
|
+
failedAssertions: [],
|
|
231
|
+
durationMs: 4_500,
|
|
232
|
+
ref: { run: "run-c", result: 1 },
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
truncated: 2,
|
|
236
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// 展示层的小格式化。注意边界:MetricCell 一律自带 display(格式化发生在计算侧),
|
|
2
|
+
// 组件不重算;这里只服务 OverviewData 这类携带裸数字的字段(耗时、成本、比率),
|
|
3
|
+
// 以及 class 名拼接。缺数据的统一文案也钉在这里,保证各组件同词。
|
|
4
|
+
|
|
5
|
+
/** 全 null / 无样本的统一文案。绝不画 0(docs/reports.md「null ≠ 0」)。 */
|
|
6
|
+
export const MISSING_TEXT = "no data";
|
|
7
|
+
|
|
8
|
+
/** 拼 class 名:过滤空值,末尾接使用者透传的 className。 */
|
|
9
|
+
export function cx(...parts: (string | undefined | false)[]): string {
|
|
10
|
+
return parts.filter(Boolean).join(" ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 毫秒 → 人读耗时("850ms" / "1.2s" / "4m 20s" / "1h 4m")。 */
|
|
14
|
+
export function formatDurationMs(ms: number): string {
|
|
15
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
16
|
+
const s = ms / 1000;
|
|
17
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
18
|
+
const m = Math.floor(s / 60);
|
|
19
|
+
if (m < 60) return `${m}m ${Math.round(s % 60)}s`;
|
|
20
|
+
const h = Math.floor(m / 60);
|
|
21
|
+
return `${h}h ${m % 60}m`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 美元金额;小额保留更多位数,不四舍五入成 $0.00 的假零。 */
|
|
25
|
+
export function formatUSD(usd: number): string {
|
|
26
|
+
if (usd === 0) return "$0";
|
|
27
|
+
const digits = Math.abs(usd) >= 0.01 ? 2 : 4;
|
|
28
|
+
return `$${usd.toFixed(digits)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 0..1 的比率 → 整数百分比。 */
|
|
32
|
+
export function formatPercent(ratio: number): string {
|
|
33
|
+
return `${Math.round(ratio * 100)}%`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// niceeval/report/react —— 纯渲染的报告积木(docs/reports.md 第一档,原型实现)。
|
|
2
|
+
//
|
|
3
|
+
// 契约:
|
|
4
|
+
// - 组件只认「算好的可序列化数据」:零 IO、零 hooks、零数据操作,可进 "use client";
|
|
5
|
+
// - 不 hydrate 也完整:renderToStaticMarkup 的产物即成品——排序在数据侧预排,
|
|
6
|
+
// hover 退化为 title,下钻是普通 <a>,展开折叠是 <details>;
|
|
7
|
+
// - 样式随包发布:配套 ./styles.css(nre-* 稳定类名),使用者在其后加载覆盖即可;
|
|
8
|
+
// - 跨块配色一致:维度键 → 稳定散列 → 固定调色板下标(colors.ts)。
|
|
9
|
+
//
|
|
10
|
+
// 数据从哪来:niceeval/report 的计算函数(table()/matrix()/scoreboard()/scatter()/
|
|
11
|
+
// overview()/delta()/cases())。计算层在并行实验中实现;类型暂由 ./data.ts 声明,
|
|
12
|
+
// 合并时切换为从计算层 re-export。
|
|
13
|
+
|
|
14
|
+
export { RunOverview } from "./RunOverview.tsx";
|
|
15
|
+
export { MetricTable } from "./MetricTable.tsx";
|
|
16
|
+
export { MetricMatrix } from "./MetricMatrix.tsx";
|
|
17
|
+
export { Scoreboard } from "./Scoreboard.tsx";
|
|
18
|
+
export { MetricScatter } from "./MetricScatter.tsx";
|
|
19
|
+
export { DeltaTable } from "./DeltaTable.tsx";
|
|
20
|
+
export { CaseList } from "./CaseList.tsx";
|
|
21
|
+
|
|
22
|
+
// 数据契约类型(临时家在 ./data.ts,见该文件头部说明)
|
|
23
|
+
export type {
|
|
24
|
+
AttemptRef,
|
|
25
|
+
MetricColumn,
|
|
26
|
+
MetricCell,
|
|
27
|
+
TableData,
|
|
28
|
+
MatrixData,
|
|
29
|
+
ScoreboardData,
|
|
30
|
+
ScatterData,
|
|
31
|
+
OverviewData,
|
|
32
|
+
DeltaData,
|
|
33
|
+
CaseListData,
|
|
34
|
+
} from "./data.ts";
|