niceeval 0.9.0 → 0.9.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/i18n/zh-CN.d.ts +1 -1
- package/dist/report/report.js +2 -2
- package/dist/report/tree.d.ts +1 -1
- package/dist/report/tree.js +1 -1
- package/dist/results/select.d.ts +2 -2
- package/dist/results/select.js +1 -1
- package/dist/shared/aggregate.d.ts +1 -1
- package/dist/shared/aggregate.js +1 -1
- package/docs-site/zh/how-to/custom-reports.mdx +1 -1
- package/docs-site/zh/how-to/experiments.mdx +2 -2
- package/docs-site/zh/how-to/publish-report.mdx +6 -0
- package/docs-site/zh/how-to/viewing-results.mdx +14 -5
- package/docs-site/zh/reference/cli.mdx +2 -2
- package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
- package/package.json +1 -1
- package/src/cli.ts +7 -8
- package/src/i18n/en.ts +3 -3
- package/src/i18n/zh-CN.ts +3 -3
- package/src/report/dual-render.test.tsx +2 -2
- package/src/report/react/styles.css +12 -6
- package/src/report/report.ts +2 -2
- package/src/report/tree.ts +2 -2
- package/src/results/host-equivalence.test.ts +1 -1
- package/src/results/select.ts +2 -2
- package/src/shared/aggregate.ts +1 -1
- package/src/show/index.ts +2 -2
- package/src/show/render.ts +1 -1
- package/src/show/report-host.test.ts +2 -2
- package/src/show/report-host.ts +1 -1
- package/src/show/show.test.ts +1 -1
- package/src/view/app/App.test.tsx +7 -6
- package/src/view/app/App.tsx +18 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +1 -1
- package/src/view/data.ts +20 -9
- package/src/view/index.ts +2 -12
- package/src/view/server.ts +4 -4
- package/src/view/styles.css +18 -1
- package/src/view/view-report.test.ts +44 -15
package/src/view/data.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type HostReportAsset,
|
|
21
21
|
} from "../show/report-host.ts";
|
|
22
22
|
import { selectCurrentResults, filterExperiments } from "../results/select.ts";
|
|
23
|
+
import { evalPrefixPredicate } from "../shared/aggregate.ts";
|
|
23
24
|
import type { EvalResult } from "../types.ts";
|
|
24
25
|
import type {
|
|
25
26
|
SkippedRunNotice,
|
|
@@ -61,9 +62,9 @@ export interface ViewScan {
|
|
|
61
62
|
|
|
62
63
|
/** view 宿主输入的组合语义(与 show 对齐,docs/feature/reports/architecture.md「Scope 是计算入口」)。 */
|
|
63
64
|
export interface ViewScanOptions {
|
|
64
|
-
/** eval id 前缀(位置参数)
|
|
65
|
+
/** eval id 前缀(位置参数):把根滤成有效根,页面 Scope 与证据(快照明细、artifact 清单)一致收窄。 */
|
|
65
66
|
patterns?: string[];
|
|
66
|
-
/** experiment id 前缀(--
|
|
67
|
+
/** experiment id 前缀(--exp):有效根只留匹配实验。 */
|
|
67
68
|
experiment?: string;
|
|
68
69
|
/** --report 报告文件:相对 cwd 的路径。装载失败抛 ReportLoadError(CLI 打印后退出)。 */
|
|
69
70
|
report?: { path: string; cwd: string };
|
|
@@ -175,10 +176,12 @@ export async function loadLatestResultsPerEval(root = ".niceeval"): Promise<Eval
|
|
|
175
176
|
/**
|
|
176
177
|
* `niceeval view` 的数据装载入口:server 每次请求现读现算,`--out` 导出用同一份。
|
|
177
178
|
* 报告槽 Selection 恒经 selectCurrentResults 合成(现刻水位;与 `niceeval show` 调同一个
|
|
178
|
-
* 函数,裸跑与局部收窄不分叉),位置前缀 / --
|
|
179
|
+
* 函数,裸跑与局部收窄不分叉),位置前缀 / --exp 只作为 scope 传入,不切换选择口径。
|
|
179
180
|
* --report 本身不改挑选——它只换报告槽的填充,注入的 Selection 与裸跑同一份,
|
|
180
181
|
* 「裸跑 ≡ --report <ExperimentComparison>」靠这条成立(docs/feature/reports/architecture.md「Selection 是计算入口」)。
|
|
181
|
-
*
|
|
182
|
+
* 命令行收窄作用在有效根上(docs/feature/reports/view.md 开篇):证据室数据与 artifact 清单
|
|
183
|
+
* 与页面一致地只含收窄后的范围,本地与导出无分叉——收窄导出的站点(烘进 HTML 的数据、
|
|
184
|
+
* 证据文件)只含收窄到的内容。收窄之内、不在现刻水位里的历史 attempt 仍在有效根里,深链可达。
|
|
182
185
|
* 零可读结果一律抛 ViewInputError,不渲染/导出空页面(server 起不来,--out 非零退出)。
|
|
183
186
|
*/
|
|
184
187
|
export async function loadViewScan(input?: string, opts: ViewScanOptions = {}): Promise<ViewScan> {
|
|
@@ -228,6 +231,12 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
|
|
|
228
231
|
// 静态渲染成 HTML(en / zh-CN 各一遍,切界面语言不重算数据)。
|
|
229
232
|
const slot = await renderReportSlot(opts.report, opts.page, results, selection, opts.pageFailure ?? "throw");
|
|
230
233
|
|
|
234
|
+
// 有效根:命令行收窄把根滤成只含匹配实验与 attempt(docs/feature/reports/view.md 开篇)。
|
|
235
|
+
// 证据室数据与 artifact 清单从这里取数,与页面 Scope 一致收窄——本地与导出无分叉,
|
|
236
|
+
// 收窄导出的站点(烘进 HTML 的数据、证据文件)只含收窄后的范围。
|
|
237
|
+
const scopedExperiments = filterExperiments(results.experiments, opts.experiment);
|
|
238
|
+
const matchEval = patterns.length > 0 ? evalPrefixPredicate(patterns) : () => true;
|
|
239
|
+
|
|
231
240
|
// 跨快照按身份键去重:--resume 携带的条目在多份落盘里重复,只保留最新快照里的那份
|
|
232
241
|
// (与官方计算函数的聚合口径一致,Runs / Traces 的计数因此不被复印件灌票)。
|
|
233
242
|
const artifactDirs = new Map<string, string>();
|
|
@@ -236,13 +245,15 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
|
|
|
236
245
|
// 与报告槽 Selection(现刻水位,可能合成自更早快照)是两个独立概念,不混用。
|
|
237
246
|
const latestSet = new Set(latestPerExperiment.snapshots);
|
|
238
247
|
const allAttempts: AttemptHandle[] = [];
|
|
239
|
-
for (const exp of
|
|
240
|
-
for (const snap of exp.snapshots) allAttempts.push(...snap.attempts);
|
|
248
|
+
for (const exp of scopedExperiments) {
|
|
249
|
+
for (const snap of exp.snapshots) allAttempts.push(...snap.attempts.filter((a) => matchEval(a.evalId)));
|
|
241
250
|
}
|
|
242
251
|
const survivors = new Set(dedupeAttempts(allAttempts).attempts);
|
|
243
252
|
|
|
244
253
|
const snapshots: ViewSnapshot[] = [];
|
|
245
|
-
for (const exp of
|
|
254
|
+
for (const exp of scopedExperiments) {
|
|
255
|
+
// 整个实验没有匹配 eval 时不携带:有效根里没有它,连快照元数据也不烘进页面。
|
|
256
|
+
if (patterns.length > 0 && !exp.evalIds.some((id) => matchEval(id))) continue;
|
|
246
257
|
for (const snap of exp.snapshots) {
|
|
247
258
|
const kept = snap.attempts.filter((a) => survivors.has(a));
|
|
248
259
|
const latest = latestSet.has(snap);
|
|
@@ -267,10 +278,10 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
|
|
|
267
278
|
}
|
|
268
279
|
}
|
|
269
280
|
|
|
270
|
-
// 全局最新快照(
|
|
281
|
+
// 全局最新快照(跨有效根内全部实验):viewData.lastRunAt 从这里取。页内 hero 的「最后运行」
|
|
271
282
|
// 显示由 Hero 组件按 heroData(scope) 自己算,不吃这份字段。
|
|
272
283
|
let latestSnapshot: Snapshot | undefined;
|
|
273
|
-
for (const exp of
|
|
284
|
+
for (const exp of scopedExperiments) {
|
|
274
285
|
const candidate = exp.snapshots[0];
|
|
275
286
|
if (!candidate) continue;
|
|
276
287
|
if (!latestSnapshot || candidate.startedAt > latestSnapshot.startedAt) latestSnapshot = candidate;
|
package/src/view/index.ts
CHANGED
|
@@ -80,18 +80,8 @@ export async function buildView(opts: ViewOptions = {}): Promise<string> {
|
|
|
80
80
|
`--out expects a directory, got "${opts.out}". Single-file HTML export was removed: code, transcript and trace views need artifact files next to the page. Export a directory instead (e.g. --out site) and serve it with any static host.`,
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// attempt 的证据都已出站。按实验收窄发布 = 用 copySnapshots 构建只含它的发布根,再对新根导出。
|
|
86
|
-
if ((opts.scan?.patterns?.length ?? 0) > 0 || opts.scan?.experiment !== undefined) {
|
|
87
|
-
throw new ViewInputError(
|
|
88
|
-
"--out exports the whole results root and cannot be combined with eval prefixes or --experiment.\n" +
|
|
89
|
-
"To publish a site for one experiment, build a narrower results root and export that:\n" +
|
|
90
|
-
' const results = await openResults(".niceeval");\n' +
|
|
91
|
-
' await copySnapshots(results.latest().filter((s) => s.experimentId.startsWith("<prefix>/")), "<publish-root>");\n' +
|
|
92
|
-
"Then: niceeval view --results <publish-root> --out <site>",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
83
|
+
// 位置参数 / --exp 对导出同义于本地:收窄作用在有效根上,出站的页面数据与证据文件
|
|
84
|
+
// 只含收窄后的范围(docs/feature/reports/view.md「静态导出」:出站的就是收窄到的)。
|
|
95
85
|
// 静态导出保持「任一页失败整体失败」(pageFailure 缺省 "throw"),不产出半套站点。
|
|
96
86
|
const plan = await planSite(opts.input, opts.scan);
|
|
97
87
|
await writeSite(plan, out);
|
package/src/view/server.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// HTTP server:把站点管线(site.ts 的 planSite)产出的同一份产物挂在 127.0.0.1 上按路径服务。
|
|
2
2
|
// 这里不携带任何取数或布局知识——查不到清单条目就是 404,与 `--out` 写盘的文件逐字节一致
|
|
3
|
-
// (docs/feature/reports/view.md 开篇;奇偶由 site-parity 测试守护)
|
|
4
|
-
//
|
|
5
|
-
// (pageFailure: "embed")
|
|
3
|
+
// (docs/feature/reports/view.md 开篇;奇偶由 site-parity 测试守护)。宿主语义只有两条,全部
|
|
4
|
+
// 作用在管线之外:打开首页整份重建(数据永远是盘上最新)、单页渲染失败折成页内错误块
|
|
5
|
+
// (pageFailure: "embed")。位置参数 / --exp 收窄是管线输入,不是宿主语义——两宿主同义。
|
|
6
6
|
|
|
7
7
|
import { createServer, type Server } from "node:http";
|
|
8
8
|
import { type ViewScanOptions } from "./data.ts";
|
|
@@ -13,7 +13,7 @@ export interface ViewOptions {
|
|
|
13
13
|
input?: string;
|
|
14
14
|
out?: string;
|
|
15
15
|
port?: number;
|
|
16
|
-
/**
|
|
16
|
+
/** 站点管线的组合语义(位置前缀 / --exp 收窄有效根,--report 换报告槽),透传给管线。 */
|
|
17
17
|
scan?: ViewScanOptions;
|
|
18
18
|
}
|
|
19
19
|
|
package/src/view/styles.css
CHANGED
|
@@ -73,7 +73,24 @@ a { color: inherit; }
|
|
|
73
73
|
z-index: 10;
|
|
74
74
|
backdrop-filter: blur(10px);
|
|
75
75
|
}
|
|
76
|
-
/*
|
|
76
|
+
/* 页头左端的恒定品牌位:NiceEval 字标 + 45° 方块 mark,外链官网(App.tsx 的 .brand)。
|
|
77
|
+
与 hero 下 nre-powered-by 品牌行同族,由宿主渲染、报告定义不能覆盖或移除。 */
|
|
78
|
+
.brand {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: baseline;
|
|
81
|
+
gap: 12px;
|
|
82
|
+
font-weight: 690;
|
|
83
|
+
font-size: 20px;
|
|
84
|
+
flex: 0 0 auto;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
}
|
|
87
|
+
.mark {
|
|
88
|
+
width: 18px;
|
|
89
|
+
height: 18px;
|
|
90
|
+
border: 1.25px solid var(--text);
|
|
91
|
+
transform: rotate(45deg);
|
|
92
|
+
display: inline-block;
|
|
93
|
+
}
|
|
77
94
|
/* 顶部导航 .nav / .nav-tab → components/ui/tabs.tsx(Radix Tabs + Tailwind utilities)。 */
|
|
78
95
|
.lang-switch {
|
|
79
96
|
display: inline-flex;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// niceeval view 的报告槽与宿主组合语义(docs/feature/reports/architecture.md「Selection 是计算入口」
|
|
3
3
|
// 与裁决记录 6;公开行为准绳 docs-site/zh/how-to/viewing-results.mdx / custom-reports.mdx)。
|
|
4
4
|
// 覆盖:
|
|
5
|
-
// - 组合语义与 show 对齐:位置前缀收窄报告槽 Scope、--
|
|
5
|
+
// - 组合语义与 show 对齐:位置前缀收窄报告槽 Scope、--exp 过滤、匹配不到直说;
|
|
6
6
|
// - 输入语义:位置参数只表示 eval id 前缀(不随文件系统状态改变),结果根走 --results,
|
|
7
7
|
// 单开一份快照走 --snapshot(文件不可读时失败);
|
|
8
8
|
// - 报告槽恒在:裸跑填充内建报告,--report 整槽替换;en / zh-CN 双语各渲染一遍;
|
|
@@ -162,7 +162,7 @@ function bareReportHtml(scan: ViewScan): { en: string; "zh-CN": string } {
|
|
|
162
162
|
// ───────────────────────── 组合语义(与 show 对齐) ─────────────────────────
|
|
163
163
|
|
|
164
164
|
describe("loadViewScan · 组合语义", () => {
|
|
165
|
-
it("
|
|
165
|
+
it("位置前缀收窄作用在有效根上:全部页与证据室一致缩小,被滤掉的 attempt 不烘进页面数据", async () => {
|
|
166
166
|
const root = await seedRoot();
|
|
167
167
|
const scan = await loadViewScan(root, { patterns: ["weather"] });
|
|
168
168
|
const { viewData } = scan;
|
|
@@ -176,16 +176,22 @@ describe("loadViewScan · 组合语义", () => {
|
|
|
176
176
|
expect(pageHtml(scan, "traces").en).not.toContain("fixtures/button");
|
|
177
177
|
const bare = await loadViewScan(root);
|
|
178
178
|
expect(pageHtml(bare, "attempts").en).toContain("fixtures/button"); // 不收窄时在场,对照
|
|
179
|
-
// attempt
|
|
179
|
+
// 有效根即证据室:被滤掉的 attempt 不在 viewData.snapshots 里,深链两宿主一致不可达。
|
|
180
180
|
const filteredOut = viewData.snapshots
|
|
181
181
|
.flatMap((s) => s.results)
|
|
182
182
|
.find((r) => r.id === "fixtures/button");
|
|
183
|
-
expect(filteredOut
|
|
183
|
+
expect(filteredOut).toBeUndefined();
|
|
184
|
+
expect(scan.artifactDirs.size).toBe(
|
|
185
|
+
[...scan.artifactDirs.keys()].filter((base) => base.includes("weather/brooklyn")).length,
|
|
186
|
+
);
|
|
187
|
+
// 收窄之内、不在现刻水位的口径差异由 --exp 深链测试覆盖;不收窄时深链照常可达,对照:
|
|
188
|
+
const inBare = bare.viewData.snapshots.flatMap((s) => s.results).find((r) => r.id === "fixtures/button");
|
|
189
|
+
expect(inBare?.locator).toBeTruthy();
|
|
184
190
|
const { resolveAttemptLocator } = await import("./app/lib/attempt-route.ts");
|
|
185
|
-
expect(resolveAttemptLocator(viewData.snapshots,
|
|
191
|
+
expect(resolveAttemptLocator(bare.viewData.snapshots, inBare!.locator!)).toBe(inBare);
|
|
186
192
|
});
|
|
187
193
|
|
|
188
|
-
it("--
|
|
194
|
+
it("--exp 过滤:报告槽 Selection 只留该实验", async () => {
|
|
189
195
|
const root = await seedRoot();
|
|
190
196
|
const reportHtml = bareReportHtml(await loadViewScan(root, { experiment: "compare/codex" }));
|
|
191
197
|
expect(reportHtml.en).toContain("compare/codex");
|
|
@@ -468,18 +474,41 @@ describe("buildView · --out 与 --report", () => {
|
|
|
468
474
|
expect(existsSync(join(exported, "o11y.json"))).toBe(false);
|
|
469
475
|
});
|
|
470
476
|
|
|
471
|
-
it("--out
|
|
477
|
+
it("--out 接受收窄,出站的就是收窄到的:被滤掉实验的证据文件与页面数据都不出站", async () => {
|
|
472
478
|
const root = await seedRoot();
|
|
479
|
+
// 两个实验各写一份 events.json:导出后只有收窄内的那份在站里。
|
|
480
|
+
const bubDir = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "weather", "brooklyn", "a0");
|
|
481
|
+
const codexDir = join(root, "compare_codex", "2026-07-09T10-00-00-000Z", "weather", "brooklyn", "a0");
|
|
482
|
+
for (const dir of [bubDir, codexDir]) {
|
|
483
|
+
await mkdir(dir, { recursive: true });
|
|
484
|
+
await writeFile(join(dir, "events.json"), "[]", "utf-8");
|
|
485
|
+
}
|
|
486
|
+
|
|
473
487
|
const out = join(root, "site");
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
488
|
+
await buildView({ input: root, out, scan: { patterns: [], experiment: "compare/codex" } });
|
|
489
|
+
expect(existsSync(join(out, "artifact", "compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a0", "events.json"))).toBe(true);
|
|
490
|
+
// 被滤掉的实验:证据树整目录不出站,页面数据(烘进 HTML 的 viewData 与报告块)也不含它。
|
|
491
|
+
expect(existsSync(join(out, "artifact", "compare_bub"))).toBe(false);
|
|
492
|
+
const html = await readFile(join(out, "index.html"), "utf-8");
|
|
493
|
+
expect(html).not.toContain("compare/bub");
|
|
494
|
+
expect(html).toContain("compare/codex");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("eval 前缀收窄导出:不匹配 attempt 的证据不出站", async () => {
|
|
498
|
+
const root = await seedRoot();
|
|
499
|
+
const weatherDir = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "weather", "brooklyn", "a0");
|
|
500
|
+
const buttonDir = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "fixtures", "button", "a0");
|
|
501
|
+
for (const dir of [weatherDir, buttonDir]) {
|
|
502
|
+
await mkdir(dir, { recursive: true });
|
|
503
|
+
await writeFile(join(dir, "events.json"), "[]", "utf-8");
|
|
480
504
|
}
|
|
481
|
-
|
|
482
|
-
|
|
505
|
+
|
|
506
|
+
const out = join(root, "site");
|
|
507
|
+
await buildView({ input: root, out, scan: { patterns: ["weather"] } });
|
|
508
|
+
expect(existsSync(join(out, "artifact", "compare_bub/2026-07-08T10-00-00-000Z/weather/brooklyn/a0", "events.json"))).toBe(true);
|
|
509
|
+
expect(existsSync(join(out, "artifact", "compare_bub/2026-07-08T10-00-00-000Z/fixtures"))).toBe(false);
|
|
510
|
+
const html = await readFile(join(out, "index.html"), "utf-8");
|
|
511
|
+
expect(html).not.toContain("fixtures/button");
|
|
483
512
|
});
|
|
484
513
|
|
|
485
514
|
it("默认导出(无 --report):报告槽填充 ExperimentComparison,双语块与增强 runtime 恒内联", async () => {
|