@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
@@ -1,640 +1,640 @@
1
- // dsh-mneme/src/wiki/queries.js
2
- // chorography 的读侧(M1):仓库列表 / 模块图 / 页面正文 / 关键词检索。
3
- // 收尾档追加:向量检索(P37–P39,惰性物化 + cosine)。
4
- // 检索读侧零写路径的例外只有一处——向量物化写 choro_vectors(P37:检索时惰性
5
- // 补嵌,host 侧才有 embedder;那是向量账本,不是页面真源)。
6
- //
7
- // 真源纪律(spec §4 / P12):页面正文永远读仓库磁盘的 MD(root_path + DB 里的
8
- // rel_path),库里的表只是索引与状态账。slug 只从 DB 白名单解析,路径拼装零用户
9
- // 输入——面板与工具传进来的 slug 查不到模块行就返回 null,绝不用它拼路径。
10
- import { readFileSync, existsSync, realpathSync } from "node:fs";
11
- import { join, resolve, sep } from "node:path";
12
- // 过期判定复用既有三件真源(t0 的 gitHead、plan 的指纹、render 的模板版本),
13
- // 面板侧不另造一套判据——两套判据迟早会对同一仓库给出两个答案。
14
- import { gitHead } from "./t0.js";
15
- import { loadPlanFromRepo } from "./plan.js";
16
- import { TEMPLATE_VERSION, T1_SECTION_START } from "./render.js";
17
- // claim 失效判定的纯函数(借 openwiki 的 Grounded Claims):解析 diff + 逐条比对
18
- // 三列证据。放独立模块是因为它零 DB、零 IO(除 diffSince 那一次 git),可单测。
19
- import { diffSince, summarizeClaims } from "./claims.js";
20
-
21
- // 页正文去 frontmatter / CHORO 标记行后作为嵌入文本(P40:嵌页级全文)。
22
- function pageEmbedText(content) {
23
- const lines = String(content ?? "").split("\n");
24
- let start = 0;
25
- if (lines[0]?.trim() === "---") {
26
- start = 1;
27
- while (start < lines.length && lines[start].trim() !== "---") start++;
28
- start++;
29
- }
30
- return lines
31
- .slice(start)
32
- .filter((l) => !l.startsWith("<!-- CHORO:T1:") && !l.startsWith("<!-- CHORO:HUMAN:"))
33
- .join("\n")
34
- .trim();
35
- }
36
-
37
- function cosine(a, b) {
38
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || a.length === 0) return 0;
39
- let dot = 0;
40
- let na = 0;
41
- let nb = 0;
42
- for (let i = 0; i < a.length; i++) {
43
- dot += a[i] * b[i];
44
- na += a[i] * a[i];
45
- nb += b[i] * b[i];
46
- }
47
- const denom = Math.sqrt(na) * Math.sqrt(nb);
48
- return denom === 0 ? 0 : dot / denom;
49
- }
50
-
51
- export function createChoroQueries({ store }) {
52
- const db = store.db;
53
-
54
- /** 已登记仓库(含模块/页面计数,面板仓库选择器与工具的 repo 解析共用)。 */
55
- function listRepos() {
56
- const rows = db.prepare(
57
- `SELECT r.id, r.root_path, r.name, r.plan_fingerprint, r.git_baseline, r.updated_at,
58
- (SELECT COUNT(*) FROM choro_modules m WHERE m.repo_id = r.id) AS module_count,
59
- (SELECT COUNT(*) FROM choro_pages p JOIN choro_modules m2 ON p.module_id = m2.id
60
- WHERE m2.repo_id = r.id) AS page_count,
61
- (SELECT COUNT(*) FROM choro_claims c
62
- JOIN choro_pages p2 ON c.page_id = p2.id
63
- JOIN choro_modules m3 ON p2.module_id = m3.id
64
- WHERE m3.repo_id = r.id) AS claim_count
65
- FROM choro_repos r ORDER BY r.id`
66
- ).all();
67
- return rows;
68
- }
69
-
70
- /**
71
- * 面板仓库列表(契约 §3.2):在 listRepos 的行上补契约要求的字段。
72
- *
73
- * 为什么另开一个函数而不是改 listRepos 的返回形状:listRepos 的
74
- * module_count / page_count 被工具层与查询层测试锁着(下划线命名),改名就是
75
- * 破坏既有调用方;而契约要的是 modules / pages 这样的对外字段。两套名字并存
76
- * 难看,但改名的连带面比难看贵得多——装饰层把它包一层,工具层照旧读原始行。
77
- *
78
- * 每个仓库都要 fork 一次 git 判过期:这是用户点开面板才走的路(不是每轮注入的
79
- * 热路径),一次 rev-parse 几十毫秒在交互里不可感知。
80
- */
81
- /**
82
- * 契约字段的仓库对象(stale 由调用方决定是否重算)。
83
- *
84
- * 注意区分两个入口:repoFacts 每次都会 fork git 判过期;repoFromRow 是纯行投影,
85
- * 给「已经知道过期原因、手上有判定结果」的调用方用(例如自动重跑之后要紧着按
86
- * 新基线说话,不能再拿旧行算一遍)。混用会得到自相矛盾的回执。
87
- */
88
- function repoFromRow(row) {
89
- const rootPath = String(row.root_path ?? "");
90
- return {
91
- id: row.id,
92
- name: row.name,
93
- rootPath,
94
- modules: Number(row.module_count ?? 0),
95
- pages: Number(row.page_count ?? 0),
96
- claims: Number(row.claim_count ?? 0),
97
- // legacy = 这个仓库里有页面是用旧模板(或 M0 的 NULL)渲染的:重跑一次 T0 就
98
- // 会把它们升级成当前模板,面板据此提示「建议重建」而不是让用户自己比对版本。
99
- legacy: legacyPages(row.id) > 0,
100
- // enrichedPages:已经富化过(页面里有 T1 语义段、库里有 claims)的页数。
101
- // 自动刷新要看它:这类页重跑 T0 会退回骨架,所以那条路必须先问一句。
102
- enrichedPages: enrichedPageCount(row.id),
103
- lastRunAt: row.updated_at ?? null
104
- };
105
- }
106
-
107
- /**
108
- * 已富化的页数。判据取库里的 status(T1 成功后会写 enriched),而不是读盘找
109
- * T1 标记——这是条给面板列表用的轻查询,几百页也不该逐文件 readFileSync。
110
- * 两边可能短暂不一致(例如有人手改了 MD),但「要不要提醒」这种提示用途,
111
- * 库里的账本就是最合适的真源;真正判断「会丢什么」的仍是 T0 的 collectT1Loss(读盘)。
112
- */
113
- function enrichedPageCount(repoId) {
114
- const hit = db.prepare(
115
- `SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
116
- WHERE m.repo_id = ? AND p.status = 'enriched'`
117
- ).get(repoId);
118
- return Number(hit?.n ?? 0);
119
- }
120
-
121
- function legacyPages(repoId) {
122
- const hit = db.prepare(
123
- `SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
124
- WHERE m.repo_id = ? AND (p.template_version IS NULL OR p.template_version <> ?)`
125
- ).get(repoId, TEMPLATE_VERSION);
126
- return Number(hit?.n ?? 0);
127
- }
128
-
129
- function repoFacts(row) {
130
- const rootPath = String(row.root_path ?? "");
131
- // git 优先:代码变了是更硬的「该重跑」信号,plan 变了才退而求其次——两者同时
132
- // 成立时面板只说一句话,说「git」比说「plan」更贴近用户看到的现象(他刚改了码)。
133
- const head = rootPath === "" ? null : gitHead(rootPath);
134
- const baseline = String(row.git_baseline ?? "").trim();
135
- let staleReason = null;
136
- if (head && head !== baseline) {
137
- staleReason = "git";
138
- } else if (rootPath !== "") {
139
- // 读不到 git(head 为 null)时不直接判「没过期」:plan.yaml 可能已经被改过,
140
- // 那条判据不依赖 git,仍能给出答案。
141
- let planFingerprintNow = null;
142
- try { planFingerprintNow = loadPlanFromRepo(rootPath).fingerprint; } catch { planFingerprintNow = null; }
143
- // plan.yaml 校验不过时 loadPlanFromRepo 会抛——那是「现在读不出结论」,不是
144
- // 「过期了」,所以落 null 保持 stale=false,让重建按钮自己去报那条 YAML 错误。
145
- if (planFingerprintNow && planFingerprintNow !== String(row.plan_fingerprint ?? "")) {
146
- staleReason = "plan";
147
- }
148
- }
149
- return {
150
- ...repoFromRow(row),
151
- stale: staleReason !== null,
152
- staleReason
153
- };
154
- }
155
-
156
- /** 单仓的原始 listRepos 行(自动重跑之后要拿推进过的基线,不能再用旧行)。 */
157
- function listRepoRow(repoId) {
158
- return listRepos().find((r) => Number(r.id) === Number(repoId)) ?? null;
159
- }
160
-
161
- /** 契约形状的仓库列表:listRepos 的原始行 → 对外字段。 */
162
- function listRepoFacts() {
163
- return listRepos().map((row) => repoFacts(row));
164
- }
165
-
166
- /** 仓库的契约字段(按 id/name,给 /choro/page 这类已知单仓的读路径用)。 */
167
- function repoFactsById(repoId) {
168
- const row = listRepoRow(repoId);
169
- return row ? repoFacts(row) : null;
170
- }
171
-
172
- /**
173
- * 按绝对路径找已登记仓库(面板「选目录 → 生成」必需):T0 apply 落库时存的是
174
- * realpath,所以这里也必须先 realpath 再比,否则 Windows 上盘符大小写/短名/符号
175
- * 链接会让同一个目录被当成新仓库重复登记一次。
176
- */
177
- function repoByRoot(rootPath) {
178
- const raw = String(rootPath ?? "").trim();
179
- if (raw === "") return null;
180
- let normalized;
181
- try { normalized = realpathSync(resolve(raw)); } catch { return null; }
182
- return db.prepare("SELECT * FROM choro_repos WHERE root_path = ?").get(normalized) ?? null;
183
- }
184
-
185
- /** repo 解析:数字 id 或显示名(root_path 是本机私有路径,不作为对外键)。 */
186
- function resolveRepo(ref) {
187
- if (ref === undefined || ref === null || ref === "") {
188
- const rows = listRepos();
189
- if (rows.length === 1) return rows[0];
190
- if (rows.length === 0) return null;
191
- return { ambiguous: rows.map((r) => ({ id: r.id, name: r.name })) };
192
- }
193
- const byId = db.prepare("SELECT * FROM choro_repos WHERE id = ?").get(Number(ref));
194
- if (byId) return byId;
195
- const byName = db.prepare("SELECT * FROM choro_repos WHERE name = ?").get(String(ref));
196
- return byName ?? null;
197
- }
198
-
199
- /** 模块表 + 跨模块边(choro_module_map 与面板左侧树共用同一份数据)。 */
200
- function moduleMap(repoId) {
201
- const modules = db.prepare(
202
- `SELECT m.id, m.slug, m.title, m.parent_id, m.file_count, m.fingerprint, m.updated_at,
203
- p.status AS page_status, p.rel_path AS page_rel_path
204
- FROM choro_modules m
205
- LEFT JOIN choro_pages p ON p.module_id = m.id
206
- WHERE m.repo_id = ? ORDER BY m.slug`
207
- ).all(repoId);
208
- const edges = db.prepare(
209
- `SELECT from_slug, to_slug, weight FROM choro_edges WHERE repo_id = ?
210
- ORDER BY from_slug, to_slug`
211
- ).all(repoId);
212
- // 纯加法补一个 camelCase 的 relPath:面板从地志页导入知识点要拼「根路径 + 页相对
213
- // 路径」,而这里历史上直出 SQL 行的 snake_case(page_rel_path)。C 线按契约读的是
214
- // mod.relPath,字段一缺就恒 undefined,用户选完模块点导入只会看到「请先选模块」。
215
- // 原字段一律保留——src/tools.js 的 render 与 choro-command 都还在吃 file_count /
216
- // page_status,改形状会波及它们;outline() 里的同名映射是这件事的正确写法。
217
- for (const m of modules) m.relPath = m.page_rel_path ?? null;
218
- return { modules, edges };
219
- }
220
-
221
- /**
222
- * 读一页正文。slug 必须命中 DB 的模块行(P12:路径零用户输入);索引仍在但
223
- * 文件缺失或不可读时带回相对路径,让调用方给出对应的恢复提示。
224
- */
225
- function readPage(repoId, slug) {
226
- const row = db.prepare(
227
- `SELECT m.slug, m.title, m.file_count, m.parent_id, p.rel_path, p.status,
228
- p.git_baseline, p.template_version, p.generated_at, p.human_block_count
229
- FROM choro_modules m JOIN choro_pages p ON p.module_id = m.id
230
- WHERE m.repo_id = ? AND m.slug = ?`
231
- ).get(repoId, String(slug));
232
- if (!row) return null;
233
- const repo = db.prepare("SELECT root_path, name FROM choro_repos WHERE id = ?").get(repoId);
234
- const abs = join(repo.root_path, row.rel_path);
235
- let content;
236
- try {
237
- content = readFileSync(abs, "utf8");
238
- } catch (cause) {
239
- const error = cause.code === "ENOENT" || cause.code === "ENOTDIR" ? "page-missing" : "page-unreadable";
240
- return { error, slug: row.slug, relPath: row.rel_path };
241
- }
242
- return {
243
- slug: row.slug,
244
- title: row.title,
245
- fileCount: row.file_count,
246
- status: row.status,
247
- gitBaseline: row.git_baseline,
248
- templateVersion: row.template_version,
249
- generatedAt: row.generated_at,
250
- humanBlockCount: row.human_block_count,
251
- repoName: repo.name,
252
- relPath: row.rel_path,
253
- content,
254
- };
255
- }
256
-
257
- /**
258
- * 关键词检索(P10):按 `## ` 分节做大小写不敏感子串匹配,命中返回页 slug、
259
- * 节标题、起始行号与摘录。只扫 DB 里登记过的页面文件(不是全目录 glob——
260
- * 划分变更后未重算的孤儿文件不该被搜出来)。需要提示检索不完整的调用方可接收
261
- * 不可读的页 slug。q 为空返回空数组不报错。
262
- */
263
- function search(repoId, q, { moduleSlug = null, limit = 20, onUnreadablePage } = {}) {
264
- const needle = String(q ?? "").trim().toLowerCase();
265
- if (needle === "") return [];
266
- const cap = Math.max(1, Math.min(100, Number(limit) || 20));
267
- const pages = db.prepare(
268
- `SELECT m.slug, m.title AS module_title, p.rel_path
269
- FROM choro_modules m JOIN choro_pages p ON p.module_id = m.id
270
- WHERE m.repo_id = ? AND (? IS NULL OR m.slug = ?)
271
- ORDER BY m.slug`
272
- ).all(repoId, moduleSlug, moduleSlug);
273
- const repo = db.prepare("SELECT root_path FROM choro_repos WHERE id = ?").get(repoId);
274
- const hits = [];
275
- for (const page of pages) {
276
- const abs = join(repo.root_path, page.rel_path);
277
- let text;
278
- try {
279
- text = readFileSync(abs, "utf8");
280
- } catch {
281
- onUnreadablePage?.(page.slug);
282
- continue;
283
- }
284
- const lines = text.split("\n");
285
- // 分节:每个 `## ` 标题开启一节,节内所有行都属于该节;文件头(frontmatter
286
- // 之后到第一个 `## `)算 "(页首)" 一节,保证对 title/摘要的命中也有落点。
287
- let section = "(页首)";
288
- let sectionStart = 1;
289
- for (let i = 0; i < lines.length; i++) {
290
- const line = lines[i];
291
- if (i > 0 && /^## /.test(line)) {
292
- section = line.replace(/^## /, "").trim();
293
- sectionStart = i + 1;
294
- }
295
- if (line.toLowerCase().includes(needle)) {
296
- hits.push({
297
- slug: page.slug,
298
- moduleTitle: page.module_title,
299
- section,
300
- line: i + 1,
301
- sectionStart,
302
- excerpt: line.trim().slice(0, 200),
303
- });
304
- if (hits.length >= cap) return hits;
305
- }
306
- }
307
- }
308
- return hits;
309
- }
310
-
311
- /**
312
- * 按会话工作目录找已登记仓库(M3 开局注入的唯一入口)。
313
- *
314
- * 三个刻意的取舍:
315
- * 1. **祖先前缀匹配**而不是精确相等——真实会话的 cwd 常常是仓库子目录
316
- * (`D:/repo/src`),精确相等会让绝大部分会话匹配不到,等于逼用户
317
- * 「必须在仓库根开会话」这条暗规则。
318
- * 2. 两侧都做 realpath 归一:Windows 上盘符大小写、8.3 短名、符号链接
319
- * 会让同一个目录写出不同字符串;t0 落库时存的就是 realpath。
320
- * 3. **最长前缀胜出**——仓库嵌套(子模块、`.worktrees/`)时取最深那层,
321
- * 否则内层会话会被外层仓库抢走。
322
- * cwd 不存在或不是目录时返回 null(不是抛错):注入在热路径上,失败只
323
- * 意味着「本帧不注入」。
324
- */
325
- function repoByCwd(cwd) {
326
- const raw = String(cwd ?? "").trim();
327
- if (raw === "") return null;
328
- let normalized;
329
- try {
330
- normalized = realpathSync(resolve(raw));
331
- } catch {
332
- return null;
333
- }
334
- const cmp = (p) => (process.platform === "win32" ? p.toLowerCase() : p);
335
- const target = cmp(normalized);
336
- const rows = db.prepare("SELECT * FROM choro_repos ORDER BY id").all();
337
- let best = null;
338
- for (const row of rows) {
339
- const root = cmp(String(row.root_path ?? ""));
340
- if (root === "") continue;
341
- const hit = target === root || target.startsWith(root.endsWith(sep) ? root : root + sep);
342
- if (!hit) continue;
343
- if (!best || root.length > cmp(String(best.root_path)).length) best = row;
344
- }
345
- return best ?? null;
346
- }
347
-
348
- /**
349
- * 注入与 /choro 共用的最小提纲(M3):模块名 + 文件数 + 页状态 + 页相对路径。
350
- * **不读页正文**——spec §7 要求「只注标题级不注正文」,正文走 readPage。
351
- * parentSlug 单独解出来给渲染层做层级缩进(DB 里存的是 parent_id)。
352
- */
353
- function outline(repoId) {
354
- const repo = db.prepare(
355
- "SELECT id, root_path, name, git_baseline, plan_fingerprint, updated_at FROM choro_repos WHERE id = ?"
356
- ).get(repoId);
357
- if (!repo) return null;
358
- const rows = db.prepare(
359
- `SELECT m.id, m.slug, m.title, m.parent_id, m.file_count,
360
- p.status AS page_status, p.rel_path AS page_rel_path, p.generated_at
361
- FROM choro_modules m
362
- LEFT JOIN choro_pages p ON p.module_id = m.id
363
- WHERE m.repo_id = ? ORDER BY m.slug`
364
- ).all(repoId);
365
- const slugById = new Map(rows.map((r) => [r.id, r.slug]));
366
- let generatedAt = null;
367
- const modules = rows.map((r) => {
368
- if (r.generated_at && (generatedAt === null || r.generated_at > generatedAt)) generatedAt = r.generated_at;
369
- return {
370
- slug: r.slug,
371
- title: r.title,
372
- fileCount: r.file_count,
373
- parentSlug: r.parent_id === null || r.parent_id === undefined ? null : slugById.get(r.parent_id) ?? null,
374
- status: r.page_status ?? null,
375
- relPath: r.page_rel_path ?? null,
376
- };
377
- });
378
- return {
379
- repo: {
380
- id: repo.id,
381
- name: repo.name,
382
- rootPath: repo.root_path,
383
- gitBaseline: repo.git_baseline,
384
- updatedAt: repo.updated_at,
385
- },
386
- generatedAt: generatedAt ?? repo.updated_at ?? null,
387
- modules,
388
- };
389
- }
390
-
391
- /**
392
- * Claim 粒度的失效清单(借 openwiki 的 Grounded Claims):把「仓库代码变过就整仓过期」
393
- * 降成「这 2 页里的 5 条结论被改动碰过」。
394
- *
395
- * 为什么放读侧而不写成后台任务:判据是纯机械的(git diff 的 hunk 区间 × claims 的
396
- * 三列证据),不需要模型,一次 diff 几十毫秒——面板打开时现算,永远比任何落库的
397
- * 缓存新鲜。写路径一条不加,红线不动。
398
- *
399
- * **比对起点是每页自己的 verified_baseline,不是全仓 git_baseline。** claims 的行号
400
- * 是那页被富化的那一刻记下的;全仓基线是 T0 的时点。若 T1 晚于 T0(常态),用全仓
401
- * 基线起算就会把「T1 之前就发生、claim 早已算进去」的改动也算进 diff,误报 touched。
402
- * 所以按 verified_baseline 分组、每组各跑一次 git diff(同一起点只跑一次)。
403
- *
404
- * 五种「说不清」要分开说,别混成一个空清单:
405
- * - repo 不存在 → null(调用方 404)
406
- * - git 读不到 HEAD → 全仓 anchorStale + reason "git-unreadable"
407
- * - 某页基线认不出(浅克隆/force-push)→ 该页 anchorStale,**其余页照常判**
408
- * - 某页没记过验证基线(老数据/拿不到 HEAD)→ 该页 baselineUnknown,一条不判
409
- * - 起点 == HEAD → 那组真没变,clean
410
- */
411
- function claimDrift(repoId) {
412
- const repo = db.prepare("SELECT id, root_path, name, git_baseline FROM choro_repos WHERE id = ?").get(repoId);
413
- if (!repo) return null;
414
- const rootPath = String(repo.root_path ?? "");
415
- const rows = db.prepare(
416
- `SELECT c.id, c.statement, c.kind, c.source_path, c.start_line, c.end_line,
417
- m.slug AS page_slug, m.title AS page_title, p.rel_path
418
- FROM choro_claims c
419
- JOIN choro_pages p ON c.page_id = p.id
420
- JOIN choro_modules m ON p.module_id = m.id
421
- WHERE m.repo_id = ? ORDER BY m.slug, c.id`
422
- ).all(repoId);
423
- // 页面清单单算:hasT1 只能读盘(标记写在正文里,库里没有这一列);验证基线在库里。
424
- const pageRows = db.prepare(
425
- `SELECT m.slug, m.title, p.rel_path, p.verified_baseline, p.verified_at
426
- FROM choro_pages p
427
- JOIN choro_modules m ON p.module_id = m.id WHERE m.repo_id = ? ORDER BY m.slug`
428
- ).all(repoId);
429
- const head = rootPath === "" ? null : gitHead(rootPath);
430
- const baseline = String(repo.git_baseline ?? "").trim();
431
- // 每组验证基线只跑一次 diff:富化往往成批完成,同一基线的页占多数。
432
- const diffCache = new Map();
433
- const diffFor = (verified) => {
434
- if (head === null) return null;
435
- const key = String(verified ?? "").trim();
436
- if (key === "") return null; // 没记过起点 → 判不了,别拿别的页的起点凑
437
- if (diffCache.has(key)) return diffCache.get(key);
438
- let value = null;
439
- if (key === head) value = { ok: true, files: new Map(), deleted: [] };
440
- else {
441
- const diff = diffSince(rootPath, key, head);
442
- value = diff.ok ? diff : { ok: false, reason: diff.reason };
443
- }
444
- diffCache.set(key, value);
445
- return value;
446
- };
447
- const pages = pageRows.map((p) => {
448
- let hasT1 = false;
449
- try {
450
- hasT1 = readFileSync(join(rootPath, p.rel_path), "utf8").includes(T1_SECTION_START);
451
- } catch { hasT1 = false; }
452
- const verifiedBaseline = String(p.verified_baseline ?? "").trim() || null;
453
- const verifiedAt = p.verified_at ?? null;
454
- // 老数据(本功能之前富化的页)只有全仓基线可依。退到它是**保守**方向:它早于
455
- // 那页被富化的时刻,diff 会把「T1 之前就发生」的改动也算进来,只会多报不会漏报。
456
- // 记 baselineApprox 是为了面板能说清「这是退而求其次的起点」,不是当真的验证点。
457
- //
458
- // 但「验证过、却没记下基线」不是老数据,是**验证没留下凭据**(T1 那次富化写了
459
- // verified_at 却没写 verified_baseline,比如中途失败)。这种情况退到全仓基线
460
- // 就会把「富化之后才发生」的改动当成「富化之前就有的」而放过——方向是漏报,
461
- // 与上面那条恰好相反。所以判成 baselineUnknown:不知道起点,就不装作知道。
462
- const verifyWithoutBaseline = verifiedBaseline === null && verifiedAt !== null;
463
- const effective = verifyWithoutBaseline ? null : (verifiedBaseline ?? (baseline || null));
464
- const computed = diffFor(effective);
465
- return {
466
- slug: p.slug,
467
- title: p.title,
468
- hasT1,
469
- verifiedBaseline,
470
- verifiedAt,
471
- diffBaseline: effective,
472
- baselineApprox: !verifyWithoutBaseline && verifiedBaseline === null && effective !== null,
473
- baselineUnknown: effective === null,
474
- anchorStale: computed !== null && computed.ok === false,
475
- };
476
- });
477
- const claims = rows.map((r) => ({
478
- id: r.id,
479
- statement: r.statement,
480
- kind: r.kind,
481
- sourcePath: r.source_path,
482
- startLine: r.start_line,
483
- endLine: r.end_line,
484
- pageSlug: r.page_slug,
485
- pageTitle: r.page_title,
486
- }));
487
- const base = {
488
- repo: { id: repo.id, name: repo.name, rootPath, gitBaseline: baseline || null },
489
- head: head ?? null,
490
- baseline: baseline || null,
491
- };
492
- if (head === null) {
493
- return { ...base, clean: false, anchorStale: true, reason: "git-unreadable", ...summarizeClaims(claims, null, { pages }) };
494
- }
495
- // 仓库级 anchorStale 保留原来的语义:**骨架**(T0 页的文件清单、模块划分)记下的
496
- // 那次基线还认不认得出。它和逐页 claims 的起点是两件事,面板上也是两句话——
497
- // 「骨架要重建」不等于「结论都过时」。两条都算,谁也不掩盖谁。
498
- let repoStale = false;
499
- let repoStaleReason = null;
500
- if (baseline !== "" && baseline !== head) {
501
- const repoDiff = diffSince(rootPath, baseline, head);
502
- if (!repoDiff.ok) { repoStale = true; repoStaleReason = repoDiff.reason; }
503
- }
504
- const diffByPage = (slug) => {
505
- const page = pages.find((p) => p.slug === slug);
506
- if (!page) return null;
507
- const computed = diffFor(page.diffBaseline);
508
- return computed !== null && computed.ok ? { files: computed.files, deleted: computed.deleted } : null;
509
- };
510
- const summary = summarizeClaims(claims, null, { pages, diffByPage });
511
- // clean 的判据:每条结论都真判过、且一条都没被碰到。
512
- // 「有结论但起点认不出 / 没记过」不算 clean——那是不知道,不是没事(#607 的病根);
513
- // 一条结论都没有时也没有「没事」可说(那是「还没富化」,不是「全部干净」)。
514
- // 仓库基线 == HEAD → 整仓一条提交都没发生,任何 claims 都不可能被碰过。这一条既短路
515
- // 「逐页基线没记」的老数据(缺列不是判不了),也覆盖「刚跑完 T0、还没有 conclusion」
516
- // 的情形:没变就是没变,不必等富化完才敢说。
517
- // 不是这个情形时才逐页看:有哪页判不了就不说 clean,一条结论都没有也不说 clean
518
- // (那是「还没富化」,不是「全部干净」——面板上这两句不一样)。
519
- const repoAtHead = baseline !== "" && baseline === head;
520
- const undecidable = summary.totals.baselineUnknownPages + summary.totals.anchorStalePages > 0;
521
- // inconsistentPages 不参与 repoAtHead 的短路:它说的不是「起点认不出」,而是
522
- // **页面有语义正文、库里却一条 claims 都没有**(上次 T1 没写完的残局)。这种页的
523
- // 证据根本无法核对,与仓库有没有新提交无关;只要还有这种页,就不能说「全部干净」,
524
- // 否则面板会把一次没落库的富化说成没事。
525
- const clean = summary.totals.atRiskClaims === 0
526
- && summary.totals.inconsistentPages === 0
527
- && (repoAtHead || (summary.totals.claims > 0 && !undecidable && !repoStale));
528
- return {
529
- ...base,
530
- clean,
531
- anchorStale: repoStale,
532
- reason: repoStale ? repoStaleReason : null,
533
- ...summary,
534
- };
535
- }
536
-
537
- // --- chorography 向量(收尾档 P37–P39)---------------------------------
538
- // 惰性物化(P37):只在 host 侧检索路径调用(embedder 由 index.js 注入);
539
- // CLI 环境没有 embedder → 向量永不物化,检索退关键词。staleness 两维:
540
- // 页 generated_at 变了(内容变了)、行 dim ≠ 本次查询向量 dim(换 embedder
541
- // 模型了,P38)→ 重嵌。dim 失配是全表性的,遇到一次就把整仓重嵌(几十页)。
542
- async function embedPageVectors(repoId, embedder, { limit = 200, expectDim = null } = {}) {
543
- const pages = db.prepare(
544
- `SELECT p.id, p.generated_at, m.slug, m.title, p.rel_path
545
- FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
546
- WHERE m.repo_id = ? ORDER BY m.slug`
547
- ).all(repoId);
548
- const repo = db.prepare("SELECT root_path FROM choro_repos WHERE id = ?").get(repoId);
549
- const embedOne = embedder.embedSingle ? (t) => embedder.embedSingle(t) : (t) => embedder.embed(t);
550
- const rows = store.getChoroPageVectors(repoId);
551
- const bySlug = new Map(rows.map((r) => [r.slug, r]));
552
- const result = { embedded: 0, skipped: 0, stale: 0, failed: [], dimMismatch: false };
553
- // dim 失配的全量重嵌(P38):expectDim 来自查询向量(调用方先嵌查询再物化,
554
- // 维度免费拿到)。expectDim 与任何现存行失配 → 本轮 fresh 检查失效,全量重嵌;
555
- // 没给 expectDim(无查询场景)退化为只按 generated_at 判 staleness。
556
- const dimDrift = expectDim !== null && rows.some((r) => r.dim !== expectDim);
557
- if (dimDrift) result.dimMismatch = true;
558
- for (const page of pages) {
559
- if (result.embedded >= limit) break;
560
- const row = bySlug.get(page.slug);
561
- const fresh = !dimDrift && row && row.source_generated_at === page.generated_at && row.vector;
562
- if (fresh) { result.skipped++; continue; }
563
- if (row && row.vector) result.stale++;
564
- const abs = join(repo.root_path, page.rel_path);
565
- if (!existsSync(abs)) continue;
566
- let text;
567
- try {
568
- text = pageEmbedText(readFileSync(abs, "utf8"));
569
- } catch {
570
- result.failed.push({ slug: page.slug, reason: "page-unreadable" });
571
- continue;
572
- }
573
- if (text.trim() === "") { result.skipped++; continue; }
574
- let vector;
575
- try {
576
- vector = await embedOne(text);
577
- } catch (err) {
578
- result.failed.push({ slug: page.slug, reason: String(err?.message ?? err).slice(0, 120) });
579
- continue;
580
- }
581
- if (!Array.isArray(vector) || vector.length === 0) {
582
- result.failed.push({ slug: page.slug, reason: "embedder-returned-no-vector" });
583
- continue;
584
- }
585
- if (row && row.dim !== vector.length) result.dimMismatch = true;
586
- store.saveChoroPageVector(page.id, {
587
- vector,
588
- dim: vector.length,
589
- model: String(embedder.model ?? "choro"),
590
- sourceGeneratedAt: page.generated_at,
591
- createdAt: new Date().toISOString(),
592
- });
593
- result.embedded++;
594
- }
595
- return result;
596
- }
597
-
598
- /** 嵌查询文本(宿主侧)。embedder 缺席 / 不可用 → null。 */
599
- async function embedQueryText(embedder, q) {
600
- if (!embedder) return null;
601
- const embedOne = embedder.embedSingle ? (t) => embedder.embedSingle(t) : (t) => embedder.embed(t);
602
- try {
603
- const v = await embedOne(String(q ?? ""));
604
- return Array.isArray(v) && v.length > 0 ? v : null;
605
- } catch {
606
- return null;
607
- }
608
- }
609
-
610
- /** 纯余弦 Top-N:向量已给定时零嵌入。行 dim 与查询失配的行过滤掉(P38)。 */
611
- function semanticTopN(repoId, queryVector, { limit = 10 } = {}) {
612
- if (!Array.isArray(queryVector) || queryVector.length === 0) return [];
613
- const rows = store.getChoroPageVectors(repoId).filter((r) => r.vector && r.dim === queryVector.length);
614
- return rows
615
- .map((r) => ({
616
- slug: r.slug,
617
- title: r.title,
618
- score: Number(cosine(queryVector, JSON.parse(r.vector)).toFixed(4)),
619
- }))
620
- .sort((a, b) => b.score - a.score)
621
- .slice(0, Math.max(1, Math.min(50, Number(limit) || 10)));
622
- }
623
-
624
- /** 便捷组合:嵌查询 + Top-N(P39:独立数组,不与 keyword hits 混排序)。
625
- * 空查询 → [](空白查询的「语义」是噪声,面板逐键触发时尤其如此);
626
- * embedder 缺席 → null(调用方退纯关键词)。 */
627
- async function searchSemantic(repoId, embedder, q, { limit = 10 } = {}) {
628
- if (!embedder) return null;
629
- if (String(q ?? "").trim() === "") return [];
630
- const queryVector = await embedQueryText(embedder, q);
631
- if (!queryVector) return [];
632
- return semanticTopN(repoId, queryVector, { limit });
633
- }
634
-
635
- return {
636
- listRepos, listRepoFacts, repoFacts, repoFactsById, repoFromRow, listRepoRow, repoByRoot,
637
- resolveRepo, moduleMap, readPage, search, repoByCwd, outline, claimDrift,
638
- embedPageVectors, searchSemantic, embedQueryText, semanticTopN,
639
- };
640
- }
1
+ // dsh-mneme/src/wiki/queries.js
2
+ // chorography 的读侧(M1):仓库列表 / 模块图 / 页面正文 / 关键词检索。
3
+ // 收尾档追加:向量检索(P37–P39,惰性物化 + cosine)。
4
+ // 检索读侧零写路径的例外只有一处——向量物化写 choro_vectors(P37:检索时惰性
5
+ // 补嵌,host 侧才有 embedder;那是向量账本,不是页面真源)。
6
+ //
7
+ // 真源纪律(spec §4 / P12):页面正文永远读仓库磁盘的 MD(root_path + DB 里的
8
+ // rel_path),库里的表只是索引与状态账。slug 只从 DB 白名单解析,路径拼装零用户
9
+ // 输入——面板与工具传进来的 slug 查不到模块行就返回 null,绝不用它拼路径。
10
+ import { readFileSync, existsSync, realpathSync } from "node:fs";
11
+ import { join, resolve, sep } from "node:path";
12
+ // 过期判定复用既有三件真源(t0 的 gitHead、plan 的指纹、render 的模板版本),
13
+ // 面板侧不另造一套判据——两套判据迟早会对同一仓库给出两个答案。
14
+ import { gitHead } from "./t0.js";
15
+ import { loadPlanFromRepo } from "./plan.js";
16
+ import { TEMPLATE_VERSION, T1_SECTION_START } from "./render.js";
17
+ // claim 失效判定的纯函数(借 openwiki 的 Grounded Claims):解析 diff + 逐条比对
18
+ // 三列证据。放独立模块是因为它零 DB、零 IO(除 diffSince 那一次 git),可单测。
19
+ import { diffSince, summarizeClaims } from "./claims.js";
20
+
21
+ // 页正文去 frontmatter / CHORO 标记行后作为嵌入文本(P40:嵌页级全文)。
22
+ function pageEmbedText(content) {
23
+ const lines = String(content ?? "").split("\n");
24
+ let start = 0;
25
+ if (lines[0]?.trim() === "---") {
26
+ start = 1;
27
+ while (start < lines.length && lines[start].trim() !== "---") start++;
28
+ start++;
29
+ }
30
+ return lines
31
+ .slice(start)
32
+ .filter((l) => !l.startsWith("<!-- CHORO:T1:") && !l.startsWith("<!-- CHORO:HUMAN:"))
33
+ .join("\n")
34
+ .trim();
35
+ }
36
+
37
+ function cosine(a, b) {
38
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || a.length === 0) return 0;
39
+ let dot = 0;
40
+ let na = 0;
41
+ let nb = 0;
42
+ for (let i = 0; i < a.length; i++) {
43
+ dot += a[i] * b[i];
44
+ na += a[i] * a[i];
45
+ nb += b[i] * b[i];
46
+ }
47
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
48
+ return denom === 0 ? 0 : dot / denom;
49
+ }
50
+
51
+ export function createChoroQueries({ store }) {
52
+ const db = store.db;
53
+
54
+ /** 已登记仓库(含模块/页面计数,面板仓库选择器与工具的 repo 解析共用)。 */
55
+ function listRepos() {
56
+ const rows = db.prepare(
57
+ `SELECT r.id, r.root_path, r.name, r.plan_fingerprint, r.git_baseline, r.updated_at,
58
+ (SELECT COUNT(*) FROM choro_modules m WHERE m.repo_id = r.id) AS module_count,
59
+ (SELECT COUNT(*) FROM choro_pages p JOIN choro_modules m2 ON p.module_id = m2.id
60
+ WHERE m2.repo_id = r.id) AS page_count,
61
+ (SELECT COUNT(*) FROM choro_claims c
62
+ JOIN choro_pages p2 ON c.page_id = p2.id
63
+ JOIN choro_modules m3 ON p2.module_id = m3.id
64
+ WHERE m3.repo_id = r.id) AS claim_count
65
+ FROM choro_repos r ORDER BY r.id`
66
+ ).all();
67
+ return rows;
68
+ }
69
+
70
+ /**
71
+ * 面板仓库列表(契约 §3.2):在 listRepos 的行上补契约要求的字段。
72
+ *
73
+ * 为什么另开一个函数而不是改 listRepos 的返回形状:listRepos 的
74
+ * module_count / page_count 被工具层与查询层测试锁着(下划线命名),改名就是
75
+ * 破坏既有调用方;而契约要的是 modules / pages 这样的对外字段。两套名字并存
76
+ * 难看,但改名的连带面比难看贵得多——装饰层把它包一层,工具层照旧读原始行。
77
+ *
78
+ * 每个仓库都要 fork 一次 git 判过期:这是用户点开面板才走的路(不是每轮注入的
79
+ * 热路径),一次 rev-parse 几十毫秒在交互里不可感知。
80
+ */
81
+ /**
82
+ * 契约字段的仓库对象(stale 由调用方决定是否重算)。
83
+ *
84
+ * 注意区分两个入口:repoFacts 每次都会 fork git 判过期;repoFromRow 是纯行投影,
85
+ * 给「已经知道过期原因、手上有判定结果」的调用方用(例如自动重跑之后要紧着按
86
+ * 新基线说话,不能再拿旧行算一遍)。混用会得到自相矛盾的回执。
87
+ */
88
+ function repoFromRow(row) {
89
+ const rootPath = String(row.root_path ?? "");
90
+ return {
91
+ id: row.id,
92
+ name: row.name,
93
+ rootPath,
94
+ modules: Number(row.module_count ?? 0),
95
+ pages: Number(row.page_count ?? 0),
96
+ claims: Number(row.claim_count ?? 0),
97
+ // legacy = 这个仓库里有页面是用旧模板(或 M0 的 NULL)渲染的:重跑一次 T0 就
98
+ // 会把它们升级成当前模板,面板据此提示「建议重建」而不是让用户自己比对版本。
99
+ legacy: legacyPages(row.id) > 0,
100
+ // enrichedPages:已经富化过(页面里有 T1 语义段、库里有 claims)的页数。
101
+ // 自动刷新要看它:这类页重跑 T0 会退回骨架,所以那条路必须先问一句。
102
+ enrichedPages: enrichedPageCount(row.id),
103
+ lastRunAt: row.updated_at ?? null
104
+ };
105
+ }
106
+
107
+ /**
108
+ * 已富化的页数。判据取库里的 status(T1 成功后会写 enriched),而不是读盘找
109
+ * T1 标记——这是条给面板列表用的轻查询,几百页也不该逐文件 readFileSync。
110
+ * 两边可能短暂不一致(例如有人手改了 MD),但「要不要提醒」这种提示用途,
111
+ * 库里的账本就是最合适的真源;真正判断「会丢什么」的仍是 T0 的 collectT1Loss(读盘)。
112
+ */
113
+ function enrichedPageCount(repoId) {
114
+ const hit = db.prepare(
115
+ `SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
116
+ WHERE m.repo_id = ? AND p.status = 'enriched'`
117
+ ).get(repoId);
118
+ return Number(hit?.n ?? 0);
119
+ }
120
+
121
+ function legacyPages(repoId) {
122
+ const hit = db.prepare(
123
+ `SELECT COUNT(*) AS n FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
124
+ WHERE m.repo_id = ? AND (p.template_version IS NULL OR p.template_version <> ?)`
125
+ ).get(repoId, TEMPLATE_VERSION);
126
+ return Number(hit?.n ?? 0);
127
+ }
128
+
129
+ function repoFacts(row) {
130
+ const rootPath = String(row.root_path ?? "");
131
+ // git 优先:代码变了是更硬的「该重跑」信号,plan 变了才退而求其次——两者同时
132
+ // 成立时面板只说一句话,说「git」比说「plan」更贴近用户看到的现象(他刚改了码)。
133
+ const head = rootPath === "" ? null : gitHead(rootPath);
134
+ const baseline = String(row.git_baseline ?? "").trim();
135
+ let staleReason = null;
136
+ if (head && head !== baseline) {
137
+ staleReason = "git";
138
+ } else if (rootPath !== "") {
139
+ // 读不到 git(head 为 null)时不直接判「没过期」:plan.yaml 可能已经被改过,
140
+ // 那条判据不依赖 git,仍能给出答案。
141
+ let planFingerprintNow = null;
142
+ try { planFingerprintNow = loadPlanFromRepo(rootPath).fingerprint; } catch { planFingerprintNow = null; }
143
+ // plan.yaml 校验不过时 loadPlanFromRepo 会抛——那是「现在读不出结论」,不是
144
+ // 「过期了」,所以落 null 保持 stale=false,让重建按钮自己去报那条 YAML 错误。
145
+ if (planFingerprintNow && planFingerprintNow !== String(row.plan_fingerprint ?? "")) {
146
+ staleReason = "plan";
147
+ }
148
+ }
149
+ return {
150
+ ...repoFromRow(row),
151
+ stale: staleReason !== null,
152
+ staleReason
153
+ };
154
+ }
155
+
156
+ /** 单仓的原始 listRepos 行(自动重跑之后要拿推进过的基线,不能再用旧行)。 */
157
+ function listRepoRow(repoId) {
158
+ return listRepos().find((r) => Number(r.id) === Number(repoId)) ?? null;
159
+ }
160
+
161
+ /** 契约形状的仓库列表:listRepos 的原始行 → 对外字段。 */
162
+ function listRepoFacts() {
163
+ return listRepos().map((row) => repoFacts(row));
164
+ }
165
+
166
+ /** 仓库的契约字段(按 id/name,给 /choro/page 这类已知单仓的读路径用)。 */
167
+ function repoFactsById(repoId) {
168
+ const row = listRepoRow(repoId);
169
+ return row ? repoFacts(row) : null;
170
+ }
171
+
172
+ /**
173
+ * 按绝对路径找已登记仓库(面板「选目录 → 生成」必需):T0 apply 落库时存的是
174
+ * realpath,所以这里也必须先 realpath 再比,否则 Windows 上盘符大小写/短名/符号
175
+ * 链接会让同一个目录被当成新仓库重复登记一次。
176
+ */
177
+ function repoByRoot(rootPath) {
178
+ const raw = String(rootPath ?? "").trim();
179
+ if (raw === "") return null;
180
+ let normalized;
181
+ try { normalized = realpathSync(resolve(raw)); } catch { return null; }
182
+ return db.prepare("SELECT * FROM choro_repos WHERE root_path = ?").get(normalized) ?? null;
183
+ }
184
+
185
+ /** repo 解析:数字 id 或显示名(root_path 是本机私有路径,不作为对外键)。 */
186
+ function resolveRepo(ref) {
187
+ if (ref === undefined || ref === null || ref === "") {
188
+ const rows = listRepos();
189
+ if (rows.length === 1) return rows[0];
190
+ if (rows.length === 0) return null;
191
+ return { ambiguous: rows.map((r) => ({ id: r.id, name: r.name })) };
192
+ }
193
+ const byId = db.prepare("SELECT * FROM choro_repos WHERE id = ?").get(Number(ref));
194
+ if (byId) return byId;
195
+ const byName = db.prepare("SELECT * FROM choro_repos WHERE name = ?").get(String(ref));
196
+ return byName ?? null;
197
+ }
198
+
199
+ /** 模块表 + 跨模块边(choro_module_map 与面板左侧树共用同一份数据)。 */
200
+ function moduleMap(repoId) {
201
+ const modules = db.prepare(
202
+ `SELECT m.id, m.slug, m.title, m.parent_id, m.file_count, m.fingerprint, m.updated_at,
203
+ p.status AS page_status, p.rel_path AS page_rel_path
204
+ FROM choro_modules m
205
+ LEFT JOIN choro_pages p ON p.module_id = m.id
206
+ WHERE m.repo_id = ? ORDER BY m.slug`
207
+ ).all(repoId);
208
+ const edges = db.prepare(
209
+ `SELECT from_slug, to_slug, weight FROM choro_edges WHERE repo_id = ?
210
+ ORDER BY from_slug, to_slug`
211
+ ).all(repoId);
212
+ // 纯加法补一个 camelCase 的 relPath:面板从地志页导入知识点要拼「根路径 + 页相对
213
+ // 路径」,而这里历史上直出 SQL 行的 snake_case(page_rel_path)。C 线按契约读的是
214
+ // mod.relPath,字段一缺就恒 undefined,用户选完模块点导入只会看到「请先选模块」。
215
+ // 原字段一律保留——src/tools.js 的 render 与 choro-command 都还在吃 file_count /
216
+ // page_status,改形状会波及它们;outline() 里的同名映射是这件事的正确写法。
217
+ for (const m of modules) m.relPath = m.page_rel_path ?? null;
218
+ return { modules, edges };
219
+ }
220
+
221
+ /**
222
+ * 读一页正文。slug 必须命中 DB 的模块行(P12:路径零用户输入);索引仍在但
223
+ * 文件缺失或不可读时带回相对路径,让调用方给出对应的恢复提示。
224
+ */
225
+ function readPage(repoId, slug) {
226
+ const row = db.prepare(
227
+ `SELECT m.slug, m.title, m.file_count, m.parent_id, p.rel_path, p.status,
228
+ p.git_baseline, p.template_version, p.generated_at, p.human_block_count
229
+ FROM choro_modules m JOIN choro_pages p ON p.module_id = m.id
230
+ WHERE m.repo_id = ? AND m.slug = ?`
231
+ ).get(repoId, String(slug));
232
+ if (!row) return null;
233
+ const repo = db.prepare("SELECT root_path, name FROM choro_repos WHERE id = ?").get(repoId);
234
+ const abs = join(repo.root_path, row.rel_path);
235
+ let content;
236
+ try {
237
+ content = readFileSync(abs, "utf8");
238
+ } catch (cause) {
239
+ const error = cause.code === "ENOENT" || cause.code === "ENOTDIR" ? "page-missing" : "page-unreadable";
240
+ return { error, slug: row.slug, relPath: row.rel_path };
241
+ }
242
+ return {
243
+ slug: row.slug,
244
+ title: row.title,
245
+ fileCount: row.file_count,
246
+ status: row.status,
247
+ gitBaseline: row.git_baseline,
248
+ templateVersion: row.template_version,
249
+ generatedAt: row.generated_at,
250
+ humanBlockCount: row.human_block_count,
251
+ repoName: repo.name,
252
+ relPath: row.rel_path,
253
+ content,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * 关键词检索(P10):按 `## ` 分节做大小写不敏感子串匹配,命中返回页 slug、
259
+ * 节标题、起始行号与摘录。只扫 DB 里登记过的页面文件(不是全目录 glob——
260
+ * 划分变更后未重算的孤儿文件不该被搜出来)。需要提示检索不完整的调用方可接收
261
+ * 不可读的页 slug。q 为空返回空数组不报错。
262
+ */
263
+ function search(repoId, q, { moduleSlug = null, limit = 20, onUnreadablePage } = {}) {
264
+ const needle = String(q ?? "").trim().toLowerCase();
265
+ if (needle === "") return [];
266
+ const cap = Math.max(1, Math.min(100, Number(limit) || 20));
267
+ const pages = db.prepare(
268
+ `SELECT m.slug, m.title AS module_title, p.rel_path
269
+ FROM choro_modules m JOIN choro_pages p ON p.module_id = m.id
270
+ WHERE m.repo_id = ? AND (? IS NULL OR m.slug = ?)
271
+ ORDER BY m.slug`
272
+ ).all(repoId, moduleSlug, moduleSlug);
273
+ const repo = db.prepare("SELECT root_path FROM choro_repos WHERE id = ?").get(repoId);
274
+ const hits = [];
275
+ for (const page of pages) {
276
+ const abs = join(repo.root_path, page.rel_path);
277
+ let text;
278
+ try {
279
+ text = readFileSync(abs, "utf8");
280
+ } catch {
281
+ onUnreadablePage?.(page.slug);
282
+ continue;
283
+ }
284
+ const lines = text.split("\n");
285
+ // 分节:每个 `## ` 标题开启一节,节内所有行都属于该节;文件头(frontmatter
286
+ // 之后到第一个 `## `)算 "(页首)" 一节,保证对 title/摘要的命中也有落点。
287
+ let section = "(页首)";
288
+ let sectionStart = 1;
289
+ for (let i = 0; i < lines.length; i++) {
290
+ const line = lines[i];
291
+ if (i > 0 && /^## /.test(line)) {
292
+ section = line.replace(/^## /, "").trim();
293
+ sectionStart = i + 1;
294
+ }
295
+ if (line.toLowerCase().includes(needle)) {
296
+ hits.push({
297
+ slug: page.slug,
298
+ moduleTitle: page.module_title,
299
+ section,
300
+ line: i + 1,
301
+ sectionStart,
302
+ excerpt: line.trim().slice(0, 200),
303
+ });
304
+ if (hits.length >= cap) return hits;
305
+ }
306
+ }
307
+ }
308
+ return hits;
309
+ }
310
+
311
+ /**
312
+ * 按会话工作目录找已登记仓库(M3 开局注入的唯一入口)。
313
+ *
314
+ * 三个刻意的取舍:
315
+ * 1. **祖先前缀匹配**而不是精确相等——真实会话的 cwd 常常是仓库子目录
316
+ * (`D:/repo/src`),精确相等会让绝大部分会话匹配不到,等于逼用户
317
+ * 「必须在仓库根开会话」这条暗规则。
318
+ * 2. 两侧都做 realpath 归一:Windows 上盘符大小写、8.3 短名、符号链接
319
+ * 会让同一个目录写出不同字符串;t0 落库时存的就是 realpath。
320
+ * 3. **最长前缀胜出**——仓库嵌套(子模块、`.worktrees/`)时取最深那层,
321
+ * 否则内层会话会被外层仓库抢走。
322
+ * cwd 不存在或不是目录时返回 null(不是抛错):注入在热路径上,失败只
323
+ * 意味着「本帧不注入」。
324
+ */
325
+ function repoByCwd(cwd) {
326
+ const raw = String(cwd ?? "").trim();
327
+ if (raw === "") return null;
328
+ let normalized;
329
+ try {
330
+ normalized = realpathSync(resolve(raw));
331
+ } catch {
332
+ return null;
333
+ }
334
+ const cmp = (p) => (process.platform === "win32" ? p.toLowerCase() : p);
335
+ const target = cmp(normalized);
336
+ const rows = db.prepare("SELECT * FROM choro_repos ORDER BY id").all();
337
+ let best = null;
338
+ for (const row of rows) {
339
+ const root = cmp(String(row.root_path ?? ""));
340
+ if (root === "") continue;
341
+ const hit = target === root || target.startsWith(root.endsWith(sep) ? root : root + sep);
342
+ if (!hit) continue;
343
+ if (!best || root.length > cmp(String(best.root_path)).length) best = row;
344
+ }
345
+ return best ?? null;
346
+ }
347
+
348
+ /**
349
+ * 注入与 /choro 共用的最小提纲(M3):模块名 + 文件数 + 页状态 + 页相对路径。
350
+ * **不读页正文**——spec §7 要求「只注标题级不注正文」,正文走 readPage。
351
+ * parentSlug 单独解出来给渲染层做层级缩进(DB 里存的是 parent_id)。
352
+ */
353
+ function outline(repoId) {
354
+ const repo = db.prepare(
355
+ "SELECT id, root_path, name, git_baseline, plan_fingerprint, updated_at FROM choro_repos WHERE id = ?"
356
+ ).get(repoId);
357
+ if (!repo) return null;
358
+ const rows = db.prepare(
359
+ `SELECT m.id, m.slug, m.title, m.parent_id, m.file_count,
360
+ p.status AS page_status, p.rel_path AS page_rel_path, p.generated_at
361
+ FROM choro_modules m
362
+ LEFT JOIN choro_pages p ON p.module_id = m.id
363
+ WHERE m.repo_id = ? ORDER BY m.slug`
364
+ ).all(repoId);
365
+ const slugById = new Map(rows.map((r) => [r.id, r.slug]));
366
+ let generatedAt = null;
367
+ const modules = rows.map((r) => {
368
+ if (r.generated_at && (generatedAt === null || r.generated_at > generatedAt)) generatedAt = r.generated_at;
369
+ return {
370
+ slug: r.slug,
371
+ title: r.title,
372
+ fileCount: r.file_count,
373
+ parentSlug: r.parent_id === null || r.parent_id === undefined ? null : slugById.get(r.parent_id) ?? null,
374
+ status: r.page_status ?? null,
375
+ relPath: r.page_rel_path ?? null,
376
+ };
377
+ });
378
+ return {
379
+ repo: {
380
+ id: repo.id,
381
+ name: repo.name,
382
+ rootPath: repo.root_path,
383
+ gitBaseline: repo.git_baseline,
384
+ updatedAt: repo.updated_at,
385
+ },
386
+ generatedAt: generatedAt ?? repo.updated_at ?? null,
387
+ modules,
388
+ };
389
+ }
390
+
391
+ /**
392
+ * Claim 粒度的失效清单(借 openwiki 的 Grounded Claims):把「仓库代码变过就整仓过期」
393
+ * 降成「这 2 页里的 5 条结论被改动碰过」。
394
+ *
395
+ * 为什么放读侧而不写成后台任务:判据是纯机械的(git diff 的 hunk 区间 × claims 的
396
+ * 三列证据),不需要模型,一次 diff 几十毫秒——面板打开时现算,永远比任何落库的
397
+ * 缓存新鲜。写路径一条不加,红线不动。
398
+ *
399
+ * **比对起点是每页自己的 verified_baseline,不是全仓 git_baseline。** claims 的行号
400
+ * 是那页被富化的那一刻记下的;全仓基线是 T0 的时点。若 T1 晚于 T0(常态),用全仓
401
+ * 基线起算就会把「T1 之前就发生、claim 早已算进去」的改动也算进 diff,误报 touched。
402
+ * 所以按 verified_baseline 分组、每组各跑一次 git diff(同一起点只跑一次)。
403
+ *
404
+ * 五种「说不清」要分开说,别混成一个空清单:
405
+ * - repo 不存在 → null(调用方 404)
406
+ * - git 读不到 HEAD → 全仓 anchorStale + reason "git-unreadable"
407
+ * - 某页基线认不出(浅克隆/force-push)→ 该页 anchorStale,**其余页照常判**
408
+ * - 某页没记过验证基线(老数据/拿不到 HEAD)→ 该页 baselineUnknown,一条不判
409
+ * - 起点 == HEAD → 那组真没变,clean
410
+ */
411
+ function claimDrift(repoId) {
412
+ const repo = db.prepare("SELECT id, root_path, name, git_baseline FROM choro_repos WHERE id = ?").get(repoId);
413
+ if (!repo) return null;
414
+ const rootPath = String(repo.root_path ?? "");
415
+ const rows = db.prepare(
416
+ `SELECT c.id, c.statement, c.kind, c.source_path, c.start_line, c.end_line,
417
+ m.slug AS page_slug, m.title AS page_title, p.rel_path
418
+ FROM choro_claims c
419
+ JOIN choro_pages p ON c.page_id = p.id
420
+ JOIN choro_modules m ON p.module_id = m.id
421
+ WHERE m.repo_id = ? ORDER BY m.slug, c.id`
422
+ ).all(repoId);
423
+ // 页面清单单算:hasT1 只能读盘(标记写在正文里,库里没有这一列);验证基线在库里。
424
+ const pageRows = db.prepare(
425
+ `SELECT m.slug, m.title, p.rel_path, p.verified_baseline, p.verified_at
426
+ FROM choro_pages p
427
+ JOIN choro_modules m ON p.module_id = m.id WHERE m.repo_id = ? ORDER BY m.slug`
428
+ ).all(repoId);
429
+ const head = rootPath === "" ? null : gitHead(rootPath);
430
+ const baseline = String(repo.git_baseline ?? "").trim();
431
+ // 每组验证基线只跑一次 diff:富化往往成批完成,同一基线的页占多数。
432
+ const diffCache = new Map();
433
+ const diffFor = (verified) => {
434
+ if (head === null) return null;
435
+ const key = String(verified ?? "").trim();
436
+ if (key === "") return null; // 没记过起点 → 判不了,别拿别的页的起点凑
437
+ if (diffCache.has(key)) return diffCache.get(key);
438
+ let value = null;
439
+ if (key === head) value = { ok: true, files: new Map(), deleted: [] };
440
+ else {
441
+ const diff = diffSince(rootPath, key, head);
442
+ value = diff.ok ? diff : { ok: false, reason: diff.reason };
443
+ }
444
+ diffCache.set(key, value);
445
+ return value;
446
+ };
447
+ const pages = pageRows.map((p) => {
448
+ let hasT1 = false;
449
+ try {
450
+ hasT1 = readFileSync(join(rootPath, p.rel_path), "utf8").includes(T1_SECTION_START);
451
+ } catch { hasT1 = false; }
452
+ const verifiedBaseline = String(p.verified_baseline ?? "").trim() || null;
453
+ const verifiedAt = p.verified_at ?? null;
454
+ // 老数据(本功能之前富化的页)只有全仓基线可依。退到它是**保守**方向:它早于
455
+ // 那页被富化的时刻,diff 会把「T1 之前就发生」的改动也算进来,只会多报不会漏报。
456
+ // 记 baselineApprox 是为了面板能说清「这是退而求其次的起点」,不是当真的验证点。
457
+ //
458
+ // 但「验证过、却没记下基线」不是老数据,是**验证没留下凭据**(T1 那次富化写了
459
+ // verified_at 却没写 verified_baseline,比如中途失败)。这种情况退到全仓基线
460
+ // 就会把「富化之后才发生」的改动当成「富化之前就有的」而放过——方向是漏报,
461
+ // 与上面那条恰好相反。所以判成 baselineUnknown:不知道起点,就不装作知道。
462
+ const verifyWithoutBaseline = verifiedBaseline === null && verifiedAt !== null;
463
+ const effective = verifyWithoutBaseline ? null : (verifiedBaseline ?? (baseline || null));
464
+ const computed = diffFor(effective);
465
+ return {
466
+ slug: p.slug,
467
+ title: p.title,
468
+ hasT1,
469
+ verifiedBaseline,
470
+ verifiedAt,
471
+ diffBaseline: effective,
472
+ baselineApprox: !verifyWithoutBaseline && verifiedBaseline === null && effective !== null,
473
+ baselineUnknown: effective === null,
474
+ anchorStale: computed !== null && computed.ok === false,
475
+ };
476
+ });
477
+ const claims = rows.map((r) => ({
478
+ id: r.id,
479
+ statement: r.statement,
480
+ kind: r.kind,
481
+ sourcePath: r.source_path,
482
+ startLine: r.start_line,
483
+ endLine: r.end_line,
484
+ pageSlug: r.page_slug,
485
+ pageTitle: r.page_title,
486
+ }));
487
+ const base = {
488
+ repo: { id: repo.id, name: repo.name, rootPath, gitBaseline: baseline || null },
489
+ head: head ?? null,
490
+ baseline: baseline || null,
491
+ };
492
+ if (head === null) {
493
+ return { ...base, clean: false, anchorStale: true, reason: "git-unreadable", ...summarizeClaims(claims, null, { pages }) };
494
+ }
495
+ // 仓库级 anchorStale 保留原来的语义:**骨架**(T0 页的文件清单、模块划分)记下的
496
+ // 那次基线还认不认得出。它和逐页 claims 的起点是两件事,面板上也是两句话——
497
+ // 「骨架要重建」不等于「结论都过时」。两条都算,谁也不掩盖谁。
498
+ let repoStale = false;
499
+ let repoStaleReason = null;
500
+ if (baseline !== "" && baseline !== head) {
501
+ const repoDiff = diffSince(rootPath, baseline, head);
502
+ if (!repoDiff.ok) { repoStale = true; repoStaleReason = repoDiff.reason; }
503
+ }
504
+ const diffByPage = (slug) => {
505
+ const page = pages.find((p) => p.slug === slug);
506
+ if (!page) return null;
507
+ const computed = diffFor(page.diffBaseline);
508
+ return computed !== null && computed.ok ? { files: computed.files, deleted: computed.deleted } : null;
509
+ };
510
+ const summary = summarizeClaims(claims, null, { pages, diffByPage });
511
+ // clean 的判据:每条结论都真判过、且一条都没被碰到。
512
+ // 「有结论但起点认不出 / 没记过」不算 clean——那是不知道,不是没事(#607 的病根);
513
+ // 一条结论都没有时也没有「没事」可说(那是「还没富化」,不是「全部干净」)。
514
+ // 仓库基线 == HEAD → 整仓一条提交都没发生,任何 claims 都不可能被碰过。这一条既短路
515
+ // 「逐页基线没记」的老数据(缺列不是判不了),也覆盖「刚跑完 T0、还没有 conclusion」
516
+ // 的情形:没变就是没变,不必等富化完才敢说。
517
+ // 不是这个情形时才逐页看:有哪页判不了就不说 clean,一条结论都没有也不说 clean
518
+ // (那是「还没富化」,不是「全部干净」——面板上这两句不一样)。
519
+ const repoAtHead = baseline !== "" && baseline === head;
520
+ const undecidable = summary.totals.baselineUnknownPages + summary.totals.anchorStalePages > 0;
521
+ // inconsistentPages 不参与 repoAtHead 的短路:它说的不是「起点认不出」,而是
522
+ // **页面有语义正文、库里却一条 claims 都没有**(上次 T1 没写完的残局)。这种页的
523
+ // 证据根本无法核对,与仓库有没有新提交无关;只要还有这种页,就不能说「全部干净」,
524
+ // 否则面板会把一次没落库的富化说成没事。
525
+ const clean = summary.totals.atRiskClaims === 0
526
+ && summary.totals.inconsistentPages === 0
527
+ && (repoAtHead || (summary.totals.claims > 0 && !undecidable && !repoStale));
528
+ return {
529
+ ...base,
530
+ clean,
531
+ anchorStale: repoStale,
532
+ reason: repoStale ? repoStaleReason : null,
533
+ ...summary,
534
+ };
535
+ }
536
+
537
+ // --- chorography 向量(收尾档 P37–P39)---------------------------------
538
+ // 惰性物化(P37):只在 host 侧检索路径调用(embedder 由 index.js 注入);
539
+ // CLI 环境没有 embedder → 向量永不物化,检索退关键词。staleness 两维:
540
+ // 页 generated_at 变了(内容变了)、行 dim ≠ 本次查询向量 dim(换 embedder
541
+ // 模型了,P38)→ 重嵌。dim 失配是全表性的,遇到一次就把整仓重嵌(几十页)。
542
+ async function embedPageVectors(repoId, embedder, { limit = 200, expectDim = null } = {}) {
543
+ const pages = db.prepare(
544
+ `SELECT p.id, p.generated_at, m.slug, m.title, p.rel_path
545
+ FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id
546
+ WHERE m.repo_id = ? ORDER BY m.slug`
547
+ ).all(repoId);
548
+ const repo = db.prepare("SELECT root_path FROM choro_repos WHERE id = ?").get(repoId);
549
+ const embedOne = embedder.embedSingle ? (t) => embedder.embedSingle(t) : (t) => embedder.embed(t);
550
+ const rows = store.getChoroPageVectors(repoId);
551
+ const bySlug = new Map(rows.map((r) => [r.slug, r]));
552
+ const result = { embedded: 0, skipped: 0, stale: 0, failed: [], dimMismatch: false };
553
+ // dim 失配的全量重嵌(P38):expectDim 来自查询向量(调用方先嵌查询再物化,
554
+ // 维度免费拿到)。expectDim 与任何现存行失配 → 本轮 fresh 检查失效,全量重嵌;
555
+ // 没给 expectDim(无查询场景)退化为只按 generated_at 判 staleness。
556
+ const dimDrift = expectDim !== null && rows.some((r) => r.dim !== expectDim);
557
+ if (dimDrift) result.dimMismatch = true;
558
+ for (const page of pages) {
559
+ if (result.embedded >= limit) break;
560
+ const row = bySlug.get(page.slug);
561
+ const fresh = !dimDrift && row && row.source_generated_at === page.generated_at && row.vector;
562
+ if (fresh) { result.skipped++; continue; }
563
+ if (row && row.vector) result.stale++;
564
+ const abs = join(repo.root_path, page.rel_path);
565
+ if (!existsSync(abs)) continue;
566
+ let text;
567
+ try {
568
+ text = pageEmbedText(readFileSync(abs, "utf8"));
569
+ } catch {
570
+ result.failed.push({ slug: page.slug, reason: "page-unreadable" });
571
+ continue;
572
+ }
573
+ if (text.trim() === "") { result.skipped++; continue; }
574
+ let vector;
575
+ try {
576
+ vector = await embedOne(text);
577
+ } catch (err) {
578
+ result.failed.push({ slug: page.slug, reason: String(err?.message ?? err).slice(0, 120) });
579
+ continue;
580
+ }
581
+ if (!Array.isArray(vector) || vector.length === 0) {
582
+ result.failed.push({ slug: page.slug, reason: "embedder-returned-no-vector" });
583
+ continue;
584
+ }
585
+ if (row && row.dim !== vector.length) result.dimMismatch = true;
586
+ store.saveChoroPageVector(page.id, {
587
+ vector,
588
+ dim: vector.length,
589
+ model: String(embedder.model ?? "choro"),
590
+ sourceGeneratedAt: page.generated_at,
591
+ createdAt: new Date().toISOString(),
592
+ });
593
+ result.embedded++;
594
+ }
595
+ return result;
596
+ }
597
+
598
+ /** 嵌查询文本(宿主侧)。embedder 缺席 / 不可用 → null。 */
599
+ async function embedQueryText(embedder, q) {
600
+ if (!embedder) return null;
601
+ const embedOne = embedder.embedSingle ? (t) => embedder.embedSingle(t) : (t) => embedder.embed(t);
602
+ try {
603
+ const v = await embedOne(String(q ?? ""));
604
+ return Array.isArray(v) && v.length > 0 ? v : null;
605
+ } catch {
606
+ return null;
607
+ }
608
+ }
609
+
610
+ /** 纯余弦 Top-N:向量已给定时零嵌入。行 dim 与查询失配的行过滤掉(P38)。 */
611
+ function semanticTopN(repoId, queryVector, { limit = 10 } = {}) {
612
+ if (!Array.isArray(queryVector) || queryVector.length === 0) return [];
613
+ const rows = store.getChoroPageVectors(repoId).filter((r) => r.vector && r.dim === queryVector.length);
614
+ return rows
615
+ .map((r) => ({
616
+ slug: r.slug,
617
+ title: r.title,
618
+ score: Number(cosine(queryVector, JSON.parse(r.vector)).toFixed(4)),
619
+ }))
620
+ .sort((a, b) => b.score - a.score)
621
+ .slice(0, Math.max(1, Math.min(50, Number(limit) || 10)));
622
+ }
623
+
624
+ /** 便捷组合:嵌查询 + Top-N(P39:独立数组,不与 keyword hits 混排序)。
625
+ * 空查询 → [](空白查询的「语义」是噪声,面板逐键触发时尤其如此);
626
+ * embedder 缺席 → null(调用方退纯关键词)。 */
627
+ async function searchSemantic(repoId, embedder, q, { limit = 10 } = {}) {
628
+ if (!embedder) return null;
629
+ if (String(q ?? "").trim() === "") return [];
630
+ const queryVector = await embedQueryText(embedder, q);
631
+ if (!queryVector) return [];
632
+ return semanticTopN(repoId, queryVector, { limit });
633
+ }
634
+
635
+ return {
636
+ listRepos, listRepoFacts, repoFacts, repoFactsById, repoFromRow, listRepoRow, repoByRoot,
637
+ resolveRepo, moduleMap, readPage, search, repoByCwd, outline, claimDrift,
638
+ embedPageVectors, searchSemantic, embedQueryText, semanticTopN,
639
+ };
640
+ }