shieldcortex 2.0.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 (377) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -0
  3. package/dashboard/components.json +22 -0
  4. package/dashboard/eslint.config.mjs +42 -0
  5. package/dashboard/next.config.ts +7 -0
  6. package/dashboard/package-lock.json +8053 -0
  7. package/dashboard/package.json +44 -0
  8. package/dashboard/postcss.config.mjs +7 -0
  9. package/dashboard/public/file.svg +1 -0
  10. package/dashboard/public/globe.svg +1 -0
  11. package/dashboard/public/next.svg +1 -0
  12. package/dashboard/public/vercel.svg +1 -0
  13. package/dashboard/public/window.svg +1 -0
  14. package/dashboard/scripts/ensure-api.mjs +76 -0
  15. package/dashboard/src/app/error.tsx +49 -0
  16. package/dashboard/src/app/favicon.ico +0 -0
  17. package/dashboard/src/app/globals.css +130 -0
  18. package/dashboard/src/app/layout.tsx +35 -0
  19. package/dashboard/src/app/page.tsx +364 -0
  20. package/dashboard/src/components/Providers.tsx +27 -0
  21. package/dashboard/src/components/brain/ActivityPulseSystem.tsx +229 -0
  22. package/dashboard/src/components/brain/BrainMesh.tsx +133 -0
  23. package/dashboard/src/components/brain/BrainRegions.tsx +254 -0
  24. package/dashboard/src/components/brain/BrainScene.tsx +255 -0
  25. package/dashboard/src/components/brain/CategoryLabels.tsx +103 -0
  26. package/dashboard/src/components/brain/CoreSphere.tsx +215 -0
  27. package/dashboard/src/components/brain/DataFlowParticles.tsx +123 -0
  28. package/dashboard/src/components/brain/DataStreamRings.tsx +161 -0
  29. package/dashboard/src/components/brain/ElectronFlow.tsx +323 -0
  30. package/dashboard/src/components/brain/HolographicGrid.tsx +235 -0
  31. package/dashboard/src/components/brain/MemoryLinks.tsx +271 -0
  32. package/dashboard/src/components/brain/MemoryNode.tsx +245 -0
  33. package/dashboard/src/components/brain/NeuralPathways.tsx +441 -0
  34. package/dashboard/src/components/brain/SynapseNodes.tsx +312 -0
  35. package/dashboard/src/components/brain/TimelineControls.tsx +205 -0
  36. package/dashboard/src/components/chip/ChipScene.tsx +497 -0
  37. package/dashboard/src/components/chip/ChipSubstrate.tsx +238 -0
  38. package/dashboard/src/components/chip/CortexCore.tsx +210 -0
  39. package/dashboard/src/components/chip/DataBus.tsx +416 -0
  40. package/dashboard/src/components/chip/MemoryCell.tsx +225 -0
  41. package/dashboard/src/components/chip/MemoryGrid.tsx +328 -0
  42. package/dashboard/src/components/chip/QuantumCell.tsx +316 -0
  43. package/dashboard/src/components/chip/SectionLabel.tsx +113 -0
  44. package/dashboard/src/components/chip/index.ts +14 -0
  45. package/dashboard/src/components/controls/ControlPanel.tsx +106 -0
  46. package/dashboard/src/components/controls/VersionPanel.tsx +185 -0
  47. package/dashboard/src/components/dashboard/StatsPanel.tsx +164 -0
  48. package/dashboard/src/components/debug/ActivityLog.tsx +250 -0
  49. package/dashboard/src/components/debug/DebugPanel.tsx +101 -0
  50. package/dashboard/src/components/debug/QueryTester.tsx +192 -0
  51. package/dashboard/src/components/debug/RelationshipGraph.tsx +403 -0
  52. package/dashboard/src/components/debug/SqlConsole.tsx +319 -0
  53. package/dashboard/src/components/graph/KnowledgeGraph.tsx +230 -0
  54. package/dashboard/src/components/graph/OntologyGraph.tsx +631 -0
  55. package/dashboard/src/components/insights/ActivityHeatmap.tsx +131 -0
  56. package/dashboard/src/components/insights/InsightsView.tsx +46 -0
  57. package/dashboard/src/components/insights/KnowledgeMapPanel.tsx +80 -0
  58. package/dashboard/src/components/insights/QualityPanel.tsx +116 -0
  59. package/dashboard/src/components/memories/MemoriesView.tsx +150 -0
  60. package/dashboard/src/components/memories/MemoryCard.tsx +103 -0
  61. package/dashboard/src/components/memory/MemoryDetail.tsx +325 -0
  62. package/dashboard/src/components/nav/NavRail.tsx +54 -0
  63. package/dashboard/src/components/ui/button.tsx +62 -0
  64. package/dashboard/src/components/ui/card.tsx +92 -0
  65. package/dashboard/src/components/ui/input.tsx +21 -0
  66. package/dashboard/src/hooks/useDebouncedValue.ts +24 -0
  67. package/dashboard/src/hooks/useMemories.ts +458 -0
  68. package/dashboard/src/hooks/useSuggestions.ts +46 -0
  69. package/dashboard/src/lib/category-colors.ts +84 -0
  70. package/dashboard/src/lib/position-algorithm.ts +177 -0
  71. package/dashboard/src/lib/simplex-noise.ts +217 -0
  72. package/dashboard/src/lib/store.ts +88 -0
  73. package/dashboard/src/lib/utils.ts +6 -0
  74. package/dashboard/src/lib/websocket.ts +249 -0
  75. package/dashboard/src/types/memory.ts +73 -0
  76. package/dashboard/tsconfig.json +34 -0
  77. package/dist/__tests__/consolidation-merge.test.d.ts +9 -0
  78. package/dist/__tests__/consolidation-merge.test.d.ts.map +1 -0
  79. package/dist/__tests__/consolidation-merge.test.js +137 -0
  80. package/dist/__tests__/consolidation-merge.test.js.map +1 -0
  81. package/dist/__tests__/contradictions.test.d.ts +8 -0
  82. package/dist/__tests__/contradictions.test.d.ts.map +1 -0
  83. package/dist/__tests__/contradictions.test.js +78 -0
  84. package/dist/__tests__/contradictions.test.js.map +1 -0
  85. package/dist/__tests__/salience-evolution.test.d.ts +7 -0
  86. package/dist/__tests__/salience-evolution.test.d.ts.map +1 -0
  87. package/dist/__tests__/salience-evolution.test.js +151 -0
  88. package/dist/__tests__/salience-evolution.test.js.map +1 -0
  89. package/dist/__tests__/store.test.d.ts +7 -0
  90. package/dist/__tests__/store.test.d.ts.map +1 -0
  91. package/dist/__tests__/store.test.js +582 -0
  92. package/dist/__tests__/store.test.js.map +1 -0
  93. package/dist/api/control.d.ts +27 -0
  94. package/dist/api/control.d.ts.map +1 -0
  95. package/dist/api/control.js +60 -0
  96. package/dist/api/control.js.map +1 -0
  97. package/dist/api/events.d.ts +159 -0
  98. package/dist/api/events.d.ts.map +1 -0
  99. package/dist/api/events.js +155 -0
  100. package/dist/api/events.js.map +1 -0
  101. package/dist/api/version.d.ts +36 -0
  102. package/dist/api/version.d.ts.map +1 -0
  103. package/dist/api/version.js +146 -0
  104. package/dist/api/version.js.map +1 -0
  105. package/dist/api/visualization-server.d.ts +11 -0
  106. package/dist/api/visualization-server.d.ts.map +1 -0
  107. package/dist/api/visualization-server.js +1186 -0
  108. package/dist/api/visualization-server.js.map +1 -0
  109. package/dist/context/project-context.d.ts +57 -0
  110. package/dist/context/project-context.d.ts.map +1 -0
  111. package/dist/context/project-context.js +135 -0
  112. package/dist/context/project-context.js.map +1 -0
  113. package/dist/database/init.d.ts +49 -0
  114. package/dist/database/init.d.ts.map +1 -0
  115. package/dist/database/init.js +567 -0
  116. package/dist/database/init.js.map +1 -0
  117. package/dist/defence/__tests__/firewall.test.d.ts +8 -0
  118. package/dist/defence/__tests__/firewall.test.d.ts.map +1 -0
  119. package/dist/defence/__tests__/firewall.test.js +123 -0
  120. package/dist/defence/__tests__/firewall.test.js.map +1 -0
  121. package/dist/defence/__tests__/fragmentation.test.d.ts +7 -0
  122. package/dist/defence/__tests__/fragmentation.test.d.ts.map +1 -0
  123. package/dist/defence/__tests__/fragmentation.test.js +51 -0
  124. package/dist/defence/__tests__/fragmentation.test.js.map +1 -0
  125. package/dist/defence/__tests__/pipeline.test.d.ts +8 -0
  126. package/dist/defence/__tests__/pipeline.test.d.ts.map +1 -0
  127. package/dist/defence/__tests__/pipeline.test.js +61 -0
  128. package/dist/defence/__tests__/pipeline.test.js.map +1 -0
  129. package/dist/defence/__tests__/sensitivity.test.d.ts +7 -0
  130. package/dist/defence/__tests__/sensitivity.test.d.ts.map +1 -0
  131. package/dist/defence/__tests__/sensitivity.test.js +61 -0
  132. package/dist/defence/__tests__/sensitivity.test.js.map +1 -0
  133. package/dist/defence/__tests__/trust.test.d.ts +7 -0
  134. package/dist/defence/__tests__/trust.test.d.ts.map +1 -0
  135. package/dist/defence/__tests__/trust.test.js +49 -0
  136. package/dist/defence/__tests__/trust.test.js.map +1 -0
  137. package/dist/defence/audit/index.d.ts +4 -0
  138. package/dist/defence/audit/index.d.ts.map +1 -0
  139. package/dist/defence/audit/index.js +3 -0
  140. package/dist/defence/audit/index.js.map +1 -0
  141. package/dist/defence/audit/logger.d.ts +14 -0
  142. package/dist/defence/audit/logger.d.ts.map +1 -0
  143. package/dist/defence/audit/logger.js +54 -0
  144. package/dist/defence/audit/logger.js.map +1 -0
  145. package/dist/defence/audit/queries.d.ts +33 -0
  146. package/dist/defence/audit/queries.d.ts.map +1 -0
  147. package/dist/defence/audit/queries.js +103 -0
  148. package/dist/defence/audit/queries.js.map +1 -0
  149. package/dist/defence/firewall/anomaly-scorer.d.ts +8 -0
  150. package/dist/defence/firewall/anomaly-scorer.d.ts.map +1 -0
  151. package/dist/defence/firewall/anomaly-scorer.js +58 -0
  152. package/dist/defence/firewall/anomaly-scorer.js.map +1 -0
  153. package/dist/defence/firewall/encoding-detector.d.ts +13 -0
  154. package/dist/defence/firewall/encoding-detector.d.ts.map +1 -0
  155. package/dist/defence/firewall/encoding-detector.js +120 -0
  156. package/dist/defence/firewall/encoding-detector.js.map +1 -0
  157. package/dist/defence/firewall/index.d.ts +21 -0
  158. package/dist/defence/firewall/index.d.ts.map +1 -0
  159. package/dist/defence/firewall/index.js +133 -0
  160. package/dist/defence/firewall/index.js.map +1 -0
  161. package/dist/defence/firewall/instruction-detector.d.ts +12 -0
  162. package/dist/defence/firewall/instruction-detector.d.ts.map +1 -0
  163. package/dist/defence/firewall/instruction-detector.js +99 -0
  164. package/dist/defence/firewall/instruction-detector.js.map +1 -0
  165. package/dist/defence/firewall/privilege-detector.d.ts +13 -0
  166. package/dist/defence/firewall/privilege-detector.d.ts.map +1 -0
  167. package/dist/defence/firewall/privilege-detector.js +89 -0
  168. package/dist/defence/firewall/privilege-detector.js.map +1 -0
  169. package/dist/defence/fragmentation/assembly-detector.d.ts +18 -0
  170. package/dist/defence/fragmentation/assembly-detector.d.ts.map +1 -0
  171. package/dist/defence/fragmentation/assembly-detector.js +72 -0
  172. package/dist/defence/fragmentation/assembly-detector.js.map +1 -0
  173. package/dist/defence/fragmentation/entity-extractor.d.ts +19 -0
  174. package/dist/defence/fragmentation/entity-extractor.d.ts.map +1 -0
  175. package/dist/defence/fragmentation/entity-extractor.js +86 -0
  176. package/dist/defence/fragmentation/entity-extractor.js.map +1 -0
  177. package/dist/defence/fragmentation/index.d.ts +23 -0
  178. package/dist/defence/fragmentation/index.d.ts.map +1 -0
  179. package/dist/defence/fragmentation/index.js +49 -0
  180. package/dist/defence/fragmentation/index.js.map +1 -0
  181. package/dist/defence/fragmentation/temporal-analyzer.d.ts +28 -0
  182. package/dist/defence/fragmentation/temporal-analyzer.d.ts.map +1 -0
  183. package/dist/defence/fragmentation/temporal-analyzer.js +41 -0
  184. package/dist/defence/fragmentation/temporal-analyzer.js.map +1 -0
  185. package/dist/defence/index.d.ts +12 -0
  186. package/dist/defence/index.d.ts.map +1 -0
  187. package/dist/defence/index.js +18 -0
  188. package/dist/defence/index.js.map +1 -0
  189. package/dist/defence/pipeline.d.ts +9 -0
  190. package/dist/defence/pipeline.d.ts.map +1 -0
  191. package/dist/defence/pipeline.js +115 -0
  192. package/dist/defence/pipeline.js.map +1 -0
  193. package/dist/defence/scanner/index.d.ts +5 -0
  194. package/dist/defence/scanner/index.d.ts.map +1 -0
  195. package/dist/defence/scanner/index.js +5 -0
  196. package/dist/defence/scanner/index.js.map +1 -0
  197. package/dist/defence/scanner/scan-existing.d.ts +34 -0
  198. package/dist/defence/scanner/scan-existing.d.ts.map +1 -0
  199. package/dist/defence/scanner/scan-existing.js +136 -0
  200. package/dist/defence/scanner/scan-existing.js.map +1 -0
  201. package/dist/defence/sensitivity/classifier.d.ts +6 -0
  202. package/dist/defence/sensitivity/classifier.d.ts.map +1 -0
  203. package/dist/defence/sensitivity/classifier.js +50 -0
  204. package/dist/defence/sensitivity/classifier.js.map +1 -0
  205. package/dist/defence/sensitivity/index.d.ts +11 -0
  206. package/dist/defence/sensitivity/index.d.ts.map +1 -0
  207. package/dist/defence/sensitivity/index.js +13 -0
  208. package/dist/defence/sensitivity/index.js.map +1 -0
  209. package/dist/defence/sensitivity/patterns.d.ts +14 -0
  210. package/dist/defence/sensitivity/patterns.d.ts.map +1 -0
  211. package/dist/defence/sensitivity/patterns.js +67 -0
  212. package/dist/defence/sensitivity/patterns.js.map +1 -0
  213. package/dist/defence/sensitivity/redaction.d.ts +17 -0
  214. package/dist/defence/sensitivity/redaction.d.ts.map +1 -0
  215. package/dist/defence/sensitivity/redaction.js +47 -0
  216. package/dist/defence/sensitivity/redaction.js.map +1 -0
  217. package/dist/defence/trust/index.d.ts +3 -0
  218. package/dist/defence/trust/index.d.ts.map +1 -0
  219. package/dist/defence/trust/index.js +3 -0
  220. package/dist/defence/trust/index.js.map +1 -0
  221. package/dist/defence/trust/recall-filter.d.ts +10 -0
  222. package/dist/defence/trust/recall-filter.d.ts.map +1 -0
  223. package/dist/defence/trust/recall-filter.js +38 -0
  224. package/dist/defence/trust/recall-filter.js.map +1 -0
  225. package/dist/defence/trust/source-scorer.d.ts +6 -0
  226. package/dist/defence/trust/source-scorer.d.ts.map +1 -0
  227. package/dist/defence/trust/source-scorer.js +34 -0
  228. package/dist/defence/trust/source-scorer.js.map +1 -0
  229. package/dist/defence/types.d.ts +88 -0
  230. package/dist/defence/types.d.ts.map +1 -0
  231. package/dist/defence/types.js +15 -0
  232. package/dist/defence/types.js.map +1 -0
  233. package/dist/embeddings/generator.d.ts +20 -0
  234. package/dist/embeddings/generator.d.ts.map +1 -0
  235. package/dist/embeddings/generator.js +83 -0
  236. package/dist/embeddings/generator.js.map +1 -0
  237. package/dist/embeddings/index.d.ts +2 -0
  238. package/dist/embeddings/index.d.ts.map +1 -0
  239. package/dist/embeddings/index.js +2 -0
  240. package/dist/embeddings/index.js.map +1 -0
  241. package/dist/errors.d.ts +74 -0
  242. package/dist/errors.d.ts.map +1 -0
  243. package/dist/errors.js +131 -0
  244. package/dist/errors.js.map +1 -0
  245. package/dist/graph/backfill.d.ts +6 -0
  246. package/dist/graph/backfill.d.ts.map +1 -0
  247. package/dist/graph/backfill.js +33 -0
  248. package/dist/graph/backfill.js.map +1 -0
  249. package/dist/graph/extract.d.ts +21 -0
  250. package/dist/graph/extract.d.ts.map +1 -0
  251. package/dist/graph/extract.js +231 -0
  252. package/dist/graph/extract.js.map +1 -0
  253. package/dist/graph/resolve.d.ts +6 -0
  254. package/dist/graph/resolve.d.ts.map +1 -0
  255. package/dist/graph/resolve.js +126 -0
  256. package/dist/graph/resolve.js.map +1 -0
  257. package/dist/index.d.ts +31 -0
  258. package/dist/index.d.ts.map +1 -0
  259. package/dist/index.js +248 -0
  260. package/dist/index.js.map +1 -0
  261. package/dist/memory/activation.d.ts +69 -0
  262. package/dist/memory/activation.d.ts.map +1 -0
  263. package/dist/memory/activation.js +168 -0
  264. package/dist/memory/activation.js.map +1 -0
  265. package/dist/memory/consolidate.d.ts +98 -0
  266. package/dist/memory/consolidate.d.ts.map +1 -0
  267. package/dist/memory/consolidate.js +511 -0
  268. package/dist/memory/consolidate.js.map +1 -0
  269. package/dist/memory/contradiction.d.ts +69 -0
  270. package/dist/memory/contradiction.d.ts.map +1 -0
  271. package/dist/memory/contradiction.js +286 -0
  272. package/dist/memory/contradiction.js.map +1 -0
  273. package/dist/memory/decay.d.ts +62 -0
  274. package/dist/memory/decay.d.ts.map +1 -0
  275. package/dist/memory/decay.js +184 -0
  276. package/dist/memory/decay.js.map +1 -0
  277. package/dist/memory/salience.d.ts +36 -0
  278. package/dist/memory/salience.d.ts.map +1 -0
  279. package/dist/memory/salience.js +216 -0
  280. package/dist/memory/salience.js.map +1 -0
  281. package/dist/memory/similarity.d.ts +57 -0
  282. package/dist/memory/similarity.d.ts.map +1 -0
  283. package/dist/memory/similarity.js +114 -0
  284. package/dist/memory/similarity.js.map +1 -0
  285. package/dist/memory/store.d.ts +179 -0
  286. package/dist/memory/store.d.ts.map +1 -0
  287. package/dist/memory/store.js +1184 -0
  288. package/dist/memory/store.js.map +1 -0
  289. package/dist/memory/types.d.ts +97 -0
  290. package/dist/memory/types.d.ts.map +1 -0
  291. package/dist/memory/types.js +30 -0
  292. package/dist/memory/types.js.map +1 -0
  293. package/dist/server.d.ts +12 -0
  294. package/dist/server.d.ts.map +1 -0
  295. package/dist/server.js +568 -0
  296. package/dist/server.js.map +1 -0
  297. package/dist/service/install.d.ts +15 -0
  298. package/dist/service/install.d.ts.map +1 -0
  299. package/dist/service/install.js +178 -0
  300. package/dist/service/install.js.map +1 -0
  301. package/dist/service/templates.d.ts +13 -0
  302. package/dist/service/templates.d.ts.map +1 -0
  303. package/dist/service/templates.js +58 -0
  304. package/dist/service/templates.js.map +1 -0
  305. package/dist/setup/claude-md.d.ts +12 -0
  306. package/dist/setup/claude-md.d.ts.map +1 -0
  307. package/dist/setup/claude-md.js +68 -0
  308. package/dist/setup/claude-md.js.map +1 -0
  309. package/dist/setup/clawdbot.d.ts +15 -0
  310. package/dist/setup/clawdbot.d.ts.map +1 -0
  311. package/dist/setup/clawdbot.js +118 -0
  312. package/dist/setup/clawdbot.js.map +1 -0
  313. package/dist/setup/doctor.d.ts +5 -0
  314. package/dist/setup/doctor.d.ts.map +1 -0
  315. package/dist/setup/doctor.js +141 -0
  316. package/dist/setup/doctor.js.map +1 -0
  317. package/dist/setup/hooks.d.ts +6 -0
  318. package/dist/setup/hooks.d.ts.map +1 -0
  319. package/dist/setup/hooks.js +36 -0
  320. package/dist/setup/hooks.js.map +1 -0
  321. package/dist/setup/migrate.d.ts +16 -0
  322. package/dist/setup/migrate.d.ts.map +1 -0
  323. package/dist/setup/migrate.js +164 -0
  324. package/dist/setup/migrate.js.map +1 -0
  325. package/dist/setup/settings-hooks.d.ts +7 -0
  326. package/dist/setup/settings-hooks.d.ts.map +1 -0
  327. package/dist/setup/settings-hooks.js +83 -0
  328. package/dist/setup/settings-hooks.js.map +1 -0
  329. package/dist/setup/uninstall.d.ts +12 -0
  330. package/dist/setup/uninstall.d.ts.map +1 -0
  331. package/dist/setup/uninstall.js +125 -0
  332. package/dist/setup/uninstall.js.map +1 -0
  333. package/dist/tools/context.d.ts +135 -0
  334. package/dist/tools/context.d.ts.map +1 -0
  335. package/dist/tools/context.js +273 -0
  336. package/dist/tools/context.js.map +1 -0
  337. package/dist/tools/forget.d.ts +53 -0
  338. package/dist/tools/forget.d.ts.map +1 -0
  339. package/dist/tools/forget.js +179 -0
  340. package/dist/tools/forget.js.map +1 -0
  341. package/dist/tools/graph.d.ts +46 -0
  342. package/dist/tools/graph.d.ts.map +1 -0
  343. package/dist/tools/graph.js +206 -0
  344. package/dist/tools/graph.js.map +1 -0
  345. package/dist/tools/recall.d.ts +79 -0
  346. package/dist/tools/recall.d.ts.map +1 -0
  347. package/dist/tools/recall.js +156 -0
  348. package/dist/tools/recall.js.map +1 -0
  349. package/dist/tools/remember.d.ts +83 -0
  350. package/dist/tools/remember.d.ts.map +1 -0
  351. package/dist/tools/remember.js +151 -0
  352. package/dist/tools/remember.js.map +1 -0
  353. package/dist/worker/brain-worker.d.ts +100 -0
  354. package/dist/worker/brain-worker.d.ts.map +1 -0
  355. package/dist/worker/brain-worker.js +283 -0
  356. package/dist/worker/brain-worker.js.map +1 -0
  357. package/dist/worker/link-discovery.d.ts +47 -0
  358. package/dist/worker/link-discovery.d.ts.map +1 -0
  359. package/dist/worker/link-discovery.js +103 -0
  360. package/dist/worker/link-discovery.js.map +1 -0
  361. package/dist/worker/predictive-consolidation.d.ts +46 -0
  362. package/dist/worker/predictive-consolidation.d.ts.map +1 -0
  363. package/dist/worker/predictive-consolidation.js +110 -0
  364. package/dist/worker/predictive-consolidation.js.map +1 -0
  365. package/dist/worker/types.d.ts +91 -0
  366. package/dist/worker/types.d.ts.map +1 -0
  367. package/dist/worker/types.js +22 -0
  368. package/dist/worker/types.js.map +1 -0
  369. package/hooks/clawdbot/cortex-memory/HOOK.md +71 -0
  370. package/hooks/clawdbot/cortex-memory/handler.js +279 -0
  371. package/package.json +73 -0
  372. package/scripts/pre-compact-hook.mjs +716 -0
  373. package/scripts/session-end-hook.mjs +548 -0
  374. package/scripts/session-start-hook.mjs +221 -0
  375. package/scripts/start-dashboard.sh +41 -0
  376. package/scripts/stop-dashboard.sh +21 -0
  377. package/scripts/stop-hook.mjs +163 -0
@@ -0,0 +1,1184 @@
1
+ /**
2
+ * Memory Store
3
+ *
4
+ * Core CRUD operations for the memory database.
5
+ * Handles storage, retrieval, and management of memories.
6
+ */
7
+ import { getDatabase } from '../database/init.js';
8
+ import { DEFAULT_CONFIG, } from './types.js';
9
+ import { calculateSalience, suggestCategory, extractTags, } from './salience.js';
10
+ import { calculateDecayedScore, calculateReinforcementBoost, calculatePriority, } from './decay.js';
11
+ import { activateMemory as spreadActivation, getActivationBoost, pruneActivationCache, } from './activation.js';
12
+ import { jaccardSimilarity } from './similarity.js';
13
+ import { emitMemoryCreated, emitMemoryAccessed, emitMemoryDeleted, emitMemoryUpdated, persistEvent, } from '../api/events.js';
14
+ import { generateEmbedding, cosineSimilarity } from '../embeddings/index.js';
15
+ import { isPaused } from '../api/control.js';
16
+ import { extractFromMemory } from '../graph/extract.js';
17
+ import { processExtractionResult } from '../graph/resolve.js';
18
+ import { runDefencePipeline, storeFragmentationData } from '../defence/index.js';
19
+ // Anti-bloat: Maximum content size per memory (10KB)
20
+ const MAX_CONTENT_SIZE = 10 * 1024;
21
+ // Track truncation info globally for the last addMemory call
22
+ let lastTruncationInfo = null;
23
+ /**
24
+ * Truncate content if it exceeds max size
25
+ * Returns both the content and truncation info
26
+ */
27
+ function truncateContent(content) {
28
+ const originalLength = content.length;
29
+ if (originalLength > MAX_CONTENT_SIZE) {
30
+ return {
31
+ content: content.slice(0, MAX_CONTENT_SIZE) + '\n\n[Content truncated - exceeded 10KB limit]',
32
+ wasTruncated: true,
33
+ originalLength,
34
+ };
35
+ }
36
+ return { content, wasTruncated: false, originalLength };
37
+ }
38
+ /**
39
+ * Get truncation info from the last addMemory call
40
+ */
41
+ export function getLastTruncationInfo() {
42
+ return lastTruncationInfo;
43
+ }
44
+ /**
45
+ * Escape FTS5 query to prevent syntax errors
46
+ * FTS5 interprets:
47
+ * - "word-word" as "column:value" syntax
48
+ * - AND, OR, NOT as boolean operators
49
+ * - &, | as boolean operators
50
+ * We quote individual terms to search them literally
51
+ */
52
+ function escapeFts5Query(query) {
53
+ // Split on whitespace, process each term, filter empty, rejoin
54
+ return query
55
+ .split(/\s+/)
56
+ .filter(term => term.length > 0)
57
+ .map(term => {
58
+ // FTS5 boolean operators - quote them to search literally
59
+ const upperTerm = term.toUpperCase();
60
+ if (upperTerm === 'AND' || upperTerm === 'OR' || upperTerm === 'NOT') {
61
+ return `"${term}"`;
62
+ }
63
+ // If term contains special FTS5 characters, quote it
64
+ // Including: - : * ^ ( ) & | . , ; ! ? and quotes
65
+ if (/[^a-zA-Z0-9_]/.test(term)) {
66
+ // Escape existing quotes and wrap in quotes
67
+ return `"${term.replace(/"/g, '""')}"`;
68
+ }
69
+ return term;
70
+ })
71
+ .join(' ');
72
+ }
73
+ /**
74
+ * Convert database row to Memory object
75
+ */
76
+ export function rowToMemory(row) {
77
+ return {
78
+ id: row.id,
79
+ type: row.type,
80
+ category: row.category,
81
+ title: row.title,
82
+ content: row.content,
83
+ project: row.project,
84
+ tags: JSON.parse(row.tags || '[]'),
85
+ salience: row.salience,
86
+ accessCount: row.access_count,
87
+ lastAccessed: new Date(row.last_accessed),
88
+ createdAt: new Date(row.created_at),
89
+ decayedScore: row.decayed_score ?? row.salience,
90
+ metadata: JSON.parse(row.metadata || '{}'),
91
+ embedding: row.embedding,
92
+ scope: row.scope ?? 'project',
93
+ transferable: Boolean(row.transferable),
94
+ };
95
+ }
96
+ /**
97
+ * Detect if memory content suggests global applicability
98
+ * Used to auto-set scope to 'global' for transferable knowledge
99
+ */
100
+ function detectGlobalPattern(content, category, tags) {
101
+ const globalCategories = ['pattern', 'preference', 'learning'];
102
+ const globalKeywords = ['always', 'never', 'best practice', 'general rule', 'universal'];
103
+ const globalTags = ['universal', 'global', 'general', 'cross-project'];
104
+ if (globalCategories.includes(category))
105
+ return true;
106
+ if (globalKeywords.some(k => content.toLowerCase().includes(k)))
107
+ return true;
108
+ if (tags.some(t => globalTags.includes(t.toLowerCase())))
109
+ return true;
110
+ return false;
111
+ }
112
+ /**
113
+ * Error thrown when memory creation is paused
114
+ */
115
+ export class MemoryPausedError extends Error {
116
+ constructor() {
117
+ super('Memory creation is currently paused. Use the dashboard to resume.');
118
+ this.name = 'MemoryPausedError';
119
+ }
120
+ }
121
+ /**
122
+ * Error thrown when memory is blocked by the defence firewall
123
+ */
124
+ export class MemoryBlockedError extends Error {
125
+ constructor(reason) {
126
+ super(`Memory blocked by defence firewall: ${reason}`);
127
+ this.name = 'MemoryBlockedError';
128
+ }
129
+ }
130
+ /**
131
+ * Store a blocked memory in quarantine for later review
132
+ */
133
+ function quarantineMemory(input, source, result) {
134
+ try {
135
+ const db = getDatabase();
136
+ db.prepare(`INSERT INTO quarantine (title, content, source, trust_score, sensitivity_level, block_reason, threat_indicators, fragmentation_score, audit_id, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')`)
137
+ .run(input.title, input.content, `${source.type}:${source.identifier}`, result.trust.score, result.sensitivity.level, result.firewall.reason, JSON.stringify(result.firewall.threatIndicators), result.fragmentation?.score ?? null, result.auditId);
138
+ }
139
+ catch (e) {
140
+ console.error('[shieldcortex] Failed to quarantine memory:', e);
141
+ }
142
+ }
143
+ /**
144
+ * Add a new memory
145
+ */
146
+ export function addMemory(input, config = DEFAULT_CONFIG, source) {
147
+ // Check if memory creation is paused
148
+ if (isPaused()) {
149
+ throw new MemoryPausedError();
150
+ }
151
+ // DEFENCE PIPELINE: Scan content before storage
152
+ let defenceResult = null;
153
+ if (source) {
154
+ defenceResult = runDefencePipeline(input.content, input.title, source);
155
+ if (!defenceResult.allowed) {
156
+ // Store in quarantine instead of memory
157
+ quarantineMemory(input, source, defenceResult);
158
+ throw new MemoryBlockedError(defenceResult.firewall.reason);
159
+ }
160
+ }
161
+ const db = getDatabase();
162
+ // Calculate salience if not provided
163
+ const salience = input.salience ?? calculateSalience(input);
164
+ // Suggest category if not provided
165
+ const category = input.category ?? suggestCategory(input);
166
+ // Extract tags
167
+ const tags = extractTags(input);
168
+ // Determine type
169
+ const type = input.type ?? (salience >= config.consolidationThreshold ? 'long_term' : 'short_term');
170
+ // Determine scope and transferable flag for cross-project knowledge
171
+ const scope = input.scope ??
172
+ (detectGlobalPattern(input.content, category, tags) ? 'global' : 'project');
173
+ const transferable = input.transferable ?? (scope === 'global' ? 1 : 0);
174
+ const stmt = db.prepare(`
175
+ INSERT INTO memories (type, category, title, content, project, tags, salience, metadata, scope, transferable)
176
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
177
+ `);
178
+ // Anti-bloat: Truncate content if too large
179
+ const truncationResult = truncateContent(input.content);
180
+ // Store truncation info for the remember tool to access
181
+ lastTruncationInfo = {
182
+ wasTruncated: truncationResult.wasTruncated,
183
+ originalLength: truncationResult.originalLength,
184
+ truncatedLength: truncationResult.content.length,
185
+ };
186
+ const result = stmt.run(type, category, input.title, truncationResult.content, input.project || null, JSON.stringify(tags), salience, JSON.stringify(input.metadata || {}), scope, transferable);
187
+ if (defenceResult) {
188
+ db.prepare(`UPDATE memories SET trust_score = ?, sensitivity_level = ?, source = ? WHERE id = ?`)
189
+ .run(defenceResult.trust.score, defenceResult.sensitivity.level, `${source.type}:${source.identifier}`, result.lastInsertRowid);
190
+ }
191
+ const memory = getMemoryById(result.lastInsertRowid);
192
+ // Emit event for real-time dashboard (in-process)
193
+ emitMemoryCreated(memory);
194
+ // Persist event for cross-process IPC (MCP → Dashboard)
195
+ persistEvent('memory_created', { memory });
196
+ // ORGANIC FEATURE: Auto-link to related memories
197
+ // This builds the knowledge graph automatically as memories are created
198
+ try {
199
+ const relationships = detectRelationships(memory);
200
+ for (const rel of relationships.slice(0, 3)) { // Top 3 most relevant
201
+ createMemoryLink(memory.id, rel.targetId, rel.relationship, rel.strength);
202
+ }
203
+ }
204
+ catch (e) {
205
+ // Don't fail memory creation if linking fails
206
+ console.error('[shieldcortex] Auto-link failed:', e);
207
+ }
208
+ // ONTOLOGY: Extract entities and triples from this memory
209
+ try {
210
+ const extraction = extractFromMemory(input.title, truncationResult.content, category);
211
+ if (extraction.entities.length > 0) {
212
+ processExtractionResult(extraction, memory.id);
213
+ }
214
+ }
215
+ catch (e) {
216
+ console.error('[shieldcortex] Entity extraction failed:', e);
217
+ }
218
+ // DEFENCE: Store fragmentation data for cross-memory payload detection
219
+ if (source) {
220
+ try {
221
+ storeFragmentationData(memory.id, truncationResult.content);
222
+ }
223
+ catch { }
224
+ }
225
+ // SEMANTIC SEARCH: Generate embedding asynchronously (don't block INSERT)
226
+ const memoryId = memory.id;
227
+ generateEmbedding(input.title + ' ' + truncationResult.content)
228
+ .then(embedding => {
229
+ try {
230
+ db.prepare('UPDATE memories SET embedding = ? WHERE id = ?')
231
+ .run(Buffer.from(embedding.buffer), memoryId);
232
+ }
233
+ catch (e) {
234
+ console.error('[shieldcortex] Failed to store embedding:', e);
235
+ }
236
+ })
237
+ .catch(e => {
238
+ console.error('[shieldcortex] Failed to generate embedding:', e);
239
+ });
240
+ // Anti-bloat: Check if limits exceeded and trigger async cleanup
241
+ // We use setImmediate to not block the insert response
242
+ setImmediate(() => {
243
+ try {
244
+ const stats = getMemoryStats();
245
+ if (stats.shortTerm > config.maxShortTermMemories ||
246
+ stats.longTerm > config.maxLongTermMemories) {
247
+ // Import dynamically to avoid circular dependency
248
+ import('./consolidate.js').then(({ enforceMemoryLimits }) => {
249
+ enforceMemoryLimits(config);
250
+ }).catch(() => {
251
+ // Silently ignore - consolidation will happen on next scheduled run
252
+ });
253
+ }
254
+ }
255
+ catch {
256
+ // Silently ignore errors in async cleanup
257
+ }
258
+ });
259
+ return memory;
260
+ }
261
+ /**
262
+ * Get a memory by ID
263
+ */
264
+ export function getMemoryById(id) {
265
+ const db = getDatabase();
266
+ const row = db.prepare('SELECT * FROM memories WHERE id = ?').get(id);
267
+ if (!row)
268
+ return null;
269
+ return rowToMemory(row);
270
+ }
271
+ /**
272
+ * Update a memory
273
+ */
274
+ export function updateMemory(id, updates) {
275
+ const db = getDatabase();
276
+ const existing = getMemoryById(id);
277
+ if (!existing)
278
+ return null;
279
+ const fields = [];
280
+ const values = [];
281
+ if (updates.title !== undefined) {
282
+ fields.push('title = ?');
283
+ values.push(updates.title);
284
+ }
285
+ if (updates.content !== undefined) {
286
+ fields.push('content = ?');
287
+ values.push(updates.content);
288
+ }
289
+ if (updates.type !== undefined) {
290
+ fields.push('type = ?');
291
+ values.push(updates.type);
292
+ }
293
+ if (updates.category !== undefined) {
294
+ fields.push('category = ?');
295
+ values.push(updates.category);
296
+ }
297
+ if (updates.project !== undefined) {
298
+ fields.push('project = ?');
299
+ values.push(updates.project);
300
+ }
301
+ if (updates.tags !== undefined) {
302
+ fields.push('tags = ?');
303
+ values.push(JSON.stringify(updates.tags));
304
+ }
305
+ if (updates.salience !== undefined) {
306
+ fields.push('salience = ?');
307
+ values.push(updates.salience);
308
+ }
309
+ if (updates.metadata !== undefined) {
310
+ fields.push('metadata = ?');
311
+ values.push(JSON.stringify(updates.metadata));
312
+ }
313
+ if (fields.length === 0)
314
+ return existing;
315
+ values.push(id);
316
+ db.prepare(`UPDATE memories SET ${fields.join(', ')} WHERE id = ?`).run(...values);
317
+ const updatedMemory = getMemoryById(id);
318
+ // Emit event for real-time dashboard (in-process)
319
+ emitMemoryUpdated(updatedMemory);
320
+ // Persist event for cross-process IPC (MCP → Dashboard)
321
+ persistEvent('memory_updated', { memory: updatedMemory });
322
+ return updatedMemory;
323
+ }
324
+ /**
325
+ * Delete a memory
326
+ */
327
+ export function deleteMemory(id) {
328
+ const db = getDatabase();
329
+ // Get memory before deletion for event
330
+ const memory = getMemoryById(id);
331
+ const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
332
+ // Emit event for real-time dashboard (in-process)
333
+ if (result.changes > 0 && memory) {
334
+ emitMemoryDeleted(id, memory.title);
335
+ // Persist event for cross-process IPC (MCP → Dashboard)
336
+ persistEvent('memory_deleted', { memoryId: id, title: memory.title });
337
+ }
338
+ return result.changes > 0;
339
+ }
340
+ /**
341
+ * Access a memory (updates access count and timestamp, returns reinforced memory)
342
+ */
343
+ export function accessMemory(id, config = DEFAULT_CONFIG) {
344
+ const db = getDatabase();
345
+ const memory = getMemoryById(id);
346
+ if (!memory)
347
+ return null;
348
+ // Calculate new salience with reinforcement
349
+ const newSalience = calculateReinforcementBoost(memory, config);
350
+ db.prepare(`
351
+ UPDATE memories
352
+ SET access_count = access_count + 1,
353
+ last_accessed = CURRENT_TIMESTAMP,
354
+ salience = ?
355
+ WHERE id = ?
356
+ `).run(newSalience, id);
357
+ const updatedMemory = getMemoryById(id);
358
+ // Emit event for real-time dashboard (in-process)
359
+ emitMemoryAccessed(updatedMemory, newSalience);
360
+ // Persist event for cross-process IPC (MCP → Dashboard)
361
+ persistEvent('memory_accessed', { memoryId: id, memory: updatedMemory, newSalience });
362
+ // ORGANIC FEATURE: Link strengthening on co-access
363
+ // If memory A and B are both accessed within 5 minutes, strengthen their link
364
+ // This mimics Hebbian learning: "neurons that fire together, wire together"
365
+ try {
366
+ const recentlyAccessed = db.prepare(`
367
+ SELECT id FROM memories
368
+ WHERE last_accessed > datetime('now', '-5 minutes')
369
+ AND id != ?
370
+ LIMIT 10
371
+ `).all(id);
372
+ for (const recent of recentlyAccessed) {
373
+ // Check if link exists in either direction
374
+ const existingLink = db.prepare(`
375
+ SELECT id, strength FROM memory_links
376
+ WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)
377
+ `).get(id, recent.id, recent.id, id);
378
+ if (existingLink) {
379
+ // Strengthen existing link (cap at 1.0)
380
+ const newStrength = Math.min(1.0, existingLink.strength + 0.05);
381
+ db.prepare('UPDATE memory_links SET strength = ? WHERE id = ?')
382
+ .run(newStrength, existingLink.id);
383
+ }
384
+ else {
385
+ // Create new weak link for co-accessed memories
386
+ createMemoryLink(id, recent.id, 'related', 0.2);
387
+ }
388
+ }
389
+ }
390
+ catch (e) {
391
+ // Don't fail memory access if link strengthening fails
392
+ console.error('[shieldcortex] Link strengthening failed:', e);
393
+ }
394
+ // ORGANIC FEATURE: Spreading Activation (Phase 2)
395
+ // Activate this memory and spread activation to linked memories
396
+ // This makes related memories easier to recall in subsequent searches
397
+ spreadActivation(id);
398
+ return updatedMemory;
399
+ }
400
+ /**
401
+ * Reinforce a memory that appeared in search results
402
+ * Gives a small salience boost with diminishing returns based on access count.
403
+ * ORGANIC FEATURE: Searched memories get reinforced, closing the feedback loop
404
+ * so frequently-found memories grow stronger over time.
405
+ */
406
+ export function reinforceFromSearch(memoryId) {
407
+ const db = getDatabase();
408
+ const memory = db.prepare('SELECT salience, access_count FROM memories WHERE id = ?').get(memoryId);
409
+ if (!memory)
410
+ return;
411
+ // Small salience boost per search appearance (diminishing returns)
412
+ const boost = Math.max(0.005, 0.02 / (1 + memory.access_count * 0.1));
413
+ const newSalience = Math.min(1.0, memory.salience + boost);
414
+ db.prepare(`
415
+ UPDATE memories
416
+ SET last_accessed = CURRENT_TIMESTAMP,
417
+ access_count = access_count + 1,
418
+ salience = ?
419
+ WHERE id = ?
420
+ `).run(newSalience, memoryId);
421
+ }
422
+ // ============================================
423
+ // ORGANIC FEATURE: Memory Enrichment (Phase 3)
424
+ // ============================================
425
+ // Enrichment configuration
426
+ const ENRICHMENT_SIMILARITY_THRESHOLD = 0.3; // Min similarity to trigger enrichment
427
+ const ENRICHMENT_COOLDOWN_HOURS = 1; // Don't enrich same memory within 1 hour
428
+ const MAX_ENRICHMENT_SIZE = 2000; // Max chars to add per enrichment
429
+ // Track last enrichment times (in-memory, ephemeral like activation cache)
430
+ const enrichmentTimestamps = new Map();
431
+ /**
432
+ * Enrich a memory with additional context
433
+ *
434
+ * This adds timestamped context to a memory when:
435
+ * 1. The new context is sufficiently related but different (new information)
436
+ * 2. The memory hasn't been enriched recently (cooldown)
437
+ * 3. The content won't exceed the size limit
438
+ *
439
+ * ORGANIC FEATURE: Memories grow with new context over time,
440
+ * mimicking how human memories are reconsolidated with new information
441
+ *
442
+ * @param memoryId - ID of the memory to enrich
443
+ * @param newContext - New context to add
444
+ * @param contextType - Type of context ('search' | 'access' | 'related')
445
+ * @returns EnrichmentResult indicating success or failure with reason
446
+ */
447
+ export function enrichMemory(memoryId, newContext, contextType = 'access') {
448
+ const db = getDatabase();
449
+ const memory = getMemoryById(memoryId);
450
+ if (!memory) {
451
+ return { enriched: false, reason: 'Memory not found' };
452
+ }
453
+ // Check cooldown
454
+ const lastEnrichment = enrichmentTimestamps.get(memoryId);
455
+ const now = Date.now();
456
+ if (lastEnrichment && (now - lastEnrichment) < ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000) {
457
+ return { enriched: false, reason: 'Enrichment cooldown active' };
458
+ }
459
+ // Check similarity - should be related but not too similar (new info)
460
+ const similarity = jaccardSimilarity(memory.content, newContext);
461
+ if (similarity > 0.8) {
462
+ return { enriched: false, reason: 'Context too similar (no new information)' };
463
+ }
464
+ if (similarity < ENRICHMENT_SIMILARITY_THRESHOLD) {
465
+ return { enriched: false, reason: 'Context not sufficiently related' };
466
+ }
467
+ // Truncate context if needed
468
+ const truncatedContext = newContext.slice(0, MAX_ENRICHMENT_SIZE);
469
+ // Build enrichment block with timestamp
470
+ const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
471
+ const enrichmentBlock = `\n\n---\n[${timestamp}] ${contextType}: ${truncatedContext}`;
472
+ // Check size limit (leave 500 char buffer for future enrichments)
473
+ const newContent = memory.content + enrichmentBlock;
474
+ if (newContent.length > MAX_CONTENT_SIZE - 500) {
475
+ return { enriched: false, reason: 'Content size limit reached' };
476
+ }
477
+ // Update memory
478
+ db.prepare(`
479
+ UPDATE memories
480
+ SET content = ?,
481
+ last_accessed = CURRENT_TIMESTAMP
482
+ WHERE id = ?
483
+ `).run(newContent, memoryId);
484
+ // Update cooldown timestamp
485
+ enrichmentTimestamps.set(memoryId, now);
486
+ // Emit update event for dashboard
487
+ const updatedMemory = getMemoryById(memoryId);
488
+ emitMemoryUpdated(updatedMemory);
489
+ return { enriched: true, reason: `Added ${contextType} context (${truncatedContext.length} chars)` };
490
+ }
491
+ /**
492
+ * Clear enrichment cooldown for a memory (for testing)
493
+ */
494
+ export function clearEnrichmentCooldown(memoryId) {
495
+ enrichmentTimestamps.delete(memoryId);
496
+ }
497
+ /**
498
+ * Get enrichment cooldown status for a memory
499
+ */
500
+ export function getEnrichmentCooldownStatus(memoryId) {
501
+ const lastEnrichment = enrichmentTimestamps.get(memoryId);
502
+ if (!lastEnrichment) {
503
+ return { onCooldown: false, remainingMs: 0 };
504
+ }
505
+ const cooldownMs = ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000;
506
+ const elapsed = Date.now() - lastEnrichment;
507
+ const remaining = Math.max(0, cooldownMs - elapsed);
508
+ return {
509
+ onCooldown: remaining > 0,
510
+ remainingMs: remaining,
511
+ };
512
+ }
513
+ /**
514
+ * Update persisted decay scores for all memories
515
+ * Called during consolidation and periodically by the API server
516
+ * Returns the number of memories updated
517
+ */
518
+ export function updateDecayScores() {
519
+ const db = getDatabase();
520
+ // Get all memories
521
+ const memories = db.prepare('SELECT * FROM memories').all();
522
+ let updated = 0;
523
+ const updateStmt = db.prepare('UPDATE memories SET decayed_score = ? WHERE id = ?');
524
+ for (const row of memories) {
525
+ const memory = rowToMemory(row);
526
+ const decayedScore = calculateDecayedScore(memory);
527
+ // Only update if score has changed significantly (saves writes)
528
+ const currentScore = row.decayed_score;
529
+ if (currentScore === null || Math.abs(currentScore - decayedScore) > 0.01) {
530
+ updateStmt.run(decayedScore, memory.id);
531
+ updated++;
532
+ }
533
+ }
534
+ return updated;
535
+ }
536
+ /**
537
+ * Detect the likely category a query is asking about
538
+ */
539
+ function detectQueryCategory(query) {
540
+ const lower = query.toLowerCase();
541
+ if (/architect|design|structure|pattern|system|schema|model/.test(lower)) {
542
+ return 'architecture';
543
+ }
544
+ if (/error|bug|fix|issue|crash|exception|fail|problem/.test(lower)) {
545
+ return 'error';
546
+ }
547
+ if (/prefer|always|never|style|convention|like|want/.test(lower)) {
548
+ return 'preference';
549
+ }
550
+ if (/learn|discover|realiz|found\s+out|turns?\s+out/.test(lower)) {
551
+ return 'learning';
552
+ }
553
+ if (/todo|task|pending|need\s+to|should\s+do/.test(lower)) {
554
+ return 'todo';
555
+ }
556
+ if (/relation|depend|connect|link|reference/.test(lower)) {
557
+ return 'relationship';
558
+ }
559
+ return null;
560
+ }
561
+ /**
562
+ * Calculate a boost for memories linked to high-salience memories
563
+ */
564
+ function calculateLinkBoost(memoryId, db) {
565
+ try {
566
+ // Get linked memories and their salience
567
+ const linked = db.prepare(`
568
+ SELECT m.salience, ml.strength
569
+ FROM memory_links ml
570
+ JOIN memories m ON (m.id = ml.target_id OR m.id = ml.source_id)
571
+ WHERE (ml.source_id = ? OR ml.target_id = ?)
572
+ AND m.id != ?
573
+ `).all(memoryId, memoryId, memoryId);
574
+ if (linked.length === 0)
575
+ return 0;
576
+ // Calculate weighted average of linked memory salience
577
+ const totalWeight = linked.reduce((sum, l) => sum + l.strength, 0);
578
+ if (totalWeight === 0)
579
+ return 0;
580
+ const weightedSalience = linked.reduce((sum, l) => sum + l.salience * l.strength, 0) / totalWeight;
581
+ // Cap boost at 0.15
582
+ return Math.min(0.15, weightedSalience * 0.2);
583
+ }
584
+ catch {
585
+ return 0;
586
+ }
587
+ }
588
+ /**
589
+ * Calculate partial tag match score
590
+ */
591
+ function calculateTagScore(queryTags, memoryTags) {
592
+ if (queryTags.length === 0 || memoryTags.length === 0)
593
+ return 0;
594
+ // Count partial matches (substring matching)
595
+ let matches = 0;
596
+ for (const qt of queryTags) {
597
+ const qtLower = qt.toLowerCase();
598
+ if (memoryTags.some(mt => mt.toLowerCase().includes(qtLower) || qtLower.includes(mt.toLowerCase()))) {
599
+ matches++;
600
+ }
601
+ }
602
+ return (matches / queryTags.length) * 0.1;
603
+ }
604
+ /**
605
+ * Extract potential tags from a query string
606
+ */
607
+ function extractQueryTags(query) {
608
+ // Extract words that might be tags (tech terms, project-specific terms)
609
+ const words = query.toLowerCase().split(/\s+/);
610
+ return words.filter(w => w.length > 2 &&
611
+ /^[a-z][a-z0-9-]*$/.test(w) &&
612
+ !['the', 'and', 'for', 'with', 'how', 'what', 'when', 'where', 'why'].includes(w));
613
+ }
614
+ /**
615
+ * Search memories by vector similarity
616
+ * Returns memories sorted by cosine similarity to the query embedding
617
+ */
618
+ function vectorSearch(queryEmbedding, limit, project, includeGlobal = true) {
619
+ const db = getDatabase();
620
+ // Get memories with embeddings
621
+ let query = `
622
+ SELECT * FROM memories
623
+ WHERE embedding IS NOT NULL
624
+ `;
625
+ const params = [];
626
+ if (project && includeGlobal) {
627
+ query += ` AND (project = ? OR scope = 'global')`;
628
+ params.push(project);
629
+ }
630
+ else if (project) {
631
+ query += ` AND project = ?`;
632
+ params.push(project);
633
+ }
634
+ const rows = db.prepare(query).all(...params);
635
+ // Calculate similarities
636
+ const results = rows
637
+ .map(row => {
638
+ const embeddingBuffer = row.embedding;
639
+ const embedding = new Float32Array(embeddingBuffer.buffer, embeddingBuffer.byteOffset, embeddingBuffer.length / 4);
640
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
641
+ return {
642
+ memory: rowToMemory(row),
643
+ similarity,
644
+ };
645
+ })
646
+ .filter(r => r.similarity > 0.3) // Threshold for relevance
647
+ .sort((a, b) => b.similarity - a.similarity)
648
+ .slice(0, limit);
649
+ return results;
650
+ }
651
+ /**
652
+ * Search memories using full-text search, vector similarity, and filters
653
+ * Now uses hybrid search combining FTS5 keywords with semantic vector matching
654
+ */
655
+ let searchCount = 0;
656
+ export async function searchMemories(options, config = DEFAULT_CONFIG) {
657
+ if (++searchCount % 100 === 0) {
658
+ pruneActivationCache();
659
+ }
660
+ const db = getDatabase();
661
+ const limit = options.limit || 20;
662
+ const includeGlobal = options.includeGlobal ?? true;
663
+ // Detect query category for boosting
664
+ const detectedCategory = options.query ? detectQueryCategory(options.query) : null;
665
+ const queryTags = options.query ? extractQueryTags(options.query) : [];
666
+ // SEMANTIC SEARCH: Generate query embedding (may fail on first call while model loads)
667
+ let queryEmbedding = null;
668
+ let vectorResults = new Map(); // memoryId -> similarity
669
+ if (options.query && options.query.trim()) {
670
+ try {
671
+ queryEmbedding = await generateEmbedding(options.query);
672
+ const vectorHits = vectorSearch(queryEmbedding, limit * 2, options.project, includeGlobal);
673
+ for (const hit of vectorHits) {
674
+ vectorResults.set(hit.memory.id, hit.similarity);
675
+ }
676
+ }
677
+ catch (e) {
678
+ // Vector search unavailable - fall back to FTS only
679
+ console.log('[shieldcortex] Vector search unavailable, using FTS only');
680
+ }
681
+ }
682
+ let sql;
683
+ const params = [];
684
+ if (options.query && options.query.trim()) {
685
+ // Use FTS search - escape query to prevent FTS5 syntax errors
686
+ // FTS5 interprets "word-word" as "column:value", so we quote terms
687
+ const escapedQuery = escapeFts5Query(options.query.trim());
688
+ sql = `
689
+ SELECT m.*, fts.rank
690
+ FROM memories m
691
+ JOIN memories_fts fts ON m.id = fts.rowid
692
+ WHERE memories_fts MATCH ?
693
+ `;
694
+ params.push(escapedQuery);
695
+ }
696
+ else {
697
+ // No query, just filter
698
+ sql = `SELECT *, 0 as rank FROM memories m WHERE 1=1`;
699
+ }
700
+ // Add filters - include global memories if enabled
701
+ if (options.project) {
702
+ if (includeGlobal) {
703
+ sql += ` AND (m.project = ? OR m.scope = 'global')`;
704
+ }
705
+ else {
706
+ sql += ' AND m.project = ?';
707
+ }
708
+ params.push(options.project);
709
+ }
710
+ if (options.category) {
711
+ sql += ' AND m.category = ?';
712
+ params.push(options.category);
713
+ }
714
+ if (options.type) {
715
+ sql += ' AND m.type = ?';
716
+ params.push(options.type);
717
+ }
718
+ if (options.minSalience) {
719
+ sql += ' AND m.salience >= ?';
720
+ params.push(options.minSalience);
721
+ }
722
+ if (options.tags && options.tags.length > 0) {
723
+ // Use json_each() for proper JSON array parsing
724
+ // This avoids false positives from LIKE matching (e.g., "api" matching "api-gateway")
725
+ const tagPlaceholders = options.tags.map(() => '?').join(',');
726
+ sql += ` AND EXISTS (
727
+ SELECT 1 FROM json_each(m.tags)
728
+ WHERE json_each.value IN (${tagPlaceholders})
729
+ )`;
730
+ params.push(...options.tags);
731
+ }
732
+ sql += ' ORDER BY m.salience DESC, m.last_accessed DESC LIMIT ?';
733
+ params.push(limit);
734
+ const rows = db.prepare(sql).all(...params);
735
+ // Convert to SearchResult with computed scores
736
+ const results = rows.map(row => {
737
+ const memory = rowToMemory(row);
738
+ const decayedScore = calculateDecayedScore(memory, config);
739
+ memory.decayedScore = decayedScore;
740
+ // Improved FTS score normalization (BM25-style)
741
+ // FTS5 rank is negative, closer to 0 = better match
742
+ const rawRank = row.rank;
743
+ const ftsScore = rawRank ? 1 / (1 + Math.abs(rawRank)) : 0.3;
744
+ // Recency boost for recently accessed memories
745
+ const hoursSinceAccess = (Date.now() - new Date(memory.lastAccessed).getTime()) / (1000 * 60 * 60);
746
+ const recencyBoost = hoursSinceAccess < 1 ? 0.1 : (hoursSinceAccess < 24 ? 0.05 : 0);
747
+ // Category match bonus
748
+ const categoryBoost = detectedCategory && memory.category === detectedCategory ? 0.1 : 0;
749
+ // Link boost - memories connected to high-salience memories rank higher
750
+ const linkBoost = calculateLinkBoost(memory.id, db);
751
+ // Partial tag match bonus
752
+ const tagBoost = calculateTagScore(queryTags, memory.tags);
753
+ // ORGANIC FEATURE: Spreading Activation boost (Phase 2)
754
+ // Recently accessed memories and their linked neighbors get a boost
755
+ const activationBoost = getActivationBoost(memory.id);
756
+ // SEMANTIC SEARCH: Vector similarity boost (Phase 5)
757
+ // If memory was found by vector search, add similarity as a boost
758
+ const vectorSimilarity = vectorResults.get(memory.id) || 0;
759
+ const vectorBoost = vectorSimilarity * 0.3; // 30% weight for vector similarity
760
+ // Combined relevance score (adjusted weights to accommodate vector)
761
+ const relevanceScore = (ftsScore * 0.25 + // Reduced from 0.3
762
+ vectorBoost + // New: 0-0.3 from vector similarity
763
+ decayedScore * 0.2 + // Reduced from 0.25
764
+ calculatePriority(memory) * 0.05 + // Reduced from 0.1
765
+ recencyBoost + categoryBoost + linkBoost + tagBoost + activationBoost);
766
+ return { memory, relevanceScore };
767
+ });
768
+ // Sort by relevance and filter out too-decayed memories
769
+ const sortedResults = results
770
+ .filter(r => options.includeDecayed || r.memory.decayedScore >= config.salienceThreshold)
771
+ .sort((a, b) => b.relevanceScore - a.relevanceScore);
772
+ // ORGANIC FEATURE: Reinforce top search results
773
+ // This closes the feedback loop - memories that appear in searches get a small
774
+ // salience boost with diminishing returns, so useful memories grow stronger
775
+ const topResults = sortedResults.slice(0, 5);
776
+ for (const result of topResults) {
777
+ reinforceFromSearch(result.memory.id);
778
+ }
779
+ // ORGANIC FEATURE: Link co-returned search results (top 5 only)
780
+ // Memories that frequently appear together in searches become linked,
781
+ // building the knowledge graph from usage patterns
782
+ if (topResults.length >= 2) {
783
+ for (let i = 0; i < topResults.length; i++) {
784
+ for (let j = i + 1; j < topResults.length; j++) {
785
+ const idA = topResults[i].memory.id;
786
+ const idB = topResults[j].memory.id;
787
+ const existing = db.prepare('SELECT strength FROM memory_links WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)').get(idA, idB, idB, idA);
788
+ if (existing) {
789
+ const newStrength = Math.min(1.0, existing.strength + 0.03);
790
+ db.prepare('UPDATE memory_links SET strength = ? WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)').run(newStrength, idA, idB, idB, idA);
791
+ }
792
+ else {
793
+ try {
794
+ db.prepare('INSERT INTO memory_links (source_id, target_id, relationship, strength) VALUES (?, ?, ?, ?)').run(idA, idB, 'related', 0.2);
795
+ }
796
+ catch { /* ignore duplicate */ }
797
+ }
798
+ }
799
+ }
800
+ }
801
+ // ORGANIC FEATURE: Enrich top result with search context
802
+ // When a search query contains significant new information not already in the
803
+ // top result, append it as enrichment context so memories accumulate knowledge
804
+ if (sortedResults.length > 0 && options.query && options.query.length > 30) {
805
+ const topResult = sortedResults[0];
806
+ const queryWords = new Set(options.query.toLowerCase().split(/\s+/).filter(w => w.length > 3));
807
+ const contentWords = new Set(topResult.memory.content.toLowerCase().split(/\s+/));
808
+ const newWords = [...queryWords].filter(w => !contentWords.has(w));
809
+ // Only enrich if query has significant new content (>30% new words)
810
+ if (newWords.length > queryWords.size * 0.3 && options.query.length > 50) {
811
+ try {
812
+ enrichMemory(topResult.memory.id, options.query, 'search');
813
+ }
814
+ catch { /* enrichment is best-effort */ }
815
+ }
816
+ }
817
+ // Look up contradictions for top results
818
+ const finalResults = sortedResults.slice(0, limit);
819
+ for (const result of finalResults) {
820
+ const contradictions = db.prepare(`
821
+ SELECT ml.strength,
822
+ CASE WHEN ml.source_id = ? THEN ml.target_id ELSE ml.source_id END as other_id
823
+ FROM memory_links ml
824
+ WHERE ml.relationship = 'contradicts'
825
+ AND (ml.source_id = ? OR ml.target_id = ?)
826
+ `).all(result.memory.id, result.memory.id, result.memory.id);
827
+ if (contradictions.length > 0) {
828
+ result.contradictions = contradictions.map(c => {
829
+ const other = db.prepare('SELECT title FROM memories WHERE id = ?').get(c.other_id);
830
+ return { memoryId: c.other_id, title: other?.title || 'Unknown', score: c.strength };
831
+ });
832
+ }
833
+ }
834
+ return finalResults;
835
+ }
836
+ /**
837
+ * Get all memories for a project
838
+ */
839
+ export function getProjectMemories(project, config = DEFAULT_CONFIG) {
840
+ const db = getDatabase();
841
+ const rows = db.prepare(`
842
+ SELECT * FROM memories
843
+ WHERE project = ?
844
+ ORDER BY salience DESC, last_accessed DESC
845
+ `).all(project);
846
+ return rows.map(row => {
847
+ const memory = rowToMemory(row);
848
+ memory.decayedScore = calculateDecayedScore(memory, config);
849
+ return memory;
850
+ });
851
+ }
852
+ /**
853
+ * Get recent memories
854
+ */
855
+ export function getRecentMemories(limit = 10, project) {
856
+ const db = getDatabase();
857
+ let sql = 'SELECT * FROM memories';
858
+ const params = [];
859
+ if (project) {
860
+ sql += ' WHERE project = ?';
861
+ params.push(project);
862
+ }
863
+ sql += ' ORDER BY last_accessed DESC LIMIT ?';
864
+ params.push(limit);
865
+ const rows = db.prepare(sql).all(...params);
866
+ return rows.map(rowToMemory);
867
+ }
868
+ /**
869
+ * Get memories by type
870
+ */
871
+ export function getMemoriesByType(type, limit = 50) {
872
+ const db = getDatabase();
873
+ const rows = db.prepare(`
874
+ SELECT * FROM memories
875
+ WHERE type = ?
876
+ ORDER BY salience DESC, last_accessed DESC
877
+ LIMIT ?
878
+ `).all(type, limit);
879
+ return rows.map(rowToMemory);
880
+ }
881
+ /**
882
+ * Get high-priority memories (for context injection)
883
+ */
884
+ export function getHighPriorityMemories(limit = 10, project) {
885
+ const db = getDatabase();
886
+ let sql = `
887
+ SELECT * FROM memories
888
+ WHERE salience >= 0.6
889
+ `;
890
+ const params = [];
891
+ if (project) {
892
+ sql += ' AND project = ?';
893
+ params.push(project);
894
+ }
895
+ sql += ' ORDER BY salience DESC, last_accessed DESC LIMIT ?';
896
+ params.push(limit);
897
+ const rows = db.prepare(sql).all(...params);
898
+ return rows.map(rowToMemory);
899
+ }
900
+ /**
901
+ * Promote a memory from short-term to long-term
902
+ */
903
+ export function promoteMemory(id) {
904
+ const db = getDatabase();
905
+ db.prepare(`
906
+ UPDATE memories
907
+ SET type = 'long_term'
908
+ WHERE id = ? AND type = 'short_term'
909
+ `).run(id);
910
+ return getMemoryById(id);
911
+ }
912
+ /**
913
+ * Bulk delete decayed memories
914
+ */
915
+ export function cleanupDecayedMemories(config = DEFAULT_CONFIG) {
916
+ const db = getDatabase();
917
+ // Get all short-term memories and check decay
918
+ const shortTerm = getMemoriesByType('short_term', 1000);
919
+ const toDelete = [];
920
+ for (const memory of shortTerm) {
921
+ const decayedScore = calculateDecayedScore(memory, config);
922
+ if (decayedScore < config.salienceThreshold) {
923
+ toDelete.push(memory.id);
924
+ }
925
+ }
926
+ if (toDelete.length > 0) {
927
+ const placeholders = toDelete.map(() => '?').join(',');
928
+ db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...toDelete);
929
+ }
930
+ return toDelete.length;
931
+ }
932
+ /**
933
+ * Get memory statistics
934
+ */
935
+ export function getMemoryStats(project) {
936
+ const db = getDatabase();
937
+ let whereClause = '';
938
+ const params = [];
939
+ if (project) {
940
+ whereClause = 'WHERE project = ?';
941
+ params.push(project);
942
+ }
943
+ const total = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause}`).get(...params).count;
944
+ const shortTerm = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'short_term'`).get(...params).count;
945
+ const longTerm = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'long_term'`).get(...params).count;
946
+ const episodic = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'episodic'`).get(...params).count;
947
+ const avgResult = db.prepare(`SELECT AVG(salience) as avg FROM memories ${whereClause}`).get(...params);
948
+ const averageSalience = avgResult.avg || 0;
949
+ // Get counts by category
950
+ const categoryRows = db.prepare(`
951
+ SELECT category, COUNT(*) as count
952
+ FROM memories ${whereClause}
953
+ GROUP BY category
954
+ `).all(...params);
955
+ const byCategory = {};
956
+ for (const row of categoryRows) {
957
+ byCategory[row.category] = row.count;
958
+ }
959
+ return {
960
+ total,
961
+ shortTerm,
962
+ longTerm,
963
+ episodic,
964
+ byCategory,
965
+ averageSalience,
966
+ };
967
+ }
968
+ /**
969
+ * Create a link between two memories
970
+ */
971
+ export function createMemoryLink(sourceId, targetId, relationship, strength = 0.5) {
972
+ const db = getDatabase();
973
+ // Verify both memories exist
974
+ const source = getMemoryById(sourceId);
975
+ const target = getMemoryById(targetId);
976
+ if (!source || !target)
977
+ return null;
978
+ // Prevent self-links
979
+ if (sourceId === targetId)
980
+ return null;
981
+ try {
982
+ const result = db.prepare(`
983
+ INSERT INTO memory_links (source_id, target_id, relationship, strength)
984
+ VALUES (?, ?, ?, ?)
985
+ `).run(sourceId, targetId, relationship, strength);
986
+ return {
987
+ id: result.lastInsertRowid,
988
+ sourceId,
989
+ targetId,
990
+ relationship,
991
+ strength,
992
+ createdAt: new Date(),
993
+ };
994
+ }
995
+ catch {
996
+ // Link already exists (UNIQUE constraint)
997
+ return null;
998
+ }
999
+ }
1000
+ /**
1001
+ * Get all memories related to a given memory
1002
+ */
1003
+ export function getRelatedMemories(memoryId) {
1004
+ const db = getDatabase();
1005
+ // Get outgoing links (this memory references others)
1006
+ const outgoing = db.prepare(`
1007
+ SELECT m.*, ml.relationship, ml.strength
1008
+ FROM memory_links ml
1009
+ JOIN memories m ON m.id = ml.target_id
1010
+ WHERE ml.source_id = ?
1011
+ `).all(memoryId);
1012
+ // Get incoming links (other memories reference this one)
1013
+ const incoming = db.prepare(`
1014
+ SELECT m.*, ml.relationship, ml.strength
1015
+ FROM memory_links ml
1016
+ JOIN memories m ON m.id = ml.source_id
1017
+ WHERE ml.target_id = ?
1018
+ `).all(memoryId);
1019
+ const results = [];
1020
+ for (const row of outgoing) {
1021
+ results.push({
1022
+ memory: rowToMemory(row),
1023
+ relationship: row.relationship,
1024
+ strength: row.strength,
1025
+ direction: 'outgoing',
1026
+ });
1027
+ }
1028
+ for (const row of incoming) {
1029
+ results.push({
1030
+ memory: rowToMemory(row),
1031
+ relationship: row.relationship,
1032
+ strength: row.strength,
1033
+ direction: 'incoming',
1034
+ });
1035
+ }
1036
+ return results;
1037
+ }
1038
+ /**
1039
+ * Delete a memory link
1040
+ */
1041
+ export function deleteMemoryLink(sourceId, targetId) {
1042
+ const db = getDatabase();
1043
+ const result = db.prepare(`
1044
+ DELETE FROM memory_links WHERE source_id = ? AND target_id = ?
1045
+ `).run(sourceId, targetId);
1046
+ return result.changes > 0;
1047
+ }
1048
+ /**
1049
+ * Get all memory links
1050
+ */
1051
+ export function getAllMemoryLinks() {
1052
+ const db = getDatabase();
1053
+ const rows = db.prepare(`SELECT * FROM memory_links ORDER BY created_at DESC`).all();
1054
+ return rows.map(row => ({
1055
+ id: row.id,
1056
+ sourceId: row.source_id,
1057
+ targetId: row.target_id,
1058
+ relationship: row.relationship,
1059
+ strength: row.strength,
1060
+ createdAt: new Date(row.created_at),
1061
+ }));
1062
+ }
1063
+ /**
1064
+ * Detect tag-based links for a memory.
1065
+ * Finds memories sharing tags and scores by overlap count.
1066
+ */
1067
+ function detectTagLinks(db, memory, maxResults) {
1068
+ const results = [];
1069
+ if (memory.tags.length > 0) {
1070
+ const tagPlaceholders = memory.tags.map(() => '?').join(',');
1071
+ const tagMatches = db.prepare(`
1072
+ SELECT DISTINCT m.id, m.tags
1073
+ FROM memories m, json_each(m.tags)
1074
+ WHERE json_each.value IN (${tagPlaceholders})
1075
+ AND m.id != ?
1076
+ LIMIT ?
1077
+ `).all(...memory.tags, memory.id, maxResults);
1078
+ for (const match of tagMatches) {
1079
+ const matchTags = JSON.parse(match.tags);
1080
+ const sharedCount = memory.tags.filter(t => matchTags.includes(t)).length;
1081
+ const strength = Math.min(0.9, 0.3 + (sharedCount * 0.2));
1082
+ results.push({ targetId: match.id, relationship: 'related', strength });
1083
+ }
1084
+ }
1085
+ return results;
1086
+ }
1087
+ /**
1088
+ * Detect embedding-based semantic links for a memory.
1089
+ * Computes cosine similarity against top memories that have embeddings.
1090
+ */
1091
+ function detectEmbeddingLinks(db, memory, maxResults) {
1092
+ if (!memory.embedding)
1093
+ return [];
1094
+ const candidates = db.prepare(`
1095
+ SELECT id, embedding FROM memories
1096
+ WHERE embedding IS NOT NULL AND id != ?
1097
+ ORDER BY decayed_score DESC
1098
+ LIMIT 100
1099
+ `).all(memory.id);
1100
+ const results = [];
1101
+ const sourceEmbedding = new Float32Array(memory.embedding.buffer, memory.embedding.byteOffset, memory.embedding.byteLength / 4);
1102
+ for (const candidate of candidates) {
1103
+ const candidateEmbedding = new Float32Array(candidate.embedding.buffer, candidate.embedding.byteOffset, candidate.embedding.byteLength / 4);
1104
+ const similarity = cosineSimilarity(sourceEmbedding, candidateEmbedding);
1105
+ if (similarity >= 0.6) {
1106
+ results.push({
1107
+ targetId: candidate.id,
1108
+ relationship: 'related',
1109
+ strength: Math.min(0.9, similarity),
1110
+ });
1111
+ }
1112
+ }
1113
+ return results;
1114
+ }
1115
+ /**
1116
+ * Detect content-based links using FTS5 and Jaccard similarity.
1117
+ * Fallback when embeddings are not available.
1118
+ */
1119
+ function detectFtsLinks(db, memory, maxResults) {
1120
+ const queryText = `${memory.title} ${memory.content.slice(0, 200)}`;
1121
+ const escapedQuery = escapeFts5Query(queryText);
1122
+ if (!escapedQuery.trim())
1123
+ return [];
1124
+ let ftsMatches;
1125
+ try {
1126
+ ftsMatches = db.prepare(`
1127
+ SELECT m.id, m.title, m.content
1128
+ FROM memories_fts fts
1129
+ JOIN memories m ON m.id = fts.rowid
1130
+ WHERE memories_fts MATCH ?
1131
+ AND m.id != ?
1132
+ LIMIT ?
1133
+ `).all(escapedQuery, memory.id, maxResults * 2);
1134
+ }
1135
+ catch {
1136
+ return [];
1137
+ }
1138
+ const results = [];
1139
+ for (const match of ftsMatches) {
1140
+ const matchText = `${match.title} ${match.content.slice(0, 200)}`;
1141
+ const sim = jaccardSimilarity(queryText, matchText);
1142
+ if (sim >= 0.3) {
1143
+ results.push({
1144
+ targetId: match.id,
1145
+ relationship: 'related',
1146
+ strength: Math.min(0.7, sim + 0.2),
1147
+ });
1148
+ }
1149
+ }
1150
+ return results;
1151
+ }
1152
+ /**
1153
+ * Detect potential relationships for a new memory
1154
+ * Uses three strategies in priority order:
1155
+ * 1. Tag-based linking (shared tags)
1156
+ * 2. Embedding-based semantic linking (cosine similarity >= 0.6)
1157
+ * 3. FTS content similarity fallback (Jaccard similarity >= 0.3)
1158
+ */
1159
+ export function detectRelationships(memory, maxResults = 5) {
1160
+ const db = getDatabase();
1161
+ const seen = new Set();
1162
+ const results = [];
1163
+ function addResults(links) {
1164
+ for (const link of links) {
1165
+ if (!seen.has(link.targetId)) {
1166
+ seen.add(link.targetId);
1167
+ results.push(link);
1168
+ }
1169
+ }
1170
+ }
1171
+ // 1. Tag-based linking
1172
+ addResults(detectTagLinks(db, memory, maxResults));
1173
+ // 2. Embedding-based semantic linking
1174
+ addResults(detectEmbeddingLinks(db, memory, maxResults));
1175
+ // 3. FTS content similarity fallback (when no embeddings)
1176
+ if (!memory.embedding) {
1177
+ addResults(detectFtsLinks(db, memory, maxResults));
1178
+ }
1179
+ // Sort by strength descending and limit
1180
+ return results
1181
+ .sort((a, b) => b.strength - a.strength)
1182
+ .slice(0, maxResults);
1183
+ }
1184
+ //# sourceMappingURL=store.js.map