oh-my-knowledge 0.0.1 → 0.18.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +537 -332
  3. package/dist/src/analysis/coverage-analyzer.d.ts +57 -0
  4. package/dist/src/analysis/coverage-analyzer.d.ts.map +1 -0
  5. package/dist/src/analysis/coverage-analyzer.js +262 -0
  6. package/dist/src/analysis/coverage-analyzer.js.map +1 -0
  7. package/dist/src/analysis/gap-analyzer.d.ts +114 -0
  8. package/dist/src/analysis/gap-analyzer.d.ts.map +1 -0
  9. package/dist/src/analysis/gap-analyzer.js +439 -0
  10. package/dist/src/analysis/gap-analyzer.js.map +1 -0
  11. package/dist/src/analysis/hedging-classifier.d.ts +34 -0
  12. package/dist/src/analysis/hedging-classifier.d.ts.map +1 -0
  13. package/dist/src/analysis/hedging-classifier.js +144 -0
  14. package/dist/src/analysis/hedging-classifier.js.map +1 -0
  15. package/dist/src/analysis/report-diagnostics.d.ts +9 -0
  16. package/dist/src/analysis/report-diagnostics.d.ts.map +1 -0
  17. package/dist/src/analysis/report-diagnostics.js +631 -0
  18. package/dist/src/analysis/report-diagnostics.js.map +1 -0
  19. package/dist/src/authoring/evolver.d.ts +64 -0
  20. package/dist/src/authoring/evolver.d.ts.map +1 -0
  21. package/dist/src/authoring/evolver.js +275 -0
  22. package/dist/src/authoring/evolver.js.map +1 -0
  23. package/dist/src/authoring/generator.d.ts +13 -0
  24. package/dist/src/authoring/generator.d.ts.map +1 -0
  25. package/dist/src/authoring/generator.js +60 -0
  26. package/dist/src/authoring/generator.js.map +1 -0
  27. package/dist/src/cli.d.ts +3 -0
  28. package/dist/src/cli.d.ts.map +1 -0
  29. package/dist/src/cli.js +1004 -0
  30. package/dist/src/cli.js.map +1 -0
  31. package/dist/src/eval-core/cache.d.ts +11 -0
  32. package/dist/src/eval-core/cache.d.ts.map +1 -0
  33. package/dist/src/eval-core/cache.js +53 -0
  34. package/dist/src/eval-core/cache.js.map +1 -0
  35. package/dist/src/eval-core/ci-gates.d.ts +17 -0
  36. package/dist/src/eval-core/ci-gates.d.ts.map +1 -0
  37. package/dist/src/eval-core/ci-gates.js +42 -0
  38. package/dist/src/eval-core/ci-gates.js.map +1 -0
  39. package/dist/src/eval-core/dependency-checker.d.ts +37 -0
  40. package/dist/src/eval-core/dependency-checker.d.ts.map +1 -0
  41. package/dist/src/eval-core/dependency-checker.js +280 -0
  42. package/dist/src/eval-core/dependency-checker.js.map +1 -0
  43. package/dist/src/eval-core/evaluation-execution.d.ts +26 -0
  44. package/dist/src/eval-core/evaluation-execution.d.ts.map +1 -0
  45. package/dist/src/eval-core/evaluation-execution.js +196 -0
  46. package/dist/src/eval-core/evaluation-execution.js.map +1 -0
  47. package/dist/src/eval-core/evaluation-job.d.ts +48 -0
  48. package/dist/src/eval-core/evaluation-job.d.ts.map +1 -0
  49. package/dist/src/eval-core/evaluation-job.js +112 -0
  50. package/dist/src/eval-core/evaluation-job.js.map +1 -0
  51. package/dist/src/eval-core/evaluation-reporting.d.ts +28 -0
  52. package/dist/src/eval-core/evaluation-reporting.d.ts.map +1 -0
  53. package/dist/src/eval-core/evaluation-reporting.js +123 -0
  54. package/dist/src/eval-core/evaluation-reporting.js.map +1 -0
  55. package/dist/src/eval-core/execution-strategy.d.ts +11 -0
  56. package/dist/src/eval-core/execution-strategy.d.ts.map +1 -0
  57. package/dist/src/eval-core/execution-strategy.js +121 -0
  58. package/dist/src/eval-core/execution-strategy.js.map +1 -0
  59. package/dist/src/eval-core/fact-checker.d.ts +25 -0
  60. package/dist/src/eval-core/fact-checker.d.ts.map +1 -0
  61. package/dist/src/eval-core/fact-checker.js +66 -0
  62. package/dist/src/eval-core/fact-checker.js.map +1 -0
  63. package/dist/src/eval-core/schema.d.ts +30 -0
  64. package/dist/src/eval-core/schema.d.ts.map +1 -0
  65. package/dist/src/eval-core/schema.js +195 -0
  66. package/dist/src/eval-core/schema.js.map +1 -0
  67. package/dist/src/eval-core/statistics.d.ts +86 -0
  68. package/dist/src/eval-core/statistics.d.ts.map +1 -0
  69. package/dist/src/eval-core/statistics.js +210 -0
  70. package/dist/src/eval-core/statistics.js.map +1 -0
  71. package/dist/src/eval-core/task-planner.d.ts +4 -0
  72. package/dist/src/eval-core/task-planner.d.ts.map +1 -0
  73. package/dist/src/eval-core/task-planner.js +33 -0
  74. package/dist/src/eval-core/task-planner.js.map +1 -0
  75. package/dist/src/eval-workflows/each-evaluation-workflow.d.ts +133 -0
  76. package/dist/src/eval-workflows/each-evaluation-workflow.d.ts.map +1 -0
  77. package/dist/src/eval-workflows/each-evaluation-workflow.js +169 -0
  78. package/dist/src/eval-workflows/each-evaluation-workflow.js.map +1 -0
  79. package/dist/src/eval-workflows/evaluation-pipeline.d.ts +38 -0
  80. package/dist/src/eval-workflows/evaluation-pipeline.d.ts.map +1 -0
  81. package/dist/src/eval-workflows/evaluation-pipeline.js +214 -0
  82. package/dist/src/eval-workflows/evaluation-pipeline.js.map +1 -0
  83. package/dist/src/eval-workflows/evaluation-preparation.d.ts +64 -0
  84. package/dist/src/eval-workflows/evaluation-preparation.d.ts.map +1 -0
  85. package/dist/src/eval-workflows/evaluation-preparation.js +90 -0
  86. package/dist/src/eval-workflows/evaluation-preparation.js.map +1 -0
  87. package/dist/src/eval-workflows/run-evaluation.d.ts +105 -0
  88. package/dist/src/eval-workflows/run-evaluation.d.ts.map +1 -0
  89. package/dist/src/eval-workflows/run-evaluation.js +264 -0
  90. package/dist/src/eval-workflows/run-evaluation.js.map +1 -0
  91. package/dist/src/executors/anthropic-api.d.ts +3 -0
  92. package/dist/src/executors/anthropic-api.d.ts.map +1 -0
  93. package/dist/src/executors/anthropic-api.js +43 -0
  94. package/dist/src/executors/anthropic-api.js.map +1 -0
  95. package/dist/src/executors/claude-cli.d.ts +3 -0
  96. package/dist/src/executors/claude-cli.d.ts.map +1 -0
  97. package/dist/src/executors/claude-cli.js +103 -0
  98. package/dist/src/executors/claude-cli.js.map +1 -0
  99. package/dist/src/executors/claude-sdk-trace.d.ts +10 -0
  100. package/dist/src/executors/claude-sdk-trace.d.ts.map +1 -0
  101. package/dist/src/executors/claude-sdk-trace.js +100 -0
  102. package/dist/src/executors/claude-sdk-trace.js.map +1 -0
  103. package/dist/src/executors/claude-sdk.d.ts +3 -0
  104. package/dist/src/executors/claude-sdk.d.ts.map +1 -0
  105. package/dist/src/executors/claude-sdk.js +160 -0
  106. package/dist/src/executors/claude-sdk.js.map +1 -0
  107. package/dist/src/executors/gemini.d.ts +3 -0
  108. package/dist/src/executors/gemini.d.ts.map +1 -0
  109. package/dist/src/executors/gemini.js +78 -0
  110. package/dist/src/executors/gemini.js.map +1 -0
  111. package/dist/src/executors/index.d.ts +7 -0
  112. package/dist/src/executors/index.d.ts.map +1 -0
  113. package/dist/src/executors/index.js +22 -0
  114. package/dist/src/executors/index.js.map +1 -0
  115. package/dist/src/executors/openai-api.d.ts +3 -0
  116. package/dist/src/executors/openai-api.d.ts.map +1 -0
  117. package/dist/src/executors/openai-api.js +40 -0
  118. package/dist/src/executors/openai-api.js.map +1 -0
  119. package/dist/src/executors/openai-cli.d.ts +3 -0
  120. package/dist/src/executors/openai-cli.d.ts.map +1 -0
  121. package/dist/src/executors/openai-cli.js +60 -0
  122. package/dist/src/executors/openai-cli.js.map +1 -0
  123. package/dist/src/executors/script.d.ts +3 -0
  124. package/dist/src/executors/script.d.ts.map +1 -0
  125. package/dist/src/executors/script.js +63 -0
  126. package/dist/src/executors/script.js.map +1 -0
  127. package/dist/src/executors/shared.d.ts +117 -0
  128. package/dist/src/executors/shared.d.ts.map +1 -0
  129. package/dist/src/executors/shared.js +49 -0
  130. package/dist/src/executors/shared.js.map +1 -0
  131. package/dist/src/grading/assertions.d.ts +18 -0
  132. package/dist/src/grading/assertions.d.ts.map +1 -0
  133. package/dist/src/grading/assertions.js +239 -0
  134. package/dist/src/grading/assertions.js.map +1 -0
  135. package/dist/src/grading/index.d.ts +26 -0
  136. package/dist/src/grading/index.d.ts.map +1 -0
  137. package/dist/src/grading/index.js +98 -0
  138. package/dist/src/grading/index.js.map +1 -0
  139. package/dist/src/grading/judge.d.ts +13 -0
  140. package/dist/src/grading/judge.d.ts.map +1 -0
  141. package/dist/src/grading/judge.js +98 -0
  142. package/dist/src/grading/judge.js.map +1 -0
  143. package/dist/src/grading/layered-scores.d.ts +13 -0
  144. package/dist/src/grading/layered-scores.d.ts.map +1 -0
  145. package/dist/src/grading/layered-scores.js +62 -0
  146. package/dist/src/grading/layered-scores.js.map +1 -0
  147. package/dist/src/inputs/eval-config.d.ts +13 -0
  148. package/dist/src/inputs/eval-config.d.ts.map +1 -0
  149. package/dist/src/inputs/eval-config.js +136 -0
  150. package/dist/src/inputs/eval-config.js.map +1 -0
  151. package/dist/src/inputs/load-samples.d.ts +16 -0
  152. package/dist/src/inputs/load-samples.d.ts.map +1 -0
  153. package/dist/src/inputs/load-samples.js +51 -0
  154. package/dist/src/inputs/load-samples.js.map +1 -0
  155. package/dist/src/inputs/mcp-resolver.d.ts +50 -0
  156. package/dist/src/inputs/mcp-resolver.d.ts.map +1 -0
  157. package/dist/src/inputs/mcp-resolver.js +307 -0
  158. package/dist/src/inputs/mcp-resolver.js.map +1 -0
  159. package/dist/src/inputs/skill-loader.d.ts +18 -0
  160. package/dist/src/inputs/skill-loader.d.ts.map +1 -0
  161. package/dist/src/inputs/skill-loader.js +222 -0
  162. package/dist/src/inputs/skill-loader.js.map +1 -0
  163. package/dist/src/inputs/url-fetcher.d.ts +19 -0
  164. package/dist/src/inputs/url-fetcher.d.ts.map +1 -0
  165. package/dist/src/inputs/url-fetcher.js +241 -0
  166. package/dist/src/inputs/url-fetcher.js.map +1 -0
  167. package/dist/src/observability/production-analyzer.d.ts +61 -0
  168. package/dist/src/observability/production-analyzer.d.ts.map +1 -0
  169. package/dist/src/observability/production-analyzer.js +183 -0
  170. package/dist/src/observability/production-analyzer.js.map +1 -0
  171. package/dist/src/observability/trace-adapter.d.ts +75 -0
  172. package/dist/src/observability/trace-adapter.d.ts.map +1 -0
  173. package/dist/src/observability/trace-adapter.js +341 -0
  174. package/dist/src/observability/trace-adapter.js.map +1 -0
  175. package/dist/src/renderer/html-renderer.d.ts +9 -0
  176. package/dist/src/renderer/html-renderer.d.ts.map +1 -0
  177. package/dist/src/renderer/html-renderer.js +338 -0
  178. package/dist/src/renderer/html-renderer.js.map +1 -0
  179. package/dist/src/renderer/layout.d.ts +13 -0
  180. package/dist/src/renderer/layout.d.ts.map +1 -0
  181. package/dist/src/renderer/layout.js +476 -0
  182. package/dist/src/renderer/layout.js.map +1 -0
  183. package/dist/src/renderer/skill-health-renderer.d.ts +20 -0
  184. package/dist/src/renderer/skill-health-renderer.d.ts.map +1 -0
  185. package/dist/src/renderer/skill-health-renderer.js +218 -0
  186. package/dist/src/renderer/skill-health-renderer.js.map +1 -0
  187. package/dist/src/renderer/summary.d.ts +21 -0
  188. package/dist/src/renderer/summary.d.ts.map +1 -0
  189. package/dist/src/renderer/summary.js +1072 -0
  190. package/dist/src/renderer/summary.js.map +1 -0
  191. package/dist/src/renderer/table.d.ts +3 -0
  192. package/dist/src/renderer/table.d.ts.map +1 -0
  193. package/dist/src/renderer/table.js +134 -0
  194. package/dist/src/renderer/table.js.map +1 -0
  195. package/dist/src/renderer/trends.d.ts +6 -0
  196. package/dist/src/renderer/trends.d.ts.map +1 -0
  197. package/dist/src/renderer/trends.js +127 -0
  198. package/dist/src/renderer/trends.js.map +1 -0
  199. package/dist/src/server/job-store.d.ts +4 -0
  200. package/dist/src/server/job-store.d.ts.map +1 -0
  201. package/dist/src/server/job-store.js +68 -0
  202. package/dist/src/server/job-store.js.map +1 -0
  203. package/dist/src/server/report-server.d.ts +16 -0
  204. package/dist/src/server/report-server.d.ts.map +1 -0
  205. package/dist/src/server/report-server.js +196 -0
  206. package/dist/src/server/report-server.js.map +1 -0
  207. package/dist/src/server/report-store.d.ts +44 -0
  208. package/dist/src/server/report-store.d.ts.map +1 -0
  209. package/dist/src/server/report-store.js +190 -0
  210. package/dist/src/server/report-store.js.map +1 -0
  211. package/dist/src/types.d.ts +553 -0
  212. package/dist/src/types.d.ts.map +1 -0
  213. package/dist/src/types.js +2 -0
  214. package/dist/src/types.js.map +1 -0
  215. package/package.json +44 -3
@@ -0,0 +1,1072 @@
1
+ import { e, fmtNum, fmtCost, fmtDuration, COLORS, t } from './layout.js';
2
+ import { pValueCategory } from '../eval-core/statistics.js';
3
+ export function renderSummaryCards(variants, summary, lang, variance) {
4
+ // 六维对比表格:事实 / 行为 / LLM 评价 / 成本 / 效率 / 稳定性。
5
+ // 前三列(事实/行为/LLM 评价)是 task 级原始指标的 variant 聚合;成本/效率同。
6
+ // 稳定性是 variant 级散度度量(需 --repeat ≥ 2)。
7
+ //
8
+ // composite 合成分 (= (fact + behavior + judge) / 3) 从 v0.16 起不再在此表主视觉呈现,
9
+ // 仅保留在 report JSON 数据层 + Variance & Significance 表顶层 flat 字段(legacy)。
10
+ // 理由见 docs/terminology-spec.md 三-6 节:三层独立呈现避免合成分掩盖结构性差异。
11
+ const headerCols = [
12
+ { key: 'dimFact', label: t('dimFact', lang) },
13
+ { key: 'dimBehavior', label: t('dimBehavior', lang) },
14
+ { key: 'dimJudge', label: t('dimJudge', lang) },
15
+ { key: 'dimCost', label: t('dimCost', lang) },
16
+ { key: 'dimEfficiency', label: t('dimEfficiency', lang) },
17
+ { key: 'dimStability', label: t('dimStability', lang) },
18
+ ];
19
+ const thead = `<tr><th data-i18n="variants">${t('variants', lang)}</th>${headerCols.map((c) => `<th data-i18n="${c.key}">${c.label}</th>`).join('')}</tr>`;
20
+ // 渲染单层分数 cell(事实/行为/LLM 评价通用)。
21
+ // 优先读跨 run 均值(byLayer[key].mean),fallback 到 summary 的单 run avg。
22
+ // 缺数据时显示 "—" + 灰色,让读者明确看到"这一层这批样本/评委没测到",不假装有值。
23
+ function renderLayerCell(varianceMean, summaryValue, detailHtml = '') {
24
+ const v = varianceMean ?? summaryValue;
25
+ const hasValue = typeof v === 'number' && v > 0;
26
+ const color = hasValue
27
+ ? (v >= 4 ? 'var(--green)' : v >= 3 ? 'var(--yellow)' : 'var(--red)')
28
+ : 'var(--text-muted)';
29
+ const display = hasValue ? v.toFixed(2) : '—';
30
+ return `<td class="summary-cell"><div class="summary-value summary-value-primary" style="color:${color}">${display}</div>${detailHtml}</td>`;
31
+ }
32
+ const rows = variants.map((v, i) => {
33
+ const s = summary[v] || {};
34
+ const vd = variance?.perVariant[v];
35
+ const color = COLORS[i % COLORS.length];
36
+ // 事实层 cell(含事实验证率 detail,如果有)
37
+ const factDetailParts = [];
38
+ if (s.avgFactVerifiedRate != null) {
39
+ const pct = Math.round(s.avgFactVerifiedRate * 100);
40
+ factDetailParts.push(`${lang === 'zh' ? '验证率' : 'Verified'} ${pct}%`);
41
+ }
42
+ const factDetail = factDetailParts.length > 0 ? `<div class="summary-detail">${factDetailParts.join(' · ')}</div>` : '';
43
+ const factCell = renderLayerCell(vd?.byLayer?.fact?.mean, s.avgFactScore, factDetail);
44
+ // 行为层 cell(暂无 detail,后续可按工具调用成功率之类扩展)
45
+ const behaviorCell = renderLayerCell(vd?.byLayer?.behavior?.mean, s.avgBehaviorScore);
46
+ // LLM 评价层 cell
47
+ const judgeCell = renderLayerCell(vd?.byLayer?.judge?.mean, s.avgJudgeScore);
48
+ // Cost — only show execution cost (judge cost is tool overhead, not skill cost)
49
+ const execCost = s.totalExecCostUSD || 0;
50
+ const hasCost = execCost > 0 || (s.avgTotalTokens || 0) > 0;
51
+ const costCell = hasCost
52
+ ? `<td class="summary-cell"><div class="summary-value">${fmtCost(execCost)}</div><div class="summary-detail">${fmtNum(s.avgTotalTokens)} tokens/${t('tokPerReq', lang).replace('tokens/', '')}</div></td>`
53
+ : `<td class="summary-cell"><span style="color:var(--text-muted)">N/A</span></td>`;
54
+ // Efficiency
55
+ const effDetails = [];
56
+ const displayTurns = s.avgFullNumTurns ?? s.avgNumTurns;
57
+ if ((displayTurns || 0) > 0)
58
+ effDetails.push(`${displayTurns} ${t('turnsPerReq', lang)}`);
59
+ if (s.avgToolCalls != null && s.avgToolCalls > 0) {
60
+ const srPct = s.toolSuccessRate != null ? ` (${(s.toolSuccessRate * 100).toFixed(0)}% OK)` : '';
61
+ effDetails.push(`${s.avgToolCalls} tools/req${srPct}`);
62
+ }
63
+ const totalDurationMs = (s.avgDurationMs || 0) * (s.successCount || 0);
64
+ if (totalDurationMs > 0)
65
+ effDetails.push(`${lang === 'zh' ? '总计' : 'total'} ${fmtDuration(totalDurationMs)}`);
66
+ const effDetail = effDetails.length > 0 ? `<div class="summary-detail">${effDetails.join(' · ')}</div>` : '';
67
+ const avgLabel = lang === 'zh' ? '次' : 'req';
68
+ const effCell = `<td class="summary-cell"><div class="summary-value">${fmtDuration(s.avgDurationMs)}<span class="summary-unit">/${avgLabel}</span></div>${effDetail}</td>`;
69
+ // Stability — 多次运行分数一致性(test-retest reliability),统计学定义的稳定性。
70
+ // 主视觉:白话定性词 + ±σ 直观量级,让读者一眼判断。
71
+ // 副区:CV (变异系数 = σ / mean) 分两行展示 + 95% CI(置信区间)。
72
+ // 无 --repeat(variance 缺失) 时主值显示 "—" + 明示需多跑,不虚报 100%——
73
+ // 符合 omk 叙事底线"诚实交代测不到什么"。
74
+ //
75
+ // 跨样本 min~max range 不是稳定性(反映样本难度差异,非 variant 波动),已从此列移除。
76
+ // 成功率 ≠ 稳定性(执行完成率,不是分数一致性),降级到 < 100% 时的副区 alert。
77
+ const total = s.totalSamples || 0;
78
+ const successCount = s.successCount || 0;
79
+ const errorCount = s.errorCount || 0;
80
+ const successRate = total > 0 ? Number((successCount / total * 100).toFixed(1)) : 0;
81
+ const stabDetails = [];
82
+ let stabValue;
83
+ let stabColor;
84
+ // CV = σ / mean,当 mean 过小(接近 0)时 CV 发散,数值无参考价值——1-5 分数量纲下
85
+ // mean < 0.5 已属全灭场景,直接降级显示"—"。负 mean(理论上不会出现)也走降级。
86
+ if (vd && typeof vd.stddev === 'number' && typeof vd.mean === 'number' && vd.mean >= 0.5) {
87
+ const cv = Math.abs(vd.stddev / vd.mean);
88
+ const cvPct = cv * 100;
89
+ const sigma = vd.stddev;
90
+ // 主值用白话定性 + ±σ 直观量级,让非统计背景读者一眼判断。
91
+ // CV 和 CI 下沉到副区,给懂的人细看。阈值面向 1-5 分数量纲的经验值。
92
+ let label;
93
+ if (cvPct < 5) {
94
+ label = lang === 'zh' ? '稳定' : 'Stable';
95
+ stabColor = 'var(--green)';
96
+ }
97
+ else if (cvPct < 15) {
98
+ label = lang === 'zh' ? '较稳' : 'Moderate';
99
+ stabColor = 'var(--yellow)';
100
+ }
101
+ else {
102
+ label = lang === 'zh' ? '波动大' : 'Variable';
103
+ stabColor = 'var(--red)';
104
+ }
105
+ stabValue = `${label} · ±${fmtNum(sigma, 2)}`;
106
+ const ciLo = fmtNum(vd.lower, 2);
107
+ const ciHi = fmtNum(vd.upper, 2);
108
+ stabDetails.push(`CV ${cvPct.toFixed(1)}% · 95% CI [${ciLo}, ${ciHi}]`);
109
+ }
110
+ else {
111
+ // No cross-run data → honestly say "not measurable with single run".
112
+ stabValue = '—';
113
+ stabColor = 'var(--text-muted)';
114
+ stabDetails.push(`<span style="color:var(--text-muted)">${lang === 'zh' ? '需 --repeat ≥ 2' : 'needs --repeat ≥ 2'}</span>`);
115
+ }
116
+ // Execution-completion alerts:success rate < 100% 时降级到此处,避免和"稳定性"语义混淆。
117
+ if (errorCount > 0) {
118
+ stabDetails.unshift(`<span style="color:var(--red)">${successRate}% ${lang === 'zh' ? '完成率' : 'completed'} · ${errorCount} ${t('errors', lang)}</span>`);
119
+ }
120
+ // 正常情况副区只一行:`CV X.X% · 95% CI [...]`。
121
+ // 有 alert(成功率 < 100%) 时把 alert 放第一行、CV+CI 放第二行,让异常信息第一眼看到。
122
+ const stabDetail = stabDetails.length > 0
123
+ ? stabDetails.map((d) => `<div class="summary-detail">${d}</div>`).join('')
124
+ : '';
125
+ const stabCell = `<td class="summary-cell"><div class="summary-value" style="color:${stabColor}">${stabValue}</div>${stabDetail}</td>`;
126
+ return `<tr><td style="border-left:3px solid ${color};padding-left:12px"><strong>${e(v)}</strong></td>${factCell}${behaviorCell}${judgeCell}${costCell}${effCell}${stabCell}</tr>`;
127
+ }).join('');
128
+ const guideModalId = 'guide-six-dims';
129
+ const guideTitle = lang === 'zh' ? '如何阅读六维对比?' : 'How to read this 6-dim comparison?';
130
+ const guideIntro = lang === 'zh'
131
+ ? '每行是一个实验分组(Variant),六列分别衡量不同维度:'
132
+ : 'Each row is a Variant. Six columns measure independent dimensions:';
133
+ const icon = (emoji) => `<span aria-hidden="true">${emoji}</span>`;
134
+ // 维度分隔加粗(border-top 2px),让四维的视觉边界更明显。sub 缩进从 28 收到 22。
135
+ const dim = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-primary);font-weight:600"';
136
+ const dimDesc = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-secondary)"';
137
+ const dimFirst = 'style="padding:4px 0 4px;color:var(--text-primary);font-weight:600"';
138
+ const dimFirstDesc = 'style="padding:4px 0 4px;color:var(--text-secondary)"';
139
+ const sub = 'style="padding:2px 0 2px 22px;font-size:12px;color:var(--text-secondary);font-weight:500"';
140
+ const subDesc = 'style="padding:2px 0;font-size:12px;color:var(--text-muted)"';
141
+ const guideRows = lang === 'zh' ? `
142
+ <tr><td ${dimFirst}>${icon('📋')} <strong>事实</strong></td><td ${dimFirstDesc}>模型的输出说得对不对(事实声明层面)。靠规则断言判:关键词是否出现、JSON 格式是否合法等,答错了直接不给分。</td></tr>
143
+ <tr><td ${dim}>${icon('🛠️')} <strong>行为</strong></td><td ${dimDesc}>模型做事的过程有没有走对路。靠规则断言判:该调的工具有没有调、有没有超过轮次/成本上限。</td></tr>
144
+ <tr><td ${dim}>${icon('💬')} <strong>LLM 评价</strong></td><td ${dimDesc}>请一个 LLM 当评委,让它读被测模型的输出内容,按预先写好的评分规则(英文叫 rubric)打个 1-5 分。主观但能抓到规则断言判不了的"整体好不好"——比如回答是否清晰、有没有答非所问。</td></tr>
145
+ <tr><td ${dim}>${icon('💰')} <strong>成本</strong></td><td ${dimDesc}>跑这次评测花了多少 API 调用费(只算执行成本,评委那个 LLM 的钱不算进来)。</td></tr>
146
+ <tr><td ${dim}>${icon('⚡')} <strong>效率</strong></td><td ${dimDesc}>一次评测平均跑多久;附带轮次数和工具调用次数。</td></tr>
147
+ <tr><td ${dim}>${icon('🛡️')} <strong>稳定性</strong></td><td ${dimDesc}>同一份测试跑很多次,分数抖不抖。抖得越少越稳定。<strong>跑一次看不出稳定性</strong>——至少要 <code>--repeat ≥ 2</code>,不然显示"—"。</td></tr>
148
+ <tr><td ${sub}>稳定 / 较稳 / 波动大</td><td ${subDesc}>分数波动比例 &lt;5% = 稳定 · 5~15% = 一般 · &gt;15% = 波动大</td></tr>
149
+ <tr><td ${sub}>±σ</td><td ${subDesc}>每次跑出的分数,大概在平均分上下浮动多少。1-5 分数里 ±0.05 几乎不抖、±0.5 抖得很厉害</td></tr>
150
+ <tr><td ${sub}>CV</td><td ${subDesc}>分数抖动幅度占平均分的比例(例:CV 2% = 分数波动大约是平均分的 2%)</td></tr>
151
+ <tr><td ${sub}>95% CI</td><td ${subDesc}>如果跑无数次求平均,真实平均分有 95% 概率落在这个范围里——范围越窄,这次测出的均值越可信</td></tr>
152
+ ` : `
153
+ <tr><td ${dimFirst}>${icon('📋')} <strong>Fact</strong></td><td ${dimFirstDesc}>Whether the model's output is factually correct. Checked by rule-based assertions — keyword matches, JSON schema validity, etc. Wrong = zero.</td></tr>
154
+ <tr><td ${dim}>${icon('🛠️')} <strong>Behavior</strong></td><td ${dimDesc}>Whether the model followed the right process. Checked by rule-based assertions — did it call the expected tools, stay within turn/cost limits.</td></tr>
155
+ <tr><td ${dim}>${icon('💬')} <strong>LLM judge</strong></td><td ${dimDesc}>A separate LLM acts as judge: it reads the tested model's output and scores it 1-5 against a predefined rubric. Subjective, but catches "overall feel" that rule-based assertions miss — e.g., whether the answer is clear, whether it's on-topic.</td></tr>
156
+ <tr><td ${dim}>${icon('💰')} <strong>Cost</strong></td><td ${dimDesc}>API cost of this run (execution only — the judge LLM's cost isn't included here).</td></tr>
157
+ <tr><td ${dim}>${icon('⚡')} <strong>Efficiency</strong></td><td ${dimDesc}>Average time per evaluation, plus turn counts and tool call stats.</td></tr>
158
+ <tr><td ${dim}>${icon('🛡️')} <strong>Stability</strong></td><td ${dimDesc}>How much the score swings when you repeat the same test. Less swing = more stable. <strong>You can't measure stability from a single run</strong> — need <code>--repeat ≥ 2</code>, otherwise shows "—".</td></tr>
159
+ <tr><td ${sub}>Stable / Moderate / Variable</td><td ${subDesc}>Score swing as % of mean: &lt;5% = Stable · 5~15% = Moderate · &gt;15% = Variable</td></tr>
160
+ <tr><td ${sub}>±σ</td><td ${subDesc}>How much each run's score typically swings around the mean. On a 1-5 scale, ±0.05 barely moves, ±0.5 swings a lot</td></tr>
161
+ <tr><td ${sub}>CV</td><td ${subDesc}>Score swing as a percentage of the mean (e.g., CV 2% = swings are about 2% of the mean)</td></tr>
162
+ <tr><td ${sub}>95% CI</td><td ${subDesc}>If you ran infinitely many times, the true mean has a 95% chance of falling in this range — narrower = you can trust the measured mean more</td></tr>
163
+ `;
164
+ return `
165
+ <h2 style="display:flex;align-items:center;gap:4px">${lang === 'zh' ? '六维对比' : '6-Dim Comparison'} <button type="button" class="hint-btn" onclick="openModal('${guideModalId}')" aria-label="${e(guideTitle)}" aria-haspopup="dialog">?</button></h2>
166
+ <div id="${guideModalId}" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="${guideModalId}-title" onclick="if(event.target===this)closeModal('${guideModalId}')">
167
+ <div class="modal-content">
168
+ <div class="modal-header">
169
+ <strong id="${guideModalId}-title" style="font-size:1rem">${e(guideTitle)}</strong>
170
+ <button type="button" class="modal-close" onclick="closeModal('${guideModalId}')" aria-label="${lang === 'zh' ? '关闭' : 'Close'}">✕</button>
171
+ </div>
172
+ <p style="font-size:13px;color:var(--text-secondary);margin:4px 0 16px">${e(guideIntro)}</p>
173
+ <table class="modal-table"><tbody>${guideRows}</tbody></table>
174
+ </div>
175
+ </div>
176
+ <div class="table-wrap">
177
+ <table class="summary-table">
178
+ <thead>${thead}</thead>
179
+ <tbody>${rows}</tbody>
180
+ </table>
181
+ </div>`;
182
+ }
183
+ function buildDiagnostic(metric, cfg, winner, lang) {
184
+ const es = metric.effectSize;
185
+ if (!es || es.primary === 'none') {
186
+ return {
187
+ icon: '—',
188
+ text: lang === 'zh' ? '数据不足,无法判断' : 'insufficient data',
189
+ color: 'var(--text-muted)',
190
+ };
191
+ }
192
+ const isStrong = es.magnitude === 'medium' || es.magnitude === 'large';
193
+ const strongLabelZh = es.magnitude === 'large' ? '大' : '中';
194
+ const strongLabelEn = es.magnitude;
195
+ // Direction word bound into the diagnostic text so readers cannot misread
196
+ // green ✓ as "good for v2" when it actually means "v1 is significantly
197
+ // cheaper / faster / higher-quality". Each metric uses its own natural verb.
198
+ const winnerPhrase = winner
199
+ ? (lang === 'zh'
200
+ ? `${winner} 显著${cfg.winnerWordZh}`
201
+ : `${winner} significantly ${cfg.winnerWordEn.replace(' by', '')}`)
202
+ : '';
203
+ if (metric.significant && isStrong) {
204
+ return {
205
+ icon: '✓',
206
+ text: lang === 'zh'
207
+ ? `${winnerPhrase}(${strongLabelZh}差异)`
208
+ : `${winnerPhrase} (${strongLabelEn} effect)`,
209
+ color: 'var(--green)',
210
+ };
211
+ }
212
+ if (metric.significant && !isStrong) {
213
+ return {
214
+ icon: '⚠',
215
+ text: lang === 'zh'
216
+ ? '统计显著但效应微弱,别过度解读'
217
+ : 'significant but effect is trivial — do not overinterpret',
218
+ color: 'var(--yellow)',
219
+ };
220
+ }
221
+ if (!metric.significant && isStrong) {
222
+ const leadPhrase = winner
223
+ ? (lang === 'zh'
224
+ ? `${winner} 看似${cfg.winnerWordZh}但样本不足,建议加大 --repeat`
225
+ : `${winner} looks ${cfg.winnerWordEn.replace(' by', '')} but underpowered — increase --repeat`)
226
+ : (lang === 'zh'
227
+ ? `${strongLabelZh}差异但样本不足,建议加大 --repeat`
228
+ : `${strongLabelEn} effect but underpowered — increase --repeat`);
229
+ return {
230
+ icon: '⚠',
231
+ text: leadPhrase,
232
+ color: 'var(--yellow)',
233
+ };
234
+ }
235
+ return {
236
+ icon: '—',
237
+ text: lang === 'zh' ? '两变体相当,无实质差异' : 'no meaningful difference',
238
+ color: 'var(--text-muted)',
239
+ };
240
+ }
241
+ // Three-layer breakdown labels (PR-2). Rendered inside the expandable
242
+ // `<details>` beneath each comparison; composite still lives on the top table.
243
+ // UI 命名:事实 / 行为 / LLM 评价(字段 judge)——前两层规则验证,第三层 LLM 主观评分。
244
+ const LAYER_LABELS = {
245
+ fact: { zh: '事实', en: 'Fact' },
246
+ behavior: { zh: '行为', en: 'Behavior' },
247
+ judge: { zh: 'LLM 评价', en: 'LLM judge' },
248
+ };
249
+ const METRIC_CONFIGS = [
250
+ {
251
+ key: 'composite',
252
+ labelZh: '质量',
253
+ labelEn: 'Quality',
254
+ higherIsBetter: true,
255
+ winnerWordZh: '胜出',
256
+ winnerWordEn: 'wins by',
257
+ formatValue: (v) => `${v.toFixed(2)} 分`,
258
+ showRawEffectSize: true,
259
+ showPercent: true,
260
+ percentWordZh: '高',
261
+ percentWordEn: 'higher',
262
+ },
263
+ {
264
+ key: 'cost',
265
+ labelZh: '成本',
266
+ labelEn: 'Cost',
267
+ higherIsBetter: false,
268
+ winnerWordZh: '更便宜',
269
+ winnerWordEn: 'cheaper by',
270
+ formatValue: (v) => `$${v.toFixed(4)}`,
271
+ showRawEffectSize: true,
272
+ showPercent: true,
273
+ percentWordZh: '便宜',
274
+ percentWordEn: 'cheaper',
275
+ },
276
+ {
277
+ key: 'efficiency',
278
+ labelZh: '效率',
279
+ labelEn: 'Efficiency',
280
+ higherIsBetter: false,
281
+ winnerWordZh: '更快',
282
+ winnerWordEn: 'faster by',
283
+ formatValue: (v) => `${(v / 1000).toFixed(1)}s`,
284
+ showRawEffectSize: true,
285
+ showPercent: true,
286
+ percentWordZh: '快',
287
+ percentWordEn: 'faster',
288
+ },
289
+ ];
290
+ function pickMetricFromComparison(comp, key) {
291
+ if (key === 'composite') {
292
+ return {
293
+ meanDiff: comp.meanDiff,
294
+ tStatistic: comp.tStatistic,
295
+ df: comp.df,
296
+ significant: comp.significant,
297
+ effectSize: comp.effectSize,
298
+ };
299
+ }
300
+ return comp.byMetric?.[key] ?? null;
301
+ }
302
+ function getVariantMetricMean(variance, variant, key) {
303
+ const v = variance.perVariant[variant];
304
+ if (!v)
305
+ return null;
306
+ if (key === 'composite')
307
+ return v.mean;
308
+ return v.byMetric?.[key]?.mean ?? null;
309
+ }
310
+ export function renderVarianceComparisons(variance, lang, layeredStatsOpen = false) {
311
+ if (!variance || !variance.comparisons || variance.comparisons.length === 0)
312
+ return '';
313
+ const modalId = 'guide-variance-comparisons';
314
+ const title = lang === 'zh' ? '方差与显著性' : 'Variance & Significance';
315
+ const guideTitle = lang === 'zh' ? '如何阅读这张表?' : 'How to read this table?';
316
+ const headerLabels = lang === 'zh'
317
+ ? ['对比', '维度', '差距', '效应量', '显著性', '诊断']
318
+ : ['Comparison', 'Metric', 'Gap', 'Effect size', 'Significance', 'Diagnostic'];
319
+ const thead = `<tr>${headerLabels.map((h, i) => {
320
+ const cls = i === 5 ? ' class="diagnostic-cell"' : '';
321
+ return `<th${cls}>${h}</th>`;
322
+ }).join('')}</tr>`;
323
+ const magnitudeZh = { negligible: '可忽略', small: '小效应', medium: '中效应', large: '大效应' };
324
+ const magnitudeEn = { negligible: 'negligible', small: 'small effect', medium: 'medium effect', large: 'large effect' };
325
+ // Determine the winning variant for a given metric. Returns null if tied.
326
+ function pickWinner(metric, cfg, a, b) {
327
+ const diffAbs = Math.abs(metric.meanDiff);
328
+ if (diffAbs < 1e-9)
329
+ return null;
330
+ const rawHigherVariant = metric.meanDiff > 0 ? a : b;
331
+ return cfg.higherIsBetter ? rawHigherVariant : (metric.meanDiff > 0 ? b : a);
332
+ }
333
+ function buildMetricRowCells(metric, cfg, a, b, aMean, bMean, fadeDiagnostic) {
334
+ // Gap cell — metric-aware direction word + optional relative percent
335
+ const winner = pickWinner(metric, cfg, a, b);
336
+ const diffAbs = Math.abs(metric.meanDiff);
337
+ let gapCell;
338
+ if (!winner) {
339
+ gapCell = `<div class="verdict-line">${lang === 'zh' ? '持平' : 'tied'}</div>`;
340
+ }
341
+ else {
342
+ const winnerWord = lang === 'zh' ? cfg.winnerWordZh : cfg.winnerWordEn;
343
+ const detailParts = [cfg.formatValue(diffAbs)];
344
+ if (cfg.showPercent && aMean != null && bMean != null) {
345
+ const denom = Math.max(Math.abs(aMean), Math.abs(bMean));
346
+ if (denom > 0) {
347
+ const pct = (diffAbs / denom) * 100;
348
+ const pctWord = lang === 'zh' ? cfg.percentWordZh : cfg.percentWordEn;
349
+ detailParts.push(lang === 'zh' ? `${pctWord} ${pct.toFixed(0)}%` : `${pct.toFixed(0)}% ${pctWord}`);
350
+ }
351
+ }
352
+ gapCell = `
353
+ <div class="verdict-line"><strong>${e(winner)}</strong> ${winnerWord}</div>
354
+ <div class="detail-line">${detailParts.join(' · ')}</div>`;
355
+ }
356
+ // Effect size cell
357
+ const es = metric.effectSize;
358
+ let esCell;
359
+ if (!es || es.primary === 'none') {
360
+ esCell = `<div class="verdict-line">${lang === 'zh' ? '数据不足' : 'insufficient data'}</div>`;
361
+ }
362
+ else {
363
+ const magnitudeLabel = (lang === 'zh' ? magnitudeZh : magnitudeEn)[es.magnitude] || es.magnitude;
364
+ const detail = cfg.showRawEffectSize
365
+ ? `g=${Math.abs(es.hedgesG).toFixed(2)} · d=${Math.abs(es.cohensD).toFixed(2)} · n=${es.n1}+${es.n2}`
366
+ : `n=${es.n1}+${es.n2}`;
367
+ esCell = `
368
+ <div class="verdict-line">${magnitudeLabel}</div>
369
+ <div class="detail-line">${detail}</div>`;
370
+ }
371
+ // Significance cell — uniform binary verdict. Both sides just say
372
+ // "显著" or "不显著"; the p-value bucket is shown in the detail line
373
+ // so readers can distinguish "barely significant" from "strongly significant".
374
+ const sigText = metric.significant
375
+ ? (lang === 'zh' ? '显著' : 'significant')
376
+ : (lang === 'zh' ? '不显著' : 'not significant');
377
+ const pBucket = pValueCategory(metric.tStatistic, metric.df);
378
+ const sigCell = `
379
+ <div class="verdict-line">${sigText}</div>
380
+ <div class="detail-line">p${pBucket} · t=${metric.tStatistic.toFixed(2)} · df=${metric.df}</div>`;
381
+ // Diagnostic cell: colored icon + bold colored text with direction bound in.
382
+ // Visually fade if this diagnostic text is a consecutive duplicate of the row above.
383
+ const diag = buildDiagnostic(metric, cfg, winner, lang);
384
+ const fadeClass = fadeDiagnostic ? ' diag-faded' : '';
385
+ // inline-flex so the td's text-align:center centers the icon+text unit as
386
+ // a whole (a plain flex div would stretch to fill the cell and align left).
387
+ const diagCell = `
388
+ <div class="diag-cell${fadeClass}" style="display:inline-flex;align-items:center;gap:6px;line-height:1.5;text-align:left">
389
+ <span style="color:${diag.color};font-size:14px;flex-shrink:0">${diag.icon}</span>
390
+ <strong style="color:${diag.color}">${diag.text}</strong>
391
+ </div>`;
392
+ const metricLabel = lang === 'zh' ? cfg.labelZh : cfg.labelEn;
393
+ return `
394
+ <td class="verdict-line">${metricLabel}</td>
395
+ <td>${gapCell}</td>
396
+ <td>${esCell}</td>
397
+ <td>${sigCell}</td>
398
+ <td class="diagnostic-cell">${diagCell}</td>`;
399
+ }
400
+ // Per-layer display config — same shape as composite quality, just re-labeled.
401
+ // Composite rows live in METRIC_CONFIGS (the outer variance table); layer rows
402
+ // live inside an expandable <details> and only render when byLayer data exists.
403
+ function layerCfg(key) {
404
+ const labels = LAYER_LABELS[key];
405
+ return {
406
+ labelZh: labels.zh,
407
+ labelEn: labels.en,
408
+ higherIsBetter: true,
409
+ winnerWordZh: '胜出',
410
+ winnerWordEn: 'wins by',
411
+ formatValue: (v) => lang === 'zh' ? `${v.toFixed(2)} 分` : `${v.toFixed(2)} pts`,
412
+ showRawEffectSize: true,
413
+ showPercent: true,
414
+ percentWordZh: '高',
415
+ percentWordEn: 'higher',
416
+ };
417
+ }
418
+ function renderLayerBreakdown(comp) {
419
+ if (!comp.byLayer || Object.keys(comp.byLayer).length === 0)
420
+ return '';
421
+ const layerRows = ['fact', 'behavior', 'judge'].map((key) => {
422
+ const m = comp.byLayer?.[key];
423
+ if (!m)
424
+ return '';
425
+ const aMean = variance.perVariant[comp.a]?.byLayer?.[key]?.mean ?? null;
426
+ const bMean = variance.perVariant[comp.b]?.byLayer?.[key]?.mean ?? null;
427
+ const cfg = layerCfg(key);
428
+ return `<tr>${buildMetricRowCells(m, cfg, comp.a, comp.b, aMean, bMean, false)}</tr>`;
429
+ }).filter(Boolean).join('');
430
+ if (!layerRows)
431
+ return '';
432
+ const summaryLabel = lang === 'zh'
433
+ ? '展开三层独立显著性(fact / behavior / judge)'
434
+ : 'Show three-layer independent significance (fact / behavior / judge)';
435
+ const openAttr = layeredStatsOpen ? ' open' : '';
436
+ // 多重比较 disclaimer:三层独立 t 检验,family-wise error 未矫正;小样本 Cohen's d 不稳。
437
+ // 不默默修改 significant 判定(避免用户被"自动矫正"误导),而是把判读责任明示交给读者。
438
+ const disclaimerText = lang === 'zh'
439
+ ? '⚠ 三层独立检验:p 值未做多重比较矫正(建议按 Bonferroni α/3 = 0.017 判断显著);小样本(n ≤ 10)下 Cohen\'s d 效应量标签仅供探索参考,不作结论'
440
+ : '⚠ Three independent tests: p values are NOT corrected for multiple comparisons (use Bonferroni α/3 = 0.017 as the stricter threshold). With small samples (n ≤ 10), Cohen\'s d magnitude labels are exploratory only';
441
+ return `
442
+ <tr class="layer-breakdown-row">
443
+ <td colspan="6">
444
+ <details class="layer-breakdown"${openAttr}>
445
+ <summary>${e(summaryLabel)}</summary>
446
+ <div class="layer-breakdown-disclaimer">${e(disclaimerText)}</div>
447
+ <table class="summary-table variance-table layer-sub-table">
448
+ <tbody>${layerRows}</tbody>
449
+ </table>
450
+ </details>
451
+ </td>
452
+ </tr>`;
453
+ }
454
+ const rows = variance.comparisons.map((comp) => {
455
+ // Collect available metric rows for this comparison
456
+ const availableMetrics = [];
457
+ for (const cfg of METRIC_CONFIGS) {
458
+ const m = pickMetricFromComparison(comp, cfg.key);
459
+ if (m)
460
+ availableMetrics.push({ cfg, metric: m });
461
+ }
462
+ if (availableMetrics.length === 0)
463
+ return '';
464
+ const rowspan = availableMetrics.length;
465
+ const comparisonCell = `<td rowspan="${rowspan}" class="verdict-line comparison-cell"><strong>${e(comp.a)}</strong> <span style="color:var(--text-muted)">vs</span> <strong>${e(comp.b)}</strong></td>`;
466
+ // Build all metric rows first so we can detect consecutive duplicate diagnostics
467
+ // (for visual fade), then emit.
468
+ const preBuilt = availableMetrics.map((row) => {
469
+ const winner = pickWinner(row.metric, row.cfg, comp.a, comp.b);
470
+ const diag = buildDiagnostic(row.metric, row.cfg, winner, lang);
471
+ return { ...row, diagText: `${diag.icon}|${diag.text}` };
472
+ });
473
+ let prevDiagKey = '';
474
+ const mainRows = preBuilt.map((row, idx) => {
475
+ const lead = idx === 0 ? comparisonCell : '';
476
+ const aMean = getVariantMetricMean(variance, comp.a, row.cfg.key);
477
+ const bMean = getVariantMetricMean(variance, comp.b, row.cfg.key);
478
+ const fade = idx > 0 && row.diagText === prevDiagKey;
479
+ prevDiagKey = row.diagText;
480
+ return `<tr>${lead}${buildMetricRowCells(row.metric, row.cfg, comp.a, comp.b, aMean, bMean, fade)}</tr>`;
481
+ }).join('');
482
+ return mainRows + renderLayerBreakdown(comp);
483
+ }).join('');
484
+ const glossaryZh = [
485
+ { label: '差距', desc: '跨轮均值胜出者 + 绝对差值(原始单位)' },
486
+ { label: '效应量', desc: '差距相对标准差的倍数。阈值:0.2=小 / 0.5=中 / 0.8=大' },
487
+ { label: "Hedges' g", desc: '小样本修正版,n1+n2<20 时优先参考', sub: true },
488
+ { label: "Cohen's d", desc: '未修正版,n1+n2≥20 时是学术惯例', sub: true },
489
+ { label: '显著性', desc: 't 检验结论,基于 p<0.05 阈值。回答"差异真不真",和效应量"差多大"互补' },
490
+ { label: 'p 值', desc: '假设真的没差异时,观察到当前差距的概率。越小越可信。0.05 只是约定阈值', sub: true },
491
+ { label: 't 值', desc: '均值差 ÷ 估计误差,需配合 df 和效应量解读,不能单独看', sub: true },
492
+ { label: 'df 自由度', desc: '≈"有效样本量"。--repeat 3 时通常 2~4;想达到 20+ 需 --repeat 10+', sub: true },
493
+ ];
494
+ const glossaryEn = [
495
+ { label: 'Gap', desc: 'Cross-run mean winner + absolute difference (raw units)' },
496
+ { label: 'Effect size', desc: 'Gap measured in standard deviations. Thresholds: 0.2=small / 0.5=medium / 0.8=large' },
497
+ { label: "Hedges' g", desc: 'Small-sample corrected; preferred when n1+n2<20', sub: true },
498
+ { label: "Cohen's d", desc: 'Uncorrected; conventional when n1+n2≥20', sub: true },
499
+ { label: 'Significance', desc: 't-test verdict based on p<0.05 threshold. Complements effect size ("how real" vs "how big")' },
500
+ { label: 'p value', desc: 'Probability of seeing the current gap if variants were truly identical. Smaller = more confident. 0.05 is a convention', sub: true },
501
+ { label: 't value', desc: 'Mean diff ÷ estimated error. Must be read alongside df and effect size', sub: true },
502
+ { label: 'df', desc: '≈"effective sample size". --repeat 3 usually lands at 2~4; df 20+ needs --repeat 10+', sub: true },
503
+ ];
504
+ const glossaryRows = lang === 'zh' ? glossaryZh : glossaryEn;
505
+ const glossaryHtml = glossaryRows.map((r) => {
506
+ if (r.sub) {
507
+ return `<div class="modal-glossary-sub"><div class="modal-glossary-sub-label">${e(r.label)}</div><div class="modal-glossary-sub-desc">${e(r.desc)}</div></div>`;
508
+ }
509
+ return `<div class="modal-glossary-row"><div class="modal-glossary-label">${e(r.label)}</div><div class="modal-glossary-desc">${e(r.desc)}</div></div>`;
510
+ }).join('');
511
+ const diagRulesZhData = [
512
+ { variant: 'good', icon: '✓', title: '显著差异(中/大效应)', desc: '差异真实且有实际意义,可作为结论', example: '示例:v1 更便宜 · 显著 · g=1.04' },
513
+ { variant: 'warn', icon: '⚠', title: '显著但效应微弱', desc: '差异真实但太小没实际价值,别过度解读', example: '示例:p<0.05 但 g≈0.1(--repeat 很大时易出现)' },
514
+ { variant: 'warn', icon: '⚠', title: '大效应但样本不足', desc: '差距看似大但样本太少,建议加大 --repeat 再判断', example: '示例:v2 胜出 0.30 · g=1.04 · 不显著' },
515
+ { variant: 'neutral', icon: '—', title: '两变体相当', desc: '既不显著也效应微弱,可视为无差异', example: '示例:Δ≈0 · 不显著 · g<0.2' },
516
+ ];
517
+ const diagRulesEnData = [
518
+ { variant: 'good', icon: '✓', title: 'Significant, medium/large effect', desc: 'Real and meaningful — acceptable as a conclusion', example: 'e.g. v1 cheaper · significant · g=1.04' },
519
+ { variant: 'warn', icon: '⚠', title: 'Significant but trivial effect', desc: 'Real but tiny — do not overinterpret', example: 'e.g. p<0.05 but g≈0.1 (common with large --repeat)' },
520
+ { variant: 'warn', icon: '⚠', title: 'Large effect but underpowered', desc: 'Gap looks real but sample is too small — increase --repeat', example: 'e.g. v2 leads by 0.30 · g=1.04 · not significant' },
521
+ { variant: 'neutral', icon: '—', title: 'No meaningful difference', desc: 'Neither significant nor large — treat as equivalent', example: 'e.g. Δ≈0 · not significant · g<0.2' },
522
+ ];
523
+ const diagRulesData = lang === 'zh' ? diagRulesZhData : diagRulesEnData;
524
+ const diagRulesHtml = diagRulesData.map((rule) => `
525
+ <div class="diag-rule-row rule-${rule.variant}">
526
+ <span class="diag-rule-icon rule-${rule.variant}">${rule.icon}</span>
527
+ <div class="diag-rule-body">
528
+ <div class="diag-rule-title">${e(rule.title)}</div>
529
+ <div class="diag-rule-desc">${e(rule.desc)}</div>
530
+ <div class="diag-rule-example">${e(rule.example)}</div>
531
+ </div>
532
+ </div>`).join('');
533
+ const orderHint = lang === 'zh' ? '以下按表格列顺序排列' : 'Below follows the table column order';
534
+ const sectionTitle = lang === 'zh' ? '四象限诊断规则' : 'Four-quadrant diagnostic rules';
535
+ const closeLabel = lang === 'zh' ? '关闭' : 'Close';
536
+ return `
537
+ <section style="margin-top:24px">
538
+ <h2 style="display:flex;align-items:center;gap:4px">${title} <button type="button" class="hint-btn" onclick="openModal('${modalId}')" aria-label="${e(guideTitle)}" aria-haspopup="dialog">?</button></h2>
539
+ <div id="${modalId}" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="${modalId}-title" onclick="if(event.target===this)closeModal('${modalId}')">
540
+ <div class="modal-content">
541
+ <div class="modal-header">
542
+ <strong id="${modalId}-title" style="font-size:1rem">${e(guideTitle)}</strong>
543
+ <button type="button" class="modal-close" onclick="closeModal('${modalId}')" aria-label="${closeLabel}">✕</button>
544
+ </div>
545
+ <p class="modal-glossary-hint">${e(orderHint)}</p>
546
+ <div class="modal-glossary">${glossaryHtml}</div>
547
+ <div class="modal-section">
548
+ <div class="modal-section-title">${e(sectionTitle)}</div>
549
+ <div class="diag-rules">${diagRulesHtml}</div>
550
+ </div>
551
+ </div>
552
+ </div>
553
+ <div class="table-wrap">
554
+ <table class="summary-table variance-table">
555
+ <thead>${thead}</thead>
556
+ <tbody>${rows}</tbody>
557
+ </table>
558
+ </div>
559
+ </section>`;
560
+ }
561
+ const CONCLUSION_TYPES = new Set([
562
+ 'efficiency_gap', 'tool_count_gap', 'high_cost_sample',
563
+ ]);
564
+ function isConclusion(insight) {
565
+ return CONCLUSION_TYPES.has(insight.type);
566
+ }
567
+ function severityDot(severity) {
568
+ const color = {
569
+ error: 'var(--red)',
570
+ warning: 'var(--yellow)',
571
+ info: 'var(--accent)',
572
+ };
573
+ return `<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:${color[severity] || 'var(--text-muted)'};margin-right:8px;flex-shrink:0;margin-top:6px"></span>`;
574
+ }
575
+ function renderSummaryStructured(summary) {
576
+ const markerRegex = /【([^】]+)】/g;
577
+ const markers = [];
578
+ let match;
579
+ while ((match = markerRegex.exec(summary)) !== null) {
580
+ markers.push({ label: match[1], start: match.index, contentStart: match.index + match[0].length });
581
+ }
582
+ const sections = [];
583
+ for (let i = 0; i < markers.length; i++) {
584
+ const end = i + 1 < markers.length ? markers[i + 1].start : summary.length;
585
+ sections.push({ label: markers[i].label, content: summary.slice(markers[i].contentStart, end).trim() });
586
+ }
587
+ if (sections.length === 0) {
588
+ return `<div style="padding:14px 18px;font-size:13px;line-height:1.8;color:var(--text-secondary);background:var(--bg-surface);border-radius:var(--radius);border:1px solid var(--border)">${e(summary)}</div>`;
589
+ }
590
+ const sectionHtml = sections.map((section) => `
591
+ <div style="display:flex;gap:12px;align-items:baseline">
592
+ <span style="flex-shrink:0;font-size:11px;font-weight:600;color:var(--text-muted);letter-spacing:0.03em;min-width:56px">${e(section.label)}</span>
593
+ <span style="color:var(--text-secondary);font-size:13px;line-height:1.7">${e(section.content)}</span>
594
+ </div>`).join('');
595
+ return `
596
+ <div style="padding:14px 18px;background:var(--bg-surface);border-radius:var(--radius);border:1px solid var(--border);display:flex;flex-direction:column;gap:8px">
597
+ ${sectionHtml}
598
+ </div>`;
599
+ }
600
+ export function renderAnalysis(analysis, lang) {
601
+ if (!analysis)
602
+ return '';
603
+ const { insights, suggestions } = analysis;
604
+ if ((!insights || insights.length === 0) && (!suggestions || suggestions.length === 0))
605
+ return '';
606
+ const issues = (insights || []).filter((insight) => !isConclusion(insight));
607
+ const issueLabel = lang === 'zh' ? '问题与建议' : 'Issues & Suggestions';
608
+ const safeSuggestions = suggestions || [];
609
+ let issuesHtml = '';
610
+ if (issues.length > 0 || safeSuggestions.length > 0) {
611
+ const maxRows = Math.max(issues.length, safeSuggestions.length);
612
+ const rows = [];
613
+ for (let i = 0; i < maxRows; i++) {
614
+ const issue = issues[i];
615
+ const suggestion = safeSuggestions[i];
616
+ const issueContent = issue
617
+ ? `${severityDot(issue.severity)}<span>${e(issue.message)}</span>`
618
+ : '';
619
+ const suggestionContent = suggestion
620
+ ? e(suggestion)
621
+ : `<span style="color:var(--text-faint)">—</span>`;
622
+ rows.push(`
623
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:10px 16px;border-bottom:1px solid var(--border)">
624
+ <div style="display:flex;align-items:flex-start;color:var(--text-secondary);font-size:12.5px;line-height:1.6">${issueContent}</div>
625
+ <div style="color:var(--text-muted);font-size:12.5px;line-height:1.6">${suggestionContent}</div>
626
+ </div>`);
627
+ }
628
+ issuesHtml = `
629
+ <div style="margin-top:12px">
630
+ <h3 style="font-size:12px;color:var(--text-muted);font-weight:600;margin:0 0 8px;letter-spacing:0.03em">${issueLabel}</h3>
631
+ <div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden">
632
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:8px 16px;border-bottom:1px solid var(--border)">
633
+ <div style="font-size:11px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em">${lang === 'zh' ? '问题' : 'Issue'}</div>
634
+ <div style="font-size:11px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em">${lang === 'zh' ? '建议' : 'Suggestion'}</div>
635
+ </div>
636
+ ${rows.join('')}
637
+ </div>
638
+ </div>
639
+ `;
640
+ }
641
+ const summaryHtml = analysis.summary
642
+ ? renderSummaryStructured(analysis.summary)
643
+ : '';
644
+ return `
645
+ <h2 data-i18n="autoAnalysis">${t('autoAnalysis', lang)}</h2>
646
+ ${summaryHtml}
647
+ ${issuesHtml}
648
+ `;
649
+ }
650
+ export function renderAgentOverview(variants, summary, lang) {
651
+ const hasAgentData = variants.some((variant) => summary[variant]?.avgToolCalls != null && summary[variant].avgToolCalls > 0);
652
+ if (!hasAgentData)
653
+ return '';
654
+ const variantCards = variants.map((variant, i) => {
655
+ const stats = summary[variant];
656
+ if (!stats)
657
+ return '';
658
+ const color = COLORS[i % COLORS.length];
659
+ const avgTools = stats.avgToolCalls ?? 0;
660
+ const successRate = stats.toolSuccessRate != null ? `${(stats.toolSuccessRate * 100).toFixed(0)}%` : '-';
661
+ const successRateColor = (stats.toolSuccessRate ?? 1) >= 0.8 ? 'var(--green)' : 'var(--red)';
662
+ const turns = stats.avgFullNumTurns ?? stats.avgNumTurns ?? 0;
663
+ const distributionEntries = Object.entries(stats.toolDistribution || {}).sort((a, b) => b[1] - a[1]);
664
+ const maxCount = distributionEntries.length > 0 ? distributionEntries[0][1] : 0;
665
+ const distributionBars = distributionEntries.map(([tool, count]) => {
666
+ const pct = maxCount > 0 ? Math.max(8, (count / maxCount) * 100) : 0;
667
+ return `<div style="display:flex;align-items:center;gap:6px;margin:2px 0">
668
+ <span style="font-size:11px;color:var(--text-muted);width:100px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0" title="${e(tool)}">${e(tool)}</span>
669
+ <div style="flex:1;height:8px;background:var(--bg-surface);border-radius:4px">
670
+ <div style="width:${pct}%;height:100%;background:${color};border-radius:4px"></div>
671
+ </div>
672
+ <span style="font-size:11px;color:var(--text-secondary);min-width:20px">${count}</span>
673
+ </div>`;
674
+ }).join('');
675
+ return `<div style="flex:1;min-width:240px;padding:16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);border-left:3px solid ${color}">
676
+ <div style="font-weight:600;margin-bottom:12px">${e(variant)}</div>
677
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
678
+ <div>
679
+ <div style="font-size:11px;color:var(--text-muted)">${t('agentAvgTurns', lang)}</div>
680
+ <div style="font-size:20px;font-weight:600">${turns}</div>
681
+ </div>
682
+ <div>
683
+ <div style="font-size:11px;color:var(--text-muted)">${t('agentAvgTools', lang)}</div>
684
+ <div style="font-size:20px;font-weight:600">${avgTools}</div>
685
+ </div>
686
+ <div>
687
+ <div style="font-size:11px;color:var(--text-muted)">${t('agentToolSuccess', lang)}</div>
688
+ <div style="font-size:20px;font-weight:600;color:${successRateColor}">${successRate}</div>
689
+ </div>
690
+ </div>
691
+ ${distributionEntries.length > 0 ? `
692
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:4px">${t('agentToolDist', lang)}</div>
693
+ ${distributionBars}
694
+ ` : ''}
695
+ </div>`;
696
+ }).join('');
697
+ return `
698
+ <section style="margin-top:24px">
699
+ <h2>${t('agentOverview', lang)}</h2>
700
+ <div style="display:flex;gap:16px;flex-wrap:wrap">
701
+ ${variantCards}
702
+ </div>
703
+ </section>
704
+ `;
705
+ }
706
+ export function renderCoverageSection(coverage, lang) {
707
+ if (!coverage || Object.keys(coverage).length === 0)
708
+ return '';
709
+ const variantSections = Object.entries(coverage).map(([variant, knowledgeCoverage]) => {
710
+ if (knowledgeCoverage.filesTotal === 0)
711
+ return '';
712
+ const pct = Math.round(knowledgeCoverage.fileCoverageRate * 100);
713
+ const pctColor = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--yellow)' : 'var(--red)';
714
+ const barW = Math.max(2, pct);
715
+ const fileRows = knowledgeCoverage.entries
716
+ .sort((a, b) => (a.accessed === b.accessed ? 0 : a.accessed ? -1 : 1))
717
+ .map((entry) => {
718
+ const icon = entry.accessed ? '✓' : '✗';
719
+ const color = entry.accessed ? 'var(--green)' : 'var(--text-muted)';
720
+ const countBadge = entry.accessCount > 1
721
+ ? `<span style="font-size:10px;color:var(--accent);margin-left:4px">×${entry.accessCount}</span>`
722
+ : '';
723
+ const lines = entry.lineCount ? `<span style="font-size:10px;color:var(--text-muted);margin-left:4px">${entry.lineCount}L</span>` : '';
724
+ const typeTag = `<span style="font-size:10px;padding:1px 4px;border-radius:2px;background:var(--bg-surface);color:var(--text-muted);margin-left:4px">${e(entry.type)}</span>`;
725
+ return `<div style="display:flex;align-items:center;gap:4px;padding:2px 0;font-size:12px">
726
+ <span style="color:${color};width:16px;text-align:center">${icon}</span>
727
+ <span style="color:${entry.accessed ? 'var(--text-primary)' : 'var(--text-muted)'};${entry.accessed ? '' : 'text-decoration:line-through;opacity:0.6'}">${e(entry.path)}</span>
728
+ ${typeTag}${lines}${countBadge}
729
+ </div>`;
730
+ }).join('');
731
+ const uncoveredByType = {};
732
+ for (const entry of knowledgeCoverage.entries.filter((item) => !item.accessed)) {
733
+ const category = entry.path.startsWith('repos/') ? 'code' : entry.type;
734
+ (uncoveredByType[category] = uncoveredByType[category] || []).push(entry.path);
735
+ }
736
+ const hintLines = [];
737
+ const typeLabels = lang === 'zh'
738
+ ? { principle: '原则文件', semantic: '语义索引', design: '设计文档', code: '代码路径', script: '脚本工具', other: '其他知识' }
739
+ : { principle: 'Principles', semantic: 'Semantic index', design: 'Design docs', code: 'Code paths', script: 'Scripts', other: 'Other' };
740
+ for (const [type, files] of Object.entries(uncoveredByType)) {
741
+ const label = typeLabels[type] || type;
742
+ hintLines.push(`<strong>${label}</strong>(${files.length}):${files.slice(0, 3).map((file) => `<code>${e(file)}</code>`).join(', ')}${files.length > 3 ? ` +${files.length - 3}` : ''}`);
743
+ }
744
+ const uncoveredHint = hintLines.length > 0
745
+ ? `<div style="font-size:11px;color:var(--text-muted);margin-top:10px;border-top:1px solid var(--border);padding-top:8px">
746
+ <div style="margin-bottom:4px">${lang === 'zh' ? '💡 以下知识未被任何样本访问,建议补充测试用例覆盖:' : '💡 These knowledge files were not accessed by any sample — consider adding test cases:'}</div>
747
+ ${hintLines.map((line) => `<div style="margin:2px 0">${line}</div>`).join('')}
748
+ </div>`
749
+ : '';
750
+ return `<div style="flex:1;min-width:280px;padding:16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius)">
751
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
752
+ <strong>${e(variant)}</strong>
753
+ <span style="font-size:20px;font-weight:600;color:${pctColor}">${pct}%</span>
754
+ </div>
755
+ <div style="height:8px;background:var(--bg-card);border-radius:4px;margin-bottom:8px">
756
+ <div style="width:${barW}%;height:100%;background:${pctColor};border-radius:4px"></div>
757
+ </div>
758
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:8px">${knowledgeCoverage.filesCovered}/${knowledgeCoverage.filesTotal} ${lang === 'zh' ? '个文件被访问' : 'files accessed'} · ${knowledgeCoverage.grepPatternsUsed} ${lang === 'zh' ? '次搜索' : 'searches'}</div>
759
+ ${fileRows}
760
+ ${uncoveredHint}
761
+ </div>`;
762
+ }).join('');
763
+ const title = lang === 'zh' ? '本次测评的知识使用情况' : 'Knowledge usage in this evaluation';
764
+ const desc = lang === 'zh'
765
+ ? '本次测评中,哪些知识没有被使用。数字低说明测试用例没覆盖到的角落多,不是知识库内容缺失——配合下方"本次测评的知识盲区"一起看才完整。'
766
+ : 'Which knowledge files were NOT exercised by this evaluation. Low coverage means test cases leave KB corners untouched, not that the KB is incomplete. Pair with "knowledge gaps" below for the full picture.';
767
+ return `
768
+ <section style="margin-top:24px">
769
+ <h2>${title}</h2>
770
+ <p style="font-size:12px;color:var(--text-muted);margin-bottom:12px">${desc}</p>
771
+ <div style="display:flex;gap:16px;flex-wrap:wrap">
772
+ ${variantSections}
773
+ </div>
774
+ </section>
775
+ `;
776
+ }
777
+ /**
778
+ * Render the knowledge gap section: per-variant gap rate + mandatory test set
779
+ * watermark + signal classification + inventory of individual signals.
780
+ * See docs/knowledge-gap-signal-spec.md for the semantics.
781
+ */
782
+ /**
783
+ * Combined knowledge-interaction section: coverage + gap side-by-side per variant.
784
+ *
785
+ * v0.17 起替代原独立的 renderCoverageSection + renderGapSection。两者都是"测评集
786
+ * × 知识库交互"的产物(spec §二),分开展示会让读者只看一个指标得出误判结论。合并后
787
+ * 每个 variant 一张 card,左右两栏并排展示"用了多少"vs"撞了多少",形成完整诊断画像。
788
+ */
789
+ export function renderKnowledgeInteractionSection(coverage, gapReports, lang) {
790
+ const hasCov = coverage && Object.keys(coverage).length > 0;
791
+ const hasGap = gapReports && Object.keys(gapReports).length > 0;
792
+ if (!hasCov && !hasGap)
793
+ return '';
794
+ const title = lang === 'zh' ? '本次测评:测试用例 × 知识库' : 'This Evaluation: Test Set × Knowledge Base';
795
+ const desc = lang === 'zh'
796
+ ? '展示本次测评用例和知识库的交互画像——用到哪些知识(使用情况)· 哪些知识想找但没找到或模型表达不确定(盲区)。'
797
+ : 'How this test set interacts with the KB — which knowledge was exercised (usage) · which was missed or flagged as uncertain (gaps).';
798
+ const readHint = lang === 'zh'
799
+ ? '💡 读表:两者同时高 → 知识库内容有问题(有文件但答不出)· 同时低 → 测试用例太浅(没触到复杂场景)· 使用高 + 盲区低 → 理想但警惕样本驯化'
800
+ : '💡 Read together: both high → KB content issues (files exist but can\'t answer) · both low → test set too shallow · high use + low gap → ideal, but beware sample-set overfitting';
801
+ // 聚合所有 variant(coverage / gap 任一侧存在即纳入)
802
+ const allVariants = Array.from(new Set([
803
+ ...(hasCov ? Object.keys(coverage) : []),
804
+ ...(hasGap ? Object.keys(gapReports) : []),
805
+ ]));
806
+ const signalTypeLabels = {
807
+ failed_search: { zh: '搜索未命中', en: 'Search miss' },
808
+ explicit_marker: { zh: '模型标记缺口', en: 'Model-flagged gap' },
809
+ hedging: { zh: '表达不确定', en: 'Hedging' },
810
+ repeated_failure: { zh: '反复未命中', en: 'Repeated miss' },
811
+ };
812
+ const pickSignalLabel = (key) => signalTypeLabels[key][lang === 'zh' ? 'zh' : 'en'];
813
+ // severity 按 SIGNAL_WEIGHTS 映射:strong (weight 1.0) → 红色 · weak (0.5) → 灰/黄
814
+ const signalSeverity = {
815
+ failed_search: 'strong',
816
+ repeated_failure: 'strong',
817
+ explicit_marker: 'medium',
818
+ hedging: 'weak',
819
+ };
820
+ const cards = allVariants.map((variant, i) => {
821
+ const cov = coverage?.[variant];
822
+ const gap = gapReports?.[variant];
823
+ const variantColor = COLORS[i % COLORS.length];
824
+ // ─── 左栏:知识使用(coverage)────────────────────────
825
+ let covInner = '';
826
+ if (cov && cov.filesTotal > 0) {
827
+ const pct = Math.round(cov.fileCoverageRate * 100);
828
+ const pctColor = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--yellow)' : 'var(--red)';
829
+ const barW = Math.max(2, pct);
830
+ const barLabel = lang === 'zh' ? '知识使用' : 'Knowledge used';
831
+ const fileRows = cov.entries
832
+ .slice()
833
+ .sort((a, b) => (a.accessed === b.accessed ? 0 : a.accessed ? -1 : 1))
834
+ .map((entry) => {
835
+ const icon = entry.accessed ? '✓' : '✗';
836
+ const color = entry.accessed ? 'var(--green)' : 'var(--text-muted)';
837
+ const countBadge = entry.accessCount > 1
838
+ ? `<span style="font-size:10px;color:var(--accent);margin-left:4px">×${entry.accessCount}</span>`
839
+ : '';
840
+ const lines = entry.lineCount ? `<span style="font-size:10px;color:var(--text-muted);margin-left:4px">${entry.lineCount}L</span>` : '';
841
+ const typeTag = `<span style="font-size:10px;padding:1px 4px;border-radius:2px;background:var(--bg-card);color:var(--text-muted);margin-left:4px">${e(entry.type)}</span>`;
842
+ return `<div style="display:flex;align-items:center;gap:4px;padding:2px 0;font-size:12px">
843
+ <span style="color:${color};width:16px;text-align:center">${icon}</span>
844
+ <span style="color:${entry.accessed ? 'var(--text-primary)' : 'var(--text-muted)'};${entry.accessed ? '' : 'text-decoration:line-through;opacity:0.6'};word-break:break-all">${e(entry.path)}</span>
845
+ ${typeTag}${lines}${countBadge}
846
+ </div>`;
847
+ }).join('');
848
+ const uncoveredByType = {};
849
+ for (const entry of cov.entries.filter((item) => !item.accessed)) {
850
+ const category = entry.path.startsWith('repos/') ? 'code' : entry.type;
851
+ (uncoveredByType[category] = uncoveredByType[category] || []).push(entry.path);
852
+ }
853
+ const typeLabels = lang === 'zh'
854
+ ? { principle: '原则文件', semantic: '语义索引', design: '设计文档', code: '代码路径', script: '脚本工具', other: '其他知识' }
855
+ : { principle: 'Principles', semantic: 'Semantic index', design: 'Design docs', code: 'Code paths', script: 'Scripts', other: 'Other' };
856
+ const hintLines = [];
857
+ for (const [type, files] of Object.entries(uncoveredByType)) {
858
+ const label = typeLabels[type] || type;
859
+ hintLines.push(`<strong>${label}</strong>(${files.length}):${files.slice(0, 3).map((file) => `<code>${e(file)}</code>`).join(', ')}${files.length > 3 ? ` +${files.length - 3}` : ''}`);
860
+ }
861
+ const uncoveredHint = hintLines.length > 0
862
+ ? `<div style="font-size:11px;color:var(--text-muted);margin-top:10px;border-top:1px solid var(--border);padding-top:8px">
863
+ <div style="margin-bottom:4px">${lang === 'zh' ? '💡 以下知识未被任何样本访问,建议补充测试用例:' : '💡 These knowledge files were not accessed — consider adding test cases:'}</div>
864
+ ${hintLines.map((line) => `<div style="margin:2px 0">${line}</div>`).join('')}
865
+ </div>`
866
+ : '';
867
+ const uncoveredCount = cov.filesTotal - cov.filesCovered;
868
+ const summaryParts = [
869
+ `${cov.filesCovered} ${lang === 'zh' ? '命中' : 'hit'}`,
870
+ `${uncoveredCount} ${lang === 'zh' ? '未命中' : 'miss'}`,
871
+ `${cov.grepPatternsUsed} ${lang === 'zh' ? '次搜索' : 'searches'}`,
872
+ ].join(' · ');
873
+ const detailsLabel = lang === 'zh' ? `展开 ${cov.filesTotal} 个文件清单` : `Show all ${cov.filesTotal} files`;
874
+ covInner = `
875
+ <div class="ki-col-header">
876
+ <span class="ki-col-title">${lang === 'zh' ? '知识使用' : 'Knowledge used'}</span>
877
+ <span class="ki-col-value" style="color:${pctColor}">${pct}%</span>
878
+ </div>
879
+ <div class="ki-bar" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="${e(barLabel)}">
880
+ <div class="ki-bar-fill" style="width:${barW}%;background:${pctColor}"></div>
881
+ </div>
882
+ <div style="font-size:11px;color:var(--text-muted)">${summaryParts}</div>
883
+ <details class="ki-details"><summary>${detailsLabel}</summary>
884
+ ${fileRows}
885
+ ${uncoveredHint}
886
+ </details>`;
887
+ }
888
+ else {
889
+ covInner = `<div style="color:var(--text-muted);font-size:12px">${lang === 'zh' ? '知识使用数据不可用' : 'Coverage data unavailable'}</div>`;
890
+ }
891
+ // ─── 右栏:知识盲区(gap)────────────────────────────
892
+ let gapInner = '';
893
+ if (gap) {
894
+ const pct = Math.round(gap.gapRate * 100);
895
+ const pctColor = pct <= 10 ? 'var(--green)' : pct <= 30 ? 'var(--yellow)' : 'var(--red)';
896
+ const barW = Math.max(2, pct);
897
+ const barLabel = lang === 'zh' ? '知识盲区' : 'Knowledge gaps';
898
+ const weightedPct = Math.round(gap.weightedGapRate * 100);
899
+ const softShare = pct - weightedPct;
900
+ const weightedHint = lang === 'zh'
901
+ ? (softShare >= 10
902
+ ? `<strong>实际盲区 ${weightedPct}%</strong> · 另 ${softShare}% 为模型表达不确定(软信号,建议对照清单复核)`
903
+ : `<strong>实际盲区 ${weightedPct}%</strong> · 主要来自确定的搜索未命中`)
904
+ : (softShare >= 10
905
+ ? `<strong>real gaps ${weightedPct}%</strong> · another ${softShare}% is hedging (review list below)`
906
+ : `<strong>real gaps ${weightedPct}%</strong> · mostly confirmed search misses`);
907
+ const typeBadges = Object.keys(gap.byType)
908
+ .filter((key) => gap.byType[key] > 0)
909
+ .map((key) => `<span style="display:inline-block;padding:2px 8px;border-radius:var(--radius);background:var(--bg-card);font-size:var(--fs-micro);color:var(--text-secondary);margin:2px 4px 2px 0">${e(pickSignalLabel(key))} × ${gap.byType[key]}</span>`)
910
+ .join('');
911
+ const INVENTORY_CAP = 6;
912
+ // inventory 行用 border-left-color 按 signal severity 上色,不再重复标"类型"文字
913
+ const inventory = gap.signals.slice(0, INVENTORY_CAP).map((sig) => {
914
+ const severity = signalSeverity[sig.type];
915
+ const turnPart = sig.turn != null ? ` · ${lang === 'zh' ? '第' : 'turn'} ${sig.turn}${lang === 'zh' ? ' 轮' : ''}` : '';
916
+ return `<div class="ki-inventory-item" data-severity="${severity}">
917
+ <div class="ki-inventory-item-meta"><strong style="color:var(--text-secondary)">${e(sig.sampleId)}</strong>${turnPart}</div>
918
+ <div class="ki-inventory-item-ctx">${e(sig.context)}</div>
919
+ </div>`;
920
+ }).join('');
921
+ const overflowHint = gap.signals.length > INVENTORY_CAP
922
+ ? `<div style="font-size:var(--fs-micro);color:var(--text-muted);margin-top:6px">${lang === 'zh' ? `另 ${gap.signals.length - INVENTORY_CAP} 条未展示` : `+${gap.signals.length - INVENTORY_CAP} more not shown`}</div>`
923
+ : '';
924
+ const detailsLabel = lang === 'zh'
925
+ ? `展开 ${gap.signals.length} 条证据(按严重度上色: 红=确定 / 黄=模型自述 / 灰=犹豫)`
926
+ : `Show all ${gap.signals.length} evidence items (red=confirmed · yellow=self-flagged · gray=hedging)`;
927
+ gapInner = `
928
+ <div class="ki-col-header">
929
+ <span class="ki-col-title">${lang === 'zh' ? '知识盲区' : 'Knowledge gaps'}</span>
930
+ <span class="ki-col-value" style="color:${pctColor}">${pct}%</span>
931
+ </div>
932
+ <div class="ki-bar" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="${e(barLabel)}">
933
+ <div class="ki-bar-fill" style="width:${barW}%;background:${pctColor}"></div>
934
+ </div>
935
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:4px">${gap.samplesWithGap}/${gap.sampleCount} ${lang === 'zh' ? '个样本出现搜索未命中或表达不确定' : 'samples with search miss / hedging'}</div>
936
+ <div style="font-size:var(--fs-detail);color:var(--text-secondary);margin-bottom:8px">${weightedHint}</div>
937
+ ${typeBadges ? `<div>${typeBadges}</div>` : ''}
938
+ ${inventory ? `<details class="ki-details"><summary>${detailsLabel}</summary>
939
+ ${inventory}
940
+ ${overflowHint}
941
+ </details>` : ''}`;
942
+ }
943
+ else {
944
+ gapInner = `<div style="color:var(--text-muted);font-size:12px">${lang === 'zh' ? '知识盲区数据不可用' : 'Gap data unavailable'}</div>`;
945
+ }
946
+ // ─── Watermark(spec §7.1):test set 标识 ───
947
+ const watermarkBits = [];
948
+ if (gap?.testSetPath) {
949
+ const basename = gap.testSetPath.split('/').pop() || gap.testSetPath;
950
+ watermarkBits.push(`<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${e(basename)}</span>`);
951
+ }
952
+ if (gap)
953
+ watermarkBits.push(`n=${gap.sampleCount}`);
954
+ if (gap?.testSetHash)
955
+ watermarkBits.push(`sha:${e(gap.testSetHash)}`);
956
+ const watermark = watermarkBits.length > 0
957
+ ? `<div class="ki-card-meta">${watermarkBits.join(' · ')}</div>`
958
+ : '';
959
+ return `<div class="ki-card" style="border-left:3px solid ${variantColor}">
960
+ <div class="ki-card-header">
961
+ <span class="ki-card-title">${e(variant)}</span>
962
+ ${watermark}
963
+ </div>
964
+ <div class="ki-columns">
965
+ <div class="ki-col">${covInner}</div>
966
+ <div class="ki-col">${gapInner}</div>
967
+ </div>
968
+ </div>`;
969
+ }).join('');
970
+ return `
971
+ <section style="margin-top:28px;padding-top:20px;border-top:1px solid var(--border)">
972
+ <h2>${title}</h2>
973
+ <p class="ki-desc">${desc}</p>
974
+ <div class="ki-desc-hint">${readHint}</div>
975
+ ${cards}
976
+ </section>
977
+ `;
978
+ }
979
+ export function renderGapSection(gapReports, lang) {
980
+ if (!gapReports || Object.keys(gapReports).length === 0)
981
+ return '';
982
+ const title = lang === 'zh' ? '本次测评的知识盲区' : 'Knowledge gaps in this evaluation';
983
+ const desc = lang === 'zh'
984
+ ? '本次测评中,哪些知识想找但没找到、或模型表达不确定。数字高不一定代表知识库不全——也可能是测试用例问的领域知识库未覆盖。'
985
+ : 'Which knowledge the model tried to find but missed, or expressed uncertainty about. High numbers do not necessarily mean the KB is incomplete — the test set may be asking about areas the KB never covered.';
986
+ const signalTypeLabels = {
987
+ failed_search: { zh: '搜索未命中', en: 'Search miss' },
988
+ explicit_marker: { zh: '模型标记缺口', en: 'Model-flagged gap' },
989
+ hedging: { zh: '表达不确定', en: 'Hedging' },
990
+ repeated_failure: { zh: '反复未命中', en: 'Repeated miss' },
991
+ };
992
+ const pickLabel = (key) => signalTypeLabels[key][lang === 'zh' ? 'zh' : 'en'];
993
+ const variantSections = Object.entries(gapReports).map(([variant, report]) => {
994
+ const pct = Math.round(report.gapRate * 100);
995
+ // Color: lower gap rate is better (inverse of coverage). Gate at 10% / 30%.
996
+ const pctColor = pct <= 10 ? 'var(--green)' : pct <= 30 ? 'var(--yellow)' : 'var(--red)';
997
+ const barW = Math.max(2, pct);
998
+ // v0.2 加权严重度 (spec §6):weightedGapRate 按样本最强信号权重聚合,总是 ≤ gapRate,
999
+ // 差值反映"软信号(hedging / explicit_marker)占比"——若差值大,说明 gap_rate 被软信号
1000
+ // 拉高,读者该复核弱信号的真实含义。若差值小,说明信号以硬证据为主,结论可信度高。
1001
+ const weightedPct = Math.round(report.weightedGapRate * 100);
1002
+ const softSignalShare = pct - weightedPct;
1003
+ const weightedHint = lang === 'zh'
1004
+ ? (softSignalShare >= 10
1005
+ ? `<strong>实际盲区 ${weightedPct}%</strong> · 另外 ${softSignalShare}% 为模型表达不确定(软信号,建议对照右侧清单复核)`
1006
+ : `<strong>实际盲区 ${weightedPct}%</strong> · 主要来自确定的搜索未命中`)
1007
+ : (softSignalShare >= 10
1008
+ ? `<strong>real gaps ${weightedPct}%</strong> · another ${softSignalShare}% is hedging (soft signals — review the list on the right)`
1009
+ : `<strong>real gaps ${weightedPct}%</strong> · mostly from confirmed search misses`);
1010
+ // Watermark (spec §7.1): test set path + sample count + hash + explicit caveat
1011
+ const watermarkBits = [];
1012
+ if (report.testSetPath) {
1013
+ const basename = report.testSetPath.split('/').pop() || report.testSetPath;
1014
+ watermarkBits.push(`<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${e(basename)}</span>`);
1015
+ }
1016
+ watermarkBits.push(`n=${report.sampleCount}`);
1017
+ if (report.testSetHash)
1018
+ watermarkBits.push(`sha:${e(report.testSetHash)}`);
1019
+ const watermark = `<div style="font-size:var(--fs-micro);color:var(--text-muted);margin-bottom:8px;font-weight:400">${watermarkBits.join(' · ')}</div>`;
1020
+ // Classification: per-type counts
1021
+ const typeBadges = Object.keys(report.byType)
1022
+ .filter((key) => report.byType[key] > 0)
1023
+ .map((key) => `<span style="display:inline-block;padding:2px 8px;border-radius:var(--radius);background:var(--bg-card);font-size:var(--fs-micro);color:var(--text-secondary);margin:2px 4px 2px 0">${e(pickLabel(key))} × ${report.byType[key]}</span>`)
1024
+ .join('');
1025
+ // Inventory: list of specific signals (cap at 8 to keep the panel compact)
1026
+ const INVENTORY_CAP = 8;
1027
+ const inventory = report.signals.slice(0, INVENTORY_CAP).map((sig) => {
1028
+ const typeLabel = pickLabel(sig.type);
1029
+ const turnPart = sig.turn != null ? ` / ${lang === 'zh' ? '第' : 'turn'} ${sig.turn}${lang === 'zh' ? ' 轮' : ''}` : '';
1030
+ return `<div style="padding:6px 10px;margin:4px 0;background:var(--bg-card);border-left:2px solid var(--border-hover);border-radius:4px;font-size:var(--fs-detail);line-height:1.5">
1031
+ <div style="color:var(--text-muted);font-size:var(--fs-micro);margin-bottom:2px">
1032
+ <strong style="color:var(--text-secondary)">${e(sig.sampleId)}</strong>${turnPart} · ${e(typeLabel)}
1033
+ </div>
1034
+ <div style="color:var(--text-secondary);word-break:break-all">${e(sig.context)}</div>
1035
+ </div>`;
1036
+ }).join('');
1037
+ const overflowHint = report.signals.length > INVENTORY_CAP
1038
+ ? `<div style="font-size:var(--fs-micro);color:var(--text-muted);margin-top:6px">${lang === 'zh' ? `还有 ${report.signals.length - INVENTORY_CAP} 条未展示` : `+${report.signals.length - INVENTORY_CAP} more not shown`}</div>`
1039
+ : '';
1040
+ // caveat 已从底部移除:副标题已经讲清楚"撞墙多不等于知识库不全"。重复说一遍反而稀释主信号。
1041
+ return `<div style="flex:1;min-width:320px;padding:16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius)">
1042
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
1043
+ <strong>${e(variant)}</strong>
1044
+ <span style="font-size:20px;font-weight:600;color:${pctColor}">${pct}%</span>
1045
+ </div>
1046
+ ${watermark}
1047
+ <div style="height:8px;background:var(--bg-card);border-radius:4px;margin-bottom:10px">
1048
+ <div style="width:${barW}%;height:100%;background:${pctColor};border-radius:4px"></div>
1049
+ </div>
1050
+ <div style="font-size:var(--fs-detail);color:var(--text-muted);margin-bottom:4px">
1051
+ ${report.samplesWithGap} / ${report.sampleCount} ${lang === 'zh' ? '个样本出现搜索未命中或表达不确定' : 'samples with search miss or hedging'}
1052
+ </div>
1053
+ <div style="font-size:var(--fs-detail);color:var(--text-secondary);margin-bottom:10px">${weightedHint}</div>
1054
+ ${typeBadges ? `<div style="margin-bottom:10px">${typeBadges}</div>` : ''}
1055
+ ${inventory ? `<div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border)">
1056
+ <div style="font-size:var(--fs-micro);color:var(--text-muted);margin-bottom:4px">${lang === 'zh' ? '具体哪些知识未命中' : 'Missed knowledge inventory'}</div>
1057
+ ${inventory}
1058
+ ${overflowHint}
1059
+ </div>` : ''}
1060
+ </div>`;
1061
+ }).join('');
1062
+ return `
1063
+ <section style="margin-top:24px">
1064
+ <h2>${title}</h2>
1065
+ <p style="font-size:var(--fs-detail);color:var(--text-muted);margin-bottom:12px">${desc}</p>
1066
+ <div style="display:flex;gap:16px;flex-wrap:wrap">
1067
+ ${variantSections}
1068
+ </div>
1069
+ </section>
1070
+ `;
1071
+ }
1072
+ //# sourceMappingURL=summary.js.map