oh-my-knowledge 0.29.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 (253) hide show
  1. package/README.md +12 -5
  2. package/README.zh.md +12 -5
  3. package/dist/src/analysis/report-diagnostics.js +2 -0
  4. package/dist/src/analysis/report-diagnostics.js.map +1 -1
  5. package/dist/src/authoring/evolver.d.ts +7 -1
  6. package/dist/src/authoring/evolver.d.ts.map +1 -1
  7. package/dist/src/authoring/evolver.js +8 -5
  8. package/dist/src/authoring/evolver.js.map +1 -1
  9. package/dist/src/authoring/generator.d.ts +21 -23
  10. package/dist/src/authoring/generator.d.ts.map +1 -1
  11. package/dist/src/authoring/generator.js +621 -40
  12. package/dist/src/authoring/generator.js.map +1 -1
  13. package/dist/src/authoring/sample-fixer.d.ts +45 -0
  14. package/dist/src/authoring/sample-fixer.d.ts.map +1 -0
  15. package/dist/src/authoring/sample-fixer.js +186 -0
  16. package/dist/src/authoring/sample-fixer.js.map +1 -0
  17. package/dist/src/cli/commands/evolve.d.ts.map +1 -1
  18. package/dist/src/cli/commands/evolve.js +17 -0
  19. package/dist/src/cli/commands/evolve.js.map +1 -1
  20. package/dist/src/cli/commands/observe.js +2 -1
  21. package/dist/src/cli/commands/observe.js.map +1 -1
  22. package/dist/src/cli/commands/sample.d.ts +13 -0
  23. package/dist/src/cli/commands/sample.d.ts.map +1 -1
  24. package/dist/src/cli/commands/sample.js +307 -13
  25. package/dist/src/cli/commands/sample.js.map +1 -1
  26. package/dist/src/cli/i18n-dict.d.ts +1 -1
  27. package/dist/src/cli/i18n-dict.d.ts.map +1 -1
  28. package/dist/src/cli/i18n-dict.js +64 -6
  29. package/dist/src/cli/i18n-dict.js.map +1 -1
  30. package/dist/src/cli/parse-run-config.d.ts +14 -2
  31. package/dist/src/cli/parse-run-config.d.ts.map +1 -1
  32. package/dist/src/cli/parse-run-config.js +62 -10
  33. package/dist/src/cli/parse-run-config.js.map +1 -1
  34. package/dist/src/doctor/health/builtin-dimensions.d.ts.map +1 -1
  35. package/dist/src/doctor/health/builtin-dimensions.js +20 -23
  36. package/dist/src/doctor/health/builtin-dimensions.js.map +1 -1
  37. package/dist/src/doctor/health/composer.d.ts.map +1 -1
  38. package/dist/src/doctor/health/composer.js +7 -3
  39. package/dist/src/doctor/health/composer.js.map +1 -1
  40. package/dist/src/doctor/health/dimension-spec.d.ts +1 -0
  41. package/dist/src/doctor/health/dimension-spec.d.ts.map +1 -1
  42. package/dist/src/doctor/health/parser.d.ts.map +1 -1
  43. package/dist/src/doctor/health/parser.js +1 -0
  44. package/dist/src/doctor/health/parser.js.map +1 -1
  45. package/dist/src/doctor/health/prompt-builder.js +25 -17
  46. package/dist/src/doctor/health/prompt-builder.js.map +1 -1
  47. package/dist/src/doctor/html-renderer.d.ts.map +1 -1
  48. package/dist/src/doctor/html-renderer.js +76 -66
  49. package/dist/src/doctor/html-renderer.js.map +1 -1
  50. package/dist/src/doctor/rules.d.ts.map +1 -1
  51. package/dist/src/doctor/rules.js +32 -0
  52. package/dist/src/doctor/rules.js.map +1 -1
  53. package/dist/src/eval-core/cache.d.ts +16 -2
  54. package/dist/src/eval-core/cache.d.ts.map +1 -1
  55. package/dist/src/eval-core/cache.js +86 -15
  56. package/dist/src/eval-core/cache.js.map +1 -1
  57. package/dist/src/eval-core/evaluation-execution.d.ts +11 -1
  58. package/dist/src/eval-core/evaluation-execution.d.ts.map +1 -1
  59. package/dist/src/eval-core/evaluation-execution.js +78 -6
  60. package/dist/src/eval-core/evaluation-execution.js.map +1 -1
  61. package/dist/src/eval-core/evaluation-job.d.ts +2 -1
  62. package/dist/src/eval-core/evaluation-job.d.ts.map +1 -1
  63. package/dist/src/eval-core/evaluation-job.js +2 -1
  64. package/dist/src/eval-core/evaluation-job.js.map +1 -1
  65. package/dist/src/eval-core/evaluation-reporting.d.ts +7 -0
  66. package/dist/src/eval-core/evaluation-reporting.d.ts.map +1 -1
  67. package/dist/src/eval-core/evaluation-reporting.js +19 -2
  68. package/dist/src/eval-core/evaluation-reporting.js.map +1 -1
  69. package/dist/src/eval-core/execution-strategy.d.ts +1 -1
  70. package/dist/src/eval-core/execution-strategy.d.ts.map +1 -1
  71. package/dist/src/eval-core/execution-strategy.js +12 -1
  72. package/dist/src/eval-core/execution-strategy.js.map +1 -1
  73. package/dist/src/eval-core/mock-hook.cjs +203 -0
  74. package/dist/src/eval-core/mocks-runtime.d.ts +96 -0
  75. package/dist/src/eval-core/mocks-runtime.d.ts.map +1 -0
  76. package/dist/src/eval-core/mocks-runtime.js +323 -0
  77. package/dist/src/eval-core/mocks-runtime.js.map +1 -0
  78. package/dist/src/eval-core/schema.d.ts.map +1 -1
  79. package/dist/src/eval-core/schema.js +18 -5
  80. package/dist/src/eval-core/schema.js.map +1 -1
  81. package/dist/src/eval-core/task-planner.d.ts +9 -1
  82. package/dist/src/eval-core/task-planner.d.ts.map +1 -1
  83. package/dist/src/eval-core/task-planner.js +40 -1
  84. package/dist/src/eval-core/task-planner.js.map +1 -1
  85. package/dist/src/eval-workflows/batch-evaluation-workflow.d.ts +5 -1
  86. package/dist/src/eval-workflows/batch-evaluation-workflow.d.ts.map +1 -1
  87. package/dist/src/eval-workflows/batch-evaluation-workflow.js +2 -1
  88. package/dist/src/eval-workflows/batch-evaluation-workflow.js.map +1 -1
  89. package/dist/src/eval-workflows/evaluation-pipeline.d.ts +22 -1
  90. package/dist/src/eval-workflows/evaluation-pipeline.d.ts.map +1 -1
  91. package/dist/src/eval-workflows/evaluation-pipeline.js +39 -12
  92. package/dist/src/eval-workflows/evaluation-pipeline.js.map +1 -1
  93. package/dist/src/eval-workflows/evaluation-preparation.d.ts +5 -0
  94. package/dist/src/eval-workflows/evaluation-preparation.d.ts.map +1 -1
  95. package/dist/src/eval-workflows/evaluation-preparation.js +3 -1
  96. package/dist/src/eval-workflows/evaluation-preparation.js.map +1 -1
  97. package/dist/src/eval-workflows/run-evaluation.d.ts +12 -2
  98. package/dist/src/eval-workflows/run-evaluation.d.ts.map +1 -1
  99. package/dist/src/eval-workflows/run-evaluation.js +17 -5
  100. package/dist/src/eval-workflows/run-evaluation.js.map +1 -1
  101. package/dist/src/executors/claude-cli.d.ts +1 -1
  102. package/dist/src/executors/claude-cli.d.ts.map +1 -1
  103. package/dist/src/executors/claude-cli.js +57 -6
  104. package/dist/src/executors/claude-cli.js.map +1 -1
  105. package/dist/src/executors/claude-sdk-trace.d.ts.map +1 -1
  106. package/dist/src/executors/claude-sdk-trace.js +10 -4
  107. package/dist/src/executors/claude-sdk-trace.js.map +1 -1
  108. package/dist/src/executors/claude-sdk.d.ts +1 -1
  109. package/dist/src/executors/claude-sdk.d.ts.map +1 -1
  110. package/dist/src/executors/claude-sdk.js +25 -2
  111. package/dist/src/executors/claude-sdk.js.map +1 -1
  112. package/dist/src/executors/shared.d.ts +1 -1
  113. package/dist/src/executors/shared.d.ts.map +1 -1
  114. package/dist/src/executors/shared.js +6 -1
  115. package/dist/src/executors/shared.js.map +1 -1
  116. package/dist/src/grading/assertions.d.ts +5 -0
  117. package/dist/src/grading/assertions.d.ts.map +1 -1
  118. package/dist/src/grading/assertions.js +27 -1
  119. package/dist/src/grading/assertions.js.map +1 -1
  120. package/dist/src/grading/diagnostic.d.ts +40 -0
  121. package/dist/src/grading/diagnostic.d.ts.map +1 -0
  122. package/dist/src/grading/diagnostic.js +450 -0
  123. package/dist/src/grading/diagnostic.js.map +1 -0
  124. package/dist/src/grading/index.d.ts +5 -0
  125. package/dist/src/grading/index.d.ts.map +1 -1
  126. package/dist/src/grading/judge.d.ts.map +1 -1
  127. package/dist/src/grading/judge.js +54 -2
  128. package/dist/src/grading/judge.js.map +1 -1
  129. package/dist/src/grading/layered-scores.d.ts.map +1 -1
  130. package/dist/src/grading/layered-scores.js +1 -0
  131. package/dist/src/grading/layered-scores.js.map +1 -1
  132. package/dist/src/inputs/eval-config.js +19 -0
  133. package/dist/src/inputs/eval-config.js.map +1 -1
  134. package/dist/src/inputs/load-samples.d.ts +22 -2
  135. package/dist/src/inputs/load-samples.d.ts.map +1 -1
  136. package/dist/src/inputs/load-samples.js +193 -22
  137. package/dist/src/inputs/load-samples.js.map +1 -1
  138. package/dist/src/inputs/skill-loader.d.ts.map +1 -1
  139. package/dist/src/inputs/skill-loader.js +11 -3
  140. package/dist/src/inputs/skill-loader.js.map +1 -1
  141. package/dist/src/observability/experience.d.ts +260 -0
  142. package/dist/src/observability/experience.d.ts.map +1 -0
  143. package/dist/src/observability/experience.js +1233 -0
  144. package/dist/src/observability/experience.js.map +1 -0
  145. package/dist/src/observability/inbox-view-model.d.ts +11 -1
  146. package/dist/src/observability/inbox-view-model.d.ts.map +1 -1
  147. package/dist/src/observability/inbox-view-model.js +76 -3
  148. package/dist/src/observability/inbox-view-model.js.map +1 -1
  149. package/dist/src/observability/inbox.d.ts +27 -2
  150. package/dist/src/observability/inbox.d.ts.map +1 -1
  151. package/dist/src/observability/inbox.js +46 -8
  152. package/dist/src/observability/inbox.js.map +1 -1
  153. package/dist/src/observability/problem-patterns.d.ts +51 -0
  154. package/dist/src/observability/problem-patterns.d.ts.map +1 -0
  155. package/dist/src/observability/problem-patterns.js +205 -0
  156. package/dist/src/observability/problem-patterns.js.map +1 -0
  157. package/dist/src/observability/review-state.d.ts +54 -0
  158. package/dist/src/observability/review-state.d.ts.map +1 -0
  159. package/dist/src/observability/review-state.js +131 -0
  160. package/dist/src/observability/review-state.js.map +1 -0
  161. package/dist/src/observability/skill-chain-advisories.d.ts +25 -0
  162. package/dist/src/observability/skill-chain-advisories.d.ts.map +1 -0
  163. package/dist/src/observability/skill-chain-advisories.js +69 -0
  164. package/dist/src/observability/skill-chain-advisories.js.map +1 -0
  165. package/dist/src/observability/skill-chain.d.ts +61 -0
  166. package/dist/src/observability/skill-chain.d.ts.map +1 -0
  167. package/dist/src/observability/skill-chain.js +233 -0
  168. package/dist/src/observability/skill-chain.js.map +1 -0
  169. package/dist/src/observability/text-signals.d.ts +7 -0
  170. package/dist/src/observability/text-signals.d.ts.map +1 -0
  171. package/dist/src/observability/text-signals.js +22 -0
  172. package/dist/src/observability/text-signals.js.map +1 -0
  173. package/dist/src/observability/trace-adapter.d.ts +1 -1
  174. package/dist/src/observability/trace-adapter.d.ts.map +1 -1
  175. package/dist/src/observability/trace-adapter.js +1 -1
  176. package/dist/src/observability/trace-adapter.js.map +1 -1
  177. package/dist/src/observability/trace-attribution.d.ts +29 -1
  178. package/dist/src/observability/trace-attribution.d.ts.map +1 -1
  179. package/dist/src/observability/trace-attribution.js +106 -10
  180. package/dist/src/observability/trace-attribution.js.map +1 -1
  181. package/dist/src/observability/trace-segmenter.d.ts +11 -1
  182. package/dist/src/observability/trace-segmenter.d.ts.map +1 -1
  183. package/dist/src/observability/trace-segmenter.js +117 -28
  184. package/dist/src/observability/trace-segmenter.js.map +1 -1
  185. package/dist/src/observability/trace-source.d.ts +28 -1
  186. package/dist/src/observability/trace-source.d.ts.map +1 -1
  187. package/dist/src/observability/trace-source.js +356 -6
  188. package/dist/src/observability/trace-source.js.map +1 -1
  189. package/dist/src/renderer/html-renderer.d.ts.map +1 -1
  190. package/dist/src/renderer/html-renderer.js +229 -171
  191. package/dist/src/renderer/html-renderer.js.map +1 -1
  192. package/dist/src/renderer/layout.d.ts.map +1 -1
  193. package/dist/src/renderer/layout.js +108 -61
  194. package/dist/src/renderer/layout.js.map +1 -1
  195. package/dist/src/renderer/observation-inbox-renderer.d.ts.map +1 -1
  196. package/dist/src/renderer/observation-inbox-renderer.js +4567 -91
  197. package/dist/src/renderer/observation-inbox-renderer.js.map +1 -1
  198. package/dist/src/renderer/skill-detail-renderer.d.ts +15 -0
  199. package/dist/src/renderer/skill-detail-renderer.d.ts.map +1 -0
  200. package/dist/src/renderer/skill-detail-renderer.js +1234 -0
  201. package/dist/src/renderer/skill-detail-renderer.js.map +1 -0
  202. package/dist/src/renderer/skill-list-renderer.d.ts +4 -0
  203. package/dist/src/renderer/skill-list-renderer.d.ts.map +1 -0
  204. package/dist/src/renderer/skill-list-renderer.js +327 -0
  205. package/dist/src/renderer/skill-list-renderer.js.map +1 -0
  206. package/dist/src/renderer/summary.d.ts.map +1 -1
  207. package/dist/src/renderer/summary.js +78 -50
  208. package/dist/src/renderer/summary.js.map +1 -1
  209. package/dist/src/renderer/table.d.ts.map +1 -1
  210. package/dist/src/renderer/table.js +213 -167
  211. package/dist/src/renderer/table.js.map +1 -1
  212. package/dist/src/renderer/test-view.d.ts +66 -0
  213. package/dist/src/renderer/test-view.d.ts.map +1 -0
  214. package/dist/src/renderer/test-view.js +905 -0
  215. package/dist/src/renderer/test-view.js.map +1 -0
  216. package/dist/src/server/report-server.d.ts +4 -1
  217. package/dist/src/server/report-server.d.ts.map +1 -1
  218. package/dist/src/server/report-server.js +149 -4
  219. package/dist/src/server/report-server.js.map +1 -1
  220. package/dist/src/server/report-store.d.ts.map +1 -1
  221. package/dist/src/server/report-store.js +33 -1
  222. package/dist/src/server/report-store.js.map +1 -1
  223. package/dist/src/server/skill-index.d.ts +77 -0
  224. package/dist/src/server/skill-index.d.ts.map +1 -0
  225. package/dist/src/server/skill-index.js +331 -0
  226. package/dist/src/server/skill-index.js.map +1 -0
  227. package/dist/src/server/skill-insights.d.ts +92 -0
  228. package/dist/src/server/skill-insights.d.ts.map +1 -0
  229. package/dist/src/server/skill-insights.js +676 -0
  230. package/dist/src/server/skill-insights.js.map +1 -0
  231. package/dist/src/shared/hard-rules.d.ts +40 -0
  232. package/dist/src/shared/hard-rules.d.ts.map +1 -0
  233. package/dist/src/shared/hard-rules.js +200 -0
  234. package/dist/src/shared/hard-rules.js.map +1 -0
  235. package/dist/src/shared/time.d.ts +2 -0
  236. package/dist/src/shared/time.d.ts.map +1 -0
  237. package/dist/src/shared/time.js +10 -0
  238. package/dist/src/shared/time.js.map +1 -0
  239. package/dist/src/types/eval.d.ts +83 -0
  240. package/dist/src/types/eval.d.ts.map +1 -1
  241. package/dist/src/types/executor.d.ts +43 -0
  242. package/dist/src/types/executor.d.ts.map +1 -1
  243. package/dist/src/types/judge.d.ts +76 -0
  244. package/dist/src/types/judge.d.ts.map +1 -1
  245. package/dist/src/types/judge.js +3 -1
  246. package/dist/src/types/judge.js.map +1 -1
  247. package/dist/src/types/report.d.ts +63 -3
  248. package/dist/src/types/report.d.ts.map +1 -1
  249. package/dist/src/util/safe-slice.d.ts +23 -0
  250. package/dist/src/util/safe-slice.d.ts.map +1 -0
  251. package/dist/src/util/safe-slice.js +33 -0
  252. package/dist/src/util/safe-slice.js.map +1 -0
  253. package/package.json +5 -4
@@ -4,6 +4,7 @@
4
4
  import { e, fmtCost, fmtDuration, fmtKnownCost, COLORS, DEFAULT_LANG, t, layout } from './layout.js';
5
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 实心(强信号),
@@ -132,7 +133,7 @@ function renderRuntimeFingerprintTags(meta, lang) {
132
133
  }
133
134
  export function renderRunList(runs, lang = DEFAULT_LANG) {
134
135
  const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
135
- 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>`;
136
137
  if (!runs || runs.length === 0) {
137
138
  return layout(t('title', lang), `
138
139
  <main>
@@ -143,112 +144,85 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
143
144
  </main>
144
145
  `, lang);
145
146
  }
146
- 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) => {
147
166
  if (run.kind === 'batch-evaluation') {
148
167
  const m = run.meta;
149
- const scoreCol = run.items.length > 0
168
+ const scores = run.items.length > 0
150
169
  ? run.items.map((item) => {
151
170
  const baselineScore = scoreOf(item.summary.baseline);
152
171
  const skillScore = scoreOf(item.summary[item.name]);
153
- const score = skillScore ?? baselineScore;
154
- if (score == null)
155
- return `<span style="color:var(--text-muted)">${e(item.name)}: -</span>`;
156
- const color = score >= 4 ? 'var(--green)' : score >= 3 ? 'var(--yellow)' : 'var(--red)';
157
- const barW = Math.round((score / 5) * 100);
158
- return `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">` +
159
- `<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>` +
160
- `<div style="width:64px;height:6px;background:var(--bg-surface);border-radius:3px;flex-shrink:0">` +
161
- `<div style="width:${barW}%;height:100%;background:${color};border-radius:3px"></div></div>` +
162
- `<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);
163
173
  }).join('')
164
- : '<div style="color:var(--text-faint);font-size:0.6875rem;text-align:center">no score</div>';
165
- const allCostReported = run.items.every((item) => Object.values(item.summary || {}).every((v) => v.execCostReported !== false && v.judgeCostReported !== false));
166
- const totalCostReported = m.totalCostReported !== false && allCostReported;
174
+ : `<div class="rc-score-empty">${lang === 'zh' ? '无分数' : 'no score'}</div>`;
167
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);
168
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>`;
169
- return `<tr>
170
- <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>
171
- <td>${e(m.model || '-')}</td>
172
- <td>${m.sampleCount || 0}</td>
173
- <td>${scoreCol}</td>
174
- <td title="${totalCostReported ? '' : e(costCompletenessTooltip(lang))}">${fmtKnownCost(m.totalCostUSD || 0, totalCostReported)}</td>
175
- <td>${fmtDuration(totalDurationMs)}</td>
176
- <td style="white-space:nowrap"><button onclick="deleteRun('${e(run.id)}',this)" class="btn-danger" data-i18n="deleteBtnText">${t('deleteBtnText', lang)}</button></td>
177
- </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>`;
178
191
  }
179
192
  const m = run.meta;
180
- const hasScores = Object.values(run.summary || {}).some((s) => typeof s.avgCompositeScore === 'number' || typeof s.avgLlmScore === 'number');
181
- const scoreCol = hasScores
182
- ? Object.entries(run.summary || {}).map(([v, s]) => {
183
- const score = s.avgCompositeScore ?? s.avgLlmScore ?? null;
184
- if (score == null)
185
- return `<span style="color:var(--text-muted)">${e(v)}: -</span>`;
186
- const color = score >= 4 ? 'var(--green)' : score >= 3 ? 'var(--yellow)' : 'var(--red)';
187
- const barW = Math.round((score / 5) * 100);
188
- return `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">` +
189
- `<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>` +
190
- `<div style="width:64px;height:6px;background:var(--bg-surface);border-radius:3px;flex-shrink:0">` +
191
- `<div style="width:${barW}%;height:100%;background:${color};border-radius:3px"></div></div>` +
192
- `<span style="font-size:12px;font-weight:600;color:${color};min-width:24px">${score}</span></div>`;
193
- }).join('')
194
- : '<div style="color:var(--text-faint);font-size:0.6875rem;text-align:center">no score</div>';
195
- const badges = ''; // TODO: artifact kind 体系完善后,按 kind 显示评测类型标签
196
- // v0.21 B.4 — 列表页 verdict pill: 一眼分辨 progress / regress / noise.
197
- // computeVerdict 是同步纯函数(report -> level),per row 跑成本 O(samples).
198
- // 脏报告可能让 layer-gates 访问 undefined.avgFactScore 抛 NPE。
199
- // 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>`;
200
198
  let statusPill = '';
199
+ let verdictLvl = '';
201
200
  try {
202
201
  const verdict = computeVerdict(run);
203
- const lvl = verdict.level;
204
- 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>`;
205
204
  }
206
205
  catch { /* skip pill on this row */ }
207
- return `<tr>
208
- <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)">${(() => {
209
- // Extract date/time from report ID: ...-YYYYMMDD-HHmm
210
- const idMatch = run.id.match(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})$/);
211
- if (idMatch)
212
- return `${idMatch[2]}/${idMatch[3]} ${idMatch[4]}:${idMatch[5]}`;
213
- 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);
214
- })()}</span></a></td>
215
- <td>${e(m.model || '-')}</td>
216
- <td>${m.sampleCount || 0}</td>
217
- <td>${scoreCol}</td>
218
- <td>${(() => {
219
- const cost = Object.values(run.summary || {}).reduce((s, v) => s + (v.totalExecCostUSD || 0), 0);
220
- const reported = Object.values(run.summary || {}).every((v) => v.execCostReported !== false);
221
- return fmtCost(cost, reported);
222
- })()}</td>
223
- <td>${fmtDuration(Object.values(run.summary || {}).reduce((s, v) => s + (v.avgDurationMs || 0) * (v.successCount || 0), 0))}</td>
224
- <td style="white-space:nowrap"><button onclick="deleteRun('${e(run.id)}',this)" class="btn-danger" data-i18n="deleteBtnText">${t('deleteBtnText', lang)}</button></td>
225
- </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>`;
226
222
  }).join('');
227
223
  const runCount = lang === 'zh' ? `${runs.length} 次评测` : `${runs.length} runs`;
228
- // 列表累计:只 sum 那些"全 variant 都报告了 cost"的 run,跳过任一 variant not reported 的。
229
- // 排除 not reported(而不是把整体压成「—」)是因为列表常含数十个 run 仅 1-2 个是 codex,
230
- // 把全部 sum 抹成「—」会丢掉绝大多数有效成本信息。
224
+ // v0.30 — 列表页移除累计金额展示(用户反馈无须暴露);保留 evaluationRuns 用于 trend links
231
225
  const evaluationRuns = runs.filter(isEvaluationReport);
232
- const reportableRuns = evaluationRuns.filter((r) => Object.values(r.summary || {}).every((v) => v.execCostReported !== false));
233
- const unmeasuredRunsCount = evaluationRuns.length - reportableRuns.length;
234
- const totalCost = reportableRuns.reduce((s, r) => s + Object.values(r.summary || {}).reduce((sv, v) => sv + (v.totalExecCostUSD || 0), 0), 0);
235
- // partial:有 reported 就显示数字 + tooltip;全部 not reported 才显示「—」;
236
- // 全 reported(unmeasuredRunsCount=0)走老格式不包 span,保持 HTML snapshot 向后兼容。
237
- // X/N 限定信息只放 tooltip,不挂在显眼位置 — 累计行视觉应该是单纯的数字。
238
- const allUnmeasured = reportableRuns.length === 0 && runs.length > 0;
239
- const baseLabel = lang === 'zh' ? '累计' : 'Total';
240
- const costNumber = fmtCost(totalCost, !allUnmeasured);
241
- let costLabel;
242
- if (unmeasuredRunsCount === 0) {
243
- // 全 reported → 跟旧版字节级一致,不破 snapshot
244
- costLabel = `${baseLabel} ${costNumber}`;
245
- }
246
- else {
247
- const tip = lang === 'zh'
248
- ? `${reportableRuns.length}/${evaluationRuns.length} 次单次报告了 USD 成本;${unmeasuredRunsCount} 次 executor 不报(如 codex CLI),batch index 不计入累计以避免重复`
249
- : `${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`;
250
- costLabel = `<span title="${e(tip)}">${baseLabel} ${costNumber}</span>`;
251
- }
252
226
  // Collect variants with ≥2 reports for trend links
253
227
  const variantCounts = {};
254
228
  for (const run of evaluationRuns) {
@@ -266,7 +240,7 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
266
240
  return layout(t('title', lang), `
267
241
  <main>
268
242
  <h1>${t('title', lang)}</h1>
269
- <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>
270
244
  ${trendsSection}
271
245
  <div style="margin:12px 0">${skillHealthLink}</div>
272
246
  <div style="margin:12px 0;display:flex;gap:8px;align-items:center">
@@ -274,29 +248,54 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
274
248
  <span id="filter-count" style="font-size:11px;color:var(--text-muted)"></span>
275
249
  </div>
276
250
  ${renderScoringModal('guide-scoring-list', lang)}
277
- <div class="table-wrap">
278
- <table id="report-table">
279
- <thead><tr>
280
- <th data-i18n="runId">${t('runId', lang)}</th>
281
- <th data-i18n="model">${t('model', lang)}</th>
282
- <th data-i18n="samples">${t('samples', lang)}</th>
283
- <th data-i18n="score">${t('score', lang)} <button type="button" class="hint-btn" onclick="openModal('guide-scoring-list')" aria-label="${e(lang === 'zh' ? '综合分怎么算的?' : 'How is composite computed?')}" aria-haspopup="dialog">?</button></th>
284
- <th data-i18n="cost">${t('cost', lang)}</th>
285
- <th>${lang === 'zh' ? '耗时' : 'Duration'}</th>
286
- <th></th>
287
- </tr></thead>
288
- <tbody>${rows}</tbody>
289
- </table>
290
- </div>
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>
291
290
  <script>
292
291
  function filterTable(q) {
293
- var rows = document.querySelectorAll('#report-table tbody tr');
292
+ var cards = document.querySelectorAll('#run-list .run-card');
294
293
  var lower = q.toLowerCase();
295
294
  var shown = 0;
296
- rows.forEach(function(row) {
297
- var text = row.textContent.toLowerCase();
295
+ cards.forEach(function(card) {
296
+ var text = card.textContent.toLowerCase();
298
297
  var match = !q || text.indexOf(lower) !== -1;
299
- row.style.display = match ? '' : 'none';
298
+ card.style.display = match ? '' : 'none';
300
299
  if (match) shown++;
301
300
  });
302
301
  var countEl = document.getElementById('filter-count');
@@ -308,7 +307,7 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
308
307
  fetch('/api/reports/' + encodeURIComponent(id), { method: 'DELETE' })
309
308
  .then(function(r) { return r.json(); })
310
309
  .then(function(d) {
311
- if (d.ok) { btn.closest('tr').remove(); }
310
+ if (d.ok) { btn.closest('.run-card').remove(); }
312
311
  else { alert(I18N[lang].deleteFail + ': ' + (d.error || 'unknown')); }
313
312
  })
314
313
  .catch(function(err) { alert(I18N[lang].deleteFail + ': ' + err.message); });
@@ -357,52 +356,41 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
357
356
  zh: { baseline: '基线', skill: 'Skill', prompt: 'Prompt', agent: 'Agent', workflow: 'Workflow' },
358
357
  en: { baseline: 'Baseline', skill: 'Skill', prompt: 'Prompt', agent: 'Agent', workflow: 'Workflow' },
359
358
  };
359
+ // v0.30 — 实验配置弱化:单行 inline 摘要,跟 Agent 执行概览并排放(context-strip)。
360
+ // 每变体一行:左 3px 颜色条 + variant 名 + type/kind/source/strategy/cwd 用 · 分隔。
361
+ // 完整 key 名通过 hover tooltip 提供。视觉权重远低于综合分 hero。
360
362
  const variantConfigRows = (m.variantConfigs || []).map((config, i) => {
361
363
  const expTypeRaw = config.experimentType || '-';
362
364
  const expType = (typeLabels[lang] || typeLabels.en)[String(expTypeRaw)] || expTypeRaw;
363
365
  const source = config.artifactKind === 'baseline'
364
- ? (lang === 'zh' ? '无' : 'None')
366
+ ? (lang === 'zh' ? '无来源' : 'no source')
365
367
  : (sourceLabels[lang] || sourceLabels.en)[config.artifactSource] || config.artifactSource;
366
368
  const strategy = (strategyLabels[lang] || strategyLabels.en)[config.executionStrategy] || config.executionStrategy;
367
369
  const cwdRaw = config.cwd || '';
368
370
  const runtimeContext = cwdRaw
369
371
  ? cwdRaw.replace(/.*\/Projects\//, '').replace(/.*\/Documents\//, '').replace(/\/Users\/[^/]+\//, '~/')
370
- : (lang === 'zh' ? '默认' : 'default');
372
+ : (lang === 'zh' ? '默认 cwd' : 'default cwd');
371
373
  const color = COLORS[i % COLORS.length];
372
- return `<tr>
373
- <td style="border-left:3px solid ${color};padding-left:12px"><strong>${e(config.variant)}</strong></td>
374
- <td>${e(expType)}</td>
375
- <td>${e((artifactKindLabels[lang] || artifactKindLabels.en)[config.artifactKind] || config.artifactKind)}</td>
376
- <td>${e(source)}</td>
377
- <td>${e(strategy)}</td>
378
- <td title="${e(cwdRaw)}">${e(runtimeContext)}</td>
379
- </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>`;
380
380
  }).join('');
381
381
  const configModalId = 'guide-variant-config';
382
382
  const repeatSuffix = report.variance
383
383
  ? (lang === 'zh' ? ` × ${report.variance.runs} 轮` : ` × ${report.variance.runs} runs`)
384
384
  : '';
385
385
  const experimentSummary = lang === 'zh'
386
- ? `${m.sampleCount} 个评测用例 × ${variants.length} 组实验${repeatSuffix}`
386
+ ? `${m.sampleCount} 个用例 × ${variants.length} 组实验${repeatSuffix}`
387
387
  : `${pluralizeEn(m.sampleCount, 'sample')} × ${pluralizeEn(variants.length, 'variant')}${repeatSuffix}`;
388
388
  const variantConfigSection = variantConfigRows ? `
389
- <section style="margin:20px 0">
390
- <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>
391
- <p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">${experimentSummary}</p>
392
- <div class="table-wrap">
393
- <table>
394
- <thead><tr>
395
- <th>🏷️ ${t('variants', lang)}</th>
396
- <th>🧪 ${t('variantType', lang)}</th>
397
- <th>📦 ${t('variantArtifactKind', lang)}</th>
398
- <th>📂 ${t('variantArtifactSource', lang)}</th>
399
- <th>⚙️ ${t('variantExecutionStrategy', lang)}</th>
400
- <th>🖥️ ${t('variantRuntimeContext', lang)}</th>
401
- </tr></thead>
402
- <tbody>${variantConfigRows}</tbody>
403
- </table>
404
- </div>
405
- </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>
406
394
  <div id="${configModalId}" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="${configModalId}-title" onclick="if(event.target===this)closeModal('${configModalId}')">
407
395
  <div class="modal-content">
408
396
  <div class="modal-header">
@@ -430,25 +418,20 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
430
418
  </div>
431
419
  </div>
432
420
  ` : '';
433
- return layout(`${report.id}`, `
434
- <main>
435
- <nav class="nav"><a href="/${langQ}" data-i18n="backToList">${t('backToList', lang)}</a></nav>
436
- <h1>${e(report.id)}</h1>
437
- ${(() => {
438
- // Run ID 形如 `<name>-YYYYMMDD-HHMM`,把时间戳解出来作为人类可读的副标小字
439
- // (报告什么时候跑的)。完整 run-id 仍是 H1,与列表页保持一致。
421
+ // v0.30 重设计 — 评分/单测各自独立 title + meta strip(不共享头部)。
422
+ // 顶部只保留极简身份带:返回链接 + run-id pill。完整 H1 + meta tags 移进各自 tab 面板。
423
+ const idStamp = (() => {
440
424
  const idMatch = /^.+?-(\d{8})-(\d{4})$/.exec(report.id);
441
425
  if (idMatch) {
442
426
  const [, ymd, hm] = idMatch;
443
- const stamp = `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)} ${hm.slice(0, 2)}:${hm.slice(2, 4)}`;
444
- return `<p class="run-id-stamp">${lang === 'zh' ? '运行时间' : 'ran at'}: ${e(stamp)}</p>`;
427
+ return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)} ${hm.slice(0, 2)}:${hm.slice(2, 4)}`;
445
428
  }
446
429
  return '';
447
- })()}
448
- ${verdictPill}
449
- <div class="meta-tags">
450
- <span class="meta-tag">${t('model', lang)}: ${e(m.model)}</span>
451
- ${(() => {
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
+ ${(() => {
452
435
  if (m.noJudge)
453
436
  return `<span class="meta-tag">${t('judge', lang)}: none</span>`;
454
437
  const list = m.judgeModels ?? [];
@@ -458,20 +441,19 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
458
441
  return `<span class="meta-tag">${t('judge', lang)}: ${e(`${list[0].executor}:${list[0].model}`)}</span>`;
459
442
  return `<span class="meta-tag" title="${t('ensembleDesc', lang)}">${t('judgeModelsLabel', lang)}: ${list.map((j) => e(`${j.executor}:${j.model}`)).join(' · ')}</span>`;
460
443
  })()}
461
- ${m.judgeRepeat && m.judgeRepeat > 1 ? `<span class="meta-tag" title="${t('judgeStddevDesc', lang)}">${t('judgeRepeatLabel', lang)}: ${m.judgeRepeat}</span>` : ''}
462
- <span class="meta-tag">${t('executor', lang)}: ${e(m.executor || 'claude')}</span>
463
- <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>
464
- <span class="meta-tag"${totalCostReported ? '' : ` title="${e(costCompletenessTooltip(lang))}"`}>${t('totalCost', lang)}: ${fmtKnownCost(m.totalCostUSD, totalCostReported)}</span>
465
- <span class="meta-tag">${lang === 'zh' ? '耗时' : 'Duration'}: ${fmtDuration(totalDurationMs)}</span>
466
- ${m.gitInfo ? `<span class="meta-tag">${lang === 'zh' ? '提交' : 'commit'}: ${e(m.gitInfo.commitShort)}${m.gitInfo.dirty ? '*' : ''} (${e(m.gitInfo.branch)})</span>` : ''}
467
- ${m.sampleHashes ? `<span class="meta-tag" title="${t('sampleHashCountDesc', lang)}">${t('sampleHashCount', lang)}: ${Object.keys(m.sampleHashes).length}/${m.sampleCount}</span>` : ''}
468
- ${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>` : ''}
469
- ${renderDebiasModeTag(m.debiasMode, lang)}
470
- ${m.blind ? `<span class="meta-tag" style="color:var(--green)" data-i18n="blindLabel">${t('blindLabel', lang)}</span>` : ''}
471
- </div>
472
- ${(() => {
473
- // 审计指纹 (评委 prompt hash + 执行环境 fingerprint) 默认折叠 ——
474
- // 平日 review 不需要看, 复现时再展开。同 fingerprint 多次出现已合并。
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 = (() => {
475
457
  const auditTags = [
476
458
  m.judgePromptHash ? `<span class="meta-tag" title="${t('judgePromptHashDesc', lang)}">${t('judgePromptHashLabel', lang)}: <code>${e(m.judgePromptHash)}</code></span>` : '',
477
459
  renderRuntimeFingerprintTags(m, lang),
@@ -479,29 +461,105 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
479
461
  if (!auditTags)
480
462
  return '';
481
463
  return `<details class="audit-fingerprints"><summary>${lang === 'zh' ? '审计指纹(用于复现校验)' : 'Audit fingerprints (for reproducibility)'}</summary><div class="meta-tags">${auditTags}</div></details>`;
482
- })()}
483
- ${m.blind ? `
464
+ })();
465
+ const blindReveal = m.blind ? `
484
466
  <div style="margin:12px 0">
485
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>
486
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">
487
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('')}
488
470
  </div>
489
- </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>
490
503
 
491
- ${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}
492
514
 
493
515
  <section>${cards}</section>
516
+ <div class="ctx-strip">${variantConfigSection}${renderAgentOverview(variants, summary, lang)}</div>
517
+ ${auditFingerprints}
494
518
  ${methodologyAudit}
495
519
 
496
520
  ${renderVarianceComparisons(report.variance, lang, Boolean(report.meta.layeredStats), summary)}
497
521
 
498
522
  ${renderAnalysis(report, lang)}
499
523
 
500
- ${renderAgentOverview(variants, summary, lang)}
501
-
502
524
  ${renderKnowledgeInteractionSection(report.analysis?.coverage, report.analysis?.gapReports, lang)}
503
525
 
504
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>
505
563
 
506
564
  </main>
507
565
  `, lang);