niceeval 0.6.2 → 0.7.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 (216) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/main.tsx +13 -8
  179. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  180. package/src/view/app/types.ts +4 -1
  181. package/src/view/client-dist/app.css +1 -1
  182. package/src/view/client-dist/app.js +14 -14
  183. package/src/view/data.test.ts +9 -3
  184. package/src/view/data.ts +145 -49
  185. package/src/view/index.ts +48 -44
  186. package/src/view/server.ts +35 -15
  187. package/src/view/shared/types.ts +34 -5
  188. package/src/view/styles.css +224 -0
  189. package/src/view/view-report.test.ts +161 -57
  190. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  191. package/dist/report/built-ins/experiment-comparison.js +0 -119
  192. package/dist/report/built-ins/index.d.ts +0 -2
  193. package/dist/report/built-ins/index.js +0 -2
  194. package/dist/report/react/GroupSummary.d.ts +0 -8
  195. package/dist/report/react/GroupSummary.js +0 -8
  196. package/dist/report/react/RunOverview.d.ts +0 -8
  197. package/dist/report/react/RunOverview.js +0 -12
  198. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  199. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  200. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  201. package/docs-site/zh/example/showcase.mdx +0 -39
  202. package/src/report/built-in-user-parity.test.tsx +0 -597
  203. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  204. package/src/report/built-ins/index.ts +0 -7
  205. package/src/report/react/GroupSummary.tsx +0 -66
  206. package/src/report/react/RunOverview.tsx +0 -109
  207. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  208. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  209. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  213. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  214. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
@@ -62,6 +62,10 @@ a { color: inherit; }
62
62
  display: flex;
63
63
  align-items: center;
64
64
  justify-content: space-between;
65
+ /* space-between 只在有富余空间时才撑开相邻项;.shell-links 的 margin-left:auto 会把富余
66
+ 空间全部吸收到它左侧,导致品牌与导航贴脸、shell-links 与语言切换贴脸。显式 gap 保证
67
+ 每一对相邻项(品牌—导航、shell-links—语言切换)恒有最小间距,不依赖富余空间。 */
68
+ gap: 28px;
65
69
  padding: 0 clamp(20px, 5vw, 80px);
66
70
  background: color-mix(in oklch, var(--bg), transparent 4%);
67
71
  position: sticky;
@@ -959,9 +963,229 @@ a.brand:hover { opacity: 0.75; }
959
963
  .code-orphans { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
960
964
  .code-orphans-head { font-size: 11px; color: var(--muted); margin-bottom: 8px; }
961
965
 
966
+ /* ── Attempt 详情(契约:docs/feature/reports/view.md「Attempt 详情」) ── */
967
+ .modal-sect {
968
+ font-size: 11px;
969
+ font-weight: 660;
970
+ letter-spacing: 0.06em;
971
+ text-transform: uppercase;
972
+ color: var(--muted);
973
+ margin-bottom: 6px;
974
+ }
975
+ /* 断言区:仅源码不可用时出现的 fallback 呈现面。 */
976
+ .attempt-asserts { margin-bottom: 14px; }
977
+ .attempt-asserts-open {
978
+ margin-top: 6px;
979
+ border: 1px solid var(--line);
980
+ border-radius: 10px;
981
+ background: var(--panel);
982
+ overflow: hidden;
983
+ }
984
+ /* 复用 CodeView 的 .assert-detail 排版,但脱离代码行上下文:去掉行内缩进与左缘。 */
985
+ .attempt-asserts .assert-detail { padding: 10px 12px; border-left: 0; background: transparent; }
986
+ /* 长 expected/received(整文件 dump)一律有界滚动:断言明细无论住在失败索引 fallback、
987
+ 源码行展开还是 NoSourceBody,都不把页面其余区块挤出视口。 */
988
+ .assert-reason { max-height: 180px; overflow-y: auto; }
989
+ /* 失败索引行:summary 一行是标题(徽章/名字/matcher/锚),明细收在行内一次展开。 */
990
+ .fail-row { border-bottom: 1px solid var(--line); }
991
+ .fail-row:last-child { border-bottom: 0; }
992
+ .fail-sum {
993
+ display: flex;
994
+ align-items: baseline;
995
+ flex-wrap: wrap;
996
+ gap: 4px 8px;
997
+ padding: 8px 12px;
998
+ cursor: pointer;
999
+ user-select: none;
1000
+ list-style: none;
1001
+ min-width: 0;
1002
+ }
1003
+ .fail-sum::-webkit-details-marker { display: none; }
1004
+ .fail-sum::before {
1005
+ content: "";
1006
+ width: 0;
1007
+ height: 0;
1008
+ border-left: 4px solid var(--soft);
1009
+ border-top: 3.5px solid transparent;
1010
+ border-bottom: 3.5px solid transparent;
1011
+ flex-shrink: 0;
1012
+ align-self: center;
1013
+ transition: transform 0.12s;
1014
+ }
1015
+ .fail-row[open] > .fail-sum::before { transform: rotate(90deg); }
1016
+ .fail-sum:hover { background: color-mix(in oklch, var(--panel), var(--line) 30%); }
1017
+ .fail-name {
1018
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1019
+ font-size: 12px;
1020
+ color: var(--text);
1021
+ overflow-wrap: anywhere;
1022
+ }
1023
+ .fail-matcher {
1024
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1025
+ font-size: 11.5px;
1026
+ color: var(--muted);
1027
+ overflow-wrap: anywhere;
1028
+ }
1029
+ .fail-body {
1030
+ display: flex;
1031
+ flex-direction: column;
1032
+ gap: 6px;
1033
+ padding: 2px 12px 10px 24px;
1034
+ }
1035
+ .assert-loc {
1036
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1037
+ font-size: 11px;
1038
+ color: var(--soft);
1039
+ margin-left: 8px;
1040
+ white-space: nowrap;
1041
+ }
1042
+ .asserts-passed { margin-top: 8px; }
1043
+ .asserts-passed > summary,
1044
+ .asserts-passed-group > summary {
1045
+ cursor: pointer;
1046
+ user-select: none;
1047
+ font-size: 12px;
1048
+ color: var(--muted);
1049
+ }
1050
+ .asserts-passed > summary:hover,
1051
+ .asserts-passed-group > summary:hover { color: var(--text); }
1052
+ .asserts-passed-group { margin: 6px 0 0 14px; }
1053
+ .asserts-passed-group > .assert-detail { margin-top: 4px; }
1054
+
1055
+ /* ── Attempt 详情:统一时间树(整区默认收合;展开后主链分解条 + 默认收合的 children) ── */
1056
+ .attempt-timing { margin: 14px 0; }
1057
+ summary.timing-sum {
1058
+ display: flex;
1059
+ align-items: center;
1060
+ gap: 8px;
1061
+ cursor: pointer;
1062
+ user-select: none;
1063
+ list-style: none;
1064
+ }
1065
+ summary.timing-sum::-webkit-details-marker { display: none; }
1066
+ summary.timing-sum::before {
1067
+ content: "";
1068
+ width: 0;
1069
+ height: 0;
1070
+ border-left: 4px solid var(--soft);
1071
+ border-top: 3.5px solid transparent;
1072
+ border-bottom: 3.5px solid transparent;
1073
+ flex-shrink: 0;
1074
+ transition: transform 0.12s;
1075
+ }
1076
+ details[open] > summary.timing-sum::before { transform: rotate(90deg); }
1077
+ summary.timing-sum:hover .modal-sect { color: var(--text); }
1078
+ .timing-sum .modal-sect { margin-bottom: 0; }
1079
+ .phase-bar {
1080
+ display: flex;
1081
+ height: 8px;
1082
+ border-radius: 4px;
1083
+ overflow: hidden;
1084
+ gap: 1px;
1085
+ margin: 7px 0 8px;
1086
+ }
1087
+ .phase-seg { min-width: 2px; flex-basis: 0; }
1088
+ .phase-seg-1 { background: var(--series-1); }
1089
+ .phase-seg-2 { background: var(--series-2); }
1090
+ .phase-seg-3 { background: var(--series-3); }
1091
+ .phase-seg-4 { background: var(--series-4); }
1092
+ .phase-seg-5 { background: var(--series-5); }
1093
+ .phase-seg-6 { background: var(--series-6); }
1094
+ .phase-seg-bad { background: var(--bad); }
1095
+ .phase-list,
1096
+ .timing-kids { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
1097
+ .timing-kids { padding-left: 18px; margin-top: 2px; }
1098
+ .phase-row,
1099
+ .tnode-row { display: flex; align-items: center; gap: 7px; font-size: 12px; line-height: 1.7; min-width: 0; }
1100
+ summary.phase-row,
1101
+ summary.tnode-row { cursor: pointer; user-select: none; list-style: none; }
1102
+ summary.phase-row::-webkit-details-marker,
1103
+ summary.tnode-row::-webkit-details-marker { display: none; }
1104
+ summary.phase-row::before,
1105
+ summary.tnode-row::before {
1106
+ content: "";
1107
+ width: 0;
1108
+ height: 0;
1109
+ border-left: 4px solid var(--soft);
1110
+ border-top: 3.5px solid transparent;
1111
+ border-bottom: 3.5px solid transparent;
1112
+ flex-shrink: 0;
1113
+ transition: transform 0.12s;
1114
+ }
1115
+ details[open] > summary.phase-row::before,
1116
+ details[open] > summary.tnode-row::before { transform: rotate(90deg); }
1117
+ /* 无 children 的行:占位对齐有箭头的行。 */
1118
+ li.phase-row::before,
1119
+ li.tnode-row::before { content: ""; width: 4px; flex-shrink: 0; }
1120
+ .phase-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
1121
+ .phase-dot-none { background: transparent; }
1122
+ /* 名称列定宽:各行的耗时起始对齐成一列(最长 phase 名 telemetry.configure 也在宽度内)。 */
1123
+ .phase-name { color: var(--text); white-space: nowrap; width: 168px; flex-shrink: 0; }
1124
+ .phase-dur { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
1125
+ .phase-fail { color: var(--bad); font-weight: 650; }
1126
+ .phase-kids {
1127
+ font-size: 10px;
1128
+ color: var(--soft);
1129
+ border: 1px solid var(--line);
1130
+ border-radius: 999px;
1131
+ padding: 0 6px;
1132
+ line-height: 1.5;
1133
+ }
1134
+ .tnode-label {
1135
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1136
+ font-size: 11.5px;
1137
+ color: var(--muted);
1138
+ overflow: hidden;
1139
+ text-overflow: ellipsis;
1140
+ white-space: nowrap;
1141
+ min-width: 0;
1142
+ }
1143
+ .tnode-meta { font-size: 11px; color: var(--soft); padding: 3px 0 3px 18px; }
1144
+ .tnode-trace { margin: 4px 0 6px 18px; }
1145
+ .phase-closing { margin-top: 8px; }
1146
+ .phase-closing-head { font-size: 11px; color: var(--soft); margin-bottom: 2px; }
1147
+
1148
+ /* usage / sandbox 元信息 chips。 */
1149
+ .meta-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
1150
+ .meta-chip {
1151
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1152
+ font-size: 11px;
1153
+ color: var(--muted);
1154
+ border: 1px solid var(--line);
1155
+ border-radius: 6px;
1156
+ padding: 3px 9px;
1157
+ background: var(--panel);
1158
+ }
1159
+ .meta-chip b { color: var(--text); font-weight: 640; }
1160
+
962
1161
  /* lightweight TS syntax tokens */
963
1162
  .tok-comment { color: var(--soft); font-style: italic; }
964
1163
  .tok-str { color: light-dark(#0a7d3c, #7ee787); }
965
1164
  .tok-kw { color: light-dark(#9333ea, #c792ea); }
966
1165
  .tok-num { color: light-dark(#b25000, #f0a36b); }
967
1166
  .tok-fn { color: light-dark(#0b62d6, #82aaff); }
1167
+
1168
+ /* ── 外壳:导航右侧外部链接与页脚(docs/feature/reports/library/shell.md) ── */
1169
+ .shell-links { display: flex; gap: 8px; align-items: center; margin-left: auto; }
1170
+ /* 链接自带 padding 撑大可点击区域(而非仅靠文字宽度);.shell-links 的 gap 相应收窄,
1171
+ 两条链接之间的视觉间距(padding + gap + padding)与改前基本持平。 */
1172
+ .shell-links a { color: var(--soft); text-decoration: none; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; padding: 6px 4px; }
1173
+ .shell-links a:hover { color: var(--text); }
1174
+ /* ReportLink.icon:内联 SVG 字标,渲染在 label 前(shell.md「字段穷尽」)。 */
1175
+ .shell-link-icon { display: inline-flex; align-items: center; }
1176
+ .shell-link-icon svg { width: 14px; height: 14px; display: block; fill: currentColor; }
1177
+ .site-footer {
1178
+ display: flex;
1179
+ gap: 12px;
1180
+ align-items: baseline;
1181
+ justify-content: center;
1182
+ flex-wrap: wrap;
1183
+ padding: 24px 24px 32px;
1184
+ color: var(--soft);
1185
+ font-size: 12px;
1186
+ }
1187
+ /* 品牌行:hero 之下、品牌蓝,恒带官网链接(shell.md「行为约束」)。 */
1188
+ .hero .powered-by { display: block; margin-top: 16px; font-size: 12px; }
1189
+ .hero .powered-by a { color: var(--blue); text-decoration: none; }
1190
+ .hero .powered-by a:hover { text-decoration: underline; }
1191
+ .nre-page-error pre { white-space: pre-wrap; }
@@ -1,10 +1,11 @@
1
1
  // cases: docs/engineering/unit-tests/reports/cases.md
2
2
  // niceeval view 的报告槽与宿主组合语义(docs/feature/reports/architecture.md「Selection 是计算入口」
3
- // 与裁决记录 6;公开行为准绳 docs-site/zh/guides/viewing-results.mdx / custom-reports.mdx)。
3
+ // 与裁决记录 6;公开行为准绳 docs-site/zh/how-to/viewing-results.mdx / custom-reports.mdx)。
4
4
  // 覆盖:
5
- // - 组合语义与 show 对齐:位置前缀收窄报告槽 Selection、--experiment 过滤、匹配不到直说;
6
- // - 单文件模式共存:存在的文件路径 单文件模式,目录报错直说走 --run,其余按 eval 前缀;
7
- // - 报告槽恒在:裸跑填充 ExperimentComparison,--report 整槽替换;en / zh-CN 双语各渲染一遍;
5
+ // - 组合语义与 show 对齐:位置前缀收窄报告槽 Scope、--experiment 过滤、匹配不到直说;
6
+ // - 输入语义:位置参数只表示 eval id 前缀(不随文件系统状态改变),结果根走 --results,
7
+ // 单开一份快照走 --snapshot(文件不可读时失败);
8
+ // - 报告槽恒在:裸跑填充内建报告,--report 整槽替换;en / zh-CN 双语各渲染一遍;
8
9
  // 裸跑 ≡ --report <re-export ExperimentComparison 的文件>(等价性);
9
10
  // - --out 静态导出:index.html 含两个语言的报告块、官方样式与增强 runtime,报告块零 <script>;
10
11
  // - dev server 装载语义:报告文件变更 → 下次装载整页重算(mtime cache-busting)。
@@ -21,7 +22,8 @@ import { afterEach, describe, expect, it } from "vitest";
21
22
  // (see src/view/data.ts's comment) — a raw-src import would be a structurally-identical but
22
23
  // `instanceof`-incompatible class.
23
24
  import { ReportLoadError } from "../../dist/report/load.js";
24
- import { ViewInputError, loadViewScan } from "./data.ts";
25
+ import { ViewInputError, loadViewScan, type ViewScan } from "./data.ts";
26
+ import { renderHtml } from "./server.ts";
25
27
  import { buildView, resolveViewInput } from "./index.ts";
26
28
  import { runShow } from "../show/index.ts";
27
29
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
@@ -96,47 +98,62 @@ async function seedRoot(): Promise<string> {
96
98
 
97
99
  // ───────────────────────── 位置参数语义(单文件模式共存) ─────────────────────────
98
100
 
99
- describe("resolveViewInput · 位置参数语义", () => {
100
- it("存在的文件路径 → 单文件模式;其余位置参数按 eval id 前缀", async () => {
101
+ describe("resolveViewInput · 输入语义", () => {
102
+ it("位置参数只表示 eval id 前缀:文件与目录路径不改变含义", async () => {
101
103
  const root = await seedRoot();
102
104
  const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
103
- expect(resolveViewInput(root, [file])).toEqual({ input: file, patterns: [] });
105
+ // 恰好是存在文件/目录的位置参数也照常当前缀(后续按「无匹配」报错,不做模糊猜测)
106
+ expect(resolveViewInput(root, [file])).toEqual({ patterns: [file] });
107
+ expect(resolveViewInput(root, ["compare_bub"])).toEqual({ patterns: ["compare_bub"] });
104
108
  expect(resolveViewInput(root, ["weather", "fixtures/button"])).toEqual({
105
109
  patterns: ["weather", "fixtures/button"],
106
110
  });
107
111
  expect(resolveViewInput(root, [])).toEqual({ patterns: [] });
108
112
  });
109
113
 
110
- it("目录位置参数:报错直说走 --run(位置参数留给 eval id 前缀)", async () => {
114
+ it("--snapshot 单开一份快照文件;文件不可读时失败(与扫描模式的跳过相反)", async () => {
111
115
  const root = await seedRoot();
112
- expect(() => resolveViewInput(root, ["compare_bub"])).toThrow(ViewInputError);
113
- expect(() => resolveViewInput(root, ["compare_bub"])).toThrow(/--run/);
116
+ const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
117
+ expect(resolveViewInput(root, [], { snapshot: file })).toEqual({ input: file, patterns: [] });
118
+ expect(() => resolveViewInput(root, [], { snapshot: join(root, "nope.json") })).toThrow(ViewInputError);
119
+ expect(() => resolveViewInput(root, [], { snapshot: join(root, "nope.json") })).toThrow(/--snapshot/);
120
+ // 目录不是快照文件。
121
+ expect(() => resolveViewInput(root, [], { snapshot: join(root, "compare_bub") })).toThrow(/--snapshot/);
114
122
  });
115
123
 
116
- it("单文件模式不与其它位置参数或 --run 混用,歧义报错直说", async () => {
124
+ it("--results 指向不存在的目录:直说", async () => {
117
125
  const root = await seedRoot();
118
- const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
119
- expect(() => resolveViewInput(root, [file, "weather"])).toThrow(/exactly one path/);
120
- expect(() => resolveViewInput(root, [file], root)).toThrow(/--run/);
126
+ expect(() => resolveViewInput(root, [], { results: join(root, "nope") })).toThrow(/Results directory not found/);
121
127
  });
122
128
 
123
- it("--run 指向不存在的目录:直说", async () => {
129
+ it("--results 换结果根,位置参数仍是前缀", async () => {
124
130
  const root = await seedRoot();
125
- expect(() => resolveViewInput(root, [], join(root, "nope"))).toThrow(/Results directory not found/);
131
+ expect(resolveViewInput("/elsewhere", ["weather"], { results: root })).toEqual({ input: root, patterns: ["weather"] });
126
132
  });
127
133
 
128
- it("--run 换结果根,位置参数仍是前缀", async () => {
134
+ it("--results 与 --snapshot 互斥:报错直说", async () => {
129
135
  const root = await seedRoot();
130
- expect(resolveViewInput("/elsewhere", ["weather"], root)).toEqual({ input: root, patterns: ["weather"] });
136
+ const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
137
+ expect(() => resolveViewInput(root, [], { results: root, snapshot: file })).toThrow(/mutually exclusive/);
131
138
  });
132
139
  });
133
140
 
141
+
142
+ /** 单页报告(裸跑 / 树形态 --report)的报告槽 HTML:规范化后唯一页 id 恒为 `report`。 */
143
+ function slotHtml(scan: ViewScan): { en: string; "zh-CN": string } {
144
+ expect(scan.reportPages).toHaveLength(1);
145
+ expect(scan.reportPages[0]!.id).toBe("report");
146
+ return scan.reportPages[0]!.html as { en: string; "zh-CN": string };
147
+ }
148
+
134
149
  // ───────────────────────── 组合语义(与 show 对齐) ─────────────────────────
135
150
 
136
151
  describe("loadViewScan · 组合语义", () => {
137
152
  it("位置前缀收窄报告槽 Selection;证据室快照不收窄,深链恒可达", async () => {
138
153
  const root = await seedRoot();
139
- const { viewData, reportHtml } = await loadViewScan(root, { patterns: ["weather"] });
154
+ const scan = await loadViewScan(root, { patterns: ["weather"] });
155
+ const { viewData } = scan;
156
+ const reportHtml = slotHtml(scan);
140
157
  // 报告槽:两实验都只剩 weather/brooklyn 一题,范围外的失败不再出现。
141
158
  expect(reportHtml.en).toContain("compare/bub");
142
159
  expect(reportHtml.en).toContain("compare/codex");
@@ -148,7 +165,7 @@ describe("loadViewScan · 组合语义", () => {
148
165
 
149
166
  it("--experiment 过滤:报告槽 Selection 只留该实验", async () => {
150
167
  const root = await seedRoot();
151
- const { reportHtml } = await loadViewScan(root, { experiment: "compare/codex" });
168
+ const reportHtml = slotHtml(await loadViewScan(root, { experiment: "compare/codex" }));
152
169
  expect(reportHtml.en).toContain("compare/codex");
153
170
  expect(reportHtml.en).not.toContain("compare/bub");
154
171
  });
@@ -162,7 +179,7 @@ describe("loadViewScan · 组合语义", () => {
162
179
 
163
180
  it("全部缺省:报告槽使用现刻水位口径(与 show 相同的合成规则)", async () => {
164
181
  const root = await seedRoot();
165
- const { reportHtml } = await loadViewScan(root);
182
+ const reportHtml = slotHtml(await loadViewScan(root));
166
183
  // 默认报告的榜单含两个实验;官方组件的稳定类名在场。
167
184
  expect(reportHtml.en).toContain("compare/bub");
168
185
  expect(reportHtml.en).toContain("compare/codex");
@@ -175,17 +192,19 @@ describe("loadViewScan · 组合语义", () => {
175
192
  describe("loadViewScan · 默认报告槽(裸跑)", () => {
176
193
  it("裸跑产出的报告槽 HTML 与 --report <re-export ExperimentComparison 的文件> 完全一致(双语)", async () => {
177
194
  const root = await seedRoot();
178
- const bare = await loadViewScan(root);
179
- const viaReport = await loadViewScan(root, {
180
- report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
181
- });
182
- expect(bare.reportHtml.en).toBe(viaReport.reportHtml.en);
183
- expect(bare.reportHtml["zh-CN"]).toBe(viaReport.reportHtml["zh-CN"]);
195
+ const bare = slotHtml(await loadViewScan(root));
196
+ const viaReport = slotHtml(
197
+ await loadViewScan(root, {
198
+ report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
199
+ }),
200
+ );
201
+ expect(bare.en).toBe(viaReport.en);
202
+ expect(bare["zh-CN"]).toBe(viaReport["zh-CN"]);
184
203
  });
185
204
 
186
205
  it("报告槽双语渲染:同一棵树按 locale 渲染两遍,chrome 文案分语言、数据不分语言", async () => {
187
206
  const root = await seedRoot();
188
- const { reportHtml } = await loadViewScan(root);
207
+ const reportHtml = slotHtml(await loadViewScan(root));
189
208
  expect(reportHtml.en).toContain("End-to-end pass rate"); // ExperimentList 主行(en)
190
209
  expect(reportHtml["zh-CN"]).toContain("端到端成功率"); // ExperimentList 主行(zh-CN)
191
210
  for (const html of [reportHtml.en, reportHtml["zh-CN"]]) {
@@ -199,8 +218,9 @@ describe("loadViewScan · 默认报告槽(裸跑)", () => {
199
218
 
200
219
  it("失败清单与警告住在报告槽里:ExperimentList 列出失败,壳的 viewData 不携带统计产物", async () => {
201
220
  const root = await seedRoot();
202
- const { viewData, reportHtml } = await loadViewScan(root);
203
- expect(reportHtml.en).toContain("fixtures/button");
221
+ const scan = await loadViewScan(root);
222
+ const { viewData } = scan;
223
+ expect(slotHtml(scan).en).toContain("fixtures/button");
204
224
  // viewData 只有证据室数据:快照 + skipped + 壳元信息。
205
225
  expect(viewData).not.toHaveProperty("overview");
206
226
  expect(viewData).not.toHaveProperty("table");
@@ -214,14 +234,14 @@ describe("loadViewScan · --report 报告槽", () => {
214
234
  it("报告树渲染为静态 HTML:官方水位 + 自定义摆法 + <Style> 产物 + 证据室深链,零 <script>", async () => {
215
235
  const root = await seedRoot();
216
236
  const scan = await loadViewScan(root, { report: { path: EXAM_REPORT, cwd: root } });
217
- const html = scan.reportHtml.en;
237
+ const html = slotHtml(scan).en;
218
238
  expect(html).toContain("考试成绩单"); // 自定义 Section
219
239
  expect(html).toContain("nre-"); // 官方组件的稳定类名
220
240
  expect(html).toContain("<style>.exam-note { color: #4a7; }</style>"); // <Style> 随树带走
221
241
  expect(html).toMatch(/href="#\/attempt\/@[0-9a-z]+"/); // 失败案例深链进证据室(单段 locator 路由)
222
242
  expect(html).not.toContain("<script"); // 报告槽产物零客户端 JS,不 hydrate
223
243
  // 用户报告同样双语渲染两遍(壳按界面语言摆放)。
224
- expect(scan.reportHtml["zh-CN"]).toContain("考试成绩单");
244
+ expect(slotHtml(scan)["zh-CN"]).toContain("考试成绩单");
225
245
  // 证据室数据契约(__NICEEVAL_VIEW_DATA__)原样保留:快照、locator、skipped 不动。
226
246
  expect(scan.viewData.snapshots.length).toBeGreaterThan(0);
227
247
  expect(scan.viewData.snapshots.flatMap((s) => s.results).every((r) => r.locator)).toBe(true);
@@ -235,15 +255,16 @@ describe("loadViewScan · --report 报告槽", () => {
235
255
  report: { path: EXAM_REPORT, cwd: root },
236
256
  });
237
257
  // 范围外的失败(fixtures/button)不再出现在报告里;范围内的实验行都在。
238
- expect(scan.reportHtml.en).not.toContain("fixtures/button");
239
- expect(scan.reportHtml.en).toContain("compare/bub");
240
- expect(scan.reportHtml.en).toContain("compare/codex");
258
+ const html = slotHtml(scan);
259
+ expect(html.en).not.toContain("fixtures/button");
260
+ expect(html.en).toContain("compare/bub");
261
+ expect(html.en).toContain("compare/codex");
241
262
  });
242
263
 
243
264
  it("show --report 与 view --report 吃同一个报告文件,判定口径一致", async () => {
244
265
  const root = await seedRoot();
245
266
  let text = "";
246
- const code = await runShow(root, [], { run: root, report: EXAM_REPORT }, {
267
+ const code = await runShow(root, [], { results: root, report: EXAM_REPORT }, {
247
268
  out: (s) => (text += s),
248
269
  err: () => {},
249
270
  width: 120,
@@ -251,9 +272,10 @@ describe("loadViewScan · --report 报告槽", () => {
251
272
  expect(code).toBe(0);
252
273
  const scan = await loadViewScan(root, { report: { path: EXAM_REPORT, cwd: root } });
253
274
  // 同一棵树的两个面:同一份失败清单与同一个自定义 Section。
275
+ const html = slotHtml(scan);
254
276
  for (const needle of ["考试成绩单", "compare/bub", "compare/codex", "fixtures/button"]) {
255
277
  expect(text).toContain(needle);
256
- expect(scan.reportHtml.en).toContain(needle);
278
+ expect(html.en).toContain(needle);
257
279
  }
258
280
  });
259
281
 
@@ -270,6 +292,57 @@ describe("loadViewScan · --report 报告槽", () => {
270
292
  });
271
293
  });
272
294
 
295
+ // ───────────────────── 外壳:title 落点(hero / <title>)与 ReportLink.icon ─────────────────────
296
+
297
+ describe("loadViewScan · 外壳标题与 ReportLink.icon", () => {
298
+ /** 不经包入口也合法的最小外壳报告(与下方 reportSource 同一姿势):声明 title 与带 icon 的 link。 */
299
+ function shellReportSource(): string {
300
+ return [
301
+ 'const FACES = Symbol.for("niceeval.report.faces");',
302
+ 'const DEFINITION = Symbol.for("niceeval.report.definition");',
303
+ "const Block = (props) => Block[FACES].web(props);",
304
+ "Block[FACES] = {",
305
+ ' web: () => "SHELL_BODY",',
306
+ ' text: () => "SHELL_BODY",',
307
+ "};",
308
+ "const definition = {",
309
+ ' kind: "report",',
310
+ ' title: { en: "Memory Evals", "zh-CN": "记忆能力评测" },',
311
+ ' links: [{ label: "GitHub", href: "https://example.com", icon: { svg: "<svg data-mark></svg>" } }],',
312
+ " scripts: [],",
313
+ " styles: [],",
314
+ ' pages: [{ id: "report", title: "Report", content: { $$typeof: Symbol.for("react.transitional.element"), type: Block, props: {}, key: null } }],',
315
+ "};",
316
+ "Object.defineProperty(definition, DEFINITION, { value: true });",
317
+ "export default definition;",
318
+ "",
319
+ ].join("\n");
320
+ }
321
+
322
+ it("def.title 进 viewData.report.title,初始 <title> 同源烘进 HTML;link 的 icon svg 原样透传", async () => {
323
+ const root = await seedRoot();
324
+ const path = join(root, "shell-report.mjs");
325
+ await writeFile(path, shellReportSource(), "utf-8");
326
+ const scan = await loadViewScan(root, { report: { path, cwd: root } });
327
+ // 标题回退链第一级:def.title 原样(hero 与浏览器标题都吃它)。
328
+ expect(scan.viewData.report?.title).toEqual({ en: "Memory Evals", "zh-CN": "记忆能力评测" });
329
+ // ReportLink.icon 经 viewData 序列化边界原样透传(web 面在 label 前渲染,静态导出原样内联)。
330
+ expect(scan.viewData.report?.links).toEqual([
331
+ { label: "GitHub", href: "https://example.com", icon: { svg: "<svg data-mark></svg>" } },
332
+ ]);
333
+ const html = await renderHtml(scan);
334
+ expect(html).toContain("<title>Memory Evals</title>");
335
+ });
336
+
337
+ it("无 def.title 且快照无 name:标题落内置文案「Eval 运行结果 / Eval Results」,不再是产品名", async () => {
338
+ const root = await seedRoot(); // seedRoot 的快照都没有 name
339
+ const scan = await loadViewScan(root);
340
+ expect(scan.viewData.report?.title).toEqual({ en: "Eval Results", "zh-CN": "Eval 运行结果" });
341
+ const html = await renderHtml(scan);
342
+ expect(html).toContain("<title>Eval Results</title>");
343
+ });
344
+ });
345
+
273
346
  // ───────────────────────── dev server 装载语义:整页重算 ─────────────────────────
274
347
 
275
348
  describe("loadViewScan · 报告文件变更整页重算", () => {
@@ -277,20 +350,21 @@ describe("loadViewScan · 报告文件变更整页重算", () => {
277
350
  function reportSource(marker: string): string {
278
351
  return [
279
352
  'const FACES = Symbol.for("niceeval.report.faces");',
353
+ 'const DEFINITION = Symbol.for("niceeval.report.definition");',
280
354
  "const Block = (props) => Block[FACES].web(props);",
281
355
  "Block[FACES] = {",
282
356
  ` web: () => "${marker}",`,
283
357
  ` text: () => "${marker}",`,
284
358
  "};",
285
- "export default {",
286
- ' [Symbol.for("niceeval.report.definition")]: true,',
287
- " build: () => ({",
288
- ' $$typeof: Symbol.for("react.transitional.element"),',
289
- " type: Block,",
290
- " props: {},",
291
- " key: null,",
292
- " }),",
359
+ "const definition = {",
360
+ ' kind: "report",',
361
+ " links: [],",
362
+ " scripts: [],",
363
+ " styles: [],",
364
+ ' pages: [{ id: "report", title: "Report", content: { $$typeof: Symbol.for("react.transitional.element"), type: Block, props: {}, key: null } }],',
293
365
  "};",
366
+ "Object.defineProperty(definition, DEFINITION, { value: true });",
367
+ "export default definition;",
294
368
  "",
295
369
  ].join("\n");
296
370
  }
@@ -299,16 +373,16 @@ describe("loadViewScan · 报告文件变更整页重算", () => {
299
373
  const root = await seedRoot();
300
374
  const path = join(root, "report.mjs");
301
375
  await writeFile(path, reportSource("FIRST_RENDER"), "utf-8");
302
- const first = await loadViewScan(root, { report: { path, cwd: root } });
303
- expect(first.reportHtml.en).toContain("FIRST_RENDER");
376
+ const first = slotHtml(await loadViewScan(root, { report: { path, cwd: root } }));
377
+ expect(first.en).toContain("FIRST_RENDER");
304
378
 
305
379
  await writeFile(path, reportSource("SECOND_RENDER"), "utf-8");
306
380
  // mtime 精度兜底:显式把 mtime 拨到未来,确保与首次装载可区分。
307
381
  const future = new Date(Date.now() + 5000);
308
382
  await utimes(path, future, future);
309
- const second = await loadViewScan(root, { report: { path, cwd: root } });
310
- expect(second.reportHtml.en).toContain("SECOND_RENDER");
311
- expect(second.reportHtml.en).not.toContain("FIRST_RENDER");
383
+ const second = slotHtml(await loadViewScan(root, { report: { path, cwd: root } }));
384
+ expect(second.en).toContain("SECOND_RENDER");
385
+ expect(second.en).not.toContain("FIRST_RENDER");
312
386
  });
313
387
  });
314
388
 
@@ -328,12 +402,12 @@ describe("buildView · --out 与 --report", () => {
328
402
 
329
403
  const html = await readFile(join(out, "index.html"), "utf-8");
330
404
  // 双语两个 <template> 静态块都在,壳按界面语言摆放。
331
- expect(html).toContain('<template id="niceeval-report-en">');
332
- expect(html).toContain('<template id="niceeval-report-zh-CN">');
405
+ expect(html).toContain('<template id="niceeval-report-report-en">');
406
+ expect(html).toContain('<template id="niceeval-report-report-zh-CN">');
333
407
  expect(html).toContain("考试成绩单");
334
408
  expect(html).toContain("nre-"); // 官方组件样式(report/react/styles.css)随页注入
335
409
  // 报告块本体零 <script>:静态块起于 <template>,内部只有标记与 <style>。
336
- const block = html.split('<template id="niceeval-report-en">')[1]!.split("</template>")[0]!;
410
+ const block = html.split('<template id="niceeval-report-report-en">')[1]!.split("</template>")[0]!;
337
411
  expect(block).not.toContain("<script");
338
412
  // 证据室同站: artifact 按 /artifact/<base>/ 布局复制。
339
413
  expect(
@@ -341,17 +415,47 @@ describe("buildView · --out 与 --report", () => {
341
415
  ).toBe(true);
342
416
  });
343
417
 
418
+ it("--out 无档位:diff.json 有就带,o11y.json 永不复制", async () => {
419
+ const root = await seedRoot();
420
+ const artifactDir = join(root, "compare_codex", "2026-07-09T10-00-00-000Z", "weather", "brooklyn", "a0");
421
+ await mkdir(artifactDir, { recursive: true });
422
+ await writeFile(join(artifactDir, "events.json"), "[]", "utf-8");
423
+ await writeFile(join(artifactDir, "diff.json"), '{"windows":[]}', "utf-8");
424
+ await writeFile(join(artifactDir, "o11y.json"), "{}", "utf-8");
425
+
426
+ const out = join(root, "site");
427
+ await buildView({ input: root, out, allowSensitiveArtifacts: true });
428
+ const exported = join(out, "artifact", "compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a0");
429
+ expect(existsSync(join(exported, "diff.json"))).toBe(true);
430
+ expect(existsSync(join(exported, "events.json"))).toBe(true);
431
+ expect(existsSync(join(exported, "o11y.json"))).toBe(false);
432
+ });
433
+
434
+ it("--out 与位置参数 / --experiment 互斥:报错含 copySnapshots + filter 下一步", async () => {
435
+ const root = await seedRoot();
436
+ const out = join(root, "site");
437
+ for (const scan of [{ patterns: ["weather"] }, { experiment: "compare/bub" }]) {
438
+ const attempt = buildView({ input: root, out, allowSensitiveArtifacts: true, scan });
439
+ await expect(attempt).rejects.toBeInstanceOf(ViewInputError);
440
+ await expect(
441
+ buildView({ input: root, out, allowSensitiveArtifacts: true, scan }),
442
+ ).rejects.toThrow(/copySnapshots/);
443
+ }
444
+ // 同参数不带 --out 时照常收窄报告槽(不报错)。
445
+ await expect(loadViewScan(root, { patterns: ["weather"] })).resolves.toBeTruthy();
446
+ });
447
+
344
448
  it("默认导出(无 --report):报告槽填充 ExperimentComparison,双语块与增强 runtime 恒内联", async () => {
345
449
  const root = await seedRoot();
346
450
  const out = join(root, "site");
347
451
  await buildView({ input: root, out, allowSensitiveArtifacts: true });
348
452
  const html = await readFile(join(out, "index.html"), "utf-8");
349
- expect(html).toContain('<template id="niceeval-report-en">');
350
- expect(html).toContain('<template id="niceeval-report-zh-CN">');
453
+ expect(html).toContain('<template id="niceeval-report-report-en">');
454
+ expect(html).toContain('<template id="niceeval-report-report-zh-CN">');
351
455
  expect(html).toContain("nre-"); // 官方组件的稳定类名(KPI / 榜单 / 散点来自 nre 组件)
352
456
  // 渐进增强 runtime(排序 / 过滤 / tooltip)恒内联;报告块本体仍零 <script>。
353
457
  expect(html).toContain("__nreEnhanced");
354
- const block = html.split('<template id="niceeval-report-en">')[1]!.split("</template>")[0]!;
458
+ const block = html.split('<template id="niceeval-report-report-en">')[1]!.split("</template>")[0]!;
355
459
  expect(block).not.toContain("<script");
356
460
  });
357
461
  });