niceeval 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/INDEX.md +77 -45
  2. package/dist/agents/types.d.ts +28 -6
  3. package/dist/i18n/en.d.ts +2 -0
  4. package/dist/i18n/zh-CN.d.ts +2 -0
  5. package/dist/report/built-in/index.d.ts +3 -2
  6. package/dist/report/built-in/index.js +7 -8
  7. package/dist/report/built-in/standard.d.ts +1 -0
  8. package/dist/report/built-in/standard.js +30 -0
  9. package/dist/report/components.d.ts +72 -6
  10. package/dist/report/components.js +159 -10
  11. package/dist/report/compute.d.ts +31 -4
  12. package/dist/report/compute.js +131 -12
  13. package/dist/report/index.d.ts +4 -4
  14. package/dist/report/index.js +3 -2
  15. package/dist/report/locale.d.ts +39 -1
  16. package/dist/report/locale.js +69 -0
  17. package/dist/report/react/AttemptList.d.ts +3 -1
  18. package/dist/report/react/AttemptList.js +3 -3
  19. package/dist/report/react/CopyFixPrompt.d.ts +12 -0
  20. package/dist/report/react/CopyFixPrompt.js +12 -0
  21. package/dist/report/react/HeroCard.d.ts +13 -0
  22. package/dist/report/react/HeroCard.js +35 -0
  23. package/dist/report/react/PoweredBy.d.ts +5 -0
  24. package/dist/report/react/PoweredBy.js +7 -0
  25. package/dist/report/react/ScopeWarnings.d.ts +12 -0
  26. package/dist/report/react/ScopeWarnings.js +18 -0
  27. package/dist/report/react/TraceWaterfall.d.ts +14 -0
  28. package/dist/report/react/TraceWaterfall.js +22 -0
  29. package/dist/report/react/index.d.ts +6 -1
  30. package/dist/report/react/index.js +6 -0
  31. package/dist/report/report.d.ts +22 -6
  32. package/dist/report/report.js +66 -53
  33. package/dist/report/scope-warnings.d.ts +28 -0
  34. package/dist/report/scope-warnings.js +101 -0
  35. package/dist/report/text/faces.d.ts +19 -1
  36. package/dist/report/text/faces.js +61 -0
  37. package/dist/report/tree.js +6 -1
  38. package/dist/report/types.d.ts +45 -10
  39. package/dist/report/web.d.ts +5 -4
  40. package/dist/report/web.js +7 -22
  41. package/dist/results/select.d.ts +15 -2
  42. package/dist/results/select.js +75 -10
  43. package/dist/results/types.d.ts +26 -11
  44. package/dist/runner/fingerprint.d.ts +3 -3
  45. package/dist/runner/sandbox-selection.d.ts +12 -0
  46. package/dist/runner/types.d.ts +18 -6
  47. package/dist/sandbox/types.d.ts +12 -0
  48. package/docs-site/zh/explanation/evals.mdx +2 -1
  49. package/docs-site/zh/explanation/experiment.mdx +2 -0
  50. package/docs-site/zh/how-to/custom-reports.mdx +22 -6
  51. package/docs-site/zh/how-to/publish-report.mdx +6 -8
  52. package/docs-site/zh/how-to/viewing-results.mdx +3 -3
  53. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  54. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  55. package/docs-site/zh/reference/cli.mdx +1 -2
  56. package/docs-site/zh/reference/define-eval.mdx +8 -0
  57. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  58. package/docs-site/zh/reference/report-components.mdx +2 -2
  59. package/docs-site/zh/reference/results-data.mdx +2 -4
  60. package/package.json +2 -1
  61. package/src/agents/bub.ts +13 -1
  62. package/src/agents/claude-code.test.ts +43 -1
  63. package/src/agents/claude-code.ts +32 -14
  64. package/src/agents/codex.test.ts +168 -1
  65. package/src/agents/codex.ts +51 -15
  66. package/src/agents/mcp.ts +31 -0
  67. package/src/agents/post-setup.ts +33 -0
  68. package/src/agents/types.ts +28 -7
  69. package/src/cli.ts +9 -11
  70. package/src/context/context.ts +11 -5
  71. package/src/define.ts +3 -0
  72. package/src/i18n/en.ts +5 -2
  73. package/src/i18n/zh-CN.ts +5 -1
  74. package/src/index.ts +1 -0
  75. package/src/report/built-in/index.tsx +8 -7
  76. package/src/report/built-in/standard.tsx +59 -0
  77. package/src/report/components.tsx +231 -17
  78. package/src/report/compute.ts +146 -21
  79. package/src/report/dual-render.test.tsx +139 -12
  80. package/src/report/index.ts +20 -1
  81. package/src/report/locale.ts +83 -1
  82. package/src/report/react/AttemptList.tsx +13 -1
  83. package/src/report/react/CopyFixPrompt.tsx +37 -0
  84. package/src/report/react/HeroCard.tsx +59 -0
  85. package/src/report/react/PoweredBy.tsx +20 -0
  86. package/src/report/react/ScopeWarnings.tsx +74 -0
  87. package/src/report/react/TraceWaterfall.tsx +78 -0
  88. package/src/report/react/enhance.js +14 -0
  89. package/src/report/react/index.tsx +11 -0
  90. package/src/report/react/styles.css +187 -7
  91. package/src/report/report.test.ts +2 -20
  92. package/src/report/report.ts +97 -62
  93. package/src/report/scope-warnings.ts +155 -0
  94. package/src/report/site-components.test.tsx +526 -0
  95. package/src/report/text/faces.ts +66 -0
  96. package/src/report/tree.ts +8 -1
  97. package/src/report/types.ts +51 -11
  98. package/src/report/web.ts +7 -40
  99. package/src/results/copy.ts +15 -78
  100. package/src/results/host-equivalence.test.ts +5 -1
  101. package/src/results/open.ts +5 -4
  102. package/src/results/publish.ts +4 -146
  103. package/src/results/results.test.ts +86 -9
  104. package/src/results/select.ts +78 -10
  105. package/src/results/types.ts +27 -7
  106. package/src/runner/attempt.ts +10 -9
  107. package/src/runner/discover.test.ts +9 -1
  108. package/src/runner/discover.ts +3 -3
  109. package/src/runner/fingerprint.ts +9 -4
  110. package/src/runner/ledger.test.ts +30 -1
  111. package/src/runner/ledger.ts +26 -4
  112. package/src/runner/run.ts +5 -1
  113. package/src/runner/sandbox-selection.test.ts +131 -0
  114. package/src/runner/sandbox-selection.ts +110 -0
  115. package/src/runner/types.ts +19 -2
  116. package/src/sandbox/types.ts +6 -0
  117. package/src/show/index.ts +17 -10
  118. package/src/show/render.ts +11 -11
  119. package/src/show/report-host.test.ts +32 -15
  120. package/src/show/report-host.ts +5 -4
  121. package/src/show/show.test.ts +140 -3
  122. package/src/view/app/App.test.tsx +78 -17
  123. package/src/view/app/App.tsx +17 -78
  124. package/src/view/app/components/CopyControls.tsx +4 -42
  125. package/src/view/app/i18n.ts +5 -227
  126. package/src/view/app/lib/rows.ts +3 -21
  127. package/src/view/app/shared.ts +1 -3
  128. package/src/view/app/types.ts +2 -2
  129. package/src/view/artifact-serving.test.ts +1 -1
  130. package/src/view/client-dist/app.css +1 -1
  131. package/src/view/client-dist/app.js +20 -20
  132. package/src/view/data.ts +2 -13
  133. package/src/view/index.ts +1 -12
  134. package/src/view/server.ts +0 -2
  135. package/src/view/shared/types.ts +11 -6
  136. package/src/view/site-parity.test.ts +1 -1
  137. package/src/view/site.ts +1 -1
  138. package/src/view/styles.css +6 -266
  139. package/src/view/view-report.test.ts +64 -27
  140. package/src/view/app/components/LazyArtifact.tsx +0 -51
  141. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  142. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  143. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -129,6 +129,44 @@ const en = {
129
129
  "scoreboard.subjectTitle": "{questions} evals, weighted {earned} of {possible}",
130
130
  "delta.pairHeader": "pair (A → B)",
131
131
  "delta.empty": "{experiments} experiments, 0 comparable pairs",
132
+ /** ScopeWarnings 聚合层的 chrome:汇总行、kind 徽标、组头与明细折叠标签;message 本体不经字典。 */
133
+ "warnings.summary.experiments.one": "{n} experiment flagged",
134
+ "warnings.summary.experiments.other": "{n} experiments flagged",
135
+ "warnings.group.unreadableSnapshot.one": "{n} snapshot skipped",
136
+ "warnings.group.unreadableSnapshot.other": "{n} snapshots skipped",
137
+ "warnings.details.one": "{n} warning",
138
+ "warnings.details.other": "{n} warnings",
139
+ "warnings.badge.partialCoverage": "coverage {covered}/{total}",
140
+ "warnings.badge.staleSnapshot": "{gap} behind",
141
+ "warnings.badge.unfinishedSnapshot": "unfinished",
142
+ "warnings.gap.second.one": "{n} second",
143
+ "warnings.gap.second.other": "{n} seconds",
144
+ "warnings.gap.minute.one": "{n} minute",
145
+ "warnings.gap.minute.other": "{n} minutes",
146
+ "warnings.gap.hour.one": "{n} hour",
147
+ "warnings.gap.hour.other": "{n} hours",
148
+ "warnings.gap.day.one": "{n} day",
149
+ "warnings.gap.day.other": "{n} days",
150
+ /** Hero / HeroCard 的运行 meta(hero.noRuns 是 latestStartedAt 为 null 时的内置文案)。 */
151
+ "hero.lastRun": "Last run {time}",
152
+ "hero.noRuns": "No runs yet",
153
+ /** web 面的合成来源标注(仅 snapshots > 1 时显示)。 */
154
+ "hero.composedRuns": "composed from {n} runs",
155
+ /** text 面的合成来源标注(show 页首 meta 行,仅 snapshots > 1 时显示)。 */
156
+ "hero.composedSnapshots": "composed from {n} snapshots",
157
+ /** CopyFixPrompt 的 web 面 chrome(prompt 本身面向 agent、固定英文,不经词典)。 */
158
+ "copyFixPrompt.summary.one": "Fix prompt · {n} failure",
159
+ "copyFixPrompt.summary.other": "Fix prompt · {n} failures",
160
+ "copyFixPrompt.copy": "Copy fix prompt",
161
+ /** TraceWaterfall 的 chrome。 */
162
+ "traceWaterfall.empty": "No attempts",
163
+ "traceWaterfall.noTrace": "no trace",
164
+ "traceWaterfall.spans.one": "{n} span",
165
+ "traceWaterfall.spans.other": "{n} spans",
166
+ "traceWaterfall.failedSpans.one": "{n} failed",
167
+ "traceWaterfall.failedSpans.other": "{n} failed",
168
+ /** AttemptList 的 web 面过滤框占位符(filter 渐进增强)。 */
169
+ "attemptList.filterPlaceholder": "Filter attempts…",
132
170
  "tabs.tab": "Tab",
133
171
  };
134
172
  const zhCN = {
@@ -216,6 +254,37 @@ const zhCN = {
216
254
  "scoreboard.subjectTitle": "{questions} 道题,加权得分 {earned}/{possible}",
217
255
  "delta.pairHeader": "对比 (A → B)",
218
256
  "delta.empty": "{experiments} 个实验、0 个可配对",
257
+ "warnings.summary.experiments.one": "{n} 个实验的数字带警告",
258
+ "warnings.summary.experiments.other": "{n} 个实验的数字带警告",
259
+ "warnings.group.unreadableSnapshot.one": "{n} 个快照被跳过",
260
+ "warnings.group.unreadableSnapshot.other": "{n} 个快照被跳过",
261
+ "warnings.details.one": "{n} 条原始警告",
262
+ "warnings.details.other": "{n} 条原始警告",
263
+ "warnings.badge.partialCoverage": "覆盖 {covered}/{total}",
264
+ "warnings.badge.staleSnapshot": "落后 {gap}",
265
+ "warnings.badge.unfinishedSnapshot": "未收尾",
266
+ "warnings.gap.second.one": "{n} 秒",
267
+ "warnings.gap.second.other": "{n} 秒",
268
+ "warnings.gap.minute.one": "{n} 分钟",
269
+ "warnings.gap.minute.other": "{n} 分钟",
270
+ "warnings.gap.hour.one": "{n} 小时",
271
+ "warnings.gap.hour.other": "{n} 小时",
272
+ "warnings.gap.day.one": "{n} 天",
273
+ "warnings.gap.day.other": "{n} 天",
274
+ "hero.lastRun": "最后运行 {time}",
275
+ "hero.noRuns": "暂无运行",
276
+ "hero.composedRuns": "由 {n} 次运行合成",
277
+ "hero.composedSnapshots": "由 {n} 份快照合成",
278
+ "copyFixPrompt.summary.one": "修复 prompt · {n} 个失败",
279
+ "copyFixPrompt.summary.other": "修复 prompt · {n} 个失败",
280
+ "copyFixPrompt.copy": "复制修复 prompt",
281
+ "traceWaterfall.empty": "没有 attempt",
282
+ "traceWaterfall.noTrace": "无 trace",
283
+ "traceWaterfall.spans.one": "{n} 个 span",
284
+ "traceWaterfall.spans.other": "{n} 个 span",
285
+ "traceWaterfall.failedSpans.one": "{n} 个失败",
286
+ "traceWaterfall.failedSpans.other": "{n} 个失败",
287
+ "attemptList.filterPlaceholder": "筛选 attempt…",
219
288
  "tabs.tab": "Tab",
220
289
  };
221
290
  const dictionaries = {
@@ -15,10 +15,12 @@ export declare function AttemptRow({ item, attemptHref, locale, }: {
15
15
  attemptHref?: (locator: AttemptLocator) => string;
16
16
  locale?: ReportLocale;
17
17
  }): ReactElement;
18
- export declare function AttemptList({ data, total, attemptHref, className, locale, }: {
18
+ export declare function AttemptList({ data, total, filter, attemptHref, className, locale, }: {
19
19
  data: readonly AttemptListItem[];
20
20
  /** data 被 slice 时的原始数量;如实显示还剩多少条没展示。 */
21
21
  total?: number;
22
+ /** web 面加过滤输入框(按 experiment、eval、agent、verdict 或摘要文本收窄行);渐进增强,不改变数据。 */
23
+ filter?: boolean;
22
24
  attemptHref?: (locator: AttemptLocator) => string;
23
25
  className?: string;
24
26
  locale?: ReportLocale;
@@ -18,9 +18,9 @@ export function failureSummaryText(item, locale) {
18
18
  /** 一条 Attempt 的比较卡片;完整 assertions 通过 locator 下钻,不在列表内展开。 */
19
19
  export function AttemptRow({ item, attemptHref = DEFAULT_ATTEMPT_HREF, locale = DEFAULT_REPORT_LOCALE, }) {
20
20
  const reason = failureSummaryText(item, locale);
21
- return (_jsxs("li", { className: cx("nre-attempt", `nre-attempt-${item.verdict}`), children: [_jsxs("div", { className: "nre-attempt-head", children: [_jsx(AttemptLocatorBadge, { item: item, attemptHref: attemptHref }), _jsx("span", { className: "nre-attempt-eval", children: item.evalId }), _jsx("span", { className: "nre-attempt-experiment", children: item.experimentId }), _jsx("span", { className: cx("nre-attempt-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }), _jsx("span", { className: "nre-attempt-duration", children: formatDurationMs(item.durationMs) }), item.costUSD !== null && _jsx("span", { className: "nre-attempt-cost", children: formatUSD(item.costUSD) })] }), reason && _jsx("p", { className: "nre-attempt-result", children: reason })] }));
21
+ return (_jsxs("li", { className: cx("nre-attempt", `nre-attempt-${item.verdict}`), "data-nre-verdict": item.verdict, children: [_jsxs("div", { className: "nre-attempt-head", children: [_jsx(AttemptLocatorBadge, { item: item, attemptHref: attemptHref }), _jsx("span", { className: "nre-attempt-eval", children: item.evalId }), _jsx("span", { className: "nre-attempt-experiment", children: item.experimentId }), _jsx("span", { className: cx("nre-attempt-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }), _jsx("span", { className: "nre-attempt-duration", children: formatDurationMs(item.durationMs) }), item.costUSD !== null && _jsx("span", { className: "nre-attempt-cost", children: formatUSD(item.costUSD) })] }), reason && _jsx("p", { className: "nre-attempt-result", children: reason })] }));
22
22
  }
23
- export function AttemptList({ data, total, attemptHref = DEFAULT_ATTEMPT_HREF, className, locale = DEFAULT_REPORT_LOCALE, }) {
23
+ export function AttemptList({ data, total, filter = false, attemptHref = DEFAULT_ATTEMPT_HREF, className, locale = DEFAULT_REPORT_LOCALE, }) {
24
24
  const remaining = (total ?? data.length) - data.length;
25
- return (_jsxs("section", { className: cx("nre", "nre-attempt-list", className), children: [data.length === 0 && _jsx("p", { className: "nre-attempt-list-empty", children: localeText(locale, "attemptList.empty") }), _jsx("ul", { className: "nre-attempts", children: data.map((item) => (_jsx(AttemptRow, { item: item, attemptHref: attemptHref, locale: locale }, item.locator))) }), remaining > 0 && (_jsx("p", { className: "nre-truncated", children: localeText(locale, "attemptList.truncated", { n: remaining }) }))] }));
25
+ return (_jsxs("section", { className: cx("nre", "nre-attempt-list", className), children: [filter && (_jsx("input", { className: "nre-filter", "data-nre-attempt-filter": "", type: "search", placeholder: localeText(locale, "attemptList.filterPlaceholder") })), data.length === 0 && _jsx("p", { className: "nre-attempt-list-empty", children: localeText(locale, "attemptList.empty") }), _jsx("ul", { className: "nre-attempts", children: data.map((item) => (_jsx(AttemptRow, { item: item, attemptHref: attemptHref, locale: locale }, item.locator))) }), remaining > 0 && (_jsx("p", { className: "nre-truncated", children: localeText(locale, "attemptList.truncated", { n: remaining }) }))] }));
26
26
  }
@@ -0,0 +1,12 @@
1
+ import type { ReactElement } from "react";
2
+ import type { CopyFixPromptData } from "../types.ts";
3
+ import { type ReportLocale } from "../locale.ts";
4
+ /**
5
+ * 批量修复 prompt(纯 web 渲染面):折叠块内完整 prompt 文本 + 复制按钮(增强层)。
6
+ * 嵌入自有 React 页面时配合 `copyFixPromptData()` 使用;failures 为 0 返回 null。
7
+ */
8
+ export declare function CopyFixPrompt({ data, className, locale, }: {
9
+ data: CopyFixPromptData;
10
+ className?: string;
11
+ locale?: ReportLocale;
12
+ }): ReactElement | null;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DEFAULT_REPORT_LOCALE, countText, localeText } from "../locale.js";
3
+ import { cx } from "./format.js";
4
+ /**
5
+ * 批量修复 prompt(纯 web 渲染面):折叠块内完整 prompt 文本 + 复制按钮(增强层)。
6
+ * 嵌入自有 React 页面时配合 `copyFixPromptData()` 使用;failures 为 0 返回 null。
7
+ */
8
+ export function CopyFixPrompt({ data, className, locale = DEFAULT_REPORT_LOCALE, }) {
9
+ if (data.failures === 0)
10
+ return null;
11
+ return (_jsxs("details", { className: cx("nre", "nre-copy-fix-prompt", className), children: [_jsx("summary", { className: "nre-copy-fix-prompt-summary", children: countText(locale, "copyFixPrompt.summary", data.failures) }), _jsx("button", { type: "button", className: "nre-copy-fix-prompt-copy", "data-nre-copy": data.prompt, children: localeText(locale, "copyFixPrompt.copy") }), _jsx("pre", { className: "nre-copy-fix-prompt-text", children: data.prompt })] }));
12
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReactElement } from "react";
2
+ import type { HeroData } from "../types.ts";
3
+ import { type LocalizedText, type ReportLocale } from "../locale.ts";
4
+ /**
5
+ * 站点标题区(纯 web 渲染面):`<h1>` 标题 + meta 行 + 品牌行。
6
+ * 嵌入自有 React 页面时配合 `heroData()` 使用。
7
+ */
8
+ export declare function HeroCard({ title, data, className, locale, }: {
9
+ title: LocalizedText;
10
+ data: HeroData;
11
+ className?: string;
12
+ locale?: ReportLocale;
13
+ }): ReactElement;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DEFAULT_REPORT_LOCALE, localeText, resolveLocalizedText } from "../locale.js";
3
+ import { cx } from "./format.js";
4
+ import { PoweredBy } from "./PoweredBy.js";
5
+ /** ISO 时间 → 按 locale 的「最后运行」显示(年月日 时:分);不可解析原样返回。 */
6
+ function formatLastRun(iso, locale) {
7
+ const date = new Date(iso);
8
+ if (Number.isNaN(date.valueOf()))
9
+ return iso;
10
+ try {
11
+ return new Intl.DateTimeFormat(locale, {
12
+ year: "numeric",
13
+ month: "short",
14
+ day: "numeric",
15
+ hour: "2-digit",
16
+ minute: "2-digit",
17
+ }).format(date);
18
+ }
19
+ catch {
20
+ return iso;
21
+ }
22
+ }
23
+ /**
24
+ * 站点标题区(纯 web 渲染面):`<h1>` 标题 + meta 行 + 品牌行。
25
+ * 嵌入自有 React 页面时配合 `heroData()` 使用。
26
+ */
27
+ export function HeroCard({ title, data, className, locale = DEFAULT_REPORT_LOCALE, }) {
28
+ const meta = data.latestStartedAt === null
29
+ ? localeText(locale, "hero.noRuns")
30
+ : [
31
+ localeText(locale, "hero.lastRun", { time: formatLastRun(data.latestStartedAt, locale) }),
32
+ ...(data.snapshots > 1 ? [localeText(locale, "hero.composedRuns", { n: data.snapshots })] : []),
33
+ ].join(" · ");
34
+ return (_jsxs("header", { className: cx("nre", "nre-hero", className), children: [_jsx("h1", { className: "nre-hero-title", children: resolveLocalizedText(title, locale) }), _jsx("p", { className: "nre-hero-meta", children: meta }), _jsx(PoweredBy, {})] }));
35
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from "react";
2
+ /** 品牌行的固定去处:niceeval 官网,utm 标记「来自报告的品牌行」。 */
3
+ export declare const POWERED_BY_HREF = "https://niceeval.com/?utm_source=report&utm_medium=powered-by";
4
+ /** 一行品牌色小字 `Powered by NiceEval`,链接官网;HeroCard 的品牌行与它同一渲染。 */
5
+ export declare function PoweredBy(): ReactElement;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** 品牌行的固定去处:niceeval 官网,utm 标记「来自报告的品牌行」。 */
3
+ export const POWERED_BY_HREF = "https://niceeval.com/?utm_source=report&utm_medium=powered-by";
4
+ /** 一行品牌色小字 `Powered by NiceEval`,链接官网;HeroCard 的品牌行与它同一渲染。 */
5
+ export function PoweredBy() {
6
+ return (_jsx("p", { className: "nre nre-powered-by", children: _jsx("a", { href: POWERED_BY_HREF, target: "_blank", rel: "noopener", children: "Powered by NiceEval" }) }));
7
+ }
@@ -0,0 +1,12 @@
1
+ import type { ReactElement } from "react";
2
+ import type { ScopeWarning } from "../types.ts";
3
+ import { type ReportLocale } from "../locale.ts";
4
+ /**
5
+ * 选择警告区(纯 web 渲染面):按动作聚合的警告组。嵌入自有 React 页面时传
6
+ * `data={scope.warnings}`;空集返回 null,不渲染空容器。
7
+ */
8
+ export declare function ScopeWarnings({ data, className, locale, }: {
9
+ data: readonly ScopeWarning[];
10
+ className?: string;
11
+ locale?: ReportLocale;
12
+ }): ReactElement | null;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DEFAULT_REPORT_LOCALE } from "../locale.js";
3
+ import { groupScopeWarnings, warningDetailsLabel } from "../scope-warnings.js";
4
+ import { cx } from "./format.js";
5
+ /** 命令的可复制块:无 JS 时点击全选(user-select: all),enhance.js 在场时点击复制。 */
6
+ function CommandBlock({ command }) {
7
+ return (_jsx("code", { className: "nre-warning-command", "data-nre-copy": command, children: command }));
8
+ }
9
+ /**
10
+ * 选择警告区(纯 web 渲染面):按动作聚合的警告组。嵌入自有 React 页面时传
11
+ * `data={scope.warnings}`;空集返回 null,不渲染空容器。
12
+ */
13
+ export function ScopeWarnings({ data, className, locale = DEFAULT_REPORT_LOCALE, }) {
14
+ if (data.length === 0)
15
+ return null;
16
+ const { summary, groups, detailsOpen } = groupScopeWarnings(data, locale);
17
+ return (_jsx("div", { className: cx("nre", "nre-scope-warnings", className), children: _jsxs("details", { className: "nre-warnings", children: [_jsx("summary", { className: "nre-warnings-summary", children: summary }), _jsx("ul", { className: "nre-warning-groups", children: groups.map((group, i) => (_jsxs("li", { className: "nre-warning-group", "data-category": group.category, children: [_jsxs("div", { className: "nre-warning-head", children: [_jsx("span", { className: "nre-warning-title", children: group.title }), group.badges.map((badge, j) => (_jsx("span", { className: "nre-warning-badge", "data-kind": badge.kind, children: badge.text }, j))), group.headCommand !== null && _jsx(CommandBlock, { command: group.headCommand })] }), _jsxs("details", { className: "nre-warning-details", open: detailsOpen || undefined, children: [_jsx("summary", { children: warningDetailsLabel(locale, group.warnings.length) }), _jsx("ul", { children: group.warnings.map((w, j) => (_jsxs("li", { className: "nre-warning", "data-kind": w.kind, children: [w.message, group.headCommand === null && "command" in w && w.command !== undefined && (_jsx(CommandBlock, { command: w.command }))] }, j))) })] })] }, i))) })] }) }));
18
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactElement } from "react";
2
+ import type { TraceWaterfallRow } from "../types.ts";
3
+ import type { AttemptLocator } from "../../results/locator.ts";
4
+ import { type ReportLocale } from "../locale.ts";
5
+ /**
6
+ * 执行时间瀑布(纯 web 渲染面):嵌入自有 React 页面时配合 `traceWaterfallData()` 使用。
7
+ * 只画被测 agent 的原始 span;runner 生命周期节点不在 data 里,组合视图归 attempt 详情。
8
+ */
9
+ export declare function TraceWaterfall({ data, attemptHref, className, locale, }: {
10
+ data: readonly TraceWaterfallRow[];
11
+ attemptHref?: (locator: AttemptLocator) => string;
12
+ className?: string;
13
+ locale?: ReportLocale;
14
+ }): ReactElement;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DEFAULT_REPORT_LOCALE, countText, localeText } from "../locale.js";
3
+ import { cx, formatDurationMs } from "./format.js";
4
+ const DEFAULT_ATTEMPT_HREF = (locator) => `#/attempt/${locator}`;
5
+ function pct(part, total) {
6
+ if (total <= 0)
7
+ return "0%";
8
+ return `${Math.min(100, Math.max(0, (part / total) * 100)).toFixed(2)}%`;
9
+ }
10
+ /**
11
+ * 执行时间瀑布(纯 web 渲染面):嵌入自有 React 页面时配合 `traceWaterfallData()` 使用。
12
+ * 只画被测 agent 的原始 span;runner 生命周期节点不在 data 里,组合视图归 attempt 详情。
13
+ */
14
+ export function TraceWaterfall({ data, attemptHref = DEFAULT_ATTEMPT_HREF, className, locale = DEFAULT_REPORT_LOCALE, }) {
15
+ return (_jsxs("section", { className: cx("nre", "nre-trace-waterfall", className), children: [data.length === 0 && _jsx("p", { className: "nre-waterfall-empty", children: localeText(locale, "traceWaterfall.empty") }), _jsx("ul", { className: "nre-waterfall", children: data.map((row) => {
16
+ const failedSpans = row.spans.filter((span) => span.failed).length;
17
+ return (_jsxs("li", { className: "nre-waterfall-row", children: [_jsxs("div", { className: "nre-waterfall-head", children: [_jsx("a", { className: "nre-locator", href: attemptHref(row.locator), children: row.locator }), _jsx("span", { className: "nre-waterfall-eval", children: row.evalId }), _jsx("span", { className: "nre-waterfall-experiment", children: row.experimentId }), _jsx("span", { className: "nre-waterfall-duration", children: row.durationMs === null ? localeText(locale, "traceWaterfall.noTrace") : formatDurationMs(row.durationMs) }), _jsx("span", { className: "nre-waterfall-count", children: countText(locale, "traceWaterfall.spans", row.spans.length) }), failedSpans > 0 && (_jsxs("span", { className: "nre-waterfall-failed", children: ["\u2717 ", countText(locale, "traceWaterfall.failedSpans", failedSpans)] }))] }), row.durationMs !== null && row.spans.length > 0 && (_jsx("div", { className: "nre-waterfall-track", children: row.spans.map((span, i) => (_jsx("span", { className: cx("nre-waterfall-span", `nre-span-${span.kind}`, span.failed && "nre-span-failed"), style: {
18
+ left: pct(span.startOffsetMs, row.durationMs),
19
+ width: `max(${pct(span.durationMs, row.durationMs)}, 0.5%)`,
20
+ }, title: `${span.name} · ${formatDurationMs(span.durationMs)}${span.failed ? " · ✗" : ""}` }, i))) }))] }, row.locator));
21
+ }) })] }));
22
+ }
@@ -10,7 +10,12 @@ export { MetricScatter } from "./MetricScatter.tsx";
10
10
  export { MetricLine } from "./MetricLine.tsx";
11
11
  export { DeltaTable } from "./DeltaTable.tsx";
12
12
  export { Scoreboard } from "./Scoreboard.tsx";
13
- export type { AttemptListItem, AttemptLocator, DeltaData, EvalListItem, ExperimentComparisonData, ExperimentComparisonGroupData, ExperimentListEvalRow, ExperimentListItem, LineData, MatrixData, MetricCell, MetricColumn, ScatterData, ScopeSummaryData, ScopeWarning, ScoreboardData, TableData, VerdictTally, } from "../types.ts";
13
+ export { HeroCard } from "./HeroCard.tsx";
14
+ export { PoweredBy } from "./PoweredBy.tsx";
15
+ export { ScopeWarnings } from "./ScopeWarnings.tsx";
16
+ export { CopyFixPrompt } from "./CopyFixPrompt.tsx";
17
+ export { TraceWaterfall } from "./TraceWaterfall.tsx";
18
+ export type { AttemptListItem, AttemptLocator, CopyFixPromptData, DeltaData, EvalListItem, ExperimentComparisonData, ExperimentComparisonGroupData, ExperimentListEvalRow, ExperimentListItem, HeroData, LineData, MatrixData, MetricCell, MetricColumn, ScatterData, ScopeSummaryData, ScopeWarning, ScoreboardData, TableData, TraceSpanSummary, TraceWaterfallRow, VerdictTally, } from "../types.ts";
14
19
  export { DEFAULT_REPORT_LOCALE, resolveLocalizedText, resolveMetricLabel } from "../locale.ts";
15
20
  export type { LocalizedText, ReportLocale } from "../locale.ts";
16
21
  export { NRE_PALETTE, colorClassForKey, colorHexForKey, colorIndexForKey, seriesClassForKey } from "./colors.ts";
@@ -19,6 +19,12 @@ export { MetricScatter } from "./MetricScatter.js";
19
19
  export { MetricLine } from "./MetricLine.js";
20
20
  export { DeltaTable } from "./DeltaTable.js";
21
21
  export { Scoreboard } from "./Scoreboard.js";
22
+ // 站点组件的纯 web 面(data 形态;Hero 是组合组件,只住 niceeval/report)
23
+ export { HeroCard } from "./HeroCard.js";
24
+ export { PoweredBy } from "./PoweredBy.js";
25
+ export { ScopeWarnings } from "./ScopeWarnings.js";
26
+ export { CopyFixPrompt } from "./CopyFixPrompt.js";
27
+ export { TraceWaterfall } from "./TraceWaterfall.js";
22
28
  // locale(官方组件 chrome 文案;LocalizedText 的按 locale 解析也用它)
23
29
  export { DEFAULT_REPORT_LOCALE, resolveLocalizedText, resolveMetricLabel } from "../locale.js";
24
30
  // 稳定配色(自定义组件想与官方组件同键同色时用;seriesClassForKey 配 CSS 的 --nre-series)
@@ -37,11 +37,11 @@ export type HeadTag = {
37
37
  children?: string;
38
38
  };
39
39
  export interface ReportShell {
40
- /** 标题:首页 hero 与浏览器标题。页头左端是恒定的 NiceEval 品牌字标,不由 title 覆盖;回退链 def.title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」。 */
40
+ /** 站点标题:浏览器标题、show 页索引标题行与 `ctx.report.title` 的取值源;`Hero` 组件缺省消费它。回退链 def.title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」。 */
41
41
  title?: LocalizedText;
42
42
  /** 页头右侧的外部链接,如 GitHub、文档、CI。 */
43
43
  links?: ReportLink[];
44
- /** 每页页脚的一段文字;省略时不渲染页脚(品牌行恒在 hero 下方,不占页脚)。 */
44
+ /** 每页页脚的一段文字;省略时不渲染页脚(品牌行归 PoweredBy 组件,不占页脚)。 */
45
45
  footer?: LocalizedText;
46
46
  /**
47
47
  * 注入每页 `<head>` 的结构化标签,在官方与外壳样式之后按声明顺序渲染。
@@ -63,13 +63,27 @@ export interface ReportPage {
63
63
  /** 这一页的报告树;ReportDefinition 不是 ReportNode,页装不进外壳。 */
64
64
  content: ReportNode;
65
65
  }
66
- /** content / pages 互斥由类型表达,不把非法状态留到运行期。 */
66
+ /** content / pages / extends 三选一由类型表达,不把非法状态留到运行期。 */
67
67
  export type ReportDef = ReportShell & ({
68
+ /** 单页缩写,等价于只含 id `report` 的页列表。 */
68
69
  content: ReportNode;
69
70
  pages?: never;
71
+ extends?: never;
70
72
  } | {
73
+ /** 非空页列表;导航按数组顺序显示。 */
71
74
  pages: NonEmptyArray<ReportPage>;
72
75
  content?: never;
76
+ extends?: never;
77
+ } | {
78
+ /**
79
+ * 在另一份报告上叠外壳:页列表取 base 的页列表;本对象声明的外壳字段整字段覆盖
80
+ * base 的同名字段,未声明的沿用 base——没有数组拼接、没有深合并。base 是任何
81
+ * `defineReport` 产物(内建视图或自己别的报告文件的具名导出);合并在
82
+ * `defineReport` 调用时折叠完成,产物仍是普通 ReportDefinition,可以再被 extends。
83
+ */
84
+ extends: ReportDefinition;
85
+ content?: never;
86
+ pages?: never;
73
87
  });
74
88
  /**
75
89
  * defineReport 的唯一产物:只作 --report 文件的默认导出,交给宿主装载。
@@ -136,7 +150,9 @@ export interface RenderReportTextOptions extends TextRenderOptions {
136
150
  }
137
151
  /**
138
152
  * text 宿主的装载语义:选页 → resolve(组合展开 + spec 取数,唯一的 await 边界)→ 树校验 →
139
- * 遍历渲染 text 面;Scope 有挑选警告时在报告顶部前置一块 "! <message>"。不需要 react-dom
153
+ * 遍历渲染 text 面。不需要 react-dom。宿主不在报告树外另设警告通道——挑选警告的呈现件是
154
+ * `ScopeWarnings` 组件,内建报告每页都放它,自定义报告放不放是作者义务
155
+ * (docs/feature/reports/architecture.md「Scope 是计算入口」)。
140
156
  */
141
157
  export declare function renderReportToText(definition: ReportDefinition, ctx: ReportHostContext, options?: RenderReportTextOptions): Promise<string>;
142
158
  /** 页索引标题行(show 多页索引 / view 导航共用的解析结果):按 locale 解析的标题字符串。 */
@@ -161,7 +177,7 @@ export interface RenderTreeTextOptions extends TextRenderOptions {
161
177
  }
162
178
  /**
163
179
  * 渲染一页报告树的 text 面(宿主逐页调用;页选择归宿主):
164
- * resolve(组合展开 + spec 取数)→ validate → render。Scope 有挑选警告时在页顶前置
165
- * "! <message>" 块——宿主是 warning 的唯一呈现者,组件数据不复制 warning
180
+ * resolve(组合展开 + spec 取数)→ validate → render。宿主不在报告树外另设警告通道,
181
+ * 挑选警告由页内的 `ScopeWarnings` 组件呈现(内建报告每页都放它)
166
182
  */
167
183
  export declare function renderReportTreeToText(tree: ReportNode, ctx: ReportTreeHostContext, options?: RenderTreeTextOptions): Promise<string>;
@@ -1,7 +1,9 @@
1
1
  // defineReport:唯一可被宿主装载的产物 —— 一层外壳(标题、外链、页脚、head 标签、脚本、样式)加
2
2
  // 非空页列表;单页与多页不是两种机制,页数只是列表长度(docs/feature/reports/library/shell.md)。
3
3
  // 入参有两级缩写,各有精确展开:树入参 ≡ { content: 树 } ≡ pages: [{ id: "report",
4
- // title: 内置页名, content: 树 }]。`content` `pages` 恰好声明一个,没有隐式默认。
4
+ // title: 内置页名, content: 树 }]。`content` / `pages` / `extends` 恰好声明一个,没有隐式默认;
5
+ // `extends` 在另一份报告上叠外壳——页归 base、外壳逐字段覆盖,合并在调用时折叠完成,
6
+ // 宿主装载看到的永远是已折叠的普通产物。
5
7
  //
6
8
  // renderReportToText 是 text 宿主(show)的装载入口;web 宿主(view)的
7
9
  // renderReportToStaticHtml 在 ./web.ts(那一侧才 import react-dom)。管线以页为单位执行:
@@ -13,7 +15,7 @@ const REPORT_DEFINITION = Symbol.for("niceeval.report.definition");
13
15
  export const DEFAULT_PAGE_ID = "report";
14
16
  const DEFAULT_PAGE_TITLE = { en: "Report", "zh-CN": "报告" };
15
17
  // ───────────────────────── 装载规范化与静态校验 ─────────────────────────
16
- const CONTENT_NEXT_STEP = 'To render the built-in report content, write content: <ExperimentComparison /> (imported from "niceeval/report").';
18
+ const EXTENDS_NEXT_STEP = 'To render the built-in report, write extends: standard (import { standard } from "niceeval/report/built-in").';
17
19
  function isReportNodeInput(value) {
18
20
  if (value === null || value === undefined || typeof value === "boolean")
19
21
  return true;
@@ -31,7 +33,8 @@ function assertNotDefinition(value, where) {
31
33
  value.kind === "report" &&
32
34
  value[REPORT_DEFINITION] === true) {
33
35
  throw new Error(`${where} received a defineReport(...) product, but a report definition is not a report node — the shell cannot nest. ` +
34
- "Pass the page's tree or component here, and export the defineReport product only as the file's default export.");
36
+ "Pass the page's tree or component here. To layer a shell over another report, write defineReport({ extends: base, … }); " +
37
+ "otherwise export the defineReport product as the file's default export.");
35
38
  }
36
39
  }
37
40
  function assertLocalizedText(value, where) {
@@ -155,26 +158,40 @@ export function defineReport(input) {
155
158
  ? { content: input }
156
159
  : input;
157
160
  if (typeof def !== "object" || def === null) {
158
- throw new Error("defineReport expects a report tree or a config object ({ title?, links?, footer?, head?, scripts?, styles?, content | pages }). " +
159
- CONTENT_NEXT_STEP);
161
+ throw new Error("defineReport expects a report tree or a config object ({ title?, links?, footer?, head?, scripts?, styles?, content | pages | extends }). " +
162
+ EXTENDS_NEXT_STEP);
160
163
  }
161
164
  const hasContent = "content" in def && def.content !== undefined;
162
165
  const hasPages = "pages" in def && def.pages !== undefined;
163
- if (hasContent && hasPages) {
164
- throw new Error(`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}`);
166
+ const hasExtends = "extends" in def && def.extends !== undefined;
167
+ const declared = [hasContent && '"content"', hasPages && '"pages"', hasExtends && '"extends"'].filter((name) => typeof name === "string");
168
+ if (declared.length > 1) {
169
+ throw new Error(`defineReport got ${declared.join(" and ")} — declare exactly one of "content" (a single tree), "pages" (a multi-page report), or "extends" (another report plus this shell). ${EXTENDS_NEXT_STEP}`);
165
170
  }
166
- if (!hasContent && !hasPages) {
167
- throw new Error(`defineReport got neither "content" nor "pages" — declare exactly one; omission is not a meaningful value, the file must show what renders. ${CONTENT_NEXT_STEP}`);
171
+ if (declared.length === 0) {
172
+ throw new Error(`defineReport got none of "content", "pages" or "extends" — declare exactly one; omission is not a meaningful value, the file must show what renders. ${EXTENDS_NEXT_STEP}`);
168
173
  }
174
+ // extends:报告级复用的唯一位置。页归 base,本对象只贡献外壳;base 已经过 defineReport
175
+ // 校验,页不重验。
176
+ let base;
169
177
  let pages;
170
- if (hasContent) {
178
+ if (hasExtends) {
179
+ const candidate = def.extends;
180
+ if (!isReportDefinition(candidate)) {
181
+ throw new Error('defineReport "extends" must be a defineReport(...) product — the base report whose pages this report inherits. ' +
182
+ EXTENDS_NEXT_STEP);
183
+ }
184
+ base = candidate;
185
+ pages = base.pages;
186
+ }
187
+ else if (hasContent) {
171
188
  assertNotDefinition(def.content, 'defineReport "content"');
172
189
  pages = [{ id: DEFAULT_PAGE_ID, title: DEFAULT_PAGE_TITLE, content: def.content }];
173
190
  }
174
191
  else {
175
192
  const raw = def.pages;
176
193
  if (!Array.isArray(raw) || raw.length === 0) {
177
- throw new Error(`defineReport "pages" must be a non-empty array of { id, title, content }. ${CONTENT_NEXT_STEP}`);
194
+ throw new Error(`defineReport "pages" must be a non-empty array of { id, title, content }. ${EXTENDS_NEXT_STEP}`);
178
195
  }
179
196
  const seen = new Set();
180
197
  for (const page of raw) {
@@ -194,34 +211,43 @@ export function defineReport(input) {
194
211
  assertLocalizedText(def.title, "defineReport title");
195
212
  if (def.footer !== undefined)
196
213
  assertLocalizedText(def.footer, "defineReport footer");
197
- const links = def.links ?? [];
198
- if (!Array.isArray(links))
199
- throw new Error("defineReport links must be an array of { label, href }.");
200
- for (const link of links) {
201
- assertLocalizedText(link?.label, "defineReport link label");
202
- if (typeof link?.href !== "string" || link.href.length === 0) {
203
- throw new Error("defineReport link href must be a non-empty string URL.");
204
- }
205
- // icon 唯一合法形状是 { svg: string }(无类型 JS 传组件 / ReactNode / 裸字符串都在装载期拒绝):
206
- // 外壳声明经序列化边界进前端,ReactNode 过不去,可序列化是外壳契约的一部分。
207
- const icon = link.icon;
208
- if (icon !== undefined) {
209
- const svg = icon?.svg;
210
- if (typeof icon !== "object" || icon === null || typeof svg !== "string" || svg.length === 0) {
211
- throw new Error('defineReport link "icon" must be { svg: string } — an inline SVG string rendered before the label. ' +
212
- "Components and React nodes are not accepted: the shell declaration crosses a serialization boundary. " +
213
- 'Write e.g. icon: { svg: "<svg …>…</svg>" }.');
214
+ // 外壳合并:声明即整字段覆盖,未声明沿用 base(base 的字段已规范化,不重验)。
215
+ let links;
216
+ if (def.links !== undefined) {
217
+ if (!Array.isArray(def.links))
218
+ throw new Error("defineReport links must be an array of { label, href }.");
219
+ for (const link of def.links) {
220
+ assertLocalizedText(link?.label, "defineReport link label");
221
+ if (typeof link?.href !== "string" || link.href.length === 0) {
222
+ throw new Error("defineReport link href must be a non-empty string URL.");
223
+ }
224
+ // icon 唯一合法形状是 { svg: string }(无类型 JS 传组件 / ReactNode / 裸字符串都在装载期拒绝):
225
+ // 外壳声明经序列化边界进前端,ReactNode 过不去,可序列化是外壳契约的一部分。
226
+ const icon = link.icon;
227
+ if (icon !== undefined) {
228
+ const svg = icon?.svg;
229
+ if (typeof icon !== "object" || icon === null || typeof svg !== "string" || svg.length === 0) {
230
+ throw new Error('defineReport link "icon" must be { svg: string } — an inline SVG string rendered before the label. ' +
231
+ "Components and React nodes are not accepted: the shell declaration crosses a serialization boundary. " +
232
+ 'Write e.g. icon: { svg: "<svg …>…</svg>" }.');
233
+ }
214
234
  }
215
235
  }
236
+ links = def.links;
216
237
  }
238
+ else {
239
+ links = base?.links ?? [];
240
+ }
241
+ const title = def.title !== undefined ? def.title : base?.title;
242
+ const footer = def.footer !== undefined ? def.footer : base?.footer;
217
243
  const definition = {
218
244
  kind: "report",
219
- ...(def.title !== undefined ? { title: def.title } : {}),
245
+ ...(title !== undefined ? { title } : {}),
220
246
  links: [...links],
221
- ...(def.footer !== undefined ? { footer: def.footer } : {}),
222
- head: assertHeadTags(def.head),
223
- scripts: assertAssets(def.scripts, "scripts"),
224
- styles: assertAssets(def.styles, "styles"),
247
+ ...(footer !== undefined ? { footer } : {}),
248
+ head: def.head !== undefined ? assertHeadTags(def.head) : [...(base?.head ?? [])],
249
+ scripts: def.scripts !== undefined ? assertAssets(def.scripts, "scripts") : [...(base?.scripts ?? [])],
250
+ styles: def.styles !== undefined ? assertAssets(def.styles, "styles") : [...(base?.styles ?? [])],
225
251
  pages: pages,
226
252
  };
227
253
  Object.defineProperty(definition, REPORT_DEFINITION, { value: true });
@@ -283,17 +309,11 @@ export function pickReportPage(definition, pageId) {
283
309
  }
284
310
  return page;
285
311
  }
286
- /**
287
- * 挑选警告的 text 形态:每条渲染好的 message 前缀 "! ",一行一条。宿主级前置块——
288
- * 宿主是 warning 的唯一呈现者,组件数据不复制 warning;裸跑 / --report 都在报告顶上
289
- * 如实报残缺,不静默(docs/feature/reports/architecture.md「Scope 是计算入口」)。
290
- */
291
- function renderScopeWarningsText(scope, _locale) {
292
- return scope.warnings.map((w) => `! ${w.message}`).join("\n");
293
- }
294
312
  /**
295
313
  * text 宿主的装载语义:选页 → resolve(组合展开 + spec 取数,唯一的 await 边界)→ 树校验 →
296
- * 遍历渲染 text 面;Scope 有挑选警告时在报告顶部前置一块 "! <message>"。不需要 react-dom
314
+ * 遍历渲染 text 面。不需要 react-dom。宿主不在报告树外另设警告通道——挑选警告的呈现件是
315
+ * `ScopeWarnings` 组件,内建报告每页都放它,自定义报告放不放是作者义务
316
+ * (docs/feature/reports/architecture.md「Scope 是计算入口」)。
297
317
  */
298
318
  export async function renderReportToText(definition, ctx, options) {
299
319
  const page = pickReportPage(definition, options?.pageId);
@@ -305,11 +325,7 @@ export async function renderReportToText(definition, ctx, options) {
305
325
  memo: new ResolveMemo(),
306
326
  });
307
327
  validateReportTree(resolved);
308
- const textCtx = createTextContext(options);
309
- const body = renderNodeToText(resolved, textCtx);
310
- return ctx.scope.warnings.length > 0
311
- ? [renderScopeWarningsText(ctx.scope, textCtx.locale), body].join("\n\n")
312
- : body;
328
+ return renderNodeToText(resolved, createTextContext(options));
313
329
  }
314
330
  /** 页索引标题行(show 多页索引 / view 导航共用的解析结果):按 locale 解析的标题字符串。 */
315
331
  export function reportTitleText(definition, scope, locale) {
@@ -333,8 +349,8 @@ function experimentCommandFor(ctx) {
333
349
  }
334
350
  /**
335
351
  * 渲染一页报告树的 text 面(宿主逐页调用;页选择归宿主):
336
- * resolve(组合展开 + spec 取数)→ validate → render。Scope 有挑选警告时在页顶前置
337
- * "! <message>" 块——宿主是 warning 的唯一呈现者,组件数据不复制 warning
352
+ * resolve(组合展开 + spec 取数)→ validate → render。宿主不在报告树外另设警告通道,
353
+ * 挑选警告由页内的 `ScopeWarnings` 组件呈现(内建报告每页都放它)
338
354
  */
339
355
  export async function renderReportTreeToText(tree, ctx, options) {
340
356
  const resolved = await resolveReportTree(tree, {
@@ -350,8 +366,5 @@ export async function renderReportTreeToText(tree, ctx, options) {
350
366
  ? { experimentCommand: experimentCommandFor(options.commandContext) }
351
367
  : {}),
352
368
  });
353
- const body = renderNodeToText(resolved, textCtx);
354
- return ctx.scope.warnings.length > 0
355
- ? [renderScopeWarningsText(ctx.scope, textCtx.locale), body].join("\n\n")
356
- : body;
369
+ return renderNodeToText(resolved, textCtx);
357
370
  }
@@ -0,0 +1,28 @@
1
+ import type { ScopeWarning } from "../results/types.ts";
2
+ import { type ReportLocale } from "./locale.ts";
3
+ /** kind 表登记的类别:integrity(选中集合的分母可能不对)组排在 freshness(可能过期)之前。 */
4
+ export type WarningCategory = "integrity" | "freshness";
5
+ export interface ScopeWarningGroup {
6
+ category: WarningCategory;
7
+ /** 实验组为 experimentId;kind 组为登记的组头文案(含条数);未登记 kind 用 kind 原文。 */
8
+ title: string;
9
+ /** 每条警告一枚、与 warnings 同序;未登记徽标模板的成员不出徽标。 */
10
+ badges: readonly {
11
+ kind: string;
12
+ text: string;
13
+ }[];
14
+ /** 组内命令去重后恰一条时归组头(复制即推进整组);多条或零条为 null,命令随明细逐条走。 */
15
+ headCommand: string | null;
16
+ /** 原始条目(明细层,message 单源)。 */
17
+ warnings: readonly ScopeWarning[];
18
+ }
19
+ export interface GroupedScopeWarnings {
20
+ /** 分类计数汇总行,任何组数下都产出;web 面用作外层折叠块的 <summary>,text 面只在多组时打印。 */
21
+ summary: string;
22
+ groups: readonly ScopeWarningGroup[];
23
+ /** 警告总条数 ≤ 3 时组级明细默认展开(web 面第二层 <details> 的 open;阈值是行为契约,无开关)。 */
24
+ detailsOpen: boolean;
25
+ }
26
+ /** 明细折叠块的标签(「N 条原始警告」)。 */
27
+ export declare function warningDetailsLabel(locale: ReportLocale, n: number): string;
28
+ export declare function groupScopeWarnings(input: readonly ScopeWarning[], locale: ReportLocale): GroupedScopeWarnings;