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/dist/report/tree.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 报告的元素树、组件模型与 resolve 管线(docs/feature/reports/architecture.md「组件模型」
|
|
2
|
+
// 「报告树与两个宿主」、docs/feature/reports/library/layout.md)。
|
|
2
3
|
//
|
|
3
4
|
// 报告函数返回的树不是「React 树」,只是 { type, props } 节点 —— 标准 react
|
|
4
5
|
// jsx-runtime 产的元素恰好就是这个形状。本文件是基础实现:零 react 运行时依赖
|
|
5
6
|
// (只有类型层的 `import type`,编译后擦除);text 宿主遍历渲染不需要 react-dom,
|
|
6
|
-
// web 宿主(web.ts)才真正 import react
|
|
7
|
-
//
|
|
7
|
+
// web 宿主(web.ts)才真正 import react。管线固定为 装载 → resolve(组合展开 + spec 取数,
|
|
8
|
+
// 同层并行保声明序,按「同引用 input + 深相等 spec」记忆化)→ validate(两面资格)→
|
|
9
|
+
// render(纯同步)。渲染面是纯同步函数:零 IO、零 await —— 可达百 MB 的 artifact
|
|
10
|
+
// 只在 resolve 阶段被懒加载,永远不进渲染路径。
|
|
8
11
|
import { DEFAULT_REPORT_LOCALE } from "./locale.js";
|
|
9
12
|
// react/jsx-runtime 的 Fragment 是注册符号,跨 react 版本稳定;不 import react 也认得它
|
|
10
13
|
const REACT_FRAGMENT = Symbol.for("react.fragment");
|
|
@@ -16,17 +19,20 @@ function isReportElement(node) {
|
|
|
16
19
|
"props" in node &&
|
|
17
20
|
typeof node.props === "object");
|
|
18
21
|
}
|
|
19
|
-
// ─────────────────────────
|
|
20
|
-
/** 挂 faces
|
|
22
|
+
// ───────────────────────── 组件模型 ─────────────────────────
|
|
23
|
+
/** 挂 faces 的私有键:管线与树校验靠它识别双面组件。 */
|
|
21
24
|
export const COMPONENT_FACES = Symbol.for("niceeval.report.faces");
|
|
25
|
+
/** 挂组合函数的私有键:resolve 阶段靠它识别组合组件。 */
|
|
26
|
+
export const COMPONENT_COMPOSE = Symbol.for("niceeval.report.compose");
|
|
27
|
+
/** Tabs / Tab 的结构角色标记:树校验的配对规则靠它,不 import primitives(避免环)。 */
|
|
28
|
+
export const COMPONENT_ROLE = Symbol.for("niceeval.report.role");
|
|
29
|
+
/**
|
|
30
|
+
* children 是不透明值(自由文本 / CSS 字符串)而非报告树的组件标记(Text / Style):
|
|
31
|
+
* resolve 与 validate 不下钻它们的 children——那是组件自己的 props,不是树。
|
|
32
|
+
*/
|
|
33
|
+
export const COMPONENT_RAW_CHILDREN = Symbol.for("niceeval.report.rawChildren");
|
|
22
34
|
// web 面的环境上下文:web 宿主渲染前设好;宿主之外(组件直接嵌进用户 React 应用)
|
|
23
|
-
// 用默认值 —— attemptHref 默认 view 的 attempt
|
|
24
|
-
// 时总有去处);官方组件的「宿主里自动接证据室」只在宿主上下文激活时发生,
|
|
25
|
-
// 宿主外不传 attemptHref 就是纯展示,不发明断链。
|
|
26
|
-
//
|
|
27
|
-
// URL 格式取 `#/attempt/${locator}`:AttemptLocator 本身已经是 `@` 前缀的不透明短串
|
|
28
|
-
// (如 "@1x7f3q9"),原样嵌进路径段就得到 `#/attempt/@1x7f3q9`——与 docs/feature/reports/view.md「用
|
|
29
|
-
// Reports 积木重建 view」定稿的单段路由逐字一致,不额外拆分或去掉 `@`。
|
|
35
|
+
// 用默认值 —— attemptHref 默认 view 的 attempt 路由格式。
|
|
30
36
|
const DEFAULT_WEB_CONTEXT = {
|
|
31
37
|
attemptHref: (locator) => `#/attempt/${locator}`,
|
|
32
38
|
locale: DEFAULT_REPORT_LOCALE,
|
|
@@ -47,83 +53,182 @@ export function runWithWebContext(ctx, fn) {
|
|
|
47
53
|
export function isHostWebContextActive() {
|
|
48
54
|
return activeWebContext !== null;
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
export function defineComponent(input) {
|
|
57
|
+
if (typeof input === "function") {
|
|
58
|
+
const component = ((_props) => {
|
|
59
|
+
throw new Error(`Compose component ${componentDisplayName(input) ?? "(anonymous)"} can only render inside the report pipeline ` +
|
|
60
|
+
"(niceeval show / view, or renderReportToText / renderReportToStaticHtml): it assembles other components " +
|
|
61
|
+
"and needs the host context (scope, results, report). To embed in your own React page, compute data with " +
|
|
62
|
+
"the *Data functions and render the pure components from niceeval/report/react instead.");
|
|
63
|
+
});
|
|
64
|
+
component[COMPONENT_COMPOSE] = input;
|
|
65
|
+
if (componentDisplayName(input))
|
|
66
|
+
component.displayName = componentDisplayName(input);
|
|
67
|
+
return component;
|
|
58
68
|
}
|
|
69
|
+
if (typeof input?.web !== "function" || typeof input?.text !== "function") {
|
|
70
|
+
throw new Error("defineComponent requires both faces: { web(props, ctx), text(props, ctx) } (resolve is optional). " +
|
|
71
|
+
"Every report component must render in both hosts (niceeval view and niceeval show); " +
|
|
72
|
+
"define the missing face, or pass a compose function to assemble existing components instead.");
|
|
73
|
+
}
|
|
74
|
+
const faces = input;
|
|
59
75
|
// 直接调用路径:把组件当普通 React 组件嵌进用户自己的页面时走这里,web 面只接收数据形态
|
|
60
|
-
// props(R)。带 resolve 的组件若拿
|
|
76
|
+
// props(R)。带 resolve 的组件若拿 spec 形态 props 走这条裸路径,web 面会缺 data ——
|
|
61
77
|
// 这类组件只有经宿主的 resolveReportTree 解析后才安全渲染;纯数据 props 一直可以裸嵌。
|
|
62
78
|
const component = ((props) => faces.web(props, activeWebContext ?? DEFAULT_WEB_CONTEXT));
|
|
63
79
|
component[COMPONENT_FACES] = faces;
|
|
64
80
|
return component;
|
|
65
81
|
}
|
|
82
|
+
function componentDisplayName(fn) {
|
|
83
|
+
const named = fn;
|
|
84
|
+
return named.displayName || named.name || undefined;
|
|
85
|
+
}
|
|
66
86
|
export function facesOf(type) {
|
|
67
87
|
if (typeof type !== "function")
|
|
68
88
|
return undefined;
|
|
69
89
|
return type[COMPONENT_FACES];
|
|
70
90
|
}
|
|
71
|
-
|
|
91
|
+
export function composeOf(type) {
|
|
92
|
+
if (typeof type !== "function")
|
|
93
|
+
return undefined;
|
|
94
|
+
return type[COMPONENT_COMPOSE];
|
|
95
|
+
}
|
|
96
|
+
function roleOf(type) {
|
|
97
|
+
if (typeof type !== "function")
|
|
98
|
+
return undefined;
|
|
99
|
+
return type[COMPONENT_ROLE];
|
|
100
|
+
}
|
|
101
|
+
function hasRawChildren(type) {
|
|
102
|
+
return typeof type === "function" && type[COMPONENT_RAW_CHILDREN] === true;
|
|
103
|
+
}
|
|
104
|
+
// ───────────────────────── 深相等(记忆化的 spec 比较)─────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* resolve 记忆化的深相等:只递归比较可序列化值(普通对象 / 数组 / 原始值);
|
|
107
|
+
* 函数与 Metric / Dimension / NumericAxis 一类携带函数的实例按引用比较——
|
|
108
|
+
* 共享计算的成立条件是引用同一实例,引用不同的等价定义只是各算一次,不构成错误。
|
|
109
|
+
*/
|
|
110
|
+
export function deepEqualSpec(a, b) {
|
|
111
|
+
if (a === b)
|
|
112
|
+
return true;
|
|
113
|
+
if (typeof a !== typeof b)
|
|
114
|
+
return false;
|
|
115
|
+
if (typeof a !== "object" || a === null || b === null)
|
|
116
|
+
return false;
|
|
117
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
118
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
119
|
+
return false;
|
|
120
|
+
return a.every((item, i) => deepEqualSpec(item, b[i]));
|
|
121
|
+
}
|
|
122
|
+
const protoA = Object.getPrototypeOf(a);
|
|
123
|
+
const protoB = Object.getPrototypeOf(b);
|
|
124
|
+
if ((protoA !== Object.prototype && protoA !== null) || (protoB !== Object.prototype && protoB !== null)) {
|
|
125
|
+
return false; // 非纯对象(类实例等)按引用比较,=== 已在开头判过
|
|
126
|
+
}
|
|
127
|
+
const keysA = Object.keys(a).filter((k) => a[k] !== undefined);
|
|
128
|
+
const keysB = Object.keys(b).filter((k) => b[k] !== undefined);
|
|
129
|
+
if (keysA.length !== keysB.length)
|
|
130
|
+
return false;
|
|
131
|
+
return keysA.every((k) => deepEqualSpec(a[k], b[k]));
|
|
132
|
+
}
|
|
133
|
+
/** 一次页渲染内的记忆化缓存:键 = 计算函数引用 → (同引用 input + 深相等 spec) 命中。 */
|
|
134
|
+
export class ResolveMemo {
|
|
135
|
+
entries = new Map();
|
|
136
|
+
fetch(fn, input, spec, compute) {
|
|
137
|
+
let list = this.entries.get(fn);
|
|
138
|
+
if (!list)
|
|
139
|
+
this.entries.set(fn, (list = []));
|
|
140
|
+
for (const entry of list) {
|
|
141
|
+
if (entry.input === input && deepEqualSpec(entry.spec, spec))
|
|
142
|
+
return entry.result;
|
|
143
|
+
}
|
|
144
|
+
const result = compute();
|
|
145
|
+
list.push({ input, spec, result });
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** 组件 resolve 里安全取记忆化入口:经宿主管线时在场,手工直调 resolve 时退化为直接计算。 */
|
|
150
|
+
export function memoFetchOf(ctx) {
|
|
151
|
+
const internal = ctx;
|
|
152
|
+
return internal.memoFetch ?? (async (_fn, _input, _options, compute) => compute());
|
|
153
|
+
}
|
|
154
|
+
function illegalNodeError(type, path) {
|
|
155
|
+
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
156
|
+
if (typeof type === "string") {
|
|
157
|
+
return new Error(`Report trees cannot contain raw HTML <${type}>${where}. Every node must render in both hosts (niceeval show and niceeval view), and raw HTML has no text face. ` +
|
|
158
|
+
"Use <Text> for free text, the layout primitives (<Row>/<Col>/<Section>/<Table>), or move the markup into the web face of a defineComponent component.");
|
|
159
|
+
}
|
|
160
|
+
const label = componentLabel(type);
|
|
161
|
+
return new Error(`${label} is not a report component${where}: plain functions and React components cannot join a report tree because the hosts cannot render them in both faces. ` +
|
|
162
|
+
"Wrap it with defineComponent — a compose function defineComponent((props, ctx) => tree) to assemble existing components, " +
|
|
163
|
+
"or an object form defineComponent({ resolve?, text, web }) to render itself.");
|
|
164
|
+
}
|
|
165
|
+
function bareTextError(value, path) {
|
|
166
|
+
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
167
|
+
const preview = typeof value === "string" ? JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value) : String(value);
|
|
168
|
+
return new Error(`Report trees cannot contain a bare ${typeof value} (${preview})${where}: free text needs an explicit carrier for terminal line-wrapping and HTML escaping. ` +
|
|
169
|
+
"Wrap it in <Text>…</Text>.");
|
|
170
|
+
}
|
|
72
171
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* await 的组件级步骤,连同报告函数体自己的 `build()`),两个渲染面
|
|
78
|
-
* 之后都只看已解析的树、保持同步零 IO。遍历形状与 {@link validateReportTree} /
|
|
79
|
-
* {@link renderNodeToText} 一致:
|
|
80
|
-
*
|
|
81
|
-
* - 同层数组兄弟并行解析(`Promise.all`),保持原始顺序 / keys;
|
|
82
|
-
* - 双面组件带 resolve 的:调 `faces.resolve(props)` 换 props(有 children 再递归解析);
|
|
83
|
-
* - 双面组件无 resolve 的(Row / Col / Section / RunOverview…):只递归 children,自身 props
|
|
84
|
-
* 原样保留(title / className 等不能动);
|
|
85
|
-
* - 普通函数组件:同步调用展开,展开结果继续解析(与 validate / render 对函数组件的处理一致);
|
|
86
|
-
* - 字符串 intrinsic(<div>):原样返回,交给随后的 validateReportTree 报同一条错误。
|
|
87
|
-
*
|
|
88
|
-
* resolver 跑完后,树里已没有函数组件 / 未解析 props;validate / render 里对这两者的分支在
|
|
89
|
-
* 正常管线下是 no-op,但保留着——手搭树直接调 validate / render(不过 resolver)的低层用法仍需要。
|
|
172
|
+
* 管线的 resolve 阶段:递归展开组合组件(以 (props, ctx) 调用并 await 返回树)、执行双面
|
|
173
|
+
* 组件的解析面;同层 sibling 并行取数且不改变节点顺序;带 resolve 的组件按「同引用 input +
|
|
174
|
+
* 深相等 spec」记忆化。非法节点(React 组件、未经 defineComponent 的普通函数、任意 HTML
|
|
175
|
+
* intrinsic)在展开遇到时立即以完整用户反馈拒绝,不为非法节点取数。
|
|
90
176
|
*/
|
|
91
|
-
export async function resolveReportTree(node) {
|
|
177
|
+
export async function resolveReportTree(node, env) {
|
|
178
|
+
const memo = env.memo ?? new ResolveMemo();
|
|
179
|
+
const composeCtx = { scope: env.scope, results: env.results, report: env.report };
|
|
180
|
+
return resolveNode(node, { memo, composeCtx }, []);
|
|
181
|
+
}
|
|
182
|
+
async function resolveNode(node, state, path) {
|
|
92
183
|
if (node === null || node === undefined || typeof node === "boolean")
|
|
93
184
|
return node;
|
|
94
|
-
if (typeof node === "string" || typeof node === "number")
|
|
185
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
186
|
+
// 裸字符串在 validate 阶段以带指引的完整反馈拒绝;这里先如实透传,不为它取数。
|
|
95
187
|
return node;
|
|
96
|
-
|
|
97
|
-
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(node)) {
|
|
190
|
+
return Promise.all(node.map((child) => resolveNode(child, state, path)));
|
|
191
|
+
}
|
|
98
192
|
if (!isReportElement(node))
|
|
99
193
|
return node;
|
|
100
194
|
const { type, props } = node;
|
|
101
|
-
// 字符串 intrinsic:不在这里报错,让 resolve 后的 validateReportTree 抛统一的那条。
|
|
102
|
-
if (typeof type === "string")
|
|
103
|
-
return node;
|
|
104
195
|
if (type === REACT_FRAGMENT) {
|
|
105
|
-
const children = await
|
|
196
|
+
const children = await resolveNode(props.children, state, path);
|
|
106
197
|
return { ...node, props: { ...props, children } };
|
|
107
198
|
}
|
|
199
|
+
const compose = composeOf(type);
|
|
200
|
+
if (compose) {
|
|
201
|
+
// 组合组件不记忆化:它只装配、不承担取数;数据层的去重由其内部 *Data 调用经 memoFetch 命中。
|
|
202
|
+
const expanded = await compose(props, state.composeCtx);
|
|
203
|
+
return resolveNode(expanded, state, [...path, componentLabel(type)]);
|
|
204
|
+
}
|
|
108
205
|
const faces = facesOf(type);
|
|
109
206
|
if (faces) {
|
|
110
207
|
if (typeof faces.resolve === "function") {
|
|
111
|
-
const
|
|
208
|
+
const resolveFn = faces.resolve;
|
|
209
|
+
const input = props.input ?? state.composeCtx.scope;
|
|
210
|
+
const ctx = {
|
|
211
|
+
input,
|
|
212
|
+
memoFetch: (dataFn, dataInput, options, compute) => state.memo.fetch(dataFn, dataInput, options, compute),
|
|
213
|
+
};
|
|
214
|
+
const resolved = await state.memo.fetch(resolveFn, input, props, async () => resolveFn(props, ctx));
|
|
215
|
+
const resolvedProps = { ...resolved };
|
|
112
216
|
if (resolvedProps.children !== undefined) {
|
|
113
|
-
resolvedProps.children = await
|
|
217
|
+
resolvedProps.children = await resolveNode(resolvedProps.children, state, [
|
|
218
|
+
...path,
|
|
219
|
+
componentLabel(type),
|
|
220
|
+
]);
|
|
114
221
|
}
|
|
115
222
|
return { ...node, props: resolvedProps };
|
|
116
223
|
}
|
|
117
|
-
// 无 resolve 的容器 /
|
|
118
|
-
|
|
224
|
+
// 无 resolve 的容器 / 纯数据组件:只解析 children,自身 props 原样保留(title / className 不能动)。
|
|
225
|
+
if (hasRawChildren(type))
|
|
226
|
+
return node; // Text / Style 的 children 是不透明值,不是树
|
|
227
|
+
const children = await resolveNode(props.children, state, [...path, componentLabel(type)]);
|
|
119
228
|
return { ...node, props: { ...props, children } };
|
|
120
229
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const expanded = type(props);
|
|
124
|
-
return resolveReportTree(expanded);
|
|
125
|
-
}
|
|
126
|
-
return node;
|
|
230
|
+
// 非法节点:HTML intrinsic、React 组件、未经 defineComponent 的普通函数 —— 立即拒绝,不取数。
|
|
231
|
+
throw illegalNodeError(type, path);
|
|
127
232
|
}
|
|
128
233
|
// ───────────────────────── 树校验 ─────────────────────────
|
|
129
234
|
function componentLabel(type) {
|
|
@@ -137,72 +242,130 @@ function componentLabel(type) {
|
|
|
137
242
|
return "<>";
|
|
138
243
|
return `<${String(type)}>`;
|
|
139
244
|
}
|
|
245
|
+
function directChildElements(children) {
|
|
246
|
+
const out = [];
|
|
247
|
+
const visit = (node) => {
|
|
248
|
+
if (node === null || node === undefined || typeof node === "boolean")
|
|
249
|
+
return;
|
|
250
|
+
if (Array.isArray(node)) {
|
|
251
|
+
for (const child of node)
|
|
252
|
+
visit(child);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (isReportElement(node)) {
|
|
256
|
+
if (node.type === REACT_FRAGMENT) {
|
|
257
|
+
visit(node.props.children);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
out.push(node);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
out.push(node);
|
|
264
|
+
};
|
|
265
|
+
visit(children);
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
140
268
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* 普通函数组件调用展开(渲染面纯同步,重复调用无副作用)。
|
|
269
|
+
* 管线的 validate 阶段:确保展开后树中每个组件都有 text 和 web 两面。校验只看节点资格,
|
|
270
|
+
* 不限定树形:根节点可以是单个组件、Col 或 Tabs,宿主不强制任何最外层容器。
|
|
271
|
+
* 裸字符串 / 数字、HTML intrinsic、无两面资格的函数都以完整用户反馈拒绝;
|
|
272
|
+
* Tabs / Tab 的结构配对(空 Tabs、游离 Tab、非 Tab 直接子节点)也在这里校验。
|
|
146
273
|
*/
|
|
147
|
-
export function validateReportTree(node, path = []) {
|
|
274
|
+
export function validateReportTree(node, path = [], insideTabs = false) {
|
|
148
275
|
if (node === null || node === undefined || typeof node === "boolean")
|
|
149
276
|
return;
|
|
150
|
-
if (typeof node === "string" || typeof node === "number")
|
|
151
|
-
|
|
277
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
278
|
+
throw bareTextError(node, path);
|
|
279
|
+
}
|
|
152
280
|
if (Array.isArray(node)) {
|
|
153
281
|
for (const child of node)
|
|
154
|
-
validateReportTree(child, path);
|
|
282
|
+
validateReportTree(child, path, insideTabs);
|
|
155
283
|
return;
|
|
156
284
|
}
|
|
157
|
-
if (!isReportElement(node))
|
|
285
|
+
if (!isReportElement(node)) {
|
|
286
|
+
if (typeof node === "object" && node !== null && node.kind === "report") {
|
|
287
|
+
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
288
|
+
throw new Error(`A defineReport(...) product is not a report node${where}: the shell cannot nest. ` +
|
|
289
|
+
"Put the page content (a tree or component) here, and keep defineReport for the file's default export only.");
|
|
290
|
+
}
|
|
158
291
|
return;
|
|
159
|
-
const { type, props } = node;
|
|
160
|
-
if (typeof type === "string") {
|
|
161
|
-
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
162
|
-
throw new Error(`Raw HTML <${type}> has no terminal face; use <Text>, layout primitives, or a defineComponent component.${where}`);
|
|
163
292
|
}
|
|
293
|
+
const { type, props } = node;
|
|
164
294
|
if (type === REACT_FRAGMENT) {
|
|
165
|
-
validateReportTree(props.children, path);
|
|
295
|
+
validateReportTree(props.children, path, insideTabs);
|
|
166
296
|
return;
|
|
167
297
|
}
|
|
298
|
+
if (typeof type === "string")
|
|
299
|
+
throw illegalNodeError(type, path);
|
|
168
300
|
const label = componentLabel(type);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
301
|
+
const role = roleOf(type);
|
|
302
|
+
if (role === "tab" && !insideTabs) {
|
|
303
|
+
const where = path.length > 0 ? ` (in ${path.join(" > ")})` : "";
|
|
304
|
+
throw new Error(`<Tab> can only be a direct child of <Tabs>${where}. Wrap sibling views in <Tabs><Tab title="…">…</Tab></Tabs>, or drop the <Tab> wrapper if you only have one view.`);
|
|
305
|
+
}
|
|
306
|
+
if (role === "tabs") {
|
|
307
|
+
const children = directChildElements(props.children);
|
|
308
|
+
const nonTab = children.find((child) => roleOf(child.type) !== "tab");
|
|
309
|
+
if (nonTab !== undefined) {
|
|
310
|
+
throw new Error(`<Tabs> only accepts <Tab> as direct children (found ${isReportElement(nonTab) ? componentLabel(nonTab.type) : typeof nonTab} in ${[...path, label].join(" > ")}). Move the content inside a <Tab title="…">, or lift it out of <Tabs>.`);
|
|
311
|
+
}
|
|
312
|
+
if (children.length === 0) {
|
|
313
|
+
throw new Error(`<Tabs> needs at least one <Tab> child (in ${[...path, label].join(" > ")}). Add <Tab title="…">…</Tab>, or remove the empty <Tabs>.`);
|
|
314
|
+
}
|
|
315
|
+
for (const tab of children) {
|
|
316
|
+
validateReportTree(tab.props.children, [...path, label, componentLabel(tab.type)], false);
|
|
317
|
+
}
|
|
172
318
|
return;
|
|
173
319
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
320
|
+
const faces = facesOf(type);
|
|
321
|
+
if (faces) {
|
|
322
|
+
// 双面资格:无类型 JS 输入可能绕过 defineComponent 的定义期校验,这里再拦一遍
|
|
323
|
+
if (typeof faces.web !== "function" || typeof faces.text !== "function") {
|
|
324
|
+
const missing = typeof faces.web !== "function" ? "web" : "text";
|
|
325
|
+
throw new Error(`${label} is missing its ${missing} face${path.length > 0 ? ` (in ${path.join(" > ")})` : ""}: every component in a report tree must render in both hosts (niceeval show and niceeval view). Define both { text, web } in defineComponent.`);
|
|
326
|
+
}
|
|
327
|
+
// 双面组件是校验的信任边界之内的叶子,但 children 仍是报告树的一部分
|
|
328
|
+
// (Text / Style 除外:它们的 children 是不透明的自由文本 / CSS 字符串)
|
|
329
|
+
if (!hasRawChildren(type))
|
|
330
|
+
validateReportTree(props.children, [...path, label], false);
|
|
178
331
|
return;
|
|
179
332
|
}
|
|
180
|
-
|
|
181
|
-
|
|
333
|
+
if (composeOf(type)) {
|
|
334
|
+
// 正常管线下 resolve 已把组合组件展开;手搭树直接 validate 时如实指出它还没展开
|
|
335
|
+
throw new Error(`${label} is a compose component and must be expanded by the resolve pipeline before validation. ` +
|
|
336
|
+
"Render through defineReport + niceeval show/view (or renderReportToText/renderReportToStaticHtml).");
|
|
337
|
+
}
|
|
338
|
+
throw illegalNodeError(type, path);
|
|
339
|
+
}
|
|
340
|
+
function shellQuote(value) {
|
|
341
|
+
return /^[A-Za-z0-9._/@-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
182
342
|
}
|
|
183
343
|
export function createTextContext(options) {
|
|
184
344
|
const width = Math.max(20, options?.width ?? 80);
|
|
185
345
|
const locale = options?.locale ?? DEFAULT_REPORT_LOCALE;
|
|
186
346
|
// 默认下钻命令:AttemptLocator 是 `@` 前缀的不透明短串,`niceeval show @<locator>` 是
|
|
187
|
-
// show
|
|
188
|
-
// 反查 eval id 再拼一条近似命令。
|
|
347
|
+
// show 已实现的真实 CLI 语法,不需要反查 eval id 再拼一条近似命令。
|
|
189
348
|
const attemptCommand = options?.attemptCommand ?? ((locator) => `niceeval show ${locator}`);
|
|
349
|
+
const experimentCommand = options?.experimentCommand ?? ((prefix) => `niceeval show --experiment ${shellQuote(prefix)}`);
|
|
190
350
|
const make = (w) => ({
|
|
191
351
|
width: w,
|
|
192
352
|
locale,
|
|
193
353
|
attemptCommand,
|
|
354
|
+
experimentCommand,
|
|
194
355
|
render(node, childWidth) {
|
|
195
356
|
return renderNodeToText(node, childWidth === undefined ? this : make(Math.max(10, childWidth)));
|
|
196
357
|
},
|
|
197
358
|
});
|
|
198
359
|
return make(width);
|
|
199
360
|
}
|
|
200
|
-
/** text 宿主的遍历渲染:双面组件走 text
|
|
361
|
+
/** text 宿主的遍历渲染:双面组件走 text 面,块之间以换行相接。只吃已 resolve + validate 的树。 */
|
|
201
362
|
export function renderNodeToText(node, ctx) {
|
|
202
363
|
if (node === null || node === undefined || typeof node === "boolean")
|
|
203
364
|
return "";
|
|
204
|
-
if (typeof node === "string" || typeof node === "number")
|
|
205
|
-
|
|
365
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
366
|
+
// 校验先行会拦住;渲染路径自身也不宽容
|
|
367
|
+
throw bareTextError(node, []);
|
|
368
|
+
}
|
|
206
369
|
if (Array.isArray(node)) {
|
|
207
370
|
return node
|
|
208
371
|
.map((child) => renderNodeToText(child, ctx))
|
|
@@ -212,17 +375,12 @@ export function renderNodeToText(node, ctx) {
|
|
|
212
375
|
if (!isReportElement(node))
|
|
213
376
|
return "";
|
|
214
377
|
const { type, props } = node;
|
|
215
|
-
if (typeof type === "string")
|
|
216
|
-
|
|
217
|
-
throw new Error(`Raw HTML <${type}> has no terminal face; use <Text>, layout primitives, or a defineComponent component.`);
|
|
218
|
-
}
|
|
378
|
+
if (typeof type === "string")
|
|
379
|
+
throw illegalNodeError(type, []);
|
|
219
380
|
if (type === REACT_FRAGMENT)
|
|
220
381
|
return renderNodeToText(props.children, ctx);
|
|
221
382
|
const faces = facesOf(type);
|
|
222
383
|
if (faces)
|
|
223
384
|
return faces.text(props, ctx);
|
|
224
|
-
|
|
225
|
-
return renderNodeToText(type(props), ctx);
|
|
226
|
-
}
|
|
227
|
-
return "";
|
|
385
|
+
throw illegalNodeError(type, []);
|
|
228
386
|
}
|