@modusensus/dsh-mneme 0.13.0 → 0.14.0-beta.2

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 (543) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +830 -828
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +964 -964
  5. package/bin/dsh-mneme-choro.mjs +262 -254
  6. package/bin/dsh-mneme-mcp.mjs +510 -510
  7. package/bin/dsh-mneme-serve.mjs +151 -151
  8. package/cordis.patch.yml +21 -21
  9. package/docs/CONFIGURATION.md +407 -402
  10. package/docs/CONTINUITY.md +139 -139
  11. package/docs/DAEMON.md +57 -57
  12. package/docs/DIRECTORIES.md +114 -114
  13. package/docs/DISTILL.md +397 -371
  14. package/docs/DSH-GO.md +97 -97
  15. package/docs/ENTITIES.md +247 -247
  16. package/docs/FORGET.md +77 -77
  17. package/docs/KB.md +110 -110
  18. package/docs/LOCAL_MODEL.md +222 -222
  19. package/docs/SEMANTIC.md +256 -256
  20. package/docs/SHARE.md +552 -552
  21. package/docs/SLEEP.md +202 -202
  22. package/docs/STORAGE.md +122 -122
  23. package/docs/WORKFLOWS.md +366 -366
  24. package/lib/api-standalone.js +1007 -1007
  25. package/lib/api.js +3600 -3394
  26. package/lib/attachment-text.js +122 -122
  27. package/lib/bootstrap-http.js +105 -105
  28. package/lib/bootstrap.js +215 -215
  29. package/lib/candidates.js +454 -454
  30. package/lib/capabilities.js +819 -819
  31. package/lib/choro-command.js +111 -111
  32. package/lib/client.js +14420 -13436
  33. package/lib/commands.js +95 -95
  34. package/lib/config-keys.js +144 -144
  35. package/lib/config.js +1361 -1317
  36. package/lib/content-hash.js +54 -54
  37. package/lib/continuity.js +258 -258
  38. package/lib/date-hint.js +32 -0
  39. package/lib/diary/auto.js +190 -190
  40. package/lib/diary/compose.js +440 -440
  41. package/lib/diary/dir.js +59 -59
  42. package/lib/diary/export.js +97 -97
  43. package/lib/diary/index.js +26 -26
  44. package/lib/diary/library.js +189 -189
  45. package/lib/diary/qq-inbox.js +142 -142
  46. package/lib/diary/rollover.js +61 -61
  47. package/lib/diary/session-provider.js +122 -122
  48. package/lib/document-index.js +94 -94
  49. package/lib/document.js +322 -322
  50. package/lib/dream/clustering.js +123 -123
  51. package/lib/dream/decisions.js +945 -907
  52. package/lib/dream/narratives.js +44 -44
  53. package/lib/dream/sleep.js +2027 -1887
  54. package/lib/dream/triage.js +139 -0
  55. package/lib/dream.js +1879 -1764
  56. package/lib/dsh-go/host.js +162 -162
  57. package/lib/dsh-go/panel.js +2091 -2091
  58. package/lib/dsh-go/text.js +342 -342
  59. package/lib/dsh-go/views/common.js +68 -68
  60. package/lib/dsh-go/views/entity.js +48 -48
  61. package/lib/dsh-go/views/memory.js +198 -198
  62. package/lib/dsh-go/views/root.js +30 -30
  63. package/lib/dsh-go/views/settings.js +398 -398
  64. package/lib/dsh-go/views/status.js +51 -51
  65. package/lib/embedding.js +342 -342
  66. package/lib/entities/extractor.js +285 -285
  67. package/lib/fence.js +24 -24
  68. package/lib/graph/anchoring.js +184 -184
  69. package/lib/guide.js +74 -74
  70. package/lib/heat.js +144 -144
  71. package/lib/hot-memory.js +119 -107
  72. package/lib/image-describe.js +158 -0
  73. package/lib/index.js +852 -822
  74. package/lib/inject.js +656 -639
  75. package/lib/kb/index.js +309 -309
  76. package/lib/kb/routes.js +219 -219
  77. package/lib/kb/tools.js +148 -148
  78. package/lib/lang.js +607 -584
  79. package/lib/local-embedder.js +358 -358
  80. package/lib/maintenance.js +162 -162
  81. package/lib/memory-source.js +9 -9
  82. package/lib/mirror.js +330 -330
  83. package/lib/organize.js +456 -456
  84. package/lib/peak-hours.js +172 -172
  85. package/lib/pipeline-activities.js +178 -178
  86. package/lib/pipeline-attribution.js +165 -165
  87. package/lib/pipeline-command.js +279 -279
  88. package/lib/pipeline-confirm.js +142 -142
  89. package/lib/pipeline-exec.js +174 -174
  90. package/lib/pipeline-nudge-auto.js +129 -129
  91. package/lib/pipeline-nudge.js +1623 -1623
  92. package/lib/pipeline-observations.js +181 -181
  93. package/lib/pipeline-observe.js +133 -133
  94. package/lib/pipeline-runner.js +361 -361
  95. package/lib/pipeline-runs.js +442 -442
  96. package/lib/pipeline.js +716 -716
  97. package/lib/placement.js +13 -13
  98. package/lib/pre-inject-gate.js +354 -354
  99. package/lib/profile-extract.js +164 -164
  100. package/lib/profile-facts.js +356 -356
  101. package/lib/provenance-http.js +251 -251
  102. package/lib/quality-filter.js +162 -162
  103. package/lib/recall-stats.js +190 -190
  104. package/lib/reclaim-http.js +150 -150
  105. package/lib/reranker.js +269 -269
  106. package/lib/runtime/adopt.js +176 -176
  107. package/lib/runtime/closure.js +172 -172
  108. package/lib/runtime/download.js +247 -247
  109. package/lib/runtime/layout.js +265 -265
  110. package/lib/runtime/loader.js +252 -252
  111. package/lib/runtime/model-fetch.js +361 -361
  112. package/lib/runtime/provision.js +237 -237
  113. package/lib/runtime/tarball.js +130 -130
  114. package/lib/runtime/verify.js +223 -223
  115. package/lib/safe-slice.js +18 -18
  116. package/lib/scope-state.js +277 -277
  117. package/lib/scope.js +187 -187
  118. package/lib/search/adaptive.js +60 -60
  119. package/lib/search/bm25.js +96 -96
  120. package/lib/semantic.js +572 -572
  121. package/lib/sensitive-scan.js +209 -209
  122. package/lib/serve.js +228 -228
  123. package/lib/service.js +3268 -3196
  124. package/lib/session-gate.js +91 -91
  125. package/lib/session-usage.js +350 -350
  126. package/lib/settings.js +827 -802
  127. package/lib/share-http.js +281 -281
  128. package/lib/share-import.js +234 -234
  129. package/lib/share.js +335 -335
  130. package/lib/store.js +5895 -5662
  131. package/lib/summarize.js +1388 -1343
  132. package/lib/tools.js +1370 -1356
  133. package/lib/user-settings.js +230 -230
  134. package/lib/vector-index.js +180 -180
  135. package/lib/version-check.js +122 -122
  136. package/lib/wiki/claims.js +267 -0
  137. package/lib/wiki/cli-hint.js +90 -90
  138. package/lib/wiki/inject.js +164 -164
  139. package/lib/wiki/partition.js +200 -200
  140. package/lib/wiki/plan.js +214 -214
  141. package/lib/wiki/queries.js +640 -474
  142. package/lib/wiki/render.js +261 -187
  143. package/lib/wiki/scan.js +371 -371
  144. package/lib/wiki/t0.js +500 -463
  145. package/lib/wiki/t1-jobs.js +232 -232
  146. package/lib/wiki/t1.js +609 -502
  147. package/lib/write-admission.js +404 -404
  148. package/locale/en.json +6 -6
  149. package/locale/zh.json +6 -6
  150. package/package.json +15 -1
  151. package/runtime-manifest.json +526 -526
  152. package/scripts/benchmark-embed.js +201 -201
  153. package/scripts/benchmark-recall.js +249 -249
  154. package/scripts/benchmark-rerank.js +166 -166
  155. package/scripts/build-runtime-manifest.mjs +224 -224
  156. package/scripts/changelog-prep.mjs +91 -91
  157. package/scripts/check-sync.js +44 -44
  158. package/scripts/e2e-dsh.js +220 -220
  159. package/scripts/mneme-runtime.mjs +277 -277
  160. package/scripts/publish-gate.mjs +197 -197
  161. package/scripts/release-docs.mjs +210 -204
  162. package/scripts/release-notes.mjs +164 -161
  163. package/scripts/stress-dsh.js +255 -255
  164. package/scripts/sync-lib.js +47 -47
  165. package/scripts/sync-test-badge.mjs +69 -69
  166. package/scripts/test-count-sync.mjs +48 -48
  167. package/src/api-standalone.js +1007 -1007
  168. package/src/api.js +3600 -3394
  169. package/src/attachment-text.js +122 -122
  170. package/src/bootstrap-http.js +105 -105
  171. package/src/bootstrap.js +215 -215
  172. package/src/candidates.js +454 -454
  173. package/src/capabilities.js +819 -819
  174. package/src/choro-command.js +111 -111
  175. package/src/commands.js +95 -95
  176. package/src/config-keys.js +144 -144
  177. package/src/config.js +1361 -1317
  178. package/src/content-hash.js +54 -54
  179. package/src/continuity.js +258 -258
  180. package/src/date-hint.js +32 -0
  181. package/src/diary/auto.js +190 -190
  182. package/src/diary/compose.js +440 -440
  183. package/src/diary/dir.js +59 -59
  184. package/src/diary/export.js +97 -97
  185. package/src/diary/index.js +26 -26
  186. package/src/diary/library.js +189 -189
  187. package/src/diary/qq-inbox.js +142 -142
  188. package/src/diary/rollover.js +61 -61
  189. package/src/diary/session-provider.js +122 -122
  190. package/src/document-index.js +94 -94
  191. package/src/document.js +322 -322
  192. package/src/dream/clustering.js +123 -123
  193. package/src/dream/decisions.js +945 -907
  194. package/src/dream/narratives.js +44 -44
  195. package/src/dream/sleep.js +2027 -1887
  196. package/src/dream/triage.js +139 -0
  197. package/src/dream.js +1879 -1764
  198. package/src/dsh-go/host.js +162 -162
  199. package/src/dsh-go/panel.js +2091 -2091
  200. package/src/dsh-go/text.js +342 -342
  201. package/src/dsh-go/views/common.js +68 -68
  202. package/src/dsh-go/views/entity.js +48 -48
  203. package/src/dsh-go/views/memory.js +198 -198
  204. package/src/dsh-go/views/root.js +30 -30
  205. package/src/dsh-go/views/settings.js +398 -398
  206. package/src/dsh-go/views/status.js +51 -51
  207. package/src/embedding.js +342 -342
  208. package/src/entities/extractor.js +285 -285
  209. package/src/fence.js +24 -24
  210. package/src/graph/anchoring.js +184 -184
  211. package/src/guide.js +74 -74
  212. package/src/heat.js +144 -144
  213. package/src/hot-memory.js +119 -107
  214. package/src/image-describe.js +158 -0
  215. package/src/index.js +852 -822
  216. package/src/inject.js +656 -639
  217. package/src/kb/index.js +309 -309
  218. package/src/kb/routes.js +219 -219
  219. package/src/kb/tools.js +148 -148
  220. package/src/lang.js +607 -584
  221. package/src/local-embedder.js +358 -358
  222. package/src/maintenance.js +162 -162
  223. package/src/memory-source.js +9 -9
  224. package/src/mirror.js +330 -330
  225. package/src/organize.js +456 -456
  226. package/src/peak-hours.js +172 -172
  227. package/src/pipeline-activities.js +178 -178
  228. package/src/pipeline-attribution.js +165 -165
  229. package/src/pipeline-command.js +279 -279
  230. package/src/pipeline-confirm.js +142 -142
  231. package/src/pipeline-exec.js +174 -174
  232. package/src/pipeline-nudge-auto.js +129 -129
  233. package/src/pipeline-nudge.js +1623 -1623
  234. package/src/pipeline-observations.js +181 -181
  235. package/src/pipeline-observe.js +133 -133
  236. package/src/pipeline-runner.js +361 -361
  237. package/src/pipeline-runs.js +442 -442
  238. package/src/pipeline.js +716 -716
  239. package/src/placement.js +13 -13
  240. package/src/pre-inject-gate.js +354 -354
  241. package/src/profile-extract.js +164 -164
  242. package/src/profile-facts.js +356 -356
  243. package/src/provenance-http.js +251 -251
  244. package/src/quality-filter.js +162 -162
  245. package/src/recall-stats.js +190 -190
  246. package/src/reclaim-http.js +150 -150
  247. package/src/reranker.js +269 -269
  248. package/src/runtime/adopt.js +176 -176
  249. package/src/runtime/closure.js +172 -172
  250. package/src/runtime/download.js +247 -247
  251. package/src/runtime/layout.js +265 -265
  252. package/src/runtime/loader.js +252 -252
  253. package/src/runtime/model-fetch.js +361 -361
  254. package/src/runtime/provision.js +237 -237
  255. package/src/runtime/tarball.js +130 -130
  256. package/src/runtime/verify.js +223 -223
  257. package/src/safe-slice.js +18 -18
  258. package/src/scope-state.js +277 -277
  259. package/src/scope.js +187 -187
  260. package/src/search/adaptive.js +60 -60
  261. package/src/search/bm25.js +96 -96
  262. package/src/semantic.js +572 -572
  263. package/src/sensitive-scan.js +209 -209
  264. package/src/serve.js +228 -228
  265. package/src/service.js +3268 -3196
  266. package/src/session-gate.js +91 -91
  267. package/src/session-usage.js +350 -350
  268. package/src/settings.js +827 -802
  269. package/src/share-http.js +281 -281
  270. package/src/share-import.js +234 -234
  271. package/src/share.js +335 -335
  272. package/src/store.js +5895 -5662
  273. package/src/summarize.js +1388 -1343
  274. package/src/tools.js +1370 -1356
  275. package/src/user-settings.js +230 -230
  276. package/src/vector-index.js +180 -180
  277. package/src/version-check.js +122 -122
  278. package/src/wiki/claims.js +267 -0
  279. package/src/wiki/cli-hint.js +90 -90
  280. package/src/wiki/inject.js +164 -164
  281. package/src/wiki/partition.js +200 -200
  282. package/src/wiki/plan.js +214 -214
  283. package/src/wiki/queries.js +640 -474
  284. package/src/wiki/render.js +261 -187
  285. package/src/wiki/scan.js +371 -371
  286. package/src/wiki/t0.js +500 -463
  287. package/src/wiki/t1-jobs.js +232 -232
  288. package/src/wiki/t1.js +609 -502
  289. package/src/write-admission.js +404 -404
  290. package/test/adaptive-inject-budget.test.js +85 -85
  291. package/test/api-hardening.test.js +173 -173
  292. package/test/api-nudge-route.test.js +259 -259
  293. package/test/api-routes-gaps.test.js +1092 -1092
  294. package/test/api.test.js +3007 -2988
  295. package/test/attachment-text.test.js +139 -139
  296. package/test/audit.test.js +448 -448
  297. package/test/baseline-persist.test.js +248 -248
  298. package/test/benchmark.test.js +94 -94
  299. package/test/bm25-corpus-cache.test.js +145 -145
  300. package/test/bootstrap.test.js +228 -228
  301. package/test/candidates.test.js +345 -345
  302. package/test/capabilities.test.js +577 -577
  303. package/test/capacity-warning.test.js +262 -262
  304. package/test/changelog-prep.test.js +125 -125
  305. package/test/choro-command.test.js +218 -218
  306. package/test/cli-enums.test.js +37 -37
  307. package/test/cli-flag-value.test.js +86 -86
  308. package/test/cli-receive.test.js +390 -390
  309. package/test/cli-share.test.js +437 -437
  310. package/test/client-share.test.js +225 -225
  311. package/test/client.test.js +4595 -4184
  312. package/test/clustering.test.js +100 -100
  313. package/test/commands.test.js +106 -106
  314. package/test/config-keys-routes.test.js +233 -233
  315. package/test/config.test.js +153 -153
  316. package/test/conflict-freeze.test.js +344 -344
  317. package/test/conflict-ledger-enforce.test.js +452 -0
  318. package/test/conflict-ledger.test.js +242 -0
  319. package/test/conflict-reviewed.test.js +79 -79
  320. package/test/content-hash.test.js +204 -204
  321. package/test/continuity.test.js +452 -452
  322. package/test/contrast.test.js +432 -320
  323. package/test/date-hint.test.js +156 -0
  324. package/test/dedupe-candidate-window.test.js +180 -180
  325. package/test/dependency-overrides.test.js +78 -78
  326. package/test/diary-library.test.js +187 -187
  327. package/test/diary.test.js +576 -576
  328. package/test/differentiate-feedback.test.js +248 -248
  329. package/test/distill-tx-embed-gap.test.js +110 -110
  330. package/test/docs-config-keys.test.js +65 -65
  331. package/test/document-dir.test.js +233 -233
  332. package/test/document.test.js +703 -703
  333. package/test/dream-archive-guardrail.test.js +116 -116
  334. package/test/dream-baseline-clamp.test.js +174 -0
  335. package/test/dream-candidate-hybrid.test.js +281 -281
  336. package/test/dream-coverage-degrade.test.js +86 -86
  337. package/test/dream-decision-evidence.test.js +314 -314
  338. package/test/dream-failure-backoff.test.js +173 -173
  339. package/test/dream-fence.test.js +207 -207
  340. package/test/dream-maxtokens-fallback.test.js +132 -132
  341. package/test/dream-merge-guardrail.test.js +219 -219
  342. package/test/dream-peak-hours.test.js +194 -194
  343. package/test/dream.test.js +1528 -1528
  344. package/test/dsh-go-host.test.js +134 -134
  345. package/test/dsh-go-panel-actions.test.js +854 -854
  346. package/test/dsh-go-panel.test.js +251 -251
  347. package/test/embed-ready-poll.test.js +94 -94
  348. package/test/embed-text-reasons.test.js +112 -112
  349. package/test/embedding-cache-f32.test.js +276 -276
  350. package/test/embedding-ready.test.js +226 -226
  351. package/test/entities.test.js +711 -711
  352. package/test/entity-recall.test.js +179 -179
  353. package/test/epistemic.test.js +325 -325
  354. package/test/failure-query.test.js +147 -147
  355. package/test/failure-retention.test.js +105 -105
  356. package/test/fnew-0112.test.js +311 -311
  357. package/test/fnew-03.test.js +422 -422
  358. package/test/forget-corrections.test.js +70 -70
  359. package/test/graph-anchoring.test.js +184 -184
  360. package/test/graph-api.test.js +333 -209
  361. package/test/graph-boosted-relations.test.js +189 -189
  362. package/test/graph-inject-budget.test.js +103 -103
  363. package/test/graph-multipath.test.js +269 -269
  364. package/test/graph-passive-confirm.test.js +91 -91
  365. package/test/graph-weight-evolution.test.js +104 -104
  366. package/test/heat.test.js +175 -175
  367. package/test/helpers/dream-mock.js +86 -86
  368. package/test/helpers/peer-worker.mjs +25 -25
  369. package/test/helpers/runtime-source.js +60 -60
  370. package/test/helpers/tar-builder.js +80 -80
  371. package/test/helpers/write-admission-samples.js +312 -312
  372. package/test/hot-memory.test.js +259 -211
  373. package/test/image-description.test.js +271 -0
  374. package/test/image-memory.test.js +170 -170
  375. package/test/inject-content-cap.test.js +91 -91
  376. package/test/inject-first-round-bm25.test.js +89 -89
  377. package/test/inject-heat.test.js +77 -77
  378. package/test/inject-parent-gate.test.js +134 -106
  379. package/test/inject-pin-explicit.test.js +300 -300
  380. package/test/inject-pin-vector-crowd.test.js +225 -225
  381. package/test/inject-pools.test.js +195 -195
  382. package/test/inject-recall.test.js +172 -172
  383. package/test/inject-rotation.test.js +161 -161
  384. package/test/inject.test.js +553 -494
  385. package/test/issue-617-step1-query-and-fusion.test.js +361 -361
  386. package/test/kb-api-wiring.test.js +128 -128
  387. package/test/kb-collections.test.js +272 -272
  388. package/test/kb-routes.test.js +379 -379
  389. package/test/lang.test.js +98 -98
  390. package/test/lastrunat-persist.test.js +101 -101
  391. package/test/lib-smoke.test.js +116 -116
  392. package/test/llm-audit.test.js +815 -815
  393. package/test/llm-message-source.test.js +207 -207
  394. package/test/local-embedder.test.js +292 -292
  395. package/test/maintenance.test.js +501 -501
  396. package/test/mcp-stdio.test.js +385 -385
  397. package/test/mint-source.test.js +311 -311
  398. package/test/mirror-dirty.test.js +424 -424
  399. package/test/mirror-edit-digest.test.js +187 -187
  400. package/test/mirror-generation.test.js +499 -499
  401. package/test/mirror-types.test.js +208 -208
  402. package/test/mirror.test.js +249 -249
  403. package/test/narrative-bars.test.js +162 -162
  404. package/test/organize-tool.test.js +232 -232
  405. package/test/organize.test.js +418 -418
  406. package/test/panel-chart-scale.test.js +90 -0
  407. package/test/panel-tokens.test.js +245 -0
  408. package/test/peak-hours-observability.test.js +70 -70
  409. package/test/peer-blockers.test.js +193 -193
  410. package/test/persona-addressing.test.js +83 -83
  411. package/test/persona-api.test.js +172 -172
  412. package/test/persona-by-session.test.js +568 -568
  413. package/test/persona-flag.test.js +36 -36
  414. package/test/persona-inject.test.js +145 -145
  415. package/test/persona-settings.test.js +198 -198
  416. package/test/pipeline-activities.test.js +155 -155
  417. package/test/pipeline-attribution.test.js +175 -175
  418. package/test/pipeline-command.test.js +243 -243
  419. package/test/pipeline-confirm.test.js +148 -148
  420. package/test/pipeline-exec.test.js +172 -172
  421. package/test/pipeline-nudge-auto.test.js +248 -248
  422. package/test/pipeline-nudge.test.js +1278 -1278
  423. package/test/pipeline-observations.test.js +157 -157
  424. package/test/pipeline-observe.test.js +127 -127
  425. package/test/pipeline-r1r2.test.js +278 -278
  426. package/test/pipeline-runner.test.js +468 -468
  427. package/test/pipeline-runs.test.js +767 -767
  428. package/test/pipeline.test.js +636 -636
  429. package/test/placement.test.js +36 -36
  430. package/test/policy-epoch.test.js +259 -259
  431. package/test/pre-inject-gate.test.js +444 -444
  432. package/test/profile-extract.test.js +269 -269
  433. package/test/profile-facts.test.js +492 -492
  434. package/test/prompt-stance.test.js +128 -128
  435. package/test/provenance-http.test.js +367 -367
  436. package/test/publish-gate.test.js +272 -272
  437. package/test/quality-backfill.test.js +290 -290
  438. package/test/quality-filter.test.js +295 -295
  439. package/test/readme-claims.test.js +113 -113
  440. package/test/reasoning-effort.test.js +752 -752
  441. package/test/recall-anti-update.test.js +82 -82
  442. package/test/recall-evals.test.js +253 -253
  443. package/test/recall-layer.test.js +326 -326
  444. package/test/recall-retention.test.js +128 -128
  445. package/test/recall-stats.test.js +274 -274
  446. package/test/receipt-chain.test.js +451 -451
  447. package/test/reclaim-http.test.js +209 -209
  448. package/test/reflection-update-gate.test.js +129 -129
  449. package/test/reflection.test.js +226 -226
  450. package/test/reindex-backfill.test.js +475 -475
  451. package/test/reindex-scheduler.test.js +369 -369
  452. package/test/release-docs.test.js +156 -136
  453. package/test/release-notes.test.js +265 -240
  454. package/test/release-workflow.test.js +93 -93
  455. package/test/reranker-single-logit.test.js +73 -73
  456. package/test/reranker.test.js +197 -197
  457. package/test/resident-status.test.js +83 -83
  458. package/test/route-policy.test.js +113 -113
  459. package/test/runtime-adopt.test.js +206 -206
  460. package/test/runtime-cli.test.js +89 -89
  461. package/test/runtime-closure.test.js +193 -193
  462. package/test/runtime-download.test.js +302 -302
  463. package/test/runtime-layout.test.js +163 -163
  464. package/test/runtime-loader.test.js +255 -255
  465. package/test/runtime-manifest.test.js +159 -159
  466. package/test/runtime-model-fetch.test.js +348 -348
  467. package/test/runtime-provision.test.js +101 -101
  468. package/test/runtime-tarball.test.js +81 -81
  469. package/test/runtime-verify.test.js +319 -319
  470. package/test/scope-candidates.test.js +491 -491
  471. package/test/scope-distill-stamp.test.js +134 -134
  472. package/test/scope-explicit.test.js +452 -452
  473. package/test/scope-inject-soft.test.js +106 -106
  474. package/test/scope-optional-service.test.js +110 -110
  475. package/test/scope-retrieval.test.js +197 -197
  476. package/test/scope-state.test.js +610 -610
  477. package/test/scope-storage.test.js +255 -255
  478. package/test/scope-strict.test.js +367 -367
  479. package/test/search-fusion.test.js +90 -90
  480. package/test/security-doc.test.js +188 -178
  481. package/test/semantic.test.js +124 -124
  482. package/test/sensitive-scan.test.js +251 -251
  483. package/test/serve-bin.test.js +121 -121
  484. package/test/serve.test.js +258 -258
  485. package/test/service-search.test.js +174 -174
  486. package/test/service.test.js +410 -410
  487. package/test/session-gate.test.js +264 -264
  488. package/test/session-usage.test.js +317 -317
  489. package/test/settings.test.js +273 -273
  490. package/test/share-foreign-rows.test.js +144 -144
  491. package/test/share-http.test.js +144 -144
  492. package/test/share-import.test.js +577 -577
  493. package/test/share-imported-guard.test.js +311 -311
  494. package/test/share-imported-identity.test.js +111 -111
  495. package/test/share-pack.test.js +511 -511
  496. package/test/share-receive-http.test.js +756 -756
  497. package/test/share-routes.test.js +337 -337
  498. package/test/share-store.test.js +133 -133
  499. package/test/share-validate.test.js +101 -101
  500. package/test/skip-invalid-restore.test.js +262 -262
  501. package/test/sleep-action-set.test.js +441 -441
  502. package/test/sleep-catchup.test.js +315 -315
  503. package/test/sleep-failure-backoff.test.js +156 -156
  504. package/test/sleep-heat.test.js +174 -174
  505. package/test/sleep-scheduler-rearm.test.js +89 -89
  506. package/test/sleep.test.js +1455 -1418
  507. package/test/standalone-api.test.js +1344 -1344
  508. package/test/store-vector-cache.test.js +98 -98
  509. package/test/store-write-path-columns.test.js +191 -191
  510. package/test/store.test.js +744 -744
  511. package/test/stress.test.js +209 -209
  512. package/test/summarize-occurred-at.test.js +127 -127
  513. package/test/summarize-parse-diagnostics.test.js +75 -75
  514. package/test/summarize-reasoning-effort.test.js +189 -189
  515. package/test/summarize.test.js +2251 -2251
  516. package/test/supersede-state-tracking.test.js +394 -394
  517. package/test/test-count-sync.test.js +71 -71
  518. package/test/tool-state-ignore.test.js +65 -65
  519. package/test/tools-dto-schema-matrix.test.js +398 -398
  520. package/test/tools-memory-get-schema.test.js +65 -65
  521. package/test/tools.test.js +585 -585
  522. package/test/updated-at-semantics.test.js +113 -113
  523. package/test/user-settings-preview.test.js +127 -127
  524. package/test/user-settings-segments.test.js +527 -527
  525. package/test/vector-index.test.js +334 -334
  526. package/test/version-check.test.js +152 -152
  527. package/test/webServer-optional.test.js +82 -82
  528. package/test/wiki-api.test.js +734 -627
  529. package/test/wiki-claims.test.js +194 -0
  530. package/test/wiki-cli-hint.test.js +118 -118
  531. package/test/wiki-inject.test.js +265 -265
  532. package/test/wiki-partition.test.js +112 -112
  533. package/test/wiki-plan.test.js +80 -80
  534. package/test/wiki-provenance.test.js +202 -0
  535. package/test/wiki-queries.test.js +398 -277
  536. package/test/wiki-scan.test.js +208 -208
  537. package/test/wiki-store.test.js +190 -190
  538. package/test/wiki-t0.test.js +456 -380
  539. package/test/wiki-t1-jobs.test.js +125 -125
  540. package/test/wiki-t1.test.js +460 -235
  541. package/test/wiki-tools.test.js +176 -176
  542. package/test/workflow-guards.test.js +155 -155
  543. package/test/write-admission.test.js +668 -668
@@ -1,398 +1,398 @@
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_pin",
41
- "memory_register_document",
42
- "memory_runtime",
43
- "memory_organize",
44
- "set_scope_state"
45
- ];
46
-
47
- function setup(config = {}) {
48
- const store = createStore(":memory:");
49
- // config 同时喂 service 与 tools:#230 起 registerDocument 的闸在 service
50
- // 侧读同一个 config,只喂 tools 会让注册工具在矩阵里永远抛 disabled。
51
- const service = createService({ store, mirror: null, config });
52
- const registered = [];
53
- const ctx = {
54
- tools: {
55
- register(def) {
56
- registered.push(def);
57
- return () => {};
58
- }
59
- }
60
- };
61
- createTools(ctx, service, config, undefined);
62
- const byName = Object.fromEntries(registered.map((def) => [def.name, def]));
63
- return { store, service, byName };
64
- }
65
-
66
- // 驱动一个工具并立刻用它自己声明的 schema 校验真实返回值。
67
- // 返回 { value }:调用方需要时再对值做语义断言。
68
- async function drive(byName, name, args = [], exec) {
69
- const tool = byName[name];
70
- assert.ok(tool, `工具 ${name} 未注册`);
71
- const value = await tool.execute(args, exec);
72
- assertMatches(byName, name, value, `args=${JSON.stringify(args)}`);
73
- return value;
74
- }
75
-
76
- // 真实返回值必须被声明 schema 完全覆盖;失败时把违规明细一起抛出,定位更快。
77
- function assertMatches(byName, name, value, label) {
78
- const errors = validateJsonSchemaValue(byName[name].output.schema, value);
79
- assert.deepEqual(
80
- errors,
81
- [],
82
- `${name} 的真实返回未被 output schema 覆盖(${label}):${JSON.stringify(errors)}\n返回键:${JSON.stringify(Object.keys(value ?? {}))}`
83
- );
84
- }
85
-
86
- // 全形态种子:toApiList 的 7 个条件键只在对应列非 undefined 时展开,所以每种形态
87
- // 都要真实落一行,否则 B 层会(正确地)报「声明了却从未产出」。
88
- function seedShapes(store) {
89
- const rows = [];
90
- const push = (over) => {
91
- const memory = {
92
- type: "project",
93
- title: `shape-${rows.length}`,
94
- content: `形态 ${rows.length} 的正文`,
95
- source: "test",
96
- ...over
97
- };
98
- // #230:document 走存储层唯一铸造口(通用 save 拒绝 document)。
99
- rows.push(memory.type === "document" ? store.saveDocument(memory) : store.save(memory));
100
- };
101
- push({}); // 极简形态:只有基础键
102
- push({ sensitivity: "personal" });
103
- push({ occurred_at: "2026-09-15T00:00:00Z" });
104
- push({ agent_scope: "coder", workspace_scope: "proj-alpha" });
105
- push({
106
- // 全量标注:#184 的原始触发形态
107
- agent_scope: "coder",
108
- agent_scope_source: "auto",
109
- workspace_scope: "proj-alpha",
110
- workspace_scope_source: "explicit",
111
- scope_decided_at: "2026-09-15T00:00:00Z",
112
- sensitivity: "personal",
113
- occurred_at: "2026-09-14T12:00:00Z",
114
- tags: ["shape", "annotated"],
115
- importance: 5
116
- });
117
- // #230:document 指针行——doc_path 是 toApiList 的条件键,必须有一行真的产出它。
118
- push({ type: "document", title: "shape-doc", content: "指针行摘要", doc_path: "X:\\seed\\doc.md", evidence: [] });
119
- return rows;
120
- }
121
-
122
- // 清单比对必须在**暴露闸全开**的配置下做:#230 的 document 与 #231 的 organize 都是
123
- // opt-in 工具,用默认配置跑会让「注册表里少一个工具」和「清单没更新」两件事互相
124
- // 抵消——这条护栏就此静默失效,而漏登记正是 #195 要防的那类事故。
125
- test("工具矩阵与 TOOL_NAMES 清单一致(新增工具必须补进矩阵)", () => {
126
- const { byName } = setup({ documentMemoryEnabled: true, organizeEnabled: true });
127
- assert.deepEqual(Object.keys(byName).sort(), [...TOOL_NAMES].sort());
128
- });
129
-
130
- test("A 实跑:13 个工具每个可安全触达的分支,真实返回 ⊆ 声明的 output schema", async () => {
131
- const { store, service, byName } = setup({ documentMemoryEnabled: true, organizeEnabled: true });
132
- const shapes = seedShapes(store);
133
- const annotated = shapes[shapes.length - 1];
134
- const plain = shapes[0];
135
- const driven = new Set();
136
-
137
- const note = (name) => driven.add(name);
138
-
139
- // --- memory_save:created 与 merged 两条分支 ---
140
- await drive(byName, "memory_save", { type: "project", title: "save-new", content: "首次写入" });
141
- note("memory_save");
142
- await drive(byName, "memory_save", { type: "project", title: "save-new", content: "同键再写 → merged" });
143
- // 显式 scope + 敏感度 + 事件时间:把「条件展开的键」真的写进库(#184 的真实入口)
144
- const scopedSave = await drive(byName, "memory_save", {
145
- type: "preference",
146
- title: "save-scoped",
147
- content: "带显式 scope 的写入",
148
- agent_scope: "global",
149
- workspace_scope: "proj-beta",
150
- sensitivity: "personal",
151
- occurred_at: "2026-09-16T00:00:00Z"
152
- });
153
- assert.ok(scopedSave.id, "memory_save 必须回传 id");
154
-
155
- // --- memory_search:命中 / 空结果 / 显式 keyword 档 ---
156
- const hit = await drive(byName, "memory_search", { query: "形态" });
157
- assert.ok(hit.items.length > 0, "关键词检索应当命中种子行");
158
- await drive(byName, "memory_search", { query: "绝不存在的检索词-195" });
159
- await drive(byName, "memory_search", { query: "形态", mode: "keyword", limit: 2 });
160
- // occurred 时间窗分支(tools.js 的 occurredWindow 接线)
161
- await drive(byName, "memory_search", { query: "形态", occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
162
- note("memory_search");
163
-
164
- // --- memory_list:默认 / 类型过滤 / 含归档 / 分页 / occurred 窗口 ---
165
- await drive(byName, "memory_list", {});
166
- await drive(byName, "memory_list", { type: "project", limit: 1, offset: 1 });
167
- await drive(byName, "memory_list", { include_archived: true });
168
- await drive(byName, "memory_list", { occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
169
- note("memory_list");
170
-
171
- // --- memory_get:极简行 / 全量标注行(#184 的原始现场) ---
172
- await drive(byName, "memory_get", { id: plain.id });
173
- await drive(byName, "memory_get", { id: annotated.id });
174
- note("memory_get");
175
-
176
- // --- memory_update:字段修正 + 显式 scope 纠正(另一条会展开条件键的写路径) ---
177
- await drive(byName, "memory_update", { id: plain.id, title: "shape-0 renamed", importance: 4 });
178
- await drive(byName, "memory_update", {
179
- id: plain.id,
180
- content: "改过的正文",
181
- agent_scope: "global",
182
- workspace_scope: "proj-gamma",
183
- reason: "issue #195 矩阵覆盖"
184
- });
185
- note("memory_update");
186
-
187
- // --- memory_delete:删得掉 / 删不掉(不存在 → false,不抛错) ---
188
- const doomed = store.save({ type: "history", title: "doomed", content: "待删", source: "test" });
189
- const deleted = await drive(byName, "memory_delete", { id: doomed.id });
190
- assert.equal(deleted.deleted, true);
191
- const notDeleted = await drive(byName, "memory_delete", { id: "no-such-id-195" });
192
- assert.equal(notDeleted.deleted, false);
193
- note("memory_delete");
194
-
195
- // --- memory_forget / memory_archive:两个方向的分支 ---
196
- const forgetTarget = store.save({ type: "history", title: "forget-me", content: "待遗忘", source: "test" });
197
- assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id })).memory.forgotten, true);
198
- assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id, forgotten: false })).memory.forgotten, false);
199
- note("memory_forget");
200
-
201
- const archiveTarget = store.save({ type: "history", title: "archive-me", content: "待归档", source: "test" });
202
- assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id })).memory.archived, true);
203
- assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id, archived: false })).memory.archived, false);
204
- note("memory_archive");
205
-
206
- // --- memory_pin(#249 第二批):钉上 / 松开 / 幂等三条分支。
207
- // 幂等单独驱动是因为回执要报 pinned:false 而不是报错,这条分支最容易在
208
- // 「本来没钉也算成功」的语义上写错。
209
- const pinTarget = store.save({ type: "constraint", title: "pin-me", content: "每轮都要在。", source: "test" });
210
- const pinned = await drive(byName, "memory_pin", { id: pinTarget.id, note: "matrix" });
211
- assert.equal(pinned.pinned, true);
212
- assert.equal(pinned.memory_id, pinTarget.id);
213
- assert.equal(pinned.note, "matrix");
214
- // #593:pin 池由 pinnedInjectBudget 开关,本矩阵用的默认配置里它是 0(关闭),
215
- // 所以钉上了也不会注入 —— 回执必须如实报 false。这里断 false 而不是 true,
216
- // 锁的正是「返回成功不等于生效」那条口径。
217
- assert.equal(pinned.injectable, false);
218
- const unpinned = await drive(byName, "memory_pin", { id: pinTarget.id, unpin: true });
219
- assert.equal(unpinned.pinned, false);
220
- assert.equal(store.countPinned(), 0);
221
- // 本来没钉时再松一次:返回 pinned:false,不抛错(幂等)。
222
- assert.equal((await drive(byName, "memory_pin", { id: pinTarget.id, unpin: true })).pinned, false);
223
- note("memory_pin");
224
-
225
-
226
- // --- set_scope_state(#471):两个方向的分支——按下与松开各驱动一次 ---
227
- const pressed = await drive(byName, "set_scope_state", { dimension: "workspace", scope_value: "proj-alpha", state: "paused" });
228
- assert.equal(pressed.scope.state, "paused");
229
- assert.equal(pressed.scope.scope_value, "proj-alpha");
230
- const released = await drive(byName, "set_scope_state", { dimension: "workspace", scope_value: "proj-alpha", state: "active" });
231
- assert.equal(released.scope.state, "active");
232
- note("set_scope_state");
233
-
234
- // --- memory_runtime:只驱动只读与「无载荷早退」两条分支 ---
235
- // provision 会联网下载数十至上百 MB(且可能 adopt 本机载荷、硬链接数以百 MB 计),
236
- // verify 命中载荷时会真实加载模型——两者都不属于单元测试该做的事。status 与其
237
- // 「无载荷」早退分支已覆盖该 schema 的 summary/status/cost/payloadId/packages/reason
238
- // 六个键;provision 侧的 strategy/files/bytes 与 verify 侧的 dimension 由 C 层
239
- // 结构断言兜底(声明合规),分支行为本身留给 e2e/人工验证。
240
- const emptyRuntimeDir = mkdtempSync(join(tmpdir(), "mneme-195-runtime-"));
241
- try {
242
- const rt = setup({ runtimeDir: emptyRuntimeDir });
243
- await drive(rt.byName, "memory_runtime", { action: "status" });
244
- await drive(rt.byName, "memory_runtime", { action: "verify" });
245
- assert.ok(service, "service 保留引用,避免未使用变量告警");
246
- note("memory_runtime");
247
- } finally {
248
- rmSync(emptyRuntimeDir, { recursive: true, force: true });
249
- }
250
-
251
- // --- memory_register_document(#230):created 分支 + evidence 求交(含丢弃)。
252
- // superseded 分支由 document.test.js 覆盖;flag 关闭的拒绝路径同样在那边。
253
- const docDir = mkdtempSync(join(tmpdir(), "mneme-230-doc-"));
254
- const docFile = join(docDir, "report.md");
255
- try {
256
- writeFileSync(docFile, "# report\n\nbody", "utf8");
257
- const reg = await drive(byName, "memory_register_document", {
258
- path: docFile,
259
- title: "matrix-doc",
260
- summary: "指针行摘要",
261
- tags: ["matrix"],
262
- evidence: [plain.id, "fabricated-230"]
263
- });
264
- assert.equal(reg.action, "created");
265
- assert.equal(reg.evidence_kept, 1);
266
- assert.equal(reg.evidence_dropped, 1);
267
- assert.equal(reg.degraded, true);
268
- note("memory_register_document");
269
- } finally {
270
- rmSync(docDir, { recursive: true, force: true });
271
- }
272
-
273
- // --- memory_organize(#231 第二批):dryRun 与 apply 两个分支各驱动一次 ---
274
- // 两条分支的返回键集不同(dryRun 有 counts/items/created_at,apply 有
275
- // saved/merged/archived/memory_ids/dry_run_id/degraded)。只跑一边会让另一边
276
- // 「声明了却没被真实返回值验过」,正是本矩阵要抓的空洞。自建种子行而不是复用
277
- // plain:register_document 那一支会归档它吸收的 evidence,两支不该互相改状态。
278
- const organizeSeed = store.save({ type: "project", title: "organize-target", content: "库内既有的一行。", source: "test" });
279
- const organizeReport = await drive(byName, "memory_organize", {
280
- mode: "dryRun",
281
- candidates: [
282
- { type: "project", title: "organize-target", content: "同标题的另一段表述 → 应当判成 exact" },
283
- { type: "project", title: "organize-brand-new", content: "库里没有的新条目" },
284
- { type: "project", title: "", content: "缺标题 → 进 skipped,报告如实记 degraded" }
285
- ]
286
- });
287
- assert.equal(organizeReport.status, "degraded", "有候选被拒就不得报 ok(红线 4 的如实标记)");
288
- assert.equal(organizeReport.items.length, 2);
289
- assert.equal(organizeReport.items[0].verdict, "exact");
290
- assert.equal(organizeReport.items[0].match.id, organizeSeed.id);
291
- assert.equal(organizeReport.skipped.length, 1);
292
- const organized = await drive(byName, "memory_organize", {
293
- mode: "apply",
294
- run_id: organizeReport.run_id,
295
- decisions: [
296
- { action: "archive", id: organizeSeed.id },
297
- { action: "save", index: 1 },
298
- // index 99 不在这份报告的快照里 → 逐条跳过、应用合法子集(红线 4)。
299
- // 故意不用「非法 action」走这条路:枚举在宿主参数校验层就挡掉了,能到达
300
- // skipped 的只有索引与可见性两类,测试必须挑真实可达的那条。
301
- { action: "save", index: 99 }
302
- ]
303
- });
304
- assert.equal(organized.archived, 1);
305
- assert.equal(organized.saved, 1);
306
- assert.equal(organized.skipped.length, 1, "报告没收下过的索引逐条跳过 + 应用合法子集,绝不整单拒绝");
307
- assert.equal(organized.degraded, true);
308
- note("memory_organize");
309
-
310
- assert.deepEqual([...driven].sort(), [...TOOL_NAMES].sort(), "每个工具都必须至少被驱动一次(矩阵不允许空缺)");
311
- });
312
-
313
- test("A' 未命中 id 的三个工具按契约抛错(而不是返回半截对象)", async () => {
314
- const { byName } = setup();
315
- await assert.rejects(() => byName.memory_get.execute({ id: "missing-195" }), /memory not found/);
316
- await assert.rejects(() => byName.memory_forget.execute({ id: "missing-195" }), /memory not found/);
317
- await assert.rejects(() => byName.memory_archive.execute({ id: "missing-195" }), /memory not found/);
318
- });
319
-
320
- test("B DTO 产地:toApiList 全形态输出 ⊆ MEMORY_ITEM_SCHEMA,且声明键全部被真实产出", () => {
321
- const { store, service } = setup();
322
- seedShapes(store);
323
- const rows = service.toApiList(service.list({ limit: 100 }));
324
- assert.ok(rows.length >= 5, "种子行应当全部可读回");
325
-
326
- const observed = new Set();
327
- for (const dto of rows) {
328
- const errors = validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto);
329
- assert.deepEqual(errors, [], `toApiList 产出的 DTO 未被 MEMORY_ITEM_SCHEMA 覆盖:${JSON.stringify(errors)}`);
330
- for (const key of Object.keys(dto)) observed.add(key);
331
- }
332
-
333
- // 反向:声明的每个键都必须有形态真的产出它。这条让 schema 与种子互相咬合——
334
- // 以后给 toApiList 增键却忘了补种子,会在这里失败并提醒补形态。
335
- const declared = Object.keys(MEMORY_ITEM_SCHEMA.properties);
336
- const neverProduced = declared.filter((key) => !observed.has(key));
337
- assert.deepEqual(neverProduced, [], `MEMORY_ITEM_SCHEMA 声明了但矩阵从未产出的键(补种子形态或删死声明):${neverProduced.join(", ")}`);
338
- });
339
-
340
- test("B' search / list / get 三个消费方共用同一份 item schema(#184 的根因是有人另抄了一份)", () => {
341
- const { byName } = setup();
342
- const itemSchemaOf = (name) => {
343
- const props = byName[name].output.schema.properties;
344
- return name === "memory_get" ? props.memory : props.items.items;
345
- };
346
- const search = itemSchemaOf("memory_search");
347
- const list = itemSchemaOf("memory_list");
348
- const get = itemSchemaOf("memory_get");
349
- const keys = (s) => Object.keys(s.properties).sort();
350
-
351
- assert.deepEqual(keys(search), keys(MEMORY_ITEM_SCHEMA));
352
- assert.deepEqual(keys(list), keys(search));
353
- assert.deepEqual(keys(get), keys(search));
354
- for (const [name, schema] of [["search", search], ["list", list], ["get", get]]) {
355
- assert.equal(schema.additionalProperties, false, `${name} 的 item schema 必须闭合`);
356
- }
357
- });
358
-
359
- // 同清单比对那条:opt-in 工具在默认配置下不在注册表里,C 层按 TOOL_NAMES 取
360
- // byName[name],所以必须用暴露闸全开的配置,否则新工具的 schema 结构从未被检查。
361
- test("C 结构:全部 13 个工具的 output schema 闭合、required ⊆ properties、每项带 type", () => {
362
- const { byName } = setup({ organizeEnabled: true });
363
- for (const name of TOOL_NAMES) {
364
- const schema = byName[name].output.schema;
365
- assert.ok(schema, `${name} 必须声明 output.schema`);
366
- assert.equal(schema.additionalProperties, false, `${name} 的 schema 必须闭合(否则未声明键不会被拒收)`);
367
- assert.equal(schema.type, "object", `${name} 的顶层 schema 必须是 object`);
368
- for (const key of schema.required ?? []) {
369
- assert.ok(schema.properties?.[key], `${name}.required 里的 ${key} 不在 properties 中`);
370
- }
371
- for (const [key, prop] of Object.entries(schema.properties ?? {})) {
372
- assert.ok(prop.type, `${name}.${key} 缺 type——没有 type 的声明校验器无法真正约束它`);
373
- }
374
- }
375
- });
376
-
377
- test("D 负例锁:注入未声明键时校验器必须报错(否则 A/B/C 三层都是空转)", () => {
378
- const { store, service, byName } = setup();
379
- const row = store.save({ type: "project", title: "negative-control", content: "负例", source: "test" });
380
- const dto = service.toApiList([store.getById(row.id)])[0];
381
-
382
- // 正例:干净 DTO 必须通过
383
- assert.deepEqual(validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto), []);
384
- assert.deepEqual(validateJsonSchemaValue(byName.memory_get.output.schema, { memory: dto }), []);
385
-
386
- // 负例:多一个键就必须被拒——这正是 #184 的形状
387
- const poisoned = { ...dto, agent_scope_source_new: "auto" };
388
- assert.notDeepEqual(
389
- validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, poisoned),
390
- [],
391
- "未声明键必须被拒收:#184 类漂移就是靠这条被抓出来的"
392
- );
393
- assert.notDeepEqual(
394
- validateJsonSchemaValue(byName.memory_get.output.schema, { memory: poisoned }),
395
- [],
396
- "memory_get 的 schema 同样必须拒收未声明键"
397
- );
398
- });
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_pin",
41
+ "memory_register_document",
42
+ "memory_runtime",
43
+ "memory_organize",
44
+ "set_scope_state"
45
+ ];
46
+
47
+ function setup(config = {}) {
48
+ const store = createStore(":memory:");
49
+ // config 同时喂 service 与 tools:#230 起 registerDocument 的闸在 service
50
+ // 侧读同一个 config,只喂 tools 会让注册工具在矩阵里永远抛 disabled。
51
+ const service = createService({ store, mirror: null, config });
52
+ const registered = [];
53
+ const ctx = {
54
+ tools: {
55
+ register(def) {
56
+ registered.push(def);
57
+ return () => {};
58
+ }
59
+ }
60
+ };
61
+ createTools(ctx, service, config, undefined);
62
+ const byName = Object.fromEntries(registered.map((def) => [def.name, def]));
63
+ return { store, service, byName };
64
+ }
65
+
66
+ // 驱动一个工具并立刻用它自己声明的 schema 校验真实返回值。
67
+ // 返回 { value }:调用方需要时再对值做语义断言。
68
+ async function drive(byName, name, args = [], exec) {
69
+ const tool = byName[name];
70
+ assert.ok(tool, `工具 ${name} 未注册`);
71
+ const value = await tool.execute(args, exec);
72
+ assertMatches(byName, name, value, `args=${JSON.stringify(args)}`);
73
+ return value;
74
+ }
75
+
76
+ // 真实返回值必须被声明 schema 完全覆盖;失败时把违规明细一起抛出,定位更快。
77
+ function assertMatches(byName, name, value, label) {
78
+ const errors = validateJsonSchemaValue(byName[name].output.schema, value);
79
+ assert.deepEqual(
80
+ errors,
81
+ [],
82
+ `${name} 的真实返回未被 output schema 覆盖(${label}):${JSON.stringify(errors)}\n返回键:${JSON.stringify(Object.keys(value ?? {}))}`
83
+ );
84
+ }
85
+
86
+ // 全形态种子:toApiList 的 7 个条件键只在对应列非 undefined 时展开,所以每种形态
87
+ // 都要真实落一行,否则 B 层会(正确地)报「声明了却从未产出」。
88
+ function seedShapes(store) {
89
+ const rows = [];
90
+ const push = (over) => {
91
+ const memory = {
92
+ type: "project",
93
+ title: `shape-${rows.length}`,
94
+ content: `形态 ${rows.length} 的正文`,
95
+ source: "test",
96
+ ...over
97
+ };
98
+ // #230:document 走存储层唯一铸造口(通用 save 拒绝 document)。
99
+ rows.push(memory.type === "document" ? store.saveDocument(memory) : store.save(memory));
100
+ };
101
+ push({}); // 极简形态:只有基础键
102
+ push({ sensitivity: "personal" });
103
+ push({ occurred_at: "2026-09-15T00:00:00Z" });
104
+ push({ agent_scope: "coder", workspace_scope: "proj-alpha" });
105
+ push({
106
+ // 全量标注:#184 的原始触发形态
107
+ agent_scope: "coder",
108
+ agent_scope_source: "auto",
109
+ workspace_scope: "proj-alpha",
110
+ workspace_scope_source: "explicit",
111
+ scope_decided_at: "2026-09-15T00:00:00Z",
112
+ sensitivity: "personal",
113
+ occurred_at: "2026-09-14T12:00:00Z",
114
+ tags: ["shape", "annotated"],
115
+ importance: 5
116
+ });
117
+ // #230:document 指针行——doc_path 是 toApiList 的条件键,必须有一行真的产出它。
118
+ push({ type: "document", title: "shape-doc", content: "指针行摘要", doc_path: "X:\\seed\\doc.md", evidence: [] });
119
+ return rows;
120
+ }
121
+
122
+ // 清单比对必须在**暴露闸全开**的配置下做:#230 的 document 与 #231 的 organize 都是
123
+ // opt-in 工具,用默认配置跑会让「注册表里少一个工具」和「清单没更新」两件事互相
124
+ // 抵消——这条护栏就此静默失效,而漏登记正是 #195 要防的那类事故。
125
+ test("工具矩阵与 TOOL_NAMES 清单一致(新增工具必须补进矩阵)", () => {
126
+ const { byName } = setup({ documentMemoryEnabled: true, organizeEnabled: true });
127
+ assert.deepEqual(Object.keys(byName).sort(), [...TOOL_NAMES].sort());
128
+ });
129
+
130
+ test("A 实跑:13 个工具每个可安全触达的分支,真实返回 ⊆ 声明的 output schema", async () => {
131
+ const { store, service, byName } = setup({ documentMemoryEnabled: true, organizeEnabled: true });
132
+ const shapes = seedShapes(store);
133
+ const annotated = shapes[shapes.length - 1];
134
+ const plain = shapes[0];
135
+ const driven = new Set();
136
+
137
+ const note = (name) => driven.add(name);
138
+
139
+ // --- memory_save:created 与 merged 两条分支 ---
140
+ await drive(byName, "memory_save", { type: "project", title: "save-new", content: "首次写入" });
141
+ note("memory_save");
142
+ await drive(byName, "memory_save", { type: "project", title: "save-new", content: "同键再写 → merged" });
143
+ // 显式 scope + 敏感度 + 事件时间:把「条件展开的键」真的写进库(#184 的真实入口)
144
+ const scopedSave = await drive(byName, "memory_save", {
145
+ type: "preference",
146
+ title: "save-scoped",
147
+ content: "带显式 scope 的写入",
148
+ agent_scope: "global",
149
+ workspace_scope: "proj-beta",
150
+ sensitivity: "personal",
151
+ occurred_at: "2026-09-16T00:00:00Z"
152
+ });
153
+ assert.ok(scopedSave.id, "memory_save 必须回传 id");
154
+
155
+ // --- memory_search:命中 / 空结果 / 显式 keyword 档 ---
156
+ const hit = await drive(byName, "memory_search", { query: "形态" });
157
+ assert.ok(hit.items.length > 0, "关键词检索应当命中种子行");
158
+ await drive(byName, "memory_search", { query: "绝不存在的检索词-195" });
159
+ await drive(byName, "memory_search", { query: "形态", mode: "keyword", limit: 2 });
160
+ // occurred 时间窗分支(tools.js 的 occurredWindow 接线)
161
+ await drive(byName, "memory_search", { query: "形态", occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
162
+ note("memory_search");
163
+
164
+ // --- memory_list:默认 / 类型过滤 / 含归档 / 分页 / occurred 窗口 ---
165
+ await drive(byName, "memory_list", {});
166
+ await drive(byName, "memory_list", { type: "project", limit: 1, offset: 1 });
167
+ await drive(byName, "memory_list", { include_archived: true });
168
+ await drive(byName, "memory_list", { occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
169
+ note("memory_list");
170
+
171
+ // --- memory_get:极简行 / 全量标注行(#184 的原始现场) ---
172
+ await drive(byName, "memory_get", { id: plain.id });
173
+ await drive(byName, "memory_get", { id: annotated.id });
174
+ note("memory_get");
175
+
176
+ // --- memory_update:字段修正 + 显式 scope 纠正(另一条会展开条件键的写路径) ---
177
+ await drive(byName, "memory_update", { id: plain.id, title: "shape-0 renamed", importance: 4 });
178
+ await drive(byName, "memory_update", {
179
+ id: plain.id,
180
+ content: "改过的正文",
181
+ agent_scope: "global",
182
+ workspace_scope: "proj-gamma",
183
+ reason: "issue #195 矩阵覆盖"
184
+ });
185
+ note("memory_update");
186
+
187
+ // --- memory_delete:删得掉 / 删不掉(不存在 → false,不抛错) ---
188
+ const doomed = store.save({ type: "history", title: "doomed", content: "待删", source: "test" });
189
+ const deleted = await drive(byName, "memory_delete", { id: doomed.id });
190
+ assert.equal(deleted.deleted, true);
191
+ const notDeleted = await drive(byName, "memory_delete", { id: "no-such-id-195" });
192
+ assert.equal(notDeleted.deleted, false);
193
+ note("memory_delete");
194
+
195
+ // --- memory_forget / memory_archive:两个方向的分支 ---
196
+ const forgetTarget = store.save({ type: "history", title: "forget-me", content: "待遗忘", source: "test" });
197
+ assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id })).memory.forgotten, true);
198
+ assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id, forgotten: false })).memory.forgotten, false);
199
+ note("memory_forget");
200
+
201
+ const archiveTarget = store.save({ type: "history", title: "archive-me", content: "待归档", source: "test" });
202
+ assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id })).memory.archived, true);
203
+ assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id, archived: false })).memory.archived, false);
204
+ note("memory_archive");
205
+
206
+ // --- memory_pin(#249 第二批):钉上 / 松开 / 幂等三条分支。
207
+ // 幂等单独驱动是因为回执要报 pinned:false 而不是报错,这条分支最容易在
208
+ // 「本来没钉也算成功」的语义上写错。
209
+ const pinTarget = store.save({ type: "constraint", title: "pin-me", content: "每轮都要在。", source: "test" });
210
+ const pinned = await drive(byName, "memory_pin", { id: pinTarget.id, note: "matrix" });
211
+ assert.equal(pinned.pinned, true);
212
+ assert.equal(pinned.memory_id, pinTarget.id);
213
+ assert.equal(pinned.note, "matrix");
214
+ // #593:pin 池由 pinnedInjectBudget 开关,本矩阵用的默认配置里它是 0(关闭),
215
+ // 所以钉上了也不会注入 —— 回执必须如实报 false。这里断 false 而不是 true,
216
+ // 锁的正是「返回成功不等于生效」那条口径。
217
+ assert.equal(pinned.injectable, false);
218
+ const unpinned = await drive(byName, "memory_pin", { id: pinTarget.id, unpin: true });
219
+ assert.equal(unpinned.pinned, false);
220
+ assert.equal(store.countPinned(), 0);
221
+ // 本来没钉时再松一次:返回 pinned:false,不抛错(幂等)。
222
+ assert.equal((await drive(byName, "memory_pin", { id: pinTarget.id, unpin: true })).pinned, false);
223
+ note("memory_pin");
224
+
225
+
226
+ // --- set_scope_state(#471):两个方向的分支——按下与松开各驱动一次 ---
227
+ const pressed = await drive(byName, "set_scope_state", { dimension: "workspace", scope_value: "proj-alpha", state: "paused" });
228
+ assert.equal(pressed.scope.state, "paused");
229
+ assert.equal(pressed.scope.scope_value, "proj-alpha");
230
+ const released = await drive(byName, "set_scope_state", { dimension: "workspace", scope_value: "proj-alpha", state: "active" });
231
+ assert.equal(released.scope.state, "active");
232
+ note("set_scope_state");
233
+
234
+ // --- memory_runtime:只驱动只读与「无载荷早退」两条分支 ---
235
+ // provision 会联网下载数十至上百 MB(且可能 adopt 本机载荷、硬链接数以百 MB 计),
236
+ // verify 命中载荷时会真实加载模型——两者都不属于单元测试该做的事。status 与其
237
+ // 「无载荷」早退分支已覆盖该 schema 的 summary/status/cost/payloadId/packages/reason
238
+ // 六个键;provision 侧的 strategy/files/bytes 与 verify 侧的 dimension 由 C 层
239
+ // 结构断言兜底(声明合规),分支行为本身留给 e2e/人工验证。
240
+ const emptyRuntimeDir = mkdtempSync(join(tmpdir(), "mneme-195-runtime-"));
241
+ try {
242
+ const rt = setup({ runtimeDir: emptyRuntimeDir });
243
+ await drive(rt.byName, "memory_runtime", { action: "status" });
244
+ await drive(rt.byName, "memory_runtime", { action: "verify" });
245
+ assert.ok(service, "service 保留引用,避免未使用变量告警");
246
+ note("memory_runtime");
247
+ } finally {
248
+ rmSync(emptyRuntimeDir, { recursive: true, force: true });
249
+ }
250
+
251
+ // --- memory_register_document(#230):created 分支 + evidence 求交(含丢弃)。
252
+ // superseded 分支由 document.test.js 覆盖;flag 关闭的拒绝路径同样在那边。
253
+ const docDir = mkdtempSync(join(tmpdir(), "mneme-230-doc-"));
254
+ const docFile = join(docDir, "report.md");
255
+ try {
256
+ writeFileSync(docFile, "# report\n\nbody", "utf8");
257
+ const reg = await drive(byName, "memory_register_document", {
258
+ path: docFile,
259
+ title: "matrix-doc",
260
+ summary: "指针行摘要",
261
+ tags: ["matrix"],
262
+ evidence: [plain.id, "fabricated-230"]
263
+ });
264
+ assert.equal(reg.action, "created");
265
+ assert.equal(reg.evidence_kept, 1);
266
+ assert.equal(reg.evidence_dropped, 1);
267
+ assert.equal(reg.degraded, true);
268
+ note("memory_register_document");
269
+ } finally {
270
+ rmSync(docDir, { recursive: true, force: true });
271
+ }
272
+
273
+ // --- memory_organize(#231 第二批):dryRun 与 apply 两个分支各驱动一次 ---
274
+ // 两条分支的返回键集不同(dryRun 有 counts/items/created_at,apply 有
275
+ // saved/merged/archived/memory_ids/dry_run_id/degraded)。只跑一边会让另一边
276
+ // 「声明了却没被真实返回值验过」,正是本矩阵要抓的空洞。自建种子行而不是复用
277
+ // plain:register_document 那一支会归档它吸收的 evidence,两支不该互相改状态。
278
+ const organizeSeed = store.save({ type: "project", title: "organize-target", content: "库内既有的一行。", source: "test" });
279
+ const organizeReport = await drive(byName, "memory_organize", {
280
+ mode: "dryRun",
281
+ candidates: [
282
+ { type: "project", title: "organize-target", content: "同标题的另一段表述 → 应当判成 exact" },
283
+ { type: "project", title: "organize-brand-new", content: "库里没有的新条目" },
284
+ { type: "project", title: "", content: "缺标题 → 进 skipped,报告如实记 degraded" }
285
+ ]
286
+ });
287
+ assert.equal(organizeReport.status, "degraded", "有候选被拒就不得报 ok(红线 4 的如实标记)");
288
+ assert.equal(organizeReport.items.length, 2);
289
+ assert.equal(organizeReport.items[0].verdict, "exact");
290
+ assert.equal(organizeReport.items[0].match.id, organizeSeed.id);
291
+ assert.equal(organizeReport.skipped.length, 1);
292
+ const organized = await drive(byName, "memory_organize", {
293
+ mode: "apply",
294
+ run_id: organizeReport.run_id,
295
+ decisions: [
296
+ { action: "archive", id: organizeSeed.id },
297
+ { action: "save", index: 1 },
298
+ // index 99 不在这份报告的快照里 → 逐条跳过、应用合法子集(红线 4)。
299
+ // 故意不用「非法 action」走这条路:枚举在宿主参数校验层就挡掉了,能到达
300
+ // skipped 的只有索引与可见性两类,测试必须挑真实可达的那条。
301
+ { action: "save", index: 99 }
302
+ ]
303
+ });
304
+ assert.equal(organized.archived, 1);
305
+ assert.equal(organized.saved, 1);
306
+ assert.equal(organized.skipped.length, 1, "报告没收下过的索引逐条跳过 + 应用合法子集,绝不整单拒绝");
307
+ assert.equal(organized.degraded, true);
308
+ note("memory_organize");
309
+
310
+ assert.deepEqual([...driven].sort(), [...TOOL_NAMES].sort(), "每个工具都必须至少被驱动一次(矩阵不允许空缺)");
311
+ });
312
+
313
+ test("A' 未命中 id 的三个工具按契约抛错(而不是返回半截对象)", async () => {
314
+ const { byName } = setup();
315
+ await assert.rejects(() => byName.memory_get.execute({ id: "missing-195" }), /memory not found/);
316
+ await assert.rejects(() => byName.memory_forget.execute({ id: "missing-195" }), /memory not found/);
317
+ await assert.rejects(() => byName.memory_archive.execute({ id: "missing-195" }), /memory not found/);
318
+ });
319
+
320
+ test("B DTO 产地:toApiList 全形态输出 ⊆ MEMORY_ITEM_SCHEMA,且声明键全部被真实产出", () => {
321
+ const { store, service } = setup();
322
+ seedShapes(store);
323
+ const rows = service.toApiList(service.list({ limit: 100 }));
324
+ assert.ok(rows.length >= 5, "种子行应当全部可读回");
325
+
326
+ const observed = new Set();
327
+ for (const dto of rows) {
328
+ const errors = validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto);
329
+ assert.deepEqual(errors, [], `toApiList 产出的 DTO 未被 MEMORY_ITEM_SCHEMA 覆盖:${JSON.stringify(errors)}`);
330
+ for (const key of Object.keys(dto)) observed.add(key);
331
+ }
332
+
333
+ // 反向:声明的每个键都必须有形态真的产出它。这条让 schema 与种子互相咬合——
334
+ // 以后给 toApiList 增键却忘了补种子,会在这里失败并提醒补形态。
335
+ const declared = Object.keys(MEMORY_ITEM_SCHEMA.properties);
336
+ const neverProduced = declared.filter((key) => !observed.has(key));
337
+ assert.deepEqual(neverProduced, [], `MEMORY_ITEM_SCHEMA 声明了但矩阵从未产出的键(补种子形态或删死声明):${neverProduced.join(", ")}`);
338
+ });
339
+
340
+ test("B' search / list / get 三个消费方共用同一份 item schema(#184 的根因是有人另抄了一份)", () => {
341
+ const { byName } = setup();
342
+ const itemSchemaOf = (name) => {
343
+ const props = byName[name].output.schema.properties;
344
+ return name === "memory_get" ? props.memory : props.items.items;
345
+ };
346
+ const search = itemSchemaOf("memory_search");
347
+ const list = itemSchemaOf("memory_list");
348
+ const get = itemSchemaOf("memory_get");
349
+ const keys = (s) => Object.keys(s.properties).sort();
350
+
351
+ assert.deepEqual(keys(search), keys(MEMORY_ITEM_SCHEMA));
352
+ assert.deepEqual(keys(list), keys(search));
353
+ assert.deepEqual(keys(get), keys(search));
354
+ for (const [name, schema] of [["search", search], ["list", list], ["get", get]]) {
355
+ assert.equal(schema.additionalProperties, false, `${name} 的 item schema 必须闭合`);
356
+ }
357
+ });
358
+
359
+ // 同清单比对那条:opt-in 工具在默认配置下不在注册表里,C 层按 TOOL_NAMES 取
360
+ // byName[name],所以必须用暴露闸全开的配置,否则新工具的 schema 结构从未被检查。
361
+ test("C 结构:全部 13 个工具的 output schema 闭合、required ⊆ properties、每项带 type", () => {
362
+ const { byName } = setup({ organizeEnabled: true });
363
+ for (const name of TOOL_NAMES) {
364
+ const schema = byName[name].output.schema;
365
+ assert.ok(schema, `${name} 必须声明 output.schema`);
366
+ assert.equal(schema.additionalProperties, false, `${name} 的 schema 必须闭合(否则未声明键不会被拒收)`);
367
+ assert.equal(schema.type, "object", `${name} 的顶层 schema 必须是 object`);
368
+ for (const key of schema.required ?? []) {
369
+ assert.ok(schema.properties?.[key], `${name}.required 里的 ${key} 不在 properties 中`);
370
+ }
371
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
372
+ assert.ok(prop.type, `${name}.${key} 缺 type——没有 type 的声明校验器无法真正约束它`);
373
+ }
374
+ }
375
+ });
376
+
377
+ test("D 负例锁:注入未声明键时校验器必须报错(否则 A/B/C 三层都是空转)", () => {
378
+ const { store, service, byName } = setup();
379
+ const row = store.save({ type: "project", title: "negative-control", content: "负例", source: "test" });
380
+ const dto = service.toApiList([store.getById(row.id)])[0];
381
+
382
+ // 正例:干净 DTO 必须通过
383
+ assert.deepEqual(validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto), []);
384
+ assert.deepEqual(validateJsonSchemaValue(byName.memory_get.output.schema, { memory: dto }), []);
385
+
386
+ // 负例:多一个键就必须被拒——这正是 #184 的形状
387
+ const poisoned = { ...dto, agent_scope_source_new: "auto" };
388
+ assert.notDeepEqual(
389
+ validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, poisoned),
390
+ [],
391
+ "未声明键必须被拒收:#184 类漂移就是靠这条被抓出来的"
392
+ );
393
+ assert.notDeepEqual(
394
+ validateJsonSchemaValue(byName.memory_get.output.schema, { memory: poisoned }),
395
+ [],
396
+ "memory_get 的 schema 同样必须拒收未声明键"
397
+ );
398
+ });