@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,523 @@
1
+ // #230:document 型记忆——注册校验 / 摘要+doc_path 落库 / C2 去重 / supersede
2
+ // 记账 / 注入档位与预算 / 写入权分离守卫。口径基线 = #164 设计稿评审线
3
+ // (discussioncomment-18495098 + 维护者对账 18502150)。
4
+
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { createStore } from "../src/store.js";
11
+ import { createService } from "../src/service.js";
12
+ import { createDocumentRegistrar } from "../src/document.js";
13
+ import { MEMORY_ITEM_SCHEMA } from "../src/tools.js";
14
+ import { TYPE_DECAY_DEFAULTS } from "../src/heat.js";
15
+
16
+ function makeService(config = {}) {
17
+ const store = createStore(":memory:");
18
+ const service = createService({ store, mirror: null, config });
19
+ return { store, service, close: () => store.close() };
20
+ }
21
+
22
+ async function makeDocDir() {
23
+ const dir = await mkdtemp(path.join(os.tmpdir(), "dsh-mneme-doc-"));
24
+ const writeDoc = async (name, content = "# Doc\n\nbody text") => {
25
+ const p = path.join(dir, name);
26
+ await writeFile(p, content, "utf8");
27
+ return p;
28
+ };
29
+ return { dir, writeDoc, cleanup: () => rm(dir, { recursive: true, force: true }) };
30
+ }
31
+
32
+ /** 单元级 registrar:注入假 embedQuery,隔离 vector 档行为。 */
33
+ function makeRegistrar(store, config = {}, embedQuery = async () => null) {
34
+ const finalized = [];
35
+ const register = createDocumentRegistrar({
36
+ store,
37
+ config,
38
+ embedQuery,
39
+ // 与 service.js 的 pushContentHistory 同形(FIFO cap 20)
40
+ pushContentHistory: (existing, source) => [
41
+ { content: existing?.content ?? "", source, updated_at: new Date().toISOString() },
42
+ ...(Array.isArray(existing?.content_history) ? existing.content_history : [])
43
+ ].slice(0, 20),
44
+ transaction: (fn) => fn(),
45
+ finalize: (rows) => finalized.push(...rows)
46
+ });
47
+ return { register, finalized };
48
+ }
49
+
50
+ // ============================================================ 注册校验(DoD 1)
51
+
52
+ test("registration is refused while the feature flag is off", async () => {
53
+ const { service, close } = makeService({});
54
+ const { writeDoc, cleanup } = await makeDocDir();
55
+ try {
56
+ const p = await writeDoc("a.md");
57
+ await assert.rejects(
58
+ () => service.registerDocument({ path: p, title: "T", summary: "S" }),
59
+ /documentMemoryEnabled/
60
+ );
61
+ } finally {
62
+ cleanup();
63
+ close();
64
+ }
65
+ });
66
+
67
+ test("path validation: required / absolute-only / existing non-empty regular file", async () => {
68
+ const { service, close } = makeService({ documentMemoryEnabled: true });
69
+ const { dir, writeDoc, cleanup } = await makeDocDir();
70
+ try {
71
+ await assert.rejects(
72
+ () => service.registerDocument({ title: "T", summary: "S" }),
73
+ /path is required/
74
+ );
75
+ await assert.rejects(
76
+ () => service.registerDocument({ path: "rel.md", title: "T", summary: "S" }),
77
+ /must be absolute/
78
+ );
79
+ await assert.rejects(
80
+ () => service.registerDocument({ path: path.join(dir, "missing.md"), title: "T", summary: "S" }),
81
+ /not found or empty/
82
+ );
83
+ await assert.rejects(
84
+ () => service.registerDocument({ path: dir, title: "T", summary: "S" }),
85
+ /not found or empty/
86
+ );
87
+ const empty = await writeDoc("empty.md", "");
88
+ await assert.rejects(
89
+ () => service.registerDocument({ path: empty, title: "T", summary: "S" }),
90
+ /not found or empty/
91
+ );
92
+ } finally {
93
+ cleanup();
94
+ close();
95
+ }
96
+ });
97
+
98
+ test("~ paths expand to the real home and register", async () => {
99
+ const { service, close } = makeService({ documentMemoryEnabled: true });
100
+ const { writeDoc, cleanup } = await makeDocDir();
101
+ try {
102
+ const p = await writeDoc("tilde.md");
103
+ const rel = path.relative(os.homedir(), p);
104
+ const res = await service.registerDocument({
105
+ path: `~/${rel.split(path.sep).join("/")}`,
106
+ title: "TildeDoc",
107
+ summary: "S"
108
+ });
109
+ assert.equal(res.action, "created");
110
+ assert.equal(res.memory.doc_path, p);
111
+ } finally {
112
+ cleanup();
113
+ close();
114
+ }
115
+ });
116
+
117
+ test("evidence intersection: valid subset kept, unknown dropped + degraded tag, all-fabricated rejected", async () => {
118
+ const { service, close } = makeService({ documentMemoryEnabled: true });
119
+ const { writeDoc, cleanup } = await makeDocDir();
120
+ try {
121
+ const a = service.saveWithDedupe({ type: "preference", title: "P1", content: "c1" }).memory;
122
+ const b = service.saveWithDedupe({ type: "decision", title: "D1", content: "c2" }).memory;
123
+ const p = await writeDoc("r.md");
124
+
125
+ const partial = await service.registerDocument({
126
+ path: p, title: "R", summary: "S", evidence: [a.id, "nope-1", b.id]
127
+ });
128
+ assert.equal(partial.action, "created");
129
+ assert.deepEqual(partial.memory.evidence, [a.id, b.id]);
130
+ assert.equal(partial.evidence_kept, 2);
131
+ assert.equal(partial.evidence_dropped, 1);
132
+ assert.equal(partial.degraded, true);
133
+ assert.ok(partial.memory.tags.includes("evidence_degraded"));
134
+
135
+ await assert.rejects(
136
+ () => service.registerDocument({ path: p, title: "T2", summary: "S", evidence: ["nope-2"] }),
137
+ /fabricated evidence is rejected/
138
+ );
139
+
140
+ const clean = await service.registerDocument({ path: await writeDoc("clean.md"), title: "T3", summary: "S" });
141
+ assert.equal(clean.degraded, false);
142
+ assert.deepEqual(clean.memory.evidence, []);
143
+ assert.ok(!clean.memory.tags.includes("evidence_degraded"));
144
+ } finally {
145
+ cleanup();
146
+ close();
147
+ }
148
+ });
149
+
150
+ // ============================================================ supersede 记账(DoD 2)
151
+
152
+ test("re-registering the same path supersedes: old row archived + traceable, file untouched", async () => {
153
+ const { service, close } = makeService({ documentMemoryEnabled: true });
154
+ const { writeDoc, cleanup } = await makeDocDir();
155
+ try {
156
+ const p = await writeDoc("v.md");
157
+ const v1 = await service.registerDocument({ path: p, title: "T1", summary: "summary v1", tags: ["doc"] });
158
+ const v2 = await service.registerDocument({ path: p, title: "T2", summary: "summary v2" });
159
+
160
+ assert.equal(v2.action, "superseded");
161
+ assert.equal(v2.superseded.id, v1.memory.id);
162
+ assert.equal(v2.memory.doc_path, p);
163
+
164
+ const old = service.getById(v1.memory.id);
165
+ assert.equal(old.archived, true, "old row archived (no physical delete)");
166
+ assert.ok(old.content.includes(`[superseded by ${v2.memory.id}]`), "loser carries the pointer note");
167
+ assert.equal(old.content_history[0].source, "superseded");
168
+ assert.equal(old.content_history[0].content, "summary v1", "old summary traceable via content_history");
169
+ assert.equal(old.doc_path, p, "old pointer preserved");
170
+
171
+ const fresh = service.getById(v2.memory.id);
172
+ assert.equal(fresh.archived, false);
173
+ assert.equal(fresh.content, "summary v2");
174
+ } finally {
175
+ cleanup();
176
+ close();
177
+ }
178
+ });
179
+
180
+ test("same title on a different path also supersedes (new version of the moved doc)", async () => {
181
+ const { service, close } = makeService({ documentMemoryEnabled: true });
182
+ const { writeDoc, cleanup } = await makeDocDir();
183
+ try {
184
+ const p1 = await writeDoc("old.md");
185
+ const p2 = await writeDoc("new.md");
186
+ const v1 = await service.registerDocument({ path: p1, title: "Same", summary: "s1" });
187
+ const v2 = await service.registerDocument({ path: p2, title: "Same", summary: "s2" });
188
+ assert.equal(v2.action, "superseded");
189
+ assert.equal(v2.superseded.id, v1.memory.id);
190
+ assert.equal(service.getById(v1.memory.id).archived, true);
191
+ assert.equal(service.getById(v2.memory.id).doc_path, p2);
192
+ } finally {
193
+ cleanup();
194
+ close();
195
+ }
196
+ });
197
+
198
+ // ============================================================ C2 去重(DoD 6 前半)
199
+
200
+ test("vector near-duplicate on a different path/title is rejected (agent decides, no silent supersede)", async () => {
201
+ const { writeDoc, cleanup } = await makeDocDir();
202
+ const store = createStore(":memory:");
203
+ try {
204
+ store.saveDocument({
205
+ type: "document", title: "Alpha", content: "existing summary",
206
+ doc_path: "X:\\elsewhere\\a.md", embedding: [1, 0, 0], source: "tool"
207
+ });
208
+ const b = await writeDoc("b.md");
209
+ const c = await writeDoc("c.md");
210
+ const { register } = makeRegistrar(store, { documentMemoryEnabled: true }, async () => [1, 0, 0]);
211
+ await assert.rejects(
212
+ () => register({ path: b, title: "Beta", summary: "similar summary" }),
213
+ /near duplicate/
214
+ );
215
+ // 正交向量 = 无近重复 → 正常创建。
216
+ const { register: register2 } = makeRegistrar(store, { documentMemoryEnabled: true }, async () => [0, 1, 0]);
217
+ const res = await register2({ path: c, title: "Gamma", summary: "unrelated summary" });
218
+ assert.equal(res.action, "created");
219
+ } finally {
220
+ cleanup();
221
+ store.close();
222
+ }
223
+ });
224
+
225
+ test("embedder unavailable: dedupe degrades to path/title tiers, writes still land", async () => {
226
+ const { service, close } = makeService({ documentMemoryEnabled: true });
227
+ const { writeDoc, cleanup } = await makeDocDir();
228
+ try {
229
+ const p1 = await writeDoc("d1.md");
230
+ const p2 = await writeDoc("d2.md");
231
+ const r1 = await service.registerDocument({ path: p1, title: "A", summary: "identical summary text" });
232
+ const r2 = await service.registerDocument({ path: p2, title: "B", summary: "identical summary text" });
233
+ assert.equal(r1.action, "created");
234
+ assert.equal(r2.action, "created", "no vector signal = no near-dup verdict, write is never blocked");
235
+ } finally {
236
+ cleanup();
237
+ close();
238
+ }
239
+ });
240
+
241
+ // ============================================================ 写入权分离守卫(DoD 1/3 旁路防护)
242
+
243
+ test("saveWithDedupe refuses to mint document rows", () => {
244
+ const { service, close } = makeService({});
245
+ try {
246
+ assert.throws(
247
+ () => service.saveWithDedupe({ type: "document", title: "x", content: "y" }),
248
+ /registerDocument/
249
+ );
250
+ } finally {
251
+ close();
252
+ }
253
+ });
254
+
255
+ test("updateMemory cannot retype a row into document, but document summary repair passes", async () => {
256
+ const { service, close } = makeService({ documentMemoryEnabled: true });
257
+ const { writeDoc, cleanup } = await makeDocDir();
258
+ try {
259
+ const pref = service.saveWithDedupe({ type: "preference", title: "P", content: "c" }).memory;
260
+ assert.throws(() => service.update(pref.id, { type: "document" }), /registerDocument/);
261
+
262
+ const p = await writeDoc("fix.md");
263
+ const atomic = service.saveWithDedupe({ type: "decision", title: "D", content: "c" }).memory;
264
+ const doc = (await service.registerDocument({
265
+ path: p, title: "Doc", summary: "s", evidence: [atomic.id, "nope"]
266
+ })).memory;
267
+ const updated = service.update(doc.id, { content: "repaired summary", tags: ["user-tag"] });
268
+ assert.equal(updated.content, "repaired summary");
269
+ assert.equal(updated.doc_path, p, "pointer untouched by summary repair");
270
+ assert.equal(updated.archived, false);
271
+ assert.ok(
272
+ updated.tags.includes("evidence_degraded") && updated.tags.includes("user-tag"),
273
+ "system signal tag survives a tags-replacing update"
274
+ );
275
+ } finally {
276
+ cleanup();
277
+ close();
278
+ }
279
+ });
280
+
281
+ // ============================================================ 注入档位与预算(DoD 5)
282
+
283
+ function seedInjectionFixtures(service, store) {
284
+ service.saveWithDedupe({ type: "summary", title: "当前项目状态", content: "resident digest", source: "dream", importance: 5, _overwrite: true });
285
+ service.saveWithDedupe({ type: "summary", title: "叙述:topic", content: "narrative bar", source: "narrative", tags: ["topic-n"], importance: 3 });
286
+ // document 行铸造口唯一:测试种子也走 store.saveDocument(saveWithDedupe
287
+ // 有 service 守卫、store.save 有存储层守卫)。
288
+ for (let i = 1; i <= 3; i++) {
289
+ store.saveDocument({ type: "document", title: `Doc${i}`, content: `d${i}`, source: "tool", doc_path: `X:\\${i}.md`, importance: 3 });
290
+ }
291
+ }
292
+
293
+ test("injection: document rows at the next-priority tier, capped by documentInjectBudget; narrative tier re-enabled", () => {
294
+ const { store, service, close } = makeService({ documentMemoryEnabled: true, documentInjectBudget: 2, dreamNarrativeEnabled: true });
295
+ try {
296
+ seedInjectionFixtures(service, store);
297
+ const picked = service.injectCandidates({});
298
+ assert.equal(picked[0].type, "summary");
299
+ assert.equal(picked[0].source, "dream", "resident digest keeps tier 0");
300
+ assert.equal(picked.filter((m) => m.type === "document").length, 2, "document budget caps at 2 of 3");
301
+ assert.ok(picked.some((m) => m.source === "narrative"), "narrative rows inject at the next-priority tier (#230 拍板)");
302
+ } finally {
303
+ close();
304
+ }
305
+ });
306
+
307
+ test("injection: flags off → document and narrative rows stay out, behavior unchanged", () => {
308
+ const { store, service, close } = makeService({});
309
+ try {
310
+ seedInjectionFixtures(service, store);
311
+ const picked = service.injectCandidates({});
312
+ assert.ok(picked.every((m) => m.type !== "document"), "document rows excluded while flag off");
313
+ assert.ok(picked.every((m) => m.source !== "narrative"), "narrative rows excluded while dreamNarrativeEnabled off");
314
+ assert.ok(picked.some((m) => m.type === "summary" && m.source === "dream"), "dream digest unaffected");
315
+ } finally {
316
+ close();
317
+ }
318
+ });
319
+
320
+ // ============================================================ DTO / 扩展面(DoD 3)
321
+
322
+ test("toApiList exposes doc_path on pointer rows only, within MEMORY_ITEM_SCHEMA", async () => {
323
+ const { service, close } = makeService({ documentMemoryEnabled: true });
324
+ const { writeDoc, cleanup } = await makeDocDir();
325
+ try {
326
+ const pref = service.saveWithDedupe({ type: "preference", title: "P", content: "c" }).memory;
327
+ const p = await writeDoc("dto.md");
328
+ const doc = (await service.registerDocument({ path: p, title: "D", summary: "s" })).memory;
329
+
330
+ const prefDto = service.toApiList([pref])[0];
331
+ assert.ok(!("doc_path" in prefDto), "non-pointer rows keep the pre-#230 DTO shape");
332
+ const docDto = service.toApiList([doc])[0];
333
+ assert.equal(docDto.doc_path, p);
334
+ for (const key of Object.keys(docDto)) {
335
+ assert.ok(key in MEMORY_ITEM_SCHEMA.properties, `DTO key "${key}" declared in MEMORY_ITEM_SCHEMA`);
336
+ }
337
+ } finally {
338
+ cleanup();
339
+ close();
340
+ }
341
+ });
342
+
343
+ test("document rows are heat-immune (pointer rows do not decay)", () => {
344
+ assert.equal(TYPE_DECAY_DEFAULTS.document, 0);
345
+ });
346
+
347
+ // ============================================================ CodeRabbit 修复回归(PR #256 review)
348
+
349
+ test("path normalization: equivalent absolute spellings supersede instead of duplicating", async () => {
350
+ const { service, close } = makeService({ documentMemoryEnabled: true });
351
+ const { dir, writeDoc, cleanup } = await makeDocDir();
352
+ try {
353
+ const p = await writeDoc("norm.md");
354
+ const first = await service.registerDocument({ path: p, title: "V1", summary: "s1" });
355
+ // path.join 会把 "." 段吃掉,手工拼出等价但未归一化的写法。
356
+ const dotted = `${dir}${path.sep}.${path.sep}norm.md`;
357
+ const second = await service.registerDocument({ path: dotted, title: "V2", summary: "s2" });
358
+ assert.equal(second.action, "superseded", "normalized spelling hits the same-path supersede tier");
359
+ assert.equal(second.superseded.id, first.memory.id);
360
+ } finally {
361
+ cleanup();
362
+ close();
363
+ }
364
+ });
365
+
366
+ test("exact supersede tiers scan beyond any window (>200 active documents)", async () => {
367
+ const { store, close } = makeService({ documentMemoryEnabled: true });
368
+ const { writeDoc, cleanup } = await makeDocDir();
369
+ try {
370
+ // 新注册的路径必须过文件校验——目标行(窗口外的旧版)用真实文件,其余
371
+ // 209 行用虚构路径垫量。Bulk0 先种 = updated_at 最早 = list(DESC) 末位。
372
+ const pathA = await writeDoc("a.md");
373
+ const pathB = await writeDoc("b.md");
374
+ store.saveDocument({ type: "document", title: "Bulk0", content: "c0", source: "tool", doc_path: pathA });
375
+ for (let i = 1; i < 210; i++) {
376
+ store.saveDocument({ type: "document", title: `Bulk${i}`, content: `c${i}`, source: "tool", doc_path: `X:\\bulk\\${i}.md` });
377
+ }
378
+ const oldest = store.list({ type: "document", limit: null }).at(-1);
379
+ assert.equal(oldest.title, "Bulk0", "precondition: oldest row is outside the 200 window");
380
+ const { register } = makeRegistrar(store, { documentMemoryEnabled: true });
381
+ // 路径层:窗口外的同路径行必须仍被发现并 supersede。
382
+ const byPath = await register({ path: pathA, title: "Fresh path re-register", summary: "new version" });
383
+ assert.equal(byPath.action, "superseded");
384
+ assert.equal(byPath.superseded.id, oldest.id);
385
+ // 标题层:同款全量语义(Bulk0 已归档,此时窗口外最老是 Bulk1)。
386
+ const next = store.list({ type: "document", limit: null }).at(-1);
387
+ assert.equal(next.title, "Bulk1");
388
+ const byTitle = await register({ path: pathB, title: next.title, summary: "another version" });
389
+ assert.equal(byTitle.action, "superseded");
390
+ assert.equal(byTitle.superseded.id, next.id);
391
+ } finally {
392
+ cleanup();
393
+ close();
394
+ }
395
+ });
396
+
397
+ test("sensitivity is persisted and participates in the supersede matching key", async () => {
398
+ const { service, close } = makeService({ documentMemoryEnabled: true });
399
+ const { writeDoc, cleanup } = await makeDocDir();
400
+ try {
401
+ const p = await writeDoc("sens.md");
402
+ const first = await service.registerDocument({ path: p, title: "S", summary: "s1", sensitivity: "personal" });
403
+ assert.equal(first.memory.sensitivity, "personal", "sensitivity must persist (scopeKeyOf third dim)");
404
+ // 同 path + 同 sensitivity = 显式出新版。
405
+ const again = await service.registerDocument({ path: p, title: "S2", summary: "s2", sensitivity: "personal" });
406
+ assert.equal(again.action, "superseded");
407
+ // sensitivity 不同 = 另一 scope 维度,不互判(与 memory_save 的同键分离语义一致)。
408
+ const other = await service.registerDocument({ path: p, title: "S3", summary: "s3", sensitivity: "team" });
409
+ assert.equal(other.action, "created");
410
+ } finally {
411
+ cleanup();
412
+ close();
413
+ }
414
+ });
415
+
416
+ test("store guards: generic paths cannot mint or retype document rows", () => {
417
+ const { store, close } = makeService({});
418
+ try {
419
+ // save:整类拒绝——doc_path 是任意调用方可捏造的字符串,「必带 doc_path」
420
+ // 挡不住绕过注册校验的直铸(CodeRabbit 复核 #882)。
421
+ assert.throws(
422
+ () => store.save({ type: "document", title: "X", content: "c" }),
423
+ /minted only via store\.saveDocument/
424
+ );
425
+ assert.throws(
426
+ () => store.save({ type: "document", title: "X", content: "c", doc_path: "X:\\x.md" }),
427
+ /minted only via store\.saveDocument/
428
+ );
429
+ // saveDocument:唯一铸造口,且保留指针行结构不变量。
430
+ assert.throws(
431
+ () => store.saveDocument({ type: "document", title: "X", content: "c" }),
432
+ /doc_path is required/
433
+ );
434
+ const pref = store.save({ type: "preference", title: "P", content: "c" });
435
+ const doc = store.saveDocument({ type: "document", title: "D", content: "s", doc_path: "X:\\d.md" });
436
+ // update / CAS:type 不许改入或改出 document(CAS 绕过 service 守卫,存储层兜底)。
437
+ assert.throws(() => store.update(pref.id, { type: "document" }), /cannot be changed to or from/);
438
+ assert.throws(() => store.update(doc.id, { type: "preference" }), /cannot be changed to or from/);
439
+ assert.throws(
440
+ () => store.compareAndUpdate(pref.id, pref.updated_at, { type: "document" }),
441
+ /cannot be changed to or from/
442
+ );
443
+ // 同 type 的摘要修复照常(doc_path 不在 UPDATE SET,天然保持)。
444
+ const repaired = store.update(doc.id, { content: "repaired summary" });
445
+ assert.equal(repaired.content, "repaired summary");
446
+ assert.equal(repaired.doc_path, "X:\\d.md");
447
+ } finally {
448
+ close();
449
+ }
450
+ });
451
+
452
+ test("sleep cold-scan and the pattern pool exclude document rows at the query layer", () => {
453
+ const { store, close } = makeService({});
454
+ try {
455
+ const proj = store.save({ type: "project", title: "Proj", content: "c" });
456
+ const hist = store.save({ type: "history", title: "Hist", content: "c" });
457
+ // 最新行是 document——不带排除时它会占满 LIMIT 1 的扫描窗(饿池前置条件)。
458
+ store.saveDocument({ type: "document", title: "Doc", content: "s", source: "tool", doc_path: "X:\\d.md" });
459
+ // 冷扫描(getUnrecalledSince,sleep phase 2 的归档降级候选):document 豁免。
460
+ const cold = store.getUnrecalledSince(0);
461
+ assert.ok(cold.some((m) => m.id === proj.id) && cold.some((m) => m.id === hist.id));
462
+ assert.ok(cold.every((m) => m.type !== "document"), "pointer rows never become archival-demotion candidates");
463
+ // 模式扫描池:excludeTypes 在 LIMIT 之前生效。
464
+ const plain = store.list({ limit: 1 });
465
+ assert.equal(plain[0].type, "document", "precondition: newest row is a document");
466
+ const pool = store.list({ limit: 1, excludeTypes: ["document"] });
467
+ assert.notEqual(pool[0].type, "document", "ordinary memories reach the pool even under a tight limit");
468
+ // 无界扫描(注册器精确层):limit null = 全量。
469
+ assert.equal(store.list({ type: "document", limit: null }).length, 1);
470
+ } finally {
471
+ close();
472
+ }
473
+ });
474
+
475
+ test("hidden evidence ids (strictScope) are treated as nonexistent by the registrar", async () => {
476
+ const { service, close } = makeService({ documentMemoryEnabled: true });
477
+ const { writeDoc, cleanup } = await makeDocDir();
478
+ try {
479
+ const a = service.saveWithDedupe({ type: "preference", title: "P", content: "c1" }).memory;
480
+ const b = service.saveWithDedupe({ type: "decision", title: "D", content: "c2" }).memory;
481
+ const p = await writeDoc("hidden.md");
482
+ // 全部不可见 = 整单拒绝(与捏造同款;先测,避免先落行后又被同路径命中)。
483
+ await assert.rejects(
484
+ () => service.registerDocument({ path: p, title: "H", summary: "s", evidence: [b.id] }, { hiddenEvidenceIds: [b.id] }),
485
+ /unknown, archived or out of scope/
486
+ );
487
+ // 混合:不可见 id 落 dropped、不进持久化 evidence,kept/dropped 计数保持诚实。
488
+ const mixed = await service.registerDocument(
489
+ { path: p, title: "H", summary: "s", evidence: [a.id, b.id] },
490
+ { hiddenEvidenceIds: [b.id] }
491
+ );
492
+ assert.equal(mixed.evidence_kept, 1);
493
+ assert.equal(mixed.evidence_dropped, 1);
494
+ assert.equal(mixed.degraded, true);
495
+ assert.deepEqual(mixed.memory.evidence, [a.id]);
496
+ } finally {
497
+ cleanup();
498
+ close();
499
+ }
500
+ });
501
+
502
+ test("narrative gate covers the semantic paths: BM25 fallback keeps narratives out while the flag is off", () => {
503
+ const { service, close } = makeService({});
504
+ try {
505
+ service.saveWithDedupe({ type: "summary", title: "叙述:zebra-topic", content: "zebra-topic narrative bar", source: "narrative", importance: 5 });
506
+ // 无 embedder 时首轮必走 BM25 兜底——此前的门只锁规则路,语义路会漏。
507
+ const picked = service.injectCandidates({ q: "zebra-topic" });
508
+ assert.ok(picked.every((m) => m.source !== "narrative"), "semantic-path candidates respect dreamNarrativeEnabled");
509
+ } finally {
510
+ close();
511
+ }
512
+ });
513
+
514
+ test("narrative rows inject via the semantic path once dreamNarrativeEnabled is on", () => {
515
+ const { service, close } = makeService({ dreamNarrativeEnabled: true });
516
+ try {
517
+ service.saveWithDedupe({ type: "summary", title: "叙述:zebra-topic", content: "zebra-topic narrative bar", source: "narrative", importance: 5 });
518
+ const picked = service.injectCandidates({ q: "zebra-topic" });
519
+ assert.ok(picked.some((m) => m.source === "narrative"), "BM25 path admits narratives under the same flag as the rule path");
520
+ } finally {
521
+ close();
522
+ }
523
+ });