@modusensus/dsh-mneme 0.8.14 → 0.8.15

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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +655 -654
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +689 -689
  5. package/bin/dsh-mneme-mcp.mjs +510 -510
  6. package/bin/dsh-mneme-serve.mjs +151 -151
  7. package/cordis.patch.yml +21 -21
  8. package/docs/CONFIGURATION.md +263 -0
  9. package/docs/CONTINUITY.md +124 -0
  10. package/docs/DAEMON.md +46 -0
  11. package/docs/ENTITIES.md +245 -0
  12. package/docs/LOCAL_MODEL.md +222 -0
  13. package/docs/SEMANTIC.md +256 -0
  14. package/docs/SLEEP.md +164 -0
  15. package/docs/STORAGE.md +89 -0
  16. package/lib/api-standalone.js +673 -673
  17. package/lib/api.js +1437 -1433
  18. package/lib/bootstrap.js +211 -211
  19. package/lib/client.js +5444 -5295
  20. package/lib/commands.js +95 -95
  21. package/lib/config.js +880 -843
  22. package/lib/content-hash.js +54 -54
  23. package/lib/continuity.js +220 -220
  24. package/lib/document-index.js +94 -94
  25. package/lib/document.js +303 -303
  26. package/lib/dream/clustering.js +118 -118
  27. package/lib/dream/decisions.js +674 -674
  28. package/lib/dream/narratives.js +44 -44
  29. package/lib/dream/sleep.js +819 -819
  30. package/lib/dream.js +1501 -1501
  31. package/lib/embedding.js +177 -177
  32. package/lib/entities/extractor.js +252 -252
  33. package/lib/graph/anchoring.js +179 -179
  34. package/lib/guide.js +74 -74
  35. package/lib/heat.js +144 -144
  36. package/lib/hot-memory.js +46 -46
  37. package/lib/index.js +535 -503
  38. package/lib/inject.js +464 -450
  39. package/lib/lang.js +248 -229
  40. package/lib/local-embedder.js +358 -358
  41. package/lib/maintenance.js +142 -142
  42. package/lib/mirror.js +330 -330
  43. package/lib/organize.js +450 -450
  44. package/lib/peak-hours.js +117 -117
  45. package/lib/placement.js +13 -0
  46. package/lib/pre-inject-gate.js +340 -0
  47. package/lib/quality-filter.js +126 -126
  48. package/lib/recall-stats.js +175 -175
  49. package/lib/reranker.js +269 -269
  50. package/lib/runtime/adopt.js +176 -176
  51. package/lib/runtime/closure.js +172 -172
  52. package/lib/runtime/download.js +247 -247
  53. package/lib/runtime/layout.js +265 -265
  54. package/lib/runtime/loader.js +252 -252
  55. package/lib/runtime/model-fetch.js +361 -361
  56. package/lib/runtime/provision.js +237 -237
  57. package/lib/runtime/tarball.js +130 -130
  58. package/lib/runtime/verify.js +223 -204
  59. package/lib/scope.js +146 -146
  60. package/lib/search/adaptive.js +60 -60
  61. package/lib/search/bm25.js +96 -96
  62. package/lib/semantic.js +229 -229
  63. package/lib/sensitive-scan.js +209 -209
  64. package/lib/serve.js +215 -215
  65. package/lib/service.js +2663 -2605
  66. package/lib/settings.js +517 -511
  67. package/lib/store.js +3007 -3007
  68. package/lib/summarize.js +891 -856
  69. package/lib/tools.js +743 -737
  70. package/lib/vector-index.js +110 -110
  71. package/lib/version-check.js +122 -122
  72. package/lib/write-admission.js +402 -402
  73. package/locale/en.json +6 -6
  74. package/locale/zh.json +6 -6
  75. package/package.json +118 -118
  76. package/runtime-manifest.json +526 -526
  77. package/scripts/benchmark-embed.js +201 -201
  78. package/scripts/benchmark-recall.js +249 -249
  79. package/scripts/benchmark-rerank.js +166 -166
  80. package/scripts/build-runtime-manifest.mjs +224 -224
  81. package/scripts/changelog-prep.mjs +36 -36
  82. package/scripts/check-sync.js +42 -42
  83. package/scripts/e2e-dsh.js +220 -220
  84. package/scripts/mneme-runtime.mjs +277 -277
  85. package/scripts/stress-dsh.js +255 -255
  86. package/scripts/sync-lib.js +47 -47
  87. package/scripts/sync-test-badge.mjs +69 -69
  88. package/scripts/test-count-sync.mjs +44 -44
  89. package/src/api-standalone.js +673 -673
  90. package/src/api.js +1437 -1433
  91. package/src/bootstrap.js +211 -211
  92. package/src/commands.js +95 -95
  93. package/src/config.js +880 -843
  94. package/src/content-hash.js +54 -54
  95. package/src/continuity.js +220 -220
  96. package/src/document-index.js +94 -94
  97. package/src/document.js +303 -303
  98. package/src/dream/clustering.js +118 -118
  99. package/src/dream/decisions.js +674 -674
  100. package/src/dream/narratives.js +44 -44
  101. package/src/dream/sleep.js +819 -819
  102. package/src/dream.js +1501 -1501
  103. package/src/embedding.js +177 -177
  104. package/src/entities/extractor.js +252 -252
  105. package/src/graph/anchoring.js +179 -179
  106. package/src/guide.js +74 -74
  107. package/src/heat.js +144 -144
  108. package/src/hot-memory.js +46 -46
  109. package/src/index.js +535 -503
  110. package/src/inject.js +464 -450
  111. package/src/lang.js +248 -229
  112. package/src/local-embedder.js +358 -358
  113. package/src/maintenance.js +142 -142
  114. package/src/mirror.js +330 -330
  115. package/src/organize.js +450 -450
  116. package/src/peak-hours.js +117 -117
  117. package/src/placement.js +13 -0
  118. package/src/pre-inject-gate.js +340 -0
  119. package/src/quality-filter.js +126 -126
  120. package/src/recall-stats.js +175 -175
  121. package/src/reranker.js +269 -269
  122. package/src/runtime/adopt.js +176 -176
  123. package/src/runtime/closure.js +172 -172
  124. package/src/runtime/download.js +247 -247
  125. package/src/runtime/layout.js +265 -265
  126. package/src/runtime/loader.js +252 -252
  127. package/src/runtime/model-fetch.js +361 -361
  128. package/src/runtime/provision.js +237 -237
  129. package/src/runtime/tarball.js +130 -130
  130. package/src/runtime/verify.js +223 -204
  131. package/src/scope.js +146 -146
  132. package/src/search/adaptive.js +60 -60
  133. package/src/search/bm25.js +96 -96
  134. package/src/semantic.js +229 -229
  135. package/src/sensitive-scan.js +209 -209
  136. package/src/serve.js +215 -215
  137. package/src/service.js +2663 -2605
  138. package/src/settings.js +517 -511
  139. package/src/store.js +3007 -3007
  140. package/src/summarize.js +891 -856
  141. package/src/tools.js +743 -737
  142. package/src/vector-index.js +110 -110
  143. package/src/version-check.js +122 -122
  144. package/src/write-admission.js +402 -402
  145. package/test/adaptive-inject-budget.test.js +85 -85
  146. package/test/api-routes-gaps.test.js +218 -218
  147. package/test/api.test.js +1702 -1690
  148. package/test/audit.test.js +448 -448
  149. package/test/baseline-persist.test.js +248 -248
  150. package/test/benchmark.test.js +94 -94
  151. package/test/bootstrap.test.js +220 -220
  152. package/test/changelog-prep.test.js +60 -60
  153. package/test/client.test.js +1230 -1169
  154. package/test/clustering.test.js +100 -100
  155. package/test/commands.test.js +106 -106
  156. package/test/config.test.js +66 -66
  157. package/test/conflict-freeze.test.js +300 -300
  158. package/test/content-hash.test.js +204 -204
  159. package/test/continuity.test.js +333 -333
  160. package/test/document-dir.test.js +233 -233
  161. package/test/document.test.js +671 -671
  162. package/test/dream-archive-guardrail.test.js +116 -116
  163. package/test/dream-candidate-hybrid.test.js +281 -281
  164. package/test/dream-coverage-degrade.test.js +86 -86
  165. package/test/dream-failure-backoff.test.js +173 -173
  166. package/test/dream-peak-hours.test.js +194 -194
  167. package/test/dream.test.js +1385 -1383
  168. package/test/embedding-ready.test.js +196 -196
  169. package/test/entities.test.js +627 -547
  170. package/test/entity-recall.test.js +179 -179
  171. package/test/epistemic.test.js +325 -325
  172. package/test/fnew-0112.test.js +311 -311
  173. package/test/fnew-03.test.js +422 -422
  174. package/test/graph-anchoring.test.js +184 -184
  175. package/test/graph-api.test.js +175 -175
  176. package/test/graph-inject-budget.test.js +103 -103
  177. package/test/graph-passive-confirm.test.js +91 -91
  178. package/test/graph-weight-evolution.test.js +104 -104
  179. package/test/heat.test.js +175 -175
  180. package/test/helpers/dream-mock.js +82 -82
  181. package/test/helpers/peer-worker.mjs +25 -25
  182. package/test/helpers/runtime-source.js +60 -60
  183. package/test/helpers/tar-builder.js +80 -80
  184. package/test/helpers/write-admission-samples.js +308 -308
  185. package/test/hot-memory.test.js +145 -145
  186. package/test/inject-content-cap.test.js +91 -91
  187. package/test/inject-first-round-bm25.test.js +89 -89
  188. package/test/inject-heat.test.js +77 -77
  189. package/test/inject-parent-gate.test.js +106 -106
  190. package/test/inject-pools.test.js +194 -194
  191. package/test/inject-recall.test.js +105 -105
  192. package/test/inject-rotation.test.js +161 -161
  193. package/test/inject.test.js +420 -420
  194. package/test/lang.test.js +87 -87
  195. package/test/lastrunat-persist.test.js +101 -101
  196. package/test/lib-smoke.test.js +96 -96
  197. package/test/llm-audit.test.js +598 -598
  198. package/test/llm-message-source.test.js +207 -207
  199. package/test/local-embedder.test.js +292 -292
  200. package/test/maintenance.test.js +391 -391
  201. package/test/mcp-stdio.test.js +385 -385
  202. package/test/mirror-dirty.test.js +424 -424
  203. package/test/mirror-edit-digest.test.js +187 -187
  204. package/test/mirror-generation.test.js +499 -499
  205. package/test/mirror-types.test.js +208 -208
  206. package/test/mirror.test.js +249 -249
  207. package/test/narrative-bars.test.js +162 -162
  208. package/test/organize.test.js +418 -418
  209. package/test/peer-blockers.test.js +193 -193
  210. package/test/placement.test.js +36 -0
  211. package/test/policy-epoch.test.js +259 -259
  212. package/test/pre-inject-gate.test.js +434 -0
  213. package/test/quality-filter.test.js +186 -186
  214. package/test/reasoning-effort.test.js +752 -752
  215. package/test/recall-anti-update.test.js +82 -82
  216. package/test/recall-evals.test.js +253 -253
  217. package/test/recall-layer.test.js +326 -326
  218. package/test/recall-stats.test.js +189 -189
  219. package/test/receipt-chain.test.js +451 -451
  220. package/test/reflection.test.js +226 -226
  221. package/test/reindex-backfill.test.js +62 -62
  222. package/test/reranker-single-logit.test.js +73 -73
  223. package/test/reranker.test.js +197 -197
  224. package/test/resident-status.test.js +83 -83
  225. package/test/runtime-adopt.test.js +206 -206
  226. package/test/runtime-cli.test.js +89 -89
  227. package/test/runtime-closure.test.js +193 -193
  228. package/test/runtime-download.test.js +302 -302
  229. package/test/runtime-layout.test.js +163 -163
  230. package/test/runtime-loader.test.js +255 -255
  231. package/test/runtime-manifest.test.js +159 -159
  232. package/test/runtime-model-fetch.test.js +348 -348
  233. package/test/runtime-provision.test.js +101 -101
  234. package/test/runtime-tarball.test.js +81 -81
  235. package/test/runtime-verify.test.js +319 -279
  236. package/test/scope-candidates.test.js +239 -239
  237. package/test/scope-explicit.test.js +452 -452
  238. package/test/scope-inject-soft.test.js +106 -106
  239. package/test/scope-retrieval.test.js +197 -197
  240. package/test/scope-storage.test.js +255 -255
  241. package/test/scope-strict.test.js +367 -367
  242. package/test/search-fusion.test.js +90 -90
  243. package/test/semantic.test.js +124 -124
  244. package/test/sensitive-scan.test.js +207 -207
  245. package/test/serve-bin.test.js +121 -121
  246. package/test/serve.test.js +196 -196
  247. package/test/service-search.test.js +174 -174
  248. package/test/service.test.js +405 -405
  249. package/test/settings.test.js +261 -261
  250. package/test/skip-invalid-restore.test.js +262 -262
  251. package/test/sleep-action-set.test.js +422 -422
  252. package/test/sleep-heat.test.js +126 -126
  253. package/test/sleep-scheduler-rearm.test.js +89 -89
  254. package/test/sleep.test.js +527 -527
  255. package/test/standalone-api.test.js +751 -751
  256. package/test/store-vector-cache.test.js +98 -98
  257. package/test/store.test.js +396 -396
  258. package/test/stress.test.js +209 -209
  259. package/test/summarize-occurred-at.test.js +127 -0
  260. package/test/summarize-reasoning-effort.test.js +189 -189
  261. package/test/summarize.test.js +1465 -1465
  262. package/test/supersede-state-tracking.test.js +394 -0
  263. package/test/test-count-sync.test.js +60 -60
  264. package/test/tools-dto-schema-matrix.test.js +325 -325
  265. package/test/tools-memory-get-schema.test.js +65 -65
  266. package/test/tools.test.js +428 -374
  267. package/test/updated-at-semantics.test.js +113 -113
  268. package/test/vector-index.test.js +221 -221
  269. package/test/version-check.test.js +152 -152
  270. package/test/webServer-optional.test.js +82 -82
  271. package/test/write-admission.test.js +668 -668
@@ -1,325 +1,325 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { DatabaseSync } from "node:sqlite";
4
- import { mkdtempSync, rmSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { createStore } from "../src/store.js";
8
- import { createService } from "../src/service.js";
9
- import { createVectorIndex } from "../src/vector-index.js";
10
- import { createInjector } from "../src/inject.js";
11
- import { createSettings } from "../src/settings.js";
12
- import { applyDecisions } from "../src/dream.js";
13
-
14
- // ---------------------------------------------------------------- schema / legacy compat
15
-
16
- test("default epistemic_status is subjective (legacy-compatible)", () => {
17
- const store = createStore(":memory:");
18
- const saved = store.save({ type: "preference", title: "语言", content: "用户用中文交流" });
19
- assert.equal(saved.epistemic_status, "subjective", "no signal -> default subjective");
20
- assert.equal(store.getById(saved.id).epistemic_status, "subjective");
21
- store.close();
22
- });
23
-
24
- test("legacy DB without the column is migrated, old rows read back as subjective", () => {
25
- const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-epistemic-"));
26
- const dbPath = join(dir, "memory.db");
27
- try {
28
- const old = new DatabaseSync(dbPath);
29
- old.exec(`
30
- CREATE TABLE memories (
31
- id TEXT PRIMARY KEY, type TEXT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL,
32
- tags TEXT NOT NULL DEFAULT '[]', importance INTEGER NOT NULL DEFAULT 3, forgotten INTEGER NOT NULL DEFAULT 0,
33
- source TEXT, embedding TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
34
- );
35
- INSERT INTO memories (id, type, title, content, tags, importance, forgotten, created_at, updated_at)
36
- VALUES ('legacy', 'preference', '旧偏好', '用户以前说过', '[]', 3, 0, 't', 't');
37
- `);
38
- old.close();
39
-
40
- const store = createStore(dbPath);
41
- const cols = store.db.prepare("PRAGMA table_info(memories)").all().map((c) => c.name);
42
- assert.ok(cols.includes("epistemic_status"), "column added by migration");
43
- const legacy = store.getById("legacy");
44
- assert.equal(legacy.title, "旧偏好", "legacy row preserved");
45
- assert.equal(legacy.epistemic_status, "subjective", "legacy row defaults to subjective");
46
- // New writes on the migrated DB still work with the column.
47
- const saved = store.save({ type: "preference", title: "新", content: "实测结果可用" });
48
- assert.equal(saved.epistemic_status, "observation");
49
- store.close();
50
- } finally {
51
- rmSync(dir, { recursive: true, force: true });
52
- }
53
- });
54
-
55
- // ---------------------------------------------------------------- write inference
56
-
57
- test("content-based inference: subjective markers -> subjective", () => {
58
- const store = createStore(":memory:");
59
- const a = store.save({ type: "preference", title: "天气", content: "我推测明天可能会下雨" });
60
- const b = store.save({ type: "preference", title: "口味", content: "我觉得用户喜欢甜的" });
61
- assert.equal(a.epistemic_status, "subjective");
62
- assert.equal(b.epistemic_status, "subjective");
63
- store.close();
64
- });
65
-
66
- test("content-based inference: observation markers -> observation", () => {
67
- const store = createStore(":memory:");
68
- const a = store.save({ type: "project", title: "温度", content: "实测温度为35度,数据显示稳定" });
69
- const b = store.save({ type: "project", title: "观察", content: "观察到用户总是先点保存" });
70
- assert.equal(a.epistemic_status, "observation");
71
- assert.equal(b.epistemic_status, "observation");
72
- store.close();
73
- });
74
-
75
- test("content-based inference: inference markers -> inferred", () => {
76
- const store = createStore(":memory:");
77
- const a = store.save({ type: "decision", title: "喜好", content: "根据历史记录推断他喜欢猫" });
78
- const b = store.save({ type: "decision", title: "趋势", content: "综上,结论是可推断的" });
79
- assert.equal(a.epistemic_status, "inferred");
80
- assert.equal(b.epistemic_status, "inferred");
81
- store.close();
82
- });
83
-
84
- test("AI-generated types (summary/pattern) are always inferred", () => {
85
- const store = createStore(":memory:");
86
- const s = store.save({ type: "summary", title: "总览", content: "实测数据汇总" });
87
- const p = store.save({ type: "pattern", title: "模式", content: "用户反复这样操作" });
88
- assert.equal(s.epistemic_status, "inferred");
89
- assert.equal(p.epistemic_status, "inferred");
90
- store.close();
91
- });
92
-
93
- test("explicit epistemic_status wins over content inference", () => {
94
- const store = createStore(":memory:");
95
- const saved = store.save({ type: "preference", title: "x", content: "我觉得可能", epistemic_status: "observation" });
96
- assert.equal(saved.epistemic_status, "observation", "explicit value respected");
97
- store.close();
98
- });
99
-
100
- test("update re-infers when content changes, keeps status otherwise", () => {
101
- const store = createStore(":memory:");
102
- const saved = store.save({ type: "preference", title: "x", content: "我觉得可能" });
103
- assert.equal(saved.epistemic_status, "subjective");
104
- // content unchanged -> status kept
105
- const untouched = store.update(saved.id, { title: "y" });
106
- assert.equal(untouched.epistemic_status, "subjective");
107
- // content changed to observation -> status re-inferred
108
- const reInferred = store.update(saved.id, { content: "实测结果显示没问题" });
109
- assert.equal(reInferred.epistemic_status, "observation");
110
- store.close();
111
- });
112
-
113
- test("update re-infers when TYPE changes to summary/pattern (regression)", () => {
114
- const store = createStore(":memory:");
115
- const saved = store.save({ type: "project", title: "x", content: "我觉得可能" });
116
- assert.equal(saved.epistemic_status, "subjective");
117
- // type -> summary: summary/pattern are always inferred, so status must flip
118
- const asSummary = store.update(saved.id, { type: "summary" });
119
- assert.equal(asSummary.epistemic_status, "inferred");
120
- store.close();
121
- });
122
-
123
- // ---------------------------------------------------------------- retrieval ranking
124
-
125
- const embedder = {
126
- embedSingle: async () => [1, 0, 0],
127
- embed: async () => [1, 0, 0],
128
- schedule: () => {},
129
- modelHash: "mock#1",
130
- dimension: 3
131
- };
132
-
133
- test("retrieval ranking: switch off leaves scores and order untouched", async () => {
134
- const store = createStore(":memory:");
135
- const service = createService({ store, mirror: null, config: {} });
136
- const a = service.saveWithDedupe({ type: "preference", title: "猫A", content: "我觉得猫很可爱", importance: 5 });
137
- const b = service.saveWithDedupe({ type: "preference", title: "猫B", content: "实测猫很可爱", importance: 3 });
138
- assert.equal(a.memory.epistemic_status, "subjective");
139
- assert.equal(b.memory.epistemic_status, "observation");
140
-
141
- const rows = await service.searchMemories("猫", { mode: "keyword", topK: 10 });
142
- assert.equal(rows[0].id, a.memory.id, "subjective first under default importance order");
143
- assert.equal(rows[0].score, 1.0, "subjective keeps its raw score (1.0) when off");
144
- assert.equal(rows[1].score, 0.8, "observation keeps its raw score (0.8) when off");
145
- store.close();
146
- });
147
-
148
- test("retrieval ranking: switch on re-weights and re-orders mixed recall", async () => {
149
- const store = createStore(":memory:");
150
- const service = createService({ store, mirror: null, config: { trustEpistemicWeighting: true } });
151
- const vi = createVectorIndex({ store });
152
- service.setEmbedder(embedder);
153
- service.setVectorIndex(vi);
154
- const subj = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "我觉得量子计算很难", importance: 5 });
155
- const obs = service.saveWithDedupe({ type: "preference", title: "量子计算实践", content: "实测量子计算简单", importance: 3 });
156
- vi.saveEmbedding(subj.memory.id, [1, 0, 0]);
157
- vi.saveEmbedding(obs.memory.id, [1, 0, 0]);
158
- assert.equal(subj.memory.epistemic_status, "subjective");
159
- assert.equal(obs.memory.epistemic_status, "observation");
160
-
161
- // hybrid blend (default 0.6/0.4): subjective 1.0*0.6+1.0*0.4=1.0;
162
- // observation 1.0*0.6+0.8*0.4=0.92. After weighting: 1.0*0.7=0.7 vs 0.92*1.0=0.92.
163
- const rows = await service.searchMemories("量子计算", { mode: "hybrid", topK: 10 });
164
- const byId = new Map(rows.map((r) => [r.id, r]));
165
- assert.equal(byId.get(subj.memory.id).score, 0.7, "subjective weighted to 0.7");
166
- assert.equal(byId.get(obs.memory.id).score, 0.92, "observation weighted to 0.92");
167
- assert.equal(rows[0].id, obs.memory.id, "observation outranks subjective when weighting is on");
168
- store.close();
169
- });
170
-
171
- // ---------------------------------------------------------------- injection marking
172
-
173
- function setupInjector(over = {}) {
174
- const store = createStore(":memory:");
175
- const service = createService({ store, mirror: null, config: {} });
176
- const settings = createSettings(store.db);
177
- const contexts = [];
178
- const ctx = {
179
- systemPrompt: {
180
- context(def) {
181
- contexts.push(def);
182
- return () => {};
183
- }
184
- }
185
- };
186
- const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
187
- createInjector(ctx, service, settings, config);
188
- return { store, service, contexts };
189
- }
190
-
191
- test("injection marks observation memories [verified] only when enabled", () => {
192
- const { service, contexts } = setupInjector({ trustEpistemicWeighting: true });
193
- service.saveWithDedupe({ type: "preference", title: "偏好实测", content: "实测用户喜欢用命令行", importance: 5 });
194
- service.saveWithDedupe({ type: "preference", title: "偏好推测", content: "我觉得用户可能喜欢GUI", importance: 5 });
195
- const text = contexts[0].text({});
196
- assert.ok(text.includes("[verified] 偏好实测"), "observation memory is flagged");
197
- assert.ok(!text.includes("[verified] 偏好推测"), "subjective memory is not flagged");
198
- });
199
-
200
- test("injection never adds [verified] when the switch is off", () => {
201
- const { service, contexts } = setupInjector({});
202
- service.saveWithDedupe({ type: "preference", title: "偏好实测", content: "实测用户喜欢用命令行", importance: 5 });
203
- const text = contexts[0].text({});
204
- assert.ok(text.includes("偏好实测"), "memory still injected");
205
- assert.ok(!text.includes("[verified]"), "no verified marker when off");
206
- });
207
-
208
- // ---------------------------------------------------------------- decision priority
209
-
210
- test("merge keepSource prefers observation when enabled", () => {
211
- const store = createStore(":memory:");
212
- const service = createService({ store, mirror: null, config: {} });
213
- const config = { trustEpistemicWeighting: true };
214
- const a = service.saveWithDedupe({ type: "project", title: "插件甲", content: "我觉得应该用A方案", importance: 3 });
215
- const b = service.saveWithDedupe({ type: "project", title: "插件乙", content: "实测结果显示B方案更好", importance: 4 });
216
- assert.equal(a.memory.epistemic_status, "subjective");
217
- assert.equal(b.memory.epistemic_status, "observation");
218
-
219
- const { committed } = applyDecisions(
220
- [{ action: "merge", ids: [a.memory.id, b.memory.id], title: "插件总览", content: "合并", keepSource: a.memory.id }],
221
- service, null, null, config
222
- );
223
- assert.equal(committed[0].keepSource, b.memory.id, "keepSource switched to the observation memory");
224
- assert.equal(store.getById(b.memory.id).archived, false, "observation keeper stays live");
225
- assert.equal(store.getById(a.memory.id).archived, true, "subjective source archived");
226
- });
227
-
228
- test("merge keepSource keeps the LLM choice when disabled", () => {
229
- const store = createStore(":memory:");
230
- const service = createService({ store, mirror: null, config: {} });
231
- const a = service.saveWithDedupe({ type: "project", title: "插件甲", content: "我觉得应该用A方案", importance: 3 });
232
- const b = service.saveWithDedupe({ type: "project", title: "插件乙", content: "实测结果显示B方案更好", importance: 4 });
233
- const { committed } = applyDecisions(
234
- [{ action: "merge", ids: [a.memory.id, b.memory.id], title: "插件总览", content: "合并", keepSource: a.memory.id }],
235
- service
236
- );
237
- assert.equal(committed[0].keepSource, a.memory.id, "LLM keepSource respected when off");
238
- assert.equal(store.getById(a.memory.id).archived, false);
239
- assert.equal(store.getById(b.memory.id).archived, true);
240
- });
241
-
242
- test("merge never promotes an ARCHIVED observation to keeper when enabled (regression)", () => {
243
- const store = createStore(":memory:");
244
- const service = createService({ store, mirror: null, config: {} });
245
- const config = { trustEpistemicWeighting: true };
246
- // a = live subjective (LLM's keepSource), b = live observation,
247
- // c = ARCHIVED observation. Pre-fix pickBestKeeper would pick c (highest
248
- // priority, no archived check) as keeper, hit the archived-keeper guard in
249
- // applyMerge and silently drop the merge — the live b never merges.
250
- const a = service.saveWithDedupe({ type: "project", title: "甲", content: "我觉得A方案", importance: 3 });
251
- const b = service.saveWithDedupe({ type: "project", title: "乙", content: "实测B方案更好", importance: 4 });
252
- const c = service.saveWithDedupe({ type: "project", title: "丙", content: "实测C方案最好", importance: 4 });
253
- service.setArchived(c.memory.id, true);
254
-
255
- const { committed } = applyDecisions(
256
- [{ action: "merge", ids: [a.memory.id, b.memory.id, c.memory.id], title: "合并", content: "合并", keepSource: a.memory.id }],
257
- service, null, null, config
258
- );
259
- // c (archived) must NOT be promoted; the live observation b becomes keeper,
260
- // and the merge actually lands.
261
- assert.equal(committed.length, 1, "merge actually committed (not silently skipped)");
262
- assert.equal(committed[0].keepSource, b.memory.id, "live observation promoted over archived one");
263
- assert.equal(store.getById(b.memory.id).archived, false, "observation keeper stays live");
264
- assert.equal(store.getById(a.memory.id).archived, true, "subjective source archived");
265
- assert.equal(store.getById(c.memory.id).archived, true, "archived c stays archived");
266
- });
267
-
268
- test("conflict resolution prefers the observation side as winner when enabled", () => {
269
- const store = createStore(":memory:");
270
- const service = createService({ store, mirror: null, config: {} });
271
- const config = { trustEpistemicWeighting: true };
272
- const w = service.saveWithDedupe({ type: "decision", title: "截止1", content: "我觉得截止是8月20日", importance: 4 });
273
- const l = service.saveWithDedupe({ type: "decision", title: "截止2", content: "实测截止是8月15日", importance: 4 });
274
- assert.equal(w.memory.epistemic_status, "subjective");
275
- assert.equal(l.memory.epistemic_status, "observation");
276
-
277
- const { committed } = applyDecisions(
278
- [{ action: "conflict", winner: w.memory.id, loser: l.memory.id, reason: "日期更新" }],
279
- service, null, null, config
280
- );
281
- assert.equal(committed[0].winner, l.memory.id, "winner swapped to the observation memory");
282
- assert.equal(store.getById(l.memory.id).archived, false, "observation winner stays live");
283
- assert.equal(store.getById(w.memory.id).archived, true, "subjective loser archived");
284
- });
285
-
286
- test("conflict resolution keeps the LLM winner when disabled", () => {
287
- const store = createStore(":memory:");
288
- const service = createService({ store, mirror: null, config: {} });
289
- const w = service.saveWithDedupe({ type: "decision", title: "截止1", content: "我觉得截止是8月20日", importance: 4 });
290
- const l = service.saveWithDedupe({ type: "decision", title: "截止2", content: "实测截止是8月15日", importance: 4 });
291
- const { committed } = applyDecisions(
292
- [{ action: "conflict", winner: w.memory.id, loser: l.memory.id, reason: "日期更新" }],
293
- service
294
- );
295
- assert.equal(committed[0].winner, w.memory.id, "LLM winner respected when off");
296
- assert.equal(store.getById(w.memory.id).archived, false);
297
- assert.equal(store.getById(l.memory.id).archived, true);
298
- });
299
-
300
- // E2 四臂实验(Kaggle v11)当场抓到的回归:saveWithDedupe 此前丢弃显式
301
- // epistemic_status,store 回退内容推断,英文/无标记内容一律 subjective——
302
- // EPISTEMIC_WEIGHTS 对整池均匀 ×0.7,trustEpistemicWeighting 的重排整体空转
303
- // (R+ 臂与 A 臂注入序逐条相同)。显式值必须穿透创建路径;undefined 保持推断。
304
- test("saveWithDedupe forwards explicit epistemic_status (rerank needs non-uniform weights)", async () => {
305
- const store = createStore(":memory:");
306
- const builder = createService({ store, mirror: null, config: {} });
307
- const bait = builder.saveWithDedupe({ type: "preference", title: "m#0", content: "User feels that daily stand-up meetings are micromanagement theater", importance: 3, epistemic_status: "subjective" });
308
- const fact = builder.saveWithDedupe({ type: "preference", title: "m#1", content: "User is a Lead Software Engineer with 15 years of experience", importance: 3, epistemic_status: "observation" });
309
- assert.equal(store.getById(bait.memory.id).epistemic_status, "subjective", "explicit subjective stored");
310
- assert.equal(store.getById(fact.memory.id).epistemic_status, "observation", "explicit field must survive saveWithDedupe (English content, inference cannot classify)");
311
-
312
- const plain = createService({ store, mirror: null, config: { signalTransparency: true } });
313
- const weighted = createService({ store, mirror: null, config: { signalTransparency: true, trustEpistemicWeighting: true } });
314
- for (const s of [plain, weighted]) s.setVectorIndex(createVectorIndex({ store }));
315
- const opts = { mode: "auto", topK: 5, useRerank: false };
316
- const pRows = await plain.searchMemories("software engineer", opts);
317
- const wRows = await weighted.searchMemories("software engineer", opts);
318
- const pScore = Object.fromEntries(pRows.map((r) => [r.id, r.signals?.final]));
319
- const wScore = Object.fromEntries(wRows.map((r) => [r.id, r.signals?.final]));
320
- assert.ok(pScore[fact.memory.id] > 0, "observation entry is a candidate");
321
- assert.ok(
322
- Math.abs(wScore[fact.memory.id] - pScore[fact.memory.id]) < 1e-9,
323
- "observation keeps weight 1.0 — if scaled, the explicit field was dropped (all rows uniform subjective)"
324
- );
325
- });
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { createStore } from "../src/store.js";
8
+ import { createService } from "../src/service.js";
9
+ import { createVectorIndex } from "../src/vector-index.js";
10
+ import { createInjector } from "../src/inject.js";
11
+ import { createSettings } from "../src/settings.js";
12
+ import { applyDecisions } from "../src/dream.js";
13
+
14
+ // ---------------------------------------------------------------- schema / legacy compat
15
+
16
+ test("default epistemic_status is subjective (legacy-compatible)", () => {
17
+ const store = createStore(":memory:");
18
+ const saved = store.save({ type: "preference", title: "语言", content: "用户用中文交流" });
19
+ assert.equal(saved.epistemic_status, "subjective", "no signal -> default subjective");
20
+ assert.equal(store.getById(saved.id).epistemic_status, "subjective");
21
+ store.close();
22
+ });
23
+
24
+ test("legacy DB without the column is migrated, old rows read back as subjective", () => {
25
+ const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-epistemic-"));
26
+ const dbPath = join(dir, "memory.db");
27
+ try {
28
+ const old = new DatabaseSync(dbPath);
29
+ old.exec(`
30
+ CREATE TABLE memories (
31
+ id TEXT PRIMARY KEY, type TEXT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL,
32
+ tags TEXT NOT NULL DEFAULT '[]', importance INTEGER NOT NULL DEFAULT 3, forgotten INTEGER NOT NULL DEFAULT 0,
33
+ source TEXT, embedding TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
34
+ );
35
+ INSERT INTO memories (id, type, title, content, tags, importance, forgotten, created_at, updated_at)
36
+ VALUES ('legacy', 'preference', '旧偏好', '用户以前说过', '[]', 3, 0, 't', 't');
37
+ `);
38
+ old.close();
39
+
40
+ const store = createStore(dbPath);
41
+ const cols = store.db.prepare("PRAGMA table_info(memories)").all().map((c) => c.name);
42
+ assert.ok(cols.includes("epistemic_status"), "column added by migration");
43
+ const legacy = store.getById("legacy");
44
+ assert.equal(legacy.title, "旧偏好", "legacy row preserved");
45
+ assert.equal(legacy.epistemic_status, "subjective", "legacy row defaults to subjective");
46
+ // New writes on the migrated DB still work with the column.
47
+ const saved = store.save({ type: "preference", title: "新", content: "实测结果可用" });
48
+ assert.equal(saved.epistemic_status, "observation");
49
+ store.close();
50
+ } finally {
51
+ rmSync(dir, { recursive: true, force: true });
52
+ }
53
+ });
54
+
55
+ // ---------------------------------------------------------------- write inference
56
+
57
+ test("content-based inference: subjective markers -> subjective", () => {
58
+ const store = createStore(":memory:");
59
+ const a = store.save({ type: "preference", title: "天气", content: "我推测明天可能会下雨" });
60
+ const b = store.save({ type: "preference", title: "口味", content: "我觉得用户喜欢甜的" });
61
+ assert.equal(a.epistemic_status, "subjective");
62
+ assert.equal(b.epistemic_status, "subjective");
63
+ store.close();
64
+ });
65
+
66
+ test("content-based inference: observation markers -> observation", () => {
67
+ const store = createStore(":memory:");
68
+ const a = store.save({ type: "project", title: "温度", content: "实测温度为35度,数据显示稳定" });
69
+ const b = store.save({ type: "project", title: "观察", content: "观察到用户总是先点保存" });
70
+ assert.equal(a.epistemic_status, "observation");
71
+ assert.equal(b.epistemic_status, "observation");
72
+ store.close();
73
+ });
74
+
75
+ test("content-based inference: inference markers -> inferred", () => {
76
+ const store = createStore(":memory:");
77
+ const a = store.save({ type: "decision", title: "喜好", content: "根据历史记录推断他喜欢猫" });
78
+ const b = store.save({ type: "decision", title: "趋势", content: "综上,结论是可推断的" });
79
+ assert.equal(a.epistemic_status, "inferred");
80
+ assert.equal(b.epistemic_status, "inferred");
81
+ store.close();
82
+ });
83
+
84
+ test("AI-generated types (summary/pattern) are always inferred", () => {
85
+ const store = createStore(":memory:");
86
+ const s = store.save({ type: "summary", title: "总览", content: "实测数据汇总" });
87
+ const p = store.save({ type: "pattern", title: "模式", content: "用户反复这样操作" });
88
+ assert.equal(s.epistemic_status, "inferred");
89
+ assert.equal(p.epistemic_status, "inferred");
90
+ store.close();
91
+ });
92
+
93
+ test("explicit epistemic_status wins over content inference", () => {
94
+ const store = createStore(":memory:");
95
+ const saved = store.save({ type: "preference", title: "x", content: "我觉得可能", epistemic_status: "observation" });
96
+ assert.equal(saved.epistemic_status, "observation", "explicit value respected");
97
+ store.close();
98
+ });
99
+
100
+ test("update re-infers when content changes, keeps status otherwise", () => {
101
+ const store = createStore(":memory:");
102
+ const saved = store.save({ type: "preference", title: "x", content: "我觉得可能" });
103
+ assert.equal(saved.epistemic_status, "subjective");
104
+ // content unchanged -> status kept
105
+ const untouched = store.update(saved.id, { title: "y" });
106
+ assert.equal(untouched.epistemic_status, "subjective");
107
+ // content changed to observation -> status re-inferred
108
+ const reInferred = store.update(saved.id, { content: "实测结果显示没问题" });
109
+ assert.equal(reInferred.epistemic_status, "observation");
110
+ store.close();
111
+ });
112
+
113
+ test("update re-infers when TYPE changes to summary/pattern (regression)", () => {
114
+ const store = createStore(":memory:");
115
+ const saved = store.save({ type: "project", title: "x", content: "我觉得可能" });
116
+ assert.equal(saved.epistemic_status, "subjective");
117
+ // type -> summary: summary/pattern are always inferred, so status must flip
118
+ const asSummary = store.update(saved.id, { type: "summary" });
119
+ assert.equal(asSummary.epistemic_status, "inferred");
120
+ store.close();
121
+ });
122
+
123
+ // ---------------------------------------------------------------- retrieval ranking
124
+
125
+ const embedder = {
126
+ embedSingle: async () => [1, 0, 0],
127
+ embed: async () => [1, 0, 0],
128
+ schedule: () => {},
129
+ modelHash: "mock#1",
130
+ dimension: 3
131
+ };
132
+
133
+ test("retrieval ranking: switch off leaves scores and order untouched", async () => {
134
+ const store = createStore(":memory:");
135
+ const service = createService({ store, mirror: null, config: {} });
136
+ const a = service.saveWithDedupe({ type: "preference", title: "猫A", content: "我觉得猫很可爱", importance: 5 });
137
+ const b = service.saveWithDedupe({ type: "preference", title: "猫B", content: "实测猫很可爱", importance: 3 });
138
+ assert.equal(a.memory.epistemic_status, "subjective");
139
+ assert.equal(b.memory.epistemic_status, "observation");
140
+
141
+ const rows = await service.searchMemories("猫", { mode: "keyword", topK: 10 });
142
+ assert.equal(rows[0].id, a.memory.id, "subjective first under default importance order");
143
+ assert.equal(rows[0].score, 1.0, "subjective keeps its raw score (1.0) when off");
144
+ assert.equal(rows[1].score, 0.8, "observation keeps its raw score (0.8) when off");
145
+ store.close();
146
+ });
147
+
148
+ test("retrieval ranking: switch on re-weights and re-orders mixed recall", async () => {
149
+ const store = createStore(":memory:");
150
+ const service = createService({ store, mirror: null, config: { trustEpistemicWeighting: true } });
151
+ const vi = createVectorIndex({ store });
152
+ service.setEmbedder(embedder);
153
+ service.setVectorIndex(vi);
154
+ const subj = service.saveWithDedupe({ type: "preference", title: "量子计算", content: "我觉得量子计算很难", importance: 5 });
155
+ const obs = service.saveWithDedupe({ type: "preference", title: "量子计算实践", content: "实测量子计算简单", importance: 3 });
156
+ vi.saveEmbedding(subj.memory.id, [1, 0, 0]);
157
+ vi.saveEmbedding(obs.memory.id, [1, 0, 0]);
158
+ assert.equal(subj.memory.epistemic_status, "subjective");
159
+ assert.equal(obs.memory.epistemic_status, "observation");
160
+
161
+ // hybrid blend (default 0.6/0.4): subjective 1.0*0.6+1.0*0.4=1.0;
162
+ // observation 1.0*0.6+0.8*0.4=0.92. After weighting: 1.0*0.7=0.7 vs 0.92*1.0=0.92.
163
+ const rows = await service.searchMemories("量子计算", { mode: "hybrid", topK: 10 });
164
+ const byId = new Map(rows.map((r) => [r.id, r]));
165
+ assert.equal(byId.get(subj.memory.id).score, 0.7, "subjective weighted to 0.7");
166
+ assert.equal(byId.get(obs.memory.id).score, 0.92, "observation weighted to 0.92");
167
+ assert.equal(rows[0].id, obs.memory.id, "observation outranks subjective when weighting is on");
168
+ store.close();
169
+ });
170
+
171
+ // ---------------------------------------------------------------- injection marking
172
+
173
+ function setupInjector(over = {}) {
174
+ const store = createStore(":memory:");
175
+ const service = createService({ store, mirror: null, config: {} });
176
+ const settings = createSettings(store.db);
177
+ const contexts = [];
178
+ const ctx = {
179
+ systemPrompt: {
180
+ context(def) {
181
+ contexts.push(def);
182
+ return () => {};
183
+ }
184
+ }
185
+ };
186
+ const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
187
+ createInjector(ctx, service, settings, config);
188
+ return { store, service, contexts };
189
+ }
190
+
191
+ test("injection marks observation memories [verified] only when enabled", () => {
192
+ const { service, contexts } = setupInjector({ trustEpistemicWeighting: true });
193
+ service.saveWithDedupe({ type: "preference", title: "偏好实测", content: "实测用户喜欢用命令行", importance: 5 });
194
+ service.saveWithDedupe({ type: "preference", title: "偏好推测", content: "我觉得用户可能喜欢GUI", importance: 5 });
195
+ const text = contexts[0].text({});
196
+ assert.ok(text.includes("[verified] 偏好实测"), "observation memory is flagged");
197
+ assert.ok(!text.includes("[verified] 偏好推测"), "subjective memory is not flagged");
198
+ });
199
+
200
+ test("injection never adds [verified] when the switch is off", () => {
201
+ const { service, contexts } = setupInjector({});
202
+ service.saveWithDedupe({ type: "preference", title: "偏好实测", content: "实测用户喜欢用命令行", importance: 5 });
203
+ const text = contexts[0].text({});
204
+ assert.ok(text.includes("偏好实测"), "memory still injected");
205
+ assert.ok(!text.includes("[verified]"), "no verified marker when off");
206
+ });
207
+
208
+ // ---------------------------------------------------------------- decision priority
209
+
210
+ test("merge keepSource prefers observation when enabled", () => {
211
+ const store = createStore(":memory:");
212
+ const service = createService({ store, mirror: null, config: {} });
213
+ const config = { trustEpistemicWeighting: true };
214
+ const a = service.saveWithDedupe({ type: "project", title: "插件甲", content: "我觉得应该用A方案", importance: 3 });
215
+ const b = service.saveWithDedupe({ type: "project", title: "插件乙", content: "实测结果显示B方案更好", importance: 4 });
216
+ assert.equal(a.memory.epistemic_status, "subjective");
217
+ assert.equal(b.memory.epistemic_status, "observation");
218
+
219
+ const { committed } = applyDecisions(
220
+ [{ action: "merge", ids: [a.memory.id, b.memory.id], title: "插件总览", content: "合并", keepSource: a.memory.id }],
221
+ service, null, null, config
222
+ );
223
+ assert.equal(committed[0].keepSource, b.memory.id, "keepSource switched to the observation memory");
224
+ assert.equal(store.getById(b.memory.id).archived, false, "observation keeper stays live");
225
+ assert.equal(store.getById(a.memory.id).archived, true, "subjective source archived");
226
+ });
227
+
228
+ test("merge keepSource keeps the LLM choice when disabled", () => {
229
+ const store = createStore(":memory:");
230
+ const service = createService({ store, mirror: null, config: {} });
231
+ const a = service.saveWithDedupe({ type: "project", title: "插件甲", content: "我觉得应该用A方案", importance: 3 });
232
+ const b = service.saveWithDedupe({ type: "project", title: "插件乙", content: "实测结果显示B方案更好", importance: 4 });
233
+ const { committed } = applyDecisions(
234
+ [{ action: "merge", ids: [a.memory.id, b.memory.id], title: "插件总览", content: "合并", keepSource: a.memory.id }],
235
+ service
236
+ );
237
+ assert.equal(committed[0].keepSource, a.memory.id, "LLM keepSource respected when off");
238
+ assert.equal(store.getById(a.memory.id).archived, false);
239
+ assert.equal(store.getById(b.memory.id).archived, true);
240
+ });
241
+
242
+ test("merge never promotes an ARCHIVED observation to keeper when enabled (regression)", () => {
243
+ const store = createStore(":memory:");
244
+ const service = createService({ store, mirror: null, config: {} });
245
+ const config = { trustEpistemicWeighting: true };
246
+ // a = live subjective (LLM's keepSource), b = live observation,
247
+ // c = ARCHIVED observation. Pre-fix pickBestKeeper would pick c (highest
248
+ // priority, no archived check) as keeper, hit the archived-keeper guard in
249
+ // applyMerge and silently drop the merge — the live b never merges.
250
+ const a = service.saveWithDedupe({ type: "project", title: "甲", content: "我觉得A方案", importance: 3 });
251
+ const b = service.saveWithDedupe({ type: "project", title: "乙", content: "实测B方案更好", importance: 4 });
252
+ const c = service.saveWithDedupe({ type: "project", title: "丙", content: "实测C方案最好", importance: 4 });
253
+ service.setArchived(c.memory.id, true);
254
+
255
+ const { committed } = applyDecisions(
256
+ [{ action: "merge", ids: [a.memory.id, b.memory.id, c.memory.id], title: "合并", content: "合并", keepSource: a.memory.id }],
257
+ service, null, null, config
258
+ );
259
+ // c (archived) must NOT be promoted; the live observation b becomes keeper,
260
+ // and the merge actually lands.
261
+ assert.equal(committed.length, 1, "merge actually committed (not silently skipped)");
262
+ assert.equal(committed[0].keepSource, b.memory.id, "live observation promoted over archived one");
263
+ assert.equal(store.getById(b.memory.id).archived, false, "observation keeper stays live");
264
+ assert.equal(store.getById(a.memory.id).archived, true, "subjective source archived");
265
+ assert.equal(store.getById(c.memory.id).archived, true, "archived c stays archived");
266
+ });
267
+
268
+ test("conflict resolution prefers the observation side as winner when enabled", () => {
269
+ const store = createStore(":memory:");
270
+ const service = createService({ store, mirror: null, config: {} });
271
+ const config = { trustEpistemicWeighting: true };
272
+ const w = service.saveWithDedupe({ type: "decision", title: "截止1", content: "我觉得截止是8月20日", importance: 4 });
273
+ const l = service.saveWithDedupe({ type: "decision", title: "截止2", content: "实测截止是8月15日", importance: 4 });
274
+ assert.equal(w.memory.epistemic_status, "subjective");
275
+ assert.equal(l.memory.epistemic_status, "observation");
276
+
277
+ const { committed } = applyDecisions(
278
+ [{ action: "conflict", winner: w.memory.id, loser: l.memory.id, reason: "日期更新" }],
279
+ service, null, null, config
280
+ );
281
+ assert.equal(committed[0].winner, l.memory.id, "winner swapped to the observation memory");
282
+ assert.equal(store.getById(l.memory.id).archived, false, "observation winner stays live");
283
+ assert.equal(store.getById(w.memory.id).archived, true, "subjective loser archived");
284
+ });
285
+
286
+ test("conflict resolution keeps the LLM winner when disabled", () => {
287
+ const store = createStore(":memory:");
288
+ const service = createService({ store, mirror: null, config: {} });
289
+ const w = service.saveWithDedupe({ type: "decision", title: "截止1", content: "我觉得截止是8月20日", importance: 4 });
290
+ const l = service.saveWithDedupe({ type: "decision", title: "截止2", content: "实测截止是8月15日", importance: 4 });
291
+ const { committed } = applyDecisions(
292
+ [{ action: "conflict", winner: w.memory.id, loser: l.memory.id, reason: "日期更新" }],
293
+ service
294
+ );
295
+ assert.equal(committed[0].winner, w.memory.id, "LLM winner respected when off");
296
+ assert.equal(store.getById(w.memory.id).archived, false);
297
+ assert.equal(store.getById(l.memory.id).archived, true);
298
+ });
299
+
300
+ // E2 四臂实验(Kaggle v11)当场抓到的回归:saveWithDedupe 此前丢弃显式
301
+ // epistemic_status,store 回退内容推断,英文/无标记内容一律 subjective——
302
+ // EPISTEMIC_WEIGHTS 对整池均匀 ×0.7,trustEpistemicWeighting 的重排整体空转
303
+ // (R+ 臂与 A 臂注入序逐条相同)。显式值必须穿透创建路径;undefined 保持推断。
304
+ test("saveWithDedupe forwards explicit epistemic_status (rerank needs non-uniform weights)", async () => {
305
+ const store = createStore(":memory:");
306
+ const builder = createService({ store, mirror: null, config: {} });
307
+ const bait = builder.saveWithDedupe({ type: "preference", title: "m#0", content: "User feels that daily stand-up meetings are micromanagement theater", importance: 3, epistemic_status: "subjective" });
308
+ const fact = builder.saveWithDedupe({ type: "preference", title: "m#1", content: "User is a Lead Software Engineer with 15 years of experience", importance: 3, epistemic_status: "observation" });
309
+ assert.equal(store.getById(bait.memory.id).epistemic_status, "subjective", "explicit subjective stored");
310
+ assert.equal(store.getById(fact.memory.id).epistemic_status, "observation", "explicit field must survive saveWithDedupe (English content, inference cannot classify)");
311
+
312
+ const plain = createService({ store, mirror: null, config: { signalTransparency: true } });
313
+ const weighted = createService({ store, mirror: null, config: { signalTransparency: true, trustEpistemicWeighting: true } });
314
+ for (const s of [plain, weighted]) s.setVectorIndex(createVectorIndex({ store }));
315
+ const opts = { mode: "auto", topK: 5, useRerank: false };
316
+ const pRows = await plain.searchMemories("software engineer", opts);
317
+ const wRows = await weighted.searchMemories("software engineer", opts);
318
+ const pScore = Object.fromEntries(pRows.map((r) => [r.id, r.signals?.final]));
319
+ const wScore = Object.fromEntries(wRows.map((r) => [r.id, r.signals?.final]));
320
+ assert.ok(pScore[fact.memory.id] > 0, "observation entry is a candidate");
321
+ assert.ok(
322
+ Math.abs(wScore[fact.memory.id] - pScore[fact.memory.id]) < 1e-9,
323
+ "observation keeps weight 1.0 — if scaled, the explicit field was dropped (all rows uniform subjective)"
324
+ );
325
+ });