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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/dist/artifact-graph/doctor.d.ts +21 -0
  4. package/dist/artifact-graph/doctor.js +569 -0
  5. package/dist/artifact-graph/eval.d.ts +16 -0
  6. package/dist/artifact-graph/eval.js +351 -0
  7. package/dist/assets/agent-skills/omk/SKILL.md +9 -9
  8. package/dist/assets/agent-skills/omk/references/commands.md +1 -1
  9. package/dist/cli/commands/doctor.js +62 -61
  10. package/dist/cli/commands/eval/index.js +20 -2
  11. package/dist/cli/commands/init.js +1 -0
  12. package/dist/cli/commands/observe/index.d.ts +2 -2
  13. package/dist/cli/commands/observe/index.js +8 -7
  14. package/dist/cli/commands/sample.js +22 -16
  15. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  16. package/dist/cli/lib/i18n-dict/common.js +8 -0
  17. package/dist/cli/lib/i18n-dict/help.js +12 -10
  18. package/dist/cli/lib/parse-run-config/samples-discovery.d.ts +5 -7
  19. package/dist/cli/lib/parse-run-config/samples-discovery.js +10 -32
  20. package/dist/cli/lib/parse-run-config.js +4 -4
  21. package/dist/cli/lib/resolve-skill-input.js +10 -12
  22. package/dist/doctor/index.js +2 -7
  23. package/dist/doctor/messages.js +2 -2
  24. package/dist/eval-core/artifact-file-names.d.ts +15 -0
  25. package/dist/eval-core/artifact-file-names.js +45 -0
  26. package/dist/eval-core/evaluation-reporting.d.ts +1 -1
  27. package/dist/eval-core/evaluation-reporting.js +32 -12
  28. package/dist/eval-core/measurement-dirs.js +13 -7
  29. package/dist/eval-core/report-file-migration.d.ts +10 -0
  30. package/dist/eval-core/report-file-migration.js +90 -0
  31. package/dist/inputs/sample-locator.d.ts +23 -0
  32. package/dist/inputs/sample-locator.js +195 -0
  33. package/dist/inputs/skill-loader.js +7 -17
  34. package/dist/observability/inbox.js +7 -3
  35. package/dist/renderer/html-renderer.js +3 -2
  36. package/dist/renderer/skill-detail-renderer.js +832 -0
  37. package/dist/renderer/skill-list-renderer.js +2 -7
  38. package/dist/server/report-server.js +55 -15
  39. package/dist/server/report-store.js +17 -9
  40. package/dist/server/skill-index.d.ts +6 -0
  41. package/dist/server/skill-index.js +399 -15
  42. package/dist/types/artifact-graph.d.ts +93 -0
  43. package/dist/types/artifact-graph.js +1 -0
  44. package/dist/types/index.d.ts +1 -0
  45. package/dist/types/index.js +1 -0
  46. package/dist/types/skill-index.d.ts +38 -0
  47. package/package.json +1 -1
@@ -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
- // 点行直接进该 skill 最新 eval 报告(无 eval 则最新 doctor);跨维度/历史在报告页内切换。
138
- const amp = lang === DEFAULT_LANG ? '' : `&lang=${lang}`;
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 / 三阶段状态 / Evidence Card 是用户第一视角。
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,14 +6,17 @@ 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';
18
+ import { isReportFileName, reportFilePath, reportFileStem } from '../eval-core/artifact-file-names.js';
19
+ import { migrateLegacyReportFiles } from '../eval-core/report-file-migration.js';
17
20
  import { buildSkillIndex } from './skill-index.js';
18
21
  import { createFileJobStore } from './job-store.js';
19
22
  import { createFileStore, queryJob, queryJobList, queryRun, queryRunList, queryTrend } from './report-store.js';
@@ -46,18 +49,20 @@ function loadChartJsBundle() {
46
49
  }
47
50
  function listAnalyses(dir, includeCards = false) {
48
51
  const items = [];
52
+ migrateLegacyReportFiles(dir, 'observe-health');
49
53
  // live 扫描 dir 存在才做;dir 不存在(默认机器级模式下当前项目还没 .omk/observe-health、全局也空)时 live 为空,
50
54
  // 但**不能早退** —— 后面仍要按 includeCards 合并别项目卡片,否则 observe 列表会与合卡片的 /api/skills 口径分裂。
51
55
  if (existsSync(dir)) {
52
56
  for (const file of readdirSync(dir)) {
53
- if (!file.endsWith('.json'))
57
+ const id = reportFileStem(file);
58
+ if (!id)
54
59
  continue;
55
60
  try {
56
61
  const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
57
62
  if (!data.meta || !data.overall)
58
63
  continue;
59
64
  items.push({
60
- id: file.replace(/\.json$/, ''),
65
+ id,
61
66
  generatedAt: data.meta.generatedAt,
62
67
  sessionCount: data.meta.sessionCount,
63
68
  segmentCount: data.meta.segmentCount,
@@ -89,7 +94,8 @@ function listAnalyses(dir, includeCards = false) {
89
94
  return items;
90
95
  }
91
96
  function loadAnalysis(dir, id, includeCards = false) {
92
- const path = join(dir, `${id}.json`);
97
+ migrateLegacyReportFiles(dir, 'observe-health');
98
+ const path = reportFilePath(dir, id);
93
99
  if (existsSync(path)) {
94
100
  try {
95
101
  return JSON.parse(readFileSync(path, 'utf-8'));
@@ -114,9 +120,10 @@ function loadAnalysis(dir, id, includeCards = false) {
114
120
  * 优先返回含该 skill 的那份;都不含时回退首个 id 命中(单 skill / 无参行为不变)。 */
115
121
  function loadDoctorReport(dir, id, skillName, includeCards = false) {
116
122
  let fallback = null;
123
+ migrateLegacyReportFiles(dir, 'doctor');
117
124
  if (existsSync(dir)) {
118
125
  for (const file of readdirSync(dir)) {
119
- if (!file.endsWith('.json'))
126
+ if (!isReportFileName(file))
120
127
  continue;
121
128
  try {
122
129
  const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
@@ -596,6 +603,18 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
596
603
  : doctorsDir !== undefined
597
604
  ? () => doctorsDir
598
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
+ });
599
618
  async function handleRequest(req, res) {
600
619
  try {
601
620
  const parsed = new URL(req.url || '/', 'http://127.0.0.1');
@@ -746,7 +765,7 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
746
765
  }
747
766
  if (path === '/api/observe-inbox/diagnostics') {
748
767
  const runs = await reportStore.list();
749
- const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
768
+ const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
750
769
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
751
770
  res.end(JSON.stringify({
752
771
  sourceCoverage: idx.diagnosisSummary.sourceCoverage,
@@ -832,7 +851,7 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
832
851
  let ctx;
833
852
  if (skillName) {
834
853
  const runs = await reportStore.list();
835
- const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
854
+ const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
836
855
  const entry = idx.entries.find((en) => en.skillName === skillName);
837
856
  if (entry)
838
857
  ctx = buildSkillContext(entry, 'doctor', id, idx.insightsBySkill.get(entry.skillName) ?? [], lang);
@@ -994,7 +1013,7 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
994
1013
  let ctx;
995
1014
  if (report && report.kind === 'evaluation') {
996
1015
  const runs = await reportStore.list();
997
- const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
1016
+ const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
998
1017
  // 按 evalHistory 匹配(非仅最新),历史 eval 报告也能定位到所属 skill。
999
1018
  const entry = idx.entries.find((en) => en.evalHistory.some((h) => h.reportId === reportId));
1000
1019
  if (entry)
@@ -1016,14 +1035,14 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
1016
1035
  // list renderer 直接消费,不再每请求 N×detectInsights 重算。
1017
1036
  if (path === '/') {
1018
1037
  const runs = await reportStore.list();
1019
- const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
1038
+ const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
1020
1039
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1021
1040
  res.end(renderSkillList(idx, lang));
1022
1041
  return;
1023
1042
  }
1024
1043
  if (path === '/api/skills') {
1025
1044
  const runs = await reportStore.list();
1026
- const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
1045
+ const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
1027
1046
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1028
1047
  res.end(JSON.stringify({
1029
1048
  entries: idx.entries.map((entry) => ({
@@ -1036,15 +1055,36 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
1036
1055
  }));
1037
1056
  return;
1038
1057
  }
1039
- // 注:/skills/<name> 详情页(hub)已下线 — 首页点行直接进 eval/doctor 报告,跨维度靠
1040
- // 报告页 chip 条 +「全部历史」弹框,不再有中间汇总页。该路由不做兼容重定向:Studio
1041
- // 是本地临时服务器、端口每次启动都变,跨会话书签 localhost:<port> 本就失效,且 PR 内
1042
- // 已无任何链接指向它。未匹配的 /skills/<name> 直接落到下方通用 404。
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
+ }
1043
1083
  const skillDiagnosticsApiMatch = path.match(/^\/api\/skills\/(.+)\/diagnostics$/);
1044
1084
  if (skillDiagnosticsApiMatch) {
1045
1085
  const skillName = decodeURIComponent(skillDiagnosticsApiMatch[1]);
1046
1086
  const runs = await reportStore.list();
1047
- const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
1087
+ const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, skillIndexOptions());
1048
1088
  const diagnostics = idx.diagnosticsBySkill.get(skillName);
1049
1089
  if (!diagnostics) {
1050
1090
  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { readdir, readFile, writeFile, unlink, access, mkdir, rename, stat } from 'node:fs/promises';
7
7
  import { join } from 'node:path';
8
+ import { isReportFileName, reportFilePath, reportFileStem } from '../eval-core/artifact-file-names.js';
9
+ import { migrateLegacyReportFiles } from '../eval-core/report-file-migration.js';
8
10
  // Per-id in-memory mutex for safe read-modify-write.
9
11
  // Uses a queue to avoid the race window between checking and acquiring the lock.
10
12
  const locks = new Map();
@@ -67,7 +69,7 @@ export function createFileStore(dir) {
67
69
  function isEvaluationReport(report) {
68
70
  return report.kind === 'evaluation';
69
71
  }
70
- // Studio 每个 / 和 /skills/<name> 请求都调 list(),里面对每个 .json 同步 readFile +
72
+ // Studio 每个 / 和 /skills/<name> 请求都调 list(),里面对每个 .report.json 同步 readFile +
71
73
  // JSON.parse。报告数上来后这是主性能瓶颈。缓存策略:fingerprint = dir mtime + 文件名
72
74
  // 排序串 + 每个文件 mtime;任一变化 invalidate。fingerprint 算 cheap(只 stat),命中后
73
75
  // 完全跳过 readFile。
@@ -76,7 +78,7 @@ export function createFileStore(dir) {
76
78
  async function computeListFingerprint() {
77
79
  try {
78
80
  const dirStat = await stat(dir);
79
- const files = (await readdir(dir)).filter((f) => f.endsWith('.json')).sort();
81
+ const files = (await readdir(dir)).filter(isReportFileName).sort();
80
82
  const parts = await Promise.all(files.map(async (f) => {
81
83
  try {
82
84
  const s = await stat(join(dir, f));
@@ -99,18 +101,19 @@ export function createFileStore(dir) {
99
101
  catch {
100
102
  return [];
101
103
  }
104
+ migrateLegacyReportFiles(dir, 'report');
102
105
  const fp = await computeListFingerprint();
103
106
  if (fp != null && fp === cachedFingerprint && cachedRuns)
104
107
  return cachedRuns;
105
108
  const files = (await readdir(dir))
106
- .filter((f) => f.endsWith('.json'))
109
+ .filter(isReportFileName)
107
110
  .sort()
108
111
  .reverse();
109
112
  const runs = [];
110
113
  for (const file of files) {
111
114
  try {
112
115
  const data = JSON.parse(await readFile(join(dir, file), 'utf-8'));
113
- const report = normalizeReportDocument(data, file.replace(/\.json$/, ''));
116
+ const report = normalizeReportDocument(data, reportFileStem(file) ?? file);
114
117
  if (report)
115
118
  runs.push(report);
116
119
  }
@@ -128,8 +131,9 @@ export function createFileStore(dir) {
128
131
  return runs;
129
132
  }
130
133
  async function get(id) {
134
+ migrateLegacyReportFiles(dir, 'report');
131
135
  try {
132
- const data = JSON.parse(await readFile(join(dir, `${id}.json`), 'utf-8'));
136
+ const data = JSON.parse(await readFile(reportFilePath(dir, id), 'utf-8'));
133
137
  return normalizeReportDocument(data, id);
134
138
  }
135
139
  catch {
@@ -138,9 +142,11 @@ export function createFileStore(dir) {
138
142
  }
139
143
  async function save(id, report) {
140
144
  await ensureDir();
141
- const tmpPath = join(dir, `${id}.json.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`);
145
+ migrateLegacyReportFiles(dir, 'report');
146
+ const targetPath = reportFilePath(dir, id);
147
+ const tmpPath = `${targetPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
142
148
  await writeFile(tmpPath, JSON.stringify(report, null, 2));
143
- await rename(tmpPath, join(dir, `${id}.json`));
149
+ await rename(tmpPath, targetPath);
144
150
  }
145
151
  /**
146
152
  * Atomic read-modify-write with in-memory mutex.
@@ -157,8 +163,9 @@ export function createFileStore(dir) {
157
163
  });
158
164
  }
159
165
  async function remove(id) {
166
+ migrateLegacyReportFiles(dir, 'report');
160
167
  try {
161
- await unlink(join(dir, `${id}.json`));
168
+ await unlink(reportFilePath(dir, id));
162
169
  return true;
163
170
  }
164
171
  catch (err) {
@@ -169,8 +176,9 @@ export function createFileStore(dir) {
169
176
  }
170
177
  }
171
178
  async function exists(id) {
179
+ migrateLegacyReportFiles(dir, 'report');
172
180
  try {
173
- await access(join(dir, `${id}.json`));
181
+ await access(reportFilePath(dir, id));
174
182
  return true;
175
183
  }
176
184
  catch {
@@ -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/&lt;name&gt; 详情页)— 复用 buildSkillIndex 的扫描结果。 */