niceeval 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +2 -2
- package/dist/i18n/zh-CN.d.ts +3 -3
- package/dist/report/aggregate.d.ts +32 -26
- package/dist/report/aggregate.js +157 -76
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +91 -164
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +86 -73
- package/dist/report/compute.js +592 -432
- package/dist/report/flag.d.ts +28 -17
- package/dist/report/flag.js +86 -16
- package/dist/report/format.d.ts +11 -11
- package/dist/report/format.js +17 -15
- package/dist/report/index.d.ts +16 -17
- package/dist/report/index.js +20 -22
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +49 -34
- package/dist/report/locale.js +106 -58
- package/dist/report/metrics.d.ts +10 -3
- package/dist/report/metrics.js +46 -12
- package/dist/report/primitives.d.ts +42 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +10 -8
- package/dist/report/react/AttemptList.js +18 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +3 -3
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +4 -2
- package/dist/report/react/ExperimentComparison.js +5 -4
- package/dist/report/react/ExperimentList.d.ts +3 -3
- package/dist/report/react/ExperimentList.js +16 -15
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +54 -17
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +105 -149
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -20
- package/dist/report/report.js +261 -34
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +225 -157
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +2 -2
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +245 -300
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +38 -14
- package/dist/shared/aggregate.d.ts +3 -2
- package/dist/shared/aggregate.js +5 -4
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
- package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
- package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
- package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
- package/docs-site/zh/index.mdx +23 -25
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +5 -5
- package/docs-site/zh/reference/capabilities.mdx +6 -6
- package/docs-site/zh/reference/cli.mdx +9 -7
- package/docs-site/zh/reference/define-agent.mdx +1 -1
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
- package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
- package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
- package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
- package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +6 -2
- package/src/agents/index.ts +2 -2
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +3 -3
- package/src/cli.ts +42 -23
- package/src/context/context.ts +1 -1
- package/src/context/session.test.ts +1 -1
- package/src/context/session.ts +1 -1
- package/src/i18n/en.ts +18 -16
- package/src/i18n/zh-CN.ts +16 -15
- package/src/report/aggregate.ts +175 -87
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -285
- package/src/report/compute.ts +717 -515
- package/src/report/dual-render.test.tsx +738 -1148
- package/src/report/flag.ts +97 -33
- package/src/report/format.ts +18 -22
- package/src/report/index.ts +113 -58
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +120 -69
- package/src/report/metrics.ts +42 -12
- package/src/report/primitives.tsx +190 -45
- package/src/report/react/AttemptList.tsx +32 -20
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +12 -7
- package/src/report/react/ExperimentList.tsx +38 -26
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +74 -20
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/enhance.js +57 -5
- package/src/report/react/fixtures.ts +109 -156
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +139 -104
- package/src/report/react/styles.css +181 -91
- package/src/report/report.test.ts +761 -1031
- package/src/report/report.ts +425 -47
- package/src/report/text/faces.ts +257 -164
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +2 -2
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +257 -287
- package/src/report/web.ts +63 -20
- package/src/results/attempt-evidence.test.ts +4 -4
- package/src/results/attempt-evidence.ts +5 -5
- package/src/results/copy.ts +6 -6
- package/src/results/host-equivalence.test.ts +26 -14
- package/src/results/index.ts +10 -4
- package/src/results/open.ts +8 -4
- package/src/results/results.test.ts +4 -3
- package/src/results/select.ts +104 -34
- package/src/results/types.ts +36 -14
- package/src/runner/feedback/human.test.ts +1 -1
- package/src/runner/run.ts +1 -1
- package/src/sandbox/cli-commands.ts +2 -2
- package/src/scoring/judge.test.ts +1 -1
- package/src/shared/aggregate.ts +5 -4
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +107 -56
- package/src/show/render.ts +43 -27
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +86 -36
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +324 -63
- package/src/view/app/components/CodeView.tsx +10 -4
- package/src/view/app/i18n.ts +31 -17
- package/src/view/app/lib/artifact-url.ts +14 -3
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/artifact-serving.test.ts +21 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +6 -6
- package/src/view/data.test.ts +9 -3
- package/src/view/data.ts +145 -49
- package/src/view/index.ts +48 -44
- package/src/view/server.ts +35 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +224 -0
- package/src/view/view-report.test.ts +161 -57
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
- package/dist/report/built-ins/experiment-comparison.js +0 -119
- package/dist/report/built-ins/index.d.ts +0 -2
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/src/report/built-in-user-parity.test.tsx +0 -597
- package/src/report/built-ins/experiment-comparison.tsx +0 -179
- package/src/report/built-ins/index.ts +0 -7
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
- /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
package/src/report/report.ts
CHANGED
|
@@ -1,91 +1,469 @@
|
|
|
1
|
-
// defineReport
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// defineReport:唯一可被宿主装载的产物 —— 一层外壳(标题、外链、页脚、脚本、样式)加
|
|
2
|
+
// 非空页列表;单页与多页不是两种机制,页数只是列表长度(docs/feature/reports/library/shell.md)。
|
|
3
|
+
// 入参有两级缩写,各有精确展开:树入参 ≡ { content: 树 } ≡ pages: [{ id: "report",
|
|
4
|
+
// title: 内置页名, content: 树 }]。`content` 与 `pages` 恰好声明一个,没有隐式默认。
|
|
5
5
|
//
|
|
6
6
|
// renderReportToText 是 text 宿主(show)的装载入口;web 宿主(view)的
|
|
7
|
-
// renderReportToStaticHtml 在 ./web.ts(那一侧才 import react-dom)
|
|
8
|
-
//
|
|
7
|
+
// renderReportToStaticHtml 在 ./web.ts(那一侧才 import react-dom)。管线以页为单位执行:
|
|
8
|
+
// 装载(规范化 + 静态校验)→ resolve → validate → render。
|
|
9
9
|
|
|
10
|
-
import type { Results,
|
|
10
|
+
import type { Results, Scope } from "../results/types.ts";
|
|
11
11
|
import {
|
|
12
12
|
createTextContext,
|
|
13
13
|
renderNodeToText,
|
|
14
14
|
resolveReportTree,
|
|
15
15
|
validateReportTree,
|
|
16
|
+
ResolveMemo,
|
|
16
17
|
type ReportNode,
|
|
17
18
|
type TextRenderOptions,
|
|
18
19
|
} from "./tree.ts";
|
|
19
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
localizedTextEquals,
|
|
22
|
+
resolveLocalizedText,
|
|
23
|
+
type LocalizedText,
|
|
24
|
+
type ReportLocale,
|
|
25
|
+
} from "./locale.ts";
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// ───────────────────────── 公开形状 ─────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface ReportLink {
|
|
30
|
+
label: LocalizedText;
|
|
31
|
+
href: string;
|
|
32
|
+
/**
|
|
33
|
+
* 可选内联 SVG 字标,web 面渲染在 label 前,静态导出原样内联。
|
|
34
|
+
* 不收组件:外壳声明经序列化边界进前端,ReactNode 过不去,可序列化是外壳契约的一部分。
|
|
35
|
+
* 内容是作者义务,宿主不校验——与 scripts 同一约定。
|
|
36
|
+
*/
|
|
37
|
+
icon?: { svg: string };
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
/** src 是相对顶层报告文件的路径;两种形态不可同时出现。 */
|
|
41
|
+
export type ReportAsset = { src: string; inline?: never } | { inline: string; src?: never };
|
|
42
|
+
|
|
43
|
+
export interface ReportShell {
|
|
44
|
+
/** 标题:首页 hero 与浏览器标题。页头左端是恒定的 NiceEval 品牌字标,不由 title 覆盖;回退链 def.title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」。 */
|
|
45
|
+
title?: LocalizedText;
|
|
46
|
+
/** 页头右侧的外部链接,如 GitHub、文档、CI。 */
|
|
47
|
+
links?: ReportLink[];
|
|
48
|
+
/** 每页页脚的一段文字;省略时不渲染页脚(品牌行恒在 hero 下方,不占页脚)。 */
|
|
49
|
+
footer?: LocalizedText;
|
|
50
|
+
/** 注入每个页面的脚本,在官方增强脚本之后、按声明顺序于 </body> 前加载。 */
|
|
51
|
+
scripts?: ReportAsset[];
|
|
52
|
+
/** 注入每个页面的样式表,在官方样式之后按声明顺序加载。 */
|
|
53
|
+
styles?: ReportAsset[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type NonEmptyArray<T> = readonly [T, ...T[]];
|
|
57
|
+
|
|
58
|
+
export interface ReportPage {
|
|
59
|
+
/** 页面身份:`--page <id>` 的取值、web 路由 `#/page/<id>` 与导航锚。小写字母、数字与连字符。 */
|
|
60
|
+
id: string;
|
|
61
|
+
/** 导航中的页名。 */
|
|
62
|
+
title: LocalizedText;
|
|
63
|
+
/** 这一页的报告树;ReportDefinition 不是 ReportNode,页装不进外壳。 */
|
|
64
|
+
content: ReportNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** content / pages 互斥由类型表达,不把非法状态留到运行期。 */
|
|
68
|
+
export type ReportDef = ReportShell &
|
|
69
|
+
(
|
|
70
|
+
| { content: ReportNode; pages?: never }
|
|
71
|
+
| { pages: NonEmptyArray<ReportPage>; content?: never }
|
|
72
|
+
);
|
|
73
|
+
|
|
29
74
|
const REPORT_DEFINITION: unique symbol = Symbol.for("niceeval.report.definition");
|
|
30
75
|
|
|
76
|
+
/**
|
|
77
|
+
* defineReport 的唯一产物:只作 --report 文件的默认导出,交给宿主装载。
|
|
78
|
+
* 它不是 ReportNode——不能放进任何 content 或报告树,外壳因此不可嵌套。
|
|
79
|
+
* 字段是装载规范化后的形态:pages 恒非空,links / scripts / styles 恒为数组。
|
|
80
|
+
*/
|
|
31
81
|
export interface ReportDefinition {
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
readonly kind: "report";
|
|
83
|
+
readonly title?: LocalizedText;
|
|
84
|
+
readonly links: readonly ReportLink[];
|
|
85
|
+
readonly footer?: LocalizedText;
|
|
86
|
+
readonly scripts: readonly ReportAsset[];
|
|
87
|
+
readonly styles: readonly ReportAsset[];
|
|
88
|
+
readonly pages: NonEmptyArray<ReportPage>;
|
|
34
89
|
}
|
|
35
90
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
91
|
+
/** 规范化后的报告声明,经组合组件 ctx.report 只读可见(scripts / styles 是注入资产,不进)。 */
|
|
92
|
+
export interface ReportMeta {
|
|
93
|
+
/** 走完回退链(声明 title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」)后的标题。 */
|
|
94
|
+
title: LocalizedText;
|
|
95
|
+
/** 页头外链;声明省略时为空数组。 */
|
|
96
|
+
links: readonly ReportLink[];
|
|
97
|
+
footer?: LocalizedText;
|
|
98
|
+
/** 规范化后的页列表(id 与导航页名),恒非空。 */
|
|
99
|
+
pages: NonEmptyArray<{ id: string; title: LocalizedText }>;
|
|
100
|
+
/** 当前渲染中的页 id。 */
|
|
101
|
+
pageId: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 单页缩写展开出的唯一页 id 与内置页名。 */
|
|
105
|
+
export const DEFAULT_PAGE_ID = "report";
|
|
106
|
+
const DEFAULT_PAGE_TITLE: LocalizedText = { en: "Report", "zh-CN": "报告" };
|
|
107
|
+
|
|
108
|
+
// ───────────────────────── 装载规范化与静态校验 ─────────────────────────
|
|
109
|
+
|
|
110
|
+
const CONTENT_NEXT_STEP =
|
|
111
|
+
'To render the built-in report content, write content: <ExperimentComparison /> (imported from "niceeval/report").';
|
|
112
|
+
|
|
113
|
+
function isReportNodeInput(value: unknown): boolean {
|
|
114
|
+
if (value === null || value === undefined || typeof value === "boolean") return true;
|
|
115
|
+
if (Array.isArray(value)) return true;
|
|
116
|
+
return (
|
|
117
|
+
typeof value === "object" &&
|
|
118
|
+
value !== null &&
|
|
119
|
+
"type" in value &&
|
|
120
|
+
"props" in value &&
|
|
121
|
+
(value as { kind?: unknown }).kind !== "report"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertNotDefinition(value: unknown, where: string): void {
|
|
126
|
+
if (
|
|
127
|
+
typeof value === "object" &&
|
|
128
|
+
value !== null &&
|
|
129
|
+
(value as { kind?: unknown }).kind === "report" &&
|
|
130
|
+
(value as Record<symbol, unknown>)[REPORT_DEFINITION] === true
|
|
131
|
+
) {
|
|
40
132
|
throw new Error(
|
|
41
|
-
|
|
133
|
+
`${where} received a defineReport(...) product, but a report definition is not a report node — the shell cannot nest. ` +
|
|
134
|
+
"Pass the page's tree or component here, and export the defineReport product only as the file's default export.",
|
|
42
135
|
);
|
|
43
136
|
}
|
|
44
|
-
return { build, [REPORT_DEFINITION]: true };
|
|
45
137
|
}
|
|
46
138
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
139
|
+
function assertLocalizedText(value: unknown, where: string): asserts value is LocalizedText {
|
|
140
|
+
if (typeof value === "string") {
|
|
141
|
+
if (value.length === 0) {
|
|
142
|
+
throw new Error(`${where} must not be an empty string. Give it a visible label, e.g. "Overview".`);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
147
|
+
const hasNonEmpty = Object.values(value as Record<string, unknown>).some(
|
|
148
|
+
(v) => typeof v === "string" && v.length > 0,
|
|
149
|
+
);
|
|
150
|
+
if (!hasNonEmpty) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`${where} is a LocalizedText object with no non-empty value. Provide at least one locale entry, e.g. { en: "Overview" }.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
throw new Error(
|
|
158
|
+
`${where} must be a LocalizedText (a string, or a { [locale]: string } record); got ${typeof value}.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const PAGE_ID_PATTERN = /^[a-z0-9-]+$/;
|
|
163
|
+
|
|
164
|
+
function assertAssets(assets: unknown, field: "scripts" | "styles"): ReportAsset[] {
|
|
165
|
+
if (assets === undefined) return [];
|
|
166
|
+
if (!Array.isArray(assets)) {
|
|
167
|
+
throw new Error(`defineReport ${field} must be an array of { src } or { inline } entries.`);
|
|
168
|
+
}
|
|
169
|
+
for (const asset of assets as Array<Record<string, unknown>>) {
|
|
170
|
+
const hasSrc = typeof asset?.src === "string";
|
|
171
|
+
const hasInline = typeof asset?.inline === "string";
|
|
172
|
+
if (hasSrc === hasInline) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Each defineReport ${field} entry must have exactly one of "src" (a path relative to the report file) or "inline" (literal content).`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (hasSrc) {
|
|
178
|
+
const src = asset.src as string;
|
|
179
|
+
const segments = src.split(/[\\/]+/);
|
|
180
|
+
if (src.startsWith("/") || /^[A-Za-z]:/.test(src) || src.startsWith("~") || segments.includes("..")) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`defineReport ${field} src "${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.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return assets as ReportAsset[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function defineReport(content: ReportNode): ReportDefinition;
|
|
191
|
+
export function defineReport(def: ReportDef): ReportDefinition;
|
|
192
|
+
export function defineReport(input: ReportNode | ReportDef): ReportDefinition {
|
|
193
|
+
assertNotDefinition(input, "defineReport(...)");
|
|
194
|
+
const def: ReportDef = isReportNodeInput(input)
|
|
195
|
+
? ({ content: input as ReportNode } as ReportDef)
|
|
196
|
+
: (input as ReportDef);
|
|
197
|
+
if (typeof def !== "object" || def === null) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"defineReport expects a report tree or a config object ({ title?, links?, footer?, scripts?, styles?, content | pages }). " +
|
|
200
|
+
CONTENT_NEXT_STEP,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hasContent = "content" in def && def.content !== undefined;
|
|
205
|
+
const hasPages = "pages" in def && def.pages !== undefined;
|
|
206
|
+
if (hasContent && hasPages) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`defineReport got both "content" and "pages" — declare exactly one. Keep "pages" for a multi-page report, or keep a single tree in "content". ${CONTENT_NEXT_STEP}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!hasContent && !hasPages) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`defineReport got neither "content" nor "pages" — declare exactly one; omission is not a meaningful value, the file must show what renders. ${CONTENT_NEXT_STEP}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let pages: ReportPage[];
|
|
218
|
+
if (hasContent) {
|
|
219
|
+
assertNotDefinition(def.content, 'defineReport "content"');
|
|
220
|
+
pages = [{ id: DEFAULT_PAGE_ID, title: DEFAULT_PAGE_TITLE, content: def.content as ReportNode }];
|
|
221
|
+
} else {
|
|
222
|
+
const raw = def.pages as unknown;
|
|
223
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`defineReport "pages" must be a non-empty array of { id, title, content }. ${CONTENT_NEXT_STEP}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const seen = new Set<string>();
|
|
229
|
+
for (const page of raw as Array<Record<string, unknown>>) {
|
|
230
|
+
if (typeof page?.id !== "string" || !PAGE_ID_PATTERN.test(page.id)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Report page id ${JSON.stringify(page?.id)} is invalid: ids are lowercase letters, digits and hyphens (they become --page values and #/page/<id> routes). Rename it, e.g. "overview".`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (seen.has(page.id)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Report page id "${page.id}" is declared twice — ids must be unique within one file (they are the --page selector and the web route). Rename one of the pages.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
seen.add(page.id);
|
|
241
|
+
assertLocalizedText(page.title, `Report page "${page.id}" title`);
|
|
242
|
+
assertNotDefinition(page.content, `Report page "${page.id}" content`);
|
|
243
|
+
}
|
|
244
|
+
pages = raw as ReportPage[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (def.title !== undefined) assertLocalizedText(def.title, "defineReport title");
|
|
248
|
+
if (def.footer !== undefined) assertLocalizedText(def.footer, "defineReport footer");
|
|
249
|
+
const links = def.links ?? [];
|
|
250
|
+
if (!Array.isArray(links)) throw new Error("defineReport links must be an array of { label, href }.");
|
|
251
|
+
for (const link of links) {
|
|
252
|
+
assertLocalizedText((link as ReportLink)?.label, "defineReport link label");
|
|
253
|
+
if (typeof (link as ReportLink)?.href !== "string" || (link as ReportLink).href.length === 0) {
|
|
254
|
+
throw new Error("defineReport link href must be a non-empty string URL.");
|
|
255
|
+
}
|
|
256
|
+
// icon 唯一合法形状是 { svg: string }(无类型 JS 传组件 / ReactNode / 裸字符串都在装载期拒绝):
|
|
257
|
+
// 外壳声明经序列化边界进前端,ReactNode 过不去,可序列化是外壳契约的一部分。
|
|
258
|
+
const icon = (link as { icon?: unknown }).icon;
|
|
259
|
+
if (icon !== undefined) {
|
|
260
|
+
const svg = (icon as { svg?: unknown })?.svg;
|
|
261
|
+
if (typeof icon !== "object" || icon === null || typeof svg !== "string" || svg.length === 0) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
'defineReport link "icon" must be { svg: string } — an inline SVG string rendered before the label. ' +
|
|
264
|
+
"Components and React nodes are not accepted: the shell declaration crosses a serialization boundary. " +
|
|
265
|
+
'Write e.g. icon: { svg: "<svg …>…</svg>" }.',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const definition = {
|
|
272
|
+
kind: "report" as const,
|
|
273
|
+
...(def.title !== undefined ? { title: def.title } : {}),
|
|
274
|
+
links: [...links],
|
|
275
|
+
...(def.footer !== undefined ? { footer: def.footer } : {}),
|
|
276
|
+
scripts: assertAssets(def.scripts, "scripts"),
|
|
277
|
+
styles: assertAssets(def.styles, "styles"),
|
|
278
|
+
pages: pages as unknown as NonEmptyArray<ReportPage>,
|
|
279
|
+
};
|
|
280
|
+
Object.defineProperty(definition, REPORT_DEFINITION, { value: true });
|
|
281
|
+
return definition;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 宿主装载报告文件时用:默认导出是不是 defineReport 的产物。 */
|
|
52
285
|
export function isReportDefinition(value: unknown): value is ReportDefinition {
|
|
53
286
|
return (
|
|
54
|
-
|
|
287
|
+
typeof value === "object" &&
|
|
55
288
|
value !== null &&
|
|
56
|
-
(value as
|
|
57
|
-
|
|
289
|
+
(value as { kind?: unknown }).kind === "report" &&
|
|
290
|
+
(value as Record<symbol, unknown>)[REPORT_DEFINITION] === true
|
|
58
291
|
);
|
|
59
292
|
}
|
|
60
293
|
|
|
294
|
+
// ───────────────────────── ReportMeta(标题回退单点)─────────────────────────
|
|
295
|
+
|
|
296
|
+
/** 标题回退链的终点:内置文案「Eval 运行结果 / Eval Results」(shell.md「行为约束」)。 */
|
|
297
|
+
export const FALLBACK_REPORT_TITLE: LocalizedText = { en: "Eval Results", "zh-CN": "Eval 运行结果" };
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 标题回退链的单点实现:def.title → Scope 中唯一且相同(LocalizedText 深相等)的非空快照
|
|
301
|
+
* name → 内置文案「Eval 运行结果 / Eval Results」。快照中没有 name 或存在多个不同 name 时
|
|
302
|
+
* 都落到内置文案,不按数组顺序挑。
|
|
303
|
+
*/
|
|
304
|
+
export function resolveReportTitle(definition: ReportDefinition, scope: Scope): LocalizedText {
|
|
305
|
+
if (definition.title !== undefined) return definition.title;
|
|
306
|
+
const names = scope.snapshots
|
|
307
|
+
.map((s) => s.name)
|
|
308
|
+
.filter((name): name is LocalizedText => name !== undefined && name !== "");
|
|
309
|
+
if (names.length === 0) return FALLBACK_REPORT_TITLE;
|
|
310
|
+
const first = names[0]!;
|
|
311
|
+
return names.every((name) => localizedTextEquals(name, first)) ? first : FALLBACK_REPORT_TITLE;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 规范化声明 → 组合组件可见的 ReportMeta(scripts / styles 是注入资产,不进)。 */
|
|
315
|
+
export function buildReportMeta(definition: ReportDefinition, scope: Scope, pageId: string): ReportMeta {
|
|
316
|
+
return {
|
|
317
|
+
title: resolveReportTitle(definition, scope),
|
|
318
|
+
links: definition.links,
|
|
319
|
+
...(definition.footer !== undefined ? { footer: definition.footer } : {}),
|
|
320
|
+
pages: definition.pages.map((page) => ({ id: page.id, title: page.title })) as unknown as NonEmptyArray<{
|
|
321
|
+
id: string;
|
|
322
|
+
title: LocalizedText;
|
|
323
|
+
}>,
|
|
324
|
+
pageId,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ───────────────────────── 页选择与 text 宿主入口 ─────────────────────────
|
|
329
|
+
|
|
330
|
+
/** `--page` 未命中:宿主据此按用法错误退出并列出可用页 id。 */
|
|
331
|
+
export class ReportPageNotFoundError extends Error {
|
|
332
|
+
readonly pageId: string;
|
|
333
|
+
readonly available: string[];
|
|
334
|
+
constructor(pageId: string, available: string[]) {
|
|
335
|
+
super(`page "${pageId}" not found. Available pages: ${available.join(", ")}`);
|
|
336
|
+
this.pageId = pageId;
|
|
337
|
+
this.available = available;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function pickReportPage(definition: ReportDefinition, pageId?: string): ReportPage {
|
|
342
|
+
if (pageId === undefined) return definition.pages[0];
|
|
343
|
+
const page = definition.pages.find((p) => p.id === pageId);
|
|
344
|
+
if (!page) {
|
|
345
|
+
throw new ReportPageNotFoundError(
|
|
346
|
+
pageId,
|
|
347
|
+
definition.pages.map((p) => p.id),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return page;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 宿主注入的渲染上下文:官方口径挑好的 Scope 与结果根完整读取面。 */
|
|
354
|
+
export interface ReportHostContext {
|
|
355
|
+
scope: Scope;
|
|
356
|
+
/** 组合组件 ctx.results 的来源;历史视图从这里自行挑 Snapshot[]。 */
|
|
357
|
+
results: Results;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface RenderReportTextOptions extends TextRenderOptions {
|
|
361
|
+
/** 渲染哪一页;缺省第一页。未命中抛 ReportPageNotFoundError。 */
|
|
362
|
+
pageId?: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
61
365
|
/**
|
|
62
|
-
* 挑选警告的 text 形态:每条渲染好的 message 前缀 "! "
|
|
63
|
-
*
|
|
64
|
-
*
|
|
366
|
+
* 挑选警告的 text 形态:每条渲染好的 message 前缀 "! ",一行一条。宿主级前置块——
|
|
367
|
+
* 宿主是 warning 的唯一呈现者,组件数据不复制 warning;裸跑 / --report 都在报告顶上
|
|
368
|
+
* 如实报残缺,不静默(docs/feature/reports/architecture.md「Scope 是计算入口」)。
|
|
65
369
|
*/
|
|
66
|
-
function
|
|
67
|
-
return warnings.map((w) => `! ${w.message}`).join("\n");
|
|
370
|
+
function renderScopeWarningsText(scope: Scope, _locale: ReportLocale): string {
|
|
371
|
+
return scope.warnings.map((w) => `! ${w.message}`).join("\n");
|
|
68
372
|
}
|
|
69
373
|
|
|
70
374
|
/**
|
|
71
|
-
* text
|
|
72
|
-
* text 面;
|
|
73
|
-
* 已经渲染同一条时不重复。不需要 react-dom。
|
|
375
|
+
* text 宿主的装载语义:选页 → resolve(组合展开 + spec 取数,唯一的 await 边界)→ 树校验 →
|
|
376
|
+
* 遍历渲染 text 面;Scope 有挑选警告时在报告顶部前置一块 "! <message>"。不需要 react-dom。
|
|
74
377
|
*/
|
|
75
378
|
export async function renderReportToText(
|
|
76
379
|
definition: ReportDefinition,
|
|
77
|
-
ctx:
|
|
78
|
-
options?:
|
|
380
|
+
ctx: ReportHostContext,
|
|
381
|
+
options?: RenderReportTextOptions,
|
|
79
382
|
): Promise<string> {
|
|
80
|
-
const
|
|
81
|
-
const
|
|
383
|
+
const page = pickReportPage(definition, options?.pageId);
|
|
384
|
+
const meta = buildReportMeta(definition, ctx.scope, page.id);
|
|
385
|
+
const resolved = await resolveReportTree(page.content, {
|
|
386
|
+
scope: ctx.scope,
|
|
387
|
+
results: ctx.results,
|
|
388
|
+
report: meta,
|
|
389
|
+
memo: new ResolveMemo(),
|
|
390
|
+
});
|
|
82
391
|
validateReportTree(resolved);
|
|
83
392
|
const textCtx = createTextContext(options);
|
|
84
393
|
const body = renderNodeToText(resolved, textCtx);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
394
|
+
return ctx.scope.warnings.length > 0
|
|
395
|
+
? [renderScopeWarningsText(ctx.scope, textCtx.locale), body].join("\n\n")
|
|
396
|
+
: body;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 页索引标题行(show 多页索引 / view 导航共用的解析结果):按 locale 解析的标题字符串。 */
|
|
400
|
+
export function reportTitleText(definition: ReportDefinition, scope: Scope, locale: ReportLocale): string {
|
|
401
|
+
return resolveLocalizedText(resolveReportTitle(definition, scope), locale);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ───────────────────────── 逐页(树)渲染入口:宿主联系面 ─────────────────────────
|
|
405
|
+
|
|
406
|
+
/** 宿主索引命令的完整上下文(docs/feature/reports/show/reports.md「索引命令携带完整上下文」)。 */
|
|
407
|
+
export interface HostCommandContext {
|
|
408
|
+
patterns: string[];
|
|
409
|
+
results?: string;
|
|
410
|
+
experiment?: string;
|
|
411
|
+
report?: string;
|
|
412
|
+
page?: string;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function quoteArg(value: string): string {
|
|
416
|
+
return /^[A-Za-z0-9._/@-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** 按上下文拼组索引的可复制命令:`niceeval show <patterns> --experiment <id> [--results/--report/--page]`。 */
|
|
420
|
+
function experimentCommandFor(ctx: HostCommandContext): (experimentIdPrefix: string) => string {
|
|
421
|
+
return (prefix) => {
|
|
422
|
+
const parts = ["niceeval show", ...ctx.patterns.map(quoteArg), `--experiment ${quoteArg(prefix)}`];
|
|
423
|
+
if (ctx.results !== undefined) parts.push(`--results ${quoteArg(ctx.results)}`);
|
|
424
|
+
if (ctx.report !== undefined) parts.push(`--report ${quoteArg(ctx.report)}`);
|
|
425
|
+
if (ctx.page !== undefined) parts.push(`--page ${quoteArg(ctx.page)}`);
|
|
426
|
+
return parts.join(" ");
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** 逐页渲染的宿主上下文:官方口径的 Scope、结果根读取面与规范化声明(ctx.report)。 */
|
|
431
|
+
export interface ReportTreeHostContext {
|
|
432
|
+
scope: Scope;
|
|
433
|
+
results: Results;
|
|
434
|
+
report: ReportMeta;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export interface RenderTreeTextOptions extends TextRenderOptions {
|
|
438
|
+
/** 组索引命令的完整上下文;给了就按它拼命令,experimentCommand 显式注入时以后者为准。 */
|
|
439
|
+
commandContext?: HostCommandContext;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 渲染一页报告树的 text 面(宿主逐页调用;页选择归宿主):
|
|
444
|
+
* resolve(组合展开 + spec 取数)→ validate → render。Scope 有挑选警告时在页顶前置
|
|
445
|
+
* "! <message>" 块——宿主是 warning 的唯一呈现者,组件数据不复制 warning。
|
|
446
|
+
*/
|
|
447
|
+
export async function renderReportTreeToText(
|
|
448
|
+
tree: ReportNode,
|
|
449
|
+
ctx: ReportTreeHostContext,
|
|
450
|
+
options?: RenderTreeTextOptions,
|
|
451
|
+
): Promise<string> {
|
|
452
|
+
const resolved = await resolveReportTree(tree, {
|
|
453
|
+
scope: ctx.scope,
|
|
454
|
+
results: ctx.results,
|
|
455
|
+
report: ctx.report,
|
|
456
|
+
memo: new ResolveMemo(),
|
|
457
|
+
});
|
|
458
|
+
validateReportTree(resolved);
|
|
459
|
+
const textCtx = createTextContext({
|
|
460
|
+
...options,
|
|
461
|
+
...(options?.experimentCommand === undefined && options?.commandContext !== undefined
|
|
462
|
+
? { experimentCommand: experimentCommandFor(options.commandContext) }
|
|
463
|
+
: {}),
|
|
464
|
+
});
|
|
465
|
+
const body = renderNodeToText(resolved, textCtx);
|
|
466
|
+
return ctx.scope.warnings.length > 0
|
|
467
|
+
? [renderScopeWarningsText(ctx.scope, textCtx.locale), body].join("\n\n")
|
|
90
468
|
: body;
|
|
91
469
|
}
|