oh-my-knowledge 0.18.0 → 0.20.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 (113) hide show
  1. package/README.md +596 -326
  2. package/README.zh.md +917 -0
  3. package/dist/src/analysis/failure-clusterer.d.ts +96 -0
  4. package/dist/src/analysis/failure-clusterer.d.ts.map +1 -0
  5. package/dist/src/analysis/failure-clusterer.js +298 -0
  6. package/dist/src/analysis/failure-clusterer.js.map +1 -0
  7. package/dist/src/analysis/sample-diagnostics.d.ts +78 -0
  8. package/dist/src/analysis/sample-diagnostics.d.ts.map +1 -0
  9. package/dist/src/analysis/sample-diagnostics.js +259 -0
  10. package/dist/src/analysis/sample-diagnostics.js.map +1 -0
  11. package/dist/src/analysis/saturation.d.ts +85 -0
  12. package/dist/src/analysis/saturation.d.ts.map +1 -0
  13. package/dist/src/analysis/saturation.js +174 -0
  14. package/dist/src/analysis/saturation.js.map +1 -0
  15. package/dist/src/cli.js +772 -18
  16. package/dist/src/cli.js.map +1 -1
  17. package/dist/src/eval-core/bootstrap.d.ts +72 -0
  18. package/dist/src/eval-core/bootstrap.d.ts.map +1 -0
  19. package/dist/src/eval-core/bootstrap.js +174 -0
  20. package/dist/src/eval-core/bootstrap.js.map +1 -0
  21. package/dist/src/eval-core/evaluation-execution.d.ts +15 -1
  22. package/dist/src/eval-core/evaluation-execution.d.ts.map +1 -1
  23. package/dist/src/eval-core/evaluation-execution.js +37 -3
  24. package/dist/src/eval-core/evaluation-execution.js.map +1 -1
  25. package/dist/src/eval-core/evaluation-job.d.ts +10 -2
  26. package/dist/src/eval-core/evaluation-job.d.ts.map +1 -1
  27. package/dist/src/eval-core/evaluation-job.js +9 -1
  28. package/dist/src/eval-core/evaluation-job.js.map +1 -1
  29. package/dist/src/eval-core/evaluation-reporting.d.ts.map +1 -1
  30. package/dist/src/eval-core/evaluation-reporting.js +90 -0
  31. package/dist/src/eval-core/evaluation-reporting.js.map +1 -1
  32. package/dist/src/eval-core/schema.d.ts.map +1 -1
  33. package/dist/src/eval-core/schema.js +69 -0
  34. package/dist/src/eval-core/schema.js.map +1 -1
  35. package/dist/src/eval-core/verdict.d.ts +74 -0
  36. package/dist/src/eval-core/verdict.d.ts.map +1 -0
  37. package/dist/src/eval-core/verdict.js +283 -0
  38. package/dist/src/eval-core/verdict.js.map +1 -0
  39. package/dist/src/eval-workflows/each-evaluation-workflow.d.ts +16 -3
  40. package/dist/src/eval-workflows/each-evaluation-workflow.d.ts.map +1 -1
  41. package/dist/src/eval-workflows/each-evaluation-workflow.js +10 -2
  42. package/dist/src/eval-workflows/each-evaluation-workflow.js.map +1 -1
  43. package/dist/src/eval-workflows/evaluation-pipeline.d.ts +17 -1
  44. package/dist/src/eval-workflows/evaluation-pipeline.d.ts.map +1 -1
  45. package/dist/src/eval-workflows/evaluation-pipeline.js +45 -3
  46. package/dist/src/eval-workflows/evaluation-pipeline.js.map +1 -1
  47. package/dist/src/eval-workflows/run-evaluation.d.ts +23 -2
  48. package/dist/src/eval-workflows/run-evaluation.d.ts.map +1 -1
  49. package/dist/src/eval-workflows/run-evaluation.js +104 -4
  50. package/dist/src/eval-workflows/run-evaluation.js.map +1 -1
  51. package/dist/src/grading/assertions.d.ts +16 -0
  52. package/dist/src/grading/assertions.d.ts.map +1 -1
  53. package/dist/src/grading/assertions.js +385 -111
  54. package/dist/src/grading/assertions.js.map +1 -1
  55. package/dist/src/grading/debias-validate.d.ts +84 -0
  56. package/dist/src/grading/debias-validate.d.ts.map +1 -0
  57. package/dist/src/grading/debias-validate.js +173 -0
  58. package/dist/src/grading/debias-validate.js.map +1 -0
  59. package/dist/src/grading/gold-cli.d.ts +88 -0
  60. package/dist/src/grading/gold-cli.d.ts.map +1 -0
  61. package/dist/src/grading/gold-cli.js +251 -0
  62. package/dist/src/grading/gold-cli.js.map +1 -0
  63. package/dist/src/grading/gold-dataset.d.ts +73 -0
  64. package/dist/src/grading/gold-dataset.d.ts.map +1 -0
  65. package/dist/src/grading/gold-dataset.js +161 -0
  66. package/dist/src/grading/gold-dataset.js.map +1 -0
  67. package/dist/src/grading/human-gold.d.ts +102 -0
  68. package/dist/src/grading/human-gold.d.ts.map +1 -0
  69. package/dist/src/grading/human-gold.js +188 -0
  70. package/dist/src/grading/human-gold.js.map +1 -0
  71. package/dist/src/grading/index.d.ts +27 -2
  72. package/dist/src/grading/index.d.ts.map +1 -1
  73. package/dist/src/grading/index.js +36 -18
  74. package/dist/src/grading/index.js.map +1 -1
  75. package/dist/src/grading/judge.d.ts +65 -2
  76. package/dist/src/grading/judge.d.ts.map +1 -1
  77. package/dist/src/grading/judge.js +280 -23
  78. package/dist/src/grading/judge.js.map +1 -1
  79. package/dist/src/inputs/eval-config.js +19 -0
  80. package/dist/src/inputs/eval-config.js.map +1 -1
  81. package/dist/src/observability/{production-analyzer.d.ts → skill-health-analyzer.d.ts} +24 -2
  82. package/dist/src/observability/skill-health-analyzer.d.ts.map +1 -0
  83. package/dist/src/observability/{production-analyzer.js → skill-health-analyzer.js} +61 -6
  84. package/dist/src/observability/skill-health-analyzer.js.map +1 -0
  85. package/dist/src/observability/trace-adapter.d.ts.map +1 -1
  86. package/dist/src/observability/trace-adapter.js +27 -1
  87. package/dist/src/observability/trace-adapter.js.map +1 -1
  88. package/dist/src/renderer/html-renderer.d.ts.map +1 -1
  89. package/dist/src/renderer/html-renderer.js +40 -6
  90. package/dist/src/renderer/html-renderer.js.map +1 -1
  91. package/dist/src/renderer/layout.d.ts.map +1 -1
  92. package/dist/src/renderer/layout.js +138 -4
  93. package/dist/src/renderer/layout.js.map +1 -1
  94. package/dist/src/renderer/skill-health-renderer.d.ts +2 -2
  95. package/dist/src/renderer/skill-health-renderer.d.ts.map +1 -1
  96. package/dist/src/renderer/skill-health-renderer.js +39 -4
  97. package/dist/src/renderer/skill-health-renderer.js.map +1 -1
  98. package/dist/src/renderer/summary.d.ts +28 -1
  99. package/dist/src/renderer/summary.d.ts.map +1 -1
  100. package/dist/src/renderer/summary.js +322 -8
  101. package/dist/src/renderer/summary.js.map +1 -1
  102. package/dist/src/renderer/table.d.ts.map +1 -1
  103. package/dist/src/renderer/table.js +63 -2
  104. package/dist/src/renderer/table.js.map +1 -1
  105. package/dist/src/server/report-server.d.ts +2 -1
  106. package/dist/src/server/report-server.d.ts.map +1 -1
  107. package/dist/src/server/report-server.js +397 -2
  108. package/dist/src/server/report-server.js.map +1 -1
  109. package/dist/src/types.d.ts +247 -0
  110. package/dist/src/types.d.ts.map +1 -1
  111. package/package.json +24 -6
  112. package/dist/src/observability/production-analyzer.d.ts.map +0 -1
  113. package/dist/src/observability/production-analyzer.js.map +0 -1
@@ -1,5 +1,274 @@
1
1
  import { e, fmtNum, fmtCost, fmtDuration, COLORS, t } from './layout.js';
2
2
  import { pValueCategory } from '../eval-core/statistics.js';
3
+ import { computeVerdict } from '../eval-core/verdict.js';
4
+ /**
5
+ * Verdict pill — sticky banner at the top of the HTML report giving the same
6
+ * one-line conclusion as the `omk bench verdict` CLI. Both surfaces share the
7
+ * computeVerdict rule engine so they can never disagree.
8
+ *
9
+ * Color coding follows traffic-light convention plus a yellow band for
10
+ * UNDERPOWERED / NOISE / CAUTIOUS — none of which are "ship" but none of which
11
+ * are "regress" either. SOLO is grey because it's an info pill, not a verdict.
12
+ */
13
+ function levelColor(level) {
14
+ switch (level) {
15
+ case 'PROGRESS': return { bg: 'rgba(46, 204, 113, 0.15)', fg: 'var(--green)', border: 'var(--green)' };
16
+ case 'REGRESS': return { bg: 'rgba(231, 76, 60, 0.15)', fg: 'var(--red)', border: 'var(--red)' };
17
+ case 'CAUTIOUS': return { bg: 'rgba(255, 200, 80, 0.15)', fg: 'var(--yellow)', border: 'var(--yellow)' };
18
+ case 'UNDERPOWERED':
19
+ case 'NOISE': return { bg: 'rgba(180, 180, 180, 0.15)', fg: 'var(--text-muted)', border: 'var(--text-muted)' };
20
+ case 'SOLO': return { bg: 'rgba(180, 180, 180, 0.10)', fg: 'var(--text-secondary)', border: 'var(--border)' };
21
+ }
22
+ }
23
+ function levelLabel(level, lang) {
24
+ if (lang === 'zh') {
25
+ switch (level) {
26
+ case 'PROGRESS': return '进步';
27
+ case 'CAUTIOUS': return '需谨慎';
28
+ case 'REGRESS': return '回退';
29
+ case 'NOISE': return '无信号';
30
+ case 'UNDERPOWERED': return '样本不足';
31
+ case 'SOLO': return '单变体';
32
+ }
33
+ }
34
+ return level;
35
+ }
36
+ export function renderVerdictPill(report, lang) {
37
+ const result = computeVerdict(report);
38
+ const c = levelColor(result.level);
39
+ const label = levelLabel(result.level, lang);
40
+ const ration = result.rationale;
41
+ const bullets = [];
42
+ if (ration.significance)
43
+ bullets.push(`<span style="color:var(--text-secondary)">${e(ration.significance)}</span>`);
44
+ if (ration.layerWinners)
45
+ bullets.push(`<span style="color:var(--text-muted);font-size:12px">${lang === 'zh' ? '层' : 'layers'}: ${e(ration.layerWinners)}</span>`);
46
+ if (ration.sampleSize)
47
+ bullets.push(`<span style="color:var(--text-muted);font-size:12px">${lang === 'zh' ? '样本' : 'samples'}: ${e(ration.sampleSize)}</span>`);
48
+ if (ration.judgeAgreement)
49
+ bullets.push(`<span style="color:var(--text-muted);font-size:12px">${e(ration.judgeAgreement)}</span>`);
50
+ const ship = ration.shipRecommendation
51
+ ? `<div style="margin-top:8px;font-size:13px"><strong>${lang === 'zh' ? '建议' : 'Recommendation'}:</strong> ${e(ration.shipRecommendation)}</div>`
52
+ : '';
53
+ return `
54
+ <div style="margin:8px 0 16px;padding:14px 18px;background:${c.bg};border-left:4px solid ${c.border};border-radius:var(--radius)">
55
+ <div style="display:flex;align-items:baseline;gap:12px;flex-wrap:wrap">
56
+ <span style="font-size:13px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em">${lang === 'zh' ? '一句话结论' : 'Verdict'}</span>
57
+ <strong style="color:${c.fg};font-size:18px">${e(label)}</strong>
58
+ <span style="color:var(--text-secondary);font-size:14px">${e(result.headline)}</span>
59
+ </div>
60
+ <div style="margin-top:6px;display:flex;flex-direction:column;gap:2px">${bullets.join('')}</div>
61
+ ${ship}
62
+ </div>`;
63
+ }
64
+ /**
65
+ * Pairwise diff (treatment vs control) bootstrap CI table — populated only when
66
+ * --bootstrap was used and at least 2 variants ran. Each row shows whether
67
+ * treatment significantly outperformed control on compositeScore mean.
68
+ */
69
+ export function renderPairwiseDiff(pairs, lang) {
70
+ if (!pairs || pairs.length === 0)
71
+ return '';
72
+ const validPairs = pairs.filter((p) => p.diffBootstrapCI);
73
+ if (validPairs.length === 0)
74
+ return '';
75
+ const rows = validPairs.map((p) => {
76
+ const ci = p.diffBootstrapCI;
77
+ const sigClass = ci.significant ? 'green' : 'text-muted';
78
+ const sigText = ci.significant ? t('bootstrapDiffSignificant', lang) : t('bootstrapDiffNotSignificant', lang);
79
+ const estColor = ci.estimate > 0 ? 'var(--green)' : ci.estimate < 0 ? 'var(--red)' : 'var(--text-muted)';
80
+ return `<tr>
81
+ <td><strong>${e(p.treatment)}</strong> ${lang === 'zh' ? 'vs' : 'vs'} ${e(p.control)}</td>
82
+ <td style="text-align:center;color:${estColor}"><strong>${ci.estimate >= 0 ? '+' : ''}${ci.estimate}</strong></td>
83
+ <td style="text-align:center;font-size:11px">[${ci.low}, ${ci.high}]</td>
84
+ <td style="text-align:center;color:var(--${sigClass})">${sigText}</td>
85
+ <td style="text-align:center;color:var(--text-muted);font-size:11px">${ci.samples}</td>
86
+ </tr>`;
87
+ }).join('');
88
+ return `
89
+ <h2 style="margin-top:24px">${lang === 'zh' ? '配对对比 (Bootstrap CI)' : 'Pairwise comparison (bootstrap CI)'}</h2>
90
+ <p style="font-size:13px;color:var(--text-secondary);margin:4px 0 12px">${lang === 'zh' ? 'control vs treatment 的均值差 95% CI。CI 不含 0 = 显著差异。bootstrap 不假设分布,适合 LLM 序数评分。' : '95% CI on (treatment - control) mean diff. 0 outside CI = significant. Bootstrap is distribution-free, fits ordinal LLM scores.'}</p>
91
+ <div class="table-wrap">
92
+ <table class="summary-table">
93
+ <thead><tr>
94
+ <th>${lang === 'zh' ? '对照' : 'Pair'}</th>
95
+ <th title="${t('bootstrapDiffLabel', lang)}">${t('bootstrapDiffLabel', lang)}</th>
96
+ <th>95% CI</th>
97
+ <th>${lang === 'zh' ? '显著性' : 'Significance'}</th>
98
+ <th>${lang === 'zh' ? '重采样数' : 'samples'}</th>
99
+ </tr></thead>
100
+ <tbody>${rows}</tbody>
101
+ </table>
102
+ </div>`;
103
+ }
104
+ /**
105
+ * Human-gold agreement section — rendered when --gold-dir was used at run time
106
+ * and the report has been re-persisted with `meta.humanAgreement`. Shows α
107
+ * (primary), bootstrap CI on α, weighted κ, Pearson — and surfaces a
108
+ * contamination warning prominently when the gold annotator id overlaps with
109
+ * the judge model id.
110
+ */
111
+ export function renderHumanAgreement(agreement, lang) {
112
+ if (!agreement)
113
+ return '';
114
+ if (agreement.sampleCount === 0)
115
+ return '';
116
+ const fmt = (x) => Number.isNaN(x) ? 'NaN' : x.toFixed(3);
117
+ const a = agreement;
118
+ const alphaColor = Number.isNaN(a.alpha)
119
+ ? 'var(--text-muted)'
120
+ : a.alpha >= 0.8 ? 'var(--green)'
121
+ : a.alpha >= 0.667 ? 'var(--yellow)'
122
+ : 'var(--red)';
123
+ const interpret = lang === 'zh'
124
+ ? (Number.isNaN(a.alpha)
125
+ ? '评分多样性不足,α 未定义'
126
+ : a.alpha >= 0.8 ? '高度一致 — 结论可放心使用'
127
+ : a.alpha >= 0.667 ? '可接受 — 谨慎结论'
128
+ : a.alpha >= 0.4 ? '较弱一致 — 结论需配合 CI 与人工抽检'
129
+ : a.alpha >= 0 ? '偏差较大 — 排查 rubric / prompt'
130
+ : '系统性反向 — 重新审视判分逻辑')
131
+ : (Number.isNaN(a.alpha)
132
+ ? 'insufficient rating variance for α'
133
+ : a.alpha >= 0.8 ? 'high agreement — conclusions trustworthy'
134
+ : a.alpha >= 0.667 ? 'acceptable — conclude cautiously'
135
+ : a.alpha >= 0.4 ? 'weak agreement — pair with CI + spot-checks'
136
+ : a.alpha >= 0 ? 'large divergence — investigate rubric / prompt'
137
+ : 'systematic inversion — judge logic needs review');
138
+ const warningBlock = a.contaminationWarning
139
+ ? `<div style="margin:8px 0;padding:8px 12px;background:rgba(255, 200, 80, 0.12);border-left:3px solid var(--yellow);font-size:13px"><strong>${lang === 'zh' ? '污染警告' : 'Contamination warning'}:</strong> ${e(a.contaminationWarning)}</div>`
140
+ : '';
141
+ const missingNote = (a.missingCount > 0 || a.unscoredCount > 0)
142
+ ? `<p style="font-size:12px;color:var(--text-muted);margin:4px 0">${lang === 'zh' ? '注: 报告缺' : 'Note: report missing'} ${a.missingCount} ${lang === 'zh' ? '条 sample' : 'samples'}${a.unscoredCount > 0 ? `, ${a.unscoredCount} ${lang === 'zh' ? '条无评分' : 'unscored'}` : ''}</p>`
143
+ : '';
144
+ return `
145
+ <h2 style="margin-top:24px">${lang === 'zh' ? '人工锚点 (Human Gold)' : 'Human gold anchor'}</h2>
146
+ <p style="font-size:13px;color:var(--text-secondary);margin:4px 0 12px">${lang === 'zh' ? '对比 LLM 评委分与外部标注的一致性。α 解决"评委对不对",区别于 CI 解决"评委稳不稳"。' : 'Agreement between the LLM judge and external annotations. α addresses "is the judge correct"; the bootstrap CI addresses "is it consistent".'}</p>
147
+ ${warningBlock}
148
+ <div class="table-wrap">
149
+ <table class="summary-table">
150
+ <thead><tr>
151
+ <th>${lang === 'zh' ? '指标' : 'Metric'}</th>
152
+ <th style="text-align:center">${lang === 'zh' ? '值' : 'Value'}</th>
153
+ <th style="text-align:center">95% CI</th>
154
+ <th>${lang === 'zh' ? '说明' : 'Note'}</th>
155
+ </tr></thead>
156
+ <tbody>
157
+ <tr>
158
+ <td><strong>Krippendorff α</strong></td>
159
+ <td style="text-align:center;color:${alphaColor}"><strong>${fmt(a.alpha)}</strong></td>
160
+ <td style="text-align:center;font-size:11px">[${fmt(a.alphaCI.low)}, ${fmt(a.alphaCI.high)}]</td>
161
+ <td style="font-size:12px;color:var(--text-secondary)">${lang === 'zh' ? '主指标,序数加权' : 'primary, ordinal-weighted'}</td>
162
+ </tr>
163
+ <tr>
164
+ <td>${lang === 'zh' ? '加权 κ' : 'weighted κ'}</td>
165
+ <td style="text-align:center">${fmt(a.weightedKappa)}</td>
166
+ <td style="text-align:center;color:var(--text-muted)">—</td>
167
+ <td style="font-size:12px;color:var(--text-secondary)">${lang === 'zh' ? '副指标,平方加权' : 'secondary, quadratic'}</td>
168
+ </tr>
169
+ <tr>
170
+ <td>Pearson r</td>
171
+ <td style="text-align:center">${fmt(a.pearson)}</td>
172
+ <td style="text-align:center;color:var(--text-muted)">—</td>
173
+ <td style="font-size:12px;color:var(--text-secondary)">${lang === 'zh' ? '只看 rank order' : 'rank-order only'}</td>
174
+ </tr>
175
+ </tbody>
176
+ </table>
177
+ </div>
178
+ <p style="margin:8px 0 0;font-size:13px"><strong>${lang === 'zh' ? '解读' : 'Reading'}:</strong> ${e(interpret)}</p>
179
+ <p style="margin:4px 0 0;font-size:12px;color:var(--text-muted)">${lang === 'zh' ? '标注者' : 'annotator'}: ${e(a.goldAnnotator)} (v${e(a.goldVersion)}) · variant: <strong>${e(a.variant)}</strong> · n=${a.sampleCount}</p>
180
+ ${missingNote}`;
181
+ }
182
+ /**
183
+ * Saturation curve — answers "did I run enough samples?". Renders an inline
184
+ * SVG of mean ± 95% CI shading vs cumulative N, one curve per variant. The
185
+ * verdict line below the chart calls out the saturation point (when the curve
186
+ * has flattened enough that more samples don't materially shrink the CI).
187
+ *
188
+ * Hidden when variance is absent (single-run reports) or when there are fewer
189
+ * than 2 checkpoints. We keep the chart visible at 2-4 checkpoints (= repeat
190
+ * < 5) so users can see the trajectory; the verdict is only computed at
191
+ * repeat ≥ 5.
192
+ */
193
+ export function renderSaturationCurve(saturation, variants, lang) {
194
+ if (!saturation)
195
+ return '';
196
+ const checkpoints = saturation.checkpointSampleCounts;
197
+ if (!checkpoints || checkpoints.length < 2)
198
+ return '';
199
+ // Layout: 600 × 280 SVG, leave 50px on the left for y-axis and 30px at the
200
+ // bottom for x-axis labels. Score is the 1-5 Likert range; pin axes to that.
201
+ const width = 600, height = 280, padL = 50, padR = 20, padT = 16, padB = 32;
202
+ const plotW = width - padL - padR;
203
+ const plotH = height - padT - padB;
204
+ const xMax = Math.max(...checkpoints);
205
+ const xMin = Math.min(...checkpoints);
206
+ const yMin = 1, yMax = 5;
207
+ const xScale = (x) => padL + (plotW * (x - xMin)) / Math.max(1, xMax - xMin);
208
+ const yScale = (y) => padT + plotH - (plotH * (y - yMin)) / (yMax - yMin);
209
+ // Per-variant curves and CI ribbons.
210
+ const seriesParts = [];
211
+ variants.forEach((variant, i) => {
212
+ const trace = saturation.perVariant[variant];
213
+ if (!trace || trace.length === 0)
214
+ return;
215
+ const color = COLORS[i % COLORS.length];
216
+ // CI ribbon: polygon walking forward along upper bound and back along lower.
217
+ const upper = trace.map((p) => `${xScale(p.n).toFixed(1)},${yScale(p.ciHigh).toFixed(1)}`);
218
+ const lower = trace.map((p) => `${xScale(p.n).toFixed(1)},${yScale(p.ciLow).toFixed(1)}`).reverse();
219
+ const ribbonPoints = [...upper, ...lower].join(' ');
220
+ seriesParts.push(`<polygon points="${ribbonPoints}" fill="${color}" fill-opacity="0.15" stroke="none" />`);
221
+ // Mean line.
222
+ const linePoints = trace.map((p) => `${xScale(p.n).toFixed(1)},${yScale(p.mean).toFixed(1)}`).join(' ');
223
+ seriesParts.push(`<polyline points="${linePoints}" fill="none" stroke="${color}" stroke-width="2" />`);
224
+ // Mean dots.
225
+ for (const p of trace) {
226
+ seriesParts.push(`<circle cx="${xScale(p.n).toFixed(1)}" cy="${yScale(p.mean).toFixed(1)}" r="3" fill="${color}" />`);
227
+ }
228
+ });
229
+ // Y-axis ticks at 1..5; x-axis ticks at every checkpoint.
230
+ const yTicks = [1, 2, 3, 4, 5].map((y) => `<line x1="${padL}" y1="${yScale(y)}" x2="${width - padR}" y2="${yScale(y)}" stroke="var(--border)" stroke-width="0.5" />
231
+ <text x="${padL - 6}" y="${yScale(y) + 4}" font-size="10" text-anchor="end" fill="var(--text-muted)">${y}</text>`).join('');
232
+ const xTicks = checkpoints.map((n) => `<text x="${xScale(n)}" y="${height - padB + 14}" font-size="10" text-anchor="middle" fill="var(--text-muted)">${n}</text>`).join('');
233
+ // Per-variant verdict block underneath.
234
+ const verdictRows = [];
235
+ if (saturation.verdicts) {
236
+ for (const variant of variants) {
237
+ const v = saturation.verdicts[variant];
238
+ if (!v)
239
+ continue;
240
+ const flagColor = v.saturated ? 'var(--green)' : 'var(--yellow)';
241
+ const flagText = v.saturated
242
+ ? (lang === 'zh' ? `已饱和 (N=${v.atN})` : `saturated at N=${v.atN}`)
243
+ : (lang === 'zh' ? '尚未饱和' : 'not yet saturated');
244
+ const conf = v.confidence === 'low'
245
+ ? `<span style="color:var(--yellow)">⚠ ${lang === 'zh' ? '样本太少,结论参考意义有限' : 'low confidence, interpret cautiously'}</span>`
246
+ : '';
247
+ verdictRows.push(`<tr><td><strong>${e(variant)}</strong></td>
248
+ <td style="color:${flagColor}"><strong>${flagText}</strong></td>
249
+ <td style="font-size:12px;color:var(--text-secondary)">${e(v.reason)} ${conf}</td></tr>`);
250
+ }
251
+ }
252
+ const noteHtml = saturation.verdicts
253
+ ? `<div class="table-wrap"><table class="summary-table">
254
+ <thead><tr>
255
+ <th>${lang === 'zh' ? '变体' : 'Variant'}</th>
256
+ <th>${lang === 'zh' ? '判定' : 'Verdict'}</th>
257
+ <th>${lang === 'zh' ? '依据' : 'Rationale'}</th>
258
+ </tr></thead><tbody>${verdictRows.join('')}</tbody></table></div>`
259
+ : `<p style="font-size:13px;color:var(--text-muted);margin:8px 0 0">${lang === 'zh' ? '提示:repeat ≥ 5 时才会输出饱和判定。当前数据只够画曲线,不足以下结论。' : 'Note: saturation verdict needs repeat ≥ 5. Current data plots the curve only.'}</p>`;
260
+ return `
261
+ <h2 style="margin-top:24px">${lang === 'zh' ? '饱和曲线 (Saturation curve)' : 'Saturation curve'}</h2>
262
+ <p style="font-size:13px;color:var(--text-secondary);margin:4px 0 12px">${lang === 'zh' ? '随累积样本数 N 增长的均值与 95% CI。CI 宽度衰减放缓即饱和——再多样本对结论无实质收益。' : 'Mean and 95% CI as cumulative N grows. When CI shrink rate flattens, the evidence has saturated — more samples buy little.'}</p>
263
+ <svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${width}px;height:auto;display:block">
264
+ ${yTicks}
265
+ <text x="${padL - 36}" y="${padT + plotH / 2}" font-size="11" text-anchor="middle" fill="var(--text-muted)" transform="rotate(-90 ${padL - 36} ${padT + plotH / 2})">${lang === 'zh' ? '均值' : 'mean'}</text>
266
+ <text x="${padL + plotW / 2}" y="${height - 4}" font-size="11" text-anchor="middle" fill="var(--text-muted)">${lang === 'zh' ? '累积样本数 N' : 'cumulative N'}</text>
267
+ ${xTicks}
268
+ ${seriesParts.join('\n')}
269
+ </svg>
270
+ ${noteHtml}`;
271
+ }
3
272
  export function renderSummaryCards(variants, summary, lang, variance) {
4
273
  // 六维对比表格:事实 / 行为 / LLM 评价 / 成本 / 效率 / 稳定性。
5
274
  // 前三列(事实/行为/LLM 评价)是 task 级原始指标的 variant 聚合;成本/效率同。
@@ -131,7 +400,7 @@ export function renderSummaryCards(variants, summary, lang, variance) {
131
400
  ? '每行是一个实验分组(Variant),六列分别衡量不同维度:'
132
401
  : 'Each row is a Variant. Six columns measure independent dimensions:';
133
402
  const icon = (emoji) => `<span aria-hidden="true">${emoji}</span>`;
134
- // 维度分隔加粗(border-top 2px),让四维的视觉边界更明显。sub 缩进从 28 收到 22。
403
+ // 维度分隔加粗(border-top 2px),让六维的视觉边界更明显。sub 缩进从 28 收到 22。
135
404
  const dim = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-primary);font-weight:600"';
136
405
  const dimDesc = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-secondary)"';
137
406
  const dimFirst = 'style="padding:4px 0 4px;color:var(--text-primary);font-weight:600"';
@@ -178,6 +447,51 @@ export function renderSummaryCards(variants, summary, lang, variance) {
178
447
  <thead>${thead}</thead>
179
448
  <tbody>${rows}</tbody>
180
449
  </table>
450
+ </div>
451
+ ${renderJudgeAgreementBlock(variants, summary, lang)}`;
452
+ }
453
+ /**
454
+ * Cross-sample inter-judge agreement table — only renders when at least one variant
455
+ * has multi-judge ensemble data. This is the v0.20.2 "blog headline" view: shows
456
+ * Pearson + MAD across the whole sample set, the metric that refutes "Claude judge
457
+ * Claude same-modality bias".
458
+ */
459
+ function renderJudgeAgreementBlock(variants, summary, lang) {
460
+ const variantsWithEnsemble = variants.filter((v) => summary[v]?.judgeAgreement);
461
+ if (variantsWithEnsemble.length === 0)
462
+ return '';
463
+ const rows = variantsWithEnsemble.map((v) => {
464
+ const s = summary[v];
465
+ const ag = s.judgeAgreement;
466
+ const judgeList = (s.judgeModels || []).map((j) => `<code>${e(j)}</code>`).join(', ');
467
+ const pearsonCell = ag.pearson != null
468
+ ? `<span title="${t('pearsonDesc', lang)}" style="color:${ag.pearson >= 0.7 ? 'var(--green)' : ag.pearson >= 0.4 ? 'var(--yellow)' : 'var(--red)'}"><strong>${ag.pearson}</strong></span>`
469
+ : `<span style="color:var(--text-muted)">—</span>`;
470
+ const madCell = `<span title="${t('madDesc', lang)}" style="color:${ag.meanAbsDiff < 0.5 ? 'var(--green)' : ag.meanAbsDiff < 1.5 ? 'var(--yellow)' : 'var(--red)'}"><strong>${ag.meanAbsDiff}</strong></span>`;
471
+ return `<tr>
472
+ <td><strong>${e(v)}</strong></td>
473
+ <td style="font-size:11px;color:var(--text-muted)">${judgeList}</td>
474
+ <td style="text-align:center">${pearsonCell}</td>
475
+ <td style="text-align:center">${madCell}</td>
476
+ <td style="text-align:center;color:var(--text-muted)">${ag.sampleCount}</td>
477
+ <td style="text-align:center;color:var(--text-muted)">${ag.pairCount}</td>
478
+ </tr>`;
479
+ }).join('');
480
+ return `
481
+ <h2 style="margin-top:24px">${t('agreementHeader', lang)}</h2>
482
+ <p style="font-size:13px;color:var(--text-secondary);margin:4px 0 12px">${t('agreementDesc', lang)}</p>
483
+ <div class="table-wrap">
484
+ <table class="summary-table">
485
+ <thead><tr>
486
+ <th>${t('variants', lang)}</th>
487
+ <th>${t('judgeModelsLabel', lang)}</th>
488
+ <th title="${t('pearsonDesc', lang)}">${t('pearsonLabel', lang)}</th>
489
+ <th title="${t('madDesc', lang)}">${t('madLabel', lang)}</th>
490
+ <th>${lang === 'zh' ? '用例数' : 'Samples'}</th>
491
+ <th>${lang === 'zh' ? 'Judge 对数' : 'Pairs'}</th>
492
+ </tr></thead>
493
+ <tbody>${rows}</tbody>
494
+ </table>
181
495
  </div>`;
182
496
  }
183
497
  function buildDiagnostic(metric, cfg, winner, lang) {
@@ -221,10 +535,10 @@ function buildDiagnostic(metric, cfg, winner, lang) {
221
535
  if (!metric.significant && isStrong) {
222
536
  const leadPhrase = winner
223
537
  ? (lang === 'zh'
224
- ? `${winner} 看似${cfg.winnerWordZh}但样本不足,建议加大 --repeat`
538
+ ? `${winner} 看似${cfg.winnerWordZh}但用例不足,建议加大 --repeat`
225
539
  : `${winner} looks ${cfg.winnerWordEn.replace(' by', '')} but underpowered — increase --repeat`)
226
540
  : (lang === 'zh'
227
- ? `${strongLabelZh}差异但样本不足,建议加大 --repeat`
541
+ ? `${strongLabelZh}差异但用例不足,建议加大 --repeat`
228
542
  : `${strongLabelEn} effect but underpowered — increase --repeat`);
229
543
  return {
230
544
  icon: '⚠',
@@ -511,7 +825,7 @@ export function renderVarianceComparisons(variance, lang, layeredStatsOpen = fal
511
825
  const diagRulesZhData = [
512
826
  { variant: 'good', icon: '✓', title: '显著差异(中/大效应)', desc: '差异真实且有实际意义,可作为结论', example: '示例:v1 更便宜 · 显著 · g=1.04' },
513
827
  { 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 · 不显著' },
828
+ { variant: 'warn', icon: '⚠', title: '大效应但用例不足', desc: '差距看似大但用例太少,建议加大 --repeat 再判断', example: '示例:v2 胜出 0.30 · g=1.04 · 不显著' },
515
829
  { variant: 'neutral', icon: '—', title: '两变体相当', desc: '既不显著也效应微弱,可视为无差异', example: '示例:Δ≈0 · 不显著 · g<0.2' },
516
830
  ];
517
831
  const diagRulesEnData = [
@@ -743,7 +1057,7 @@ export function renderCoverageSection(coverage, lang) {
743
1057
  }
744
1058
  const uncoveredHint = hintLines.length > 0
745
1059
  ? `<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>
1060
+ <div style="margin-bottom:4px">${lang === 'zh' ? '💡 以下知识未被任何用例覆盖,建议补充测评用例:' : '💡 These knowledge files were not accessed by any sample — consider adding test cases:'}</div>
747
1061
  ${hintLines.map((line) => `<div style="margin:2px 0">${line}</div>`).join('')}
748
1062
  </div>`
749
1063
  : '';
@@ -796,7 +1110,7 @@ export function renderKnowledgeInteractionSection(coverage, gapReports, lang) {
796
1110
  ? '展示本次测评用例和知识库的交互画像——用到哪些知识(使用情况)· 哪些知识想找但没找到或模型表达不确定(盲区)。'
797
1111
  : 'How this test set interacts with the KB — which knowledge was exercised (usage) · which was missed or flagged as uncertain (gaps).';
798
1112
  const readHint = lang === 'zh'
799
- ? '💡 读表:两者同时高 → 知识库内容有问题(有文件但答不出)· 同时低 → 测试用例太浅(没触到复杂场景)· 使用高 + 盲区低 → 理想但警惕样本驯化'
1113
+ ? '💡 读表:两者同时高 → 知识库内容有问题(有文件但答不出)· 同时低 → 测评用例太浅(没触到复杂场景)· 使用高 + 盲区低 → 理想但警惕用例驯化'
800
1114
  : '💡 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
1115
  // 聚合所有 variant(coverage / gap 任一侧存在即纳入)
802
1116
  const allVariants = Array.from(new Set([
@@ -860,7 +1174,7 @@ export function renderKnowledgeInteractionSection(coverage, gapReports, lang) {
860
1174
  }
861
1175
  const uncoveredHint = hintLines.length > 0
862
1176
  ? `<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>
1177
+ <div style="margin-bottom:4px">${lang === 'zh' ? '💡 以下知识未被任何用例覆盖,建议补充测评用例:' : '💡 These knowledge files were not accessed — consider adding test cases:'}</div>
864
1178
  ${hintLines.map((line) => `<div style="margin:2px 0">${line}</div>`).join('')}
865
1179
  </div>`
866
1180
  : '';
@@ -932,7 +1246,7 @@ export function renderKnowledgeInteractionSection(coverage, gapReports, lang) {
932
1246
  <div class="ki-bar" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="${e(barLabel)}">
933
1247
  <div class="ki-bar-fill" style="width:${barW}%;background:${pctColor}"></div>
934
1248
  </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>
1249
+ <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
1250
  <div style="font-size:var(--fs-detail);color:var(--text-secondary);margin-bottom:8px">${weightedHint}</div>
937
1251
  ${typeBadges ? `<div>${typeBadges}</div>` : ''}
938
1252
  ${inventory ? `<details class="ki-details"><summary>${detailsLabel}</summary>