oh-my-knowledge 0.48.0 → 0.49.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 (243) hide show
  1. package/README.md +50 -18
  2. package/README.zh.md +55 -23
  3. package/dist/analysis/coverage-analyzer.d.ts +1 -0
  4. package/dist/analysis/coverage-analyzer.js +125 -62
  5. package/dist/analysis/failure-clusterer.js +2 -1
  6. package/dist/analysis/gap-analyzer.d.ts +2 -2
  7. package/dist/analysis/gap-analyzer.js +13 -3
  8. package/dist/analysis/hedging-classifier.d.ts +2 -2
  9. package/dist/analysis/hedging-classifier.js +3 -4
  10. package/dist/analysis/report-diagnostics.js +9 -7
  11. package/dist/analysis/sample-diagnostics.js +6 -6
  12. package/dist/artifact-graph/doctor.js +15 -7
  13. package/dist/assets/agent-skills/omk/SKILL.md +27 -7
  14. package/dist/assets/agent-skills/omk/references/commands.md +18 -17
  15. package/dist/authoring/evolver.d.ts +10 -6
  16. package/dist/authoring/evolver.js +496 -83
  17. package/dist/authoring/generator.d.ts +3 -3
  18. package/dist/authoring/generator.js +5 -10
  19. package/dist/authoring/sample-fixer.d.ts +8 -6
  20. package/dist/authoring/sample-fixer.js +76 -5
  21. package/dist/cli/commands/doctor.js +31 -14
  22. package/dist/cli/commands/eval/index.d.ts +3 -0
  23. package/dist/cli/commands/eval/index.js +163 -17
  24. package/dist/cli/commands/evolve.d.ts +4 -4
  25. package/dist/cli/commands/evolve.js +27 -13
  26. package/dist/cli/commands/init.js +16 -3
  27. package/dist/cli/commands/observe/inbox.js +28 -21
  28. package/dist/cli/commands/observe/index.js +20 -11
  29. package/dist/cli/commands/observe/ingest.d.ts +3 -0
  30. package/dist/cli/commands/observe/ingest.js +30 -2
  31. package/dist/cli/commands/sample.d.ts +6 -3
  32. package/dist/cli/commands/sample.js +72 -68
  33. package/dist/cli/lib/codex-model-hint.d.ts +9 -0
  34. package/dist/cli/lib/codex-model-hint.js +45 -0
  35. package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
  36. package/dist/cli/lib/generation-failure-hint.js +61 -0
  37. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  38. package/dist/cli/lib/i18n-dict/common.js +4 -0
  39. package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
  40. package/dist/cli/lib/i18n-dict/gen.js +38 -6
  41. package/dist/cli/lib/i18n-dict/help.js +6 -6
  42. package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
  43. package/dist/cli/lib/i18n-dict/init.js +13 -9
  44. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  45. package/dist/cli/lib/i18n-dict/run.js +34 -2
  46. package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
  47. package/dist/cli/lib/llm-failure-classifier.js +8 -0
  48. package/dist/cli/lib/parse-run-config.d.ts +6 -5
  49. package/dist/cli/lib/parse-run-config.js +16 -9
  50. package/dist/cli/lib/runtime-defaults.d.ts +21 -0
  51. package/dist/cli/lib/runtime-defaults.js +79 -0
  52. package/dist/diagnosis/observe-mapper.js +14 -15
  53. package/dist/diagnosis/observe-producer.js +3 -1
  54. package/dist/diagnosis/studio-projection.js +14 -7
  55. package/dist/diagnosis/types.d.ts +2 -0
  56. package/dist/diagnosis/types.js +12 -0
  57. package/dist/doctor/endpoint-rule.js +2 -1
  58. package/dist/eval-core/artifact-file-names.js +18 -1
  59. package/dist/eval-core/artifact-index.d.ts +7 -11
  60. package/dist/eval-core/artifact-index.js +139 -80
  61. package/dist/eval-core/cache.d.ts +12 -3
  62. package/dist/eval-core/cache.js +89 -29
  63. package/dist/eval-core/comparability.js +10 -6
  64. package/dist/eval-core/evaluation-execution.d.ts +2 -1
  65. package/dist/eval-core/evaluation-execution.js +122 -37
  66. package/dist/eval-core/evaluation-job.d.ts +4 -1
  67. package/dist/eval-core/evaluation-job.js +4 -1
  68. package/dist/eval-core/evaluation-reporting.d.ts +15 -13
  69. package/dist/eval-core/evaluation-reporting.js +54 -52
  70. package/dist/eval-core/execution-strategy.d.ts +2 -0
  71. package/dist/eval-core/execution-strategy.js +11 -9
  72. package/dist/eval-core/fact-checker.js +15 -7
  73. package/dist/eval-core/holdout.js +3 -2
  74. package/dist/eval-core/judge-independence.d.ts +2 -2
  75. package/dist/eval-core/mock-hook.cjs +23 -6
  76. package/dist/eval-core/mocks-runtime.js +30 -8
  77. package/dist/eval-core/report-document.d.ts +12 -0
  78. package/dist/eval-core/report-document.js +1151 -0
  79. package/dist/eval-core/report-extensions.d.ts +4 -0
  80. package/dist/eval-core/report-extensions.js +500 -0
  81. package/dist/eval-core/report-file-migration.js +7 -2
  82. package/dist/eval-core/resume-compatibility.d.ts +31 -0
  83. package/dist/eval-core/resume-compatibility.js +141 -0
  84. package/dist/eval-core/sample-fingerprint.d.ts +12 -0
  85. package/dist/eval-core/sample-fingerprint.js +193 -0
  86. package/dist/eval-core/schema.js +86 -31
  87. package/dist/eval-core/verdict.d.ts +8 -4
  88. package/dist/eval-core/verdict.js +24 -10
  89. package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
  90. package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
  91. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
  92. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
  93. package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
  94. package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
  95. package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
  96. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
  97. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
  98. package/dist/eval-workflows/evaluation-pipeline.js +12 -7
  99. package/dist/eval-workflows/run-evaluation.d.ts +9 -7
  100. package/dist/eval-workflows/run-evaluation.js +79 -51
  101. package/dist/executors/anthropic-api.js +65 -9
  102. package/dist/executors/claude-cli.js +16 -79
  103. package/dist/executors/claude-protocol.d.ts +28 -0
  104. package/dist/executors/claude-protocol.js +180 -0
  105. package/dist/executors/claude-sdk-trace.js +56 -28
  106. package/dist/executors/claude-sdk.d.ts +1 -0
  107. package/dist/executors/claude-sdk.js +39 -93
  108. package/dist/executors/codex-cli-trace.js +166 -31
  109. package/dist/executors/codex-cli.d.ts +6 -8
  110. package/dist/executors/codex-cli.js +49 -151
  111. package/dist/executors/codex-protocol.d.ts +24 -0
  112. package/dist/executors/codex-protocol.js +234 -0
  113. package/dist/executors/codex-sdk.js +68 -120
  114. package/dist/executors/gemini.js +88 -13
  115. package/dist/executors/index.d.ts +2 -3
  116. package/dist/executors/index.js +5 -3
  117. package/dist/executors/openai-api.js +70 -9
  118. package/dist/executors/runtime-fingerprint.js +88 -11
  119. package/dist/executors/script-command.d.ts +8 -0
  120. package/dist/executors/script-command.js +87 -0
  121. package/dist/executors/script.js +202 -29
  122. package/dist/executors/shared.d.ts +35 -3
  123. package/dist/executors/shared.js +113 -15
  124. package/dist/grading/assertions.d.ts +1 -1
  125. package/dist/grading/assertions.js +19 -9
  126. package/dist/grading/diagnostic.d.ts +9 -2
  127. package/dist/grading/diagnostic.js +25 -2
  128. package/dist/grading/index.js +10 -4
  129. package/dist/grading/judge.js +19 -6
  130. package/dist/grading/layered-scores.d.ts +2 -3
  131. package/dist/grading/layered-scores.js +2 -3
  132. package/dist/inputs/load-samples.d.ts +1 -2
  133. package/dist/inputs/load-samples.js +23 -1
  134. package/dist/inputs/mcp-resolver.js +6 -3
  135. package/dist/inputs/sample-document.d.ts +11 -0
  136. package/dist/inputs/sample-document.js +96 -0
  137. package/dist/managed/evidence.d.ts +1 -0
  138. package/dist/managed/evidence.js +1 -1
  139. package/dist/managed/store.js +200 -91
  140. package/dist/observability/codex-trace-adapter.d.ts +5 -0
  141. package/dist/observability/codex-trace-adapter.js +850 -0
  142. package/dist/observability/experience.d.ts +32 -6
  143. package/dist/observability/experience.js +2695 -459
  144. package/dist/observability/feedback-matchers.js +16 -1
  145. package/dist/observability/inbox-view-model.d.ts +1 -1
  146. package/dist/observability/inbox-view-model.js +19 -14
  147. package/dist/observability/inbox.d.ts +7 -1
  148. package/dist/observability/inbox.js +632 -124
  149. package/dist/observability/problem-patterns.js +2 -0
  150. package/dist/observability/review-state.d.ts +6 -0
  151. package/dist/observability/review-state.js +235 -63
  152. package/dist/observability/skill-chain-advisories.js +1 -1
  153. package/dist/observability/skill-chain.js +17 -4
  154. package/dist/observability/skill-health-analyzer.d.ts +32 -7
  155. package/dist/observability/skill-health-analyzer.js +194 -121
  156. package/dist/observability/skill-health-report.d.ts +10 -0
  157. package/dist/observability/skill-health-report.js +620 -0
  158. package/dist/observability/soft-standards/constants.d.ts +0 -1
  159. package/dist/observability/soft-standards/constants.js +0 -1
  160. package/dist/observability/soft-standards/index.d.ts +1 -1
  161. package/dist/observability/soft-standards/index.js +1 -1
  162. package/dist/observability/soft-standards/llm-extractor.js +8 -10
  163. package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
  164. package/dist/observability/soft-standards/skill-standards-store.js +59 -18
  165. package/dist/observability/soft-standards/types.d.ts +2 -2
  166. package/dist/observability/trace-adapter.d.ts +12 -7
  167. package/dist/observability/trace-adapter.js +11 -9
  168. package/dist/observability/trace-attribution.d.ts +13 -5
  169. package/dist/observability/trace-attribution.js +315 -21
  170. package/dist/observability/trace-ingestion.d.ts +9 -0
  171. package/dist/observability/trace-ingestion.js +80 -0
  172. package/dist/observability/trace-ir.d.ts +113 -0
  173. package/dist/observability/trace-ir.js +87 -0
  174. package/dist/observability/trace-segmenter.d.ts +19 -6
  175. package/dist/observability/trace-segmenter.js +377 -196
  176. package/dist/observability/trace-session-index.d.ts +19 -0
  177. package/dist/observability/trace-session-index.js +68 -0
  178. package/dist/observability/trace-source.d.ts +12 -4
  179. package/dist/observability/trace-source.js +939 -215
  180. package/dist/renderer/html-renderer.js +37 -6
  181. package/dist/renderer/icons.js +3 -0
  182. package/dist/renderer/observation-inbox-renderer.js +208 -90
  183. package/dist/renderer/skill-detail-renderer.js +452 -109
  184. package/dist/renderer/skill-health-renderer.js +69 -12
  185. package/dist/renderer/summary.js +28 -7
  186. package/dist/renderer/table.js +21 -4
  187. package/dist/renderer/test-view.d.ts +1 -0
  188. package/dist/renderer/test-view.js +44 -9
  189. package/dist/server/indexed-report-store.js +14 -18
  190. package/dist/server/job-store.js +64 -26
  191. package/dist/server/report-server.js +190 -78
  192. package/dist/server/report-store.js +57 -80
  193. package/dist/server/skill-index.js +143 -49
  194. package/dist/server/skill-insights.js +44 -5
  195. package/dist/shared/artifact-graph.d.ts +3 -0
  196. package/dist/shared/artifact-graph.js +224 -0
  197. package/dist/shared/assertion-types.d.ts +8 -0
  198. package/dist/shared/assertion-types.js +46 -0
  199. package/dist/shared/atomic-json.d.ts +8 -0
  200. package/dist/shared/atomic-json.js +33 -0
  201. package/dist/shared/diagnosis-schema.d.ts +9 -0
  202. package/dist/shared/diagnosis-schema.js +181 -0
  203. package/dist/shared/doctor-report.d.ts +3 -0
  204. package/dist/shared/doctor-report.js +103 -0
  205. package/dist/shared/evaluation-job.d.ts +6 -0
  206. package/dist/shared/evaluation-job.js +217 -0
  207. package/dist/shared/executor-result.d.ts +17 -0
  208. package/dist/shared/executor-result.js +221 -0
  209. package/dist/shared/file-lock.d.ts +12 -0
  210. package/dist/shared/file-lock.js +129 -0
  211. package/dist/shared/json-value.d.ts +5 -0
  212. package/dist/shared/json-value.js +36 -0
  213. package/dist/shared/keyed-mutex.d.ts +7 -0
  214. package/dist/shared/keyed-mutex.js +24 -0
  215. package/dist/shared/record-count.d.ts +8 -0
  216. package/dist/shared/record-count.js +43 -0
  217. package/dist/shared/sample-contract.d.ts +3 -0
  218. package/dist/shared/sample-contract.js +332 -0
  219. package/dist/shared/timestamp.d.ts +6 -0
  220. package/dist/shared/timestamp.js +64 -0
  221. package/dist/shared/token-usage.d.ts +19 -0
  222. package/dist/shared/token-usage.js +50 -0
  223. package/dist/shared/tool-call-status.d.ts +8 -0
  224. package/dist/shared/tool-call-status.js +28 -0
  225. package/dist/shared/tool-identity.d.ts +21 -0
  226. package/dist/shared/tool-identity.js +84 -0
  227. package/dist/shared/tool-search.js +73 -16
  228. package/dist/shared/trace-projection.d.ts +5 -0
  229. package/dist/shared/trace-projection.js +20 -0
  230. package/dist/shared/trace-source-kind.d.ts +3 -0
  231. package/dist/shared/trace-source-kind.js +12 -0
  232. package/dist/types/diagnosis.d.ts +2 -0
  233. package/dist/types/eval.d.ts +4 -0
  234. package/dist/types/executor.d.ts +32 -5
  235. package/dist/types/index.d.ts +1 -0
  236. package/dist/types/index.js +1 -0
  237. package/dist/types/judge.d.ts +2 -0
  238. package/dist/types/observability.d.ts +116 -9
  239. package/dist/types/report.d.ts +58 -6
  240. package/dist/types/skill-index.d.ts +7 -0
  241. package/dist/types/trace.d.ts +2 -0
  242. package/dist/types/trace.js +1 -0
  243. package/package.json +9 -5
@@ -1,7 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
2
- import { join, normalize } from 'node:path';
1
+ import { existsSync, readFileSync, readdirSync, } from 'node:fs';
2
+ import { isAbsolute, join, normalize } from 'node:path';
3
3
  import { hashString } from '../eval-core/evaluation-reporting.js';
4
4
  import { OMK_HOME } from '../eval-core/default-dirs.js';
5
+ import { writeJsonFileAtomic } from '../shared/atomic-json.js';
6
+ import { withFileLock } from '../shared/file-lock.js';
7
+ import { isRfc3339Timestamp } from '../shared/timestamp.js';
5
8
  /**
6
9
  * 受管记录的 per-record 文件存储。一条记录一个 `.omk/managed/<id>.json`,镜像 report-store 的
7
10
  * 成熟模式(原子 tmp+rename):每次 install 只碰自己那个文件,independent write、不丢别人、
@@ -19,6 +22,8 @@ export function globalManagedDir() {
19
22
  return join(OMK_HOME, 'managed');
20
23
  }
21
24
  export function recordPath(dir, id) {
25
+ if (!isManagedRecordId(id))
26
+ throw new TypeError('invalid managed record id');
22
27
  return join(dir, `${id}.json`);
23
28
  }
24
29
  /** 稳定身份 = hash(kind, name)。源路径是可变属性、不进 id。kind 取自固定枚举(无 `|`),分隔可注入。 */
@@ -31,6 +36,18 @@ export { hashArtifactSource, isDistributablePath, distributableCopyFilter } from
31
36
  function isStringField(v) {
32
37
  return typeof v === 'string';
33
38
  }
39
+ function isNonEmptyString(v) {
40
+ return typeof v === 'string' && v.length > 0;
41
+ }
42
+ function isManagedRecordId(v) {
43
+ return typeof v === 'string' && /^[0-9a-f]{12}$/.test(v);
44
+ }
45
+ function isNonNegativeSafeInteger(v) {
46
+ return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
47
+ }
48
+ function isRate(v) {
49
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 1;
50
+ }
34
51
  /** 仅当字段缺省或是 string 才放行(可选 string 字段的脏值守卫,如 source.url / evidence.verdict)。 */
35
52
  function isOptionalString(v) {
36
53
  return v === undefined || typeof v === 'string';
@@ -47,27 +64,41 @@ function isOptionalSha(v) {
47
64
  }
48
65
  // 受管记录可安装的 kind(managed 记录绝不是 baseline)。
49
66
  const MANAGED_KINDS = new Set(['skill', 'prompt', 'agent', 'workflow']);
67
+ const MANAGED_DECISIONS = new Set(['promote', 'reject', 'rollback']);
68
+ const VERDICTS = new Set([
69
+ 'PROGRESS',
70
+ 'CAUTIOUS',
71
+ 'REGRESS',
72
+ 'NOISE',
73
+ 'UNDERPOWERED',
74
+ 'SOLO',
75
+ ]);
76
+ const OVERRIDE_VERDICTS = new Set([...VERDICTS, 'UNKNOWN']);
77
+ const OVERRIDDEN_BLOCKS = new Set(['drifted', 'no_evidence', 'incomparable', 'verdict_blocked']);
50
78
  // 校验到**运行时实际收窄**的边界,不止「字段是 string」:记录文件是用户可手改、且可能随仓库分发(被
51
79
  // loadAllManagedRecords 无 opt-in 读到)的不可信输入。若只查 string,畸形记录(如 source.url 是对象、
52
80
  // sourceKind 是任意串)会绕过 validator,在下游(omk list 的 sourceLabel ?? → dispWidth(对象) 等)
53
81
  // 抛 TypeError 让命令崩溃。这里把 source / evidence 收窄到 list 等消费方实际解引用的类型,判脏即丢弃该文件。
54
- function isManagedArtifactRecord(value) {
82
+ function isManagedArtifactRecord(value, expectedId) {
55
83
  if (!value || typeof value !== 'object')
56
84
  return false;
57
85
  const r = value;
58
86
  if (!(r.recordKind === 'managed-artifact'
59
87
  && r.schemaVersion === 2
60
- && isStringField(r.id)
61
- && isStringField(r.name)
88
+ && isManagedRecordId(r.id)
89
+ && (expectedId === undefined || r.id === expectedId)
90
+ && isNonEmptyString(r.name)
62
91
  && isStringField(r.kind) && MANAGED_KINDS.has(r.kind)
63
- && isStringField(r.contentHash)
92
+ && r.id === managedRecordId(r.kind, r.name)
93
+ && isNonEmptyString(r.contentHash)
94
+ && isRfc3339Timestamp(r.installedAt)
64
95
  && r.source && typeof r.source === 'object'
65
96
  && Array.isArray(r.distribution)
66
97
  && Array.isArray(r.evidence)
67
98
  && Array.isArray(r.decisions)))
68
99
  return false;
69
100
  const src = r.source;
70
- if (!(isStringField(src.locator)
101
+ if (!(isNonEmptyString(src.locator)
71
102
  && (src.sourceKind === 'file' || src.sourceKind === 'git')
72
103
  && typeof src.isDirectorySkill === 'boolean'
73
104
  && isOptionalString(src.url)
@@ -77,22 +108,51 @@ function isManagedArtifactRecord(value) {
77
108
  // 读取的 locator(畸形但可通过上面 string 校验)→ 「读了什么」与「显示什么」不一致。判脏丢弃。
78
109
  if (src.sourceKind === 'file' && (src.url !== undefined || src.ref !== undefined))
79
110
  return false;
80
- const okDist = r.distribution.every((d) => d && typeof d === 'object'
81
- && isStringField(d.path) && isStringField(d.contentHash));
111
+ const distributionPaths = new Set();
112
+ const okDist = r.distribution.every((d) => {
113
+ if (!d || typeof d !== 'object')
114
+ return false;
115
+ const target = d;
116
+ if (!isNonEmptyString(target.label)
117
+ || !isNonEmptyString(target.path)
118
+ || !isAbsolute(target.path)
119
+ || !isNonEmptyString(target.contentHash)
120
+ || !isRfc3339Timestamp(target.copiedAt))
121
+ return false;
122
+ const pathKey = normalize(target.path).replace(/[\\/]+$/, '') || target.path;
123
+ if (distributionPaths.has(pathKey))
124
+ return false;
125
+ distributionPaths.add(pathKey);
126
+ return true;
127
+ });
128
+ const evidenceKeys = new Set();
82
129
  const okEv = r.evidence.every((e) => {
83
130
  if (!e || typeof e !== 'object')
84
131
  return false;
85
132
  const ev = e;
86
- if (!(isStringField(ev.reportId) && isStringField(ev.contentHash) && isStringField(ev.recordedAt)))
133
+ if (!isNonEmptyString(ev.reportId)
134
+ || !isNonEmptyString(ev.contentHash)
135
+ || !isRfc3339Timestamp(ev.recordedAt)
136
+ || (ev.verdict !== undefined && !VERDICTS.has(String(ev.verdict))))
87
137
  return false;
88
- if (!isOptionalString(ev.verdict))
138
+ const evidenceKey = `${ev.reportId}\0${ev.contentHash}`;
139
+ if (evidenceKeys.has(evidenceKey))
89
140
  return false;
141
+ evidenceKeys.add(evidenceKey);
142
+ if (ev.sampleCoverage !== undefined) {
143
+ const coverage = ev.sampleCoverage;
144
+ if (!coverage
145
+ || typeof coverage !== 'object'
146
+ || !isNonNegativeSafeInteger(coverage.count)
147
+ || !isNonEmptyString(coverage.hash))
148
+ return false;
149
+ }
90
150
  // comparability 若存在:必须是带 string cliVersion 的对象(list / promote 会读它)。可选 marker
91
151
  // judgePromptHash / debiasMode 同样收窄到声明类型 —— 否则任意类型脏值会穿过 validator 原样进 `omk list
92
152
  // --json`(及未来 promote gate)消费方。
93
153
  if (ev.comparability !== undefined) {
94
154
  const c = ev.comparability;
95
- if (!c || typeof c !== 'object' || !isStringField(c.cliVersion))
155
+ if (!c || typeof c !== 'object' || !isNonEmptyString(c.cliVersion))
96
156
  return false;
97
157
  if (!isOptionalString(c.judgePromptHash))
98
158
  return false;
@@ -111,36 +171,53 @@ function isManagedArtifactRecord(value) {
111
171
  if (!d || typeof d !== 'object')
112
172
  return false;
113
173
  const dec = d;
114
- if (!isStringField(dec.decisionKind))
174
+ if (!MANAGED_DECISIONS.has(String(dec.decisionKind))
175
+ || !isNonEmptyString(dec.actor)
176
+ || !isRfc3339Timestamp(dec.decidedAt))
115
177
  return false;
116
178
  // promote/reject/rollback 的证据指针(promote 写)同样收窄:任意类型脏值不得穿过 validator 进
117
179
  // deriveManagedState / promote 消费方。actor / decidedAt / reason 是 install 后才追加的可选展示字段,
118
180
  // 旧记录(install 时 decisions 恒空)无,按 optional 读。
119
- if (!isOptionalString(dec.actor) || !isOptionalString(dec.decidedAt) || !isOptionalString(dec.reason))
181
+ if (!isOptionalString(dec.reason))
182
+ return false;
183
+ if ((dec.contentHash !== undefined && !isNonEmptyString(dec.contentHash))
184
+ || (dec.reportId !== undefined && !isNonEmptyString(dec.reportId)))
120
185
  return false;
121
- if (!isOptionalString(dec.contentHash) || !isOptionalString(dec.reportId))
186
+ if ((dec.decisionKind === 'promote' || dec.decisionKind === 'rollback')
187
+ && !isNonEmptyString(dec.contentHash))
122
188
  return false;
123
189
  if (dec.override !== undefined) {
124
190
  const o = dec.override;
125
- if (!o || typeof o !== 'object' || !isStringField(o.verdict))
191
+ if (dec.decisionKind !== 'promote'
192
+ || !o
193
+ || typeof o !== 'object'
194
+ || !OVERRIDE_VERDICTS.has(String(o.verdict)))
126
195
  return false;
127
196
  if (o.overriddenBlocks !== undefined
128
- && !(Array.isArray(o.overriddenBlocks) && o.overriddenBlocks.every((b) => typeof b === 'string')))
197
+ && !(Array.isArray(o.overriddenBlocks)
198
+ && o.overriddenBlocks.every((block) => OVERRIDDEN_BLOCKS.has(String(block)))))
129
199
  return false;
130
200
  }
131
201
  return true;
132
202
  });
133
203
  // observations(#235)缺省合法(旧记录无);present 必须是数组,每条窄到声明类型 —— healthBand / confidence
134
204
  // 驱动读时 marker(deriveProductionGap),畸形脏值会让 list / Studio 误判,故判脏丢整条记录(同 okEv / okDec)。
205
+ const observationIds = new Set();
135
206
  const okObs = r.observations === undefined || (Array.isArray(r.observations) && r.observations.every((o) => {
136
207
  if (!o || typeof o !== 'object')
137
208
  return false;
138
209
  const obs = o;
139
210
  if (obs.observationKind !== 'production-health')
140
211
  return false;
141
- if (!isStringField(obs.reportId) || !isStringField(obs.observedAt))
212
+ if (!isNonEmptyString(obs.reportId)
213
+ || observationIds.has(obs.reportId)
214
+ || !isRfc3339Timestamp(obs.observedAt))
142
215
  return false;
143
- if (typeof obs.gapRate !== 'number' || typeof obs.weightedGapRate !== 'number' || typeof obs.segmentCount !== 'number')
216
+ observationIds.add(obs.reportId);
217
+ if (!isRate(obs.gapRate)
218
+ || !isRate(obs.weightedGapRate)
219
+ || obs.weightedGapRate > obs.gapRate
220
+ || !isNonNegativeSafeInteger(obs.segmentCount))
144
221
  return false;
145
222
  if (obs.confidence !== 'high' && obs.confidence !== 'low' && obs.confidence !== 'underpowered')
146
223
  return false;
@@ -149,17 +226,20 @@ function isManagedArtifactRecord(value) {
149
226
  const g = obs.gapByType;
150
227
  if (!g || typeof g !== 'object')
151
228
  return false;
152
- return ['failed_search', 'explicit_marker', 'hedging', 'repeated_failure'].every((k) => typeof g[k] === 'number');
229
+ return ['failed_search', 'explicit_marker', 'hedging', 'repeated_failure']
230
+ .every((key) => isNonNegativeSafeInteger(g[key]));
153
231
  }));
154
232
  return okDist && okEv && okDec && okObs;
155
233
  }
156
234
  export function loadManagedRecord(dir, id) {
235
+ if (!isManagedRecordId(id))
236
+ return null;
157
237
  const path = recordPath(dir, id);
158
238
  if (!existsSync(path))
159
239
  return null;
160
240
  try {
161
241
  const parsed = JSON.parse(readFileSync(path, 'utf-8'));
162
- return isManagedArtifactRecord(parsed) ? parsed : null;
242
+ return isManagedArtifactRecord(parsed, id) ? parsed : null;
163
243
  }
164
244
  catch {
165
245
  return null;
@@ -169,12 +249,13 @@ function readRecordsFromDir(dir) {
169
249
  if (!existsSync(dir))
170
250
  return [];
171
251
  const out = [];
172
- for (const entry of readdirSync(dir)) {
173
- if (!entry.endsWith('.json'))
252
+ for (const entry of readdirSync(dir).sort()) {
253
+ const match = /^([0-9a-f]{12})\.json$/.exec(entry);
254
+ if (!match)
174
255
  continue;
175
256
  try {
176
257
  const parsed = JSON.parse(readFileSync(join(dir, entry), 'utf-8'));
177
- if (isManagedArtifactRecord(parsed))
258
+ if (isManagedArtifactRecord(parsed, match[1]))
178
259
  out.push(parsed);
179
260
  }
180
261
  catch {
@@ -183,6 +264,17 @@ function readRecordsFromDir(dir) {
183
264
  }
184
265
  return out;
185
266
  }
267
+ function persistManagedRecord(dir, record) {
268
+ if (!isManagedArtifactRecord(record, record.id)) {
269
+ throw new TypeError('invalid managed artifact record');
270
+ }
271
+ writeJsonFileAtomic(recordPath(dir, record.id), record);
272
+ }
273
+ function withManagedRecordLock(dir, recordId, operation) {
274
+ if (!isManagedRecordId(recordId))
275
+ throw new TypeError('invalid managed record id');
276
+ return withFileLock(join(dir, `${recordId}.lock`), operation, { label: `managed record ${recordId}` });
277
+ }
186
278
  /** 读全部记录。项目目录空 → 兜底全局(镜像 observe inbox 的 project→global)。 */
187
279
  export function loadAllManagedRecords(dir = managedDir()) {
188
280
  const local = readRecordsFromDir(dir);
@@ -244,13 +336,14 @@ export function mergeManagedRecord(prev, next) {
244
336
  }
245
337
  /** 读旧记录 → 合并 → 原子 tmp+rename 只写该 id 的文件。返回合并后记录。 */
246
338
  export function upsertManagedRecord(dir, record) {
247
- const merged = mergeManagedRecord(loadManagedRecord(dir, record.id), record);
248
- mkdirSync(dir, { recursive: true });
249
- const path = recordPath(dir, record.id);
250
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
251
- writeFileSync(tmp, JSON.stringify(merged, null, 2));
252
- renameSync(tmp, path);
253
- return merged;
339
+ if (!isManagedArtifactRecord(record, record.id)) {
340
+ throw new TypeError('invalid managed artifact record');
341
+ }
342
+ return withManagedRecordLock(dir, record.id, () => {
343
+ const merged = mergeManagedRecord(loadManagedRecord(dir, record.id), record);
344
+ persistManagedRecord(dir, merged);
345
+ return merged;
346
+ });
254
347
  }
255
348
  /**
256
349
  * 追加一条评测证据(append-only)。与 install 的 upsert 路径相反——`mergeManagedRecord` 刻意**保留旧
@@ -260,21 +353,25 @@ export function upsertManagedRecord(dir, record) {
260
353
  * null —— 这是"管理是 install 显式 opt-in"的体现:eval 绝不为未纳管的 skill 凭空建记录。
261
354
  */
262
355
  export function appendManagedEvidence(dir, recordId, evidence) {
263
- const prev = loadManagedRecord(dir, recordId);
264
- if (!prev)
356
+ if (!isManagedRecordId(recordId))
265
357
  return null;
266
- const dup = prev.evidence.some((e) => e.reportId === evidence.reportId && e.contentHash === evidence.contentHash);
267
- const merged = dup
268
- ? prev
269
- : { ...prev, evidence: [...prev.evidence, evidence] };
270
- if (!dup) {
271
- mkdirSync(dir, { recursive: true });
272
- const path = recordPath(dir, recordId);
273
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
274
- writeFileSync(tmp, JSON.stringify(merged, null, 2));
275
- renameSync(tmp, path);
276
- }
277
- return merged;
358
+ return withManagedRecordLock(dir, recordId, () => {
359
+ const prev = loadManagedRecord(dir, recordId);
360
+ if (!prev)
361
+ return null;
362
+ const duplicateIndex = prev.evidence.findIndex((entry) => entry.reportId === evidence.reportId && entry.contentHash === evidence.contentHash);
363
+ const candidateEvidence = duplicateIndex >= 0
364
+ ? prev.evidence.map((entry, index) => index === duplicateIndex ? evidence : entry)
365
+ : [...prev.evidence, evidence];
366
+ if (!isManagedArtifactRecord({ ...prev, evidence: candidateEvidence }, recordId)) {
367
+ throw new TypeError('invalid managed evidence');
368
+ }
369
+ if (duplicateIndex >= 0)
370
+ return prev;
371
+ const merged = { ...prev, evidence: candidateEvidence };
372
+ persistManagedRecord(dir, merged);
373
+ return merged;
374
+ });
278
375
  }
279
376
  /**
280
377
  * 追加一条 observe 生产健康观测(append-only,#235)。同 evidence 不走 upsert:`mergeManagedRecord` 保留旧
@@ -283,21 +380,26 @@ export function appendManagedEvidence(dir, recordId, evidence) {
283
380
  * 匹配)返回 null —— 与 evidence 同口径:observe 绝不为未纳管 skill 凭空建记录(管理是 install 显式 opt-in)。
284
381
  */
285
382
  export function appendManagedObservation(dir, recordId, observation) {
286
- const prev = loadManagedRecord(dir, recordId);
287
- if (!prev)
383
+ if (!isManagedRecordId(recordId))
288
384
  return null;
289
- const dup = (prev.observations ?? []).some((o) => o.reportId === observation.reportId);
290
- const merged = dup
291
- ? prev
292
- : { ...prev, observations: [...(prev.observations ?? []), observation] };
293
- if (!dup) {
294
- mkdirSync(dir, { recursive: true });
295
- const path = recordPath(dir, recordId);
296
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
297
- writeFileSync(tmp, JSON.stringify(merged, null, 2));
298
- renameSync(tmp, path);
299
- }
300
- return merged;
385
+ return withManagedRecordLock(dir, recordId, () => {
386
+ const prev = loadManagedRecord(dir, recordId);
387
+ if (!prev)
388
+ return null;
389
+ const observations = prev.observations ?? [];
390
+ const duplicateIndex = observations.findIndex((entry) => entry.reportId === observation.reportId);
391
+ const candidateObservations = duplicateIndex >= 0
392
+ ? observations.map((entry, index) => index === duplicateIndex ? observation : entry)
393
+ : [...observations, observation];
394
+ if (!isManagedArtifactRecord({ ...prev, observations: candidateObservations }, recordId)) {
395
+ throw new TypeError('invalid managed observation');
396
+ }
397
+ if (duplicateIndex >= 0)
398
+ return prev;
399
+ const merged = { ...prev, observations: candidateObservations };
400
+ persistManagedRecord(dir, merged);
401
+ return merged;
402
+ });
301
403
  }
302
404
  /**
303
405
  * 把受管记录的 drift 基线(contentHash)重锚到新源内容哈,保留 evidence / decisions / source /
@@ -309,18 +411,20 @@ export function appendManagedObservation(dir, recordId, observation) {
309
411
  * 返回 null(同 evidence / decision:管理是 install 显式 opt-in)。
310
412
  */
311
413
  export function rebaselineManagedContentHash(dir, recordId, newHash) {
312
- const prev = loadManagedRecord(dir, recordId);
313
- if (!prev)
414
+ if (!isNonEmptyString(newHash))
415
+ throw new TypeError('newHash must be a non-empty string');
416
+ if (!isManagedRecordId(recordId))
314
417
  return null;
315
- if (prev.contentHash === newHash)
316
- return prev;
317
- const merged = { ...prev, contentHash: newHash };
318
- mkdirSync(dir, { recursive: true });
319
- const path = recordPath(dir, recordId);
320
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
321
- writeFileSync(tmp, JSON.stringify(merged, null, 2));
322
- renameSync(tmp, path);
323
- return merged;
418
+ return withManagedRecordLock(dir, recordId, () => {
419
+ const prev = loadManagedRecord(dir, recordId);
420
+ if (!prev)
421
+ return null;
422
+ if (prev.contentHash === newHash)
423
+ return prev;
424
+ const merged = { ...prev, contentHash: newHash };
425
+ persistManagedRecord(dir, merged);
426
+ return merged;
427
+ });
324
428
  }
325
429
  /**
326
430
  * 当前内容(contentHash === record.contentHash)是否处于 promoted 态。决定是 append-only 事件流,
@@ -379,37 +483,42 @@ export function currentPromoteOverride(record) {
379
483
  * (未 install)返回 null,与 evidence 同口径(管理是 install 显式 opt-in,绝不为未纳管 skill 凭空建记录)。
380
484
  */
381
485
  export function appendManagedDecision(dir, recordId, decision) {
382
- const prev = loadManagedRecord(dir, recordId);
383
- if (!prev)
486
+ if (!isManagedRecordId(recordId))
384
487
  return null;
385
- const isPromotion = decision.decisionKind === 'promote' || decision.decisionKind === 'rollback';
386
- const onCurrent = decision.contentHash !== undefined && decision.contentHash === prev.contentHash;
387
- const dup = isPromotion && onCurrent
388
- // promote/rollback 当前内容:不改变 promoted 态 → no-op(promote 当已 promoted、rollback 当未 promoted)。
389
- ? (decision.decisionKind === 'promote') === isCurrentlyPromoted(prev)
390
- : decision.contentHash !== undefined
391
- && prev.decisions.some((d) => d.decisionKind === decision.decisionKind && d.contentHash === decision.contentHash);
392
- const merged = dup
393
- ? prev
394
- : { ...prev, decisions: [...prev.decisions, decision] };
395
- if (!dup) {
396
- mkdirSync(dir, { recursive: true });
397
- const path = recordPath(dir, recordId);
398
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
399
- writeFileSync(tmp, JSON.stringify(merged, null, 2));
400
- renameSync(tmp, path);
401
- }
402
- return merged;
488
+ return withManagedRecordLock(dir, recordId, () => {
489
+ const prev = loadManagedRecord(dir, recordId);
490
+ if (!prev)
491
+ return null;
492
+ if (!isManagedArtifactRecord({ ...prev, decisions: [...prev.decisions, decision] }, recordId)) {
493
+ throw new TypeError('invalid managed decision');
494
+ }
495
+ const isPromotion = decision.decisionKind === 'promote' || decision.decisionKind === 'rollback';
496
+ const onCurrent = decision.contentHash !== undefined && decision.contentHash === prev.contentHash;
497
+ const dup = isPromotion && onCurrent
498
+ // promote/rollback 当前内容:不改变 promoted 态 → no-op(promote 当已 promoted、rollback 当未 promoted)。
499
+ ? (decision.decisionKind === 'promote') === isCurrentlyPromoted(prev)
500
+ : decision.contentHash !== undefined
501
+ && prev.decisions.some((d) => d.decisionKind === decision.decisionKind && d.contentHash === decision.contentHash);
502
+ if (dup)
503
+ return prev;
504
+ const merged = { ...prev, decisions: [...prev.decisions, decision] };
505
+ persistManagedRecord(dir, merged);
506
+ return merged;
507
+ });
403
508
  }
404
509
  /**
405
510
  * 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
406
511
  * 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
407
512
  */
408
513
  export function buildManagedArtifactRecord(input) {
514
+ const id = input.id ?? managedRecordId(input.kind, input.name);
515
+ if (id !== managedRecordId(input.kind, input.name)) {
516
+ throw new TypeError('managed record id must match kind and name');
517
+ }
409
518
  return {
410
519
  recordKind: 'managed-artifact',
411
520
  schemaVersion: 2,
412
- id: input.id ?? managedRecordId(input.kind, input.name),
521
+ id,
413
522
  name: input.name,
414
523
  kind: input.kind,
415
524
  source: input.source,
@@ -0,0 +1,5 @@
1
+ /** Codex rollout JSONL -> source-neutral Trace IR. */
2
+ import type { TraceSession } from './trace-ir.js';
3
+ export declare function isCodexJsonl(records: unknown[]): boolean;
4
+ export declare function isCodexGuardianRollout(records: unknown[]): boolean;
5
+ export declare function parseCodexSessionFile(filePath: string, rawRecords: unknown[]): TraceSession;