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.
- package/dist/artifact-graph/doctor.js +1 -1
- package/dist/artifact-graph/eval.d.ts +16 -0
- package/dist/artifact-graph/eval.js +471 -0
- package/dist/doctor/index.js +25 -10
- package/dist/eval-core/artifact-file-names.d.ts +1 -1
- package/dist/eval-core/artifact-file-names.js +2 -3
- package/dist/eval-core/evaluation-reporting.d.ts +1 -1
- package/dist/eval-core/evaluation-reporting.js +32 -12
- package/dist/inputs/load-samples.js +27 -0
- package/dist/renderer/html-renderer.js +3 -2
- package/dist/renderer/skill-detail-renderer.js +902 -0
- package/dist/renderer/skill-list-renderer.js +2 -7
- package/dist/server/report-server.js +45 -11
- package/dist/server/skill-index.d.ts +6 -0
- package/dist/server/skill-index.js +405 -5
- package/dist/types/artifact-graph.d.ts +1 -1
- package/dist/types/eval.d.ts +10 -0
- package/dist/types/report.d.ts +3 -1
- package/dist/types/skill-index.d.ts +42 -0
- package/package.json +3 -3
|
@@ -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
|
|
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
|
+
}
|
package/dist/doctor/index.js
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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(
|
|
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(
|
|
36
|
-
|
|
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 的时间戳后缀 `
|
|
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 的时间戳后缀 `
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
}
|