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
@@ -2,8 +2,9 @@
2
2
  * HTML report renderer — orchestrates sub-modules.
3
3
  */
4
4
  import { e, fmtCost, fmtDuration, fmtKnownCost, COLORS, DEFAULT_LANG, t, layout } from './layout.js';
5
- import { renderAgentOverview, renderAnalysis, renderHumanAgreement, renderKnowledgeInteractionSection, renderPairwiseDiff, renderSaturationCurve, renderSummaryCards, renderVarianceComparisons, renderVerdictPill, levelLabel, levelTooltip, } from './summary.js';
5
+ import { renderAgentOverview, renderAnalysis, renderKnowledgeInteractionSection, renderMethodologyAudit, renderScoringModal, renderSummaryCards, renderVarianceComparisons, renderVerdictPill, levelLabel, levelTooltip, } from './summary.js';
6
6
  import { renderSampleTable } from './table.js';
7
+ import { renderTestView, TEST_VIEW_CSS, TEST_VIEW_JS } from './test-view.js';
7
8
  import { renderTrendsBody } from './trends.js';
8
9
  import { computeVerdict } from '../eval-core/verdict.js';
9
10
  // v0.21 B.4 — 列表页 status pill 用的 dot. PROGRESS/REGRESS 实心(强信号),
@@ -31,36 +32,6 @@ function improvementOf(baselineScore, skillScore) {
31
32
  const delta = ((skillScore - baselineScore) / baselineScore * 100).toFixed(0);
32
33
  return skillScore >= baselineScore ? `+${delta}%` : `${delta}%`;
33
34
  }
34
- function renderRuntimeTag(runtime, label, lang) {
35
- if (!runtime)
36
- return '';
37
- const versions = [];
38
- if (runtime.binary?.version)
39
- versions.push(`${lang === 'zh' ? '二进制' : 'binary'} ${runtime.binary.version}`);
40
- if (runtime.sdk?.version)
41
- versions.push(`sdk ${runtime.sdk.version}`);
42
- const versionText = versions.length > 0 ? ` · ${versions.map(e).join(' · ')}` : '';
43
- const title = lang === 'zh'
44
- ? [
45
- `executor=${runtime.executor}`,
46
- `model=${runtime.model}`,
47
- `kind=${runtime.kind}`,
48
- `system=${runtime.capabilities.systemPrompt}`,
49
- `cost=${runtime.capabilities.costUSD}`,
50
- `trace=${runtime.capabilities.trace}`,
51
- `skillIsolation=${runtime.capabilities.skillIsolation}`,
52
- ].join('; ')
53
- : [
54
- `executor=${runtime.executor}`,
55
- `model=${runtime.model}`,
56
- `kind=${runtime.kind}`,
57
- `system=${runtime.capabilities.systemPrompt}`,
58
- `cost=${runtime.capabilities.costUSD}`,
59
- `trace=${runtime.capabilities.trace}`,
60
- `skillIsolation=${runtime.capabilities.skillIsolation}`,
61
- ].join('; ');
62
- return `<span class="meta-tag" title="${e(title)}">${e(label)}: <code>${e(runtime.fingerprint)}</code>${versionText}</span>`;
63
- }
64
35
  function costCompletenessTooltip(lang) {
65
36
  return lang === 'zh'
66
37
  ? '部分 executor 或评委不回传 USD 成本。这里显示的是已上报成本下界,真实花费可能更高。'
@@ -84,33 +55,85 @@ function renderDebiasModeTag(modes, lang) {
84
55
  function pluralizeEn(count, singular, plural = `${singular}s`) {
85
56
  return `${count} ${count === 1 ? singular : plural}`;
86
57
  }
87
- function renderJudgeRuntimeTags(meta, lang) {
88
- if (meta.noJudge)
89
- return '';
90
- const entries = meta.judgeModels ?? [];
91
- if (entries.length === 0)
92
- return '';
93
- if (entries.length === 1) {
94
- return renderRuntimeTag(entries[0].runtime, lang === 'zh' ? '评委指纹' : 'Judge runtime', lang);
95
- }
96
- return entries
97
- .slice()
98
- .sort((a, b) => `${a.executor}:${a.model}`.localeCompare(`${b.executor}:${b.model}`))
99
- .map((entry) => renderRuntimeTag(entry.runtime, lang === 'zh' ? `评委指纹 ${entry.executor}:${entry.model}` : `Judge runtime ${entry.executor}:${entry.model}`, lang))
100
- .join('');
101
- }
102
- function renderExecutorRuntimeTags(meta, lang) {
58
+ function gatherRuntimeScopes(meta) {
59
+ const scopes = [];
103
60
  if (meta.executorRuntimes && Object.keys(meta.executorRuntimes).length > 0) {
104
- return Object.entries(meta.executorRuntimes)
61
+ Object.entries(meta.executorRuntimes)
105
62
  .sort(([a], [b]) => a.localeCompare(b))
106
- .map(([key, runtime]) => renderRuntimeTag(runtime, lang === 'zh' ? `执行器指纹 ${key}` : `Executor runtime ${key}`, lang))
107
- .join('');
63
+ .forEach(([key, runtime]) => {
64
+ if (runtime)
65
+ scopes.push({ role: 'executor', scope: key, runtime });
66
+ });
67
+ }
68
+ else if (meta.executorRuntime) {
69
+ scopes.push({ role: 'executor', scope: '', runtime: meta.executorRuntime });
70
+ }
71
+ if (!meta.noJudge) {
72
+ (meta.judgeModels ?? [])
73
+ .slice()
74
+ .sort((a, b) => `${a.executor}:${a.model}`.localeCompare(`${b.executor}:${b.model}`))
75
+ .forEach((entry) => {
76
+ if (entry.runtime)
77
+ scopes.push({ role: 'judge', scope: `${entry.executor}:${entry.model}`, runtime: entry.runtime });
78
+ });
79
+ }
80
+ return scopes;
81
+ }
82
+ function runtimeVersionText(runtime, lang) {
83
+ const versions = [];
84
+ if (runtime.binary?.version)
85
+ versions.push(`${lang === 'zh' ? '二进制' : 'binary'} ${runtime.binary.version}`);
86
+ if (runtime.sdk?.version)
87
+ versions.push(`sdk ${runtime.sdk.version}`);
88
+ return versions.length > 0 ? ` · ${versions.map(e).join(' · ')}` : '';
89
+ }
90
+ function runtimeTooltip(runtime) {
91
+ return [
92
+ `executor=${runtime.executor}`,
93
+ `model=${runtime.model}`,
94
+ `kind=${runtime.kind}`,
95
+ `system=${runtime.capabilities.systemPrompt}`,
96
+ `cost=${runtime.capabilities.costUSD}`,
97
+ `trace=${runtime.capabilities.trace}`,
98
+ `skillIsolation=${runtime.capabilities.skillIsolation}`,
99
+ ].join('; ');
100
+ }
101
+ // 同 fingerprint + 同 binary/sdk 版本 = 同一执行环境,合并为一条 tag。
102
+ // 旧版给每个 executor / judge 单独一条 pill,3 个角色用同一 binary 时
103
+ // fingerprint 重复 3 遍,扫读成本高。新版按 (fingerprint, versionText)
104
+ // 分组,scope 合并到 tag 内 "适用于 ..." 后缀。
105
+ function renderRuntimeFingerprintTags(meta, lang) {
106
+ const scopes = gatherRuntimeScopes(meta);
107
+ if (scopes.length === 0)
108
+ return '';
109
+ const groups = new Map();
110
+ for (const s of scopes) {
111
+ const versionText = runtimeVersionText(s.runtime, lang);
112
+ const key = `${s.runtime.fingerprint}|${versionText}`;
113
+ let g = groups.get(key);
114
+ if (!g) {
115
+ g = { runtime: s.runtime, versionText, executors: [], judges: [] };
116
+ groups.set(key, g);
117
+ }
118
+ if (s.role === 'executor')
119
+ g.executors.push(s.scope || (lang === 'zh' ? '默认' : 'default'));
120
+ else
121
+ g.judges.push(s.scope);
108
122
  }
109
- return renderRuntimeTag(meta.executorRuntime, lang === 'zh' ? '执行器指纹' : 'Executor runtime', lang);
123
+ const groupLabel = lang === 'zh' ? '执行环境指纹' : 'Runtime fingerprint';
124
+ return Array.from(groups.values()).map((g) => {
125
+ const scopeBits = [];
126
+ if (g.executors.length > 0)
127
+ scopeBits.push(`${lang === 'zh' ? '执行器' : 'executor'} ${g.executors.join(', ')}`);
128
+ if (g.judges.length > 0)
129
+ scopeBits.push(`${lang === 'zh' ? '评委' : 'judge'} ${g.judges.join(', ')}`);
130
+ const scopeText = scopeBits.length > 0 ? ` · ${lang === 'zh' ? '适用' : 'used by'} ${e(scopeBits.join('; '))}` : '';
131
+ return `<span class="meta-tag" title="${e(runtimeTooltip(g.runtime))}">${e(groupLabel)}: <code>${e(g.runtime.fingerprint)}</code>${g.versionText}${scopeText}</span>`;
132
+ }).join('');
110
133
  }
111
134
  export function renderRunList(runs, lang = DEFAULT_LANG) {
112
135
  const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
113
- const skillHealthLink = `<a href="/analyses${langQ}" style="color:var(--text-muted);font-size:12px;text-decoration:none;border:1px solid var(--border);padding:4px 10px;border-radius:var(--radius);display:inline-block">📊 <span data-i18n="skillHealthTitle">${t('skillHealthTitle', lang)}</span> →</a>`;
136
+ const skillHealthLink = `<a class="page-secondary-link" href="/analyses${langQ}">📊 <span data-i18n="skillHealthTitle">${t('skillHealthTitle', lang)}</span> →</a>`;
114
137
  if (!runs || runs.length === 0) {
115
138
  return layout(t('title', lang), `
116
139
  <main>
@@ -121,112 +144,85 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
121
144
  </main>
122
145
  `, lang);
123
146
  }
124
- const rows = runs.map((run) => {
147
+ // v0.30 — 卡片式列表(取代 7 列 dense table),每条占 4-5 行垂直空间但信息密度更可读。
148
+ // 顶部 verdict pill 主导视觉,id+date 是身份,scores 是成绩,底部 meta 是元数据,
149
+ // delete 默认隐藏在 hover 出现避免误点。批量报告共用同一组件,batch pill 替代 verdict。
150
+ const formatDateFromId = (id, fallbackTs) => {
151
+ const idMatch = id.match(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})$/);
152
+ if (idMatch)
153
+ return `${idMatch[2]}/${idMatch[3]} ${idMatch[4]}:${idMatch[5]}`;
154
+ return fallbackTs ? new Date(fallbackTs).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
155
+ };
156
+ const renderScoreBar = (label, score) => {
157
+ if (score == null)
158
+ return `<span class="rc-score-empty">${e(label)}: —</span>`;
159
+ const cls = score >= 4 ? 'pass' : score >= 3 ? 'warn' : 'fail';
160
+ const barW = Math.round((score / 5) * 100);
161
+ return `<div class="rc-score-row"><span class="rc-score-label" title="${e(label)}">${e(label)}</span>` +
162
+ `<div class="rc-score-bar"><div class="rc-score-bar-fill rc-score-bar-fill--${cls}" style="width:${barW}%"></div></div>` +
163
+ `<span class="rc-score-num rc-score-num--${cls}">${score.toFixed(2)}</span></div>`;
164
+ };
165
+ const cards = runs.map((run) => {
125
166
  if (run.kind === 'batch-evaluation') {
126
167
  const m = run.meta;
127
- const scoreCol = run.items.length > 0
168
+ const scores = run.items.length > 0
128
169
  ? run.items.map((item) => {
129
170
  const baselineScore = scoreOf(item.summary.baseline);
130
171
  const skillScore = scoreOf(item.summary[item.name]);
131
- const score = skillScore ?? baselineScore;
132
- if (score == null)
133
- return `<span style="color:var(--text-muted)">${e(item.name)}: -</span>`;
134
- const color = score >= 4 ? 'var(--green)' : score >= 3 ? 'var(--yellow)' : 'var(--red)';
135
- const barW = Math.round((score / 5) * 100);
136
- return `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">` +
137
- `<span title="${e(item.name)}" style="font-size:11px;color:var(--text-muted);width:56px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0">${e(item.name)}</span>` +
138
- `<div style="width:64px;height:6px;background:var(--bg-surface);border-radius:3px;flex-shrink:0">` +
139
- `<div style="width:${barW}%;height:100%;background:${color};border-radius:3px"></div></div>` +
140
- `<span style="font-size:12px;font-weight:600;color:${color};min-width:24px">${score.toFixed(2)}</span></div>`;
172
+ return renderScoreBar(item.name, skillScore ?? baselineScore);
141
173
  }).join('')
142
- : '<div style="color:var(--text-faint);font-size:0.6875rem;text-align:center">no score</div>';
143
- const allCostReported = run.items.every((item) => Object.values(item.summary || {}).every((v) => v.execCostReported !== false && v.judgeCostReported !== false));
144
- const totalCostReported = m.totalCostReported !== false && allCostReported;
174
+ : `<div class="rc-score-empty">${lang === 'zh' ? '无分数' : 'no score'}</div>`;
145
175
  const totalDurationMs = run.items.reduce((sum, item) => (sum + Object.values(item.summary || {}).reduce((inner, v) => inner + (v.avgDurationMs || 0) * (v.successCount || 0), 0)), 0);
146
176
  const statusPill = `<span class="run-status" title="${lang === 'zh' ? '批量评测' : 'batch evaluation'}"><span class="run-status-dot" aria-hidden="true">◇</span>${lang === 'zh' ? '批量' : 'Batch'}</span>`;
147
- return `<tr>
148
- <td>${statusPill}<a href="/reports/${e(run.id)}${langQ}"><span style="color:var(--text-primary)">${e(run.id)}</span><br><span style="font-size:0.6875rem;color:var(--text-muted)">${m.timestamp ? new Date(m.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : e(run.id)}</span></a></td>
149
- <td>${e(m.model || '-')}</td>
150
- <td>${m.sampleCount || 0}</td>
151
- <td>${scoreCol}</td>
152
- <td title="${totalCostReported ? '' : e(costCompletenessTooltip(lang))}">${fmtKnownCost(m.totalCostUSD || 0, totalCostReported)}</td>
153
- <td>${fmtDuration(totalDurationMs)}</td>
154
- <td style="white-space:nowrap"><button onclick="deleteRun('${e(run.id)}',this)" class="btn-danger" data-i18n="deleteBtnText">${t('deleteBtnText', lang)}</button></td>
155
- </tr>`;
177
+ const date = formatDateFromId(run.id, m.timestamp);
178
+ return `<a class="run-card" href="/reports/${e(run.id)}${langQ}">
179
+ <div class="run-card-h">
180
+ ${statusPill}
181
+ <span class="run-card-id">${e(run.id)}</span>
182
+ ${date ? `<span class="run-card-date">${e(date)}</span>` : ''}
183
+ </div>
184
+ <div class="run-card-meta">${e(m.model || '-')} · ${m.sampleCount || 0} ${lang === 'zh' ? '用例' : 'samples'} · ${run.items.length} skills</div>
185
+ <div class="run-card-scores">${scores}</div>
186
+ <div class="run-card-foot">
187
+ <span class="run-card-stat">${fmtDuration(totalDurationMs)}</span>
188
+ <button class="run-card-delete" onclick="event.preventDefault();event.stopPropagation();deleteRun('${e(run.id)}',this)" data-i18n="deleteBtnText" aria-label="${e(t('deleteBtnText', lang))}">×</button>
189
+ </div>
190
+ </a>`;
156
191
  }
157
192
  const m = run.meta;
158
- const hasScores = Object.values(run.summary || {}).some((s) => typeof s.avgCompositeScore === 'number' || typeof s.avgLlmScore === 'number');
159
- const scoreCol = hasScores
160
- ? Object.entries(run.summary || {}).map(([v, s]) => {
161
- const score = s.avgCompositeScore ?? s.avgLlmScore ?? null;
162
- if (score == null)
163
- return `<span style="color:var(--text-muted)">${e(v)}: -</span>`;
164
- const color = score >= 4 ? 'var(--green)' : score >= 3 ? 'var(--yellow)' : 'var(--red)';
165
- const barW = Math.round((score / 5) * 100);
166
- return `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">` +
167
- `<span title="${e(v)}" style="font-size:11px;color:var(--text-muted);width:56px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0">${e(v)}</span>` +
168
- `<div style="width:64px;height:6px;background:var(--bg-surface);border-radius:3px;flex-shrink:0">` +
169
- `<div style="width:${barW}%;height:100%;background:${color};border-radius:3px"></div></div>` +
170
- `<span style="font-size:12px;font-weight:600;color:${color};min-width:24px">${score}</span></div>`;
171
- }).join('')
172
- : '<div style="color:var(--text-faint);font-size:0.6875rem;text-align:center">no score</div>';
173
- const badges = ''; // TODO: artifact kind 体系完善后,按 kind 显示评测类型标签
174
- // v0.21 B.4 — 列表页 verdict pill: 一眼分辨 progress / regress / noise.
175
- // computeVerdict 是同步纯函数(report -> level),per row 跑成本 O(samples).
176
- // 脏报告可能让 layer-gates 访问 undefined.avgFactScore 抛 NPE。
177
- // try/catch 兜底,失败的 row 不显示 pill,避免一个坏 report 撤掉整页。
193
+ const summary = run.summary || {};
194
+ const hasScores = Object.values(summary).some((s) => typeof s.avgCompositeScore === 'number' || typeof s.avgLlmScore === 'number');
195
+ const scoresHtml = hasScores
196
+ ? Object.entries(summary).map(([v, s]) => renderScoreBar(v, s.avgCompositeScore ?? s.avgLlmScore ?? null)).join('')
197
+ : `<div class="rc-score-empty">${lang === 'zh' ? '无分数' : 'no score'}</div>`;
178
198
  let statusPill = '';
199
+ let verdictLvl = '';
179
200
  try {
180
201
  const verdict = computeVerdict(run);
181
- const lvl = verdict.level;
182
- statusPill = `<span class="run-status verdict-${lvl}" title="${e(levelTooltip(lvl, lang))}" aria-label="${e(levelLabel(lvl, lang))} — ${e(levelTooltip(lvl, lang))}"><span class="run-status-dot" aria-hidden="true">${levelDot(lvl)}</span>${e(levelLabel(lvl, lang))}</span>`;
202
+ verdictLvl = verdict.level;
203
+ statusPill = `<span class="run-status verdict-${verdictLvl}" title="${e(levelTooltip(verdictLvl, lang))}" aria-label="${e(levelLabel(verdictLvl, lang))} — ${e(levelTooltip(verdictLvl, lang))}"><span class="run-status-dot" aria-hidden="true">${levelDot(verdictLvl)}</span>${e(levelLabel(verdictLvl, lang))}</span>`;
183
204
  }
184
205
  catch { /* skip pill on this row */ }
185
- return `<tr>
186
- <td>${statusPill}<a href="/reports/${e(run.id)}${langQ}"><span style="color:var(--text-primary)">${e(run.id)}${badges}</span><br><span style="font-size:0.6875rem;color:var(--text-muted)">${(() => {
187
- // Extract date/time from report ID: ...-YYYYMMDD-HHmm
188
- const idMatch = run.id.match(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})$/);
189
- if (idMatch)
190
- return `${idMatch[2]}/${idMatch[3]} ${idMatch[4]}:${idMatch[5]}`;
191
- return m.timestamp ? new Date(m.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : e(run.id);
192
- })()}</span></a></td>
193
- <td>${e(m.model || '-')}</td>
194
- <td>${m.sampleCount || 0}</td>
195
- <td>${scoreCol}</td>
196
- <td>${(() => {
197
- const cost = Object.values(run.summary || {}).reduce((s, v) => s + (v.totalExecCostUSD || 0), 0);
198
- const reported = Object.values(run.summary || {}).every((v) => v.execCostReported !== false);
199
- return fmtCost(cost, reported);
200
- })()}</td>
201
- <td>${fmtDuration(Object.values(run.summary || {}).reduce((s, v) => s + (v.avgDurationMs || 0) * (v.successCount || 0), 0))}</td>
202
- <td style="white-space:nowrap"><button onclick="deleteRun('${e(run.id)}',this)" class="btn-danger" data-i18n="deleteBtnText">${t('deleteBtnText', lang)}</button></td>
203
- </tr>`;
206
+ const date = formatDateFromId(run.id, m.timestamp);
207
+ const variants = (m.variants || []).join(' · ');
208
+ const dur = Object.values(summary).reduce((s, v) => s + (v.avgDurationMs || 0) * (v.successCount || 0), 0);
209
+ return `<a class="run-card${verdictLvl ? ` verdict-${verdictLvl}` : ''}" href="/reports/${e(run.id)}${langQ}">
210
+ <div class="run-card-h">
211
+ ${statusPill}
212
+ <span class="run-card-id">${e(run.id)}</span>
213
+ ${date ? `<span class="run-card-date">${e(date)}</span>` : ''}
214
+ </div>
215
+ <div class="run-card-meta">${e(m.model || '-')} · ${m.sampleCount || 0} ${lang === 'zh' ? '用例' : 'samples'}${variants ? ` · ${e(variants)}` : ''}</div>
216
+ <div class="run-card-scores">${scoresHtml}</div>
217
+ <div class="run-card-foot">
218
+ <span class="run-card-stat">${fmtDuration(dur)}</span>
219
+ <button class="run-card-delete" onclick="event.preventDefault();event.stopPropagation();deleteRun('${e(run.id)}',this)" data-i18n="deleteBtnText" aria-label="${e(t('deleteBtnText', lang))}">×</button>
220
+ </div>
221
+ </a>`;
204
222
  }).join('');
205
223
  const runCount = lang === 'zh' ? `${runs.length} 次评测` : `${runs.length} runs`;
206
- // 列表累计:只 sum 那些"全 variant 都报告了 cost"的 run,跳过任一 variant not reported 的。
207
- // 排除 not reported(而不是把整体压成「—」)是因为列表常含数十个 run 仅 1-2 个是 codex,
208
- // 把全部 sum 抹成「—」会丢掉绝大多数有效成本信息。
224
+ // v0.30 — 列表页移除累计金额展示(用户反馈无须暴露);保留 evaluationRuns 用于 trend links
209
225
  const evaluationRuns = runs.filter(isEvaluationReport);
210
- const reportableRuns = evaluationRuns.filter((r) => Object.values(r.summary || {}).every((v) => v.execCostReported !== false));
211
- const unmeasuredRunsCount = evaluationRuns.length - reportableRuns.length;
212
- const totalCost = reportableRuns.reduce((s, r) => s + Object.values(r.summary || {}).reduce((sv, v) => sv + (v.totalExecCostUSD || 0), 0), 0);
213
- // partial:有 reported 就显示数字 + tooltip;全部 not reported 才显示「—」;
214
- // 全 reported(unmeasuredRunsCount=0)走老格式不包 span,保持 HTML snapshot 向后兼容。
215
- // X/N 限定信息只放 tooltip,不挂在显眼位置 — 累计行视觉应该是单纯的数字。
216
- const allUnmeasured = reportableRuns.length === 0 && runs.length > 0;
217
- const baseLabel = lang === 'zh' ? '累计' : 'Total';
218
- const costNumber = fmtCost(totalCost, !allUnmeasured);
219
- let costLabel;
220
- if (unmeasuredRunsCount === 0) {
221
- // 全 reported → 跟旧版字节级一致,不破 snapshot
222
- costLabel = `${baseLabel} ${costNumber}`;
223
- }
224
- else {
225
- const tip = lang === 'zh'
226
- ? `${reportableRuns.length}/${evaluationRuns.length} 次单次报告了 USD 成本;${unmeasuredRunsCount} 次 executor 不报(如 codex CLI),batch index 不计入累计以避免重复`
227
- : `${reportableRuns.length}/${evaluationRuns.length} evaluation reports reported USD cost; ${unmeasuredRunsCount} excluded (executor doesn't report, e.g. codex CLI); batch indexes are excluded to avoid double counting`;
228
- costLabel = `<span title="${e(tip)}">${baseLabel} ${costNumber}</span>`;
229
- }
230
226
  // Collect variants with ≥2 reports for trend links
231
227
  const variantCounts = {};
232
228
  for (const run of evaluationRuns) {
@@ -244,36 +240,62 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
244
240
  return layout(t('title', lang), `
245
241
  <main>
246
242
  <h1>${t('title', lang)}</h1>
247
- <p class="subtitle" data-i18n="subtitle">${t('subtitle', lang)} &middot; ${runCount} &middot; ${costLabel}</p>
243
+ <p class="subtitle" data-i18n="subtitle">${t('subtitle', lang)} &middot; ${runCount}</p>
248
244
  ${trendsSection}
249
245
  <div style="margin:12px 0">${skillHealthLink}</div>
250
246
  <div style="margin:12px 0;display:flex;gap:8px;align-items:center">
251
247
  <input id="filter-input" type="text" placeholder="${lang === 'zh' ? '搜索报告名称、变体...' : 'Filter by name, variant...'}" style="flex:1;max-width:320px;padding:6px 10px;font-size:13px;background:var(--bg-surface);color:var(--text-primary);border:1px solid var(--border);border-radius:var(--radius);outline:none" oninput="filterTable(this.value)">
252
248
  <span id="filter-count" style="font-size:11px;color:var(--text-muted)"></span>
253
249
  </div>
254
- <div class="table-wrap">
255
- <table id="report-table">
256
- <thead><tr>
257
- <th data-i18n="runId">${t('runId', lang)}</th>
258
- <th data-i18n="model">${t('model', lang)}</th>
259
- <th data-i18n="samples">${t('samples', lang)}</th>
260
- <th data-i18n="score">${t('score', lang)}</th>
261
- <th data-i18n="cost">${t('cost', lang)}</th>
262
- <th>${lang === 'zh' ? '耗时' : 'Duration'}</th>
263
- <th></th>
264
- </tr></thead>
265
- <tbody>${rows}</tbody>
266
- </table>
267
- </div>
250
+ ${renderScoringModal('guide-scoring-list', lang)}
251
+ <div id="run-list" class="run-list">${cards}</div>
252
+ <style>
253
+ /* 列表页 — 卡片式布局,顶部 verdict pill 主导,底部 hover 露出 delete */
254
+ .page-secondary-link { color:var(--text-secondary);font-size:13px;text-decoration:none;border:1px solid var(--border);padding:6px 14px;border-radius:var(--radius);display:inline-block;background:var(--bg-surface);transition:border-color .15s,color .15s }
255
+ .page-secondary-link:hover { color:var(--text-primary);border-color:var(--border-hover);text-decoration:none }
256
+ .run-list { display:flex;flex-direction:column;gap:12px;margin:16px 0 }
257
+ /* verdict 用左边粗带 + 浅色填充背景双重视觉提示,远看也能区分。
258
+ PROGRESS = sage 绿(进步),REGRESS = 砖红(回归),CAUTIOUS = 琥珀(警示),其他 = 灰(无信号) */
259
+ .run-card { display:block;padding:18px 22px;background:var(--bg-surface);border-radius:var(--radius);box-shadow:var(--shadow-sm);text-decoration:none;color:inherit;transition:box-shadow .15s,transform .05s;border-left:5px solid transparent;position:relative }
260
+ .run-card:hover { box-shadow:var(--shadow-md);text-decoration:none }
261
+ .run-card:hover .run-card-delete { opacity:1 }
262
+ .run-card.verdict-PROGRESS { border-left-color:var(--green);background:linear-gradient(90deg,var(--green-bg) 0%,var(--bg-surface) 80%) }
263
+ .run-card.verdict-REGRESS { border-left-color:var(--red);background:linear-gradient(90deg,var(--red-bg) 0%,var(--bg-surface) 80%) }
264
+ .run-card.verdict-CAUTIOUS { border-left-color:var(--yellow);background:linear-gradient(90deg,var(--yellow-bg) 0%,var(--bg-surface) 80%) }
265
+ .run-card.verdict-NOISE, .run-card.verdict-UNDERPOWERED { border-left-color:var(--text-faint);opacity:0.85 }
266
+ .run-card.verdict-SOLO { border-left-color:var(--accent);background:linear-gradient(90deg,var(--info-bg) 0%,var(--bg-surface) 80%) }
267
+ .run-card-h { display:flex;align-items:center;gap:12px;margin-bottom:6px;flex-wrap:wrap }
268
+ .run-card-id { font-size:15px;font-weight:600;color:var(--text-primary);font-family:"SF Mono",Menlo,Consolas,monospace;letter-spacing:-0.01em }
269
+ .run-card-date { font-size:12px;color:var(--text-muted);font-variant-numeric:tabular-nums;margin-left:auto }
270
+ .run-card-meta { font-size:13px;color:var(--text-secondary);margin:6px 0 12px;line-height:1.6 }
271
+ .run-card-scores { display:flex;flex-direction:column;gap:4px;margin:8px 0 }
272
+ .run-card-foot { display:flex;gap:16px;margin-top:10px;align-items:center;font-size:12px;color:var(--text-muted);font-variant-numeric:tabular-nums }
273
+ .run-card-stat { font-family:"SF Mono",Menlo,monospace }
274
+ .run-card-delete { margin-left:auto;background:transparent;border:none;color:var(--text-muted);font-size:18px;line-height:1;width:28px;height:28px;border-radius:14px;cursor:pointer;opacity:0;transition:opacity .15s,background .15s,color .15s;outline:none;appearance:none }
275
+ .run-card-delete:hover { background:var(--red-bg);color:var(--red) }
276
+ /* 分数行(每个 variant 一行) */
277
+ .rc-score-row { display:flex;align-items:center;gap:10px;font-size:13px;line-height:1.6 }
278
+ .rc-score-label { font-size:12px;color:var(--text-secondary);min-width:80px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0 }
279
+ .rc-score-bar { width:120px;height:6px;background:var(--bg-soft);border-radius:3px;flex-shrink:0;overflow:hidden }
280
+ .rc-score-bar-fill { height:100%;border-radius:3px;transition:width .2s }
281
+ .rc-score-bar-fill--pass { background:var(--green) }
282
+ .rc-score-bar-fill--warn { background:var(--yellow) }
283
+ .rc-score-bar-fill--fail { background:var(--red) }
284
+ .rc-score-num { font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;font-family:"SF Mono",Menlo,monospace;min-width:36px }
285
+ .rc-score-num--pass { color:var(--green) }
286
+ .rc-score-num--warn { color:var(--yellow) }
287
+ .rc-score-num--fail { color:var(--red) }
288
+ .rc-score-empty { color:var(--text-faint);font-size:12px;font-style:italic }
289
+ </style>
268
290
  <script>
269
291
  function filterTable(q) {
270
- var rows = document.querySelectorAll('#report-table tbody tr');
292
+ var cards = document.querySelectorAll('#run-list .run-card');
271
293
  var lower = q.toLowerCase();
272
294
  var shown = 0;
273
- rows.forEach(function(row) {
274
- var text = row.textContent.toLowerCase();
295
+ cards.forEach(function(card) {
296
+ var text = card.textContent.toLowerCase();
275
297
  var match = !q || text.indexOf(lower) !== -1;
276
- row.style.display = match ? '' : 'none';
298
+ card.style.display = match ? '' : 'none';
277
299
  if (match) shown++;
278
300
  });
279
301
  var countEl = document.getElementById('filter-count');
@@ -285,7 +307,7 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
285
307
  fetch('/api/reports/' + encodeURIComponent(id), { method: 'DELETE' })
286
308
  .then(function(r) { return r.json(); })
287
309
  .then(function(d) {
288
- if (d.ok) { btn.closest('tr').remove(); }
310
+ if (d.ok) { btn.closest('.run-card').remove(); }
289
311
  else { alert(I18N[lang].deleteFail + ': ' + (d.error || 'unknown')); }
290
312
  })
291
313
  .catch(function(err) { alert(I18N[lang].deleteFail + ': ' + err.message); });
@@ -309,9 +331,7 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
309
331
  const summary = report.summary || {};
310
332
  const results = report.results || [];
311
333
  const cards = renderSummaryCards(variants, summary, lang, report.variance);
312
- const pairwiseDiff = renderPairwiseDiff(report.meta.pairComparisons, lang);
313
- const humanAgreement = renderHumanAgreement(report.meta.humanAgreement, lang);
314
- const saturationCurve = renderSaturationCurve(report.variance?.saturation, variants, lang);
334
+ const methodologyAudit = renderMethodologyAudit(report, variants, summary, lang);
315
335
  const verdictPill = renderVerdictPill(report, lang);
316
336
  const sampleTable = renderSampleTable(variants, results, lang);
317
337
  const totalExecCost = Object.values(summary).reduce((s, v) => s + (v.totalExecCostUSD || 0), 0);
@@ -336,52 +356,41 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
336
356
  zh: { baseline: '基线', skill: 'Skill', prompt: 'Prompt', agent: 'Agent', workflow: 'Workflow' },
337
357
  en: { baseline: 'Baseline', skill: 'Skill', prompt: 'Prompt', agent: 'Agent', workflow: 'Workflow' },
338
358
  };
359
+ // v0.30 — 实验配置弱化:单行 inline 摘要,跟 Agent 执行概览并排放(context-strip)。
360
+ // 每变体一行:左 3px 颜色条 + variant 名 + type/kind/source/strategy/cwd 用 · 分隔。
361
+ // 完整 key 名通过 hover tooltip 提供。视觉权重远低于综合分 hero。
339
362
  const variantConfigRows = (m.variantConfigs || []).map((config, i) => {
340
363
  const expTypeRaw = config.experimentType || '-';
341
364
  const expType = (typeLabels[lang] || typeLabels.en)[String(expTypeRaw)] || expTypeRaw;
342
365
  const source = config.artifactKind === 'baseline'
343
- ? (lang === 'zh' ? '无' : 'None')
366
+ ? (lang === 'zh' ? '无来源' : 'no source')
344
367
  : (sourceLabels[lang] || sourceLabels.en)[config.artifactSource] || config.artifactSource;
345
368
  const strategy = (strategyLabels[lang] || strategyLabels.en)[config.executionStrategy] || config.executionStrategy;
346
369
  const cwdRaw = config.cwd || '';
347
370
  const runtimeContext = cwdRaw
348
371
  ? cwdRaw.replace(/.*\/Projects\//, '').replace(/.*\/Documents\//, '').replace(/\/Users\/[^/]+\//, '~/')
349
- : (lang === 'zh' ? '默认' : 'default');
372
+ : (lang === 'zh' ? '默认 cwd' : 'default cwd');
350
373
  const color = COLORS[i % COLORS.length];
351
- return `<tr>
352
- <td style="border-left:3px solid ${color};padding-left:12px"><strong>${e(config.variant)}</strong></td>
353
- <td>${e(expType)}</td>
354
- <td>${e((artifactKindLabels[lang] || artifactKindLabels.en)[config.artifactKind] || config.artifactKind)}</td>
355
- <td>${e(source)}</td>
356
- <td>${e(strategy)}</td>
357
- <td title="${e(cwdRaw)}">${e(runtimeContext)}</td>
358
- </tr>`;
374
+ const artifactKindLabel = e((artifactKindLabels[lang] || artifactKindLabels.en)[config.artifactKind] || config.artifactKind);
375
+ const tooltip = `${t('variantType', lang)}: ${expType} · ${t('variantArtifactKind', lang)}: ${artifactKindLabel} · ${t('variantArtifactSource', lang)}: ${source} · ${t('variantExecutionStrategy', lang)}: ${strategy} · ${t('variantRuntimeContext', lang)}: ${cwdRaw || (lang === 'zh' ? '默认' : 'default')}`;
376
+ return `<div class="ctx-row" style="border-left:3px solid ${color}" title="${e(tooltip)}">
377
+ <span class="ctx-row-name">${e(config.variant)}</span>
378
+ <span class="ctx-row-bits">${e(expType)} <span class="ctx-sep">·</span> ${artifactKindLabel} <span class="ctx-sep">·</span> ${e(source)} <span class="ctx-sep">·</span> ${e(strategy)} <span class="ctx-sep">·</span> ${e(runtimeContext)}</span>
379
+ </div>`;
359
380
  }).join('');
360
381
  const configModalId = 'guide-variant-config';
361
382
  const repeatSuffix = report.variance
362
383
  ? (lang === 'zh' ? ` × ${report.variance.runs} 轮` : ` × ${report.variance.runs} runs`)
363
384
  : '';
364
385
  const experimentSummary = lang === 'zh'
365
- ? `${m.sampleCount} 个评测用例 × ${variants.length} 组实验${repeatSuffix}`
386
+ ? `${m.sampleCount} 个用例 × ${variants.length} 组实验${repeatSuffix}`
366
387
  : `${pluralizeEn(m.sampleCount, 'sample')} × ${pluralizeEn(variants.length, 'variant')}${repeatSuffix}`;
367
388
  const variantConfigSection = variantConfigRows ? `
368
- <section style="margin:20px 0">
369
- <h2 style="display:flex;align-items:center;gap:4px">${t('variantConfig', lang)} <button type="button" class="hint-btn" onclick="openModal('${configModalId}')" aria-label="${e(t('variantConfigDesc', lang))}" aria-haspopup="dialog">?</button></h2>
370
- <p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">${experimentSummary}</p>
371
- <div class="table-wrap">
372
- <table>
373
- <thead><tr>
374
- <th>🏷️ ${t('variants', lang)}</th>
375
- <th>🧪 ${t('variantType', lang)}</th>
376
- <th>📦 ${t('variantArtifactKind', lang)}</th>
377
- <th>📂 ${t('variantArtifactSource', lang)}</th>
378
- <th>⚙️ ${t('variantExecutionStrategy', lang)}</th>
379
- <th>🖥️ ${t('variantRuntimeContext', lang)}</th>
380
- </tr></thead>
381
- <tbody>${variantConfigRows}</tbody>
382
- </table>
383
- </div>
384
- </section>
389
+ <div class="ctx-block">
390
+ <div class="ctx-h">${t('variantConfig', lang)} <button type="button" class="hint-btn" onclick="openModal('${configModalId}')" aria-label="${e(t('variantConfigDesc', lang))}" aria-haspopup="dialog">?</button></div>
391
+ <div class="ctx-sub">${experimentSummary}</div>
392
+ <div class="ctx-rows">${variantConfigRows}</div>
393
+ </div>
385
394
  <div id="${configModalId}" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="${configModalId}-title" onclick="if(event.target===this)closeModal('${configModalId}')">
386
395
  <div class="modal-content">
387
396
  <div class="modal-header">
@@ -409,14 +418,20 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
409
418
  </div>
410
419
  </div>
411
420
  ` : '';
412
- return layout(`${report.id}`, `
413
- <main>
414
- <nav class="nav"><a href="/${langQ}" data-i18n="backToList">${t('backToList', lang)}</a></nav>
415
- <h1>${e(report.id)}</h1>
416
- ${verdictPill}
417
- <div class="meta-tags">
418
- <span class="meta-tag">${t('model', lang)}: ${e(m.model)}</span>
419
- ${(() => {
421
+ // v0.30 重设计 — 评分/单测各自独立 title + meta strip(不共享头部)。
422
+ // 顶部只保留极简身份带:返回链接 + run-id pill。完整 H1 + meta tags 移进各自 tab 面板。
423
+ const idStamp = (() => {
424
+ const idMatch = /^.+?-(\d{8})-(\d{4})$/.exec(report.id);
425
+ if (idMatch) {
426
+ const [, ymd, hm] = idMatch;
427
+ return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)} ${hm.slice(0, 2)}:${hm.slice(2, 4)}`;
428
+ }
429
+ return '';
430
+ })();
431
+ // ──────────── 评分 tab 的 meta tags(原版全部留这里) ────────────
432
+ const scoreMetaTags = `<div class="meta-tags">
433
+ <span class="meta-tag">${t('model', lang)}: ${e(m.model)}</span>
434
+ ${(() => {
420
435
  if (m.noJudge)
421
436
  return `<span class="meta-tag">${t('judge', lang)}: none</span>`;
422
437
  const list = m.judgeModels ?? [];
@@ -426,39 +441,125 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
426
441
  return `<span class="meta-tag">${t('judge', lang)}: ${e(`${list[0].executor}:${list[0].model}`)}</span>`;
427
442
  return `<span class="meta-tag" title="${t('ensembleDesc', lang)}">${t('judgeModelsLabel', lang)}: ${list.map((j) => e(`${j.executor}:${j.model}`)).join(' · ')}</span>`;
428
443
  })()}
429
- ${m.judgeRepeat && m.judgeRepeat > 1 ? `<span class="meta-tag" title="${t('judgeStddevDesc', lang)}">${t('judgeRepeatLabel', lang)}: ${m.judgeRepeat}</span>` : ''}
430
- <span class="meta-tag">${t('executor', lang)}: ${e(m.executor || 'claude')}</span>
431
- <span class="meta-tag"${execCostReported ? '' : ` title="${e(lang === 'zh' ? 'executor 不报 USD 成本(如 codex CLI),无法估算' : 'executor does not report USD cost (e.g. codex CLI); not measurable')}"`}>${t('cost', lang)}: ${fmtCost(totalExecCost, execCostReported)}</span>
432
- <span class="meta-tag"${totalCostReported ? '' : ` title="${e(costCompletenessTooltip(lang))}"`}>${t('totalCost', lang)}: ${fmtKnownCost(m.totalCostUSD, totalCostReported)}</span>
433
- <span class="meta-tag">${lang === 'zh' ? '耗时' : 'Duration'}: ${fmtDuration(totalDurationMs)}</span>
434
- ${m.gitInfo ? `<span class="meta-tag">${lang === 'zh' ? '提交' : 'commit'}: ${e(m.gitInfo.commitShort)}${m.gitInfo.dirty ? '*' : ''} (${e(m.gitInfo.branch)})</span>` : ''}
435
- ${m.judgePromptHash ? `<span class="meta-tag" title="${t('judgePromptHashDesc', lang)}">${t('judgePromptHashLabel', lang)}: <code>${e(m.judgePromptHash)}</code></span>` : ''}${renderExecutorRuntimeTags(m, lang)}${renderJudgeRuntimeTags(m, lang)}
436
- ${m.sampleHashes ? `<span class="meta-tag" style="color:var(--text-muted)" title="${t('sampleHashCountDesc', lang)}">${t('sampleHashCount', lang)}: ${Object.keys(m.sampleHashes).length}/${m.sampleCount}</span>` : ''}
437
- ${m.evaluationFramework ? `<span class="meta-tag" title="${t('evalFrameworkDesc', lang)}">${t('evalFrameworkLabel', lang)}: ${m.evaluationFramework === 'bootstrap' ? t('evalFrameworkBootstrap', lang) : m.evaluationFramework === 'both' ? t('evalFrameworkBoth', lang) : t('evalFrameworkTTest', lang)}</span>` : ''}
438
- ${renderDebiasModeTag(m.debiasMode, lang)}
439
- ${m.blind ? `<span class="meta-tag" style="color:var(--green)" data-i18n="blindLabel">${t('blindLabel', lang)}</span>` : ''}
440
- </div>
441
- ${m.blind ? `
444
+ ${m.judgeRepeat && m.judgeRepeat > 1 ? `<span class="meta-tag" title="${t('judgeStddevDesc', lang)}">${t('judgeRepeatLabel', lang)}: ${m.judgeRepeat}</span>` : ''}
445
+ <span class="meta-tag">${t('executor', lang)}: ${e(m.executor || 'claude')}</span>
446
+ ${m.effort ? `<span class="meta-tag" title="${e(lang === 'zh' ? 'executor LLM 的扩展思考预算(--effort)。跨 effort 报告不可严格比较' : 'reasoning effort for executor LLM (--effort); reports across different efforts are not strictly comparable')}">effort: ${e(m.effort)}</span>` : ''}
447
+ <span class="meta-tag"${execCostReported ? '' : ` title="${e(lang === 'zh' ? 'executor 不报 USD 成本(如 codex CLI),无法估算' : 'executor does not report USD cost (e.g. codex CLI); not measurable')}"`}>${t('cost', lang)}: ${fmtCost(totalExecCost, execCostReported)}</span>
448
+ <span class="meta-tag"${totalCostReported ? '' : ` title="${e(costCompletenessTooltip(lang))}"`}>${t('totalCost', lang)}: ${fmtKnownCost(m.totalCostUSD, totalCostReported)}</span>
449
+ <span class="meta-tag">${lang === 'zh' ? '耗时' : 'Duration'}: ${fmtDuration(totalDurationMs)}</span>
450
+ ${m.gitInfo ? `<span class="meta-tag">${lang === 'zh' ? '提交' : 'commit'}: ${e(m.gitInfo.commitShort)}${m.gitInfo.dirty ? '*' : ''} (${e(m.gitInfo.branch)})</span>` : ''}
451
+ ${m.sampleHashes ? `<span class="meta-tag" title="${t('sampleHashCountDesc', lang)}">${t('sampleHashCount', lang)}: ${Object.keys(m.sampleHashes).length}/${m.sampleCount}</span>` : ''}
452
+ ${m.evaluationFramework ? `<span class="meta-tag" title="${t('evalFrameworkDesc', lang)}">${t('evalFrameworkLabel', lang)}: ${m.evaluationFramework === 'bootstrap' ? t('evalFrameworkBootstrap', lang) : m.evaluationFramework === 'both' ? t('evalFrameworkBoth', lang) : t('evalFrameworkTTest', lang)}</span>` : ''}
453
+ ${renderDebiasModeTag(m.debiasMode, lang)}
454
+ ${m.blind ? `<span class="meta-tag" style="color:var(--green)" data-i18n="blindLabel">${t('blindLabel', lang)}</span>` : ''}
455
+ </div>`;
456
+ const auditFingerprints = (() => {
457
+ const auditTags = [
458
+ m.judgePromptHash ? `<span class="meta-tag" title="${t('judgePromptHashDesc', lang)}">${t('judgePromptHashLabel', lang)}: <code>${e(m.judgePromptHash)}</code></span>` : '',
459
+ renderRuntimeFingerprintTags(m, lang),
460
+ ].join('');
461
+ if (!auditTags)
462
+ return '';
463
+ return `<details class="audit-fingerprints"><summary>${lang === 'zh' ? '审计指纹(用于复现校验)' : 'Audit fingerprints (for reproducibility)'}</summary><div class="meta-tags">${auditTags}</div></details>`;
464
+ })();
465
+ const blindReveal = m.blind ? `
442
466
  <div style="margin:12px 0">
443
467
  <button onclick="document.getElementById('blind-reveal').style.display=document.getElementById('blind-reveal').style.display==='none'?'block':'none'" data-i18n="revealBlind">${t('revealBlind', lang)}</button>
444
468
  <div id="blind-reveal" style="display:none;margin-top:8px;padding:12px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius)" role="region" aria-label="Blind variant mapping">
445
469
  ${Object.entries(m.blindMap || {}).map(([label, real]) => `<div style="font-size:13px;color:var(--text-secondary)"><strong>Variant ${e(label)}</strong> → ${e(real)}</div>`).join('')}
446
470
  </div>
447
- </div>` : ''}
471
+ </div>` : '';
472
+ // ──────────── 单测 tab 的 meta tags(只有 sample 维度数据,不含 cost / model) ────────────
473
+ const sampleSnaps = report.sampleSnapshots ?? {};
474
+ let testPassed = 0;
475
+ let testFailed = 0;
476
+ let testTripwire = 0;
477
+ for (const r of (report.results || [])) {
478
+ const v = m.variants?.[0] ? r.variants[m.variants[0]] : null;
479
+ if (!v)
480
+ continue;
481
+ const passed = (v.assertions?.details ?? []).every((d) => d.passed);
482
+ if (passed)
483
+ testPassed += 1;
484
+ else if (sampleSnaps[r.sample_id]?.tripwire || (v.diagnostic?.rootCause ?? []).includes('tripwire_intentional'))
485
+ testTripwire += 1;
486
+ else
487
+ testFailed += 1;
488
+ }
489
+ const testMetaTags = `<div class="meta-tags">
490
+ <span class="meta-tag">${m.sampleCount} ${lang === 'zh' ? '用例' : 'samples'}</span>
491
+ <span class="meta-tag" style="color:var(--green)">✓ ${testPassed} ${lang === 'zh' ? '通过' : 'passed'}</span>
492
+ ${testFailed > 0 ? `<span class="meta-tag" style="color:var(--red)">✗ ${testFailed} ${lang === 'zh' ? '失败' : 'failed'}</span>` : ''}
493
+ ${testTripwire > 0 ? `<span class="meta-tag" style="color:#7a6b89">◆ ${testTripwire} ${lang === 'zh' ? '诱错触发' : 'tripwire'}</span>` : ''}
494
+ <span class="meta-tag">${(m.variants || []).join(' · ')}</span>
495
+ </div>`;
496
+ return layout(`${report.id}`, `
497
+ <main>
498
+ <nav class="nav"><a href="/${langQ}" data-i18n="backToList">${t('backToList', lang)}</a></nav>
499
+ <div class="report-id-line">
500
+ <code class="report-id-pill">${e(report.id)}</code>
501
+ ${idStamp ? `<span class="report-id-stamp">${e(idStamp)}</span>` : ''}
502
+ </div>
448
503
 
449
- ${variantConfigSection}
504
+ <div class="omk-view-tabs" role="tablist">
505
+ <button type="button" class="omk-view-tab omk-view-tab--active" data-view="score" onclick="omkSwitchView('score')">${lang === 'zh' ? '📊 评测视角' : '📊 Score view'}</button>
506
+ <button type="button" class="omk-view-tab" data-view="test" onclick="omkSwitchView('test')">${lang === 'zh' ? '✅ 功能视角' : '✅ Functional view'}</button>
507
+ </div>
508
+
509
+ <div class="omk-view-panel" data-view="score">
510
+ <p class="subtitle">${lang === 'zh' ? '主观质量打分(judge)+ 客观断言聚合,产出综合评分与 verdict' : 'Subjective judge + objective assertions aggregated into composite + verdict'}</p>
511
+ ${verdictPill}
512
+ ${scoreMetaTags}
513
+ ${blindReveal}
450
514
 
451
- <section>${cards}${pairwiseDiff}${humanAgreement}${saturationCurve}</section>
515
+ <section>${cards}</section>
516
+ <div class="ctx-strip">${variantConfigSection}${renderAgentOverview(variants, summary, lang)}</div>
517
+ ${auditFingerprints}
518
+ ${methodologyAudit}
452
519
 
453
520
  ${renderVarianceComparisons(report.variance, lang, Boolean(report.meta.layeredStats), summary)}
454
521
 
455
522
  ${renderAnalysis(report, lang)}
456
523
 
457
- ${renderAgentOverview(variants, summary, lang)}
458
-
459
524
  ${renderKnowledgeInteractionSection(report.analysis?.coverage, report.analysis?.gapReports, lang)}
460
525
 
461
526
  <section>${sampleTable}</section>
527
+ </div>
528
+
529
+ <div class="omk-view-panel" data-view="test" hidden>
530
+ <p class="subtitle">${lang === 'zh' ? '每条用例当一条功能测试看:期望(rubric/assertions/mocks)、实际执行(trace)、失败时给诊断建议' : 'Each sample as a functional test: expected(rubric/assertions/mocks), actual(trace), diagnostic suggestions on failure'}</p>
531
+ ${testMetaTags}
532
+ ${renderTestView(report, lang)}
533
+ </div>
534
+
535
+ <style>${TEST_VIEW_CSS}
536
+ /* Context strip — 实验配置 + Agent 概览并排,作为辅助元数据,视觉权重低于综合分 hero / 六维 / 用例详情 */
537
+ .ctx-strip { display:grid;gap:14px;grid-template-columns:1fr 1fr;margin:14px 0 24px }
538
+ @media (max-width:760px) { .ctx-strip { grid-template-columns:1fr } }
539
+ .ctx-block { background:var(--bg-surface);border-radius:var(--radius);box-shadow:var(--shadow-sm);padding:12px 16px }
540
+ .ctx-h { font-size:13px;color:var(--text-secondary);font-weight:600;margin-bottom:2px;display:flex;align-items:center;gap:4px;letter-spacing:0.02em }
541
+ .ctx-sub { font-size:12px;color:var(--text-muted);margin-bottom:10px }
542
+ .ctx-rows { display:flex;flex-direction:column;gap:6px }
543
+ .ctx-row { padding:8px 12px;background:var(--bg-soft);border-radius:4px;display:flex;align-items:center;gap:10px;font-size:13px;line-height:1.5;flex-wrap:wrap }
544
+ .ctx-row-name { font-weight:600;color:var(--text-primary);font-family:"SF Mono",Menlo,monospace;font-size:12.5px;letter-spacing:-0.01em }
545
+ .ctx-row-bits { color:var(--text-secondary);flex:1;min-width:0;font-size:12.5px }
546
+ .ctx-sep { color:var(--text-muted);margin:0 2px;opacity:0.6 }
547
+ /* 报告身份带 — 极简一行,run-id pill + 时间戳 + verdict pill。完整 H1 + meta 移进各 tab 自己的 panel。 */
548
+ .report-id-line { display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:8px 0 18px }
549
+ .report-id-pill { font-size:14px;font-family:"SF Mono",Menlo,monospace;color:var(--text-primary);background:var(--bg-surface);padding:5px 12px;border-radius:14px;border:1px solid var(--border);letter-spacing:-0.01em;font-weight:500 }
550
+ .report-id-stamp { font-size:12px;color:var(--text-muted);font-variant-numeric:tabular-nums }
551
+ /* 顶部 tab 切换器 — 显式 reset border-radius / appearance / outline,
552
+ 防止全局 button 规则给底部加奇怪圆角和深色背景。 */
553
+ .omk-view-tabs { display:flex;gap:0;margin:16px 0 24px;border-bottom:1px solid var(--border) }
554
+ .omk-view-tab { padding:12px 20px;background:transparent;border:none;border-radius:0;cursor:pointer;color:var(--text-secondary);border-bottom:2px solid transparent;font-size:14px;font-weight:500;outline:none;appearance:none;-webkit-appearance:none;margin-bottom:-1px;font-family:inherit }
555
+ .omk-view-tab--active { color:var(--text-primary);border-bottom-color:var(--accent);font-weight:600 }
556
+ .omk-view-tab:hover:not(.omk-view-tab--active) { color:var(--text-primary) }
557
+ .omk-view-tab:focus-visible { outline:2px solid var(--accent);outline-offset:-4px;border-radius:4px }
558
+ /* tab 面板内的 H1 — 跟列表 H1 同级别但加 emoji 区分视角 */
559
+ .omk-view-panel h1 { font-size:1.5rem;margin:0 0 6px }
560
+ .omk-view-panel .subtitle { font-size:14px;color:var(--text-secondary);margin:0 0 20px;line-height:1.6 }
561
+ </style>
562
+ <script>${TEST_VIEW_JS}</script>
462
563
 
463
564
  </main>
464
565
  `, lang);
@@ -517,9 +618,14 @@ export function renderBatchEvaluationDetail(report, lang = DEFAULT_LANG) {
517
618
  return `<span class="meta-tag" title="${t('ensembleDesc', lang)}">${t('judgeModelsLabel', lang)}: ${list.map((j) => e(`${j.executor}:${j.model}`)).join(' · ')}</span>`;
518
619
  })()}
519
620
  <span class="meta-tag">${t('executor', lang)}: ${e(m.executor || 'claude')}</span>
520
- ${renderExecutorRuntimeTags(m, lang)}${renderJudgeRuntimeTags(m, lang)}
521
621
  <span class="meta-tag"${totalCostReported ? '' : ` title="${e(costCompletenessTooltip(lang))}"`}>${t('totalCost', lang)}: ${fmtKnownCost(m.totalCostUSD, totalCostReported)}</span>
522
622
  </div>
623
+ ${(() => {
624
+ const auditTags = renderRuntimeFingerprintTags(m, lang);
625
+ if (!auditTags)
626
+ return '';
627
+ return `<details class="audit-fingerprints"><summary>${lang === 'zh' ? '审计指纹(用于复现校验)' : 'Audit fingerprints (for reproducibility)'}</summary><div class="meta-tags">${auditTags}</div></details>`;
628
+ })()}
523
629
 
524
630
  <section>
525
631
  <h2>${t('batchOverview', lang)}</h2>