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.
- package/README.md +596 -326
- package/README.zh.md +917 -0
- package/dist/src/analysis/failure-clusterer.d.ts +96 -0
- package/dist/src/analysis/failure-clusterer.d.ts.map +1 -0
- package/dist/src/analysis/failure-clusterer.js +298 -0
- package/dist/src/analysis/failure-clusterer.js.map +1 -0
- package/dist/src/analysis/sample-diagnostics.d.ts +78 -0
- package/dist/src/analysis/sample-diagnostics.d.ts.map +1 -0
- package/dist/src/analysis/sample-diagnostics.js +259 -0
- package/dist/src/analysis/sample-diagnostics.js.map +1 -0
- package/dist/src/analysis/saturation.d.ts +85 -0
- package/dist/src/analysis/saturation.d.ts.map +1 -0
- package/dist/src/analysis/saturation.js +174 -0
- package/dist/src/analysis/saturation.js.map +1 -0
- package/dist/src/cli.js +772 -18
- package/dist/src/cli.js.map +1 -1
- package/dist/src/eval-core/bootstrap.d.ts +72 -0
- package/dist/src/eval-core/bootstrap.d.ts.map +1 -0
- package/dist/src/eval-core/bootstrap.js +174 -0
- package/dist/src/eval-core/bootstrap.js.map +1 -0
- package/dist/src/eval-core/evaluation-execution.d.ts +15 -1
- package/dist/src/eval-core/evaluation-execution.d.ts.map +1 -1
- package/dist/src/eval-core/evaluation-execution.js +37 -3
- package/dist/src/eval-core/evaluation-execution.js.map +1 -1
- package/dist/src/eval-core/evaluation-job.d.ts +10 -2
- package/dist/src/eval-core/evaluation-job.d.ts.map +1 -1
- package/dist/src/eval-core/evaluation-job.js +9 -1
- package/dist/src/eval-core/evaluation-job.js.map +1 -1
- package/dist/src/eval-core/evaluation-reporting.d.ts.map +1 -1
- package/dist/src/eval-core/evaluation-reporting.js +90 -0
- package/dist/src/eval-core/evaluation-reporting.js.map +1 -1
- package/dist/src/eval-core/schema.d.ts.map +1 -1
- package/dist/src/eval-core/schema.js +69 -0
- package/dist/src/eval-core/schema.js.map +1 -1
- package/dist/src/eval-core/verdict.d.ts +74 -0
- package/dist/src/eval-core/verdict.d.ts.map +1 -0
- package/dist/src/eval-core/verdict.js +283 -0
- package/dist/src/eval-core/verdict.js.map +1 -0
- package/dist/src/eval-workflows/each-evaluation-workflow.d.ts +16 -3
- package/dist/src/eval-workflows/each-evaluation-workflow.d.ts.map +1 -1
- package/dist/src/eval-workflows/each-evaluation-workflow.js +10 -2
- package/dist/src/eval-workflows/each-evaluation-workflow.js.map +1 -1
- package/dist/src/eval-workflows/evaluation-pipeline.d.ts +17 -1
- package/dist/src/eval-workflows/evaluation-pipeline.d.ts.map +1 -1
- package/dist/src/eval-workflows/evaluation-pipeline.js +45 -3
- package/dist/src/eval-workflows/evaluation-pipeline.js.map +1 -1
- package/dist/src/eval-workflows/run-evaluation.d.ts +23 -2
- package/dist/src/eval-workflows/run-evaluation.d.ts.map +1 -1
- package/dist/src/eval-workflows/run-evaluation.js +104 -4
- package/dist/src/eval-workflows/run-evaluation.js.map +1 -1
- package/dist/src/grading/assertions.d.ts +16 -0
- package/dist/src/grading/assertions.d.ts.map +1 -1
- package/dist/src/grading/assertions.js +385 -111
- package/dist/src/grading/assertions.js.map +1 -1
- package/dist/src/grading/debias-validate.d.ts +84 -0
- package/dist/src/grading/debias-validate.d.ts.map +1 -0
- package/dist/src/grading/debias-validate.js +173 -0
- package/dist/src/grading/debias-validate.js.map +1 -0
- package/dist/src/grading/gold-cli.d.ts +88 -0
- package/dist/src/grading/gold-cli.d.ts.map +1 -0
- package/dist/src/grading/gold-cli.js +251 -0
- package/dist/src/grading/gold-cli.js.map +1 -0
- package/dist/src/grading/gold-dataset.d.ts +73 -0
- package/dist/src/grading/gold-dataset.d.ts.map +1 -0
- package/dist/src/grading/gold-dataset.js +161 -0
- package/dist/src/grading/gold-dataset.js.map +1 -0
- package/dist/src/grading/human-gold.d.ts +102 -0
- package/dist/src/grading/human-gold.d.ts.map +1 -0
- package/dist/src/grading/human-gold.js +188 -0
- package/dist/src/grading/human-gold.js.map +1 -0
- package/dist/src/grading/index.d.ts +27 -2
- package/dist/src/grading/index.d.ts.map +1 -1
- package/dist/src/grading/index.js +36 -18
- package/dist/src/grading/index.js.map +1 -1
- package/dist/src/grading/judge.d.ts +65 -2
- package/dist/src/grading/judge.d.ts.map +1 -1
- package/dist/src/grading/judge.js +280 -23
- package/dist/src/grading/judge.js.map +1 -1
- package/dist/src/inputs/eval-config.js +19 -0
- package/dist/src/inputs/eval-config.js.map +1 -1
- package/dist/src/observability/{production-analyzer.d.ts → skill-health-analyzer.d.ts} +24 -2
- package/dist/src/observability/skill-health-analyzer.d.ts.map +1 -0
- package/dist/src/observability/{production-analyzer.js → skill-health-analyzer.js} +61 -6
- package/dist/src/observability/skill-health-analyzer.js.map +1 -0
- package/dist/src/observability/trace-adapter.d.ts.map +1 -1
- package/dist/src/observability/trace-adapter.js +27 -1
- package/dist/src/observability/trace-adapter.js.map +1 -1
- package/dist/src/renderer/html-renderer.d.ts.map +1 -1
- package/dist/src/renderer/html-renderer.js +40 -6
- package/dist/src/renderer/html-renderer.js.map +1 -1
- package/dist/src/renderer/layout.d.ts.map +1 -1
- package/dist/src/renderer/layout.js +138 -4
- package/dist/src/renderer/layout.js.map +1 -1
- package/dist/src/renderer/skill-health-renderer.d.ts +2 -2
- package/dist/src/renderer/skill-health-renderer.d.ts.map +1 -1
- package/dist/src/renderer/skill-health-renderer.js +39 -4
- package/dist/src/renderer/skill-health-renderer.js.map +1 -1
- package/dist/src/renderer/summary.d.ts +28 -1
- package/dist/src/renderer/summary.d.ts.map +1 -1
- package/dist/src/renderer/summary.js +322 -8
- package/dist/src/renderer/summary.js.map +1 -1
- package/dist/src/renderer/table.d.ts.map +1 -1
- package/dist/src/renderer/table.js +63 -2
- package/dist/src/renderer/table.js.map +1 -1
- package/dist/src/server/report-server.d.ts +2 -1
- package/dist/src/server/report-server.d.ts.map +1 -1
- package/dist/src/server/report-server.js +397 -2
- package/dist/src/server/report-server.js.map +1 -1
- package/dist/src/types.d.ts +247 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +24 -6
- package/dist/src/observability/production-analyzer.d.ts.map +0 -1
- 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)
|
|
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}
|
|
538
|
+
? `${winner} 看似${cfg.winnerWordZh}但用例不足,建议加大 --repeat`
|
|
225
539
|
: `${winner} looks ${cfg.winnerWordEn.replace(' by', '')} but underpowered — increase --repeat`)
|
|
226
540
|
: (lang === 'zh'
|
|
227
|
-
? `${strongLabelZh}
|
|
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: '
|
|
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' ? '💡
|
|
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' ? '💡
|
|
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' ? '
|
|
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>
|