oh-my-knowledge 0.42.0 → 0.44.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/artifact-graph/eval.d.ts +16 -0
- package/dist/artifact-graph/eval.js +351 -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/index.js +2 -7
- 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 +45 -0
- package/dist/eval-core/evaluation-reporting.d.ts +1 -1
- package/dist/eval-core/evaluation-reporting.js +32 -12
- 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/renderer/html-renderer.js +3 -2
- package/dist/renderer/skill-detail-renderer.js +832 -0
- package/dist/renderer/skill-list-renderer.js +2 -7
- package/dist/server/report-server.js +55 -15
- package/dist/server/report-store.js +17 -9
- package/dist/server/skill-index.d.ts +6 -0
- package/dist/server/skill-index.js +399 -15
- 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/dist/types/skill-index.d.ts +38 -0
- package/package.json +1 -1
|
@@ -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')));
|
|
@@ -149,9 +149,10 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
|
|
|
149
149
|
// 顶部 verdict pill 主导视觉,id+date 是身份,scores 是成绩,底部 meta 是元数据,
|
|
150
150
|
// delete 默认隐藏在 hover 出现避免误点。批量报告共用同一组件,batch pill 替代 verdict。
|
|
151
151
|
const formatDateFromId = (id, fallbackTs) => {
|
|
152
|
-
//
|
|
152
|
+
// 兼容三代 run id 后缀:旧 `…YYYYMMDD-HHmm`、过渡期 `…YYYYMMDD-HHmmss-rand4`、
|
|
153
|
+
// 统一后 `…YYYYMMDDTHHmmss-rand4`(含秒+随机)。
|
|
153
154
|
// 都从 id 直接派生本地展示时间(确定性,不走时区敏感的 toLocaleString);新增的秒段 + 随机后缀可选匹配。
|
|
154
|
-
const idMatch = id.match(/(\d{4})(\d{2})(\d{2})
|
|
155
|
+
const idMatch = id.match(/(\d{4})(\d{2})(\d{2})(?:T|-)(\d{2})(\d{2})(?:\d{2})?(?:-[a-z0-9]+)?$/);
|
|
155
156
|
if (idMatch)
|
|
156
157
|
return `${idMatch[2]}/${idMatch[3]} ${idMatch[4]}:${idMatch[5]}`;
|
|
157
158
|
return fallbackTs ? new Date(fallbackTs).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
|