@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,325 @@
1
+ // issue #195:全工具矩阵的「DTO 键集 ⊆ output schema」系统性断言——防 #184 类漂移。
2
+ //
3
+ // 背景:#184 是 memory_get 的内联 output schema 漏声明了 v0.8.1 新增的 scope 来源
4
+ // 三键,而 schema 是 `additionalProperties: false`——toApiList 条件展开的键一旦没被
5
+ // 声明,宿主 in-process 校验直接拒收("invalid output"),表现为「个别 id 必失败、
6
+ // 多数正常」。单点回归(tools-memory-get-schema.test.js)只护住了 memory_get 一个
7
+ // 工具:换一个工具、换一个键,同类事故可以原样重演。
8
+ //
9
+ // 这里把护栏提到矩阵层,四层断言各管一段:
10
+ // A 实跑:9 个工具每个可安全触达的分支,拿**真实 execute 的返回值**过生产同款校验器
11
+ // (validateJsonSchemaValue)——不写手抄的期望值,让运行时自己说话;
12
+ // B 产地:DTO 的唯一产地 service.toApiList 在全形态数据下的输出 ⊆ MEMORY_ITEM_SCHEMA,
13
+ // 并反向要求声明里的每个键都被至少一种形态真实产出(声明了却永不出现的键=死声明,
14
+ // 会在下一次 schema 增键时立刻暴露);
15
+ // C 结构:每个工具的 schema 必须闭合(additionalProperties:false)、required ⊆ properties、
16
+ // 每项都带 type——否则 A/B 两层会因为校验器形同虚设而静默失效;
17
+ // D 负例锁:故意注入一个未声明键,断言校验器必须报错。没有这一层,上面三层可以一起
18
+ // 绿着却毫无保护力(校验器被换掉、schema 被放开都会静默通过)。
19
+ import test from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
25
+ import { createStore } from "../src/store.js";
26
+ import { createService } from "../src/service.js";
27
+ import { createTools, MEMORY_ITEM_SCHEMA } from "../src/tools.js";
28
+
29
+ // 矩阵清单:新增工具必须同时补进本文件的分支表,否则下面第一条断言会先失败提醒
30
+ // ——这是「系统性」的含义:漏一个工具不会静默通过。
31
+ const TOOL_NAMES = [
32
+ "memory_save",
33
+ "memory_search",
34
+ "memory_list",
35
+ "memory_get",
36
+ "memory_update",
37
+ "memory_delete",
38
+ "memory_forget",
39
+ "memory_archive",
40
+ "memory_register_document",
41
+ "memory_runtime"
42
+ ];
43
+
44
+ function setup(config = {}) {
45
+ const store = createStore(":memory:");
46
+ // config 同时喂 service 与 tools:#230 起 registerDocument 的闸在 service
47
+ // 侧读同一个 config,只喂 tools 会让注册工具在矩阵里永远抛 disabled。
48
+ const service = createService({ store, mirror: null, config });
49
+ const registered = [];
50
+ const ctx = {
51
+ tools: {
52
+ register(def) {
53
+ registered.push(def);
54
+ return () => {};
55
+ }
56
+ }
57
+ };
58
+ createTools(ctx, service, config, undefined);
59
+ const byName = Object.fromEntries(registered.map((def) => [def.name, def]));
60
+ return { store, service, byName };
61
+ }
62
+
63
+ // 驱动一个工具并立刻用它自己声明的 schema 校验真实返回值。
64
+ // 返回 { value }:调用方需要时再对值做语义断言。
65
+ async function drive(byName, name, args = [], exec) {
66
+ const tool = byName[name];
67
+ assert.ok(tool, `工具 ${name} 未注册`);
68
+ const value = await tool.execute(args, exec);
69
+ assertMatches(byName, name, value, `args=${JSON.stringify(args)}`);
70
+ return value;
71
+ }
72
+
73
+ // 真实返回值必须被声明 schema 完全覆盖;失败时把违规明细一起抛出,定位更快。
74
+ function assertMatches(byName, name, value, label) {
75
+ const errors = validateJsonSchemaValue(byName[name].output.schema, value);
76
+ assert.deepEqual(
77
+ errors,
78
+ [],
79
+ `${name} 的真实返回未被 output schema 覆盖(${label}):${JSON.stringify(errors)}\n返回键:${JSON.stringify(Object.keys(value ?? {}))}`
80
+ );
81
+ }
82
+
83
+ // 全形态种子:toApiList 的 7 个条件键只在对应列非 undefined 时展开,所以每种形态
84
+ // 都要真实落一行,否则 B 层会(正确地)报「声明了却从未产出」。
85
+ function seedShapes(store) {
86
+ const rows = [];
87
+ const push = (over) => {
88
+ const memory = {
89
+ type: "project",
90
+ title: `shape-${rows.length}`,
91
+ content: `形态 ${rows.length} 的正文`,
92
+ source: "test",
93
+ ...over
94
+ };
95
+ // #230:document 走存储层唯一铸造口(通用 save 拒绝 document)。
96
+ rows.push(memory.type === "document" ? store.saveDocument(memory) : store.save(memory));
97
+ };
98
+ push({}); // 极简形态:只有基础键
99
+ push({ sensitivity: "personal" });
100
+ push({ occurred_at: "2026-09-15T00:00:00Z" });
101
+ push({ agent_scope: "coder", workspace_scope: "proj-alpha" });
102
+ push({
103
+ // 全量标注:#184 的原始触发形态
104
+ agent_scope: "coder",
105
+ agent_scope_source: "auto",
106
+ workspace_scope: "proj-alpha",
107
+ workspace_scope_source: "explicit",
108
+ scope_decided_at: "2026-09-15T00:00:00Z",
109
+ sensitivity: "personal",
110
+ occurred_at: "2026-09-14T12:00:00Z",
111
+ tags: ["shape", "annotated"],
112
+ importance: 5
113
+ });
114
+ // #230:document 指针行——doc_path 是 toApiList 的条件键,必须有一行真的产出它。
115
+ push({ type: "document", title: "shape-doc", content: "指针行摘要", doc_path: "X:\\seed\\doc.md", evidence: [] });
116
+ return rows;
117
+ }
118
+
119
+ test("工具矩阵与 TOOL_NAMES 清单一致(新增工具必须补进矩阵)", () => {
120
+ const { byName } = setup();
121
+ assert.deepEqual(Object.keys(byName).sort(), [...TOOL_NAMES].sort());
122
+ });
123
+
124
+ test("A 实跑:10 个工具每个可安全触达的分支,真实返回 ⊆ 声明的 output schema", async () => {
125
+ const { store, service, byName } = setup({ documentMemoryEnabled: true });
126
+ const shapes = seedShapes(store);
127
+ const annotated = shapes[shapes.length - 1];
128
+ const plain = shapes[0];
129
+ const driven = new Set();
130
+
131
+ const note = (name) => driven.add(name);
132
+
133
+ // --- memory_save:created 与 merged 两条分支 ---
134
+ await drive(byName, "memory_save", { type: "project", title: "save-new", content: "首次写入" });
135
+ note("memory_save");
136
+ await drive(byName, "memory_save", { type: "project", title: "save-new", content: "同键再写 → merged" });
137
+ // 显式 scope + 敏感度 + 事件时间:把「条件展开的键」真的写进库(#184 的真实入口)
138
+ const scopedSave = await drive(byName, "memory_save", {
139
+ type: "preference",
140
+ title: "save-scoped",
141
+ content: "带显式 scope 的写入",
142
+ agent_scope: "global",
143
+ workspace_scope: "proj-beta",
144
+ sensitivity: "personal",
145
+ occurred_at: "2026-09-16T00:00:00Z"
146
+ });
147
+ assert.ok(scopedSave.id, "memory_save 必须回传 id");
148
+
149
+ // --- memory_search:命中 / 空结果 / 显式 keyword 档 ---
150
+ const hit = await drive(byName, "memory_search", { query: "形态" });
151
+ assert.ok(hit.items.length > 0, "关键词检索应当命中种子行");
152
+ await drive(byName, "memory_search", { query: "绝不存在的检索词-195" });
153
+ await drive(byName, "memory_search", { query: "形态", mode: "keyword", limit: 2 });
154
+ // occurred 时间窗分支(tools.js 的 occurredWindow 接线)
155
+ await drive(byName, "memory_search", { query: "形态", occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
156
+ note("memory_search");
157
+
158
+ // --- memory_list:默认 / 类型过滤 / 含归档 / 分页 / occurred 窗口 ---
159
+ await drive(byName, "memory_list", {});
160
+ await drive(byName, "memory_list", { type: "project", limit: 1, offset: 1 });
161
+ await drive(byName, "memory_list", { include_archived: true });
162
+ await drive(byName, "memory_list", { occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
163
+ note("memory_list");
164
+
165
+ // --- memory_get:极简行 / 全量标注行(#184 的原始现场) ---
166
+ await drive(byName, "memory_get", { id: plain.id });
167
+ await drive(byName, "memory_get", { id: annotated.id });
168
+ note("memory_get");
169
+
170
+ // --- memory_update:字段修正 + 显式 scope 纠正(另一条会展开条件键的写路径) ---
171
+ await drive(byName, "memory_update", { id: plain.id, title: "shape-0 renamed", importance: 4 });
172
+ await drive(byName, "memory_update", {
173
+ id: plain.id,
174
+ content: "改过的正文",
175
+ agent_scope: "global",
176
+ workspace_scope: "proj-gamma",
177
+ reason: "issue #195 矩阵覆盖"
178
+ });
179
+ note("memory_update");
180
+
181
+ // --- memory_delete:删得掉 / 删不掉(不存在 → false,不抛错) ---
182
+ const doomed = store.save({ type: "history", title: "doomed", content: "待删", source: "test" });
183
+ const deleted = await drive(byName, "memory_delete", { id: doomed.id });
184
+ assert.equal(deleted.deleted, true);
185
+ const notDeleted = await drive(byName, "memory_delete", { id: "no-such-id-195" });
186
+ assert.equal(notDeleted.deleted, false);
187
+ note("memory_delete");
188
+
189
+ // --- memory_forget / memory_archive:两个方向的分支 ---
190
+ const forgetTarget = store.save({ type: "history", title: "forget-me", content: "待遗忘", source: "test" });
191
+ assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id })).memory.forgotten, true);
192
+ assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id, forgotten: false })).memory.forgotten, false);
193
+ note("memory_forget");
194
+
195
+ const archiveTarget = store.save({ type: "history", title: "archive-me", content: "待归档", source: "test" });
196
+ assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id })).memory.archived, true);
197
+ assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id, archived: false })).memory.archived, false);
198
+ note("memory_archive");
199
+
200
+ // --- memory_runtime:只驱动只读与「无载荷早退」两条分支 ---
201
+ // provision 会联网下载数十至上百 MB(且可能 adopt 本机载荷、硬链接数以百 MB 计),
202
+ // verify 命中载荷时会真实加载模型——两者都不属于单元测试该做的事。status 与其
203
+ // 「无载荷」早退分支已覆盖该 schema 的 summary/status/cost/payloadId/packages/reason
204
+ // 六个键;provision 侧的 strategy/files/bytes 与 verify 侧的 dimension 由 C 层
205
+ // 结构断言兜底(声明合规),分支行为本身留给 e2e/人工验证。
206
+ const emptyRuntimeDir = mkdtempSync(join(tmpdir(), "mneme-195-runtime-"));
207
+ try {
208
+ const rt = setup({ runtimeDir: emptyRuntimeDir });
209
+ await drive(rt.byName, "memory_runtime", { action: "status" });
210
+ await drive(rt.byName, "memory_runtime", { action: "verify" });
211
+ assert.ok(service, "service 保留引用,避免未使用变量告警");
212
+ note("memory_runtime");
213
+ } finally {
214
+ rmSync(emptyRuntimeDir, { recursive: true, force: true });
215
+ }
216
+
217
+ // --- memory_register_document(#230):created 分支 + evidence 求交(含丢弃)。
218
+ // superseded 分支由 document.test.js 覆盖;flag 关闭的拒绝路径同样在那边。
219
+ const docDir = mkdtempSync(join(tmpdir(), "mneme-230-doc-"));
220
+ const docFile = join(docDir, "report.md");
221
+ try {
222
+ writeFileSync(docFile, "# report\n\nbody", "utf8");
223
+ const reg = await drive(byName, "memory_register_document", {
224
+ path: docFile,
225
+ title: "matrix-doc",
226
+ summary: "指针行摘要",
227
+ tags: ["matrix"],
228
+ evidence: [plain.id, "fabricated-230"]
229
+ });
230
+ assert.equal(reg.action, "created");
231
+ assert.equal(reg.evidence_kept, 1);
232
+ assert.equal(reg.evidence_dropped, 1);
233
+ assert.equal(reg.degraded, true);
234
+ note("memory_register_document");
235
+ } finally {
236
+ rmSync(docDir, { recursive: true, force: true });
237
+ }
238
+
239
+ assert.deepEqual([...driven].sort(), [...TOOL_NAMES].sort(), "每个工具都必须至少被驱动一次(矩阵不允许空缺)");
240
+ });
241
+
242
+ test("A' 未命中 id 的三个工具按契约抛错(而不是返回半截对象)", async () => {
243
+ const { byName } = setup();
244
+ await assert.rejects(() => byName.memory_get.execute({ id: "missing-195" }), /memory not found/);
245
+ await assert.rejects(() => byName.memory_forget.execute({ id: "missing-195" }), /memory not found/);
246
+ await assert.rejects(() => byName.memory_archive.execute({ id: "missing-195" }), /memory not found/);
247
+ });
248
+
249
+ test("B DTO 产地:toApiList 全形态输出 ⊆ MEMORY_ITEM_SCHEMA,且声明键全部被真实产出", () => {
250
+ const { store, service } = setup();
251
+ seedShapes(store);
252
+ const rows = service.toApiList(service.list({ limit: 100 }));
253
+ assert.ok(rows.length >= 5, "种子行应当全部可读回");
254
+
255
+ const observed = new Set();
256
+ for (const dto of rows) {
257
+ const errors = validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto);
258
+ assert.deepEqual(errors, [], `toApiList 产出的 DTO 未被 MEMORY_ITEM_SCHEMA 覆盖:${JSON.stringify(errors)}`);
259
+ for (const key of Object.keys(dto)) observed.add(key);
260
+ }
261
+
262
+ // 反向:声明的每个键都必须有形态真的产出它。这条让 schema 与种子互相咬合——
263
+ // 以后给 toApiList 增键却忘了补种子,会在这里失败并提醒补形态。
264
+ const declared = Object.keys(MEMORY_ITEM_SCHEMA.properties);
265
+ const neverProduced = declared.filter((key) => !observed.has(key));
266
+ assert.deepEqual(neverProduced, [], `MEMORY_ITEM_SCHEMA 声明了但矩阵从未产出的键(补种子形态或删死声明):${neverProduced.join(", ")}`);
267
+ });
268
+
269
+ test("B' search / list / get 三个消费方共用同一份 item schema(#184 的根因是有人另抄了一份)", () => {
270
+ const { byName } = setup();
271
+ const itemSchemaOf = (name) => {
272
+ const props = byName[name].output.schema.properties;
273
+ return name === "memory_get" ? props.memory : props.items.items;
274
+ };
275
+ const search = itemSchemaOf("memory_search");
276
+ const list = itemSchemaOf("memory_list");
277
+ const get = itemSchemaOf("memory_get");
278
+ const keys = (s) => Object.keys(s.properties).sort();
279
+
280
+ assert.deepEqual(keys(search), keys(MEMORY_ITEM_SCHEMA));
281
+ assert.deepEqual(keys(list), keys(search));
282
+ assert.deepEqual(keys(get), keys(search));
283
+ for (const [name, schema] of [["search", search], ["list", list], ["get", get]]) {
284
+ assert.equal(schema.additionalProperties, false, `${name} 的 item schema 必须闭合`);
285
+ }
286
+ });
287
+
288
+ test("C 结构:全部 10 个工具的 output schema 闭合、required ⊆ properties、每项带 type", () => {
289
+ const { byName } = setup();
290
+ for (const name of TOOL_NAMES) {
291
+ const schema = byName[name].output.schema;
292
+ assert.ok(schema, `${name} 必须声明 output.schema`);
293
+ assert.equal(schema.additionalProperties, false, `${name} 的 schema 必须闭合(否则未声明键不会被拒收)`);
294
+ assert.equal(schema.type, "object", `${name} 的顶层 schema 必须是 object`);
295
+ for (const key of schema.required ?? []) {
296
+ assert.ok(schema.properties?.[key], `${name}.required 里的 ${key} 不在 properties 中`);
297
+ }
298
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
299
+ assert.ok(prop.type, `${name}.${key} 缺 type——没有 type 的声明校验器无法真正约束它`);
300
+ }
301
+ }
302
+ });
303
+
304
+ test("D 负例锁:注入未声明键时校验器必须报错(否则 A/B/C 三层都是空转)", () => {
305
+ const { store, service, byName } = setup();
306
+ const row = store.save({ type: "project", title: "negative-control", content: "负例", source: "test" });
307
+ const dto = service.toApiList([store.getById(row.id)])[0];
308
+
309
+ // 正例:干净 DTO 必须通过
310
+ assert.deepEqual(validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto), []);
311
+ assert.deepEqual(validateJsonSchemaValue(byName.memory_get.output.schema, { memory: dto }), []);
312
+
313
+ // 负例:多一个键就必须被拒——这正是 #184 的形状
314
+ const poisoned = { ...dto, agent_scope_source_new: "auto" };
315
+ assert.notDeepEqual(
316
+ validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, poisoned),
317
+ [],
318
+ "未声明键必须被拒收:#184 类漂移就是靠这条被抓出来的"
319
+ );
320
+ assert.notDeepEqual(
321
+ validateJsonSchemaValue(byName.memory_get.output.schema, { memory: poisoned }),
322
+ [],
323
+ "memory_get 的 schema 同样必须拒收未声明键"
324
+ );
325
+ });
@@ -1,65 +1,65 @@
1
- // #184 回归:memory_get 的内联输出 schema 漏声明 v0.8.1 的 scope 来源三键
2
- // (agent_scope_source / workspace_scope_source / scope_decided_at),且
3
- // additionalProperties:false → toApiList 对被标注行条件展开这些键后,宿主
4
- // in-process 校验直接拒收("invalid output"),表现为「个别 id 必失败、多数
5
- // 正常」。本文件用生产同款校验器(@deepseek-ai/dsh-tools 的
6
- // validateJsonSchemaValue)复现并防漂移;修复 = memory_get 复用与
7
- // list/search 共享的 MEMORY_ITEM_SCHEMA,不再维护内联副本。
8
- import test from "node:test";
9
- import assert from "node:assert/strict";
10
- import { validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
11
- import { createStore } from "../src/store.js";
12
- import { createService } from "../src/service.js";
13
- import { createTools, MEMORY_ITEM_SCHEMA } from "../src/tools.js";
14
-
15
- function setup() {
16
- const store = createStore(":memory:");
17
- const service = createService({ store, mirror: null, config: {} });
18
- const registered = [];
19
- const ctx = {
20
- tools: {
21
- register(def) {
22
- registered.push(def);
23
- return () => {};
24
- }
25
- }
26
- };
27
- createTools(ctx, service, {}, undefined);
28
- const byName = Object.fromEntries(registered.map((def) => [def.name, def]));
29
- return { store, service, byName };
30
- }
31
-
32
- // memory_get.execute 的返回 = { memory: toApiList([getById(id)])[0] }——契约
33
- // 测试直接对「toApiList 产出的 DTO ↔ 声明的 output schema」做校验(DTO 键的
34
- // 唯一来源就是 toApiList,execute 只是包装)。
35
- test("memory_get output validates for fully scope-annotated rows (#184)", () => {
36
- const { store, service, byName } = setup();
37
- const saved = store.save({
38
- type: "preference",
39
- title: "annotated row",
40
- content: "带全量 scope 来源标注的记忆行",
41
- source: "test",
42
- agent_scope: "coder",
43
- agent_scope_source: "auto",
44
- workspace_scope: "proj-alpha",
45
- workspace_scope_source: "auto",
46
- scope_decided_at: "2026-09-15T00:00:00Z"
47
- });
48
- const get = byName.memory_get;
49
- const dto = service.toApiList([store.getById(saved.id)])[0];
50
- const res = { memory: dto };
51
- assert.deepEqual(validateJsonSchemaValue(get.output.schema, res), []);
52
- });
53
-
54
- test("memory_get item schema reuses MEMORY_ITEM_SCHEMA (no inline copy) (#184)", () => {
55
- const { byName } = setup();
56
- const get = byName.memory_get;
57
- // defineTool 编译会克隆 schema,恒等不成立——断言键集与严格性同源。
58
- const compiled = get.output.schema.properties.memory;
59
- assert.deepEqual(
60
- Object.keys(compiled.properties).sort(),
61
- Object.keys(MEMORY_ITEM_SCHEMA.properties).sort()
62
- );
63
- assert.equal(compiled.additionalProperties, false);
64
- assert.deepEqual(compiled.required.sort(), ["content", "created_at", "id", "importance", "title", "type", "updated_at"]);
65
- });
1
+ // #184 回归:memory_get 的内联输出 schema 漏声明 v0.8.1 的 scope 来源三键
2
+ // (agent_scope_source / workspace_scope_source / scope_decided_at),且
3
+ // additionalProperties:false → toApiList 对被标注行条件展开这些键后,宿主
4
+ // in-process 校验直接拒收("invalid output"),表现为「个别 id 必失败、多数
5
+ // 正常」。本文件用生产同款校验器(@deepseek-ai/dsh-tools 的
6
+ // validateJsonSchemaValue)复现并防漂移;修复 = memory_get 复用与
7
+ // list/search 共享的 MEMORY_ITEM_SCHEMA,不再维护内联副本。
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
11
+ import { createStore } from "../src/store.js";
12
+ import { createService } from "../src/service.js";
13
+ import { createTools, MEMORY_ITEM_SCHEMA } from "../src/tools.js";
14
+
15
+ function setup() {
16
+ const store = createStore(":memory:");
17
+ const service = createService({ store, mirror: null, config: {} });
18
+ const registered = [];
19
+ const ctx = {
20
+ tools: {
21
+ register(def) {
22
+ registered.push(def);
23
+ return () => {};
24
+ }
25
+ }
26
+ };
27
+ createTools(ctx, service, {}, undefined);
28
+ const byName = Object.fromEntries(registered.map((def) => [def.name, def]));
29
+ return { store, service, byName };
30
+ }
31
+
32
+ // memory_get.execute 的返回 = { memory: toApiList([getById(id)])[0] }——契约
33
+ // 测试直接对「toApiList 产出的 DTO ↔ 声明的 output schema」做校验(DTO 键的
34
+ // 唯一来源就是 toApiList,execute 只是包装)。
35
+ test("memory_get output validates for fully scope-annotated rows (#184)", () => {
36
+ const { store, service, byName } = setup();
37
+ const saved = store.save({
38
+ type: "preference",
39
+ title: "annotated row",
40
+ content: "带全量 scope 来源标注的记忆行",
41
+ source: "test",
42
+ agent_scope: "coder",
43
+ agent_scope_source: "auto",
44
+ workspace_scope: "proj-alpha",
45
+ workspace_scope_source: "auto",
46
+ scope_decided_at: "2026-09-15T00:00:00Z"
47
+ });
48
+ const get = byName.memory_get;
49
+ const dto = service.toApiList([store.getById(saved.id)])[0];
50
+ const res = { memory: dto };
51
+ assert.deepEqual(validateJsonSchemaValue(get.output.schema, res), []);
52
+ });
53
+
54
+ test("memory_get item schema reuses MEMORY_ITEM_SCHEMA (no inline copy) (#184)", () => {
55
+ const { byName } = setup();
56
+ const get = byName.memory_get;
57
+ // defineTool 编译会克隆 schema,恒等不成立——断言键集与严格性同源。
58
+ const compiled = get.output.schema.properties.memory;
59
+ assert.deepEqual(
60
+ Object.keys(compiled.properties).sort(),
61
+ Object.keys(MEMORY_ITEM_SCHEMA.properties).sort()
62
+ );
63
+ assert.equal(compiled.additionalProperties, false);
64
+ assert.deepEqual(compiled.required.sort(), ["content", "created_at", "id", "importance", "title", "type", "updated_at"]);
65
+ });