oh-my-knowledge 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/artifact-graph/doctor.d.ts +21 -0
- package/dist/artifact-graph/doctor.js +569 -0
- package/dist/assets/agent-skills/omk/SKILL.md +9 -9
- package/dist/assets/agent-skills/omk/references/commands.md +1 -1
- package/dist/cli/commands/doctor.js +62 -61
- package/dist/cli/commands/eval/index.js +20 -2
- package/dist/cli/commands/init.js +1 -0
- package/dist/cli/commands/observe/index.d.ts +2 -2
- package/dist/cli/commands/observe/index.js +8 -7
- package/dist/cli/commands/sample.js +22 -16
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +8 -0
- package/dist/cli/lib/i18n-dict/help.js +12 -10
- package/dist/cli/lib/parse-run-config/samples-discovery.d.ts +5 -7
- package/dist/cli/lib/parse-run-config/samples-discovery.js +10 -32
- package/dist/cli/lib/parse-run-config.js +4 -4
- package/dist/cli/lib/resolve-skill-input.js +10 -12
- package/dist/doctor/messages.js +2 -2
- package/dist/eval-core/artifact-file-names.d.ts +15 -0
- package/dist/eval-core/artifact-file-names.js +46 -0
- package/dist/eval-core/evaluation-reporting.js +2 -1
- package/dist/eval-core/measurement-dirs.js +13 -7
- package/dist/eval-core/report-file-migration.d.ts +10 -0
- package/dist/eval-core/report-file-migration.js +90 -0
- package/dist/inputs/sample-locator.d.ts +23 -0
- package/dist/inputs/sample-locator.js +195 -0
- package/dist/inputs/skill-loader.js +7 -17
- package/dist/observability/inbox.js +7 -3
- package/dist/server/report-server.js +10 -4
- package/dist/server/report-store.js +17 -9
- package/dist/server/skill-index.js +16 -11
- package/dist/types/artifact-graph.d.ts +93 -0
- package/dist/types/artifact-graph.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const REPORT_FILE_SUFFIX = ".report.json";
|
|
2
|
+
export declare const GRAPH_FILE_SUFFIX = ".graph.json";
|
|
3
|
+
export declare const CARD_FILE_SUFFIX = ".card.md";
|
|
4
|
+
export declare function safeArtifactFileStem(id: string): string;
|
|
5
|
+
export declare function reportFileName(stem: string): string;
|
|
6
|
+
export declare function reportFilePath(dir: string, stem: string): string;
|
|
7
|
+
export declare function reportFileStem(fileName: string): string | null;
|
|
8
|
+
export declare function isReportFileName(fileName: string): boolean;
|
|
9
|
+
export declare function graphFileName(stem: string): string;
|
|
10
|
+
export declare function cardFileName(stem: string): string;
|
|
11
|
+
export declare function runTimestamp(date?: Date): string;
|
|
12
|
+
export declare function randomRunToken(): string;
|
|
13
|
+
export declare function runFileSuffix(counter?: number): string;
|
|
14
|
+
export declare function stripDomainPrefix(id: string, domain: string): string;
|
|
15
|
+
export declare function doctorReportFileStem(skillName: string, reportId: string): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
export const REPORT_FILE_SUFFIX = '.report.json';
|
|
3
|
+
export const GRAPH_FILE_SUFFIX = '.graph.json';
|
|
4
|
+
export const CARD_FILE_SUFFIX = '.card.md';
|
|
5
|
+
export function safeArtifactFileStem(id) {
|
|
6
|
+
return id.replaceAll(/[/\\:*?"<>|]/g, '_');
|
|
7
|
+
}
|
|
8
|
+
export function reportFileName(stem) {
|
|
9
|
+
return `${safeArtifactFileStem(stem)}${REPORT_FILE_SUFFIX}`;
|
|
10
|
+
}
|
|
11
|
+
export function reportFilePath(dir, stem) {
|
|
12
|
+
return join(dir, reportFileName(stem));
|
|
13
|
+
}
|
|
14
|
+
export function reportFileStem(fileName) {
|
|
15
|
+
return fileName.endsWith(REPORT_FILE_SUFFIX)
|
|
16
|
+
? fileName.slice(0, -REPORT_FILE_SUFFIX.length)
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
export function isReportFileName(fileName) {
|
|
20
|
+
return reportFileStem(fileName) !== null;
|
|
21
|
+
}
|
|
22
|
+
export function graphFileName(stem) {
|
|
23
|
+
return `${safeArtifactFileStem(stem)}${GRAPH_FILE_SUFFIX}`;
|
|
24
|
+
}
|
|
25
|
+
export function cardFileName(stem) {
|
|
26
|
+
return `${safeArtifactFileStem(stem)}${CARD_FILE_SUFFIX}`;
|
|
27
|
+
}
|
|
28
|
+
export function runTimestamp(date = new Date()) {
|
|
29
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
30
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}T${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
31
|
+
}
|
|
32
|
+
export function randomRunToken() {
|
|
33
|
+
return Math.random().toString(36).slice(2, 6);
|
|
34
|
+
}
|
|
35
|
+
export function runFileSuffix(counter) {
|
|
36
|
+
const middle = counter === undefined ? '' : `-${counter}`;
|
|
37
|
+
return `${runTimestamp()}${middle}-${randomRunToken()}`;
|
|
38
|
+
}
|
|
39
|
+
export function stripDomainPrefix(id, domain) {
|
|
40
|
+
const safeId = safeArtifactFileStem(id);
|
|
41
|
+
const prefix = `${domain}-`;
|
|
42
|
+
return safeId.startsWith(prefix) ? safeId.slice(prefix.length) : safeId;
|
|
43
|
+
}
|
|
44
|
+
export function doctorReportFileStem(skillName, reportId) {
|
|
45
|
+
return `${safeArtifactFileStem(skillName)}-${stripDomainPrefix(reportId, 'doctor')}`;
|
|
46
|
+
}
|
|
@@ -5,6 +5,7 @@ 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
9
|
import { buildVariantSummary } from './schema.js';
|
|
9
10
|
import { buildVariantConfig, resolveExecutionStrategy } from './execution-strategy.js';
|
|
10
11
|
import { getJudgePromptHash } from '../grading/judge.js';
|
|
@@ -280,7 +281,7 @@ export function persistReport(report, outputDir) {
|
|
|
280
281
|
return null;
|
|
281
282
|
if (!existsSync(outputDir))
|
|
282
283
|
mkdirSync(outputDir, { recursive: true });
|
|
283
|
-
const filePath =
|
|
284
|
+
const filePath = reportFilePath(outputDir, report.id);
|
|
284
285
|
writeFileSync(filePath, JSON.stringify(report, null, 2));
|
|
285
286
|
// 产物发现索引:报告落项目本地后,best-effort 追加全局轻卡片,让 omk studio 跨项目聚合成机器级总览。
|
|
286
287
|
// 永不抛、永不阻断报告落盘(正文是 source of truth)。
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { DEFAULT_OBSERVE_HEALTH_DIR, DEFAULT_DOCTORS_DIR, DEFAULT_REPORTS_DIR } from './default-dirs.js';
|
|
4
|
+
import { isReportFileName } from './artifact-file-names.js';
|
|
5
|
+
import { migrateLegacyReportFiles } from './report-file-migration.js';
|
|
4
6
|
/**
|
|
5
7
|
* 测量产物的「项目优先 → 全局兜底」目录解析,镜像 managed 的 `resolveManagedDir`
|
|
6
8
|
* (src/managed/store.ts)。测量产物绑用例集上下文(construct validity,不可全局化),
|
|
7
9
|
* 默认落项目 `.omk/`,全局作显式 opt-in。
|
|
8
10
|
*
|
|
9
|
-
* 「记录优先」—— 目录里有匹配
|
|
10
|
-
* 避免空项目目录遮蔽全局数据。项目目录按**调用时** `cwd()`
|
|
11
|
-
*
|
|
11
|
+
* 「记录优先」—— 目录里有匹配 report 文件才算数(不是「目录存在」),与 managed 同口径,
|
|
12
|
+
* 避免空项目目录遮蔽全局数据。项目目录按**调用时** `cwd()` 求值(函数,不是 import 时
|
|
13
|
+
* 冻结的常量),studio 长会话 per-request 解析才正确。
|
|
12
14
|
*/
|
|
13
15
|
/** dir 里是否有至少一个满足 match 的文件(短路,不读文件内容)。 */
|
|
14
16
|
function hasMatchingFile(dir, match) {
|
|
@@ -22,7 +24,7 @@ function hasMatchingFile(dir, match) {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
// —— observe-health(skill 健康度报告)——
|
|
25
|
-
// 文件名 `{
|
|
27
|
+
// 文件名 `{run}.report.json`,目录已表达 observe-health 域。
|
|
26
28
|
/** 项目级 observe-health 目录(相对调用时 cwd)。 */
|
|
27
29
|
export function projectObserveHealthDir(cwd = process.cwd()) {
|
|
28
30
|
return join(cwd, '.omk', 'observe-health');
|
|
@@ -34,7 +36,9 @@ export function globalObserveHealthDir() {
|
|
|
34
36
|
/** 权威 observe-health 目录:项目有报告取项目,否则全局有取全局,都空回项目(同 resolveManagedDir)。
|
|
35
37
|
* `global` 可注入(默认真实全局目录),仅供测试用受控 temp 目录复现 project↔global 兜底。 */
|
|
36
38
|
export function resolveObserveHealthDir(dir = projectObserveHealthDir(), global = globalObserveHealthDir()) {
|
|
37
|
-
|
|
39
|
+
migrateLegacyReportFiles(dir, 'observe-health');
|
|
40
|
+
migrateLegacyReportFiles(global, 'observe-health');
|
|
41
|
+
const has = (d) => hasMatchingFile(d, isReportFileName);
|
|
38
42
|
if (has(dir))
|
|
39
43
|
return dir;
|
|
40
44
|
if (dir !== global && has(global))
|
|
@@ -42,7 +46,7 @@ export function resolveObserveHealthDir(dir = projectObserveHealthDir(), global
|
|
|
42
46
|
return dir;
|
|
43
47
|
}
|
|
44
48
|
// —— doctors(体检报告)——
|
|
45
|
-
// 文件名 `{skill}-{
|
|
49
|
+
// 文件名 `{skill}-{run}.report.json`,谓词用 report 后缀即可(真解析在下游 scanDoctorReports)。
|
|
46
50
|
/** 项目级 doctors 目录(相对调用时 cwd)。 */
|
|
47
51
|
export function projectDoctorsDir(cwd = process.cwd()) {
|
|
48
52
|
return join(cwd, '.omk', 'doctors');
|
|
@@ -54,7 +58,9 @@ export function globalDoctorsDir() {
|
|
|
54
58
|
/** 权威 doctors 目录:项目有报告取项目,否则全局有取全局,都空回项目(同 resolveManagedDir)。
|
|
55
59
|
* `global` 可注入(默认真实全局目录),仅供测试用受控 temp 目录复现 project↔global 兜底。 */
|
|
56
60
|
export function resolveDoctorsDir(dir = projectDoctorsDir(), global = globalDoctorsDir()) {
|
|
57
|
-
|
|
61
|
+
migrateLegacyReportFiles(dir, 'doctor');
|
|
62
|
+
migrateLegacyReportFiles(global, 'doctor');
|
|
63
|
+
const has = (d) => hasMatchingFile(d, isReportFileName);
|
|
58
64
|
if (has(dir))
|
|
59
65
|
return dir;
|
|
60
66
|
if (dir !== global && has(global))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LegacyReportDomain = 'report' | 'doctor' | 'observe-health' | 'observe-inbox';
|
|
2
|
+
/**
|
|
3
|
+
* One-shot migration for pre-`.report.json` run artifacts.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately scoped to measurement report directories. It does not make
|
|
6
|
+
* bare `.json` a normal reader format again; it just renames known legacy report
|
|
7
|
+
* files into the canonical name so old local data can be discovered, rotated, and
|
|
8
|
+
* eventually garbage-collected by the new code paths.
|
|
9
|
+
*/
|
|
10
|
+
export declare function migrateLegacyReportFiles(dir: string, domain: LegacyReportDomain): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { doctorReportFileStem, isReportFileName, reportFilePath, stripDomainPrefix } from './artifact-file-names.js';
|
|
4
|
+
function legacyJsonStem(fileName) {
|
|
5
|
+
if (!fileName.endsWith('.json'))
|
|
6
|
+
return null;
|
|
7
|
+
if (isReportFileName(fileName))
|
|
8
|
+
return null;
|
|
9
|
+
if (fileName.includes('.json.tmp.'))
|
|
10
|
+
return null;
|
|
11
|
+
return fileName.slice(0, -'.json'.length);
|
|
12
|
+
}
|
|
13
|
+
function isObject(value) {
|
|
14
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function looksLikeObserveHealthReport(value) {
|
|
17
|
+
return isObject(value.meta) && isObject(value.bySkill) && isObject(value.overall);
|
|
18
|
+
}
|
|
19
|
+
function targetStemForLegacyReport(domain, fileName, data) {
|
|
20
|
+
const fileStem = legacyJsonStem(fileName);
|
|
21
|
+
if (!fileStem || !isObject(data))
|
|
22
|
+
return null;
|
|
23
|
+
if (domain === 'report') {
|
|
24
|
+
const kind = data.kind;
|
|
25
|
+
return (kind === 'evaluation' || kind === 'batch-evaluation') && typeof data.id === 'string' && data.id
|
|
26
|
+
? data.id
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
if (domain === 'doctor') {
|
|
30
|
+
if (data.kind !== 'doctor' || typeof data.id !== 'string' || !Array.isArray(data.skills) || data.skills.length !== 1)
|
|
31
|
+
return null;
|
|
32
|
+
const skill = data.skills[0];
|
|
33
|
+
return isObject(skill) && typeof skill.skillName === 'string' && skill.skillName
|
|
34
|
+
? doctorReportFileStem(skill.skillName, data.id)
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
if (domain === 'observe-health') {
|
|
38
|
+
if (data.kind !== 'observe-health' && !looksLikeObserveHealthReport(data))
|
|
39
|
+
return null;
|
|
40
|
+
return stripDomainPrefix(fileStem.replace(/-observe-health$/, ''), 'observe-health');
|
|
41
|
+
}
|
|
42
|
+
if (data.kind !== 'observe-inbox')
|
|
43
|
+
return null;
|
|
44
|
+
return stripDomainPrefix(fileStem.replace(/-observe-inbox$/, ''), 'observe-inbox');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One-shot migration for pre-`.report.json` run artifacts.
|
|
48
|
+
*
|
|
49
|
+
* This is deliberately scoped to measurement report directories. It does not make
|
|
50
|
+
* bare `.json` a normal reader format again; it just renames known legacy report
|
|
51
|
+
* files into the canonical name so old local data can be discovered, rotated, and
|
|
52
|
+
* eventually garbage-collected by the new code paths.
|
|
53
|
+
*/
|
|
54
|
+
export function migrateLegacyReportFiles(dir, domain) {
|
|
55
|
+
if (!existsSync(dir))
|
|
56
|
+
return;
|
|
57
|
+
let files;
|
|
58
|
+
try {
|
|
59
|
+
files = readdirSync(dir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
if (!legacyJsonStem(file))
|
|
66
|
+
continue;
|
|
67
|
+
const sourcePath = join(dir, file);
|
|
68
|
+
let targetStem = null;
|
|
69
|
+
try {
|
|
70
|
+
targetStem = targetStemForLegacyReport(domain, file, JSON.parse(readFileSync(sourcePath, 'utf-8')));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!targetStem)
|
|
76
|
+
continue;
|
|
77
|
+
const targetPath = reportFilePath(dir, targetStem);
|
|
78
|
+
try {
|
|
79
|
+
if (existsSync(targetPath)) {
|
|
80
|
+
unlinkSync(sourcePath);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
renameSync(sourcePath, targetPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Best-effort migration only. Readers will ignore the legacy file if it remains.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const PROJECT_SAMPLE_FILENAMES: readonly ["eval-samples.json", "eval-samples.yaml", "eval-samples.yml"];
|
|
2
|
+
export declare const SKILL_LOCAL_SAMPLE_FILENAMES: readonly ["samples.json", "samples.yaml", "samples.yml"];
|
|
3
|
+
export interface DeprecatedSkillSamplesHint {
|
|
4
|
+
oldPath: string;
|
|
5
|
+
newPath: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function isExistingDirectory(path: string): boolean;
|
|
8
|
+
export declare function hasLoadableSampleFile(dir: string): boolean;
|
|
9
|
+
export declare function findProjectSamplesFile(dir: string): string | null;
|
|
10
|
+
export declare function skillLocalSamplesDir(skillRoot: string): string;
|
|
11
|
+
export declare function defaultSkillLocalSamplesFile(skillRoot: string): string;
|
|
12
|
+
export declare function hasUsableSamplesPath(path: string): boolean;
|
|
13
|
+
export declare function findSkillLocalSamplesDir(skillRoot: string): string | null;
|
|
14
|
+
export declare function findSkillSamplesPath(skillRoot: string): string | null;
|
|
15
|
+
export declare function findDeprecatedSkillSamplesHint(skillRoot: string): DeprecatedSkillSamplesHint | null;
|
|
16
|
+
export declare function findFlatSkillSamplesPath(skillDir: string, skillName: string): string | null;
|
|
17
|
+
export declare function defaultFlatSkillSamplesFile(skillDir: string, skillName: string): string;
|
|
18
|
+
export declare function isDirectorySkillRoot(path: string): boolean;
|
|
19
|
+
export declare function findNamedSkillSamplesPath(skillDir: string, skillName: string): string | null;
|
|
20
|
+
export declare function findSingleTreatmentSamplesPath(treatmentExpr: string, skillDir: string, cwd?: string): string | null;
|
|
21
|
+
export declare function findSingleTreatmentDeprecatedSamplesHint(treatmentExpr: string, skillDir: string, cwd?: string): DeprecatedSkillSamplesHint | null;
|
|
22
|
+
export declare function findDoctorSamplesPath(target: string | null, cwd: string): string | null;
|
|
23
|
+
export declare function findDoctorDeprecatedSamplesHint(target: string | null, cwd: string): DeprecatedSkillSamplesHint | null;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
export const PROJECT_SAMPLE_FILENAMES = ['eval-samples.json', 'eval-samples.yaml', 'eval-samples.yml'];
|
|
4
|
+
export const SKILL_LOCAL_SAMPLE_FILENAMES = ['samples.json', 'samples.yaml', 'samples.yml'];
|
|
5
|
+
const SAMPLE_FILE_RE = /\.(json|ya?ml)$/i;
|
|
6
|
+
const RESERVED_SAMPLE_FILE_RE = /^(report|health|_)/i;
|
|
7
|
+
function isExistingFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
return existsSync(path) && statSync(path).isFile();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isExistingDirectory(path) {
|
|
16
|
+
try {
|
|
17
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function hasLoadableSampleFile(dir) {
|
|
24
|
+
if (!isExistingDirectory(dir))
|
|
25
|
+
return false;
|
|
26
|
+
try {
|
|
27
|
+
return readdirSync(dir).some((file) => SAMPLE_FILE_RE.test(file) && !RESERVED_SAMPLE_FILE_RE.test(file));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function findProjectSamplesFile(dir) {
|
|
34
|
+
for (const name of PROJECT_SAMPLE_FILENAMES) {
|
|
35
|
+
const candidate = join(dir, name);
|
|
36
|
+
if (isExistingFile(candidate))
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function skillLocalSamplesDir(skillRoot) {
|
|
42
|
+
return join(skillRoot, '.omk');
|
|
43
|
+
}
|
|
44
|
+
export function defaultSkillLocalSamplesFile(skillRoot) {
|
|
45
|
+
return join(skillLocalSamplesDir(skillRoot), SKILL_LOCAL_SAMPLE_FILENAMES[0]);
|
|
46
|
+
}
|
|
47
|
+
export function hasUsableSamplesPath(path) {
|
|
48
|
+
if (isExistingFile(path))
|
|
49
|
+
return true;
|
|
50
|
+
return hasLoadableSampleFile(path);
|
|
51
|
+
}
|
|
52
|
+
export function findSkillLocalSamplesDir(skillRoot) {
|
|
53
|
+
const dir = skillLocalSamplesDir(skillRoot);
|
|
54
|
+
return hasLoadableSampleFile(dir) ? dir : null;
|
|
55
|
+
}
|
|
56
|
+
export function findSkillSamplesPath(skillRoot) {
|
|
57
|
+
return findSkillLocalSamplesDir(skillRoot);
|
|
58
|
+
}
|
|
59
|
+
export function findDeprecatedSkillSamplesHint(skillRoot) {
|
|
60
|
+
if (!isDirectorySkillRoot(skillRoot))
|
|
61
|
+
return null;
|
|
62
|
+
for (const name of PROJECT_SAMPLE_FILENAMES) {
|
|
63
|
+
const oldPath = join(skillRoot, name);
|
|
64
|
+
if (isExistingFile(oldPath)) {
|
|
65
|
+
return { oldPath, newPath: defaultSkillLocalSamplesFile(skillRoot) };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
export function findFlatSkillSamplesPath(skillDir, skillName) {
|
|
71
|
+
for (const ext of ['json', 'yaml', 'yml']) {
|
|
72
|
+
const candidate = join(skillDir, `${skillName}.eval-samples.${ext}`);
|
|
73
|
+
if (isExistingFile(candidate))
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
export function defaultFlatSkillSamplesFile(skillDir, skillName) {
|
|
79
|
+
return join(skillDir, `${skillName}.eval-samples.json`);
|
|
80
|
+
}
|
|
81
|
+
export function isDirectorySkillRoot(path) {
|
|
82
|
+
return isExistingDirectory(path) && isExistingFile(join(path, 'SKILL.md'));
|
|
83
|
+
}
|
|
84
|
+
export function findNamedSkillSamplesPath(skillDir, skillName) {
|
|
85
|
+
const flatSkillPath = join(skillDir, `${skillName}.md`);
|
|
86
|
+
if (isExistingFile(flatSkillPath)) {
|
|
87
|
+
return findFlatSkillSamplesPath(skillDir, skillName);
|
|
88
|
+
}
|
|
89
|
+
const dirSkillRoot = join(skillDir, skillName);
|
|
90
|
+
if (isDirectorySkillRoot(dirSkillRoot)) {
|
|
91
|
+
return findSkillSamplesPath(dirSkillRoot);
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function findSamplesForExistingSkillPath(path) {
|
|
96
|
+
if (isExistingDirectory(path)) {
|
|
97
|
+
return findSkillSamplesPath(path);
|
|
98
|
+
}
|
|
99
|
+
const parent = dirname(path);
|
|
100
|
+
if (basename(path) === 'SKILL.md') {
|
|
101
|
+
return findSkillSamplesPath(parent);
|
|
102
|
+
}
|
|
103
|
+
if (/\.md$/i.test(path)) {
|
|
104
|
+
const skillName = basename(path).replace(/\.md$/i, '');
|
|
105
|
+
return findFlatSkillSamplesPath(parent, skillName);
|
|
106
|
+
}
|
|
107
|
+
return findProjectSamplesFile(parent);
|
|
108
|
+
}
|
|
109
|
+
function findDeprecatedSamplesForExistingSkillPath(path) {
|
|
110
|
+
if (isExistingDirectory(path)) {
|
|
111
|
+
return findDeprecatedSkillSamplesHint(path);
|
|
112
|
+
}
|
|
113
|
+
if (basename(path) === 'SKILL.md') {
|
|
114
|
+
return findDeprecatedSkillSamplesHint(dirname(path));
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
export function findSingleTreatmentSamplesPath(treatmentExpr, skillDir, cwd = process.cwd()) {
|
|
119
|
+
const resolved = resolve(cwd, treatmentExpr);
|
|
120
|
+
if (existsSync(resolved)) {
|
|
121
|
+
const samplesPath = findSamplesForExistingSkillPath(resolved);
|
|
122
|
+
if (samplesPath)
|
|
123
|
+
return samplesPath;
|
|
124
|
+
}
|
|
125
|
+
return findNamedSkillSamplesPath(skillDir, treatmentExpr);
|
|
126
|
+
}
|
|
127
|
+
export function findSingleTreatmentDeprecatedSamplesHint(treatmentExpr, skillDir, cwd = process.cwd()) {
|
|
128
|
+
const resolved = resolve(cwd, treatmentExpr);
|
|
129
|
+
if (existsSync(resolved)) {
|
|
130
|
+
return findDeprecatedSamplesForExistingSkillPath(resolved);
|
|
131
|
+
}
|
|
132
|
+
const flatSkillPath = join(skillDir, `${treatmentExpr}.md`);
|
|
133
|
+
if (isExistingFile(flatSkillPath))
|
|
134
|
+
return null;
|
|
135
|
+
return findDeprecatedSkillSamplesHint(join(skillDir, treatmentExpr));
|
|
136
|
+
}
|
|
137
|
+
function projectSampleSearchDirs(target, cwd) {
|
|
138
|
+
const dirs = [];
|
|
139
|
+
const add = (dir) => {
|
|
140
|
+
const abs = resolve(dir);
|
|
141
|
+
if (!dirs.includes(abs))
|
|
142
|
+
dirs.push(abs);
|
|
143
|
+
};
|
|
144
|
+
if (target) {
|
|
145
|
+
const absTarget = resolve(target);
|
|
146
|
+
if (existsSync(absTarget)) {
|
|
147
|
+
if (isExistingDirectory(absTarget)) {
|
|
148
|
+
if (isDirectorySkillRoot(absTarget)) {
|
|
149
|
+
add(dirname(dirname(absTarget)));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
add(absTarget);
|
|
153
|
+
add(dirname(absTarget));
|
|
154
|
+
add(dirname(dirname(absTarget)));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const parent = dirname(absTarget);
|
|
159
|
+
if (basename(absTarget) === 'SKILL.md') {
|
|
160
|
+
add(dirname(dirname(parent)));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
add(parent);
|
|
164
|
+
add(dirname(parent));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
add(cwd);
|
|
170
|
+
return dirs;
|
|
171
|
+
}
|
|
172
|
+
export function findDoctorSamplesPath(target, cwd) {
|
|
173
|
+
if (target) {
|
|
174
|
+
const absTarget = resolve(target);
|
|
175
|
+
if (existsSync(absTarget)) {
|
|
176
|
+
const targetSamples = findSamplesForExistingSkillPath(absTarget);
|
|
177
|
+
if (targetSamples)
|
|
178
|
+
return targetSamples;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const dir of projectSampleSearchDirs(target, cwd)) {
|
|
182
|
+
const samplesPath = findProjectSamplesFile(dir);
|
|
183
|
+
if (samplesPath)
|
|
184
|
+
return samplesPath;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
export function findDoctorDeprecatedSamplesHint(target, cwd) {
|
|
189
|
+
if (!target)
|
|
190
|
+
return null;
|
|
191
|
+
const absTarget = resolve(cwd, target);
|
|
192
|
+
if (!existsSync(absTarget))
|
|
193
|
+
return null;
|
|
194
|
+
return findDeprecatedSamplesForExistingSkillPath(absTarget);
|
|
195
|
+
}
|
|
@@ -5,6 +5,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
5
5
|
import { extractSkillHardRules, extractSkillWorkflows } from '../shared/hard-rules.js';
|
|
6
6
|
import { hashArtifactSource, hashBytes, isDistributablePath } from './content-hash.js';
|
|
7
7
|
import { materializeIsolatedCopy } from './materialize-copy.js';
|
|
8
|
+
import { findFlatSkillSamplesPath, findSkillSamplesPath } from './sample-locator.js';
|
|
8
9
|
function parseFrontmatterPreflight(content) {
|
|
9
10
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
10
11
|
if (!match)
|
|
@@ -388,14 +389,9 @@ export function discoverBatchSkills(skillDir) {
|
|
|
388
389
|
const mdMatch = entry.endsWith('.md') && !entry.endsWith('.eval-samples.json');
|
|
389
390
|
if (mdMatch) {
|
|
390
391
|
const name = entry.slice(0, -3);
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
join(skillDir,
|
|
394
|
-
join(skillDir, `${name}.eval-samples.yaml`),
|
|
395
|
-
join(skillDir, `${name}.eval-samples.yml`),
|
|
396
|
-
].filter(existsSync);
|
|
397
|
-
if (candidates.length > 0) {
|
|
398
|
-
skills.push({ name, skillPath: join(skillDir, entry), samplesPath: candidates[0] });
|
|
392
|
+
const samplesPath = findFlatSkillSamplesPath(skillDir, name);
|
|
393
|
+
if (samplesPath) {
|
|
394
|
+
skills.push({ name, skillPath: join(skillDir, entry), samplesPath });
|
|
399
395
|
}
|
|
400
396
|
else {
|
|
401
397
|
warned.push(name);
|
|
@@ -406,15 +402,9 @@ export function discoverBatchSkills(skillDir) {
|
|
|
406
402
|
const skillMd = join(entryPath, 'SKILL.md');
|
|
407
403
|
if (!existsSync(skillMd))
|
|
408
404
|
continue;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
...(existsSync(omkDir) ? [omkDir] : []),
|
|
413
|
-
join(entryPath, 'eval-samples.json'),
|
|
414
|
-
join(entryPath, 'eval-samples.yaml'),
|
|
415
|
-
join(entryPath, 'eval-samples.yml'),
|
|
416
|
-
];
|
|
417
|
-
const samplesPath = candidates.find(existsSync);
|
|
405
|
+
if (existsSync(join(skillDir, `${entry}.md`)))
|
|
406
|
+
continue;
|
|
407
|
+
const samplesPath = findSkillSamplesPath(entryPath);
|
|
418
408
|
if (samplesPath) {
|
|
419
409
|
skills.push({ name: entry, skillPath: skillMd, samplesPath });
|
|
420
410
|
}
|
|
@@ -2,13 +2,15 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { OMK_HOME } from '../eval-core/default-dirs.js';
|
|
5
|
+
import { isReportFileName, reportFilePath } from '../eval-core/artifact-file-names.js';
|
|
6
|
+
import { migrateLegacyReportFiles } from '../eval-core/report-file-migration.js';
|
|
5
7
|
import { extractGapSignalsFromTrace } from '../analysis/gap-analyzer.js';
|
|
6
8
|
import { ccTracesToResultEntries } from './trace-adapter.js';
|
|
7
9
|
import { isSearchToolCall, toolCallQuery } from '../shared/tool-search.js';
|
|
8
10
|
import { durationMsBetween } from '../shared/time.js';
|
|
9
11
|
import { buildObservationExperienceReport, normalizeObservationExperienceReport, } from './experience.js';
|
|
10
12
|
// observe inbox(观测收件箱)产物根目录。导出名沿用 *_OBSERVATIONS_DIR 以少动 importer,
|
|
11
|
-
// 但落盘目录已统一到 observe-inbox 词根(命令 omk observe inbox /
|
|
13
|
+
// 但落盘目录已统一到 observe-inbox 词根(命令 omk observe inbox / kind observe-inbox)。
|
|
12
14
|
// 项目级 .omk/observe-inbox 优先、全局兜底 —— 这套 project/global 归属是既有正常行为,本次只改名不改归属。
|
|
13
15
|
export const DEFAULT_PROJECT_OBSERVATIONS_DIR = join(process.cwd(), '.omk', 'observe-inbox');
|
|
14
16
|
export const DEFAULT_GLOBAL_OBSERVATIONS_DIR = join(OMK_HOME, 'observe-inbox');
|
|
@@ -529,10 +531,11 @@ function compareInboxItems(a, b) {
|
|
|
529
531
|
}
|
|
530
532
|
export function saveObservationInboxReport(report, outDir = DEFAULT_OBSERVATIONS_DIR) {
|
|
531
533
|
mkdirSync(outDir, { recursive: true });
|
|
534
|
+
migrateLegacyReportFiles(outDir, 'observe-inbox');
|
|
532
535
|
// 保留毫秒;同秒不同毫秒生成的两份 report 不应静默互相覆盖。
|
|
533
536
|
// 例: '2026-05-07T12:00:00.999Z' → '2026-05-07T12-00-00-999'
|
|
534
537
|
const stamp = report.meta.generatedAt.replace(/[:.]/g, '-').replace(/Z$/, '');
|
|
535
|
-
const path =
|
|
538
|
+
const path = reportFilePath(outDir, stamp);
|
|
536
539
|
writeFileSync(path, JSON.stringify(report, null, 2));
|
|
537
540
|
return path;
|
|
538
541
|
}
|
|
@@ -543,8 +546,9 @@ export function loadObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
|
543
546
|
}
|
|
544
547
|
return [];
|
|
545
548
|
}
|
|
549
|
+
migrateLegacyReportFiles(dir, 'observe-inbox');
|
|
546
550
|
return readdirSync(dir)
|
|
547
|
-
.filter(
|
|
551
|
+
.filter(isReportFileName)
|
|
548
552
|
.map((file) => {
|
|
549
553
|
try {
|
|
550
554
|
const report = normalizeObservationInboxReport(JSON.parse(readFileSync(join(dir, file), 'utf-8')));
|
|
@@ -14,6 +14,8 @@ import { renderManagedList, renderManagedHistory } from '../renderer/managed-his
|
|
|
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
16
|
import { listObserveCards, listDoctorCards, listLiveObserveCards } from '../eval-core/artifact-index.js';
|
|
17
|
+
import { isReportFileName, reportFilePath, reportFileStem } from '../eval-core/artifact-file-names.js';
|
|
18
|
+
import { migrateLegacyReportFiles } from '../eval-core/report-file-migration.js';
|
|
17
19
|
import { buildSkillIndex } from './skill-index.js';
|
|
18
20
|
import { createFileJobStore } from './job-store.js';
|
|
19
21
|
import { createFileStore, queryJob, queryJobList, queryRun, queryRunList, queryTrend } from './report-store.js';
|
|
@@ -46,18 +48,20 @@ function loadChartJsBundle() {
|
|
|
46
48
|
}
|
|
47
49
|
function listAnalyses(dir, includeCards = false) {
|
|
48
50
|
const items = [];
|
|
51
|
+
migrateLegacyReportFiles(dir, 'observe-health');
|
|
49
52
|
// live 扫描 dir 存在才做;dir 不存在(默认机器级模式下当前项目还没 .omk/observe-health、全局也空)时 live 为空,
|
|
50
53
|
// 但**不能早退** —— 后面仍要按 includeCards 合并别项目卡片,否则 observe 列表会与合卡片的 /api/skills 口径分裂。
|
|
51
54
|
if (existsSync(dir)) {
|
|
52
55
|
for (const file of readdirSync(dir)) {
|
|
53
|
-
|
|
56
|
+
const id = reportFileStem(file);
|
|
57
|
+
if (!id)
|
|
54
58
|
continue;
|
|
55
59
|
try {
|
|
56
60
|
const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
57
61
|
if (!data.meta || !data.overall)
|
|
58
62
|
continue;
|
|
59
63
|
items.push({
|
|
60
|
-
id
|
|
64
|
+
id,
|
|
61
65
|
generatedAt: data.meta.generatedAt,
|
|
62
66
|
sessionCount: data.meta.sessionCount,
|
|
63
67
|
segmentCount: data.meta.segmentCount,
|
|
@@ -89,7 +93,8 @@ function listAnalyses(dir, includeCards = false) {
|
|
|
89
93
|
return items;
|
|
90
94
|
}
|
|
91
95
|
function loadAnalysis(dir, id, includeCards = false) {
|
|
92
|
-
|
|
96
|
+
migrateLegacyReportFiles(dir, 'observe-health');
|
|
97
|
+
const path = reportFilePath(dir, id);
|
|
93
98
|
if (existsSync(path)) {
|
|
94
99
|
try {
|
|
95
100
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -114,9 +119,10 @@ function loadAnalysis(dir, id, includeCards = false) {
|
|
|
114
119
|
* 优先返回含该 skill 的那份;都不含时回退首个 id 命中(单 skill / 无参行为不变)。 */
|
|
115
120
|
function loadDoctorReport(dir, id, skillName, includeCards = false) {
|
|
116
121
|
let fallback = null;
|
|
122
|
+
migrateLegacyReportFiles(dir, 'doctor');
|
|
117
123
|
if (existsSync(dir)) {
|
|
118
124
|
for (const file of readdirSync(dir)) {
|
|
119
|
-
if (!file
|
|
125
|
+
if (!isReportFileName(file))
|
|
120
126
|
continue;
|
|
121
127
|
try {
|
|
122
128
|
const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|