oh-my-knowledge 0.28.0 → 0.30.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 (281) hide show
  1. package/README.md +49 -3
  2. package/README.zh.md +49 -3
  3. package/dist/src/analysis/failure-clusterer.js +1 -1
  4. package/dist/src/analysis/failure-clusterer.js.map +1 -1
  5. package/dist/src/analysis/gap-analyzer.d.ts +8 -1
  6. package/dist/src/analysis/gap-analyzer.d.ts.map +1 -1
  7. package/dist/src/analysis/gap-analyzer.js +65 -33
  8. package/dist/src/analysis/gap-analyzer.js.map +1 -1
  9. package/dist/src/analysis/report-diagnostics.js +2 -0
  10. package/dist/src/analysis/report-diagnostics.js.map +1 -1
  11. package/dist/src/analysis/sample-diagnostics.js +14 -14
  12. package/dist/src/analysis/sample-diagnostics.js.map +1 -1
  13. package/dist/src/authoring/evolver.d.ts +7 -1
  14. package/dist/src/authoring/evolver.d.ts.map +1 -1
  15. package/dist/src/authoring/evolver.js +8 -5
  16. package/dist/src/authoring/evolver.js.map +1 -1
  17. package/dist/src/authoring/generator.d.ts +21 -23
  18. package/dist/src/authoring/generator.d.ts.map +1 -1
  19. package/dist/src/authoring/generator.js +621 -40
  20. package/dist/src/authoring/generator.js.map +1 -1
  21. package/dist/src/authoring/sample-fixer.d.ts +45 -0
  22. package/dist/src/authoring/sample-fixer.d.ts.map +1 -0
  23. package/dist/src/authoring/sample-fixer.js +186 -0
  24. package/dist/src/authoring/sample-fixer.js.map +1 -0
  25. package/dist/src/cli/commands/evolve.d.ts.map +1 -1
  26. package/dist/src/cli/commands/evolve.js +17 -0
  27. package/dist/src/cli/commands/evolve.js.map +1 -1
  28. package/dist/src/cli/commands/observe.d.ts.map +1 -1
  29. package/dist/src/cli/commands/observe.js +170 -0
  30. package/dist/src/cli/commands/observe.js.map +1 -1
  31. package/dist/src/cli/commands/registry.d.ts.map +1 -1
  32. package/dist/src/cli/commands/registry.js +9 -1
  33. package/dist/src/cli/commands/registry.js.map +1 -1
  34. package/dist/src/cli/commands/sample.d.ts +13 -0
  35. package/dist/src/cli/commands/sample.d.ts.map +1 -1
  36. package/dist/src/cli/commands/sample.js +307 -13
  37. package/dist/src/cli/commands/sample.js.map +1 -1
  38. package/dist/src/cli/commands/studio.d.ts.map +1 -1
  39. package/dist/src/cli/commands/studio.js +8 -0
  40. package/dist/src/cli/commands/studio.js.map +1 -1
  41. package/dist/src/cli/i18n-dict.d.ts +1 -1
  42. package/dist/src/cli/i18n-dict.d.ts.map +1 -1
  43. package/dist/src/cli/i18n-dict.js +189 -17
  44. package/dist/src/cli/i18n-dict.js.map +1 -1
  45. package/dist/src/cli/parse-run-config.d.ts +14 -2
  46. package/dist/src/cli/parse-run-config.d.ts.map +1 -1
  47. package/dist/src/cli/parse-run-config.js +62 -10
  48. package/dist/src/cli/parse-run-config.js.map +1 -1
  49. package/dist/src/doctor/health/builtin-dimensions.d.ts.map +1 -1
  50. package/dist/src/doctor/health/builtin-dimensions.js +20 -23
  51. package/dist/src/doctor/health/builtin-dimensions.js.map +1 -1
  52. package/dist/src/doctor/health/composer.d.ts.map +1 -1
  53. package/dist/src/doctor/health/composer.js +7 -3
  54. package/dist/src/doctor/health/composer.js.map +1 -1
  55. package/dist/src/doctor/health/dimension-spec.d.ts +1 -0
  56. package/dist/src/doctor/health/dimension-spec.d.ts.map +1 -1
  57. package/dist/src/doctor/health/parser.d.ts.map +1 -1
  58. package/dist/src/doctor/health/parser.js +1 -0
  59. package/dist/src/doctor/health/parser.js.map +1 -1
  60. package/dist/src/doctor/health/prompt-builder.js +25 -17
  61. package/dist/src/doctor/health/prompt-builder.js.map +1 -1
  62. package/dist/src/doctor/html-renderer.d.ts.map +1 -1
  63. package/dist/src/doctor/html-renderer.js +76 -66
  64. package/dist/src/doctor/html-renderer.js.map +1 -1
  65. package/dist/src/doctor/rules.d.ts.map +1 -1
  66. package/dist/src/doctor/rules.js +32 -0
  67. package/dist/src/doctor/rules.js.map +1 -1
  68. package/dist/src/eval-core/cache.d.ts +16 -2
  69. package/dist/src/eval-core/cache.d.ts.map +1 -1
  70. package/dist/src/eval-core/cache.js +86 -15
  71. package/dist/src/eval-core/cache.js.map +1 -1
  72. package/dist/src/eval-core/evaluation-execution.d.ts +11 -1
  73. package/dist/src/eval-core/evaluation-execution.d.ts.map +1 -1
  74. package/dist/src/eval-core/evaluation-execution.js +78 -6
  75. package/dist/src/eval-core/evaluation-execution.js.map +1 -1
  76. package/dist/src/eval-core/evaluation-job.d.ts +2 -1
  77. package/dist/src/eval-core/evaluation-job.d.ts.map +1 -1
  78. package/dist/src/eval-core/evaluation-job.js +2 -1
  79. package/dist/src/eval-core/evaluation-job.js.map +1 -1
  80. package/dist/src/eval-core/evaluation-reporting.d.ts +7 -0
  81. package/dist/src/eval-core/evaluation-reporting.d.ts.map +1 -1
  82. package/dist/src/eval-core/evaluation-reporting.js +19 -2
  83. package/dist/src/eval-core/evaluation-reporting.js.map +1 -1
  84. package/dist/src/eval-core/execution-strategy.d.ts +1 -1
  85. package/dist/src/eval-core/execution-strategy.d.ts.map +1 -1
  86. package/dist/src/eval-core/execution-strategy.js +12 -1
  87. package/dist/src/eval-core/execution-strategy.js.map +1 -1
  88. package/dist/src/eval-core/mock-hook.cjs +203 -0
  89. package/dist/src/eval-core/mocks-runtime.d.ts +96 -0
  90. package/dist/src/eval-core/mocks-runtime.d.ts.map +1 -0
  91. package/dist/src/eval-core/mocks-runtime.js +323 -0
  92. package/dist/src/eval-core/mocks-runtime.js.map +1 -0
  93. package/dist/src/eval-core/schema.d.ts.map +1 -1
  94. package/dist/src/eval-core/schema.js +18 -5
  95. package/dist/src/eval-core/schema.js.map +1 -1
  96. package/dist/src/eval-core/task-planner.d.ts +9 -1
  97. package/dist/src/eval-core/task-planner.d.ts.map +1 -1
  98. package/dist/src/eval-core/task-planner.js +40 -1
  99. package/dist/src/eval-core/task-planner.js.map +1 -1
  100. package/dist/src/eval-workflows/batch-evaluation-workflow.d.ts +5 -1
  101. package/dist/src/eval-workflows/batch-evaluation-workflow.d.ts.map +1 -1
  102. package/dist/src/eval-workflows/batch-evaluation-workflow.js +2 -1
  103. package/dist/src/eval-workflows/batch-evaluation-workflow.js.map +1 -1
  104. package/dist/src/eval-workflows/evaluation-pipeline.d.ts +22 -1
  105. package/dist/src/eval-workflows/evaluation-pipeline.d.ts.map +1 -1
  106. package/dist/src/eval-workflows/evaluation-pipeline.js +39 -12
  107. package/dist/src/eval-workflows/evaluation-pipeline.js.map +1 -1
  108. package/dist/src/eval-workflows/evaluation-preparation.d.ts +5 -0
  109. package/dist/src/eval-workflows/evaluation-preparation.d.ts.map +1 -1
  110. package/dist/src/eval-workflows/evaluation-preparation.js +3 -1
  111. package/dist/src/eval-workflows/evaluation-preparation.js.map +1 -1
  112. package/dist/src/eval-workflows/run-evaluation.d.ts +12 -2
  113. package/dist/src/eval-workflows/run-evaluation.d.ts.map +1 -1
  114. package/dist/src/eval-workflows/run-evaluation.js +17 -5
  115. package/dist/src/eval-workflows/run-evaluation.js.map +1 -1
  116. package/dist/src/executors/claude-cli.d.ts +1 -1
  117. package/dist/src/executors/claude-cli.d.ts.map +1 -1
  118. package/dist/src/executors/claude-cli.js +57 -6
  119. package/dist/src/executors/claude-cli.js.map +1 -1
  120. package/dist/src/executors/claude-sdk-trace.d.ts.map +1 -1
  121. package/dist/src/executors/claude-sdk-trace.js +10 -4
  122. package/dist/src/executors/claude-sdk-trace.js.map +1 -1
  123. package/dist/src/executors/claude-sdk.d.ts +1 -1
  124. package/dist/src/executors/claude-sdk.d.ts.map +1 -1
  125. package/dist/src/executors/claude-sdk.js +25 -2
  126. package/dist/src/executors/claude-sdk.js.map +1 -1
  127. package/dist/src/executors/shared.d.ts +1 -1
  128. package/dist/src/executors/shared.d.ts.map +1 -1
  129. package/dist/src/executors/shared.js +6 -1
  130. package/dist/src/executors/shared.js.map +1 -1
  131. package/dist/src/grading/assertions.d.ts +5 -0
  132. package/dist/src/grading/assertions.d.ts.map +1 -1
  133. package/dist/src/grading/assertions.js +27 -1
  134. package/dist/src/grading/assertions.js.map +1 -1
  135. package/dist/src/grading/debias-validate.js +4 -4
  136. package/dist/src/grading/debias-validate.js.map +1 -1
  137. package/dist/src/grading/diagnostic.d.ts +40 -0
  138. package/dist/src/grading/diagnostic.d.ts.map +1 -0
  139. package/dist/src/grading/diagnostic.js +450 -0
  140. package/dist/src/grading/diagnostic.js.map +1 -0
  141. package/dist/src/grading/gold-cli.js +2 -2
  142. package/dist/src/grading/gold-cli.js.map +1 -1
  143. package/dist/src/grading/index.d.ts +5 -0
  144. package/dist/src/grading/index.d.ts.map +1 -1
  145. package/dist/src/grading/judge.d.ts.map +1 -1
  146. package/dist/src/grading/judge.js +54 -2
  147. package/dist/src/grading/judge.js.map +1 -1
  148. package/dist/src/grading/layered-scores.d.ts.map +1 -1
  149. package/dist/src/grading/layered-scores.js +1 -0
  150. package/dist/src/grading/layered-scores.js.map +1 -1
  151. package/dist/src/inputs/eval-config.js +19 -0
  152. package/dist/src/inputs/eval-config.js.map +1 -1
  153. package/dist/src/inputs/load-samples.d.ts +22 -2
  154. package/dist/src/inputs/load-samples.d.ts.map +1 -1
  155. package/dist/src/inputs/load-samples.js +193 -22
  156. package/dist/src/inputs/load-samples.js.map +1 -1
  157. package/dist/src/inputs/skill-loader.d.ts.map +1 -1
  158. package/dist/src/inputs/skill-loader.js +11 -3
  159. package/dist/src/inputs/skill-loader.js.map +1 -1
  160. package/dist/src/observability/experience.d.ts +260 -0
  161. package/dist/src/observability/experience.d.ts.map +1 -0
  162. package/dist/src/observability/experience.js +1233 -0
  163. package/dist/src/observability/experience.js.map +1 -0
  164. package/dist/src/observability/inbox-view-model.d.ts +27 -0
  165. package/dist/src/observability/inbox-view-model.d.ts.map +1 -0
  166. package/dist/src/observability/inbox-view-model.js +135 -0
  167. package/dist/src/observability/inbox-view-model.js.map +1 -0
  168. package/dist/src/observability/inbox.d.ts +125 -0
  169. package/dist/src/observability/inbox.d.ts.map +1 -0
  170. package/dist/src/observability/inbox.js +741 -0
  171. package/dist/src/observability/inbox.js.map +1 -0
  172. package/dist/src/observability/problem-patterns.d.ts +51 -0
  173. package/dist/src/observability/problem-patterns.d.ts.map +1 -0
  174. package/dist/src/observability/problem-patterns.js +205 -0
  175. package/dist/src/observability/problem-patterns.js.map +1 -0
  176. package/dist/src/observability/review-state.d.ts +54 -0
  177. package/dist/src/observability/review-state.d.ts.map +1 -0
  178. package/dist/src/observability/review-state.js +131 -0
  179. package/dist/src/observability/review-state.js.map +1 -0
  180. package/dist/src/observability/skill-chain-advisories.d.ts +25 -0
  181. package/dist/src/observability/skill-chain-advisories.d.ts.map +1 -0
  182. package/dist/src/observability/skill-chain-advisories.js +69 -0
  183. package/dist/src/observability/skill-chain-advisories.js.map +1 -0
  184. package/dist/src/observability/skill-chain.d.ts +61 -0
  185. package/dist/src/observability/skill-chain.d.ts.map +1 -0
  186. package/dist/src/observability/skill-chain.js +233 -0
  187. package/dist/src/observability/skill-chain.js.map +1 -0
  188. package/dist/src/observability/text-signals.d.ts +7 -0
  189. package/dist/src/observability/text-signals.d.ts.map +1 -0
  190. package/dist/src/observability/text-signals.js +22 -0
  191. package/dist/src/observability/text-signals.js.map +1 -0
  192. package/dist/src/observability/trace-adapter.d.ts +12 -64
  193. package/dist/src/observability/trace-adapter.d.ts.map +1 -1
  194. package/dist/src/observability/trace-adapter.js +9 -355
  195. package/dist/src/observability/trace-adapter.js.map +1 -1
  196. package/dist/src/observability/trace-attribution.d.ts +56 -0
  197. package/dist/src/observability/trace-attribution.d.ts.map +1 -0
  198. package/dist/src/observability/trace-attribution.js +200 -0
  199. package/dist/src/observability/trace-attribution.js.map +1 -0
  200. package/dist/src/observability/trace-segmenter.d.ts +52 -0
  201. package/dist/src/observability/trace-segmenter.d.ts.map +1 -0
  202. package/dist/src/observability/trace-segmenter.js +320 -0
  203. package/dist/src/observability/trace-segmenter.js.map +1 -0
  204. package/dist/src/observability/trace-source.d.ts +99 -0
  205. package/dist/src/observability/trace-source.d.ts.map +1 -0
  206. package/dist/src/observability/trace-source.js +492 -0
  207. package/dist/src/observability/trace-source.js.map +1 -0
  208. package/dist/src/renderer/html-renderer.d.ts.map +1 -1
  209. package/dist/src/renderer/html-renderer.js +324 -218
  210. package/dist/src/renderer/html-renderer.js.map +1 -1
  211. package/dist/src/renderer/layout.d.ts.map +1 -1
  212. package/dist/src/renderer/layout.js +155 -71
  213. package/dist/src/renderer/layout.js.map +1 -1
  214. package/dist/src/renderer/observation-inbox-renderer.d.ts +4 -0
  215. package/dist/src/renderer/observation-inbox-renderer.d.ts.map +1 -0
  216. package/dist/src/renderer/observation-inbox-renderer.js +5376 -0
  217. package/dist/src/renderer/observation-inbox-renderer.js.map +1 -0
  218. package/dist/src/renderer/skill-detail-renderer.d.ts +15 -0
  219. package/dist/src/renderer/skill-detail-renderer.d.ts.map +1 -0
  220. package/dist/src/renderer/skill-detail-renderer.js +1234 -0
  221. package/dist/src/renderer/skill-detail-renderer.js.map +1 -0
  222. package/dist/src/renderer/skill-list-renderer.d.ts +4 -0
  223. package/dist/src/renderer/skill-list-renderer.d.ts.map +1 -0
  224. package/dist/src/renderer/skill-list-renderer.js +327 -0
  225. package/dist/src/renderer/skill-list-renderer.js.map +1 -0
  226. package/dist/src/renderer/summary.d.ts +17 -0
  227. package/dist/src/renderer/summary.d.ts.map +1 -1
  228. package/dist/src/renderer/summary.js +346 -74
  229. package/dist/src/renderer/summary.js.map +1 -1
  230. package/dist/src/renderer/table.d.ts.map +1 -1
  231. package/dist/src/renderer/table.js +213 -167
  232. package/dist/src/renderer/table.js.map +1 -1
  233. package/dist/src/renderer/test-view.d.ts +66 -0
  234. package/dist/src/renderer/test-view.d.ts.map +1 -0
  235. package/dist/src/renderer/test-view.js +905 -0
  236. package/dist/src/renderer/test-view.js.map +1 -0
  237. package/dist/src/renderer/trends.d.ts.map +1 -1
  238. package/dist/src/renderer/trends.js +3 -1
  239. package/dist/src/renderer/trends.js.map +1 -1
  240. package/dist/src/server/report-server.d.ts +6 -1
  241. package/dist/src/server/report-server.d.ts.map +1 -1
  242. package/dist/src/server/report-server.js +182 -6
  243. package/dist/src/server/report-server.js.map +1 -1
  244. package/dist/src/server/report-store.d.ts.map +1 -1
  245. package/dist/src/server/report-store.js +33 -1
  246. package/dist/src/server/report-store.js.map +1 -1
  247. package/dist/src/server/skill-index.d.ts +77 -0
  248. package/dist/src/server/skill-index.d.ts.map +1 -0
  249. package/dist/src/server/skill-index.js +331 -0
  250. package/dist/src/server/skill-index.js.map +1 -0
  251. package/dist/src/server/skill-insights.d.ts +92 -0
  252. package/dist/src/server/skill-insights.d.ts.map +1 -0
  253. package/dist/src/server/skill-insights.js +676 -0
  254. package/dist/src/server/skill-insights.js.map +1 -0
  255. package/dist/src/shared/hard-rules.d.ts +40 -0
  256. package/dist/src/shared/hard-rules.d.ts.map +1 -0
  257. package/dist/src/shared/hard-rules.js +200 -0
  258. package/dist/src/shared/hard-rules.js.map +1 -0
  259. package/dist/src/shared/time.d.ts +2 -0
  260. package/dist/src/shared/time.d.ts.map +1 -0
  261. package/dist/src/shared/time.js +10 -0
  262. package/dist/src/shared/time.js.map +1 -0
  263. package/dist/src/shared/tool-search.d.ts +9 -0
  264. package/dist/src/shared/tool-search.d.ts.map +1 -0
  265. package/dist/src/shared/tool-search.js +104 -0
  266. package/dist/src/shared/tool-search.js.map +1 -0
  267. package/dist/src/types/eval.d.ts +83 -0
  268. package/dist/src/types/eval.d.ts.map +1 -1
  269. package/dist/src/types/executor.d.ts +47 -0
  270. package/dist/src/types/executor.d.ts.map +1 -1
  271. package/dist/src/types/judge.d.ts +76 -0
  272. package/dist/src/types/judge.d.ts.map +1 -1
  273. package/dist/src/types/judge.js +3 -1
  274. package/dist/src/types/judge.js.map +1 -1
  275. package/dist/src/types/report.d.ts +63 -3
  276. package/dist/src/types/report.d.ts.map +1 -1
  277. package/dist/src/util/safe-slice.d.ts +23 -0
  278. package/dist/src/util/safe-slice.d.ts.map +1 -0
  279. package/dist/src/util/safe-slice.js +33 -0
  280. package/dist/src/util/safe-slice.js.map +1 -0
  281. package/package.json +5 -4
@@ -0,0 +1,676 @@
1
+ /** Resolve which variant in `report` 是这个 entry / insight 关注的。
2
+ * 优先用调用方传的 `preferred`(entry.eval?.variantName);找不到再 fallback 到第一个非 baseline。
3
+ * 这避免 multi-treatment 报告里所有 detector 都默认拿第一个 treatment 的数据,导致
4
+ * /skills/<treatment-2> 的详情页加载的是 treatment-1 的 failureModes / coverage / illustrations。 */
5
+ function resolveVariant(report, preferred) {
6
+ const all = report.meta.variants ?? [];
7
+ if (preferred && all.includes(preferred))
8
+ return preferred;
9
+ return all.find((v) => v !== 'baseline') ?? null;
10
+ }
11
+ function summarizeToolCall(tc) {
12
+ const tool = tc.tool;
13
+ const inp = tc.input;
14
+ let arg = '';
15
+ if (inp) {
16
+ if (typeof inp.command === 'string')
17
+ arg = inp.command.slice(0, 60);
18
+ else if (typeof inp.file_path === 'string')
19
+ arg = inp.file_path;
20
+ else if (typeof inp.url === 'string')
21
+ arg = inp.url;
22
+ else if (typeof inp.query === 'string')
23
+ arg = inp.query.slice(0, 50);
24
+ }
25
+ const status = tc.success ? '' : ' [失败]';
26
+ return arg ? `${tool}: ${arg}${status}` : `${tool}${status}`;
27
+ }
28
+ function getFailedSamples(report, variant) {
29
+ const out = [];
30
+ for (const r of report.results) {
31
+ const v = r.variants?.[variant];
32
+ if (!v)
33
+ continue;
34
+ const passed = (v.assertions?.details ?? []).every((d) => d.passed);
35
+ if (passed)
36
+ continue;
37
+ const isTripwire = (v.diagnostic?.rootCause || []).includes('tripwire_intentional')
38
+ || report.sampleSnapshots?.[r.sample_id]?.tripwire === true;
39
+ if (isTripwire)
40
+ continue;
41
+ const firstFail = v.assertions?.details?.find((d) => !d.passed);
42
+ out.push({
43
+ sampleId: r.sample_id,
44
+ rootCause: (v.diagnostic?.rootCause ?? []),
45
+ failureModes: (v.diagnostic?.failureModes ?? []),
46
+ diagnosticSummary: v.diagnostic?.summary ?? '',
47
+ prompt: report.sampleSnapshots?.[r.sample_id]?.prompt,
48
+ llmOutput: v.outputPreview ?? undefined,
49
+ toolCalls: v.toolCalls?.slice(0, 6).map(summarizeToolCall),
50
+ firstFailedAssertion: firstFail
51
+ ? `${firstFail.type}: ${typeof firstFail.value === 'object' ? JSON.stringify(firstFail.value) : String(firstFail.value)}`
52
+ : undefined,
53
+ });
54
+ }
55
+ return out;
56
+ }
57
+ /**
58
+ * 阈值依据:
59
+ * >= 3:high — 同 failure mode 重复 3 次以上,可以排除偶发,定为强信号;
60
+ * >= 2:medium — 重复出现意味着不是单条 sample 抽风,但 2 次还不算稳定模式;
61
+ * 1:low — 单条命中,可能是 sample 设计问题或 LLM 一次性失误,不阻塞但记录。
62
+ *
63
+ * 各 detector 内部一般也用 `>= 2` 作为 cluster 阈值(详情参考各 detector 注释)。
64
+ *
65
+ * 采样不足提醒:单 skill < 2 个非诱错 sample 时不会触发任何 cluster-based insight,
66
+ * 这是设计行为(没数据不下结论),用户层面应该看到"用例数 < 5,建议补样本"提示
67
+ * (在 detail page 空态有,list 页面 todo)。
68
+ */
69
+ function severityOfCount(n) {
70
+ if (n >= 3)
71
+ return 'high';
72
+ if (n >= 2)
73
+ return 'medium';
74
+ return 'low';
75
+ }
76
+ function pickIllustrations(samples, n = 2) {
77
+ return samples.slice(0, n).map((s) => ({
78
+ sampleId: s.sampleId,
79
+ samplePrompt: s.prompt?.slice(0, 200),
80
+ llmOutput: s.llmOutput?.slice(0, 200),
81
+ toolCalls: s.toolCalls,
82
+ failedAssertion: s.firstFailedAssertion,
83
+ }));
84
+ }
85
+ // ────────── 7 类 detection ──────────
86
+ /** failureMode `环境拦截` cluster — 实际是 sample 设计问题,不是 skill 问题。 */
87
+ function detectEnvironmentBlocked(evalReport, variantName) {
88
+ if (!evalReport)
89
+ return null;
90
+ const variant = resolveVariant(evalReport, variantName);
91
+ if (!variant)
92
+ return null;
93
+ const failed = getFailedSamples(evalReport, variant);
94
+ const blocked = failed.filter((f) => f.failureModes.includes('环境拦截'));
95
+ if (blocked.length < 2)
96
+ return null;
97
+ const sampleIds = blocked.map((s) => s.sampleId).join(', ');
98
+ return {
99
+ id: 'environment-blocked-mocks',
100
+ category: 'environment-blocked-mocks',
101
+ audience: 'sample-author',
102
+ title: `${blocked.length} 条评测样本因 mock 没盖到 LLM 想调的工具而 fail`,
103
+ description: 'LLM 行为正确但工具调用被 mock-strict 拦截 — 这是 sample 设计问题,不是 skill 问题。要么 mock 列表漏写,要么 command_glob/file_path 写得太严没匹配到 LLM 真实调用。',
104
+ severity: severityOfCount(blocked.length),
105
+ affectedCount: blocked.length,
106
+ stageRefs: { evalSampleIds: blocked.map((s) => s.sampleId) },
107
+ evidence: [
108
+ {
109
+ perspective: 'eval-functional', status: 'flagged',
110
+ message: `sample ${sampleIds} 失败模式都是「环境拦截」(LLM 调用被 mocks-strict deny)`,
111
+ ref: blocked[0].sampleId,
112
+ illustrations: pickIllustrations(blocked, 2),
113
+ },
114
+ {
115
+ perspective: 'doctor', status: 'silent',
116
+ message: 'doctor 不检查 mock 设计完备性(超出静态体检范围)',
117
+ },
118
+ ],
119
+ recommendations: [
120
+ {
121
+ action: `复查 sample ${blocked[0].sampleId} 的 mocks 列表,确认 LLM 实际调用形态被覆盖`,
122
+ priority: severityOfCount(blocked.length),
123
+ patch: {
124
+ target: 'sample-mocks',
125
+ location: `sample ${blocked[0].sampleId} 的 mocks 数组`,
126
+ snippet: `// 看 LLM 实际调了什么(toolCalls 里),把缺的工具加进去
127
+ {
128
+ "tool": "Bash", // 或 Read/WebFetch 等,跟 illustration 里的 toolCalls 对上
129
+ "match": { "command_glob": "*<LLM 实际调的命令片段>*" },
130
+ "return": { "stdout": "<模拟成功输出>", "exit": 0 }
131
+ }`,
132
+ },
133
+ },
134
+ {
135
+ action: 'file_path 严格匹配挂的话,改用 file_path_endswith 后缀匹配',
136
+ priority: 'medium',
137
+ patch: {
138
+ target: 'sample-mocks',
139
+ location: '所有 file_path 严格相等的 mock',
140
+ snippet: `// before: { "match": { "file_path": "tasks/state.json" } }
141
+ // after:
142
+ { "match": { "file_path_endswith": "tasks/state.json" } }`,
143
+ },
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ /** sample.environment.files_available 缺声明 + skill 文档没强调依赖。 */
149
+ function detectSkillDocGap(doctor, evalReport, observe, variantName) {
150
+ const depRule = doctor?.results.find((r) => r.ruleId === 'dependencies_present'
151
+ && (r.status === 'warn' || r.status === 'fail'));
152
+ let failedDocSamples = [];
153
+ if (evalReport) {
154
+ const variant = resolveVariant(evalReport, variantName);
155
+ if (variant) {
156
+ failedDocSamples = getFailedSamples(evalReport, variant).filter((f) => f.rootCause.includes('skill_doc_missing') || f.rootCause.includes('skill_doc_unclear'));
157
+ }
158
+ }
159
+ if (!depRule && failedDocSamples.length === 0)
160
+ return null;
161
+ const missingFiles = depRule?.detail?.missing_files ?? [];
162
+ const exampleFile = missingFiles[0] ?? 'XXX.md';
163
+ const evidence = [];
164
+ evidence.push(depRule
165
+ ? {
166
+ perspective: 'doctor', status: 'flagged',
167
+ message: `doctor 静态检查:skill 引用了 ${missingFiles.length || 1} 个文件但 sample 没声明已就绪${missingFiles.length > 0 ? ` (${missingFiles.join(', ')})` : ''}`,
168
+ ref: depRule.ruleId,
169
+ }
170
+ : {
171
+ perspective: 'doctor', status: 'blind',
172
+ message: 'eval 失败 sample 标 skill 文档缺漏,但 doctor 没卡 — 文档可能写得不规范但 doctor 规则识别不出',
173
+ });
174
+ if (failedDocSamples.length > 0) {
175
+ evidence.push({
176
+ perspective: 'eval-functional', status: 'flagged',
177
+ message: `${failedDocSamples.length} 条评测样本失败,LLM 没找到该读哪段文档(${failedDocSamples.map((f) => f.sampleId).join(', ')})`,
178
+ ref: failedDocSamples[0].sampleId,
179
+ illustrations: pickIllustrations(failedDocSamples, 2),
180
+ });
181
+ }
182
+ else {
183
+ evidence.push({
184
+ perspective: 'eval-functional', status: 'silent',
185
+ message: '本批 evaluation 没命中文档缺漏类失败',
186
+ });
187
+ }
188
+ evidence.push(observe
189
+ ? observe.gapRate >= 0.2
190
+ ? {
191
+ perspective: 'observe', status: 'flagged',
192
+ message: `生产里 ${(observe.gapRate * 100).toFixed(0)}% 段也命中知识库 gap(LLM 找不到该读哪段) — 跟评测信号印证`,
193
+ }
194
+ : {
195
+ perspective: 'observe', status: 'silent',
196
+ message: `生产 gap 率 ${(observe.gapRate * 100).toFixed(0)}%,真实使用中此问题触发不多`,
197
+ }
198
+ : { perspective: 'observe', status: 'na', message: '没跑 observe,无法看真实生产是否也踩' });
199
+ const severity = (depRule && failedDocSamples.length >= 2) ? 'high'
200
+ : failedDocSamples.length >= 2 || depRule ? 'medium' : 'low';
201
+ const recs = [];
202
+ if (depRule) {
203
+ recs.push({
204
+ action: `在 sample.environment.files_available 加上文件路径,告诉 LLM "这些文件已就绪,无需探测"`,
205
+ priority: severity,
206
+ patch: {
207
+ target: 'sample-environment',
208
+ location: 'samples.json 各 sample 的 environment 块',
209
+ snippet: `"environment": {
210
+ "files_available": [
211
+ "${exampleFile}"${missingFiles.length > 1 ? `,
212
+ "${missingFiles[1]}"` : ''}
213
+ ],
214
+ "notes": "所有依赖文件已就绪,LLM 跳过探测直接进工作流"
215
+ }`,
216
+ },
217
+ });
218
+ }
219
+ if (failedDocSamples.length > 0) {
220
+ recs.push({
221
+ action: `在 SKILL.md 引用这些文件的章节加显眼提示,LLM 容易漏读`,
222
+ priority: severity,
223
+ patch: {
224
+ target: 'skill',
225
+ location: 'SKILL.md 中引用 ' + (missingFiles[0] ?? '相关文件') + ' 的章节',
226
+ snippet: `## <相关章节>
227
+
228
+ ⚠️ **必读前置**:本步骤依赖 \`${exampleFile}\` 中的内容,务必先 Read 该文件再继续。
229
+
230
+ <原步骤说明>...`,
231
+ },
232
+ });
233
+ }
234
+ return {
235
+ id: 'skill-doc-gap',
236
+ category: 'skill-doc-gap',
237
+ audience: failedDocSamples.length > 0 ? 'skill-author' : 'sample-author',
238
+ title: failedDocSamples.length > 0
239
+ ? `LLM 漏读 skill 引用的文件,导致 ${failedDocSamples.length} 条样本失败`
240
+ : 'skill 引用了文件但没声明在 sample.environment 里',
241
+ description: 'skill 文档提到某些前置文件,但 sample 没标"已就绪"。LLM 看 prompt 时会试图先探测/Read 这些文件,在 mocks-strict 环境下被拦,流程在第一步就停。',
242
+ severity,
243
+ affectedCount: failedDocSamples.length || 1,
244
+ stageRefs: {
245
+ ...(depRule ? { doctorRuleIds: ['dependencies_present'] } : {}),
246
+ ...(failedDocSamples.length > 0 ? { evalSampleIds: failedDocSamples.map((f) => f.sampleId) } : {}),
247
+ ...((observe?.gapRate ?? 0) >= 0.2 ? { observeRefs: ['gap'] } : {}),
248
+ },
249
+ evidence,
250
+ recommendations: recs,
251
+ };
252
+ }
253
+ /** failureMode 是 skill 写法层反模式(硬编码/幻觉/跳步/误读) — 真要改 skill。 */
254
+ function detectFailureModeSkillIssue(evalReport, variantName) {
255
+ if (!evalReport)
256
+ return null;
257
+ const variant = resolveVariant(evalReport, variantName);
258
+ if (!variant)
259
+ return null;
260
+ const failed = getFailedSamples(evalReport, variant);
261
+ if (failed.length === 0)
262
+ return null;
263
+ const SKILL_MODES = new Set(['硬编码值', '幻觉输出', '工作流跳步', '误读约束', '工具误用']);
264
+ // 排除已被 skill-doc-gap 覆盖的(rootCause skill_doc_missing/unclear)
265
+ const candidates = failed.filter((f) => {
266
+ const docIssue = f.rootCause.includes('skill_doc_missing') || f.rootCause.includes('skill_doc_unclear');
267
+ if (docIssue)
268
+ return false;
269
+ return f.failureModes.some((m) => SKILL_MODES.has(m));
270
+ });
271
+ // 找最大簇 — 同 failureMode 在 ≥ 2 sample
272
+ const modeCount = {};
273
+ for (const f of candidates) {
274
+ for (const m of f.failureModes) {
275
+ if (!SKILL_MODES.has(m))
276
+ continue;
277
+ if (!modeCount[m])
278
+ modeCount[m] = [];
279
+ modeCount[m].push(f);
280
+ }
281
+ }
282
+ const top = Object.entries(modeCount).filter(([, fs]) => fs.length >= 2)
283
+ .sort((a, b) => b[1].length - a[1].length);
284
+ if (top.length === 0)
285
+ return null;
286
+ const [topMode, topSamples] = top[0];
287
+ const MODE_USERFACING_DESC = {
288
+ '硬编码值': 'LLM 把本来该用变量传的值写死(如 namespace 直接写成 "testuser001")',
289
+ '幻觉输出': 'LLM 声称完成了某动作但实际没调对应工具,输出虚构 id / commit hash / 文件路径',
290
+ '工作流跳步': 'LLM 没按 skill 教的步骤顺序走,跳过了关键中间步骤',
291
+ '误读约束': 'skill 文档里写清楚的约束(如"必须用 X 不能用 Y")LLM 没遵守',
292
+ '工具误用': 'LLM 选错工具或传错参数,反复尝试同一失败动作',
293
+ };
294
+ const MODE_PATCH_HINT = {
295
+ '硬编码值': {
296
+ location: 'SKILL.md 涉及具体值传递的章节(如「参数获取」「身份注入」节)',
297
+ snippet: `# Before(LLM 容易硬编码):
298
+ 直接把 namespace 设成 "testuser001"。
299
+
300
+ # After(强制变量化):
301
+ **严禁**直接写死 namespace。先调 \`<your-cli> auth status -o json\` 拿到 username,赋给变量后再传:
302
+ \`\`\`bash
303
+ NAMESPACE=$(<your-cli> auth status -o json | jq -r '.username')
304
+ <your-cli> project create --namespace "$NAMESPACE"
305
+ \`\`\``,
306
+ },
307
+ '幻觉输出': {
308
+ location: 'SKILL.md 涉及工具调用结果回传的章节',
309
+ snippet: `# 加显眼"诚实输出"约束:
310
+ **严禁**编造 commit hash / id / 文件路径等关键标识符。这些值**必须**从前一步工具调用的真实返回里提取,不能凭印象写。
311
+ 若工具调用失败,如实告知用户失败原因,**不要**伪装成功。`,
312
+ },
313
+ '工作流跳步': {
314
+ location: 'SKILL.md 工作流章节(如「步骤 1...步骤 N」)',
315
+ snippet: `# 用编号 + 必做标记强化顺序:
316
+ ## 工作流(必须按序执行)
317
+ 1. **第 1 步必做**: <动作 A>
318
+ 2. **第 2 步必做**: <动作 B,依赖第 1 步的输出>
319
+ 3. **第 3 步可选**: <分支 C>
320
+
321
+ 每完成一步,在响应中明确说"已完成第 N 步: ..."`,
322
+ },
323
+ '误读约束': {
324
+ location: 'SKILL.md 中相关约束的位置',
325
+ snippet: `# 把约束抬到段落开头 + 加 ⚠️/❌:
326
+ ⚠️ **硬规则**: 必须用 \`<工具 A>\`,**严禁**用 \`<工具 B>\` 绕过。
327
+ 原因: <为什么>。
328
+ <原步骤说明>`,
329
+ },
330
+ '工具误用': {
331
+ location: 'SKILL.md 工具选择章节',
332
+ snippet: `# 加工具选择对照表:
333
+ | 场景 | 用 | 不用 |
334
+ |------|----|------|
335
+ | 查询 X | tool-A | tool-B(参数语义不一样) |
336
+ ...`,
337
+ },
338
+ };
339
+ return {
340
+ id: `failure-mode-skill:${topMode}`,
341
+ category: 'failure-mode-skill',
342
+ audience: 'skill-author',
343
+ title: `${topSamples.length} 条样本踩同一个写法坑:${topMode}`,
344
+ description: MODE_USERFACING_DESC[topMode] ?? `多 sample 共享 failureMode 「${topMode}」,skill 文档对此没抑制`,
345
+ severity: severityOfCount(topSamples.length),
346
+ affectedCount: topSamples.length,
347
+ stageRefs: { evalSampleIds: topSamples.map((s) => s.sampleId) },
348
+ evidence: [
349
+ {
350
+ perspective: 'eval-functional', status: 'flagged',
351
+ message: `sample ${topSamples.map((s) => s.sampleId).join(', ')} 都因「${topMode}」失败 — 单一原因反复踩,说明 skill 没抑制这种写法`,
352
+ ref: topSamples[0].sampleId,
353
+ illustrations: pickIllustrations(topSamples, 2),
354
+ },
355
+ ],
356
+ recommendations: [
357
+ {
358
+ action: `在 SKILL.md 中针对「${topMode}」加专门提示`,
359
+ priority: severityOfCount(topSamples.length),
360
+ ...(MODE_PATCH_HINT[topMode] ? {
361
+ patch: {
362
+ target: 'skill',
363
+ location: MODE_PATCH_HINT[topMode].location,
364
+ snippet: MODE_PATCH_HINT[topMode].snippet,
365
+ },
366
+ } : {}),
367
+ },
368
+ {
369
+ action: '加针对该模式的诱错样本(tripwire),防止 skill 改进后再退化',
370
+ priority: 'low',
371
+ },
372
+ ],
373
+ };
374
+ }
375
+ /** observe 高失败率 — 真实环境 skill 跑不稳。 */
376
+ function detectProductionInstability(doctor, observe) {
377
+ if (!observe || observe.failureRate < 0.4)
378
+ return null;
379
+ const depRule = doctor?.results.find((r) => r.ruleId === 'dependencies_present'
380
+ && (r.status === 'warn' || r.status === 'fail'));
381
+ return {
382
+ id: 'production-instability',
383
+ category: 'production-instability',
384
+ audience: 'skill-author',
385
+ title: `生产环境跑这个 skill 时,工具失败率 ${(observe.failureRate * 100).toFixed(0)}%`,
386
+ description: '真实用户用这个 skill,LLM 调出去的工具经常报错。可能是凭证/网络/上游 SLA 问题,也可能是 skill 教错了用什么工具/参数。',
387
+ severity: 'high',
388
+ affectedCount: Math.round(observe.segmentCount * observe.failureRate),
389
+ stageRefs: {
390
+ observeRefs: ['high-failure-rate'],
391
+ ...(depRule ? { doctorRuleIds: ['dependencies_present'] } : {}),
392
+ },
393
+ evidence: [
394
+ {
395
+ perspective: 'observe', status: 'flagged',
396
+ message: `${observe.segmentCount} 段产线轨迹,工具调用失败率 ${(observe.failureRate * 100).toFixed(0)}%`,
397
+ },
398
+ depRule
399
+ ? {
400
+ perspective: 'doctor', status: 'flagged',
401
+ message: `doctor 也警告依赖问题:${depRule.message}`,
402
+ ref: depRule.ruleId,
403
+ }
404
+ : {
405
+ perspective: 'doctor', status: doctor ? 'silent' : 'na',
406
+ message: doctor
407
+ ? 'doctor 没警告依赖,失败可能在 CLI/凭证/上游 API 层(skill 自身可能没错)'
408
+ : '没跑 doctor,无法对照',
409
+ },
410
+ ],
411
+ recommendations: [
412
+ {
413
+ action: '排查产线工具失败原因:CLI 安装 / 凭证有效期 / 网络 / 上游 API SLA',
414
+ priority: 'high',
415
+ },
416
+ ...(depRule ? [{
417
+ action: '依据 doctor 警告补依赖声明 / 改 skill 引用',
418
+ priority: 'high',
419
+ }] : []),
420
+ {
421
+ action: '如果不是 skill 写错而是环境问题,在 skill 加重试 / 容错指南',
422
+ priority: 'medium',
423
+ patch: {
424
+ target: 'skill',
425
+ location: 'SKILL.md 工具调用章节',
426
+ snippet: `# 容错指南:
427
+ 若 \`<工具>\` 调用失败:
428
+ 1. 检查环境(凭证 / 网络 / 工具版本)
429
+ 2. 重试 1-2 次
430
+ 3. 仍失败:**如实告知用户**,不要继续假装完成`,
431
+ },
432
+ },
433
+ ],
434
+ };
435
+ }
436
+ /** observe gap + eval coverage 漏 — sample 没盖到生产真实使用的文件。 */
437
+ function detectCoverageGap(evalReport, observe, variantName) {
438
+ if (!observe || observe.gapRate === 0)
439
+ return null;
440
+ let evalUncovered = [];
441
+ if (evalReport?.analysis?.coverage) {
442
+ const variant = resolveVariant(evalReport, variantName);
443
+ if (variant && evalReport.analysis.coverage[variant]) {
444
+ evalUncovered = evalReport.analysis.coverage[variant].uncoveredFiles || [];
445
+ }
446
+ }
447
+ const evidence = [
448
+ {
449
+ perspective: 'observe', status: 'flagged',
450
+ message: `生产里 ${(observe.gapRate * 100).toFixed(0)}% 段命中知识库 gap — LLM 在真实使用中找不到该读哪段`,
451
+ },
452
+ ];
453
+ if (evalUncovered.length > 0) {
454
+ evidence.push({
455
+ perspective: 'eval-functional', status: 'flagged',
456
+ message: `评测样本也漏覆盖 ${evalUncovered.length} 个文件:${evalUncovered.slice(0, 3).join(', ')}${evalUncovered.length > 3 ? '...' : ''}`,
457
+ });
458
+ }
459
+ else {
460
+ evidence.push({
461
+ perspective: 'eval-functional', status: 'silent',
462
+ message: '评测覆盖率 OK,gap 主要来自生产用法跟 sample 设计有差距',
463
+ });
464
+ }
465
+ const severity = observe.gapRate >= 0.4 ? 'high'
466
+ : observe.gapRate >= 0.2 ? 'medium' : 'low';
467
+ return {
468
+ id: 'coverage-gap',
469
+ category: 'coverage-gap',
470
+ audience: 'sample-author',
471
+ title: '生产 + 评测都没覆盖到部分知识库文件',
472
+ description: '观测到生产用户访问了 skill 引用的某些文件,但你的评测 sample 没测到这些路径 — sample 设计跟真实使用脱节。',
473
+ severity,
474
+ affectedCount: Math.max(evalUncovered.length, Math.round(observe.segmentCount * observe.gapRate)),
475
+ stageRefs: { observeRefs: ['gap', 'uncovered-files'] },
476
+ evidence,
477
+ recommendations: [
478
+ ...(evalUncovered.length > 0
479
+ ? [{
480
+ action: `补 1-2 条 sample 专门测覆盖这些文件:${evalUncovered.slice(0, 2).join(', ')}`,
481
+ priority: severity,
482
+ patch: {
483
+ target: 'sample-environment',
484
+ location: 'samples.json 加新 sample',
485
+ snippet: `{
486
+ "sample_id": "s${String(99).padStart(3, '0')}",
487
+ "prompt": "<让 LLM 必须读 ${evalUncovered[0]} 才能完成的任务>",
488
+ "rubric": "应该 Read ${evalUncovered[0]} 后再继续",
489
+ "assertions": [
490
+ { "type": "tool_input_contains", "value": "Read:${evalUncovered[0]}", "weight": 1 }
491
+ ],
492
+ "environment": { "files_available": ["${evalUncovered[0]}"] }
493
+ }`,
494
+ },
495
+ }]
496
+ : []),
497
+ {
498
+ action: '检查 skill 是否引用了实际不存在 / 不该引用的文件,清理冗余引用',
499
+ priority: 'low',
500
+ },
501
+ ],
502
+ };
503
+ }
504
+ /** doctor skill_readable warn — skill 文档过长。 */
505
+ function detectSkillTooLong(doctor, observe) {
506
+ const readableRule = doctor?.results.find((r) => r.ruleId === 'skill_readable' && r.status === 'warn');
507
+ if (!readableRule)
508
+ return null;
509
+ const evidence = [
510
+ { perspective: 'doctor', status: 'flagged', message: readableRule.message, ref: readableRule.ruleId },
511
+ ];
512
+ if (observe && observe.gapRate >= 0.2) {
513
+ evidence.push({
514
+ perspective: 'observe', status: 'flagged',
515
+ message: `生产 gap ${(observe.gapRate * 100).toFixed(0)}% — 长 skill 让 LLM 找不到段落,跟 doctor 警告印证`,
516
+ });
517
+ }
518
+ return {
519
+ id: 'skill-too-long',
520
+ category: 'skill-too-long',
521
+ audience: 'skill-author',
522
+ title: 'skill 文档太长,LLM 容易漏读',
523
+ description: 'skill 内容超过推荐上限。长 skill 在 LLM 上下文里会被压缩注意力,LLM 跟着前几段走,后面的关键约束容易漏读。',
524
+ severity: observe && observe.gapRate >= 0.3 ? 'medium' : 'low',
525
+ affectedCount: 1,
526
+ stageRefs: {
527
+ doctorRuleIds: ['skill_readable'],
528
+ ...(observe && observe.gapRate >= 0.2 ? { observeRefs: ['gap'] } : {}),
529
+ },
530
+ evidence,
531
+ recommendations: [
532
+ {
533
+ action: '把示例 / 长段移到 references/ 子文件,SKILL.md 主体保持精简',
534
+ priority: 'medium',
535
+ patch: {
536
+ target: 'skill',
537
+ location: 'SKILL.md + 新建 references/ 目录',
538
+ snippet: `# 拆分前(SKILL.md 18000 字)
539
+ ## 工作流详解
540
+ <2000 字详细示例 1>
541
+ <2000 字详细示例 2>
542
+ ...
543
+
544
+ # 拆分后(SKILL.md 主体 5000 字)
545
+ ## 工作流(简版)
546
+ - 步骤 A:见 [references/workflow-a.md](references/workflow-a.md) 的详细示例
547
+ - 步骤 B:见 [references/workflow-b.md](references/workflow-b.md)`,
548
+ },
549
+ },
550
+ {
551
+ action: '考虑拆成多个子 skill,各负责单一职责',
552
+ priority: 'low',
553
+ },
554
+ ],
555
+ };
556
+ }
557
+ /** doctor 盲区 — 多个 sample 同 failureMode 但 doctor 无对应规则。属于 omk 维护者待办。 */
558
+ function detectOmkDoctorBlindspot(doctor, evalReport, variantName) {
559
+ if (!evalReport)
560
+ return null;
561
+ const variant = resolveVariant(evalReport, variantName);
562
+ if (!variant)
563
+ return null;
564
+ const failed = getFailedSamples(evalReport, variant);
565
+ const SKILL_MODES = new Set(['硬编码值', '幻觉输出', '工作流跳步', '误读约束']);
566
+ const candidates = failed.filter((f) => {
567
+ const docIssue = f.rootCause.includes('skill_doc_missing') || f.rootCause.includes('skill_doc_unclear');
568
+ if (docIssue)
569
+ return false;
570
+ return f.failureModes.some((m) => SKILL_MODES.has(m));
571
+ });
572
+ if (candidates.length < 2)
573
+ return null;
574
+ const observedModes = [...new Set(candidates.flatMap((c) => c.failureModes.filter((m) => SKILL_MODES.has(m))))];
575
+ return {
576
+ id: 'omk-doctor-blindspot',
577
+ category: 'omk-doctor-blindspot',
578
+ audience: 'omk-maintainer',
579
+ title: `omk 工具反馈:doctor 没规则识别 ${observedModes.join(' / ')} 类反模式`,
580
+ description: '本次评测里 ≥ 2 条 sample 因这些行为反模式失败,但 doctor 跑下来全 pass。说明 doctor 当前规则覆盖不到这类问题 — 给 omk 仓库提 issue 加 composer rule 即可。普通 skill 开发者可忽略本条。',
581
+ severity: candidates.length >= 3 ? 'medium' : 'low',
582
+ affectedCount: candidates.length,
583
+ stageRefs: { evalSampleIds: candidates.map((c) => c.sampleId) },
584
+ evidence: [
585
+ {
586
+ perspective: 'eval-functional', status: 'flagged',
587
+ message: `${candidates.length} 条样本因 ${observedModes.join(' / ')} 失败,doctor 全 pass`,
588
+ ref: candidates[0].sampleId,
589
+ },
590
+ {
591
+ perspective: 'doctor',
592
+ status: doctor ? 'blind' : 'na',
593
+ message: doctor
594
+ ? `doctor 当前 BUILTIN_RULES 不含这类反模式检测 — 需 omk 加 LLM-judge composer rule`
595
+ : '没跑 doctor 无法对照',
596
+ },
597
+ ],
598
+ recommendations: [
599
+ {
600
+ action: '给 omk 仓库提 issue:doctor 加 composer rule 识别 ' + observedModes.join(' / ') + ' 类反模式',
601
+ priority: 'medium',
602
+ patch: {
603
+ target: 'doctor-rule',
604
+ location: 'omk 仓库 src/doctor/health/(新建 composer rule)',
605
+ snippet: `// omk 仓库新增 composer rule(omk 维护者负责,非 skill 开发者职责)
606
+ export const skillAntiPatternComposer: ComposerRule = {
607
+ id: 'skill_anti_patterns',
608
+ kind: 'composer',
609
+ severity: 'warn',
610
+ labelKey: 'doctor.skill_anti_patterns.label',
611
+ async checkAll(ctx): Promise<ComposerOutcome[]> {
612
+ // 用 LLM judge 扫 skill 文本,识别 ${observedModes.join(' / ')} 等反模式
613
+ return [...];
614
+ },
615
+ };`,
616
+ },
617
+ },
618
+ ],
619
+ };
620
+ }
621
+ // ────────── 入口 ──────────
622
+ const SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
623
+ export function detectInsights(entry, evalReport) {
624
+ // entry.eval.variantName 是当前 skill entry 对应的 treatment variant 名;
625
+ // multi-treatment 报告里同一份 evalReport 会被 N 个 entry 共用,每个 entry 都该
626
+ // 只看自己 variant 那部分数据,否则 /skills/<treatment-2> 会看到 treatment-1 的
627
+ // failureModes / coverage / illustrations / verdict。
628
+ const variantName = entry.eval?.variantName ?? null;
629
+ const out = [];
630
+ const detectors = [
631
+ () => detectEnvironmentBlocked(evalReport, variantName),
632
+ () => detectSkillDocGap(entry.doctor, evalReport, entry.observe, variantName),
633
+ () => detectFailureModeSkillIssue(evalReport, variantName),
634
+ () => detectCoverageGap(evalReport, entry.observe, variantName),
635
+ () => detectProductionInstability(entry.doctor, entry.observe),
636
+ () => detectSkillTooLong(entry.doctor, entry.observe),
637
+ () => detectOmkDoctorBlindspot(entry.doctor, evalReport, variantName),
638
+ ];
639
+ for (const detect of detectors) {
640
+ const ins = detect();
641
+ if (ins)
642
+ out.push(ins);
643
+ }
644
+ out.sort((a, b) => {
645
+ const sa = SEVERITY_RANK[a.severity];
646
+ const sb = SEVERITY_RANK[b.severity];
647
+ if (sa !== sb)
648
+ return sb - sa;
649
+ return b.affectedCount - a.affectedCount;
650
+ });
651
+ return out;
652
+ }
653
+ export function flattenRecommendations(insights) {
654
+ const seen = new Map();
655
+ for (const ins of insights) {
656
+ for (const rec of ins.recommendations) {
657
+ const prev = seen.get(rec.action);
658
+ if (!prev || SEVERITY_RANK[rec.priority] > SEVERITY_RANK[prev.priority]) {
659
+ seen.set(rec.action, rec);
660
+ }
661
+ }
662
+ }
663
+ return [...seen.values()].sort((a, b) => SEVERITY_RANK[b.priority] - SEVERITY_RANK[a.priority]);
664
+ }
665
+ /** 按 audience 分桶 — UI 拆区用。 */
666
+ export function groupInsightsByAudience(insights) {
667
+ const out = {
668
+ 'skill-author': [],
669
+ 'sample-author': [],
670
+ 'omk-maintainer': [],
671
+ };
672
+ for (const ins of insights)
673
+ out[ins.audience].push(ins);
674
+ return out;
675
+ }
676
+ //# sourceMappingURL=skill-insights.js.map