@modusensus/dsh-mneme 0.8.3 → 0.8.5

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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +727 -636
  3. package/bin/cli.mjs +0 -0
  4. package/bin/dsh-mneme-mcp.mjs +510 -0
  5. package/cordis.patch.yml +21 -21
  6. package/lib/api-standalone.js +506 -316
  7. package/lib/api.js +1403 -1316
  8. package/lib/bootstrap.js +211 -0
  9. package/lib/client.js +4657 -4366
  10. package/lib/commands.js +95 -95
  11. package/lib/config.js +615 -510
  12. package/lib/document.js +221 -0
  13. package/lib/dream/clustering.js +118 -118
  14. package/lib/dream/decisions.js +658 -658
  15. package/lib/dream/narratives.js +44 -0
  16. package/lib/dream/sleep.js +702 -700
  17. package/lib/dream.js +147 -16
  18. package/lib/embedding.js +177 -177
  19. package/lib/entities/extractor.js +249 -249
  20. package/lib/guide.js +44 -0
  21. package/lib/heat.js +144 -136
  22. package/lib/hot-memory.js +46 -46
  23. package/lib/index.js +515 -515
  24. package/lib/inject.js +375 -295
  25. package/lib/lang.js +208 -179
  26. package/lib/local-embedder.js +320 -320
  27. package/lib/mirror.js +198 -198
  28. package/lib/organize.js +450 -0
  29. package/lib/quality-filter.js +126 -126
  30. package/lib/recall-stats.js +125 -0
  31. package/lib/reranker.js +269 -269
  32. package/lib/runtime/adopt.js +176 -176
  33. package/lib/runtime/closure.js +172 -172
  34. package/lib/runtime/download.js +247 -247
  35. package/lib/runtime/layout.js +265 -235
  36. package/lib/runtime/loader.js +252 -249
  37. package/lib/runtime/model-fetch.js +361 -361
  38. package/lib/runtime/provision.js +237 -237
  39. package/lib/runtime/tarball.js +130 -130
  40. package/lib/runtime/verify.js +204 -191
  41. package/lib/scope.js +118 -118
  42. package/lib/search/adaptive.js +60 -22
  43. package/lib/search/bm25.js +96 -96
  44. package/lib/service.js +2324 -2087
  45. package/lib/settings.js +459 -420
  46. package/lib/store.js +2549 -2367
  47. package/lib/summarize.js +820 -449
  48. package/lib/tools.js +702 -574
  49. package/lib/vector-index.js +110 -110
  50. package/lib/version-check.js +122 -122
  51. package/package.json +109 -108
  52. package/runtime-manifest.json +526 -596
  53. package/scripts/benchmark-embed.js +201 -201
  54. package/scripts/benchmark-recall.js +199 -199
  55. package/scripts/benchmark-rerank.js +166 -166
  56. package/scripts/build-runtime-manifest.mjs +224 -217
  57. package/scripts/check-sync.js +42 -42
  58. package/scripts/e2e-dsh.js +220 -220
  59. package/scripts/mneme-runtime.mjs +277 -277
  60. package/scripts/stress-dsh.js +255 -255
  61. package/scripts/sync-lib.js +47 -47
  62. package/scripts/sync-test-badge.mjs +67 -61
  63. package/src/api-standalone.js +506 -316
  64. package/src/api.js +1403 -1316
  65. package/src/bootstrap.js +211 -0
  66. package/src/commands.js +95 -95
  67. package/src/config.js +615 -510
  68. package/src/document.js +221 -0
  69. package/src/dream/clustering.js +118 -118
  70. package/src/dream/decisions.js +658 -658
  71. package/src/dream/narratives.js +44 -0
  72. package/src/dream/sleep.js +702 -700
  73. package/src/dream.js +147 -16
  74. package/src/embedding.js +177 -177
  75. package/src/entities/extractor.js +249 -249
  76. package/src/guide.js +44 -0
  77. package/src/heat.js +144 -136
  78. package/src/hot-memory.js +46 -46
  79. package/src/index.js +515 -515
  80. package/src/inject.js +375 -295
  81. package/src/lang.js +208 -179
  82. package/src/local-embedder.js +320 -320
  83. package/src/mirror.js +198 -198
  84. package/src/organize.js +450 -0
  85. package/src/quality-filter.js +126 -126
  86. package/src/recall-stats.js +125 -0
  87. package/src/reranker.js +269 -269
  88. package/src/runtime/adopt.js +176 -176
  89. package/src/runtime/closure.js +172 -172
  90. package/src/runtime/download.js +247 -247
  91. package/src/runtime/layout.js +265 -235
  92. package/src/runtime/loader.js +252 -249
  93. package/src/runtime/model-fetch.js +361 -361
  94. package/src/runtime/provision.js +237 -237
  95. package/src/runtime/tarball.js +130 -130
  96. package/src/runtime/verify.js +204 -191
  97. package/src/scope.js +118 -118
  98. package/src/search/adaptive.js +60 -22
  99. package/src/search/bm25.js +96 -96
  100. package/src/service.js +2324 -2087
  101. package/src/settings.js +459 -420
  102. package/src/store.js +2549 -2367
  103. package/src/summarize.js +820 -449
  104. package/src/tools.js +702 -574
  105. package/src/vector-index.js +110 -110
  106. package/src/version-check.js +122 -122
  107. package/test/adaptive-inject-budget.test.js +85 -0
  108. package/test/api-routes-gaps.test.js +218 -218
  109. package/test/api.test.js +1640 -1482
  110. package/test/audit.test.js +448 -448
  111. package/test/benchmark.test.js +94 -94
  112. package/test/bootstrap.test.js +220 -0
  113. package/test/client.test.js +860 -736
  114. package/test/clustering.test.js +100 -100
  115. package/test/commands.test.js +106 -106
  116. package/test/config.test.js +66 -66
  117. package/test/conflict-freeze.test.js +300 -300
  118. package/test/document.test.js +523 -0
  119. package/test/dream-archive-guardrail.test.js +116 -116
  120. package/test/dream-candidate-hybrid.test.js +281 -281
  121. package/test/dream-coverage-degrade.test.js +86 -86
  122. package/test/dream.test.js +1308 -1254
  123. package/test/embedding-ready.test.js +196 -196
  124. package/test/entities.test.js +547 -547
  125. package/test/entity-recall.test.js +179 -0
  126. package/test/epistemic.test.js +298 -298
  127. package/test/fnew-0112.test.js +311 -311
  128. package/test/fnew-03.test.js +422 -422
  129. package/test/graph-api.test.js +175 -175
  130. package/test/heat.test.js +175 -148
  131. package/test/helpers/dream-mock.js +82 -82
  132. package/test/helpers/peer-worker.mjs +25 -25
  133. package/test/helpers/runtime-source.js +60 -60
  134. package/test/helpers/tar-builder.js +80 -80
  135. package/test/hot-memory.test.js +145 -145
  136. package/test/inject-content-cap.test.js +91 -0
  137. package/test/inject-first-round-bm25.test.js +89 -89
  138. package/test/inject-heat.test.js +61 -0
  139. package/test/inject-pools.test.js +170 -0
  140. package/test/inject-recall.test.js +105 -0
  141. package/test/inject-rotation.test.js +161 -161
  142. package/test/inject.test.js +283 -249
  143. package/test/lang.test.js +87 -87
  144. package/test/lib-smoke.test.js +96 -96
  145. package/test/llm-audit.test.js +462 -309
  146. package/test/llm-message-source.test.js +177 -177
  147. package/test/local-embedder.test.js +249 -249
  148. package/test/mcp-stdio.test.js +385 -0
  149. package/test/mirror-dirty.test.js +424 -424
  150. package/test/mirror-edit-digest.test.js +187 -187
  151. package/test/mirror-generation.test.js +499 -499
  152. package/test/mirror.test.js +249 -249
  153. package/test/narrative-bars.test.js +162 -0
  154. package/test/organize.test.js +418 -0
  155. package/test/peer-blockers.test.js +193 -193
  156. package/test/policy-epoch.test.js +259 -259
  157. package/test/quality-filter.test.js +186 -186
  158. package/test/reasoning-effort.test.js +746 -746
  159. package/test/recall-evals.test.js +235 -235
  160. package/test/recall-layer.test.js +326 -326
  161. package/test/recall-stats.test.js +123 -0
  162. package/test/receipt-chain.test.js +451 -451
  163. package/test/reflection.test.js +226 -226
  164. package/test/reindex-backfill.test.js +62 -62
  165. package/test/reranker-single-logit.test.js +73 -73
  166. package/test/reranker.test.js +197 -197
  167. package/test/resident-status.test.js +83 -0
  168. package/test/runtime-adopt.test.js +206 -206
  169. package/test/runtime-cli.test.js +89 -89
  170. package/test/runtime-closure.test.js +193 -193
  171. package/test/runtime-download.test.js +302 -302
  172. package/test/runtime-layout.test.js +163 -163
  173. package/test/runtime-loader.test.js +255 -237
  174. package/test/runtime-manifest.test.js +159 -158
  175. package/test/runtime-model-fetch.test.js +348 -348
  176. package/test/runtime-provision.test.js +101 -101
  177. package/test/runtime-tarball.test.js +81 -81
  178. package/test/runtime-verify.test.js +279 -214
  179. package/test/scope-candidates.test.js +239 -239
  180. package/test/scope-explicit.test.js +452 -452
  181. package/test/scope-retrieval.test.js +197 -197
  182. package/test/scope-storage.test.js +255 -255
  183. package/test/scope-strict.test.js +231 -231
  184. package/test/search-fusion.test.js +90 -90
  185. package/test/semantic.test.js +124 -124
  186. package/test/service-search.test.js +174 -174
  187. package/test/service.test.js +405 -405
  188. package/test/settings.test.js +261 -261
  189. package/test/skip-invalid-restore.test.js +262 -262
  190. package/test/sleep-action-set.test.js +422 -422
  191. package/test/sleep-heat.test.js +126 -125
  192. package/test/sleep-scheduler-rearm.test.js +89 -89
  193. package/test/sleep.test.js +410 -373
  194. package/test/standalone-api.test.js +544 -359
  195. package/test/store-vector-cache.test.js +98 -98
  196. package/test/store.test.js +396 -383
  197. package/test/stress.test.js +209 -209
  198. package/test/summarize.test.js +1288 -575
  199. package/test/tools-dto-schema-matrix.test.js +325 -0
  200. package/test/tools-memory-get-schema.test.js +65 -65
  201. package/test/tools.test.js +374 -341
  202. package/test/updated-at-semantics.test.js +113 -113
  203. package/test/vector-index.test.js +221 -221
  204. package/test/version-check.test.js +152 -152
  205. package/test/webServer-optional.test.js +82 -82
@@ -0,0 +1,221 @@
1
+ // dsh-mneme/src/document.js
2
+ // document 型记忆注册(issue #230,口径基线 = #164 设计稿评审线):agent 产
3
+ // 长文档的指针行铸造口。写入权分离——全文归 agent(管线对文件零读零写零改),
4
+ // 管线只做四件事:注册校验(文件存在 + 路径合法 + evidence 求交)、摘要 +
5
+ // doc_path 落库(库里只存这三样)、C2 vector 档比对去重、supersede 记账
6
+ // (出新版 = 新摘要行 supersede 旧行,旧文件不删,content_history 可追溯)。
7
+ //
8
+ // 独立成模块(AGENTS.md 尺寸约定,同 recallStats 先例):service.js 已过
9
+ // 2000 行参考线,这里只依赖注入 service 内部件,barrel 出口在 service.js,
10
+ // 调用方零改动。防绕过三层兜底:service 两条业务守卫(saveWithDedupe /
11
+ // updateMemory)+ 存储层唯一铸造口(store.saveDocument,通用 save/update/
12
+ // CAS 整类拒绝 document)——注册是唯一铸造口。
13
+
14
+ import { statSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { isAbsolute, resolve } from "node:path";
17
+ import { scopeKeyOf } from "./scope.js";
18
+ import { cosineSimilarity } from "./dream/clustering.js";
19
+
20
+ // C2 vector 档阈值(#164 拍板 0.92,与 findSessionDuplicate 的 vector 档同源)。
21
+ // 刻意不做 24h 时间窗:那个窗是会话去重的「近邻界」(findSessionDuplicate 注释),
22
+ // 不是相似度语义本身;document 版本更新天然跨天/跨周,套窗会把一周后重注册的
23
+ // 新版判成「无近重复」,留下两行活跃摘要漂移——supersede 探测必须全时段。
24
+ const DOCUMENT_MIN_SIM = 0.92;
25
+ const CANDIDATE_LIMIT = 200;
26
+
27
+ // 追加到被取代行正文的指针注记。刻意不带语言分支:id 是跨语言稳定键(用户
28
+ // 对 tag/章节名建议用英文同理——指针性内容不翻译);可追溯链 = 注记里的新行
29
+ // id + 旧行 content_history 里的旧摘要(source=superseded)。
30
+ const supersededByPointer = (id) => `\n\n[superseded by ${id}]`;
31
+
32
+ /**
33
+ * Build the document registrar. Injected deps are service.js closure members
34
+ * (store / embedQuery / transaction / finalize) so this module stays free of
35
+ * service-internal wiring; `finalize` is the single write epilogue (mirror
36
+ * sync + notify + re-embed) so callers never duplicate it.
37
+ *
38
+ * @returns {(payload: object) => Promise<object>}
39
+ * `{ action: "created"|"superseded", memory, superseded?, evidence_kept,
40
+ * evidence_dropped, degraded }`
41
+ * @throws flag 关 / 路径非法或文件缺失 / title 或 summary 为空 / evidence 全部
42
+ * 捏造 / 仅 vector 近重复(疑似重复外档,交 agent 裁决而不是静默二选一)。
43
+ */
44
+ export function createDocumentRegistrar({ store, config, embedQuery, pushContentHistory, transaction, finalize }) {
45
+ return async function registerDocument(payload, { hiddenEvidenceIds = [] } = {}) {
46
+ // opt-in 总闸(#230 对齐 #228 形态):关 = 注册入口整体不存在,错误信息
47
+ // 指回配置键,agent 能准确转告用户去开。
48
+ if (config?.documentMemoryEnabled !== true) {
49
+ throw new Error("document memory is disabled (documentMemoryEnabled)");
50
+ }
51
+
52
+ // --- 1. 路径合法 + 文件存在 --------------------------------------------
53
+ // 只认展开 ~ 后的绝对路径:相对路径依赖进程 cwd,注册时的 cwd 与读取时
54
+ // 的 cwd 不保证一致,指针必须自带完整定位。校验存在 + 常规文件 + 非空——
55
+ // 空文件/被截断成 0 字节是「文字结构意外破坏」的最早信号,注册时拦住
56
+ // 而不是落一条死指针(回退路径:agent 修好文件后重注册,同 path/title
57
+ // 自动 supersede 旧行)。
58
+ const rawPath = String(payload?.path ?? "").trim();
59
+ if (!rawPath) throw new Error("registerDocument: path is required");
60
+ // 只展开 ~ / ~/(~user 形式拒收为普通相对路径走 isAbsolute 拦截——不猜测
61
+ // 其他用户的 home,错展开比报错更糟)。
62
+ const isTilde = rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\");
63
+ const absolute = isTilde ? resolve(homedir(), rawPath.slice(2)) : rawPath;
64
+ if (!isAbsolute(absolute)) {
65
+ throw new Error(`registerDocument: path must be absolute (got "${rawPath}")`);
66
+ }
67
+ // 所有绝对路径统一 resolve 归一化(. / .. / 重复分隔符)——归一化不做的话,
68
+ // D:\a\.\b.md 与 D:\a\b.md 会被判成两个路径,同文件绕过 same-path
69
+ // supersede,留下双活跃指针。realpathSync 刻意不用:符号链接合并会改写
70
+ // agent 提交的路径字面量,指针的可读性比追符号链更重要。
71
+ const expanded = resolve(absolute);
72
+ let stat = null;
73
+ try { stat = statSync(expanded); } catch { /* missing/unreadable → null */ }
74
+ if (!stat?.isFile() || stat.size <= 0) {
75
+ throw new Error(`registerDocument: document file not found or empty: ${expanded}`);
76
+ }
77
+
78
+ // --- 2. evidence 求交(sleep.js 扫描集求交同款语义)---------------------
79
+ // 引用的原子条必须真实存在且未归档(归档行已被降级,不再为新鲜 document
80
+ // 背书)。红线 4 宽容形态:合法子集落库 + evidence_degraded 系统标记;
81
+ // 提供了 evidence 但全军覆没 = 捏造信号,整单拒绝,agent 修正后重试。
82
+ const wanted = [...new Set(
83
+ (Array.isArray(payload?.evidence) ? payload.evidence : [])
84
+ .map((id) => String(id ?? "").trim())
85
+ .filter(Boolean)
86
+ )];
87
+ // strictScope(tools 层传入,#170 复核项 4 同款):调用者 scope 看不见的
88
+ // 行按「不存在」处理——不得为跨 scope id 建立引用(存在性泄漏),与
89
+ // unknown/archived 同落 dropped,evidence_kept/dropped 计数保持诚实。
90
+ const hidden = new Set((Array.isArray(hiddenEvidenceIds) ? hiddenEvidenceIds : []).map((id) => String(id)));
91
+ const kept = [];
92
+ const dropped = [];
93
+ for (const id of wanted) {
94
+ const row = hidden.has(id) ? null : store.getById(id);
95
+ (row && !row.archived ? kept : dropped).push(id);
96
+ }
97
+ if (wanted.length > 0 && kept.length === 0) {
98
+ throw new Error(
99
+ `registerDocument: all ${wanted.length} evidence ids are unknown, archived or out of scope — fabricated evidence is rejected`
100
+ );
101
+ }
102
+
103
+ const title = String(payload?.title ?? "").trim();
104
+ if (!title) throw new Error("registerDocument: title is required");
105
+ const summary = String(payload?.summary ?? "").trim();
106
+ if (!summary) throw new Error("registerDocument: summary is required");
107
+
108
+ // --- 3. supersede 目标探测(三层)---------------------------------------
109
+ // 同 doc_path / 同标题 = agent 显式的「出新版」意图 → supersede;仅 vector
110
+ // 近重复而路径标题都不同 = 疑似重复外档 → 拒绝并指路。C2×C1 硬线:比对
111
+ // 不得越过矛盾裁决替 agent 二选一——改既有行还是归档后重注册,判断权留在
112
+ // agent 手里。去重键带 scope(scopeKeyOf):跨作用域永不互判(防泄漏,与
113
+ // saveWithDedupe 的 scopeMatches 同口径)。
114
+ const scopeMatches = (m) =>
115
+ scopeKeyOf(m.agent_scope) === scopeKeyOf(payload?.agent_scope) &&
116
+ scopeKeyOf(m.workspace_scope) === scopeKeyOf(payload?.workspace_scope) &&
117
+ scopeKeyOf(m.sensitivity) === scopeKeyOf(payload?.sensitivity);
118
+ const samePath = (a, b) => {
119
+ if (!a || !b) return false;
120
+ // Windows 文件系统大小写不敏感:比较前归一,否则 C:\a.md 与 c:\A.md
121
+ // 会被判成两个版本。
122
+ const norm = (p) => (process.platform === "win32" ? String(p).toLowerCase() : String(p));
123
+ return norm(a) === norm(b);
124
+ };
125
+ // 精确层(同路径/同标题)必须全量扫描、不允许窗口截断:>200 行活跃
126
+ // document 时,窗口外的同路径旧版漏检会留下双活跃版本——supersede 判定
127
+ // 是正确性要求,不是性能优化对象。vector 档比对保留 CANDIDATE_LIMIT
128
+ // (getEmbeddings + 两两 cosine 才是有界对象)。
129
+ const scopeMatched = store
130
+ .list({ type: "document", limit: null })
131
+ .filter((m) => scopeMatches(m));
132
+ const explicitTarget = scopeMatched.find((m) => samePath(m.doc_path, expanded))
133
+ ?? scopeMatched.find((m) => m.title.trim() === title);
134
+ if (!explicitTarget) {
135
+ // vector 档:embedder 不可用/向量缺失一律跳过——去重是增强不是写入依赖
136
+ // (findSessionDuplicate 同原则)。probe 用 title+summary,与行向量
137
+ // (同两段拼接 embed)同构。
138
+ const candidates = scopeMatched.slice(0, CANDIDATE_LIMIT);
139
+ try {
140
+ const vecs = store.getEmbeddings(candidates.map((m) => m.id));
141
+ if (vecs.size) {
142
+ const probe = await embedQuery([title, summary].filter(Boolean).join("\n"));
143
+ if (probe) {
144
+ let best = null;
145
+ for (const m of candidates) {
146
+ const v = vecs.get(m.id);
147
+ if (!v) continue; // 无向量的既有行不参与比对(无信号 = 不判定)
148
+ const sim = cosineSimilarity(probe, v);
149
+ if (sim >= DOCUMENT_MIN_SIM && (!best || sim > best.sim)) best = { m, sim };
150
+ }
151
+ if (best) {
152
+ throw new Error(
153
+ `registerDocument: summary is a near duplicate (C2 >= ${DOCUMENT_MIN_SIM}, sim ${best.sim.toFixed(3)}) ` +
154
+ `of document row ${best.m.id} "${best.m.title}" — update that row via memory_update, or archive it ` +
155
+ `before registering a separate document`
156
+ );
157
+ }
158
+ }
159
+ }
160
+ } catch (e) {
161
+ // 近重复拒绝是业务结果,原样上抛;embedder 故障吞掉回落(只做路径/标题层)。
162
+ if (e instanceof Error && e.message.startsWith("registerDocument:")) throw e;
163
+ }
164
+ }
165
+
166
+ // --- 4. 落库 + supersede 记账(同事务)----------------------------------
167
+ // 新摘要行:库里只存 summary + doc_path + evidence(+ scope 标注),旧文件
168
+ // 零触碰。supersede:旧行 content_history 存旧摘要(source=superseded)、
169
+ // 正文追加指向新行的注记、归档(降可及性可复活,无物理删除)。
170
+ const userTags = (Array.isArray(payload?.tags) ? payload.tags : [])
171
+ .map((t) => String(t ?? "").trim())
172
+ .filter(Boolean);
173
+ const tags = [...new Set([...userTags, ...(dropped.length ? ["evidence_degraded"] : [])])];
174
+ const result = transaction(() => {
175
+ // saveDocument 是存储层唯一 document 铸造口(通用 save/update/CAS 拒绝
176
+ // document——CodeRabbit 复核 #8/#882):registerDocument 传入的 doc_path
177
+ // 已过归一化与文件校验。
178
+ const created = store.saveDocument({
179
+ type: "document",
180
+ title,
181
+ content: summary,
182
+ tags,
183
+ importance: Number.isInteger(payload?.importance)
184
+ ? Math.min(5, Math.max(1, payload.importance))
185
+ : 3,
186
+ source: payload?.source ?? "tool",
187
+ evidence: kept,
188
+ doc_path: expanded,
189
+ // sensitivity 进 supersede 匹配键(scopeKeyOf 第三维):不落库的话,
190
+ // 首注册带 sensitivity、重注册同键会在匹配时判成两 scope,旧版漏检。
191
+ ...(payload?.sensitivity !== undefined ? { sensitivity: payload.sensitivity } : {}),
192
+ ...(payload?.agent_scope !== undefined
193
+ ? { agent_scope: payload.agent_scope, agent_scope_source: payload.agent_scope_source }
194
+ : {}),
195
+ ...(payload?.workspace_scope !== undefined
196
+ ? { workspace_scope: payload.workspace_scope, workspace_scope_source: payload.workspace_scope_source }
197
+ : {})
198
+ });
199
+ let superseded;
200
+ const loser = explicitTarget ? store.getById(explicitTarget.id) : null;
201
+ if (loser && !loser.archived) {
202
+ store.update(loser.id, {
203
+ content: `${loser.content}${supersededByPointer(created.id)}`,
204
+ content_history: pushContentHistory(loser, "superseded")
205
+ });
206
+ store.setArchived(loser.id, true);
207
+ superseded = store.getById(loser.id);
208
+ }
209
+ return { created, superseded };
210
+ });
211
+ finalize(result.superseded ? [result.created, result.superseded] : [result.created]);
212
+ return {
213
+ action: result.superseded ? "superseded" : "created",
214
+ memory: result.created,
215
+ ...(result.superseded ? { superseded: result.superseded } : {}),
216
+ evidence_kept: kept.length,
217
+ evidence_dropped: dropped.length,
218
+ degraded: dropped.length > 0
219
+ };
220
+ };
221
+ }
@@ -1,118 +1,118 @@
1
- // Cosine similarity of two equal-length numeric vectors. Returns 0 for empty
2
- // or mismatched-length inputs, and for zero-norm vectors (numerically stable).
3
- export function cosineSimilarity(a, b) {
4
- if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 0;
5
- let dot = 0, na = 0, nb = 0;
6
- for (let i = 0; i < a.length; i++) {
7
- dot += a[i] * b[i];
8
- na += a[i] * a[i];
9
- nb += b[i] * b[i];
10
- }
11
- if (na === 0 || nb === 0) return 0;
12
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
13
- }
14
-
15
- // K-Means clustering of numeric vectors. Returns one index array per cluster.
16
- // Uses k-means++ seeding and repairs empty clusters mid-iteration.
17
- export function kMeans(vectors, k, opts = {}) {
18
- const n = vectors.length;
19
- if (n === 0) return [];
20
- const { maxIter = 100, tol = 1e-4 } = opts ?? {};
21
- if (k <= 0 || k >= n) return [vectors.map((_, i) => i)];
22
-
23
- const dim = vectors[0].length;
24
- const randomIndex = () => Math.floor(Math.random() * n);
25
- const squaredDist = (v, c) => {
26
- let s = 0;
27
- for (let d = 0; d < dim; d++) s += (v[d] - c[d]) * (v[d] - c[d]);
28
- return s;
29
- };
30
-
31
- // k-means++ seeding: first centroid uniform-random, later centroids sampled
32
- // with probability proportional to squared distance from nearest centroid.
33
- const centroids = [[...vectors[randomIndex()]]];
34
- while (centroids.length < k) {
35
- const dist = new Array(n);
36
- let sum = 0;
37
- for (let i = 0; i < n; i++) {
38
- let best = Infinity;
39
- for (const c of centroids) best = Math.min(best, squaredDist(vectors[i], c));
40
- dist[i] = best;
41
- sum += best;
42
- }
43
- if (sum === 0) {
44
- // All points coincide with a centroid; fall back to uniform random.
45
- centroids.push([...vectors[randomIndex()]]);
46
- } else {
47
- let r = Math.random() * sum;
48
- let pick = 0;
49
- for (let i = 0; i < n; i++) {
50
- r -= dist[i];
51
- if (r <= 0) { pick = i; break; }
52
- }
53
- centroids.push([...vectors[pick]]);
54
- }
55
- }
56
-
57
- const assignment = new Array(n).fill(0);
58
- for (let iter = 0; iter < maxIter; iter++) {
59
- // Assign each point to its nearest centroid.
60
- let changed = false;
61
- for (let i = 0; i < n; i++) {
62
- let best = 0, bestD = Infinity;
63
- for (let j = 0; j < k; j++) {
64
- const d = squaredDist(vectors[i], centroids[j]);
65
- if (d < bestD) { bestD = d; best = j; }
66
- }
67
- if (assignment[i] !== best) { assignment[i] = best; changed = true; }
68
- }
69
- if (!changed) break; // stable assignment => converged.
70
-
71
- // Recompute centroids as cluster means; reseed empty clusters randomly.
72
- const sums = Array.from({ length: k }, () => new Array(dim).fill(0));
73
- const counts = new Array(k).fill(0);
74
- for (let i = 0; i < n; i++) {
75
- counts[assignment[i]]++;
76
- for (let d = 0; d < dim; d++) sums[assignment[i]][d] += vectors[i][d];
77
- }
78
- let maxMove = 0;
79
- for (let j = 0; j < k; j++) {
80
- if (counts[j] === 0) {
81
- centroids[j] = [...vectors[randomIndex()]];
82
- maxMove = Infinity; // never call this converged this round.
83
- } else {
84
- for (let d = 0; d < dim; d++) {
85
- const mean = sums[j][d] / counts[j];
86
- const move = mean - centroids[j][d];
87
- if (move * move > maxMove) maxMove = move * move;
88
- centroids[j][d] = mean;
89
- }
90
- }
91
- }
92
- if (Math.sqrt(maxMove) < tol) break; // centroids moved less than tol.
93
- }
94
-
95
- // Build clusters from final assignments.
96
- const clusters = Array.from({ length: k }, () => []);
97
- for (let i = 0; i < n; i++) clusters[assignment[i]].push(i);
98
- return clusters;
99
- }
100
-
101
- // Group memories by vector clustering, preserving original object references.
102
- export function clusterMemories(memories, vectors, k) {
103
- return kMeans(vectors, k).map((indices) => indices.map((i) => memories[i]));
104
- }
105
-
106
- // Find memory pairs that are highly similar yet could contradict each other.
107
- // Only same-type pairs count; each pair reported once, lower index first.
108
- export function findPotentialConflicts(memories, vectors, threshold = 0.85) {
109
- const pairs = [];
110
- for (let i = 0; i < memories.length; i++) {
111
- for (let j = i + 1; j < memories.length; j++) {
112
- if (memories[i].type !== memories[j].type) continue;
113
- const sim = cosineSimilarity(vectors[i], vectors[j]);
114
- if (sim > threshold) pairs.push({ a: memories[i], b: memories[j], similarity: sim });
115
- }
116
- }
117
- return pairs;
118
- }
1
+ // Cosine similarity of two equal-length numeric vectors. Returns 0 for empty
2
+ // or mismatched-length inputs, and for zero-norm vectors (numerically stable).
3
+ export function cosineSimilarity(a, b) {
4
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 0;
5
+ let dot = 0, na = 0, nb = 0;
6
+ for (let i = 0; i < a.length; i++) {
7
+ dot += a[i] * b[i];
8
+ na += a[i] * a[i];
9
+ nb += b[i] * b[i];
10
+ }
11
+ if (na === 0 || nb === 0) return 0;
12
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
13
+ }
14
+
15
+ // K-Means clustering of numeric vectors. Returns one index array per cluster.
16
+ // Uses k-means++ seeding and repairs empty clusters mid-iteration.
17
+ export function kMeans(vectors, k, opts = {}) {
18
+ const n = vectors.length;
19
+ if (n === 0) return [];
20
+ const { maxIter = 100, tol = 1e-4 } = opts ?? {};
21
+ if (k <= 0 || k >= n) return [vectors.map((_, i) => i)];
22
+
23
+ const dim = vectors[0].length;
24
+ const randomIndex = () => Math.floor(Math.random() * n);
25
+ const squaredDist = (v, c) => {
26
+ let s = 0;
27
+ for (let d = 0; d < dim; d++) s += (v[d] - c[d]) * (v[d] - c[d]);
28
+ return s;
29
+ };
30
+
31
+ // k-means++ seeding: first centroid uniform-random, later centroids sampled
32
+ // with probability proportional to squared distance from nearest centroid.
33
+ const centroids = [[...vectors[randomIndex()]]];
34
+ while (centroids.length < k) {
35
+ const dist = new Array(n);
36
+ let sum = 0;
37
+ for (let i = 0; i < n; i++) {
38
+ let best = Infinity;
39
+ for (const c of centroids) best = Math.min(best, squaredDist(vectors[i], c));
40
+ dist[i] = best;
41
+ sum += best;
42
+ }
43
+ if (sum === 0) {
44
+ // All points coincide with a centroid; fall back to uniform random.
45
+ centroids.push([...vectors[randomIndex()]]);
46
+ } else {
47
+ let r = Math.random() * sum;
48
+ let pick = 0;
49
+ for (let i = 0; i < n; i++) {
50
+ r -= dist[i];
51
+ if (r <= 0) { pick = i; break; }
52
+ }
53
+ centroids.push([...vectors[pick]]);
54
+ }
55
+ }
56
+
57
+ const assignment = new Array(n).fill(0);
58
+ for (let iter = 0; iter < maxIter; iter++) {
59
+ // Assign each point to its nearest centroid.
60
+ let changed = false;
61
+ for (let i = 0; i < n; i++) {
62
+ let best = 0, bestD = Infinity;
63
+ for (let j = 0; j < k; j++) {
64
+ const d = squaredDist(vectors[i], centroids[j]);
65
+ if (d < bestD) { bestD = d; best = j; }
66
+ }
67
+ if (assignment[i] !== best) { assignment[i] = best; changed = true; }
68
+ }
69
+ if (!changed) break; // stable assignment => converged.
70
+
71
+ // Recompute centroids as cluster means; reseed empty clusters randomly.
72
+ const sums = Array.from({ length: k }, () => new Array(dim).fill(0));
73
+ const counts = new Array(k).fill(0);
74
+ for (let i = 0; i < n; i++) {
75
+ counts[assignment[i]]++;
76
+ for (let d = 0; d < dim; d++) sums[assignment[i]][d] += vectors[i][d];
77
+ }
78
+ let maxMove = 0;
79
+ for (let j = 0; j < k; j++) {
80
+ if (counts[j] === 0) {
81
+ centroids[j] = [...vectors[randomIndex()]];
82
+ maxMove = Infinity; // never call this converged this round.
83
+ } else {
84
+ for (let d = 0; d < dim; d++) {
85
+ const mean = sums[j][d] / counts[j];
86
+ const move = mean - centroids[j][d];
87
+ if (move * move > maxMove) maxMove = move * move;
88
+ centroids[j][d] = mean;
89
+ }
90
+ }
91
+ }
92
+ if (Math.sqrt(maxMove) < tol) break; // centroids moved less than tol.
93
+ }
94
+
95
+ // Build clusters from final assignments.
96
+ const clusters = Array.from({ length: k }, () => []);
97
+ for (let i = 0; i < n; i++) clusters[assignment[i]].push(i);
98
+ return clusters;
99
+ }
100
+
101
+ // Group memories by vector clustering, preserving original object references.
102
+ export function clusterMemories(memories, vectors, k) {
103
+ return kMeans(vectors, k).map((indices) => indices.map((i) => memories[i]));
104
+ }
105
+
106
+ // Find memory pairs that are highly similar yet could contradict each other.
107
+ // Only same-type pairs count; each pair reported once, lower index first.
108
+ export function findPotentialConflicts(memories, vectors, threshold = 0.85) {
109
+ const pairs = [];
110
+ for (let i = 0; i < memories.length; i++) {
111
+ for (let j = i + 1; j < memories.length; j++) {
112
+ if (memories[i].type !== memories[j].type) continue;
113
+ const sim = cosineSimilarity(vectors[i], vectors[j]);
114
+ if (sim > threshold) pairs.push({ a: memories[i], b: memories[j], similarity: sim });
115
+ }
116
+ }
117
+ return pairs;
118
+ }