oh-my-knowledge 0.28.0 → 0.30.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 (281) hide show
  1. package/README.md +49 -3
  2. package/README.zh.md +49 -3
  3. package/dist/src/analysis/failure-clusterer.js +1 -1
  4. package/dist/src/analysis/failure-clusterer.js.map +1 -1
  5. package/dist/src/analysis/gap-analyzer.d.ts +8 -1
  6. package/dist/src/analysis/gap-analyzer.d.ts.map +1 -1
  7. package/dist/src/analysis/gap-analyzer.js +65 -33
  8. package/dist/src/analysis/gap-analyzer.js.map +1 -1
  9. package/dist/src/analysis/report-diagnostics.js +2 -0
  10. package/dist/src/analysis/report-diagnostics.js.map +1 -1
  11. package/dist/src/analysis/sample-diagnostics.js +14 -14
  12. package/dist/src/analysis/sample-diagnostics.js.map +1 -1
  13. package/dist/src/authoring/evolver.d.ts +7 -1
  14. package/dist/src/authoring/evolver.d.ts.map +1 -1
  15. package/dist/src/authoring/evolver.js +8 -5
  16. package/dist/src/authoring/evolver.js.map +1 -1
  17. package/dist/src/authoring/generator.d.ts +21 -23
  18. package/dist/src/authoring/generator.d.ts.map +1 -1
  19. package/dist/src/authoring/generator.js +621 -40
  20. package/dist/src/authoring/generator.js.map +1 -1
  21. package/dist/src/authoring/sample-fixer.d.ts +45 -0
  22. package/dist/src/authoring/sample-fixer.d.ts.map +1 -0
  23. package/dist/src/authoring/sample-fixer.js +186 -0
  24. package/dist/src/authoring/sample-fixer.js.map +1 -0
  25. package/dist/src/cli/commands/evolve.d.ts.map +1 -1
  26. package/dist/src/cli/commands/evolve.js +17 -0
  27. package/dist/src/cli/commands/evolve.js.map +1 -1
  28. package/dist/src/cli/commands/observe.d.ts.map +1 -1
  29. package/dist/src/cli/commands/observe.js +170 -0
  30. package/dist/src/cli/commands/observe.js.map +1 -1
  31. package/dist/src/cli/commands/registry.d.ts.map +1 -1
  32. package/dist/src/cli/commands/registry.js +9 -1
  33. package/dist/src/cli/commands/registry.js.map +1 -1
  34. package/dist/src/cli/commands/sample.d.ts +13 -0
  35. package/dist/src/cli/commands/sample.d.ts.map +1 -1
  36. package/dist/src/cli/commands/sample.js +307 -13
  37. package/dist/src/cli/commands/sample.js.map +1 -1
  38. package/dist/src/cli/commands/studio.d.ts.map +1 -1
  39. package/dist/src/cli/commands/studio.js +8 -0
  40. package/dist/src/cli/commands/studio.js.map +1 -1
  41. package/dist/src/cli/i18n-dict.d.ts +1 -1
  42. package/dist/src/cli/i18n-dict.d.ts.map +1 -1
  43. package/dist/src/cli/i18n-dict.js +189 -17
  44. package/dist/src/cli/i18n-dict.js.map +1 -1
  45. package/dist/src/cli/parse-run-config.d.ts +14 -2
  46. package/dist/src/cli/parse-run-config.d.ts.map +1 -1
  47. package/dist/src/cli/parse-run-config.js +62 -10
  48. package/dist/src/cli/parse-run-config.js.map +1 -1
  49. package/dist/src/doctor/health/builtin-dimensions.d.ts.map +1 -1
  50. package/dist/src/doctor/health/builtin-dimensions.js +20 -23
  51. package/dist/src/doctor/health/builtin-dimensions.js.map +1 -1
  52. package/dist/src/doctor/health/composer.d.ts.map +1 -1
  53. package/dist/src/doctor/health/composer.js +7 -3
  54. package/dist/src/doctor/health/composer.js.map +1 -1
  55. package/dist/src/doctor/health/dimension-spec.d.ts +1 -0
  56. package/dist/src/doctor/health/dimension-spec.d.ts.map +1 -1
  57. package/dist/src/doctor/health/parser.d.ts.map +1 -1
  58. package/dist/src/doctor/health/parser.js +1 -0
  59. package/dist/src/doctor/health/parser.js.map +1 -1
  60. package/dist/src/doctor/health/prompt-builder.js +25 -17
  61. package/dist/src/doctor/health/prompt-builder.js.map +1 -1
  62. package/dist/src/doctor/html-renderer.d.ts.map +1 -1
  63. package/dist/src/doctor/html-renderer.js +76 -66
  64. package/dist/src/doctor/html-renderer.js.map +1 -1
  65. package/dist/src/doctor/rules.d.ts.map +1 -1
  66. package/dist/src/doctor/rules.js +32 -0
  67. package/dist/src/doctor/rules.js.map +1 -1
  68. package/dist/src/eval-core/cache.d.ts +16 -2
  69. package/dist/src/eval-core/cache.d.ts.map +1 -1
  70. package/dist/src/eval-core/cache.js +86 -15
  71. package/dist/src/eval-core/cache.js.map +1 -1
  72. package/dist/src/eval-core/evaluation-execution.d.ts +11 -1
  73. package/dist/src/eval-core/evaluation-execution.d.ts.map +1 -1
  74. package/dist/src/eval-core/evaluation-execution.js +78 -6
  75. package/dist/src/eval-core/evaluation-execution.js.map +1 -1
  76. package/dist/src/eval-core/evaluation-job.d.ts +2 -1
  77. package/dist/src/eval-core/evaluation-job.d.ts.map +1 -1
  78. package/dist/src/eval-core/evaluation-job.js +2 -1
  79. package/dist/src/eval-core/evaluation-job.js.map +1 -1
  80. package/dist/src/eval-core/evaluation-reporting.d.ts +7 -0
  81. package/dist/src/eval-core/evaluation-reporting.d.ts.map +1 -1
  82. package/dist/src/eval-core/evaluation-reporting.js +19 -2
  83. package/dist/src/eval-core/evaluation-reporting.js.map +1 -1
  84. package/dist/src/eval-core/execution-strategy.d.ts +1 -1
  85. package/dist/src/eval-core/execution-strategy.d.ts.map +1 -1
  86. package/dist/src/eval-core/execution-strategy.js +12 -1
  87. package/dist/src/eval-core/execution-strategy.js.map +1 -1
  88. package/dist/src/eval-core/mock-hook.cjs +203 -0
  89. package/dist/src/eval-core/mocks-runtime.d.ts +96 -0
  90. package/dist/src/eval-core/mocks-runtime.d.ts.map +1 -0
  91. package/dist/src/eval-core/mocks-runtime.js +323 -0
  92. package/dist/src/eval-core/mocks-runtime.js.map +1 -0
  93. package/dist/src/eval-core/schema.d.ts.map +1 -1
  94. package/dist/src/eval-core/schema.js +18 -5
  95. package/dist/src/eval-core/schema.js.map +1 -1
  96. package/dist/src/eval-core/task-planner.d.ts +9 -1
  97. package/dist/src/eval-core/task-planner.d.ts.map +1 -1
  98. package/dist/src/eval-core/task-planner.js +40 -1
  99. package/dist/src/eval-core/task-planner.js.map +1 -1
  100. package/dist/src/eval-workflows/batch-evaluation-workflow.d.ts +5 -1
  101. package/dist/src/eval-workflows/batch-evaluation-workflow.d.ts.map +1 -1
  102. package/dist/src/eval-workflows/batch-evaluation-workflow.js +2 -1
  103. package/dist/src/eval-workflows/batch-evaluation-workflow.js.map +1 -1
  104. package/dist/src/eval-workflows/evaluation-pipeline.d.ts +22 -1
  105. package/dist/src/eval-workflows/evaluation-pipeline.d.ts.map +1 -1
  106. package/dist/src/eval-workflows/evaluation-pipeline.js +39 -12
  107. package/dist/src/eval-workflows/evaluation-pipeline.js.map +1 -1
  108. package/dist/src/eval-workflows/evaluation-preparation.d.ts +5 -0
  109. package/dist/src/eval-workflows/evaluation-preparation.d.ts.map +1 -1
  110. package/dist/src/eval-workflows/evaluation-preparation.js +3 -1
  111. package/dist/src/eval-workflows/evaluation-preparation.js.map +1 -1
  112. package/dist/src/eval-workflows/run-evaluation.d.ts +12 -2
  113. package/dist/src/eval-workflows/run-evaluation.d.ts.map +1 -1
  114. package/dist/src/eval-workflows/run-evaluation.js +17 -5
  115. package/dist/src/eval-workflows/run-evaluation.js.map +1 -1
  116. package/dist/src/executors/claude-cli.d.ts +1 -1
  117. package/dist/src/executors/claude-cli.d.ts.map +1 -1
  118. package/dist/src/executors/claude-cli.js +57 -6
  119. package/dist/src/executors/claude-cli.js.map +1 -1
  120. package/dist/src/executors/claude-sdk-trace.d.ts.map +1 -1
  121. package/dist/src/executors/claude-sdk-trace.js +10 -4
  122. package/dist/src/executors/claude-sdk-trace.js.map +1 -1
  123. package/dist/src/executors/claude-sdk.d.ts +1 -1
  124. package/dist/src/executors/claude-sdk.d.ts.map +1 -1
  125. package/dist/src/executors/claude-sdk.js +25 -2
  126. package/dist/src/executors/claude-sdk.js.map +1 -1
  127. package/dist/src/executors/shared.d.ts +1 -1
  128. package/dist/src/executors/shared.d.ts.map +1 -1
  129. package/dist/src/executors/shared.js +6 -1
  130. package/dist/src/executors/shared.js.map +1 -1
  131. package/dist/src/grading/assertions.d.ts +5 -0
  132. package/dist/src/grading/assertions.d.ts.map +1 -1
  133. package/dist/src/grading/assertions.js +27 -1
  134. package/dist/src/grading/assertions.js.map +1 -1
  135. package/dist/src/grading/debias-validate.js +4 -4
  136. package/dist/src/grading/debias-validate.js.map +1 -1
  137. package/dist/src/grading/diagnostic.d.ts +40 -0
  138. package/dist/src/grading/diagnostic.d.ts.map +1 -0
  139. package/dist/src/grading/diagnostic.js +450 -0
  140. package/dist/src/grading/diagnostic.js.map +1 -0
  141. package/dist/src/grading/gold-cli.js +2 -2
  142. package/dist/src/grading/gold-cli.js.map +1 -1
  143. package/dist/src/grading/index.d.ts +5 -0
  144. package/dist/src/grading/index.d.ts.map +1 -1
  145. package/dist/src/grading/judge.d.ts.map +1 -1
  146. package/dist/src/grading/judge.js +54 -2
  147. package/dist/src/grading/judge.js.map +1 -1
  148. package/dist/src/grading/layered-scores.d.ts.map +1 -1
  149. package/dist/src/grading/layered-scores.js +1 -0
  150. package/dist/src/grading/layered-scores.js.map +1 -1
  151. package/dist/src/inputs/eval-config.js +19 -0
  152. package/dist/src/inputs/eval-config.js.map +1 -1
  153. package/dist/src/inputs/load-samples.d.ts +22 -2
  154. package/dist/src/inputs/load-samples.d.ts.map +1 -1
  155. package/dist/src/inputs/load-samples.js +193 -22
  156. package/dist/src/inputs/load-samples.js.map +1 -1
  157. package/dist/src/inputs/skill-loader.d.ts.map +1 -1
  158. package/dist/src/inputs/skill-loader.js +11 -3
  159. package/dist/src/inputs/skill-loader.js.map +1 -1
  160. package/dist/src/observability/experience.d.ts +260 -0
  161. package/dist/src/observability/experience.d.ts.map +1 -0
  162. package/dist/src/observability/experience.js +1233 -0
  163. package/dist/src/observability/experience.js.map +1 -0
  164. package/dist/src/observability/inbox-view-model.d.ts +27 -0
  165. package/dist/src/observability/inbox-view-model.d.ts.map +1 -0
  166. package/dist/src/observability/inbox-view-model.js +135 -0
  167. package/dist/src/observability/inbox-view-model.js.map +1 -0
  168. package/dist/src/observability/inbox.d.ts +125 -0
  169. package/dist/src/observability/inbox.d.ts.map +1 -0
  170. package/dist/src/observability/inbox.js +741 -0
  171. package/dist/src/observability/inbox.js.map +1 -0
  172. package/dist/src/observability/problem-patterns.d.ts +51 -0
  173. package/dist/src/observability/problem-patterns.d.ts.map +1 -0
  174. package/dist/src/observability/problem-patterns.js +205 -0
  175. package/dist/src/observability/problem-patterns.js.map +1 -0
  176. package/dist/src/observability/review-state.d.ts +54 -0
  177. package/dist/src/observability/review-state.d.ts.map +1 -0
  178. package/dist/src/observability/review-state.js +131 -0
  179. package/dist/src/observability/review-state.js.map +1 -0
  180. package/dist/src/observability/skill-chain-advisories.d.ts +25 -0
  181. package/dist/src/observability/skill-chain-advisories.d.ts.map +1 -0
  182. package/dist/src/observability/skill-chain-advisories.js +69 -0
  183. package/dist/src/observability/skill-chain-advisories.js.map +1 -0
  184. package/dist/src/observability/skill-chain.d.ts +61 -0
  185. package/dist/src/observability/skill-chain.d.ts.map +1 -0
  186. package/dist/src/observability/skill-chain.js +233 -0
  187. package/dist/src/observability/skill-chain.js.map +1 -0
  188. package/dist/src/observability/text-signals.d.ts +7 -0
  189. package/dist/src/observability/text-signals.d.ts.map +1 -0
  190. package/dist/src/observability/text-signals.js +22 -0
  191. package/dist/src/observability/text-signals.js.map +1 -0
  192. package/dist/src/observability/trace-adapter.d.ts +12 -64
  193. package/dist/src/observability/trace-adapter.d.ts.map +1 -1
  194. package/dist/src/observability/trace-adapter.js +9 -355
  195. package/dist/src/observability/trace-adapter.js.map +1 -1
  196. package/dist/src/observability/trace-attribution.d.ts +56 -0
  197. package/dist/src/observability/trace-attribution.d.ts.map +1 -0
  198. package/dist/src/observability/trace-attribution.js +200 -0
  199. package/dist/src/observability/trace-attribution.js.map +1 -0
  200. package/dist/src/observability/trace-segmenter.d.ts +52 -0
  201. package/dist/src/observability/trace-segmenter.d.ts.map +1 -0
  202. package/dist/src/observability/trace-segmenter.js +320 -0
  203. package/dist/src/observability/trace-segmenter.js.map +1 -0
  204. package/dist/src/observability/trace-source.d.ts +99 -0
  205. package/dist/src/observability/trace-source.d.ts.map +1 -0
  206. package/dist/src/observability/trace-source.js +492 -0
  207. package/dist/src/observability/trace-source.js.map +1 -0
  208. package/dist/src/renderer/html-renderer.d.ts.map +1 -1
  209. package/dist/src/renderer/html-renderer.js +324 -218
  210. package/dist/src/renderer/html-renderer.js.map +1 -1
  211. package/dist/src/renderer/layout.d.ts.map +1 -1
  212. package/dist/src/renderer/layout.js +155 -71
  213. package/dist/src/renderer/layout.js.map +1 -1
  214. package/dist/src/renderer/observation-inbox-renderer.d.ts +4 -0
  215. package/dist/src/renderer/observation-inbox-renderer.d.ts.map +1 -0
  216. package/dist/src/renderer/observation-inbox-renderer.js +5376 -0
  217. package/dist/src/renderer/observation-inbox-renderer.js.map +1 -0
  218. package/dist/src/renderer/skill-detail-renderer.d.ts +15 -0
  219. package/dist/src/renderer/skill-detail-renderer.d.ts.map +1 -0
  220. package/dist/src/renderer/skill-detail-renderer.js +1234 -0
  221. package/dist/src/renderer/skill-detail-renderer.js.map +1 -0
  222. package/dist/src/renderer/skill-list-renderer.d.ts +4 -0
  223. package/dist/src/renderer/skill-list-renderer.d.ts.map +1 -0
  224. package/dist/src/renderer/skill-list-renderer.js +327 -0
  225. package/dist/src/renderer/skill-list-renderer.js.map +1 -0
  226. package/dist/src/renderer/summary.d.ts +17 -0
  227. package/dist/src/renderer/summary.d.ts.map +1 -1
  228. package/dist/src/renderer/summary.js +346 -74
  229. package/dist/src/renderer/summary.js.map +1 -1
  230. package/dist/src/renderer/table.d.ts.map +1 -1
  231. package/dist/src/renderer/table.js +213 -167
  232. package/dist/src/renderer/table.js.map +1 -1
  233. package/dist/src/renderer/test-view.d.ts +66 -0
  234. package/dist/src/renderer/test-view.d.ts.map +1 -0
  235. package/dist/src/renderer/test-view.js +905 -0
  236. package/dist/src/renderer/test-view.js.map +1 -0
  237. package/dist/src/renderer/trends.d.ts.map +1 -1
  238. package/dist/src/renderer/trends.js +3 -1
  239. package/dist/src/renderer/trends.js.map +1 -1
  240. package/dist/src/server/report-server.d.ts +6 -1
  241. package/dist/src/server/report-server.d.ts.map +1 -1
  242. package/dist/src/server/report-server.js +182 -6
  243. package/dist/src/server/report-server.js.map +1 -1
  244. package/dist/src/server/report-store.d.ts.map +1 -1
  245. package/dist/src/server/report-store.js +33 -1
  246. package/dist/src/server/report-store.js.map +1 -1
  247. package/dist/src/server/skill-index.d.ts +77 -0
  248. package/dist/src/server/skill-index.d.ts.map +1 -0
  249. package/dist/src/server/skill-index.js +331 -0
  250. package/dist/src/server/skill-index.js.map +1 -0
  251. package/dist/src/server/skill-insights.d.ts +92 -0
  252. package/dist/src/server/skill-insights.d.ts.map +1 -0
  253. package/dist/src/server/skill-insights.js +676 -0
  254. package/dist/src/server/skill-insights.js.map +1 -0
  255. package/dist/src/shared/hard-rules.d.ts +40 -0
  256. package/dist/src/shared/hard-rules.d.ts.map +1 -0
  257. package/dist/src/shared/hard-rules.js +200 -0
  258. package/dist/src/shared/hard-rules.js.map +1 -0
  259. package/dist/src/shared/time.d.ts +2 -0
  260. package/dist/src/shared/time.d.ts.map +1 -0
  261. package/dist/src/shared/time.js +10 -0
  262. package/dist/src/shared/time.js.map +1 -0
  263. package/dist/src/shared/tool-search.d.ts +9 -0
  264. package/dist/src/shared/tool-search.d.ts.map +1 -0
  265. package/dist/src/shared/tool-search.js +104 -0
  266. package/dist/src/shared/tool-search.js.map +1 -0
  267. package/dist/src/types/eval.d.ts +83 -0
  268. package/dist/src/types/eval.d.ts.map +1 -1
  269. package/dist/src/types/executor.d.ts +47 -0
  270. package/dist/src/types/executor.d.ts.map +1 -1
  271. package/dist/src/types/judge.d.ts +76 -0
  272. package/dist/src/types/judge.d.ts.map +1 -1
  273. package/dist/src/types/judge.js +3 -1
  274. package/dist/src/types/judge.js.map +1 -1
  275. package/dist/src/types/report.d.ts +63 -3
  276. package/dist/src/types/report.d.ts.map +1 -1
  277. package/dist/src/util/safe-slice.d.ts +23 -0
  278. package/dist/src/util/safe-slice.d.ts.map +1 -0
  279. package/dist/src/util/safe-slice.js +33 -0
  280. package/dist/src/util/safe-slice.js.map +1 -0
  281. package/package.json +5 -4
@@ -0,0 +1,905 @@
1
+ /**
2
+ * 单测视角渲染器 — 把 EvaluationReport 当作 vitest 风格的测试结果看:
3
+ *
4
+ * 每个 sample = 一个 test case
5
+ * ┌─ 用例设计(测试契约): prompt / rubric / 期望 assertions / 工具调用 mock
6
+ * ├─ 执行轨迹: thinking + tool calls + 断言绑定到 turn
7
+ * └─ 断言汇总: 每条 assertion 在哪一步触发,pass/fail
8
+ *
9
+ * 数据全部来自 EvaluationReport,不引入新数据源:
10
+ * - 设计 ← report.sampleSnapshots[sample_id]
11
+ * - 执行 ← VariantResult.turns / .toolCalls / .mockStats / .fullOutput
12
+ * - 断言 ← VariantResult.assertions.details (已经有 passed/failed)
13
+ *
14
+ * 旧 report 缺 sampleSnapshots 时降级显示("无设计快照,只有执行结果")。
15
+ */
16
+ import { e } from './layout.js';
17
+ // ──────────────────────────────────────────────────────────────────
18
+ // Assertion → Turn 绑定:扫执行轨迹找断言匹配的 turn 编号
19
+ // ──────────────────────────────────────────────────────────────────
20
+ /** Tool call 的稳定签名,用于在 turns 里反查 turn 序号。 */
21
+ export function toolCallSignature(tc) {
22
+ return `${tc.tool}::${JSON.stringify(tc.input)}`;
23
+ }
24
+ /** 走 turns 数组,给每个 tool call 标一个 turn 编号(1-based)。导出供单测验证。 */
25
+ export function indexToolCallsByTurn(turns) {
26
+ const map = new Map();
27
+ if (!turns)
28
+ return map;
29
+ turns.forEach((turn, i) => {
30
+ if (turn.toolCalls) {
31
+ for (const tc of turn.toolCalls) {
32
+ const sig = toolCallSignature(tc);
33
+ if (!map.has(sig))
34
+ map.set(sig, i + 1);
35
+ }
36
+ }
37
+ });
38
+ return map;
39
+ }
40
+ /** 字符串解析:`tool_input_contains` / `tool_output_contains` 用 `Tool:substring` 格式。 */
41
+ export function parseToolColonValue(value) {
42
+ const idx = value.indexOf(':');
43
+ if (idx < 0)
44
+ return { tool: '', needle: value };
45
+ return { tool: value.slice(0, idx), needle: value.slice(idx + 1) };
46
+ }
47
+ /** 把单条 assertion 绑定到某一步。匹配不到返回 null(contains/regex 等作用于 fullOutput
48
+ * 的天然没有 turn 绑定)。导出供单测。
49
+ *
50
+ * 局限:mock_hit 的 turn 绑定靠"该 tool 的第 N 次调用 = 第 N 个 mock"启发式,
51
+ * 和真实 mock matcher(按 match 规则)可能不一致——状态机 mock 场景下 turn 标签可能近似。
52
+ * pass/fail 仍以 AssertionDetail.passed 为准。 */
53
+ export function findAssertionTurn(assertion, detail, toolCalls, turnByCallSig) {
54
+ const type = assertion.type;
55
+ if (type === 'tool_input_contains' || type === 'tool_output_contains' || type === 'tool_input_not_contains') {
56
+ const value = String(assertion.value ?? detail.value ?? '');
57
+ const { tool, needle } = parseToolColonValue(value);
58
+ for (const tc of toolCalls) {
59
+ if (tool && tc.tool !== tool)
60
+ continue;
61
+ const target = type === 'tool_output_contains'
62
+ ? JSON.stringify(tc.output ?? '')
63
+ : JSON.stringify(tc.input ?? '');
64
+ if (target.includes(needle)) {
65
+ return { turn: turnByCallSig.get(toolCallSignature(tc)) ?? null, matchedCall: tc };
66
+ }
67
+ }
68
+ return { turn: null };
69
+ }
70
+ if (type === 'tools_called' || type === 'tools_not_called') {
71
+ const targets = (assertion.values ?? [String(assertion.value ?? '')]).filter(Boolean);
72
+ for (const tc of toolCalls) {
73
+ if (targets.includes(tc.tool)) {
74
+ return { turn: turnByCallSig.get(toolCallSignature(tc)) ?? null, matchedCall: tc };
75
+ }
76
+ }
77
+ return { turn: null };
78
+ }
79
+ if (type === 'mock_hit') {
80
+ // mock_hit 的 value 形如 "Bash:2",对应 mockStats.perMock 的 key。
81
+ // 不靠 mockStats 反推 turn(没有那个映射),靠 assertion.value 解析的 tool + 第 N 个该 tool 的调用。
82
+ const value = String(assertion.value ?? detail.value ?? '');
83
+ const { tool, needle } = parseToolColonValue(value);
84
+ const targetIndex = parseInt(needle, 10);
85
+ if (!Number.isFinite(targetIndex))
86
+ return { turn: null };
87
+ let counter = 0;
88
+ for (const tc of toolCalls) {
89
+ if (tc.tool !== tool)
90
+ continue;
91
+ counter += 1;
92
+ if (counter === targetIndex) {
93
+ return { turn: turnByCallSig.get(toolCallSignature(tc)) ?? null, matchedCall: tc };
94
+ }
95
+ }
96
+ return { turn: null };
97
+ }
98
+ // contains / not_contains / regex 作用于 fullOutput,无 turn 绑定
99
+ return { turn: null };
100
+ }
101
+ // ──────────────────────────────────────────────────────────────────
102
+ // 渲染:per-sample 卡片
103
+ // ──────────────────────────────────────────────────────────────────
104
+ /** 用例的整体 verdict:全部 assertion 通过 → pass;任一失败 → fail;无 assertion → 视为 pass(谨慎)。 */
105
+ export function sampleVerdict(details) {
106
+ if (!details || details.length === 0)
107
+ return 'pass';
108
+ return details.every((d) => d.passed) ? 'pass' : 'fail';
109
+ }
110
+ /**
111
+ * 判断 sample 是否是"诱错样本(tripwire)"——视觉上要跟普通 fail 区分开。
112
+ *
113
+ * 识别来源(任一即算):
114
+ * 1. snapshot.tripwire === true(generator/作者显式标记)
115
+ * 2. diagnostic.rootCause 包含 'tripwire_intentional'(LLM 诊断时自己识破的)
116
+ *
117
+ * 用途:UI 把诱错样本的失败渲染成紫色 "🪤 诱错触发" 而非红色 "✗ FAIL",
118
+ * 避免使用者误以为是要修的真 bug。
119
+ */
120
+ export function isTripwireSample(snapshot, variant) {
121
+ if (snapshot?.tripwire === true)
122
+ return true;
123
+ const rc = variant.diagnostic?.rootCause || [];
124
+ return rc.includes('tripwire_intentional');
125
+ }
126
+ /** 作用于 LLM 全文输出的 assertion 类型(不绑定到具体 turn,而是检查 fullOutput / final answer)。 */
127
+ const OUTPUT_LEVEL_ASSERTION_TYPES = new Set([
128
+ 'contains', 'not_contains', 'regex',
129
+ 'min_length', 'max_length', 'length',
130
+ 'json_schema', 'starts_with', 'ends_with',
131
+ 'rouge_n_min', 'bleu_min', 'levenshtein_max',
132
+ 'embedding_similarity_min', 'answer_relevancy', 'faithfulness',
133
+ ]);
134
+ function renderAssertionLine(d, turn, lang) {
135
+ const icon = d.passed ? '✅' : '❌';
136
+ const valueRepr = typeof d.value === 'object' ? JSON.stringify(d.value) : String(d.value);
137
+ let turnTag;
138
+ if (turn !== null) {
139
+ turnTag = `<span class="tv-turn-tag">@ Turn ${turn}</span>`;
140
+ }
141
+ else if (OUTPUT_LEVEL_ASSERTION_TYPES.has(d.type)) {
142
+ turnTag = `<span class="tv-turn-tag">${lang === 'zh' ? '@ 全文输出' : '@ full output'}</span>`;
143
+ }
144
+ else if (d.passed && d.type === 'tools_not_called') {
145
+ turnTag = `<span class="tv-turn-tag tv-turn-tag--ok">${lang === 'zh' ? '未触发(通过)' : 'never triggered (pass)'}</span>`;
146
+ }
147
+ else {
148
+ turnTag = `<span class="tv-turn-tag tv-turn-tag--miss">${lang === 'zh' ? '未匹配到任何 turn' : 'no matching turn'}</span>`;
149
+ }
150
+ return `<div class="tv-assert ${d.passed ? 'tv-assert--pass' : 'tv-assert--fail'}">
151
+ ${icon} <code>${e(d.type)}</code> <code class="tv-assert-value">${e(valueRepr)}</code>${turnTag}
152
+ ${d.message ? `<div class="tv-assert-msg">${e(d.message)}</div>` : ''}
153
+ </div>`;
154
+ }
155
+ function renderDesignSection(snapshot, lang) {
156
+ // 注意:本函数返回的内容会被外层 `<details><summary>用例设计</summary>` 包起来
157
+ //(见 renderSubSections 的 .tv-section),所以这里不再重复"📋 用例设计"标题。
158
+ if (!snapshot) {
159
+ return `<div class="tv-empty">${lang === 'zh' ? '该 report 早于单测视角支持时间,缺设计快照' : 'Report predates test-view support; no design snapshot'}</div>`;
160
+ }
161
+ const meta = [];
162
+ if (snapshot.capability && snapshot.capability.length > 0) {
163
+ const capLabel = lang === 'zh' ? '能力(capability)' : 'capability';
164
+ meta.push(`<span class="tv-meta-pill">${capLabel}: ${snapshot.capability.map(e).join(' · ')}</span>`);
165
+ }
166
+ if (snapshot.construct) {
167
+ const lbl = lang === 'zh' ? '验证目标(construct)' : 'construct';
168
+ meta.push(`<span class="tv-meta-pill">${lbl}: ${e(constructValueLabel(snapshot.construct, lang))}</span>`);
169
+ }
170
+ if (snapshot.difficulty) {
171
+ const lbl = lang === 'zh' ? '难度(difficulty)' : 'difficulty';
172
+ meta.push(`<span class="tv-meta-pill">${lbl}: ${e(difficultyValueLabel(snapshot.difficulty, lang))}</span>`);
173
+ }
174
+ if (snapshot.provenance) {
175
+ const lbl = lang === 'zh' ? '来源(provenance)' : 'provenance';
176
+ meta.push(`<span class="tv-meta-pill">${lbl}: ${e(provenanceValueLabel(snapshot.provenance, lang))}</span>`);
177
+ }
178
+ const expectedAssertions = (snapshot.assertions ?? []).map((a) => renderExpectedAssertion(a)).join('');
179
+ const mocksBlock = snapshot.mocks && snapshot.mocks.length > 0
180
+ ? `<details class="tv-collapsible"><summary>${lang === 'zh' ? `工具调用 Mock(${snapshot.mocks.length} 条)` : `Tool-call mocks (${snapshot.mocks.length})`}</summary>
181
+ <div class="tv-mocks">${snapshot.mocks.map((m, i) => renderMock(m, i + 1)).join('')}</div>
182
+ </details>`
183
+ : '';
184
+ return `<div class="tv-section--design-body">
185
+ <div class="tv-prompt"><strong>${lang === 'zh' ? '提示词(Prompt):' : 'Prompt:'}</strong> <span>${e(snapshot.prompt)}</span></div>
186
+ ${snapshot.rubric ? `<div class="tv-rubric"><strong>${lang === 'zh' ? '评分标准(Rubric):' : 'Rubric:'}</strong> <span>${e(snapshot.rubric)}</span></div>` : ''}
187
+ ${snapshot.context ? `<details class="tv-collapsible"><summary>${lang === 'zh' ? '上下文(Context)' : 'Context'}</summary><pre class="tv-pre">${e(snapshot.context)}</pre></details>` : ''}
188
+ ${meta.length > 0 ? `<div class="tv-meta-pills">${meta.join(' ')}</div>` : ''}
189
+ ${expectedAssertions ? `<div class="tv-expected">
190
+ <div class="tv-expected-h">${lang === 'zh' ? '期望(assertions):' : 'Expected (assertions):'}</div>
191
+ ${expectedAssertions}
192
+ </div>` : ''}
193
+ ${mocksBlock}
194
+ </div>`;
195
+ }
196
+ /** construct 取值常见三种,zh 显示纯中文;非枚举的自由值原样回显(作者自定义,无法翻译)。 */
197
+ function constructValueLabel(v, lang) {
198
+ if (lang !== 'zh')
199
+ return v;
200
+ const map = {
201
+ 'necessity': '必要性',
202
+ 'capability': '能力',
203
+ 'quality': '质量',
204
+ };
205
+ return map[v] ?? v;
206
+ }
207
+ function difficultyValueLabel(v, lang) {
208
+ if (lang !== 'zh')
209
+ return v;
210
+ const map = {
211
+ 'easy': '容易',
212
+ 'medium': '中等',
213
+ 'hard': '困难',
214
+ };
215
+ return map[v] ?? v;
216
+ }
217
+ function provenanceValueLabel(v, lang) {
218
+ if (lang !== 'zh')
219
+ return v;
220
+ const map = {
221
+ 'human': '人工编写',
222
+ 'llm-generated': 'LLM 生成',
223
+ 'production-trace': '线上轨迹',
224
+ };
225
+ return map[v] ?? v;
226
+ }
227
+ function renderExpectedAssertion(a) {
228
+ const typeLabel = e(a.type);
229
+ const valueRepr = a.values
230
+ ? JSON.stringify(a.values)
231
+ : a.value !== undefined
232
+ ? String(a.value)
233
+ : a.pattern
234
+ ? `regex /${a.pattern}/${a.flags ?? ''}`
235
+ : '';
236
+ const weight = typeof a.weight === 'number' ? ` (w=${a.weight})` : '';
237
+ return `<div class="tv-expect-line">• <code>${typeLabel}</code> <code class="tv-assert-value">${e(valueRepr)}</code>${weight}</div>`;
238
+ }
239
+ function renderMock(m, n) {
240
+ const match = m.match ? JSON.stringify(m.match) : '(any)';
241
+ let returnRepr;
242
+ if (m.return !== undefined) {
243
+ returnRepr = typeof m.return === 'string' ? m.return : JSON.stringify(m.return);
244
+ }
245
+ else if (m.return_seq) {
246
+ returnRepr = `seq(${m.return_seq.length})`;
247
+ }
248
+ else if (m.return_file) {
249
+ returnRepr = `file: ${m.return_file}`;
250
+ }
251
+ else {
252
+ returnRepr = '(no return)';
253
+ }
254
+ return `<div class="tv-mock-line">
255
+ <span class="tv-mock-idx">#${n}</span>
256
+ <code class="tv-mock-tool">${e(m.tool)}</code>
257
+ <code class="tv-mock-match">${e(match)}</code>
258
+ <span class="tv-mock-arrow">→</span>
259
+ <code class="tv-mock-return">${e(returnRepr.length > 200 ? returnRepr.slice(0, 200) + '…' : returnRepr)}</code>
260
+ </div>`;
261
+ }
262
+ function renderExecutionTrace(variant, detailsByTurn, matchedMockNumbersByTurn, lang) {
263
+ const turns = variant.turns ?? [];
264
+ if (turns.length === 0) {
265
+ return `<div class="tv-section tv-section--trace">
266
+ <div class="tv-section-h">${lang === 'zh' ? '🎬 执行轨迹' : '🎬 Execution trace'}</div>
267
+ <div class="tv-empty">${lang === 'zh' ? '该 variant 无 turn 轨迹(executor 未上报)' : 'No turn trace from executor'}</div>
268
+ </div>`;
269
+ }
270
+ const stepsHtml = turns.map((turn, i) => {
271
+ const turnNum = i + 1;
272
+ const isAssistant = turn.role === 'assistant';
273
+ const thinkingBlock = isAssistant && turn.content
274
+ ? `<details class="tv-thinking"><summary>${lang === 'zh' ? '展开 thinking' : 'show thinking'} (${turn.content.length} ${lang === 'zh' ? '字' : 'chars'})</summary><div class="tv-thinking-body">${e(turn.content)}</div></details>`
275
+ : '';
276
+ const calls = (turn.toolCalls ?? []).map((tc) => renderToolCall(tc, turnNum, matchedMockNumbersByTurn.get(turnNum) ?? [])).join('');
277
+ const assertsForTurn = (detailsByTurn.get(turnNum) ?? []);
278
+ const turnAssertsHtml = assertsForTurn.map((d) => `<div class="tv-trace-assert ${d.passed ? 'tv-assert--pass' : 'tv-assert--fail'}">${d.passed ? '✅' : '❌'} <code>${e(d.type)}</code> <code class="tv-assert-value">${e(typeof d.value === 'object' ? JSON.stringify(d.value) : String(d.value))}</code></div>`).join('');
279
+ const durationTag = turn.durationMs ? `<span class="tv-turn-dur">${(turn.durationMs / 1000).toFixed(1)}s</span>` : '';
280
+ return `<div class="tv-turn">
281
+ <div class="tv-turn-h">Turn ${turnNum} · <code>${e(turn.role)}</code> ${durationTag}</div>
282
+ ${thinkingBlock}
283
+ ${calls}
284
+ ${turnAssertsHtml}
285
+ </div>`;
286
+ }).join('');
287
+ const finalOutput = variant.fullOutput
288
+ ? `<div class="tv-final"><div class="tv-final-h">${lang === 'zh' ? '最终回答' : 'Final answer'}</div><pre class="tv-pre">${e(variant.fullOutput.length > 2000 ? variant.fullOutput.slice(0, 2000) + '…' : variant.fullOutput)}</pre></div>`
289
+ : '';
290
+ return `<div class="tv-section tv-section--trace">
291
+ <div class="tv-section-h">${lang === 'zh' ? '🎬 执行轨迹' : '🎬 Execution trace'}</div>
292
+ ${stepsHtml}
293
+ ${finalOutput}
294
+ </div>`;
295
+ }
296
+ function renderToolCall(tc, _turnNum, mockHitsHere) {
297
+ const inputRepr = typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input ?? '');
298
+ const outputRepr = typeof tc.output === 'string' ? tc.output : JSON.stringify(tc.output ?? '');
299
+ const mockBadge = mockHitsHere.length > 0
300
+ ? `<span class="tv-mock-badge" title="matched mock #${mockHitsHere.join(',#')}">✓ mock#${mockHitsHere.join(',#')}</span>`
301
+ : '';
302
+ return `<div class="tv-call ${tc.success ? '' : 'tv-call--err'}">
303
+ <div class="tv-call-h">🔧 <code>${e(tc.tool)}</code> ${mockBadge}</div>
304
+ <details><summary>input</summary><pre class="tv-pre">${e(inputRepr.length > 1000 ? inputRepr.slice(0, 1000) + '…' : inputRepr)}</pre></details>
305
+ <details><summary>output</summary><pre class="tv-pre">${e(outputRepr.length > 1000 ? outputRepr.slice(0, 1000) + '…' : outputRepr)}</pre></details>
306
+ </div>`;
307
+ }
308
+ function renderAssertionsSummary(details, expected, toolCalls, turns, lang) {
309
+ if (!details || details.length === 0) {
310
+ return {
311
+ html: `<div class="tv-section tv-section--asserts">
312
+ <div class="tv-section-h">${lang === 'zh' ? '📊 断言结果' : '📊 Assertion results'}</div>
313
+ <div class="tv-empty">${lang === 'zh' ? '该 sample 没有声明 assertion' : 'No assertions declared on this sample'}</div>
314
+ </div>`,
315
+ detailsByTurn: new Map(),
316
+ matchedMocks: [],
317
+ };
318
+ }
319
+ const turnByCallSig = indexToolCallsByTurn(turns);
320
+ const detailsByTurn = new Map();
321
+ const matchedMocks = [];
322
+ const lines = [];
323
+ details.forEach((d, i) => {
324
+ const expectedAssertion = expected?.[i];
325
+ const binding = expectedAssertion
326
+ ? findAssertionTurn(expectedAssertion, d, toolCalls, turnByCallSig)
327
+ : { turn: null };
328
+ if (binding.turn !== null) {
329
+ const arr = detailsByTurn.get(binding.turn) ?? [];
330
+ arr.push(d);
331
+ detailsByTurn.set(binding.turn, arr);
332
+ }
333
+ if (expectedAssertion?.type === 'mock_hit' && binding.turn !== null) {
334
+ const value = String(expectedAssertion.value ?? d.value ?? '');
335
+ const { needle } = parseToolColonValue(value);
336
+ const idx = parseInt(needle, 10);
337
+ if (Number.isFinite(idx))
338
+ matchedMocks.push(idx);
339
+ }
340
+ lines.push(renderAssertionLine(d, binding.turn, lang));
341
+ });
342
+ const passCount = details.filter((d) => d.passed).length;
343
+ return {
344
+ html: `<div class="tv-section tv-section--asserts">
345
+ <div class="tv-section-h">${lang === 'zh' ? '📊 断言结果' : '📊 Assertion results'} <span class="tv-section-stat">(${passCount}/${details.length})</span></div>
346
+ ${lines.join('')}
347
+ </div>`,
348
+ detailsByTurn,
349
+ matchedMocks,
350
+ };
351
+ }
352
+ function buildMatchedMocksByTurn(matchedMockIndices, expected, toolCalls, turns) {
353
+ // matchedMockIndices: list of mock indices(1-based)that have an associated turn-bound assertion.
354
+ // 重新走一遍 toolCalls,把 mock idx 对应的 turn 编号收集起来。
355
+ const out = new Map();
356
+ if (matchedMockIndices.length === 0 || !expected)
357
+ return out;
358
+ const turnByCallSig = indexToolCallsByTurn(turns);
359
+ const counters = new Map();
360
+ // 给每个 toolCall 配上"它在该 tool 内的第几次调用"
361
+ const callIndexByCall = new Map();
362
+ for (const tc of toolCalls) {
363
+ const next = (counters.get(tc.tool) ?? 0) + 1;
364
+ counters.set(tc.tool, next);
365
+ callIndexByCall.set(toolCallSignature(tc), next);
366
+ }
367
+ // 对每条 mock_hit 期望,找到匹配的 toolCall 的 turn
368
+ for (const a of expected) {
369
+ if (a.type !== 'mock_hit')
370
+ continue;
371
+ const value = String(a.value ?? '');
372
+ const { tool, needle } = parseToolColonValue(value);
373
+ const want = parseInt(needle, 10);
374
+ if (!Number.isFinite(want))
375
+ continue;
376
+ if (!matchedMockIndices.includes(want))
377
+ continue;
378
+ for (const tc of toolCalls) {
379
+ if (tc.tool !== tool)
380
+ continue;
381
+ const sig = toolCallSignature(tc);
382
+ if (callIndexByCall.get(sig) === want) {
383
+ const turn = turnByCallSig.get(sig);
384
+ if (turn) {
385
+ const arr = out.get(turn) ?? [];
386
+ arr.push(want);
387
+ out.set(turn, arr);
388
+ }
389
+ break;
390
+ }
391
+ }
392
+ }
393
+ return out;
394
+ }
395
+ /**
396
+ * Diagnostic 区 — 与 Judge 评价彻底独立的功能性测试视角。
397
+ *
398
+ * 数据来源:variant.diagnostic(eval pipeline 在 failed sample 上跑独立 LLM 调用产出)。
399
+ * --no-diagnostic 跑会缺这个字段,显示降级提示。
400
+ *
401
+ * 与 Judge 评价的区别:
402
+ * - Judge 答"打几分"(主观质量),归 📊 评分 tab。
403
+ * - Diagnostic 答"哪错了 + skill 怎么改"(可操作建议),归 ✅ 单测 tab。
404
+ * 此 renderer 只负责后者,不读 llmReason / llmReasoning。
405
+ *
406
+ * 设计:fail 用例顶部默认展开;pass 用例不显示这个 section(诊断只对 fail 有意义)。
407
+ */
408
+ function renderDiagnosticSection(variant, verdict, lang) {
409
+ // pass 用例不需要诊断
410
+ if (verdict === 'pass')
411
+ return '';
412
+ const diag = variant.diagnostic;
413
+ if (!diag) {
414
+ return `<div class="tv-diag">
415
+ <p class="tv-diag-text"><em>${lang === 'zh' ? '此用例无诊断(可能跑了 --no-diagnostic 或 report 早于诊断功能)。重跑 omk eval 即可看到针对性建议。' : 'No diagnostic on this failed case. Re-run omk eval to get targeted suggestions.'}</em></p>
416
+ </div>`;
417
+ }
418
+ if (!diag.ok) {
419
+ return `<div class="tv-diag">
420
+ <p class="tv-diag-text"><em>${lang === 'zh' ? '诊断生成失败:' : 'Diagnostic generation failed:'} ${e(diag.error || '')}</em></p>
421
+ ${diag.summary ? `<p class="tv-diag-text">${e(diag.summary)}</p>` : ''}
422
+ </div>`;
423
+ }
424
+ // 单一建议块:三类 suggestion 中只挑非空的展示。优先级 sample > skill > none(用例bug 比 skill 改进比"无需改"更紧迫)。
425
+ let suggestion = null;
426
+ if (diag.suggestion.sample) {
427
+ suggestion = { label: lang === 'zh' ? '◇ 建议改 sample' : '◇ Edit sample', body: diag.suggestion.sample };
428
+ }
429
+ else if (diag.suggestion.skill) {
430
+ suggestion = { label: lang === 'zh' ? '◇ 建议改 skill' : '◇ Edit skill', body: diag.suggestion.skill };
431
+ }
432
+ else if (diag.suggestion.none) {
433
+ suggestion = { label: lang === 'zh' ? '◇ 无需改动' : '◇ No fix needed', body: diag.suggestion.none };
434
+ }
435
+ const suggestionHtml = suggestion
436
+ ? `<div class="tv-suggestion">
437
+ <span class="tv-suggestion-label">${suggestion.label}</span>
438
+ <p class="tv-suggestion-body">${e(suggestion.body)}</p>
439
+ </div>`
440
+ : '';
441
+ // 失败模式标签 — 中文枚举,直接显著展示在 summary 下
442
+ const failureModes = diag.failureModes ?? [];
443
+ const failureModesHtml = failureModes.length > 0
444
+ ? `<div class="tv-diag-modes">
445
+ <span class="tv-diag-modes-label">${lang === 'zh' ? '失败模式:' : 'Failure modes:'}</span>
446
+ ${failureModes.map((m) => `<span class="tv-diag-mode-pill tv-diag-mode--${e(failureModeSlug(m))}">${e(m)}</span>`).join('')}
447
+ </div>`
448
+ : '';
449
+ // 工作流校验清单 — 默认展开,放在 suggestion 之上(最直接的"哪步对哪步错"视图)
450
+ const workflowChecks = diag.workflowChecks ?? [];
451
+ const workflowChecksHtml = workflowChecks.length > 0
452
+ ? `<div class="tv-diag-workflow">
453
+ <p class="tv-diag-workflow-title">${lang === 'zh' ? '工作流校验' : 'Workflow checklist'}</p>
454
+ <ul class="tv-diag-workflow-list">
455
+ ${workflowChecks.map((c) => `
456
+ <li class="tv-diag-workflow-item tv-diag-workflow-item--${c.passed ? 'pass' : 'fail'}">
457
+ <span class="tv-diag-workflow-icon">${c.passed ? '✓' : '✗'}</span>
458
+ <div class="tv-diag-workflow-body">
459
+ <span class="tv-diag-workflow-step">${e(c.step)}</span>
460
+ ${c.evidence ? `<span class="tv-diag-workflow-evidence">${e(c.evidence)}</span>` : ''}
461
+ </div>
462
+ </li>
463
+ `).join('')}
464
+ </ul>
465
+ </div>`
466
+ : '';
467
+ // 详情区(rootCause + expected/actual + 其余 suggestion)默认折叠
468
+ const rootCausePills = diag.rootCause.length > 0
469
+ ? `<p><strong>${lang === 'zh' ? '归因:' : 'Root cause:'}</strong>${diag.rootCause.map((c) => `<span class="tv-diag-rc-pill tv-diag-rc--${e(c)}">${e(rootCauseLabel(c, lang))}</span>`).join('')}</p>`
470
+ : '';
471
+ const detailsContent = [];
472
+ if (rootCausePills)
473
+ detailsContent.push(rootCausePills);
474
+ if (diag.expected)
475
+ detailsContent.push(`<p><strong>${lang === 'zh' ? '期望:' : 'Expected:'}</strong>${e(diag.expected)}</p>`);
476
+ if (diag.actual)
477
+ detailsContent.push(`<p><strong>${lang === 'zh' ? '实际:' : 'Actual:'}</strong>${e(diag.actual)}</p>`);
478
+ // 其它 suggestion 槽位也露出来(避免完全隐藏)
479
+ if (diag.suggestion.sample && suggestion?.label !== (lang === 'zh' ? '◇ 建议改 sample' : '◇ Edit sample')) {
480
+ detailsContent.push(`<p><strong>${lang === 'zh' ? '另:改 sample' : 'Also: edit sample'}</strong>${e(diag.suggestion.sample)}</p>`);
481
+ }
482
+ if (diag.suggestion.skill && suggestion?.label !== (lang === 'zh' ? '◇ 建议改 skill' : '◇ Edit skill')) {
483
+ detailsContent.push(`<p><strong>${lang === 'zh' ? '另:改 skill' : 'Also: edit skill'}</strong>${e(diag.suggestion.skill)}</p>`);
484
+ }
485
+ const detailsHtml = detailsContent.length > 0
486
+ ? `<details class="tv-diag-details"><summary>${lang === 'zh' ? '展开归因细节' : 'show root cause / expected vs actual'}</summary>
487
+ <div class="tv-diag-details-body">${detailsContent.join('')}</div>
488
+ </details>`
489
+ : '';
490
+ // 部分内容(salvage)时挂个小提示,让用户知道诊断 LLM 输出被截断
491
+ const partialBadge = diag.error?.startsWith('partial:')
492
+ ? `<p class="tv-diag-partial"><em>${lang === 'zh' ? '⚠ 诊断 LLM 输出被截断,以下为部分内容' : '⚠ Diagnostic LLM output truncated; partial content shown'}</em></p>`
493
+ : '';
494
+ return `<div class="tv-diag">
495
+ ${partialBadge}
496
+ ${diag.summary ? `<p class="tv-diag-text">${e(diag.summary)}</p>` : ''}
497
+ ${failureModesHtml}
498
+ ${workflowChecksHtml}
499
+ ${suggestionHtml}
500
+ ${detailsHtml}
501
+ </div>`;
502
+ }
503
+ /** 失败模式中文标签 → CSS-safe slug,用于挂 tv-diag-mode--<slug> 的颜色样式。 */
504
+ function failureModeSlug(m) {
505
+ const map = {
506
+ '工作流跳步': 'workflow-skip',
507
+ '硬编码值': 'hardcoded',
508
+ '幻觉输出': 'hallucination',
509
+ '工具误用': 'tool-misuse',
510
+ '环境拦截': 'env-blocked',
511
+ '误读约束': 'misread',
512
+ '其他': 'other',
513
+ };
514
+ return map[m] ?? 'other';
515
+ }
516
+ /** rootCause 英文枚举 → 中文/英文短标签(给已有 rc-pill 用,UI 不再直接吐 enum 名)。 */
517
+ function rootCauseLabel(c, lang) {
518
+ const zh = {
519
+ 'skill_doc_unclear': 'skill 文档模糊',
520
+ 'skill_doc_missing': 'skill 缺失场景',
521
+ 'llm_misread': 'LLM 误读',
522
+ 'sample_design': 'sample 设计 bug',
523
+ 'tripwire_intentional': '诱错样本(预期)',
524
+ };
525
+ const en = {
526
+ 'skill_doc_unclear': 'skill doc unclear',
527
+ 'skill_doc_missing': 'skill doc missing',
528
+ 'llm_misread': 'LLM misread',
529
+ 'sample_design': 'sample design bug',
530
+ 'tripwire_intentional': 'tripwire (expected fail)',
531
+ };
532
+ return (lang === 'zh' ? zh : en)[c] ?? c;
533
+ }
534
+ /** 把 design / trace / assertions 三段子内容包成 <details>(默认折叠),供 pass row 展开 / fail card 折叠后查看用。 */
535
+ function renderSubSections(d, lang) {
536
+ const { snapshot, variant } = d;
537
+ const toolCalls = variant.toolCalls ?? [];
538
+ const turns = variant.turns;
539
+ const summary = renderAssertionsSummary(variant.assertions?.details, snapshot?.assertions, toolCalls, turns, lang);
540
+ const matchedByTurn = buildMatchedMocksByTurn(summary.matchedMocks, snapshot?.assertions, toolCalls, turns);
541
+ const designHtml = renderDesignSection(snapshot, lang);
542
+ const traceHtml = renderExecutionTrace(variant, summary.detailsByTurn, matchedByTurn, lang);
543
+ const totalAsserts = variant.assertions?.total ?? 0;
544
+ const passedAsserts = variant.assertions?.passed ?? 0;
545
+ return `
546
+ <details class="tv-section"><summary>${lang === 'zh' ? '用例设计' : 'Test design'}</summary><div class="tv-section-body">${designHtml}</div></details>
547
+ <details class="tv-section"><summary>${lang === 'zh' ? '执行轨迹' : 'Execution trace'}<span class="tv-section-stat">(${turns?.length ?? 0} ${lang === 'zh' ? '步' : 'steps'} · ${toolCalls.length} ${lang === 'zh' ? '工具调用' : 'tool calls'})</span></summary><div class="tv-section-body">${traceHtml}</div></details>
548
+ <details class="tv-section"><summary>${lang === 'zh' ? '断言结果' : 'Assertion results'}<span class="tv-section-stat">(${passedAsserts}/${totalAsserts})</span></summary><div class="tv-section-body">${summary.html}</div></details>
549
+ `;
550
+ }
551
+ function buildCardData(sample_id, snapshot, variant) {
552
+ const rawVerdict = sampleVerdict(variant.assertions?.details);
553
+ const isTripwire = rawVerdict === 'fail' && isTripwireSample(snapshot, variant);
554
+ return { sample_id, snapshot, variant, cardKind: isTripwire ? 'tripwire' : rawVerdict };
555
+ }
556
+ /** Header 公共部分:状态点 + sample_id + 标题 + 耗时。
557
+ * 评分(compositeScore)和成本($cost)挪到详情区,列表只保留状态识别需要的最小信息。 */
558
+ function renderCardHeader(d, lang) {
559
+ const { sample_id, snapshot, variant, cardKind } = d;
560
+ const dur = variant.timing?.execMs ?? variant.durationMs;
561
+ const durTag = dur ? `${(dur / 1000).toFixed(1)}s` : '—';
562
+ const titleSnippet = snapshot?.prompt
563
+ ? snapshot.prompt.slice(0, 80).replace(/\n/g, ' ')
564
+ : (lang === 'zh' ? '(无 prompt 快照)' : '(no prompt snapshot)');
565
+ return `
566
+ <span class="tv-dot tv-dot--${cardKind}"></span>
567
+ <code class="tv-id">${e(sample_id)}</code>
568
+ <span class="tv-title">${e(titleSnippet)}${snapshot?.prompt && snapshot.prompt.length > 80 ? '…' : ''}</span>
569
+ <span class="tv-meta">${durTag}</span>
570
+ `;
571
+ }
572
+ /** Pass 用例:单行折叠,点击展开看完整内容(design/trace/assertions)。 */
573
+ function renderPassRow(d, lang) {
574
+ return `<details class="tv-row tv-row--pass">
575
+ <summary class="tv-row-h">${renderCardHeader(d, lang)}</summary>
576
+ <div class="tv-row-body">${renderSubSections(d, lang)}</div>
577
+ </details>`;
578
+ }
579
+ /** Fail / tripwire 卡片:默认展开,顶部诊断区,sub-section 折叠。 */
580
+ function renderFailCard(d, lang) {
581
+ const tripwireBanner = d.cardKind === 'tripwire'
582
+ ? `<div class="tv-tripwire-banner">
583
+ <div class="tv-tripwire-banner-title">${lang === 'zh' ? '◆ 这是诱错样本' : '◆ This is a tripwire sample'}</div>
584
+ <div class="tv-tripwire-banner-body">${lang === 'zh'
585
+ ? '用户 prompt 故意诱导 LLM 走错。LLM 失败 = 已记录的 LLM 边界数据点,不是要修的 bug。跨版本对比这条用例的 pass 率,可观察 LLM 抗诱导能力变化。'
586
+ : 'The user prompt intentionally misdirects the LLM. A failure here is a recorded LLM-susceptibility data point — not a bug to fix.'}</div>
587
+ </div>`
588
+ : '';
589
+ const diagnosticHtml = renderDiagnosticSection(d.variant, 'fail', lang);
590
+ // 默认折叠(用户反馈:fail/tripwire 也别自动展开,跟 pass 行为一致)。
591
+ // 卡片头部加一行诊断 summary 摘要,折叠态也能看到失败原因不用展开。
592
+ const diagSummaryPreview = d.variant.diagnostic?.ok && d.variant.diagnostic.summary
593
+ ? `<div class="tv-card-preview">${e(d.variant.diagnostic.summary.slice(0, 120))}${d.variant.diagnostic.summary.length > 120 ? '…' : ''}</div>`
594
+ : '';
595
+ return `<details class="tv-card tv-card--${d.cardKind}">
596
+ <summary class="tv-card-h">${renderCardHeader(d, lang)}</summary>
597
+ ${diagSummaryPreview}
598
+ <div class="tv-card-body">
599
+ ${tripwireBanner}
600
+ ${diagnosticHtml}
601
+ ${renderSubSections(d, lang)}
602
+ </div>
603
+ </details>`;
604
+ }
605
+ // ──────────────────────────────────────────────────────────────────
606
+ // 主入口
607
+ // ──────────────────────────────────────────────────────────────────
608
+ export function renderTestView(report, lang) {
609
+ const variants = report.meta.variants || [];
610
+ const results = report.results || [];
611
+ const snapshots = report.sampleSnapshots ?? {};
612
+ if (variants.length === 0 || results.length === 0) {
613
+ return `<div class="tv-root"><div class="tv-empty-page">${lang === 'zh' ? '无 variant 或 result,无法渲染单测视角' : 'No variants or results to render'}</div></div>`;
614
+ }
615
+ const hasSnapshots = Object.keys(snapshots).length > 0;
616
+ const fallbackBanner = hasSnapshots ? '' : `<div class="tv-warn">${lang === 'zh' ? '⚠ 该 report 缺 sampleSnapshots(早期版本生成),只能展示执行结果,无设计信息' : '⚠ Report has no sampleSnapshots (older format); only execution results shown'}</div>`;
617
+ const variantSections = variants.map((variant, idx) => {
618
+ // 按 verdict 分三组:pass(单行) / fail(完整卡片) / tripwire(诱错卡片)。
619
+ const passData = [];
620
+ const failData = [];
621
+ const tripwireData = [];
622
+ for (const r of results) {
623
+ const v = r.variants[variant];
624
+ if (!v)
625
+ continue;
626
+ const d = buildCardData(r.sample_id, snapshots[r.sample_id], v);
627
+ if (d.cardKind === 'pass')
628
+ passData.push(d);
629
+ else if (d.cardKind === 'tripwire')
630
+ tripwireData.push(d);
631
+ else
632
+ failData.push(d);
633
+ }
634
+ const passSection = passData.length > 0
635
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '通过用例' : 'Passed'}</div>
636
+ <div class="tv-rows">${passData.map((d) => renderPassRow(d, lang)).join('')}</div>`
637
+ : '';
638
+ const failSection = failData.length > 0
639
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '失败用例' : 'Failed'}</div>
640
+ ${failData.map((d) => renderFailCard(d, lang)).join('')}`
641
+ : '';
642
+ const tripwireSection = tripwireData.length > 0
643
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '诱错样本' : 'Tripwire samples'}</div>
644
+ ${tripwireData.map((d) => renderFailCard(d, lang)).join('')}`
645
+ : '';
646
+ const variantStats = computeVariantStats(report, variant);
647
+ const tripwireStat = variantStats.tripwire > 0
648
+ ? `<span class="tv-stat tv-stat--tripwire"><span class="tv-stat-num">${variantStats.tripwire}</span>${lang === 'zh' ? '诱错触发' : 'tripwire'}</span>`
649
+ : '';
650
+ return `<div class="tv-variant-panel" data-variant="${e(variant)}" ${idx === 0 ? '' : 'hidden'}>
651
+ <div class="tv-variant-stats">
652
+ <span class="tv-stat tv-stat--pass"><span class="tv-stat-num">${variantStats.passed}</span>${lang === 'zh' ? '通过' : 'passed'}</span>
653
+ <span class="tv-stat tv-stat--fail"><span class="tv-stat-num">${variantStats.failed}</span>${lang === 'zh' ? '失败' : 'failed'}</span>
654
+ ${tripwireStat}
655
+ <span class="tv-stat"><span class="tv-stat-num">${variantStats.avgScore.toFixed(1)}</span>${lang === 'zh' ? '平均分' : 'avg / 5'}</span>
656
+ </div>
657
+ ${failSection}
658
+ ${tripwireSection}
659
+ ${passSection}
660
+ </div>`;
661
+ }).join('');
662
+ // 只 1 个 variant 时不显示切换器(SOLO 模式没对照,显示 tab 反而困惑)。
663
+ const variantTabsHtml = variants.length > 1
664
+ ? `<div class="tv-variant-tabs">${variants.map((variant, idx) => `<button type="button" class="tv-vtab ${idx === 0 ? 'tv-vtab--active' : ''}" data-target="${e(variant)}" onclick="omkSwitchVariant(this)">${e(variant)}</button>`).join('')}</div>`
665
+ : '';
666
+ // 整个 test view 包在 tv-root 里,scoped 浅色岛(覆盖外层深色 layout)。
667
+ return `<div class="tv-root">${fallbackBanner}
668
+ ${variantTabsHtml}
669
+ ${variantSections}</div>`;
670
+ }
671
+ function computeVariantStats(report, variant) {
672
+ const snapshots = report.sampleSnapshots ?? {};
673
+ let passed = 0;
674
+ let failed = 0;
675
+ let tripwire = 0;
676
+ let scoreSum = 0;
677
+ let scoreCount = 0;
678
+ for (const r of report.results) {
679
+ const v = r.variants[variant];
680
+ if (!v)
681
+ continue;
682
+ const verdict = sampleVerdict(v.assertions?.details);
683
+ if (verdict === 'pass') {
684
+ passed += 1;
685
+ }
686
+ else if (isTripwireSample(snapshots[r.sample_id], v)) {
687
+ tripwire += 1; // 诱错样本失败单独统计,不算"失败"也不算"通过"
688
+ }
689
+ else {
690
+ failed += 1;
691
+ }
692
+ if (typeof v.compositeScore === 'number') {
693
+ scoreSum += v.compositeScore;
694
+ scoreCount += 1;
695
+ }
696
+ }
697
+ return {
698
+ passed,
699
+ failed,
700
+ tripwire,
701
+ avgScore: scoreCount > 0 ? scoreSum / scoreCount : 0,
702
+ };
703
+ }
704
+ /** 单测视角的 CSS。
705
+ *
706
+ * 设计原则:
707
+ * - **scoped 浅色岛**:外层 layout 是深色主题(评分 tab 用),test view 在 .tv-root
708
+ * 里覆盖一套浅色变量,实现"切到单测 tab = 切到杂志阅读模式"。
709
+ * - **潘通启发的哑色调**:sage/dusty rose/mauve(饱和度 ~50%),不像 GitHub error log。
710
+ * - **大字号 + 高行高**:正文 15px / 行高 1.75,代码 13px。中文优先 PingFang/YaHei。
711
+ * - **subtle shadow 替代 border**:卡片间 16-24px 留白,box-shadow 浮起感。
712
+ * - **default-collapsed**:通过的用例只显一行;失败/诱错展开诊断,其余 section 折叠。
713
+ */
714
+ export const TEST_VIEW_CSS = `
715
+ /* tv-root: 浅色岛,scoped 覆盖外层深色 layout */
716
+ .tv-root {
717
+ --tv-bg:#fbfaf7; --tv-bg-card:#ffffff; --tv-bg-soft:rgba(58,58,58,.025);
718
+ --tv-text:#3a3a3a; --tv-text-2:#7a7a7a; --tv-text-3:#a8a8a8;
719
+ --tv-divider:#e8e3da;
720
+ /* 加大 pass / fail / tripwire 的色相距离 + 暗度差,避免远看雷同。
721
+ pass: 鼠尾草绿(暗一档,靠近林地);fail: 砖红土陶(暗一档,远离 pink);tripwire: 紫罗兰(独立色相)。 */
722
+ --tv-pass:#5e8252; --tv-fail:#9c4a3f; --tv-tripwire:#7a6b89;
723
+ --tv-pass-soft:rgba(94,130,82,.10); --tv-fail-soft:rgba(156,74,63,.10); --tv-tripwire-soft:rgba(122,107,137,.10);
724
+ --tv-accent:#5a7a93; --tv-warn:#b08030;
725
+ --tv-shadow:0 1px 3px rgba(58,58,58,.06);
726
+ --tv-shadow-hover:0 2px 8px rgba(58,58,58,.10);
727
+ background:var(--tv-bg); color:var(--tv-text);
728
+ padding:32px 0; margin:-16px 0 0; border-radius:6px;
729
+ font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;
730
+ font-size:15px; line-height:1.75;
731
+ }
732
+ /* Variant tabs — 显式 reset 浏览器默认 button 样式(border-radius / outline / appearance)
733
+ 防止 layout 那边的全局 button 规则给底部加了奇怪圆角和深色背景。 */
734
+ .tv-root .tv-variant-tabs { display:flex;gap:0;margin:0 0 24px;border-bottom:1px solid var(--tv-divider) }
735
+ .tv-root .tv-vtab { padding:12px 20px;background:transparent;border:none;border-radius:0;cursor:pointer;color:var(--tv-text-2);border-bottom:2px solid transparent;font-size:14px;font-family:inherit;font-weight:500;outline:none;appearance:none;-webkit-appearance:none;margin-bottom:-1px }
736
+ .tv-root .tv-vtab:hover { color:var(--tv-text) }
737
+ .tv-root .tv-vtab--active { color:var(--tv-text);border-bottom-color:var(--tv-accent);font-weight:600 }
738
+ .tv-root .tv-vtab:focus-visible { outline:2px solid var(--tv-accent);outline-offset:-4px;border-radius:4px }
739
+ /* tv-variant-panel: 不打横向 padding,内容直接对齐 .tv-root 边界,跟外层 omk-view-panel 的 h1/subtitle 同基线。 */
740
+ /* Stats — 更扁平的数字+标签布局 */
741
+ .tv-root .tv-variant-stats { display:flex;gap:32px;margin:0 0 24px;font-size:14px;color:var(--tv-text-2);align-items:baseline }
742
+ .tv-root .tv-stat { display:flex;align-items:baseline;gap:6px }
743
+ .tv-root .tv-stat-num { color:var(--tv-text);font-weight:600;font-size:18px;font-variant-numeric:tabular-nums }
744
+ .tv-root .tv-stat--pass .tv-stat-num { color:var(--tv-pass) }
745
+ .tv-root .tv-stat--fail .tv-stat-num { color:var(--tv-fail) }
746
+ .tv-root .tv-stat--tripwire .tv-stat-num { color:var(--tv-tripwire) }
747
+ /* Group separator */
748
+ .tv-root .tv-group-sep { display:flex;align-items:center;gap:14px;margin:32px 0 16px;color:var(--tv-text-3);font-size:12px;text-transform:uppercase;letter-spacing:0.08em;font-weight:500 }
749
+ .tv-root .tv-group-sep::after { content:'';flex:1;height:1px;background:var(--tv-divider) }
750
+ /* Pass rows — one-line list,左边一道淡 sage 色条做"通过"暗示 */
751
+ .tv-root .tv-rows { display:flex;flex-direction:column;gap:6px;margin:0 }
752
+ .tv-root .tv-row { background:var(--tv-bg-card);border-radius:6px;box-shadow:var(--tv-shadow);transition:box-shadow .15s;border-left:3px solid var(--tv-pass) }
753
+ .tv-root .tv-row:hover { box-shadow:var(--tv-shadow-hover) }
754
+ .tv-root .tv-row[open] { box-shadow:var(--tv-shadow-hover) }
755
+ .tv-root .tv-row-h { display:flex;align-items:center;gap:14px;padding:14px 20px;cursor:pointer;list-style:none;font-size:15px }
756
+ .tv-root .tv-row-h::-webkit-details-marker { display:none }
757
+ .tv-root .tv-row-h::after { content:'›';margin-left:8px;color:var(--tv-text-3);font-size:18px;transition:transform .15s }
758
+ .tv-root .tv-row[open] .tv-row-h::after { transform:rotate(90deg) }
759
+ /* Cards (fail / tripwire) — 默认折叠,跟 pass row 一致;头部下面加一行 preview 让折叠态也有信息密度 */
760
+ .tv-root .tv-card { background:var(--tv-bg-card);border-radius:8px;box-shadow:var(--tv-shadow);margin:0 0 12px;transition:box-shadow .15s }
761
+ .tv-root .tv-card:hover { box-shadow:var(--tv-shadow-hover) }
762
+ .tv-root .tv-card--fail { border-left:3px solid var(--tv-fail) }
763
+ .tv-root .tv-card--tripwire { border-left:3px solid var(--tv-tripwire) }
764
+ .tv-root .tv-card-h { display:flex;align-items:center;gap:14px;padding:16px 22px;cursor:pointer;list-style:none;font-size:15px }
765
+ .tv-root .tv-card-h::-webkit-details-marker { display:none }
766
+ .tv-root .tv-card-h::after { content:'›';margin-left:8px;color:var(--tv-text-3);font-size:18px;transition:transform .15s }
767
+ .tv-root .tv-card[open] .tv-card-h::after { transform:rotate(90deg) }
768
+ .tv-root .tv-card[open] .tv-card-h { border-bottom:1px solid var(--tv-divider) }
769
+ .tv-root .tv-card-preview { padding:0 22px 14px;color:var(--tv-text-2);font-size:14px;line-height:1.7;border-bottom:1px solid var(--tv-divider) }
770
+ .tv-root .tv-card[open] .tv-card-preview { display:none }
771
+ /* Status dots — 用形状 + 色 + 字符的三重区分,避免色盲 / 远看雷同。
772
+ pass: 实心圆 sage;fail: 红 ✗ 字符(更显眼);tripwire: 紫色 ◆ 菱形。 */
773
+ .tv-root .tv-dot { display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;font-size:14px;font-weight:700;line-height:1 }
774
+ .tv-root .tv-dot--pass::before { content:'';width:10px;height:10px;border-radius:50%;background:var(--tv-pass);box-shadow:0 0 0 3px var(--tv-pass-soft) }
775
+ .tv-root .tv-dot--fail::before { content:'✗';color:var(--tv-fail);font-size:16px;font-weight:700 }
776
+ .tv-root .tv-dot--tripwire::before { content:'◆';color:var(--tv-tripwire);font-size:13px }
777
+ .tv-root .tv-id { font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;font-size:13px;color:var(--tv-text-3) }
778
+ .tv-root .tv-title { flex:1;font-size:15px;color:var(--tv-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap }
779
+ .tv-root .tv-meta { font-size:13px;color:var(--tv-text-3);font-variant-numeric:tabular-nums;font-family:"SF Mono","JetBrains Mono",Menlo,monospace }
780
+ /* Body padding */
781
+ .tv-root .tv-card-body, .tv-root .tv-row-body { padding:22px }
782
+ /* Tripwire banner — 卡片顶部的紫色解释带 */
783
+ .tv-root .tv-tripwire-banner { margin:-22px -22px 20px;padding:18px 22px;background:rgba(139,124,147,.08);border-radius:8px 8px 0 0;border-bottom:1px solid var(--tv-divider) }
784
+ .tv-root .tv-tripwire-banner-title { font-size:14px;font-weight:600;color:var(--tv-tripwire);margin-bottom:6px;letter-spacing:0.02em }
785
+ .tv-root .tv-tripwire-banner-body { font-size:14px;color:var(--tv-text-2);line-height:1.7 }
786
+ /* Diagnosis — 一段白话 + 单一蓝色建议块 + 折叠细节 */
787
+ .tv-root .tv-diag { margin:4px 0 16px }
788
+ .tv-root .tv-diag-text { font-size:15px;line-height:1.75;color:var(--tv-text);margin:0 0 16px }
789
+ .tv-root .tv-suggestion { background:rgba(90,122,147,.06);border-left:3px solid var(--tv-accent);padding:14px 18px;margin:12px 0;border-radius:0 4px 4px 0 }
790
+ .tv-root .tv-suggestion-label { display:inline-block;color:var(--tv-accent);font-weight:600;font-size:13px;margin-right:8px;letter-spacing:0.02em }
791
+ .tv-root .tv-suggestion-body { margin:6px 0 0;font-size:14.5px;line-height:1.7;color:var(--tv-text) }
792
+ .tv-root .tv-diag-details { margin-top:14px }
793
+ .tv-root .tv-diag-details > summary { cursor:pointer;font-size:13px;color:var(--tv-text-2);list-style:none }
794
+ .tv-root .tv-diag-details > summary::-webkit-details-marker { display:none }
795
+ .tv-root .tv-diag-details > summary::before { content:'▸ ';color:var(--tv-text-3);display:inline-block;width:16px }
796
+ .tv-root .tv-diag-details[open] > summary::before { content:'▾ ' }
797
+ .tv-root .tv-diag-details-body { padding:10px 0 0 16px;font-size:14px;line-height:1.7;color:var(--tv-text-2) }
798
+ .tv-root .tv-diag-details-body p { margin:6px 0 }
799
+ .tv-root .tv-diag-details-body strong { color:var(--tv-text);margin-right:6px }
800
+ /* RootCause inline pill */
801
+ .tv-root .tv-diag-rc-pill { font-size:11px;padding:2px 8px;border-radius:10px;background:var(--tv-bg-soft);color:var(--tv-text-2);margin-right:6px;display:inline-block }
802
+ .tv-root .tv-diag-rc--tripwire_intentional { background:rgba(139,124,147,.12);color:var(--tv-tripwire) }
803
+ .tv-root .tv-diag-rc--sample_design { background:rgba(90,122,147,.10);color:var(--tv-accent) }
804
+ .tv-root .tv-diag-rc--skill_doc_unclear, .tv-diag-rc--skill_doc_missing, .tv-root .tv-diag-rc--llm_misread { background:rgba(176,128,48,.10);color:var(--tv-warn) }
805
+ /* Failure-mode chips — 中文标签,显著展示在 summary 下 */
806
+ .tv-root .tv-diag-modes { display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:8px 0 14px }
807
+ .tv-root .tv-diag-modes-label { font-size:12px;color:var(--tv-text-2);margin-right:4px }
808
+ .tv-root .tv-diag-mode-pill { font-size:12px;padding:3px 10px;border-radius:11px;background:var(--tv-bg-soft);color:var(--tv-text);font-weight:500 }
809
+ .tv-root .tv-diag-mode--workflow-skip { background:rgba(176,128,48,.12);color:var(--tv-warn) }
810
+ .tv-root .tv-diag-mode--hardcoded { background:rgba(176,128,48,.12);color:var(--tv-warn) }
811
+ .tv-root .tv-diag-mode--hallucination { background:rgba(180,75,75,.14);color:#b44b4b }
812
+ .tv-root .tv-diag-mode--tool-misuse { background:rgba(176,128,48,.12);color:var(--tv-warn) }
813
+ .tv-root .tv-diag-mode--env-blocked { background:rgba(90,122,147,.12);color:var(--tv-accent) }
814
+ .tv-root .tv-diag-mode--misread { background:rgba(176,128,48,.12);color:var(--tv-warn) }
815
+ .tv-root .tv-diag-mode--other { background:var(--tv-bg-soft);color:var(--tv-text-2) }
816
+ /* Workflow checklist — 默认展开,放在 suggestion 之上,直观展示 LLM 走对哪几步 */
817
+ .tv-root .tv-diag-workflow { margin:14px 0;padding:12px 14px;background:var(--tv-bg-soft);border-radius:6px }
818
+ .tv-root .tv-diag-workflow-title { font-size:13px;font-weight:600;color:var(--tv-text);margin:0 0 10px;letter-spacing:0.02em }
819
+ .tv-root .tv-diag-workflow-list { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px }
820
+ .tv-root .tv-diag-workflow-item { display:flex;align-items:flex-start;gap:10px;line-height:1.6 }
821
+ .tv-root .tv-diag-workflow-icon { flex-shrink:0;font-weight:700;font-size:14px;width:16px;text-align:center;margin-top:2px }
822
+ .tv-root .tv-diag-workflow-item--pass .tv-diag-workflow-icon { color:#3a8a3a }
823
+ .tv-root .tv-diag-workflow-item--fail .tv-diag-workflow-icon { color:#b44b4b }
824
+ .tv-root .tv-diag-workflow-body { display:flex;flex-direction:column;gap:2px;flex:1 }
825
+ .tv-root .tv-diag-workflow-step { font-size:14px;color:var(--tv-text) }
826
+ .tv-root .tv-diag-workflow-evidence { font-size:12.5px;color:var(--tv-text-2);line-height:1.55 }
827
+ .tv-root .tv-diag-workflow-item--fail .tv-diag-workflow-step { color:#b44b4b }
828
+ /* Partial salvage 提示 */
829
+ .tv-root .tv-diag-partial { font-size:12px;color:var(--tv-text-3);margin:0 0 8px;font-style:italic }
830
+ /* Sub-sections — design / trace / assertions, 全部默认折叠 */
831
+ .tv-root .tv-section { margin-top:6px;border-top:1px solid var(--tv-divider);padding-top:14px }
832
+ .tv-root .tv-section > summary { cursor:pointer;font-size:14px;font-weight:500;color:var(--tv-text);padding:6px 0;list-style:none }
833
+ .tv-root .tv-section > summary::-webkit-details-marker { display:none }
834
+ .tv-root .tv-section > summary::before { content:'▸ ';color:var(--tv-text-3);display:inline-block;width:16px;font-weight:400 }
835
+ .tv-root .tv-section[open] > summary::before { content:'▾ ' }
836
+ .tv-root .tv-section-stat { font-weight:400;color:var(--tv-text-3);margin-left:6px;font-size:13px }
837
+ .tv-root .tv-section-body { padding:14px 0 0 16px;font-size:14.5px;line-height:1.7 }
838
+ /* Design fields */
839
+ .tv-root .tv-prompt, .tv-root .tv-rubric { font-size:14.5px;line-height:1.75;margin:8px 0;color:var(--tv-text-2) }
840
+ .tv-root .tv-prompt strong, .tv-root .tv-rubric strong { color:var(--tv-text);margin-right:6px;font-weight:600 }
841
+ .tv-root .tv-meta-pills { margin:10px 0;display:flex;flex-wrap:wrap;gap:8px }
842
+ .tv-root .tv-meta-pill { font-size:12px;padding:3px 10px;background:var(--tv-bg-soft);border-radius:10px;color:var(--tv-text-2);font-family:"SF Mono",Menlo,monospace }
843
+ .tv-root .tv-expected { margin-top:12px }
844
+ .tv-root .tv-expected-h { font-size:13px;color:var(--tv-text-2);margin-bottom:6px }
845
+ .tv-root .tv-expect-line { font-size:13px;font-family:"SF Mono",Menlo,monospace;color:var(--tv-text-2);padding:3px 0;line-height:1.6 }
846
+ .tv-root .tv-collapsible { margin-top:10px;font-size:13px }
847
+ .tv-root .tv-collapsible > summary { cursor:pointer;color:var(--tv-text-2) }
848
+ /* Mocks */
849
+ .tv-root .tv-mocks { margin-top:8px }
850
+ .tv-root .tv-mock-line { font-size:12px;font-family:"SF Mono",Menlo,monospace;padding:3px 0;display:flex;flex-wrap:wrap;gap:6px;align-items:center;line-height:1.6 }
851
+ .tv-root .tv-mock-idx { color:var(--tv-text-3);font-weight:600 }
852
+ .tv-root .tv-mock-tool { background:var(--tv-bg-soft);padding:2px 6px;border-radius:3px }
853
+ .tv-root .tv-mock-match, .tv-root .tv-mock-return { color:var(--tv-text-2);overflow:hidden;text-overflow:ellipsis;max-width:340px;white-space:nowrap }
854
+ .tv-root .tv-mock-arrow { color:var(--tv-text-3) }
855
+ /* Trace turns */
856
+ .tv-root .tv-turn { margin:10px 0;padding:12px 16px;background:var(--tv-bg-soft);border-radius:4px;border-left:2px solid var(--tv-divider) }
857
+ .tv-root .tv-turn-h { font-size:14px;font-weight:500;color:var(--tv-text);margin-bottom:6px }
858
+ .tv-root .tv-turn-dur { color:var(--tv-text-3);font-weight:400;font-size:12px;margin-left:6px }
859
+ .tv-root .tv-thinking { margin:6px 0;font-size:13px }
860
+ .tv-root .tv-thinking summary { cursor:pointer;color:var(--tv-text-3);font-style:italic }
861
+ .tv-root .tv-thinking-body { padding:10px 14px;background:rgba(58,58,58,.04);border-radius:4px;color:var(--tv-text-2);white-space:pre-wrap;font-size:13px;line-height:1.7;margin-top:6px }
862
+ /* Tool calls */
863
+ .tv-root .tv-call { margin:6px 0;padding:10px 14px;background:rgba(58,58,58,.03);border-radius:4px;font-size:13px }
864
+ .tv-root .tv-call--err { border-left:2px solid var(--tv-fail) }
865
+ .tv-root .tv-call-h { font-weight:500;color:var(--tv-text);margin-bottom:4px;font-size:14px }
866
+ .tv-root .tv-call details { margin:4px 0;font-size:12px }
867
+ .tv-root .tv-call summary { cursor:pointer;color:var(--tv-text-3);padding:2px 0 }
868
+ .tv-root .tv-pre { font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;font-size:13px;background:rgba(58,58,58,.04);padding:10px 14px;border-radius:4px;overflow:auto;max-height:320px;white-space:pre-wrap;margin:4px 0;color:var(--tv-text-2);line-height:1.55 }
869
+ .tv-root .tv-mock-badge { font-size:12px;color:var(--tv-pass);background:rgba(125,155,118,.12);padding:2px 8px;border-radius:10px;margin-left:8px }
870
+ .tv-root .tv-trace-assert { font-size:13px;font-family:"SF Mono",Menlo,monospace;padding:3px 0;margin-top:6px;line-height:1.5 }
871
+ .tv-root .tv-trace-assert.tv-assert--fail { color:var(--tv-fail) }
872
+ .tv-root .tv-trace-assert.tv-assert--pass { color:var(--tv-text-2) }
873
+ /* Final answer */
874
+ .tv-root .tv-final { margin-top:14px }
875
+ .tv-root .tv-final-h { font-size:14px;font-weight:500;color:var(--tv-text);margin-bottom:6px }
876
+ /* Assertions */
877
+ .tv-root .tv-assert { font-size:14px;font-family:"SF Mono",Menlo,monospace;padding:5px 0;line-height:1.6 }
878
+ .tv-root .tv-assert--pass { color:var(--tv-text-2) }
879
+ .tv-root .tv-assert--fail { color:var(--tv-fail) }
880
+ .tv-root .tv-assert-value { background:rgba(58,58,58,.04);padding:2px 6px;border-radius:3px;margin:0 2px }
881
+ .tv-root .tv-turn-tag { font-size:12px;color:var(--tv-text-3);margin-left:8px;font-style:italic;font-family:-apple-system,sans-serif }
882
+ .tv-root .tv-turn-tag--ok { color:var(--tv-pass) }
883
+ .tv-root .tv-turn-tag--miss { color:var(--tv-fail) }
884
+ .tv-root .tv-assert-msg { font-size:13px;color:var(--tv-text-3);margin-left:20px;margin-top:3px }
885
+ /* Empty/warn states */
886
+ .tv-root .tv-empty, .tv-root .tv-empty-page { color:var(--tv-text-3);font-style:italic;padding:14px 0;font-size:14px }
887
+ .tv-root .tv-warn { color:var(--tv-warn);background:rgba(176,128,48,.08);padding:14px 18px;border-radius:4px;margin:8px 24px;font-size:14px;line-height:1.7 }
888
+ `;
889
+ /** 单测视角配套 JS:variant 切换 + view tab 切换。挂在 layout 末尾。 */
890
+ export const TEST_VIEW_JS = `
891
+ function omkSwitchVariant(btn) {
892
+ var target = btn.getAttribute('data-target');
893
+ var tabs = btn.parentElement.querySelectorAll('.tv-vtab');
894
+ tabs.forEach(function(t){ t.classList.toggle('tv-vtab--active', t === btn); });
895
+ var panels = btn.parentElement.parentElement.querySelectorAll('.tv-variant-panel');
896
+ panels.forEach(function(p){ p.hidden = (p.getAttribute('data-variant') !== target); });
897
+ }
898
+ function omkSwitchView(name) {
899
+ var tabs = document.querySelectorAll('.omk-view-tab');
900
+ tabs.forEach(function(t){ t.classList.toggle('omk-view-tab--active', t.getAttribute('data-view') === name); });
901
+ var panels = document.querySelectorAll('.omk-view-panel');
902
+ panels.forEach(function(p){ p.hidden = (p.getAttribute('data-view') !== name); });
903
+ }
904
+ `;
905
+ //# sourceMappingURL=test-view.js.map