@modusensus/dsh-mneme 0.14.0-beta.1 → 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 -830
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +964 -964
  5. package/bin/dsh-mneme-choro.mjs +262 -262
  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 -404
  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 -3600
  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 -14322
  33. package/lib/commands.js +95 -95
  34. package/lib/config-keys.js +144 -144
  35. package/lib/config.js +1361 -1344
  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 -945
  52. package/lib/dream/narratives.js +44 -44
  53. package/lib/dream/sleep.js +2027 -2027
  54. package/lib/dream/triage.js +139 -139
  55. package/lib/dream.js +1879 -1879
  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 -119
  72. package/lib/image-describe.js +158 -0
  73. package/lib/index.js +852 -852
  74. package/lib/inject.js +656 -651
  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 -595
  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 -3268
  124. package/lib/session-gate.js +91 -91
  125. package/lib/session-usage.js +350 -350
  126. package/lib/settings.js +827 -817
  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 -5895
  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 -267
  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 -640
  142. package/lib/wiki/render.js +261 -261
  143. package/lib/wiki/scan.js +371 -371
  144. package/lib/wiki/t0.js +500 -500
  145. package/lib/wiki/t1-jobs.js +232 -232
  146. package/lib/wiki/t1.js +609 -609
  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 +1 -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 -210
  162. package/scripts/release-notes.mjs +164 -164
  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 -3600
  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 -1344
  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 -945
  194. package/src/dream/narratives.js +44 -44
  195. package/src/dream/sleep.js +2027 -2027
  196. package/src/dream/triage.js +139 -139
  197. package/src/dream.js +1879 -1879
  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 -119
  214. package/src/image-describe.js +158 -0
  215. package/src/index.js +852 -852
  216. package/src/inject.js +656 -651
  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 -595
  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 -3268
  266. package/src/session-gate.js +91 -91
  267. package/src/session-usage.js +350 -350
  268. package/src/settings.js +827 -817
  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 -5895
  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 -267
  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 -640
  284. package/src/wiki/render.js +261 -261
  285. package/src/wiki/scan.js +371 -371
  286. package/src/wiki/t0.js +500 -500
  287. package/src/wiki/t1-jobs.js +232 -232
  288. package/src/wiki/t1.js +609 -609
  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 -2998
  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 -4425
  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 -452
  318. package/test/conflict-ledger.test.js +242 -242
  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 -432
  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 -174
  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 -333
  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 -259
  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 -109
  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 -553
  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 -90
  407. package/test/panel-tokens.test.js +245 -245
  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 -156
  453. package/test/release-notes.test.js +265 -265
  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 -188
  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 -1455
  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 -734
  529. package/test/wiki-claims.test.js +194 -194
  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 -202
  535. package/test/wiki-queries.test.js +398 -398
  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 -456
  539. package/test/wiki-t1-jobs.test.js +125 -125
  540. package/test/wiki-t1.test.js +460 -460
  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
package/lib/organize.js CHANGED
@@ -1,456 +1,456 @@
1
- // dsh-mneme/src/organize.js
2
- // agent 主动整理接口(issue #231,口径基线 = #164 的「第三腿触发」):dryRun
3
- // 出比对报告 → agent 判断 → apply 落库;筛除项进归档不删;调用全程留审计回执。
4
- //
5
- // 只做功能本体(维护者口径,issue #231):落在 service 层并带测试。**调用入口在
6
- // 第二批**(#231:`tools.js` 的 `memory_organize` + `organizeEnabled` 默认关)——
7
- // 本模块至今不读任何配置键,闸门与 scope 盖章都在调用方那一侧,所以本体语义与
8
- // 第一批逐字节相同。
9
- //
10
- // 独立成模块(AGENTS.md 尺寸约定,同 document.js / recall-stats.js 先例):
11
- // service.js 只做依赖注入 + barrel 出口,调用方零改动。
12
- //
13
- // 四条硬规则,都是「宁可什么都不做」的形态:
14
- // 1. dryRun 不写记忆表:它只读 + 写一行审计(dream_runs)。比对是报告,不是动作。
15
- // 2. apply 必须引用一次真实 dryRun——run_id 且 run_type='organize',且不是 apply
16
- // 自己的回执(靠 outcome.dry_run_id 区分)、也不是 failed 的报告,且**还没落地过**
17
- // (报告行上的 outcome.applied_at,同一份报告只认一次,挡掉重放)。没有比对过的
18
- // 候选一律不落库,堵死「跳过报告直接写」这条绕过路径。
19
- // 3. apply 只重放 dryRun 校验过的那份规范化快照,且只认快照里的索引:被 dryRun
20
- // 跳过的候选没有资格落库;scope 比对按**候选自己的** scope(candidate ?? payload),
21
- // 不会出现「报告按 scope A 比对、落地却写成 scope B」的错位。
22
- // 3b. archive 只认报告里命中过的 id(item.match.id,落在报告的 decisions 列):报告
23
- // 是唯一的判断依据,整理接口不能变成第二条「按 id 改库」的通道。
24
- // 4. 筛除 = 归档(store.setArchived),绝不物理删除:归档可恢复,删除不可逆。
25
- //
26
- // 宽容形态(仓库红线 4,同 issue #89)只管**输入**:单条非法候选/决策跳过 + 应用
27
- // 合法子集 + run 记 degraded,不整单拒绝。基础设施错误(嵌入器抛错、存储异常)
28
- // 相反——记 failed 并原样上抛,绝不静默降级成「没找到重复」。审计口径复用
29
- // dream_runs 的 receipt 语义(buildReceipt / parseReceipt 同一格式),不新建审计面。
30
-
31
- import { createHash, randomUUID } from "node:crypto";
32
- import { scopeKeyOf } from "./scope.js";
33
- // 比对池要先剔掉外来行:报告里的 item.match.id 既给出「命中」结论,也是 apply 的
34
- // archive 动作唯一许可的目标集(硬规则 3b),而 archive 直接 store.setArchived,
35
- // 不经 saveWithDedupe 那道闸门——留着外来行,整理接口就能替别人决定归档面。
36
- import { isImportedRow } from "./memory-source.js";
37
- import { cosineSimilarity } from "./dream/clustering.js";
38
- import { buildReceipt } from "./dream.js";
39
-
40
- // 与 document 的 C2 档(#230)和 findSessionDuplicate 的 vector 档同源:同一份
41
- // 「近重复」语义只有一个阈值,不在第三个地方另发明一个。
42
- const MIN_SIM = 0.92;
43
- // 向量比对对象的上限(document.js 同口径:精确层全量、向量层有界)。精确层
44
- // 不能截断——同标题漏检会让报告给出错误的 verdict。
45
- const CANDIDATE_LIMIT = 200;
46
- // 单次 dryRun 的候选硬上限:主场景是「长段工作收尾整理」,不是批量导入;不设界
47
- // 的话一次调用会把整库拉进比对。
48
- const MAX_ITEMS = 50;
49
- // scope 标注只做搬运(判定在 store / service 侧),省得整理接口成为第二条绕过
50
- // scope 治理的写入通道。载荷级给的值是候选级的缺省——比对按它判 scope,落地就必须
51
- // 带上它,否则报告与写入落在两个 scope 里。
52
- const SCOPE_KEYS = ["sensitivity", "agent_scope", "workspace_scope", "agent_scope_source", "workspace_scope_source"];
53
- // 比对键必须与 saveWithDedupe 的去重键**同口径**:`src/service.js` 的 scopeMatches 只比
54
- // sensitivity / agent_scope / workspace_scope 三列,来源两列不进键(它们是元数据,合并时
55
- // 还会把被并入行的 auto 来源升级成 explicit)。比对键里多带来源,就会出现「dryRun 报 new、
56
- // apply 却把它并进既有行」——报告与写入错位,正是本模块硬规则 3 要挡的那类不一致。
57
- const MATCH_SCOPE_KEYS = ["sensitivity", "agent_scope", "workspace_scope"];
58
-
59
- /**
60
- * Build the organizer.
61
- *
62
- * Injected deps are service.js closure members (store / embedQuery / saveWithDedupe
63
- * / transaction / finalize) so this module stays free of service-internal wiring;
64
- * `saveWithDedupe` is the normal write path, so its own epilogue (mirror sync and
65
- * notify) is never duplicated here. `finalize` is the post-commit re-embed hook
66
- * (document.js 同款): transaction 提交后补重嵌入,见 service.js › transaction 注释。
67
- *
68
- * @returns {{ organize: (payload: object, opts?: object) => Promise<object> }}
69
- * `organize({ mode: "dryRun"|"apply", ... }, opts)` — 一个入口带 mode 参数
70
- * (维护者倾向的形态),内部两个具名函数各自可测。
71
- */
72
- export function createOrganizer({ store, embedQuery, saveWithDedupe, transaction, finalize }) {
73
- const hashOf = (value) => createHash("sha256").update(JSON.stringify(value ?? null)).digest("hex");
74
-
75
- /**
76
- * 候选规范化。返回 `{ candidate }` 或 `{ error }`——错误在 dryRun 里进 skipped,
77
- * 不抛整单(红线 4)。document 刻意在此拦下:它的唯一铸造口是 registerDocument
78
- * (#230),service 的通用 save 路径也会拒绝,这里提前拦截只是为了报告不撒谎。
79
- */
80
- function normalizeCandidate(raw, defaults = {}) {
81
- const type = String(raw?.type ?? "").trim();
82
- const title = String(raw?.title ?? "").trim();
83
- const content = String(raw?.content ?? "").trim();
84
- if (!type || !title || !content) return { error: "type/title/content are required" };
85
- if (type === "document") return { error: "document rows are minted only via registerDocument (#230)" };
86
- const candidate = {
87
- type,
88
- title,
89
- content,
90
- importance: Number.isInteger(raw?.importance) ? Math.min(5, Math.max(1, raw.importance)) : 3,
91
- tags: [...new Set((Array.isArray(raw?.tags) ? raw.tags : []).map((t) => String(t ?? "").trim()).filter(Boolean))],
92
- source: String(raw?.source ?? "organize")
93
- };
94
- for (const key of SCOPE_KEYS) {
95
- const value = raw?.[key] ?? defaults[key];
96
- if (value !== undefined) candidate[key] = value;
97
- }
98
- return { candidate };
99
- }
100
-
101
- /** 标题归一(同 findSessionDuplicate 的 title 档):大小写与标点不构成「不同标题」。 */
102
- const normalizeTitle = (s) => String(s ?? "").toLowerCase().replace(/[\s\p{P}]+/gu, "");
103
-
104
- /**
105
- * 向量档最佳命中。拿不到向量/嵌入器时返回 null(无信号 = 不判定,不是报错);
106
- * 但**抛出的**错误不再吞掉——嵌入器故障下的空信号会被读成「全是新条目」,让
107
- * agent 把重复条写进库,比整单失败更糟,交 dryRun 记 failed 后上抛。
108
- */
109
- async function bestNear(candidate, rows, vecs) {
110
- if (!vecs?.size) return null;
111
- const probe = await embedQuery([candidate.title, candidate.content].filter(Boolean).join("\n"));
112
- if (!probe) return null;
113
- let best = null;
114
- for (const m of rows) {
115
- const v = vecs.get(m.id);
116
- if (!v) continue; // 无向量的既有行不参与比对
117
- const sim = cosineSimilarity(probe, v);
118
- if (sim >= MIN_SIM && (!best || sim > best.sim)) best = { memory: m, sim };
119
- }
120
- return best;
121
- }
122
-
123
- /**
124
- * 比对报告。**不写记忆表**;只写一行 dream_runs 审计(run_type='organize')。
125
- *
126
- * @param {object} payload
127
- * `{ candidates: Array<{type,title,content,importance?,tags?}>, agent_scope?,
128
- * workspace_scope?, sensitivity? }`
129
- * @returns {Promise<{run_id, created_at, status, counts, items, skipped}>}
130
- * items[i] = `{ index, type, title, verdict: "exact"|"near"|"new", match }`
131
- */
132
- async function dryRun(payload = {}) {
133
- const raws = Array.isArray(payload.candidates) ? payload.candidates : null;
134
- if (!raws) throw new Error("organize.dryRun: candidates must be an array");
135
- const bounded = raws.slice(0, MAX_ITEMS);
136
- const defaults = Object.fromEntries(SCOPE_KEYS.filter((k) => payload?.[k] !== undefined).map((k) => [k, payload[k]]));
137
-
138
- const skipped = [];
139
- const accepted = [];
140
- bounded.forEach((raw, index) => {
141
- const { candidate, error } = normalizeCandidate(raw, defaults);
142
- if (error) skipped.push({ index, error });
143
- else accepted.push({ index, candidate });
144
- });
145
- // 审计存规范化快照(带原始索引),不存调用方原始输入:apply 要重放的就是这份
146
- // ——原始输入里混着被跳过的候选,也没有随行的 payload 级 scope。
147
- const snapshot = accepted.map(({ index, candidate }) => ({ index, ...candidate }));
148
-
149
- // scope 同等才比对(跨 scope 永不互判——防泄漏,与 saveWithDedupe 的去重键
150
- // 「type+title+scope」同口径)。**按候选自己的 scope 比,不按载荷级**:
151
- // normalizeCandidate 允许逐条给 scope(raw ?? payload),若这里只看 payload,
152
- // 一条 agent_scope:"agent-b" 的候选就会拿默认 scope 的行去比——同一个 scope 里
153
- // 明明已有同标题行也报 "new",报告 scope 与写入 scope 就此错位(硬规则 3)。
154
- // 行扫描与向量读取都按 (type, scope) 缓存:一批候选通常同类型同 scope,避免逐条全表扫。
155
- // 比对键用 MATCH_SCOPE_KEYS(三列值),搬运用 SCOPE_KEYS(五列)——两件事,别混:
156
- // 「怎么判同一行」跟 write 路径一致,「怎么把 scope 搬到候选上」才需要连来源一起带。
157
- const scopeOf = (source) => MATCH_SCOPE_KEYS.map((k) => scopeKeyOf(source?.[k])).join("\u0000");
158
- const scopeMatches = (m, source) => MATCH_SCOPE_KEYS.every((k) => scopeKeyOf(m[k]) === scopeKeyOf(source?.[k]));
159
- const cacheKey = (candidate) => `${candidate.type}\u0000${scopeOf(candidate)}`;
160
- const rowsByScope = new Map();
161
- const rowsFor = (candidate) => {
162
- const key = cacheKey(candidate);
163
- if (!rowsByScope.has(key)) {
164
- rowsByScope.set(
165
- key,
166
- store.list({ type: candidate.type, limit: null }).filter((m) => !isImportedRow(m) && !m.archived && scopeMatches(m, candidate))
167
- );
168
- }
169
- return rowsByScope.get(key);
170
- };
171
- const vecsByScope = new Map();
172
- const vecsFor = (candidate, rows) => {
173
- const key = cacheKey(candidate);
174
- if (!vecsByScope.has(key)) {
175
- vecsByScope.set(key, store.getEmbeddings(rows.slice(0, CANDIDATE_LIMIT).map((m) => m.id)));
176
- }
177
- return vecsByScope.get(key);
178
- };
179
-
180
- const runId = randomUUID();
181
- const createdAt = new Date().toISOString();
182
- // 哈希与 input 必须指向同一份东西:审计存的是规范化快照(apply 要重放它),
183
- // 所以哈希也算快照——否则回执里的 snapshot_hash 描述的是调用方原始输入,
184
- // 谁也校验不了。
185
- const snapshotHash = hashOf(snapshot);
186
- const record = (status, { error = null, decisions = items, skipped: skipList = skipped, applied = 0 } = {}) =>
187
- store.saveDreamRun({
188
- id: runId,
189
- created_at: createdAt,
190
- status,
191
- error,
192
- provider: null,
193
- model: null,
194
- snapshot_hash: snapshotHash,
195
- input_count: snapshot.length,
196
- input: snapshot,
197
- decisions,
198
- applied,
199
- summary_stored: 0,
200
- receipt: buildReceipt({ runId, status, snapshotHash, inputCount: snapshot.length, applied, summaryStored: false }),
201
- policy_epoch: 0,
202
- run_type: "organize",
203
- ...(skipList.length ? { skipped: skipList } : {})
204
- });
205
-
206
- const items = [];
207
- try {
208
- for (const { index, candidate } of accepted) {
209
- const rows = rowsFor(candidate);
210
- let item = { index, type: candidate.type, title: candidate.title, verdict: "new", match: null };
211
- const exact = rows.find((m) => normalizeTitle(m.title) === normalizeTitle(candidate.title));
212
- if (exact) {
213
- item = { ...item, verdict: "exact", match: { id: exact.id, title: exact.title, sim: null } };
214
- } else {
215
- const near = await bestNear(candidate, rows, vecsFor(candidate, rows));
216
- if (near) {
217
- item = { ...item, verdict: "near", match: { id: near.memory.id, title: near.memory.title, sim: near.sim } };
218
- }
219
- }
220
- items.push(item);
221
- }
222
- } catch (e) {
223
- // 审计先留痕、再上抛:一份没有回执的失败报告,事后无法与「没调用过」区分。
224
- record("failed", { error: String(e?.message ?? e) });
225
- throw e;
226
- }
227
-
228
- const counts = {
229
- total: items.length,
230
- exact: items.filter((i) => i.verdict === "exact").length,
231
- near: items.filter((i) => i.verdict === "near").length,
232
- new: items.filter((i) => i.verdict === "new").length,
233
- skipped: skipped.length,
234
- truncated: raws.length - bounded.length
235
- };
236
- // 有被拒的候选 → degraded(如实标记),而不是 ok 掩盖过去;一条都没报 → noop。
237
- const status = skipped.length ? "degraded" : items.length ? "ok" : "noop";
238
- record(status);
239
- return { run_id: runId, created_at: createdAt, status, counts, items, skipped };
240
- }
241
-
242
- /**
243
- * 按 agent 的判断落库。decisions 逐条:
244
- * `{ action: "save", index }` — 写候选(走 saveWithDedupe:同键自动合并)
245
- * `{ action: "discard", index }` — 不写(只进回执,不删任何东西)
246
- * `{ action: "archive", id }` — 库内被取代的行 → 归档(可恢复,不删)
247
- *
248
- * @param {object} payload `{ run_id, decisions }`
249
- * @param {object} [opts] `{ hiddenIds }` — 调用者 scope 看不见的行按「不存在」
250
- * 处理(#170 复核项 4:无存在性泄漏),与 document 的 hiddenEvidenceIds 同形:
251
- * **可见性由可信调用方算好传入**(tools 层先例:register_document),不取 payload
252
- * 里的字段——那是调用方可自选的内容,不是授权。
253
- * @returns {{run_id, dry_run_id, status, saved, merged, discarded, archived, memory_ids, skipped, degraded}}
254
- * `saved` 只数**新建**的行,合并进既有行的算 `merged`(两者之和 = 落地生效的条数);
255
- * `memory_ids` 含合并命中的既有行 id(它们同样需要重嵌入)。
256
- */
257
- function apply(payload = {}, { hiddenIds = [] } = {}) {
258
- const dryRunId = String(payload?.run_id ?? "").trim();
259
- if (!dryRunId) {
260
- throw new Error("organize.apply: run_id is required (apply must reference a dryRun report)");
261
- }
262
- const report = store.getDreamRun(dryRunId);
263
- if (report?.run_type !== "organize") {
264
- throw new Error(`organize.apply: unknown organize run "${dryRunId}"`);
265
- }
266
- // apply 回执与 dryRun 报告共用 run_type='organize':只有 outcome.dry_run_id 能
267
- // 把它们分开。不查这一条,apply 的 run_id 可以被当成 dry_run_id,串成
268
- // apply → apply 的审计链,报告那一环就此消失。
269
- if (report.outcome?.dry_run_id) {
270
- throw new Error(`organize.apply: "${dryRunId}" is an apply receipt, not a dryRun report`);
271
- }
272
- if (report.status === "failed") {
273
- throw new Error(`organize.apply: dryRun "${dryRunId}" failed — re-run dryRun before applying`);
274
- }
275
- // 同一份报告只能落地一次。凭证写在**报告行**上(下面的 outcome.applied_at),
276
- // 与数据同事务;只看 apply 回执挡不住「重放同一 run_id」——那会各记一份收据,
277
- // 审计计数虚高,破坏「全留审计」的承诺。
278
- if (report.outcome?.applied_at) {
279
- throw new Error(`organize.apply: dryRun "${dryRunId}" was already applied at ${report.outcome.applied_at}`);
280
- }
281
- const decisions = Array.isArray(payload?.decisions) ? payload.decisions : null;
282
- if (!decisions) throw new Error("organize.apply: decisions must be an array");
283
-
284
- const snapshot = Array.isArray(report.input) ? report.input : [];
285
- const byIndex = new Map(snapshot.map((entry) => [Number(entry?.index), entry]));
286
- const hidden = new Set((Array.isArray(hiddenIds) ? hiddenIds : []).map((id) => String(id)));
287
- // 报告认定过的「库内被取代行」:dryRun 的 items 落在报告的 decisions 列里,
288
- // 只有那里命中过的 id 才有资格归档。不查这一条,apply 可以归档报告里根本
289
- // 没出现过的行(评审实测:preference-only 的报告里归档了 project 行)——
290
- // 整理接口就成了绕过报告的第二条改库通道。
291
- const matchedIds = new Set(
292
- (Array.isArray(report.decisions) ? report.decisions : [])
293
- .map((d) => (d?.match?.id != null ? String(d.match.id) : null))
294
- .filter(Boolean)
295
- );
296
- const skipped = [];
297
- const byId = {};
298
- const memoryIds = [];
299
- const savedRows = [];
300
- let saved = 0; // 真正新建的行
301
- let merged = 0; // 合并进既有行(同键命中)——不是新建,不能算进 saved
302
- let discarded = 0;
303
- let archived = 0;
304
-
305
- const runDecisions = () => {
306
- decisions.forEach((decision, i) => {
307
- const action = String(decision?.action ?? "").trim();
308
- if (action === "save") {
309
- const index = Number(decision?.index);
310
- const entry = Number.isInteger(index) ? byIndex.get(index) : undefined;
311
- if (!entry) {
312
- // 索引不在快照里 = dryRun 从没校验过它(越界,或当时就被 normalize 拒了)。
313
- skipped.push({ index: i, action, error: "candidate index was not accepted by this dryRun" });
314
- return;
315
- }
316
- const { index: _originalIndex, ...candidate } = entry;
317
- // 这里不吞异常:能走到这一步的候选是 dryRun 已经规范化过的合法输入,抛出
318
- // 来的只能是基础设施错误 → 让它冒到事务外,整批回滚 + failed(红线 4 的
319
- // 「单条跳过」只覆盖输入级错误,那一步在 normalizeCandidate)。
320
- const result = saveWithDedupe(candidate);
321
- if (result?.action === "created") saved++;
322
- else merged++;
323
- savedRows.push(result.memory);
324
- memoryIds.push(result.memory.id);
325
- byId[result.memory.id] = action;
326
- return;
327
- }
328
- if (action === "discard") {
329
- // 与 save 同一把尺:index 必须指向 dryRun 真的收下的候选。否则这是一条针对
330
- // 不存在的候选的「裁决」,记进回执就是虚账(skipped 为空、可能还报 noop)。
331
- const index = Number(decision?.index);
332
- const entry = Number.isInteger(index) ? byIndex.get(index) : undefined;
333
- if (!entry) {
334
- skipped.push({ index: i, action, error: "candidate index was not accepted by this dryRun" });
335
- return;
336
- }
337
- discarded++;
338
- return;
339
- }
340
- if (action === "archive") {
341
- const id = String(decision?.id ?? "").trim();
342
- // 可见性先于「是否被报告命中」:调用方看不见的行一律按不存在处理(不泄漏
343
- // 存在性,错误文案也维持「unknown, archived or out of scope」这一句)。
344
- const row = !id || hidden.has(id) ? null : store.getById(id);
345
- if (!row) {
346
- skipped.push({ index: i, action, ids: id ? [id] : [], error: "unknown, archived or out of scope" });
347
- return;
348
- }
349
- // 看得见、也存在,但报告从没把它标成命中行 → 拒绝:报告是唯一的判断依据。
350
- if (!matchedIds.has(id)) {
351
- skipped.push({ index: i, action, ids: [id], error: "id was not flagged as a match by this dryRun" });
352
- return;
353
- }
354
- if (row.archived) {
355
- byId[id] = "keep"; // 已归档 = 目标状态已达,不重复计数
356
- return;
357
- }
358
- store.setArchived(id, true);
359
- archived++;
360
- byId[id] = action;
361
- return;
362
- }
363
- skipped.push({ index: i, action, error: `unknown action "${action}"` });
364
- });
365
- };
366
-
367
- const applyRunId = randomUUID();
368
- const createdAt = new Date().toISOString();
369
- const snapshotHash = report.snapshot_hash;
370
- // 回执的 applied/byId 由调用方传入:失败路径必须报 0(事务已回滚,累积的
371
- // saved/archived 是尝试值,写进回执就是撒谎)。
372
- const record = (status, error, { byId: outcomeById = byId, applied = saved + merged + archived } = {}) => store.saveDreamRun({
373
- id: applyRunId,
374
- created_at: createdAt,
375
- status,
376
- error,
377
- provider: null,
378
- model: null,
379
- snapshot_hash: snapshotHash,
380
- input_count: snapshot.length,
381
- input: snapshot,
382
- decisions,
383
- // dry_run_id 是这条回执与它所指报告的连接键:审计能还原「报告 → 判断 → 落地」
384
- // 三步,而不用新建审计面。
385
- outcome: { dry_run_id: dryRunId, byId: outcomeById },
386
- applied,
387
- summary_stored: 0,
388
- receipt: buildReceipt({
389
- runId: applyRunId,
390
- status,
391
- snapshotHash,
392
- inputCount: snapshot.length,
393
- applied,
394
- summaryStored: false
395
- }),
396
- policy_epoch: 0,
397
- run_type: "organize",
398
- ...(skipped.length ? { skipped } : {})
399
- });
400
-
401
- let status;
402
- try {
403
- transaction(() => {
404
- runDecisions();
405
- // 给报告打「已落地」标记,与数据同事务:标记写不进去就整批回滚,不会出现
406
- // 「库改了、报告却还能再落地一次」的窗口。
407
- store.saveDreamRun({
408
- ...report,
409
- outcome: { ...(report.outcome ?? {}), applied_at: createdAt, applied_by: applyRunId }
410
- });
411
- // 只有 discard 的批次什么都没改 → noop(绝不虚报 ok)。
412
- status = skipped.length ? "degraded" : saved + merged + archived > 0 ? "ok" : "noop";
413
- // 成功回执与数据同事务:审计写不进去就一起回滚,不留「库改了却没有回执」
414
- // 的窗口(回执是这条链上唯一的证据)。
415
- record(status, null);
416
- });
417
- } catch (e) {
418
- // 事务已回滚 → 失败回执单独写。审计本身失败不能顶掉原始错误(调用方要处理的
419
- // 是原始错误那一件),但也别静默:挂到它身上,两件事都看得见。
420
- try {
421
- record("failed", String(e?.message ?? e), { byId: {}, applied: 0 });
422
- } catch (auditError) {
423
- e.audit_error = String(auditError?.message ?? auditError);
424
- }
425
- throw e;
426
- }
427
-
428
- // transaction 只负责镜像与通知,重嵌入留给调用方(service.js › transaction
429
- // 注释);document.js 用同一个 finalize 先例。放在提交之后,事务里 scheduleEmbed
430
- // 会被 txDepth 挡掉,不补这一步新写的记忆就没有向量。
431
- if (savedRows.length && typeof finalize === "function") finalize(savedRows);
432
-
433
- return {
434
- run_id: applyRunId,
435
- dry_run_id: dryRunId,
436
- status,
437
- saved,
438
- merged,
439
- discarded,
440
- archived,
441
- memory_ids: memoryIds,
442
- skipped,
443
- degraded: skipped.length > 0
444
- };
445
- }
446
-
447
- /** 一个入口带 mode 参数(维护者口径);opts 是可信调用上下文,不取 payload 里的字段。 */
448
- async function organize(payload = {}, opts = {}) {
449
- const mode = String(payload?.mode ?? "dryRun").trim();
450
- if (mode === "dryRun") return dryRun(payload);
451
- if (mode === "apply") return apply(payload, opts);
452
- throw new Error(`organize: unknown mode "${mode}" (dryRun | apply)`);
453
- }
454
-
455
- return { organize };
456
- }
1
+ // dsh-mneme/src/organize.js
2
+ // agent 主动整理接口(issue #231,口径基线 = #164 的「第三腿触发」):dryRun
3
+ // 出比对报告 → agent 判断 → apply 落库;筛除项进归档不删;调用全程留审计回执。
4
+ //
5
+ // 只做功能本体(维护者口径,issue #231):落在 service 层并带测试。**调用入口在
6
+ // 第二批**(#231:`tools.js` 的 `memory_organize` + `organizeEnabled` 默认关)——
7
+ // 本模块至今不读任何配置键,闸门与 scope 盖章都在调用方那一侧,所以本体语义与
8
+ // 第一批逐字节相同。
9
+ //
10
+ // 独立成模块(AGENTS.md 尺寸约定,同 document.js / recall-stats.js 先例):
11
+ // service.js 只做依赖注入 + barrel 出口,调用方零改动。
12
+ //
13
+ // 四条硬规则,都是「宁可什么都不做」的形态:
14
+ // 1. dryRun 不写记忆表:它只读 + 写一行审计(dream_runs)。比对是报告,不是动作。
15
+ // 2. apply 必须引用一次真实 dryRun——run_id 且 run_type='organize',且不是 apply
16
+ // 自己的回执(靠 outcome.dry_run_id 区分)、也不是 failed 的报告,且**还没落地过**
17
+ // (报告行上的 outcome.applied_at,同一份报告只认一次,挡掉重放)。没有比对过的
18
+ // 候选一律不落库,堵死「跳过报告直接写」这条绕过路径。
19
+ // 3. apply 只重放 dryRun 校验过的那份规范化快照,且只认快照里的索引:被 dryRun
20
+ // 跳过的候选没有资格落库;scope 比对按**候选自己的** scope(candidate ?? payload),
21
+ // 不会出现「报告按 scope A 比对、落地却写成 scope B」的错位。
22
+ // 3b. archive 只认报告里命中过的 id(item.match.id,落在报告的 decisions 列):报告
23
+ // 是唯一的判断依据,整理接口不能变成第二条「按 id 改库」的通道。
24
+ // 4. 筛除 = 归档(store.setArchived),绝不物理删除:归档可恢复,删除不可逆。
25
+ //
26
+ // 宽容形态(仓库红线 4,同 issue #89)只管**输入**:单条非法候选/决策跳过 + 应用
27
+ // 合法子集 + run 记 degraded,不整单拒绝。基础设施错误(嵌入器抛错、存储异常)
28
+ // 相反——记 failed 并原样上抛,绝不静默降级成「没找到重复」。审计口径复用
29
+ // dream_runs 的 receipt 语义(buildReceipt / parseReceipt 同一格式),不新建审计面。
30
+
31
+ import { createHash, randomUUID } from "node:crypto";
32
+ import { scopeKeyOf } from "./scope.js";
33
+ // 比对池要先剔掉外来行:报告里的 item.match.id 既给出「命中」结论,也是 apply 的
34
+ // archive 动作唯一许可的目标集(硬规则 3b),而 archive 直接 store.setArchived,
35
+ // 不经 saveWithDedupe 那道闸门——留着外来行,整理接口就能替别人决定归档面。
36
+ import { isImportedRow } from "./memory-source.js";
37
+ import { cosineSimilarity } from "./dream/clustering.js";
38
+ import { buildReceipt } from "./dream.js";
39
+
40
+ // 与 document 的 C2 档(#230)和 findSessionDuplicate 的 vector 档同源:同一份
41
+ // 「近重复」语义只有一个阈值,不在第三个地方另发明一个。
42
+ const MIN_SIM = 0.92;
43
+ // 向量比对对象的上限(document.js 同口径:精确层全量、向量层有界)。精确层
44
+ // 不能截断——同标题漏检会让报告给出错误的 verdict。
45
+ const CANDIDATE_LIMIT = 200;
46
+ // 单次 dryRun 的候选硬上限:主场景是「长段工作收尾整理」,不是批量导入;不设界
47
+ // 的话一次调用会把整库拉进比对。
48
+ const MAX_ITEMS = 50;
49
+ // scope 标注只做搬运(判定在 store / service 侧),省得整理接口成为第二条绕过
50
+ // scope 治理的写入通道。载荷级给的值是候选级的缺省——比对按它判 scope,落地就必须
51
+ // 带上它,否则报告与写入落在两个 scope 里。
52
+ const SCOPE_KEYS = ["sensitivity", "agent_scope", "workspace_scope", "agent_scope_source", "workspace_scope_source"];
53
+ // 比对键必须与 saveWithDedupe 的去重键**同口径**:`src/service.js` 的 scopeMatches 只比
54
+ // sensitivity / agent_scope / workspace_scope 三列,来源两列不进键(它们是元数据,合并时
55
+ // 还会把被并入行的 auto 来源升级成 explicit)。比对键里多带来源,就会出现「dryRun 报 new、
56
+ // apply 却把它并进既有行」——报告与写入错位,正是本模块硬规则 3 要挡的那类不一致。
57
+ const MATCH_SCOPE_KEYS = ["sensitivity", "agent_scope", "workspace_scope"];
58
+
59
+ /**
60
+ * Build the organizer.
61
+ *
62
+ * Injected deps are service.js closure members (store / embedQuery / saveWithDedupe
63
+ * / transaction / finalize) so this module stays free of service-internal wiring;
64
+ * `saveWithDedupe` is the normal write path, so its own epilogue (mirror sync and
65
+ * notify) is never duplicated here. `finalize` is the post-commit re-embed hook
66
+ * (document.js 同款): transaction 提交后补重嵌入,见 service.js › transaction 注释。
67
+ *
68
+ * @returns {{ organize: (payload: object, opts?: object) => Promise<object> }}
69
+ * `organize({ mode: "dryRun"|"apply", ... }, opts)` — 一个入口带 mode 参数
70
+ * (维护者倾向的形态),内部两个具名函数各自可测。
71
+ */
72
+ export function createOrganizer({ store, embedQuery, saveWithDedupe, transaction, finalize }) {
73
+ const hashOf = (value) => createHash("sha256").update(JSON.stringify(value ?? null)).digest("hex");
74
+
75
+ /**
76
+ * 候选规范化。返回 `{ candidate }` 或 `{ error }`——错误在 dryRun 里进 skipped,
77
+ * 不抛整单(红线 4)。document 刻意在此拦下:它的唯一铸造口是 registerDocument
78
+ * (#230),service 的通用 save 路径也会拒绝,这里提前拦截只是为了报告不撒谎。
79
+ */
80
+ function normalizeCandidate(raw, defaults = {}) {
81
+ const type = String(raw?.type ?? "").trim();
82
+ const title = String(raw?.title ?? "").trim();
83
+ const content = String(raw?.content ?? "").trim();
84
+ if (!type || !title || !content) return { error: "type/title/content are required" };
85
+ if (type === "document") return { error: "document rows are minted only via registerDocument (#230)" };
86
+ const candidate = {
87
+ type,
88
+ title,
89
+ content,
90
+ importance: Number.isInteger(raw?.importance) ? Math.min(5, Math.max(1, raw.importance)) : 3,
91
+ tags: [...new Set((Array.isArray(raw?.tags) ? raw.tags : []).map((t) => String(t ?? "").trim()).filter(Boolean))],
92
+ source: String(raw?.source ?? "organize")
93
+ };
94
+ for (const key of SCOPE_KEYS) {
95
+ const value = raw?.[key] ?? defaults[key];
96
+ if (value !== undefined) candidate[key] = value;
97
+ }
98
+ return { candidate };
99
+ }
100
+
101
+ /** 标题归一(同 findSessionDuplicate 的 title 档):大小写与标点不构成「不同标题」。 */
102
+ const normalizeTitle = (s) => String(s ?? "").toLowerCase().replace(/[\s\p{P}]+/gu, "");
103
+
104
+ /**
105
+ * 向量档最佳命中。拿不到向量/嵌入器时返回 null(无信号 = 不判定,不是报错);
106
+ * 但**抛出的**错误不再吞掉——嵌入器故障下的空信号会被读成「全是新条目」,让
107
+ * agent 把重复条写进库,比整单失败更糟,交 dryRun 记 failed 后上抛。
108
+ */
109
+ async function bestNear(candidate, rows, vecs) {
110
+ if (!vecs?.size) return null;
111
+ const probe = await embedQuery([candidate.title, candidate.content].filter(Boolean).join("\n"));
112
+ if (!probe) return null;
113
+ let best = null;
114
+ for (const m of rows) {
115
+ const v = vecs.get(m.id);
116
+ if (!v) continue; // 无向量的既有行不参与比对
117
+ const sim = cosineSimilarity(probe, v);
118
+ if (sim >= MIN_SIM && (!best || sim > best.sim)) best = { memory: m, sim };
119
+ }
120
+ return best;
121
+ }
122
+
123
+ /**
124
+ * 比对报告。**不写记忆表**;只写一行 dream_runs 审计(run_type='organize')。
125
+ *
126
+ * @param {object} payload
127
+ * `{ candidates: Array<{type,title,content,importance?,tags?}>, agent_scope?,
128
+ * workspace_scope?, sensitivity? }`
129
+ * @returns {Promise<{run_id, created_at, status, counts, items, skipped}>}
130
+ * items[i] = `{ index, type, title, verdict: "exact"|"near"|"new", match }`
131
+ */
132
+ async function dryRun(payload = {}) {
133
+ const raws = Array.isArray(payload.candidates) ? payload.candidates : null;
134
+ if (!raws) throw new Error("organize.dryRun: candidates must be an array");
135
+ const bounded = raws.slice(0, MAX_ITEMS);
136
+ const defaults = Object.fromEntries(SCOPE_KEYS.filter((k) => payload?.[k] !== undefined).map((k) => [k, payload[k]]));
137
+
138
+ const skipped = [];
139
+ const accepted = [];
140
+ bounded.forEach((raw, index) => {
141
+ const { candidate, error } = normalizeCandidate(raw, defaults);
142
+ if (error) skipped.push({ index, error });
143
+ else accepted.push({ index, candidate });
144
+ });
145
+ // 审计存规范化快照(带原始索引),不存调用方原始输入:apply 要重放的就是这份
146
+ // ——原始输入里混着被跳过的候选,也没有随行的 payload 级 scope。
147
+ const snapshot = accepted.map(({ index, candidate }) => ({ index, ...candidate }));
148
+
149
+ // scope 同等才比对(跨 scope 永不互判——防泄漏,与 saveWithDedupe 的去重键
150
+ // 「type+title+scope」同口径)。**按候选自己的 scope 比,不按载荷级**:
151
+ // normalizeCandidate 允许逐条给 scope(raw ?? payload),若这里只看 payload,
152
+ // 一条 agent_scope:"agent-b" 的候选就会拿默认 scope 的行去比——同一个 scope 里
153
+ // 明明已有同标题行也报 "new",报告 scope 与写入 scope 就此错位(硬规则 3)。
154
+ // 行扫描与向量读取都按 (type, scope) 缓存:一批候选通常同类型同 scope,避免逐条全表扫。
155
+ // 比对键用 MATCH_SCOPE_KEYS(三列值),搬运用 SCOPE_KEYS(五列)——两件事,别混:
156
+ // 「怎么判同一行」跟 write 路径一致,「怎么把 scope 搬到候选上」才需要连来源一起带。
157
+ const scopeOf = (source) => MATCH_SCOPE_KEYS.map((k) => scopeKeyOf(source?.[k])).join("\u0000");
158
+ const scopeMatches = (m, source) => MATCH_SCOPE_KEYS.every((k) => scopeKeyOf(m[k]) === scopeKeyOf(source?.[k]));
159
+ const cacheKey = (candidate) => `${candidate.type}\u0000${scopeOf(candidate)}`;
160
+ const rowsByScope = new Map();
161
+ const rowsFor = (candidate) => {
162
+ const key = cacheKey(candidate);
163
+ if (!rowsByScope.has(key)) {
164
+ rowsByScope.set(
165
+ key,
166
+ store.list({ type: candidate.type, limit: null }).filter((m) => !isImportedRow(m) && !m.archived && scopeMatches(m, candidate))
167
+ );
168
+ }
169
+ return rowsByScope.get(key);
170
+ };
171
+ const vecsByScope = new Map();
172
+ const vecsFor = (candidate, rows) => {
173
+ const key = cacheKey(candidate);
174
+ if (!vecsByScope.has(key)) {
175
+ vecsByScope.set(key, store.getEmbeddings(rows.slice(0, CANDIDATE_LIMIT).map((m) => m.id)));
176
+ }
177
+ return vecsByScope.get(key);
178
+ };
179
+
180
+ const runId = randomUUID();
181
+ const createdAt = new Date().toISOString();
182
+ // 哈希与 input 必须指向同一份东西:审计存的是规范化快照(apply 要重放它),
183
+ // 所以哈希也算快照——否则回执里的 snapshot_hash 描述的是调用方原始输入,
184
+ // 谁也校验不了。
185
+ const snapshotHash = hashOf(snapshot);
186
+ const record = (status, { error = null, decisions = items, skipped: skipList = skipped, applied = 0 } = {}) =>
187
+ store.saveDreamRun({
188
+ id: runId,
189
+ created_at: createdAt,
190
+ status,
191
+ error,
192
+ provider: null,
193
+ model: null,
194
+ snapshot_hash: snapshotHash,
195
+ input_count: snapshot.length,
196
+ input: snapshot,
197
+ decisions,
198
+ applied,
199
+ summary_stored: 0,
200
+ receipt: buildReceipt({ runId, status, snapshotHash, inputCount: snapshot.length, applied, summaryStored: false }),
201
+ policy_epoch: 0,
202
+ run_type: "organize",
203
+ ...(skipList.length ? { skipped: skipList } : {})
204
+ });
205
+
206
+ const items = [];
207
+ try {
208
+ for (const { index, candidate } of accepted) {
209
+ const rows = rowsFor(candidate);
210
+ let item = { index, type: candidate.type, title: candidate.title, verdict: "new", match: null };
211
+ const exact = rows.find((m) => normalizeTitle(m.title) === normalizeTitle(candidate.title));
212
+ if (exact) {
213
+ item = { ...item, verdict: "exact", match: { id: exact.id, title: exact.title, sim: null } };
214
+ } else {
215
+ const near = await bestNear(candidate, rows, vecsFor(candidate, rows));
216
+ if (near) {
217
+ item = { ...item, verdict: "near", match: { id: near.memory.id, title: near.memory.title, sim: near.sim } };
218
+ }
219
+ }
220
+ items.push(item);
221
+ }
222
+ } catch (e) {
223
+ // 审计先留痕、再上抛:一份没有回执的失败报告,事后无法与「没调用过」区分。
224
+ record("failed", { error: String(e?.message ?? e) });
225
+ throw e;
226
+ }
227
+
228
+ const counts = {
229
+ total: items.length,
230
+ exact: items.filter((i) => i.verdict === "exact").length,
231
+ near: items.filter((i) => i.verdict === "near").length,
232
+ new: items.filter((i) => i.verdict === "new").length,
233
+ skipped: skipped.length,
234
+ truncated: raws.length - bounded.length
235
+ };
236
+ // 有被拒的候选 → degraded(如实标记),而不是 ok 掩盖过去;一条都没报 → noop。
237
+ const status = skipped.length ? "degraded" : items.length ? "ok" : "noop";
238
+ record(status);
239
+ return { run_id: runId, created_at: createdAt, status, counts, items, skipped };
240
+ }
241
+
242
+ /**
243
+ * 按 agent 的判断落库。decisions 逐条:
244
+ * `{ action: "save", index }` — 写候选(走 saveWithDedupe:同键自动合并)
245
+ * `{ action: "discard", index }` — 不写(只进回执,不删任何东西)
246
+ * `{ action: "archive", id }` — 库内被取代的行 → 归档(可恢复,不删)
247
+ *
248
+ * @param {object} payload `{ run_id, decisions }`
249
+ * @param {object} [opts] `{ hiddenIds }` — 调用者 scope 看不见的行按「不存在」
250
+ * 处理(#170 复核项 4:无存在性泄漏),与 document 的 hiddenEvidenceIds 同形:
251
+ * **可见性由可信调用方算好传入**(tools 层先例:register_document),不取 payload
252
+ * 里的字段——那是调用方可自选的内容,不是授权。
253
+ * @returns {{run_id, dry_run_id, status, saved, merged, discarded, archived, memory_ids, skipped, degraded}}
254
+ * `saved` 只数**新建**的行,合并进既有行的算 `merged`(两者之和 = 落地生效的条数);
255
+ * `memory_ids` 含合并命中的既有行 id(它们同样需要重嵌入)。
256
+ */
257
+ function apply(payload = {}, { hiddenIds = [] } = {}) {
258
+ const dryRunId = String(payload?.run_id ?? "").trim();
259
+ if (!dryRunId) {
260
+ throw new Error("organize.apply: run_id is required (apply must reference a dryRun report)");
261
+ }
262
+ const report = store.getDreamRun(dryRunId);
263
+ if (report?.run_type !== "organize") {
264
+ throw new Error(`organize.apply: unknown organize run "${dryRunId}"`);
265
+ }
266
+ // apply 回执与 dryRun 报告共用 run_type='organize':只有 outcome.dry_run_id 能
267
+ // 把它们分开。不查这一条,apply 的 run_id 可以被当成 dry_run_id,串成
268
+ // apply → apply 的审计链,报告那一环就此消失。
269
+ if (report.outcome?.dry_run_id) {
270
+ throw new Error(`organize.apply: "${dryRunId}" is an apply receipt, not a dryRun report`);
271
+ }
272
+ if (report.status === "failed") {
273
+ throw new Error(`organize.apply: dryRun "${dryRunId}" failed — re-run dryRun before applying`);
274
+ }
275
+ // 同一份报告只能落地一次。凭证写在**报告行**上(下面的 outcome.applied_at),
276
+ // 与数据同事务;只看 apply 回执挡不住「重放同一 run_id」——那会各记一份收据,
277
+ // 审计计数虚高,破坏「全留审计」的承诺。
278
+ if (report.outcome?.applied_at) {
279
+ throw new Error(`organize.apply: dryRun "${dryRunId}" was already applied at ${report.outcome.applied_at}`);
280
+ }
281
+ const decisions = Array.isArray(payload?.decisions) ? payload.decisions : null;
282
+ if (!decisions) throw new Error("organize.apply: decisions must be an array");
283
+
284
+ const snapshot = Array.isArray(report.input) ? report.input : [];
285
+ const byIndex = new Map(snapshot.map((entry) => [Number(entry?.index), entry]));
286
+ const hidden = new Set((Array.isArray(hiddenIds) ? hiddenIds : []).map((id) => String(id)));
287
+ // 报告认定过的「库内被取代行」:dryRun 的 items 落在报告的 decisions 列里,
288
+ // 只有那里命中过的 id 才有资格归档。不查这一条,apply 可以归档报告里根本
289
+ // 没出现过的行(评审实测:preference-only 的报告里归档了 project 行)——
290
+ // 整理接口就成了绕过报告的第二条改库通道。
291
+ const matchedIds = new Set(
292
+ (Array.isArray(report.decisions) ? report.decisions : [])
293
+ .map((d) => (d?.match?.id != null ? String(d.match.id) : null))
294
+ .filter(Boolean)
295
+ );
296
+ const skipped = [];
297
+ const byId = {};
298
+ const memoryIds = [];
299
+ const savedRows = [];
300
+ let saved = 0; // 真正新建的行
301
+ let merged = 0; // 合并进既有行(同键命中)——不是新建,不能算进 saved
302
+ let discarded = 0;
303
+ let archived = 0;
304
+
305
+ const runDecisions = () => {
306
+ decisions.forEach((decision, i) => {
307
+ const action = String(decision?.action ?? "").trim();
308
+ if (action === "save") {
309
+ const index = Number(decision?.index);
310
+ const entry = Number.isInteger(index) ? byIndex.get(index) : undefined;
311
+ if (!entry) {
312
+ // 索引不在快照里 = dryRun 从没校验过它(越界,或当时就被 normalize 拒了)。
313
+ skipped.push({ index: i, action, error: "candidate index was not accepted by this dryRun" });
314
+ return;
315
+ }
316
+ const { index: _originalIndex, ...candidate } = entry;
317
+ // 这里不吞异常:能走到这一步的候选是 dryRun 已经规范化过的合法输入,抛出
318
+ // 来的只能是基础设施错误 → 让它冒到事务外,整批回滚 + failed(红线 4 的
319
+ // 「单条跳过」只覆盖输入级错误,那一步在 normalizeCandidate)。
320
+ const result = saveWithDedupe(candidate);
321
+ if (result?.action === "created") saved++;
322
+ else merged++;
323
+ savedRows.push(result.memory);
324
+ memoryIds.push(result.memory.id);
325
+ byId[result.memory.id] = action;
326
+ return;
327
+ }
328
+ if (action === "discard") {
329
+ // 与 save 同一把尺:index 必须指向 dryRun 真的收下的候选。否则这是一条针对
330
+ // 不存在的候选的「裁决」,记进回执就是虚账(skipped 为空、可能还报 noop)。
331
+ const index = Number(decision?.index);
332
+ const entry = Number.isInteger(index) ? byIndex.get(index) : undefined;
333
+ if (!entry) {
334
+ skipped.push({ index: i, action, error: "candidate index was not accepted by this dryRun" });
335
+ return;
336
+ }
337
+ discarded++;
338
+ return;
339
+ }
340
+ if (action === "archive") {
341
+ const id = String(decision?.id ?? "").trim();
342
+ // 可见性先于「是否被报告命中」:调用方看不见的行一律按不存在处理(不泄漏
343
+ // 存在性,错误文案也维持「unknown, archived or out of scope」这一句)。
344
+ const row = !id || hidden.has(id) ? null : store.getById(id);
345
+ if (!row) {
346
+ skipped.push({ index: i, action, ids: id ? [id] : [], error: "unknown, archived or out of scope" });
347
+ return;
348
+ }
349
+ // 看得见、也存在,但报告从没把它标成命中行 → 拒绝:报告是唯一的判断依据。
350
+ if (!matchedIds.has(id)) {
351
+ skipped.push({ index: i, action, ids: [id], error: "id was not flagged as a match by this dryRun" });
352
+ return;
353
+ }
354
+ if (row.archived) {
355
+ byId[id] = "keep"; // 已归档 = 目标状态已达,不重复计数
356
+ return;
357
+ }
358
+ store.setArchived(id, true);
359
+ archived++;
360
+ byId[id] = action;
361
+ return;
362
+ }
363
+ skipped.push({ index: i, action, error: `unknown action "${action}"` });
364
+ });
365
+ };
366
+
367
+ const applyRunId = randomUUID();
368
+ const createdAt = new Date().toISOString();
369
+ const snapshotHash = report.snapshot_hash;
370
+ // 回执的 applied/byId 由调用方传入:失败路径必须报 0(事务已回滚,累积的
371
+ // saved/archived 是尝试值,写进回执就是撒谎)。
372
+ const record = (status, error, { byId: outcomeById = byId, applied = saved + merged + archived } = {}) => store.saveDreamRun({
373
+ id: applyRunId,
374
+ created_at: createdAt,
375
+ status,
376
+ error,
377
+ provider: null,
378
+ model: null,
379
+ snapshot_hash: snapshotHash,
380
+ input_count: snapshot.length,
381
+ input: snapshot,
382
+ decisions,
383
+ // dry_run_id 是这条回执与它所指报告的连接键:审计能还原「报告 → 判断 → 落地」
384
+ // 三步,而不用新建审计面。
385
+ outcome: { dry_run_id: dryRunId, byId: outcomeById },
386
+ applied,
387
+ summary_stored: 0,
388
+ receipt: buildReceipt({
389
+ runId: applyRunId,
390
+ status,
391
+ snapshotHash,
392
+ inputCount: snapshot.length,
393
+ applied,
394
+ summaryStored: false
395
+ }),
396
+ policy_epoch: 0,
397
+ run_type: "organize",
398
+ ...(skipped.length ? { skipped } : {})
399
+ });
400
+
401
+ let status;
402
+ try {
403
+ transaction(() => {
404
+ runDecisions();
405
+ // 给报告打「已落地」标记,与数据同事务:标记写不进去就整批回滚,不会出现
406
+ // 「库改了、报告却还能再落地一次」的窗口。
407
+ store.saveDreamRun({
408
+ ...report,
409
+ outcome: { ...(report.outcome ?? {}), applied_at: createdAt, applied_by: applyRunId }
410
+ });
411
+ // 只有 discard 的批次什么都没改 → noop(绝不虚报 ok)。
412
+ status = skipped.length ? "degraded" : saved + merged + archived > 0 ? "ok" : "noop";
413
+ // 成功回执与数据同事务:审计写不进去就一起回滚,不留「库改了却没有回执」
414
+ // 的窗口(回执是这条链上唯一的证据)。
415
+ record(status, null);
416
+ });
417
+ } catch (e) {
418
+ // 事务已回滚 → 失败回执单独写。审计本身失败不能顶掉原始错误(调用方要处理的
419
+ // 是原始错误那一件),但也别静默:挂到它身上,两件事都看得见。
420
+ try {
421
+ record("failed", String(e?.message ?? e), { byId: {}, applied: 0 });
422
+ } catch (auditError) {
423
+ e.audit_error = String(auditError?.message ?? auditError);
424
+ }
425
+ throw e;
426
+ }
427
+
428
+ // transaction 只负责镜像与通知,重嵌入留给调用方(service.js › transaction
429
+ // 注释);document.js 用同一个 finalize 先例。放在提交之后,事务里 scheduleEmbed
430
+ // 会被 txDepth 挡掉,不补这一步新写的记忆就没有向量。
431
+ if (savedRows.length && typeof finalize === "function") finalize(savedRows);
432
+
433
+ return {
434
+ run_id: applyRunId,
435
+ dry_run_id: dryRunId,
436
+ status,
437
+ saved,
438
+ merged,
439
+ discarded,
440
+ archived,
441
+ memory_ids: memoryIds,
442
+ skipped,
443
+ degraded: skipped.length > 0
444
+ };
445
+ }
446
+
447
+ /** 一个入口带 mode 参数(维护者口径);opts 是可信调用上下文,不取 payload 里的字段。 */
448
+ async function organize(payload = {}, opts = {}) {
449
+ const mode = String(payload?.mode ?? "dryRun").trim();
450
+ if (mode === "dryRun") return dryRun(payload);
451
+ if (mode === "apply") return apply(payload, opts);
452
+ throw new Error(`organize: unknown mode "${mode}" (dryRun | apply)`);
453
+ }
454
+
455
+ return { organize };
456
+ }