oh-my-knowledge 0.39.0 → 0.40.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/dist/assets/agent-skills/omk/references/commands.md +1 -0
- package/dist/authoring/evolver.js +9 -5
- package/dist/cli/commands/list.d.ts +4 -1
- package/dist/cli/commands/list.js +12 -4
- package/dist/cli/commands/observe/index.d.ts +9 -0
- package/dist/cli/commands/observe/index.js +74 -1
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +8 -0
- package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/list.js +4 -0
- package/dist/cli/lib/record-evolve-outcome.js +19 -10
- package/dist/eval-core/bootstrap.d.ts +53 -2
- package/dist/eval-core/bootstrap.js +82 -5
- package/dist/eval-core/evaluation-reporting.js +37 -15
- package/dist/eval-core/schema.js +5 -3
- package/dist/eval-core/verdict.d.ts +30 -0
- package/dist/eval-core/verdict.js +71 -21
- package/dist/grading/assertions.js +28 -0
- package/dist/grading/debias-validate.js +6 -2
- package/dist/grading/index.js +6 -1
- package/dist/grading/layered-scores.d.ts +15 -0
- package/dist/grading/layered-scores.js +64 -38
- package/dist/managed/index.d.ts +1 -0
- package/dist/managed/index.js +1 -0
- package/dist/managed/list-view.d.ts +9 -1
- package/dist/managed/list-view.js +7 -1
- package/dist/managed/observe-feedback.d.ts +60 -0
- package/dist/managed/observe-feedback.js +49 -0
- package/dist/managed/store.d.ts +28 -1
- package/dist/managed/store.js +84 -2
- package/dist/observability/skill-health-analyzer.d.ts +5 -0
- package/dist/observability/skill-health-analyzer.js +3 -2
- package/dist/renderer/managed-history-renderer.js +80 -3
- package/dist/renderer/summary.js +23 -22
- package/dist/types/judge.d.ts +8 -0
- package/dist/types/managed.d.ts +33 -0
- package/dist/types/report.d.ts +10 -0
- package/package.json +1 -1
|
@@ -403,6 +403,7 @@ omk observe [sessionsDir] [flags]
|
|
|
403
403
|
|
|
404
404
|
**Flags:**
|
|
405
405
|
|
|
406
|
+
- `--feedback` `boolean`:把生产健康观测反哺已纳管的同名 skill(--no-feedback 关闭)
|
|
406
407
|
- `--from` `option`:起始时间 ISO,优先级高于 --last
|
|
407
408
|
- `--global` `boolean`:写全局 ~/.oh-my-knowledge/observe-health,而非项目 .omk/observe-health
|
|
408
409
|
- `--kb` `option`:知识库 root,启用 KB-aware 分析
|
|
@@ -868,11 +868,15 @@ export async function evolveSkill({ skillPath, samplesPath, rounds = 5, target =
|
|
|
868
868
|
// Significance accept gate: accept only when the candidate is *significantly*
|
|
869
869
|
// above the current best on the decision (val) set, not merely numerically higher
|
|
870
870
|
// — rejecting gains indistinguishable from judge noise. `lastReport` is the current
|
|
871
|
-
// best's fresh eval and `candidateReport` the candidate's, over the same samples
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
871
|
+
// best's fresh eval and `candidateReport` the candidate's, over the same samples.
|
|
872
|
+
// **Deliberately UNPAIRED here** (independent resampling, `decideAccept` → `bootstrapDiffCI`),
|
|
873
|
+
// unlike the report verdict / debias which switched to `bootstrapPairedDiffCI`: this is an
|
|
874
|
+
// *optimization accept-gate*, and the unpaired bootstrap's wider, conservative CI is the
|
|
875
|
+
// wanted bias — it raises the bar to accept, so evolve does not chase paired-tightened,
|
|
876
|
+
// marginally-significant gains that risk overfitting the decision set. Power is not the goal
|
|
877
|
+
// for de-/escalation gates; not accepting noise is. Under-powered decision sets degrade to the
|
|
878
|
+
// legacy point-estimate accept (note: that path compares the prior-round best scalar, not this
|
|
879
|
+
// fresh re-eval) and flag `gate.underpowered`.
|
|
876
880
|
const valIds = split ? split.valIds : new Set(allSampleIds);
|
|
877
881
|
const bestScores = perSampleComposite(lastReport, lastVariantKey, valIds);
|
|
878
882
|
const candScores = perSampleComposite(candidateReport, candidateVariantKey, valIds);
|
|
@@ -2,7 +2,10 @@ import { BaseCommand } from '../oclif/base-command.js';
|
|
|
2
2
|
import { sanitizeCell } from '../lib/cell-format.js';
|
|
3
3
|
import { type ManagedListRow } from '../../managed/index.js';
|
|
4
4
|
import type { CliLang } from '../lib/i18n.js';
|
|
5
|
-
/** CJK
|
|
5
|
+
/** CJK 全角字符 + 星平面 emoji 按 2 列计宽,使含中文表头 / emoji 标记(如 🔬)的列也能对齐。
|
|
6
|
+
* 逐**码点**迭代(for...of),星平面 emoji 是单码点,加 `\u{1F300}-\u{1FAFF}`(含 🔬 U+1F52C)判 2 列。
|
|
7
|
+
* 注:`⚠️` 是「U+26A0 + U+FE0F」两码点、各 1 列合计 2,本就与 2 列渲染对齐,不另判(避免重复计数);
|
|
8
|
+
* `✓`(U+2713)终端按 1 列渲染、保持 1。 */
|
|
6
9
|
export declare function dispWidth(s: string): number;
|
|
7
10
|
/** 按**显示宽度**截断(不是 code unit):逐码点累加 dispWidth,绝不切断 surrogate 对、CJK 也不溢出列。 */
|
|
8
11
|
export declare function truncate(s: string, max: number): string;
|
|
@@ -4,11 +4,14 @@ import { BaseCommand } from '../oclif/base-command.js';
|
|
|
4
4
|
import { tCli } from '../lib/i18n.js';
|
|
5
5
|
import { sanitizeCell } from '../lib/cell-format.js';
|
|
6
6
|
import { globalManagedDir, listManagedRows, managedDir, resolveManagedDir, } from '../../managed/index.js';
|
|
7
|
-
/** CJK
|
|
7
|
+
/** CJK 全角字符 + 星平面 emoji 按 2 列计宽,使含中文表头 / emoji 标记(如 🔬)的列也能对齐。
|
|
8
|
+
* 逐**码点**迭代(for...of),星平面 emoji 是单码点,加 `\u{1F300}-\u{1FAFF}`(含 🔬 U+1F52C)判 2 列。
|
|
9
|
+
* 注:`⚠️` 是「U+26A0 + U+FE0F」两码点、各 1 列合计 2,本就与 2 列渲染对齐,不另判(避免重复计数);
|
|
10
|
+
* `✓`(U+2713)终端按 1 列渲染、保持 1。 */
|
|
8
11
|
export function dispWidth(s) {
|
|
9
12
|
let w = 0;
|
|
10
13
|
for (const ch of s)
|
|
11
|
-
w += /[
|
|
14
|
+
w += /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩\u{1F300}-\u{1FAFF}]/u.test(ch) ? 2 : 1;
|
|
12
15
|
return w;
|
|
13
16
|
}
|
|
14
17
|
function pad(s, width) {
|
|
@@ -45,7 +48,9 @@ export function renderTable(rows, lang) {
|
|
|
45
48
|
truncate(sanitizeCell(r.name), 40), // name 与 source 同为用户可控、可超长 → 同样按显示宽度截断,防撑爆表宽
|
|
46
49
|
r.kind, // ArtifactKind 枚举(validator 已收窄),无需洗
|
|
47
50
|
// 不可达 → 标「?」(drift 未核),绝不冒充 stale;reachable 且漂移才 stale ⚠️;已人工接受标 promoted ✓。
|
|
48
|
-
|
|
51
|
+
// 生产盲区 🔬 与生命周期**正交**(observe 量线上部署版),叠加在 state token 之后,不替换 state。
|
|
52
|
+
(!r.reachable ? `${r.state} ?` : r.state === 'stale' ? 'stale ⚠️' : r.state === 'promoted' ? 'promoted ✓' : r.state)
|
|
53
|
+
+ (r.productionGap ? ' 🔬' : ''),
|
|
49
54
|
r.latestVerdict ? sanitizeCell(r.latestVerdict) : '—',
|
|
50
55
|
`${r.currentEvidenceCount}/${r.totalEvidenceCount}`,
|
|
51
56
|
truncate(sanitizeCell(r.sourceLabel), 48),
|
|
@@ -99,7 +104,8 @@ export default class List extends BaseCommand {
|
|
|
99
104
|
const hasDrift = rows.some((r) => r.drifted);
|
|
100
105
|
const hasUnreachable = rows.some((r) => !r.reachable);
|
|
101
106
|
const hasPromoted = rows.some((r) => r.state === 'promoted');
|
|
102
|
-
|
|
107
|
+
const hasProductionGap = rows.some((r) => r.productionGap);
|
|
108
|
+
if (hasDrift || hasUnreachable || hasPromoted || hasProductionGap)
|
|
103
109
|
process.stderr.write('\n');
|
|
104
110
|
if (hasPromoted)
|
|
105
111
|
process.stderr.write(tCli('cli.list.promoted_note', lang));
|
|
@@ -107,6 +113,8 @@ export default class List extends BaseCommand {
|
|
|
107
113
|
process.stderr.write(tCli('cli.list.drift_note', lang));
|
|
108
114
|
if (hasUnreachable)
|
|
109
115
|
process.stderr.write(tCli('cli.list.unreachable_note', lang));
|
|
116
|
+
if (hasProductionGap)
|
|
117
|
+
process.stderr.write(tCli('cli.list.production_gap_note', lang));
|
|
110
118
|
process.stderr.write(tCli('cli.list.legend', lang));
|
|
111
119
|
});
|
|
112
120
|
}
|
|
@@ -9,6 +9,14 @@ export declare function persistObserveHealthReport(report: SkillHealthReport, ou
|
|
|
9
9
|
id: string;
|
|
10
10
|
jsonPath: string;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* SkillHealthReport → managed 反哺的结构化最小入参(#235)。纯映射、可单测 —— 把「observe 报告 →
|
|
14
|
+
* ObserveReportView」这段层间胶水从 CLI 副作用里拆出来,免得 healthBand 取错字段 / observedAt 取错时刻
|
|
15
|
+
* 这类映射 bug 无人验。`observedAt` 取**流量窗口结束时刻**(timeRange.to,空则退 generatedAt),不是「此刻」
|
|
16
|
+
* 的 generatedAt —— 否则 latest-wins 会把所有观测当成一样新(见 ManagedObservation.observedAt)。
|
|
17
|
+
* `healthBand` 由 observability 的 `healthBandOf` 逐 skill 算(阈值单一来源,注入以保可测)。
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildObserveReportView(report: SkillHealthReport, reportId: string, healthBandOf: (weightedGapRate: number) => 'green' | 'yellow' | 'red'): import('../../../managed/index.js').ObserveReportView;
|
|
12
20
|
export default class Observe extends BaseCommand {
|
|
13
21
|
static description: string;
|
|
14
22
|
static examples: {
|
|
@@ -27,6 +35,7 @@ export default class Observe extends BaseCommand {
|
|
|
27
35
|
skills: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
36
|
'output-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
37
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
38
|
+
feedback: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
30
39
|
};
|
|
31
40
|
run(): Promise<void>;
|
|
32
41
|
}
|
|
@@ -23,6 +23,68 @@ export function persistObserveHealthReport(report, outDir) {
|
|
|
23
23
|
indexObserveWrite(report, jsonPath, outDir, id);
|
|
24
24
|
return { id, jsonPath };
|
|
25
25
|
}
|
|
26
|
+
// 盲区信号类型 → 人话标签(建议补样本提示用)。技术枚举键的展示名,zh/en 分列。
|
|
27
|
+
const GAP_AREA_LABELS = {
|
|
28
|
+
failed_search: { zh: '检索失败', en: 'failed search' },
|
|
29
|
+
explicit_marker: { zh: '显式缺口', en: 'explicit gap' },
|
|
30
|
+
hedging: { zh: '含糊回避', en: 'hedging' },
|
|
31
|
+
repeated_failure: { zh: '反复失败', en: 'repeated failure' },
|
|
32
|
+
};
|
|
33
|
+
/** 取盲区计数最高的前几类,组成「建议补哪类用例」的人话区域串(只展示信号所在,不生成具体用例)。
|
|
34
|
+
* 只迭代**四个已知盲区类型**(GAP_AREA_LABELS 的键),记录 / 报告里若混入额外键一律忽略,不进展示。 */
|
|
35
|
+
function topGapAreas(gapByType, lang) {
|
|
36
|
+
const sep = lang === 'zh' ? '、' : ', ';
|
|
37
|
+
const areas = Object.keys(GAP_AREA_LABELS)
|
|
38
|
+
.map((k) => [k, gapByType[k] ?? 0])
|
|
39
|
+
.filter(([, n]) => n > 0)
|
|
40
|
+
.sort((a, b) => b[1] - a[1])
|
|
41
|
+
.slice(0, 3)
|
|
42
|
+
.map(([k]) => GAP_AREA_LABELS[k][lang])
|
|
43
|
+
.join(sep);
|
|
44
|
+
return areas || (lang === 'zh' ? '未归类盲区' : 'uncategorized gaps');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* SkillHealthReport → managed 反哺的结构化最小入参(#235)。纯映射、可单测 —— 把「observe 报告 →
|
|
48
|
+
* ObserveReportView」这段层间胶水从 CLI 副作用里拆出来,免得 healthBand 取错字段 / observedAt 取错时刻
|
|
49
|
+
* 这类映射 bug 无人验。`observedAt` 取**流量窗口结束时刻**(timeRange.to,空则退 generatedAt),不是「此刻」
|
|
50
|
+
* 的 generatedAt —— 否则 latest-wins 会把所有观测当成一样新(见 ManagedObservation.observedAt)。
|
|
51
|
+
* `healthBand` 由 observability 的 `healthBandOf` 逐 skill 算(阈值单一来源,注入以保可测)。
|
|
52
|
+
*/
|
|
53
|
+
export function buildObserveReportView(report, reportId, healthBandOf) {
|
|
54
|
+
return {
|
|
55
|
+
reportId,
|
|
56
|
+
observedAt: report.meta.timeRange?.to || report.meta.generatedAt,
|
|
57
|
+
skills: Object.values(report.bySkill).map((s) => ({
|
|
58
|
+
skillName: s.skillName,
|
|
59
|
+
segmentCount: s.segmentCount,
|
|
60
|
+
gapRate: s.gap.gapRate,
|
|
61
|
+
weightedGapRate: s.gap.weightedGapRate,
|
|
62
|
+
confidence: s.confidence,
|
|
63
|
+
healthBand: healthBandOf(s.gap.weightedGapRate),
|
|
64
|
+
gapByType: s.gap.byType,
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* observe → 管理支柱反哺(#235):把每个 skill 的生产健康落成观测追加进同名受管记录,并打印「已记录 / 生产
|
|
70
|
+
* 盲区警示」。**非致命**:管理是 observe 旁路,任何异常都不该让 observe 失败(try/catch 吞掉)。observability /
|
|
71
|
+
* managed 运行时函数动态 import,与 observe 主体一致、不拖累 CLI 启动。
|
|
72
|
+
*/
|
|
73
|
+
async function recordObserveFeedback(report, reportId, lang) {
|
|
74
|
+
try {
|
|
75
|
+
const { healthBandOf } = await import('../../../observability/skill-health-analyzer.js');
|
|
76
|
+
const { recordObserveHealth } = await import('../../../managed/index.js');
|
|
77
|
+
const written = recordObserveHealth(buildObserveReportView(report, reportId, healthBandOf));
|
|
78
|
+
for (const w of written) {
|
|
79
|
+
process.stdout.write(w.isProductionGap
|
|
80
|
+
? tCli('cli.observe.production_gap', lang, { name: w.name, areas: topGapAreas(w.gapByType, lang) })
|
|
81
|
+
: tCli('cli.observe.observation_recorded', lang, { name: w.name }));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// 反哺是 observe 旁路,任何异常都不该让 observe 失败。
|
|
86
|
+
}
|
|
87
|
+
}
|
|
26
88
|
// `omk observe <sessions-dir>` 是默认命令 —— 分析 sessions 目录的 skill 调用健康度,产出 observe-health 报告(JSON),
|
|
27
89
|
// 由 Studio 健康报告页按需渲染。observe 这条线的另一条产物是观测收件箱(observe-inbox),走子命令 ingest / inbox / show。
|
|
28
90
|
export default class Observe extends BaseCommand {
|
|
@@ -80,6 +142,14 @@ export default class Observe extends BaseCommand {
|
|
|
80
142
|
en: 'Write to global ~/.oh-my-knowledge/observe-health instead of project .omk/observe-health',
|
|
81
143
|
}),
|
|
82
144
|
}),
|
|
145
|
+
feedback: Flags.boolean({
|
|
146
|
+
default: true,
|
|
147
|
+
allowNo: true,
|
|
148
|
+
description: bilingual({
|
|
149
|
+
zh: '把生产健康观测反哺已纳管的同名 skill(--no-feedback 关闭)',
|
|
150
|
+
en: 'Feed production-health observations back to managed skills of the same name (--no-feedback to disable)',
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
83
153
|
};
|
|
84
154
|
async run() {
|
|
85
155
|
const { args, flags } = await this.parse(Observe);
|
|
@@ -120,7 +190,7 @@ export default class Observe extends BaseCommand {
|
|
|
120
190
|
const outDir = flags['output-dir']
|
|
121
191
|
? resolve(flags['output-dir'])
|
|
122
192
|
: (flags.global ? globalObserveHealthDir() : projectObserveHealthDir());
|
|
123
|
-
const { jsonPath } = persistObserveHealthReport(report, outDir);
|
|
193
|
+
const { id, jsonPath } = persistObserveHealthReport(report, outDir);
|
|
124
194
|
const { sessionCount, segmentCount, toolCallCount, toolFailureRate } = report.meta;
|
|
125
195
|
console.log('');
|
|
126
196
|
console.log(`sessions: ${sessionCount} · segments: ${segmentCount} · tool calls: ${toolCallCount} · fail rate: ${(toolFailureRate * 100).toFixed(1)}%`);
|
|
@@ -139,6 +209,9 @@ export default class Observe extends BaseCommand {
|
|
|
139
209
|
console.log('');
|
|
140
210
|
console.log(`report written to: ${jsonPath}`);
|
|
141
211
|
console.log(tCli('cli.observe.view_hint', lang));
|
|
212
|
+
// #235 受管反哺:把生产健康观测落进同名受管 skill(--no-feedback 关)。非致命旁路。
|
|
213
|
+
if (flags.feedback)
|
|
214
|
+
await recordObserveFeedback(report, id, lang);
|
|
142
215
|
});
|
|
143
216
|
}
|
|
144
217
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CliMessage } from './types.js';
|
|
2
|
-
export type CommonMessageKey = 'cli.common.unknown_domain' | 'cli.common.error_prefix' | 'cli.common.skill_dir_not_found' | 'cli.common.skill_file_not_found' | 'cli.common.skill_dir_no_skill_md' | 'cli.common.report_not_found' | 'cli.common.no_judge_model' | 'cli.common.judge_models_single_only' | 'cli.common.warn_load_samples_failed' | 'cli.update.new_version_available' | 'cli.update.box_title' | 'cli.update.box_version_line' | 'cli.update.box_upgrade_line' | 'cli.update.box_silence_line' | 'cli.observe.view_hint' | 'cli.studio.started' | 'cli.studio.stop_hint' | 'cli.studio.open_failed' | 'cli.doctor.no_skill_found' | 'cli.doctor.samples_detected' | 'cli.doctor.progress_skill_start' | 'cli.doctor.progress_skill_done';
|
|
2
|
+
export type CommonMessageKey = 'cli.common.unknown_domain' | 'cli.common.error_prefix' | 'cli.common.skill_dir_not_found' | 'cli.common.skill_file_not_found' | 'cli.common.skill_dir_no_skill_md' | 'cli.common.report_not_found' | 'cli.common.no_judge_model' | 'cli.common.judge_models_single_only' | 'cli.common.warn_load_samples_failed' | 'cli.update.new_version_available' | 'cli.update.box_title' | 'cli.update.box_version_line' | 'cli.update.box_upgrade_line' | 'cli.update.box_silence_line' | 'cli.observe.view_hint' | 'cli.observe.observation_recorded' | 'cli.observe.production_gap' | 'cli.studio.started' | 'cli.studio.stop_hint' | 'cli.studio.open_failed' | 'cli.doctor.no_skill_found' | 'cli.doctor.samples_detected' | 'cli.doctor.progress_skill_start' | 'cli.doctor.progress_skill_done';
|
|
3
3
|
export declare const commonDict: Record<CommonMessageKey, CliMessage>;
|
|
@@ -59,6 +59,14 @@ export const commonDict = {
|
|
|
59
59
|
zh: '分析 JSON 已写入 output-dir;后续可用 omk observe 持续生成健康报告。',
|
|
60
60
|
en: 'Analysis JSON written to output-dir; use omk observe to keep producing health reports.',
|
|
61
61
|
},
|
|
62
|
+
'cli.observe.observation_recorded': {
|
|
63
|
+
zh: '🔭 已为受管 skill「{name}」记录生产健康观测。\n',
|
|
64
|
+
en: '🔭 Recorded a production-health observation for managed skill "{name}".\n',
|
|
65
|
+
},
|
|
66
|
+
'cli.observe.production_gap': {
|
|
67
|
+
zh: '⚠️ 受管 skill「{name}」线上检测到生产盲区({areas})。建议补充对应用例后重跑 omk eval —— observe 只提示,不改样本集。\n',
|
|
68
|
+
en: '⚠️ Managed skill "{name}" shows production gaps in real traffic ({areas}). Consider adding matching samples and re-running omk eval — observe only suggests, it does not edit the sample set.\n',
|
|
69
|
+
},
|
|
62
70
|
'cli.studio.started': {
|
|
63
71
|
zh: 'studio 已启动:{url}',
|
|
64
72
|
en: 'Studio running at {url}',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CliMessage } from './types.js';
|
|
2
|
-
export type ListMessageKey = 'cli.list.header' | 'cli.list.empty' | 'cli.list.empty_hint' | 'cli.list.col_name' | 'cli.list.col_kind' | 'cli.list.col_state' | 'cli.list.col_verdict' | 'cli.list.col_evidence' | 'cli.list.col_source' | 'cli.list.drift_note' | 'cli.list.unreachable_note' | 'cli.list.promoted_note' | 'cli.list.legend';
|
|
2
|
+
export type ListMessageKey = 'cli.list.header' | 'cli.list.empty' | 'cli.list.empty_hint' | 'cli.list.col_name' | 'cli.list.col_kind' | 'cli.list.col_state' | 'cli.list.col_verdict' | 'cli.list.col_evidence' | 'cli.list.col_source' | 'cli.list.drift_note' | 'cli.list.unreachable_note' | 'cli.list.promoted_note' | 'cli.list.production_gap_note' | 'cli.list.legend';
|
|
3
3
|
export declare const listDict: Record<ListMessageKey, CliMessage>;
|
|
@@ -29,6 +29,10 @@ export const listDict = {
|
|
|
29
29
|
zh: '✓ = 当前版本已按证据人工接受为 promoted(omk promote)。\n',
|
|
30
30
|
en: '✓ = current version accepted as promoted on evidence (omk promote).\n',
|
|
31
31
|
},
|
|
32
|
+
'cli.list.production_gap_note': {
|
|
33
|
+
zh: '🔬 = observe 在线上检测到生产盲区(与生命周期无关的版本无关信号);建议补对应用例后重跑 omk eval。\n',
|
|
34
|
+
en: '🔬 = observe detected a production gap in real traffic (a version-agnostic signal, orthogonal to lifecycle); add matching samples and re-run omk eval.\n',
|
|
35
|
+
},
|
|
32
36
|
'cli.list.legend': {
|
|
33
37
|
zh: '证据列 = 当前有效 / 全部(历史含旧内容证据,供回滚)。\n',
|
|
34
38
|
en: 'EVIDENCE column = current / total (history keeps old-content evidence for rollback).\n',
|
|
@@ -2,26 +2,35 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { createOverlayReportStore } from '../../server/report-store.js';
|
|
3
3
|
import { projectReportsDir, globalReportsDir } from '../../eval-core/measurement-dirs.js';
|
|
4
4
|
import { computeVerdict } from '../../eval-core/verdict.js';
|
|
5
|
-
import {
|
|
5
|
+
import { bootstrapPairedDiffCI, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES } from '../../eval-core/bootstrap.js';
|
|
6
6
|
import { resolveManagedDir, managedDir, loadAllManagedRecords, appendManagedEvidence, rebaselineManagedContentHash, buildEvidenceRef, probeSourceState, } from '../../managed/index.js';
|
|
7
7
|
/**
|
|
8
8
|
* round-bestRound vs round-0 的忠实 verdict:复刻 eval 管线(evaluation-reporting.ts)对两变体抽
|
|
9
|
-
* per-sample composite →
|
|
9
|
+
* per-sample composite → 按 sample 配对 bootstrapPairedDiffCI → computeVerdict,与 `omk eval --bootstrap`
|
|
10
|
+
* 同口径、同 α / 重采样数。
|
|
10
11
|
* 不自造门限:评委是否显著、是否 PROGRESS 全交给既有 computeVerdict。
|
|
11
12
|
*/
|
|
12
13
|
function winnerVerdict(report, winnerVariant) {
|
|
13
14
|
const baseline = 'round-0';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// 按 sample **配对**(与 evaluation-reporting 主 A/B 同口径 —— 本函数职责就是复刻 eval 管线):baseline 与
|
|
16
|
+
// winner 在同一 sample 上都可测(composite > 0)才入对。同一 sample 两版分数正相关,配对 bootstrap 收紧 diff
|
|
17
|
+
// CI;独立重采样会高估方差、保守失功效。diff = b − a = winner − baseline。
|
|
18
|
+
const compositeOf = (v, r) => {
|
|
19
|
+
const e = r.variants[v];
|
|
20
|
+
return e && typeof e.compositeScore === 'number' && e.compositeScore > 0 ? e.compositeScore : undefined;
|
|
21
|
+
};
|
|
22
|
+
const pairs = [];
|
|
23
|
+
for (const r of report.results) {
|
|
24
|
+
const a = compositeOf(baseline, r);
|
|
25
|
+
const b = compositeOf(winnerVariant, r);
|
|
26
|
+
if (a !== undefined && b !== undefined)
|
|
27
|
+
pairs.push({ a, b });
|
|
28
|
+
}
|
|
29
|
+
const pairComparisons = pairs.length >= 2
|
|
21
30
|
? [{
|
|
22
31
|
control: baseline,
|
|
23
32
|
treatment: winnerVariant,
|
|
24
|
-
diffBootstrapCI:
|
|
33
|
+
diffBootstrapCI: bootstrapPairedDiffCI(pairs, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES),
|
|
25
34
|
}]
|
|
26
35
|
: undefined;
|
|
27
36
|
const slice = {
|
|
@@ -17,8 +17,14 @@
|
|
|
17
17
|
* - bootstrapDiffCI: CI for the difference (B - A); 0 outside the CI = significant
|
|
18
18
|
* - bootstrapWithMetric: generic interface so saturation analysis can reuse
|
|
19
19
|
*
|
|
20
|
-
* Reproducibility:
|
|
21
|
-
*
|
|
20
|
+
* Reproducibility: CIs are **deterministic by default** — when no `seed` is passed,
|
|
21
|
+
* a fixed `DEFAULT_BOOTSTRAP_SEED` is used, so the same eval run twice yields
|
|
22
|
+
* byte-identical CIs (and a stable verdict near the significance boundary). This is
|
|
23
|
+
* a measurement-validity requirement: an unseeded `Math.random()` would let the
|
|
24
|
+
* `significant` flag flip between identical runs. Library callers (and specific paths
|
|
25
|
+
* such as `eval gold compare --seed`) may pass an explicit `seed` to vary the draw; the
|
|
26
|
+
* main `omk eval` deliberately exposes no seed knob — a fixed default also prevents
|
|
27
|
+
* seed-shopping for significance.
|
|
22
28
|
*/
|
|
23
29
|
export interface BootstrapCI {
|
|
24
30
|
/** Lower bound of the CI. */
|
|
@@ -42,6 +48,21 @@ export interface BootstrapDiffCI extends BootstrapCI {
|
|
|
42
48
|
export declare const DEFAULT_BOOTSTRAP_SAMPLES = 1000;
|
|
43
49
|
/** Default significance level; 0.05 → 95% CI. */
|
|
44
50
|
export declare const DEFAULT_BOOTSTRAP_ALPHA = 0.05;
|
|
51
|
+
/**
|
|
52
|
+
* Fixed default bootstrap seed → CIs are **deterministic by default** (omk default-strict:
|
|
53
|
+
* reproducibility affects verdict validity, so it is on by default, not opt-in). The specific
|
|
54
|
+
* value is arbitrary — only that it is **fixed** matters; it is an implementation detail, not a
|
|
55
|
+
* user-facing constant, so unlike DEFAULT_BOOTSTRAP_SAMPLES / α it is neither cited in docs nor
|
|
56
|
+
* guarded by `doc-constants-drift.test.ts`. Callers wanting a different draw pass an explicit `seed`.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DEFAULT_BOOTSTRAP_SEED = 20260616;
|
|
59
|
+
/**
|
|
60
|
+
* Confidence-level label for display: `(1 − α)·100%`. Multiple-comparison (Bonferroni) correction
|
|
61
|
+
* shrinks a pairwise α to α/K and widens the CI accordingly — the label must track α so a corrected
|
|
62
|
+
* (wider) interval is never mislabeled "95%". No alpha (single comparison / classic A-B) ⇒ nominal 95%.
|
|
63
|
+
* Shared single source for the HTML renderer and the `omk eval` CLI verdict so both read one scale.
|
|
64
|
+
*/
|
|
65
|
+
export declare function ciLevelLabel(alpha?: number): string;
|
|
45
66
|
/**
|
|
46
67
|
* Bootstrap confidence interval for the mean of a single sample.
|
|
47
68
|
*
|
|
@@ -68,6 +89,36 @@ export declare function bootstrapMeanCI(scores: number[], alpha?: number, sample
|
|
|
68
89
|
* @returns BootstrapDiffCI with low/high of (B - A) and significant flag.
|
|
69
90
|
*/
|
|
70
91
|
export declare function bootstrapDiffCI(scoresA: number[], scoresB: number[], alpha?: number, samples?: number, seed?: number): BootstrapDiffCI;
|
|
92
|
+
/**
|
|
93
|
+
* Bootstrap CI for the *difference* of two means on **paired** observations — when A and B
|
|
94
|
+
* are two measurements of the **same unit** (e.g. control vs treatment on the same sample,
|
|
95
|
+
* or original vs alternate judge prompt on the same response). Resamples the **pair indices
|
|
96
|
+
* jointly** and averages each pair's `b - a`, so the within-pair correlation is preserved.
|
|
97
|
+
*
|
|
98
|
+
* Why paired (vs `bootstrapDiffCI`'s independent resampling): when A and B move together
|
|
99
|
+
* across units (the usual case — the same sample scored by two variants is positively
|
|
100
|
+
* correlated), much of each group's variance is shared and cancels in the per-pair diff.
|
|
101
|
+
* The independent (unpaired) bootstrap ignores that, over-states the diff's variance, and
|
|
102
|
+
* widens the CI — *conservative*, costing real power. Use paired whenever the design is
|
|
103
|
+
* paired; use `bootstrapDiffCI` only for genuinely independent groups (or where a deliberate
|
|
104
|
+
* conservative bias is wanted). The point estimate is identical (mean of per-pair diffs =
|
|
105
|
+
* difference of paired means); only the CI tightens.
|
|
106
|
+
*
|
|
107
|
+
* `significant` is derived from the **rounded** `low`/`high` (the persisted bounds), so the
|
|
108
|
+
* flag never contradicts what is stored / displayed: a CI that rounds to include 0 reads as
|
|
109
|
+
* not-significant. (Computing it on the unrounded bounds would let the JSON say `low: 0,
|
|
110
|
+
* significant: true` — a self-contradictory `CI=[0, …]` that downstream `computeVerdict` and
|
|
111
|
+
* external consumers cannot reconcile.) Matches `bootstrapDiffCI`.
|
|
112
|
+
*
|
|
113
|
+
* @param pairs Aligned observations; `a` = control/baseline, `b` = treatment. diff = b - a.
|
|
114
|
+
* @param alpha Significance level. Default 0.05.
|
|
115
|
+
* @param samples Bootstrap resamples. Default 1000.
|
|
116
|
+
* @param seed Optional seed (deterministic by default — see module header).
|
|
117
|
+
*/
|
|
118
|
+
export declare function bootstrapPairedDiffCI(pairs: Array<{
|
|
119
|
+
a: number;
|
|
120
|
+
b: number;
|
|
121
|
+
}>, alpha?: number, samples?: number, seed?: number): BootstrapDiffCI;
|
|
71
122
|
/**
|
|
72
123
|
* Generic bootstrap CI for an arbitrary sample-level metric. Used by
|
|
73
124
|
* saturation analysis to get CI on metrics like stddev or
|
|
@@ -17,8 +17,14 @@
|
|
|
17
17
|
* - bootstrapDiffCI: CI for the difference (B - A); 0 outside the CI = significant
|
|
18
18
|
* - bootstrapWithMetric: generic interface so saturation analysis can reuse
|
|
19
19
|
*
|
|
20
|
-
* Reproducibility:
|
|
21
|
-
*
|
|
20
|
+
* Reproducibility: CIs are **deterministic by default** — when no `seed` is passed,
|
|
21
|
+
* a fixed `DEFAULT_BOOTSTRAP_SEED` is used, so the same eval run twice yields
|
|
22
|
+
* byte-identical CIs (and a stable verdict near the significance boundary). This is
|
|
23
|
+
* a measurement-validity requirement: an unseeded `Math.random()` would let the
|
|
24
|
+
* `significant` flag flip between identical runs. Library callers (and specific paths
|
|
25
|
+
* such as `eval gold compare --seed`) may pass an explicit `seed` to vary the draw; the
|
|
26
|
+
* main `omk eval` deliberately exposes no seed knob — a fixed default also prevents
|
|
27
|
+
* seed-shopping for significance.
|
|
22
28
|
*/
|
|
23
29
|
/**
|
|
24
30
|
* Default number of bootstrap resamples. Every eval path uses this unless
|
|
@@ -28,6 +34,24 @@
|
|
|
28
34
|
export const DEFAULT_BOOTSTRAP_SAMPLES = 1000;
|
|
29
35
|
/** Default significance level; 0.05 → 95% CI. */
|
|
30
36
|
export const DEFAULT_BOOTSTRAP_ALPHA = 0.05;
|
|
37
|
+
/**
|
|
38
|
+
* Fixed default bootstrap seed → CIs are **deterministic by default** (omk default-strict:
|
|
39
|
+
* reproducibility affects verdict validity, so it is on by default, not opt-in). The specific
|
|
40
|
+
* value is arbitrary — only that it is **fixed** matters; it is an implementation detail, not a
|
|
41
|
+
* user-facing constant, so unlike DEFAULT_BOOTSTRAP_SAMPLES / α it is neither cited in docs nor
|
|
42
|
+
* guarded by `doc-constants-drift.test.ts`. Callers wanting a different draw pass an explicit `seed`.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_BOOTSTRAP_SEED = 20260616;
|
|
45
|
+
/**
|
|
46
|
+
* Confidence-level label for display: `(1 − α)·100%`. Multiple-comparison (Bonferroni) correction
|
|
47
|
+
* shrinks a pairwise α to α/K and widens the CI accordingly — the label must track α so a corrected
|
|
48
|
+
* (wider) interval is never mislabeled "95%". No alpha (single comparison / classic A-B) ⇒ nominal 95%.
|
|
49
|
+
* Shared single source for the HTML renderer and the `omk eval` CLI verdict so both read one scale.
|
|
50
|
+
*/
|
|
51
|
+
export function ciLevelLabel(alpha) {
|
|
52
|
+
const pct = (1 - (alpha ?? DEFAULT_BOOTSTRAP_ALPHA)) * 100;
|
|
53
|
+
return `${Number.isInteger(pct) ? pct : Number(pct.toFixed(1))}%`;
|
|
54
|
+
}
|
|
31
55
|
/** Mulberry32 PRNG — seedable, deterministic for tests. */
|
|
32
56
|
function mulberry32(seed) {
|
|
33
57
|
let s = seed >>> 0;
|
|
@@ -40,9 +64,9 @@ function mulberry32(seed) {
|
|
|
40
64
|
};
|
|
41
65
|
}
|
|
42
66
|
function makeRng(seed) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return mulberry32(seed);
|
|
67
|
+
// 默认确定性:无显式 seed 时退 DEFAULT_BOOTSTRAP_SEED(而非 Math.random)——否则同一 eval 两跑会得到
|
|
68
|
+
// 不同 CI,临界点 significant 翻转 → verdict 不可复现。见模块头 Reproducibility。
|
|
69
|
+
return mulberry32(seed ?? DEFAULT_BOOTSTRAP_SEED);
|
|
46
70
|
}
|
|
47
71
|
/** Sample n indices with replacement from [0, length) using the given PRNG. */
|
|
48
72
|
function resampleIndices(length, n, rng) {
|
|
@@ -150,6 +174,59 @@ export function bootstrapDiffCI(scoresA, scoresB, alpha = DEFAULT_BOOTSTRAP_ALPH
|
|
|
150
174
|
significant: !(low <= 0 && 0 <= high),
|
|
151
175
|
};
|
|
152
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Bootstrap CI for the *difference* of two means on **paired** observations — when A and B
|
|
179
|
+
* are two measurements of the **same unit** (e.g. control vs treatment on the same sample,
|
|
180
|
+
* or original vs alternate judge prompt on the same response). Resamples the **pair indices
|
|
181
|
+
* jointly** and averages each pair's `b - a`, so the within-pair correlation is preserved.
|
|
182
|
+
*
|
|
183
|
+
* Why paired (vs `bootstrapDiffCI`'s independent resampling): when A and B move together
|
|
184
|
+
* across units (the usual case — the same sample scored by two variants is positively
|
|
185
|
+
* correlated), much of each group's variance is shared and cancels in the per-pair diff.
|
|
186
|
+
* The independent (unpaired) bootstrap ignores that, over-states the diff's variance, and
|
|
187
|
+
* widens the CI — *conservative*, costing real power. Use paired whenever the design is
|
|
188
|
+
* paired; use `bootstrapDiffCI` only for genuinely independent groups (or where a deliberate
|
|
189
|
+
* conservative bias is wanted). The point estimate is identical (mean of per-pair diffs =
|
|
190
|
+
* difference of paired means); only the CI tightens.
|
|
191
|
+
*
|
|
192
|
+
* `significant` is derived from the **rounded** `low`/`high` (the persisted bounds), so the
|
|
193
|
+
* flag never contradicts what is stored / displayed: a CI that rounds to include 0 reads as
|
|
194
|
+
* not-significant. (Computing it on the unrounded bounds would let the JSON say `low: 0,
|
|
195
|
+
* significant: true` — a self-contradictory `CI=[0, …]` that downstream `computeVerdict` and
|
|
196
|
+
* external consumers cannot reconcile.) Matches `bootstrapDiffCI`.
|
|
197
|
+
*
|
|
198
|
+
* @param pairs Aligned observations; `a` = control/baseline, `b` = treatment. diff = b - a.
|
|
199
|
+
* @param alpha Significance level. Default 0.05.
|
|
200
|
+
* @param samples Bootstrap resamples. Default 1000.
|
|
201
|
+
* @param seed Optional seed (deterministic by default — see module header).
|
|
202
|
+
*/
|
|
203
|
+
export function bootstrapPairedDiffCI(pairs, alpha = DEFAULT_BOOTSTRAP_ALPHA, samples = DEFAULT_BOOTSTRAP_SAMPLES, seed) {
|
|
204
|
+
if (pairs.length === 0) {
|
|
205
|
+
return { low: 0, high: 0, estimate: 0, samples: 0, significant: false };
|
|
206
|
+
}
|
|
207
|
+
const n = pairs.length;
|
|
208
|
+
const diffs = pairs.map((p) => p.b - p.a);
|
|
209
|
+
const rng = makeRng(seed);
|
|
210
|
+
const resampleDiffMeans = new Array(samples);
|
|
211
|
+
for (let s = 0; s < samples; s++) {
|
|
212
|
+
const idx = resampleIndices(n, n, rng);
|
|
213
|
+
let sum = 0;
|
|
214
|
+
for (const i of idx)
|
|
215
|
+
sum += diffs[i];
|
|
216
|
+
resampleDiffMeans[s] = sum / n;
|
|
217
|
+
}
|
|
218
|
+
resampleDiffMeans.sort((a, b) => a - b);
|
|
219
|
+
const low = round4(sortedQuantile(resampleDiffMeans, alpha / 2));
|
|
220
|
+
const high = round4(sortedQuantile(resampleDiffMeans, 1 - alpha / 2));
|
|
221
|
+
return {
|
|
222
|
+
low,
|
|
223
|
+
high,
|
|
224
|
+
estimate: round4(mean(diffs)),
|
|
225
|
+
samples,
|
|
226
|
+
// significant 与持久化的(舍入)边界一致 —— 见函数头:绝不出现「low:0 但 significant:true」自相矛盾。
|
|
227
|
+
significant: !(low <= 0 && 0 <= high),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
153
230
|
/**
|
|
154
231
|
* Generic bootstrap CI for an arbitrary sample-level metric. Used by
|
|
155
232
|
* saturation analysis to get CI on metrics like stddev or
|
|
@@ -8,7 +8,7 @@ import { indexReportWrite } from './artifact-index.js';
|
|
|
8
8
|
import { buildVariantSummary } from './schema.js';
|
|
9
9
|
import { buildVariantConfig, resolveExecutionStrategy } from './execution-strategy.js';
|
|
10
10
|
import { getJudgePromptHash } from '../grading/judge.js';
|
|
11
|
-
import { bootstrapMeanCI,
|
|
11
|
+
import { bootstrapMeanCI, bootstrapPairedDiffCI, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES, } from './bootstrap.js';
|
|
12
12
|
import { getExecutorRuntimeFingerprint } from '../executors/runtime-fingerprint.js';
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
function findPackageJson(startDir) {
|
|
@@ -121,6 +121,9 @@ export function aggregateReport({ runId, variants, model, judgeModel, noJudge, e
|
|
|
121
121
|
const bootstrapSamples = request?.bootstrapSamples ?? DEFAULT_BOOTSTRAP_SAMPLES;
|
|
122
122
|
let pairComparisons;
|
|
123
123
|
if (bootstrapEnabled) {
|
|
124
|
+
// 不变量(见 grading/layered-scores.ts):composite 在至少一层可测时恒 ≥ 1,`compositeScore === 0`
|
|
125
|
+
// 当且仅当该样本**无任何可测层**(真·缺测,如纯评委样本且评委失败)。故 `> 0` 过滤精确剔除非测量、
|
|
126
|
+
// 绝不丢"低分内容"(评委失败已在上游当缺测,不会以 0 进 composite)。下同(control / treatment)。
|
|
124
127
|
for (const variant of variants) {
|
|
125
128
|
const entries = Object.values(results).map((r) => r[variant]).filter(Boolean);
|
|
126
129
|
const compositeScores = entries
|
|
@@ -135,23 +138,42 @@ export function aggregateReport({ runId, variants, model, judgeModel, noJudge, e
|
|
|
135
138
|
if (variants.length >= 2) {
|
|
136
139
|
pairComparisons = [];
|
|
137
140
|
const controlName = variants[0];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
const sampleRecords = Object.values(results);
|
|
142
|
+
// **配对** diff CI:A/B 是同一批 sample 分别过 control / treatment(配对设计),按 sample 对齐 ——
|
|
143
|
+
// 同一 sample 上 control 与 treatment 都可测(composite > 0,见上不变量)才入对。同一 sample 两 variant
|
|
144
|
+
// 的分数正相关,配对 bootstrap 据此抵消共有方差、收紧 diff CI、更有功效;旧的独立(非配对)重采样高估
|
|
145
|
+
// 方差、CI 偏宽、保守失功效(见 bootstrapPairedDiffCI)。点估计不变,只收紧 CI。
|
|
146
|
+
// 先收齐每个 treatment 的配对数据;只有 ≥2 对(否则无 CI 可算)才算一个真正被检验的比较。
|
|
147
|
+
const eligible = [];
|
|
142
148
|
for (let i = 1; i < variants.length; i++) {
|
|
143
149
|
const treatmentName = variants[i];
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
diffBootstrapCI: bootstrapDiffCI(controlScores, treatmentScores, DEFAULT_BOOTSTRAP_ALPHA, bootstrapSamples),
|
|
153
|
-
});
|
|
150
|
+
const pairs = [];
|
|
151
|
+
for (const r of sampleRecords) {
|
|
152
|
+
const c = r[controlName];
|
|
153
|
+
const t = r[treatmentName];
|
|
154
|
+
const a = c && typeof c.compositeScore === 'number' && c.compositeScore > 0 ? c.compositeScore : undefined;
|
|
155
|
+
const b = t && typeof t.compositeScore === 'number' && t.compositeScore > 0 ? t.compositeScore : undefined;
|
|
156
|
+
if (a !== undefined && b !== undefined)
|
|
157
|
+
pairs.push({ a, b });
|
|
154
158
|
}
|
|
159
|
+
if (pairs.length >= 2)
|
|
160
|
+
eligible.push({ treatment: treatmentName, pairs });
|
|
161
|
+
}
|
|
162
|
+
// 多重比较(Bonferroni)校正:同时检验 K 个 treatment-vs-control 假设时,family-wise 假阳性随 K 膨胀
|
|
163
|
+
// (computeVerdict 的 worst-case roll-up 取最差 —— 任一对假阳即拉高总判定)。每对 CI 用 α/K(K = 实际
|
|
164
|
+
// 产出 CI 的比较数)把 family-wise error 压回名义 α。K=1(单 treatment / 经典 A-B)即 α 不变、与历史单对
|
|
165
|
+
// 口径逐字节一致,此时不写 alpha 字段(渲染按名义 95% CI,既有报告 / 快照不动)。CI 与 significant 同在 α/K
|
|
166
|
+
// 下算,二者自洽(绝不出现 CI 含 0 却 significant 的矛盾)。注:K 很大时 α/K 落到极端分位,1000 重采样的
|
|
167
|
+
// 尾部分位偏粗,大 K 慎读 —— 不在本 PR 提采样数。
|
|
168
|
+
const familySize = eligible.length;
|
|
169
|
+
const perComparisonAlpha = familySize >= 1 ? DEFAULT_BOOTSTRAP_ALPHA / familySize : DEFAULT_BOOTSTRAP_ALPHA;
|
|
170
|
+
for (const { treatment, pairs } of eligible) {
|
|
171
|
+
pairComparisons.push({
|
|
172
|
+
control: controlName,
|
|
173
|
+
treatment,
|
|
174
|
+
diffBootstrapCI: bootstrapPairedDiffCI(pairs, perComparisonAlpha, bootstrapSamples),
|
|
175
|
+
...(familySize >= 2 ? { alpha: perComparisonAlpha } : {}),
|
|
176
|
+
});
|
|
155
177
|
}
|
|
156
178
|
}
|
|
157
179
|
}
|
package/dist/eval-core/schema.js
CHANGED
|
@@ -88,7 +88,8 @@ export function buildVariantResult(execResult, gradeResult, options) {
|
|
|
88
88
|
layeredScores.factScore = assertionFact != null
|
|
89
89
|
? Number(((assertionFact + hardScore) / 2).toFixed(2))
|
|
90
90
|
: hardScore;
|
|
91
|
-
// Recompute composite from updated layers (
|
|
91
|
+
// Recompute composite from updated layers. 仅过滤 null/undefined(缺测层);评委失败时 judgeScore
|
|
92
|
+
// 本就是 undefined(见 grading 的 score=0 修复:评委失败=缺测、不以 0 进层),不存在「0 分」要保留。
|
|
92
93
|
const scores = [layeredScores.factScore, layeredScores.behaviorScore, layeredScores.judgeScore].filter((s) => s != null);
|
|
93
94
|
compositeScore = scores.length > 0 ? Number((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2)) : compositeScore;
|
|
94
95
|
}
|
|
@@ -204,8 +205,9 @@ export function buildVariantSummary(entries) {
|
|
|
204
205
|
};
|
|
205
206
|
})(),
|
|
206
207
|
...(() => {
|
|
207
|
-
//
|
|
208
|
-
//
|
|
208
|
+
// 各层分量为 null/undefined = 该层缺测(无对应断言 / 未配 judge / 评委失败)。评委失败已在 grading 层
|
|
209
|
+
// 当缺测、judgeScore 留 undefined(见 score=0 修复),不存在「0 分内容」要保留;评委有效分恒 1-5。
|
|
210
|
+
// filter != null 即精确剔除缺测层。
|
|
209
211
|
const factScores = ok.map((e) => e.layeredScores?.factScore).filter((s) => s != null);
|
|
210
212
|
const behaviorScores = ok.map((e) => e.layeredScores?.behaviorScore).filter((s) => s != null);
|
|
211
213
|
const judgeScores = ok.map((e) => e.layeredScores?.judgeScore).filter((s) => s != null);
|