nlm-memory 0.4.2 → 0.5.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 (285) hide show
  1. package/README.md +72 -34
  2. package/dist/cli/nlm.js +223 -33
  3. package/dist/cli/nlm.js.map +1 -1
  4. package/dist/core/adapters/cursor.d.ts +45 -0
  5. package/dist/core/adapters/cursor.js +397 -0
  6. package/dist/core/adapters/cursor.js.map +1 -0
  7. package/dist/core/adapters/from-source.js +10 -0
  8. package/dist/core/adapters/from-source.js.map +1 -1
  9. package/dist/core/adapters/windsurf.d.ts +44 -0
  10. package/dist/core/adapters/windsurf.js +299 -0
  11. package/dist/core/adapters/windsurf.js.map +1 -0
  12. package/dist/core/hook/claude-settings.d.ts +12 -5
  13. package/dist/core/hook/claude-settings.js +21 -6
  14. package/dist/core/hook/claude-settings.js.map +1 -1
  15. package/dist/core/sources/source-registry.d.ts +1 -1
  16. package/dist/core/sources/source-registry.js +18 -0
  17. package/dist/core/sources/source-registry.js.map +1 -1
  18. package/dist/core/storage/sqlite-session-store.d.ts +2 -0
  19. package/dist/core/storage/sqlite-session-store.js +38 -2
  20. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  21. package/dist/hook/hook-auth.d.ts +13 -0
  22. package/dist/hook/hook-auth.js +19 -0
  23. package/dist/hook/hook-auth.js.map +1 -0
  24. package/dist/hook/prompt-recall-hook.js +7 -1
  25. package/dist/hook/prompt-recall-hook.js.map +1 -1
  26. package/dist/hook/session-start-hook.js +4 -1
  27. package/dist/hook/session-start-hook.js.map +1 -1
  28. package/dist/hook/stop-hook.js +4 -1
  29. package/dist/hook/stop-hook.js.map +1 -1
  30. package/dist/http/app.d.ts +2 -0
  31. package/dist/http/app.js +76 -1
  32. package/dist/http/app.js.map +1 -1
  33. package/dist/install/claude-code.js +1 -1
  34. package/dist/install/claude-code.js.map +1 -1
  35. package/dist/install/cursor.d.ts +25 -0
  36. package/dist/install/cursor.js +43 -0
  37. package/dist/install/cursor.js.map +1 -0
  38. package/dist/install/nlm-dir-perms.d.ts +19 -0
  39. package/dist/install/nlm-dir-perms.js +43 -0
  40. package/dist/install/nlm-dir-perms.js.map +1 -0
  41. package/dist/install/ollama.d.ts +18 -1
  42. package/dist/install/ollama.js +62 -7
  43. package/dist/install/ollama.js.map +1 -1
  44. package/dist/install/setup.d.ts +4 -0
  45. package/dist/install/setup.js +141 -18
  46. package/dist/install/setup.js.map +1 -1
  47. package/dist/install/windsurf.d.ts +25 -0
  48. package/dist/install/windsurf.js +43 -0
  49. package/dist/install/windsurf.js.map +1 -0
  50. package/dist/mcp/server.js +20 -1
  51. package/dist/mcp/server.js.map +1 -1
  52. package/dist/shared/types.d.ts +4 -0
  53. package/dist/ui/assets/{index-BA6IpU8g.css → index-Beo8psd-.css} +1 -1
  54. package/dist/ui/assets/index-CSPTTeeM.js +69 -0
  55. package/dist/ui/index.html +2 -2
  56. package/package.json +26 -1
  57. package/plugin/scripts/prompt-recall-hook.mjs +55 -4
  58. package/plugin/scripts/stop-hook.mjs +57 -6
  59. package/.agents/plugins/marketplace.json +0 -20
  60. package/.github/workflows/ci.yml +0 -30
  61. package/dist/ui/assets/index-B_qIVV0k.js +0 -69
  62. package/docs/methodology/re-derivation-rate.md +0 -112
  63. package/docs/methodology/useful-hit-rate.md +0 -79
  64. package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
  65. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
  66. package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
  67. package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
  68. package/docs/plans/desktop-product.md +0 -69
  69. package/docs/plans/factstore-design.md +0 -236
  70. package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1389
  71. package/logs/CHANGELOG/CHANGELOG.md +0 -337
  72. package/migrations/000_initial_schema.sql +0 -174
  73. package/migrations/001_entity_type_rename.sql +0 -17
  74. package/migrations/002_adapter_state_extend.sql +0 -12
  75. package/migrations/003_session_embeddings.sql +0 -11
  76. package/migrations/004_facts.sql +0 -46
  77. package/migrations/005_sources.sql +0 -31
  78. package/migrations/006_providers.sql +0 -33
  79. package/migrations/007_source_tokens.sql +0 -17
  80. package/migrations/008_fts_rebuild.sql +0 -9
  81. package/migrations/009_session_embedding_chunks.sql +0 -46
  82. package/migrations/010_sources_opencode.sql +0 -30
  83. package/migrations/011_sources_hermes_agent.sql +0 -30
  84. package/migrations/012_sources_aider.sql +0 -30
  85. package/migrations/013_adapter_state_failure_count.sql +0 -12
  86. package/plugin-hermes-agent/README.md +0 -49
  87. package/plugin-hermes-agent/__init__.py +0 -75
  88. package/plugin-hermes-agent/plugin.yaml +0 -15
  89. package/scripts/backfill-citations.mjs +0 -0
  90. package/scripts/build-codex-plugin.mjs +0 -61
  91. package/scripts/deepseek-probe.mjs +0 -67
  92. package/scripts/extract-triples.mjs +0 -207
  93. package/scripts/longmemeval/embedding-cache.ts +0 -77
  94. package/scripts/longmemeval/fetch-dataset.sh +0 -25
  95. package/scripts/longmemeval/run-harness.ts +0 -315
  96. package/scripts/longmemeval/scorer.ts +0 -99
  97. package/scripts/longmemeval/tsconfig.json +0 -9
  98. package/scripts/longmemeval/types.ts +0 -35
  99. package/scripts/nlm-daily-digest.py +0 -239
  100. package/scripts/nlm-daily-digest.sh +0 -28
  101. package/src/cli/classify-parity.ts +0 -257
  102. package/src/cli/launchctl-helpers.ts +0 -49
  103. package/src/cli/nlm.ts +0 -885
  104. package/src/core/actions/actions-log.ts +0 -118
  105. package/src/core/actions/overlay.ts +0 -117
  106. package/src/core/adapters/aider.ts +0 -205
  107. package/src/core/adapters/claude-code.ts +0 -293
  108. package/src/core/adapters/common.ts +0 -54
  109. package/src/core/adapters/from-source.ts +0 -57
  110. package/src/core/adapters/hermes-agent.ts +0 -240
  111. package/src/core/adapters/hermes.ts +0 -277
  112. package/src/core/adapters/jsonl-generic.ts +0 -208
  113. package/src/core/adapters/opencode.ts +0 -281
  114. package/src/core/adapters/pi.ts +0 -264
  115. package/src/core/classifier/prompt.ts +0 -200
  116. package/src/core/dataset/build-dataset.ts +0 -463
  117. package/src/core/embedding/chunk-body.ts +0 -76
  118. package/src/core/embedding/embed-backfill.ts +0 -210
  119. package/src/core/embedding/embed-normalize.ts +0 -135
  120. package/src/core/facts/backfill-facts.ts +0 -254
  121. package/src/core/facts/extract-facts.ts +0 -50
  122. package/src/core/hook/citation-detect.ts +0 -124
  123. package/src/core/hook/cite-memo.ts +0 -68
  124. package/src/core/hook/claude-settings.ts +0 -166
  125. package/src/core/hook/gate.ts +0 -25
  126. package/src/core/hook/hook-log.ts +0 -41
  127. package/src/core/hook/memo-sweep.ts +0 -164
  128. package/src/core/hook/memo.ts +0 -67
  129. package/src/core/hook/pointer-block.ts +0 -26
  130. package/src/core/hook/select.ts +0 -32
  131. package/src/core/hook/transcript.ts +0 -121
  132. package/src/core/ingest/ingest-session.ts +0 -111
  133. package/src/core/providers/provider-models.ts +0 -100
  134. package/src/core/providers/provider-registry.ts +0 -196
  135. package/src/core/recall/citation-log.ts +0 -108
  136. package/src/core/recall/filter.ts +0 -27
  137. package/src/core/recall/index.ts +0 -6
  138. package/src/core/recall/match-fields.ts +0 -40
  139. package/src/core/recall/query-log.ts +0 -149
  140. package/src/core/recall/query-shape.ts +0 -66
  141. package/src/core/recall/recall-service.ts +0 -320
  142. package/src/core/recall/recent-log.ts +0 -59
  143. package/src/core/recall/tokenize.ts +0 -18
  144. package/src/core/recall/useful-scan.ts +0 -336
  145. package/src/core/recall-facts/fact-query-log.ts +0 -150
  146. package/src/core/recall-facts/fact-recall-service.ts +0 -327
  147. package/src/core/scheduler/scan-once.ts +0 -142
  148. package/src/core/scheduler/scheduler.ts +0 -225
  149. package/src/core/sources/source-registry.ts +0 -260
  150. package/src/core/storage/db-restore.ts +0 -133
  151. package/src/core/storage/live-status.ts +0 -45
  152. package/src/core/storage/migrate.ts +0 -72
  153. package/src/core/storage/sqlite-fact-store.ts +0 -304
  154. package/src/core/storage/sqlite-session-store.ts +0 -765
  155. package/src/hook/prompt-recall-hook.ts +0 -174
  156. package/src/hook/session-end-hook.ts +0 -81
  157. package/src/hook/session-start-hook.ts +0 -165
  158. package/src/hook/stop-hook.ts +0 -236
  159. package/src/http/app.ts +0 -1137
  160. package/src/install/claude-code.ts +0 -128
  161. package/src/install/codex.ts +0 -367
  162. package/src/install/hermes-agent.ts +0 -76
  163. package/src/install/hermes.ts +0 -78
  164. package/src/install/ollama.ts +0 -211
  165. package/src/install/setup.ts +0 -368
  166. package/src/llm/classifier-box.ts +0 -64
  167. package/src/llm/deepseek-client.ts +0 -150
  168. package/src/llm/env-autoload.ts +0 -55
  169. package/src/llm/ollama-client.ts +0 -189
  170. package/src/mcp/server.ts +0 -534
  171. package/src/ports/fact-store.ts +0 -102
  172. package/src/ports/llm-client.ts +0 -52
  173. package/src/ports/logger.ts +0 -16
  174. package/src/ports/session-store.ts +0 -45
  175. package/src/ports/transcript-adapter.ts +0 -55
  176. package/src/shared/types.ts +0 -145
  177. package/src/ui/App.tsx +0 -58
  178. package/src/ui/components/PromoteOpenButton.tsx +0 -65
  179. package/src/ui/components/SessionDrawer.tsx +0 -136
  180. package/src/ui/components/SideNav.tsx +0 -162
  181. package/src/ui/components/Skeleton.tsx +0 -107
  182. package/src/ui/index.html +0 -13
  183. package/src/ui/lib/actions.ts +0 -30
  184. package/src/ui/lib/api.ts +0 -92
  185. package/src/ui/lib/dataset.ts +0 -141
  186. package/src/ui/lib/registries.ts +0 -155
  187. package/src/ui/lib/view-settings.ts +0 -41
  188. package/src/ui/main.tsx +0 -15
  189. package/src/ui/pages/Live.tsx +0 -229
  190. package/src/ui/pages/Pulse.tsx +0 -415
  191. package/src/ui/pages/Recall.tsx +0 -190
  192. package/src/ui/pages/River.tsx +0 -308
  193. package/src/ui/pages/Search.tsx +0 -93
  194. package/src/ui/pages/Stub.tsx +0 -9
  195. package/src/ui/pages/Thread.tsx +0 -262
  196. package/src/ui/pages/settings/Classifier.tsx +0 -227
  197. package/src/ui/pages/settings/Data.tsx +0 -190
  198. package/src/ui/pages/settings/Index.tsx +0 -65
  199. package/src/ui/pages/settings/Labels.tsx +0 -224
  200. package/src/ui/pages/settings/Providers.tsx +0 -305
  201. package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
  202. package/src/ui/pages/settings/Sources.tsx +0 -326
  203. package/src/ui/pages/settings/Views.tsx +0 -96
  204. package/src/ui/styles.css +0 -1766
  205. package/src/ui/tsconfig.json +0 -21
  206. package/src/ui/vite.config.ts +0 -19
  207. package/tests/fixtures/claude_code/short_session.jsonl +0 -2
  208. package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
  209. package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
  210. package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
  211. package/tests/fixtures/facts.ts +0 -17
  212. package/tests/fixtures/golden-corpus.ts +0 -85
  213. package/tests/fixtures/hermes/paired_request_dump.json +0 -24
  214. package/tests/fixtures/hermes/paired_session.json +0 -23
  215. package/tests/fixtures/hermes/request_dump.json +0 -28
  216. package/tests/fixtures/hermes/session_iso.json +0 -38
  217. package/tests/fixtures/hermes/session_unix.json +0 -38
  218. package/tests/fixtures/hermes/system_only.json +0 -18
  219. package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
  220. package/tests/fixtures/pi/short-successful.jsonl +0 -5
  221. package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
  222. package/tests/fixtures/sessions.ts +0 -22
  223. package/tests/integration/backfill-facts.test.ts +0 -362
  224. package/tests/integration/citation-explicit.test.ts +0 -111
  225. package/tests/integration/cite-event.test.ts +0 -169
  226. package/tests/integration/cite-memo.test.ts +0 -87
  227. package/tests/integration/db-restore.test.ts +0 -153
  228. package/tests/integration/embed-backfill.test.ts +0 -176
  229. package/tests/integration/fact-supersedence.test.ts +0 -313
  230. package/tests/integration/fts-index.test.ts +0 -60
  231. package/tests/integration/getbyids-sqlite.test.ts +0 -60
  232. package/tests/integration/hermes-agent-hooks.test.ts +0 -248
  233. package/tests/integration/hook-claude-settings.test.ts +0 -205
  234. package/tests/integration/hook-log.test.ts +0 -54
  235. package/tests/integration/hook-memo.test.ts +0 -68
  236. package/tests/integration/hook-pre-compact.test.ts +0 -105
  237. package/tests/integration/hook-subagent-start.test.ts +0 -102
  238. package/tests/integration/http.test.ts +0 -401
  239. package/tests/integration/keyword-search-fts.test.ts +0 -66
  240. package/tests/integration/mcp-recall-logging.test.ts +0 -88
  241. package/tests/integration/mcp.test.ts +0 -248
  242. package/tests/integration/memo-sweep.test.ts +0 -91
  243. package/tests/integration/prompt-recall-hook.test.ts +0 -88
  244. package/tests/integration/provider-registry.test.ts +0 -107
  245. package/tests/integration/recall-golden.test.ts +0 -59
  246. package/tests/integration/recall-sqlite.test.ts +0 -169
  247. package/tests/integration/scheduler.test.ts +0 -391
  248. package/tests/integration/session-end-hook.test.ts +0 -48
  249. package/tests/integration/session-start-hook.test.ts +0 -126
  250. package/tests/integration/source-registry.test.ts +0 -120
  251. package/tests/integration/sqlite-fact-store.test.ts +0 -346
  252. package/tests/integration/stop-hook.test.ts +0 -560
  253. package/tests/integration/wal-checkpoint.test.ts +0 -49
  254. package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
  255. package/tests/unit/core/adapters/aider.test.ts +0 -230
  256. package/tests/unit/core/adapters/claude-code.test.ts +0 -118
  257. package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
  258. package/tests/unit/core/adapters/hermes.test.ts +0 -81
  259. package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
  260. package/tests/unit/core/adapters/opencode.test.ts +0 -354
  261. package/tests/unit/core/adapters/pi.test.ts +0 -110
  262. package/tests/unit/core/classifier/prompt.test.ts +0 -126
  263. package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
  264. package/tests/unit/core/facts/extract-facts.test.ts +0 -117
  265. package/tests/unit/core/filter.test.ts +0 -40
  266. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
  267. package/tests/unit/core/hook/citation-detect.test.ts +0 -124
  268. package/tests/unit/core/hook/gate.test.ts +0 -29
  269. package/tests/unit/core/hook/pointer-block.test.ts +0 -22
  270. package/tests/unit/core/hook/select.test.ts +0 -66
  271. package/tests/unit/core/match-fields.test.ts +0 -39
  272. package/tests/unit/core/mcp-cite-session.test.ts +0 -51
  273. package/tests/unit/core/providers/provider-models.test.ts +0 -101
  274. package/tests/unit/core/query-shape.test.ts +0 -92
  275. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
  276. package/tests/unit/core/recall-service.test.ts +0 -200
  277. package/tests/unit/core/storage/live-status.test.ts +0 -54
  278. package/tests/unit/core/tokenize.test.ts +0 -32
  279. package/tests/unit/core/useful-scan.test.ts +0 -537
  280. package/tests/unit/llm/embed.test.ts +0 -93
  281. package/tests/unit/llm/ollama-client.test.ts +0 -124
  282. package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
  283. package/tsconfig.json +0 -31
  284. package/tsconfig.test.json +0 -11
  285. package/vitest.config.ts +0 -22
@@ -1,126 +0,0 @@
1
- /**
2
- * coerceClassifyResult — defensive parser over raw LLM JSON output. Focuses
3
- * on the Phase B.2 facts[] additions; existing fields are covered by the
4
- * end-to-end OllamaClient tests.
5
- */
6
-
7
- import { describe, expect, it } from "vitest";
8
- import {
9
- CLASSIFIER_SYSTEM_PROMPT,
10
- PREDICATE_VOCABULARY,
11
- coerceClassifyResult,
12
- } from "../../../../src/core/classifier/prompt.js";
13
-
14
- describe("coerceClassifyResult — facts", () => {
15
- function baseFields() {
16
- return {
17
- label: "L",
18
- summary: "S",
19
- entities: [],
20
- decisions: [],
21
- open: [],
22
- confidence: 0.8,
23
- };
24
- }
25
-
26
- it("returns an empty facts array when the key is missing entirely", () => {
27
- expect(coerceClassifyResult(baseFields()).facts).toEqual([]);
28
- });
29
-
30
- it("returns an empty facts array when facts is not an array", () => {
31
- expect(
32
- coerceClassifyResult({ ...baseFields(), facts: "not-an-array" }).facts,
33
- ).toEqual([]);
34
- });
35
-
36
- it("normalizes subject + predicate to lowercase and trims value", () => {
37
- const out = coerceClassifyResult({
38
- ...baseFields(),
39
- facts: [
40
- { kind: "decision", subject: "NLM-Memory-TS", predicate: "Framework", value: " Hono " },
41
- ],
42
- });
43
- expect(out.facts).toEqual([
44
- { kind: "decision", subject: "nlm-memory-ts", predicate: "framework", value: "Hono" },
45
- ]);
46
- });
47
-
48
- it("drops facts with predicates outside the closed vocabulary (no 'other' escape hatch)", () => {
49
- const out = coerceClassifyResult({
50
- ...baseFields(),
51
- facts: [
52
- { kind: "decision", subject: "x", predicate: "color-of-the-bikeshed", value: "blue" },
53
- { kind: "decision", subject: "x", predicate: "framework", value: "Hono" },
54
- ],
55
- });
56
- expect(out.facts.map((f) => f.predicate)).toEqual(["framework"]);
57
- });
58
-
59
- it("PREDICATE_VOCABULARY does not include 'other'", () => {
60
- // Removed in Phase B.5 after pilot showed `other` was 43% of writes and
61
- // almost all slop. Off-vocab facts now get dropped by the coercer rather
62
- // than forced into a catch-all bucket.
63
- expect(PREDICATE_VOCABULARY).not.toContain("other");
64
- });
65
-
66
- it("drops facts missing required fields (subject, predicate, value)", () => {
67
- const out = coerceClassifyResult({
68
- ...baseFields(),
69
- facts: [
70
- { kind: "decision", subject: "", predicate: "framework", value: "Hono" },
71
- { kind: "decision", subject: "x", predicate: "", value: "Hono" },
72
- { kind: "decision", subject: "x", predicate: "framework", value: "" },
73
- { kind: "decision", subject: "good", predicate: "framework", value: "Hono" },
74
- ],
75
- });
76
- expect(out.facts).toEqual([
77
- { kind: "decision", subject: "good", predicate: "framework", value: "Hono" },
78
- ]);
79
- });
80
-
81
- it("drops facts with an invalid kind", () => {
82
- const out = coerceClassifyResult({
83
- ...baseFields(),
84
- facts: [
85
- { kind: "garbage", subject: "x", predicate: "framework", value: "Hono" },
86
- { kind: "attribute", subject: "x", predicate: "framework", value: "Hono" },
87
- ],
88
- });
89
- expect(out.facts.map((f) => f.kind)).toEqual(["attribute"]);
90
- });
91
-
92
- it("clamps sourceQuote to 500 chars and trims whitespace", () => {
93
- const long = " ".repeat(10) + "a".repeat(600) + " ".repeat(10);
94
- const out = coerceClassifyResult({
95
- ...baseFields(),
96
- facts: [
97
- { kind: "decision", subject: "x", predicate: "framework", value: "Hono", sourceQuote: long },
98
- ],
99
- });
100
- expect(out.facts[0]?.sourceQuote).toBe("a".repeat(500));
101
- });
102
-
103
- it("omits sourceQuote when blank or non-string", () => {
104
- const out = coerceClassifyResult({
105
- ...baseFields(),
106
- facts: [
107
- { kind: "decision", subject: "a", predicate: "framework", value: "v", sourceQuote: " " },
108
- { kind: "decision", subject: "b", predicate: "framework", value: "v", sourceQuote: 42 },
109
- ],
110
- });
111
- expect(out.facts[0]?.sourceQuote).toBeUndefined();
112
- expect(out.facts[1]?.sourceQuote).toBeUndefined();
113
- });
114
- });
115
-
116
- describe("CLASSIFIER_SYSTEM_PROMPT", () => {
117
- it("includes the facts field in the requested JSON shape", () => {
118
- expect(CLASSIFIER_SYSTEM_PROMPT).toContain('"facts"');
119
- });
120
-
121
- it("inlines the predicate vocabulary so the LLM sees the closed list", () => {
122
- for (const p of PREDICATE_VOCABULARY) {
123
- expect(CLASSIFIER_SYSTEM_PROMPT).toContain(p);
124
- }
125
- });
126
- });
@@ -1,100 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- chunkSessionText,
4
- MAX_CHUNK_CHARS,
5
- OVERLAP_CHARS,
6
- } from "../../../../src/core/embedding/chunk-body.js";
7
-
8
- describe("chunkSessionText", () => {
9
- it("returns empty array when label, summary, and body are all blank", () => {
10
- expect(chunkSessionText({})).toEqual([]);
11
- expect(chunkSessionText({ label: "", summary: " ", body: "" })).toEqual([]);
12
- });
13
-
14
- it("returns a header-only chunk when body is empty", () => {
15
- const chunks = chunkSessionText({ label: "Meeting notes", summary: "Q4 plan" });
16
- expect(chunks).toEqual(["Meeting notes Q4 plan"]);
17
- });
18
-
19
- it("returns one chunk when header + body fits in maxChars", () => {
20
- const chunks = chunkSessionText(
21
- { label: "L", summary: "S", body: "hello world" },
22
- { maxChars: 100, overlap: 10 },
23
- );
24
- expect(chunks).toEqual(["L S hello world"]);
25
- });
26
-
27
- it("splits body into multiple chunks with overlap when over maxChars", () => {
28
- // body 250 chars, maxChars=100, overlap=20, no header → step=80
29
- // chunk 0: body[0..100], chunk 1: body[80..180], chunk 2: body[160..250]
30
- const body = "x".repeat(250);
31
- const chunks = chunkSessionText(
32
- { body },
33
- { maxChars: 100, overlap: 20 },
34
- );
35
- expect(chunks).toHaveLength(3);
36
- expect(chunks[0]!.length).toBe(100);
37
- expect(chunks[1]!.length).toBe(100);
38
- expect(chunks[2]!.length).toBe(90);
39
- });
40
-
41
- it("preserves overlap content between adjacent chunks", () => {
42
- // Recognisable letters so we can confirm the boundary overlaps.
43
- const body =
44
- "A".repeat(50) +
45
- "B".repeat(50) +
46
- "C".repeat(50) +
47
- "D".repeat(50); // 200 chars
48
- const chunks = chunkSessionText(
49
- { body },
50
- { maxChars: 80, overlap: 20 },
51
- );
52
- // chunk 0: body[0..80] → AAA...AAA BBB...BBB BB (50 A + 30 B)
53
- // chunk 1: body[60..140] → 40 B + 40 C overlapping the last 20 B from chunk 0
54
- expect(chunks[0]!.slice(-10)).toBe("B".repeat(10));
55
- expect(chunks[1]!.slice(0, 20)).toBe("B".repeat(20)); // overlap
56
- });
57
-
58
- it("accounts for header in first-chunk budget", () => {
59
- const header = "h".repeat(20);
60
- const body = "b".repeat(200);
61
- const chunks = chunkSessionText(
62
- { label: header, body },
63
- { maxChars: 100, overlap: 10 },
64
- );
65
- // First chunk: 20-char header + space + body[0..79] = 100 chars total
66
- // Second chunk: body[69..169] (90 chars body budget - 10 overlap from start of body)
67
- expect(chunks[0]!.startsWith(header + " ")).toBe(true);
68
- expect(chunks[0]!.length).toBeLessThanOrEqual(100);
69
- });
70
-
71
- it("respects defaults (MAX_CHUNK_CHARS, OVERLAP_CHARS) when no opts passed", () => {
72
- const body = "y".repeat(MAX_CHUNK_CHARS * 2 + 1000);
73
- const chunks = chunkSessionText({ body });
74
- expect(chunks.length).toBeGreaterThan(1);
75
- for (const c of chunks) {
76
- expect(c.length).toBeLessThanOrEqual(MAX_CHUNK_CHARS);
77
- }
78
- // Overlap default sanity: consecutive chunks should share OVERLAP_CHARS
79
- expect(OVERLAP_CHARS).toBeGreaterThan(0);
80
- });
81
-
82
- it("throws on invalid options", () => {
83
- expect(() => chunkSessionText({ body: "x" }, { maxChars: 0 })).toThrow();
84
- expect(() => chunkSessionText({ body: "x" }, { maxChars: 100, overlap: -1 })).toThrow();
85
- expect(() => chunkSessionText({ body: "x" }, { maxChars: 100, overlap: 100 })).toThrow();
86
- });
87
-
88
- it("trims whitespace at chunk boundaries", () => {
89
- const body = "alpha " + "z".repeat(200);
90
- const chunks = chunkSessionText({ body }, { maxChars: 100, overlap: 20 });
91
- for (const c of chunks) {
92
- expect(c).toBe(c.trim());
93
- }
94
- });
95
-
96
- it("returns at least one chunk for tiny non-empty input", () => {
97
- expect(chunkSessionText({ body: "x" })).toEqual(["x"]);
98
- expect(chunkSessionText({ label: "x" })).toEqual(["x"]);
99
- });
100
- });
@@ -1,117 +0,0 @@
1
- /**
2
- * extractFacts — pure transform from ClassifyResult to Fact[]. No DB, no
3
- * randomness (idGenerator is injected).
4
- */
5
-
6
- import { describe, expect, it } from "vitest";
7
- import { extractFacts } from "../../../../src/core/facts/extract-facts.js";
8
- import type { ClassifyResult } from "../../../../src/ports/llm-client.js";
9
-
10
- function classifyResult(overrides: Partial<ClassifyResult> = {}): ClassifyResult {
11
- return {
12
- label: "L",
13
- summary: "S",
14
- entities: [],
15
- decisions: [],
16
- open: [],
17
- confidence: 0.9,
18
- facts: [],
19
- ...overrides,
20
- };
21
- }
22
-
23
- describe("extractFacts", () => {
24
- it("returns an empty array when classify result has no facts", () => {
25
- const out = extractFacts(classifyResult(), "sess_1", "2026-05-19T10:00:00Z");
26
- expect(out).toEqual([]);
27
- });
28
-
29
- it("maps classifier facts to full Fact records with injected id + timestamp", () => {
30
- let n = 0;
31
- const result = classifyResult({
32
- confidence: 0.85,
33
- facts: [
34
- { kind: "decision", subject: "nlm-memory-ts", predicate: "framework", value: "Hono" },
35
- {
36
- kind: "attribute",
37
- subject: "mac-pro",
38
- predicate: "endpoint",
39
- value: "http://macpro:8080/v1",
40
- sourceQuote: "endpoint at :8080",
41
- },
42
- ],
43
- });
44
- const out = extractFacts(result, "sess_1", "2026-05-19T10:00:00Z", {
45
- idGenerator: () => `fact_${++n}`,
46
- });
47
- expect(out).toEqual([
48
- {
49
- id: "fact_1",
50
- kind: "decision",
51
- subject: "nlm-memory-ts",
52
- predicate: "framework",
53
- value: "Hono",
54
- sourceSessionId: "sess_1",
55
- sourceQuote: null,
56
- createdAt: "2026-05-19T10:00:00Z",
57
- supersededBy: null,
58
- confidence: 0.85,
59
- },
60
- {
61
- id: "fact_2",
62
- kind: "attribute",
63
- subject: "mac-pro",
64
- predicate: "endpoint",
65
- value: "http://macpro:8080/v1",
66
- sourceSessionId: "sess_1",
67
- sourceQuote: "endpoint at :8080",
68
- createdAt: "2026-05-19T10:00:00Z",
69
- supersededBy: null,
70
- confidence: 0.85,
71
- },
72
- ]);
73
- });
74
-
75
- it("drops all facts when classifier confidence is below 0.4", () => {
76
- const result = classifyResult({
77
- confidence: 0.35,
78
- facts: [
79
- { kind: "decision", subject: "x", predicate: "framework", value: "y" },
80
- ],
81
- });
82
- expect(extractFacts(result, "sess_1", "2026-05-19T10:00:00Z")).toEqual([]);
83
- });
84
-
85
- it("keeps facts when confidence is exactly the floor (0.4)", () => {
86
- const result = classifyResult({
87
- confidence: 0.4,
88
- facts: [
89
- { kind: "decision", subject: "x", predicate: "framework", value: "y" },
90
- ],
91
- });
92
- expect(extractFacts(result, "sess_1", "2026-05-19T10:00:00Z")).toHaveLength(1);
93
- });
94
-
95
- it("uses default id generator (fact_<uuid>) when none provided", () => {
96
- const result = classifyResult({
97
- facts: [{ kind: "decision", subject: "x", predicate: "framework", value: "y" }],
98
- });
99
- const out = extractFacts(result, "sess_1", "2026-05-19T10:00:00Z");
100
- expect(out[0]?.id).toMatch(/^fact_[0-9a-f-]{36}$/);
101
- });
102
-
103
- it("each fact gets its own id from the generator (no reuse)", () => {
104
- let n = 0;
105
- const result = classifyResult({
106
- facts: [
107
- { kind: "decision", subject: "a", predicate: "framework", value: "x" },
108
- { kind: "decision", subject: "b", predicate: "framework", value: "y" },
109
- { kind: "decision", subject: "c", predicate: "framework", value: "z" },
110
- ],
111
- });
112
- const out = extractFacts(result, "sess_1", "2026-05-19T10:00:00Z", {
113
- idGenerator: () => `fact_${++n}`,
114
- });
115
- expect(out.map((f) => f.id)).toEqual(["fact_1", "fact_2", "fact_3"]);
116
- });
117
- });
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { applyFilter } from "../../../src/core/recall/filter.js";
3
- import { makeSession } from "../../fixtures/sessions.js";
4
-
5
- describe("applyFilter", () => {
6
- const noDecisionsNoOpen = makeSession({ id: "a", entities: ["Whtnxt"] });
7
- const onlyDecisions = makeSession({ id: "b", entities: ["NLM"], decisions: ["picked Hono"] });
8
- const onlyOpen = makeSession({ id: "c", entities: ["NLM"], open: ["pgvector later"] });
9
- const both = makeSession({
10
- id: "d",
11
- entities: ["NLM", "Whtnxt"],
12
- decisions: ["use SQLite + sqlite-vec"],
13
- open: ["Tauri or Electron"],
14
- });
15
- const corpus = [noDecisionsNoOpen, onlyDecisions, onlyOpen, both];
16
-
17
- it("returns input unchanged when filter is empty", () => {
18
- expect(applyFilter(corpus, {})).toEqual(corpus);
19
- });
20
-
21
- it("filters by entity tag", () => {
22
- const result = applyFilter(corpus, { entity: "NLM" });
23
- expect(result.map((s) => s.id)).toEqual(["b", "c", "d"]);
24
- });
25
-
26
- it("filters by kind=decision (drops sessions with no decisions)", () => {
27
- const result = applyFilter(corpus, { kind: "decision" });
28
- expect(result.map((s) => s.id)).toEqual(["b", "d"]);
29
- });
30
-
31
- it("filters by kind=open (drops sessions with no open questions)", () => {
32
- const result = applyFilter(corpus, { kind: "open" });
33
- expect(result.map((s) => s.id)).toEqual(["c", "d"]);
34
- });
35
-
36
- it("combines entity and kind constraints (AND semantics)", () => {
37
- const result = applyFilter(corpus, { entity: "Whtnxt", kind: "decision" });
38
- expect(result.map((s) => s.id)).toEqual(["d"]);
39
- });
40
- });
@@ -1,96 +0,0 @@
1
- /**
2
- * cite_session tool_use handling in the Stop hook detector.
3
- *
4
- * The MCP server's citeSessionHandler already calls appendCitation() directly
5
- * when the model invokes cite_session. The Stop hook must NOT detect the same
6
- * cite_session tool_use and write a second log entry (double-count). The A1
7
- * sub-case in detectCitations now skips cite_session calls entirely.
8
- *
9
- * Implicit citations via other NLM tools (get_session, recall_sessions, etc.)
10
- * still fire through the A2 path as before.
11
- */
12
-
13
- import { describe, expect, it } from "vitest";
14
- import { detectCitations } from "../../../../src/core/hook/citation-detect.js";
15
-
16
- describe("detectCitations — cite_session skipped to prevent double-count", () => {
17
- it("does not detect cite_session as a citation (MCP handler already logged it)", () => {
18
- const result = detectCitations({
19
- responseText: "Based on that session, here is my answer.",
20
- toolUses: [
21
- {
22
- name: "mcp__nlm-memory__cite_session",
23
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
24
- },
25
- ],
26
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
27
- });
28
- expect(result).toEqual([]);
29
- });
30
-
31
- it("does not detect cite_session even for a surfaced ID", () => {
32
- const result = detectCitations({
33
- responseText: "",
34
- toolUses: [
35
- {
36
- name: "mcp__nlm-memory__cite_session",
37
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
38
- },
39
- ],
40
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
41
- });
42
- expect(result).toEqual([]);
43
- });
44
-
45
- it("falls back to prose detection when cite_session is skipped and ID appears in text", () => {
46
- const result = detectCitations({
47
- responseText: "Per cc_sub_a139f4ab7ca5aa909, the decision was FTS5.",
48
- toolUses: [
49
- {
50
- name: "mcp__nlm-memory__cite_session",
51
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
52
- },
53
- ],
54
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
55
- });
56
- expect(result).toHaveLength(1);
57
- expect(result[0]).toEqual({ id: "cc_sub_a139f4ab7ca5aa909", kind: "prose" });
58
- });
59
-
60
- it("returns empty when multiple cite_session calls are skipped and no prose match", () => {
61
- const result = detectCitations({
62
- responseText: "",
63
- toolUses: [
64
- {
65
- name: "mcp__nlm-memory__cite_session",
66
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
67
- },
68
- {
69
- name: "mcp__nlm-memory__cite_session",
70
- input: { id: "hm_20260427_6ff562" },
71
- },
72
- ],
73
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909", "hm_20260427_6ff562"],
74
- });
75
- expect(result).toEqual([]);
76
- });
77
-
78
- it("A2 path (get_session) still fires while cite_session is skipped for the same turn", () => {
79
- const result = detectCitations({
80
- responseText: "",
81
- toolUses: [
82
- {
83
- name: "mcp__nlm-memory__cite_session",
84
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
85
- },
86
- {
87
- name: "mcp__nlm-memory__get_session",
88
- input: { id: "hm_20260427_6ff562" },
89
- },
90
- ],
91
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909", "hm_20260427_6ff562"],
92
- });
93
- expect(result).toHaveLength(1);
94
- expect(result[0]).toMatchObject({ id: "hm_20260427_6ff562", kind: "tool_use" });
95
- });
96
- });
@@ -1,124 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- detectCitations,
4
- detectCitedIds,
5
- } from "../../../../src/core/hook/citation-detect.js";
6
-
7
- describe("detectCitedIds (back-compat prose-only)", () => {
8
- it("returns IDs that appear as substrings in the response", () => {
9
- const surfaced = new Set([
10
- "cc_sub_a139f4ab7ca5aa909",
11
- "hm_20260427_6ff562",
12
- ]);
13
- const text = "Per cc_sub_a139f4ab7ca5aa909 and hm_20260427_6ff562 we chose FTS5.";
14
- expect(detectCitedIds(text, surfaced).sort()).toEqual(
15
- ["cc_sub_a139f4ab7ca5aa909", "hm_20260427_6ff562"].sort(),
16
- );
17
- });
18
-
19
- it("returns empty when no surfaced IDs appear", () => {
20
- expect(detectCitedIds("unrelated", new Set(["cc_sub_abc123def456"]))).toEqual([]);
21
- });
22
-
23
- it("ignores IDs shorter than the minimum length", () => {
24
- expect(detectCitedIds("a ab abc abcdef", new Set(["a", "ab", "abc"]))).toEqual([]);
25
- });
26
- });
27
-
28
- describe("detectCitations (combined tool_use + prose)", () => {
29
- it("emits a tool_use citation when an NLM MCP tool input references a surfaced ID", () => {
30
- const result = detectCitations({
31
- responseText: "Let me look at that.",
32
- toolUses: [
33
- {
34
- name: "mcp__nlm-memory__get_session",
35
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
36
- },
37
- ],
38
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909", "hm_20260427_6ff562"],
39
- });
40
- expect(result).toEqual([
41
- { id: "cc_sub_a139f4ab7ca5aa909", kind: "tool_use" },
42
- ]);
43
- });
44
-
45
- it("emits a prose citation when only the response text mentions the ID", () => {
46
- const result = detectCitations({
47
- responseText: "We decided on FTS5 per cc_sub_a139f4ab7ca5aa909.",
48
- toolUses: [],
49
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
50
- });
51
- expect(result).toEqual([
52
- { id: "cc_sub_a139f4ab7ca5aa909", kind: "prose" },
53
- ]);
54
- });
55
-
56
- it("prefers tool_use over prose when both fire on the same ID", () => {
57
- const result = detectCitations({
58
- responseText: "Per cc_sub_a139f4ab7ca5aa909, here is the answer.",
59
- toolUses: [
60
- {
61
- name: "mcp__nlm-memory__get_session",
62
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
63
- },
64
- ],
65
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
66
- });
67
- expect(result).toEqual([
68
- { id: "cc_sub_a139f4ab7ca5aa909", kind: "tool_use" },
69
- ]);
70
- });
71
-
72
- it("ignores non-NLM tool_use blocks even when they happen to contain the ID", () => {
73
- const result = detectCitations({
74
- responseText: "",
75
- toolUses: [
76
- {
77
- name: "Bash",
78
- input: { command: "grep cc_sub_a139f4ab7ca5aa909 /tmp/log" },
79
- },
80
- ],
81
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
82
- });
83
- expect(result).toEqual([]);
84
- });
85
-
86
- it("handles multiple NLM tool calls in one turn", () => {
87
- const result = detectCitations({
88
- responseText: "",
89
- toolUses: [
90
- {
91
- name: "mcp__nlm-memory__get_session",
92
- input: { id: "cc_sub_a139f4ab7ca5aa909" },
93
- },
94
- {
95
- name: "mcp__nlm-memory__get_session",
96
- input: { id: "hm_20260427_6ff562" },
97
- },
98
- ],
99
- surfacedIds: [
100
- "cc_sub_a139f4ab7ca5aa909",
101
- "hm_20260427_6ff562",
102
- "cc_unused_id_xyz",
103
- ],
104
- });
105
- expect(result.map((c) => c.id).sort()).toEqual(
106
- ["cc_sub_a139f4ab7ca5aa909", "hm_20260427_6ff562"].sort(),
107
- );
108
- expect(result.every((c) => c.kind === "tool_use")).toBe(true);
109
- });
110
-
111
- it("recognizes recall_sessions tool calls with query+limit (no direct id) — does not emit when no ID present in input", () => {
112
- const result = detectCitations({
113
- responseText: "",
114
- toolUses: [
115
- {
116
- name: "mcp__nlm-memory__recall_sessions",
117
- input: { query: "FTS5 vs pgvector", limit: 5 },
118
- },
119
- ],
120
- surfacedIds: ["cc_sub_a139f4ab7ca5aa909"],
121
- });
122
- expect(result).toEqual([]);
123
- });
124
- });
@@ -1,29 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { classifyPrompt } from "../../../../src/core/hook/gate.js";
3
-
4
- describe("classifyPrompt", () => {
5
- it("classifies obvious generative openers as generative", () => {
6
- expect(classifyPrompt("draft a LinkedIn post about FTS5")).toBe("generative");
7
- expect(classifyPrompt("write the migration")).toBe("generative");
8
- expect(classifyPrompt("brainstorm names for the feature")).toBe("generative");
9
- expect(classifyPrompt("Create a test file")).toBe("generative");
10
- });
11
-
12
- it("classifies retrospective prompts as evaluate", () => {
13
- expect(classifyPrompt("what did we decide about pgvector")).toBe("evaluate");
14
- expect(classifyPrompt("have I worked with this client before")).toBe("evaluate");
15
- expect(classifyPrompt("why is the recall backend returning zero results")).toBe("evaluate");
16
- });
17
-
18
- it("strips leading filler before checking the opener", () => {
19
- expect(classifyPrompt("can you write a script")).toBe("generative");
20
- expect(classifyPrompt("please draft the email")).toBe("generative");
21
- expect(classifyPrompt("could you tell me what we decided")).toBe("evaluate");
22
- });
23
-
24
- it("defaults to evaluate for empty or ambiguous prompts", () => {
25
- expect(classifyPrompt("")).toBe("evaluate");
26
- expect(classifyPrompt("the FTS5 work")).toBe("evaluate");
27
- expect(classifyPrompt("fix the failing test")).toBe("evaluate");
28
- });
29
- });
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { formatPointerBlock } from "../../../../src/core/hook/pointer-block.js";
3
-
4
- describe("formatPointerBlock", () => {
5
- it("returns an empty string for no hits", () => {
6
- expect(formatPointerBlock([])).toBe("");
7
- });
8
-
9
- it("renders a header, one line per hit, and the tool footer", () => {
10
- const block = formatPointerBlock([
11
- { id: "sess_a", label: "FTS5 vs pgvector decision", startedAt: "2026-05-15T10:00:00.000Z" },
12
- { id: "sess_b", label: "Semantic recall via sqlite-vec", startedAt: "2026-05-17T09:30:00.000Z" },
13
- ]);
14
- expect(block).toContain("## Possibly-relevant prior sessions (nlm-memory)");
15
- expect(block).toContain("- sess_a · FTS5 vs pgvector decision (2026-05-15)");
16
- expect(block).toContain("- sess_b · Semantic recall via sqlite-vec (2026-05-17)");
17
- expect(block).toContain("recall_sessions");
18
- expect(block).toContain("get_session");
19
- expect(block).toContain("recall_facts");
20
- expect(block).toContain("get_fact_history");
21
- });
22
- });