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.
Files changed (89) hide show
  1. package/README.md +1 -2
  2. package/README.zh.md +1 -2
  3. package/dist/analysis/report-diagnostics.js +27 -0
  4. package/dist/assets/agent-skills/omk/references/commands.md +2 -2
  5. package/dist/authoring/evolver.js +9 -5
  6. package/dist/authoring/generator.js +3 -2
  7. package/dist/cli/commands/eval/index.d.ts +0 -1
  8. package/dist/cli/commands/eval/index.js +10 -10
  9. package/dist/cli/commands/list.d.ts +4 -1
  10. package/dist/cli/commands/list.js +12 -4
  11. package/dist/cli/commands/observe/index.d.ts +9 -0
  12. package/dist/cli/commands/observe/index.js +74 -1
  13. package/dist/cli/lib/cmd-flags.d.ts +0 -1
  14. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  15. package/dist/cli/lib/i18n-dict/common.js +8 -0
  16. package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
  17. package/dist/cli/lib/i18n-dict/list.js +4 -0
  18. package/dist/cli/lib/i18n-dict/run.js +2 -2
  19. package/dist/cli/lib/parse-run-config.d.ts +0 -1
  20. package/dist/cli/lib/parse-run-config.js +0 -2
  21. package/dist/cli/lib/record-evolve-outcome.js +19 -10
  22. package/dist/eval-core/bootstrap.d.ts +53 -2
  23. package/dist/eval-core/bootstrap.js +82 -5
  24. package/dist/eval-core/evaluation-job.d.ts +1 -2
  25. package/dist/eval-core/evaluation-job.js +1 -2
  26. package/dist/eval-core/evaluation-reporting.d.ts +0 -1
  27. package/dist/eval-core/evaluation-reporting.js +39 -53
  28. package/dist/eval-core/execution-strategy.js +3 -2
  29. package/dist/eval-core/judge-independence.d.ts +28 -0
  30. package/dist/eval-core/judge-independence.js +29 -0
  31. package/dist/eval-core/schema.js +5 -3
  32. package/dist/eval-core/verdict.d.ts +39 -1
  33. package/dist/eval-core/verdict.js +113 -25
  34. package/dist/eval-workflows/batch-evaluation-workflow.d.ts +1 -1
  35. package/dist/eval-workflows/batch-evaluation-workflow.js +1 -2
  36. package/dist/eval-workflows/evaluation-pipeline/report-finalize.d.ts +1 -5
  37. package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +1 -8
  38. package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +1 -2
  39. package/dist/eval-workflows/evaluation-pipeline/run-state.js +1 -2
  40. package/dist/eval-workflows/evaluation-pipeline.d.ts +1 -2
  41. package/dist/eval-workflows/evaluation-pipeline.js +1 -3
  42. package/dist/eval-workflows/run-evaluation.d.ts +2 -3
  43. package/dist/eval-workflows/run-evaluation.js +1 -2
  44. package/dist/executors/claude-cli.js +5 -6
  45. package/dist/executors/claude-sdk.d.ts +5 -2
  46. package/dist/executors/claude-sdk.js +13 -8
  47. package/dist/executors/codex-cli.js +3 -4
  48. package/dist/executors/shared.d.ts +2 -0
  49. package/dist/executors/shared.js +15 -0
  50. package/dist/grading/assertions.js +34 -122
  51. package/dist/grading/gold-cli.js +1 -1
  52. package/dist/grading/human-gold.d.ts +5 -3
  53. package/dist/grading/human-gold.js +5 -3
  54. package/dist/grading/index.d.ts +4 -4
  55. package/dist/grading/index.js +6 -1
  56. package/dist/grading/judge.d.ts +6 -14
  57. package/dist/grading/judge.js +5 -88
  58. package/dist/grading/layered-scores.d.ts +15 -0
  59. package/dist/grading/layered-scores.js +64 -38
  60. package/dist/inputs/eval-config.js +6 -2
  61. package/dist/managed/evidence.js +1 -2
  62. package/dist/managed/index.d.ts +1 -0
  63. package/dist/managed/index.js +1 -0
  64. package/dist/managed/list-view.d.ts +9 -1
  65. package/dist/managed/list-view.js +7 -1
  66. package/dist/managed/observe-feedback.d.ts +60 -0
  67. package/dist/managed/observe-feedback.js +49 -0
  68. package/dist/managed/store.d.ts +28 -1
  69. package/dist/managed/store.js +84 -2
  70. package/dist/managed/version-scores.js +1 -1
  71. package/dist/observability/skill-health-analyzer.d.ts +5 -0
  72. package/dist/observability/skill-health-analyzer.js +3 -2
  73. package/dist/renderer/html-renderer.js +0 -9
  74. package/dist/renderer/layout.js +4 -4
  75. package/dist/renderer/managed-history-renderer.js +80 -3
  76. package/dist/renderer/summary.js +46 -23
  77. package/dist/shared/llm-prompts/debias-instructions.d.ts +4 -0
  78. package/dist/shared/llm-prompts/debias-instructions.js +44 -0
  79. package/dist/shared/llm-prompts/judge-prompts.d.ts +30 -0
  80. package/dist/shared/llm-prompts/judge-prompts.js +205 -0
  81. package/dist/shared/llm-prompts/registry.d.ts +27 -0
  82. package/dist/shared/llm-prompts/registry.js +69 -0
  83. package/dist/types/eval.d.ts +8 -8
  84. package/dist/types/judge.d.ts +9 -1
  85. package/dist/types/managed.d.ts +33 -0
  86. package/dist/types/report.d.ts +11 -3
  87. package/package.json +1 -1
  88. package/dist/grading/debias-validate.d.ts +0 -83
  89. 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
- // bootstrapDiffCI resamples the two arrays independently (conservative not a paired
873
- // bootstrap). Under-powered decision sets degrade to the legacy point-estimate accept
874
- // (note: that path compares the prior-round best scalar, not this fresh re-eval) and
875
- // flag `gate.underpowered`.
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(默认 3.5)。**fact 层的本职是测 deterministic 端点,不是测轨迹**。
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(默认 3.5),fact 条目少之后单条
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
- : 3.5;
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({ zh: 'verdict 阈值,默认 3.5', en: 'Verdict threshold, default 3.5' }),
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 全角字符按 2 列计宽,使含中文表头的列也能对齐。 */
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 全角字符按 2 列计宽,使含中文表头的列也能对齐。 */
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 += /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/.test(ch) ? 2 : 1;
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
- !r.reachable ? `${r.state} ?` : r.state === 'stale' ? 'stale ⚠️' : r.state === 'promoted' ? 'promoted ✓' : r.state,
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
- if (hasDrift || hasUnreachable || hasPromoted)
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
  }
@@ -96,7 +96,6 @@ export interface EvalFlags {
96
96
  'no-strict-baseline'?: boolean;
97
97
  effort?: string;
98
98
  'no-diagnostic'?: boolean;
99
- blind?: boolean;
100
99
  repeat?: string;
101
100
  'judge-repeat'?: string;
102
101
  bootstrap?: boolean;
@@ -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 已生效: judge prompt 退回 v2-cot, 与 < v0.21 报告 hash 一致。\n',
56
- en: 'ℹ --no-debias-length is active: judge prompt reverts to v2-cot, matching < v0.21 report hashes.\n',
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',
@@ -42,7 +42,6 @@ export interface RunConfig {
42
42
  lang: 'zh' | 'en' | undefined;
43
43
  mcpConfig: string | undefined;
44
44
  verbose: boolean | undefined;
45
- blind?: boolean | undefined;
46
45
  retry?: number;
47
46
  resume?: string;
48
47
  layeredStats?: boolean;
@@ -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 { bootstrapDiffCI, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES } from '../../eval-core/bootstrap.js';
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 → bootstrapDiffCI → computeVerdict,与 `omk eval --bootstrap` 同口径、同 α / 重采样数。
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
- const scoresOf = (v) => report.results
15
- .map((r) => r.variants[v])
16
- .filter((e) => !!e && typeof e.compositeScore === 'number' && e.compositeScore > 0)
17
- .map((e) => e.compositeScore);
18
- const ctrl = scoresOf(baseline);
19
- const treat = scoresOf(winnerVariant);
20
- const pairComparisons = ctrl.length >= 2 && treat.length >= 2
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: bootstrapDiffCI(ctrl, treat, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES),
33
+ diffBootstrapCI: bootstrapPairedDiffCI(pairs, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES),
25
34
  }]
26
35
  : undefined;
27
36
  const slice = {