nlm-memory 0.19.0 → 0.20.1

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 (398) hide show
  1. package/README.md +17 -10
  2. package/dist/cli/digest.js +21 -0
  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 +766 -55
  9. package/dist/cli/nlm.js.map +1 -1
  10. package/dist/core/actions/overlay.d.ts +20 -6
  11. package/dist/core/actions/overlay.js +32 -18
  12. package/dist/core/actions/overlay.js.map +1 -1
  13. package/dist/core/adapters/claude-code.js +6 -0
  14. package/dist/core/adapters/claude-code.js.map +1 -1
  15. package/dist/core/classifier/hierarchical-classify.d.ts +6 -2
  16. package/dist/core/classifier/hierarchical-classify.js +10 -6
  17. package/dist/core/classifier/hierarchical-classify.js.map +1 -1
  18. package/dist/core/classifier/prompt.js +3 -3
  19. package/dist/core/digest/compose.d.ts +10 -0
  20. package/dist/core/digest/compose.js +7 -1
  21. package/dist/core/digest/compose.js.map +1 -1
  22. package/dist/core/embedding/embed-backfill.d.ts +13 -2
  23. package/dist/core/embedding/embed-backfill.js +181 -43
  24. package/dist/core/embedding/embed-backfill.js.map +1 -1
  25. package/dist/core/embedding/embedding-config.d.ts +17 -0
  26. package/dist/core/embedding/embedding-config.js +12 -0
  27. package/dist/core/embedding/embedding-config.js.map +1 -0
  28. package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
  29. package/dist/core/embedding/pg-embed-backfill.js +264 -0
  30. package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
  31. package/dist/core/entities/dedup-suggest.d.ts +25 -0
  32. package/dist/core/entities/dedup-suggest.js +107 -0
  33. package/dist/core/entities/dedup-suggest.js.map +1 -0
  34. package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
  35. package/dist/core/eval/classifier-fixture-eval.js +217 -0
  36. package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
  37. package/dist/core/eval/extraction-scoring.d.ts +63 -0
  38. package/dist/core/eval/extraction-scoring.js +79 -0
  39. package/dist/core/eval/extraction-scoring.js.map +1 -0
  40. package/dist/core/eval/run-eval.d.ts +40 -0
  41. package/dist/core/eval/run-eval.js +34 -0
  42. package/dist/core/eval/run-eval.js.map +1 -0
  43. package/dist/core/exemplars/capture-from-session.d.ts +1 -0
  44. package/dist/core/exemplars/capture-from-session.js +6 -2
  45. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  46. package/dist/core/exemplars/diff-parse.d.ts +23 -0
  47. package/dist/core/exemplars/diff-parse.js +73 -0
  48. package/dist/core/exemplars/diff-parse.js.map +1 -0
  49. package/dist/core/exemplars/embed-backfill.d.ts +36 -0
  50. package/dist/core/exemplars/embed-backfill.js +85 -0
  51. package/dist/core/exemplars/embed-backfill.js.map +1 -0
  52. package/dist/core/exemplars/embed-text.d.ts +15 -0
  53. package/dist/core/exemplars/embed-text.js +18 -0
  54. package/dist/core/exemplars/embed-text.js.map +1 -0
  55. package/dist/core/exemplars/extract-exemplar.js +2 -65
  56. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  57. package/dist/core/exemplars/recall-code.js +4 -0
  58. package/dist/core/exemplars/recall-code.js.map +1 -1
  59. package/dist/core/exemplars/warm-embedder.d.ts +14 -0
  60. package/dist/core/exemplars/warm-embedder.js +18 -0
  61. package/dist/core/exemplars/warm-embedder.js.map +1 -0
  62. package/dist/core/health/corpus-state.d.ts +8 -0
  63. package/dist/core/health/corpus-state.js +11 -0
  64. package/dist/core/health/corpus-state.js.map +1 -0
  65. package/dist/core/health/embed-inflight.d.ts +9 -0
  66. package/dist/core/health/embed-inflight.js +30 -0
  67. package/dist/core/health/embed-inflight.js.map +1 -0
  68. package/dist/core/health/embedding-lane-state.d.ts +6 -0
  69. package/dist/core/health/embedding-lane-state.js +18 -0
  70. package/dist/core/health/embedding-lane-state.js.map +1 -0
  71. package/dist/core/health/warmup-state.d.ts +12 -0
  72. package/dist/core/health/warmup-state.js +12 -0
  73. package/dist/core/health/warmup-state.js.map +1 -0
  74. package/dist/core/hook/gate.d.ts +13 -6
  75. package/dist/core/hook/gate.js +47 -6
  76. package/dist/core/hook/gate.js.map +1 -1
  77. package/dist/core/hook/hook-log.d.ts +5 -0
  78. package/dist/core/hook/hook-log.js.map +1 -1
  79. package/dist/core/hook/memo.d.ts +12 -0
  80. package/dist/core/hook/memo.js +41 -1
  81. package/dist/core/hook/memo.js.map +1 -1
  82. package/dist/core/hook/pointer-block.d.ts +1 -0
  83. package/dist/core/hook/pointer-block.js +23 -2
  84. package/dist/core/hook/pointer-block.js.map +1 -1
  85. package/dist/core/hook/query-extract.d.ts +2 -1
  86. package/dist/core/hook/query-extract.js +10 -1
  87. package/dist/core/hook/query-extract.js.map +1 -1
  88. package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
  89. package/dist/core/hook/resolve-conversation-by-query.js +98 -0
  90. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
  91. package/dist/core/hook/select.d.ts +8 -0
  92. package/dist/core/hook/select.js +9 -2
  93. package/dist/core/hook/select.js.map +1 -1
  94. package/dist/core/hook/transcript.js +16 -2
  95. package/dist/core/hook/transcript.js.map +1 -1
  96. package/dist/core/ingest/ingest-session.d.ts +5 -0
  97. package/dist/core/ingest/ingest-session.js +3 -0
  98. package/dist/core/ingest/ingest-session.js.map +1 -1
  99. package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
  100. package/dist/core/ingest/reclassify-oversized.js +3 -0
  101. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  102. package/dist/core/ingest/reprocess.d.ts +96 -0
  103. package/dist/core/ingest/reprocess.js +275 -0
  104. package/dist/core/ingest/reprocess.js.map +1 -0
  105. package/dist/core/integrity/check-invariants.d.ts +6 -1
  106. package/dist/core/integrity/check-invariants.js +209 -3
  107. package/dist/core/integrity/check-invariants.js.map +1 -1
  108. package/dist/core/metrics/corpus-stats.d.ts +49 -0
  109. package/dist/core/metrics/corpus-stats.js +111 -0
  110. package/dist/core/metrics/corpus-stats.js.map +1 -0
  111. package/dist/core/metrics/re-derivation.d.ts +48 -0
  112. package/dist/core/metrics/re-derivation.js +87 -0
  113. package/dist/core/metrics/re-derivation.js.map +1 -0
  114. package/dist/core/recall/citation-log.d.ts +8 -0
  115. package/dist/core/recall/citation-log.js +17 -0
  116. package/dist/core/recall/citation-log.js.map +1 -1
  117. package/dist/core/recall/query-intent.d.ts +7 -0
  118. package/dist/core/recall/query-intent.js +25 -0
  119. package/dist/core/recall/query-intent.js.map +1 -0
  120. package/dist/core/recall/query-log.d.ts +4 -0
  121. package/dist/core/recall/query-log.js +4 -0
  122. package/dist/core/recall/query-log.js.map +1 -1
  123. package/dist/core/recall/recall-service.d.ts +37 -1
  124. package/dist/core/recall/recall-service.js +106 -63
  125. package/dist/core/recall/recall-service.js.map +1 -1
  126. package/dist/core/recall/related-exemplars.d.ts +1 -0
  127. package/dist/core/recall/related-exemplars.js +4 -1
  128. package/dist/core/recall/related-exemplars.js.map +1 -1
  129. package/dist/core/recall/related-facts.js +2 -2
  130. package/dist/core/recall/related-facts.js.map +1 -1
  131. package/dist/core/recall/reranker.d.ts +2 -1
  132. package/dist/core/recall/reranker.js +3 -3
  133. package/dist/core/recall/reranker.js.map +1 -1
  134. package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
  135. package/dist/core/recall-facts/fact-query-log.js +2 -0
  136. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  137. package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
  138. package/dist/core/recall-facts/fact-recall-service.js +21 -8
  139. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  140. package/dist/core/scheduler/scan-once.d.ts +6 -6
  141. package/dist/core/scheduler/scan-once.js +13 -49
  142. package/dist/core/scheduler/scan-once.js.map +1 -1
  143. package/dist/core/scheduler/scheduler.d.ts +10 -0
  144. package/dist/core/scheduler/scheduler.js +80 -28
  145. package/dist/core/scheduler/scheduler.js.map +1 -1
  146. package/dist/core/signals/code-signal.d.ts +49 -0
  147. package/dist/core/signals/code-signal.js +53 -0
  148. package/dist/core/signals/code-signal.js.map +1 -0
  149. package/dist/core/storage/db-restore.js +3 -1
  150. package/dist/core/storage/db-restore.js.map +1 -1
  151. package/dist/core/storage/fact-batch.d.ts +9 -0
  152. package/dist/core/storage/fact-batch.js +14 -0
  153. package/dist/core/storage/fact-batch.js.map +1 -0
  154. package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
  155. package/dist/core/storage/pg-code-exemplar-store.js +7 -0
  156. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  157. package/dist/core/storage/pg-embedding-config.d.ts +29 -0
  158. package/dist/core/storage/pg-embedding-config.js +53 -0
  159. package/dist/core/storage/pg-embedding-config.js.map +1 -0
  160. package/dist/core/storage/pg-entity-store.d.ts +12 -0
  161. package/dist/core/storage/pg-entity-store.js +79 -0
  162. package/dist/core/storage/pg-entity-store.js.map +1 -0
  163. package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
  164. package/dist/core/storage/pg-fact-ingest.js +34 -0
  165. package/dist/core/storage/pg-fact-ingest.js.map +1 -0
  166. package/dist/core/storage/pg-fact-store.d.ts +3 -0
  167. package/dist/core/storage/pg-fact-store.js +26 -30
  168. package/dist/core/storage/pg-fact-store.js.map +1 -1
  169. package/dist/core/storage/pg-migrate.d.ts +18 -0
  170. package/dist/core/storage/pg-migrate.js +73 -0
  171. package/dist/core/storage/pg-migrate.js.map +1 -0
  172. package/dist/core/storage/pg-session-store.d.ts +9 -0
  173. package/dist/core/storage/pg-session-store.js +204 -76
  174. package/dist/core/storage/pg-session-store.js.map +1 -1
  175. package/dist/core/storage/pg-storage.d.ts +9 -7
  176. package/dist/core/storage/pg-storage.js +20 -50
  177. package/dist/core/storage/pg-storage.js.map +1 -1
  178. package/dist/core/storage/pg-workstream-store.d.ts +24 -0
  179. package/dist/core/storage/pg-workstream-store.js +96 -0
  180. package/dist/core/storage/pg-workstream-store.js.map +1 -0
  181. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
  182. package/dist/core/storage/sqlite-code-exemplar-store.js +11 -0
  183. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  184. package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
  185. package/dist/core/storage/sqlite-embedding-config.js +27 -0
  186. package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
  187. package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
  188. package/dist/core/storage/sqlite-entity-store.js +96 -0
  189. package/dist/core/storage/sqlite-entity-store.js.map +1 -0
  190. package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
  191. package/dist/core/storage/sqlite-fact-store.js +76 -25
  192. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  193. package/dist/core/storage/sqlite-session-store.d.ts +20 -5
  194. package/dist/core/storage/sqlite-session-store.js +183 -68
  195. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  196. package/dist/core/storage/sqlite-storage.d.ts +8 -8
  197. package/dist/core/storage/sqlite-storage.js +21 -32
  198. package/dist/core/storage/sqlite-storage.js.map +1 -1
  199. package/dist/core/storage/sqlite-workstream-store.d.ts +24 -0
  200. package/dist/core/storage/sqlite-workstream-store.js +98 -0
  201. package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
  202. package/dist/core/util/with-timeout.d.ts +5 -0
  203. package/dist/core/util/with-timeout.js +20 -0
  204. package/dist/core/util/with-timeout.js.map +1 -0
  205. package/dist/core/work-digest/active-spans.d.ts +8 -0
  206. package/dist/core/work-digest/active-spans.js +29 -0
  207. package/dist/core/work-digest/active-spans.js.map +1 -0
  208. package/dist/core/work-digest/attribute.d.ts +15 -0
  209. package/dist/core/work-digest/attribute.js +76 -0
  210. package/dist/core/work-digest/attribute.js.map +1 -0
  211. package/dist/core/work-digest/build-work-digest.d.ts +12 -0
  212. package/dist/core/work-digest/build-work-digest.js +69 -0
  213. package/dist/core/work-digest/build-work-digest.js.map +1 -0
  214. package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
  215. package/dist/core/work-digest/compose-work-digest.js +52 -0
  216. package/dist/core/work-digest/compose-work-digest.js.map +1 -0
  217. package/dist/core/work-digest/merge-active.d.ts +10 -0
  218. package/dist/core/work-digest/merge-active.js +22 -0
  219. package/dist/core/work-digest/merge-active.js.map +1 -0
  220. package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
  221. package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
  222. package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
  223. package/dist/core/work-digest/topics.d.ts +18 -0
  224. package/dist/core/work-digest/topics.js +26 -0
  225. package/dist/core/work-digest/topics.js.map +1 -0
  226. package/dist/core/work-digest/types.d.ts +45 -0
  227. package/dist/core/work-digest/types.js +2 -0
  228. package/dist/core/work-digest/types.js.map +1 -0
  229. package/dist/core/workstream/bind.d.ts +25 -0
  230. package/dist/core/workstream/bind.js +23 -0
  231. package/dist/core/workstream/bind.js.map +1 -0
  232. package/dist/core/workstream/compose-recall.d.ts +2 -0
  233. package/dist/core/workstream/compose-recall.js +39 -0
  234. package/dist/core/workstream/compose-recall.js.map +1 -0
  235. package/dist/core/workstream/merge-suggest.d.ts +19 -0
  236. package/dist/core/workstream/merge-suggest.js +61 -0
  237. package/dist/core/workstream/merge-suggest.js.map +1 -0
  238. package/dist/core/workstream/model.d.ts +20 -0
  239. package/dist/core/workstream/model.js +9 -0
  240. package/dist/core/workstream/model.js.map +1 -0
  241. package/dist/core/workstream/name-match.d.ts +8 -0
  242. package/dist/core/workstream/name-match.js +18 -0
  243. package/dist/core/workstream/name-match.js.map +1 -0
  244. package/dist/core/workstream/resolve.d.ts +10 -0
  245. package/dist/core/workstream/resolve.js +21 -0
  246. package/dist/core/workstream/resolve.js.map +1 -0
  247. package/dist/core/workstream/rollup.d.ts +12 -0
  248. package/dist/core/workstream/rollup.js +17 -0
  249. package/dist/core/workstream/rollup.js.map +1 -0
  250. package/dist/core/workstream/work-topics.d.ts +10 -0
  251. package/dist/core/workstream/work-topics.js +55 -0
  252. package/dist/core/workstream/work-topics.js.map +1 -0
  253. package/dist/hook/hook-helpers.d.ts +4 -0
  254. package/dist/hook/hook-helpers.js +36 -0
  255. package/dist/hook/hook-helpers.js.map +1 -0
  256. package/dist/hook/pre-compact-hook.d.ts +23 -0
  257. package/dist/hook/pre-compact-hook.js +66 -0
  258. package/dist/hook/pre-compact-hook.js.map +1 -0
  259. package/dist/hook/prompt-recall-hook.d.ts +37 -0
  260. package/dist/hook/prompt-recall-hook.js +120 -20
  261. package/dist/hook/prompt-recall-hook.js.map +1 -1
  262. package/dist/hook/recall-gate.d.ts +29 -0
  263. package/dist/hook/recall-gate.js +50 -0
  264. package/dist/hook/recall-gate.js.map +1 -0
  265. package/dist/hook/recall-over-http.d.ts +1 -1
  266. package/dist/hook/recall-over-http.js +11 -11
  267. package/dist/hook/recall-over-http.js.map +1 -1
  268. package/dist/hook/recent-context.d.ts +25 -0
  269. package/dist/hook/recent-context.js +104 -0
  270. package/dist/hook/recent-context.js.map +1 -0
  271. package/dist/hook/score-floor.d.ts +27 -0
  272. package/dist/hook/score-floor.js +42 -0
  273. package/dist/hook/score-floor.js.map +1 -0
  274. package/dist/hook/session-end-hook.js +8 -29
  275. package/dist/hook/session-end-hook.js.map +1 -1
  276. package/dist/hook/session-start-hook.js +24 -59
  277. package/dist/hook/session-start-hook.js.map +1 -1
  278. package/dist/hook/stop-hook.js +22 -59
  279. package/dist/hook/stop-hook.js.map +1 -1
  280. package/dist/hook/subagent-start-hook.d.ts +28 -0
  281. package/dist/hook/subagent-start-hook.js +81 -0
  282. package/dist/hook/subagent-start-hook.js.map +1 -0
  283. package/dist/http/app.js +28 -8
  284. package/dist/http/app.js.map +1 -1
  285. package/dist/install/setup.js +2 -0
  286. package/dist/install/setup.js.map +1 -1
  287. package/dist/llm/build-classifier.d.ts +22 -0
  288. package/dist/llm/build-classifier.js +42 -0
  289. package/dist/llm/build-classifier.js.map +1 -0
  290. package/dist/llm/build-embedder.d.ts +11 -0
  291. package/dist/llm/build-embedder.js +42 -0
  292. package/dist/llm/build-embedder.js.map +1 -0
  293. package/dist/llm/bundled-embedder-client.d.ts +37 -0
  294. package/dist/llm/bundled-embedder-client.js +90 -0
  295. package/dist/llm/bundled-embedder-client.js.map +1 -0
  296. package/dist/llm/classifier-box.d.ts +13 -5
  297. package/dist/llm/classifier-box.js +28 -6
  298. package/dist/llm/classifier-box.js.map +1 -1
  299. package/dist/llm/classifier-egress.d.ts +18 -0
  300. package/dist/llm/classifier-egress.js +59 -0
  301. package/dist/llm/classifier-egress.js.map +1 -0
  302. package/dist/llm/client-shared.d.ts +4 -0
  303. package/dist/llm/client-shared.js +39 -0
  304. package/dist/llm/client-shared.js.map +1 -0
  305. package/dist/llm/deepseek-client.d.ts +20 -0
  306. package/dist/llm/deepseek-client.js +59 -45
  307. package/dist/llm/deepseek-client.js.map +1 -1
  308. package/dist/llm/embedder-info.d.ts +15 -0
  309. package/dist/llm/embedder-info.js +27 -0
  310. package/dist/llm/embedder-info.js.map +1 -0
  311. package/dist/llm/model-quirks.d.ts +4 -0
  312. package/dist/llm/model-quirks.js +7 -0
  313. package/dist/llm/model-quirks.js.map +1 -0
  314. package/dist/llm/naming.d.ts +5 -0
  315. package/dist/llm/naming.js +20 -0
  316. package/dist/llm/naming.js.map +1 -0
  317. package/dist/llm/ollama-client.d.ts +7 -8
  318. package/dist/llm/ollama-client.js +59 -51
  319. package/dist/llm/ollama-client.js.map +1 -1
  320. package/dist/llm/ollama-code-embedder.d.ts +8 -3
  321. package/dist/llm/ollama-code-embedder.js +14 -24
  322. package/dist/llm/ollama-code-embedder.js.map +1 -1
  323. package/dist/llm/openai-code-embedder-client.d.ts +25 -0
  324. package/dist/llm/openai-code-embedder-client.js +52 -0
  325. package/dist/llm/openai-code-embedder-client.js.map +1 -0
  326. package/dist/llm/openai-embedder-client.d.ts +41 -0
  327. package/dist/llm/openai-embedder-client.js +83 -0
  328. package/dist/llm/openai-embedder-client.js.map +1 -0
  329. package/dist/mcp/server.d.ts +47 -2
  330. package/dist/mcp/server.js +303 -18
  331. package/dist/mcp/server.js.map +1 -1
  332. package/dist/ports/code-embedder.d.ts +6 -1
  333. package/dist/ports/code-exemplar-store.d.ts +5 -0
  334. package/dist/ports/entity-store.d.ts +35 -0
  335. package/dist/ports/entity-store.js +7 -0
  336. package/dist/ports/entity-store.js.map +1 -0
  337. package/dist/ports/fact-store.d.ts +0 -0
  338. package/dist/ports/fact-store.js.map +1 -1
  339. package/dist/ports/llm-client.d.ts +9 -1
  340. package/dist/ports/session-store.d.ts +12 -0
  341. package/dist/ports/storage.d.ts +7 -30
  342. package/dist/ports/storage.js +3 -7
  343. package/dist/ports/storage.js.map +1 -1
  344. package/dist/ports/transcript-adapter.d.ts +2 -0
  345. package/dist/ports/workstream-store.d.ts +21 -0
  346. package/dist/ports/workstream-store.js +2 -0
  347. package/dist/ports/workstream-store.js.map +1 -0
  348. package/dist/shared/types.d.ts +23 -2
  349. package/fixtures/classifier-gold/README.md +94 -0
  350. package/fixtures/classifier-gold/reference.json +318 -0
  351. package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
  352. package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
  353. package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
  354. package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
  355. package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
  356. package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
  357. package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
  358. package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
  359. package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
  360. package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
  361. package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
  362. package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
  363. package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
  364. package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
  365. package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
  366. package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
  367. package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
  368. package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
  369. package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
  370. package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
  371. package/migrations/000_initial_schema.sql +1 -1
  372. package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
  373. package/migrations/025_workstreams.sql +30 -0
  374. package/migrations/026_embedding_config.sql +12 -0
  375. package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
  376. package/migrations/028_classifier_provenance.sql +5 -0
  377. package/migrations/pg/001_initial.sql +34 -0
  378. package/migrations/pg/019_split_replaces.sql +16 -7
  379. package/migrations/pg/025_workstreams.sql +34 -0
  380. package/migrations/pg/026_embedding_config.sql +12 -0
  381. package/migrations/pg/028_classifier_provenance.sql +5 -0
  382. package/migrations/pg/029_entity_variants.sql +9 -0
  383. package/nlm/index.js +395 -37
  384. package/package.json +12 -2
  385. package/plugin/.codex-plugin/plugin.json +1 -1
  386. package/plugin/scripts/prompt-recall-hook.mjs +402 -41
  387. package/plugin/scripts/session-start-hook.mjs +286 -60
  388. package/plugin/scripts/stop-hook.mjs +85 -65
  389. package/plugin-hermes-agent/__init__.py +2 -1
  390. package/templates/agent-contract/README.md +24 -0
  391. package/templates/agent-contract/claude-code.md +40 -0
  392. package/templates/agent-contract/generic.md +38 -0
  393. package/dist/core/embedding/embed-normalize.d.ts +0 -28
  394. package/dist/core/embedding/embed-normalize.js +0 -98
  395. package/dist/core/embedding/embed-normalize.js.map +0 -1
  396. package/dist/core/storage/pg-tx-context.d.ts +0 -52
  397. package/dist/core/storage/pg-tx-context.js +0 -149
  398. package/dist/core/storage/pg-tx-context.js.map +0 -1
@@ -0,0 +1,275 @@
1
+ /**
2
+ * reprocess: retroactive re-classification of prior sessions under a
3
+ * stronger configured lane.
4
+ *
5
+ * Selection: sessions with a non-empty body whose classifier_model is NULL,
6
+ * differs from the current lane model, or (when --min-confidence is given)
7
+ * whose stored confidence falls below the threshold even for the same model.
8
+ *
9
+ * Per session: classifyAdaptive on the stored body, then a full insertSession
10
+ * upsert (refreshes chunks + facts). A below-floor classification updates the
11
+ * session row (label, summary, markers, provenance) but preserves prior facts
12
+ * and their embeddings unchanged. It is counted in belowFloorOverwrites.
13
+ * Workstream binding (workstream_id) is NOT touched by the upsert.
14
+ *
15
+ * Resumable via a JSON state file (default ~/.nlm/reprocess.state). A lane
16
+ * change (different provider or model) invalidates the done-set.
17
+ *
18
+ * SQLite only. Pg variant deferred; file task against nlm#TODO in report.
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { homedir } from "node:os";
23
+ import { classifyAdaptive } from "../classifier/hierarchical-classify.js";
24
+ import { extractFacts } from "../facts/extract-facts.js";
25
+ const DEFAULT_STATE_PATH = join(homedir(), ".nlm", "reprocess.state");
26
+ const SAVE_EVERY = 25;
27
+ const CONFIDENCE_FLOOR = 0.4;
28
+ function confidenceBand(conf) {
29
+ if (conf === null)
30
+ return "null";
31
+ if (conf < 0.4)
32
+ return "<0.4";
33
+ if (conf < 0.6)
34
+ return "0.4-0.6";
35
+ if (conf < 0.8)
36
+ return "0.6-0.8";
37
+ return ">=0.8";
38
+ }
39
+ function loadState(path, lane) {
40
+ if (!existsSync(path))
41
+ return new Set();
42
+ try {
43
+ const raw = JSON.parse(readFileSync(path, "utf8"));
44
+ if (raw.lane?.provider === lane.provider && raw.lane?.model === lane.model) {
45
+ return new Set(raw.done ?? []);
46
+ }
47
+ return new Set();
48
+ }
49
+ catch {
50
+ return new Set();
51
+ }
52
+ }
53
+ function saveState(path, done, lane) {
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ writeFileSync(path, JSON.stringify({ done: [...done].sort(), lane }));
56
+ }
57
+ export function selectReprocessCandidates(db, model, minConfidence, opts) {
58
+ const params = [];
59
+ let classifierClause;
60
+ if (opts?.onlyNull) {
61
+ classifierClause = "classifier_model IS NULL";
62
+ }
63
+ else if (minConfidence !== undefined) {
64
+ classifierClause = "(classifier_model IS NULL OR classifier_model != ? OR classifier_confidence < ?)";
65
+ params.push(model, minConfidence);
66
+ }
67
+ else {
68
+ classifierClause = "(classifier_model IS NULL OR classifier_model != ?)";
69
+ params.push(model);
70
+ }
71
+ const excludeModels = opts?.excludeModels ?? [];
72
+ let excludeClause = "";
73
+ if (excludeModels.length > 0) {
74
+ const placeholders = excludeModels.map(() => "?").join(", ");
75
+ excludeClause = ` AND (classifier_model IS NULL OR classifier_model NOT IN (${placeholders}))`;
76
+ params.push(...excludeModels);
77
+ }
78
+ const sql = "SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min, " +
79
+ "label, summary, status, transcript_kind, transcript_path, " +
80
+ "transcript_offset, transcript_length, " +
81
+ "classifier_provider, classifier_model, classifier_confidence " +
82
+ "FROM sessions " +
83
+ "WHERE body IS NOT NULL AND length(body) > 0 " +
84
+ "AND " +
85
+ classifierClause +
86
+ excludeClause +
87
+ " " +
88
+ "ORDER BY started_at DESC";
89
+ return db.prepare(sql).all(...params);
90
+ }
91
+ export async function reprocess(deps, opts = {}) {
92
+ const { db, store, factStore, embedder, classifier, classifierDescriptor } = deps;
93
+ const log = deps.log ?? ((m) => console.error(m));
94
+ const statePath = opts.statePath ?? DEFAULT_STATE_PATH;
95
+ const lane = {
96
+ provider: classifierDescriptor.provider,
97
+ model: classifierDescriptor.model,
98
+ };
99
+ const allCandidates = selectReprocessCandidates(db, classifierDescriptor.model, opts.minConfidence, {
100
+ ...(opts.onlyNull ? { onlyNull: true } : {}),
101
+ ...(opts.excludeModels ? { excludeModels: opts.excludeModels } : {}),
102
+ });
103
+ if (opts.dryRun) {
104
+ const groupMap = new Map();
105
+ for (const row of allCandidates) {
106
+ const key = `${row.classifier_provider ?? ""}\u0000${row.classifier_model ?? ""}\u0000${confidenceBand(row.classifier_confidence)}`;
107
+ const existing = groupMap.get(key);
108
+ if (existing) {
109
+ groupMap.set(key, { ...existing, count: existing.count + 1 });
110
+ }
111
+ else {
112
+ groupMap.set(key, {
113
+ classifierProvider: row.classifier_provider,
114
+ classifierModel: row.classifier_model,
115
+ confidenceBand: confidenceBand(row.classifier_confidence),
116
+ count: 1,
117
+ });
118
+ }
119
+ }
120
+ const cohort = [...groupMap.values()].sort((a, b) => b.count - a.count);
121
+ let stateNote;
122
+ if (existsSync(statePath)) {
123
+ const n = loadState(statePath, lane).size;
124
+ stateNote = `state file present, ${n} sessions already done, real run will skip them`;
125
+ }
126
+ return {
127
+ totalEligible: allCandidates.length,
128
+ processed: 0,
129
+ succeeded: 0,
130
+ failed: 0,
131
+ skippedAlreadyDone: 0,
132
+ limitSkipped: 0,
133
+ belowFloorOverwrites: 0,
134
+ meanConfidenceOld: null,
135
+ meanConfidenceNew: null,
136
+ cohort,
137
+ ...(stateNote !== undefined ? { stateNote } : {}),
138
+ };
139
+ }
140
+ if (deps.embeddingConfig && deps.embedderDescriptor) {
141
+ const stored = deps.embeddingConfig.getLane("prose");
142
+ if (stored) {
143
+ let dim = null;
144
+ try {
145
+ const result = await deps.embedder.embed("nlm probe", "query");
146
+ dim = result.vector.length;
147
+ }
148
+ catch {
149
+ // probe failure: pre-tracking tolerance, proceed
150
+ }
151
+ if (dim !== null) {
152
+ const mismatch = stored.provider !== deps.embedderDescriptor.provider ||
153
+ stored.model !== deps.embedderDescriptor.model ||
154
+ stored.dim !== dim;
155
+ if (mismatch && !opts.forceEmbed) {
156
+ const msg = `reprocess: prose embedding lane mismatch: stored ${stored.provider}/${stored.model}@${stored.dim}` +
157
+ ` vs runtime ${deps.embedderDescriptor.provider}/${deps.embedderDescriptor.model}@${dim}.` +
158
+ ` Re-embedding under a mismatched lane writes mixed vectors. Run nlm embed-backfill first,` +
159
+ ` or pass --force-embed to override.`;
160
+ log(msg);
161
+ throw new Error(msg);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ const bodyStmt = db.prepare("SELECT body FROM sessions WHERE id = ?");
167
+ const done = loadState(statePath, lane);
168
+ const skippedByState = allCandidates.filter((r) => done.has(r.id)).length;
169
+ const candidates = allCandidates.filter((r) => !done.has(r.id));
170
+ const work = opts.limit !== undefined ? candidates.slice(0, opts.limit) : candidates;
171
+ const total = work.length;
172
+ let processed = 0;
173
+ let succeeded = 0;
174
+ let failed = 0;
175
+ let belowFloorOverwrites = 0;
176
+ let sumConfidenceOld = 0;
177
+ let countOld = 0;
178
+ let sumConfidenceNew = 0;
179
+ let countNew = 0;
180
+ for (let i = 0; i < work.length; i++) {
181
+ const row = work[i];
182
+ const sid = row.id;
183
+ const idx = i + 1;
184
+ const bodyRow = bodyStmt.get(sid);
185
+ if (!bodyRow?.body) {
186
+ failed++;
187
+ processed++;
188
+ log(`[reprocess] empty body ${sid}: skipping`);
189
+ opts.onProgress?.(idx, total, sid, "empty_body");
190
+ continue;
191
+ }
192
+ const body = bodyRow.body;
193
+ let classification;
194
+ try {
195
+ classification = await classifyAdaptive(body, classifier, {
196
+ ...(opts.classifyTimeoutMs ? { perCallTimeoutMs: opts.classifyTimeoutMs } : {}),
197
+ });
198
+ }
199
+ catch (e) {
200
+ failed++;
201
+ processed++;
202
+ log(`[reprocess] classify failed ${sid}: ${e instanceof Error ? e.message : String(e)}`);
203
+ opts.onProgress?.(idx, total, sid, "classify_failed");
204
+ continue;
205
+ }
206
+ if (row.classifier_confidence !== null) {
207
+ sumConfidenceOld += row.classifier_confidence;
208
+ countOld++;
209
+ }
210
+ sumConfidenceNew += classification.confidence;
211
+ countNew++;
212
+ const belowFloor = classification.confidence < CONFIDENCE_FLOOR;
213
+ if (belowFloor) {
214
+ belowFloorOverwrites++;
215
+ if (opts.verbose) {
216
+ log(`[reprocess] below-floor ${sid}: old=${row.classifier_confidence ?? "null"} new=${classification.confidence}`);
217
+ }
218
+ }
219
+ else if (opts.verbose) {
220
+ log(`[reprocess] ${sid}: old=${row.classifier_confidence ?? "null"} new=${classification.confidence}`);
221
+ }
222
+ const record = {
223
+ id: sid,
224
+ runtime: row.runtime,
225
+ runtimeSessionId: row.runtime_session_id,
226
+ startedAt: row.started_at,
227
+ endedAt: row.ended_at,
228
+ durationMin: row.duration_min,
229
+ label: classification.label,
230
+ summary: classification.summary,
231
+ body,
232
+ status: row.status,
233
+ transcriptKind: row.transcript_kind,
234
+ transcriptPath: row.transcript_path,
235
+ transcriptOffset: row.transcript_offset,
236
+ transcriptLength: row.transcript_length,
237
+ entities: classification.entities,
238
+ decisions: classification.decisions,
239
+ openQuestions: classification.open,
240
+ classifier: { ...classifierDescriptor, confidence: classification.confidence },
241
+ };
242
+ try {
243
+ const factSinkArg = belowFloor
244
+ ? null
245
+ : { factStore, facts: extractFacts(classification, sid, row.started_at) };
246
+ await store.insertSession(record, embedder, null, factSinkArg);
247
+ done.add(sid);
248
+ succeeded++;
249
+ processed++;
250
+ opts.onProgress?.(idx, total, sid, "ok");
251
+ if (succeeded % SAVE_EVERY === 0)
252
+ saveState(statePath, done, lane);
253
+ }
254
+ catch (e) {
255
+ failed++;
256
+ processed++;
257
+ log(`[reprocess] ingest failed ${sid}: ${e instanceof Error ? e.message : String(e)}`);
258
+ opts.onProgress?.(idx, total, sid, "ingest_failed");
259
+ }
260
+ }
261
+ saveState(statePath, done, lane);
262
+ const limitSkipped = opts.limit !== undefined ? candidates.length - work.length : 0;
263
+ return {
264
+ totalEligible: allCandidates.length,
265
+ processed,
266
+ succeeded,
267
+ failed,
268
+ skippedAlreadyDone: skippedByState,
269
+ limitSkipped,
270
+ belowFloorOverwrites,
271
+ meanConfidenceOld: countOld > 0 ? sumConfidenceOld / countOld : null,
272
+ meanConfidenceNew: countNew > 0 ? sumConfidenceNew / countNew : null,
273
+ };
274
+ }
275
+ //# sourceMappingURL=reprocess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reprocess.js","sourceRoot":"","sources":["../../../src/core/ingest/reprocess.ts"],"names":[],"mappings":"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"}
@@ -13,8 +13,12 @@
13
13
  * 'replaces' edges (BFS, depth-capped at 100)
14
14
  * I4 every session_edges endpoint exists in sessions
15
15
  * I5 at most one active fact per (subject, predicate); every superseded_by
16
- * references an existing facts.id
16
+ * references an existing facts.id; superseded_by forms no cycle (I5c)
17
17
  * I6 every non-null adapter_state.session_id references a sessions.id
18
+ * I7 every fact_embeddings row has a live (active, non-retired) parent fact
19
+ * I7b chunk-level ghost: every session_embedding_chunks row has a map entry
20
+ * (I7b-1); every session_chunk_map row references an existing session
21
+ * (I7b-2)
18
22
  */
19
23
  import type Database from "better-sqlite3";
20
24
  import type { Pool } from "pg";
@@ -36,6 +40,7 @@ export declare function runCheapChecksOnPg(pool: Pool): Promise<ReadonlyArray<Vi
36
40
  export interface FixReport {
37
41
  readonly deletedSelfLoops: number;
38
42
  readonly restoredToClosed: number;
43
+ readonly deletedGhostEmbeddings: number;
39
44
  }
40
45
  export declare function applyFixOnSqlite(db: Database.Database): FixReport;
41
46
  export declare function applyFixOnPg(pool: Pool): Promise<FixReport>;
@@ -13,9 +13,29 @@
13
13
  * 'replaces' edges (BFS, depth-capped at 100)
14
14
  * I4 every session_edges endpoint exists in sessions
15
15
  * I5 at most one active fact per (subject, predicate); every superseded_by
16
- * references an existing facts.id
16
+ * references an existing facts.id; superseded_by forms no cycle (I5c)
17
17
  * I6 every non-null adapter_state.session_id references a sessions.id
18
+ * I7 every fact_embeddings row has a live (active, non-retired) parent fact
19
+ * I7b chunk-level ghost: every session_embedding_chunks row has a map entry
20
+ * (I7b-1); every session_chunk_map row references an existing session
21
+ * (I7b-2)
18
22
  */
23
+ // Multi-valued predicates legitimately hold parallel active facts (e.g.
24
+ // nlm-memory|stack = TypeScript AND SQLite, both simultaneously true).
25
+ // Force-superseding one copy would destroy a true fact, which is the
26
+ // #351-bug-3 trap. I5a skips subject|predicate groups whose predicate is
27
+ // in this set and instead reports them as an informational count under
28
+ // the id "I5a-mv". Entries MUST be members of PREDICATE_VOCABULARY
29
+ // (src/core/classifier/prompt.ts): predicates are coerced to that closed
30
+ // vocab before facts are written, so any other token here is inert. Add
31
+ // predicates only when parallelism is a domain invariant, not when the
32
+ // classifier merely failed to supersede (#358).
33
+ const MULTI_VALUED_PREDICATES = new Set([
34
+ "stack",
35
+ "integration",
36
+ "dependency",
37
+ ]);
38
+ const MV_IN = [...MULTI_VALUED_PREDICATES].map((p) => `'${p}'`).join(", ");
19
39
  // ─── SQL strings (portable) ──────────────────────────────────────────────────
20
40
  const SQL_I1 = `
21
41
  SELECT from_session AS bad_id
@@ -58,10 +78,23 @@ const SQL_I4_MISSING_TO = `
58
78
  // One row per conflicting (subject, predicate) group — the count is the number
59
79
  // of conflicts to resolve, not the number of facts involved. MIN(id) keeps the
60
80
  // projection aggregate-only so PostgreSQL's GROUP BY strictness accepts it.
81
+ // Multi-valued predicates are excluded here; they are counted by SQL_I5A_MULTI_VALUED.
61
82
  const SQL_I5_DUPLICATE_FACTS = `
62
83
  SELECT MIN(id) AS bad_id
63
84
  FROM facts
64
- WHERE superseded_by IS NULL
85
+ WHERE superseded_by IS NULL AND retired_at IS NULL
86
+ AND predicate NOT IN (${MV_IN})
87
+ GROUP BY subject, predicate
88
+ HAVING COUNT(*) > 1
89
+ LIMIT 6
90
+ `;
91
+ // One row per (subject, predicate) group in the multi-valued exemption set that
92
+ // holds more than one active fact. Count drives the informational I5a-mv report.
93
+ const SQL_I5A_MULTI_VALUED = `
94
+ SELECT MIN(id) AS bad_id
95
+ FROM facts
96
+ WHERE superseded_by IS NULL AND retired_at IS NULL
97
+ AND predicate IN (${MV_IN})
65
98
  GROUP BY subject, predicate
66
99
  HAVING COUNT(*) > 1
67
100
  LIMIT 6
@@ -80,6 +113,67 @@ const SQL_I6 = `
80
113
  AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.id = a.session_id)
81
114
  LIMIT 6
82
115
  `;
116
+ // An embedding row must have a live (active, non-retired) parent fact.
117
+ // Superseded/retired facts leave the ANN index at write time (ingest collapse,
118
+ // markSuperseded, retire, session cascade); a row here means a write path
119
+ // skipped cleanup; the NLM #351 orphan class.
120
+ //
121
+ // SQLite: fact_embeddings is a vec0 virtual table; its shadow rowids table
122
+ // (fact_embeddings_rowids, column: id) supports standard LEFT JOIN.
123
+ const SQL_I7_GHOST_SQLITE = `
124
+ SELECT r.id AS bad_id
125
+ FROM fact_embeddings_rowids r
126
+ LEFT JOIN facts f ON f.id = r.id
127
+ WHERE f.id IS NULL OR f.superseded_by IS NOT NULL OR f.retired_at IS NOT NULL
128
+ LIMIT 6
129
+ `;
130
+ // PG: fact_embeddings is a regular table with fact_id as PK; the FK ON DELETE
131
+ // CASCADE means f.id IS NULL cannot occur, but the condition is kept symmetric.
132
+ const SQL_I7_GHOST_PG = `
133
+ SELECT fe.fact_id AS bad_id
134
+ FROM fact_embeddings fe
135
+ LEFT JOIN facts f ON f.id = fe.fact_id
136
+ WHERE f.id IS NULL OR f.superseded_by IS NOT NULL OR f.retired_at IS NOT NULL
137
+ LIMIT 6
138
+ `;
139
+ // I7b-1: chunk vectors with no map row (vec0 has no FK; interrupted deletes
140
+ // orphan here). Uses the session_embedding_chunks_rowids shadow table for
141
+ // full-scan access: vec0 tables do not support unfiltered SELECT without a
142
+ // KNN predicate. For INTEGER PK vec0 tables the shadow column is chunk_id
143
+ // (not id, which is null for non-text PKs).
144
+ const SQL_I7B_1_GHOST_SQLITE = `
145
+ SELECT CAST(r.chunk_id AS TEXT) AS bad_id
146
+ FROM session_embedding_chunks_rowids r
147
+ LEFT JOIN session_chunk_map m ON m.chunk_id = r.chunk_id
148
+ WHERE m.chunk_id IS NULL
149
+ LIMIT 6
150
+ `;
151
+ // I7b-2: map rows pointing at missing sessions (SQLite; FK ON DELETE CASCADE
152
+ // fires in normal operation but vec0 deletes are not atomic with the map).
153
+ const SQL_I7B_2_ORPHAN_MAP_SQLITE = `
154
+ SELECT m.session_id AS bad_id
155
+ FROM session_chunk_map m
156
+ LEFT JOIN sessions s ON s.id = m.session_id
157
+ WHERE s.id IS NULL
158
+ LIMIT 6
159
+ `;
160
+ // PG: session_embedding_chunks.session_id and session_chunk_map both carry
161
+ // FK ON DELETE CASCADE to sessions; both checks are FK-impossible in practice
162
+ // and land as regression guards.
163
+ const SQL_I7B_1_GHOST_PG = `
164
+ SELECT CAST(c.chunk_id AS TEXT) AS bad_id
165
+ FROM session_embedding_chunks c
166
+ LEFT JOIN session_chunk_map m ON m.chunk_id = c.chunk_id
167
+ WHERE m.chunk_id IS NULL
168
+ LIMIT 6
169
+ `;
170
+ const SQL_I7B_2_ORPHAN_MAP_PG = `
171
+ SELECT m.session_id AS bad_id
172
+ FROM session_chunk_map m
173
+ LEFT JOIN sessions s ON s.id = m.session_id
174
+ WHERE s.id IS NULL
175
+ LIMIT 6
176
+ `;
83
177
  const MAX_CYCLE_DEPTH = 100;
84
178
  export function detectCycles(loadEdges) {
85
179
  const edges = loadEdges();
@@ -177,17 +271,48 @@ export function runChecksOnSqlite(db) {
177
271
  const count = sqliteCount(db, SQL_I5_DUPLICATE_FACTS);
178
272
  violations.push(buildViolation("I5a", "multiple active facts for same (subject, predicate)", i5DupSamples, count));
179
273
  }
274
+ // I5a-mv: informational count of multi-valued predicate groups (not violations).
275
+ const i5aMvCount = sqliteCount(db, SQL_I5A_MULTI_VALUED);
276
+ if (i5aMvCount > 0) {
277
+ violations.push(buildViolation("I5a-mv", `multi-valued predicate groups exempted from I5a (${[...MULTI_VALUED_PREDICATES].join(", ")})`, [], i5aMvCount));
278
+ }
180
279
  const i5DangleSamples = sqliteRows(db, SQL_I5_DANGLING_SUPERSEDED_BY);
181
280
  if (i5DangleSamples.length > 0) {
182
281
  const count = sqliteCount(db, SQL_I5_DANGLING_SUPERSEDED_BY);
183
282
  violations.push(buildViolation("I5b", "facts.superseded_by references non-existent facts.id", i5DangleSamples, count));
184
283
  }
284
+ // I5c — facts.superseded_by is a functional graph; a cycle traps every fact in
285
+ // it as recall-ineligible forever (both copies have a non-null superseded_by).
286
+ const i5cCycle = detectCycles(() => db
287
+ .prepare("SELECT id AS from_session, superseded_by AS to_session FROM facts WHERE superseded_by IS NOT NULL")
288
+ .all());
289
+ if (i5cCycle) {
290
+ violations.push({ ...i5cCycle, id: "I5c", description: "facts.superseded_by forms a cycle" });
291
+ }
185
292
  // I6
186
293
  const i6Samples = sqliteRows(db, SQL_I6);
187
294
  if (i6Samples.length > 0) {
188
295
  const count = sqliteCount(db, SQL_I6);
189
296
  violations.push(buildViolation("I6", "adapter_state.session_id references non-existent sessions.id", i6Samples, count));
190
297
  }
298
+ // I7
299
+ const i7Samples = sqliteRows(db, SQL_I7_GHOST_SQLITE);
300
+ if (i7Samples.length > 0) {
301
+ const count = sqliteCount(db, SQL_I7_GHOST_SQLITE);
302
+ violations.push(buildViolation("I7", "fact embedding without a live parent fact", i7Samples, count));
303
+ }
304
+ // I7b-1
305
+ const i7b1Samples = sqliteRows(db, SQL_I7B_1_GHOST_SQLITE);
306
+ if (i7b1Samples.length > 0) {
307
+ const count = sqliteCount(db, SQL_I7B_1_GHOST_SQLITE);
308
+ violations.push(buildViolation("I7b-1", "chunk embedding without a map entry", i7b1Samples, count));
309
+ }
310
+ // I7b-2
311
+ const i7b2Samples = sqliteRows(db, SQL_I7B_2_ORPHAN_MAP_SQLITE);
312
+ if (i7b2Samples.length > 0) {
313
+ const count = sqliteCount(db, SQL_I7B_2_ORPHAN_MAP_SQLITE);
314
+ violations.push(buildViolation("I7b-2", "session_chunk_map references non-existent session", i7b2Samples, count));
315
+ }
191
316
  return violations;
192
317
  }
193
318
  // ─── PostgreSQL runner ────────────────────────────────────────────────────────
@@ -247,16 +372,45 @@ export async function runChecksOnPg(pool) {
247
372
  const count = await pgCount(pool, SQL_I5_DUPLICATE_FACTS);
248
373
  violations.push(buildViolation("I5a", "multiple active facts for same (subject, predicate)", i5DupSamples, count));
249
374
  }
375
+ // I5a-mv: informational count of multi-valued predicate groups (not violations).
376
+ const i5aMvCount = await pgCount(pool, SQL_I5A_MULTI_VALUED);
377
+ if (i5aMvCount > 0) {
378
+ violations.push(buildViolation("I5a-mv", `multi-valued predicate groups exempted from I5a (${[...MULTI_VALUED_PREDICATES].join(", ")})`, [], i5aMvCount));
379
+ }
250
380
  if (i5DangleSamples.length > 0) {
251
381
  const count = await pgCount(pool, SQL_I5_DANGLING_SUPERSEDED_BY);
252
382
  violations.push(buildViolation("I5b", "facts.superseded_by references non-existent facts.id", i5DangleSamples, count));
253
383
  }
384
+ // I5c — fact supersedence cycle (see SQLite runner).
385
+ const factEdges = await pool.query("SELECT id AS from_session, superseded_by AS to_session FROM facts WHERE superseded_by IS NOT NULL");
386
+ const i5cCycle = detectCycles(() => factEdges.rows);
387
+ if (i5cCycle) {
388
+ violations.push({ ...i5cCycle, id: "I5c", description: "facts.superseded_by forms a cycle" });
389
+ }
254
390
  // I6
255
391
  const i6Samples = await pgRows(pool, SQL_I6);
256
392
  if (i6Samples.length > 0) {
257
393
  const count = await pgCount(pool, SQL_I6);
258
394
  violations.push(buildViolation("I6", "adapter_state.session_id references non-existent sessions.id", i6Samples, count));
259
395
  }
396
+ // I7
397
+ const i7Samples = await pgRows(pool, SQL_I7_GHOST_PG);
398
+ if (i7Samples.length > 0) {
399
+ const count = await pgCount(pool, SQL_I7_GHOST_PG);
400
+ violations.push(buildViolation("I7", "fact embedding without a live parent fact", i7Samples, count));
401
+ }
402
+ // I7b-1
403
+ const i7b1Samples = await pgRows(pool, SQL_I7B_1_GHOST_PG);
404
+ if (i7b1Samples.length > 0) {
405
+ const count = await pgCount(pool, SQL_I7B_1_GHOST_PG);
406
+ violations.push(buildViolation("I7b-1", "chunk embedding without a map entry", i7b1Samples, count));
407
+ }
408
+ // I7b-2
409
+ const i7b2Samples = await pgRows(pool, SQL_I7B_2_ORPHAN_MAP_PG);
410
+ if (i7b2Samples.length > 0) {
411
+ const count = await pgCount(pool, SQL_I7B_2_ORPHAN_MAP_PG);
412
+ violations.push(buildViolation("I7b-2", "session_chunk_map references non-existent session", i7b2Samples, count));
413
+ }
260
414
  return violations;
261
415
  }
262
416
  // ─── Cheap subset (I1 + I2 + I6) for scheduled watchdog ─────────────────────
@@ -278,15 +432,33 @@ export function runCheapChecksOnSqlite(db) {
278
432
  if (i6Samples.length > 0) {
279
433
  violations.push(buildViolation("I6", "adapter_state.session_id references non-existent sessions.id", i6Samples, sqliteCount(db, SQL_I6)));
280
434
  }
435
+ // I7: LEFT JOIN over fact_embeddings is index-only; within the cheap budget.
436
+ const i7Samples = sqliteRows(db, SQL_I7_GHOST_SQLITE);
437
+ if (i7Samples.length > 0) {
438
+ violations.push(buildViolation("I7", "fact embedding without a live parent fact", i7Samples, sqliteCount(db, SQL_I7_GHOST_SQLITE)));
439
+ }
440
+ // I7b-1
441
+ const i7b1CheapSamples = sqliteRows(db, SQL_I7B_1_GHOST_SQLITE);
442
+ if (i7b1CheapSamples.length > 0) {
443
+ violations.push(buildViolation("I7b-1", "chunk embedding without a map entry", i7b1CheapSamples, sqliteCount(db, SQL_I7B_1_GHOST_SQLITE)));
444
+ }
445
+ // I7b-2
446
+ const i7b2CheapSamples = sqliteRows(db, SQL_I7B_2_ORPHAN_MAP_SQLITE);
447
+ if (i7b2CheapSamples.length > 0) {
448
+ violations.push(buildViolation("I7b-2", "session_chunk_map references non-existent session", i7b2CheapSamples, sqliteCount(db, SQL_I7B_2_ORPHAN_MAP_SQLITE)));
449
+ }
281
450
  return violations;
282
451
  }
283
452
  export async function runCheapChecksOnPg(pool) {
284
453
  const violations = [];
285
- const [i1, i2, i2r, i6] = await Promise.all([
454
+ const [i1, i2, i2r, i6, i7, i7b1, i7b2] = await Promise.all([
286
455
  pgRows(pool, SQL_I1),
287
456
  pgRows(pool, SQL_I2_ORPHANED),
288
457
  pgRows(pool, SQL_I2_ORPHANED_REPLACED),
289
458
  pgRows(pool, SQL_I6),
459
+ pgRows(pool, SQL_I7_GHOST_PG),
460
+ pgRows(pool, SQL_I7B_1_GHOST_PG),
461
+ pgRows(pool, SQL_I7B_2_ORPHAN_MAP_PG),
290
462
  ]);
291
463
  if (i1.length > 0) {
292
464
  violations.push(buildViolation("I1", "self-loop edges in session_edges", i1, await pgCount(pool, SQL_I1)));
@@ -300,6 +472,15 @@ export async function runCheapChecksOnPg(pool) {
300
472
  if (i6.length > 0) {
301
473
  violations.push(buildViolation("I6", "adapter_state.session_id references non-existent sessions.id", i6, await pgCount(pool, SQL_I6)));
302
474
  }
475
+ if (i7.length > 0) {
476
+ violations.push(buildViolation("I7", "fact embedding without a live parent fact", i7, await pgCount(pool, SQL_I7_GHOST_PG)));
477
+ }
478
+ if (i7b1.length > 0) {
479
+ violations.push(buildViolation("I7b-1", "chunk embedding without a map entry", i7b1, await pgCount(pool, SQL_I7B_1_GHOST_PG)));
480
+ }
481
+ if (i7b2.length > 0) {
482
+ violations.push(buildViolation("I7b-2", "session_chunk_map references non-existent session", i7b2, await pgCount(pool, SQL_I7B_2_ORPHAN_MAP_PG)));
483
+ }
303
484
  return violations;
304
485
  }
305
486
  export function applyFixOnSqlite(db) {
@@ -322,9 +503,24 @@ export function applyFixOnSqlite(db) {
322
503
  AND id NOT IN (SELECT to_session FROM session_edges WHERE kind = 'replaces')
323
504
  `)
324
505
  .run();
506
+ // I7 fix: vec0 only supports single-value DELETE, so loop over bad fact_ids.
507
+ const ghostIds = db
508
+ .prepare(`
509
+ SELECT r.id AS bad_id
510
+ FROM fact_embeddings_rowids r
511
+ LEFT JOIN facts f ON f.id = r.id
512
+ WHERE f.id IS NULL OR f.superseded_by IS NOT NULL OR f.retired_at IS NOT NULL
513
+ `)
514
+ .all();
515
+ const delGhostEmb = db.prepare("DELETE FROM fact_embeddings WHERE fact_id = ?");
516
+ let deletedGhostEmbeddings = 0;
517
+ for (const { bad_id } of ghostIds) {
518
+ deletedGhostEmbeddings += delGhostEmb.run(bad_id).changes;
519
+ }
325
520
  return {
326
521
  deletedSelfLoops: deleteResult.changes,
327
522
  restoredToClosed: updateSuperseded.changes + updateReplaced.changes,
523
+ deletedGhostEmbeddings,
328
524
  };
329
525
  }
330
526
  export async function applyFixOnPg(pool) {
@@ -340,10 +536,20 @@ export async function applyFixOnPg(pool) {
340
536
  SET status = 'closed', updated_at = NOW()
341
537
  WHERE status = 'replaced'
342
538
  AND id NOT IN (SELECT to_session FROM session_edges WHERE kind = 'replaces')
539
+ `);
540
+ const ghostEmbResult = await pool.query(`
541
+ DELETE FROM fact_embeddings
542
+ WHERE fact_id IN (
543
+ SELECT fe.fact_id
544
+ FROM fact_embeddings fe
545
+ LEFT JOIN facts f ON f.id = fe.fact_id
546
+ WHERE f.id IS NULL OR f.superseded_by IS NOT NULL OR f.retired_at IS NOT NULL
547
+ )
343
548
  `);
344
549
  return {
345
550
  deletedSelfLoops: deleteResult.rowCount ?? 0,
346
551
  restoredToClosed: (updateSuperseded.rowCount ?? 0) + (updateReplaced.rowCount ?? 0),
552
+ deletedGhostEmbeddings: ghostEmbResult.rowCount ?? 0,
347
553
  };
348
554
  }
349
555
  //# sourceMappingURL=check-invariants.js.map