oh-my-knowledge 0.39.0 → 0.41.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 +1 -2
- package/README.zh.md +1 -2
- package/dist/analysis/report-diagnostics.js +27 -0
- package/dist/assets/agent-skills/omk/references/commands.md +2 -2
- package/dist/authoring/evolver.js +9 -5
- package/dist/authoring/generator.js +3 -2
- package/dist/cli/commands/eval/index.d.ts +0 -1
- package/dist/cli/commands/eval/index.js +10 -10
- 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/cmd-flags.d.ts +0 -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/i18n-dict/run.js +2 -2
- package/dist/cli/lib/parse-run-config.d.ts +0 -1
- package/dist/cli/lib/parse-run-config.js +0 -2
- 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-job.d.ts +1 -2
- package/dist/eval-core/evaluation-job.js +1 -2
- package/dist/eval-core/evaluation-reporting.d.ts +0 -1
- package/dist/eval-core/evaluation-reporting.js +39 -53
- package/dist/eval-core/execution-strategy.js +3 -2
- package/dist/eval-core/judge-independence.d.ts +28 -0
- package/dist/eval-core/judge-independence.js +29 -0
- package/dist/eval-core/schema.js +5 -3
- package/dist/eval-core/verdict.d.ts +39 -1
- package/dist/eval-core/verdict.js +113 -25
- package/dist/eval-workflows/batch-evaluation-workflow.d.ts +1 -1
- package/dist/eval-workflows/batch-evaluation-workflow.js +1 -2
- package/dist/eval-workflows/evaluation-pipeline/report-finalize.d.ts +1 -5
- package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +1 -8
- package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +1 -2
- package/dist/eval-workflows/evaluation-pipeline/run-state.js +1 -2
- package/dist/eval-workflows/evaluation-pipeline.d.ts +1 -2
- package/dist/eval-workflows/evaluation-pipeline.js +1 -3
- package/dist/eval-workflows/run-evaluation.d.ts +2 -3
- package/dist/eval-workflows/run-evaluation.js +1 -2
- package/dist/executors/claude-cli.js +5 -6
- package/dist/executors/claude-sdk.d.ts +5 -2
- package/dist/executors/claude-sdk.js +13 -8
- package/dist/executors/codex-cli.js +3 -4
- package/dist/executors/shared.d.ts +2 -0
- package/dist/executors/shared.js +15 -0
- package/dist/grading/assertions.js +34 -122
- package/dist/grading/gold-cli.js +1 -1
- package/dist/grading/human-gold.d.ts +5 -3
- package/dist/grading/human-gold.js +5 -3
- package/dist/grading/index.d.ts +4 -4
- package/dist/grading/index.js +6 -1
- package/dist/grading/judge.d.ts +6 -14
- package/dist/grading/judge.js +5 -88
- package/dist/grading/layered-scores.d.ts +15 -0
- package/dist/grading/layered-scores.js +64 -38
- package/dist/inputs/eval-config.js +6 -2
- package/dist/managed/evidence.js +1 -2
- 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/managed/version-scores.js +1 -1
- package/dist/observability/skill-health-analyzer.d.ts +5 -0
- package/dist/observability/skill-health-analyzer.js +3 -2
- package/dist/renderer/html-renderer.js +0 -9
- package/dist/renderer/layout.js +4 -4
- package/dist/renderer/managed-history-renderer.js +80 -3
- package/dist/renderer/summary.js +46 -23
- package/dist/shared/llm-prompts/debias-instructions.d.ts +4 -0
- package/dist/shared/llm-prompts/debias-instructions.js +44 -0
- package/dist/shared/llm-prompts/judge-prompts.d.ts +30 -0
- package/dist/shared/llm-prompts/judge-prompts.js +205 -0
- package/dist/shared/llm-prompts/registry.d.ts +27 -0
- package/dist/shared/llm-prompts/registry.js +69 -0
- package/dist/types/eval.d.ts +8 -8
- package/dist/types/judge.d.ts +9 -1
- package/dist/types/managed.d.ts +33 -0
- package/dist/types/report.d.ts +11 -3
- package/package.json +1 -1
- package/dist/grading/debias-validate.d.ts +0 -83
- package/dist/grading/debias-validate.js +0 -172
package/README.md
CHANGED
|
@@ -105,8 +105,7 @@ RAG-specific evals: see RAGAS (separate niche, complementary to omk). Full compa
|
|
|
105
105
|
| **Git & remote sources** | install / eval from a local git ref or a remote git URL (`--git-url`); directory-skills run in a content-addressed **isolated copy** so `references/` assets are real measured input, not just `SKILL.md` |
|
|
106
106
|
| **Evidence-gated management** | `omk install` registers a managed record; `omk eval` auto-writes evidence bound by content fingerprint, moving a skill `installed → measurable`; `omk list` surfaces each managed skill's status (installed / measurable / promoted / stale); `omk promote` accepts a version once its evidence passes the gate (default PROGRESS only); `omk rollback` revokes that acceptance, returning the skill to `measurable`. [spec →](docs/specs/evidence-gated-management.md) |
|
|
107
107
|
| **Sample design science** | sample schema with `capability` / `difficulty` / `construct` / `provenance` metadata (HF Dataset Cards style); studio surfaces coverage breakdown plus `rubric_clarity_low` / `capability_thin` flags. [docs/specs/sample-design-spec.md](docs/specs/sample-design-spec.md) |
|
|
108
|
-
| **Multi-judge ensemble** | `--judge-models claude:opus,openai:gpt-4o` cross-vendor scoring + agreement metrics |
|
|
109
|
-
| **Blind A/B** | `--blind` hides variant names; HTML report has a reveal button |
|
|
108
|
+
| **Multi-judge ensemble** | `--judge-models claude:opus,openai-api:gpt-4o` cross-vendor scoring + agreement metrics |
|
|
110
109
|
| **Multi-run variance** | `--repeat N` repeats the eval and computes mean / SD / CI / t-test |
|
|
111
110
|
| **MCP URL fetching** | pull content from private-doc URLs via an MCP server (SSO-protected knowledge bases, etc.) |
|
|
112
111
|
| **Auto analysis** | detects low-discrimination assertions, flat scores, all-pass / all-fail, expensive samples |
|
package/README.zh.md
CHANGED
|
@@ -105,8 +105,7 @@ RAG 专项评测请看 RAGAS(独立 niche,跟 omk 互补)。完整对比
|
|
|
105
105
|
| **Git / 远端源** | install / eval 支持本地 git ref 或远端 git URL(`--git-url`);目录-skill 在内容寻址**隔离副本**里执行,`references/` 资产是真实测量输入,不只是 `SKILL.md` |
|
|
106
106
|
| **证据门控管理** | `omk install` 登记受管记录;`omk eval` 按内容指纹自动写入证据,把 skill 从 `installed` 推到 `measurable`;`omk list` 查看各受管 skill 的状态(installed / measurable / promoted / stale);`omk promote` 在证据过门禁(默认仅 PROGRESS)后把该版本接受为当前版本;`omk rollback` 撤销这次接受,让 skill 回到 `measurable`。[规范 →](docs/zh/specs/evidence-gated-management.md) |
|
|
107
107
|
| **用例设计科学性** | Sample schema 加 `capability` / `difficulty` / `construct` / `provenance` 元数据字段(HF Dataset Cards 风),studio 输出 coverage 分桶 + `rubric_clarity_low` / `capability_thin` issue。[docs/zh/specs/sample-design-spec.md](docs/zh/specs/sample-design-spec.md) |
|
|
108
|
-
| **多评委 ensemble** | `--judge-models claude:opus,openai:gpt-4o` 跨厂商评分 + agreement 度量 |
|
|
109
|
-
| **盲测 A/B** | `--blind` 隐藏变体名称,HTML 报告有揭晓按钮 |
|
|
108
|
+
| **多评委 ensemble** | `--judge-models claude:opus,openai-api:gpt-4o` 跨厂商评分 + agreement 度量 |
|
|
110
109
|
| **多轮方差分析** | `--repeat N` 重复 N 次,计算均值/标准差/置信区间/t 检验 |
|
|
111
110
|
| **MCP URL 获取** | 通过 MCP Server 获取私有文档 URL 内容(SSO 保护的知识库等) |
|
|
112
111
|
| **自动分析** | 检测低区分度断言、均匀分数、全通过/全失败、高成本用例 |
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Auto-analysis: detect patterns and generate insights from evaluation results.
|
|
3
3
|
*/
|
|
4
4
|
import { normalizeCapability } from './sample-diagnostics.js';
|
|
5
|
+
import { analyzeJudgeIndependence } from '../eval-core/judge-independence.js';
|
|
5
6
|
/**
|
|
6
7
|
* Analyze an evaluation report and produce structured insights.
|
|
7
8
|
*/
|
|
@@ -15,6 +16,10 @@ export function analyzeResults(report, opts = {}) {
|
|
|
15
16
|
const sampleQuality = opts.samples
|
|
16
17
|
? buildSampleQualityAggregate(opts.samples)
|
|
17
18
|
: undefined;
|
|
19
|
+
// 评委独立性对**单变体**报告同样适用(SOLO 只有绝对分、没有 A/B 差值去抵消自我偏好,
|
|
20
|
+
// 反而更该报),故放在 variants<2 早退之前;仅 results 为空(dry-run)时不评估。
|
|
21
|
+
if (results.length > 0)
|
|
22
|
+
detectJudgeIndependence(report, insights);
|
|
18
23
|
if (results.length === 0 || variants.length < 2) {
|
|
19
24
|
return { insights, ...(sampleQuality && { sampleQuality }) };
|
|
20
25
|
}
|
|
@@ -38,11 +43,33 @@ export function analyzeResults(report, opts = {}) {
|
|
|
38
43
|
detectAgentAssertionDiscrimination(results, variants, insights);
|
|
39
44
|
// 10. Suggest --repeat when score variance is high and no repeat data
|
|
40
45
|
detectNeedRepeat(report, results, variants, insights);
|
|
46
|
+
// 11. Judge independence — 已在 variants<2 早退前调过(单变体也适用),此处不重复。
|
|
41
47
|
return {
|
|
42
48
|
insights,
|
|
43
49
|
...(sampleQuality && { sampleQuality }),
|
|
44
50
|
};
|
|
45
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 评委独立性诊断:同厂商评委(自我偏好敞口)/ 单厂商 ensemble(一致性不反驳共有偏置)。
|
|
54
|
+
* 判定走单一来源 `analyzeJudgeIndependence`;挂了 gold 校准则软化为 info。
|
|
55
|
+
*/
|
|
56
|
+
function detectJudgeIndependence(report, insights) {
|
|
57
|
+
const ind = analyzeJudgeIndependence(report);
|
|
58
|
+
if (ind.sameVendorJudge) {
|
|
59
|
+
insights.push({
|
|
60
|
+
type: 'judge_self_preference',
|
|
61
|
+
severity: ind.goldCalibrated ? 'info' : 'warning',
|
|
62
|
+
details: { judgeVendors: ind.judgeVendors, outputVendors: ind.outputVendors, goldCalibrated: ind.goldCalibrated },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (ind.singleVendorEnsemble) {
|
|
66
|
+
insights.push({
|
|
67
|
+
type: 'single_vendor_ensemble',
|
|
68
|
+
severity: ind.goldCalibrated ? 'info' : 'warning',
|
|
69
|
+
details: { judgeVendors: ind.judgeVendors, goldCalibrated: ind.goldCalibrated },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
46
73
|
/**
|
|
47
74
|
* Build sample design science aggregate from sample metadata.
|
|
48
75
|
*
|
|
@@ -67,7 +67,6 @@ omk eval [flags]
|
|
|
67
67
|
**Flags:**
|
|
68
68
|
|
|
69
69
|
- `--batch` `boolean`:batch 模式:baseline vs 每个 skill
|
|
70
|
-
- `--blind` `boolean`:judge blind 模式
|
|
71
70
|
- `--bootstrap` `boolean`:加 bootstrap CI
|
|
72
71
|
- `--bootstrap-samples` `option`:bootstrap 重采样次数,默认 1000
|
|
73
72
|
- `--budget-per-sample-ms` `option`:单 sample 时长上限 ms(必须 > 0,不传则无上限)
|
|
@@ -82,7 +81,7 @@ omk eval [flags]
|
|
|
82
81
|
- `--executor` `option`:执行器:claude / claude-sdk / codex / codex-sdk / openai-api / gemini / 自定义命令(默认 claude)。
|
|
83
82
|
- `--global` `boolean`:报告写全局 ~/.oh-my-knowledge/reports,而非项目 .omk/
|
|
84
83
|
- `--gold-dir` `option`:gold dataset 目录
|
|
85
|
-
- `--judge-models` `option`:评委配置,格式 executor:model[,...],例 claude:haiku 或 claude:opus,openai:gpt-4o(≥ 2 个 = ensemble)。默认 <executor>:haiku。
|
|
84
|
+
- `--judge-models` `option`:评委配置,格式 executor:model[,...],例 claude:haiku 或 claude:opus,openai-api:gpt-4o(≥ 2 个 = ensemble)。默认 <executor>:haiku。
|
|
86
85
|
- `--judge-repeat` `option`:每个 dim 评 N 次
|
|
87
86
|
- `--lang` `option` (默认 `zh`):输出语言 zh|en,优先级 CLI > OMK_LANG env > zh。
|
|
88
87
|
- `--layered-stats` `boolean`:输出分层统计
|
|
@@ -403,6 +402,7 @@ omk observe [sessionsDir] [flags]
|
|
|
403
402
|
|
|
404
403
|
**Flags:**
|
|
405
404
|
|
|
405
|
+
- `--feedback` `boolean`:把生产健康观测反哺已纳管的同名 skill(--no-feedback 关闭)
|
|
406
406
|
- `--from` `option`:起始时间 ISO,优先级高于 --last
|
|
407
407
|
- `--global` `boolean`:写全局 ~/.oh-my-knowledge/observe-health,而非项目 .omk/observe-health
|
|
408
408
|
- `--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);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createExecutor } from '../executors/index.js';
|
|
2
|
+
import { DEFAULT_GATE_THRESHOLD } from '../eval-core/verdict.js';
|
|
2
3
|
/**
|
|
3
4
|
* Generator 默认模型 'opus' (跟 eval 默认对齐)。
|
|
4
5
|
* lean=true 路径会自动追加 `--effort low`,关掉 opus 默认的扩展思考,
|
|
@@ -76,7 +77,7 @@ const SYSTEM_PROMPT = `你是一个评测用例生成器。你的任务是根据
|
|
|
76
77
|
来涨数量)。omk 的评分体系是 layered scoring: **fact 层**(deterministic 字面/工具断言)
|
|
77
78
|
+ **behavior 层**(代价指标如 turn 数 / 工具失败率) + **judge 层**(主观语义评分,从
|
|
78
79
|
sample.rubric 派生维度,judge LLM 看 trace 评 1-5)三层独立计分,verdict 是三层
|
|
79
|
-
独立过 threshold(默认
|
|
80
|
+
独立过 threshold(默认 ${DEFAULT_GATE_THRESHOLD})。**fact 层的本职是测 deterministic 端点,不是测轨迹**。
|
|
80
81
|
|
|
81
82
|
**断言哲学(关键):fact 测结果+里程碑,过程质量交 judge**
|
|
82
83
|
─────────────────────────────────────────────────────────────
|
|
@@ -109,7 +110,7 @@ const SYSTEM_PROMPT = `你是一个评测用例生成器。你的任务是根据
|
|
|
109
110
|
- tools_not_called 反模式断言 0-1 条(禁止接触某禁忌工具,如 tripwire sample)
|
|
110
111
|
- rubric 3-5 个判分维度(细致写明 judge 该看什么),由 sample.rubric 字段承载
|
|
111
112
|
|
|
112
|
-
*测量学背景:* 当前 omk verdict 三层独立 threshold(默认
|
|
113
|
+
*测量学背景:* 当前 omk verdict 三层独立 threshold(默认 ${DEFAULT_GATE_THRESHOLD}),fact 条目少之后单条
|
|
113
114
|
权重大、单次评测方差大,**强烈建议** 评测时带 \`--repeat 2\` 或更大测稳定性(coefficient
|
|
114
115
|
of variation),并参考 bootstrap CI 而非点估计。这是 fact 层稀疏化的代价,换来的是
|
|
115
116
|
fact 信号干净(不被 trajectory 字面噪音污染)。
|
|
@@ -37,7 +37,6 @@ export default class Eval extends BaseCommand {
|
|
|
37
37
|
'no-strict-baseline': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
38
38
|
effort: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
39
39
|
'no-diagnostic': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
40
|
-
blind: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
41
40
|
repeat: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
42
41
|
'judge-repeat': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
43
42
|
bootstrap: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -8,6 +8,7 @@ import { parseRunConfig } from '../../lib/parse-run-config.js';
|
|
|
8
8
|
import { makeOnProgress } from '../../lib/progress.js';
|
|
9
9
|
import { computeRunTally } from '../../lib/run-tally.js';
|
|
10
10
|
import { DEFAULT_BOOTSTRAP_SAMPLES } from '../../../eval-core/bootstrap.js';
|
|
11
|
+
import { DEFAULT_GATE_THRESHOLD } from '../../../eval-core/verdict.js';
|
|
11
12
|
import { EVALUATION_REPORT_SCHEMA_VERSION } from '../../../eval-core/evaluation-reporting.js';
|
|
12
13
|
function isDryRunReport(report) {
|
|
13
14
|
return Boolean(report && typeof report === 'object' && report.dryRun === true);
|
|
@@ -17,9 +18,11 @@ function isDryRunBatchReport(report) {
|
|
|
17
18
|
}
|
|
18
19
|
function verdictOptions(values) {
|
|
19
20
|
const rawThreshold = values.threshold;
|
|
21
|
+
// 不传 --threshold 时返回 undefined,由 computeVerdict 应用 DEFAULT_GATE_THRESHOLD ——
|
|
22
|
+
// 避免在此再硬编码一份 3.5(单一来源在 verdict.ts)。
|
|
20
23
|
const gateThreshold = rawThreshold !== undefined && Number.isFinite(Number(rawThreshold))
|
|
21
24
|
? Number(rawThreshold)
|
|
22
|
-
:
|
|
25
|
+
: undefined;
|
|
23
26
|
const rawTrivial = values['trivial-diff'];
|
|
24
27
|
const triviallySmallDiff = rawTrivial !== undefined && Number.isFinite(Number(rawTrivial))
|
|
25
28
|
? Number(rawTrivial)
|
|
@@ -158,9 +161,6 @@ async function announceSavedReport({ report, filePath, reportsDir, values, lang,
|
|
|
158
161
|
async function runEval(_args, flags, lang) {
|
|
159
162
|
const { values, config, evalConfig } = parseRunConfig({ ...flags });
|
|
160
163
|
const { runEvaluation, runMultiple, runBatchEvaluation } = await import('../../../eval-workflows/run-evaluation.js');
|
|
161
|
-
if (values.blind !== undefined) {
|
|
162
|
-
config.blind = values.blind;
|
|
163
|
-
}
|
|
164
164
|
config.onProgress = makeOnProgress(lang);
|
|
165
165
|
const repeatRaw = values.repeat;
|
|
166
166
|
const parsedRepeat = repeatRaw !== undefined ? Number(repeatRaw) : (evalConfig?.repeat ?? 1);
|
|
@@ -368,8 +368,8 @@ export default class Eval extends BaseCommand {
|
|
|
368
368
|
}),
|
|
369
369
|
'judge-models': Flags.string({
|
|
370
370
|
description: bilingual({
|
|
371
|
-
zh: '评委配置,格式 executor:model[,...],例 claude:haiku 或 claude:opus,openai:gpt-4o(≥ 2 个 = ensemble)。默认 <executor>:haiku。',
|
|
372
|
-
en: 'Judge config: executor:model[,...]. e.g. claude:haiku or claude:opus,openai:gpt-4o (≥ 2 = ensemble). Default <executor>:haiku.',
|
|
371
|
+
zh: '评委配置,格式 executor:model[,...],例 claude:haiku 或 claude:opus,openai-api:gpt-4o(≥ 2 个 = ensemble)。默认 <executor>:haiku。',
|
|
372
|
+
en: 'Judge config: executor:model[,...]. e.g. claude:haiku or claude:opus,openai-api:gpt-4o (≥ 2 = ensemble). Default <executor>:haiku.',
|
|
373
373
|
}),
|
|
374
374
|
}),
|
|
375
375
|
'output-dir': Flags.string({
|
|
@@ -453,9 +453,6 @@ export default class Eval extends BaseCommand {
|
|
|
453
453
|
}),
|
|
454
454
|
}),
|
|
455
455
|
// ── eval-runner extra ──
|
|
456
|
-
blind: Flags.boolean({
|
|
457
|
-
description: bilingual({ zh: 'judge blind 模式', en: 'Blind judge mode' }),
|
|
458
|
-
}),
|
|
459
456
|
repeat: Flags.string({
|
|
460
457
|
description: bilingual({ zh: '每个 sample 重复跑 N 次', en: 'Repeat each sample N times' }),
|
|
461
458
|
parse: integerStringParser('--repeat', { min: 1 }),
|
|
@@ -490,7 +487,10 @@ export default class Eval extends BaseCommand {
|
|
|
490
487
|
parse: numberStringParser('--budget-per-sample-ms', { minExclusive: 0 }),
|
|
491
488
|
}),
|
|
492
489
|
threshold: Flags.string({
|
|
493
|
-
description: bilingual({
|
|
490
|
+
description: bilingual({
|
|
491
|
+
zh: `verdict 阈值,默认 ${DEFAULT_GATE_THRESHOLD}`,
|
|
492
|
+
en: `Verdict threshold, default ${DEFAULT_GATE_THRESHOLD}`,
|
|
493
|
+
}),
|
|
494
494
|
parse: numberStringParser('--threshold'),
|
|
495
495
|
}),
|
|
496
496
|
'trivial-diff': Flags.string({
|
|
@@ -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',
|
|
@@ -52,8 +52,8 @@ export const runDict = {
|
|
|
52
52
|
en: '⚠ --judge-repeat "{value}" is invalid (expected an integer ≥ 1), falling back to 1 judge call\n',
|
|
53
53
|
},
|
|
54
54
|
'cli.run.no_debias_length_active': {
|
|
55
|
-
zh: 'ℹ --no-debias-length
|
|
56
|
-
en: 'ℹ --no-debias-length is active: judge prompt
|
|
55
|
+
zh: 'ℹ --no-debias-length 已生效:judge prompt 去掉长度去偏指令(debias-off 变体),hash 与默认开启时不同。\n',
|
|
56
|
+
en: 'ℹ --no-debias-length is active: the judge prompt drops the length-debias instruction (debias-off variant); its hash differs from the default.\n',
|
|
57
57
|
},
|
|
58
58
|
'cli.run.invalid_bootstrap_samples': {
|
|
59
59
|
zh: '⚠ --bootstrap-samples "{value}" 无效 (期望 ≥ 100 的整数), 已按 1000 执行\n',
|
|
@@ -94,7 +94,6 @@ export function parseRunConfig(values) {
|
|
|
94
94
|
const verbose = values.verbose ?? false;
|
|
95
95
|
const retry = Math.max(0, Number(values.retry ?? 0) || 0);
|
|
96
96
|
const resume = values.resume;
|
|
97
|
-
const blind = values.blind ?? evalConfig?.blind ?? false;
|
|
98
97
|
const layeredStats = values['layered-stats'] ?? false;
|
|
99
98
|
// strict-baseline default true. Reconcile both flag forms with eval.yaml fallback.
|
|
100
99
|
// Priority: --no-strict-baseline > --strict-baseline > eval.yaml strictBaseline > true。
|
|
@@ -144,7 +143,6 @@ export function parseRunConfig(values) {
|
|
|
144
143
|
verbose,
|
|
145
144
|
retry,
|
|
146
145
|
resume,
|
|
147
|
-
blind,
|
|
148
146
|
layeredStats,
|
|
149
147
|
budget: evalConfig?.budget,
|
|
150
148
|
strictBaseline,
|
|
@@ -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 = {
|