oh-my-knowledge 0.45.0 → 0.46.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 -1
- package/README.zh.md +1 -1
- package/dist/assets/agent-skills/omk/SKILL.md +3 -3
- package/dist/assets/agent-skills/omk/references/commands.md +13 -6
- package/dist/cli/commands/doctor.d.ts +3 -2
- package/dist/cli/commands/doctor.js +66 -63
- package/dist/cli/lib/cmd-flags.d.ts +3 -2
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +0 -4
- package/dist/doctor/endpoint-rule.js +1 -1
- package/dist/doctor/health/composer.js +162 -50
- package/dist/doctor/health/consensus.d.ts +43 -0
- package/dist/doctor/health/consensus.js +224 -0
- package/dist/doctor/health/dimension-spec.d.ts +6 -0
- package/dist/doctor/health/register.d.ts +0 -1
- package/dist/doctor/health/register.js +0 -1
- package/dist/doctor/index.js +3 -0
- package/dist/doctor/messages.d.ts +1 -1
- package/dist/doctor/messages.js +8 -0
- package/dist/doctor/rules.d.ts +4 -2
- package/dist/doctor/rules.js +4 -2
- package/dist/executors/claude-cli.js +1 -1
- package/dist/observability/skill-chain-advisories.js +2 -2
- package/dist/renderer/doctor-detail-renderer.js +30 -2
- package/dist/renderer/report-shell.d.ts +1 -1
- package/dist/renderer/report-shell.js +5 -0
- package/dist/renderer/skill-detail-renderer.js +844 -100
- package/dist/server/skill-index.js +37 -1
- package/dist/shared/llm-prompts/registry.js +2 -1
- package/dist/shared/llm-prompts/skill-health-merge.d.ts +22 -0
- package/dist/shared/llm-prompts/skill-health-merge.js +79 -0
- package/dist/shared/llm-prompts/skill-health.js +1 -1
- package/dist/types/doctor.d.ts +32 -8
- package/dist/types/skill-index.d.ts +10 -0
- package/package.json +4 -3
|
@@ -18,6 +18,8 @@ import { createExecutor } from '../../executors/index.js';
|
|
|
18
18
|
import { getRegisteredHealthDimensions } from './dimension-registry.js';
|
|
19
19
|
import { buildHealthPrompt } from './prompt-builder.js';
|
|
20
20
|
import { deriveOverallHealth, extractJson, parseHealthOutput } from './parser.js';
|
|
21
|
+
import { enumerateFindingsForMerge, mergeHealthSamples, mergeHealthSamplesLlm, parseMergeClusters, } from './consensus.js';
|
|
22
|
+
import { buildHealthMergePrompt } from '../../shared/llm-prompts/skill-health-merge.js';
|
|
21
23
|
export const SKILL_HEALTH_COMPOSER_ID = 'skill_health';
|
|
22
24
|
const defaultExecutorFactory = createExecutor;
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -125,6 +127,24 @@ function listSiblingSkills(skillRoot) {
|
|
|
125
127
|
.sort();
|
|
126
128
|
}
|
|
127
129
|
// ---------------------------------------------------------------------------
|
|
130
|
+
// 有界并发 map
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
/** 最多 limit 个任务并发,返回与 items 同序的结果(worker-pool,对齐 eval-core 口径)。 */
|
|
133
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
134
|
+
const results = new Array(items.length);
|
|
135
|
+
let next = 0;
|
|
136
|
+
const worker = async () => {
|
|
137
|
+
while (next < items.length) {
|
|
138
|
+
const i = next;
|
|
139
|
+
next += 1;
|
|
140
|
+
results[i] = await fn(items[i], i);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const workers = Math.max(1, Math.min(Number.isFinite(limit) ? limit : 1, items.length));
|
|
144
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
128
148
|
// Composer factory
|
|
129
149
|
// ---------------------------------------------------------------------------
|
|
130
150
|
export function makeSkillHealthComposer(execFactory = defaultExecutorFactory) {
|
|
@@ -139,7 +159,7 @@ export function makeSkillHealthComposer(execFactory = defaultExecutorFactory) {
|
|
|
139
159
|
async function composerCheckAll(ctx, execFactory) {
|
|
140
160
|
const dims = getRegisteredHealthDimensions();
|
|
141
161
|
// Programmatic opt-out: runHealthCheck=false → 给 summary 一条 skipped,
|
|
142
|
-
// 各维度不展开避免污染报告。
|
|
162
|
+
// 各维度不展开避免污染报告。eval preflight 走这条(只跑静态 rule,不触发 LLM)。
|
|
143
163
|
if (!ctx.runHealthCheck) {
|
|
144
164
|
return [{
|
|
145
165
|
subId: '_summary',
|
|
@@ -176,44 +196,124 @@ async function composerCheckAll(ctx, execFactory) {
|
|
|
176
196
|
const siblingSkills = listSiblingSkills(skillRoot);
|
|
177
197
|
const prompt = buildHealthPrompt({ skillName, skillContent: content, skillRoot, subFiles, siblingSkills }, dims);
|
|
178
198
|
const executor = execFactory(ctx.executorName);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
// 采样次数(self-consistency):默认 1;非有限值(NaN/Infinity)兜底 1;上限 10 防 fat-finger
|
|
200
|
+
// / 资源耗尽(并发默认=采样数,会一起放大)。
|
|
201
|
+
const MAX_HEALTH_SAMPLES = 10;
|
|
202
|
+
const rawSamples = ctx.healthSamples;
|
|
203
|
+
const requested = Math.min(MAX_HEALTH_SAMPLES, Math.max(1, Math.floor(Number.isFinite(rawSamples) ? rawSamples : 1)));
|
|
204
|
+
// 跨采样累计的执行指标(token / cost / 时长 / 轮次都求和,反映真实总开销)。
|
|
205
|
+
const agg = {
|
|
206
|
+
durationMs: 0, durationApiMs: 0,
|
|
207
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
|
|
208
|
+
costUSD: 0, numTurns: 0, fullNumTurns: 0,
|
|
209
|
+
};
|
|
210
|
+
const accumulate = (r) => {
|
|
211
|
+
agg.durationMs += r.durationMs ?? 0;
|
|
212
|
+
agg.durationApiMs += r.durationApiMs ?? 0;
|
|
213
|
+
agg.inputTokens += r.inputTokens ?? 0;
|
|
214
|
+
agg.outputTokens += r.outputTokens ?? 0;
|
|
215
|
+
agg.cacheReadTokens += r.cacheReadTokens ?? 0;
|
|
216
|
+
agg.cacheCreationTokens += r.cacheCreationTokens ?? 0;
|
|
217
|
+
agg.costUSD += r.costUSD ?? 0;
|
|
218
|
+
agg.numTurns += r.numTurns ?? 0;
|
|
219
|
+
agg.fullNumTurns += r.fullNumTurns ?? 0;
|
|
220
|
+
};
|
|
221
|
+
// 采样并发:默认全并行(并发=采样数,样本间相互独立),`--concurrency 1` 退回串行。
|
|
222
|
+
// 非有限值兜底为 requested。
|
|
223
|
+
const rawConc = ctx.healthConcurrency;
|
|
224
|
+
const concurrency = Math.max(1, Math.min(requested, Math.floor(Number.isFinite(rawConc) ? rawConc : requested)));
|
|
225
|
+
const runSample = async () => {
|
|
226
|
+
let res;
|
|
227
|
+
try {
|
|
228
|
+
res = await executor({
|
|
229
|
+
model: ctx.model,
|
|
230
|
+
prompt,
|
|
231
|
+
cwd: skillRoot ?? ctx.cwd,
|
|
232
|
+
skillDir: skillRoot ?? null,
|
|
233
|
+
timeoutMs: ctx.timeoutMs,
|
|
234
|
+
effort: ctx.effort ?? 'low',
|
|
235
|
+
lean: true,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
return { error: { phase: 'executor', msg: err instanceof Error ? err.message : String(err) } };
|
|
240
|
+
}
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
return { res, error: { phase: 'executor', msg: res.error ?? 'unknown' } };
|
|
243
|
+
if (!res.output || res.output.trim().length === 0)
|
|
244
|
+
return { res, error: { phase: 'empty_output', msg: '' } };
|
|
245
|
+
const cleaned = extractJson(res.output);
|
|
246
|
+
if (cleaned === null)
|
|
247
|
+
return { res, error: { phase: 'extract', msg: '', extra: { rawOutput: res.output.slice(0, 4000) } } };
|
|
248
|
+
const pr = parseHealthOutput(cleaned, dims);
|
|
249
|
+
// 全维度 _missing → 该样本的 JSON 里 dim_id 缺失或写错,丢弃(不当作"全部通过")。
|
|
250
|
+
if ([...pr.byDimId.values()].every((r) => r._missing)) {
|
|
251
|
+
return { res, error: { phase: 'extract', msg: 'LLM output missing all dimension IDs', extra: { rawSample: cleaned.slice(0, 3000) } } };
|
|
252
|
+
}
|
|
253
|
+
return { res, parsed: pr };
|
|
254
|
+
};
|
|
255
|
+
// 跑 N 次(并发受 concurrency 限),收集成功解析的样本。单样本报错 / 空输出 / JSON 抽不出 /
|
|
256
|
+
// 全维度漏报都跳过(记最后一次错误);只要 ≥1 成功就走并集合并,全失败才报 fail。
|
|
257
|
+
const sampleResults = await mapWithConcurrency(Array.from({ length: requested }, (_, i) => i), concurrency, () => runSample());
|
|
258
|
+
const parsed = [];
|
|
259
|
+
let lastError = null;
|
|
260
|
+
for (const sr of sampleResults) {
|
|
261
|
+
if (sr.res)
|
|
262
|
+
accumulate(sr.res);
|
|
263
|
+
if (sr.parsed)
|
|
264
|
+
parsed.push(sr.parsed);
|
|
265
|
+
else if (sr.error)
|
|
266
|
+
lastError = sr.error;
|
|
200
267
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return [errorSummaryOutcome(ctx, 'extract', '', { rawOutput: res.output.slice(0, 4000) })];
|
|
268
|
+
if (parsed.length === 0) {
|
|
269
|
+
return [errorSummaryOutcome(ctx, lastError?.phase ?? 'extract', lastError?.msg ?? '', lastError?.extra ?? {})];
|
|
204
270
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
271
|
+
// 归并:healthMerge='string' 走字符串键;='llm'(CLI 默认)且确有可并 finding 时,多跑一次
|
|
272
|
+
// LLM 聚类(跨措辞同根因归并),失败回退 string。单样本时两者都等价于直通 + support={1,1}。
|
|
273
|
+
const mergeStrategy = ctx.healthMerge ?? 'string';
|
|
274
|
+
let merged = null;
|
|
275
|
+
let mergeMode = 'string';
|
|
276
|
+
if (mergeStrategy === 'llm' && parsed.length > 1) {
|
|
277
|
+
const tagged = enumerateFindingsForMerge(parsed, dims);
|
|
278
|
+
// 仅当某维度内 ≥2 条 finding 才值得跑 LLM 聚类(否则没东西可并,白花一次调用)。
|
|
279
|
+
const mergeable = dims.some((d) => tagged.filter((t) => t.dimId === d.id).length >= 2);
|
|
280
|
+
if (mergeable) {
|
|
281
|
+
let mres = null;
|
|
282
|
+
try {
|
|
283
|
+
mres = await executor({
|
|
284
|
+
model: ctx.model,
|
|
285
|
+
prompt: buildHealthMergePrompt(dims, tagged),
|
|
286
|
+
cwd: skillRoot ?? ctx.cwd,
|
|
287
|
+
skillDir: skillRoot ?? null,
|
|
288
|
+
timeoutMs: ctx.timeoutMs,
|
|
289
|
+
effort: ctx.effort ?? 'low',
|
|
290
|
+
lean: true,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
mres = null;
|
|
295
|
+
}
|
|
296
|
+
if (mres)
|
|
297
|
+
accumulate(mres);
|
|
298
|
+
const cleaned = mres?.ok && mres.output ? extractJson(mres.output) : null;
|
|
299
|
+
const clusters = cleaned ? parseMergeClusters(cleaned) : null;
|
|
300
|
+
if (clusters) {
|
|
301
|
+
merged = mergeHealthSamplesLlm(parsed, dims, tagged, clusters);
|
|
302
|
+
mergeMode = 'llm';
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
mergeMode = 'string(llm-fallback)'; // LLM 合并失败 → 用字符串键兜底
|
|
306
|
+
}
|
|
307
|
+
}
|
|
212
308
|
}
|
|
309
|
+
if (!merged)
|
|
310
|
+
merged = mergeHealthSamples(parsed, dims);
|
|
311
|
+
const overall = merged.overall ?? deriveOverallHealth(merged.byDimId);
|
|
312
|
+
const succeeded = parsed.length;
|
|
213
313
|
// 汇总 finding 计数(框架算,不信 LLM 算术)
|
|
214
314
|
const finding = { '错误': 0, '警告': 0, '建议': 0 };
|
|
215
315
|
const dimCount = { '健康': 0, '亚健康': 0, '不健康': 0, '不适用': 0 };
|
|
216
|
-
for (const r of
|
|
316
|
+
for (const r of merged.byDimId.values()) {
|
|
217
317
|
if (r._missing)
|
|
218
318
|
continue;
|
|
219
319
|
dimCount[r.level] += 1;
|
|
@@ -222,7 +322,7 @@ async function composerCheckAll(ctx, execFactory) {
|
|
|
222
322
|
}
|
|
223
323
|
// N 条维度 outcome
|
|
224
324
|
const dimOutcomes = dims.map((dim) => {
|
|
225
|
-
const r =
|
|
325
|
+
const r = merged.byDimId.get(dim.id);
|
|
226
326
|
return {
|
|
227
327
|
subId: dim.id,
|
|
228
328
|
labelKey: dim.labelKey,
|
|
@@ -240,35 +340,47 @@ async function composerCheckAll(ctx, execFactory) {
|
|
|
240
340
|
},
|
|
241
341
|
};
|
|
242
342
|
});
|
|
343
|
+
// summary message:多采样时(requested>1)追加采样次数说明,让终端看到"并集来自几次采样"。
|
|
344
|
+
let summaryMessage = tDoctorMessage('cli.doctor.health.summary.message', ctx.lang, {
|
|
345
|
+
overall,
|
|
346
|
+
h: dimCount['健康'], sh: dimCount['亚健康'], bad: dimCount['不健康'], na: dimCount['不适用'],
|
|
347
|
+
err: finding['错误'], warn: finding['警告'], sug: finding['建议'],
|
|
348
|
+
});
|
|
349
|
+
if (requested > 1) {
|
|
350
|
+
summaryMessage += tDoctorMessage(succeeded < requested
|
|
351
|
+
? 'cli.doctor.health.summary.samples_degraded'
|
|
352
|
+
: 'cli.doctor.health.summary.samples', ctx.lang, { requested, succeeded });
|
|
353
|
+
}
|
|
354
|
+
const sampleDegraded = requested > 1 && succeeded < requested;
|
|
243
355
|
// 1 条 summary outcome
|
|
244
356
|
const summaryOutcome = {
|
|
245
357
|
subId: '_summary',
|
|
246
358
|
severity: 'info',
|
|
247
359
|
labelKey: 'cli.doctor.health.summary.label',
|
|
248
|
-
status: '
|
|
249
|
-
message:
|
|
250
|
-
|
|
251
|
-
h: dimCount['健康'], sh: dimCount['亚健康'], bad: dimCount['不健康'], na: dimCount['不适用'],
|
|
252
|
-
err: finding['错误'], warn: finding['警告'], sug: finding['建议'],
|
|
253
|
-
}),
|
|
254
|
-
hint: parseResult.topSuggestions.slice(0, 3).join(' | ')
|
|
360
|
+
status: sampleDegraded ? 'warn' : 'pass',
|
|
361
|
+
message: summaryMessage,
|
|
362
|
+
hint: merged.topSuggestions.slice(0, 3).join(' | ')
|
|
255
363
|
|| tDoctorMessage('cli.doctor.health.summary.no_top', ctx.lang),
|
|
256
364
|
detail: {
|
|
257
365
|
overall,
|
|
366
|
+
// 采样次数:requested=请求几次,succeeded=成功解析几次。finding 的 support=k/n
|
|
367
|
+
// 即"被评估的 n 次里有 k 次报了这条"。mergeMode=归并实现(string / llm / 回退)。
|
|
368
|
+
samples: { requested, succeeded, concurrency, degraded: sampleDegraded },
|
|
369
|
+
mergeMode,
|
|
258
370
|
dimensionLevelCount: dimCount,
|
|
259
371
|
findingCountByLevel: finding,
|
|
260
|
-
topSuggestions:
|
|
261
|
-
featurePoints:
|
|
262
|
-
durationMs:
|
|
263
|
-
durationApiMs:
|
|
264
|
-
// turns:claude-cli 在内部跑的 LLM 轮次(每轮 = 1 次 messages.create)
|
|
372
|
+
topSuggestions: merged.topSuggestions,
|
|
373
|
+
featurePoints: merged.featurePoints,
|
|
374
|
+
durationMs: agg.durationMs,
|
|
375
|
+
durationApiMs: agg.durationApiMs,
|
|
376
|
+
// turns:claude-cli 在内部跑的 LLM 轮次(每轮 = 1 次 messages.create),多采样累加。
|
|
265
377
|
// numTurns:assistant turn 数 / fullNumTurns:含 tool turn 的总数
|
|
266
|
-
llmTurns: { numTurns:
|
|
378
|
+
llmTurns: { numTurns: agg.numTurns, fullNumTurns: agg.fullNumTurns },
|
|
267
379
|
tokens: {
|
|
268
|
-
input:
|
|
269
|
-
cacheRead:
|
|
380
|
+
input: agg.inputTokens, output: agg.outputTokens,
|
|
381
|
+
cacheRead: agg.cacheReadTokens, cacheCreation: agg.cacheCreationTokens,
|
|
270
382
|
},
|
|
271
|
-
costUSD:
|
|
383
|
+
costUSD: agg.costUSD,
|
|
272
384
|
executor: { name: ctx.executorName, model: ctx.model },
|
|
273
385
|
},
|
|
274
386
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 多采样共识合并 —— self-consistency 抹平单次采样方差。
|
|
3
|
+
*
|
|
4
|
+
* composer 串行跑 N 次 LLM,各次产出独立的 HealthParseResult。本模块把这 N 份
|
|
5
|
+
* 结果**按维度取并集**:同根因 finding 跨采样合并成一条,标注支持度 `k/n`
|
|
6
|
+
* (该维度被评估的 n 次里,有 k 次报了这条)。**默认不过滤** —— 召回优先,
|
|
7
|
+
* 低支持度的也保留并如实标注,让重复体检看到的是"并集",而不是每次抖动的子集。
|
|
8
|
+
*
|
|
9
|
+
* 两种归并实现,共用 assembleDimResult 出维度结果:
|
|
10
|
+
* - mergeHealthSamples(string,默认):按 findingKey(反引号锚点 / 归一化描述前缀)
|
|
11
|
+
* 聚类。便宜、无额外 LLM 调用;缺点是同根因不同措辞可能漏并(拆成多条低支持度)。
|
|
12
|
+
* - mergeHealthSamplesLlm(option C):用一次 LLM 聚类(skill-health-merge prompt)的
|
|
13
|
+
* 结果分组,跨措辞归并最准;composer 在 healthMerge='llm' 时调用,失败回退 string。
|
|
14
|
+
*/
|
|
15
|
+
import type { HealthDimensionSpec, HealthFinding, HealthFindingLevel } from './dimension-spec.js';
|
|
16
|
+
import type { HealthParseResult } from './parser.js';
|
|
17
|
+
/**
|
|
18
|
+
* 同根因合并键。锚点优先(跨采样最稳),退回归一化描述前缀。
|
|
19
|
+
* 不含 level:同一问题不同采样 level 可能不同,合并时单独取最严重。
|
|
20
|
+
*/
|
|
21
|
+
export declare function findingKey(f: HealthFinding): string;
|
|
22
|
+
export declare function mergeHealthSamples(samples: HealthParseResult[], dims: HealthDimensionSpec[]): HealthParseResult;
|
|
23
|
+
/** 带稳定 id 的采样 finding(喂给 merge prompt + 回填分组)。 */
|
|
24
|
+
export interface TaggedFinding {
|
|
25
|
+
id: string;
|
|
26
|
+
sampleIdx: number;
|
|
27
|
+
dimId: string;
|
|
28
|
+
finding: HealthFinding;
|
|
29
|
+
}
|
|
30
|
+
/** 枚举所有采样里所有维度的 finding,赋稳定 id(f0, f1, …),供 LLM 聚类引用。 */
|
|
31
|
+
export declare function enumerateFindingsForMerge(samples: HealthParseResult[], dims: HealthDimensionSpec[]): TaggedFinding[];
|
|
32
|
+
/** merge LLM 输出的一个聚类。 */
|
|
33
|
+
export interface MergeCluster {
|
|
34
|
+
dimId: string;
|
|
35
|
+
findingIds: string[];
|
|
36
|
+
level?: HealthFindingLevel;
|
|
37
|
+
description?: string;
|
|
38
|
+
suggestion?: string;
|
|
39
|
+
}
|
|
40
|
+
/** 解析 merge LLM 的 clusters JSON(已 extractJson)。非法返回 null → composer 回退 string。 */
|
|
41
|
+
export declare function parseMergeClusters(jsonText: string): MergeCluster[] | null;
|
|
42
|
+
/** 按 LLM 聚类结果合并。未被任何 cluster 覆盖的 finding 兜底自成一组(绝不丢)。 */
|
|
43
|
+
export declare function mergeHealthSamplesLlm(samples: HealthParseResult[], dims: HealthDimensionSpec[], tagged: TaggedFinding[], clusters: MergeCluster[]): HealthParseResult;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 多采样共识合并 —— self-consistency 抹平单次采样方差。
|
|
3
|
+
*
|
|
4
|
+
* composer 串行跑 N 次 LLM,各次产出独立的 HealthParseResult。本模块把这 N 份
|
|
5
|
+
* 结果**按维度取并集**:同根因 finding 跨采样合并成一条,标注支持度 `k/n`
|
|
6
|
+
* (该维度被评估的 n 次里,有 k 次报了这条)。**默认不过滤** —— 召回优先,
|
|
7
|
+
* 低支持度的也保留并如实标注,让重复体检看到的是"并集",而不是每次抖动的子集。
|
|
8
|
+
*
|
|
9
|
+
* 两种归并实现,共用 assembleDimResult 出维度结果:
|
|
10
|
+
* - mergeHealthSamples(string,默认):按 findingKey(反引号锚点 / 归一化描述前缀)
|
|
11
|
+
* 聚类。便宜、无额外 LLM 调用;缺点是同根因不同措辞可能漏并(拆成多条低支持度)。
|
|
12
|
+
* - mergeHealthSamplesLlm(option C):用一次 LLM 聚类(skill-health-merge prompt)的
|
|
13
|
+
* 结果分组,跨措辞归并最准;composer 在 healthMerge='llm' 时调用,失败回退 string。
|
|
14
|
+
*/
|
|
15
|
+
import { deriveOverallHealth } from './parser.js';
|
|
16
|
+
const FINDING_SEVERITY = { 错误: 3, 警告: 2, 建议: 1 };
|
|
17
|
+
function isFindingLevel(s) {
|
|
18
|
+
return s === '错误' || s === '警告' || s === '建议';
|
|
19
|
+
}
|
|
20
|
+
function deriveDimLevel(findings) {
|
|
21
|
+
if (findings.some((f) => f.level === '错误'))
|
|
22
|
+
return '不健康';
|
|
23
|
+
if (findings.length > 0)
|
|
24
|
+
return '亚健康';
|
|
25
|
+
return '健康';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 同根因合并键。锚点优先(跨采样最稳),退回归一化描述前缀。
|
|
29
|
+
* 不含 level:同一问题不同采样 level 可能不同,合并时单独取最严重。
|
|
30
|
+
*/
|
|
31
|
+
export function findingKey(f) {
|
|
32
|
+
const text = `${f.description ?? ''} ${f.suggestion ?? ''}`;
|
|
33
|
+
const anchors = [...text.matchAll(/`([^`]+)`/g)]
|
|
34
|
+
.map((m) => m[1].trim().toLowerCase())
|
|
35
|
+
.filter((s) => s.length > 0);
|
|
36
|
+
if (anchors.length > 0) {
|
|
37
|
+
return `a:${[...new Set(anchors)].sort().join(',')}`;
|
|
38
|
+
}
|
|
39
|
+
// 去掉空白 / 标点 / 符号后取前缀(中文字符保留)
|
|
40
|
+
const norm = (f.description ?? '').toLowerCase().replace(/[\s\p{P}\p{S}]+/gu, '');
|
|
41
|
+
return `d:${norm.slice(0, 48)}`;
|
|
42
|
+
}
|
|
43
|
+
/** 由分组好的 FindingGroup 组装一个维度结果(support、排序、维度 level 派生)。 */
|
|
44
|
+
function assembleDimResult(presentLevels, n, groups, suggestions) {
|
|
45
|
+
const findings = groups.map((g) => {
|
|
46
|
+
// representative 取描述更详尽的一条;level 取组内最严重(除非 LLM 显式给了)。
|
|
47
|
+
const rep = [...g.members].sort((a, b) => (b.description?.length ?? 0) - (a.description?.length ?? 0))[0];
|
|
48
|
+
const worst = g.members.reduce((acc, f) => (FINDING_SEVERITY[f.level] > FINDING_SEVERITY[acc] ? f.level : acc), '建议');
|
|
49
|
+
return {
|
|
50
|
+
...rep,
|
|
51
|
+
// override.level 只能"升"不能"降":worst(组内最严重)是下限,LLM 聚类觉得更严重才采纳。
|
|
52
|
+
// 否则一次归并能把真 错误 降成 警告(维度 不健康→亚健康),把真问题藏掉。
|
|
53
|
+
level: g.override?.level && FINDING_SEVERITY[g.override.level] > FINDING_SEVERITY[worst]
|
|
54
|
+
? g.override.level
|
|
55
|
+
: worst,
|
|
56
|
+
description: g.override?.description ?? rep.description,
|
|
57
|
+
suggestion: g.override?.suggestion ?? rep.suggestion,
|
|
58
|
+
support: { k: g.sampleIdxs.size, n },
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
// 排序:支持度高在前,其次错误优先 —— 最可信、最严重的排最前。
|
|
62
|
+
findings.sort((x, y) => {
|
|
63
|
+
const sk = (y.support?.k ?? 0) - (x.support?.k ?? 0);
|
|
64
|
+
if (sk !== 0)
|
|
65
|
+
return sk;
|
|
66
|
+
return FINDING_SEVERITY[y.level] - FINDING_SEVERITY[x.level];
|
|
67
|
+
});
|
|
68
|
+
// 零 finding 时:只有所有采样都判不适用才算不适用(任一判健康 = 适用且健康)。
|
|
69
|
+
const level = findings.length > 0
|
|
70
|
+
? deriveDimLevel(findings)
|
|
71
|
+
: presentLevels.every((l) => l === '不适用') ? '不适用' : '健康';
|
|
72
|
+
return { level, findings, suggestions };
|
|
73
|
+
}
|
|
74
|
+
/** 该维度被"实际评估"过的采样(present 且非 _missing;健康=0 finding 也算评估过)。 */
|
|
75
|
+
function presentSamplesFor(samples, dimId) {
|
|
76
|
+
const out = [];
|
|
77
|
+
samples.forEach((s, idx) => {
|
|
78
|
+
const r = s.byDimId.get(dimId);
|
|
79
|
+
if (r != null && !r._missing)
|
|
80
|
+
out.push({ idx, r });
|
|
81
|
+
});
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function finalize(byDimId, samples) {
|
|
85
|
+
const topSet = new Set();
|
|
86
|
+
for (const s of samples)
|
|
87
|
+
for (const t of s.topSuggestions)
|
|
88
|
+
topSet.add(t);
|
|
89
|
+
const featurePoints = samples.find((s) => s.featurePoints.length > 0)?.featurePoints ?? [];
|
|
90
|
+
return { byDimId, overall: deriveOverallHealth(byDimId), topSuggestions: [...topSet], featurePoints };
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// string 归并(默认,findingKey 聚类)
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
export function mergeHealthSamples(samples, dims) {
|
|
96
|
+
const byDimId = new Map();
|
|
97
|
+
for (const dim of dims) {
|
|
98
|
+
const present = presentSamplesFor(samples, dim.id);
|
|
99
|
+
if (present.length === 0) {
|
|
100
|
+
byDimId.set(dim.id, { level: '不适用', findings: [], suggestions: [], _missing: true });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const suggestionsSet = new Set();
|
|
104
|
+
const groupMap = new Map();
|
|
105
|
+
for (const { idx, r } of present) {
|
|
106
|
+
for (const sug of r.suggestions)
|
|
107
|
+
suggestionsSet.add(sug);
|
|
108
|
+
for (const f of r.findings) {
|
|
109
|
+
const key = findingKey(f);
|
|
110
|
+
let g = groupMap.get(key);
|
|
111
|
+
if (!g) {
|
|
112
|
+
g = { members: [], sampleIdxs: new Set() };
|
|
113
|
+
groupMap.set(key, g);
|
|
114
|
+
}
|
|
115
|
+
g.members.push(f);
|
|
116
|
+
g.sampleIdxs.add(idx);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// n(支持度分母)= 已评估且适用的采样数;不适用采样不算"评估过这条 finding"。
|
|
120
|
+
const assessedN = present.filter((p) => p.r.level !== '不适用').length;
|
|
121
|
+
byDimId.set(dim.id, assembleDimResult(present.map((p) => p.r.level), assessedN, [...groupMap.values()], [...suggestionsSet]));
|
|
122
|
+
}
|
|
123
|
+
return finalize(byDimId, samples);
|
|
124
|
+
}
|
|
125
|
+
/** 枚举所有采样里所有维度的 finding,赋稳定 id(f0, f1, …),供 LLM 聚类引用。 */
|
|
126
|
+
export function enumerateFindingsForMerge(samples, dims) {
|
|
127
|
+
const out = [];
|
|
128
|
+
let counter = 0;
|
|
129
|
+
samples.forEach((s, sampleIdx) => {
|
|
130
|
+
for (const dim of dims) {
|
|
131
|
+
const r = s.byDimId.get(dim.id);
|
|
132
|
+
if (!r || r._missing)
|
|
133
|
+
continue;
|
|
134
|
+
for (const f of r.findings) {
|
|
135
|
+
out.push({ id: `f${counter}`, sampleIdx, dimId: dim.id, finding: f });
|
|
136
|
+
counter += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** 解析 merge LLM 的 clusters JSON(已 extractJson)。非法返回 null → composer 回退 string。 */
|
|
143
|
+
export function parseMergeClusters(jsonText) {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(jsonText);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (!parsed || typeof parsed !== 'object')
|
|
152
|
+
return null;
|
|
153
|
+
const raw = parsed.clusters;
|
|
154
|
+
if (!Array.isArray(raw))
|
|
155
|
+
return null;
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const item of raw) {
|
|
158
|
+
if (!item || typeof item !== 'object')
|
|
159
|
+
continue;
|
|
160
|
+
const c = item;
|
|
161
|
+
if (typeof c.dim_id !== 'string')
|
|
162
|
+
continue;
|
|
163
|
+
if (!Array.isArray(c.finding_ids))
|
|
164
|
+
continue;
|
|
165
|
+
const ids = c.finding_ids.filter((x) => typeof x === 'string');
|
|
166
|
+
if (ids.length === 0)
|
|
167
|
+
continue;
|
|
168
|
+
out.push({
|
|
169
|
+
dimId: c.dim_id,
|
|
170
|
+
findingIds: ids,
|
|
171
|
+
level: isFindingLevel(c.level) ? c.level : undefined,
|
|
172
|
+
description: typeof c.description === 'string' && c.description.trim() ? c.description : undefined,
|
|
173
|
+
suggestion: typeof c.suggestion === 'string' && c.suggestion.trim() ? c.suggestion : undefined,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return out.length > 0 ? out : null;
|
|
177
|
+
}
|
|
178
|
+
/** 按 LLM 聚类结果合并。未被任何 cluster 覆盖的 finding 兜底自成一组(绝不丢)。 */
|
|
179
|
+
export function mergeHealthSamplesLlm(samples, dims, tagged, clusters) {
|
|
180
|
+
const byId = new Map(tagged.map((t) => [t.id, t]));
|
|
181
|
+
const covered = new Set();
|
|
182
|
+
const groupsByDim = new Map();
|
|
183
|
+
const addGroup = (dimId, g) => {
|
|
184
|
+
if (!groupsByDim.has(dimId))
|
|
185
|
+
groupsByDim.set(dimId, []);
|
|
186
|
+
groupsByDim.get(dimId).push(g);
|
|
187
|
+
};
|
|
188
|
+
for (const c of clusters) {
|
|
189
|
+
const members = c.findingIds
|
|
190
|
+
.map((id) => byId.get(id))
|
|
191
|
+
.filter((t) => t != null && t.dimId === c.dimId && !covered.has(t.id));
|
|
192
|
+
if (members.length === 0)
|
|
193
|
+
continue;
|
|
194
|
+
for (const m of members)
|
|
195
|
+
covered.add(m.id);
|
|
196
|
+
addGroup(c.dimId, {
|
|
197
|
+
members: members.map((m) => m.finding),
|
|
198
|
+
sampleIdxs: new Set(members.map((m) => m.sampleIdx)),
|
|
199
|
+
override: { description: c.description, suggestion: c.suggestion, level: c.level },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// 兜底:LLM 漏分配的 finding 自成一组,绝不丢。
|
|
203
|
+
for (const t of tagged) {
|
|
204
|
+
if (covered.has(t.id))
|
|
205
|
+
continue;
|
|
206
|
+
covered.add(t.id);
|
|
207
|
+
addGroup(t.dimId, { members: [t.finding], sampleIdxs: new Set([t.sampleIdx]) });
|
|
208
|
+
}
|
|
209
|
+
const byDimId = new Map();
|
|
210
|
+
for (const dim of dims) {
|
|
211
|
+
const present = presentSamplesFor(samples, dim.id);
|
|
212
|
+
if (present.length === 0) {
|
|
213
|
+
byDimId.set(dim.id, { level: '不适用', findings: [], suggestions: [], _missing: true });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const suggestionsSet = new Set();
|
|
217
|
+
for (const { r } of present)
|
|
218
|
+
for (const sug of r.suggestions)
|
|
219
|
+
suggestionsSet.add(sug);
|
|
220
|
+
const assessedN = present.filter((p) => p.r.level !== '不适用').length;
|
|
221
|
+
byDimId.set(dim.id, assembleDimResult(present.map((p) => p.r.level), assessedN, groupsByDim.get(dim.id) ?? [], [...suggestionsSet]));
|
|
222
|
+
}
|
|
223
|
+
return finalize(byDimId, samples);
|
|
224
|
+
}
|
|
@@ -19,6 +19,12 @@ export interface HealthFinding {
|
|
|
19
19
|
evidence: string;
|
|
20
20
|
description: string;
|
|
21
21
|
suggestion?: string;
|
|
22
|
+
/** 多采样共识的支持度:N 次采样里有 k 次报了这条(同根因)finding。
|
|
23
|
+
* 单次采样(healthSamples=1)时不设或为 {k:1,n:1};renderer 仅在 n>1 时展示。 */
|
|
24
|
+
support?: {
|
|
25
|
+
k: number;
|
|
26
|
+
n: number;
|
|
27
|
+
};
|
|
22
28
|
}
|
|
23
29
|
export interface HealthDimensionResult {
|
|
24
30
|
level: HealthDimensionLevel;
|
package/dist/doctor/index.js
CHANGED
|
@@ -220,6 +220,9 @@ export async function runDoctor(opts) {
|
|
|
220
220
|
timeoutMs: opts.timeoutMs,
|
|
221
221
|
effort: opts.effort,
|
|
222
222
|
runHealthCheck: opts.runHealthCheck ?? false,
|
|
223
|
+
healthSamples: opts.healthSamples,
|
|
224
|
+
healthMerge: opts.healthMerge,
|
|
225
|
+
healthConcurrency: opts.healthConcurrency,
|
|
223
226
|
};
|
|
224
227
|
const skillReports = [];
|
|
225
228
|
const totals = { pass: 0, warn: 0, fail: 0 };
|
|
@@ -14,7 +14,7 @@ interface MessageEntry {
|
|
|
14
14
|
zh: string;
|
|
15
15
|
en: string;
|
|
16
16
|
}
|
|
17
|
-
export type DoctorMessageKey = 'cli.doctor.rule.skill_readable' | 'cli.doctor.rule.skill_metadata' | 'cli.doctor.rule.dependencies' | 'cli.doctor.rule.samples_contract' | 'cli.doctor.rule.skill_health_check' | 'cli.doctor.health.skipped' | 'cli.doctor.health.no_dimensions' | 'cli.doctor.health.fail.executor' | 'cli.doctor.health.fail.parse' | 'cli.doctor.health.fail.empty_output' | 'cli.doctor.health.hint.executor' | 'cli.doctor.health.hint.parse' | 'cli.doctor.health.dim.message' | 'cli.doctor.health.dim.missing' | 'cli.doctor.health.summary.label' | 'cli.doctor.health.summary.message' | 'cli.doctor.health.summary.no_top' | 'cli.doctor.health.dim.trigger-boundary' | 'cli.doctor.health.dim.doc-clarity' | 'cli.doctor.health.dim.instr-precision' | 'cli.doctor.health.dim.dependency' | 'cli.doctor.health.dim.tool-conventions' | 'cli.doctor.health.dim.security' | 'cli.doctor.health.dim.examples' | 'cli.doctor.skill_readable.pass' | 'cli.doctor.skill_metadata.pass' | 'cli.doctor.dependencies.pass' | 'cli.doctor.samples_contract.pass' | 'cli.doctor.skill_readable.fail.missing' | 'cli.doctor.skill_readable.fail.empty' | 'cli.doctor.skill_readable.fail.too_short' | 'cli.doctor.skill_readable.hint.missing' | 'cli.doctor.skill_readable.hint.too_short' | 'cli.doctor.skill_metadata.fail.frontmatter_invalid' | 'cli.doctor.skill_metadata.fail.hardrules_invalid' | 'cli.doctor.skill_metadata.fail.workflows_invalid' | 'cli.doctor.skill_metadata.fail.missing_skillmd' | 'cli.doctor.skill_metadata.hint.frontmatter' | 'cli.doctor.skill_metadata.hint.hardrules' | 'cli.doctor.skill_metadata.hint.workflows' | 'cli.doctor.skill_metadata.hint.missing_skillmd' | 'cli.doctor.dependencies.fail' | 'cli.doctor.dependencies.hint.tool' | 'cli.doctor.dependencies.hint.file' | 'cli.doctor.dependencies.hint.env' | 'cli.doctor.dependencies.hint.preflight' | 'cli.doctor.dependencies.issue.tool_not_found' | 'cli.doctor.dependencies.issue.file_not_found' | 'cli.doctor.dependencies.issue.env_not_set' | 'cli.doctor.dependencies.issue.preflight_failed' | 'cli.doctor.samples_contract.skipped' | 'cli.doctor.samples_contract.warn.empty' | 'cli.doctor.samples_contract.warn.missing_prompt' | 'cli.doctor.samples_contract.hint';
|
|
17
|
+
export type DoctorMessageKey = 'cli.doctor.rule.skill_readable' | 'cli.doctor.rule.skill_metadata' | 'cli.doctor.rule.dependencies' | 'cli.doctor.rule.samples_contract' | 'cli.doctor.rule.skill_health_check' | 'cli.doctor.health.skipped' | 'cli.doctor.health.no_dimensions' | 'cli.doctor.health.fail.executor' | 'cli.doctor.health.fail.parse' | 'cli.doctor.health.fail.empty_output' | 'cli.doctor.health.hint.executor' | 'cli.doctor.health.hint.parse' | 'cli.doctor.health.dim.message' | 'cli.doctor.health.dim.missing' | 'cli.doctor.health.summary.label' | 'cli.doctor.health.summary.message' | 'cli.doctor.health.summary.samples' | 'cli.doctor.health.summary.samples_degraded' | 'cli.doctor.health.summary.no_top' | 'cli.doctor.health.dim.trigger-boundary' | 'cli.doctor.health.dim.doc-clarity' | 'cli.doctor.health.dim.instr-precision' | 'cli.doctor.health.dim.dependency' | 'cli.doctor.health.dim.tool-conventions' | 'cli.doctor.health.dim.security' | 'cli.doctor.health.dim.examples' | 'cli.doctor.skill_readable.pass' | 'cli.doctor.skill_metadata.pass' | 'cli.doctor.dependencies.pass' | 'cli.doctor.samples_contract.pass' | 'cli.doctor.skill_readable.fail.missing' | 'cli.doctor.skill_readable.fail.empty' | 'cli.doctor.skill_readable.fail.too_short' | 'cli.doctor.skill_readable.hint.missing' | 'cli.doctor.skill_readable.hint.too_short' | 'cli.doctor.skill_metadata.fail.frontmatter_invalid' | 'cli.doctor.skill_metadata.fail.hardrules_invalid' | 'cli.doctor.skill_metadata.fail.workflows_invalid' | 'cli.doctor.skill_metadata.fail.missing_skillmd' | 'cli.doctor.skill_metadata.hint.frontmatter' | 'cli.doctor.skill_metadata.hint.hardrules' | 'cli.doctor.skill_metadata.hint.workflows' | 'cli.doctor.skill_metadata.hint.missing_skillmd' | 'cli.doctor.dependencies.fail' | 'cli.doctor.dependencies.hint.tool' | 'cli.doctor.dependencies.hint.file' | 'cli.doctor.dependencies.hint.env' | 'cli.doctor.dependencies.hint.preflight' | 'cli.doctor.dependencies.issue.tool_not_found' | 'cli.doctor.dependencies.issue.file_not_found' | 'cli.doctor.dependencies.issue.env_not_set' | 'cli.doctor.dependencies.issue.preflight_failed' | 'cli.doctor.samples_contract.skipped' | 'cli.doctor.samples_contract.warn.empty' | 'cli.doctor.samples_contract.warn.missing_prompt' | 'cli.doctor.samples_contract.hint';
|
|
18
18
|
export declare const DOCTOR_MESSAGES: Record<DoctorMessageKey, MessageEntry>;
|
|
19
19
|
export declare function tDoctorMessage(key: DoctorMessageKey, lang?: Lang, params?: Record<string, string | number>): string;
|
|
20
20
|
export {};
|
package/dist/doctor/messages.js
CHANGED
|
@@ -65,6 +65,14 @@ export const DOCTOR_MESSAGES = {
|
|
|
65
65
|
zh: '{overall} | 维度: 健康 {h}/亚健康 {sh}/不健康 {bad}/不适用 {na} | finding: 错误 {err}/警告 {warn}/建议 {sug}',
|
|
66
66
|
en: '{overall} | dims: healthy {h}/sub {sh}/unhealthy {bad}/n-a {na} | findings: err {err}/warn {warn}/sug {sug}',
|
|
67
67
|
},
|
|
68
|
+
'cli.doctor.health.summary.samples': {
|
|
69
|
+
zh: ' | 采样 {succeeded}/{requested} 次(并集去重,finding 标注 k/n 支持度)',
|
|
70
|
+
en: ' | sampled {succeeded}/{requested} (unioned & deduped; findings tagged k/n support)',
|
|
71
|
+
},
|
|
72
|
+
'cli.doctor.health.summary.samples_degraded': {
|
|
73
|
+
zh: ' | 采样仅成功解析 {succeeded}/{requested} 次(已降级,finding 的 k/n 只基于成功样本)',
|
|
74
|
+
en: ' | parsed only {succeeded}/{requested} samples (degraded; k/n support uses successful samples only)',
|
|
75
|
+
},
|
|
68
76
|
'cli.doctor.health.summary.no_top': {
|
|
69
77
|
zh: '完整详情见 --json 输出或 --html 报告',
|
|
70
78
|
en: 'Full detail in --json output or --html report',
|
package/dist/doctor/rules.d.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* omk doctor 内置规则注册表。
|
|
3
3
|
*
|
|
4
4
|
* BUILTIN_RULES 是**纯静态/低成本检查**, 不碰 LLM 连通性 — executor / judge
|
|
5
|
-
* 连通性由 evaluation preflight 负责。用户入口 `omk doctor`
|
|
6
|
-
* skill_health composer 做 LLM 审计;`--static-only`
|
|
5
|
+
* 连通性由 evaluation preflight 负责。用户入口 `omk doctor` 默认会跑这些静态
|
|
6
|
+
* 规则,再跑 skill_health composer 做 LLM 审计;`--static-only` 只跑静态规则。
|
|
7
|
+
* samples_contract_aligned 需要 samples,仍只归 eval preflight
|
|
8
|
+
* (run-evaluation.ts 经 runDoctor 调,dependencies_present 守依赖完整性)。
|
|
7
9
|
*
|
|
8
10
|
* 每条 rule 回答一个独立的「skill 能不能被有意义评测」子问题:
|
|
9
11
|
* - skill_readable: 文件能读、内容非空且有最小长度
|
package/dist/doctor/rules.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* omk doctor 内置规则注册表。
|
|
3
3
|
*
|
|
4
4
|
* BUILTIN_RULES 是**纯静态/低成本检查**, 不碰 LLM 连通性 — executor / judge
|
|
5
|
-
* 连通性由 evaluation preflight 负责。用户入口 `omk doctor`
|
|
6
|
-
* skill_health composer 做 LLM 审计;`--static-only`
|
|
5
|
+
* 连通性由 evaluation preflight 负责。用户入口 `omk doctor` 默认会跑这些静态
|
|
6
|
+
* 规则,再跑 skill_health composer 做 LLM 审计;`--static-only` 只跑静态规则。
|
|
7
|
+
* samples_contract_aligned 需要 samples,仍只归 eval preflight
|
|
8
|
+
* (run-evaluation.ts 经 runDoctor 调,dependencies_present 守依赖完整性)。
|
|
7
9
|
*
|
|
8
10
|
* 每条 rule 回答一个独立的「skill 能不能被有意义评测」子问题:
|
|
9
11
|
* - skill_readable: 文件能读、内容非空且有最小长度
|