nlm-memory 0.20.0 → 0.21.0

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 (471) hide show
  1. package/README.md +19 -10
  2. package/dist/cli/digest.js +60 -1
  3. package/dist/cli/digest.js.map +1 -1
  4. package/dist/cli/init.d.ts +12 -0
  5. package/dist/cli/init.js +49 -0
  6. package/dist/cli/init.js.map +1 -0
  7. package/dist/cli/nlm.d.ts +13 -1
  8. package/dist/cli/nlm.js +872 -61
  9. package/dist/cli/nlm.js.map +1 -1
  10. package/dist/cli/scope-backfill.d.ts +34 -0
  11. package/dist/cli/scope-backfill.js +237 -0
  12. package/dist/cli/scope-backfill.js.map +1 -0
  13. package/dist/cli/scope-coverage.d.ts +44 -0
  14. package/dist/cli/scope-coverage.js +205 -0
  15. package/dist/cli/scope-coverage.js.map +1 -0
  16. package/dist/core/actions/overlay.d.ts +20 -6
  17. package/dist/core/actions/overlay.js +32 -18
  18. package/dist/core/actions/overlay.js.map +1 -1
  19. package/dist/core/adapters/claude-code.d.ts +14 -0
  20. package/dist/core/adapters/claude-code.js +36 -0
  21. package/dist/core/adapters/claude-code.js.map +1 -1
  22. package/dist/core/classifier/prompt.js +3 -3
  23. package/dist/core/dataset/build-dataset.js +1 -0
  24. package/dist/core/dataset/build-dataset.js.map +1 -1
  25. package/dist/core/digest/compose.d.ts +13 -3
  26. package/dist/core/digest/compose.js +23 -18
  27. package/dist/core/digest/compose.js.map +1 -1
  28. package/dist/core/digest/hook-injection.d.ts +43 -0
  29. package/dist/core/digest/hook-injection.js +79 -0
  30. package/dist/core/digest/hook-injection.js.map +1 -0
  31. package/dist/core/embedding/embed-backfill.d.ts +13 -2
  32. package/dist/core/embedding/embed-backfill.js +181 -43
  33. package/dist/core/embedding/embed-backfill.js.map +1 -1
  34. package/dist/core/embedding/embedding-config.d.ts +17 -0
  35. package/dist/core/embedding/embedding-config.js +12 -0
  36. package/dist/core/embedding/embedding-config.js.map +1 -0
  37. package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
  38. package/dist/core/embedding/pg-embed-backfill.js +264 -0
  39. package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
  40. package/dist/core/entities/dedup-suggest.d.ts +25 -0
  41. package/dist/core/entities/dedup-suggest.js +107 -0
  42. package/dist/core/entities/dedup-suggest.js.map +1 -0
  43. package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
  44. package/dist/core/eval/classifier-fixture-eval.js +217 -0
  45. package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
  46. package/dist/core/eval/extraction-scoring.d.ts +63 -0
  47. package/dist/core/eval/extraction-scoring.js +79 -0
  48. package/dist/core/eval/extraction-scoring.js.map +1 -0
  49. package/dist/core/eval/run-eval.d.ts +40 -0
  50. package/dist/core/eval/run-eval.js +34 -0
  51. package/dist/core/eval/run-eval.js.map +1 -0
  52. package/dist/core/exemplars/capture-from-session.d.ts +2 -0
  53. package/dist/core/exemplars/capture-from-session.js +7 -3
  54. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  55. package/dist/core/exemplars/diff-parse.d.ts +23 -0
  56. package/dist/core/exemplars/diff-parse.js +73 -0
  57. package/dist/core/exemplars/diff-parse.js.map +1 -0
  58. package/dist/core/exemplars/embed-backfill.d.ts +36 -0
  59. package/dist/core/exemplars/embed-backfill.js +85 -0
  60. package/dist/core/exemplars/embed-backfill.js.map +1 -0
  61. package/dist/core/exemplars/embed-text.d.ts +15 -0
  62. package/dist/core/exemplars/embed-text.js +18 -0
  63. package/dist/core/exemplars/embed-text.js.map +1 -0
  64. package/dist/core/exemplars/extract-exemplar.d.ts +2 -0
  65. package/dist/core/exemplars/extract-exemplar.js +9 -66
  66. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  67. package/dist/core/exemplars/ingest-exemplar.d.ts +1 -0
  68. package/dist/core/exemplars/ingest-exemplar.js +1 -0
  69. package/dist/core/exemplars/ingest-exemplar.js.map +1 -1
  70. package/dist/core/exemplars/recall-code.js +4 -0
  71. package/dist/core/exemplars/recall-code.js.map +1 -1
  72. package/dist/core/exemplars/warm-embedder.d.ts +14 -0
  73. package/dist/core/exemplars/warm-embedder.js +18 -0
  74. package/dist/core/exemplars/warm-embedder.js.map +1 -0
  75. package/dist/core/health/corpus-state.d.ts +8 -0
  76. package/dist/core/health/corpus-state.js +11 -0
  77. package/dist/core/health/corpus-state.js.map +1 -0
  78. package/dist/core/health/embed-inflight.d.ts +9 -0
  79. package/dist/core/health/embed-inflight.js +30 -0
  80. package/dist/core/health/embed-inflight.js.map +1 -0
  81. package/dist/core/health/embedding-lane-state.d.ts +6 -0
  82. package/dist/core/health/embedding-lane-state.js +18 -0
  83. package/dist/core/health/embedding-lane-state.js.map +1 -0
  84. package/dist/core/health/warmup-state.d.ts +12 -0
  85. package/dist/core/health/warmup-state.js +12 -0
  86. package/dist/core/health/warmup-state.js.map +1 -0
  87. package/dist/core/hook/claude-settings.d.ts +1 -1
  88. package/dist/core/hook/claude-settings.js +0 -1
  89. package/dist/core/hook/claude-settings.js.map +1 -1
  90. package/dist/core/hook/gate.d.ts +13 -6
  91. package/dist/core/hook/gate.js +47 -6
  92. package/dist/core/hook/gate.js.map +1 -1
  93. package/dist/core/hook/hook-log.d.ts +5 -0
  94. package/dist/core/hook/hook-log.js.map +1 -1
  95. package/dist/core/hook/memo.d.ts +12 -0
  96. package/dist/core/hook/memo.js +41 -1
  97. package/dist/core/hook/memo.js.map +1 -1
  98. package/dist/core/hook/pointer-block.d.ts +1 -0
  99. package/dist/core/hook/pointer-block.js +23 -2
  100. package/dist/core/hook/pointer-block.js.map +1 -1
  101. package/dist/core/hook/query-extract.d.ts +2 -1
  102. package/dist/core/hook/query-extract.js +10 -1
  103. package/dist/core/hook/query-extract.js.map +1 -1
  104. package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
  105. package/dist/core/hook/resolve-conversation-by-query.js +98 -0
  106. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
  107. package/dist/core/hook/select.d.ts +8 -0
  108. package/dist/core/hook/select.js +9 -2
  109. package/dist/core/hook/select.js.map +1 -1
  110. package/dist/core/hook/transcript.js +16 -2
  111. package/dist/core/hook/transcript.js.map +1 -1
  112. package/dist/core/ingest/backfill-derivables.d.ts +92 -0
  113. package/dist/core/ingest/backfill-derivables.js +175 -0
  114. package/dist/core/ingest/backfill-derivables.js.map +1 -0
  115. package/dist/core/ingest/ingest-session.d.ts +5 -0
  116. package/dist/core/ingest/ingest-session.js +23 -0
  117. package/dist/core/ingest/ingest-session.js.map +1 -1
  118. package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
  119. package/dist/core/ingest/reclassify-oversized.js +9 -0
  120. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  121. package/dist/core/ingest/reprocess.d.ts +96 -0
  122. package/dist/core/ingest/reprocess.js +280 -0
  123. package/dist/core/ingest/reprocess.js.map +1 -0
  124. package/dist/core/ingest/transcript-derivables.d.ts +27 -0
  125. package/dist/core/ingest/transcript-derivables.js +138 -0
  126. package/dist/core/ingest/transcript-derivables.js.map +1 -0
  127. package/dist/core/integrity/check-invariants.d.ts +6 -1
  128. package/dist/core/integrity/check-invariants.js +209 -3
  129. package/dist/core/integrity/check-invariants.js.map +1 -1
  130. package/dist/core/metrics/corpus-stats.d.ts +49 -0
  131. package/dist/core/metrics/corpus-stats.js +111 -0
  132. package/dist/core/metrics/corpus-stats.js.map +1 -0
  133. package/dist/core/metrics/re-derivation.d.ts +48 -0
  134. package/dist/core/metrics/re-derivation.js +87 -0
  135. package/dist/core/metrics/re-derivation.js.map +1 -0
  136. package/dist/core/outcome/coverage.d.ts +33 -0
  137. package/dist/core/outcome/coverage.js +46 -0
  138. package/dist/core/outcome/coverage.js.map +1 -0
  139. package/dist/core/outcome/rollup.d.ts +33 -0
  140. package/dist/core/outcome/rollup.js +113 -0
  141. package/dist/core/outcome/rollup.js.map +1 -0
  142. package/dist/core/recall/citation-log.d.ts +8 -0
  143. package/dist/core/recall/citation-log.js +17 -0
  144. package/dist/core/recall/citation-log.js.map +1 -1
  145. package/dist/core/recall/hook-recall-log.d.ts +2 -2
  146. package/dist/core/recall/hook-recall-log.js +2 -2
  147. package/dist/core/recall/query-intent.d.ts +7 -0
  148. package/dist/core/recall/query-intent.js +25 -0
  149. package/dist/core/recall/query-intent.js.map +1 -0
  150. package/dist/core/recall/query-log.d.ts +4 -0
  151. package/dist/core/recall/query-log.js +4 -0
  152. package/dist/core/recall/query-log.js.map +1 -1
  153. package/dist/core/recall/recall-service.d.ts +37 -1
  154. package/dist/core/recall/recall-service.js +106 -63
  155. package/dist/core/recall/recall-service.js.map +1 -1
  156. package/dist/core/recall/related-exemplars.d.ts +1 -0
  157. package/dist/core/recall/related-exemplars.js +4 -1
  158. package/dist/core/recall/related-exemplars.js.map +1 -1
  159. package/dist/core/recall/related-facts.js +2 -2
  160. package/dist/core/recall/related-facts.js.map +1 -1
  161. package/dist/core/recall/reranker.d.ts +2 -1
  162. package/dist/core/recall/reranker.js +3 -3
  163. package/dist/core/recall/reranker.js.map +1 -1
  164. package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
  165. package/dist/core/recall-facts/fact-query-log.js +2 -0
  166. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  167. package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
  168. package/dist/core/recall-facts/fact-recall-service.js +22 -8
  169. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  170. package/dist/core/scheduler/scan-once.d.ts +6 -6
  171. package/dist/core/scheduler/scan-once.js +15 -49
  172. package/dist/core/scheduler/scan-once.js.map +1 -1
  173. package/dist/core/scheduler/scheduler.d.ts +10 -0
  174. package/dist/core/scheduler/scheduler.js +109 -13
  175. package/dist/core/scheduler/scheduler.js.map +1 -1
  176. package/dist/core/scope/alias-map.d.ts +10 -0
  177. package/dist/core/scope/alias-map.js +41 -0
  178. package/dist/core/scope/alias-map.js.map +1 -0
  179. package/dist/core/scope/derive-scope.d.ts +8 -0
  180. package/dist/core/scope/derive-scope.js +54 -0
  181. package/dist/core/scope/derive-scope.js.map +1 -0
  182. package/dist/core/scope/scope-clause.d.ts +16 -0
  183. package/dist/core/scope/scope-clause.js +21 -0
  184. package/dist/core/scope/scope-clause.js.map +1 -0
  185. package/dist/core/scope/stamp-flag.d.ts +1 -0
  186. package/dist/core/scope/stamp-flag.js +4 -0
  187. package/dist/core/scope/stamp-flag.js.map +1 -0
  188. package/dist/core/signals/code-signal.d.ts +50 -0
  189. package/dist/core/signals/code-signal.js +54 -0
  190. package/dist/core/signals/code-signal.js.map +1 -0
  191. package/dist/core/signals/ingest-signal.d.ts +1 -1
  192. package/dist/core/signals/ingest-signal.js +3 -1
  193. package/dist/core/signals/ingest-signal.js.map +1 -1
  194. package/dist/core/signals/stamp-scope.d.ts +18 -0
  195. package/dist/core/signals/stamp-scope.js +28 -0
  196. package/dist/core/signals/stamp-scope.js.map +1 -0
  197. package/dist/core/storage/db-restore.js +3 -1
  198. package/dist/core/storage/db-restore.js.map +1 -1
  199. package/dist/core/storage/fact-batch.d.ts +9 -0
  200. package/dist/core/storage/fact-batch.js +14 -0
  201. package/dist/core/storage/fact-batch.js.map +1 -0
  202. package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
  203. package/dist/core/storage/pg-code-exemplar-store.js +13 -2
  204. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  205. package/dist/core/storage/pg-embedding-config.d.ts +29 -0
  206. package/dist/core/storage/pg-embedding-config.js +53 -0
  207. package/dist/core/storage/pg-embedding-config.js.map +1 -0
  208. package/dist/core/storage/pg-entity-store.d.ts +12 -0
  209. package/dist/core/storage/pg-entity-store.js +79 -0
  210. package/dist/core/storage/pg-entity-store.js.map +1 -0
  211. package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
  212. package/dist/core/storage/pg-fact-ingest.js +34 -0
  213. package/dist/core/storage/pg-fact-ingest.js.map +1 -0
  214. package/dist/core/storage/pg-fact-store.d.ts +3 -0
  215. package/dist/core/storage/pg-fact-store.js +26 -30
  216. package/dist/core/storage/pg-fact-store.js.map +1 -1
  217. package/dist/core/storage/pg-migrate.d.ts +18 -0
  218. package/dist/core/storage/pg-migrate.js +73 -0
  219. package/dist/core/storage/pg-migrate.js.map +1 -0
  220. package/dist/core/storage/pg-session-store.d.ts +11 -2
  221. package/dist/core/storage/pg-session-store.js +224 -96
  222. package/dist/core/storage/pg-session-store.js.map +1 -1
  223. package/dist/core/storage/pg-signal-store.js +4 -3
  224. package/dist/core/storage/pg-signal-store.js.map +1 -1
  225. package/dist/core/storage/pg-storage.d.ts +9 -7
  226. package/dist/core/storage/pg-storage.js +20 -50
  227. package/dist/core/storage/pg-storage.js.map +1 -1
  228. package/dist/core/storage/pg-workstream-store.d.ts +25 -0
  229. package/dist/core/storage/pg-workstream-store.js +97 -0
  230. package/dist/core/storage/pg-workstream-store.js.map +1 -0
  231. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
  232. package/dist/core/storage/sqlite-code-exemplar-store.js +15 -2
  233. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  234. package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
  235. package/dist/core/storage/sqlite-embedding-config.js +27 -0
  236. package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
  237. package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
  238. package/dist/core/storage/sqlite-entity-store.js +96 -0
  239. package/dist/core/storage/sqlite-entity-store.js.map +1 -0
  240. package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
  241. package/dist/core/storage/sqlite-fact-store.js +81 -27
  242. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  243. package/dist/core/storage/sqlite-outcome-store.d.ts +89 -0
  244. package/dist/core/storage/sqlite-outcome-store.js +158 -0
  245. package/dist/core/storage/sqlite-outcome-store.js.map +1 -0
  246. package/dist/core/storage/sqlite-session-store.d.ts +42 -7
  247. package/dist/core/storage/sqlite-session-store.js +208 -95
  248. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  249. package/dist/core/storage/sqlite-signal-store.js +4 -2
  250. package/dist/core/storage/sqlite-signal-store.js.map +1 -1
  251. package/dist/core/storage/sqlite-storage.d.ts +8 -8
  252. package/dist/core/storage/sqlite-storage.js +21 -32
  253. package/dist/core/storage/sqlite-storage.js.map +1 -1
  254. package/dist/core/storage/sqlite-workstream-store.d.ts +25 -0
  255. package/dist/core/storage/sqlite-workstream-store.js +99 -0
  256. package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
  257. package/dist/core/storage/supersedence-log.js +2 -0
  258. package/dist/core/storage/supersedence-log.js.map +1 -1
  259. package/dist/core/telemetry/probe-filter.d.ts +35 -0
  260. package/dist/core/telemetry/probe-filter.js +56 -0
  261. package/dist/core/telemetry/probe-filter.js.map +1 -0
  262. package/dist/core/update-check/check.d.ts +6 -0
  263. package/dist/core/update-check/check.js +35 -0
  264. package/dist/core/update-check/check.js.map +1 -1
  265. package/dist/core/work-digest/active-spans.d.ts +8 -0
  266. package/dist/core/work-digest/active-spans.js +29 -0
  267. package/dist/core/work-digest/active-spans.js.map +1 -0
  268. package/dist/core/work-digest/attribute.d.ts +15 -0
  269. package/dist/core/work-digest/attribute.js +76 -0
  270. package/dist/core/work-digest/attribute.js.map +1 -0
  271. package/dist/core/work-digest/build-work-digest.d.ts +12 -0
  272. package/dist/core/work-digest/build-work-digest.js +69 -0
  273. package/dist/core/work-digest/build-work-digest.js.map +1 -0
  274. package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
  275. package/dist/core/work-digest/compose-work-digest.js +52 -0
  276. package/dist/core/work-digest/compose-work-digest.js.map +1 -0
  277. package/dist/core/work-digest/merge-active.d.ts +10 -0
  278. package/dist/core/work-digest/merge-active.js +22 -0
  279. package/dist/core/work-digest/merge-active.js.map +1 -0
  280. package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
  281. package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
  282. package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
  283. package/dist/core/work-digest/topics.d.ts +18 -0
  284. package/dist/core/work-digest/topics.js +26 -0
  285. package/dist/core/work-digest/topics.js.map +1 -0
  286. package/dist/core/work-digest/types.d.ts +45 -0
  287. package/dist/core/work-digest/types.js +2 -0
  288. package/dist/core/work-digest/types.js.map +1 -0
  289. package/dist/core/workstream/bind.d.ts +26 -0
  290. package/dist/core/workstream/bind.js +42 -0
  291. package/dist/core/workstream/bind.js.map +1 -0
  292. package/dist/core/workstream/compose-recall.d.ts +2 -0
  293. package/dist/core/workstream/compose-recall.js +39 -0
  294. package/dist/core/workstream/compose-recall.js.map +1 -0
  295. package/dist/core/workstream/merge-suggest.d.ts +19 -0
  296. package/dist/core/workstream/merge-suggest.js +61 -0
  297. package/dist/core/workstream/merge-suggest.js.map +1 -0
  298. package/dist/core/workstream/model.d.ts +21 -0
  299. package/dist/core/workstream/model.js +9 -0
  300. package/dist/core/workstream/model.js.map +1 -0
  301. package/dist/core/workstream/name-match.d.ts +8 -0
  302. package/dist/core/workstream/name-match.js +18 -0
  303. package/dist/core/workstream/name-match.js.map +1 -0
  304. package/dist/core/workstream/resolve.d.ts +10 -0
  305. package/dist/core/workstream/resolve.js +21 -0
  306. package/dist/core/workstream/resolve.js.map +1 -0
  307. package/dist/core/workstream/rollup.d.ts +12 -0
  308. package/dist/core/workstream/rollup.js +17 -0
  309. package/dist/core/workstream/rollup.js.map +1 -0
  310. package/dist/core/workstream/work-topics.d.ts +10 -0
  311. package/dist/core/workstream/work-topics.js +55 -0
  312. package/dist/core/workstream/work-topics.js.map +1 -0
  313. package/dist/hook/hook-helpers.d.ts +4 -0
  314. package/dist/hook/hook-helpers.js +36 -0
  315. package/dist/hook/hook-helpers.js.map +1 -0
  316. package/dist/hook/pre-compact-hook.d.ts +23 -0
  317. package/dist/hook/pre-compact-hook.js +67 -0
  318. package/dist/hook/pre-compact-hook.js.map +1 -0
  319. package/dist/hook/prompt-recall-hook.d.ts +38 -0
  320. package/dist/hook/prompt-recall-hook.js +124 -20
  321. package/dist/hook/prompt-recall-hook.js.map +1 -1
  322. package/dist/hook/recall-gate.d.ts +29 -0
  323. package/dist/hook/recall-gate.js +50 -0
  324. package/dist/hook/recall-gate.js.map +1 -0
  325. package/dist/hook/recall-over-http.d.ts +3 -2
  326. package/dist/hook/recall-over-http.js +26 -13
  327. package/dist/hook/recall-over-http.js.map +1 -1
  328. package/dist/hook/recent-context.d.ts +25 -0
  329. package/dist/hook/recent-context.js +104 -0
  330. package/dist/hook/recent-context.js.map +1 -0
  331. package/dist/hook/score-floor.d.ts +27 -0
  332. package/dist/hook/score-floor.js +42 -0
  333. package/dist/hook/score-floor.js.map +1 -0
  334. package/dist/hook/session-end-hook.js +8 -29
  335. package/dist/hook/session-end-hook.js.map +1 -1
  336. package/dist/hook/session-start-hook.js +26 -60
  337. package/dist/hook/session-start-hook.js.map +1 -1
  338. package/dist/hook/stop-hook.js +24 -60
  339. package/dist/hook/stop-hook.js.map +1 -1
  340. package/dist/http/app.d.ts +6 -0
  341. package/dist/http/app.js +52 -44
  342. package/dist/http/app.js.map +1 -1
  343. package/dist/install/setup.js +2 -0
  344. package/dist/install/setup.js.map +1 -1
  345. package/dist/llm/build-classifier.d.ts +22 -0
  346. package/dist/llm/build-classifier.js +42 -0
  347. package/dist/llm/build-classifier.js.map +1 -0
  348. package/dist/llm/build-embedder.d.ts +11 -0
  349. package/dist/llm/build-embedder.js +42 -0
  350. package/dist/llm/build-embedder.js.map +1 -0
  351. package/dist/llm/bundled-embedder-client.d.ts +37 -0
  352. package/dist/llm/bundled-embedder-client.js +90 -0
  353. package/dist/llm/bundled-embedder-client.js.map +1 -0
  354. package/dist/llm/classifier-box.d.ts +13 -5
  355. package/dist/llm/classifier-box.js +28 -6
  356. package/dist/llm/classifier-box.js.map +1 -1
  357. package/dist/llm/classifier-egress.d.ts +18 -0
  358. package/dist/llm/classifier-egress.js +59 -0
  359. package/dist/llm/classifier-egress.js.map +1 -0
  360. package/dist/llm/client-shared.d.ts +4 -0
  361. package/dist/llm/client-shared.js +39 -0
  362. package/dist/llm/client-shared.js.map +1 -0
  363. package/dist/llm/deepseek-client.d.ts +20 -0
  364. package/dist/llm/deepseek-client.js +59 -45
  365. package/dist/llm/deepseek-client.js.map +1 -1
  366. package/dist/llm/embedder-info.d.ts +15 -0
  367. package/dist/llm/embedder-info.js +27 -0
  368. package/dist/llm/embedder-info.js.map +1 -0
  369. package/dist/llm/model-quirks.d.ts +4 -0
  370. package/dist/llm/model-quirks.js +7 -0
  371. package/dist/llm/model-quirks.js.map +1 -0
  372. package/dist/llm/naming.d.ts +5 -0
  373. package/dist/llm/naming.js +20 -0
  374. package/dist/llm/naming.js.map +1 -0
  375. package/dist/llm/ollama-client.d.ts +7 -8
  376. package/dist/llm/ollama-client.js +59 -51
  377. package/dist/llm/ollama-client.js.map +1 -1
  378. package/dist/llm/ollama-code-embedder.d.ts +8 -3
  379. package/dist/llm/ollama-code-embedder.js +14 -24
  380. package/dist/llm/ollama-code-embedder.js.map +1 -1
  381. package/dist/llm/openai-code-embedder-client.d.ts +25 -0
  382. package/dist/llm/openai-code-embedder-client.js +52 -0
  383. package/dist/llm/openai-code-embedder-client.js.map +1 -0
  384. package/dist/llm/openai-embedder-client.d.ts +41 -0
  385. package/dist/llm/openai-embedder-client.js +83 -0
  386. package/dist/llm/openai-embedder-client.js.map +1 -0
  387. package/dist/mcp/server.d.ts +69 -2
  388. package/dist/mcp/server.js +404 -19
  389. package/dist/mcp/server.js.map +1 -1
  390. package/dist/ports/code-embedder.d.ts +6 -1
  391. package/dist/ports/code-exemplar-store.d.ts +5 -0
  392. package/dist/ports/entity-store.d.ts +35 -0
  393. package/dist/ports/entity-store.js +7 -0
  394. package/dist/ports/entity-store.js.map +1 -0
  395. package/dist/ports/fact-store.d.ts +0 -0
  396. package/dist/ports/fact-store.js.map +1 -1
  397. package/dist/ports/llm-client.d.ts +9 -1
  398. package/dist/ports/outcome.d.ts +85 -0
  399. package/dist/ports/outcome.js +11 -0
  400. package/dist/ports/outcome.js.map +1 -0
  401. package/dist/ports/session-store.d.ts +12 -6
  402. package/dist/ports/storage.d.ts +7 -30
  403. package/dist/ports/storage.js +3 -7
  404. package/dist/ports/storage.js.map +1 -1
  405. package/dist/ports/transcript-adapter.d.ts +2 -0
  406. package/dist/ports/workstream-store.d.ts +22 -0
  407. package/dist/ports/workstream-store.js +2 -0
  408. package/dist/ports/workstream-store.js.map +1 -0
  409. package/dist/shared/net.d.ts +3 -0
  410. package/dist/shared/net.js +4 -0
  411. package/dist/shared/net.js.map +1 -0
  412. package/dist/shared/types.d.ts +56 -2
  413. package/dist/shared/types.js +4 -1
  414. package/dist/shared/types.js.map +1 -1
  415. package/dist/ui/assets/{index-CMpQkbQy.js → index-CSaBtgXD.js} +1 -1
  416. package/dist/ui/index.html +1 -1
  417. package/fixtures/classifier-gold/README.md +94 -0
  418. package/fixtures/classifier-gold/reference.json +318 -0
  419. package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
  420. package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
  421. package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
  422. package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
  423. package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
  424. package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
  425. package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
  426. package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
  427. package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
  428. package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
  429. package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
  430. package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
  431. package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
  432. package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
  433. package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
  434. package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
  435. package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
  436. package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
  437. package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
  438. package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
  439. package/migrations/000_initial_schema.sql +1 -1
  440. package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
  441. package/migrations/025_workstreams.sql +30 -0
  442. package/migrations/026_embedding_config.sql +12 -0
  443. package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
  444. package/migrations/028_classifier_provenance.sql +5 -0
  445. package/migrations/029_project_scope.sql +21 -0
  446. package/migrations/032_session_derivables.sql +11 -0
  447. package/migrations/pg/001_initial.sql +34 -0
  448. package/migrations/pg/019_split_replaces.sql +16 -7
  449. package/migrations/pg/025_workstreams.sql +34 -0
  450. package/migrations/pg/026_embedding_config.sql +12 -0
  451. package/migrations/pg/028_classifier_provenance.sql +5 -0
  452. package/migrations/pg/029_entity_variants.sql +9 -0
  453. package/migrations/pg/030_project_scope.sql +23 -0
  454. package/migrations/pg/031_widen_session_edges_kind.sql +11 -0
  455. package/migrations/pg/032_session_derivables.sql +17 -0
  456. package/nlm/index.js +405 -39
  457. package/package.json +12 -2
  458. package/plugin/.codex-plugin/plugin.json +2 -2
  459. package/plugin/scripts/prompt-recall-hook.mjs +413 -43
  460. package/plugin/scripts/session-start-hook.mjs +295 -61
  461. package/plugin/scripts/stop-hook.mjs +89 -66
  462. package/plugin-hermes-agent/__init__.py +2 -1
  463. package/templates/agent-contract/README.md +24 -0
  464. package/templates/agent-contract/claude-code.md +40 -0
  465. package/templates/agent-contract/generic.md +38 -0
  466. package/dist/core/embedding/embed-normalize.d.ts +0 -28
  467. package/dist/core/embedding/embed-normalize.js +0 -98
  468. package/dist/core/embedding/embed-normalize.js.map +0 -1
  469. package/dist/core/storage/pg-tx-context.d.ts +0 -52
  470. package/dist/core/storage/pg-tx-context.js +0 -149
  471. package/dist/core/storage/pg-tx-context.js.map +0 -1
package/dist/cli/nlm.js CHANGED
@@ -22,11 +22,12 @@
22
22
  * nlm disconnect claude-code — remove MCP block from ~/.mcp.json
23
23
  * nlm disconnect codex — remove Codex plugin
24
24
  * nlm digest — print a daily-activity digest (or --telegram to post)
25
+ * nlm init - print (or write) the agent recall contract snippet
25
26
  */
26
- import { fileURLToPath } from "node:url";
27
+ import { fileURLToPath, pathToFileURL } from "node:url";
27
28
  import { dirname, resolve, join } from "node:path";
28
29
  import { homedir } from "node:os";
29
- import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync } from "node:fs";
30
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync, realpathSync, appendFileSync } from "node:fs";
30
31
  import { execFileSync, spawn, spawnSync } from "node:child_process";
31
32
  import { Command } from "commander";
32
33
  import pkg from "../../package.json" with { type: "json" };
@@ -40,12 +41,14 @@ import { PgStorage } from "../core/storage/pg-storage.js";
40
41
  import { applyPendingRestore, stageRestore } from "../core/storage/db-restore.js";
41
42
  import { listBackupDates, resolveBackup, runRollingBackup } from "../core/storage/backup-rotation.js";
42
43
  import { createApp } from "../http/app.js";
43
- import { createMcpServer } from "../mcp/server.js";
44
- import { ClassifierBox } from "../llm/classifier-box.js";
45
- import { DeepSeekClient } from "../llm/deepseek-client.js";
46
- import { OllamaClient } from "../llm/ollama-client.js";
47
- import { OllamaCodeEmbedder } from "../llm/ollama-code-embedder.js";
44
+ import { createMcpServer, listMergeSuggestionsHandler, mergeWorkstreamsHandler, rebindSessionHandler, recallWorkstreamHandler, renameWorkstreamHandler, retireWorkstreamHandler } from "../mcp/server.js";
45
+ import { classifierEgressNotice } from "../llm/classifier-egress.js";
46
+ import { buildClassifier } from "../llm/build-classifier.js";
47
+ import { OllamaCodeEmbedder, DEFAULT_CODE_EMBED_MODEL } from "../llm/ollama-code-embedder.js";
48
+ import { OpenAICodeEmbedderClient } from "../llm/openai-code-embedder-client.js";
49
+ import { resolveEmbedderInfo } from "../llm/embedder-info.js";
48
50
  import { autoloadEnv } from "../llm/env-autoload.js";
51
+ import { buildEmbedder as _buildEmbedder } from "../llm/build-embedder.js";
49
52
  import { addHook, buildHookCommand, removeHook } from "../core/hook/claude-settings.js";
50
53
  import { codexBinaryAvailable, connectCodex, disconnectCodex, repairCodex, pluginScriptsDir, } from "../install/codex.js";
51
54
  import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath } from "../install/claude-code.js";
@@ -56,6 +59,10 @@ import { embeddingModelPresent, ensureMcpToken, ollamaModelPresent } from "../in
56
59
  import { connectCursor, disconnectCursor } from "../install/cursor.js";
57
60
  import { describeRemove, describeUpsert, installCursorRules, installOpencodeRules, installWindsurfRules, uninstallCursorRules, uninstallOpencodeRules, uninstallWindsurfRules, } from "../install/rules-install.js";
58
61
  import { runSupersedeCommand } from "./supersede.js";
62
+ import { runInitCommand } from "./init.js";
63
+ import { runScopeBackfill, formatBackfillResult } from "./scope-backfill.js";
64
+ import { runScopeCoverage, formatCoverageResult } from "./scope-coverage.js";
65
+ import { loadAliasMap } from "../core/scope/alias-map.js";
59
66
  import { getUpdateStatus } from "../core/update-check/check.js";
60
67
  import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
61
68
  import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
@@ -64,8 +71,16 @@ import { connectPi, disconnectPi, piSettingsPath } from "../install/pi.js";
64
71
  import { runSetup } from "../install/setup.js";
65
72
  import { runParity } from "./classify-parity.js";
66
73
  import { reembedCorpus } from "../core/embedding/embed-backfill.js";
74
+ import { reembedCorpusPg } from "../core/embedding/pg-embed-backfill.js";
75
+ import { backfillExemplarEmbeddings } from "../core/exemplars/embed-backfill.js";
76
+ import { warmCodeEmbedder } from "../core/exemplars/warm-embedder.js";
77
+ import { markWarm } from "../core/health/warmup-state.js";
78
+ import { setLaneHealth } from "../core/health/embedding-lane-state.js";
79
+ import { setCorpusSnapshot, corpusSnapshot } from "../core/health/corpus-state.js";
80
+ import { computeCorpusStats, sqliteCorpusStatsDeps, parseCorpusThresholds, thresholdState } from "../core/metrics/corpus-stats.js";
81
+ import { computeReDerivationRate, sqliteReDerivationDeps } from "../core/metrics/re-derivation.js";
82
+ import { reconcileLane } from "../core/embedding/embedding-config.js";
67
83
  import { backfillFacts } from "../core/facts/backfill-facts.js";
68
- import { normalizeEmbeddings } from "../core/embedding/embed-normalize.js";
69
84
  import { ScanScheduler } from "../core/scheduler/scheduler.js";
70
85
  import { MemoSweepScheduler } from "../core/hook/memo-sweep.js";
71
86
  import { isAgentLoaded, isBenignBootoutError } from "./launchctl-helpers.js";
@@ -75,14 +90,22 @@ import { applyEnvAssignment } from "./config-env.js";
75
90
  import { adapterFromSource } from "../core/adapters/from-source.js";
76
91
  import { runDigest } from "./digest.js";
77
92
  import { installScope } from "../core/signals/install-scope.js";
93
+ import { buildWorkDigest } from "../core/work-digest/build-work-digest.js";
94
+ import { composeWorkDigest } from "../core/work-digest/compose-work-digest.js";
95
+ import { defaultTopicProvider, aliasTopicProvider } from "../core/work-digest/topics.js";
78
96
  import { runChecksOnSqlite, runChecksOnPg, applyFixOnSqlite, applyFixOnPg } from "../core/integrity/check-invariants.js";
97
+ import { normalizeLabel } from "../core/workstream/model.js";
98
+ import { resolveWorkstreamId } from "../core/workstream/resolve.js";
99
+ import { suggestMerges } from "../core/entities/dedup-suggest.js";
100
+ import { DEFAULT_NLM_PORT } from "../shared/net.js";
79
101
  const __filename = fileURLToPath(import.meta.url);
80
102
  const __dirname = dirname(__filename);
81
103
  const MIGRATIONS_DIR = resolve(__dirname, "../../migrations");
104
+ const CLASSIFIER_FIXTURES_DIR = resolve(__dirname, "../../fixtures/classifier-gold");
82
105
  const PG_MIGRATIONS_DIR = join(fileURLToPath(new URL(".", import.meta.url)), "../../migrations/pg");
83
106
  const UI_DIST = resolve(__dirname, "../../dist/ui");
84
107
  const DEFAULT_DB_PATH = resolve(homedir(), ".nlm/canonical.sqlite");
85
- const DEFAULT_PORT = 3940;
108
+ const DEFAULT_PORT = Number.parseInt(DEFAULT_NLM_PORT, 10);
86
109
  function dbPath() {
87
110
  return process.env["NLM_DB_PATH"] ?? DEFAULT_DB_PATH;
88
111
  }
@@ -98,19 +121,121 @@ function port() {
98
121
  function ollamaUrl() {
99
122
  return process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
100
123
  }
101
- function buildClassifier() {
102
- // qwen3.5:4b is the production classifier as of 2026-06-12 (NLM task #320).
103
- // Scored 74.4% decision-precision vs qwen3:4b-instruct-2507-q4_K_M's 58.7% in
104
- // the 2026-06-11 eval. Requires think:false (handled by ClassifierBox via
105
- // classifierNeedsThinkDisabled) to stay within the 180s classify timeout.
106
- // Ollama is the default to keep the daemon local-first and key-free; DeepSeek
107
- // remains available via NLM_CLASSIFIER=deepseek for users who prioritize speed.
108
- const provider = (process.env["NLM_CLASSIFIER"] ?? "ollama").toLowerCase();
109
- if (provider !== "ollama")
124
+ export function resolveDigestDate(date) {
125
+ if (date !== undefined) {
126
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
127
+ throw new Error(`invalid --date "${date}"; expected YYYY-MM-DD`);
128
+ }
129
+ return date;
130
+ }
131
+ const d = new Date();
132
+ const mo = String(d.getMonth() + 1).padStart(2, "0");
133
+ const da = String(d.getDate()).padStart(2, "0");
134
+ return `${d.getFullYear()}-${mo}-${da}`;
135
+ }
136
+ function workDigestEnv() {
137
+ const idle = Number.parseInt(process.env["NLM_WORK_IDLE_THRESHOLD_MIN"] ?? "5", 10);
138
+ const deep = Number.parseInt(process.env["NLM_WORK_DEEP_BLOCK_MIN"] ?? "25", 10);
139
+ return {
140
+ idleThresholdMin: Number.isFinite(idle) && idle > 0 ? idle : 5,
141
+ deepBlockMin: Number.isFinite(deep) && deep > 0 ? deep : 25,
142
+ };
143
+ }
144
+ const TREND_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
145
+ export function shouldAppendTrend(lastLineTs, now) {
146
+ if (lastLineTs === null)
147
+ return true;
148
+ const parsed = Date.parse(lastLineTs);
149
+ if (!Number.isFinite(parsed))
150
+ return true;
151
+ return now - parsed >= TREND_INTERVAL_MS;
152
+ }
153
+ function loadTopicProvider() {
154
+ try {
155
+ const raw = readFileSync(join(homedir(), ".nlm", "work-topics.json"), "utf8");
156
+ const map = JSON.parse(raw);
157
+ return aliasTopicProvider(map);
158
+ }
159
+ catch {
160
+ return defaultTopicProvider;
161
+ }
162
+ }
163
+ /** Build the recall/document embedder. Delegates to the shared factory in
164
+ * src/llm/build-embedder.ts — see that file for provider routing details. */
165
+ function buildEmbedder() {
166
+ return _buildEmbedder();
167
+ }
168
+ /** Build the code-lane embedder. Follows the same destination as the prose
169
+ * embedder (NLM_EMBED_PROVIDER / NLM_EMBED_BASE_URL) but with its own model
170
+ * (NLM_CODE_EMBED_MODEL, e.g. text-embedding-coderankembed on LM Studio).
171
+ * Switching providers on existing exemplars requires `nlm embed-backfill
172
+ * --exemplars`. */
173
+ function buildCodeEmbedder() {
174
+ const provider = (process.env["NLM_EMBED_PROVIDER"] ?? "ollama").toLowerCase();
175
+ if (provider === "openai") {
110
176
  autoloadEnv();
111
- const model = process.env["NLM_CLASSIFIER_MODEL"]
112
- ?? (provider === "ollama" ? "qwen3.5:4b" : "deepseek-v4-flash");
113
- return new ClassifierBox({ provider, model, ollamaUrl: ollamaUrl() });
177
+ const baseUrl = process.env["NLM_EMBED_BASE_URL"];
178
+ if (!baseUrl) {
179
+ throw new Error("NLM_EMBED_PROVIDER=openai requires NLM_EMBED_BASE_URL for the code embedder too, " +
180
+ "e.g. http://localhost:1234/v1 for LM Studio.");
181
+ }
182
+ return new OpenAICodeEmbedderClient({
183
+ baseUrl,
184
+ ...(process.env["NLM_CODE_EMBED_MODEL"] ? { model: process.env["NLM_CODE_EMBED_MODEL"] } : {}),
185
+ ...(process.env["NLM_EMBED_API_KEY"] ? { apiKey: process.env["NLM_EMBED_API_KEY"] } : {}),
186
+ });
187
+ }
188
+ // The bundled provider has no code-embedding engine (CodeRankEmbed has no ONNX equivalent
189
+ // in slice 1). Fall through to the Ollama default: the startup probe will fail soft and
190
+ // the code-exemplar lane stays unknown, which is the intended degradation.
191
+ return new OllamaCodeEmbedder({ baseUrl: ollamaUrl() });
192
+ }
193
+ async function reconcileEmbeddingLanes(storage, embedder, codeEmbedder) {
194
+ const configStore = storage.embeddingConfig;
195
+ if (!configStore)
196
+ return;
197
+ const nowIso = new Date().toISOString();
198
+ try {
199
+ const result = await embedder.embed("nlm dimension probe", "query");
200
+ const dim = result.vector.length;
201
+ const info = resolveEmbedderInfo(process.env, dim);
202
+ const runtime = { lane: "prose", provider: info.provider, model: info.model, dim };
203
+ const rec = reconcileLane(configStore, runtime, nowIso);
204
+ if (rec.state === "stale") {
205
+ const stored = rec.stored;
206
+ console.error(`nlm-memory: prose embedding lane stale: stored ${stored.provider}/${stored.model}@${stored.dim}` +
207
+ ` vs runtime ${info.provider}/${info.model}@${dim}; run \`nlm embed-backfill\` to re-embed`);
208
+ setLaneHealth("prose", "stale");
209
+ }
210
+ else {
211
+ setLaneHealth("prose", "ok");
212
+ }
213
+ }
214
+ catch {
215
+ // probe failure; prose lane stays unknown
216
+ }
217
+ if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1")
218
+ return;
219
+ try {
220
+ const result = await codeEmbedder.embed("nlm dimension probe", "query");
221
+ const dim = result.dim;
222
+ const provider = (process.env["NLM_EMBED_PROVIDER"] ?? "ollama").toLowerCase();
223
+ const model = process.env["NLM_CODE_EMBED_MODEL"] ?? DEFAULT_CODE_EMBED_MODEL;
224
+ const runtime = { lane: "code", provider, model, dim };
225
+ const rec = reconcileLane(configStore, runtime, nowIso);
226
+ if (rec.state === "stale") {
227
+ const stored = rec.stored;
228
+ console.error(`nlm-memory: code embedding lane stale: stored ${stored.provider}/${stored.model}@${stored.dim}` +
229
+ ` vs runtime ${provider}/${model}@${dim}; run \`nlm embed-backfill --exemplars\` to re-embed`);
230
+ setLaneHealth("code", "stale");
231
+ }
232
+ else {
233
+ setLaneHealth("code", "ok");
234
+ }
235
+ }
236
+ catch {
237
+ // probe failure; code lane stays unknown
238
+ }
114
239
  }
115
240
  async function buildAdapters(sources) {
116
241
  // Sources table is the source of truth. Each enabled row maps to one
@@ -172,21 +297,56 @@ async function buildStack() {
172
297
  // seedDefaults (not on ProviderRegistryPort), so this narrows to the SQLite case.
173
298
  if (providers instanceof ProviderRegistry)
174
299
  await providers.seedDefaults();
175
- // Recall only uses embed(). Embeddings live on Ollama; DeepSeek doesn't
176
- // expose them. Classifier is wired separately for Phase D ingest.
177
- const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
300
+ // Recall only uses embed(). Default embedder is local Ollama; NLM_EMBED_*
301
+ // can point it at any OpenAI-compatible endpoint. Classifier is wired
302
+ // separately for Phase D ingest.
303
+ const embedder = buildEmbedder();
178
304
  const classifier = buildClassifier();
305
+ const wsStore = storage.workstreams;
179
306
  const recall = new RecallService({
180
307
  store,
181
308
  llm: embedder,
182
309
  factStore: facts,
183
310
  exemplarStore: storage.exemplars,
184
- codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
311
+ codeEmbedder: buildCodeEmbedder(),
185
312
  installScope: scope,
313
+ resolveWorkstreamMembers: async (idOrLabel) => {
314
+ const all = await wsStore.listAll();
315
+ const byId = new Map(all.map((w) => [w.id, { id: w.id, mergedInto: w.mergedInto }]));
316
+ const target = all.find((w) => w.id === idOrLabel)
317
+ ?? all.find((w) => normalizeLabel(w.label) === normalizeLabel(idOrLabel));
318
+ if (!target)
319
+ return [];
320
+ const survivor = resolveWorkstreamId(target.id, byId);
321
+ return all.filter((w) => resolveWorkstreamId(w.id, byId) === survivor).map((w) => w.id);
322
+ },
186
323
  });
187
324
  const factRecall = new FactRecallService({ factStore: facts, llm: embedder });
188
325
  return { storage, store, facts, signals, scope, sources, providers, recall, factRecall, embedder, classifier };
189
326
  }
327
+ export async function runClassifierEvalCommand(deps) {
328
+ const { runClassifierFixtureEval } = await import("../core/eval/classifier-fixture-eval.js");
329
+ const result = await runClassifierFixtureEval(deps.classify, deps.fixturesDir, deps.limit !== undefined ? { limit: deps.limit } : undefined);
330
+ if (deps.json) {
331
+ deps.stdout(JSON.stringify({ ...result, provider: deps.provider, model: deps.model }, null, 2) + "\n");
332
+ return;
333
+ }
334
+ const a = result.aggregate;
335
+ const pct = (n) => `${(n * 100).toFixed(1)}%`;
336
+ deps.stdout(`lane: ${deps.provider}/${deps.model}\n`);
337
+ deps.stdout(`n=${result.perTranscript.length}\n`);
338
+ deps.stdout(`schema-valid: ${pct(a.schemaValidRate)}\n`);
339
+ deps.stdout(`label-accuracy: ${pct(a.labelAccuracy)}\n`);
340
+ deps.stdout(`entity-F1: ${pct(a.entityF1)}\n`);
341
+ deps.stdout(`decision-F1: ${pct(a.decisionF1)}\n`);
342
+ deps.stdout(`conf-calibration: ${pct(a.confidenceCalibrationRate)}\n`);
343
+ deps.stdout(`p50-latency: ${a.p50LatencyMs}ms\n`);
344
+ deps.stdout(`p95-latency: ${a.p95LatencyMs}ms\n`);
345
+ if (a.timedOutOrUnreachable > 0) {
346
+ deps.stdout(`timed-out: ${a.timedOutOrUnreachable} fixture(s) scored as failures (LLM unreachable/timeout)\n`);
347
+ }
348
+ deps.stdout(`see: docs/classifier-tiers.md\n`);
349
+ }
190
350
  const program = new Command();
191
351
  program
192
352
  .name("nlm")
@@ -208,6 +368,7 @@ program
208
368
  const { storage, store, facts, signals, scope, sources, providers, recall, factRecall, embedder, classifier } = await buildStack();
209
369
  const { existsSync } = await import("node:fs");
210
370
  const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
371
+ const codeEmb = buildCodeEmbedder();
211
372
  const app = createApp({
212
373
  recall,
213
374
  store,
@@ -223,15 +384,19 @@ program
223
384
  embedder,
224
385
  store,
225
386
  ...(facts ? { factStore: facts } : {}),
387
+ classifierDescriptor: { provider: classifier.provider, model: classifier.model },
226
388
  },
227
389
  signalStore: signals,
228
390
  installScope: scope,
391
+ // Signal scope inheritance (NLM_SCOPE_STAMP): a signal with only a
392
+ // session_id inherits that session's stamped scope at ingest.
393
+ sessionScopeReader: store,
229
394
  // Code-exemplar lane. The store + code embedder are always wired; the
230
395
  // NLM_CODE_EXEMPLARS_ENABLED flag gates capture (POST /api/signal) and
231
396
  // the /api/exemplar + /api/recall-code routes at request time.
232
397
  exemplarStore: storage.exemplars,
233
- codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
234
- embedderInfo: { provider: "ollama", model: "nomic-embed-text", dims: 768 },
398
+ codeEmbedder: codeEmb,
399
+ embedderInfo: resolveEmbedderInfo(),
235
400
  ...(existsSync(UI_DIST) ? { uiDist: UI_DIST } : {}),
236
401
  // Wire POST /mcp only when NLM_MCP_TOKEN is present. Absent = route never
237
402
  // mounts, zero attack surface. Present = token-gated Streamable-HTTP MCP
@@ -246,12 +411,28 @@ program
246
411
  // Parity with the stdio `nlm mcp` server: remote/container MCP
247
412
  // clients hitting POST /mcp get recall_code too when the flag is on.
248
413
  exemplarStore: storage.exemplars,
249
- codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
414
+ codeEmbedder: codeEmb,
250
415
  installScope: scope,
416
+ signalStore: signals,
417
+ sessionScopeReader: store,
418
+ workDigest: { store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() },
419
+ workstreams: { store: storage.workstreams, sessions: store, facts: facts, exemplars: storage.exemplars },
251
420
  },
252
421
  }
253
422
  : {}),
254
423
  });
424
+ warmCodeEmbedder(codeEmb);
425
+ void reconcileEmbeddingLanes(storage, embedder, codeEmb).catch(() => { });
426
+ void embedder
427
+ .embed("warmup init", "query")
428
+ .then(() => markWarm("textEmbedder"))
429
+ .catch(() => { });
430
+ setImmediate(() => {
431
+ void recall
432
+ .search({ query: "warmup init", mode: "keyword", limit: 1 })
433
+ .then(() => markWarm("fts5"))
434
+ .catch(() => { });
435
+ });
255
436
  const p = port();
256
437
  serve({ fetch: app.fetch, port: p, hostname: "127.0.0.1" }, (info) => {
257
438
  console.error(`nlm-memory http listening on http://localhost:${info.port}`);
@@ -260,6 +441,14 @@ program
260
441
  }
261
442
  console.error(` db: ${dbPath()}`);
262
443
  console.error(` ollama: ${ollamaUrl()}`);
444
+ const classifier = (process.env["NLM_CLASSIFIER"] ?? "ollama").toLowerCase();
445
+ const egress = classifierEgressNotice(classifier, process.env["NLM_CLASSIFIER_BASE_URL"]);
446
+ const classifyTarget = classifier === "openai" && process.env["NLM_CLASSIFIER_BASE_URL"]
447
+ ? `${classifier} (${process.env["NLM_CLASSIFIER_BASE_URL"]})`
448
+ : classifier;
449
+ console.error(` classify: ${classifyTarget} [${egress ? "cloud egress" : "local"}]`);
450
+ if (egress)
451
+ console.error(` notice: ${egress}`);
263
452
  // Passive update notice. Fire-and-forget so a slow npm registry
264
453
  // round-trip can't delay the startup banner; surfaced only when
265
454
  // strictly behind. See src/core/update-check/check.ts for the
@@ -306,6 +495,61 @@ program
306
495
  void signals.pruneOlderThan(cutoff).catch(() => { });
307
496
  }, SIGNAL_PRUNE_INTERVAL_MS);
308
497
  signalPruneTimer.unref();
498
+ // Corpus monitor: 24h stats job + weekly re-derivation trend. SQLite only.
499
+ // First run is delayed ~60s so warmup finishes first; then repeats every 24h.
500
+ if (!(storage instanceof PgStorage)) {
501
+ const CORPUS_MONITOR_INTERVAL_MS = 24 * 60 * 60 * 1000;
502
+ const CORPUS_MONITOR_INITIAL_DELAY_MS = 60 * 1000;
503
+ const nlmDataDir = join(homedir(), ".nlm");
504
+ const corpusStatsPath = join(nlmDataDir, "corpus-stats.jsonl");
505
+ const rederivTrendPath = join(nlmDataDir, "re-derivation-trend.jsonl");
506
+ const runCorpusMonitor = async () => {
507
+ try {
508
+ const sqliteStorage = storage;
509
+ const rawDb = sqliteStorage.rawDb();
510
+ const deps = sqliteCorpusStatsDeps(rawDb, dbPath());
511
+ const stats = await computeCorpusStats(deps);
512
+ const thresholds = parseCorpusThresholds(process.env);
513
+ const state = thresholdState(stats.dbBytes, thresholds);
514
+ const ts = new Date().toISOString();
515
+ // hapaxEntities derives from the denormalized entities.session_count column,
516
+ // which drifts (~6% today) until the entity-merge primitive recomputes counts
517
+ // exactly. This is a monitoring approximation, not ground truth.
518
+ const snap = { ...stats, state, lastComputedAt: ts };
519
+ setCorpusSnapshot(snap);
520
+ mkdirSync(nlmDataDir, { recursive: true });
521
+ appendFileSync(corpusStatsPath, JSON.stringify({ ts, ...stats, state }) + "\n", "utf8");
522
+ if (state === "warn" || state === "alert") {
523
+ console.error(`[corpus-monitor] ${state}: db is ${stats.dbBytes} bytes`);
524
+ }
525
+ const now = Date.now();
526
+ let lastTrendTs = null;
527
+ try {
528
+ const trendContent = readFileSync(rederivTrendPath, "utf8");
529
+ const lines = trendContent.split("\n").filter(Boolean);
530
+ const lastLine = lines[lines.length - 1];
531
+ if (lastLine) {
532
+ const parsed = JSON.parse(lastLine);
533
+ lastTrendTs = typeof parsed.ts === "string" ? parsed.ts : null;
534
+ }
535
+ }
536
+ catch {
537
+ // file absent or unreadable; treat as no prior entry
538
+ }
539
+ if (shouldAppendTrend(lastTrendTs, now)) {
540
+ const rdDeps = sqliteReDerivationDeps(rawDb);
541
+ const report = await computeReDerivationRate(rdDeps, 42);
542
+ appendFileSync(rederivTrendPath, JSON.stringify({ ts, windowDays: 42, rate: report.rate, pairCount: report.pairs.length, eligible: report.eligible }) + "\n", "utf8");
543
+ }
544
+ }
545
+ catch (e) {
546
+ console.error(`[corpus-monitor] error: ${e instanceof Error ? e.message : String(e)}`);
547
+ }
548
+ };
549
+ const corpusMonitorTimer = setInterval(() => { void runCorpusMonitor(); }, CORPUS_MONITOR_INTERVAL_MS);
550
+ corpusMonitorTimer.unref();
551
+ setTimeout(() => { void runCorpusMonitor(); }, CORPUS_MONITOR_INITIAL_DELAY_MS).unref();
552
+ }
309
553
  // Memo sweep runs independently of the transcript scheduler — it's the
310
554
  // backstop for SessionEnd hook unreliability (crashes, kill -9, IDE
311
555
  // force-close don't fire SessionEnd, so memo files would otherwise
@@ -329,7 +573,9 @@ program
329
573
  installScope: scope,
330
574
  intervalMs: opts.intervalMin * 60_000,
331
575
  exemplarStore: storage.exemplars,
332
- codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
576
+ codeEmbedder: buildCodeEmbedder(),
577
+ workstreams: storage.workstreams,
578
+ classifierDescriptor: { provider: classifier.provider, model: classifier.model },
333
579
  });
334
580
  scheduler.start();
335
581
  console.error(` scheduler: ${adapters.map((a) => a.name).join(", ")} every ${opts.intervalMin}m`);
@@ -368,6 +614,7 @@ program
368
614
  .option("-k, --kind <kind>", "filter by marker kind (decision|open)")
369
615
  .option("-m, --mode <mode>", "recall mode: keyword, semantic, or hybrid (default: keyword)", "keyword")
370
616
  .option("-l, --limit <n>", "max results", (v) => Number.parseInt(v, 10), 10)
617
+ .option("-w, --workstream <idOrLabel>", "filter by workstream (id or label; merge chains resolve)")
371
618
  .option("--json", "emit the raw JSON result instead of rendered lines")
372
619
  .action(async (query, opts) => {
373
620
  const { storage, recall } = await buildStack();
@@ -381,6 +628,7 @@ program
381
628
  includeSuperseded: true,
382
629
  ...(opts.entity ? { entity: opts.entity } : {}),
383
630
  ...(opts.kind ? { kind: opts.kind } : {}),
631
+ ...(opts.workstream ? { workstream: opts.workstream } : {}),
384
632
  });
385
633
  if (opts.json) {
386
634
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -398,6 +646,149 @@ program
398
646
  await storage.close();
399
647
  }
400
648
  });
649
+ program
650
+ .command("recall-workstream")
651
+ .description("Recall a workstream's accumulated context (id or label)")
652
+ .argument("<idOrLabel>", "workstream id or label")
653
+ .action(async (idOrLabel) => {
654
+ const { storage, store } = await buildStack();
655
+ try {
656
+ const r = await recallWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel });
657
+ process.stdout.write(r.content[0].text + "\n");
658
+ }
659
+ finally {
660
+ await storage.close();
661
+ }
662
+ });
663
+ program
664
+ .command("rebind-session")
665
+ .description("Rebind a session to a workstream (operator correction)")
666
+ .argument("<sessionId>", "session id")
667
+ .argument("<workstream>", "target workstream id or label")
668
+ .action(async (sessionId, workstream) => {
669
+ const { storage, store } = await buildStack();
670
+ try {
671
+ const r = await rebindSessionHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { sessionId, workstream });
672
+ process.stdout.write(r.content[0].text + "\n");
673
+ }
674
+ finally {
675
+ await storage.close();
676
+ }
677
+ });
678
+ program
679
+ .command("merge-workstreams")
680
+ .description("Merge a duplicate workstream into the one to keep")
681
+ .argument("<from>", "duplicate workstream id or label")
682
+ .argument("<into>", "survivor workstream id or label")
683
+ .action(async (from, into) => {
684
+ const { storage, store } = await buildStack();
685
+ try {
686
+ const r = await mergeWorkstreamsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { from, into });
687
+ process.stdout.write(r.content[0].text + "\n");
688
+ }
689
+ finally {
690
+ await storage.close();
691
+ }
692
+ });
693
+ program
694
+ .command("rename-workstream")
695
+ .description("Rename a workstream")
696
+ .argument("<idOrLabel>", "workstream id or current label")
697
+ .argument("<label>", "new label")
698
+ .action(async (idOrLabel, label) => {
699
+ const { storage, store } = await buildStack();
700
+ try {
701
+ const r = await renameWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel, label });
702
+ process.stdout.write(r.content[0].text + "\n");
703
+ }
704
+ finally {
705
+ await storage.close();
706
+ }
707
+ });
708
+ program
709
+ .command("retire-workstream")
710
+ .description("Retire (mark dead) a workstream")
711
+ .argument("<idOrLabel>", "workstream id or label")
712
+ .action(async (idOrLabel) => {
713
+ const { storage, store } = await buildStack();
714
+ try {
715
+ const r = await retireWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel });
716
+ process.stdout.write(r.content[0].text + "\n");
717
+ }
718
+ finally {
719
+ await storage.close();
720
+ }
721
+ });
722
+ program
723
+ .command("merge-suggestions")
724
+ .description("List likely-duplicate workstreams to merge")
725
+ .option("-m, --min-score <n>", "minimum similarity score 0..1", "0.5")
726
+ .action(async (opts) => {
727
+ const minScore = Number(opts.minScore);
728
+ if (!Number.isFinite(minScore)) {
729
+ process.stderr.write(`nlm: invalid --min-score "${opts.minScore}" (expected a number 0..1)\n`);
730
+ process.exitCode = 1;
731
+ return;
732
+ }
733
+ const { storage, store } = await buildStack();
734
+ try {
735
+ const r = await listMergeSuggestionsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { minScore });
736
+ process.stdout.write(r.content[0].text + "\n");
737
+ }
738
+ finally {
739
+ await storage.close();
740
+ }
741
+ });
742
+ program
743
+ .command("eval")
744
+ .description("Evaluate recall quality (--queries) or classifier extraction quality (--classifier)")
745
+ .option("--queries <file>", "JSON file: [{ query, expectedIds }]")
746
+ .option("--classifier", "benchmark the configured classifier lane against shipped gold fixtures")
747
+ .option("--limit <n>", "run only the first N fixtures (classifier mode)", (v) => Number.parseInt(v, 10))
748
+ .option("--mode <mode>", "keyword | semantic | hybrid (queries mode)", "keyword")
749
+ .option("--json", "emit JSON instead of a table")
750
+ .action(async (opts) => {
751
+ if (opts.classifier) {
752
+ const stack = await buildStack();
753
+ try {
754
+ const limitVal = opts.limit;
755
+ await runClassifierEvalCommand({
756
+ classify: (t) => stack.classifier.classify(t),
757
+ provider: stack.classifier.provider,
758
+ model: stack.classifier.model,
759
+ fixturesDir: CLASSIFIER_FIXTURES_DIR,
760
+ ...(limitVal !== undefined ? { limit: limitVal } : {}),
761
+ json: Boolean(opts.json),
762
+ stdout: (s) => { process.stdout.write(s); },
763
+ });
764
+ }
765
+ finally {
766
+ await stack.storage.close();
767
+ }
768
+ return;
769
+ }
770
+ if (!opts.queries) {
771
+ process.stderr.write("error: required option '--queries <file>' not specified\n");
772
+ process.exitCode = 1;
773
+ return;
774
+ }
775
+ const { readFile } = await import("node:fs/promises");
776
+ const queries = JSON.parse(await readFile(opts.queries, "utf8"));
777
+ const { runEval } = await import("../core/eval/run-eval.js");
778
+ const { storage, recall } = await buildStack();
779
+ try {
780
+ const report = await runEval({ recall }, queries, { mode: opts.mode, k: 5 });
781
+ if (opts.json) {
782
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
783
+ return;
784
+ }
785
+ process.stdout.write(`mode=${report.mode} n=${report.n} R@1=${(report.rAt1 * 100).toFixed(1)}% ` +
786
+ `R@5=${(report.rAt5 * 100).toFixed(1)}% MRR=${report.mrr.toFixed(3)}\n`);
787
+ }
788
+ finally {
789
+ await storage.close();
790
+ }
791
+ });
401
792
  program
402
793
  .command("recall-code")
403
794
  .description("Semantic search over code exemplars (requires NLM_CODE_EXEMPLARS_ENABLED=1)")
@@ -413,11 +804,10 @@ program
413
804
  process.exit(1);
414
805
  }
415
806
  const { storage } = await buildStack();
416
- const { OllamaCodeEmbedder } = await import("../llm/ollama-code-embedder.js");
417
807
  const { recallCode } = await import("../core/exemplars/recall-code.js");
418
808
  const { installScope: getScope } = await import("../core/signals/install-scope.js");
419
809
  try {
420
- const embedder = new OllamaCodeEmbedder();
810
+ const embedder = buildCodeEmbedder();
421
811
  const result = await recallCode({
422
812
  query,
423
813
  installScope: getScope(),
@@ -450,6 +840,48 @@ program
450
840
  await storage.close();
451
841
  }
452
842
  });
843
+ program
844
+ .command("code-signal")
845
+ .description("Emit a PATH-(b) code signal for a commit (deterministic pass/fail exemplar). Best-effort: never blocks a commit.")
846
+ .requiredOption("--repo-path <dir>", "path to the git repo whose commit to capture")
847
+ .requiredOption("--sha <sha>", "commit sha to extract the diff from")
848
+ .requiredOption("--test-exit <n>", "test gate exit code (0 -> pass, non-zero -> fail)", (v) => Number.parseInt(v, 10))
849
+ .option("--task <s>", "task description (defaults to the changed funcname/file)")
850
+ .option("--model <s>", "model that produced the change")
851
+ .option("--repo <logical>", "logical repo name (defaults to the repo-path basename)")
852
+ .option("--dry-run", "print the payload and do not POST")
853
+ .action(async (opts) => {
854
+ const { buildCodeSignalPayload, formatCodeSignalResult } = await import("../core/signals/code-signal.js");
855
+ const payload = buildCodeSignalPayload({
856
+ repoPath: opts.repoPath,
857
+ sha: opts.sha,
858
+ testExit: opts.testExit,
859
+ ...(opts.task ? { task: opts.task } : {}),
860
+ ...(opts.model ? { model: opts.model } : {}),
861
+ ...(opts.repo ? { repo: opts.repo } : {}),
862
+ });
863
+ if (opts.dryRun) {
864
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
865
+ return;
866
+ }
867
+ const url = `http://localhost:${port()}/api/signal`;
868
+ try {
869
+ const res = await fetch(url, {
870
+ method: "POST",
871
+ headers: { "content-type": "application/json" },
872
+ body: JSON.stringify(payload),
873
+ });
874
+ if (res.status !== 202) {
875
+ process.stderr.write(`code-signal: daemon returned ${res.status} (not 202); skipping\n`);
876
+ return;
877
+ }
878
+ const accepted = (await res.json());
879
+ process.stdout.write(formatCodeSignalResult(payload.outcome, accepted.id ?? "unknown") + "\n");
880
+ }
881
+ catch {
882
+ process.stderr.write(`code-signal: daemon unreachable at ${url}; skipping\n`);
883
+ }
884
+ });
453
885
  program
454
886
  .command("misses")
455
887
  .description("Show sessions the agent explicitly fetched but the hook never surfaced (recall miss log)")
@@ -532,6 +964,43 @@ program
532
964
  }
533
965
  }
534
966
  });
967
+ program
968
+ .command("metrics")
969
+ .description("Read-only retrieval-quality metrics")
970
+ .argument("<name>", "metric name: re-derivation")
971
+ .option("--window <days>", "lookback window in days", (v) => Number.parseInt(v, 10), 90)
972
+ .option("--json", "emit JSON instead of human-readable output")
973
+ .action(async (name, opts) => {
974
+ if (name !== "re-derivation") {
975
+ console.error(`unknown metric '${name}' (available: re-derivation)`);
976
+ process.exitCode = 1;
977
+ return;
978
+ }
979
+ const { computeReDerivationRate, sqliteReDerivationDeps } = await import("../core/metrics/re-derivation.js");
980
+ const { storage, store } = await buildStack();
981
+ try {
982
+ if (typeof store.rawDb !== "function") {
983
+ console.error("re-derivation metric requires the SQLite backend");
984
+ process.exitCode = 1;
985
+ return;
986
+ }
987
+ const deps = sqliteReDerivationDeps(store.rawDb());
988
+ const report = await computeReDerivationRate(deps, opts.window);
989
+ if (opts.json) {
990
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
991
+ return;
992
+ }
993
+ const pct = (report.rate * 100).toFixed(1);
994
+ console.log(`re_derivation_rate (last ${opts.window} day(s)): ${pct}% ` +
995
+ `(${report.pairs.length} re-derived pair${report.pairs.length === 1 ? "" : "s"})`);
996
+ for (const p of report.pairs) {
997
+ console.log(` ${p.a} <-> ${p.b} jaccard=${p.jaccard} shared=${p.sharedEntities.join(", ")}`);
998
+ }
999
+ }
1000
+ finally {
1001
+ await storage.close();
1002
+ }
1003
+ });
535
1004
  program
536
1005
  .command("supersede")
537
1006
  .description("Retroactively mark a session as superseded by a newer one")
@@ -572,12 +1041,39 @@ program
572
1041
  .description("Re-embed every session into session_embedding_chunks (chunk + max-pool)")
573
1042
  .option("-l, --limit <n>", "session cap (default: all)", (v) => Number.parseInt(v, 10))
574
1043
  .option("--state <path>", "resume state file (default ~/.nlm/embed_reembed.state)")
1044
+ .option("--exemplars", "instead: embed code_exemplars rows missing a vector (repairs dropped capture embeds)")
1045
+ .option("--dry-run", "report dim mismatch and what would be dropped without writing")
575
1046
  .option("-v, --verbose", "per-session progress on stderr")
576
1047
  .action(async (opts) => {
577
- const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
578
- const report = await reembedCorpus({
579
- dbPath: dbPath(),
1048
+ if (opts.exemplars) {
1049
+ const { storage } = await buildStack();
1050
+ try {
1051
+ const report = await backfillExemplarEmbeddings({
1052
+ dbPath: dbPath(),
1053
+ embedder: buildCodeEmbedder(),
1054
+ store: storage.exemplars,
1055
+ ...(opts.limit ? { limit: opts.limit } : {}),
1056
+ ...(opts.verbose
1057
+ ? {
1058
+ onProgress: (i, n, id, status) => {
1059
+ process.stderr.write(` [${i}/${n}] ${id} ${status}\n`);
1060
+ },
1061
+ }
1062
+ : {}),
1063
+ });
1064
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
1065
+ }
1066
+ finally {
1067
+ await storage.close();
1068
+ }
1069
+ return;
1070
+ }
1071
+ const embedder = buildEmbedder();
1072
+ const pgUrl = process.env["NLM_PG_URL"];
1073
+ const sharedOpts = {
580
1074
  embedder,
1075
+ embedderProvider: resolveEmbedderInfo(process.env).provider,
1076
+ ...(opts.dryRun ? { dryRun: true } : {}),
581
1077
  ...(opts.state ? { statePath: opts.state } : {}),
582
1078
  ...(opts.limit ? { limit: opts.limit } : {}),
583
1079
  ...(opts.verbose
@@ -587,7 +1083,10 @@ program
587
1083
  },
588
1084
  }
589
1085
  : {}),
590
- });
1086
+ };
1087
+ const report = pgUrl
1088
+ ? await reembedCorpusPg({ pgUrl, ...sharedOpts })
1089
+ : await reembedCorpus({ dbPath: dbPath(), ...sharedOpts });
591
1090
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
592
1091
  });
593
1092
  program
@@ -649,6 +1148,7 @@ program
649
1148
  factStore: stack.storage.facts,
650
1149
  embedder: stack.embedder,
651
1150
  classifier: stack.classifier,
1151
+ classifierDescriptor: { provider: stack.classifier.provider, model: stack.classifier.model },
652
1152
  adapters,
653
1153
  }, { ...(opts.limit ? { limit: opts.limit } : {}), dryRun: Boolean(opts.dryRun) });
654
1154
  console.log(`reclassify-oversized: attempted=${r.attempted} ingested=${r.ingested} ` +
@@ -660,33 +1160,108 @@ program
660
1160
  }
661
1161
  });
662
1162
  program
663
- .command("embed-normalize")
664
- .description("L2-normalize every row in session_embeddings (idempotent)")
665
- .option("--dim <n>", "vector dimension (default 768)", (v) => Number.parseInt(v, 10), 768)
666
- .option("--batch <n>", "rows per commit batch (default 100)", (v) => Number.parseInt(v, 10), 100)
667
- .option("--dry-run", "report what would change without writing")
668
- .action((opts) => {
669
- const report = normalizeEmbeddings({
670
- dbPath: dbPath(),
671
- dim: opts.dim,
672
- batchSize: opts.batch,
673
- dryRun: Boolean(opts.dryRun),
674
- });
675
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
1163
+ .command("reprocess")
1164
+ .description("Retroactive: re-classify sessions that lack classification or were classified by a weaker lane.\n" +
1165
+ "Workstream binding (workstream_id) is NOT changed by this command.\n" +
1166
+ "Below-floor results (<0.4 confidence) update the session row but preserve its prior facts.")
1167
+ .option("-l, --limit <n>", "max sessions to process this run", (v) => Number.parseInt(v, 10))
1168
+ .option("--min-confidence <f>", "also reprocess same-model sessions below this confidence", (v) => Number.parseFloat(v))
1169
+ .option("--only-null", "select only sessions with no classifier stamp yet (safe lane-switch resume: sessions already stamped by a stronger model are left untouched)")
1170
+ .option("--exclude-model <tag>", "exclude sessions stamped by this model tag (repeatable; use once per model to preserve corpus quality when switching classifier lanes)", (v, prev) => [...prev, v], [])
1171
+ .option("--state <path>", "resume state file (default ~/.nlm/reprocess.state)")
1172
+ .option("--dry-run", "print cohort report without writing")
1173
+ .option("-v, --verbose", "per-session progress on stderr")
1174
+ .option("--force-embed", "proceed even when the stored prose embedding lane mismatches the runtime embedder")
1175
+ .action(async (opts) => {
1176
+ const { reprocess: runReprocess } = await import("../core/ingest/reprocess.js");
1177
+ const stack = await buildStack();
1178
+ try {
1179
+ if (!(stack.storage instanceof SqliteStorage)) {
1180
+ console.error("reprocess: only supported with SQLite storage (NLM_PG_URL must not be set)");
1181
+ await stack.storage.close();
1182
+ process.exit(1);
1183
+ }
1184
+ const embedderInfo = resolveEmbedderInfo();
1185
+ const report = await runReprocess({
1186
+ db: stack.storage.rawDb(),
1187
+ store: stack.storage.sessions,
1188
+ factStore: stack.storage.facts,
1189
+ embedder: stack.embedder,
1190
+ classifier: stack.classifier,
1191
+ classifierDescriptor: { provider: stack.classifier.provider, model: stack.classifier.model },
1192
+ embeddingConfig: stack.storage.embeddingConfig,
1193
+ embedderDescriptor: { provider: embedderInfo.provider, model: embedderInfo.model },
1194
+ }, {
1195
+ ...(opts.limit ? { limit: opts.limit } : {}),
1196
+ ...(opts.minConfidence !== undefined ? { minConfidence: opts.minConfidence } : {}),
1197
+ ...(opts.onlyNull ? { onlyNull: true } : {}),
1198
+ ...(opts.excludeModel && opts.excludeModel.length > 0 ? { excludeModels: opts.excludeModel } : {}),
1199
+ ...(opts.state ? { statePath: opts.state } : {}),
1200
+ dryRun: Boolean(opts.dryRun),
1201
+ verbose: Boolean(opts.verbose),
1202
+ forceEmbed: Boolean(opts.forceEmbed),
1203
+ ...(opts.verbose
1204
+ ? {
1205
+ onProgress: (i, n, sid, status) => {
1206
+ process.stderr.write(` [${i}/${n}] ${sid} ${status}\n`);
1207
+ },
1208
+ }
1209
+ : {}),
1210
+ });
1211
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
1212
+ }
1213
+ finally {
1214
+ await stack.storage.close();
1215
+ }
1216
+ });
1217
+ program
1218
+ .command("backfill-derivables")
1219
+ .description("Retroactive: stamp agent_persona + parent_session_id (#352 phase 2) onto sessions ingested\n" +
1220
+ "before the scheduler started stamping them at classify time. Idempotent (WHERE agent_persona IS NULL).")
1221
+ .option("--dry-run", "report counts without writing")
1222
+ .option("--with-transcript-scan", "also scan each candidate's transcript for primary_model/total_tokens/skill (slower)")
1223
+ .action(async (opts) => {
1224
+ const { backfillDerivables } = await import("../core/ingest/backfill-derivables.js");
1225
+ const stack = await buildStack();
1226
+ try {
1227
+ if (!(stack.storage instanceof SqliteStorage)) {
1228
+ console.error("backfill-derivables: only supported with SQLite storage (NLM_PG_URL must not be set)");
1229
+ await stack.storage.close();
1230
+ process.exit(1);
1231
+ }
1232
+ const report = await backfillDerivables(stack.storage.rawDb(), {
1233
+ dryRun: Boolean(opts.dryRun),
1234
+ withTranscriptScan: Boolean(opts.withTranscriptScan),
1235
+ });
1236
+ console.log(`backfill-derivables${opts.dryRun ? " (dry-run)" : ""}: ` +
1237
+ `updated=${report.updated} skipped(already-stamped)=${report.skippedAlreadyStamped} ` +
1238
+ `skipped(no-op)=${report.skippedNoop} subagent-candidates=${report.subagentCandidates} ` +
1239
+ `unknown-parent=${report.unknownParent} transcript-scanned=${report.transcriptScanned} total=${report.total}`);
1240
+ }
1241
+ finally {
1242
+ await stack.storage.close();
1243
+ }
676
1244
  });
677
1245
  program
678
1246
  .command("mcp")
679
1247
  .description("Run as an MCP stdio server (for ~/.mcp.json)")
680
1248
  .action(async () => {
681
- const { recall, store, facts, factRecall, storage, scope } = await buildStack();
1249
+ const { recall, store, facts, factRecall, storage, scope, signals } = await buildStack();
682
1250
  const server = createMcpServer({
683
1251
  recall,
684
1252
  store,
685
1253
  factStore: facts,
686
1254
  factRecall,
687
1255
  exemplarStore: storage.exemplars,
688
- codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
1256
+ codeEmbedder: buildCodeEmbedder(),
689
1257
  installScope: scope,
1258
+ signalStore: signals,
1259
+ sessionScopeReader: store,
1260
+ workDigest: { store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() },
1261
+ workstreams: { store: storage.workstreams, sessions: store, facts: facts, exemplars: storage.exemplars },
1262
+ // Tier-B outcome rollup (#352 phase 2) only has a SQLite adapter today;
1263
+ // PgStorage deployments skip get_session's `outcome` field until parity lands.
1264
+ ...(storage instanceof SqliteStorage ? { outcomeDb: storage.rawDb() } : {}),
690
1265
  });
691
1266
  const transport = new StdioServerTransport();
692
1267
  await server.connect(transport);
@@ -1074,14 +1649,12 @@ const SESSION_START_HOOK_JS = resolve(__dirname, "../hook/session-start-hook.js"
1074
1649
  const SESSION_END_HOOK_JS = resolve(__dirname, "../hook/session-end-hook.js");
1075
1650
  const STOP_HOOK_JS = resolve(__dirname, "../hook/stop-hook.js");
1076
1651
  const PRE_COMPACT_HOOK_JS = resolve(__dirname, "../hook/pre-compact-hook.js");
1077
- const SUBAGENT_START_HOOK_JS = resolve(__dirname, "../hook/subagent-start-hook.js");
1078
1652
  const ALL_HOOKS = [
1079
1653
  { event: "UserPromptSubmit", script: HOOK_JS, label: "recall" },
1080
1654
  { event: "SessionStart", script: SESSION_START_HOOK_JS, label: "session-start" },
1081
1655
  { event: "SessionEnd", script: SESSION_END_HOOK_JS, label: "session-end" },
1082
1656
  { event: "Stop", script: STOP_HOOK_JS, label: "stop" },
1083
1657
  { event: "PreCompact", script: PRE_COMPACT_HOOK_JS, label: "pre-compact" },
1084
- { event: "SubagentStart", script: SUBAGENT_START_HOOK_JS, label: "subagent-start" },
1085
1658
  ];
1086
1659
  function claudeSettingsPath() {
1087
1660
  return process.env["NLM_CLAUDE_SETTINGS"] ?? join(homedir(), ".claude", "settings.json");
@@ -1091,7 +1664,7 @@ const hook = program
1091
1664
  .description("Manage the Claude Code NLM hooks");
1092
1665
  hook
1093
1666
  .command("install")
1094
- .description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (live mode)")
1667
+ .description("Add the NLM hooks (recall + session-start + session-end + stop + pre-compact) to ~/.claude/settings.json (live mode)")
1095
1668
  .action(() => {
1096
1669
  const path = claudeSettingsPath();
1097
1670
  const installed = [];
@@ -1597,7 +2170,7 @@ program
1597
2170
  program
1598
2171
  .command("digest")
1599
2172
  .description("Compose a daily-activity digest from the running daemon (optionally post to Telegram)")
1600
- .option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ?? "3940", 10))
2173
+ .option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ?? DEFAULT_NLM_PORT, 10))
1601
2174
  .option("--telegram", "post to Telegram instead of printing to stdout (requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID)")
1602
2175
  .action(async (opts) => {
1603
2176
  autoloadEnv();
@@ -1615,6 +2188,21 @@ program
1615
2188
  process.exit(1);
1616
2189
  }
1617
2190
  });
2191
+ program
2192
+ .command("work-digest")
2193
+ .description("Print the operator's agent-assisted work recap for a day")
2194
+ .option("-d, --date <date>", "day to summarize, YYYY-MM-DD (default: today)")
2195
+ .action(async (opts) => {
2196
+ const date = resolveDigestDate(opts.date);
2197
+ const { storage, store } = await buildStack();
2198
+ try {
2199
+ const digest = await buildWorkDigest({ store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() }, date);
2200
+ console.log(composeWorkDigest(digest));
2201
+ }
2202
+ finally {
2203
+ await storage.close();
2204
+ }
2205
+ });
1618
2206
  async function gatherInstallProbe() {
1619
2207
  autoloadEnv();
1620
2208
  let reachable = false;
@@ -1678,7 +2266,7 @@ async function gatherInstallProbe() {
1678
2266
  program
1679
2267
  .command("doctor")
1680
2268
  .description("Check database integrity invariants and install/runtime health, optionally repair safe violations")
1681
- .option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2)")
2269
+ .option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2), delete ghost fact embeddings (I7)")
1682
2270
  .action(async (opts) => {
1683
2271
  const storage = await buildStorage(dbPath());
1684
2272
  let violations;
@@ -1697,7 +2285,10 @@ program
1697
2285
  if (fixReport.restoredToClosed > 0) {
1698
2286
  console.log(` fixed I2: restored ${fixReport.restoredToClosed} session(s) to closed`);
1699
2287
  }
1700
- if (fixReport.deletedSelfLoops === 0 && fixReport.restoredToClosed === 0) {
2288
+ if (fixReport.deletedGhostEmbeddings > 0) {
2289
+ console.log(` fixed I7: deleted ${fixReport.deletedGhostEmbeddings} ghost embedding(s)`);
2290
+ }
2291
+ if (fixReport.deletedSelfLoops === 0 && fixReport.restoredToClosed === 0 && fixReport.deletedGhostEmbeddings === 0) {
1701
2292
  console.log(" --fix: nothing to repair");
1702
2293
  }
1703
2294
  }
@@ -1711,7 +2302,7 @@ program
1711
2302
  finally {
1712
2303
  await storage.close();
1713
2304
  }
1714
- const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6"];
2305
+ const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6", "I7"];
1715
2306
  const byId = new Map(violations.map((v) => [v.id, v]));
1716
2307
  let anyFail = false;
1717
2308
  console.log("Database integrity:");
@@ -1726,6 +2317,12 @@ program
1726
2317
  console.log(`PASS ${id}`);
1727
2318
  }
1728
2319
  }
2320
+ // I5a-mv is informational: multi-valued predicate groups legitimately exempt
2321
+ // from I5a. Not a violation; printed separately so the count is visible.
2322
+ const i5aMv = byId.get("I5a-mv");
2323
+ if (i5aMv) {
2324
+ console.log(`INFO I5a-mv count=${i5aMv.count} ${i5aMv.description}`);
2325
+ }
1729
2326
  console.log("\nInstall & runtime health:");
1730
2327
  if (printHealthChecks(evaluateInstallHealth(await gatherInstallProbe())))
1731
2328
  anyFail = true;
@@ -1746,9 +2343,11 @@ function printHealthChecks(checks) {
1746
2343
  async function dbIntegrityCheck() {
1747
2344
  const storage = await buildStorage(dbPath());
1748
2345
  try {
1749
- const violations = storage instanceof PgStorage
2346
+ const all = storage instanceof PgStorage
1750
2347
  ? await runChecksOnPg(storage.pgPool())
1751
2348
  : runChecksOnSqlite(storage.rawDb());
2349
+ // I5a-mv is informational (exempted multi-valued groups), not a true violation.
2350
+ const violations = all.filter((v) => v.id !== "I5a-mv");
1752
2351
  if (violations.length === 0)
1753
2352
  return { id: "db-integrity", status: "ok", detail: "all invariants hold" };
1754
2353
  return {
@@ -1800,6 +2399,35 @@ program
1800
2399
  if (anyFail)
1801
2400
  process.exit(1);
1802
2401
  });
2402
+ program
2403
+ .command("health")
2404
+ .description("Print corpus monitoring snapshot from the running daemon (null before first 24h run)")
2405
+ .action(async () => {
2406
+ let corpus = null;
2407
+ try {
2408
+ const ctrl = new AbortController();
2409
+ const timer = setTimeout(() => ctrl.abort(), 1_500);
2410
+ const res = await fetch(`http://localhost:${port()}/api/health`, { signal: ctrl.signal });
2411
+ clearTimeout(timer);
2412
+ if (res.ok) {
2413
+ const body = (await res.json());
2414
+ corpus = body.corpus ?? null;
2415
+ }
2416
+ }
2417
+ catch {
2418
+ // daemon not reachable
2419
+ }
2420
+ if (corpus) {
2421
+ console.log(`corpus.state: ${corpus.state}`);
2422
+ console.log(`corpus.dbBytes: ${corpus.dbBytes}`);
2423
+ console.log(`corpus.sessions: ${corpus.sessions}`);
2424
+ console.log(`corpus.entities: ${corpus.entities}`);
2425
+ console.log(`corpus.lastComputedAt: ${corpus.lastComputedAt}`);
2426
+ }
2427
+ else {
2428
+ console.log("corpus: (not yet available; daemon runs the first check ~60s after start)");
2429
+ }
2430
+ });
1803
2431
  program
1804
2432
  .command("backup")
1805
2433
  .description("Write a dated snapshot to ~/.nlm/backups/ and prune ones past the retention window")
@@ -1862,8 +2490,191 @@ program
1862
2490
  console.log(`Staged ${opts.from} (${validation.sessions} sessions, schema v${validation.schemaVersion}).`);
1863
2491
  console.log("Run `nlm restart` to apply — the current DB is archived aside, not deleted.");
1864
2492
  });
1865
- program.parseAsync().catch((e) => {
1866
- console.error("nlm: fatal", e);
1867
- process.exit(1);
2493
+ program
2494
+ .command("init")
2495
+ .description("Print (or write) the agent recall contract snippet")
2496
+ .requiredOption("--agent <name>", "agent variant: claude-code or generic")
2497
+ .option("--write <path>", "append the contract to this file instead of printing to stdout")
2498
+ .option("--force", "when --write: replace the existing block instead of refusing")
2499
+ .action((opts) => {
2500
+ runInitCommand({
2501
+ agent: opts.agent,
2502
+ write: opts.write,
2503
+ force: Boolean(opts.force),
2504
+ stdout: (s) => { process.stdout.write(s); },
2505
+ stderr: (s) => { process.stderr.write(s); },
2506
+ });
1868
2507
  });
2508
+ const entitiesCmd = program.command("entities").description("Entity table operations");
2509
+ entitiesCmd
2510
+ .command("dedup")
2511
+ .description("Suggest (or apply) conservative entity merge pairs. Default: dry run.")
2512
+ .option("--apply-safe", "execute safe-class merges via the merge primitive", false)
2513
+ .option("--interactive", "step through likely-class pairs with y/n on stdin", false)
2514
+ .action(async (opts) => {
2515
+ const storage = await buildStorage(dbPath());
2516
+ let rows;
2517
+ try {
2518
+ if (storage instanceof SqliteStorage) {
2519
+ rows = storage
2520
+ .rawDb()
2521
+ .prepare("SELECT canonical, session_count, status FROM entities ORDER BY canonical")
2522
+ .all();
2523
+ }
2524
+ else {
2525
+ const res = await storage
2526
+ .pgPool()
2527
+ .query("SELECT canonical, session_count, status FROM entities ORDER BY canonical");
2528
+ rows = res.rows;
2529
+ }
2530
+ }
2531
+ catch (err) {
2532
+ await storage.close();
2533
+ console.error("nlm entities dedup: failed to enumerate entities:", err);
2534
+ process.exit(1);
2535
+ }
2536
+ const inputs = rows.map((r) => ({
2537
+ canonical: r.canonical,
2538
+ sessionCount: r.session_count,
2539
+ status: r.status,
2540
+ }));
2541
+ const suggestions = suggestMerges(inputs);
2542
+ const safe = suggestions.filter((s) => s.cls === "safe");
2543
+ const likely = suggestions.filter((s) => s.cls === "likely");
2544
+ let merged = 0;
2545
+ let failed = 0;
2546
+ let skipped = 0;
2547
+ if (!opts.applySafe && !opts.interactive) {
2548
+ console.log(`safe (${safe.length}):`);
2549
+ for (const s of safe)
2550
+ console.log(` ${s.source} -> ${s.target}`);
2551
+ console.log(`likely (${likely.length}):`);
2552
+ for (const s of likely)
2553
+ console.log(` ${s.source} -> ${s.target}`);
2554
+ console.log(`\ntotal: safe=${safe.length} likely=${likely.length} (dry run)`);
2555
+ await storage.close();
2556
+ return;
2557
+ }
2558
+ if (opts.applySafe) {
2559
+ for (const s of safe) {
2560
+ try {
2561
+ await storage.entities.merge(s.source, s.target);
2562
+ console.log(`merged: ${s.source} -> ${s.target}`);
2563
+ merged++;
2564
+ }
2565
+ catch (err) {
2566
+ console.error(`failed: ${s.source} -> ${s.target}: ${err}`);
2567
+ failed++;
2568
+ }
2569
+ }
2570
+ }
2571
+ else {
2572
+ for (const s of safe) {
2573
+ console.log(`safe (skipped): ${s.source} -> ${s.target}`);
2574
+ skipped++;
2575
+ }
2576
+ }
2577
+ if (opts.interactive && likely.length > 0) {
2578
+ const { createInterface } = await import("node:readline");
2579
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2580
+ const ask = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
2581
+ for (const s of likely) {
2582
+ const answer = await ask(`merge ${s.source} -> ${s.target}? [y/N] `);
2583
+ if (answer.trim().toLowerCase() === "y") {
2584
+ try {
2585
+ await storage.entities.merge(s.source, s.target);
2586
+ console.log(`merged: ${s.source} -> ${s.target}`);
2587
+ merged++;
2588
+ }
2589
+ catch (err) {
2590
+ console.error(`failed: ${s.source} -> ${s.target}: ${err}`);
2591
+ failed++;
2592
+ }
2593
+ }
2594
+ else {
2595
+ skipped++;
2596
+ }
2597
+ }
2598
+ rl.close();
2599
+ }
2600
+ else {
2601
+ skipped += likely.length;
2602
+ }
2603
+ await storage.close();
2604
+ const remaining = suggestions.length - merged - failed;
2605
+ console.log(`\nsummary: merged=${merged} failed=${failed} suggested-remaining=${remaining}`);
2606
+ if (failed > 0)
2607
+ process.exit(1);
2608
+ });
2609
+ const scopeCmd = program
2610
+ .command("scope")
2611
+ .description("Scope management commands");
2612
+ scopeCmd
2613
+ .command("backfill")
2614
+ .description("Derive and stamp scope on legacy sessions/facts/exemplars/signals/workstreams from stored transcript evidence")
2615
+ .option("--apply", "write changes (default: dry run, no writes)")
2616
+ .option("--db <path>", "SQLite DB file to backfill (default: canonical NLM DB)")
2617
+ .option("--json", "emit machine-readable JSON summary instead of human-readable output")
2618
+ .action(async (opts) => {
2619
+ const targetPath = opts.db ?? dbPath();
2620
+ const storage = SqliteStorage.create({ dbPath: targetPath, migrationsDir: MIGRATIONS_DIR });
2621
+ const aliasMap = loadAliasMap();
2622
+ try {
2623
+ const result = await runScopeBackfill({
2624
+ db: storage.rawDb(),
2625
+ apply: Boolean(opts.apply),
2626
+ aliasMap,
2627
+ });
2628
+ if (opts.json) {
2629
+ const out = {
2630
+ dryRun: result.dryRun,
2631
+ sessions: { total: result.sessions.total, byScope: result.sessions.byScope },
2632
+ facts: { total: result.facts.total, byScope: result.facts.byScope },
2633
+ exemplars: { total: result.exemplars.total, byScope: result.exemplars.byScope },
2634
+ signals: { total: result.signals.total, byScope: result.signals.byScope },
2635
+ workstreams: { total: result.workstreams.total, byScope: result.workstreams.byScope },
2636
+ skipped: result.skipped,
2637
+ };
2638
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
2639
+ }
2640
+ else {
2641
+ formatBackfillResult(result, (s) => process.stdout.write(s));
2642
+ }
2643
+ }
2644
+ finally {
2645
+ await storage.close();
2646
+ }
2647
+ });
2648
+ scopeCmd
2649
+ .command("coverage")
2650
+ .description("Report per-table and recall-weighted scope coverage fractions")
2651
+ .option("--db <path>", "SQLite DB file (default: canonical NLM DB)")
2652
+ .option("--query-log <path>", "query log path for recall-weighted coverage")
2653
+ .option("--window <n>", "last N log entries for recall-weighted coverage (default 200)", (v) => Number.parseInt(v, 10), 200)
2654
+ .option("--json", "emit machine-readable JSON instead of human-readable output")
2655
+ .action(async (opts) => {
2656
+ const targetPath = opts.db ?? dbPath();
2657
+ const result = await runScopeCoverage({
2658
+ dbPath: targetPath,
2659
+ ...(opts.queryLog ? { queryLogPath: opts.queryLog } : {}),
2660
+ window: opts.window,
2661
+ });
2662
+ if (opts.json) {
2663
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2664
+ }
2665
+ else {
2666
+ formatCoverageResult(result, (s) => process.stdout.write(s));
2667
+ }
2668
+ });
2669
+ // Entrypoint guard: only parse argv when this file IS the executed script.
2670
+ // realpathSync resolves the npm bin symlink so the global `nlm` shim still
2671
+ // matches; without the guard, importing any helper from this module inside
2672
+ // vitest ran the CLI against the test runner's argv and exited the worker.
2673
+ const entryPath = process.argv[1];
2674
+ if (entryPath && import.meta.url === pathToFileURL(realpathSync(entryPath)).href) {
2675
+ program.parseAsync().catch((e) => {
2676
+ console.error("nlm: fatal", e);
2677
+ process.exit(1);
2678
+ });
2679
+ }
1869
2680
  //# sourceMappingURL=nlm.js.map