niceeval 0.7.1 → 0.8.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/dist/report/components.d.ts +4 -5
- package/dist/report/components.js +7 -7
- package/dist/report/compute.d.ts +4 -4
- package/dist/report/compute.js +8 -12
- package/dist/report/index.d.ts +2 -2
- package/dist/report/report.d.ts +23 -1
- package/dist/report/report.js +82 -5
- package/dist/report/types.d.ts +0 -10
- package/dist/results/types.d.ts +0 -11
- package/docs-site/zh/how-to/custom-reports.mdx +3 -3
- package/docs-site/zh/how-to/publish-report.mdx +43 -9
- package/docs-site/zh/how-to/viewing-results.mdx +4 -4
- package/docs-site/zh/reference/cli.mdx +2 -3
- package/docs-site/zh/reference/report-components.mdx +2 -2
- package/docs-site/zh/reference/results-data.mdx +2 -4
- package/docs-site/zh/troubleshooting/debugging.mdx +2 -2
- package/package.json +7 -6
- package/src/cli.ts +1 -5
- package/src/context/context.ts +11 -5
- package/src/report/components.tsx +13 -15
- package/src/report/compute.ts +8 -20
- package/src/report/index.ts +1 -1
- package/src/report/report.test.ts +2 -20
- package/src/report/report.ts +128 -6
- package/src/report/shell-head.test.ts +102 -0
- package/src/report/types.ts +0 -11
- package/src/results/copy.ts +15 -78
- package/src/results/publish.ts +4 -146
- package/src/results/results.test.ts +8 -8
- package/src/results/types.ts +0 -7
- package/src/show/report-host.ts +13 -0
- package/src/view/app/components/CodeView.test.tsx +142 -0
- package/src/view/app/components/CodeView.tsx +15 -1
- package/src/view/app/components/Transcript.tsx +28 -1
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/guards.test.ts +108 -0
- package/src/view/app/lib/guards.ts +13 -3
- package/src/view/app/lib/transcript-data.tsx +14 -0
- package/src/view/app/types.ts +17 -1
- package/src/view/artifact-serving.test.ts +1 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.ts +54 -14
- package/src/view/index.ts +18 -58
- package/src/view/server.ts +49 -144
- package/src/view/site-head.test.ts +177 -0
- package/src/view/site-parity.test.ts +117 -0
- package/src/view/site.ts +209 -0
- package/src/view/styles.css +10 -0
- package/src/view/view-report.test.ts +6 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReportComponent } from "./tree.ts";
|
|
2
2
|
import type { ReportLocale } from "./locale.ts";
|
|
3
3
|
import type { AttemptLocator } from "../results/locator.ts";
|
|
4
|
-
import type { AttemptListItem, DeltaData,
|
|
4
|
+
import type { AttemptListItem, DeltaData, EvalListItem, ExperimentComparisonData, ExperimentListItem, LineData, MatrixData, ReportInput, ScatterData, ScopeSummaryData, ScoreboardData, TableData } from "./types.ts";
|
|
5
5
|
import { type DeltaTableOptions, type MetricLineOptions, type MetricMatrixOptions, type MetricScatterOptions, type MetricTableOptions, type ScoreboardOptions } from "./compute.ts";
|
|
6
6
|
type Never<T> = {
|
|
7
7
|
[K in keyof T]?: never;
|
|
@@ -38,7 +38,7 @@ export declare const ExperimentComparison: ReportComponent<ExperimentComparisonP
|
|
|
38
38
|
interface EntityListChrome extends ChromeProps {
|
|
39
39
|
attemptHref?: (locator: AttemptLocator) => string;
|
|
40
40
|
}
|
|
41
|
-
export type ExperimentListProps = DataProps<readonly ExperimentListItem[],
|
|
41
|
+
export type ExperimentListProps = DataProps<readonly ExperimentListItem[], Record<never, never>, EntityListChrome & {
|
|
42
42
|
/** web 面在比较表前显示实验过滤框;text 面忽略。 */
|
|
43
43
|
filter?: boolean;
|
|
44
44
|
/**
|
|
@@ -49,10 +49,10 @@ export type ExperimentListProps = DataProps<readonly ExperimentListItem[], Entit
|
|
|
49
49
|
}>;
|
|
50
50
|
/** 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。 */
|
|
51
51
|
export declare const ExperimentList: ReportComponent<ExperimentListProps>;
|
|
52
|
-
export type EvalListProps = DataProps<readonly EvalListItem[],
|
|
52
|
+
export type EvalListProps = DataProps<readonly EvalListItem[], Record<never, never>, EntityListChrome>;
|
|
53
53
|
/** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
|
|
54
54
|
export declare const EvalList: ReportComponent<EvalListProps>;
|
|
55
|
-
export type AttemptListProps = DataProps<readonly AttemptListItem[],
|
|
55
|
+
export type AttemptListProps = DataProps<readonly AttemptListItem[], Record<never, never>, EntityListChrome & {
|
|
56
56
|
/** 过滤 / 截断前的总数;省略时等于 data 长度。 */
|
|
57
57
|
total?: number;
|
|
58
58
|
}>;
|
|
@@ -63,7 +63,6 @@ export interface FailureListProps {
|
|
|
63
63
|
limit?: number;
|
|
64
64
|
/** 默认宿主注入的 Scope。 */
|
|
65
65
|
input?: ReportInput;
|
|
66
|
-
redact?: (text: string) => string;
|
|
67
66
|
attemptHref?: (locator: AttemptLocator) => string;
|
|
68
67
|
locale?: ReportLocale;
|
|
69
68
|
className?: string;
|
|
@@ -261,8 +261,8 @@ export const ExperimentList = makeDataComponent({
|
|
|
261
261
|
name: "ExperimentList",
|
|
262
262
|
dataFnName: "experimentListData",
|
|
263
263
|
shapeName: "ExperimentListItem[]",
|
|
264
|
-
dataFn: (input
|
|
265
|
-
specKeys: [
|
|
264
|
+
dataFn: (input) => experimentListData(input),
|
|
265
|
+
specKeys: [],
|
|
266
266
|
validate: validateExperimentListData,
|
|
267
267
|
web: (props, ctx) => (_jsx(ExperimentListWeb, { data: props.data, filter: props.filter, relativeTo: props.relativeTo, locale: props.locale ?? ctx.locale, attemptHref: hrefOf(props, ctx) ?? ctx.attemptHref, className: props.className })),
|
|
268
268
|
text: (props, ctx) => experimentListText(props.data, ctx, props.relativeTo),
|
|
@@ -272,8 +272,8 @@ export const EvalList = makeDataComponent({
|
|
|
272
272
|
name: "EvalList",
|
|
273
273
|
dataFnName: "evalListData",
|
|
274
274
|
shapeName: "EvalListItem[]",
|
|
275
|
-
dataFn: (input
|
|
276
|
-
specKeys: [
|
|
275
|
+
dataFn: (input) => evalListData(input),
|
|
276
|
+
specKeys: [],
|
|
277
277
|
validate: validateEvalListData,
|
|
278
278
|
web: (props, ctx) => (_jsx(EvalListWeb, { data: props.data, locale: props.locale ?? ctx.locale, attemptHref: hrefOf(props, ctx) ?? ctx.attemptHref, className: props.className })),
|
|
279
279
|
text: (props, ctx) => evalListText(props.data, ctx),
|
|
@@ -283,8 +283,8 @@ export const AttemptList = makeDataComponent({
|
|
|
283
283
|
name: "AttemptList",
|
|
284
284
|
dataFnName: "attemptListData",
|
|
285
285
|
shapeName: "AttemptListItem[]",
|
|
286
|
-
dataFn: (input
|
|
287
|
-
specKeys: [
|
|
286
|
+
dataFn: (input) => attemptListData(input),
|
|
287
|
+
specKeys: [],
|
|
288
288
|
validate: validateAttemptListData,
|
|
289
289
|
web: (props, ctx) => (_jsx(AttemptListWeb, { data: props.data, total: props.total, locale: props.locale ?? ctx.locale, attemptHref: hrefOf(props, ctx) ?? ctx.attemptHref, className: props.className })),
|
|
290
290
|
text: (props, ctx) => attemptListText(props.data, props.total, ctx),
|
|
@@ -297,7 +297,7 @@ export const AttemptList = makeDataComponent({
|
|
|
297
297
|
*/
|
|
298
298
|
export const FailureList = defineComponent(async (props, ctx) => {
|
|
299
299
|
const input = props.input ?? ctx.scope;
|
|
300
|
-
const all = await attemptListData(input
|
|
300
|
+
const all = await attemptListData(input);
|
|
301
301
|
// attempt 开始时间不在列表条目里(它不是列表展示字段);从同一 input 的读取面按 locator 对回。
|
|
302
302
|
const startedAtByLocator = new Map();
|
|
303
303
|
for (const item of collectItems(resolveInput(input).snapshots)) {
|
package/dist/report/compute.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AttemptListItem, DeltaData, DeltaPair, DimensionInput,
|
|
1
|
+
import type { AttemptListItem, DeltaData, DeltaPair, DimensionInput, EvalListItem, ExperimentComparisonData, ExperimentListItem, FlagPairs, LineData, MatrixData, Metric, NumericAxis, ReportInput, ScatterData, ScopeSummaryData, ScoreboardData, TableData } from "./types.ts";
|
|
2
2
|
import type { JsonValue } from "../types.ts";
|
|
3
3
|
export interface MetricTableOptions {
|
|
4
4
|
/** 行维度(内置 / 自定义 / flag() / runConfig())。 */
|
|
@@ -23,9 +23,9 @@ export interface MetricMatrixOptions {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function metricMatrixData(input: ReportInput, options: MetricMatrixOptions): Promise<MatrixData>;
|
|
25
25
|
/** `attemptListData(input)`:每个 Attempt 一项,顺序取自 Scope 展平顺序(不重排)。 */
|
|
26
|
-
export declare function attemptListData(input: ReportInput
|
|
26
|
+
export declare function attemptListData(input: ReportInput): Promise<AttemptListItem[]>;
|
|
27
27
|
/** `evalListData(input)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
|
|
28
|
-
export declare function evalListData(input: ReportInput
|
|
28
|
+
export declare function evalListData(input: ReportInput): Promise<EvalListItem[]>;
|
|
29
29
|
/**
|
|
30
30
|
* `experimentListData(input)`:每个 experiment 一项,展开到每道 Eval;初始按端到端成功率
|
|
31
31
|
* 从高到低(缺数据沉底,同分按 id)。一行只有一套 agent / model / flags 是输入约束:
|
|
@@ -33,7 +33,7 @@ export declare function evalListData(input: ReportInput, options?: EntityListDat
|
|
|
33
33
|
* Snapshot[] 时若同一 experiment 混入不一致的可比性配置,按完整用户反馈失败并指引——
|
|
34
34
|
* 看跨配置演化用 snapshot 维度或 MetricLine,不把两套配置拼成一行冒充单一配置。
|
|
35
35
|
*/
|
|
36
|
-
export declare function experimentListData(input: ReportInput
|
|
36
|
+
export declare function experimentListData(input: ReportInput): Promise<ExperimentListItem[]>;
|
|
37
37
|
/**
|
|
38
38
|
* `scopeSummaryData(input)`:范围摘要——快照时间窗、experiment / eval / attempt 数、
|
|
39
39
|
* 两级判定计票、端到端成功率与总成本(docs/feature/reports/library/summaries.md)。
|
package/dist/report/compute.js
CHANGED
|
@@ -117,9 +117,8 @@ function failureSummaryOf(result) {
|
|
|
117
117
|
}
|
|
118
118
|
return { summary: null, more: 0 };
|
|
119
119
|
}
|
|
120
|
-
const identityRedact = (text) => text;
|
|
121
120
|
/** AttemptList / ExperimentList / EvalList 共用的叶子构造:一个 Item → 一个 AttemptListItem。 */
|
|
122
|
-
async function attemptListItemOf(item
|
|
121
|
+
async function attemptListItemOf(item) {
|
|
123
122
|
const result = item.attempt.result;
|
|
124
123
|
const { summary, more } = failureSummaryOf(result);
|
|
125
124
|
return {
|
|
@@ -128,7 +127,7 @@ async function attemptListItemOf(item, redact) {
|
|
|
128
127
|
attempt: result.attempt,
|
|
129
128
|
agent: result.agent,
|
|
130
129
|
verdict: result.verdict,
|
|
131
|
-
failureSummary: summary
|
|
130
|
+
failureSummary: summary,
|
|
132
131
|
moreFailures: more,
|
|
133
132
|
examScore: await computeCell(examScore, [item]),
|
|
134
133
|
durationMs: result.durationMs,
|
|
@@ -137,16 +136,14 @@ async function attemptListItemOf(item, redact) {
|
|
|
137
136
|
};
|
|
138
137
|
}
|
|
139
138
|
/** `attemptListData(input)`:每个 Attempt 一项,顺序取自 Scope 展平顺序(不重排)。 */
|
|
140
|
-
export async function attemptListData(input
|
|
139
|
+
export async function attemptListData(input) {
|
|
141
140
|
const { snapshots } = resolveInput(input);
|
|
142
|
-
const redact = options?.redact ?? identityRedact;
|
|
143
141
|
const items = collectItems(snapshots);
|
|
144
|
-
return Promise.all(items.map((item) => attemptListItemOf(item
|
|
142
|
+
return Promise.all(items.map((item) => attemptListItemOf(item)));
|
|
145
143
|
}
|
|
146
144
|
/** `evalListData(input)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
|
|
147
|
-
export async function evalListData(input
|
|
145
|
+
export async function evalListData(input) {
|
|
148
146
|
const { snapshots } = resolveInput(input);
|
|
149
|
-
const redact = options?.redact ?? identityRedact;
|
|
150
147
|
const items = collectItems(snapshots);
|
|
151
148
|
const groups = new Map();
|
|
152
149
|
for (const item of items) {
|
|
@@ -161,7 +158,7 @@ export async function evalListData(input, options) {
|
|
|
161
158
|
for (const group of groups.values()) {
|
|
162
159
|
const sorted = [...group].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
|
|
163
160
|
const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
|
|
164
|
-
const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item
|
|
161
|
+
const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item)));
|
|
165
162
|
out.push({
|
|
166
163
|
experimentId: experimentIdOf(sorted[0]),
|
|
167
164
|
evalId: evalIdOf(sorted[0]),
|
|
@@ -182,9 +179,8 @@ export async function evalListData(input, options) {
|
|
|
182
179
|
* Snapshot[] 时若同一 experiment 混入不一致的可比性配置,按完整用户反馈失败并指引——
|
|
183
180
|
* 看跨配置演化用 snapshot 维度或 MetricLine,不把两套配置拼成一行冒充单一配置。
|
|
184
181
|
*/
|
|
185
|
-
export async function experimentListData(input
|
|
182
|
+
export async function experimentListData(input) {
|
|
186
183
|
const { snapshots } = resolveInput(input);
|
|
187
|
-
const redact = options?.redact ?? identityRedact;
|
|
188
184
|
// 可比性配置单义检查:同一 experiment 的输入快照必须共享一套可比性配置。
|
|
189
185
|
const configByExperiment = new Map();
|
|
190
186
|
for (const snapshot of snapshots) {
|
|
@@ -211,7 +207,7 @@ export async function experimentListData(input, options) {
|
|
|
211
207
|
for (const [evalId, evalItems] of evalGroups) {
|
|
212
208
|
const sorted = [...evalItems].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
|
|
213
209
|
const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
|
|
214
|
-
const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item
|
|
210
|
+
const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item)));
|
|
215
211
|
evalRows.push({
|
|
216
212
|
evalId,
|
|
217
213
|
verdict,
|
package/dist/report/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { assistantTurns, costUSD, defineMetric, durationMs, endToEndPassRate, examScore, executionReliability, repeatedFailedCommands, taskPassRate, tokens, } from "./metrics.ts";
|
|
2
2
|
export { flag, numericFlag, numericRunConfig, runConfig } from "./flag.ts";
|
|
3
3
|
export { buildReportMeta, defineReport, isReportDefinition, pickReportPage, renderReportToText, renderReportTreeToText, reportTitleText, resolveReportTitle, ReportPageNotFoundError, DEFAULT_PAGE_ID, } from "./report.ts";
|
|
4
|
-
export type { HostCommandContext, NonEmptyArray, RenderReportTextOptions, RenderTreeTextOptions, ReportTreeHostContext, ReportAsset, ReportDef, ReportDefinition, ReportHostContext, ReportLink, ReportMeta, ReportPage, ReportShell, } from "./report.ts";
|
|
4
|
+
export type { HostCommandContext, NonEmptyArray, RenderReportTextOptions, RenderTreeTextOptions, ReportTreeHostContext, HeadTag, ReportAsset, ReportDef, ReportDefinition, ReportHostContext, ReportLink, ReportMeta, ReportPage, ReportShell, } from "./report.ts";
|
|
5
5
|
export { defineComponent, createTextContext, renderNodeToText, resolveReportTree, validateReportTree, ResolveMemo } from "./tree.ts";
|
|
6
6
|
export type { ComponentFaces, ComposeContext, ReportComponent, ReportElement, ReportNode, ResolveContext, ResolveEnv, TextContext, TextRenderOptions, WebContext, } from "./tree.ts";
|
|
7
7
|
export { Col, Row, Section, Style, Tab, Table, Tabs, Text } from "./primitives.tsx";
|
|
@@ -14,6 +14,6 @@ export { AttemptList, DeltaTable, EvalList, ExperimentComparison, ExperimentList
|
|
|
14
14
|
export type { AttemptListProps, DataProps, DeltaTableProps, EvalListProps, ExperimentComparisonProps, ExperimentListProps, FailureListProps, MetricBarsProps, MetricLineProps, MetricMatrixProps, MetricScatterProps, MetricTableProps, ScoreboardProps, ScopeSummaryProps, } from "./components.tsx";
|
|
15
15
|
export { attemptListData, deltaTableData, evalListData, experimentComparisonData, experimentListData, metricLineData, metricMatrixData, metricScatterData, metricTableData, pairsByFlag, scopeSummaryData, scoreboardData, } from "./compute.ts";
|
|
16
16
|
export type { DeltaTableOptions, MetricLineOptions, MetricMatrixOptions, MetricScatterOptions, MetricTableOptions, ScoreboardOptions, } from "./compute.ts";
|
|
17
|
-
export type { Aggregator, AttemptListItem, AttemptLocator, BuiltInDimension, CustomDimension, DeltaData, DeltaPair, DimensionInput, DimensionOptions, DimensionRef,
|
|
17
|
+
export type { Aggregator, AttemptListItem, AttemptLocator, BuiltInDimension, CustomDimension, DeltaData, DeltaPair, DimensionInput, DimensionOptions, DimensionRef, EvalListItem, ExperimentComparisonData, ExperimentComparisonGroupData, ExperimentListEvalRow, ExperimentListItem, FlagPairs, LineData, MatrixData, Metric, MetricAggregate, MetricCell, MetricColumn, NumericAxis, NumericAxisOptions, NumericRunConfigAxisOptions, ReportInput, RunConfigKey, ScatterData, ScopeSummaryData, ScopeWarning, ScoreboardData, TableData, VerdictTally, } from "./types.ts";
|
|
18
18
|
export type { AttemptHandle, Results, Scope, Snapshot } from "../results/types.ts";
|
|
19
19
|
export { experimentGroupOf } from "../shared/aggregate.ts";
|
package/dist/report/report.d.ts
CHANGED
|
@@ -21,6 +21,21 @@ export type ReportAsset = {
|
|
|
21
21
|
inline: string;
|
|
22
22
|
src?: never;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* 结构化 head 标签。tag 是白名单闭集——head 是元数据与第三方脚本的注入口,不是 HTML 后门。
|
|
26
|
+
* attrs 值为 true 渲染裸布尔属性(async、defer),字符串渲染 `key="value"`(值转义后落 HTML);
|
|
27
|
+
* 属性语义与脚本内容同一约定——作者义务,宿主不校验。
|
|
28
|
+
* meta / link 无子内容由类型表达;script / style 的 children 是原样文本,不转义。
|
|
29
|
+
*/
|
|
30
|
+
export type HeadTag = {
|
|
31
|
+
tag: "meta" | "link";
|
|
32
|
+
attrs: Record<string, string | true>;
|
|
33
|
+
children?: never;
|
|
34
|
+
} | {
|
|
35
|
+
tag: "script" | "style";
|
|
36
|
+
attrs?: Record<string, string | true>;
|
|
37
|
+
children?: string;
|
|
38
|
+
};
|
|
24
39
|
export interface ReportShell {
|
|
25
40
|
/** 标题:首页 hero 与浏览器标题。页头左端是恒定的 NiceEval 品牌字标,不由 title 覆盖;回退链 def.title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」。 */
|
|
26
41
|
title?: LocalizedText;
|
|
@@ -28,6 +43,12 @@ export interface ReportShell {
|
|
|
28
43
|
links?: ReportLink[];
|
|
29
44
|
/** 每页页脚的一段文字;省略时不渲染页脚(品牌行恒在 hero 下方,不占页脚)。 */
|
|
30
45
|
footer?: LocalizedText;
|
|
46
|
+
/**
|
|
47
|
+
* 注入每页 `<head>` 的结构化标签,在官方与外壳样式之后按声明顺序渲染。
|
|
48
|
+
* 第三方 snippet(分析、埋点、评论)、SEO meta、favicon、字体、JSON-LD 的家:
|
|
49
|
+
* 声明什么标签就渲染什么标签,宿主只做结构校验,新的第三方接入不需要契约变更。
|
|
50
|
+
*/
|
|
51
|
+
head?: HeadTag[];
|
|
31
52
|
/** 注入每个页面的脚本,在官方增强脚本之后、按声明顺序于 </body> 前加载。 */
|
|
32
53
|
scripts?: ReportAsset[];
|
|
33
54
|
/** 注入每个页面的样式表,在官方样式之后按声明顺序加载。 */
|
|
@@ -53,13 +74,14 @@ export type ReportDef = ReportShell & ({
|
|
|
53
74
|
/**
|
|
54
75
|
* defineReport 的唯一产物:只作 --report 文件的默认导出,交给宿主装载。
|
|
55
76
|
* 它不是 ReportNode——不能放进任何 content 或报告树,外壳因此不可嵌套。
|
|
56
|
-
* 字段是装载规范化后的形态:pages 恒非空,links / scripts / styles 恒为数组。
|
|
77
|
+
* 字段是装载规范化后的形态:pages 恒非空,links / head / scripts / styles 恒为数组。
|
|
57
78
|
*/
|
|
58
79
|
export interface ReportDefinition {
|
|
59
80
|
readonly kind: "report";
|
|
60
81
|
readonly title?: LocalizedText;
|
|
61
82
|
readonly links: readonly ReportLink[];
|
|
62
83
|
readonly footer?: LocalizedText;
|
|
84
|
+
readonly head: readonly HeadTag[];
|
|
63
85
|
readonly scripts: readonly ReportAsset[];
|
|
64
86
|
readonly styles: readonly ReportAsset[];
|
|
65
87
|
readonly pages: NonEmptyArray<ReportPage>;
|
package/dist/report/report.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// defineReport:唯一可被宿主装载的产物 —— 一层外壳(
|
|
1
|
+
// defineReport:唯一可被宿主装载的产物 —— 一层外壳(标题、外链、页脚、head 标签、脚本、样式)加
|
|
2
2
|
// 非空页列表;单页与多页不是两种机制,页数只是列表长度(docs/feature/reports/library/shell.md)。
|
|
3
3
|
// 入参有两级缩写,各有精确展开:树入参 ≡ { content: 树 } ≡ pages: [{ id: "report",
|
|
4
4
|
// title: 内置页名, content: 树 }]。`content` 与 `pages` 恰好声明一个,没有隐式默认。
|
|
@@ -51,6 +51,13 @@ function assertLocalizedText(value, where) {
|
|
|
51
51
|
throw new Error(`${where} must be a LocalizedText (a string, or a { [locale]: string } record); got ${typeof value}.`);
|
|
52
52
|
}
|
|
53
53
|
const PAGE_ID_PATTERN = /^[a-z0-9-]+$/;
|
|
54
|
+
/** 本地资产路径纪律(shell.md「行为约束」):相对报告文件的普通相对路径,拒绝 `..` 段、绝对路径与 `~`。 */
|
|
55
|
+
function assertLocalAssetPath(src, where) {
|
|
56
|
+
const segments = src.split(/[\\/]+/);
|
|
57
|
+
if (src.startsWith("/") || /^[A-Za-z]:/.test(src) || src.startsWith("~") || segments.includes("..")) {
|
|
58
|
+
throw new Error(`defineReport ${where} "${src}" is not allowed: only plain relative paths (optionally with a ./ prefix) resolve against the report file — no ".." segments, absolute paths, or "~". Move the asset next to the report file and reference it relatively.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
54
61
|
function assertAssets(assets, field) {
|
|
55
62
|
if (assets === undefined)
|
|
56
63
|
return [];
|
|
@@ -65,21 +72,90 @@ function assertAssets(assets, field) {
|
|
|
65
72
|
}
|
|
66
73
|
if (hasSrc) {
|
|
67
74
|
const src = asset.src;
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
throw new Error(`defineReport ${field} src "${src}" is
|
|
75
|
+
// 外链不属于增强层资产:第三方外链标签的家是 head 通道。
|
|
76
|
+
if (/^https?:\/\//i.test(src) || src.startsWith("//")) {
|
|
77
|
+
throw new Error(`defineReport ${field} src "${src}" is an external URL — ${field} take local files and inline content (the host pipeline vendors them). Declare third-party external tags in "head" instead, e.g. head: [{ tag: "script", attrs: { async: true, src: "…" } }].`);
|
|
71
78
|
}
|
|
79
|
+
assertLocalAssetPath(src, `${field} src`);
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
return assets;
|
|
75
83
|
}
|
|
84
|
+
const HEAD_TAG_NAMES = new Set(["meta", "link", "script", "style"]);
|
|
85
|
+
const HEAD_ATTR_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_.:-]*$/;
|
|
86
|
+
function assertHeadTags(tags) {
|
|
87
|
+
if (tags === undefined)
|
|
88
|
+
return [];
|
|
89
|
+
if (!Array.isArray(tags)) {
|
|
90
|
+
throw new Error('defineReport head must be an array of { tag, attrs?, children? } entries (tag: "meta" | "link" | "script" | "style").');
|
|
91
|
+
}
|
|
92
|
+
for (const entry of tags) {
|
|
93
|
+
const tag = entry?.tag;
|
|
94
|
+
// 白名单闭集:head 是元数据与第三方脚本的注入口,不是 HTML 后门;标题走 title 字段回退链。
|
|
95
|
+
if (typeof tag !== "string" || !HEAD_TAG_NAMES.has(tag)) {
|
|
96
|
+
throw new Error(`defineReport head tag ${JSON.stringify(tag)} is not allowed — head injects metadata and third-party tags, and the allowed tags are "meta", "link", "script", "style". For the document title, use the shell "title" field instead.`);
|
|
97
|
+
}
|
|
98
|
+
const attrs = entry.attrs;
|
|
99
|
+
if (attrs !== undefined && (typeof attrs !== "object" || attrs === null || Array.isArray(attrs))) {
|
|
100
|
+
throw new Error(`defineReport head <${tag}> attrs must be a { name: string | true } record (true renders a bare boolean attribute like async).`);
|
|
101
|
+
}
|
|
102
|
+
if ((tag === "meta" || tag === "link") && attrs === undefined) {
|
|
103
|
+
throw new Error(`defineReport head <${tag}> needs attrs — a bare <${tag}> renders nothing. Declare e.g. { tag: "${tag}", attrs: { ${tag === "meta" ? 'name: "…", content: "…"' : 'rel: "…", href: "…"'} } }.`);
|
|
104
|
+
}
|
|
105
|
+
const attrRecord = (attrs ?? {});
|
|
106
|
+
for (const [name, value] of Object.entries(attrRecord)) {
|
|
107
|
+
if (!HEAD_ATTR_NAME_PATTERN.test(name)) {
|
|
108
|
+
throw new Error(`defineReport head <${tag}> attribute name ${JSON.stringify(name)} is not a valid HTML attribute name. Use letters, digits, "-", "_", ":" or ".".`);
|
|
109
|
+
}
|
|
110
|
+
if (value !== true && typeof value !== "string") {
|
|
111
|
+
throw new Error(`defineReport head <${tag}> attribute "${name}" must be a string or true (true renders a bare boolean attribute like async); got ${typeof value}.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 宿主自有的文档单例:charset / viewport 由宿主外壳拥有,声明它们装载报错。
|
|
115
|
+
if (tag === "meta" && attrRecord.charset !== undefined) {
|
|
116
|
+
throw new Error("defineReport head must not declare <meta charset> — the document charset is owned by the host shell. Remove the entry.");
|
|
117
|
+
}
|
|
118
|
+
if (tag === "meta" && typeof attrRecord.name === "string" && attrRecord.name.toLowerCase() === "viewport") {
|
|
119
|
+
throw new Error('defineReport head must not declare <meta name="viewport"> — the viewport is owned by the host shell. Remove the entry.');
|
|
120
|
+
}
|
|
121
|
+
const children = entry.children;
|
|
122
|
+
if (children !== undefined) {
|
|
123
|
+
if (tag === "meta" || tag === "link") {
|
|
124
|
+
throw new Error(`defineReport head <${tag}> does not take children — <${tag}> is a void element; put the content in attrs.`);
|
|
125
|
+
}
|
|
126
|
+
if (typeof children !== "string") {
|
|
127
|
+
throw new Error(`defineReport head <${tag}> children must be a string of literal ${tag === "script" ? "JavaScript" : "CSS"}; got ${typeof children}.`);
|
|
128
|
+
}
|
|
129
|
+
// children 原样落进标签,闭合序列在该上下文无法转义,会提前截断标签。
|
|
130
|
+
if (children.toLowerCase().includes(`</${tag}`)) {
|
|
131
|
+
throw new Error(`defineReport head <${tag}> children contain "</${tag}>" — that sequence cannot be escaped inside a <${tag}> and would close the tag early. Split the content into two entries or move it into a local file asset.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// src / href 按 scheme 分流:http(s) 外链原样透传;其余按本地路径纪律解析。
|
|
135
|
+
for (const name of ["src", "href"]) {
|
|
136
|
+
const value = attrRecord[name];
|
|
137
|
+
if (typeof value !== "string")
|
|
138
|
+
continue;
|
|
139
|
+
if (/^https?:\/\//i.test(value))
|
|
140
|
+
continue;
|
|
141
|
+
if (value.startsWith("//")) {
|
|
142
|
+
throw new Error(`defineReport head <${tag}> ${name} "${value}" is protocol-relative — declare the scheme explicitly, e.g. "https:${value}".`);
|
|
143
|
+
}
|
|
144
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) {
|
|
145
|
+
throw new Error(`defineReport head <${tag}> ${name} "${value}" uses a scheme other than http(s) — external head assets must be http(s) URLs. Anything else, ship as a local file next to the report and reference it relatively.`);
|
|
146
|
+
}
|
|
147
|
+
assertLocalAssetPath(value, `head <${tag}> ${name}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return tags;
|
|
151
|
+
}
|
|
76
152
|
export function defineReport(input) {
|
|
77
153
|
assertNotDefinition(input, "defineReport(...)");
|
|
78
154
|
const def = isReportNodeInput(input)
|
|
79
155
|
? { content: input }
|
|
80
156
|
: input;
|
|
81
157
|
if (typeof def !== "object" || def === null) {
|
|
82
|
-
throw new Error("defineReport expects a report tree or a config object ({ title?, links?, footer?, scripts?, styles?, content | pages }). " +
|
|
158
|
+
throw new Error("defineReport expects a report tree or a config object ({ title?, links?, footer?, head?, scripts?, styles?, content | pages }). " +
|
|
83
159
|
CONTENT_NEXT_STEP);
|
|
84
160
|
}
|
|
85
161
|
const hasContent = "content" in def && def.content !== undefined;
|
|
@@ -143,6 +219,7 @@ export function defineReport(input) {
|
|
|
143
219
|
...(def.title !== undefined ? { title: def.title } : {}),
|
|
144
220
|
links: [...links],
|
|
145
221
|
...(def.footer !== undefined ? { footer: def.footer } : {}),
|
|
222
|
+
head: assertHeadTags(def.head),
|
|
146
223
|
scripts: assertAssets(def.scripts, "scripts"),
|
|
147
224
|
styles: assertAssets(def.styles, "styles"),
|
|
148
225
|
pages: pages,
|
package/dist/report/types.d.ts
CHANGED
|
@@ -359,13 +359,3 @@ export interface ExperimentListItem {
|
|
|
359
359
|
lastRunAt: string;
|
|
360
360
|
evalRows: ExperimentListEvalRow[];
|
|
361
361
|
}
|
|
362
|
-
/** 三个实体列表共用的计算选项。 */
|
|
363
|
-
export interface EntityListDataOptions {
|
|
364
|
-
/**
|
|
365
|
-
* 展示层遮蔽:只改写这次组件数据中的自由文本——条目本身与任何嵌套 attempt 条目的
|
|
366
|
-
* `failureSummary`;身份与分类字段(experimentId、evalId、locator、数值指标)不经它。
|
|
367
|
-
* 只作用于这次计算产出的组件数据,不改盘上或任何导出目录里的 artifact;
|
|
368
|
-
* 发布 artifact 的脱敏用 copySnapshots({ redact })。
|
|
369
|
-
*/
|
|
370
|
-
redact?: (text: string) => string;
|
|
371
|
-
}
|
package/dist/results/types.d.ts
CHANGED
|
@@ -33,13 +33,6 @@ export interface SnapshotMeta {
|
|
|
33
33
|
completedAt?: string;
|
|
34
34
|
/** 写入时刻该实验已知的 eval 并集 —— 残缺检测的分母随数据走(copySnapshots 自动补记,writer 可声明)。 */
|
|
35
35
|
knownEvalIds?: string[];
|
|
36
|
-
/**
|
|
37
|
-
* 发布拷贝的自描述标记:copySnapshots 补记,消毒函数 → "applied"、redact: false → "none";
|
|
38
|
-
* 本地事实根没有此字段。只声明流程,不证明无秘密;view --out 据此分级防呆。
|
|
39
|
-
*/
|
|
40
|
-
publish?: {
|
|
41
|
-
redaction: "applied" | "none";
|
|
42
|
-
};
|
|
43
36
|
/** 项目名(来自 config.name),透传给 `niceeval view` 顶部 hero 显示。 */
|
|
44
37
|
name?: LocalizedText;
|
|
45
38
|
}
|
|
@@ -117,10 +110,6 @@ export interface Snapshot {
|
|
|
117
110
|
dir: string;
|
|
118
111
|
/** 写入时刻该实验已知的 eval 并集(可选);copySnapshots 自动补记,writer.snapshot() 也可声明。 */
|
|
119
112
|
knownEvalIds?: string[];
|
|
120
|
-
/** 发布拷贝的自描述标记(见 SnapshotMeta.publish);本地事实根没有此字段。 */
|
|
121
|
-
publish?: {
|
|
122
|
-
redaction: "applied" | "none";
|
|
123
|
-
};
|
|
124
113
|
}
|
|
125
114
|
/** 一个实验的全部历史:同一 experiment id 的历次快照归在一起。 */
|
|
126
115
|
export interface Experiment {
|
|
@@ -6,7 +6,7 @@ description: "一份报告就是一个报告文件:官方宿主打开结果、
|
|
|
6
6
|
|
|
7
7
|
[查看结果](/zh/how-to/viewing-results)讲「用」:官方两扇门 `niceeval show`(终端)和 `niceeval view`(网页)怎么看。本页讲「写」:官方摆法不够时,怎么写一份自己的报告——考试成绩单、代码行数榜、质量 × 成本 frontier。
|
|
8
8
|
|
|
9
|
-
一份报告就是一个报告文件。你不用打开结果目录、不用写渲染代码、不用起自己的应用:`niceeval show` / `niceeval view` 本体就是宿主——替你打开结果、把数据注入进来,你用官方组件和 `Row` / `Col` 摆版面,写完把文件路径递给 `--report
|
|
9
|
+
一份报告就是一个报告文件。你不用打开结果目录、不用写渲染代码、不用起自己的应用:`niceeval show` / `niceeval view` 本体就是宿主——替你打开结果、把数据注入进来,你用官方组件和 `Row` / `Col` 摆版面,写完把文件路径递给 `--report`,终端和网页两扇门就都认它——官方的证据深链、`--results` 换根、静态导出,自定义报告全部原样享有。
|
|
10
10
|
|
|
11
11
|
## show / view 的默认报告也是一份报告定义
|
|
12
12
|
|
|
@@ -33,7 +33,7 @@ import { ExperimentComparison } from "niceeval/report";
|
|
|
33
33
|
|
|
34
34
|
先交代唯一的前置:报告文件是 `.tsx`,写它的项目要装 `react`(写自定义组件的 web 面还要 `@types/react`),tsconfig 里 `compilerOptions.jsx` 设为 `"react-jsx"`。裸跑 `niceeval show` / `niceeval view` 不需要这些——只有自己写报告文件才需要。
|
|
35
35
|
|
|
36
|
-
报告基座是 `defineReport`:宿主打开结果目录(含 `--
|
|
36
|
+
报告基座是 `defineReport`:宿主打开结果目录(含 `--results` 指定的结果根)、按官方口径挑好结果快照,注入给你的函数;你只负责折数据和摆积木:
|
|
37
37
|
|
|
38
38
|
```tsx
|
|
39
39
|
// reports/exam.tsx —— 一份定义,两扇门共用
|
|
@@ -65,7 +65,7 @@ niceeval view --report reports/exam.tsx # 网页:同一棵树走网页面
|
|
|
65
65
|
|
|
66
66
|
挑选提醒(覆盖不全、快照过期、有没跑完的运行)不用你在报告里专门摆一个组件来显示:不管你的报告摆了哪些组件,`niceeval show` 和 `niceeval view` 都会把 `selection.warnings` 显示在报告正文上方,不会因为你没摆某一块就静默丢掉。
|
|
67
67
|
|
|
68
|
-
命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--
|
|
68
|
+
命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--results` 把结果根换成指定目录,`--experiment` 让 Selection 只留该实验。`--history` 与 `--report` 互斥——趋势在报告里用 `exp.snapshots` 自己摆;证据切面(`--eval` / `--execution` / `--diff`)只看证据,不渲染报告。
|
|
69
69
|
|
|
70
70
|
页面里的每个组件都是**双面**的:网页面是 React 渲染,终端面是字符渲染,两面吃同一份算好的数据。实体列表按 experiment → Eval → Attempt 展示事实;指标表、矩阵、条形图、成绩单、散点图、趋势图和差异表展示聚合值。完整清单见[报告组件](/zh/reference/report-components)。网页面的实体、格子和点深链到 Attempt 详情,终端面印出对应的 `niceeval show <eval id>` 下钻命令。
|
|
71
71
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "通过 CI 发布报告"
|
|
3
3
|
sidebarTitle: "CI 发布报告"
|
|
4
|
-
description: "把经过 copySnapshots 大小预检的结果目录提交进仓库,CI 用一行 view --
|
|
4
|
+
description: "把经过 copySnapshots 大小预检的结果目录提交进仓库,CI 用一行 view --results 导出报告站;超大文件在 commit 前就会得到可执行错误。"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
`niceeval view --out <目录>` 把查看器导出成一个纯静态目录:报告、结果快照列表、transcript、trace 瀑布,和本地 `niceeval view` 看到的完全一样(导出行为见[查看结果 · 导出与静态托管](/zh/how-to/viewing-results#导出与静态托管))。CI 发布只需要让结果数据到构建机手里,但不要直接提交本地事实根 `.niceeval/`:逐字符串截断能防一条失控输出膨胀,不能保证整个文件小于 Git host 的限制。先用 `copySnapshots` 生成经过 50 MiB 单文件预检的发布结果根,再提交这个目录。
|
|
@@ -20,18 +20,15 @@ const results = await openResults(".niceeval");
|
|
|
20
20
|
await rm(output, { recursive: true, force: true });
|
|
21
21
|
await copySnapshots(results.latest(), output, {
|
|
22
22
|
artifacts: ["sources", "events", "trace", "o11y", "agentSetup"],
|
|
23
|
-
redact: (text) => text.replaceAll(/sk-[A-Za-z0-9]+/g, "[redacted]"),
|
|
24
23
|
});
|
|
25
24
|
```
|
|
26
25
|
|
|
27
|
-
`redact` 是必填项:给一个函数改写待发布文件里的自由文本,或者确认这批数据可以原文公开、显式传 `redact: false`。要发布的站点谁都能翻到 prompt 和工具输出,这个选择必须写在脚本里。
|
|
28
|
-
|
|
29
26
|
运行 `npx tsx scripts/publish-results.ts`,然后提交 `report-data/`。`copySnapshots` 在创建目录前检查所有待发布文件;任何文件超过 50 MiB 时整体失败并列出路径、大小和处理建议,不会留下半份目录。`diff` 缺省不发布;需要 diff 时显式加进 `artifacts`,它也受同一个预算约束。历史版本留下的超大 events / trace 不会被悄悄改写,预检会要求你排除这类证据或用当前版本重跑。
|
|
30
27
|
|
|
31
28
|
## 构建命令就是导出命令
|
|
32
29
|
|
|
33
30
|
```bash
|
|
34
|
-
npx niceeval view --
|
|
31
|
+
npx niceeval view --results report-data --out site
|
|
35
32
|
```
|
|
36
33
|
|
|
37
34
|
`view` 对零可读结果直接报错、非零退出,不会导出一张空报告——`report-data/` checkout 坏掉,或所有落盘与当前 niceeval 的 schemaVersion 不兼容被整批跳过时,构建失败,Vercel / GitHub Pages 保留上一次部署。错误逐条列出被跳过的快照目录与原因,schemaVersion 场景还给出能直接查看旧落盘的 `npx niceeval@<版本> view` 命令。
|
|
@@ -41,11 +38,48 @@ npx niceeval view --run report-data --out site
|
|
|
41
38
|
不传 `--report` 时,发布出来的首页是默认报告。想让首页换成自己的报告,把 [`defineReport` 报告文件](/zh/how-to/custom-reports)传给 `--report` 就行——attempt 证据页(transcript、trace、代码视图)仍在同一个站里,报告里的每个数字点进去就是对应证据,和本地 `view --report` 看到的一模一样:
|
|
42
39
|
|
|
43
40
|
```bash
|
|
44
|
-
npx niceeval view --
|
|
41
|
+
npx niceeval view --results report-data --report reports/exam.tsx --out site
|
|
45
42
|
```
|
|
46
43
|
|
|
47
44
|
报告文件和 `report-data/` 一样提交在仓库里,改完版面 push,线上就跟着更新。用下面的 `vercel.json` / workflow 时,把构建命令换成这一行即可,其余配置不用动。
|
|
48
45
|
|
|
46
|
+
## 接站点分析与第三方脚本
|
|
47
|
+
|
|
48
|
+
发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { ExperimentComparison, defineReport } from "niceeval/report";
|
|
52
|
+
|
|
53
|
+
export default defineReport({
|
|
54
|
+
title: "Memory Evals",
|
|
55
|
+
head: [
|
|
56
|
+
{ tag: "script", attrs: { async: true, src: "https://www.googletagmanager.com/gtag/js?id=G-XXXX" } },
|
|
57
|
+
{
|
|
58
|
+
tag: "script",
|
|
59
|
+
children: `
|
|
60
|
+
window.dataLayer = window.dataLayer || [];
|
|
61
|
+
function gtag(){dataLayer.push(arguments);}
|
|
62
|
+
gtag('js', new Date());
|
|
63
|
+
gtag('config', 'G-XXXX');
|
|
64
|
+
`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
content: <ExperimentComparison />,
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
规则只有几条:
|
|
72
|
+
|
|
73
|
+
- `tag` 支持 `meta`、`link`、`script`、`style` 四种,按声明顺序渲染进每一页的 `<head>`。SEO meta、favicon、字体、JSON-LD 都走这里。
|
|
74
|
+
- `attrs` 的值写 `true` 输出裸属性(`async`、`defer`),写字符串输出 `key="value"`。靠 `data-*` 属性配置的第三方脚本(评论、埋点)直接把属性抄进来。
|
|
75
|
+
- `src` / `href` 写 `https://` 外链时原样保留;写 `./favicon.svg` 这类相对路径时,文件会自动复制进导出站的 `assets/` 并改好引用。
|
|
76
|
+
- 脚本会原样发布并在读者浏览器里执行,别在里面嵌密钥。
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// 靠 data-* 配置的埋点脚本:属性照抄
|
|
80
|
+
{ tag: "script", attrs: { async: true, src: "https://tracker.example/t.js", "data-project": "memory-evals" } }
|
|
81
|
+
```
|
|
82
|
+
|
|
49
83
|
## 接托管平台
|
|
50
84
|
|
|
51
85
|
**Vercel**:仓库根放一个 `vercel.json`,导入项目后 push 即部署。
|
|
@@ -53,7 +87,7 @@ npx niceeval view --run report-data --report reports/exam.tsx --out site
|
|
|
53
87
|
```json
|
|
54
88
|
{
|
|
55
89
|
"installCommand": "pnpm install --frozen-lockfile",
|
|
56
|
-
"buildCommand": "npx niceeval view --
|
|
90
|
+
"buildCommand": "npx niceeval view --results report-data --out site",
|
|
57
91
|
"outputDirectory": "site"
|
|
58
92
|
}
|
|
59
93
|
```
|
|
@@ -83,7 +117,7 @@ jobs:
|
|
|
83
117
|
node-version: 22
|
|
84
118
|
cache: pnpm
|
|
85
119
|
- run: pnpm install --frozen-lockfile
|
|
86
|
-
- run: npx niceeval view --
|
|
120
|
+
- run: npx niceeval view --results report-data --out site
|
|
87
121
|
- uses: actions/upload-pages-artifact@v3
|
|
88
122
|
with:
|
|
89
123
|
path: site
|
|
@@ -102,4 +136,4 @@ jobs:
|
|
|
102
136
|
|
|
103
137
|
## 发布的是选中的证据
|
|
104
138
|
|
|
105
|
-
整站导出会带上发布结果根里选中的 transcript、源码快照和 trace。新写入的超大 events / trace 字符串可能带结构化截断标记。`copySnapshots`
|
|
139
|
+
整站导出会带上发布结果根里选中的 transcript、源码快照和 trace。新写入的超大 events / trace 字符串可能带结构化截断标记。`copySnapshots` 只做选择和整文件大小预检,不改写任何内容。发布的站点谁都能翻到 prompt 和工具输出,发布到公网前确认结果内容适合公开——NiceEval 在记录时就不把环境变量值和命令输出写进结果文件,transcript 里是 Agent 自己的输入输出和你的 eval 任务本身。
|
|
@@ -66,7 +66,7 @@ Sandbox 创建、setup 或 teardown 错误不依赖 trace。`result.json` 保存
|
|
|
66
66
|
|
|
67
67
|
同一个实验多次跑会留下多份结果,不带 `@<locator>` 的默认视图只回答一个问题——「现在整体怎样」:每个 experiment × eval 取时间上最新的那份判定,同一个 experiment 跨多次运行拼出来。按前缀只重跑了部分 eval 时,其余 eval 的判定从更早的运行补齐,报告永远是全局最新,不会因为一次局部重跑变残缺。合成是有标注的:每份判定都带上它产生的时间,能看出报告是从哪几次运行拼出来的。合成结果可能混着不同版本的被测代码——所以收工判定以 `--force` 全量重跑为准,迭代途中的 `show` 负责快、收尾的全量跑负责真。
|
|
68
68
|
|
|
69
|
-
单个 eval 视图里,多 experiment、多 Attempt 时的断言明细块默认挑最新一次失败的 Attempt 展开;没有失败就挑最新一次。这只是一个默认展开的启发式,不是精确选择——需要精确看某一次 Attempt,复制那一行的 `@<locator>` 直接 `show` 它即可。`--experiment compare` 按路径段前缀把 Selection 收窄到整个 `compare` 组,`--experiment compare/bub` 只留一个 experiment;`--
|
|
69
|
+
单个 eval 视图里,多 experiment、多 Attempt 时的断言明细块默认挑最新一次失败的 Attempt 展开;没有失败就挑最新一次。这只是一个默认展开的启发式,不是精确选择——需要精确看某一次 Attempt,复制那一行的 `@<locator>` 直接 `show` 它即可。`--experiment compare` 按路径段前缀把 Selection 收窄到整个 `compare` 组,`--experiment compare/bub` 只留一个 experiment;`--results <目录>` 换结果根,`--snapshot <snapshot.json>` 只看某一次落盘的快照,`--history` 看跨 run 趋势。`--report <文件>` 同时替换 `show` / `view` 的默认报告。位置前缀、`--results`、`--experiment` 对自定义报告同样生效;`--history` 与 `--report` 互斥。
|
|
70
70
|
|
|
71
71
|
`niceeval view` 的每个视图都有 CLI 对应物:
|
|
72
72
|
|
|
@@ -79,7 +79,7 @@ Sandbox 创建、setup 或 teardown 错误不依赖 trace。`result.json` 保存
|
|
|
79
79
|
| AI 对话、thinking、Skill 加载与工具调用(有 OTel 时补时间) | `niceeval show @<locator> --execution` |
|
|
80
80
|
| 单次 Attempt 的阶段耗时分解 | `niceeval show @<locator> --timing`;逐节点审计用 `--timing=full` |
|
|
81
81
|
| 文件改动 | `niceeval show @<locator> --diff` |
|
|
82
|
-
| 历史 run 列表 | `niceeval show <eval id> --history`;钉死某一次用
|
|
82
|
+
| 历史 run 列表 | `niceeval show <eval id> --history`;钉死某一次用 `view --snapshot <snapshot.json>` 或复制该行 `@<locator>` 直接 `show` |
|
|
83
83
|
|
|
84
84
|
### 默认分组比较报告
|
|
85
85
|
|
|
@@ -319,7 +319,7 @@ compare/codex-gpt-5.4 · 5 runs · passed 2/5
|
|
|
319
319
|
2026-07-07T16-40 ✗ failed 3 attempts $0.05 gate calledTool("get_weather")
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
✓✗ 交替说明这个 eval 在抖,该修的是稳定性(被测程序或断言),反复重跑碰运气只会烧钱;连续绿转红的拐点就是回归引入的位置,用
|
|
322
|
+
✓✗ 交替说明这个 eval 在抖,该修的是稳定性(被测程序或断言),反复重跑碰运气只会烧钱;连续绿转红的拐点就是回归引入的位置,用 `view --snapshot <snapshot.json>` 钉住拐点前后两次细看。时间轴只列真实执行——缓存携带的旧结果是判定的复印件,不占行,否则趋势会被复印件灌满假数据。不带 eval id 的 `niceeval show --history` 给每个 experiment 的 per-run 通过率序列,同一份趋势的榜单视角。
|
|
323
323
|
|
|
324
324
|
两次 run 的精确对比(这次修复具体翻转了哪些 eval)不做成 flag:用 `DeltaTable` 积木写一份报告递给 `--report`,几行就是一份自定义对比报告,终端和网页两扇门都认——内置命令只管固定摆法,自定义口径见[自定义报告](/zh/how-to/custom-reports)。
|
|
325
325
|
|
|
@@ -360,7 +360,7 @@ NiceEval 写入 `<dir>/index.html`,并把查看器要读取的 artifact(`sou
|
|
|
360
360
|
- `diff.json` 和 `o11y.json` 不会被复制。查看器不读取它们,且 diff 可能达到上百 MB。
|
|
361
361
|
- 用 `file://` 直接打开 `index.html` 时浏览器不允许 fetch artifact,代码视图会提示源码不可用。本地预览用 http 服务打开。
|
|
362
362
|
|
|
363
|
-
最简单的流程是在跑过 eval 的机器上导出,把产物目录直接部署,不需要额外步骤。要让站点随 push 自动更新,先用 `copySnapshots` 生成经过单文件预算检查的发布结果根,把它提交进仓库,再让 CI 对这个目录运行 `view --
|
|
363
|
+
最简单的流程是在跑过 eval 的机器上导出,把产物目录直接部署,不需要额外步骤。要让站点随 push 自动更新,先用 `copySnapshots` 生成经过单文件预算检查的发布结果根,把它提交进仓库,再让 CI 对这个目录运行 `view --results <目录> --out`——workflow 与平台接线见[通过 CI 发布报告](/zh/how-to/publish-report)。
|
|
364
364
|
|
|
365
365
|
## Artifact 说明
|
|
366
366
|
|
|
@@ -88,7 +88,6 @@ npx niceeval exp compare-models weather
|
|
|
88
88
|
| `--json` | string | 额外写一份 JSON 结果(`RunSummary` 原样序列化)到指定路径,供 CI 或下游脚本消费。 |
|
|
89
89
|
| `--out` | string | `view` 命令专用:把结果查看器静态导出到指定目录。 |
|
|
90
90
|
| `--port` | number | `view` 命令专用:指定本地服务器监听端口。 |
|
|
91
|
-
| `--allow-sensitive-artifacts` | boolean | `view --out` 专用:对非发布根(快照没有 publish:{redaction:"applied"} 标记)导出时的显式确认——静态站会原样携带未消毒的证据文件。 |
|
|
92
91
|
| `--source` | boolean | `show` 命令专用:该 attempt 运行时保存的 Eval 源码,gate/soft 断言标回源码行(证据切面)。 |
|
|
93
92
|
| `--execution` | boolean | `show` 命令专用:该 attempt 的标准执行事件流(消息、thinking、Skill load、工具调用/结果);有 OTel 时同一节点补时间(证据切面)。 |
|
|
94
93
|
| `--timing` | boolean | `show` 命令专用:整个 Attempt 的统一时间树;裸 `--timing` 给有界诊断投影,`--timing=full` 逐节点展开全部 runner/已关联 OTel 节点。 |
|
|
@@ -152,7 +151,7 @@ npx niceeval exp compare --output ci --strict --json .niceeval/ci-summary.json -
|
|
|
152
151
|
npx niceeval view
|
|
153
152
|
```
|
|
154
153
|
|
|
155
|
-
打开本地结果查看器。它和 `show` 共用同一份默认报告和同一套默认选择——对每个 experiment、每个 eval,取历次运行里最新的那份判定;只补跑部分 eval 时,其余 eval 从更早的运行补齐。默认报告按 experiment id 的父目录分组,只在同组内比较。`show` 输出终端文本,`view` 输出网页并提供可交互的证据浏览;eval ID 前缀、`--experiment`、`--
|
|
154
|
+
打开本地结果查看器。它和 `show` 共用同一份默认报告和同一套默认选择——对每个 experiment、每个 eval,取历次运行里最新的那份判定;只补跑部分 eval 时,其余 eval 从更早的运行补齐。默认报告按 experiment id 的父目录分组,只在同组内比较。`show` 输出终端文本,`view` 输出网页并提供可交互的证据浏览;eval ID 前缀、`--experiment`、`--results` 对两者的收窄一致。完整说明见[查看结果](/zh/how-to/viewing-results)。
|
|
156
155
|
|
|
157
156
|
## `show [id-prefix...]`
|
|
158
157
|
|
|
@@ -168,7 +167,7 @@ npx niceeval show weather/brooklyn --history
|
|
|
168
167
|
|
|
169
168
|
`show` 是终端结果入口,适合人直接阅读,也适合 coding agent 在上下文窗口里逐级下钻。位置参数选「看哪些 eval」(ID 前缀)或直接用 `@<locator>` 精确选一个 attempt;不带位置参数时显示按实验组分区的默认比较报告,指定 eval ID 前缀只收窄报告覆盖的 Eval,不改变组边界。
|
|
170
169
|
|
|
171
|
-
`@<locator>` 不带证据 flag 时给出该 attempt 的紧凑全景(断言摘要、执行摘要、可选 OTel 时间、diff 摘要);`--eval`、`--execution`、`--diff` 是同一 attempt 的证据切面,分别展开运行时保存的 Eval 源码、标准执行事件流、工作区文件改动,因此必须搭配 `@<locator>` 精确指名一个 attempt。`--
|
|
170
|
+
`@<locator>` 不带证据 flag 时给出该 attempt 的紧凑全景(断言摘要、执行摘要、可选 OTel 时间、diff 摘要);`--eval`、`--execution`、`--diff` 是同一 attempt 的证据切面,分别展开运行时保存的 Eval 源码、标准执行事件流、工作区文件改动,因此必须搭配 `@<locator>` 精确指名一个 attempt。`--results <目录>` 指定结果根,`--history` 查看跨 run 趋势。完整的阅读顺序、输出示例和 artifact 说明见[查看结果](/zh/how-to/viewing-results)。
|
|
172
171
|
|
|
173
172
|
## `--early-exit` 与 `--strict`
|
|
174
173
|
|