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
|
@@ -134,13 +134,8 @@ function renderPanel(a, lang) {
|
|
|
134
134
|
// renderSkillModal 已移除:首页点行直接进 skill 详情页(doctor/eval/observe 已在该页合一),弹框预览冗余。
|
|
135
135
|
function renderRow(entry, insights, langQ, lang) {
|
|
136
136
|
const h = assessHealth(entry, insights, lang);
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
const href = entry.eval
|
|
140
|
-
? `/reports/${encodeURIComponent(entry.eval.reportId)}${langQ}`
|
|
141
|
-
: entry.doctor
|
|
142
|
-
? `/doctors/${encodeURIComponent(entry.doctor.reportId)}?skill=${encodeURIComponent(entry.skillName)}${amp}`
|
|
143
|
-
: `/runs${langQ}`;
|
|
137
|
+
// 点行先进入 skill hub:Skill Map / 三阶段状态 / 图谱摘要是用户第一视角。
|
|
138
|
+
const href = `/skills/${encodeURIComponent(entry.skillName)}${langQ}`;
|
|
144
139
|
const dT = entry.doctor ? entry.doctor.passCount + entry.doctor.warnCount + entry.doctor.failCount : 0;
|
|
145
140
|
const dP = dT > 0 ? Math.round(((entry.doctor.passCount + entry.doctor.warnCount * 0.5) / dT) * 100) : null;
|
|
146
141
|
const eP = entry.eval?.compositeScore != null ? Math.round((entry.eval.compositeScore / 5) * 100) : null;
|
|
@@ -6,13 +6,14 @@ import { renderReportDocumentDetail, renderTrendsPage, renderRunList } from '../
|
|
|
6
6
|
import { renderSkillList } from '../renderer/skill-list-renderer.js';
|
|
7
7
|
import { renderSkillHealthReport } from '../renderer/skill-health-renderer.js';
|
|
8
8
|
import { renderDoctorDetail } from '../renderer/doctor-detail-renderer.js';
|
|
9
|
-
import { assessHealth } from '../renderer/skill-detail-renderer.js';
|
|
9
|
+
import { assessHealth, renderSkillDetail } from '../renderer/skill-detail-renderer.js';
|
|
10
10
|
import { renderObservationInboxPage } from '../renderer/observation-inbox-renderer.js';
|
|
11
11
|
import { DEFAULT_LANG, t, layout } from '../renderer/layout.js';
|
|
12
12
|
import { loadAllManagedRecords, resolveManagedDir, managedDir as projectManagedDir, listManagedRows, buildVersionScores } from '../managed/index.js';
|
|
13
13
|
import { renderManagedList, renderManagedHistory } from '../renderer/managed-history-renderer.js';
|
|
14
14
|
import { DEFAULT_JOBS_DIR } from '../eval-core/default-dirs.js';
|
|
15
15
|
import { resolveObserveHealthDir, projectObserveHealthDir, resolveDoctorsDir, projectDoctorsDir, projectReportsDir, globalReportsDir } from '../eval-core/measurement-dirs.js';
|
|
16
|
+
import { evalGraphDirForReportOutput } from '../artifact-graph/eval.js';
|
|
16
17
|
import { listObserveCards, listDoctorCards, listLiveObserveCards } from '../eval-core/artifact-index.js';
|
|
17
18
|
import { isReportFileName, reportFilePath, reportFileStem } from '../eval-core/artifact-file-names.js';
|
|
18
19
|
import { migrateLegacyReportFiles } from '../eval-core/report-file-migration.js';
|
|
@@ -602,6 +603,18 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
|
|
|
602
603
|
: doctorsDir !== undefined
|
|
603
604
|
? () => doctorsDir
|
|
604
605
|
: () => resolveDoctorsDir(projectDoctorsDir());
|
|
606
|
+
const includeReportCards = reportsDir === undefined && store == null;
|
|
607
|
+
const evalGraphDirs = reportsDir !== undefined
|
|
608
|
+
? [evalGraphDirForReportOutput(reportsDir)]
|
|
609
|
+
: store
|
|
610
|
+
? []
|
|
611
|
+
: [evalGraphDirForReportOutput(projectReportsDir()), evalGraphDirForReportOutput(globalReportsDir())];
|
|
612
|
+
const skillIndexOptions = () => ({
|
|
613
|
+
includeObserveCards,
|
|
614
|
+
includeDoctorCards,
|
|
615
|
+
includeReportCards,
|
|
616
|
+
evalGraphDirs,
|
|
617
|
+
});
|
|
605
618
|
async function handleRequest(req, res) {
|
|
606
619
|
try {
|
|
607
620
|
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
@@ -752,7 +765,7 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
|
|
|
752
765
|
}
|
|
753
766
|
if (path === '/api/observe-inbox/diagnostics') {
|
|
754
767
|
const runs = await reportStore.list();
|
|
755
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir,
|
|
768
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
756
769
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
757
770
|
res.end(JSON.stringify({
|
|
758
771
|
sourceCoverage: idx.diagnosisSummary.sourceCoverage,
|
|
@@ -838,7 +851,7 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
|
|
|
838
851
|
let ctx;
|
|
839
852
|
if (skillName) {
|
|
840
853
|
const runs = await reportStore.list();
|
|
841
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir,
|
|
854
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
842
855
|
const entry = idx.entries.find((en) => en.skillName === skillName);
|
|
843
856
|
if (entry)
|
|
844
857
|
ctx = buildSkillContext(entry, 'doctor', id, idx.insightsBySkill.get(entry.skillName) ?? [], lang);
|
|
@@ -1000,7 +1013,7 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
|
|
|
1000
1013
|
let ctx;
|
|
1001
1014
|
if (report && report.kind === 'evaluation') {
|
|
1002
1015
|
const runs = await reportStore.list();
|
|
1003
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir,
|
|
1016
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
1004
1017
|
// 按 evalHistory 匹配(非仅最新),历史 eval 报告也能定位到所属 skill。
|
|
1005
1018
|
const entry = idx.entries.find((en) => en.evalHistory.some((h) => h.reportId === reportId));
|
|
1006
1019
|
if (entry)
|
|
@@ -1022,14 +1035,14 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
|
|
|
1022
1035
|
// list renderer 直接消费,不再每请求 N×detectInsights 重算。
|
|
1023
1036
|
if (path === '/') {
|
|
1024
1037
|
const runs = await reportStore.list();
|
|
1025
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir,
|
|
1038
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
1026
1039
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1027
1040
|
res.end(renderSkillList(idx, lang));
|
|
1028
1041
|
return;
|
|
1029
1042
|
}
|
|
1030
1043
|
if (path === '/api/skills') {
|
|
1031
1044
|
const runs = await reportStore.list();
|
|
1032
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir,
|
|
1045
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
1033
1046
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1034
1047
|
res.end(JSON.stringify({
|
|
1035
1048
|
entries: idx.entries.map((entry) => ({
|
|
@@ -1042,15 +1055,36 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
|
|
|
1042
1055
|
}));
|
|
1043
1056
|
return;
|
|
1044
1057
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1058
|
+
const skillHubMatch = path.match(/^\/skills\/(.+)$/);
|
|
1059
|
+
if (skillHubMatch) {
|
|
1060
|
+
let skillName;
|
|
1061
|
+
try {
|
|
1062
|
+
skillName = decodeURIComponent(skillHubMatch[1]);
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1066
|
+
res.end(lang === 'en' ? 'skill not found' : '未找到该 skill');
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const runs = await reportStore.list();
|
|
1070
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
1071
|
+
const entry = idx.entries.find((en) => en.skillName === skillName);
|
|
1072
|
+
if (!entry) {
|
|
1073
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1074
|
+
res.end(lang === 'en' ? 'skill not found' : '未找到该 skill');
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const report = entry.eval ? await queryRun(reportStore, entry.eval.reportId) : null;
|
|
1078
|
+
const evalReport = report?.kind === 'evaluation' ? report : null;
|
|
1079
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1080
|
+
res.end(renderSkillDetail(entry, evalReport, lang, idx.insightsBySkill.get(entry.skillName) ?? []));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1049
1083
|
const skillDiagnosticsApiMatch = path.match(/^\/api\/skills\/(.+)\/diagnostics$/);
|
|
1050
1084
|
if (skillDiagnosticsApiMatch) {
|
|
1051
1085
|
const skillName = decodeURIComponent(skillDiagnosticsApiMatch[1]);
|
|
1052
1086
|
const runs = await reportStore.list();
|
|
1053
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir,
|
|
1087
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
|
|
1054
1088
|
const diagnostics = idx.diagnosticsBySkill.get(skillName);
|
|
1055
1089
|
if (!diagnostics) {
|
|
1056
1090
|
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -13,6 +13,12 @@ export interface BuildSkillIndexOptions {
|
|
|
13
13
|
includeObserveCards?: boolean;
|
|
14
14
|
/** 合并别项目 doctor 卡片(机器级模式)。固定 --doctors-dir / --global 时为 false,只看该目录。 */
|
|
15
15
|
includeDoctorCards?: boolean;
|
|
16
|
+
/** 合并别项目 eval report 卡片对应的 graph sidecar。 */
|
|
17
|
+
includeReportCards?: boolean;
|
|
18
|
+
/** 当前可见 eval graph sidecar 目录。 */
|
|
19
|
+
evalGraphDirs?: string[];
|
|
20
|
+
/** 当前可见 doctor graph sidecar 目录。默认跟随 doctorsDir 推导。 */
|
|
21
|
+
doctorGraphDirs?: string[];
|
|
16
22
|
}
|
|
17
23
|
export declare function buildSkillIndex(reports: ReportDocument[], analysesDir: string, doctorsDir: string, observationsDir?: string, opts?: BuildSkillIndexOptions): SkillIndex;
|
|
18
24
|
/** 单 skill 详情查询(用于 /skills/<name> 详情页)— 复用 buildSkillIndex 的扫描结果。 */
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
17
17
|
import { join, isAbsolute, basename, dirname } from 'node:path';
|
|
18
|
-
import { isReportFileName, reportFileStem } from '../eval-core/artifact-file-names.js';
|
|
18
|
+
import { GRAPH_FILE_SUFFIX, graphFileName, isReportFileName, reportFileStem } from '../eval-core/artifact-file-names.js';
|
|
19
19
|
import { migrateLegacyReportFiles } from '../eval-core/report-file-migration.js';
|
|
20
|
+
import { doctorGraphDirForDoctorOutput } from '../artifact-graph/doctor.js';
|
|
21
|
+
import { evalGraphDirForReportOutput } from '../artifact-graph/eval.js';
|
|
20
22
|
import { confidenceOf } from '../observability/skill-health-analyzer.js';
|
|
21
23
|
import { computeVerdict } from '../eval-core/verdict.js';
|
|
22
|
-
import { artifactIndexDir, listLiveDoctorCards, cardToDoctorSnapshot, listLiveObserveCards, cardTargetSentinel } from '../eval-core/artifact-index.js';
|
|
24
|
+
import { artifactIndexDir, listLiveDoctorCards, cardToDoctorSnapshot, listLiveObserveCards, listLiveReportCards, cardTargetSentinel } from '../eval-core/artifact-index.js';
|
|
23
25
|
import { detectInsights } from './skill-insights.js';
|
|
24
26
|
import { DEFAULT_OBSERVATIONS_DIR, loadLatestObservationInboxReports } from '../observability/inbox.js';
|
|
25
27
|
import { buildStudioDiagnosisSummary, mergeDiagnosisBundles } from '../diagnosis/studio-projection.js';
|
|
@@ -64,7 +66,59 @@ function safeDirJsonContentFingerprint(dir) {
|
|
|
64
66
|
});
|
|
65
67
|
return `${dir}|${dirMtimeMs}|${fileParts.join(',')}`;
|
|
66
68
|
}
|
|
67
|
-
function
|
|
69
|
+
function safeDirGraphContentFingerprint(dir) {
|
|
70
|
+
let dirMtimeMs;
|
|
71
|
+
let graphFiles;
|
|
72
|
+
try {
|
|
73
|
+
dirMtimeMs = statSync(dir).mtimeMs;
|
|
74
|
+
graphFiles = readdirSync(dir).filter((f) => f.endsWith(GRAPH_FILE_SUFFIX)).sort();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return `missing:${dir}`;
|
|
78
|
+
}
|
|
79
|
+
const fileParts = graphFiles.map((f) => {
|
|
80
|
+
try {
|
|
81
|
+
const fStat = statSync(join(dir, f));
|
|
82
|
+
return `${f}:${fStat.mtimeMs}:${fStat.size}`;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return `${f}:?`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return `${dir}|${dirMtimeMs}|${fileParts.join(',')}`;
|
|
89
|
+
}
|
|
90
|
+
function fileFingerprint(path) {
|
|
91
|
+
try {
|
|
92
|
+
const stat = statSync(path);
|
|
93
|
+
return `${path}:${stat.mtimeMs}:${stat.size}`;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return `${path}:missing`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function uniqueStrings(values) {
|
|
100
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
101
|
+
}
|
|
102
|
+
function graphSidecarPathForReportPath(reportPath, reportId) {
|
|
103
|
+
return join(evalGraphDirForReportOutput(dirname(reportPath)), graphFileName(reportId));
|
|
104
|
+
}
|
|
105
|
+
function graphSidecarPathForDoctorCard(cardPath, cardId) {
|
|
106
|
+
return join(doctorGraphDirForDoctorOutput(dirname(cardPath)), graphFileName(cardId));
|
|
107
|
+
}
|
|
108
|
+
function buildGraphFingerprint(evalGraphDirs, doctorGraphDirs, includeReportCards, includeDoctorCards) {
|
|
109
|
+
const dirPart = [
|
|
110
|
+
...uniqueStrings(evalGraphDirs).map((dir) => `eg:${safeDirGraphContentFingerprint(dir)}`),
|
|
111
|
+
...uniqueStrings(doctorGraphDirs).map((dir) => `dg:${safeDirGraphContentFingerprint(dir)}`),
|
|
112
|
+
].join('|');
|
|
113
|
+
const reportCardPart = includeReportCards
|
|
114
|
+
? listLiveReportCards().map((card) => fileFingerprint(graphSidecarPathForReportPath(card.path, card.id))).sort().join('|')
|
|
115
|
+
: '';
|
|
116
|
+
const doctorCardPart = includeDoctorCards
|
|
117
|
+
? listLiveDoctorCards().map((card) => fileFingerprint(graphSidecarPathForDoctorCard(card.path, card.id))).sort().join('|')
|
|
118
|
+
: '';
|
|
119
|
+
return `${dirPart}|erc:${reportCardPart}|drc:${doctorCardPart}`;
|
|
120
|
+
}
|
|
121
|
+
function buildIndexFingerprint(reports, analysesDir, doctorsDir, observationsDir, includeObserveCards, includeDoctorCards, graphFingerprint) {
|
|
68
122
|
// `d:` 跟 `o:` 前缀("d for doctors / o for observability analyses")沿用 pre-fix
|
|
69
123
|
// 字面格式,避免外部 logging / debug 时 grep fingerprint 字符串的格式漂移。
|
|
70
124
|
// 每一段的 right-hand-side 从旧的 "{dir-mtime}-{file-count}" 双标量升级成
|
|
@@ -83,7 +137,7 @@ function buildIndexFingerprint(reports, analysesDir, doctorsDir, observationsDir
|
|
|
83
137
|
// 卡片目录指纹 + 真身存在性 sentinel 都纳入:后者使「卡片真身被带外删」也能让缓存失效(否则悬空卡片继续展示)。
|
|
84
138
|
const doctorCardsFp = includeDoctorCards ? `${safeDirJsonContentFingerprint(artifactIndexDir('doctor'))}|t:${cardTargetSentinel('doctor')}` : '';
|
|
85
139
|
const observeCardsFp = includeObserveCards ? `${safeDirJsonContentFingerprint(artifactIndexDir('observe-health'))}|t:${cardTargetSentinel('observe-health')}` : '';
|
|
86
|
-
return `${reportIds}|d:${doctorsFp}|o:${analysesFp}|obs:${observationsFp}|dc:${doctorCardsFp}|oc:${observeCardsFp}|inc:${includeObserveCards ? 'o' : ''}${includeDoctorCards ? 'd' : ''}`;
|
|
140
|
+
return `${reportIds}|d:${doctorsFp}|o:${analysesFp}|obs:${observationsFp}|dc:${doctorCardsFp}|oc:${observeCardsFp}|g:${graphFingerprint}|inc:${includeObserveCards ? 'o' : ''}${includeDoctorCards ? 'd' : ''}`;
|
|
87
141
|
}
|
|
88
142
|
/** 测试 / 调试用:强制清掉 in-process skill-index 缓存。 */
|
|
89
143
|
export function _resetSkillIndexCache() {
|
|
@@ -227,6 +281,334 @@ function latestEvalSnapshot(list) {
|
|
|
227
281
|
return null;
|
|
228
282
|
return list[list.length - 1];
|
|
229
283
|
}
|
|
284
|
+
function isArtifactGraphDocument(value) {
|
|
285
|
+
if (!value || typeof value !== 'object')
|
|
286
|
+
return false;
|
|
287
|
+
const graph = value;
|
|
288
|
+
return graph.documentKind === 'artifact-graph'
|
|
289
|
+
&& graph.schemaVersion === 1
|
|
290
|
+
&& typeof graph.graphId === 'string'
|
|
291
|
+
&& !!graph.source
|
|
292
|
+
&& (graph.source.sourceKind === 'doctor' || graph.source.sourceKind === 'eval' || graph.source.sourceKind === 'observe')
|
|
293
|
+
&& Array.isArray(graph.nodes)
|
|
294
|
+
&& Array.isArray(graph.edges);
|
|
295
|
+
}
|
|
296
|
+
function readArtifactGraph(path) {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
299
|
+
if (!isArtifactGraphDocument(parsed))
|
|
300
|
+
return null;
|
|
301
|
+
return { graph: parsed, path };
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function scanGraphDir(dir) {
|
|
308
|
+
if (!existsSync(dir))
|
|
309
|
+
return [];
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const file of readdirSync(dir).sort()) {
|
|
312
|
+
if (!file.endsWith(GRAPH_FILE_SUFFIX))
|
|
313
|
+
continue;
|
|
314
|
+
const loaded = readArtifactGraph(join(dir, file));
|
|
315
|
+
if (loaded)
|
|
316
|
+
out.push(loaded);
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
function loadGraphsForSkillIndex(options) {
|
|
321
|
+
const byPath = new Map();
|
|
322
|
+
const add = (loaded) => {
|
|
323
|
+
if (loaded)
|
|
324
|
+
byPath.set(loaded.path, loaded);
|
|
325
|
+
};
|
|
326
|
+
for (const dir of uniqueStrings(options.evalGraphDirs)) {
|
|
327
|
+
for (const graph of scanGraphDir(dir))
|
|
328
|
+
add(graph);
|
|
329
|
+
}
|
|
330
|
+
for (const dir of uniqueStrings(options.doctorGraphDirs)) {
|
|
331
|
+
for (const graph of scanGraphDir(dir))
|
|
332
|
+
add(graph);
|
|
333
|
+
}
|
|
334
|
+
if (options.includeReportCards) {
|
|
335
|
+
for (const card of listLiveReportCards()) {
|
|
336
|
+
add(readArtifactGraph(graphSidecarPathForReportPath(card.path, card.id)));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (options.includeDoctorCards) {
|
|
340
|
+
for (const card of listLiveDoctorCards()) {
|
|
341
|
+
add(readArtifactGraph(graphSidecarPathForDoctorCard(card.path, card.id)));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return [...byPath.values()];
|
|
345
|
+
}
|
|
346
|
+
function graphSkillNodes(graph) {
|
|
347
|
+
return graph.nodes.filter((node) => node.nodeKind === 'skill');
|
|
348
|
+
}
|
|
349
|
+
function graphSkillNames(graph) {
|
|
350
|
+
return uniqueStrings([
|
|
351
|
+
graph.scope.skillName ?? '',
|
|
352
|
+
...graphSkillNodes(graph).map((node) => node.label),
|
|
353
|
+
]);
|
|
354
|
+
}
|
|
355
|
+
function nodeArtifactHash(node) {
|
|
356
|
+
const hash = node?.binding?.keys?.artifactHash;
|
|
357
|
+
return hash && hash !== 'no-skill' ? hash : undefined;
|
|
358
|
+
}
|
|
359
|
+
function nodeSourceLocator(node) {
|
|
360
|
+
const locator = node?.attrs?.display?.sourceLocator;
|
|
361
|
+
return typeof locator === 'string' && locator.trim() ? locator : undefined;
|
|
362
|
+
}
|
|
363
|
+
function graphSourceLocator(graph, skillNode) {
|
|
364
|
+
if (graph.source.sourceKind === 'eval') {
|
|
365
|
+
// Eval graph 的 scope.sourceLocator 是 samplesPath;skill 路径在 skill node 上。
|
|
366
|
+
return nodeSourceLocator(skillNode);
|
|
367
|
+
}
|
|
368
|
+
return graph.scope.sourceLocator ?? nodeSourceLocator(skillNode);
|
|
369
|
+
}
|
|
370
|
+
function graphNodeMap(graph) {
|
|
371
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
372
|
+
}
|
|
373
|
+
function graphHasSkill(graph, entry) {
|
|
374
|
+
const names = graphSkillNames(graph);
|
|
375
|
+
return names.includes(entry.skillName) || (entry.eval ? names.includes(entry.eval.variantName) : false);
|
|
376
|
+
}
|
|
377
|
+
function graphMatchesDoctorEntry(graph, entry) {
|
|
378
|
+
return graphHasSkill(graph, entry);
|
|
379
|
+
}
|
|
380
|
+
function evalVariantName(entry) {
|
|
381
|
+
return entry.eval?.variantName ?? null;
|
|
382
|
+
}
|
|
383
|
+
function evalVariantNode(graph, entry) {
|
|
384
|
+
const variantName = evalVariantName(entry);
|
|
385
|
+
if (!variantName)
|
|
386
|
+
return undefined;
|
|
387
|
+
return graph.nodes.find((node) => node.nodeKind === 'variant' && node.label === variantName);
|
|
388
|
+
}
|
|
389
|
+
function evalSkillNodeForVariant(graph, variantNode) {
|
|
390
|
+
const nodesById = graphNodeMap(graph);
|
|
391
|
+
return graph.edges
|
|
392
|
+
.filter((edge) => edge.fromNodeId === variantNode.id && edge.edgeKind === 'derived_from')
|
|
393
|
+
.map((edge) => nodesById.get(edge.toNodeId))
|
|
394
|
+
.find((node) => node?.nodeKind === 'skill');
|
|
395
|
+
}
|
|
396
|
+
function graphMatchesEvalEntry(graph, entry) {
|
|
397
|
+
return evalVariantNode(graph, entry) !== undefined;
|
|
398
|
+
}
|
|
399
|
+
function pickLatestGraph(graphs) {
|
|
400
|
+
if (graphs.length === 0)
|
|
401
|
+
return null;
|
|
402
|
+
return [...graphs].sort((a, b) => {
|
|
403
|
+
const at = a.graph.generatedAt || '';
|
|
404
|
+
const bt = b.graph.generatedAt || '';
|
|
405
|
+
if (at !== bt)
|
|
406
|
+
return at.localeCompare(bt);
|
|
407
|
+
return a.graph.graphId.localeCompare(b.graph.graphId);
|
|
408
|
+
})[graphs.length - 1];
|
|
409
|
+
}
|
|
410
|
+
function countGraphNodes(graph, nodeKind) {
|
|
411
|
+
return graph.nodes.filter((node) => node.nodeKind === nodeKind).length;
|
|
412
|
+
}
|
|
413
|
+
function graphNodePreview(node) {
|
|
414
|
+
return {
|
|
415
|
+
stableKey: node.stableKey,
|
|
416
|
+
nodeKind: node.nodeKind,
|
|
417
|
+
label: node.label,
|
|
418
|
+
...(node.status ? { status: node.status } : {}),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function graphNodePreviews(graph, nodeKinds, nodeIds) {
|
|
422
|
+
const kinds = new Set(nodeKinds);
|
|
423
|
+
return graph.nodes
|
|
424
|
+
.filter((node) => kinds.has(node.nodeKind) && (!nodeIds || nodeIds.has(node.id)))
|
|
425
|
+
.map(graphNodePreview);
|
|
426
|
+
}
|
|
427
|
+
function projectDoctorStage(graph, path) {
|
|
428
|
+
if (graph.source.sourceKind !== 'doctor')
|
|
429
|
+
return undefined;
|
|
430
|
+
const skillNode = graphSkillNodes(graph)[0];
|
|
431
|
+
const sourceLocator = graphSourceLocator(graph, skillNode);
|
|
432
|
+
const base = {
|
|
433
|
+
sourceKind: 'doctor',
|
|
434
|
+
sourceId: graph.source.sourceId,
|
|
435
|
+
graphId: graph.graphId,
|
|
436
|
+
generatedAt: graph.generatedAt,
|
|
437
|
+
graphPath: path,
|
|
438
|
+
nodeCount: graph.nodes.length,
|
|
439
|
+
edgeCount: graph.edges.length,
|
|
440
|
+
};
|
|
441
|
+
return {
|
|
442
|
+
stage: {
|
|
443
|
+
...base,
|
|
444
|
+
references: countGraphNodes(graph, 'reference'),
|
|
445
|
+
scripts: countGraphNodes(graph, 'script'),
|
|
446
|
+
workflows: countGraphNodes(graph, 'workflow'),
|
|
447
|
+
workflowNodes: countGraphNodes(graph, 'workflow_node'),
|
|
448
|
+
hardRules: countGraphNodes(graph, 'hard_rule'),
|
|
449
|
+
definitionNodes: graphNodePreviews(graph, [
|
|
450
|
+
'skill_file',
|
|
451
|
+
'frontmatter',
|
|
452
|
+
'reference',
|
|
453
|
+
'script',
|
|
454
|
+
'preflight',
|
|
455
|
+
'tool',
|
|
456
|
+
'hard_rule',
|
|
457
|
+
'workflow',
|
|
458
|
+
'workflow_node',
|
|
459
|
+
'doctor_rule_result',
|
|
460
|
+
]),
|
|
461
|
+
},
|
|
462
|
+
...(graph.scope.artifactHash ? { artifactHash: graph.scope.artifactHash } : {}),
|
|
463
|
+
...(sourceLocator ? { sourceLocator } : {}),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function projectEvalStage(graph, path, entry) {
|
|
467
|
+
if (graph.source.sourceKind !== 'eval')
|
|
468
|
+
return undefined;
|
|
469
|
+
const variantNode = evalVariantNode(graph, entry);
|
|
470
|
+
if (!variantNode)
|
|
471
|
+
return undefined;
|
|
472
|
+
const skillNode = evalSkillNodeForVariant(graph, variantNode);
|
|
473
|
+
const artifactHash = nodeArtifactHash(variantNode) ?? nodeArtifactHash(skillNode);
|
|
474
|
+
const sourceLocator = graphSourceLocator(graph, skillNode);
|
|
475
|
+
const nodesById = graphNodeMap(graph);
|
|
476
|
+
const projectedNodeIds = new Set([variantNode.id]);
|
|
477
|
+
const projectedEdgeIds = new Set();
|
|
478
|
+
if (skillNode)
|
|
479
|
+
projectedNodeIds.add(skillNode.id);
|
|
480
|
+
const addEdge = (edge) => {
|
|
481
|
+
projectedEdgeIds.add(edge.id);
|
|
482
|
+
projectedNodeIds.add(edge.fromNodeId);
|
|
483
|
+
projectedNodeIds.add(edge.toNodeId);
|
|
484
|
+
};
|
|
485
|
+
for (const edge of graph.edges) {
|
|
486
|
+
if (edge.fromNodeId === variantNode.id && (edge.edgeKind === 'evaluates' || edge.edgeKind === 'derived_from')) {
|
|
487
|
+
addEdge(edge);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const evalResultNodes = graph.edges
|
|
491
|
+
.filter((edge) => edge.toNodeId === variantNode.id && edge.edgeKind === 'derived_from')
|
|
492
|
+
.map((edge) => nodesById.get(edge.fromNodeId))
|
|
493
|
+
.filter((node) => node?.nodeKind === 'eval_result');
|
|
494
|
+
for (const node of evalResultNodes)
|
|
495
|
+
projectedNodeIds.add(node.id);
|
|
496
|
+
for (const edge of graph.edges) {
|
|
497
|
+
if (!evalResultNodes.some((node) => node.id === edge.fromNodeId || node.id === edge.toNodeId))
|
|
498
|
+
continue;
|
|
499
|
+
if (edge.edgeKind === 'derived_from' || edge.edgeKind === 'evaluates' || edge.edgeKind === 'passes' || edge.edgeKind === 'fails' || edge.edgeKind === 'diagnoses') {
|
|
500
|
+
addEdge(edge);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const declaredCoverageStableKeys = new Set();
|
|
504
|
+
let coverageEdges = 0;
|
|
505
|
+
for (const edge of graph.edges) {
|
|
506
|
+
if (edge.edgeKind !== 'covers')
|
|
507
|
+
continue;
|
|
508
|
+
const from = nodesById.get(edge.fromNodeId);
|
|
509
|
+
const to = nodesById.get(edge.toNodeId);
|
|
510
|
+
if (from?.nodeKind !== 'sample' || !to)
|
|
511
|
+
continue;
|
|
512
|
+
if (!projectedNodeIds.has(from.id))
|
|
513
|
+
continue;
|
|
514
|
+
if (artifactHash && nodeArtifactHash(to) !== artifactHash)
|
|
515
|
+
continue;
|
|
516
|
+
addEdge(edge);
|
|
517
|
+
coverageEdges += 1;
|
|
518
|
+
if (to.stableKey)
|
|
519
|
+
declaredCoverageStableKeys.add(to.stableKey);
|
|
520
|
+
}
|
|
521
|
+
const sampleIds = new Set();
|
|
522
|
+
const assertionIds = new Set();
|
|
523
|
+
const diagnosticIds = new Set();
|
|
524
|
+
let failedAssertionEdges = 0;
|
|
525
|
+
for (const edge of graph.edges) {
|
|
526
|
+
if (!projectedEdgeIds.has(edge.id))
|
|
527
|
+
continue;
|
|
528
|
+
const from = nodesById.get(edge.fromNodeId);
|
|
529
|
+
const to = nodesById.get(edge.toNodeId);
|
|
530
|
+
if (from?.nodeKind === 'sample')
|
|
531
|
+
sampleIds.add(from.id);
|
|
532
|
+
if (to?.nodeKind === 'sample')
|
|
533
|
+
sampleIds.add(to.id);
|
|
534
|
+
if (from?.nodeKind === 'assertion')
|
|
535
|
+
assertionIds.add(from.id);
|
|
536
|
+
if (to?.nodeKind === 'assertion')
|
|
537
|
+
assertionIds.add(to.id);
|
|
538
|
+
if (from?.nodeKind === 'diagnostic')
|
|
539
|
+
diagnosticIds.add(from.id);
|
|
540
|
+
if (to?.nodeKind === 'diagnostic')
|
|
541
|
+
diagnosticIds.add(to.id);
|
|
542
|
+
if (edge.edgeKind === 'fails')
|
|
543
|
+
failedAssertionEdges += 1;
|
|
544
|
+
}
|
|
545
|
+
const measurementNodes = graphNodePreviews(graph, [
|
|
546
|
+
'sample',
|
|
547
|
+
'assertion',
|
|
548
|
+
'eval_result',
|
|
549
|
+
'diagnostic',
|
|
550
|
+
], projectedNodeIds);
|
|
551
|
+
return {
|
|
552
|
+
stage: {
|
|
553
|
+
sourceKind: 'eval',
|
|
554
|
+
sourceId: graph.source.sourceId,
|
|
555
|
+
graphId: graph.graphId,
|
|
556
|
+
generatedAt: graph.generatedAt,
|
|
557
|
+
graphPath: path,
|
|
558
|
+
nodeCount: projectedNodeIds.size,
|
|
559
|
+
edgeCount: projectedEdgeIds.size,
|
|
560
|
+
variantName: variantNode.label,
|
|
561
|
+
samples: sampleIds.size,
|
|
562
|
+
assertions: assertionIds.size,
|
|
563
|
+
failedAssertionEdges,
|
|
564
|
+
diagnostics: diagnosticIds.size,
|
|
565
|
+
measurementNodes,
|
|
566
|
+
coverageEdges,
|
|
567
|
+
declaredCoverageStableKeys: [...declaredCoverageStableKeys].sort(),
|
|
568
|
+
},
|
|
569
|
+
...(artifactHash ? { artifactHash } : {}),
|
|
570
|
+
...(sourceLocator ? { sourceLocator } : {}),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function graphSnapshotBinding(stages) {
|
|
574
|
+
const hashes = uniqueStrings(stages.map((stage) => stage.artifactHash ?? ''));
|
|
575
|
+
const locators = uniqueStrings(stages.map((stage) => stage.sourceLocator ?? ''));
|
|
576
|
+
if (stages.length > 0 && hashes.length === 1 && stages.every((stage) => stage.artifactHash === hashes[0])) {
|
|
577
|
+
return { bindingStrength: 'content-hash', artifactHash: hashes[0], ...(locators[0] ? { sourceLocator: locators[0] } : {}) };
|
|
578
|
+
}
|
|
579
|
+
if (hashes.length > 0) {
|
|
580
|
+
return { bindingStrength: 'mixed', ...(locators[0] ? { sourceLocator: locators[0] } : {}) };
|
|
581
|
+
}
|
|
582
|
+
if (locators.length > 0) {
|
|
583
|
+
return { bindingStrength: 'source-locator', sourceLocator: locators[0] };
|
|
584
|
+
}
|
|
585
|
+
return { bindingStrength: 'name-only' };
|
|
586
|
+
}
|
|
587
|
+
function graphSnapshotForEntry(entry, graphs) {
|
|
588
|
+
const doctorCandidates = graphs.filter(({ graph }) => graph.source.sourceKind === 'doctor'
|
|
589
|
+
&& (!entry.doctor || graph.source.sourceId === entry.doctor.reportId)
|
|
590
|
+
&& graphMatchesDoctorEntry(graph, entry));
|
|
591
|
+
const evalCandidates = graphs.filter(({ graph }) => graph.source.sourceKind === 'eval'
|
|
592
|
+
&& (!entry.eval || graph.source.sourceId === entry.eval.reportId)
|
|
593
|
+
&& graphMatchesEvalEntry(graph, entry));
|
|
594
|
+
const doctor = pickLatestGraph(doctorCandidates);
|
|
595
|
+
const evalGraph = pickLatestGraph(evalCandidates);
|
|
596
|
+
if (!doctor && !evalGraph)
|
|
597
|
+
return undefined;
|
|
598
|
+
const doctorStage = doctor ? projectDoctorStage(doctor.graph, doctor.path) : undefined;
|
|
599
|
+
const evalStage = evalGraph ? projectEvalStage(evalGraph.graph, evalGraph.path, entry) : undefined;
|
|
600
|
+
const projectedStages = [];
|
|
601
|
+
if (doctorStage)
|
|
602
|
+
projectedStages.push(doctorStage);
|
|
603
|
+
if (evalStage)
|
|
604
|
+
projectedStages.push(evalStage);
|
|
605
|
+
const binding = graphSnapshotBinding(projectedStages);
|
|
606
|
+
return {
|
|
607
|
+
...binding,
|
|
608
|
+
...(doctorStage ? { doctor: doctorStage.stage } : {}),
|
|
609
|
+
...(evalStage ? { eval: evalStage.stage } : {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
230
612
|
/** 扫 doctorsDir/*.report.json,按 skill 名分桶,**返回该 skill 的所有历史 snapshot**(asc 时序)。
|
|
231
613
|
* renderer 用最后一项做"当前",前面项画 sparkline。 */
|
|
232
614
|
function scanDoctorReports(dir) {
|
|
@@ -265,8 +647,15 @@ function scanDoctorReports(dir) {
|
|
|
265
647
|
export function buildSkillIndex(reports, analysesDir, doctorsDir, observationsDir = DEFAULT_OBSERVATIONS_DIR, opts = {}) {
|
|
266
648
|
const includeObserveCards = opts.includeObserveCards ?? false;
|
|
267
649
|
const includeDoctorCards = opts.includeDoctorCards ?? false;
|
|
650
|
+
const includeReportCards = opts.includeReportCards ?? false;
|
|
651
|
+
const evalGraphDirs = uniqueStrings(opts.evalGraphDirs ?? []);
|
|
652
|
+
const doctorGraphDirs = uniqueStrings([
|
|
653
|
+
doctorGraphDirForDoctorOutput(doctorsDir),
|
|
654
|
+
...(opts.doctorGraphDirs ?? []),
|
|
655
|
+
]);
|
|
656
|
+
const graphFingerprint = buildGraphFingerprint(evalGraphDirs, doctorGraphDirs, includeReportCards, includeDoctorCards);
|
|
268
657
|
// 命中缓存就直接返回(fingerprint 覆盖 reports + 两个 dir + 卡片 include 模式的变化信号)
|
|
269
|
-
const fp = buildIndexFingerprint(reports, analysesDir, doctorsDir, observationsDir, includeObserveCards, includeDoctorCards);
|
|
658
|
+
const fp = buildIndexFingerprint(reports, analysesDir, doctorsDir, observationsDir, includeObserveCards, includeDoctorCards, graphFingerprint);
|
|
270
659
|
if (_indexCache && _indexCache.fingerprint === fp) {
|
|
271
660
|
return _indexCache.result;
|
|
272
661
|
}
|
|
@@ -409,6 +798,17 @@ export function buildSkillIndex(reports, analysesDir, doctorsDir, observationsDi
|
|
|
409
798
|
else if (hasMed)
|
|
410
799
|
ent.band = 'yellow';
|
|
411
800
|
}
|
|
801
|
+
const graphs = loadGraphsForSkillIndex({
|
|
802
|
+
evalGraphDirs,
|
|
803
|
+
doctorGraphDirs,
|
|
804
|
+
includeReportCards,
|
|
805
|
+
includeDoctorCards,
|
|
806
|
+
});
|
|
807
|
+
for (const ent of entries) {
|
|
808
|
+
const graph = graphSnapshotForEntry(ent, graphs);
|
|
809
|
+
if (graph)
|
|
810
|
+
ent.graph = graph;
|
|
811
|
+
}
|
|
412
812
|
summary.red = entries.filter((e) => e.band === 'red').length;
|
|
413
813
|
summary.yellow = entries.filter((e) => e.band === 'yellow').length;
|
|
414
814
|
summary.green = entries.filter((e) => e.band === 'green').length;
|
|
@@ -29,7 +29,7 @@ export type ArtifactGraphNodeRole = 'entity' | 'observation' | 'aggregate';
|
|
|
29
29
|
export type ArtifactGraphNodeKind = 'skill' | 'skill_file' | 'frontmatter' | 'reference' | 'script' | 'tool' | 'env' | 'preflight' | 'hard_rule' | 'workflow' | 'workflow_node' | 'sample' | 'assertion' | 'variant' | 'doctor_rule_result' | 'eval_result' | 'judge_dimension' | 'diagnostic' | 'trace_session' | 'skill_invocation' | 'tool_call' | 'gap_signal';
|
|
30
30
|
export type ArtifactGraphStatus = 'ok' | 'warning' | 'failed' | 'skipped' | 'unknown' | 'not_measured';
|
|
31
31
|
export interface ArtifactGraphBinding {
|
|
32
|
-
bindingStrength: 'content-hash' | 'source-locator' | 'runtime-trace' | 'name-only' | 'aggregate';
|
|
32
|
+
bindingStrength: 'content-hash' | 'source-locator' | 'runtime-trace' | 'name-only' | 'aggregate' | 'explicit';
|
|
33
33
|
keys: Record<string, string>;
|
|
34
34
|
}
|
|
35
35
|
export interface ArtifactGraphAttrs {
|
package/dist/types/eval.d.ts
CHANGED
|
@@ -27,6 +27,14 @@ export interface Assertion {
|
|
|
27
27
|
export type SampleProvenance = 'human' | 'llm-generated' | 'production-trace';
|
|
28
28
|
/** sample 难度等级。简单分桶,跟 IRT 风格 fine-grained difficulty 不同。 */
|
|
29
29
|
export type SampleDifficulty = 'easy' | 'medium' | 'hard';
|
|
30
|
+
/** Sample 可选声明的 skill 结构锚点。用于 Skill Map / 诊断,不参与 grading / judge / verdict。 */
|
|
31
|
+
export type SampleCoverageTargetKind = 'skill' | 'skill_file' | 'frontmatter' | 'reference' | 'script' | 'hard_rule' | 'workflow' | 'workflow_node';
|
|
32
|
+
export interface SampleCoverageTarget {
|
|
33
|
+
/** 目标类型。避免裸 kind,保持 Artifact.kind 语义唯一。 */
|
|
34
|
+
targetKind: SampleCoverageTargetKind;
|
|
35
|
+
/** 稳定引用:路径或 ID。reference/script 用 skill 根相对路径,workflow_node 用 workflowId.nodeId。 */
|
|
36
|
+
ref: string;
|
|
37
|
+
}
|
|
30
38
|
/** Sample 评测环境前置:声明性"已就绪"清单,LLM 看到后跳过环境探测,直接进入工作流。
|
|
31
39
|
* 类比 unit test 的 fixture / setup —— 评测是测 skill 工作流,不是测环境探测能力。 */
|
|
32
40
|
export interface SampleEnvironment {
|
|
@@ -112,6 +120,8 @@ export interface Sample {
|
|
|
112
120
|
* curated 用 `'human'`,production trace 抽样用 `'production-trace'`。
|
|
113
121
|
* 纯文档 / 诊断用。 */
|
|
114
122
|
provenance?: SampleProvenance;
|
|
123
|
+
/** 该 sample 可选声明的 skill 结构锚点。纯文档 / 图谱诊断用,不进入评分、评委或 verdict。 */
|
|
124
|
+
covers?: SampleCoverageTarget[];
|
|
115
125
|
/** 诱错样本(tripwire)标记。true = 此 sample 故意设计成 LLM 应该 fail 的诱导陷阱
|
|
116
126
|
* (如:用户用错误前提诱导 / 跳步骤 / 用错参数类型),用于测 skill 是否能让 LLM
|
|
117
127
|
* 识破并纠正。Diagnostic 看到 tripwire:true 时会建议"无需改 skill"(rootCause:
|
package/dist/types/report.d.ts
CHANGED
|
@@ -346,6 +346,7 @@ export interface ResultEntry {
|
|
|
346
346
|
* - assertions: 期望(运行后的 pass/fail 结果在 VariantResult.assertions.details 里)
|
|
347
347
|
* - mocks: 工具调用模拟返回(LLM 调到匹配的 tool/参数时拿到这段假返回,而不是真去调外部系统)
|
|
348
348
|
* - capability / construct / difficulty: 元数据
|
|
349
|
+
* - covers: sample 可选声明的 skill 结构锚点
|
|
349
350
|
* - context: 附加上下文(代码片段等)
|
|
350
351
|
*
|
|
351
352
|
* 旧 report 没有这个字段,renderer 据此 fallback(隐藏单测 tab 或提示"无设计快照")。
|
|
@@ -361,6 +362,7 @@ export interface SampleSnapshot {
|
|
|
361
362
|
difficulty?: import('./eval.js').SampleDifficulty;
|
|
362
363
|
construct?: string;
|
|
363
364
|
provenance?: import('./eval.js').SampleProvenance;
|
|
365
|
+
covers?: import('./eval.js').SampleCoverageTarget[];
|
|
364
366
|
/** Diagnostic 用 — tripwire sample 不该建议改 skill。 */
|
|
365
367
|
tripwire?: boolean;
|
|
366
368
|
}
|
|
@@ -476,7 +478,7 @@ export interface AnalysisResult {
|
|
|
476
478
|
coverage?: Record<string, KnowledgeCoverage>;
|
|
477
479
|
/** Per-variant knowledge gap reports. See docs/specs/knowledge-gap-signal-spec.md */
|
|
478
480
|
gapReports?: Record<string, GapReport>;
|
|
479
|
-
/** Sample design science aggregate. Built from
|
|
481
|
+
/** Sample design science aggregate. Built from measurement metadata
|
|
480
482
|
* (capability / difficulty / construct / provenance); persisted on report
|
|
481
483
|
* for studio to surface coverage gaps. See docs/specs/sample-design-spec.md. */
|
|
482
484
|
sampleQuality?: SampleQualityAggregate;
|