oh-my-knowledge 0.34.0 → 0.36.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 (76) hide show
  1. package/README.md +3 -0
  2. package/README.zh.md +3 -0
  3. package/dist/assets/agent-skills/omk/SKILL.md +2 -2
  4. package/dist/assets/agent-skills/omk/references/commands.md +45 -0
  5. package/dist/authoring/evolver.js +13 -4
  6. package/dist/cli/commands/doctor.js +2 -1
  7. package/dist/cli/commands/eval/index.d.ts +1 -0
  8. package/dist/cli/commands/eval/index.js +40 -5
  9. package/dist/cli/commands/install.d.ts +2 -0
  10. package/dist/cli/commands/install.js +45 -20
  11. package/dist/cli/commands/list.d.ts +46 -0
  12. package/dist/cli/commands/list.js +243 -0
  13. package/dist/cli/commands/sample.d.ts +1 -1
  14. package/dist/cli/commands/sample.js +25 -12
  15. package/dist/cli/lib/cmd-flags.d.ts +1 -0
  16. package/dist/cli/lib/i18n-dict/install.d.ts +1 -1
  17. package/dist/cli/lib/i18n-dict/install.js +20 -0
  18. package/dist/cli/lib/i18n-dict/list.d.ts +3 -0
  19. package/dist/cli/lib/i18n-dict/list.js +32 -0
  20. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  21. package/dist/cli/lib/i18n-dict/run.js +8 -0
  22. package/dist/cli/lib/i18n-dict.d.ts +2 -1
  23. package/dist/cli/lib/i18n-dict.js +2 -0
  24. package/dist/cli/lib/parse-run-config/variant-resolution.js +7 -0
  25. package/dist/cli/lib/run-tally.js +1 -1
  26. package/dist/cli/lib/shared.js +1 -1
  27. package/dist/doctor/index.js +6 -6
  28. package/dist/eval-core/cache.d.ts +11 -3
  29. package/dist/eval-core/cache.js +13 -5
  30. package/dist/eval-core/dependency-checker.js +5 -3
  31. package/dist/eval-core/evaluation-execution.js +4 -1
  32. package/dist/eval-core/evaluation-reporting.d.ts +1 -0
  33. package/dist/eval-core/evaluation-reporting.js +20 -9
  34. package/dist/eval-core/execution-strategy.js +6 -4
  35. package/dist/eval-core/task-planner.js +2 -1
  36. package/dist/eval-workflows/batch-evaluation-workflow.js +3 -2
  37. package/dist/eval-workflows/evaluation-preparation.js +3 -1
  38. package/dist/eval-workflows/run-evaluation.js +2 -2
  39. package/dist/inputs/content-hash.d.ts +28 -0
  40. package/dist/inputs/content-hash.js +106 -0
  41. package/dist/inputs/eval-config.js +44 -11
  42. package/dist/inputs/materialize-copy.d.ts +37 -0
  43. package/dist/inputs/materialize-copy.js +193 -0
  44. package/dist/inputs/skill-loader.d.ts +65 -2
  45. package/dist/inputs/skill-loader.js +314 -16
  46. package/dist/inputs/source-resolver.d.ts +16 -9
  47. package/dist/inputs/source-resolver.js +57 -77
  48. package/dist/managed/evidence.d.ts +22 -0
  49. package/dist/managed/evidence.js +143 -0
  50. package/dist/managed/index.d.ts +2 -0
  51. package/dist/managed/index.js +2 -0
  52. package/dist/managed/list-view.d.ts +55 -0
  53. package/dist/managed/list-view.js +66 -0
  54. package/dist/managed/store.d.ts +16 -20
  55. package/dist/managed/store.js +85 -84
  56. package/dist/observability/experience.d.ts +2 -0
  57. package/dist/observability/experience.js +31 -2
  58. package/dist/observability/inbox.js +31 -5
  59. package/dist/observability/review-state.js +22 -11
  60. package/dist/observability/soft-standards/llm-extractor.js +3 -3
  61. package/dist/observability/soft-standards/skill-standards-store.d.ts +1 -0
  62. package/dist/observability/soft-standards/skill-standards-store.js +77 -14
  63. package/dist/observability/soft-standards/types.d.ts +2 -2
  64. package/dist/renderer/html-renderer.js +3 -3
  65. package/dist/renderer/layout.js +2 -2
  66. package/dist/server/report-server.js +1 -1
  67. package/dist/server/report-store.d.ts +1 -1
  68. package/dist/server/report-store.js +16 -15
  69. package/dist/server/skill-index.js +5 -4
  70. package/dist/types/doctor.d.ts +2 -2
  71. package/dist/types/doctor.js +1 -1
  72. package/dist/types/eval.d.ts +12 -1
  73. package/dist/types/managed.d.ts +29 -4
  74. package/dist/types/observability.d.ts +6 -6
  75. package/dist/types/report.d.ts +24 -5
  76. package/package.json +3 -3
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
3
2
  import { homedir } from 'node:os';
4
3
  import { join, normalize } from 'node:path';
@@ -26,87 +25,22 @@ export function recordPath(dir, id) {
26
25
  export function managedRecordId(kind, name) {
27
26
  return hashString(`${kind}|${name}`);
28
27
  }
29
- /**
30
- * 不进 artifact「可分发树」的条目 —— omk 的评测 / 迭代 / VCS / 系统产物,既不该被拷进 agent
31
- * skill 目录,也不该计入 artifact contentHash。区分两类语义,避免误伤合法嵌套资产:
32
- * - **任意层级排除**:隐藏元数据 / VCS / 系统 / 依赖目录,任何深度出现都是噪声
33
- * (`.omk` = samples / managed / observations;`.git`;`node_modules`;OS 垃圾);
34
- * - **仅源根第一层排除**:omk 保留的工作目录,只在 skill 根有保留语义,嵌套同名是用户合法资产
35
- * (`evolve` 是 `<skillDir>/evolve/` 候选快照;但 `references/evolve/guide.md` 应正常分发并计入 hash)。
36
- * spec 里 artifact content hash 与 sample-set hash 是分开的证据轴,故只补样本(.omk)不该改 hash。
37
- */
38
- const GLOBAL_EXCLUDED_NAMES = new Set(['.omk', '.git', 'node_modules', '.DS_Store', 'Thumbs.db']);
39
- const ROOT_ONLY_EXCLUDED_NAMES = new Set(['evolve']);
40
- /**
41
- * 给定相对源根的路径分段(空数组 = 源根本身),判断是否进可分发树。hash 的 walk、copy 的 filter、
42
- * git 物化共用此一处,保证三者完全一致。
43
- *
44
- * 关键:检查**每一段**而非只看叶子。本地 walk / cpSync 是逐层下降、命中目录即剪枝,只看叶子也够;
45
- * 但 git ls-tree 给的是**扁平路径**(如 `.omk/samples.json`),只看叶子 `samples.json` 会让 `.omk`
46
- * 内容漏过。故:任一段命中全局排除即排除;首段命中 root-only(evolve)即排除(嵌套同名是合法资产)。
47
- */
48
- export function isDistributablePath(segments) {
49
- if (segments.length === 0)
50
- return true; // 源根永远算
51
- for (const seg of segments) {
52
- if (GLOBAL_EXCLUDED_NAMES.has(seg))
53
- return false;
54
- }
55
- if (ROOT_ONLY_EXCLUDED_NAMES.has(segments[0]))
56
- return false;
57
- return true;
58
- }
59
- /**
60
- * artifact 内容 hash —— drift baseline 与 evidence 绑定的依据。
61
- * - 文件-skill:单个 .md 的字节;
62
- * - 目录-skill:覆盖**整棵可分发目录树**(SKILL.md + references/ 等资产,但排除 .omk / .git /
63
- * evolve 等评测迭代产物),按相对路径排序后把每个文件的`路径 + 字节长度 + 内容`喂进同一个
64
- * sha256 —— 改任意资产都会令 hash 变化、drift 不漏;只补样本则 hash 不动。
65
- * 用 createHash 直接喂 Buffer(字节级,二进制资产也稳;分隔符是运行时字节,源码里不引入任何不可见字符)。
66
- * 读时(未来 list / drift 检查)用同一函数重算比对。
67
- */
68
- export function hashArtifactSource(source, isDirectorySkill) {
69
- if (!isDirectorySkill) {
70
- return createHash('sha256').update(readFileSync(source)).digest('hex').slice(0, 12);
71
- }
72
- const rels = [];
73
- const walk = (dir, segments) => {
74
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
75
- const segs = [...segments, entry.name];
76
- if (!isDistributablePath(segs))
77
- continue;
78
- // 软链既非 isFile 也非 isDirectory,天然跳过 —— 与 copyArtifactToTarget 的 filter 一致
79
- // (hash 覆盖的 == 分发出去的),避免软链目标改变却不触发 drift,也回避软链环。
80
- if (entry.isDirectory())
81
- walk(join(dir, entry.name), segs);
82
- else if (entry.isFile())
83
- rels.push(segs.join('/'));
84
- }
85
- };
86
- walk(source, []);
87
- rels.sort();
88
- const h = createHash('sha256');
89
- const sep = Buffer.from([0]);
90
- for (const rel of rels) {
91
- const content = readFileSync(join(source, rel));
92
- // 路径与内容都做长度前缀,彻底排除"不同树拼出同一串"的歧义(文件名虽不含 NUL,核心层仍按可注入防)。
93
- h.update(String(Buffer.byteLength(rel)));
94
- h.update(sep);
95
- h.update(rel);
96
- h.update(sep);
97
- h.update(String(content.length));
98
- h.update(sep);
99
- h.update(content);
100
- h.update(sep);
101
- }
102
- return h.digest('hex').slice(0, 12);
103
- }
28
+ // 内容指纹工具已下沉到 inputs 层(install 与 eval 共用同一处,保证指纹同空间);此处 re-export
29
+ // 保持 managed 公共入口不破(install 仍从 managed 取 hashArtifactSource / isDistributablePath)。
30
+ export { hashArtifactSource, isDistributablePath, distributableCopyFilter } from '../inputs/content-hash.js';
104
31
  function isStringField(v) {
105
32
  return typeof v === 'string';
106
33
  }
107
- // 校验到元素级:记录文件是用户可手改的(透明性是设计价值),`evidence:[null]` / 缺字段的 distribution
108
- // 都是现实的坏手改;若只查 Array.isArray,坏元素会在 deriveManagedState / mergeManagedRecord 里
109
- // 解引用崩溃(且在 load 的 try/catch 之外)。这里直接判脏 → load 丢弃该文件,消费方永远拿不到半成品。
34
+ /** 仅当字段缺省或是 string 才放行(可选 string 字段的脏值守卫,如 source.url / evidence.verdict)。 */
35
+ function isOptionalString(v) {
36
+ return v === undefined || typeof v === 'string';
37
+ }
38
+ // 受管记录可安装的 kind(managed 记录绝不是 baseline)。
39
+ const MANAGED_KINDS = new Set(['skill', 'prompt', 'agent', 'workflow']);
40
+ // 校验到**运行时实际收窄**的边界,不止「字段是 string」:记录文件是用户可手改、且可能随仓库分发(被
41
+ // loadAllManagedRecords 无 opt-in 读到)的不可信输入。若只查 string,畸形记录(如 source.url 是对象、
42
+ // sourceKind 是任意串)会绕过 validator,在下游(omk list 的 sourceLabel ?? → dispWidth(对象) 等)
43
+ // 抛 TypeError 让命令崩溃。这里把 source / evidence 收窄到 list 等消费方实际解引用的类型,判脏即丢弃该文件。
110
44
  function isManagedArtifactRecord(value) {
111
45
  if (!value || typeof value !== 'object')
112
46
  return false;
@@ -115,19 +49,49 @@ function isManagedArtifactRecord(value) {
115
49
  && r.schemaVersion === 2
116
50
  && isStringField(r.id)
117
51
  && isStringField(r.name)
118
- && isStringField(r.kind)
52
+ && isStringField(r.kind) && MANAGED_KINDS.has(r.kind)
119
53
  && isStringField(r.contentHash)
120
54
  && r.source && typeof r.source === 'object'
121
- && isStringField(r.source.locator)
122
- && isStringField(r.source.sourceKind)
123
55
  && Array.isArray(r.distribution)
124
56
  && Array.isArray(r.evidence)
125
57
  && Array.isArray(r.decisions)))
126
58
  return false;
59
+ const src = r.source;
60
+ if (!(isStringField(src.locator)
61
+ && (src.sourceKind === 'file' || src.sourceKind === 'git')
62
+ && typeof src.isDirectorySkill === 'boolean'
63
+ && isOptionalString(src.url)
64
+ && isOptionalString(src.ref)))
65
+ return false;
66
+ // url / ref 是 git-only 字段。file 源带 url 时,list 的 sourceLabel 会显示假 url、掩盖真实被 probe /
67
+ // 读取的 locator(畸形但可通过上面 string 校验)→ 「读了什么」与「显示什么」不一致。判脏丢弃。
68
+ if (src.sourceKind === 'file' && (src.url !== undefined || src.ref !== undefined))
69
+ return false;
127
70
  const okDist = r.distribution.every((d) => d && typeof d === 'object'
128
71
  && isStringField(d.path) && isStringField(d.contentHash));
129
- const okEv = r.evidence.every((e) => e && typeof e === 'object'
130
- && isStringField(e.reportId) && isStringField(e.contentHash));
72
+ const okEv = r.evidence.every((e) => {
73
+ if (!e || typeof e !== 'object')
74
+ return false;
75
+ const ev = e;
76
+ if (!(isStringField(ev.reportId) && isStringField(ev.contentHash) && isStringField(ev.recordedAt)))
77
+ return false;
78
+ if (!isOptionalString(ev.verdict))
79
+ return false;
80
+ // comparability 若存在:必须是带 string cliVersion 的对象(list / promote 会读它)。可选 marker
81
+ // judgePromptHash / debiasMode 同样收窄到声明类型 —— 否则任意类型脏值会穿过 validator 原样进 `omk list
82
+ // --json`(及未来 promote gate)消费方。
83
+ if (ev.comparability !== undefined) {
84
+ const c = ev.comparability;
85
+ if (!c || typeof c !== 'object' || !isStringField(c.cliVersion))
86
+ return false;
87
+ if (!isOptionalString(c.judgePromptHash))
88
+ return false;
89
+ if (c.debiasMode !== undefined
90
+ && !(Array.isArray(c.debiasMode) && c.debiasMode.every((m) => m === 'length' || m === 'position')))
91
+ return false;
92
+ }
93
+ return true;
94
+ });
131
95
  const okDec = r.decisions.every((d) => d && typeof d === 'object'
132
96
  && isStringField(d.decisionKind));
133
97
  return okDist && okEv && okDec;
@@ -175,6 +139,19 @@ export function loadAllManagedRecords(dir = managedDir()) {
175
139
  }
176
140
  return local;
177
141
  }
142
+ /**
143
+ * 哪个 managed 目录是权威(有记录的那个):项目目录非空取项目,否则全局非空取全局,都空回项目。
144
+ * 与 `loadAllManagedRecords` 的 project→global 回退**同口径** —— 写方(append evidence)据此写回
145
+ * 读方实际取记录的同一目录,避免"读全局、写项目"把证据落到空目录。
146
+ */
147
+ export function resolveManagedDir(dir = managedDir()) {
148
+ if (readRecordsFromDir(dir).length > 0)
149
+ return dir;
150
+ const global = globalManagedDir();
151
+ if (dir !== global && readRecordsFromDir(global).length > 0)
152
+ return global;
153
+ return dir;
154
+ }
178
155
  /**
179
156
  * 纯合并逻辑(无 IO):install 写的是事实,绝不动 evidence/decisions(那是 eval/promote 的地盘)。
180
157
  * - distribution 按 path 去重(同路径以新值替换);
@@ -217,6 +194,30 @@ export function upsertManagedRecord(dir, record) {
217
194
  renameSync(tmp, path);
218
195
  return merged;
219
196
  }
197
+ /**
198
+ * 追加一条评测证据(append-only)。与 install 的 upsert 路径相反——`mergeManagedRecord` 刻意**保留旧
199
+ * evidence、丢弃 next.evidence**(install 只写事实),所以证据写入不能走 upsert,必须独立 load→push→
200
+ * 原子重写。按 `(reportId, contentHash)` 去重:重跑同一份 eval 不堆重复条目(reportId 含运行身份,
201
+ * 同内容重测会是新 reportId → 新条目,正是要的版本史)。记录不存在(未 install / 名字不匹配)返回
202
+ * null —— 这是"管理是 install 显式 opt-in"的体现:eval 绝不为未纳管的 skill 凭空建记录。
203
+ */
204
+ export function appendManagedEvidence(dir, recordId, evidence) {
205
+ const prev = loadManagedRecord(dir, recordId);
206
+ if (!prev)
207
+ return null;
208
+ const dup = prev.evidence.some((e) => e.reportId === evidence.reportId && e.contentHash === evidence.contentHash);
209
+ const merged = dup
210
+ ? prev
211
+ : { ...prev, evidence: [...prev.evidence, evidence] };
212
+ if (!dup) {
213
+ mkdirSync(dir, { recursive: true });
214
+ const path = recordPath(dir, recordId);
215
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
216
+ writeFileSync(tmp, JSON.stringify(merged, null, 2));
217
+ renameSync(tmp, path);
218
+ }
219
+ return merged;
220
+ }
220
221
  /**
221
222
  * 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
222
223
  * 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
@@ -4,6 +4,7 @@ import type { SkillSegment } from './trace-segmenter.js';
4
4
  export { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
5
5
  export type { TextMatchRange } from './feedback-matchers.js';
6
6
  export type { ExperienceAssistiveInference, ExperienceAssistiveInferenceCautionCode, ExperienceAssistiveInferenceCode, ExperienceAssistiveInferenceConfidence, ExperienceChecklistContribution, ExperienceChecklistItem, ExperienceChecklistItemStatus, ExperienceEpisode, ExperienceEpisodeArtifact, ExperienceEpisodeArtifactKind, ExperienceEpisodeBoundaryReason, ExperienceEpisodeOutcome, ExperienceEpisodeRole, ExperienceEvidenceChain, ExperienceEvidenceKind, ExperienceEvidenceRef, ExperienceFeedbackAttribution, ExperienceFeedbackAttributionReason, ExperienceFeedbackAttributionRole, ExperienceFeedbackSignal, ExperienceFeedbackSignalType, ExperienceGoalEvidenceRef, ExperienceGoalSlice, ExperienceGoalSliceReasonCode, ExperienceInvocation, ExperienceMessageRange, ExperienceOrchestrationEdge, ExperienceOrchestrationEdgeKind, ExperienceOrchestrationEdgeStatus, ExperienceOutcomeClosure, ExperienceParentReason, ExperienceReviewBasisCode, ExperienceReviewIndicators, ExperienceReviewPriority, ExperienceReviewerReport, ExperienceReviewerReportFinding, ExperienceReviewerReportFindingLevel, ExperienceReviewerReportFindingSource, ExperienceReviewerReportScope, ExperienceReviewerReportStep, ExperienceReviewerReportStepStatus, ExperienceRuleFinding, ExperienceRuleFindingCode, ExperienceRuleFindingLevel, ExperienceRuntimeSkillType, ExperienceRuntimeSkillTypeSource, ExperienceSessionStory, ExperienceSessionStoryAnswer, ExperienceSessionStoryAnswerKey, ExperienceSessionStoryGoalSlice, ExperienceSessionStoryGraphEdge, ExperienceSessionStoryGraphNode, ExperienceSessionStoryNode, ExperienceSessionStoryNodeKind, ExperienceSessionStorySkillLink, ExperienceSessionStorySkillRole, ExperienceSessionStorySubagentDispatch, ExperienceSessionSummary, ExperienceSkillSegment, ExperienceSkillSummary, ExperienceTimelineBranch, ExperienceTimelineEvent, ExperienceTimelineTree, ObservationExperienceReport, ObservationReviewState, };
7
+ export declare const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
7
8
  export declare function aggregateExperienceChecklistItemStatus(statuses: ExperienceChecklistItemStatus[]): ExperienceChecklistItemStatus;
8
9
  interface BuildExperienceInput {
9
10
  sessions: CcSession[];
@@ -13,6 +14,7 @@ interface BuildExperienceInput {
13
14
  reviewState?: ObservationReviewState;
14
15
  }
15
16
  export declare function buildObservationExperienceReport(input: BuildExperienceInput): ObservationExperienceReport;
17
+ export declare function normalizeObservationExperienceReport(value: unknown): ObservationExperienceReport | null;
16
18
  export declare function isExperienceTraceInProgress(session: ExperienceSessionSummary): boolean;
17
19
  export declare function hasRecognizableUserGoalText(value: string | undefined): boolean;
18
20
  export declare function foldExperienceChecklistItems(items: ExperienceChecklistItem[]): {
@@ -7,6 +7,7 @@ import { durationMsBetween } from '../shared/time.js';
7
7
  import { loadExpectedToolsForSkill, loadFrontmatterSkillType, loadSkillDeclarationCheck, } from './experience-frontmatter.js';
8
8
  import { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
9
9
  export { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
10
+ export const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
10
11
  export function aggregateExperienceChecklistItemStatus(statuses) {
11
12
  if (statuses.includes('degraded'))
12
13
  return 'degraded';
@@ -131,8 +132,8 @@ export function buildObservationExperienceReport(input) {
131
132
  const sessions = summarizeExperienceSessions(invocations, sessionGroupsByKey, input.generatedAt, input.reviewState);
132
133
  const skills = summarizeExperienceSkills(sessions, invocations);
133
134
  return {
134
- reportKind: 'observe-experience',
135
- schemaVersion: 1,
135
+ kind: 'observe-experience',
136
+ schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
136
137
  scope: 'evidence-only',
137
138
  generatedAt: input.generatedAt,
138
139
  meta: {
@@ -148,6 +149,34 @@ export function buildObservationExperienceReport(input) {
148
149
  skills,
149
150
  };
150
151
  }
152
+ export function normalizeObservationExperienceReport(value) {
153
+ if (!value || typeof value !== 'object')
154
+ return null;
155
+ const report = value;
156
+ const kind = report.kind === 'observe-experience' ? report.kind : null;
157
+ if (!kind)
158
+ return null;
159
+ if (report.schemaVersion !== OBSERVATION_EXPERIENCE_SCHEMA_VERSION)
160
+ return null;
161
+ if (report.scope !== 'evidence-only')
162
+ return null;
163
+ if (typeof report.generatedAt !== 'string' || !report.meta || typeof report.meta !== 'object')
164
+ return null;
165
+ if (!Array.isArray(report.goalSlices) || !Array.isArray(report.invocations) || !Array.isArray(report.sessions) || !Array.isArray(report.skills)) {
166
+ return null;
167
+ }
168
+ return {
169
+ kind: 'observe-experience',
170
+ schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
171
+ scope: 'evidence-only',
172
+ generatedAt: report.generatedAt,
173
+ meta: report.meta,
174
+ goalSlices: report.goalSlices,
175
+ invocations: report.invocations,
176
+ sessions: report.sessions,
177
+ skills: report.skills,
178
+ };
179
+ }
151
180
  function relatedObservationItems(segment, items) {
152
181
  return items.filter((item) => item.skillName === segment.skillName
153
182
  && (item.sessionId === segment.sessionId || item.recentSessionIds.includes(segment.sessionId))
@@ -6,10 +6,11 @@ import { extractGapSignalsFromTrace } from '../analysis/gap-analyzer.js';
6
6
  import { ccTracesToResultEntries } from './trace-adapter.js';
7
7
  import { isSearchToolCall, toolCallQuery } from '../shared/tool-search.js';
8
8
  import { durationMsBetween } from '../shared/time.js';
9
- import { buildObservationExperienceReport } from './experience.js';
9
+ import { buildObservationExperienceReport, normalizeObservationExperienceReport, } from './experience.js';
10
10
  export const DEFAULT_PROJECT_OBSERVATIONS_DIR = join(process.cwd(), '.omk', 'observations');
11
11
  export const DEFAULT_GLOBAL_OBSERVATIONS_DIR = join(homedir(), '.oh-my-knowledge', 'observations');
12
12
  export const DEFAULT_OBSERVATIONS_DIR = DEFAULT_PROJECT_OBSERVATIONS_DIR;
13
+ const OBSERVATION_INBOX_SCHEMA_VERSION = 2;
13
14
  function hashString(input) {
14
15
  return createHash('sha256').update(input).digest('hex').slice(0, 16);
15
16
  }
@@ -432,8 +433,8 @@ export function buildObservationInboxReport(tracePath, options = {}) {
432
433
  const items = finishInboxAggregation(aggregationState);
433
434
  const experience = buildObservationExperienceReport({ sessions, segments, items, generatedAt, reviewState: options.reviewState });
434
435
  const report = {
435
- reportKind: 'observe-inbox',
436
- schemaVersion: 1,
436
+ kind: 'observe-inbox',
437
+ schemaVersion: OBSERVATION_INBOX_SCHEMA_VERSION,
437
438
  meta: {
438
439
  tracePath,
439
440
  generatedAt,
@@ -543,7 +544,9 @@ export function loadObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
543
544
  .filter((file) => file.endsWith('-observe-inbox.json'))
544
545
  .map((file) => {
545
546
  try {
546
- const report = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
547
+ const report = normalizeObservationInboxReport(JSON.parse(readFileSync(join(dir, file), 'utf-8')));
548
+ if (!report)
549
+ return null;
547
550
  report.items = report.items.map((item) => {
548
551
  const sourceKind = item.sourceKind;
549
552
  return {
@@ -567,7 +570,30 @@ export function loadObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
567
570
  return null;
568
571
  }
569
572
  })
570
- .filter((r) => r?.reportKind === 'observe-inbox');
573
+ .filter((r) => r?.kind === 'observe-inbox');
574
+ }
575
+ function normalizeObservationInboxReport(value) {
576
+ if (!value || typeof value !== 'object')
577
+ return null;
578
+ const report = value;
579
+ const kind = report.kind === 'observe-inbox' ? report.kind : null;
580
+ if (!kind)
581
+ return null;
582
+ if (report.schemaVersion !== OBSERVATION_INBOX_SCHEMA_VERSION)
583
+ return null;
584
+ if (!report.meta || typeof report.meta !== 'object' || !Array.isArray(report.items))
585
+ return null;
586
+ const experience = report.experience === undefined
587
+ ? undefined
588
+ : normalizeObservationExperienceReport(report.experience) ?? undefined;
589
+ return {
590
+ kind: 'observe-inbox',
591
+ schemaVersion: OBSERVATION_INBOX_SCHEMA_VERSION,
592
+ meta: report.meta,
593
+ items: report.items,
594
+ ...(experience ? { experience } : {}),
595
+ ...(report.diagnostics !== undefined ? { diagnostics: report.diagnostics } : {}),
596
+ };
571
597
  }
572
598
  export function loadLatestObservationInboxReport(dir = DEFAULT_OBSERVATIONS_DIR) {
573
599
  const reports = loadObservationInboxReports(dir);
@@ -38,10 +38,11 @@ export function observationMetricAnnotationVerdict(state, ref, metricKey) {
38
38
  export function observationReviewStatePath(observationsDir) {
39
39
  return join(observationsDir, 'review-state.json');
40
40
  }
41
+ const OBSERVATION_REVIEW_STATE_SCHEMA_VERSION = 2;
41
42
  export function emptyObservationReviewState(now = new Date().toISOString()) {
42
43
  return {
43
- reportKind: 'observe-review-state',
44
- schemaVersion: 1,
44
+ kind: 'observe-review-state',
45
+ schemaVersion: OBSERVATION_REVIEW_STATE_SCHEMA_VERSION,
45
46
  updatedAt: now,
46
47
  entries: {},
47
48
  };
@@ -52,15 +53,7 @@ export function loadObservationReviewState(observationsDir) {
52
53
  return emptyObservationReviewState();
53
54
  try {
54
55
  const parsed = JSON.parse(readFileSync(path, 'utf-8'));
55
- if (parsed.reportKind !== 'observe-review-state' || parsed.schemaVersion !== 1 || !parsed.entries || typeof parsed.entries !== 'object') {
56
- return emptyObservationReviewState();
57
- }
58
- return {
59
- reportKind: 'observe-review-state',
60
- schemaVersion: 1,
61
- updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
62
- entries: Object.fromEntries(Object.entries(parsed.entries).filter(([, entry]) => isReviewStateEntry(entry))),
63
- };
56
+ return normalizeObservationReviewState(parsed) ?? emptyObservationReviewState();
64
57
  }
65
58
  catch {
66
59
  return emptyObservationReviewState();
@@ -92,6 +85,24 @@ export function updateObservationReviewState(observationsDir, update, now = new
92
85
  writeFileSync(observationReviewStatePath(observationsDir), JSON.stringify(state, null, 2));
93
86
  return state;
94
87
  }
88
+ function normalizeObservationReviewState(value) {
89
+ if (!value || typeof value !== 'object')
90
+ return null;
91
+ const parsed = value;
92
+ const kind = parsed.kind === 'observe-review-state' ? parsed.kind : null;
93
+ if (!kind)
94
+ return null;
95
+ if (parsed.schemaVersion !== OBSERVATION_REVIEW_STATE_SCHEMA_VERSION)
96
+ return null;
97
+ if (!parsed.entries || typeof parsed.entries !== 'object')
98
+ return null;
99
+ return {
100
+ kind: 'observe-review-state',
101
+ schemaVersion: OBSERVATION_REVIEW_STATE_SCHEMA_VERSION,
102
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
103
+ entries: Object.fromEntries(Object.entries(parsed.entries).filter(([, entry]) => isReviewStateEntry(entry))),
104
+ };
105
+ }
95
106
  export function deleteObservationReviewState(observationsDir, targetType, targetId, now = new Date().toISOString()) {
96
107
  if (!isReviewTargetType(targetType))
97
108
  throw new Error('invalid review targetType');
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { createExecutor } from '../../executors/index.js';
3
3
  import { readPromptDocument } from '../../shared/llm-prompts/index.js';
4
4
  import { DEFAULT_LLM_ENHANCED_REVIEW_MODEL, PROMPTS_DIR, SOFT_STANDARD_PROMPT_ID, SOFT_STANDARD_PROMPT_VERSION, } from './constants.js';
5
- import { hashText, loadExisting, markStale, skillDerivedStandardsDir, skillDerivedStandardsPath, } from './skill-standards-store.js';
5
+ import { hashText, loadExisting, markStale, SKILL_DERIVED_STANDARDS_SCHEMA_VERSION, skillDerivedStandardsDir, skillDerivedStandardsPath, } from './skill-standards-store.js';
6
6
  import { evaluateRuntimeStandardNodes } from './runtime-evaluator.js';
7
7
  export async function extractSkillSoftStandards(options) {
8
8
  const { observationsDir, skillChain } = options;
@@ -47,8 +47,8 @@ export async function extractSkillSoftStandards(options) {
47
47
  source: 'llm_soft_standard',
48
48
  }));
49
49
  const record = {
50
- reportKind: 'observe-skill-derived-standards',
51
- schemaVersion: 1,
50
+ kind: 'observe-skill-derived-standards',
51
+ schemaVersion: SKILL_DERIVED_STANDARDS_SCHEMA_VERSION,
52
52
  skillName: skillChain.skillName,
53
53
  sourceSkillPath: skillChain.definition.path,
54
54
  sourceHash,
@@ -1,4 +1,5 @@
1
1
  import type { ResolveSkillStandardsOptions, ResolvedSkillStandards, SkillDerivedStandardStatus, SkillDerivedStandards } from './types.js';
2
+ export declare const SKILL_DERIVED_STANDARDS_SCHEMA_VERSION = 2;
2
3
  export declare function skillDerivedStandardsDir(observationsDir: string): string;
3
4
  export declare function skillDerivedStandardsPath(observationsDir: string, skillName: string): string;
4
5
  export declare function loadSkillDerivedStandards(observationsDir: string): Record<string, SkillDerivedStandards>;
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { buildObservationSkillChain } from '../skill-chain.js';
5
+ export const SKILL_DERIVED_STANDARDS_SCHEMA_VERSION = 2;
5
6
  export function skillDerivedStandardsDir(observationsDir) {
6
7
  return join(observationsDir, 'skill-derived');
7
8
  }
@@ -17,8 +18,8 @@ export function loadSkillDerivedStandards(observationsDir) {
17
18
  if (!file.endsWith('.json'))
18
19
  continue;
19
20
  try {
20
- const parsed = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
21
- if (isSkillDerivedStandards(parsed))
21
+ const parsed = normalizeSkillDerivedStandards(JSON.parse(readFileSync(join(dir, file), 'utf-8')));
22
+ if (parsed)
22
23
  out[parsed.skillName] = parsed;
23
24
  }
24
25
  catch {
@@ -51,11 +52,13 @@ export function updateSkillDerivedStandardStatus(observationsDir, skillName, sta
51
52
  }
52
53
  export function resolveSkillStandards(skillName, options) {
53
54
  const skillChain = options.skillChain ?? buildObservationSkillChain(skillName, options.cwd ?? process.cwd());
54
- const derived = options.derivedStandards
55
- ? isSkillDerivedStandards(options.derivedStandards)
56
- ? options.derivedStandards
57
- : options.derivedStandards[skillName]
58
- : loadSkillDerivedStandards(options.observationsDir)[skillName];
55
+ const directDerived = normalizeSkillDerivedStandards(options.derivedStandards);
56
+ const mappedDerived = options.derivedStandards && !isSkillDerivedStandards(options.derivedStandards)
57
+ ? normalizeSkillDerivedStandards(options.derivedStandards[skillName])
58
+ : undefined;
59
+ const derived = directDerived
60
+ ?? mappedDerived
61
+ ?? loadSkillDerivedStandards(options.observationsDir)[skillName];
59
62
  const active = [];
60
63
  const candidates = [];
61
64
  const hasFrontmatterHardRules = skillChain.healthCheck.hardRules.declared && skillChain.healthCheck.hardRules.rules.length > 0;
@@ -112,8 +115,7 @@ export function loadExisting(path) {
112
115
  if (!existsSync(path))
113
116
  return undefined;
114
117
  try {
115
- const parsed = JSON.parse(readFileSync(path, 'utf-8'));
116
- return isSkillDerivedStandards(parsed) ? parsed : undefined;
118
+ return normalizeSkillDerivedStandards(JSON.parse(readFileSync(path, 'utf-8')));
117
119
  }
118
120
  catch {
119
121
  return undefined;
@@ -129,13 +131,74 @@ export function markStale(record, generatedAt) {
129
131
  };
130
132
  }
131
133
  export function isSkillDerivedStandards(value) {
134
+ return normalizeSkillDerivedStandards(value) !== undefined;
135
+ }
136
+ function normalizeSkillDerivedStandards(value) {
132
137
  if (!value || typeof value !== 'object')
133
- return false;
138
+ return undefined;
134
139
  const item = value;
135
- return item.reportKind === 'observe-skill-derived-standards'
136
- && item.schemaVersion === 1
137
- && typeof item.skillName === 'string'
138
- && Array.isArray(item.standards);
140
+ const kind = item.kind === 'observe-skill-derived-standards' ? item.kind : null;
141
+ if (!kind)
142
+ return undefined;
143
+ if (item.schemaVersion !== SKILL_DERIVED_STANDARDS_SCHEMA_VERSION)
144
+ return undefined;
145
+ if (typeof item.skillName !== 'string' || typeof item.generatedAt !== 'string' || typeof item.model !== 'string' || typeof item.executor !== 'string') {
146
+ return undefined;
147
+ }
148
+ if (typeof item.promptId !== 'string' || typeof item.promptVersion !== 'string' || !Array.isArray(item.standards))
149
+ return undefined;
150
+ const standards = item.standards
151
+ .map(normalizeSkillDerivedStandard)
152
+ .filter((record) => record !== null);
153
+ return {
154
+ kind: 'observe-skill-derived-standards',
155
+ schemaVersion: SKILL_DERIVED_STANDARDS_SCHEMA_VERSION,
156
+ skillName: item.skillName,
157
+ ...(typeof item.sourceSkillPath === 'string' ? { sourceSkillPath: item.sourceSkillPath } : {}),
158
+ ...(typeof item.sourceHash === 'string' ? { sourceHash: item.sourceHash } : {}),
159
+ generatedAt: item.generatedAt,
160
+ model: item.model,
161
+ executor: item.executor,
162
+ promptId: item.promptId,
163
+ promptVersion: item.promptVersion,
164
+ ...(typeof item.promptHash === 'string' ? { promptHash: item.promptHash } : {}),
165
+ ...(typeof item.runtimeEvidenceHash === 'string' ? { runtimeEvidenceHash: item.runtimeEvidenceHash } : {}),
166
+ ...(item.enhancedReview && typeof item.enhancedReview === 'object' ? { enhancedReview: item.enhancedReview } : {}),
167
+ standards,
168
+ };
169
+ }
170
+ function normalizeSkillDerivedStandard(value) {
171
+ if (!value || typeof value !== 'object')
172
+ return null;
173
+ const item = value;
174
+ const standardKind = item.standardKind === 'hard_rule_candidate' || item.standardKind === 'workflow_candidate'
175
+ ? item.standardKind
176
+ : item.kind === 'hard_rule_candidate' || item.kind === 'workflow_candidate'
177
+ ? item.kind
178
+ : null;
179
+ if (!standardKind)
180
+ return null;
181
+ if (typeof item.id !== 'string' || typeof item.title !== 'string' || typeof item.body !== 'string')
182
+ return null;
183
+ if (item.status !== 'pending_review' && item.status !== 'author_confirmed' && item.status !== 'rejected' && item.status !== 'stale') {
184
+ return null;
185
+ }
186
+ if (item.source !== 'llm_soft_standard')
187
+ return null;
188
+ if (item.confidence !== 'low' && item.confidence !== 'medium' && item.confidence !== 'high')
189
+ return null;
190
+ if (!Array.isArray(item.evidence) || item.evidence.some((entry) => typeof entry !== 'string'))
191
+ return null;
192
+ return {
193
+ id: item.id,
194
+ standardKind,
195
+ status: item.status,
196
+ title: item.title,
197
+ body: item.body,
198
+ source: item.source,
199
+ confidence: item.confidence,
200
+ evidence: item.evidence,
201
+ };
139
202
  }
140
203
  function candidateRank(status) {
141
204
  if (status === 'pending_review')
@@ -14,8 +14,8 @@ export interface SkillDerivedStandard {
14
14
  evidence: string[];
15
15
  }
16
16
  export interface SkillDerivedStandards {
17
- reportKind: 'observe-skill-derived-standards';
18
- schemaVersion: 1;
17
+ kind: 'observe-skill-derived-standards';
18
+ schemaVersion: 2;
19
19
  skillName: string;
20
20
  sourceSkillPath?: string;
21
21
  sourceHash?: string;
@@ -21,7 +21,7 @@ function levelDot(level) {
21
21
  }
22
22
  }
23
23
  function isEvaluationReport(document) {
24
- return document.reportKind === 'evaluation';
24
+ return document.kind === 'evaluation';
25
25
  }
26
26
  function scoreOf(summary) {
27
27
  return summary?.avgCompositeScore ?? summary?.avgLlmScore ?? null;
@@ -163,7 +163,7 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
163
163
  `<span class="rc-score-num rc-score-num--${cls}">${score.toFixed(2)}</span></div>`;
164
164
  };
165
165
  const cards = runs.map((run) => {
166
- if (run.reportKind === 'batch-evaluation') {
166
+ if (run.kind === 'batch-evaluation') {
167
167
  const m = run.meta;
168
168
  const scores = run.items.length > 0
169
169
  ? run.items.map((item) => {
@@ -652,7 +652,7 @@ export function renderBatchEvaluationDetail(report, lang = DEFAULT_LANG) {
652
652
  export function renderReportDocumentDetail(report, lang = DEFAULT_LANG) {
653
653
  if (!report)
654
654
  return renderRunDetail(null, lang);
655
- return report.reportKind === 'batch-evaluation'
655
+ return report.kind === 'batch-evaluation'
656
656
  ? renderBatchEvaluationDetail(report, lang)
657
657
  : renderRunDetail(report, lang);
658
658
  }
@@ -159,7 +159,7 @@ export const I18N = {
159
159
  diffColCoverage: '覆盖',
160
160
  viewTrendLink: '查看趋势 →',
161
161
  artifactHashLabel: '版本指纹',
162
- artifactHashTooltip: 'skill 文件内容的 SHA-256 前 12 位(不含路径/时间/git),用于辨别报告对应哪一版 skill;同文件多次跑指纹不变,改一字节就变——防止"改动效果"和"随机波动"混淆',
162
+ artifactHashTooltip: 'skill 内容指纹的 SHA-256 前 12 位(不含路径/时间/git):目录-skill(本地或 git)覆盖整棵可分发树(SKILL.md + references/ 资产,排除 .omk/.git/node_modules/evolve;改任意资产都变,git 源经隔离副本物化、整树暴露给 executor),单文件-skill 取该 .md 字节;用于辨别报告对应哪一版 skill,同输入指纹不变——防止"改动效果"和"随机波动"混淆',
163
163
  switchLang: '英文',
164
164
  },
165
165
  en: {
@@ -272,7 +272,7 @@ export const I18N = {
272
272
  diffColCoverage: 'Coverage',
273
273
  viewTrendLink: 'trend →',
274
274
  artifactHashLabel: 'Version fingerprint',
275
- artifactHashTooltip: 'First 12 hex chars of SHA-256 over the skill file content (content-only: no path/time/git); identifies which version of the skill this report ran — same file = same fingerprint, any byte change = different fingerprint. Keeps "intentional change" separate from "random variance"',
275
+ artifactHashTooltip: 'First 12 hex chars of SHA-256 over the skill content (content-only: no path/time/git): a directory-skill (local or git) covers the whole distributable tree (SKILL.md + references/ assets, excluding .omk/.git/node_modules/evolve; any asset change flips it; git sources are materialized into an isolated copy whose whole tree is exposed to the executor), a file-skill covers the single .md bytes. Identifies which version of the skill this report ran — same input = same fingerprint. Keeps "intentional change" separate from "random variance"',
276
276
  switchLang: 'Chinese',
277
277
  },
278
278
  };
@@ -762,7 +762,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
762
762
  let evalReport = null;
763
763
  if (entry.eval) {
764
764
  const r = await reportStore.get(entry.eval.reportId);
765
- if (r && r.reportKind === 'evaluation')
765
+ if (r && r.kind === 'evaluation')
766
766
  evalReport = r;
767
767
  }
768
768
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -20,7 +20,7 @@ export declare function queryJobList(jobStore: JobStore, query?: JobQuery): Prom
20
20
  export declare function queryJob(jobStore: JobStore, id: string): Promise<EvaluationJob | null>;
21
21
  export interface RunListItem {
22
22
  id: string;
23
- reportKind: ReportDocument['reportKind'];
23
+ kind: ReportDocument['kind'];
24
24
  meta: ReportDocument['meta'];
25
25
  summary?: EvaluationReport['summary'];
26
26
  items?: BatchEvaluationReport['items'];