sensemaking 0.17.1 → 0.18.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 (425) hide show
  1. package/README.md +15 -13
  2. package/dist/cjs/cli/map.js +144 -3
  3. package/dist/cjs/cli/map.js.map +1 -1
  4. package/dist/cjs/cli/named.js +19 -11
  5. package/dist/cjs/cli/named.js.map +1 -1
  6. package/dist/cjs/cli/path.js +186 -26
  7. package/dist/cjs/cli/path.js.map +1 -1
  8. package/dist/cjs/cli/peek.js +145 -4
  9. package/dist/cjs/cli/peek.js.map +1 -1
  10. package/dist/cjs/cli/related.js +2 -2
  11. package/dist/cjs/cli/related.js.map +1 -1
  12. package/dist/cjs/cli/search.js +2 -2
  13. package/dist/cjs/cli/search.js.map +1 -1
  14. package/dist/cjs/cli/shared.d.cts +3 -3
  15. package/dist/cjs/cli/shared.d.ts +3 -3
  16. package/dist/cjs/cli/shared.js +156 -58
  17. package/dist/cjs/cli/shared.js.map +1 -1
  18. package/dist/cjs/cli/sql.js +151 -10
  19. package/dist/cjs/cli/sql.js.map +1 -1
  20. package/dist/cjs/cli/status.js +177 -52
  21. package/dist/cjs/cli/status.js.map +1 -1
  22. package/dist/cjs/commands/map.d.cts +2 -2
  23. package/dist/cjs/commands/map.d.ts +2 -2
  24. package/dist/cjs/commands/map.js +373 -67
  25. package/dist/cjs/commands/map.js.map +1 -1
  26. package/dist/cjs/commands/peek.d.cts +2 -2
  27. package/dist/cjs/commands/peek.d.ts +2 -2
  28. package/dist/cjs/commands/peek.js +321 -79
  29. package/dist/cjs/commands/peek.js.map +1 -1
  30. package/dist/cjs/commands/related.d.cts +2 -2
  31. package/dist/cjs/commands/related.d.ts +2 -2
  32. package/dist/cjs/commands/related.js +55 -10
  33. package/dist/cjs/commands/related.js.map +1 -1
  34. package/dist/cjs/commands/scope.d.cts +7 -7
  35. package/dist/cjs/commands/scope.d.ts +7 -7
  36. package/dist/cjs/commands/scope.js +382 -45
  37. package/dist/cjs/commands/scope.js.map +1 -1
  38. package/dist/cjs/commands/search.d.cts +2 -2
  39. package/dist/cjs/commands/search.d.ts +2 -2
  40. package/dist/cjs/commands/search.js +292 -99
  41. package/dist/cjs/commands/search.js.map +1 -1
  42. package/dist/cjs/commands/signals.d.cts +4 -10
  43. package/dist/cjs/commands/signals.d.ts +4 -10
  44. package/dist/cjs/commands/signals.js +91 -45
  45. package/dist/cjs/commands/signals.js.map +1 -1
  46. package/dist/cjs/commands/status.d.cts +2 -2
  47. package/dist/cjs/commands/status.d.ts +2 -2
  48. package/dist/cjs/commands/status.js +247 -10
  49. package/dist/cjs/commands/status.js.map +1 -1
  50. package/dist/cjs/config/access.d.cts +1 -0
  51. package/dist/cjs/config/access.d.ts +1 -0
  52. package/dist/cjs/config/access.js +7 -0
  53. package/dist/cjs/config/access.js.map +1 -1
  54. package/dist/cjs/config/index.d.cts +1 -1
  55. package/dist/cjs/config/index.d.ts +1 -1
  56. package/dist/cjs/config/index.js +3 -0
  57. package/dist/cjs/config/index.js.map +1 -1
  58. package/dist/cjs/config/types.d.cts +1 -0
  59. package/dist/cjs/config/types.d.ts +1 -0
  60. package/dist/cjs/config/types.js.map +1 -1
  61. package/dist/cjs/config/validate.js +10 -1
  62. package/dist/cjs/config/validate.js.map +1 -1
  63. package/dist/cjs/embed/distribution.d.cts +3 -3
  64. package/dist/cjs/embed/distribution.d.ts +3 -3
  65. package/dist/cjs/embed/distribution.js +162 -6
  66. package/dist/cjs/embed/distribution.js.map +1 -1
  67. package/dist/cjs/embed/langfit.d.cts +2 -2
  68. package/dist/cjs/embed/langfit.d.ts +2 -2
  69. package/dist/cjs/embed/langfit.js +57 -44
  70. package/dist/cjs/embed/langfit.js.map +1 -1
  71. package/dist/cjs/embed/query.d.cts +6 -13
  72. package/dist/cjs/embed/query.d.ts +6 -13
  73. package/dist/cjs/embed/query.js +56 -168
  74. package/dist/cjs/embed/query.js.map +1 -1
  75. package/dist/cjs/embed/types.d.cts +1 -0
  76. package/dist/cjs/embed/types.d.ts +1 -0
  77. package/dist/cjs/embed/types.js +11 -2
  78. package/dist/cjs/embed/types.js.map +1 -1
  79. package/dist/cjs/errors.d.cts +1 -1
  80. package/dist/cjs/errors.d.ts +1 -1
  81. package/dist/cjs/errors.js.map +1 -1
  82. package/dist/cjs/features/embed.js +218 -11
  83. package/dist/cjs/features/embed.js.map +1 -1
  84. package/dist/cjs/features/index.d.cts +1 -1
  85. package/dist/cjs/features/index.d.ts +1 -1
  86. package/dist/cjs/features/index.js +6 -0
  87. package/dist/cjs/features/index.js.map +1 -1
  88. package/dist/cjs/features/links.d.cts +7 -2
  89. package/dist/cjs/features/links.d.ts +7 -2
  90. package/dist/cjs/features/links.js +710 -198
  91. package/dist/cjs/features/links.js.map +1 -1
  92. package/dist/cjs/features/rank.js +231 -31
  93. package/dist/cjs/features/rank.js.map +1 -1
  94. package/dist/cjs/features/sections.js +218 -8
  95. package/dist/cjs/features/sections.js.map +1 -1
  96. package/dist/cjs/features/tags.js +236 -40
  97. package/dist/cjs/features/tags.js.map +1 -1
  98. package/dist/cjs/features/types.d.cts +9 -5
  99. package/dist/cjs/features/types.d.ts +9 -5
  100. package/dist/cjs/graph/traverse.d.cts +2 -2
  101. package/dist/cjs/graph/traverse.d.ts +2 -2
  102. package/dist/cjs/graph/traverse.js +321 -42
  103. package/dist/cjs/graph/traverse.js.map +1 -1
  104. package/dist/cjs/index.d.cts +3 -2
  105. package/dist/cjs/index.d.ts +3 -2
  106. package/dist/cjs/index.js +2 -2
  107. package/dist/cjs/index.js.map +1 -1
  108. package/dist/cjs/lib/guarded-tick.d.cts +1 -0
  109. package/dist/cjs/lib/guarded-tick.d.ts +1 -0
  110. package/dist/cjs/lib/guarded-tick.js +19 -0
  111. package/dist/cjs/lib/guarded-tick.js.map +1 -0
  112. package/dist/cjs/output/column-hint.d.cts +1 -2
  113. package/dist/cjs/output/column-hint.d.ts +1 -2
  114. package/dist/cjs/output/column-hint.js +1 -4
  115. package/dist/cjs/output/column-hint.js.map +1 -1
  116. package/dist/cjs/output/output.d.cts +2 -1
  117. package/dist/cjs/output/output.d.ts +2 -1
  118. package/dist/cjs/output/output.js +515 -97
  119. package/dist/cjs/output/output.js.map +1 -1
  120. package/dist/cjs/scan/frontmatter.js.map +1 -1
  121. package/dist/cjs/scan/index.js.map +1 -1
  122. package/dist/cjs/scan/reparse.d.cts +10 -0
  123. package/dist/cjs/scan/reparse.d.ts +10 -0
  124. package/dist/cjs/scan/reparse.js +104 -0
  125. package/dist/cjs/scan/reparse.js.map +1 -0
  126. package/dist/cjs/store/duckdb/batch.d.cts +16 -0
  127. package/dist/cjs/store/duckdb/batch.d.ts +16 -0
  128. package/dist/cjs/store/duckdb/batch.js +208 -0
  129. package/dist/cjs/store/duckdb/batch.js.map +1 -0
  130. package/dist/cjs/store/duckdb/connection.d.cts +5 -0
  131. package/dist/cjs/store/duckdb/connection.d.ts +5 -0
  132. package/dist/cjs/store/duckdb/connection.js +772 -0
  133. package/dist/cjs/store/duckdb/connection.js.map +1 -0
  134. package/dist/cjs/store/duckdb/lexical.d.cts +10 -0
  135. package/dist/cjs/store/duckdb/lexical.d.ts +10 -0
  136. package/dist/cjs/store/duckdb/lexical.js +502 -0
  137. package/dist/cjs/store/duckdb/lexical.js.map +1 -0
  138. package/dist/cjs/store/duckdb/native.d.cts +3 -0
  139. package/dist/cjs/store/duckdb/native.d.ts +3 -0
  140. package/dist/cjs/store/duckdb/native.js +360 -0
  141. package/dist/cjs/store/duckdb/native.js.map +1 -0
  142. package/dist/cjs/store/duckdb/open.d.cts +12 -0
  143. package/dist/cjs/store/duckdb/open.d.ts +12 -0
  144. package/dist/cjs/store/duckdb/open.js +501 -0
  145. package/dist/cjs/store/duckdb/open.js.map +1 -0
  146. package/dist/cjs/store/duckdb/reconcile.d.cts +6 -0
  147. package/dist/cjs/store/duckdb/reconcile.d.ts +6 -0
  148. package/dist/cjs/store/duckdb/reconcile.js +753 -0
  149. package/dist/cjs/store/duckdb/reconcile.js.map +1 -0
  150. package/dist/cjs/store/duckdb/sql-functions.d.cts +2 -0
  151. package/dist/cjs/store/duckdb/sql-functions.d.ts +2 -0
  152. package/dist/cjs/store/duckdb/sql-functions.js +203 -0
  153. package/dist/cjs/store/duckdb/sql-functions.js.map +1 -0
  154. package/dist/cjs/store/duckdb/store.d.cts +5 -0
  155. package/dist/cjs/store/duckdb/store.d.ts +5 -0
  156. package/dist/cjs/store/duckdb/store.js +504 -0
  157. package/dist/cjs/store/duckdb/store.js.map +1 -0
  158. package/dist/cjs/store/duckdb/vectors.d.cts +9 -0
  159. package/dist/cjs/store/duckdb/vectors.d.ts +9 -0
  160. package/dist/cjs/store/duckdb/vectors.js +502 -0
  161. package/dist/cjs/store/duckdb/vectors.js.map +1 -0
  162. package/dist/cjs/store/index.d.cts +7 -0
  163. package/dist/cjs/store/index.d.ts +7 -0
  164. package/dist/cjs/store/index.js +201 -0
  165. package/dist/cjs/store/index.js.map +1 -0
  166. package/dist/cjs/store/meta.d.cts +3 -0
  167. package/dist/cjs/store/meta.d.ts +3 -0
  168. package/dist/cjs/store/meta.js +215 -0
  169. package/dist/cjs/store/meta.js.map +1 -0
  170. package/dist/cjs/store/shared.d.cts +5 -0
  171. package/dist/cjs/store/shared.d.ts +5 -0
  172. package/dist/cjs/store/shared.js +252 -0
  173. package/dist/cjs/store/shared.js.map +1 -0
  174. package/dist/cjs/store/sql-functions.d.cts +2 -0
  175. package/dist/cjs/store/sql-functions.d.ts +2 -0
  176. package/dist/cjs/store/sql-functions.js +48 -0
  177. package/dist/cjs/store/sql-functions.js.map +1 -0
  178. package/dist/cjs/store/sqlite/connection.d.cts +3 -0
  179. package/dist/cjs/store/sqlite/connection.d.ts +3 -0
  180. package/dist/cjs/store/sqlite/connection.js +516 -0
  181. package/dist/cjs/store/sqlite/connection.js.map +1 -0
  182. package/dist/cjs/store/sqlite/lexical.d.cts +2 -0
  183. package/dist/cjs/store/sqlite/lexical.d.ts +2 -0
  184. package/dist/cjs/store/sqlite/lexical.js +177 -0
  185. package/dist/cjs/store/sqlite/lexical.js.map +1 -0
  186. package/dist/cjs/{db → store/sqlite}/open.d.cts +5 -5
  187. package/dist/cjs/{db → store/sqlite}/open.d.ts +5 -5
  188. package/dist/cjs/store/sqlite/open.js +604 -0
  189. package/dist/cjs/store/sqlite/open.js.map +1 -0
  190. package/dist/cjs/store/sqlite/reconcile.d.cts +10 -0
  191. package/dist/cjs/store/sqlite/reconcile.d.ts +10 -0
  192. package/dist/cjs/store/sqlite/reconcile.js +1020 -0
  193. package/dist/cjs/store/sqlite/reconcile.js.map +1 -0
  194. package/dist/cjs/store/sqlite/sql-functions.js +34 -0
  195. package/dist/cjs/store/sqlite/sql-functions.js.map +1 -0
  196. package/dist/cjs/store/sqlite/store.d.cts +5 -0
  197. package/dist/cjs/store/sqlite/store.d.ts +5 -0
  198. package/dist/cjs/store/sqlite/store.js +588 -0
  199. package/dist/cjs/store/sqlite/store.js.map +1 -0
  200. package/dist/cjs/store/sqlite/vectors.d.cts +13 -0
  201. package/dist/cjs/store/sqlite/vectors.d.ts +13 -0
  202. package/dist/cjs/store/sqlite/vectors.js +470 -0
  203. package/dist/cjs/store/sqlite/vectors.js.map +1 -0
  204. package/dist/cjs/store/transaction.d.cts +5 -0
  205. package/dist/cjs/store/transaction.d.ts +5 -0
  206. package/dist/cjs/store/transaction.js +297 -0
  207. package/dist/cjs/store/transaction.js.map +1 -0
  208. package/dist/cjs/store/types.d.cts +91 -0
  209. package/dist/cjs/store/types.d.ts +91 -0
  210. package/dist/cjs/store/types.js +15 -0
  211. package/dist/cjs/store/types.js.map +1 -0
  212. package/dist/cjs/watch.d.cts +3 -0
  213. package/dist/cjs/watch.d.ts +3 -0
  214. package/dist/cjs/watch.js +215 -73
  215. package/dist/cjs/watch.js.map +1 -1
  216. package/dist/esm/cli/map.js +4 -4
  217. package/dist/esm/cli/map.js.map +1 -1
  218. package/dist/esm/cli/named.js +2 -2
  219. package/dist/esm/cli/named.js.map +1 -1
  220. package/dist/esm/cli/path.js +4 -4
  221. package/dist/esm/cli/path.js.map +1 -1
  222. package/dist/esm/cli/peek.js +4 -4
  223. package/dist/esm/cli/peek.js.map +1 -1
  224. package/dist/esm/cli/related.js +2 -2
  225. package/dist/esm/cli/related.js.map +1 -1
  226. package/dist/esm/cli/search.js +1 -1
  227. package/dist/esm/cli/search.js.map +1 -1
  228. package/dist/esm/cli/shared.d.ts +3 -3
  229. package/dist/esm/cli/shared.js +17 -17
  230. package/dist/esm/cli/shared.js.map +1 -1
  231. package/dist/esm/cli/sql.js +2 -2
  232. package/dist/esm/cli/sql.js.map +1 -1
  233. package/dist/esm/cli/status.js +23 -22
  234. package/dist/esm/cli/status.js.map +1 -1
  235. package/dist/esm/commands/map.d.ts +2 -2
  236. package/dist/esm/commands/map.js +51 -41
  237. package/dist/esm/commands/map.js.map +1 -1
  238. package/dist/esm/commands/peek.d.ts +2 -2
  239. package/dist/esm/commands/peek.js +58 -50
  240. package/dist/esm/commands/peek.js.map +1 -1
  241. package/dist/esm/commands/related.d.ts +2 -2
  242. package/dist/esm/commands/related.js +9 -9
  243. package/dist/esm/commands/related.js.map +1 -1
  244. package/dist/esm/commands/scope.d.ts +7 -7
  245. package/dist/esm/commands/scope.js +28 -17
  246. package/dist/esm/commands/scope.js.map +1 -1
  247. package/dist/esm/commands/search.d.ts +2 -2
  248. package/dist/esm/commands/search.js +42 -35
  249. package/dist/esm/commands/search.js.map +1 -1
  250. package/dist/esm/commands/signals.d.ts +4 -10
  251. package/dist/esm/commands/signals.js +17 -13
  252. package/dist/esm/commands/signals.js.map +1 -1
  253. package/dist/esm/commands/status.d.ts +2 -2
  254. package/dist/esm/commands/status.js +13 -7
  255. package/dist/esm/commands/status.js.map +1 -1
  256. package/dist/esm/config/access.d.ts +1 -0
  257. package/dist/esm/config/access.js +5 -0
  258. package/dist/esm/config/access.js.map +1 -1
  259. package/dist/esm/config/index.d.ts +1 -1
  260. package/dist/esm/config/index.js +1 -1
  261. package/dist/esm/config/index.js.map +1 -1
  262. package/dist/esm/config/types.d.ts +1 -0
  263. package/dist/esm/config/types.js.map +1 -1
  264. package/dist/esm/config/validate.js +10 -1
  265. package/dist/esm/config/validate.js.map +1 -1
  266. package/dist/esm/embed/distribution.d.ts +3 -3
  267. package/dist/esm/embed/distribution.js +5 -5
  268. package/dist/esm/embed/distribution.js.map +1 -1
  269. package/dist/esm/embed/langfit.d.ts +2 -2
  270. package/dist/esm/embed/langfit.js +4 -4
  271. package/dist/esm/embed/langfit.js.map +1 -1
  272. package/dist/esm/embed/query.d.ts +6 -13
  273. package/dist/esm/embed/query.js +23 -84
  274. package/dist/esm/embed/query.js.map +1 -1
  275. package/dist/esm/embed/types.d.ts +1 -0
  276. package/dist/esm/embed/types.js +5 -3
  277. package/dist/esm/embed/types.js.map +1 -1
  278. package/dist/esm/errors.d.ts +1 -1
  279. package/dist/esm/errors.js.map +1 -1
  280. package/dist/esm/features/embed.js +22 -10
  281. package/dist/esm/features/embed.js.map +1 -1
  282. package/dist/esm/features/index.d.ts +1 -1
  283. package/dist/esm/features/index.js +1 -1
  284. package/dist/esm/features/index.js.map +1 -1
  285. package/dist/esm/features/links.d.ts +7 -2
  286. package/dist/esm/features/links.js +124 -97
  287. package/dist/esm/features/links.js.map +1 -1
  288. package/dist/esm/features/rank.js +15 -8
  289. package/dist/esm/features/rank.js.map +1 -1
  290. package/dist/esm/features/sections.js +20 -7
  291. package/dist/esm/features/sections.js.map +1 -1
  292. package/dist/esm/features/tags.js +19 -18
  293. package/dist/esm/features/tags.js.map +1 -1
  294. package/dist/esm/features/types.d.ts +9 -5
  295. package/dist/esm/features/types.js +1 -1
  296. package/dist/esm/features/types.js.map +1 -1
  297. package/dist/esm/graph/traverse.d.ts +2 -2
  298. package/dist/esm/graph/traverse.js +26 -23
  299. package/dist/esm/graph/traverse.js.map +1 -1
  300. package/dist/esm/index.d.ts +3 -2
  301. package/dist/esm/index.js +1 -1
  302. package/dist/esm/index.js.map +1 -1
  303. package/dist/esm/lib/guarded-tick.d.ts +1 -0
  304. package/dist/esm/lib/guarded-tick.js +8 -0
  305. package/dist/esm/lib/guarded-tick.js.map +1 -0
  306. package/dist/esm/output/column-hint.d.ts +1 -2
  307. package/dist/esm/output/column-hint.js +3 -2
  308. package/dist/esm/output/column-hint.js.map +1 -1
  309. package/dist/esm/output/output.d.ts +2 -1
  310. package/dist/esm/output/output.js +27 -23
  311. package/dist/esm/output/output.js.map +1 -1
  312. package/dist/esm/scan/frontmatter.js +1 -1
  313. package/dist/esm/scan/frontmatter.js.map +1 -1
  314. package/dist/esm/scan/index.js +1 -1
  315. package/dist/esm/scan/index.js.map +1 -1
  316. package/dist/esm/scan/reparse.d.ts +10 -0
  317. package/dist/esm/scan/reparse.js +30 -0
  318. package/dist/esm/scan/reparse.js.map +1 -0
  319. package/dist/esm/store/duckdb/batch.d.ts +16 -0
  320. package/dist/esm/store/duckdb/batch.js +96 -0
  321. package/dist/esm/store/duckdb/batch.js.map +1 -0
  322. package/dist/esm/store/duckdb/connection.d.ts +5 -0
  323. package/dist/esm/store/duckdb/connection.js +97 -0
  324. package/dist/esm/store/duckdb/connection.js.map +1 -0
  325. package/dist/esm/store/duckdb/lexical.d.ts +10 -0
  326. package/dist/esm/store/duckdb/lexical.js +163 -0
  327. package/dist/esm/store/duckdb/lexical.js.map +1 -0
  328. package/dist/esm/store/duckdb/native.d.ts +3 -0
  329. package/dist/esm/store/duckdb/native.js +96 -0
  330. package/dist/esm/store/duckdb/native.js.map +1 -0
  331. package/dist/esm/store/duckdb/open.d.ts +12 -0
  332. package/dist/esm/store/duckdb/open.js +127 -0
  333. package/dist/esm/store/duckdb/open.js.map +1 -0
  334. package/dist/esm/store/duckdb/reconcile.d.ts +6 -0
  335. package/dist/esm/store/duckdb/reconcile.js +141 -0
  336. package/dist/esm/store/duckdb/reconcile.js.map +1 -0
  337. package/dist/esm/store/duckdb/sql-functions.d.ts +2 -0
  338. package/dist/esm/store/duckdb/sql-functions.js +56 -0
  339. package/dist/esm/store/duckdb/sql-functions.js.map +1 -0
  340. package/dist/esm/store/duckdb/store.d.ts +5 -0
  341. package/dist/esm/store/duckdb/store.js +104 -0
  342. package/dist/esm/store/duckdb/store.js.map +1 -0
  343. package/dist/esm/store/duckdb/vectors.d.ts +9 -0
  344. package/dist/esm/store/duckdb/vectors.js +171 -0
  345. package/dist/esm/store/duckdb/vectors.js.map +1 -0
  346. package/dist/esm/store/index.d.ts +7 -0
  347. package/dist/esm/store/index.js +35 -0
  348. package/dist/esm/store/index.js.map +1 -0
  349. package/dist/esm/store/meta.d.ts +3 -0
  350. package/dist/esm/store/meta.js +17 -0
  351. package/dist/esm/store/meta.js.map +1 -0
  352. package/dist/esm/store/shared.d.ts +5 -0
  353. package/dist/esm/store/shared.js +24 -0
  354. package/dist/esm/store/shared.js.map +1 -0
  355. package/dist/esm/store/sql-functions.d.ts +2 -0
  356. package/dist/esm/store/sql-functions.js +29 -0
  357. package/dist/esm/store/sql-functions.js.map +1 -0
  358. package/dist/esm/store/sqlite/connection.d.ts +3 -0
  359. package/dist/esm/store/sqlite/connection.js +50 -0
  360. package/dist/esm/store/sqlite/connection.js.map +1 -0
  361. package/dist/esm/store/sqlite/lexical.d.ts +2 -0
  362. package/dist/esm/store/sqlite/lexical.js +21 -0
  363. package/dist/esm/store/sqlite/lexical.js.map +1 -0
  364. package/dist/esm/{db → store/sqlite}/open.d.ts +5 -5
  365. package/dist/esm/store/sqlite/open.js +193 -0
  366. package/dist/esm/store/sqlite/open.js.map +1 -0
  367. package/dist/esm/store/sqlite/reconcile.d.ts +10 -0
  368. package/dist/esm/{db → store/sqlite}/reconcile.js +101 -99
  369. package/dist/esm/store/sqlite/reconcile.js.map +1 -0
  370. package/dist/esm/store/sqlite/sql-functions.js +23 -0
  371. package/dist/esm/store/sqlite/sql-functions.js.map +1 -0
  372. package/dist/esm/store/sqlite/store.d.ts +5 -0
  373. package/dist/esm/store/sqlite/store.js +82 -0
  374. package/dist/esm/store/sqlite/store.js.map +1 -0
  375. package/dist/esm/store/sqlite/vectors.d.ts +13 -0
  376. package/dist/esm/store/sqlite/vectors.js +90 -0
  377. package/dist/esm/store/sqlite/vectors.js.map +1 -0
  378. package/dist/esm/store/transaction.d.ts +5 -0
  379. package/dist/esm/store/transaction.js +57 -0
  380. package/dist/esm/store/transaction.js.map +1 -0
  381. package/dist/esm/store/types.d.ts +91 -0
  382. package/dist/esm/store/types.js +11 -0
  383. package/dist/esm/store/types.js.map +1 -0
  384. package/dist/esm/watch.d.ts +3 -0
  385. package/dist/esm/watch.js +39 -22
  386. package/dist/esm/watch.js.map +1 -1
  387. package/package.json +3 -1
  388. package/schema.json +5 -0
  389. package/skills/sense/EXAMPLES.md +3 -3
  390. package/skills/sense/SKILL.md +16 -14
  391. package/skills/sense-bases/EXAMPLES.md +2 -2
  392. package/skills/sense-bases/SKILL.md +6 -6
  393. package/skills/sense-setup/EXAMPLES.md +1 -1
  394. package/skills/sense-setup/SKILL.md +5 -4
  395. package/dist/cjs/db/index.d.cts +0 -4
  396. package/dist/cjs/db/index.d.ts +0 -4
  397. package/dist/cjs/db/index.js +0 -42
  398. package/dist/cjs/db/index.js.map +0 -1
  399. package/dist/cjs/db/open.js +0 -230
  400. package/dist/cjs/db/open.js.map +0 -1
  401. package/dist/cjs/db/reconcile.d.cts +0 -10
  402. package/dist/cjs/db/reconcile.d.ts +0 -10
  403. package/dist/cjs/db/reconcile.js +0 -579
  404. package/dist/cjs/db/reconcile.js.map +0 -1
  405. package/dist/cjs/db/shared.d.cts +0 -5
  406. package/dist/cjs/db/shared.d.ts +0 -5
  407. package/dist/cjs/db/shared.js +0 -45
  408. package/dist/cjs/db/shared.js.map +0 -1
  409. package/dist/cjs/db/sql-functions.js +0 -64
  410. package/dist/cjs/db/sql-functions.js.map +0 -1
  411. package/dist/esm/db/index.d.ts +0 -4
  412. package/dist/esm/db/index.js +0 -5
  413. package/dist/esm/db/index.js.map +0 -1
  414. package/dist/esm/db/open.js +0 -165
  415. package/dist/esm/db/open.js.map +0 -1
  416. package/dist/esm/db/reconcile.d.ts +0 -10
  417. package/dist/esm/db/reconcile.js.map +0 -1
  418. package/dist/esm/db/shared.d.ts +0 -5
  419. package/dist/esm/db/shared.js +0 -20
  420. package/dist/esm/db/shared.js.map +0 -1
  421. package/dist/esm/db/sql-functions.js +0 -47
  422. package/dist/esm/db/sql-functions.js.map +0 -1
  423. /package/dist/cjs/{db → store/sqlite}/sql-functions.d.cts +0 -0
  424. /package/dist/cjs/{db → store/sqlite}/sql-functions.d.ts +0 -0
  425. /package/dist/esm/{db → store/sqlite}/sql-functions.d.ts +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/embed.ts"],"sourcesContent":["import type { Block, Chunk } from '../chunk/index.ts';\nimport { CHUNK_VERSION, chunkFromBlocks, parse } from '../chunk/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { modelIdentity } from '../embed/identity.ts';\nimport type { Feature } from './types.ts';\n\n// int8 vectors with a per-vector scale, NULL vector = not yet embedded. Reconcile writes dirty\n// rows; embedding tops up on the next search, so staleness costs recall. Providers, the vector\n// fill, and the search-time helpers live under src/embed/. Block/group structure (D1-D5) lives\n// in src/chunk; this keeps only the title/summary prefix and the empty-chunk drop (D6).\n\nexport type { Chunk };\n\n// Deterministic, so embed time can re-derive text from stored line ranges; the title/summary\n// prefix mirrors bm25 field weighting, and an empty chunk is dropped before the prefix is added.\n//\n// Chunks the body, not the raw file, with `offset` shifting line numbers back onto the raw\n// file so a range stays a direct Read range (sections is 1-indexed over raw too). Chunking raw\n// put the frontmatter block in its own leading chunk on every note with a heading, which made\n// `lines` point at YAML and made frontmatter-only notes near-identical to each other. It also\n// disagreed with FTS, which indexes the body alone.\nfunction chunksOf(blocks: Block[], body: string, search?: { title: string; summary: string }, offset = 0, chunkTokens?: number): Chunk[] {\n const prefix = [search?.title, search?.summary].filter(Boolean).join('\\n');\n // chunk()'s default text mode is 'raw' (D9), the shipped choice. A hardcoded option change\n // here must bump src/chunk/version.ts's CHUNK_VERSION -- the signature for every chunker\n // input not already carried in config.\n return chunkFromBlocks(blocks, body, chunkTokens !== undefined ? { targetTokens: chunkTokens } : undefined).map((c) => ({\n startLine: c.startLine + offset,\n endLine: c.endLine + offset,\n text: prefix ? `${prefix}\\n${c.text}` : c.text,\n }));\n}\n\nexport const embed: Feature = {\n name: 'embed',\n schema(db) {\n db.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector BLOB, PRIMARY KEY (\"path\", chunk))`);\n },\n extract(raw, body, search, _data, cfg, blocks) {\n // Lines the frontmatter occupies, so body line 1 maps back to its raw line number.\n // blocks comes from parseFile's shared parse; falls back to parsing body for a direct call.\n return chunksOf(blocks ?? parse(body), body, search, raw.split('\\n').length - body.split('\\n').length, cfg?.embed?.chunkTokens);\n },\n remove(db, path) {\n db.prepare('DELETE FROM embeddings WHERE \"path\" = ?').run(path);\n },\n // A tree with no embedding model never had extract() run for the doc (db.ts's per-file\n // filter skips it), so extracted is undefined here -- store nothing, i.e. no rows.\n store(db, path, extracted) {\n if (!extracted) return;\n const insert = db.prepare('INSERT INTO embeddings (\"path\", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)');\n (extracted as Chunk[]).forEach((c, idx) => insert.run(path, idx, c.startLine, c.endLine));\n },\n enabledForFile(_cfg, file) {\n return file.embed;\n },\n // Provider/model/chunk-version/identity, keyed so a rebuild notice can name it \"embed\n // settings\". Driven by embedConfig(cfg) directly (a model can be configured with no preset\n // yet using vectors), not by whether this feature is itself active.\n signature(cfg) {\n const e = embedConfig(cfg);\n if (!e) return 'embed:off';\n // Weight identity from identity.ts, no network: a static model's resolved sha or local\n // size+mtime, appended once known so changed weights re-embed.\n const identity = e.provider === 'static' ? modelIdentity(e.model) : undefined;\n // W3b: chunkTokens rides the version token, since it's the one owner lever the chunker\n // itself takes -- changing or clearing it must rebuild exactly like a chunker version bump.\n const chunkVersion = e.chunkTokens !== undefined ? `${CHUNK_VERSION}:${e.chunkTokens}` : CHUNK_VERSION;\n return `embed:${e.provider}:${e.model}:${chunkVersion}${identity !== undefined ? `@${identity}` : ''}`;\n },\n};\n"],"names":["CHUNK_VERSION","chunkFromBlocks","parse","embedConfig","modelIdentity","chunksOf","blocks","body","search","offset","chunkTokens","prefix","title","summary","filter","Boolean","join","undefined","targetTokens","map","c","startLine","endLine","text","embed","name","schema","db","exec","extract","raw","_data","cfg","split","length","remove","path","prepare","run","store","extracted","insert","forEach","idx","enabledForFile","_cfg","file","signature","e","identity","provider","model","chunkVersion"],"mappings":"AACA,SAASA,aAAa,EAAEC,eAAe,EAAEC,KAAK,QAAQ,oBAAoB;AAC1E,SAASC,WAAW,QAAQ,qBAAqB;AACjD,SAASC,aAAa,QAAQ,uBAAuB;AAUrD,6FAA6F;AAC7F,iGAAiG;AACjG,EAAE;AACF,2FAA2F;AAC3F,+FAA+F;AAC/F,8FAA8F;AAC9F,8FAA8F;AAC9F,oDAAoD;AACpD,SAASC,SAASC,MAAe,EAAEC,IAAY,EAAEC,MAA2C,EAAEC,SAAS,CAAC,EAAEC,WAAoB;IAC5H,MAAMC,SAAS;QAACH,mBAAAA,6BAAAA,OAAQI,KAAK;QAAEJ,mBAAAA,6BAAAA,OAAQK,OAAO;KAAC,CAACC,MAAM,CAACC,SAASC,IAAI,CAAC;IACrE,2FAA2F;IAC3F,yFAAyF;IACzF,uCAAuC;IACvC,OAAOf,gBAAgBK,QAAQC,MAAMG,gBAAgBO,YAAY;QAAEC,cAAcR;IAAY,IAAIO,WAAWE,GAAG,CAAC,CAACC,IAAO,CAAA;YACtHC,WAAWD,EAAEC,SAAS,GAAGZ;YACzBa,SAASF,EAAEE,OAAO,GAAGb;YACrBc,MAAMZ,SAAS,GAAGA,OAAO,EAAE,EAAES,EAAEG,IAAI,EAAE,GAAGH,EAAEG,IAAI;QAChD,CAAA;AACF;AAEA,OAAO,MAAMC,QAAiB;IAC5BC,MAAM;IACNC,QAAOC,EAAE;QACPA,GAAGC,IAAI,CAAC,CAAC,8JAA8J,CAAC;IAC1K;IACAC,SAAQC,GAAG,EAAEvB,IAAI,EAAEC,MAAM,EAAEuB,KAAK,EAAEC,GAAG,EAAE1B,MAAM;YAG4D0B;QAFvG,mFAAmF;QACnF,4FAA4F;QAC5F,OAAO3B,SAASC,mBAAAA,oBAAAA,SAAUJ,MAAMK,OAAOA,MAAMC,QAAQsB,IAAIG,KAAK,CAAC,MAAMC,MAAM,GAAG3B,KAAK0B,KAAK,CAAC,MAAMC,MAAM,EAAEF,gBAAAA,2BAAAA,aAAAA,IAAKR,KAAK,cAAVQ,iCAAAA,WAAYtB,WAAW;IAChI;IACAyB,QAAOR,EAAE,EAAES,IAAI;QACbT,GAAGU,OAAO,CAAC,2CAA2CC,GAAG,CAACF;IAC5D;IACA,uFAAuF;IACvF,mFAAmF;IACnFG,OAAMZ,EAAE,EAAES,IAAI,EAAEI,SAAS;QACvB,IAAI,CAACA,WAAW;QAChB,MAAMC,SAASd,GAAGU,OAAO,CAAC;QACzBG,UAAsBE,OAAO,CAAC,CAACtB,GAAGuB,MAAQF,OAAOH,GAAG,CAACF,MAAMO,KAAKvB,EAAEC,SAAS,EAAED,EAAEE,OAAO;IACzF;IACAsB,gBAAeC,IAAI,EAAEC,IAAI;QACvB,OAAOA,KAAKtB,KAAK;IACnB;IACA,sFAAsF;IACtF,2FAA2F;IAC3F,oEAAoE;IACpEuB,WAAUf,GAAG;QACX,MAAMgB,IAAI7C,YAAY6B;QACtB,IAAI,CAACgB,GAAG,OAAO;QACf,uFAAuF;QACvF,+DAA+D;QAC/D,MAAMC,WAAWD,EAAEE,QAAQ,KAAK,WAAW9C,cAAc4C,EAAEG,KAAK,IAAIlC;QACpE,uFAAuF;QACvF,4FAA4F;QAC5F,MAAMmC,eAAeJ,EAAEtC,WAAW,KAAKO,YAAY,GAAGjB,cAAc,CAAC,EAAEgD,EAAEtC,WAAW,EAAE,GAAGV;QACzF,OAAO,CAAC,MAAM,EAAEgD,EAAEE,QAAQ,CAAC,CAAC,EAAEF,EAAEG,KAAK,CAAC,CAAC,EAAEC,eAAeH,aAAahC,YAAY,CAAC,CAAC,EAAEgC,UAAU,GAAG,IAAI;IACxG;AACF,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/embed.ts"],"sourcesContent":["import type { Block, Chunk } from '../chunk/index.ts';\nimport { CHUNK_VERSION, chunkFromBlocks, parse } from '../chunk/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { modelIdentity } from '../embed/identity.ts';\nimport type { Feature } from './types.ts';\n\n// int8 vectors with a per-vector scale, NULL vector = not yet embedded. Reconcile writes dirty\n// rows; embedding tops up on the next search, so staleness costs recall. Providers, the vector\n// fill, and the search-time helpers live under src/embed/. Block/group structure (D1-D5) lives\n// in src/chunk; this keeps only the title/summary prefix and the empty-chunk drop (D6).\n\nexport type { Chunk };\n\n// Deterministic, so embed time can re-derive text from stored line ranges; the title/summary\n// prefix mirrors bm25 field weighting, and an empty chunk is dropped before the prefix is added.\n//\n// Chunks the body, not the raw file, with `offset` shifting line numbers back onto the raw\n// file so a range stays a direct Read range (sections is 1-indexed over raw too). Chunking raw\n// put the frontmatter block in its own leading chunk on every note with a heading, which made\n// `lines` point at YAML and made frontmatter-only notes near-identical to each other. It also\n// disagreed with FTS, which indexes the body alone.\nfunction chunksOf(blocks: Block[], body: string, search?: { title: string; summary: string }, offset = 0, chunkTokens?: number): Chunk[] {\n const prefix = [search?.title, search?.summary].filter(Boolean).join('\\n');\n // chunk()'s default text mode is 'raw' (D9), the shipped choice. A hardcoded option change\n // here must bump src/chunk/version.ts's CHUNK_VERSION -- the signature for every chunker\n // input not already carried in config.\n return chunkFromBlocks(blocks, body, chunkTokens !== undefined ? { targetTokens: chunkTokens } : undefined).map((c) => ({\n startLine: c.startLine + offset,\n endLine: c.endLine + offset,\n text: prefix ? `${prefix}\\n${c.text}` : c.text,\n }));\n}\n\nexport const embed: Feature = {\n name: 'embed',\n async schema(db) {\n await db.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector BLOB, PRIMARY KEY (\"path\", chunk))`);\n },\n extract(raw, body, search, _data, cfg, blocks) {\n // Lines the frontmatter occupies, so body line 1 maps back to its raw line number.\n // blocks comes from parseFile's shared parse; falls back to parsing body for a direct call.\n return chunksOf(blocks ?? parse(body), body, search, raw.split('\\n').length - body.split('\\n').length, cfg?.embed?.chunkTokens);\n },\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM embeddings WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n // A tree with no embedding model never had extract() run for the doc (reconcile.ts's per-file\n // filter skips it), so extracted is undefined here -- those docs contribute no rows.\n async store(db, docs) {\n const rows: unknown[][] = [];\n for (const { path, extracted } of docs) {\n if (!extracted) continue;\n (extracted as Chunk[]).forEach((c, idx) => rows.push([path, idx, c.startLine, c.endLine]));\n }\n if (rows.length === 0) return;\n await db.runBatch('INSERT INTO embeddings (\"path\", chunk, start_line, end_line, scale, vector) VALUES (?, ?, ?, ?, NULL, NULL)', rows);\n },\n enabledForFile(_cfg, file) {\n return file.embed;\n },\n // Provider/model/chunk-version/identity, keyed so a rebuild notice can name it \"embed\n // settings\". Driven by embedConfig(cfg) directly (a model can be configured with no preset\n // yet using vectors), not by whether this feature is itself active.\n signature(cfg) {\n const e = embedConfig(cfg);\n if (!e) return 'embed:off';\n // Weight identity from identity.ts, no network: a static model's resolved sha or local\n // size+mtime, appended once known so changed weights re-embed.\n const identity = e.provider === 'static' ? modelIdentity(e.model) : undefined;\n // W3b: chunkTokens rides the version token, since it's the one owner lever the chunker\n // itself takes -- changing or clearing it must rebuild exactly like a chunker version bump.\n const chunkVersion = e.chunkTokens !== undefined ? `${CHUNK_VERSION}:${e.chunkTokens}` : CHUNK_VERSION;\n return `embed:${e.provider}:${e.model}:${chunkVersion}${identity !== undefined ? `@${identity}` : ''}`;\n },\n};\n"],"names":["CHUNK_VERSION","chunkFromBlocks","parse","embedConfig","modelIdentity","chunksOf","blocks","body","search","offset","chunkTokens","prefix","title","summary","filter","Boolean","join","undefined","targetTokens","map","c","startLine","endLine","text","embed","name","schema","db","exec","extract","raw","_data","cfg","split","length","remove","paths","runBatch","p","store","docs","rows","path","extracted","forEach","idx","push","enabledForFile","_cfg","file","signature","e","identity","provider","model","chunkVersion"],"mappings":"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"}
@@ -2,6 +2,6 @@ import type { Config } from '../config/index.js';
2
2
  import type { Feature } from './types.js';
3
3
  export declare const FEATURES: Feature[];
4
4
  export declare function activeFeatures(cfg: Config): Feature[];
5
- export { linkEdges } from './links.js';
5
+ export { LINK_EDGES_SQL, linkEdges, toEdges } from './links.js';
6
6
  export type { Section } from './sections.js';
7
7
  export type { Feature } from './types.js';
@@ -15,4 +15,4 @@ export const FEATURES = [
15
15
  export function activeFeatures(cfg) {
16
16
  return FEATURES.filter((feature)=>featureEnabled(cfg, feature.name));
17
17
  }
18
- export { linkEdges } from './links.js';
18
+ export { LINK_EDGES_SQL, linkEdges, toEdges } from './links.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/index.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { featureEnabled } from '../config/index.ts';\nimport { embed } from './embed.ts';\nimport { links } from './links.ts';\nimport { rank } from './rank.ts';\nimport { sections } from './sections.ts';\nimport { tags } from './tags.ts';\nimport type { Feature } from './types.ts';\n\n// Registry order matters: rank reads the links table in afterReconcile.\nexport const FEATURES: Feature[] = [links, sections, tags, rank, embed];\n\nexport function activeFeatures(cfg: Config): Feature[] {\n return FEATURES.filter((feature) => featureEnabled(cfg, feature.name));\n}\n\nexport { linkEdges } from './links.ts';\nexport type { Section } from './sections.ts';\nexport type { Feature } from './types.ts';\n"],"names":["featureEnabled","embed","links","rank","sections","tags","FEATURES","activeFeatures","cfg","filter","feature","name","linkEdges"],"mappings":"AACA,SAASA,cAAc,QAAQ,qBAAqB;AACpD,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,IAAI,QAAQ,YAAY;AACjC,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,IAAI,QAAQ,YAAY;AAGjC,wEAAwE;AACxE,OAAO,MAAMC,WAAsB;IAACJ;IAAOE;IAAUC;IAAMF;IAAMF;CAAM,CAAC;AAExE,OAAO,SAASM,eAAeC,GAAW;IACxC,OAAOF,SAASG,MAAM,CAAC,CAACC,UAAYV,eAAeQ,KAAKE,QAAQC,IAAI;AACtE;AAEA,SAASC,SAAS,QAAQ,aAAa"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/index.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { featureEnabled } from '../config/index.ts';\nimport { embed } from './embed.ts';\nimport { links } from './links.ts';\nimport { rank } from './rank.ts';\nimport { sections } from './sections.ts';\nimport { tags } from './tags.ts';\nimport type { Feature } from './types.ts';\n\n// Registry order matters: rank reads the links table in afterReconcile.\nexport const FEATURES: Feature[] = [links, sections, tags, rank, embed];\n\nexport function activeFeatures(cfg: Config): Feature[] {\n return FEATURES.filter((feature) => featureEnabled(cfg, feature.name));\n}\n\nexport { LINK_EDGES_SQL, linkEdges, toEdges } from './links.ts';\nexport type { Section } from './sections.ts';\nexport type { Feature } from './types.ts';\n"],"names":["featureEnabled","embed","links","rank","sections","tags","FEATURES","activeFeatures","cfg","filter","feature","name","LINK_EDGES_SQL","linkEdges","toEdges"],"mappings":"AACA,SAASA,cAAc,QAAQ,qBAAqB;AACpD,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,IAAI,QAAQ,YAAY;AACjC,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,IAAI,QAAQ,YAAY;AAGjC,wEAAwE;AACxE,OAAO,MAAMC,WAAsB;IAACJ;IAAOE;IAAUC;IAAMF;IAAMF;CAAM,CAAC;AAExE,OAAO,SAASM,eAAeC,GAAW;IACxC,OAAOF,SAASG,MAAM,CAAC,CAACC,UAAYV,eAAeQ,KAAKE,QAAQC,IAAI;AACtE;AAEA,SAASC,cAAc,EAAEC,SAAS,EAAEC,OAAO,QAAQ,aAAa"}
@@ -1,4 +1,9 @@
1
- import type { DatabaseSync } from 'node:sqlite';
1
+ import type { Connection } from '../store/types.js';
2
2
  import type { Feature } from './types.js';
3
- export declare function linkEdges(db: DatabaseSync): [string, string][];
3
+ export declare const LINK_EDGES_SQL = "SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)";
4
+ export declare function toEdges(rows: Array<{
5
+ src: string;
6
+ dst: string;
7
+ }>): [string, string][];
8
+ export declare function linkEdges(db: Connection): Promise<[string, string][]>;
4
9
  export declare const links: Feature;
@@ -107,33 +107,54 @@ function buildByBase(files) {
107
107
  }
108
108
  return byBase;
109
109
  }
110
- // Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE
111
- // keys on (src, target): a link/embed sibling pair shares one resolution, so either row's
112
- // mismatch rewrites both.
113
- function resolveRows(db, rows, pathSet, byBase) {
114
- const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');
115
- let changed = false;
110
+ // resolveTarget applied to `rows`, returning just the rows whose dst actually changed -- the
111
+ // caller writes those in one runBatch call, keyed on (src, target) so a link/embed sibling
112
+ // pair sharing one resolution rewrites both.
113
+ function resolveRows(rows, pathSet, byBase) {
114
+ const changed = [];
116
115
  for (const row of rows){
117
116
  const dst = resolveTarget(row.src, row.target, pathSet, byBase);
118
- if (dst !== row.dst) {
119
- update.run(dst, row.src, row.target);
120
- changed = true;
121
- }
117
+ if (dst !== row.dst) changed.push({
118
+ ...row,
119
+ dst
120
+ });
122
121
  }
123
122
  return changed;
124
123
  }
124
+ async function writeResolved(db, rows) {
125
+ if (rows.length === 0) return;
126
+ await db.runBatch('UPDATE links SET dst = ? WHERE src = ? AND target = ?', rows.map((r)=>[
127
+ r.dst,
128
+ r.src,
129
+ r.target
130
+ ]));
131
+ }
125
132
  // A new or deleted file can change any note's resolution, so re-resolve the whole table.
126
133
  // Fallback for cold builds and large deltas -- see afterReconcile's threshold.
127
- function resolveAll(db, files) {
134
+ async function resolveAll(db, files) {
128
135
  const pathSet = new Set(files.map((f)=>f.relPath));
129
136
  const byBase = buildByBase(files);
130
- const rows = db.prepare('SELECT src, target, dst, embed FROM links').all();
131
- return resolveRows(db, rows, pathSet, byBase);
137
+ const stmt = await db.prepare('SELECT src, target, dst, embed FROM links');
138
+ const rows = await stmt.all();
139
+ const changed = resolveRows(rows, pathSet, byBase);
140
+ await writeResolved(db, changed);
141
+ return changed.length > 0;
142
+ }
143
+ // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable limit
144
+ // (SQLITE_MAX_VARIABLE_NUMBER, 32766); harmless headroom on DuckDB, which has no such cap.
145
+ async function selectIn(db, column, keys) {
146
+ const rows = [];
147
+ for(let i = 0; i < keys.length; i += 500){
148
+ const chunk = keys.slice(i, i + 500);
149
+ const stmt = await db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${chunk.map(()=>'?').join(', ')})`);
150
+ rows.push(...await stmt.all(...chunk));
151
+ }
152
+ return rows;
132
153
  }
133
154
  // Re-resolve only what this reconcile could have touched: re-stored sources, links whose
134
155
  // target basename matches an added/vanished file, and links whose dst just vanished. All
135
156
  // three are indexed lookups, so this never reads the whole table.
136
- function resolveIncremental(db, delta) {
157
+ async function resolveIncremental(db, delta) {
137
158
  const pathSet = new Set(delta.files.map((f)=>f.relPath));
138
159
  const byBase = buildByBase(delta.files);
139
160
  const changedBasenames = new Set();
@@ -144,113 +165,119 @@ function resolveIncremental(db, delta) {
144
165
  const collect = (rows)=>{
145
166
  for (const row of rows)candidates.set(`${row.src}\0${row.target}\0${row.embed}`, row);
146
167
  };
147
- // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable
148
- // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).
149
- const collectIn = (column, keys)=>{
150
- for(let i = 0; i < keys.length; i += 500){
151
- const chunk = keys.slice(i, i + 500);
152
- const placeholders = chunk.map(()=>'?').join(', ');
153
- collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk));
154
- }
155
- };
156
- collectIn('src', delta.reparsed);
157
- collectIn('target_base', [
168
+ collect(await selectIn(db, 'src', delta.reparsed));
169
+ collect(await selectIn(db, 'target_base', [
158
170
  ...changedBasenames
159
- ]);
160
- collectIn('dst', delta.vanished);
161
- return resolveRows(db, [
171
+ ]));
172
+ collect(await selectIn(db, 'dst', delta.vanished));
173
+ const changed = resolveRows([
162
174
  ...candidates.values()
163
175
  ], pathSet, byBase);
176
+ await writeResolved(db, changed);
177
+ return changed.length > 0;
164
178
  }
165
- // Resolved edges, for rank and for search's graph expansion.
166
- export function linkEdges(db) {
167
- // One edge per row, as 0.12's grain always had it -- two written targets resolving to one
168
- // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an
169
- // embed whose exact (src, target) also exists as a link: that second row is new with the
170
- // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the
171
- // primary key and fires only for embed rows; both branches still scan the table.
172
- // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --
173
- // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for
174
- // backlinks/peek.
175
- const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst
179
+ // One edge per row, as 0.12's grain always had it -- two written targets resolving to one
180
+ // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an
181
+ // embed whose exact (src, target) also exists as a link: that second row is new with the
182
+ // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the
183
+ // primary key and fires only for embed rows; both branches still scan the table.
184
+ // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --
185
+ // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for
186
+ // backlinks/peek. Exported (not just linkEdges below) so commands/signals.ts's async caller,
187
+ // which only has the Store's prepare(), can run the identical statement itself.
188
+ export const LINK_EDGES_SQL = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst
176
189
  UNION ALL
177
190
  SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst
178
191
  AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;
179
- return db.prepare(sql).all().map((r)=>[
192
+ export function toEdges(rows) {
193
+ return rows.map((r)=>[
180
194
  r.src,
181
195
  r.dst
182
196
  ]);
183
197
  }
184
- // remove() has already deleted the rows by the time afterReconcile runs, so it records here
185
- // whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.
186
- const removedWithLinks = new WeakMap();
187
- function recordRemoved(delta, path) {
188
- let set = removedWithLinks.get(delta);
189
- if (!set) {
190
- set = new Set();
191
- removedWithLinks.set(delta, set);
192
- }
193
- set.add(path);
194
- }
195
- // remove() runs per path; the vanished list is an array, so cache the Set per delta.
196
- const vanishedSets = new WeakMap();
197
- function vanishedSet(delta) {
198
- let set = vanishedSets.get(delta);
199
- if (!set) {
200
- set = new Set(delta.vanished);
201
- vanishedSets.set(delta, set);
202
- }
203
- return set;
198
+ // Resolved edges, for rank (inside reconcile's afterReconcile pass).
199
+ export async function linkEdges(db) {
200
+ const stmt = await db.prepare(LINK_EDGES_SQL);
201
+ return toEdges(await stmt.all());
204
202
  }
203
+ // remove()/store() have already deleted rows by the time afterReconcile runs, so this records
204
+ // whether any reconcile pass deleted at least one link row -- whether a file vanished, or a
205
+ // reparse's stale rows shrank. dstChanged alone misses the reparsed-to-zero-links case (no
206
+ // surviving rows to re-resolve). Keyed on the delta object, so the state dies with the reconcile.
207
+ const removedWithLinks = new WeakMap();
205
208
  export const links = {
206
209
  name: 'links',
207
- schema (db) {
208
- db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');
209
- db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');
210
- db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');
210
+ async schema (db) {
211
+ await db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');
212
+ await db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');
213
+ await db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');
211
214
  },
212
215
  extract,
213
- // Reparsed docs are diffed in store() below instead of wiped here: deleting and
214
- // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge
215
- // change and recompute PageRank -- measured as most of the remaining update cost.
216
- remove (db, path, delta) {
217
- if (!vanishedSet(delta).has(path)) return;
218
- const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);
219
- if (Number(result.changes) > 0) recordRemoved(delta, path);
216
+ // Reparsed docs are diffed in store() below instead of wiped here: deleting and re-inserting
217
+ // resets dst to NULL, which made every touch-only reparse read as an edge change and
218
+ // recompute PageRank -- measured as most of the remaining update cost. Only genuinely
219
+ // vanished files clear here.
220
+ async remove (db, paths, delta) {
221
+ const vanishedSet = new Set(delta.vanished);
222
+ const vanished = paths.filter((p)=>vanishedSet.has(p));
223
+ if (vanished.length === 0) return;
224
+ const existingRows = await selectIn(db, 'src', vanished);
225
+ await db.runBatch('DELETE FROM links WHERE src = ?', vanished.map((p)=>[
226
+ p
227
+ ]));
228
+ if (existingRows.length > 0) removedWithLinks.set(delta, true);
220
229
  },
221
- store (db, path, extracted, delta) {
222
- const targets = extracted;
223
- // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.
224
- // \0 can't appear in a target, so the composite key never collides.
225
- let stale;
226
- if (targets.length === 0) {
227
- stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);
228
- } else {
229
- const placeholders = targets.map(()=>'?').join(', ');
230
- const keys = targets.map((t)=>`${t.target}\0${t.embed ? '1' : '0'}`);
231
- stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);
230
+ async store (db, docs, delta) {
231
+ const byPath = new Map(docs.map((d)=>[
232
+ d.path,
233
+ d.extracted
234
+ ]));
235
+ const addedSet = new Set(delta.added);
236
+ const reparsedExisting = docs.map((d)=>d.path).filter((p)=>!addedSet.has(p));
237
+ // Stale rows (existing target/embed pairs the new parse no longer contains) are real edge
238
+ // removals; diffed in JS from one bulk SELECT rather than a per-file NOT IN statement, so
239
+ // this whole pass costs one query plus one batched DELETE regardless of file count.
240
+ const existingRows = await selectIn(db, 'src', reparsedExisting);
241
+ const staleRows = existingRows.filter((row)=>{
242
+ var _byPath_get;
243
+ const targets = (_byPath_get = byPath.get(row.src)) !== null && _byPath_get !== void 0 ? _byPath_get : [];
244
+ return !targets.some((t)=>t.target === row.target && (t.embed ? 1 : 0) === row.embed);
245
+ });
246
+ if (staleRows.length > 0) {
247
+ await db.runBatch('DELETE FROM links WHERE src = ? AND target = ? AND embed = ?', staleRows.map((r)=>[
248
+ r.src,
249
+ r.target,
250
+ r.embed
251
+ ]));
252
+ removedWithLinks.set(delta, true);
253
+ }
254
+ // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same value
255
+ // and reports no change; only genuinely new rows start at NULL. A same-note anchor's dst is
256
+ // always its own src, never another file's basename, so it gets no target_base -- SQL's
257
+ // `IN` never matches NULL, keeping it out of resolveIncremental's target_base lookup.
258
+ const upsertRows = [];
259
+ for (const [path, targets] of byPath){
260
+ for (const { target, embed } of targets)upsertRows.push([
261
+ path,
262
+ target,
263
+ target.startsWith('#') ? null : baseKey(cleanTarget(target)),
264
+ embed ? 1 : 0
265
+ ]);
266
+ }
267
+ if (upsertRows.length > 0) {
268
+ await db.runBatch('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base', upsertRows);
232
269
  }
233
- if (Number(stale.changes) > 0) recordRemoved(delta, path);
234
- // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same
235
- // value and reports no change; only genuinely new rows start at NULL.
236
- const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');
237
- // A same-note anchor's dst is always its own src, never another file's basename, so it
238
- // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of
239
- // resolveIncremental's cross-file collectIn('target_base', ...) entirely.
240
- for (const { target, embed } of targets)insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);
241
270
  },
242
- afterReconcile (db, delta) {
243
- var _ref;
271
+ async afterReconcile (db, delta) {
244
272
  var _removedWithLinks_get;
245
- // Any deleted rows that carried edges mean the edge set shrank -- whether the file
246
- // vanished or was reparsed down to fewer/no links. dstChanged alone misses the
247
- // reparsed-to-zero-links case (no surviving rows to re-resolve).
248
- const removeHadLinks = ((_ref = (_removedWithLinks_get = removedWithLinks.get(delta)) === null || _removedWithLinks_get === void 0 ? void 0 : _removedWithLinks_get.size) !== null && _ref !== void 0 ? _ref : 0) > 0;
273
+ // Any deleted rows mean the edge set shrank -- whether the file vanished or was reparsed
274
+ // down to fewer/no links.
275
+ const removeHadLinks = (_removedWithLinks_get = removedWithLinks.get(delta)) !== null && _removedWithLinks_get !== void 0 ? _removedWithLinks_get : false;
249
276
  const churn = delta.reparsed.length + delta.vanished.length;
250
277
  // Past this share of the tree, a full pass is the only one guaranteed to match
251
278
  // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.
252
279
  const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;
253
- const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);
280
+ const dstChanged = large ? await resolveAll(db, delta.files) : await resolveIncremental(db, delta);
254
281
  delta.linksChanged = dstChanged || removeHadLinks;
255
282
  }
256
283
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport type { FileStat } from '../scan/index.ts';\nimport { maskRegions } from './fences.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`![](x.md)` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\n// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #\n// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise\n// the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n // backlinks/peek.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n // A same-note anchor's dst is always its own src, never another file's basename, so it\n // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of\n // resolveIncremental's cross-file collectIn('target_base', ...) entirely.\n for (const { target, embed } of targets) insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["posix","maskRegions","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","seen","Set","results","add","embed","key","has","push","m","matchAll","index","dest","includes","value","Object","values","item","Array","isArray","exec","cleanTarget","replace","baseKey","path","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","p","added","vanished","collect","collectIn","column","keys","i","chunk","slice","placeholders","reparsed","linkEdges","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","links","name","schema","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { FileStat } from '../scan/index.ts';\nimport type { Connection } from '../store/types.ts';\nimport { maskRegions } from './fences.ts';\nimport type { ExtractedDoc, Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`![](x.md)` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\n// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #\n// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise\n// the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// resolveTarget applied to `rows`, returning just the rows whose dst actually changed -- the\n// caller writes those in one runBatch call, keyed on (src, target) so a link/embed sibling\n// pair sharing one resolution rewrites both.\nfunction resolveRows(rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): LinkRow[] {\n const changed: LinkRow[] = [];\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) changed.push({ ...row, dst });\n }\n return changed;\n}\n\nasync function writeResolved(db: Connection, rows: LinkRow[]): Promise<void> {\n if (rows.length === 0) return;\n await db.runBatch(\n 'UPDATE links SET dst = ? WHERE src = ? AND target = ?',\n rows.map((r) => [r.dst, r.src, r.target])\n );\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nasync function resolveAll(db: Connection, files: FileStat[]): Promise<boolean> {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const stmt = await db.prepare('SELECT src, target, dst, embed FROM links');\n const rows = (await stmt.all()) as unknown as LinkRow[];\n const changed = resolveRows(rows, pathSet, byBase);\n await writeResolved(db, changed);\n return changed.length > 0;\n}\n\n// Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable limit\n// (SQLITE_MAX_VARIABLE_NUMBER, 32766); harmless headroom on DuckDB, which has no such cap.\nasync function selectIn(db: Connection, column: string, keys: string[]): Promise<LinkRow[]> {\n const rows: LinkRow[] = [];\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const stmt = await db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${chunk.map(() => '?').join(', ')})`);\n rows.push(...((await stmt.all(...chunk)) as unknown as LinkRow[]));\n }\n return rows;\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nasync function resolveIncremental(db: Connection, delta: ReconcileDelta): Promise<boolean> {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n collect(await selectIn(db, 'src', delta.reparsed));\n collect(await selectIn(db, 'target_base', [...changedBasenames]));\n collect(await selectIn(db, 'dst', delta.vanished));\n\n const changed = resolveRows([...candidates.values()], pathSet, byBase);\n await writeResolved(db, changed);\n return changed.length > 0;\n}\n\n// One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n// dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n// embed whose exact (src, target) also exists as a link: that second row is new with the\n// embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n// primary key and fires only for embed rows; both branches still scan the table.\n// Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n// Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n// backlinks/peek. Exported (not just linkEdges below) so commands/signals.ts's async caller,\n// which only has the Store's prepare(), can run the identical statement itself.\nexport const LINK_EDGES_SQL = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n\nexport function toEdges(rows: Array<{ src: string; dst: string }>): [string, string][] {\n return rows.map((r) => [r.src, r.dst]);\n}\n\n// Resolved edges, for rank (inside reconcile's afterReconcile pass).\nexport async function linkEdges(db: Connection): Promise<[string, string][]> {\n const stmt = await db.prepare(LINK_EDGES_SQL);\n return toEdges((await stmt.all()) as Array<{ src: string; dst: string }>);\n}\n\n// remove()/store() have already deleted rows by the time afterReconcile runs, so this records\n// whether any reconcile pass deleted at least one link row -- whether a file vanished, or a\n// reparse's stale rows shrank. dstChanged alone misses the reparsed-to-zero-links case (no\n// surviving rows to re-resolve). Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, boolean>();\n\nexport const links: Feature = {\n name: 'links',\n async schema(db) {\n await db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n await db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n await db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and re-inserting\n // resets dst to NULL, which made every touch-only reparse read as an edge change and\n // recompute PageRank -- measured as most of the remaining update cost. Only genuinely\n // vanished files clear here.\n async remove(db, paths, delta) {\n const vanishedSet = new Set(delta.vanished);\n const vanished = paths.filter((p) => vanishedSet.has(p));\n if (vanished.length === 0) return;\n const existingRows = await selectIn(db, 'src', vanished);\n await db.runBatch(\n 'DELETE FROM links WHERE src = ?',\n vanished.map((p) => [p])\n );\n if (existingRows.length > 0) removedWithLinks.set(delta, true);\n },\n async store(db, docs: ExtractedDoc[], delta) {\n const byPath = new Map(docs.map((d) => [d.path, d.extracted as Array<{ target: string; embed: boolean }>]));\n const addedSet = new Set(delta.added);\n const reparsedExisting = docs.map((d) => d.path).filter((p) => !addedSet.has(p));\n\n // Stale rows (existing target/embed pairs the new parse no longer contains) are real edge\n // removals; diffed in JS from one bulk SELECT rather than a per-file NOT IN statement, so\n // this whole pass costs one query plus one batched DELETE regardless of file count.\n const existingRows = await selectIn(db, 'src', reparsedExisting);\n const staleRows = existingRows.filter((row) => {\n const targets = byPath.get(row.src) ?? [];\n return !targets.some((t) => t.target === row.target && (t.embed ? 1 : 0) === row.embed);\n });\n if (staleRows.length > 0) {\n await db.runBatch(\n 'DELETE FROM links WHERE src = ? AND target = ? AND embed = ?',\n staleRows.map((r) => [r.src, r.target, r.embed])\n );\n removedWithLinks.set(delta, true);\n }\n\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same value\n // and reports no change; only genuinely new rows start at NULL. A same-note anchor's dst is\n // always its own src, never another file's basename, so it gets no target_base -- SQL's\n // `IN` never matches NULL, keeping it out of resolveIncremental's target_base lookup.\n const upsertRows: unknown[][] = [];\n for (const [path, targets] of byPath) {\n for (const { target, embed } of targets) upsertRows.push([path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0]);\n }\n if (upsertRows.length > 0) {\n await db.runBatch('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base', upsertRows);\n }\n },\n async afterReconcile(db, delta) {\n // Any deleted rows mean the edge set shrank -- whether the file vanished or was reparsed\n // down to fewer/no links.\n const removeHadLinks = removedWithLinks.get(delta) ?? false;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? await resolveAll(db, delta.files) : await resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["posix","maskRegions","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","seen","Set","results","add","embed","key","has","push","m","matchAll","index","dest","includes","value","Object","values","item","Array","isArray","exec","cleanTarget","replace","baseKey","path","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","rows","changed","row","dst","writeResolved","db","runBatch","r","resolveAll","stmt","prepare","all","selectIn","column","keys","i","chunk","slice","resolveIncremental","delta","changedBasenames","p","added","vanished","collect","reparsed","LINK_EDGES_SQL","toEdges","linkEdges","removedWithLinks","WeakMap","links","name","schema","remove","vanishedSet","filter","existingRows","store","docs","byPath","d","extracted","addedSet","reparsedExisting","staleRows","targets","some","t","upsertRows","afterReconcile","removeHadLinks","churn","large","dstChanged","linksChanged"],"mappings":"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"}
@@ -4,17 +4,24 @@ import { linkEdges } from './links.js';
4
4
  // note" prior. Depends on links (enforced in config.featureEnabled).
5
5
  export const rank = {
6
6
  name: 'rank',
7
- schema (db) {
8
- const columns = db.prepare('PRAGMA table_info(frontmatter)').all();
9
- if (!columns.some((c)=>c.name === '_rank')) db.exec('ALTER TABLE frontmatter ADD COLUMN "_rank" REAL');
7
+ async schema (db) {
8
+ const stmt = await db.prepare('PRAGMA table_info(frontmatter)');
9
+ const columns = await stmt.all();
10
+ if (!columns.some((c)=>c.name === '_rank')) await db.exec('ALTER TABLE frontmatter ADD COLUMN "_rank" REAL');
10
11
  },
11
- afterReconcile (db, delta) {
12
+ async afterReconcile (db, delta) {
12
13
  // Only an explicit linksChanged:false skips. Rank also depends on the node set, so any
13
14
  // add or vanish recomputes -- otherwise a new linkless note keeps _rank NULL.
14
15
  if (delta.linksChanged === false && delta.added.length === 0 && delta.vanished.length === 0) return;
15
- const nodes = db.prepare('SELECT "path" FROM frontmatter').all().map((r)=>r.path);
16
- const ranks = pagerank(nodes, linkEdges(db));
17
- const update = db.prepare('UPDATE frontmatter SET "_rank" = ? WHERE "path" = ?');
18
- for (const [path, value] of ranks)update.run(value, path);
16
+ const pathsStmt = await db.prepare('SELECT "path" FROM frontmatter');
17
+ const nodes = (await pathsStmt.all()).map((r)=>r.path);
18
+ const ranks = pagerank(nodes, await linkEdges(db));
19
+ if (ranks.size === 0) return;
20
+ await db.runBatch('UPDATE frontmatter SET "_rank" = ? WHERE "path" = ?', [
21
+ ...ranks
22
+ ].map(([path, value])=>[
23
+ value,
24
+ path
25
+ ]));
19
26
  }
20
27
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/rank.ts"],"sourcesContent":["import { pagerank } from '../graph/graph.ts';\nimport { linkEdges } from './links.ts';\nimport type { Feature } from './types.ts';\n\n// frontmatter._rank: PageRank over resolved links -- a static \"how load-bearing is this\n// note\" prior. Depends on links (enforced in config.featureEnabled).\n\nexport const rank: Feature = {\n name: 'rank',\n schema(db) {\n const columns = db.prepare('PRAGMA table_info(frontmatter)').all() as Array<{ name: string }>;\n if (!columns.some((c) => c.name === '_rank')) db.exec('ALTER TABLE frontmatter ADD COLUMN \"_rank\" REAL');\n },\n afterReconcile(db, delta) {\n // Only an explicit linksChanged:false skips. Rank also depends on the node set, so any\n // add or vanish recomputes -- otherwise a new linkless note keeps _rank NULL.\n if (delta.linksChanged === false && delta.added.length === 0 && delta.vanished.length === 0) return;\n const nodes = (db.prepare('SELECT \"path\" FROM frontmatter').all() as Array<{ path: string }>).map((r) => r.path);\n const ranks = pagerank(nodes, linkEdges(db));\n const update = db.prepare('UPDATE frontmatter SET \"_rank\" = ? WHERE \"path\" = ?');\n for (const [path, value] of ranks) update.run(value, path);\n },\n};\n"],"names":["pagerank","linkEdges","rank","name","schema","db","columns","prepare","all","some","c","exec","afterReconcile","delta","linksChanged","added","length","vanished","nodes","map","r","path","ranks","update","value","run"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,oBAAoB;AAC7C,SAASC,SAAS,QAAQ,aAAa;AAGvC,wFAAwF;AACxF,qEAAqE;AAErE,OAAO,MAAMC,OAAgB;IAC3BC,MAAM;IACNC,QAAOC,EAAE;QACP,MAAMC,UAAUD,GAAGE,OAAO,CAAC,kCAAkCC,GAAG;QAChE,IAAI,CAACF,QAAQG,IAAI,CAAC,CAACC,IAAMA,EAAEP,IAAI,KAAK,UAAUE,GAAGM,IAAI,CAAC;IACxD;IACAC,gBAAeP,EAAE,EAAEQ,KAAK;QACtB,uFAAuF;QACvF,8EAA8E;QAC9E,IAAIA,MAAMC,YAAY,KAAK,SAASD,MAAME,KAAK,CAACC,MAAM,KAAK,KAAKH,MAAMI,QAAQ,CAACD,MAAM,KAAK,GAAG;QAC7F,MAAME,QAAQ,AAACb,GAAGE,OAAO,CAAC,kCAAkCC,GAAG,GAA+BW,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;QAC/G,MAAMC,QAAQtB,SAASkB,OAAOjB,UAAUI;QACxC,MAAMkB,SAASlB,GAAGE,OAAO,CAAC;QAC1B,KAAK,MAAM,CAACc,MAAMG,MAAM,IAAIF,MAAOC,OAAOE,GAAG,CAACD,OAAOH;IACvD;AACF,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/rank.ts"],"sourcesContent":["import { pagerank } from '../graph/graph.ts';\nimport { linkEdges } from './links.ts';\nimport type { Feature } from './types.ts';\n\n// frontmatter._rank: PageRank over resolved links -- a static \"how load-bearing is this\n// note\" prior. Depends on links (enforced in config.featureEnabled).\n\nexport const rank: Feature = {\n name: 'rank',\n async schema(db) {\n const stmt = await db.prepare('PRAGMA table_info(frontmatter)');\n const columns = (await stmt.all()) as Array<{ name: string }>;\n if (!columns.some((c) => c.name === '_rank')) await db.exec('ALTER TABLE frontmatter ADD COLUMN \"_rank\" REAL');\n },\n async afterReconcile(db, delta) {\n // Only an explicit linksChanged:false skips. Rank also depends on the node set, so any\n // add or vanish recomputes -- otherwise a new linkless note keeps _rank NULL.\n if (delta.linksChanged === false && delta.added.length === 0 && delta.vanished.length === 0) return;\n const pathsStmt = await db.prepare('SELECT \"path\" FROM frontmatter');\n const nodes = ((await pathsStmt.all()) as Array<{ path: string }>).map((r) => r.path);\n const ranks = pagerank(nodes, await linkEdges(db));\n if (ranks.size === 0) return;\n await db.runBatch(\n 'UPDATE frontmatter SET \"_rank\" = ? WHERE \"path\" = ?',\n [...ranks].map(([path, value]) => [value, path])\n );\n },\n};\n"],"names":["pagerank","linkEdges","rank","name","schema","db","stmt","prepare","columns","all","some","c","exec","afterReconcile","delta","linksChanged","added","length","vanished","pathsStmt","nodes","map","r","path","ranks","size","runBatch","value"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,oBAAoB;AAC7C,SAASC,SAAS,QAAQ,aAAa;AAGvC,wFAAwF;AACxF,qEAAqE;AAErE,OAAO,MAAMC,OAAgB;IAC3BC,MAAM;IACN,MAAMC,QAAOC,EAAE;QACb,MAAMC,OAAO,MAAMD,GAAGE,OAAO,CAAC;QAC9B,MAAMC,UAAW,MAAMF,KAAKG,GAAG;QAC/B,IAAI,CAACD,QAAQE,IAAI,CAAC,CAACC,IAAMA,EAAER,IAAI,KAAK,UAAU,MAAME,GAAGO,IAAI,CAAC;IAC9D;IACA,MAAMC,gBAAeR,EAAE,EAAES,KAAK;QAC5B,uFAAuF;QACvF,8EAA8E;QAC9E,IAAIA,MAAMC,YAAY,KAAK,SAASD,MAAME,KAAK,CAACC,MAAM,KAAK,KAAKH,MAAMI,QAAQ,CAACD,MAAM,KAAK,GAAG;QAC7F,MAAME,YAAY,MAAMd,GAAGE,OAAO,CAAC;QACnC,MAAMa,QAAQ,AAAE,CAAA,MAAMD,UAAUV,GAAG,EAAC,EAA+BY,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;QACpF,MAAMC,QAAQxB,SAASoB,OAAO,MAAMnB,UAAUI;QAC9C,IAAImB,MAAMC,IAAI,KAAK,GAAG;QACtB,MAAMpB,GAAGqB,QAAQ,CACf,uDACA;eAAIF;SAAM,CAACH,GAAG,CAAC,CAAC,CAACE,MAAMI,MAAM,GAAK;gBAACA;gBAAOJ;aAAK;IAEnD;AACF,EAAE"}
@@ -26,15 +26,28 @@ function extract(raw) {
26
26
  }
27
27
  export const sections = {
28
28
  name: 'sections',
29
- schema (db) {
30
- db.exec(`CREATE TABLE IF NOT EXISTS sections ("path" TEXT, idx INTEGER, level INTEGER, heading TEXT, start_line INTEGER, end_line INTEGER, tokens INTEGER, PRIMARY KEY ("path", idx))`);
29
+ async schema (db) {
30
+ await db.exec(`CREATE TABLE IF NOT EXISTS sections ("path" TEXT, idx INTEGER, level INTEGER, heading TEXT, start_line INTEGER, end_line INTEGER, tokens INTEGER, PRIMARY KEY ("path", idx))`);
31
31
  },
32
32
  extract,
33
- remove (db, path) {
34
- db.prepare('DELETE FROM sections WHERE "path" = ?').run(path);
33
+ async remove (db, paths) {
34
+ if (paths.length === 0) return;
35
+ await db.runBatch('DELETE FROM sections WHERE "path" = ?', paths.map((p)=>[
36
+ p
37
+ ]));
35
38
  },
36
- store (db, path, extracted) {
37
- const insert = db.prepare('INSERT INTO sections ("path", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');
38
- extracted.forEach((s, idx)=>insert.run(path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens));
39
+ async store (db, docs) {
40
+ const rows = [];
41
+ for (const { path, extracted } of docs)extracted.forEach((s, idx)=>rows.push([
42
+ path,
43
+ idx,
44
+ s.level,
45
+ s.heading,
46
+ s.startLine,
47
+ s.endLine,
48
+ s.tokens
49
+ ]));
50
+ if (rows.length === 0) return;
51
+ await db.runBatch('INSERT INTO sections ("path", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?)', rows);
39
52
  }
40
53
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/sections.ts"],"sourcesContent":["import { estimateTokens } from '../chunk/index.ts';\nimport { fenceTracker } from './fences.ts';\nimport type { Feature } from './types.ts';\n\n// sections(path, idx, level, heading, start_line, end_line, tokens): the heading outline,\n// 1-indexed over the raw file so a row is a direct Read range; tokens is estimateTokens (D5),\n// CJK-aware rather than a flat chars/4.\n\nexport interface Section {\n level: number;\n heading: string;\n startLine: number;\n endLine: number;\n tokens: number;\n}\n\n// Headings outside fenced code blocks.\nfunction extract(raw: string): Section[] {\n const lines = raw.split('\\n');\n const found: Section[] = [];\n const fence = fenceTracker();\n for (let i = 0; i < lines.length; i++) {\n if (fence.feed(lines[i])) continue;\n if (fence.inFence) continue;\n const m = lines[i].match(/^(#{1,6}) +(.*)/);\n if (m) found.push({ level: m[1].length, heading: m[2].trim(), startLine: i + 1, endLine: lines.length, tokens: 0 });\n }\n for (let s = 0; s < found.length; s++) {\n if (s + 1 < found.length) found[s].endLine = found[s + 1].startLine - 1;\n const text = lines.slice(found[s].startLine - 1, found[s].endLine).join('\\n');\n found[s].tokens = Math.ceil(estimateTokens(text));\n }\n return found;\n}\n\nexport const sections: Feature = {\n name: 'sections',\n schema(db) {\n db.exec(`CREATE TABLE IF NOT EXISTS sections (\"path\" TEXT, idx INTEGER, level INTEGER, heading TEXT, start_line INTEGER, end_line INTEGER, tokens INTEGER, PRIMARY KEY (\"path\", idx))`);\n },\n extract,\n remove(db, path) {\n db.prepare('DELETE FROM sections WHERE \"path\" = ?').run(path);\n },\n store(db, path, extracted) {\n const insert = db.prepare('INSERT INTO sections (\"path\", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');\n (extracted as Section[]).forEach((s, idx) => insert.run(path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens));\n },\n};\n"],"names":["estimateTokens","fenceTracker","extract","raw","lines","split","found","fence","i","length","feed","inFence","m","match","push","level","heading","trim","startLine","endLine","tokens","s","text","slice","join","Math","ceil","sections","name","schema","db","exec","remove","path","prepare","run","store","extracted","insert","forEach","idx"],"mappings":"AAAA,SAASA,cAAc,QAAQ,oBAAoB;AACnD,SAASC,YAAY,QAAQ,cAAc;AAe3C,uCAAuC;AACvC,SAASC,QAAQC,GAAW;IAC1B,MAAMC,QAAQD,IAAIE,KAAK,CAAC;IACxB,MAAMC,QAAmB,EAAE;IAC3B,MAAMC,QAAQN;IACd,IAAK,IAAIO,IAAI,GAAGA,IAAIJ,MAAMK,MAAM,EAAED,IAAK;QACrC,IAAID,MAAMG,IAAI,CAACN,KAAK,CAACI,EAAE,GAAG;QAC1B,IAAID,MAAMI,OAAO,EAAE;QACnB,MAAMC,IAAIR,KAAK,CAACI,EAAE,CAACK,KAAK,CAAC;QACzB,IAAID,GAAGN,MAAMQ,IAAI,CAAC;YAAEC,OAAOH,CAAC,CAAC,EAAE,CAACH,MAAM;YAAEO,SAASJ,CAAC,CAAC,EAAE,CAACK,IAAI;YAAIC,WAAWV,IAAI;YAAGW,SAASf,MAAMK,MAAM;YAAEW,QAAQ;QAAE;IACnH;IACA,IAAK,IAAIC,IAAI,GAAGA,IAAIf,MAAMG,MAAM,EAAEY,IAAK;QACrC,IAAIA,IAAI,IAAIf,MAAMG,MAAM,EAAEH,KAAK,CAACe,EAAE,CAACF,OAAO,GAAGb,KAAK,CAACe,IAAI,EAAE,CAACH,SAAS,GAAG;QACtE,MAAMI,OAAOlB,MAAMmB,KAAK,CAACjB,KAAK,CAACe,EAAE,CAACH,SAAS,GAAG,GAAGZ,KAAK,CAACe,EAAE,CAACF,OAAO,EAAEK,IAAI,CAAC;QACxElB,KAAK,CAACe,EAAE,CAACD,MAAM,GAAGK,KAAKC,IAAI,CAAC1B,eAAesB;IAC7C;IACA,OAAOhB;AACT;AAEA,OAAO,MAAMqB,WAAoB;IAC/BC,MAAM;IACNC,QAAOC,EAAE;QACPA,GAAGC,IAAI,CAAC,CAAC,4KAA4K,CAAC;IACxL;IACA7B;IACA8B,QAAOF,EAAE,EAAEG,IAAI;QACbH,GAAGI,OAAO,CAAC,yCAAyCC,GAAG,CAACF;IAC1D;IACAG,OAAMN,EAAE,EAAEG,IAAI,EAAEI,SAAS;QACvB,MAAMC,SAASR,GAAGI,OAAO,CAAC;QACzBG,UAAwBE,OAAO,CAAC,CAAClB,GAAGmB,MAAQF,OAAOH,GAAG,CAACF,MAAMO,KAAKnB,EAAEN,KAAK,EAAEM,EAAEL,OAAO,EAAEK,EAAEH,SAAS,EAAEG,EAAEF,OAAO,EAAEE,EAAED,MAAM;IACzH;AACF,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/sections.ts"],"sourcesContent":["import { estimateTokens } from '../chunk/index.ts';\nimport { fenceTracker } from './fences.ts';\nimport type { Feature } from './types.ts';\n\n// sections(path, idx, level, heading, start_line, end_line, tokens): the heading outline,\n// 1-indexed over the raw file so a row is a direct Read range; tokens is estimateTokens (D5),\n// CJK-aware rather than a flat chars/4.\n\nexport interface Section {\n level: number;\n heading: string;\n startLine: number;\n endLine: number;\n tokens: number;\n}\n\n// Headings outside fenced code blocks.\nfunction extract(raw: string): Section[] {\n const lines = raw.split('\\n');\n const found: Section[] = [];\n const fence = fenceTracker();\n for (let i = 0; i < lines.length; i++) {\n if (fence.feed(lines[i])) continue;\n if (fence.inFence) continue;\n const m = lines[i].match(/^(#{1,6}) +(.*)/);\n if (m) found.push({ level: m[1].length, heading: m[2].trim(), startLine: i + 1, endLine: lines.length, tokens: 0 });\n }\n for (let s = 0; s < found.length; s++) {\n if (s + 1 < found.length) found[s].endLine = found[s + 1].startLine - 1;\n const text = lines.slice(found[s].startLine - 1, found[s].endLine).join('\\n');\n found[s].tokens = Math.ceil(estimateTokens(text));\n }\n return found;\n}\n\nexport const sections: Feature = {\n name: 'sections',\n async schema(db) {\n await db.exec(`CREATE TABLE IF NOT EXISTS sections (\"path\" TEXT, idx INTEGER, level INTEGER, heading TEXT, start_line INTEGER, end_line INTEGER, tokens INTEGER, PRIMARY KEY (\"path\", idx))`);\n },\n extract,\n async remove(db, paths) {\n if (paths.length === 0) return;\n await db.runBatch(\n 'DELETE FROM sections WHERE \"path\" = ?',\n paths.map((p) => [p])\n );\n },\n async store(db, docs) {\n const rows: unknown[][] = [];\n for (const { path, extracted } of docs) (extracted as Section[]).forEach((s, idx) => rows.push([path, idx, s.level, s.heading, s.startLine, s.endLine, s.tokens]));\n if (rows.length === 0) return;\n await db.runBatch('INSERT INTO sections (\"path\", idx, level, heading, start_line, end_line, tokens) VALUES (?, ?, ?, ?, ?, ?, ?)', rows);\n },\n};\n"],"names":["estimateTokens","fenceTracker","extract","raw","lines","split","found","fence","i","length","feed","inFence","m","match","push","level","heading","trim","startLine","endLine","tokens","s","text","slice","join","Math","ceil","sections","name","schema","db","exec","remove","paths","runBatch","map","p","store","docs","rows","path","extracted","forEach","idx"],"mappings":"AAAA,SAASA,cAAc,QAAQ,oBAAoB;AACnD,SAASC,YAAY,QAAQ,cAAc;AAe3C,uCAAuC;AACvC,SAASC,QAAQC,GAAW;IAC1B,MAAMC,QAAQD,IAAIE,KAAK,CAAC;IACxB,MAAMC,QAAmB,EAAE;IAC3B,MAAMC,QAAQN;IACd,IAAK,IAAIO,IAAI,GAAGA,IAAIJ,MAAMK,MAAM,EAAED,IAAK;QACrC,IAAID,MAAMG,IAAI,CAACN,KAAK,CAACI,EAAE,GAAG;QAC1B,IAAID,MAAMI,OAAO,EAAE;QACnB,MAAMC,IAAIR,KAAK,CAACI,EAAE,CAACK,KAAK,CAAC;QACzB,IAAID,GAAGN,MAAMQ,IAAI,CAAC;YAAEC,OAAOH,CAAC,CAAC,EAAE,CAACH,MAAM;YAAEO,SAASJ,CAAC,CAAC,EAAE,CAACK,IAAI;YAAIC,WAAWV,IAAI;YAAGW,SAASf,MAAMK,MAAM;YAAEW,QAAQ;QAAE;IACnH;IACA,IAAK,IAAIC,IAAI,GAAGA,IAAIf,MAAMG,MAAM,EAAEY,IAAK;QACrC,IAAIA,IAAI,IAAIf,MAAMG,MAAM,EAAEH,KAAK,CAACe,EAAE,CAACF,OAAO,GAAGb,KAAK,CAACe,IAAI,EAAE,CAACH,SAAS,GAAG;QACtE,MAAMI,OAAOlB,MAAMmB,KAAK,CAACjB,KAAK,CAACe,EAAE,CAACH,SAAS,GAAG,GAAGZ,KAAK,CAACe,EAAE,CAACF,OAAO,EAAEK,IAAI,CAAC;QACxElB,KAAK,CAACe,EAAE,CAACD,MAAM,GAAGK,KAAKC,IAAI,CAAC1B,eAAesB;IAC7C;IACA,OAAOhB;AACT;AAEA,OAAO,MAAMqB,WAAoB;IAC/BC,MAAM;IACN,MAAMC,QAAOC,EAAE;QACb,MAAMA,GAAGC,IAAI,CAAC,CAAC,4KAA4K,CAAC;IAC9L;IACA7B;IACA,MAAM8B,QAAOF,EAAE,EAAEG,KAAK;QACpB,IAAIA,MAAMxB,MAAM,KAAK,GAAG;QACxB,MAAMqB,GAAGI,QAAQ,CACf,yCACAD,MAAME,GAAG,CAAC,CAACC,IAAM;gBAACA;aAAE;IAExB;IACA,MAAMC,OAAMP,EAAE,EAAEQ,IAAI;QAClB,MAAMC,OAAoB,EAAE;QAC5B,KAAK,MAAM,EAAEC,IAAI,EAAEC,SAAS,EAAE,IAAIH,KAAM,AAACG,UAAwBC,OAAO,CAAC,CAACrB,GAAGsB,MAAQJ,KAAKzB,IAAI,CAAC;gBAAC0B;gBAAMG;gBAAKtB,EAAEN,KAAK;gBAAEM,EAAEL,OAAO;gBAAEK,EAAEH,SAAS;gBAAEG,EAAEF,OAAO;gBAAEE,EAAED,MAAM;aAAC;QAChK,IAAImB,KAAK9B,MAAM,KAAK,GAAG;QACvB,MAAMqB,GAAGI,QAAQ,CAAC,iHAAiHK;IACrI;AACF,EAAE"}