oh-my-knowledge 0.43.0 → 0.45.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.
@@ -506,7 +506,7 @@ export function renderDoctorEvidenceCard(graph, skill, lang) {
506
506
  ];
507
507
  const hiddenStructure = renderStructureDetails(graph, lang);
508
508
  return [
509
- `## ${zh ? 'Skill Evidence Card' : 'Skill Evidence Card'}:${skill.skillName}`,
509
+ `## ${zh ? '知识图谱摘要' : 'Skill Map Summary'}${zh ? ':' : ': '}${skill.skillName}`,
510
510
  '',
511
511
  statusSentence,
512
512
  '',
@@ -0,0 +1,16 @@
1
+ import type { ArtifactGraphDocument, EvaluationReport } from '../types/index.js';
2
+ export interface BuildEvalGraphOptions {
3
+ report: EvaluationReport;
4
+ sourcePath: string;
5
+ generatedAt?: string;
6
+ }
7
+ export interface PersistEvalGraphOptions extends BuildEvalGraphOptions {
8
+ outputDir: string;
9
+ fileStem?: string;
10
+ }
11
+ export interface PersistEvalGraphResult {
12
+ graphPath: string;
13
+ }
14
+ export declare function evalGraphDirForReportOutput(reportOutputDir: string): string;
15
+ export declare function buildEvalArtifactGraph(options: BuildEvalGraphOptions): ArtifactGraphDocument;
16
+ export declare function persistEvalGraphSidecar(options: PersistEvalGraphOptions): PersistEvalGraphResult;
@@ -0,0 +1,471 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { graphFileName } from '../eval-core/artifact-file-names.js';
5
+ function shortHash(input) {
6
+ return createHash('sha256').update(input).digest('hex').slice(0, 12);
7
+ }
8
+ function jsonPointerToken(value) {
9
+ return value.replaceAll('~', '~0').replaceAll('/', '~1');
10
+ }
11
+ function sampleSetHash(sampleHashes) {
12
+ if (!sampleHashes || Object.keys(sampleHashes).length === 0)
13
+ return undefined;
14
+ const canonical = Object.entries(sampleHashes)
15
+ .sort(([a], [b]) => a.localeCompare(b))
16
+ .map(([id, hash]) => `${id}:${hash}`)
17
+ .join('|');
18
+ return shortHash(canonical);
19
+ }
20
+ function statusFromScore(score) {
21
+ // Display band only. This is not the verdict gate and intentionally does not
22
+ // reference DEFAULT_GATE_THRESHOLD or statistical significance decisions.
23
+ if (score === undefined || !Number.isFinite(score))
24
+ return 'unknown';
25
+ if (score >= 4)
26
+ return 'ok';
27
+ if (score >= 3)
28
+ return 'warning';
29
+ return 'failed';
30
+ }
31
+ function assertionStatus(result) {
32
+ // Assertion topology status only. Pure LLM-scored samples have no assertion
33
+ // pass/fail edge, so they stay unknown here even when compositeScore is high.
34
+ if (result.error || !result.ok)
35
+ return 'failed';
36
+ const details = result.assertions?.details;
37
+ if (!details || details.length === 0)
38
+ return 'unknown';
39
+ return details.every((detail) => detail.passed) ? 'ok' : 'failed';
40
+ }
41
+ function artifactBinding(variant, hash) {
42
+ if (hash && hash !== 'no-skill') {
43
+ return { bindingStrength: 'content-hash', keys: { artifactHash: hash } };
44
+ }
45
+ return { bindingStrength: 'name-only', keys: { variantName: variant } };
46
+ }
47
+ function sampleBinding(sampleId, hash) {
48
+ if (hash) {
49
+ return { bindingStrength: 'content-hash', keys: { sampleHash: hash } };
50
+ }
51
+ return { bindingStrength: 'name-only', keys: { sampleId } };
52
+ }
53
+ function variantConfigByName(report) {
54
+ return new Map((report.meta.variantConfigs ?? []).map((config) => [config.variant, config]));
55
+ }
56
+ function scopeArtifactKind(report) {
57
+ const kinds = new Set((report.meta.variantConfigs ?? [])
58
+ .map((config) => config.artifactKind)
59
+ .filter((kind) => kind !== 'baseline'));
60
+ return kinds.size === 1 ? [...kinds][0] : undefined;
61
+ }
62
+ function sampleEvidence(report, sampleId) {
63
+ return [{
64
+ sourceKind: 'sample',
65
+ sourceId: sampleId,
66
+ selector: { selectorKind: 'sample-id', value: sampleId },
67
+ contentHash: report.meta.sampleHashes?.[sampleId],
68
+ label: sampleId,
69
+ }];
70
+ }
71
+ function evalResultEvidence(report, resultIndex, variant) {
72
+ return [{
73
+ sourceKind: 'eval-report',
74
+ sourceId: report.id,
75
+ selector: {
76
+ selectorKind: 'json-pointer',
77
+ value: `/results/${resultIndex}/variants/${jsonPointerToken(variant)}`,
78
+ },
79
+ label: `${variant} result`,
80
+ }];
81
+ }
82
+ function assertionEvidence(report, sampleId, index, resultIndex, variant) {
83
+ if (report.sampleSnapshots?.[sampleId]?.assertions?.[index]) {
84
+ return [{
85
+ sourceKind: 'sample',
86
+ sourceId: sampleId,
87
+ selector: {
88
+ selectorKind: 'json-pointer',
89
+ value: `/sampleSnapshots/${jsonPointerToken(sampleId)}/assertions/${index}`,
90
+ },
91
+ contentHash: report.meta.sampleHashes?.[sampleId],
92
+ label: `assertion ${index + 1}`,
93
+ }];
94
+ }
95
+ if (resultIndex !== undefined && variant !== undefined) {
96
+ return [{
97
+ sourceKind: 'eval-report',
98
+ sourceId: report.id,
99
+ selector: {
100
+ selectorKind: 'json-pointer',
101
+ value: `/results/${resultIndex}/variants/${jsonPointerToken(variant)}/assertions/details/${index}`,
102
+ },
103
+ label: `${variant} assertion ${index + 1}`,
104
+ }];
105
+ }
106
+ return [{
107
+ sourceKind: 'sample',
108
+ sourceId: sampleId,
109
+ contentHash: report.meta.sampleHashes?.[sampleId],
110
+ label: `assertion ${index + 1}`,
111
+ }];
112
+ }
113
+ function sampleStableKey(report, sampleId) {
114
+ const hash = report.meta.sampleHashes?.[sampleId];
115
+ return hash ? `v1:sample:${hash}` : `v1:sample:${report.id}:${sampleId}`;
116
+ }
117
+ function assertionStableKey(report, sampleId, index) {
118
+ return `${sampleStableKey(report, sampleId)}:assertion:${index}`;
119
+ }
120
+ function sampleAttrs(snapshot) {
121
+ if (!snapshot)
122
+ return undefined;
123
+ const display = {};
124
+ if (snapshot.capability?.length)
125
+ display.capability = snapshot.capability;
126
+ if (snapshot.construct)
127
+ display.construct = snapshot.construct;
128
+ if (snapshot.difficulty)
129
+ display.difficulty = snapshot.difficulty;
130
+ if (snapshot.provenance)
131
+ display.provenance = snapshot.provenance;
132
+ if (snapshot.tripwire)
133
+ display.tripwire = true;
134
+ if (snapshot.assertions?.length)
135
+ display.assertionCount = snapshot.assertions.length;
136
+ if (snapshot.covers?.length)
137
+ display.declaredCoverageTargetCount = snapshot.covers.length;
138
+ return Object.keys(display).length > 0 ? { display } : undefined;
139
+ }
140
+ const COVERAGE_TARGET_NODE_KIND = {
141
+ skill: 'skill',
142
+ skill_file: 'skill_file',
143
+ frontmatter: 'frontmatter',
144
+ reference: 'reference',
145
+ script: 'script',
146
+ hard_rule: 'hard_rule',
147
+ workflow: 'workflow',
148
+ workflow_node: 'workflow_node',
149
+ };
150
+ function normalizeCoverageRef(target) {
151
+ const raw = target.ref.trim().replaceAll('\\', '/');
152
+ if (target.targetKind === 'reference' || target.targetKind === 'script' || target.targetKind === 'skill_file') {
153
+ return raw.replace(/^\/+/, '').replace(/^\.\//, '');
154
+ }
155
+ return raw;
156
+ }
157
+ function coverageTargetStableKey(target, artifactHash) {
158
+ const ref = normalizeCoverageRef(target);
159
+ switch (target.targetKind) {
160
+ case 'skill':
161
+ return `v1:skill:${artifactHash}`;
162
+ case 'skill_file':
163
+ return `v1:skill-file:${artifactHash}:${ref || 'SKILL.md'}`;
164
+ case 'frontmatter':
165
+ return `v1:frontmatter:${artifactHash}`;
166
+ case 'reference':
167
+ return `v1:reference:${artifactHash}:${ref}`;
168
+ case 'script':
169
+ return `v1:script:${artifactHash}:${ref}`;
170
+ case 'hard_rule':
171
+ return `v1:hard-rule:${artifactHash}:${ref}`;
172
+ case 'workflow':
173
+ return `v1:workflow:${artifactHash}:${ref}`;
174
+ case 'workflow_node':
175
+ return `v1:workflow-node:${artifactHash}:${ref}`;
176
+ }
177
+ }
178
+ function coverageTargetLabel(target) {
179
+ const ref = normalizeCoverageRef(target);
180
+ switch (target.targetKind) {
181
+ case 'skill':
182
+ return ref && ref !== 'skill' ? ref : 'SKILL.md';
183
+ case 'skill_file':
184
+ return ref || 'SKILL.md';
185
+ case 'frontmatter':
186
+ return 'frontmatter';
187
+ default:
188
+ return ref;
189
+ }
190
+ }
191
+ function coverageEvidence(report, sampleId, targetIndex, target) {
192
+ return [{
193
+ sourceKind: 'sample',
194
+ sourceId: sampleId,
195
+ selector: {
196
+ selectorKind: 'json-pointer',
197
+ value: `/sampleSnapshots/${jsonPointerToken(sampleId)}/covers/${targetIndex}`,
198
+ },
199
+ contentHash: report.meta.sampleHashes?.[sampleId],
200
+ label: `${sampleId} covers ${target.targetKind}:${normalizeCoverageRef(target)}`,
201
+ }];
202
+ }
203
+ export function evalGraphDirForReportOutput(reportOutputDir) {
204
+ return basename(reportOutputDir) === 'reports'
205
+ ? join(dirname(reportOutputDir), 'graphs', 'eval')
206
+ : join(reportOutputDir, 'graphs', 'eval');
207
+ }
208
+ export function buildEvalArtifactGraph(options) {
209
+ const { report, sourcePath } = options;
210
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
211
+ const nodes = [];
212
+ const edges = [];
213
+ const nodeIdsByStableKey = new Map();
214
+ const configs = variantConfigByName(report);
215
+ const skillArtifacts = report.meta.variants
216
+ .map((variant) => ({
217
+ variant,
218
+ artifactHash: report.meta.artifactHashes?.[variant],
219
+ config: configs.get(variant),
220
+ }))
221
+ .filter((item) => item.config?.artifactKind === 'skill'
222
+ && typeof item.artifactHash === 'string'
223
+ && item.artifactHash.length > 0
224
+ && item.artifactHash !== 'no-skill');
225
+ const addNode = (stableKey, nodeKind, nodeRole, label, extra = {}) => {
226
+ const existing = nodeIdsByStableKey.get(stableKey);
227
+ if (existing)
228
+ return existing;
229
+ const id = `node:${shortHash(stableKey)}`;
230
+ nodeIdsByStableKey.set(stableKey, id);
231
+ nodes.push({
232
+ id,
233
+ stableKey,
234
+ nodeKind,
235
+ nodeRole,
236
+ layer: 'measurement',
237
+ label,
238
+ ...extra,
239
+ });
240
+ return id;
241
+ };
242
+ const addEdge = (fromNodeId, toNodeId, edgeKind, extra = {}) => {
243
+ const id = `edge:${shortHash(`${fromNodeId}|${edgeKind}|${toNodeId}|${edges.length}`)}`;
244
+ edges.push({
245
+ id,
246
+ fromNodeId,
247
+ toNodeId,
248
+ edgeKind,
249
+ layer: 'measurement',
250
+ ...extra,
251
+ });
252
+ };
253
+ const addCoverageEdges = (sampleId, sampleNodeId, snapshot) => {
254
+ if (!snapshot.covers?.length || skillArtifacts.length === 0)
255
+ return;
256
+ const seen = new Set();
257
+ snapshot.covers.forEach((target, targetIndex) => {
258
+ const targetRef = normalizeCoverageRef(target);
259
+ if (!targetRef && target.targetKind !== 'skill' && target.targetKind !== 'frontmatter')
260
+ return;
261
+ for (const artifact of skillArtifacts) {
262
+ const stableKey = coverageTargetStableKey(target, artifact.artifactHash);
263
+ const dedupeKey = `${sampleNodeId}|${stableKey}`;
264
+ if (seen.has(dedupeKey))
265
+ continue;
266
+ seen.add(dedupeKey);
267
+ const evidenceRefs = coverageEvidence(report, sampleId, targetIndex, target);
268
+ const targetNodeId = addNode(stableKey, COVERAGE_TARGET_NODE_KIND[target.targetKind], 'entity', coverageTargetLabel(target), {
269
+ binding: { bindingStrength: 'content-hash', keys: { artifactHash: artifact.artifactHash } },
270
+ attrs: {
271
+ display: {
272
+ targetKind: target.targetKind,
273
+ ref: targetRef,
274
+ variant: artifact.variant,
275
+ sourceLocator: artifact.config.locator,
276
+ },
277
+ },
278
+ evidenceRefs,
279
+ });
280
+ addEdge(sampleNodeId, targetNodeId, 'covers', {
281
+ confidence: 1,
282
+ binding: {
283
+ bindingStrength: 'explicit',
284
+ keys: {
285
+ sampleId,
286
+ targetKind: target.targetKind,
287
+ targetRef,
288
+ artifactHash: artifact.artifactHash,
289
+ },
290
+ },
291
+ attrs: { producer: { source: 'sample.covers' } },
292
+ evidenceRefs,
293
+ });
294
+ }
295
+ });
296
+ };
297
+ const variantNodeIds = new Map();
298
+ for (const variant of report.meta.variants) {
299
+ const artifactHash = report.meta.artifactHashes?.[variant];
300
+ const config = configs.get(variant);
301
+ const variantNodeId = addNode(`v1:variant:${report.id}:${variant}`, 'variant', 'entity', variant, {
302
+ status: statusFromScore(report.summary?.[variant]?.avgCompositeScore),
303
+ binding: artifactBinding(variant, artifactHash),
304
+ metrics: {
305
+ ...(report.summary?.[variant]?.avgCompositeScore !== undefined
306
+ ? { avgCompositeScore: report.summary[variant].avgCompositeScore }
307
+ : {}),
308
+ ...(report.summary?.[variant]?.totalSamples !== undefined
309
+ ? { totalSamples: report.summary[variant].totalSamples }
310
+ : {}),
311
+ },
312
+ attrs: {
313
+ display: {
314
+ ...(config ? {
315
+ artifactKind: config.artifactKind,
316
+ artifactSource: config.artifactSource,
317
+ experimentRole: config.experimentRole,
318
+ executionStrategy: config.executionStrategy,
319
+ } : {}),
320
+ },
321
+ },
322
+ evidenceRefs: [{
323
+ sourceKind: 'eval-report',
324
+ sourceId: report.id,
325
+ selector: { selectorKind: 'json-pointer', value: `/summary/${jsonPointerToken(variant)}` },
326
+ label: `${variant} summary`,
327
+ }],
328
+ });
329
+ variantNodeIds.set(variant, variantNodeId);
330
+ if (config?.artifactKind === 'skill' && artifactHash && artifactHash !== 'no-skill') {
331
+ const skillNodeId = addNode(`v1:skill:${artifactHash}`, 'skill', 'entity', variant, {
332
+ binding: { bindingStrength: 'content-hash', keys: { artifactHash } },
333
+ attrs: {
334
+ display: {
335
+ variant,
336
+ sourceLocator: config.locator,
337
+ },
338
+ },
339
+ evidenceRefs: [{
340
+ sourceKind: 'eval-report',
341
+ sourceId: report.id,
342
+ selector: { selectorKind: 'json-pointer', value: `/meta/artifactHashes/${jsonPointerToken(variant)}` },
343
+ contentHash: artifactHash,
344
+ label: `${variant} artifact hash`,
345
+ }],
346
+ });
347
+ addEdge(variantNodeId, skillNodeId, 'derived_from');
348
+ }
349
+ }
350
+ for (const [sampleId, snapshot] of Object.entries(report.sampleSnapshots ?? {})) {
351
+ const sampleNodeId = addNode(sampleStableKey(report, sampleId), 'sample', 'entity', sampleId, {
352
+ binding: sampleBinding(sampleId, report.meta.sampleHashes?.[sampleId]),
353
+ attrs: sampleAttrs(snapshot),
354
+ evidenceRefs: sampleEvidence(report, sampleId),
355
+ });
356
+ snapshot.assertions?.forEach((assertion, index) => {
357
+ const assertionNodeId = addNode(assertionStableKey(report, sampleId, index), 'assertion', 'entity', `assertion: ${assertion.type}`, {
358
+ attrs: { display: { type: assertion.type, weight: assertion.weight ?? 1 } },
359
+ evidenceRefs: assertionEvidence(report, sampleId, index),
360
+ });
361
+ addEdge(sampleNodeId, assertionNodeId, 'contains');
362
+ });
363
+ addCoverageEdges(sampleId, sampleNodeId, snapshot);
364
+ }
365
+ for (const [resultIndex, result] of report.results.entries()) {
366
+ const sampleNodeId = addNode(sampleStableKey(report, result.sample_id), 'sample', 'entity', result.sample_id, {
367
+ binding: sampleBinding(result.sample_id, report.meta.sampleHashes?.[result.sample_id]),
368
+ attrs: sampleAttrs(report.sampleSnapshots?.[result.sample_id]),
369
+ evidenceRefs: sampleEvidence(report, result.sample_id),
370
+ });
371
+ for (const [variant, variantResult] of Object.entries(result.variants)) {
372
+ const variantNodeId = variantNodeIds.get(variant);
373
+ if (!variantNodeId)
374
+ continue;
375
+ addEdge(variantNodeId, sampleNodeId, 'evaluates', {
376
+ status: assertionStatus(variantResult),
377
+ evidenceRefs: evalResultEvidence(report, resultIndex, variant),
378
+ });
379
+ const evalResultNodeId = addNode(`v1:eval-result:${report.id}:${variant}:${result.sample_id}`, 'eval_result', 'observation', `${variant} / ${result.sample_id}`, {
380
+ status: assertionStatus(variantResult),
381
+ metrics: {
382
+ durationMs: variantResult.durationMs,
383
+ costUSD: variantResult.costUSD,
384
+ ...(variantResult.compositeScore !== undefined ? { compositeScore: variantResult.compositeScore } : {}),
385
+ ...(variantResult.llmScore !== undefined ? { llmScore: variantResult.llmScore } : {}),
386
+ ...(variantResult.assertions ? { assertionScore: variantResult.assertions.score } : {}),
387
+ },
388
+ attrs: {
389
+ display: {
390
+ ok: variantResult.ok,
391
+ ...(variantResult.error ? { error: variantResult.error } : {}),
392
+ },
393
+ },
394
+ evidenceRefs: evalResultEvidence(report, resultIndex, variant),
395
+ });
396
+ addEdge(evalResultNodeId, variantNodeId, 'derived_from');
397
+ addEdge(evalResultNodeId, sampleNodeId, 'evaluates');
398
+ variantResult.assertions?.details.forEach((detail, index) => {
399
+ const assertionNodeId = addNode(assertionStableKey(report, result.sample_id, index), 'assertion', 'entity', `assertion: ${detail.type}`, {
400
+ attrs: { display: { type: detail.type, weight: detail.weight } },
401
+ evidenceRefs: assertionEvidence(report, result.sample_id, index, resultIndex, variant),
402
+ });
403
+ addEdge(evalResultNodeId, assertionNodeId, detail.passed ? 'passes' : 'fails', {
404
+ status: detail.passed ? 'ok' : 'failed',
405
+ evidenceRefs: evalResultEvidence(report, resultIndex, variant),
406
+ });
407
+ });
408
+ for (const [dimension, dimensionResult] of Object.entries(variantResult.dimensions ?? {})) {
409
+ const dimensionNodeId = addNode(`v1:judge-dimension:${report.id}:${variant}:${result.sample_id}:${dimension}`, 'judge_dimension', 'observation', dimension, {
410
+ status: statusFromScore(dimensionResult.score),
411
+ metrics: { score: dimensionResult.score },
412
+ attrs: { display: { reason: dimensionResult.reason } },
413
+ evidenceRefs: evalResultEvidence(report, resultIndex, variant),
414
+ });
415
+ addEdge(dimensionNodeId, evalResultNodeId, 'derived_from');
416
+ }
417
+ if (variantResult.diagnostic) {
418
+ const diagnosticNodeId = addNode(`v1:diagnostic:${report.id}:${variant}:${result.sample_id}`, 'diagnostic', 'observation', `diagnostic: ${variant} / ${result.sample_id}`, {
419
+ status: variantResult.diagnostic.ok ? 'warning' : 'failed',
420
+ attrs: {
421
+ display: {
422
+ rootCause: variantResult.diagnostic.rootCause,
423
+ failureModes: variantResult.diagnostic.failureModes ?? [],
424
+ },
425
+ },
426
+ evidenceRefs: evalResultEvidence(report, resultIndex, variant),
427
+ });
428
+ addEdge(diagnosticNodeId, evalResultNodeId, 'diagnoses', {
429
+ status: variantResult.diagnostic.ok ? 'warning' : 'failed',
430
+ });
431
+ }
432
+ }
433
+ }
434
+ return {
435
+ documentKind: 'artifact-graph',
436
+ schemaVersion: 1,
437
+ graphId: `eval:${report.id}`,
438
+ generatedAt,
439
+ source: {
440
+ sourceKind: 'eval',
441
+ sourceId: report.id,
442
+ sourcePath,
443
+ cliVersion: report.meta.cliVersion,
444
+ },
445
+ scope: {
446
+ cwd: process.cwd(),
447
+ artifactKind: scopeArtifactKind(report),
448
+ sourceLocator: report.meta.request?.samplesPath,
449
+ sampleSetHash: sampleSetHash(report.meta.sampleHashes),
450
+ },
451
+ nodes,
452
+ edges,
453
+ summaries: [{
454
+ summaryKind: 'coverage',
455
+ title: 'Eval measurement graph',
456
+ severity: report.results.some((result) => Object.values(result.variants).some((variant) => assertionStatus(variant) === 'failed'))
457
+ ? 'medium'
458
+ : 'info',
459
+ }],
460
+ };
461
+ }
462
+ export function persistEvalGraphSidecar(options) {
463
+ const graphDir = evalGraphDirForReportOutput(options.outputDir);
464
+ if (!existsSync(graphDir))
465
+ mkdirSync(graphDir, { recursive: true });
466
+ const fileStem = options.fileStem ?? options.report.id;
467
+ const graphPath = join(graphDir, graphFileName(fileStem));
468
+ const graph = buildEvalArtifactGraph(options);
469
+ writeFileSync(graphPath, JSON.stringify(graph, null, 2));
470
+ return { graphPath };
471
+ }
@@ -7,11 +7,14 @@
7
7
  * fatal-fail 不中断后续 rule 执行(让用户一次看到全貌),但 report.outcome='failed',
8
8
  * CLI 据此 exit 1 / abort eval。
9
9
  */
10
- import { existsSync, statSync } from 'node:fs';
10
+ import { existsSync, readFileSync, statSync } from 'node:fs';
11
11
  import { basename, dirname, join, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { runFileSuffix } from '../eval-core/artifact-file-names.js';
12
14
  import { discoverVariants, resolveArtifacts } from '../inputs/skill-loader.js';
13
15
  import { DOCTOR_REPORT_SCHEMA_VERSION, isComposerRule } from '../types/doctor.js';
14
16
  import { getRegisteredRules } from './rules.js';
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
18
  // ---------------------------------------------------------------------------
16
19
  // Target resolution
17
20
  // ---------------------------------------------------------------------------
@@ -163,18 +166,30 @@ function inferSkillPath(artifact, baseDir) {
163
166
  // ---------------------------------------------------------------------------
164
167
  // Public entry
165
168
  // ---------------------------------------------------------------------------
166
- let reportIdCounter = 0;
167
169
  function nextReportId() {
168
- reportIdCounter += 1;
169
- const ts = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15);
170
- // 进程内计数器只防同进程同秒撞;跨进程同秒(两个 omk doctor 并发 / 不同项目)仍会撞同 id,
171
- // 经机器级卡片 dedup 当唯一键用时会静默并掉一份。加 4 位随机根治跨进程撞名(id 是标签非测量数)。
172
- const rand = Math.random().toString(36).slice(2, 6);
173
- return `doctor-${ts}-${reportIdCounter}-${rand}`;
170
+ return `doctor-${runFileSuffix()}`;
171
+ }
172
+ function findPackageJson(startDir) {
173
+ let dir = startDir;
174
+ for (let i = 0; i < 5; i += 1) {
175
+ const candidate = join(dir, 'package.json');
176
+ if (existsSync(candidate))
177
+ return candidate;
178
+ dir = dirname(dir);
179
+ }
180
+ return join(startDir, '..', 'package.json');
174
181
  }
175
182
  function readCliVersion() {
176
- // 不依赖 package.json import(避免 type 解析复杂度);用环境变量或退回 'unknown'
177
- return process.env.npm_package_version ?? '0.0.0';
183
+ const envVersion = process.env.npm_package_version;
184
+ if (envVersion)
185
+ return envVersion;
186
+ try {
187
+ const pkg = JSON.parse(readFileSync(findPackageJson(__dirname), 'utf-8'));
188
+ if (typeof pkg.version === 'string' && pkg.version.length > 0)
189
+ return pkg.version;
190
+ }
191
+ catch { /* fall through */ }
192
+ return '0.0.0';
178
193
  }
179
194
  export async function runDoctor(opts) {
180
195
  // 默认规则 = 内置 + registerRule() 注册的 custom。test 注入走 opts.rules。
@@ -10,6 +10,6 @@ export declare function graphFileName(stem: string): string;
10
10
  export declare function cardFileName(stem: string): string;
11
11
  export declare function runTimestamp(date?: Date): string;
12
12
  export declare function randomRunToken(): string;
13
- export declare function runFileSuffix(counter?: number): string;
13
+ export declare function runFileSuffix(): string;
14
14
  export declare function stripDomainPrefix(id: string, domain: string): string;
15
15
  export declare function doctorReportFileStem(skillName: string, reportId: string): string;
@@ -32,9 +32,8 @@ export function runTimestamp(date = new Date()) {
32
32
  export function randomRunToken() {
33
33
  return Math.random().toString(36).slice(2, 6);
34
34
  }
35
- export function runFileSuffix(counter) {
36
- const middle = counter === undefined ? '' : `-${counter}`;
37
- return `${runTimestamp()}${middle}-${randomRunToken()}`;
35
+ export function runFileSuffix() {
36
+ return `${runTimestamp()}-${randomRunToken()}`;
38
37
  }
39
38
  export function stripDomainPrefix(id, domain) {
40
39
  const safeId = safeArtifactFileStem(id);
@@ -33,7 +33,7 @@ export interface PersistableReport {
33
33
  }
34
34
  export declare function persistReport(report: PersistableReport, outputDir: string | null): string | null;
35
35
  /**
36
- * run id 的时间戳后缀 `YYYYMMDD-HHmmss-rand4`。
36
+ * run id 的时间戳后缀 `YYYYMMDDTHHmmss-rand4`。
37
37
  * 含秒 + 4 位随机:id 是 run 标签(非测量数),但被 studio 机器级 dedup 与 managed 证据 (reportId,
38
38
  * contentHash) 去重当唯一键用。分钟级会让跨项目 / 同分钟重跑撞同 id → 索引静默顶掉一份、managed 错并一条。
39
39
  * 秒+随机根治撞名,保证每次 run 全局唯一。供 generateRunId 与 evolve 合并 id 共用,避免靠 split 反解格式。
@@ -5,7 +5,8 @@ import { createHash } from 'node:crypto';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { DEFAULT_REPORTS_DIR } from './default-dirs.js';
7
7
  import { indexReportWrite } from './artifact-index.js';
8
- import { reportFilePath } from './artifact-file-names.js';
8
+ import { randomRunToken, reportFilePath, runTimestamp } from './artifact-file-names.js';
9
+ import { persistEvalGraphSidecar } from '../artifact-graph/eval.js';
9
10
  import { buildVariantSummary } from './schema.js';
10
11
  import { buildVariantConfig, resolveExecutionStrategy } from './execution-strategy.js';
11
12
  import { getJudgePromptHash } from '../grading/judge.js';
@@ -272,10 +273,25 @@ export function aggregateReport({ runId, variants, model, judgeModel, noJudge, e
272
273
  ...(s.difficulty ? { difficulty: s.difficulty } : {}),
273
274
  ...(s.construct ? { construct: s.construct } : {}),
274
275
  ...(s.provenance ? { provenance: s.provenance } : {}),
276
+ ...(s.covers && s.covers.length > 0 ? { covers: s.covers } : {}),
275
277
  ...(s.tripwire ? { tripwire: true } : {}),
276
278
  }])),
277
279
  };
278
280
  }
281
+ function isEvaluationReport(report) {
282
+ return report['kind'] === 'evaluation';
283
+ }
284
+ function persistEvalGraphSidecarSafely(report, outputDir, sourcePath) {
285
+ if (!isEvaluationReport(report))
286
+ return;
287
+ try {
288
+ persistEvalGraphSidecar({ report, outputDir, sourcePath, fileStem: report.id });
289
+ }
290
+ catch (err) {
291
+ const message = err instanceof Error ? err.message : String(err);
292
+ process.stderr.write(`[omk] 写入 eval 图谱失败:${message}\n`);
293
+ }
294
+ }
279
295
  export function persistReport(report, outputDir) {
280
296
  if (!outputDir)
281
297
  return null;
@@ -283,28 +299,32 @@ export function persistReport(report, outputDir) {
283
299
  mkdirSync(outputDir, { recursive: true });
284
300
  const filePath = reportFilePath(outputDir, report.id);
285
301
  writeFileSync(filePath, JSON.stringify(report, null, 2));
302
+ persistEvalGraphSidecarSafely(report, outputDir, filePath);
286
303
  // 产物发现索引:报告落项目本地后,best-effort 追加全局轻卡片,让 omk studio 跨项目聚合成机器级总览。
287
304
  // 永不抛、永不阻断报告落盘(正文是 source of truth)。
288
305
  indexReportWrite(report, filePath, outputDir);
289
306
  return filePath;
290
307
  }
291
308
  /**
292
- * run id 的时间戳后缀 `YYYYMMDD-HHmmss-rand4`。
309
+ * run id 的时间戳后缀 `YYYYMMDDTHHmmss-rand4`。
293
310
  * 含秒 + 4 位随机:id 是 run 标签(非测量数),但被 studio 机器级 dedup 与 managed 证据 (reportId,
294
311
  * contentHash) 去重当唯一键用。分钟级会让跨项目 / 同分钟重跑撞同 id → 索引静默顶掉一份、managed 错并一条。
295
312
  * 秒+随机根治撞名,保证每次 run 全局唯一。供 generateRunId 与 evolve 合并 id 共用,避免靠 split 反解格式。
296
313
  */
297
314
  export function runIdSuffix() {
298
- const d = new Date();
299
- const pad = (n) => String(n).padStart(2, '0');
300
- const date = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
301
- const time = `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
302
- const rand = Math.random().toString(36).slice(2, 6);
303
- return `${date}-${time}-${rand}`;
315
+ return `${runTimestamp()}-${randomRunToken()}`;
316
+ }
317
+ function safeRunSubject(subject) {
318
+ const sanitized = subject
319
+ .replaceAll(/[\\/:]/g, '-')
320
+ .replaceAll(/[^a-zA-Z0-9._@-]/g, '_')
321
+ .replace(/^-+|-+$/g, '');
322
+ return sanitized || 'run';
323
+ }
324
+ function primaryRunSubject(variants) {
325
+ const nonBaseline = variants.filter((variant) => variant !== 'baseline');
326
+ return nonBaseline.at(-1) ?? variants.at(-1) ?? 'run';
304
327
  }
305
328
  export function generateRunId(variants) {
306
- const variantPart = variants
307
- .map((variant) => variant.replaceAll(/[\\/:]/g, '-').replaceAll(/[^a-zA-Z0-9._@-]/g, '_'))
308
- .join('-vs-');
309
- return `${variantPart}-${runIdSuffix()}`;
329
+ return `${safeRunSubject(primaryRunSubject(variants))}-${runIdSuffix()}`;
310
330
  }