niceeval 0.4.5 → 0.5.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/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +10 -2
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/bub.ts +14 -4
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +104 -14
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- package/src/view/loader.ts +0 -257
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// 报告的元素树与双面组件基座(docs/reports.md「元素树与两个宿主」「双面组件」)。
|
|
2
|
+
//
|
|
3
|
+
// 报告函数返回的树不是「React 树」,只是 { type, props } 节点 —— 标准 react
|
|
4
|
+
// jsx-runtime 产的元素恰好就是这个形状。本文件是基础实现:零 react 运行时依赖
|
|
5
|
+
// (只有类型层的 `import type`,编译后擦除);text 宿主遍历渲染不需要 react-dom,
|
|
6
|
+
// web 宿主(web.ts)才真正 import react。渲染面是纯同步函数:零 IO、零 await ——
|
|
7
|
+
// 计算全部发生在报告函数体里,可达百 MB 的工件永远不进渲染路径。
|
|
8
|
+
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import type { AttemptRef } from "../results/index.ts";
|
|
11
|
+
|
|
12
|
+
// ───────────────────────── 节点形状 ─────────────────────────
|
|
13
|
+
|
|
14
|
+
/** 标准 jsx-runtime 元素形状;text 宿主只认 type / props,不管 $$typeof。 */
|
|
15
|
+
export interface ReportElement {
|
|
16
|
+
type: unknown;
|
|
17
|
+
props: Record<string, unknown>;
|
|
18
|
+
key?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 报告树节点:元素、文本、数组 / Fragment 的儿子们,或渲染为空的空值。 */
|
|
22
|
+
export type ReportNode = ReportElement | string | number | boolean | null | undefined | ReportNode[];
|
|
23
|
+
|
|
24
|
+
// react/jsx-runtime 的 Fragment 是注册符号,跨 react 版本稳定;不 import react 也认得它
|
|
25
|
+
const REACT_FRAGMENT = Symbol.for("react.fragment");
|
|
26
|
+
|
|
27
|
+
function isReportElement(node: unknown): node is ReportElement {
|
|
28
|
+
return (
|
|
29
|
+
typeof node === "object" &&
|
|
30
|
+
node !== null &&
|
|
31
|
+
!Array.isArray(node) &&
|
|
32
|
+
"type" in node &&
|
|
33
|
+
"props" in node &&
|
|
34
|
+
typeof (node as ReportElement).props === "object"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ───────────────────────── 双面组件 ─────────────────────────
|
|
39
|
+
|
|
40
|
+
/** 挂 faces 的私有键:text 宿主与树校验靠它识别双面组件。 */
|
|
41
|
+
export const COMPONENT_FACES: unique symbol = Symbol.for("niceeval.report.faces");
|
|
42
|
+
|
|
43
|
+
export interface TextContext {
|
|
44
|
+
/** 可用列宽;Row 分栏后变窄。 */
|
|
45
|
+
width: number;
|
|
46
|
+
/** 容器组件渲染 children 用,宽度显式传递。 */
|
|
47
|
+
render(node: ReportNode, width?: number): string;
|
|
48
|
+
/** 下钻命令,通证据室。 */
|
|
49
|
+
attemptCommand(ref: AttemptRef): string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WebContext {
|
|
53
|
+
/** 证据室深链,同 view 的 attempt 路由。 */
|
|
54
|
+
attemptHref(ref: AttemptRef): string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ComponentFaces<P> {
|
|
58
|
+
/** 真 React JSX 在这个面里;返回静态可渲染的 ReactNode。 */
|
|
59
|
+
web(props: P, ctx: WebContext): ReactNode;
|
|
60
|
+
text(props: P, ctx: TextContext): string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 双面组件的产物:可直接用于 JSX(React 把它当函数组件调用,走 web 面),
|
|
65
|
+
* text 宿主经 COMPONENT_FACES 调 text 面。
|
|
66
|
+
*/
|
|
67
|
+
export type ReportComponent<P> = ((props: P) => ReactNode) & {
|
|
68
|
+
[COMPONENT_FACES]: ComponentFaces<P>;
|
|
69
|
+
displayName?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// web 面的环境上下文:web 宿主渲染前设好;宿主之外(组件直接嵌进用户 React 应用)
|
|
73
|
+
// 用默认值 —— attemptHref 默认 view 的 attempt 路由格式(自定义组件显式调 ctx.attemptHref
|
|
74
|
+
// 时总有去处);官方组件的「宿主里自动接证据室」只在宿主上下文激活时发生,
|
|
75
|
+
// 宿主外不传 attemptHref 就是纯展示,不发明断链。
|
|
76
|
+
const DEFAULT_WEB_CONTEXT: WebContext = {
|
|
77
|
+
attemptHref: (ref) => `#/attempt/${ref.run}/${ref.result}`,
|
|
78
|
+
};
|
|
79
|
+
let activeWebContext: WebContext | null = null;
|
|
80
|
+
|
|
81
|
+
/** web 宿主用:在给定 WebContext 下同步渲染(React 静态渲染本身是同步的)。 */
|
|
82
|
+
export function runWithWebContext<T>(ctx: WebContext, fn: () => T): T {
|
|
83
|
+
const prev = activeWebContext;
|
|
84
|
+
activeWebContext = ctx;
|
|
85
|
+
try {
|
|
86
|
+
return fn();
|
|
87
|
+
} finally {
|
|
88
|
+
activeWebContext = prev;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 官方组件的装配用:宿主上下文激活时才把 ctx.attemptHref 当默认下钻。 */
|
|
93
|
+
export function isHostWebContextActive(): boolean {
|
|
94
|
+
return activeWebContext !== null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 定义一个双面组件:faces 两键必填 —— 少实现一个面编译不过,配对是结构义务。
|
|
99
|
+
* 基础实现不 import react;产物以可调用组件的形状兼容 React 渲染。
|
|
100
|
+
*/
|
|
101
|
+
export function defineComponent<P>(faces: ComponentFaces<P>): ReportComponent<P> {
|
|
102
|
+
if (typeof faces?.web !== "function" || typeof faces?.text !== "function") {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"defineComponent requires both faces: { web(props, ctx), text(props, ctx) }. " +
|
|
105
|
+
"Every report component must render in both hosts (niceeval view and niceeval show).",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const component = ((props: P) => faces.web(props, activeWebContext ?? DEFAULT_WEB_CONTEXT)) as ReportComponent<P>;
|
|
109
|
+
component[COMPONENT_FACES] = faces;
|
|
110
|
+
return component;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function facesOf(type: unknown): ComponentFaces<unknown> | undefined {
|
|
114
|
+
if (typeof type !== "function") return undefined;
|
|
115
|
+
return (type as Partial<ReportComponent<unknown>>)[COMPONENT_FACES] as ComponentFaces<unknown> | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ───────────────────────── 树校验 ─────────────────────────
|
|
119
|
+
|
|
120
|
+
function componentLabel(type: unknown): string {
|
|
121
|
+
if (typeof type === "string") return `<${type}>`;
|
|
122
|
+
if (typeof type === "function") {
|
|
123
|
+
const name = (type as { displayName?: string; name?: string }).displayName || (type as { name?: string }).name;
|
|
124
|
+
return name ? `<${name}>` : "<anonymous component>";
|
|
125
|
+
}
|
|
126
|
+
if (type === REACT_FRAGMENT) return "<>";
|
|
127
|
+
return `<${String(type)}>`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 渲染前树校验:页面树里只放双面组件、排版原语与普通组合函数,字符串 intrinsic
|
|
132
|
+
* (<div>)报错、指名组件路径。这是运行时校验而非编译期(标准 JSX 下 TS 把一切
|
|
133
|
+
* JSX 表达式统一成 JSX.Element);两个宿主渲染前跑同一遍 —— 不做单侧宽容,否则
|
|
134
|
+
* 对着 view 写的页面到 show 才炸。校验只下钻 children(children 就是报告树);
|
|
135
|
+
* 普通函数组件调用展开(渲染面纯同步,重复调用无副作用)。
|
|
136
|
+
*/
|
|
137
|
+
export function validateReportTree(node: ReportNode, path: string[] = []): void {
|
|
138
|
+
if (node === null || node === undefined || typeof node === "boolean") return;
|
|
139
|
+
if (typeof node === "string" || typeof node === "number") return;
|
|
140
|
+
if (Array.isArray(node)) {
|
|
141
|
+
for (const child of node) validateReportTree(child, path);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isReportElement(node)) return;
|
|
145
|
+
const { type, props } = node;
|
|
146
|
+
if (typeof type === "string") {
|
|
147
|
+
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Raw HTML <${type}> has no terminal face; use <Text>, layout primitives, or a defineComponent component.${where}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (type === REACT_FRAGMENT) {
|
|
153
|
+
validateReportTree(props.children as ReportNode, path);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const label = componentLabel(type);
|
|
157
|
+
if (facesOf(type)) {
|
|
158
|
+
// 双面组件是校验的信任边界之内的叶子,但 children 仍是报告树的一部分
|
|
159
|
+
validateReportTree(props.children as ReportNode, [...path, label]);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (typeof type === "function") {
|
|
163
|
+
// 普通函数组件 = 用户拿函数组合页面片段:调用展开继续校验
|
|
164
|
+
const expanded = (type as (p: unknown) => ReportNode)(props);
|
|
165
|
+
validateReportTree(expanded, [...path, label]);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
169
|
+
throw new Error(`Unsupported node type ${label} in report tree.${where}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ───────────────────────── text 渲染 ─────────────────────────
|
|
173
|
+
|
|
174
|
+
export interface TextRenderOptions {
|
|
175
|
+
/** 终端可用列宽;默认 80。 */
|
|
176
|
+
width?: number;
|
|
177
|
+
/** 下钻命令的生成;宿主注入,默认指向 view 的 attempt 路由。 */
|
|
178
|
+
attemptCommand?: (ref: AttemptRef) => string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createTextContext(options?: TextRenderOptions): TextContext {
|
|
182
|
+
const width = Math.max(20, options?.width ?? 80);
|
|
183
|
+
const attemptCommand =
|
|
184
|
+
options?.attemptCommand ?? ((ref: AttemptRef) => `niceeval view "#/attempt/${ref.run}/${ref.result}"`);
|
|
185
|
+
const make = (w: number): TextContext => ({
|
|
186
|
+
width: w,
|
|
187
|
+
attemptCommand,
|
|
188
|
+
render(node, childWidth) {
|
|
189
|
+
return renderNodeToText(node, childWidth === undefined ? this : make(Math.max(10, childWidth)));
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return make(width);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** text 宿主的遍历渲染:双面组件走 text 面,普通函数调用展开,块之间以换行相接。 */
|
|
196
|
+
export function renderNodeToText(node: ReportNode, ctx: TextContext): string {
|
|
197
|
+
if (node === null || node === undefined || typeof node === "boolean") return "";
|
|
198
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
199
|
+
if (Array.isArray(node)) {
|
|
200
|
+
return node
|
|
201
|
+
.map((child) => renderNodeToText(child, ctx))
|
|
202
|
+
.filter((text) => text.length > 0)
|
|
203
|
+
.join("\n");
|
|
204
|
+
}
|
|
205
|
+
if (!isReportElement(node)) return "";
|
|
206
|
+
const { type, props } = node;
|
|
207
|
+
if (typeof type === "string") {
|
|
208
|
+
// 校验先行会拦住;这里兜底同一条错误,渲染路径自身也不宽容
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Raw HTML <${type}> has no terminal face; use <Text>, layout primitives, or a defineComponent component.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (type === REACT_FRAGMENT) return renderNodeToText(props.children as ReportNode, ctx);
|
|
214
|
+
const faces = facesOf(type);
|
|
215
|
+
if (faces) return faces.text(props, ctx);
|
|
216
|
+
if (typeof type === "function") {
|
|
217
|
+
return renderNodeToText((type as (p: unknown) => ReportNode)(props), ctx);
|
|
218
|
+
}
|
|
219
|
+
return "";
|
|
220
|
+
}
|
package/src/report/types.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// niceeval/report 的公开类型:指标(Metric)、维度(Dimension)
|
|
2
|
-
// (
|
|
3
|
-
//
|
|
4
|
-
// 兼容性跟随 npm 版本。
|
|
1
|
+
// niceeval/report 的公开类型:指标(Metric)、维度(Dimension / param())与计算函数
|
|
2
|
+
// 产物(即组件的 data props)。数据契约照 docs/reports.md「计算函数与数据契约」;
|
|
3
|
+
// 这些不是持久化格式,没有 format / schemaVersion 信封,兼容性跟随 npm 版本。
|
|
5
4
|
|
|
6
|
-
import type { AttemptHandle } from "../results/
|
|
5
|
+
import type { AttemptHandle, AttemptRef, SelectionWarning } from "../results/index.ts";
|
|
6
|
+
|
|
7
|
+
export type { AttemptRef, SelectionWarning };
|
|
7
8
|
|
|
8
9
|
// ───────────────────────── 指标与聚合 ─────────────────────────
|
|
9
10
|
|
|
@@ -24,11 +25,12 @@ export interface MetricAggregate {
|
|
|
24
25
|
/**
|
|
25
26
|
* 指标:纯函数,吃一个 AttemptHandle 吐一个值(null = 此 attempt 测不了这个指标,
|
|
26
27
|
* 不进聚合;0 = 测了结果是零,照常进),外加名字、两级聚合方式和渲染提示。
|
|
27
|
-
* 内置指标与自定义指标是同一个类型,没有特权。
|
|
28
|
+
* 内置指标与自定义指标是同一个类型,没有特权。name 走字面量泛型:列键锚在指标
|
|
29
|
+
* 对象上(`row.cells[passRate.name]`),拼错列名编译不过。
|
|
28
30
|
*/
|
|
29
|
-
export interface Metric {
|
|
31
|
+
export interface Metric<Name extends string = string> {
|
|
30
32
|
/** MetricColumn.key 与列头的来源;同一次计算里重名是错误。 */
|
|
31
|
-
name:
|
|
33
|
+
name: Name;
|
|
32
34
|
/** 列头;省略时用 name。 */
|
|
33
35
|
label?: string;
|
|
34
36
|
description?: string;
|
|
@@ -47,8 +49,10 @@ export interface Metric {
|
|
|
47
49
|
display?: (value: number) => string;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
// ───────────────────────── 维度 ─────────────────────────
|
|
53
|
+
|
|
50
54
|
/**
|
|
51
|
-
* 维度:attempt
|
|
55
|
+
* 维度:attempt 分到哪一组。内置维度就是结果已有的身份字段;自定义维度是一个函数。
|
|
52
56
|
* - "evalGroup" = eval id 的第一段:"algebra/quadratic" → "algebra"(考试里的「科目」)
|
|
53
57
|
* - "snapshot" = "<experimentId> @ <startedAt>",把两次快照并排成行,与 view 的 Compare 同口径
|
|
54
58
|
*/
|
|
@@ -61,7 +65,23 @@ export type Dimension =
|
|
|
61
65
|
| "snapshot"
|
|
62
66
|
| { name: string; of: (attempt: AttemptHandle) => string };
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
/**
|
|
69
|
+
* param() 的产物:把 experiment 声明的 params 当维度(series / rows / columns / points
|
|
70
|
+
* 槽,按声明值分组)或轴(MetricLine 的 x 槽,要求数值并驱动刻度)。
|
|
71
|
+
* 未声明该 param 的 experiment 不猜:分组如实归「(unset)」,作轴不画点、注脚报数。
|
|
72
|
+
*/
|
|
73
|
+
export interface ParamRef {
|
|
74
|
+
readonly kind: "param";
|
|
75
|
+
readonly name: string;
|
|
76
|
+
/** 组标签 / 轴标签;函数形态把声明值折成组名(如 `(v) => \`${v} agents\``)。 */
|
|
77
|
+
readonly label?: string | ((value: string | number | boolean) => string);
|
|
78
|
+
readonly unit?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 维度槽的输入:内置/自定义维度,或 experiment 声明的 param。 */
|
|
82
|
+
export type DimensionInput = Dimension | ParamRef;
|
|
83
|
+
|
|
84
|
+
// ───────────────────────── 计算产物(组件 data props)─────────────────────────
|
|
65
85
|
|
|
66
86
|
export interface MetricColumn {
|
|
67
87
|
/** = metric.name,与 cells 的键对应。 */
|
|
@@ -81,20 +101,19 @@ export interface MetricCell {
|
|
|
81
101
|
samples: number;
|
|
82
102
|
/** 组内 attempt 总数;samples < total = 有 attempt 测不了这个指标。 */
|
|
83
103
|
total: number;
|
|
84
|
-
/**
|
|
85
|
-
|
|
104
|
+
/**
|
|
105
|
+
* 这个格子由哪些 attempt 算出 —— 回到证据的引用。必填(可空数组):
|
|
106
|
+
* 「每个数字点进去就是证据」是页面的核心承诺,可选字段会让深链静默缺失。
|
|
107
|
+
*/
|
|
108
|
+
refs: AttemptRef[];
|
|
86
109
|
}
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
import type { AttemptRef } from "../results/types.ts";
|
|
91
|
-
export type { AttemptRef };
|
|
92
|
-
|
|
93
|
-
export interface TableData {
|
|
111
|
+
/** 列键 K 来自 columns 元组的字面量 name:拼错列名编译不过,不是运行时 undefined。 */
|
|
112
|
+
export interface TableData<K extends string = string> {
|
|
94
113
|
/** 行维度名,如 "agent"。 */
|
|
95
114
|
dimension: string;
|
|
96
115
|
columns: MetricColumn[];
|
|
97
|
-
rows: { key: string; cells: Record<
|
|
116
|
+
rows: { key: string; cells: Record<K, MetricCell> }[];
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
export interface MatrixData {
|
|
@@ -108,8 +127,12 @@ export interface MatrixData {
|
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
export interface ScoreboardData {
|
|
111
|
-
/**
|
|
112
|
-
|
|
130
|
+
/**
|
|
131
|
+
* 被打分的维度名,如 "agent"。
|
|
132
|
+
* (计算函数的维度槽叫 rows,与 MetricTable.data 统一;数据形状上行数组已占用
|
|
133
|
+
* rows 一词,维度名沿用 TableData 的 dimension。)
|
|
134
|
+
*/
|
|
135
|
+
dimension: string;
|
|
113
136
|
fullMarks: number;
|
|
114
137
|
/** 实际生效的权重表(按匹配顺序:最长前缀在前)—— 成绩单可审计。 */
|
|
115
138
|
weights: { prefix: string; weight: number }[];
|
|
@@ -151,6 +174,31 @@ export interface ScatterData {
|
|
|
151
174
|
}[];
|
|
152
175
|
}
|
|
153
176
|
|
|
177
|
+
/** MetricLine 的 x 轴:experiment 声明的 param,数值驱动刻度。 */
|
|
178
|
+
export interface LineAxis {
|
|
179
|
+
/** param 名。 */
|
|
180
|
+
key: string;
|
|
181
|
+
label: string;
|
|
182
|
+
unit?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface LineData {
|
|
186
|
+
x: LineAxis;
|
|
187
|
+
/** 系列维度名(param 或普通维度)。 */
|
|
188
|
+
series?: string;
|
|
189
|
+
y: MetricColumn;
|
|
190
|
+
rows: {
|
|
191
|
+
/** 点的键(experiment id):每个点 = 一个 experiment 的聚合。 */
|
|
192
|
+
key: string;
|
|
193
|
+
series?: string;
|
|
194
|
+
/** param 声明值;未声明或非数值 → null,点不画、注脚报数。 */
|
|
195
|
+
x: number | null;
|
|
196
|
+
/** 已格式化的 x("300 ms");x 为 null 时为空串。 */
|
|
197
|
+
xDisplay: string;
|
|
198
|
+
y: MetricCell;
|
|
199
|
+
}[];
|
|
200
|
+
}
|
|
201
|
+
|
|
154
202
|
export interface OverviewData {
|
|
155
203
|
snapshots: { experimentId: string; agent: string; model?: string; startedAt: string }[];
|
|
156
204
|
totals: {
|
|
@@ -164,27 +212,27 @@ export interface OverviewData {
|
|
|
164
212
|
costUSD: number | null;
|
|
165
213
|
durationMs: number;
|
|
166
214
|
};
|
|
167
|
-
/**
|
|
168
|
-
warnings:
|
|
215
|
+
/** 选集的警告随行(结构化,含渲染好的 message),RunOverview 直接渲染。 */
|
|
216
|
+
warnings: SelectionWarning[];
|
|
169
217
|
}
|
|
170
218
|
|
|
171
|
-
export interface DeltaData {
|
|
219
|
+
export interface DeltaData<K extends string = string> {
|
|
172
220
|
columns: MetricColumn[];
|
|
173
221
|
rows: {
|
|
174
222
|
/** pair 的 label,如 "bub"。 */
|
|
175
223
|
key: string;
|
|
176
|
-
/**
|
|
224
|
+
/** 基线侧:experiment id 或快照键 "<experimentId> @ <startedAt>"。 */
|
|
177
225
|
a: { experimentId: string };
|
|
178
226
|
/** 对比侧。 */
|
|
179
227
|
b: { experimentId: string };
|
|
180
228
|
cells: Record<
|
|
181
|
-
|
|
229
|
+
K,
|
|
182
230
|
{
|
|
183
231
|
a: MetricCell;
|
|
184
232
|
b: MetricCell;
|
|
185
233
|
/** b.value - a.value;任一侧 null → null,不硬算。 */
|
|
186
234
|
delta: number | null;
|
|
187
|
-
/** 已带符号("+12%" / "-$0.80"),涨跌好坏由 better 判定。 */
|
|
235
|
+
/** 已带符号("+12%" / "-$0.80" / "±0"),涨跌好坏由 better 判定。 */
|
|
188
236
|
display: string;
|
|
189
237
|
}
|
|
190
238
|
>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// web 宿主(view --report)的装载入口:同一棵树走 web 面,renderToStaticMarkup 吐静态
|
|
2
|
+
// HTML 烘进查看器的报告槽。只有这一侧真正 import react-dom(import 边界即运行时边界),
|
|
3
|
+
// 所以本文件不从 niceeval/report 的入口 re-export —— 宿主与测试按源路径 import。
|
|
4
|
+
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
8
|
+
|
|
9
|
+
// tsx 按「tsconfig 所在目录」为界应用 jsx 配置:CLI 在用户项目 cwd 下跑时,包内
|
|
10
|
+
// .tsx(primitives / react 组件的 web 面)落在用户 tsconfig 覆盖范围之外,esbuild
|
|
11
|
+
// 退化成 classic JSX(编译产物引用全局 React)。这里补上全局 React,web 面在两种
|
|
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";
|
|
18
|
+
import type { ReportContext, ReportDefinition } from "./report.ts";
|
|
19
|
+
|
|
20
|
+
export interface StaticHtmlOptions {
|
|
21
|
+
/** 证据室深链;缺省用 view 的 attempt 路由 `#/attempt/<run>/<result>`。 */
|
|
22
|
+
attemptHref?: (ref: AttemptRef) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** build → 渲染前树校验(与 text 宿主同一遍)→ 备好官方水位 → 静态渲染 web 面。 */
|
|
26
|
+
export async function renderReportToStaticHtml(
|
|
27
|
+
definition: ReportDefinition,
|
|
28
|
+
ctx: ReportContext,
|
|
29
|
+
options?: StaticHtmlOptions,
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
const node = await definition.build(ctx);
|
|
32
|
+
validateReportTree(node);
|
|
33
|
+
const defaultData = await prepareDefaultReportData(ctx.selection);
|
|
34
|
+
const webCtx: WebContext = {
|
|
35
|
+
attemptHref: options?.attemptHref ?? ((ref) => `#/attempt/${ref.run}/${ref.result}`),
|
|
36
|
+
};
|
|
37
|
+
return runWithDefaultReportData(defaultData, () =>
|
|
38
|
+
runWithWebContext(webCtx, () => renderToStaticMarkup(node as ReactNode)),
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/results/copy.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
//
|
|
1
|
+
// copySnapshots:把选中快照按格式感知地复制到另一个目录(定稿见 docs/results-lib.md「复制与瘦身」)。
|
|
2
2
|
//
|
|
3
3
|
// 发布场景的原语:只带指定工件、只带选中的 attempt,布局知识不外泄。
|
|
4
4
|
// 工件复制忠实于源(copyFile 原字节,不重新序列化、不消毒);summary.json 按选中条目重建,
|
|
5
|
-
// 版本元数据保留,榜单计数按选中条目重算 ——
|
|
6
|
-
//
|
|
5
|
+
// 版本元数据保留,榜单计数按选中条目重算 —— 产物是一个标准 run 目录,openResults /
|
|
6
|
+
// `niceeval view` 直接能读。唯一随行补记的是挑选时的覆盖事实:每个复制出的快照带上
|
|
7
|
+
// knownEvalIds(复制时刻该实验已知的 eval 并集),发布目录上重新 openResults().latest(),
|
|
8
|
+
// 残缺警告被同一套机制重新算出来,不靠发布者转述。
|
|
7
9
|
|
|
8
|
-
import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { copyFile, mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
9
11
|
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import type { EvalResult, RunSummary } from "../
|
|
11
|
-
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../
|
|
12
|
-
import type { Usage } from "../o11y/types.ts";
|
|
12
|
+
import type { EvalResult, RunSummary, Usage } from "../types.ts";
|
|
13
|
+
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
|
|
13
14
|
import { artifactFileOf, attemptDirOf } from "./format.ts";
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
15
|
+
import { experimentOfSnapshot } from "./open.ts";
|
|
16
|
+
import { isNewerRunDir } from "./select.ts";
|
|
17
|
+
import type { ArtifactKind, AttemptHandle, RunDir, Selection, Snapshot } from "./types.ts";
|
|
16
18
|
import { ARTIFACT_KINDS } from "./types.ts";
|
|
17
19
|
|
|
18
|
-
export interface
|
|
20
|
+
export interface CopySnapshotsOptions {
|
|
19
21
|
/** 要带上的工件种类;省略 = 全部五类。diff 可达百 MB、o11y 查看器不读,发布时常见地不带。 */
|
|
20
22
|
artifacts?: ArtifactKind[];
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export interface
|
|
25
|
+
export interface CopySnapshotsResult {
|
|
24
26
|
/** 目标 run 目录的绝对路径(summary.json 所在目录)。 */
|
|
25
27
|
dir: string;
|
|
26
28
|
/** 重建后的 summary(与写入磁盘的一致)。 */
|
|
@@ -30,30 +32,31 @@ export interface CopiedRun {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* 把选中快照的全部 attempt 复制成 `destDir` 下的一个标准 run 目录。
|
|
36
|
+
* 输入收 Selection 或手工挑的 Snapshot[](与 Reports 计算函数同一输入约定);
|
|
37
|
+
* 快照可以来自不同物理 run(latest() 的典型输出)。目标目录非空即报错,不静默覆盖、不合并。
|
|
36
38
|
*/
|
|
37
|
-
export async function
|
|
38
|
-
|
|
39
|
+
export async function copySnapshots(
|
|
40
|
+
selection: Selection | Snapshot[],
|
|
39
41
|
destDir: string,
|
|
40
|
-
opts?:
|
|
41
|
-
): Promise<
|
|
42
|
-
const selected = Array.isArray(
|
|
42
|
+
opts?: CopySnapshotsOptions,
|
|
43
|
+
): Promise<CopySnapshotsResult> {
|
|
44
|
+
const selected = Array.isArray(selection) ? selection : selection.snapshots;
|
|
43
45
|
if (selected.length === 0) {
|
|
44
46
|
throw new Error(
|
|
45
|
-
"
|
|
47
|
+
"copySnapshots got no snapshots to copy. Check the experiments filter, or pass snapshots from openResults().latest().",
|
|
46
48
|
);
|
|
47
49
|
}
|
|
48
50
|
const kinds = opts?.artifacts ?? [...ARTIFACT_KINDS];
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
throw new Error(`copyRun destination ${dest} is a source run directory. Pick a different destination.`);
|
|
51
|
+
for (const kind of kinds) {
|
|
52
|
+
if (!ARTIFACT_KINDS.includes(kind)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Unknown artifact kind "${String(kind)}" in copySnapshots options. Valid kinds: ${ARTIFACT_KINDS.join(", ")}.`,
|
|
55
|
+
);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
const dest = resolve(destDir);
|
|
59
|
+
await assertEmptyDestination(dest);
|
|
57
60
|
|
|
58
61
|
// 第一趟:按目标工件目录选出胜者(最新 run 的那份),顺序取首次出现处 —— 两趟避免
|
|
59
62
|
// 「旧的先落盘、新的再去清理覆盖」的中间态。
|
|
@@ -70,9 +73,9 @@ export async function copyRun(
|
|
|
70
73
|
continue;
|
|
71
74
|
}
|
|
72
75
|
warnings.push(
|
|
73
|
-
`warning: multiple attempts map to "${relDir}" in the copied run; kept the one from the newest run. Dedupe attempts or re-select snapshots before
|
|
76
|
+
`warning: multiple attempts map to "${relDir}" in the copied run; kept the one from the newest run. Dedupe attempts or re-select snapshots before copySnapshots to avoid this.`,
|
|
74
77
|
);
|
|
75
|
-
if (
|
|
78
|
+
if (isNewerRunDir(attempt.runDir, existing.runDir)) winners.set(relDir, attempt);
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -91,19 +94,37 @@ export async function copyRun(
|
|
|
91
94
|
results.push(slimForCopy(attempt.result, relDir, copied));
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
const summary = rebuildSummary(selected,
|
|
97
|
+
const summary = rebuildSummary(selected, results, dest);
|
|
95
98
|
await writeFile(join(dest, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
96
99
|
return { dir: dest, summary, warnings };
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
/** 目标目录非空即报错:盘上不该出现「我没写的东西被动过」的惊讶;发布脚本要幂等就自己先清目录。 */
|
|
103
|
+
async function assertEmptyDestination(dest: string): Promise<void> {
|
|
104
|
+
let entries: string[];
|
|
105
|
+
try {
|
|
106
|
+
entries = await readdir(dest);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if ((e as NodeJS.ErrnoException | null)?.code === "ENOENT") return;
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Destination "${dest}" is not a usable directory (${e instanceof Error ? e.message : String(e)}). Pass a new or empty directory.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (entries.length > 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Destination directory "${dest}" is not empty. copySnapshots never overwrites or merges; delete the directory first if you want to replace it.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
/** 源工件定位:与读取面同一候选顺序(本 run 的 artifactsDir 优先,artifactBase 回退)。 */
|
|
100
121
|
async function findArtifactFiles(
|
|
101
122
|
attempt: AttemptHandle,
|
|
102
123
|
kinds: ArtifactKind[],
|
|
103
124
|
): Promise<{ kind: ArtifactKind; source: string }[]> {
|
|
104
125
|
const candidates: string[] = [];
|
|
105
|
-
if (attempt.result.artifactsDir) candidates.push(join(attempt.
|
|
106
|
-
if (attempt.result.artifactBase) candidates.push(join(dirname(attempt.
|
|
126
|
+
if (attempt.result.artifactsDir) candidates.push(join(attempt.runDir.dir, attempt.result.artifactsDir));
|
|
127
|
+
if (attempt.result.artifactBase) candidates.push(join(dirname(attempt.runDir.dir), attempt.result.artifactBase));
|
|
107
128
|
|
|
108
129
|
const found: { kind: ArtifactKind; source: string }[] = [];
|
|
109
130
|
for (const kind of kinds) {
|
|
@@ -142,15 +163,11 @@ function slimForCopy(r: EvalResult, relDir: string, copied: Set<ArtifactKind>):
|
|
|
142
163
|
};
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
function rebuildSummary(
|
|
146
|
-
selected: SnapshotHandle[],
|
|
147
|
-
sourceRuns: RunHandle[],
|
|
148
|
-
results: EvalResult[],
|
|
149
|
-
dest: string,
|
|
150
|
-
): RunSummary {
|
|
166
|
+
function rebuildSummary(selected: Snapshot[], results: EvalResult[], dest: string): RunSummary {
|
|
151
167
|
// 版本元数据与 run 级元数据取最新源 run 的(producer 缺失的 legacy 源保持缺失,不冒充)。
|
|
168
|
+
const sourceRuns = uniqueRuns(selected);
|
|
152
169
|
let newest = sourceRuns[0];
|
|
153
|
-
for (const run of sourceRuns) if (
|
|
170
|
+
for (const run of sourceRuns) if (isNewerRunDir(run, newest)) newest = run;
|
|
154
171
|
|
|
155
172
|
const counts = { passed: 0, failed: 0, skipped: 0, errored: 0 };
|
|
156
173
|
for (const r of results) counts[r.outcome] += 1;
|
|
@@ -171,9 +188,46 @@ function rebuildSummary(
|
|
|
171
188
|
...usageAndCost(results),
|
|
172
189
|
results,
|
|
173
190
|
outputDir: dest,
|
|
191
|
+
snapshots: snapshotsMeta(selected, newest.summary.startedAt),
|
|
174
192
|
};
|
|
175
193
|
}
|
|
176
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 快照级元数据补记:startedAt 保留各快照自己的时刻(它们可能来自不同源 run),
|
|
197
|
+
* knownEvalIds = 复制时刻该实验已知的 eval 并集(openResults 产出的快照从所属实验取,
|
|
198
|
+
* 手工构造的快照退回「同 id 输入快照的覆盖 ∪ 携带值」)—— 残缺检测的分母随数据走。
|
|
199
|
+
*/
|
|
200
|
+
function snapshotsMeta(selected: Snapshot[], summaryStartedAt: string): NonNullable<RunSummary["snapshots"]> {
|
|
201
|
+
// 同一 experiment 被选了多个快照(未 dedupe)时取最新的那个,与工件胜者规则一致。
|
|
202
|
+
const bySnapshotId = new Map<string, Snapshot>();
|
|
203
|
+
for (const snapshot of selected) {
|
|
204
|
+
const existing = bySnapshotId.get(snapshot.experimentId);
|
|
205
|
+
if (!existing || isNewerRunDir(snapshot.runDir, existing.runDir)) {
|
|
206
|
+
bySnapshotId.set(snapshot.experimentId, snapshot);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const meta: NonNullable<RunSummary["snapshots"]> = {};
|
|
211
|
+
for (const [experimentId, snapshot] of bySnapshotId) {
|
|
212
|
+
const union = experimentOfSnapshot(snapshot)?.evalIds ?? fallbackUnion(selected, experimentId);
|
|
213
|
+
meta[experimentId] = {
|
|
214
|
+
...(snapshot.startedAt !== summaryStartedAt ? { startedAt: snapshot.startedAt } : {}),
|
|
215
|
+
knownEvalIds: union,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return meta;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function fallbackUnion(selected: Snapshot[], experimentId: string): string[] {
|
|
222
|
+
const ids = new Set<string>();
|
|
223
|
+
for (const snapshot of selected) {
|
|
224
|
+
if (snapshot.experimentId !== experimentId) continue;
|
|
225
|
+
for (const ev of snapshot.evals) ids.add(ev.id);
|
|
226
|
+
for (const known of snapshot.knownEvalIds ?? []) ids.add(known);
|
|
227
|
+
}
|
|
228
|
+
return [...ids].sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
177
231
|
/** usage / 成本按选中条目重算;口径同 runner 的 summarize(没有任何 usage 就不写)。 */
|
|
178
232
|
function usageAndCost(results: EvalResult[]): Pick<RunSummary, "usage" | "estimatedCostUSD"> {
|
|
179
233
|
const usages = results.map((r) => r.usage).filter((u): u is Usage => u !== undefined);
|
|
@@ -191,10 +245,10 @@ function usageAndCost(results: EvalResult[]): Pick<RunSummary, "usage" | "estima
|
|
|
191
245
|
return { usage, estimatedCostUSD: cost || undefined };
|
|
192
246
|
}
|
|
193
247
|
|
|
194
|
-
function uniqueRuns(snapshots:
|
|
195
|
-
const runs:
|
|
248
|
+
function uniqueRuns(snapshots: Snapshot[]): RunDir[] {
|
|
249
|
+
const runs: RunDir[] = [];
|
|
196
250
|
for (const snapshot of snapshots) {
|
|
197
|
-
if (!runs.includes(snapshot.
|
|
251
|
+
if (!runs.includes(snapshot.runDir)) runs.push(snapshot.runDir);
|
|
198
252
|
}
|
|
199
253
|
return runs;
|
|
200
254
|
}
|