sensemaking 0.18.3 → 0.19.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 (368) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/chunk/group.js +10 -23
  3. package/dist/cjs/chunk/group.js.map +1 -1
  4. package/dist/cjs/chunk/parse.js +2 -5
  5. package/dist/cjs/chunk/parse.js.map +1 -1
  6. package/dist/cjs/cli/index.js.map +1 -1
  7. package/dist/cjs/cli/shared.js +6 -10
  8. package/dist/cjs/cli/shared.js.map +1 -1
  9. package/dist/cjs/cli/status.js +44 -10
  10. package/dist/cjs/cli/status.js.map +1 -1
  11. package/dist/cjs/cli.js +2 -8
  12. package/dist/cjs/cli.js.map +1 -1
  13. package/dist/cjs/commands/map.js +1 -3
  14. package/dist/cjs/commands/map.js.map +1 -1
  15. package/dist/cjs/commands/peek.js.map +1 -1
  16. package/dist/cjs/commands/related.js +3 -5
  17. package/dist/cjs/commands/related.js.map +1 -1
  18. package/dist/cjs/commands/scope.js.map +1 -1
  19. package/dist/cjs/commands/search.js +6 -8
  20. package/dist/cjs/commands/search.js.map +1 -1
  21. package/dist/cjs/commands/signals.js.map +1 -1
  22. package/dist/cjs/config/access.js.map +1 -1
  23. package/dist/cjs/config/index.d.cts +1 -1
  24. package/dist/cjs/config/index.d.ts +1 -1
  25. package/dist/cjs/config/index.js +3 -0
  26. package/dist/cjs/config/index.js.map +1 -1
  27. package/dist/cjs/config/load.js +10 -24
  28. package/dist/cjs/config/load.js.map +1 -1
  29. package/dist/cjs/config/resolve.js.map +1 -1
  30. package/dist/cjs/config/signals.js +2 -3
  31. package/dist/cjs/config/signals.js.map +1 -1
  32. package/dist/cjs/config/types.d.cts +2 -1
  33. package/dist/cjs/config/types.d.ts +2 -1
  34. package/dist/cjs/config/types.js +8 -0
  35. package/dist/cjs/config/types.js.map +1 -1
  36. package/dist/cjs/config/validate.d.cts +1 -1
  37. package/dist/cjs/config/validate.d.ts +1 -1
  38. package/dist/cjs/config/validate.js +9 -10
  39. package/dist/cjs/config/validate.js.map +1 -1
  40. package/dist/cjs/embed/cohere.js +2 -2
  41. package/dist/cjs/embed/cohere.js.map +1 -1
  42. package/dist/cjs/embed/distribution.js +3 -3
  43. package/dist/cjs/embed/distribution.js.map +1 -1
  44. package/dist/cjs/embed/handoff.d.cts +3 -0
  45. package/dist/cjs/embed/handoff.d.ts +3 -0
  46. package/dist/cjs/embed/handoff.js +39 -0
  47. package/dist/cjs/embed/handoff.js.map +1 -0
  48. package/dist/cjs/embed/identity.d.cts +11 -10
  49. package/dist/cjs/embed/identity.d.ts +11 -10
  50. package/dist/cjs/embed/identity.js +30 -15
  51. package/dist/cjs/embed/identity.js.map +1 -1
  52. package/dist/cjs/embed/langfit.js +2 -3
  53. package/dist/cjs/embed/langfit.js.map +1 -1
  54. package/dist/cjs/embed/languages.js +2 -4
  55. package/dist/cjs/embed/languages.js.map +1 -1
  56. package/dist/cjs/embed/openai.js +2 -2
  57. package/dist/cjs/embed/openai.js.map +1 -1
  58. package/dist/cjs/embed/query.js +58 -20
  59. package/dist/cjs/embed/query.js.map +1 -1
  60. package/dist/cjs/embed/registry.js +1 -2
  61. package/dist/cjs/embed/registry.js.map +1 -1
  62. package/dist/cjs/embed/static.d.cts +1 -1
  63. package/dist/cjs/embed/static.d.ts +1 -1
  64. package/dist/cjs/embed/static.js +8 -10
  65. package/dist/cjs/embed/static.js.map +1 -1
  66. package/dist/cjs/embed/store.d.cts +2 -2
  67. package/dist/cjs/embed/store.d.ts +2 -2
  68. package/dist/cjs/embed/store.js +48 -34
  69. package/dist/cjs/embed/store.js.map +1 -1
  70. package/dist/cjs/embed/types.js +1 -3
  71. package/dist/cjs/embed/types.js.map +1 -1
  72. package/dist/cjs/errors.d.cts +1 -1
  73. package/dist/cjs/errors.d.ts +1 -1
  74. package/dist/cjs/errors.js.map +1 -1
  75. package/dist/cjs/features/embed.js +19 -17
  76. package/dist/cjs/features/embed.js.map +1 -1
  77. package/dist/cjs/features/fences.js +5 -10
  78. package/dist/cjs/features/fences.js.map +1 -1
  79. package/dist/cjs/features/links.js +20 -37
  80. package/dist/cjs/features/links.js.map +1 -1
  81. package/dist/cjs/features/sections.js +21 -19
  82. package/dist/cjs/features/sections.js.map +1 -1
  83. package/dist/cjs/features/tags.js +5 -8
  84. package/dist/cjs/features/tags.js.map +1 -1
  85. package/dist/cjs/graph/traverse.js +1 -2
  86. package/dist/cjs/graph/traverse.js.map +1 -1
  87. package/dist/cjs/index.d.cts +1 -1
  88. package/dist/cjs/index.d.ts +1 -1
  89. package/dist/cjs/index.js +3 -0
  90. package/dist/cjs/index.js.map +1 -1
  91. package/dist/cjs/output/column-hint.js +3 -6
  92. package/dist/cjs/output/column-hint.js.map +1 -1
  93. package/dist/cjs/output/output.js +7 -14
  94. package/dist/cjs/output/output.js.map +1 -1
  95. package/dist/cjs/output/search-error.js +2 -5
  96. package/dist/cjs/output/search-error.js.map +1 -1
  97. package/dist/cjs/scan/frontmatter.d.cts +1 -0
  98. package/dist/cjs/scan/frontmatter.d.ts +1 -0
  99. package/dist/cjs/scan/frontmatter.js +14 -18
  100. package/dist/cjs/scan/frontmatter.js.map +1 -1
  101. package/dist/cjs/scan/list.js +2 -3
  102. package/dist/cjs/scan/list.js.map +1 -1
  103. package/dist/cjs/scan/reparse.js +12 -26
  104. package/dist/cjs/scan/reparse.js.map +1 -1
  105. package/dist/cjs/scan/worker-error.js.map +1 -1
  106. package/dist/cjs/store/cache.d.cts +2 -0
  107. package/dist/cjs/store/cache.d.ts +2 -0
  108. package/dist/cjs/store/cache.js +20 -0
  109. package/dist/cjs/store/cache.js.map +1 -0
  110. package/dist/cjs/store/duckdb/batch.js +4 -16
  111. package/dist/cjs/store/duckdb/batch.js.map +1 -1
  112. package/dist/cjs/store/duckdb/connection.js +8 -20
  113. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  114. package/dist/cjs/store/duckdb/fieldStats.js +2 -8
  115. package/dist/cjs/store/duckdb/fieldStats.js.map +1 -1
  116. package/dist/cjs/store/duckdb/lexical.js +21 -29
  117. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  118. package/dist/cjs/store/duckdb/native.js.map +1 -1
  119. package/dist/cjs/store/duckdb/open.js +12 -13
  120. package/dist/cjs/store/duckdb/open.js.map +1 -1
  121. package/dist/cjs/store/duckdb/reconcile.js +4 -11
  122. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  123. package/dist/cjs/store/duckdb/sql-functions.js +2 -4
  124. package/dist/cjs/store/duckdb/sql-functions.js.map +1 -1
  125. package/dist/cjs/store/duckdb/store.js +2 -3
  126. package/dist/cjs/store/duckdb/store.js.map +1 -1
  127. package/dist/cjs/store/duckdb/vectors.js +8 -18
  128. package/dist/cjs/store/duckdb/vectors.js.map +1 -1
  129. package/dist/cjs/store/index.d.cts +3 -2
  130. package/dist/cjs/store/index.d.ts +3 -2
  131. package/dist/cjs/store/index.js +7 -8
  132. package/dist/cjs/store/index.js.map +1 -1
  133. package/dist/cjs/store/native.js +6 -16
  134. package/dist/cjs/store/native.js.map +1 -1
  135. package/dist/cjs/store/sql-functions.js.map +1 -1
  136. package/dist/cjs/store/sqlite/connection.js +4 -8
  137. package/dist/cjs/store/sqlite/connection.js.map +1 -1
  138. package/dist/cjs/store/sqlite/fieldStats.js.map +1 -1
  139. package/dist/cjs/store/sqlite/lexical.js +3 -5
  140. package/dist/cjs/store/sqlite/lexical.js.map +1 -1
  141. package/dist/cjs/store/sqlite/open.d.cts +0 -1
  142. package/dist/cjs/store/sqlite/open.d.ts +0 -1
  143. package/dist/cjs/store/sqlite/open.js +22 -40
  144. package/dist/cjs/store/sqlite/open.js.map +1 -1
  145. package/dist/cjs/store/sqlite/reconcile.js +2 -3
  146. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  147. package/dist/cjs/store/sqlite/sql-functions.js +2 -4
  148. package/dist/cjs/store/sqlite/sql-functions.js.map +1 -1
  149. package/dist/cjs/store/sqlite/store.js.map +1 -1
  150. package/dist/cjs/store/transaction.js +1 -3
  151. package/dist/cjs/store/transaction.js.map +1 -1
  152. package/dist/cjs/store/turso/connection.js +2 -6
  153. package/dist/cjs/store/turso/connection.js.map +1 -1
  154. package/dist/cjs/store/turso/fieldStats.js.map +1 -1
  155. package/dist/cjs/store/turso/lexical.d.cts +2 -0
  156. package/dist/cjs/store/turso/lexical.d.ts +2 -0
  157. package/dist/cjs/store/turso/lexical.js +347 -0
  158. package/dist/cjs/store/turso/lexical.js.map +1 -0
  159. package/dist/cjs/store/turso/native.js.map +1 -1
  160. package/dist/cjs/store/turso/open.d.cts +1 -1
  161. package/dist/cjs/store/turso/open.d.ts +1 -1
  162. package/dist/cjs/store/turso/open.js +81 -54
  163. package/dist/cjs/store/turso/open.js.map +1 -1
  164. package/dist/cjs/store/turso/reconcile.js +22 -22
  165. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  166. package/dist/cjs/store/turso/store.js +18 -41
  167. package/dist/cjs/store/turso/store.js.map +1 -1
  168. package/dist/cjs/store/turso/vectors.d.cts +8 -0
  169. package/dist/cjs/store/turso/vectors.d.ts +8 -0
  170. package/dist/cjs/store/turso/vectors.js +435 -0
  171. package/dist/cjs/store/turso/vectors.js.map +1 -0
  172. package/dist/cjs/store/types.js +2 -3
  173. package/dist/cjs/store/types.js.map +1 -1
  174. package/dist/cjs/store/vectors.js.map +1 -1
  175. package/dist/cjs/text/segment.d.cts +1 -0
  176. package/dist/cjs/text/segment.d.ts +1 -0
  177. package/dist/cjs/text/segment.js +10 -14
  178. package/dist/cjs/text/segment.js.map +1 -1
  179. package/dist/cjs/watch.js +2 -5
  180. package/dist/cjs/watch.js.map +1 -1
  181. package/dist/cjs/workers/parse.js.map +1 -1
  182. package/dist/esm/chunk/group.js +10 -23
  183. package/dist/esm/chunk/group.js.map +1 -1
  184. package/dist/esm/chunk/parse.js +4 -8
  185. package/dist/esm/chunk/parse.js.map +1 -1
  186. package/dist/esm/cli/index.js +3 -5
  187. package/dist/esm/cli/index.js.map +1 -1
  188. package/dist/esm/cli/shared.js +12 -22
  189. package/dist/esm/cli/shared.js.map +1 -1
  190. package/dist/esm/cli/status.js +43 -9
  191. package/dist/esm/cli/status.js.map +1 -1
  192. package/dist/esm/cli.js +2 -8
  193. package/dist/esm/cli.js.map +1 -1
  194. package/dist/esm/commands/map.js +1 -3
  195. package/dist/esm/commands/map.js.map +1 -1
  196. package/dist/esm/commands/peek.js.map +1 -1
  197. package/dist/esm/commands/related.js +3 -5
  198. package/dist/esm/commands/related.js.map +1 -1
  199. package/dist/esm/commands/scope.js +3 -5
  200. package/dist/esm/commands/scope.js.map +1 -1
  201. package/dist/esm/commands/search.js +10 -16
  202. package/dist/esm/commands/search.js.map +1 -1
  203. package/dist/esm/commands/signals.js +3 -7
  204. package/dist/esm/commands/signals.js.map +1 -1
  205. package/dist/esm/config/access.js +8 -13
  206. package/dist/esm/config/access.js.map +1 -1
  207. package/dist/esm/config/index.d.ts +1 -1
  208. package/dist/esm/config/index.js +1 -1
  209. package/dist/esm/config/index.js.map +1 -1
  210. package/dist/esm/config/load.js +10 -24
  211. package/dist/esm/config/load.js.map +1 -1
  212. package/dist/esm/config/resolve.js +2 -3
  213. package/dist/esm/config/resolve.js.map +1 -1
  214. package/dist/esm/config/signals.js +3 -5
  215. package/dist/esm/config/signals.js.map +1 -1
  216. package/dist/esm/config/types.d.ts +2 -1
  217. package/dist/esm/config/types.js +8 -2
  218. package/dist/esm/config/types.js.map +1 -1
  219. package/dist/esm/config/validate.d.ts +1 -1
  220. package/dist/esm/config/validate.js +7 -10
  221. package/dist/esm/config/validate.js.map +1 -1
  222. package/dist/esm/embed/cohere.js +2 -2
  223. package/dist/esm/embed/cohere.js.map +1 -1
  224. package/dist/esm/embed/distribution.js +1 -1
  225. package/dist/esm/embed/distribution.js.map +1 -1
  226. package/dist/esm/embed/handoff.d.ts +3 -0
  227. package/dist/esm/embed/handoff.js +21 -0
  228. package/dist/esm/embed/handoff.js.map +1 -0
  229. package/dist/esm/embed/identity.d.ts +11 -10
  230. package/dist/esm/embed/identity.js +29 -26
  231. package/dist/esm/embed/identity.js.map +1 -1
  232. package/dist/esm/embed/langfit.js +2 -3
  233. package/dist/esm/embed/langfit.js.map +1 -1
  234. package/dist/esm/embed/languages.js +2 -4
  235. package/dist/esm/embed/languages.js.map +1 -1
  236. package/dist/esm/embed/openai.js +2 -2
  237. package/dist/esm/embed/openai.js.map +1 -1
  238. package/dist/esm/embed/query.js +21 -6
  239. package/dist/esm/embed/query.js.map +1 -1
  240. package/dist/esm/embed/registry.js +1 -2
  241. package/dist/esm/embed/registry.js.map +1 -1
  242. package/dist/esm/embed/static.d.ts +1 -1
  243. package/dist/esm/embed/static.js +8 -10
  244. package/dist/esm/embed/static.js.map +1 -1
  245. package/dist/esm/embed/store.d.ts +2 -2
  246. package/dist/esm/embed/store.js +28 -23
  247. package/dist/esm/embed/store.js.map +1 -1
  248. package/dist/esm/embed/types.js +1 -3
  249. package/dist/esm/embed/types.js.map +1 -1
  250. package/dist/esm/errors.d.ts +1 -1
  251. package/dist/esm/errors.js.map +1 -1
  252. package/dist/esm/features/embed.js +16 -16
  253. package/dist/esm/features/embed.js.map +1 -1
  254. package/dist/esm/features/fences.js +9 -19
  255. package/dist/esm/features/fences.js.map +1 -1
  256. package/dist/esm/features/links.js +24 -49
  257. package/dist/esm/features/links.js.map +1 -1
  258. package/dist/esm/features/sections.js +20 -20
  259. package/dist/esm/features/sections.js.map +1 -1
  260. package/dist/esm/features/tags.js +5 -8
  261. package/dist/esm/features/tags.js.map +1 -1
  262. package/dist/esm/features/types.js.map +1 -1
  263. package/dist/esm/graph/traverse.js +1 -2
  264. package/dist/esm/graph/traverse.js.map +1 -1
  265. package/dist/esm/index.d.ts +1 -1
  266. package/dist/esm/index.js +1 -1
  267. package/dist/esm/index.js.map +1 -1
  268. package/dist/esm/output/column-hint.js +3 -6
  269. package/dist/esm/output/column-hint.js.map +1 -1
  270. package/dist/esm/output/output.js +9 -20
  271. package/dist/esm/output/output.js.map +1 -1
  272. package/dist/esm/output/search-error.js +2 -5
  273. package/dist/esm/output/search-error.js.map +1 -1
  274. package/dist/esm/scan/frontmatter.d.ts +1 -0
  275. package/dist/esm/scan/frontmatter.js +13 -18
  276. package/dist/esm/scan/frontmatter.js.map +1 -1
  277. package/dist/esm/scan/list.js +2 -3
  278. package/dist/esm/scan/list.js.map +1 -1
  279. package/dist/esm/scan/reparse.js +16 -34
  280. package/dist/esm/scan/reparse.js.map +1 -1
  281. package/dist/esm/scan/worker-error.js.map +1 -1
  282. package/dist/esm/store/cache.d.ts +2 -0
  283. package/dist/esm/store/cache.js +11 -0
  284. package/dist/esm/store/cache.js.map +1 -0
  285. package/dist/esm/store/duckdb/batch.js +4 -16
  286. package/dist/esm/store/duckdb/batch.js.map +1 -1
  287. package/dist/esm/store/duckdb/connection.js +8 -20
  288. package/dist/esm/store/duckdb/connection.js.map +1 -1
  289. package/dist/esm/store/duckdb/fieldStats.js +4 -12
  290. package/dist/esm/store/duckdb/fieldStats.js.map +1 -1
  291. package/dist/esm/store/duckdb/lexical.js +29 -36
  292. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  293. package/dist/esm/store/duckdb/native.js +2 -7
  294. package/dist/esm/store/duckdb/native.js.map +1 -1
  295. package/dist/esm/store/duckdb/open.js +16 -23
  296. package/dist/esm/store/duckdb/open.js.map +1 -1
  297. package/dist/esm/store/duckdb/reconcile.js +4 -11
  298. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  299. package/dist/esm/store/duckdb/sql-functions.js +4 -9
  300. package/dist/esm/store/duckdb/sql-functions.js.map +1 -1
  301. package/dist/esm/store/duckdb/store.js +6 -14
  302. package/dist/esm/store/duckdb/store.js.map +1 -1
  303. package/dist/esm/store/duckdb/vectors.js +14 -31
  304. package/dist/esm/store/duckdb/vectors.js.map +1 -1
  305. package/dist/esm/store/index.d.ts +3 -2
  306. package/dist/esm/store/index.js +5 -6
  307. package/dist/esm/store/index.js.map +1 -1
  308. package/dist/esm/store/native.js +10 -25
  309. package/dist/esm/store/native.js.map +1 -1
  310. package/dist/esm/store/sql-functions.js +3 -5
  311. package/dist/esm/store/sql-functions.js.map +1 -1
  312. package/dist/esm/store/sqlite/connection.js +4 -8
  313. package/dist/esm/store/sqlite/connection.js.map +1 -1
  314. package/dist/esm/store/sqlite/fieldStats.js +2 -6
  315. package/dist/esm/store/sqlite/fieldStats.js.map +1 -1
  316. package/dist/esm/store/sqlite/lexical.js +5 -9
  317. package/dist/esm/store/sqlite/lexical.js.map +1 -1
  318. package/dist/esm/store/sqlite/open.d.ts +0 -1
  319. package/dist/esm/store/sqlite/open.js +21 -41
  320. package/dist/esm/store/sqlite/open.js.map +1 -1
  321. package/dist/esm/store/sqlite/reconcile.js +6 -13
  322. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  323. package/dist/esm/store/sqlite/sql-functions.js +2 -4
  324. package/dist/esm/store/sqlite/sql-functions.js.map +1 -1
  325. package/dist/esm/store/sqlite/store.js +1 -2
  326. package/dist/esm/store/sqlite/store.js.map +1 -1
  327. package/dist/esm/store/transaction.js +2 -5
  328. package/dist/esm/store/transaction.js.map +1 -1
  329. package/dist/esm/store/turso/connection.js +2 -6
  330. package/dist/esm/store/turso/connection.js.map +1 -1
  331. package/dist/esm/store/turso/fieldStats.js +2 -5
  332. package/dist/esm/store/turso/fieldStats.js.map +1 -1
  333. package/dist/esm/store/turso/lexical.d.ts +2 -0
  334. package/dist/esm/store/turso/lexical.js +128 -0
  335. package/dist/esm/store/turso/lexical.js.map +1 -0
  336. package/dist/esm/store/turso/native.js +2 -7
  337. package/dist/esm/store/turso/native.js.map +1 -1
  338. package/dist/esm/store/turso/open.d.ts +1 -1
  339. package/dist/esm/store/turso/open.js +36 -29
  340. package/dist/esm/store/turso/open.js.map +1 -1
  341. package/dist/esm/store/turso/reconcile.js +22 -22
  342. package/dist/esm/store/turso/reconcile.js.map +1 -1
  343. package/dist/esm/store/turso/store.js +17 -28
  344. package/dist/esm/store/turso/store.js.map +1 -1
  345. package/dist/esm/store/turso/vectors.d.ts +8 -0
  346. package/dist/esm/store/turso/vectors.js +98 -0
  347. package/dist/esm/store/turso/vectors.js.map +1 -0
  348. package/dist/esm/store/types.js +2 -3
  349. package/dist/esm/store/types.js.map +1 -1
  350. package/dist/esm/store/vectors.js +2 -3
  351. package/dist/esm/store/vectors.js.map +1 -1
  352. package/dist/esm/text/segment.d.ts +1 -0
  353. package/dist/esm/text/segment.js +13 -21
  354. package/dist/esm/text/segment.js.map +1 -1
  355. package/dist/esm/watch.js +2 -5
  356. package/dist/esm/watch.js.map +1 -1
  357. package/dist/esm/workers/parse.js.map +1 -1
  358. package/package.json +3 -1
  359. package/schema.json +2 -2
  360. package/skills/sense/SKILL.md +2 -2
  361. package/skills/sense-setup/SKILL.md +3 -3
  362. package/dist/cjs/store/meta.d.cts +0 -3
  363. package/dist/cjs/store/meta.d.ts +0 -3
  364. package/dist/cjs/store/meta.js +0 -215
  365. package/dist/cjs/store/meta.js.map +0 -1
  366. package/dist/esm/store/meta.d.ts +0 -3
  367. package/dist/esm/store/meta.js +0 -17
  368. package/dist/esm/store/meta.js.map +0 -1
@@ -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 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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"}
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 { stashChunkText } from '../embed/handoff.ts';\nimport { modelIdentity } from '../embed/identity.ts';\nimport { countLines } from '../scan/frontmatter.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 and embedding tops up on the next search, so staleness costs recall.\n\nexport type { Chunk };\n\n// The title/summary prefix mirrors bm25 field weighting. Chunks the body, not the raw file, so\n// `offset` shifts line numbers back onto raw and a range stays a direct Read range.\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 // A hardcoded option change here must bump src/chunk/version.ts's CHUNK_VERSION, the signature\n // for every chunker 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, countLines(raw) - countLines(body), 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 const texts = new Map<string, string[]>();\n for (const { path, extracted } of docs) {\n if (!extracted) continue;\n const chunks = extracted as Chunk[];\n chunks.forEach((c, idx) => rows.push([path, idx, c.startLine, c.endLine]));\n texts.set(\n path,\n chunks.map((c) => c.text)\n );\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 // The text extract() just computed, handed to the embed pass that follows in this same\n // process rather than re-derived from the file there (src/embed/handoff.ts).\n stashChunkText(db, texts);\n },\n enabledForFile(_cfg, file) {\n return file.embed;\n },\n // Keyed so a rebuild notice can name it \"embed settings\". Driven by embedConfig(cfg) directly:\n // a model can be configured with no preset yet using vectors.\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","stashChunkText","modelIdentity","countLines","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","remove","paths","length","runBatch","p","store","docs","rows","texts","Map","path","extracted","chunks","forEach","idx","push","set","enabledForFile","_cfg","file","signature","e","identity","provider","model","chunkVersion"],"mappings":"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"}
@@ -1,8 +1,5 @@
1
- // Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column
2
- // 0 -- no indent allowance, a deliberate divergence from CommonMark's up-to-3-space rule (see
3
- // test/unit/features/fences.test.ts DIVERGENCES table). A backtick opener's info string must not itself
4
- // contain a backtick (spec rule). A closer is a run of the SAME character, length >= the
5
- // opener's, holding nothing but trailing spaces after the run.
1
+ // Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column 0, no indent
2
+ // allowance (diverges from CommonMark's up-to-3-space rule). Closer: same char, length >= opener's, trailing spaces only.
6
3
  const BACKTICK_OPEN = /^(`{3,})(.*)$/;
7
4
  const TILDE_OPEN = /^(~{3,})(.*)$/;
8
5
  export function fenceTracker() {
@@ -80,10 +77,8 @@ export function commentTracker() {
80
77
  }
81
78
  };
82
79
  }
83
- // CommonMark's HTML-block type-6 list (fixed by the spec, not a drifting enumeration): a line
84
- // starting with an open or close tag of one of these, at column 0 (leading whitespace/`>`
85
- // blockquote markers tolerated), opens a block that swallows following lines -- tags, links,
86
- // and comments alike -- until a blank line closes it.
80
+ // CommonMark's HTML-block type-6 tag list (fixed by the spec). A line opening or closing one of these at
81
+ // column 0 (leading whitespace/`>` tolerated) swallows following lines -- tags, links, comments alike -- until a blank line.
87
82
  const HTML_BLOCK_TAGS = new Set([
88
83
  'address',
89
84
  'article',
@@ -160,12 +155,8 @@ const HTML_PRE_CLOSE_RE = /<\/(?:script|pre|style|textarea)>/i; // any of the fo
160
155
  // An opening or closing tag at column 0, tag name captured for the lookups above.
161
156
  const HTML_BLOCK_OPEN_RE = /^<\/?([a-zA-Z][a-zA-Z0-9]*)(?:[ \t]|\/?>|$)/;
162
157
  const BLANK_LINE_RE = /^[ \t>]*$/; // whitespace/blockquote-markers only -- a paragraph break, and what ends a type-6 block
163
- // Body with fenced-code, HTML-block, and <!-- --> comment regions replaced by spaces (newlines
164
- // preserved, so line numbers and offsets survive) plus inline code spans masked. The single
165
- // region masker for tags/links: fence and HTML-block delimiter+content lines are opaque, an
166
- // unclosed comment dies at its containing block's end (or the next blank line at paragraph
167
- // level -- never silently to end of file), and a closed comment masks exactly its span.
168
- // Bodies without a backtick/tilde/`<` fast-path out untouched.
158
+ // Body with fenced-code, HTML-block, and <!-- --> comment regions replaced by spaces (newlines preserved, so line numbers and offsets survive), inline code spans masked too.
159
+ // An unclosed comment dies at its block's end or the next paragraph blank line, never silently to end of file; bodies without a backtick/tilde/`<` fast-path out untouched.
169
160
  export function maskRegions(body) {
170
161
  if (!/[`~<]/.test(body)) return body;
171
162
  const fence = fenceTracker();
@@ -195,7 +186,7 @@ export function maskRegions(body) {
195
186
  }
196
187
  const uncommented = comment.inComment || line.includes('<!--') ? comment.mask(line) : line;
197
188
  if (comment.inComment && BLANK_LINE_RE.test(line)) comment.close(); // paragraph-level: dies at the next blank line
198
- // Indented or blockquoted HTML blocks still swallow their content in Obsidian, so the
189
+ // Indented or blockquoted HTML blocks swallow their content in Obsidian too, so the
199
190
  // opener test runs after stripping leading whitespace and > markers.
200
191
  const stripped = uncommented.replace(/^[ \t>]*/, '');
201
192
  if (stripped[0] === '<') {
@@ -222,9 +213,8 @@ export function maskRegions(body) {
222
213
  return uncommented.includes('`') ? maskCodeSpans(uncommented) : uncommented;
223
214
  }).join('\n');
224
215
  }
225
- // A code span opens on a run of N backticks and closes at the next run of exactly N -- a
226
- // shorter or longer run in between is literal text, not a delimiter (CommonMark code spans).
227
- // Masked with spaces so column positions and tag-boundary whitespace are unaffected.
216
+ // A code span opens on a run of N backticks and closes at the next run of exactly N (CommonMark code spans);
217
+ // a shorter/longer run in between is literal text. Masked with spaces so column positions stay unaffected.
228
218
  export function maskCodeSpans(line) {
229
219
  let out = '';
230
220
  let i = 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/fences.ts"],"sourcesContent":["// Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column\n// 0 -- no indent allowance, a deliberate divergence from CommonMark's up-to-3-space rule (see\n// test/unit/features/fences.test.ts DIVERGENCES table). A backtick opener's info string must not itself\n// contain a backtick (spec rule). A closer is a run of the SAME character, length >= the\n// opener's, holding nothing but trailing spaces after the run.\n\nexport interface FenceTracker {\n feed(line: string): boolean; // true iff this line is a fence delimiter (open or close)\n readonly inFence: boolean;\n}\n\nconst BACKTICK_OPEN = /^(`{3,})(.*)$/;\nconst TILDE_OPEN = /^(~{3,})(.*)$/;\n\nexport function fenceTracker(): FenceTracker {\n let inFence = false;\n let fenceChar = '';\n let fenceLen = 0;\n return {\n feed(line: string): boolean {\n if (!inFence) {\n const bt = BACKTICK_OPEN.exec(line);\n if (bt && !bt[2].includes('`')) {\n inFence = true;\n fenceChar = '`';\n fenceLen = bt[1].length;\n return true;\n }\n const td = TILDE_OPEN.exec(line);\n if (td) {\n inFence = true;\n fenceChar = '~';\n fenceLen = td[1].length;\n return true;\n }\n return false;\n }\n const run = fenceChar === '`' ? /^(`+)(.*)$/ : /^(~+)(.*)$/;\n const m = run.exec(line);\n if (m && m[1].length >= fenceLen && m[2].trim() === '') {\n inFence = false;\n return true;\n }\n return false;\n },\n get inFence() {\n return inFence;\n },\n };\n}\n\n// Masks <!-- ... --> spans that may cross line boundaries, one line at a time so callers can\n// interleave it with their own per-line state. In isolation an unclosed <!-- stays open forever\n// (mask() has no notion of a line boundary that should kill it); maskRegions() below is what\n// calls close() at the point an unclosed comment actually dies -- a blank line at paragraph\n// level, or its enclosing HTML block's end.\nexport interface CommentTracker {\n mask(line: string): string;\n close(): void; // force-ends an unclosed comment (paragraph blank line, or its HTML block ending)\n readonly inComment: boolean;\n}\n\nexport function commentTracker(): CommentTracker {\n let inComment = false;\n return {\n mask(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (inComment) {\n const end = line.indexOf('-->', i);\n if (end === -1) {\n out += ' '.repeat(line.length - i);\n i = line.length;\n } else {\n out += ' '.repeat(end + 3 - i);\n i = end + 3;\n inComment = false;\n }\n continue;\n }\n const start = line.indexOf('<!--', i);\n if (start === -1) {\n out += line.slice(i);\n i = line.length;\n } else {\n out += `${line.slice(i, start)} `;\n i = start + 4;\n inComment = true;\n }\n }\n return out;\n },\n close() {\n inComment = false;\n },\n get inComment() {\n return inComment;\n },\n };\n}\n\n// CommonMark's HTML-block type-6 list (fixed by the spec, not a drifting enumeration): a line\n// starting with an open or close tag of one of these, at column 0 (leading whitespace/`>`\n// blockquote markers tolerated), opens a block that swallows following lines -- tags, links,\n// and comments alike -- until a blank line closes it.\nconst HTML_BLOCK_TAGS = new Set([\n 'address',\n 'article',\n 'aside',\n 'base',\n 'basefont',\n 'blockquote',\n 'body',\n 'caption',\n 'center',\n 'col',\n 'colgroup',\n 'dd',\n 'details',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'footer',\n 'form',\n 'frame',\n 'frameset',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hr',\n 'html',\n 'iframe',\n 'legend',\n 'li',\n 'link',\n 'main',\n 'menu',\n 'menuitem',\n 'nav',\n 'noframes',\n 'ol',\n 'optgroup',\n 'option',\n 'p',\n 'param',\n 'search',\n 'section',\n 'summary',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'title',\n 'tr',\n 'track',\n 'ul',\n]);\n// Type-1 blocks (script/pre/style/textarea): closes on the line holding the matching end tag,\n// not on a blank line, and that line is the last one swallowed.\nconst HTML_PRE_TAGS = new Set(['script', 'pre', 'style', 'textarea']);\nconst HTML_PRE_CLOSE_RE = /<\\/(?:script|pre|style|textarea)>/i; // any of the four closers ends the block, not only the tag that opened it\n// An opening or closing tag at column 0, tag name captured for the lookups above.\nconst HTML_BLOCK_OPEN_RE = /^<\\/?([a-zA-Z][a-zA-Z0-9]*)(?:[ \\t]|\\/?>|$)/;\nconst BLANK_LINE_RE = /^[ \\t>]*$/; // whitespace/blockquote-markers only -- a paragraph break, and what ends a type-6 block\n\n// Body with fenced-code, HTML-block, and <!-- --> comment regions replaced by spaces (newlines\n// preserved, so line numbers and offsets survive) plus inline code spans masked. The single\n// region masker for tags/links: fence and HTML-block delimiter+content lines are opaque, an\n// unclosed comment dies at its containing block's end (or the next blank line at paragraph\n// level -- never silently to end of file), and a closed comment masks exactly its span.\n// Bodies without a backtick/tilde/`<` fast-path out untouched.\nexport function maskRegions(body: string): string {\n if (!/[`~<]/.test(body)) return body;\n const fence = fenceTracker();\n const comment = commentTracker();\n let inHtmlBlock = false;\n let htmlBlockClose: RegExp | null = null; // set while inside a type-1 (script/pre/style/textarea) block\n return body\n .split('\\n')\n .map((line) => {\n if (inHtmlBlock) {\n if (htmlBlockClose) {\n if (htmlBlockClose.test(line)) {\n inHtmlBlock = false;\n htmlBlockClose = null;\n }\n return ' '.repeat(line.length);\n }\n if (BLANK_LINE_RE.test(line)) {\n // Block content never feeds the comment tracker, so an inner <!-- was only ever\n // masked text; nothing to close here.\n inHtmlBlock = false;\n return line;\n }\n return ' '.repeat(line.length);\n }\n if (!comment.inComment) {\n const isDelim = fence.feed(line);\n if (isDelim || fence.inFence) return ' '.repeat(line.length);\n }\n const uncommented = comment.inComment || line.includes('<!--') ? comment.mask(line) : line;\n if (comment.inComment && BLANK_LINE_RE.test(line)) comment.close(); // paragraph-level: dies at the next blank line\n // Indented or blockquoted HTML blocks still swallow their content in Obsidian, so the\n // opener test runs after stripping leading whitespace and > markers.\n const stripped = uncommented.replace(/^[ \\t>]*/, '');\n if (stripped[0] === '<') {\n const openMatch = HTML_BLOCK_OPEN_RE.exec(stripped);\n if (openMatch) {\n const tagName = openMatch[1].toLowerCase();\n const isClosingTag = stripped[1] === '/';\n if (!isClosingTag && HTML_PRE_TAGS.has(tagName)) {\n // The end condition can be met on the opening line itself: <script>x</script>\n // is a one-line block and the next line parses (Obsidian-verified).\n if (!HTML_PRE_CLOSE_RE.test(stripped.slice(openMatch[0].length))) {\n inHtmlBlock = true;\n htmlBlockClose = HTML_PRE_CLOSE_RE;\n }\n return ' '.repeat(line.length);\n }\n if (HTML_BLOCK_TAGS.has(tagName)) {\n inHtmlBlock = true;\n return ' '.repeat(line.length);\n }\n }\n }\n // Inline code spans hide their content too: `[[x]]` in prose is not a link.\n return uncommented.includes('`') ? maskCodeSpans(uncommented) : uncommented;\n })\n .join('\\n');\n}\n\n// A code span opens on a run of N backticks and closes at the next run of exactly N -- a\n// shorter or longer run in between is literal text, not a delimiter (CommonMark code spans).\n// Masked with spaces so column positions and tag-boundary whitespace are unaffected.\nexport function maskCodeSpans(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (line[i] !== '`') {\n out += line[i];\n i++;\n continue;\n }\n let j = i;\n while (line[j] === '`') j++;\n const n = j - i;\n let k = j;\n let closeStart = -1;\n let closeEnd = -1;\n while (k < line.length) {\n if (line[k] !== '`') {\n k++;\n continue;\n }\n let m = k;\n while (line[m] === '`') m++;\n if (m - k === n) {\n closeStart = k;\n closeEnd = m;\n break;\n }\n k = m;\n }\n if (closeStart >= 0) {\n out += ' '.repeat(closeEnd - i);\n i = closeEnd;\n } else {\n out += line.slice(i, j);\n i = j;\n }\n }\n return out;\n}\n"],"names":["BACKTICK_OPEN","TILDE_OPEN","fenceTracker","inFence","fenceChar","fenceLen","feed","line","bt","exec","includes","length","td","run","m","trim","commentTracker","inComment","mask","out","i","end","indexOf","repeat","start","slice","close","HTML_BLOCK_TAGS","Set","HTML_PRE_TAGS","HTML_PRE_CLOSE_RE","HTML_BLOCK_OPEN_RE","BLANK_LINE_RE","maskRegions","body","test","fence","comment","inHtmlBlock","htmlBlockClose","split","map","isDelim","uncommented","stripped","replace","openMatch","tagName","toLowerCase","isClosingTag","has","maskCodeSpans","join","j","n","k","closeStart","closeEnd"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/fences.ts"],"sourcesContent":["// Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column 0, no indent\n// allowance (diverges from CommonMark's up-to-3-space rule). Closer: same char, length >= opener's, trailing spaces only.\n\nexport interface FenceTracker {\n feed(line: string): boolean; // true iff this line is a fence delimiter (open or close)\n readonly inFence: boolean;\n}\n\nconst BACKTICK_OPEN = /^(`{3,})(.*)$/;\nconst TILDE_OPEN = /^(~{3,})(.*)$/;\n\nexport function fenceTracker(): FenceTracker {\n let inFence = false;\n let fenceChar = '';\n let fenceLen = 0;\n return {\n feed(line: string): boolean {\n if (!inFence) {\n const bt = BACKTICK_OPEN.exec(line);\n if (bt && !bt[2].includes('`')) {\n inFence = true;\n fenceChar = '`';\n fenceLen = bt[1].length;\n return true;\n }\n const td = TILDE_OPEN.exec(line);\n if (td) {\n inFence = true;\n fenceChar = '~';\n fenceLen = td[1].length;\n return true;\n }\n return false;\n }\n const run = fenceChar === '`' ? /^(`+)(.*)$/ : /^(~+)(.*)$/;\n const m = run.exec(line);\n if (m && m[1].length >= fenceLen && m[2].trim() === '') {\n inFence = false;\n return true;\n }\n return false;\n },\n get inFence() {\n return inFence;\n },\n };\n}\n\n// Masks <!-- ... --> spans that may cross line boundaries, one line at a time so callers can interleave their own per-line state.\n// An unclosed comment stays open forever unless closed externally; maskRegions() below calls close() at a paragraph blank line or the enclosing HTML block's end.\nexport interface CommentTracker {\n mask(line: string): string;\n close(): void; // force-ends an unclosed comment (paragraph blank line, or its HTML block ending)\n readonly inComment: boolean;\n}\n\nexport function commentTracker(): CommentTracker {\n let inComment = false;\n return {\n mask(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (inComment) {\n const end = line.indexOf('-->', i);\n if (end === -1) {\n out += ' '.repeat(line.length - i);\n i = line.length;\n } else {\n out += ' '.repeat(end + 3 - i);\n i = end + 3;\n inComment = false;\n }\n continue;\n }\n const start = line.indexOf('<!--', i);\n if (start === -1) {\n out += line.slice(i);\n i = line.length;\n } else {\n out += `${line.slice(i, start)} `;\n i = start + 4;\n inComment = true;\n }\n }\n return out;\n },\n close() {\n inComment = false;\n },\n get inComment() {\n return inComment;\n },\n };\n}\n\n// CommonMark's HTML-block type-6 tag list (fixed by the spec). A line opening or closing one of these at\n// column 0 (leading whitespace/`>` tolerated) swallows following lines -- tags, links, comments alike -- until a blank line.\nconst HTML_BLOCK_TAGS = new Set([\n 'address',\n 'article',\n 'aside',\n 'base',\n 'basefont',\n 'blockquote',\n 'body',\n 'caption',\n 'center',\n 'col',\n 'colgroup',\n 'dd',\n 'details',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'footer',\n 'form',\n 'frame',\n 'frameset',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hr',\n 'html',\n 'iframe',\n 'legend',\n 'li',\n 'link',\n 'main',\n 'menu',\n 'menuitem',\n 'nav',\n 'noframes',\n 'ol',\n 'optgroup',\n 'option',\n 'p',\n 'param',\n 'search',\n 'section',\n 'summary',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'title',\n 'tr',\n 'track',\n 'ul',\n]);\n// Type-1 blocks (script/pre/style/textarea): closes on the line holding the matching end tag,\n// not on a blank line, and that line is the last one swallowed.\nconst HTML_PRE_TAGS = new Set(['script', 'pre', 'style', 'textarea']);\nconst HTML_PRE_CLOSE_RE = /<\\/(?:script|pre|style|textarea)>/i; // any of the four closers ends the block, not only the tag that opened it\n// An opening or closing tag at column 0, tag name captured for the lookups above.\nconst HTML_BLOCK_OPEN_RE = /^<\\/?([a-zA-Z][a-zA-Z0-9]*)(?:[ \\t]|\\/?>|$)/;\nconst BLANK_LINE_RE = /^[ \\t>]*$/; // whitespace/blockquote-markers only -- a paragraph break, and what ends a type-6 block\n\n// Body with fenced-code, HTML-block, and <!-- --> comment regions replaced by spaces (newlines preserved, so line numbers and offsets survive), inline code spans masked too.\n// An unclosed comment dies at its block's end or the next paragraph blank line, never silently to end of file; bodies without a backtick/tilde/`<` fast-path out untouched.\nexport function maskRegions(body: string): string {\n if (!/[`~<]/.test(body)) return body;\n const fence = fenceTracker();\n const comment = commentTracker();\n let inHtmlBlock = false;\n let htmlBlockClose: RegExp | null = null; // set while inside a type-1 (script/pre/style/textarea) block\n return body\n .split('\\n')\n .map((line) => {\n if (inHtmlBlock) {\n if (htmlBlockClose) {\n if (htmlBlockClose.test(line)) {\n inHtmlBlock = false;\n htmlBlockClose = null;\n }\n return ' '.repeat(line.length);\n }\n if (BLANK_LINE_RE.test(line)) {\n // Block content never feeds the comment tracker, so an inner <!-- was only ever\n // masked text; nothing to close here.\n inHtmlBlock = false;\n return line;\n }\n return ' '.repeat(line.length);\n }\n if (!comment.inComment) {\n const isDelim = fence.feed(line);\n if (isDelim || fence.inFence) return ' '.repeat(line.length);\n }\n const uncommented = comment.inComment || line.includes('<!--') ? comment.mask(line) : line;\n if (comment.inComment && BLANK_LINE_RE.test(line)) comment.close(); // paragraph-level: dies at the next blank line\n // Indented or blockquoted HTML blocks swallow their content in Obsidian too, so the\n // opener test runs after stripping leading whitespace and > markers.\n const stripped = uncommented.replace(/^[ \\t>]*/, '');\n if (stripped[0] === '<') {\n const openMatch = HTML_BLOCK_OPEN_RE.exec(stripped);\n if (openMatch) {\n const tagName = openMatch[1].toLowerCase();\n const isClosingTag = stripped[1] === '/';\n if (!isClosingTag && HTML_PRE_TAGS.has(tagName)) {\n // The end condition can be met on the opening line itself: <script>x</script>\n // is a one-line block and the next line parses (Obsidian-verified).\n if (!HTML_PRE_CLOSE_RE.test(stripped.slice(openMatch[0].length))) {\n inHtmlBlock = true;\n htmlBlockClose = HTML_PRE_CLOSE_RE;\n }\n return ' '.repeat(line.length);\n }\n if (HTML_BLOCK_TAGS.has(tagName)) {\n inHtmlBlock = true;\n return ' '.repeat(line.length);\n }\n }\n }\n // Inline code spans hide their content too: `[[x]]` in prose is not a link.\n return uncommented.includes('`') ? maskCodeSpans(uncommented) : uncommented;\n })\n .join('\\n');\n}\n\n// A code span opens on a run of N backticks and closes at the next run of exactly N (CommonMark code spans);\n// a shorter/longer run in between is literal text. Masked with spaces so column positions stay unaffected.\nexport function maskCodeSpans(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (line[i] !== '`') {\n out += line[i];\n i++;\n continue;\n }\n let j = i;\n while (line[j] === '`') j++;\n const n = j - i;\n let k = j;\n let closeStart = -1;\n let closeEnd = -1;\n while (k < line.length) {\n if (line[k] !== '`') {\n k++;\n continue;\n }\n let m = k;\n while (line[m] === '`') m++;\n if (m - k === n) {\n closeStart = k;\n closeEnd = m;\n break;\n }\n k = m;\n }\n if (closeStart >= 0) {\n out += ' '.repeat(closeEnd - i);\n i = closeEnd;\n } else {\n out += line.slice(i, j);\n i = j;\n }\n }\n return out;\n}\n"],"names":["BACKTICK_OPEN","TILDE_OPEN","fenceTracker","inFence","fenceChar","fenceLen","feed","line","bt","exec","includes","length","td","run","m","trim","commentTracker","inComment","mask","out","i","end","indexOf","repeat","start","slice","close","HTML_BLOCK_TAGS","Set","HTML_PRE_TAGS","HTML_PRE_CLOSE_RE","HTML_BLOCK_OPEN_RE","BLANK_LINE_RE","maskRegions","body","test","fence","comment","inHtmlBlock","htmlBlockClose","split","map","isDelim","uncommented","stripped","replace","openMatch","tagName","toLowerCase","isClosingTag","has","maskCodeSpans","join","j","n","k","closeStart","closeEnd"],"mappings":"AAAA,4GAA4G;AAC5G,0HAA0H;AAO1H,MAAMA,gBAAgB;AACtB,MAAMC,aAAa;AAEnB,OAAO,SAASC;IACd,IAAIC,UAAU;IACd,IAAIC,YAAY;IAChB,IAAIC,WAAW;IACf,OAAO;QACLC,MAAKC,IAAY;YACf,IAAI,CAACJ,SAAS;gBACZ,MAAMK,KAAKR,cAAcS,IAAI,CAACF;gBAC9B,IAAIC,MAAM,CAACA,EAAE,CAAC,EAAE,CAACE,QAAQ,CAAC,MAAM;oBAC9BP,UAAU;oBACVC,YAAY;oBACZC,WAAWG,EAAE,CAAC,EAAE,CAACG,MAAM;oBACvB,OAAO;gBACT;gBACA,MAAMC,KAAKX,WAAWQ,IAAI,CAACF;gBAC3B,IAAIK,IAAI;oBACNT,UAAU;oBACVC,YAAY;oBACZC,WAAWO,EAAE,CAAC,EAAE,CAACD,MAAM;oBACvB,OAAO;gBACT;gBACA,OAAO;YACT;YACA,MAAME,MAAMT,cAAc,MAAM,eAAe;YAC/C,MAAMU,IAAID,IAAIJ,IAAI,CAACF;YACnB,IAAIO,KAAKA,CAAC,CAAC,EAAE,CAACH,MAAM,IAAIN,YAAYS,CAAC,CAAC,EAAE,CAACC,IAAI,OAAO,IAAI;gBACtDZ,UAAU;gBACV,OAAO;YACT;YACA,OAAO;QACT;QACA,IAAIA;YACF,OAAOA;QACT;IACF;AACF;AAUA,OAAO,SAASa;IACd,IAAIC,YAAY;IAChB,OAAO;QACLC,MAAKX,IAAY;YACf,IAAIY,MAAM;YACV,IAAIC,IAAI;YACR,MAAOA,IAAIb,KAAKI,MAAM,CAAE;gBACtB,IAAIM,WAAW;oBACb,MAAMI,MAAMd,KAAKe,OAAO,CAAC,OAAOF;oBAChC,IAAIC,QAAQ,CAAC,GAAG;wBACdF,OAAO,IAAII,MAAM,CAAChB,KAAKI,MAAM,GAAGS;wBAChCA,IAAIb,KAAKI,MAAM;oBACjB,OAAO;wBACLQ,OAAO,IAAII,MAAM,CAACF,MAAM,IAAID;wBAC5BA,IAAIC,MAAM;wBACVJ,YAAY;oBACd;oBACA;gBACF;gBACA,MAAMO,QAAQjB,KAAKe,OAAO,CAAC,QAAQF;gBACnC,IAAII,UAAU,CAAC,GAAG;oBAChBL,OAAOZ,KAAKkB,KAAK,CAACL;oBAClBA,IAAIb,KAAKI,MAAM;gBACjB,OAAO;oBACLQ,OAAO,GAAGZ,KAAKkB,KAAK,CAACL,GAAGI,OAAO,IAAI,CAAC;oBACpCJ,IAAII,QAAQ;oBACZP,YAAY;gBACd;YACF;YACA,OAAOE;QACT;QACAO;YACET,YAAY;QACd;QACA,IAAIA;YACF,OAAOA;QACT;IACF;AACF;AAEA,yGAAyG;AACzG,6HAA6H;AAC7H,MAAMU,kBAAkB,IAAIC,IAAI;IAC9B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AACD,8FAA8F;AAC9F,gEAAgE;AAChE,MAAMC,gBAAgB,IAAID,IAAI;IAAC;IAAU;IAAO;IAAS;CAAW;AACpE,MAAME,oBAAoB,sCAAsC,0EAA0E;AAC1I,kFAAkF;AAClF,MAAMC,qBAAqB;AAC3B,MAAMC,gBAAgB,aAAa,wFAAwF;AAE3H,8KAA8K;AAC9K,4KAA4K;AAC5K,OAAO,SAASC,YAAYC,IAAY;IACtC,IAAI,CAAC,QAAQC,IAAI,CAACD,OAAO,OAAOA;IAChC,MAAME,QAAQlC;IACd,MAAMmC,UAAUrB;IAChB,IAAIsB,cAAc;IAClB,IAAIC,iBAAgC,MAAM,8DAA8D;IACxG,OAAOL,KACJM,KAAK,CAAC,MACNC,GAAG,CAAC,CAAClC;QACJ,IAAI+B,aAAa;YACf,IAAIC,gBAAgB;gBAClB,IAAIA,eAAeJ,IAAI,CAAC5B,OAAO;oBAC7B+B,cAAc;oBACdC,iBAAiB;gBACnB;gBACA,OAAO,IAAIhB,MAAM,CAAChB,KAAKI,MAAM;YAC/B;YACA,IAAIqB,cAAcG,IAAI,CAAC5B,OAAO;gBAC5B,gFAAgF;gBAChF,sCAAsC;gBACtC+B,cAAc;gBACd,OAAO/B;YACT;YACA,OAAO,IAAIgB,MAAM,CAAChB,KAAKI,MAAM;QAC/B;QACA,IAAI,CAAC0B,QAAQpB,SAAS,EAAE;YACtB,MAAMyB,UAAUN,MAAM9B,IAAI,CAACC;YAC3B,IAAImC,WAAWN,MAAMjC,OAAO,EAAE,OAAO,IAAIoB,MAAM,CAAChB,KAAKI,MAAM;QAC7D;QACA,MAAMgC,cAAcN,QAAQpB,SAAS,IAAIV,KAAKG,QAAQ,CAAC,UAAU2B,QAAQnB,IAAI,CAACX,QAAQA;QACtF,IAAI8B,QAAQpB,SAAS,IAAIe,cAAcG,IAAI,CAAC5B,OAAO8B,QAAQX,KAAK,IAAI,+CAA+C;QACnH,oFAAoF;QACpF,qEAAqE;QACrE,MAAMkB,WAAWD,YAAYE,OAAO,CAAC,YAAY;QACjD,IAAID,QAAQ,CAAC,EAAE,KAAK,KAAK;YACvB,MAAME,YAAYf,mBAAmBtB,IAAI,CAACmC;YAC1C,IAAIE,WAAW;gBACb,MAAMC,UAAUD,SAAS,CAAC,EAAE,CAACE,WAAW;gBACxC,MAAMC,eAAeL,QAAQ,CAAC,EAAE,KAAK;gBACrC,IAAI,CAACK,gBAAgBpB,cAAcqB,GAAG,CAACH,UAAU;oBAC/C,8EAA8E;oBAC9E,oEAAoE;oBACpE,IAAI,CAACjB,kBAAkBK,IAAI,CAACS,SAASnB,KAAK,CAACqB,SAAS,CAAC,EAAE,CAACnC,MAAM,IAAI;wBAChE2B,cAAc;wBACdC,iBAAiBT;oBACnB;oBACA,OAAO,IAAIP,MAAM,CAAChB,KAAKI,MAAM;gBAC/B;gBACA,IAAIgB,gBAAgBuB,GAAG,CAACH,UAAU;oBAChCT,cAAc;oBACd,OAAO,IAAIf,MAAM,CAAChB,KAAKI,MAAM;gBAC/B;YACF;QACF;QACA,4EAA4E;QAC5E,OAAOgC,YAAYjC,QAAQ,CAAC,OAAOyC,cAAcR,eAAeA;IAClE,GACCS,IAAI,CAAC;AACV;AAEA,6GAA6G;AAC7G,2GAA2G;AAC3G,OAAO,SAASD,cAAc5C,IAAY;IACxC,IAAIY,MAAM;IACV,IAAIC,IAAI;IACR,MAAOA,IAAIb,KAAKI,MAAM,CAAE;QACtB,IAAIJ,IAAI,CAACa,EAAE,KAAK,KAAK;YACnBD,OAAOZ,IAAI,CAACa,EAAE;YACdA;YACA;QACF;QACA,IAAIiC,IAAIjC;QACR,MAAOb,IAAI,CAAC8C,EAAE,KAAK,IAAKA;QACxB,MAAMC,IAAID,IAAIjC;QACd,IAAImC,IAAIF;QACR,IAAIG,aAAa,CAAC;QAClB,IAAIC,WAAW,CAAC;QAChB,MAAOF,IAAIhD,KAAKI,MAAM,CAAE;YACtB,IAAIJ,IAAI,CAACgD,EAAE,KAAK,KAAK;gBACnBA;gBACA;YACF;YACA,IAAIzC,IAAIyC;YACR,MAAOhD,IAAI,CAACO,EAAE,KAAK,IAAKA;YACxB,IAAIA,IAAIyC,MAAMD,GAAG;gBACfE,aAAaD;gBACbE,WAAW3C;gBACX;YACF;YACAyC,IAAIzC;QACN;QACA,IAAI0C,cAAc,GAAG;YACnBrC,OAAO,IAAII,MAAM,CAACkC,WAAWrC;YAC7BA,IAAIqC;QACN,OAAO;YACLtC,OAAOZ,KAAKkB,KAAK,CAACL,GAAGiC;YACrBjC,IAAIiC;QACN;IACF;IACA,OAAOlC;AACT"}
@@ -1,21 +1,17 @@
1
1
  import posix from 'node:path/posix';
2
2
  import { maskRegions } from './fences.js';
3
- // links(src, target, target_base, dst, embed): target as written, target_base its baseKey
4
- // (indexed, for the incremental resolve below), dst the resolved path or NULL (a
5
- // queryable dead link), embed whether it's `![[x]]`/`![](x.md)` rather than `[[x]]`/`[](x.md)`
6
- // -- Obsidian's grain: a target both linked and embedded in the same note is two rows.
7
- // Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and
8
- // frontmatter values that are exactly a wikilink ("[[X]]"; mid-string and ![[...]] forms are
9
- // not links there -- Obsidian's rule, probe-verified).
10
- // One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #
11
- // keeps the anchor as the target (a same-note self-link needs a heading name), otherwise
12
- // the anchor splits off. Null = not a link.
3
+ // links(src, target, target_base, dst, embed): target as written, target_base its indexed baseKey, dst the resolved path or NULL (a queryable dead link).
4
+ // embed distinguishes `![[x]]`/`![](x.md)` from `[[x]]`/`[](x.md)` -- Obsidian's grain: a target both linked and embedded in one note is two rows.
5
+ // Alias stripped after |; a leading # keeps the anchor as the target (self-link needs a
6
+ // heading name), else the anchor splits off. Null = not a link.
13
7
  function parseWikilinkInner(inner) {
14
8
  const beforeAlias = inner.split('|')[0].trim();
15
9
  if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;
16
10
  const target = beforeAlias.split('#')[0].trim();
17
11
  return target || null;
18
12
  }
13
+ // A frontmatter value counts only when it is exactly a wikilink ("[[X]]") -- mid-string and
14
+ // ![[...]] forms don't count there, per Obsidian's rule (probe-verified).
19
15
  function extract(_raw, rawBody, _search, data) {
20
16
  const body = /\[\[|\]\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;
21
17
  const seen = new Set();
@@ -36,12 +32,8 @@ function extract(_raw, rawBody, _search, data) {
36
32
  const target = parseWikilinkInner(m[1]);
37
33
  if (target) add(target, body[((_m_index = m.index) !== null && _m_index !== void 0 ? _m_index : 0) - 1] === '!');
38
34
  }
39
- // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full
40
- // linkpath resolution, so \](Zektor) resolves like [[Zektor]] and \](#anchor) is a
41
- // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)
42
- // and titled links (dest stops at whitespace) are skipped.
43
- // A space in the destination is only valid ahead of a quoted title; a domain-shaped first
44
- // segment (www.example.com/...) is external even without a scheme.
35
+ // Any internal destination, not only .md-suffixed: \](Zektor) resolves like [[Zektor]] and
36
+ // \](#anchor) is a self-edge. External URLs and titled links (dest stops at whitespace) skip.
45
37
  for (const m of body.matchAll(/(!)?\[[^\]]*\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+(?:"[^)]*"|'[^)]*'))?\)/g)){
46
38
  const dest = m[2].trim();
47
39
  // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative
@@ -70,9 +62,8 @@ function cleanTarget(target) {
70
62
  function baseKey(path) {
71
63
  return posix.basename(path).replace(/\.md$/i, '').toLowerCase();
72
64
  }
73
- // Obsidian-style: exact relative path (with/without .md), path relative to the linking
74
- // note's directory, then basename match (shortest path wins on ties -- verified against a
75
- // cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).
65
+ // Obsidian-style: exact relative path (with/without .md), path relative to the linking note's
66
+ // directory, then basename match (shortest path wins ties, verified against a cold-loaded vault).
76
67
  function resolveTarget(src, target, pathSet, byBase) {
77
68
  // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.
78
69
  if (target.startsWith('#')) return src;
@@ -107,9 +98,8 @@ function buildByBase(files) {
107
98
  }
108
99
  return byBase;
109
100
  }
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.
101
+ // resolveTarget applied to `rows`, returning only the rows whose dst changed, so the caller
102
+ // can write them in one runBatch call.
113
103
  function resolveRows(rows, pathSet, byBase) {
114
104
  const changed = [];
115
105
  for (const row of rows){
@@ -151,9 +141,8 @@ async function selectIn(db, column, keys) {
151
141
  }
152
142
  return rows;
153
143
  }
154
- // Re-resolve only what this reconcile could have touched: re-stored sources, links whose
155
- // target basename matches an added/vanished file, and links whose dst just vanished. All
156
- // three are indexed lookups, so this never reads the whole table.
144
+ // Re-resolves only what this reconcile could have touched: re-stored sources, links whose
145
+ // target basename matches an added/vanished file, and links whose dst vanished -- all indexed.
157
146
  async function resolveIncremental(db, delta) {
158
147
  const pathSet = new Set(delta.files.map((f)=>f.relPath));
159
148
  const byBase = buildByBase(delta.files);
@@ -176,15 +165,8 @@ async function resolveIncremental(db, delta) {
176
165
  await writeResolved(db, changed);
177
166
  return changed.length > 0;
178
167
  }
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.
168
+ // One edge per (src, target); an embed row is excluded only when the same pair also exists as
169
+ // a link. Self-edges are dropped so PageRank mass is not self-recycled.
188
170
  export const LINK_EDGES_SQL = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst
189
171
  UNION ALL
190
172
  SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst
@@ -200,10 +182,8 @@ export async function linkEdges(db) {
200
182
  const stmt = await db.prepare(LINK_EDGES_SQL);
201
183
  return toEdges(await stmt.all());
202
184
  }
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.
185
+ // Records whether remove()/store() deleted any link row, since afterReconcile runs after both
186
+ // and dstChanged alone misses the reparsed-to-zero-links case. Keyed on delta so state dies with it.
207
187
  const removedWithLinks = new WeakMap();
208
188
  export const links = {
209
189
  name: 'links',
@@ -213,10 +193,8 @@ export const links = {
213
193
  await db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');
214
194
  },
215
195
  extract,
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.
196
+ // Reparsed docs are diffed in store() instead of wiped here: delete-and-reinsert resets dst
197
+ // to NULL, making every touch-only reparse look like an edge change and recompute PageRank.
220
198
  async remove (db, paths, delta) {
221
199
  const vanishedSet = new Set(delta.vanished);
222
200
  const vanished = paths.filter((p)=>vanishedSet.has(p));
@@ -234,9 +212,8 @@ export const links = {
234
212
  ]));
235
213
  const addedSet = new Set(delta.added);
236
214
  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.
215
+ // Stale rows (target/embed pairs missing from the new parse) are diffed in JS from one
216
+ // bulk SELECT, so this pass costs one query plus one batched DELETE regardless of file count.
240
217
  const existingRows = await selectIn(db, 'src', reparsedExisting);
241
218
  const staleRows = existingRows.filter((row)=>{
242
219
  var _byPath_get;
@@ -251,10 +228,8 @@ export const links = {
251
228
  ]));
252
229
  removedWithLinks.set(delta, true);
253
230
  }
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.
231
+ // Upsert preserves dst on surviving rows; only new rows start at NULL. A same-note anchor
232
+ // gets no target_base, so SQL's `IN` excludes it from resolveIncremental's lookup.
258
233
  const upsertRows = [];
259
234
  for (const [path, targets] of byPath){
260
235
  for (const { target, embed } of targets)upsertRows.push([
@@ -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 { 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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"}
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 indexed baseKey, dst the resolved path or NULL (a queryable dead link).\n// embed distinguishes `![[x]]`/`![](x.md)` from `[[x]]`/`[](x.md)` -- Obsidian's grain: a target both linked and embedded in one note is two rows.\n\n// Alias stripped after |; a leading # keeps the anchor as the target (self-link needs a\n// heading name), else 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\n// A frontmatter value counts only when it is exactly a wikilink (\"[[X]]\") -- mid-string and\n// ![[...]] forms don't count there, per Obsidian's rule (probe-verified).\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: \\](Zektor) resolves like [[Zektor]] and\n // \\](#anchor) is a self-edge. External URLs and titled links (dest stops at whitespace) skip.\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 note's\n// directory, then basename match (shortest path wins ties, verified against a cold-loaded vault).\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 only the rows whose dst changed, so the caller\n// can write them in one runBatch call.\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-resolves only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst vanished -- all indexed.\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 (src, target); an embed row is excluded only when the same pair also exists as\n// a link. Self-edges are dropped so PageRank mass is not self-recycled.\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// Records whether remove()/store() deleted any link row, since afterReconcile runs after both\n// and dstChanged alone misses the reparsed-to-zero-links case. Keyed on delta so state dies with it.\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() instead of wiped here: delete-and-reinsert resets dst\n // to NULL, making every touch-only reparse look like an edge change and recompute PageRank.\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 (target/embed pairs missing from the new parse) are diffed in JS from one\n // bulk SELECT, so this 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; only new rows start at NULL. A same-note anchor\n // gets no target_base, so SQL's `IN` excludes it from resolveIncremental's 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":"AAAA,OAAOA,WAAW,kBAAkB;AAGpC,SAASC,WAAW,QAAQ,cAAc;AAG1C,0JAA0J;AAC1J,mJAAmJ;AAEnJ,wFAAwF;AACxF,gEAAgE;AAChE,SAASC,mBAAmBC,KAAa;IACvC,MAAMC,cAAcD,MAAME,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,IAAI;IAC5C,IAAIF,YAAYG,UAAU,CAAC,MAAM,OAAOH,YAAYI,MAAM,GAAG,IAAIJ,cAAc;IAC/E,MAAMK,SAASL,YAAYC,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,IAAI;IAC7C,OAAOG,UAAU;AACnB;AAEA,4FAA4F;AAC5F,0EAA0E;AAC1E,SAASC,QAAQC,IAAY,EAAEC,OAAe,EAAEC,OAA4C,EAAEC,IAA8B;IAC1H,MAAMC,OAAO,YAAYC,IAAI,CAACJ,WAAWX,YAAYW,WAAWA;IAChE,MAAMK,OAAO,IAAIC;IACjB,MAAMC,UAAqD,EAAE;IAC7D,MAAMC,MAAM,CAACX,QAAgBY;QAC3B,MAAMC,MAAM,GAAGb,OAAO,EAAE,EAAEY,QAAQ,MAAM,KAAK;QAC7C,IAAIJ,KAAKM,GAAG,CAACD,MAAM;QACnBL,KAAKG,GAAG,CAACE;QACTH,QAAQK,IAAI,CAAC;YAAEf;YAAQY;QAAM;IAC/B;IACA,uFAAuF;IACvF,6DAA6D;IAC7D,KAAK,MAAMI,KAAKV,KAAKW,QAAQ,CAAC,kBAAmB;YAEjBD;QAD9B,MAAMhB,SAASP,mBAAmBuB,CAAC,CAAC,EAAE;QACtC,IAAIhB,QAAQW,IAAIX,QAAQM,IAAI,CAAC,EAACU,WAAAA,EAAEE,KAAK,cAAPF,sBAAAA,WAAW,KAAK,EAAE,KAAK;IACvD;IACA,2FAA2F;IAC3F,8FAA8F;IAC9F,KAAK,MAAMA,KAAKV,KAAKW,QAAQ,CAAC,6EAA8E;QAC1G,MAAME,OAAOH,CAAC,CAAC,EAAE,CAACnB,IAAI;QACtB,qFAAqF;QACrF,oFAAoF;QACpF,IAAI,CAACsB,QAAQA,KAAKC,QAAQ,CAAC,QAAQ;QACnC,IAAI,oCAAoCb,IAAI,CAACY,OAAO;QACpD,MAAMnB,SAASmB,KAAKrB,UAAU,CAAC,OAAOqB,OAAOA,KAAKvB,KAAK,CAAC,IAAI,CAAC,EAAE;QAC/D,IAAII,QAAQW,IAAIX,QAAQgB,CAAC,CAAC,EAAE,KAAK;IACnC;IACA,KAAK,MAAMK,SAASC,OAAOC,MAAM,CAAClB,iBAAAA,kBAAAA,OAAQ,CAAC,GAAI;QAC7C,KAAK,MAAMmB,QAAQC,MAAMC,OAAO,CAACL,SAASA,QAAQ;YAACA;SAAM,CAAE;YACzD,IAAI,OAAOG,SAAS,UAAU;YAC9B,MAAMR,IAAI,iBAAiBW,IAAI,CAACH,KAAK3B,IAAI;YACzC,IAAI,CAACmB,GAAG;YACR,MAAMhB,SAASP,mBAAmBuB,CAAC,CAAC,EAAE;YACtC,IAAIhB,QAAQW,IAAIX,QAAQ;QAC1B;IACF;IACA,OAAOU;AACT;AAEA,SAASkB,YAAY5B,MAAc;IACjC,OAAOA,OAAO6B,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,SAAS;AACrD;AAEA,SAASC,QAAQC,IAAY;IAC3B,OAAOxC,MAAMyC,QAAQ,CAACD,MAAMF,OAAO,CAAC,UAAU,IAAII,WAAW;AAC/D;AAEA,8FAA8F;AAC9F,kGAAkG;AAClG,SAASC,cAAcC,GAAW,EAAEnC,MAAc,EAAEoC,OAAoB,EAAEC,MAA6B;IACrG,4FAA4F;IAC5F,IAAIrC,OAAOF,UAAU,CAAC,MAAM,OAAOqC;IACnC,MAAMG,QAAQV,YAAY5B;IAC1B,MAAMuC,UAAUhD,MAAMiD,SAAS,CAACjD,MAAMkD,IAAI,CAAClD,MAAMmD,OAAO,CAACP,MAAMG;IAC/D,KAAK,MAAMK,aAAa;QAACL;QAAO,GAAGA,MAAM,GAAG,CAAC;QAAEC;QAAS,GAAGA,QAAQ,GAAG,CAAC;KAAC,CAAE;QACxE,IAAIH,QAAQtB,GAAG,CAAC6B,YAAY,OAAOA;IACrC;IACA,MAAMC,aAAaP,OAAOQ,GAAG,CAACf,QAAQQ;IACtC,IAAI,CAACM,YAAY,OAAO;IACxB,0FAA0F;IAC1F,+DAA+D;IAC/D,OAAOA,WAAWxB,QAAQ,CAACe,OAAOA,MAAMS,UAAU,CAAC,EAAE;AACvD;AAEA,6FAA6F;AAC7F,yFAAyF;AACzF,SAASE,YAAYC,KAAiB;IACpC,MAAMV,SAAS,IAAIW;IACnB,MAAMC,QAAQF,MAAMG,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO,EAAEC,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEvD,MAAM,GAAGwD,EAAExD,MAAM,IAAKuD,CAAAA,IAAIC,IAAI,CAAC,IAAID,IAAIC,IAAI,IAAI,CAAA;IAC1G,KAAK,MAAMxB,QAAQkB,MAAO;QACxB,MAAMpC,MAAMiB,QAAQC;QACpB,MAAMyB,OAAOnB,OAAOQ,GAAG,CAAChC;QACxB,IAAI2C,MAAMA,KAAKzC,IAAI,CAACgB;aACfM,OAAOoB,GAAG,CAAC5C,KAAK;YAACkB;SAAK;IAC7B;IACA,OAAOM;AACT;AASA,4FAA4F;AAC5F,uCAAuC;AACvC,SAASqB,YAAYC,IAAe,EAAEvB,OAAoB,EAAEC,MAA6B;IACvF,MAAMuB,UAAqB,EAAE;IAC7B,KAAK,MAAMC,OAAOF,KAAM;QACtB,MAAMG,MAAM5B,cAAc2B,IAAI1B,GAAG,EAAE0B,IAAI7D,MAAM,EAAEoC,SAASC;QACxD,IAAIyB,QAAQD,IAAIC,GAAG,EAAEF,QAAQ7C,IAAI,CAAC;YAAE,GAAG8C,GAAG;YAAEC;QAAI;IAClD;IACA,OAAOF;AACT;AAEA,eAAeG,cAAcC,EAAc,EAAEL,IAAe;IAC1D,IAAIA,KAAK5D,MAAM,KAAK,GAAG;IACvB,MAAMiE,GAAGC,QAAQ,CACf,yDACAN,KAAKT,GAAG,CAAC,CAACgB,IAAM;YAACA,EAAEJ,GAAG;YAAEI,EAAE/B,GAAG;YAAE+B,EAAElE,MAAM;SAAC;AAE5C;AAEA,yFAAyF;AACzF,+EAA+E;AAC/E,eAAemE,WAAWH,EAAc,EAAEjB,KAAiB;IACzD,MAAMX,UAAU,IAAI3B,IAAIsC,MAAMG,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO;IAClD,MAAMf,SAASS,YAAYC;IAC3B,MAAMqB,OAAO,MAAMJ,GAAGK,OAAO,CAAC;IAC9B,MAAMV,OAAQ,MAAMS,KAAKE,GAAG;IAC5B,MAAMV,UAAUF,YAAYC,MAAMvB,SAASC;IAC3C,MAAM0B,cAAcC,IAAIJ;IACxB,OAAOA,QAAQ7D,MAAM,GAAG;AAC1B;AAEA,2FAA2F;AAC3F,2FAA2F;AAC3F,eAAewE,SAASP,EAAc,EAAEQ,MAAc,EAAEC,IAAc;IACpE,MAAMd,OAAkB,EAAE;IAC1B,IAAK,IAAIe,IAAI,GAAGA,IAAID,KAAK1E,MAAM,EAAE2E,KAAK,IAAK;QACzC,MAAMC,QAAQF,KAAKG,KAAK,CAACF,GAAGA,IAAI;QAChC,MAAMN,OAAO,MAAMJ,GAAGK,OAAO,CAAC,CAAC,gDAAgD,EAAEG,OAAO,KAAK,EAAEG,MAAMzB,GAAG,CAAC,IAAM,KAAKT,IAAI,CAAC,MAAM,CAAC,CAAC;QACjIkB,KAAK5C,IAAI,IAAM,MAAMqD,KAAKE,GAAG,IAAIK;IACnC;IACA,OAAOhB;AACT;AAEA,0FAA0F;AAC1F,+FAA+F;AAC/F,eAAekB,mBAAmBb,EAAc,EAAEc,KAAqB;IACrE,MAAM1C,UAAU,IAAI3B,IAAIqE,MAAM/B,KAAK,CAACG,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO;IACxD,MAAMf,SAASS,YAAYgC,MAAM/B,KAAK;IAEtC,MAAMgC,mBAAmB,IAAItE;IAC7B,KAAK,MAAMuE,KAAKF,MAAMG,KAAK,CAAEF,iBAAiBpE,GAAG,CAACmB,QAAQkD;IAC1D,KAAK,MAAMA,KAAKF,MAAMI,QAAQ,CAAEH,iBAAiBpE,GAAG,CAACmB,QAAQkD;IAE7D,+EAA+E;IAC/E,MAAMpC,aAAa,IAAII;IACvB,MAAMmC,UAAU,CAACxB;QACf,KAAK,MAAME,OAAOF,KAAMf,WAAWa,GAAG,CAAC,GAAGI,IAAI1B,GAAG,CAAC,EAAE,EAAE0B,IAAI7D,MAAM,CAAC,EAAE,EAAE6D,IAAIjD,KAAK,EAAE,EAAEiD;IACpF;IACAsB,QAAQ,MAAMZ,SAASP,IAAI,OAAOc,MAAMM,QAAQ;IAChDD,QAAQ,MAAMZ,SAASP,IAAI,eAAe;WAAIe;KAAiB;IAC/DI,QAAQ,MAAMZ,SAASP,IAAI,OAAOc,MAAMI,QAAQ;IAEhD,MAAMtB,UAAUF,YAAY;WAAId,WAAWrB,MAAM;KAAG,EAAEa,SAASC;IAC/D,MAAM0B,cAAcC,IAAIJ;IACxB,OAAOA,QAAQ7D,MAAM,GAAG;AAC1B;AAEA,8FAA8F;AAC9F,wEAAwE;AACxE,OAAO,MAAMsF,iBAAiB,CAAC;;;4GAG6E,CAAC,CAAC;AAE9G,OAAO,SAASC,QAAQ3B,IAAyC;IAC/D,OAAOA,KAAKT,GAAG,CAAC,CAACgB,IAAM;YAACA,EAAE/B,GAAG;YAAE+B,EAAEJ,GAAG;SAAC;AACvC;AAEA,qEAAqE;AACrE,OAAO,eAAeyB,UAAUvB,EAAc;IAC5C,MAAMI,OAAO,MAAMJ,GAAGK,OAAO,CAACgB;IAC9B,OAAOC,QAAS,MAAMlB,KAAKE,GAAG;AAChC;AAEA,8FAA8F;AAC9F,qGAAqG;AACrG,MAAMkB,mBAAmB,IAAIC;AAE7B,OAAO,MAAMC,QAAiB;IAC5BC,MAAM;IACN,MAAMC,QAAO5B,EAAE;QACb,MAAMA,GAAGrC,IAAI,CAAC;QACd,MAAMqC,GAAGrC,IAAI,CAAC;QACd,MAAMqC,GAAGrC,IAAI,CAAC;IAChB;IACA1B;IACA,4FAA4F;IAC5F,4FAA4F;IAC5F,MAAM4F,QAAO7B,EAAE,EAAEf,KAAK,EAAE6B,KAAK;QAC3B,MAAMgB,cAAc,IAAIrF,IAAIqE,MAAMI,QAAQ;QAC1C,MAAMA,WAAWjC,MAAM8C,MAAM,CAAC,CAACf,IAAMc,YAAYhF,GAAG,CAACkE;QACrD,IAAIE,SAASnF,MAAM,KAAK,GAAG;QAC3B,MAAMiG,eAAe,MAAMzB,SAASP,IAAI,OAAOkB;QAC/C,MAAMlB,GAAGC,QAAQ,CACf,mCACAiB,SAAShC,GAAG,CAAC,CAAC8B,IAAM;gBAACA;aAAE;QAEzB,IAAIgB,aAAajG,MAAM,GAAG,GAAGyF,iBAAiB/B,GAAG,CAACqB,OAAO;IAC3D;IACA,MAAMmB,OAAMjC,EAAE,EAAEkC,IAAoB,EAAEpB,KAAK;QACzC,MAAMqB,SAAS,IAAInD,IAAIkD,KAAKhD,GAAG,CAAC,CAACkD,IAAM;gBAACA,EAAErE,IAAI;gBAAEqE,EAAEC,SAAS;aAA8C;QACzG,MAAMC,WAAW,IAAI7F,IAAIqE,MAAMG,KAAK;QACpC,MAAMsB,mBAAmBL,KAAKhD,GAAG,CAAC,CAACkD,IAAMA,EAAErE,IAAI,EAAEgE,MAAM,CAAC,CAACf,IAAM,CAACsB,SAASxF,GAAG,CAACkE;QAE7E,uFAAuF;QACvF,8FAA8F;QAC9F,MAAMgB,eAAe,MAAMzB,SAASP,IAAI,OAAOuC;QAC/C,MAAMC,YAAYR,aAAaD,MAAM,CAAC,CAAClC;gBACrBsC;YAAhB,MAAMM,WAAUN,cAAAA,OAAOtD,GAAG,CAACgB,IAAI1B,GAAG,eAAlBgE,yBAAAA,cAAuB,EAAE;YACzC,OAAO,CAACM,QAAQC,IAAI,CAAC,CAACC,IAAMA,EAAE3G,MAAM,KAAK6D,IAAI7D,MAAM,IAAI,AAAC2G,CAAAA,EAAE/F,KAAK,GAAG,IAAI,CAAA,MAAOiD,IAAIjD,KAAK;QACxF;QACA,IAAI4F,UAAUzG,MAAM,GAAG,GAAG;YACxB,MAAMiE,GAAGC,QAAQ,CACf,gEACAuC,UAAUtD,GAAG,CAAC,CAACgB,IAAM;oBAACA,EAAE/B,GAAG;oBAAE+B,EAAElE,MAAM;oBAAEkE,EAAEtD,KAAK;iBAAC;YAEjD4E,iBAAiB/B,GAAG,CAACqB,OAAO;QAC9B;QAEA,0FAA0F;QAC1F,mFAAmF;QACnF,MAAM8B,aAA0B,EAAE;QAClC,KAAK,MAAM,CAAC7E,MAAM0E,QAAQ,IAAIN,OAAQ;YACpC,KAAK,MAAM,EAAEnG,MAAM,EAAEY,KAAK,EAAE,IAAI6F,QAASG,WAAW7F,IAAI,CAAC;gBAACgB;gBAAM/B;gBAAQA,OAAOF,UAAU,CAAC,OAAO,OAAOgC,QAAQF,YAAY5B;gBAAUY,QAAQ,IAAI;aAAE;QACtJ;QACA,IAAIgG,WAAW7G,MAAM,GAAG,GAAG;YACzB,MAAMiE,GAAGC,QAAQ,CAAC,uKAAuK2C;QAC3L;IACF;IACA,MAAMC,gBAAe7C,EAAE,EAAEc,KAAK;YAGLU;QAFvB,yFAAyF;QACzF,0BAA0B;QAC1B,MAAMsB,kBAAiBtB,wBAAAA,iBAAiB3C,GAAG,CAACiC,oBAArBU,mCAAAA,wBAA+B;QAEtD,MAAMuB,QAAQjC,MAAMM,QAAQ,CAACrF,MAAM,GAAG+E,MAAMI,QAAQ,CAACnF,MAAM;QAC3D,+EAA+E;QAC/E,iFAAiF;QACjF,MAAMiH,QAAQlC,MAAM/B,KAAK,CAAChD,MAAM,KAAK,KAAKgH,QAAQ,MAAMjC,MAAM/B,KAAK,CAAChD,MAAM;QAE1E,MAAMkH,aAAaD,QAAQ,MAAM7C,WAAWH,IAAIc,MAAM/B,KAAK,IAAI,MAAM8B,mBAAmBb,IAAIc;QAC5FA,MAAMoC,YAAY,GAAGD,cAAcH;IACrC;AACF,EAAE"}
@@ -1,25 +1,23 @@
1
- import { estimateTokens } from '../chunk/index.js';
2
- import { fenceTracker } from './fences.js';
3
- // Headings outside fenced code blocks.
4
- function extract(raw) {
5
- const lines = raw.split('\n');
6
- const found = [];
7
- const fence = fenceTracker();
8
- for(let i = 0; i < lines.length; i++){
9
- if (fence.feed(lines[i])) continue;
10
- if (fence.inFence) continue;
11
- const m = lines[i].match(/^(#{1,6}) +(.*)/);
12
- if (m) found.push({
13
- level: m[1].length,
14
- heading: m[2].trim(),
15
- startLine: i + 1,
16
- endLine: lines.length,
1
+ import { estimateTokens, parse } from '../chunk/index.js';
2
+ import { countLines } from '../scan/frontmatter.js';
3
+ // Heading blocks parse() already found (mdast/CommonMark fences), offset back onto the raw file;
4
+ // a section runs to just before the next heading, or EOF.
5
+ function sectionsFromBlocks(blocks, raw, body) {
6
+ const rawLines = raw.split('\n');
7
+ const offset = rawLines.length - countLines(body);
8
+ const found = blocks.filter((b)=>b.type === 'heading').map((b)=>{
9
+ var _b_depth, _b_text;
10
+ return {
11
+ level: (_b_depth = b.depth) !== null && _b_depth !== void 0 ? _b_depth : 1,
12
+ heading: ((_b_text = b.text) !== null && _b_text !== void 0 ? _b_text : '').trim(),
13
+ startLine: b.startLine + offset,
14
+ endLine: rawLines.length,
17
15
  tokens: 0
18
- });
19
- }
16
+ };
17
+ });
20
18
  for(let s = 0; s < found.length; s++){
21
19
  if (s + 1 < found.length) found[s].endLine = found[s + 1].startLine - 1;
22
- const text = lines.slice(found[s].startLine - 1, found[s].endLine).join('\n');
20
+ const text = rawLines.slice(found[s].startLine - 1, found[s].endLine).join('\n');
23
21
  found[s].tokens = Math.ceil(estimateTokens(text));
24
22
  }
25
23
  return found;
@@ -29,7 +27,9 @@ export const sections = {
29
27
  async schema (db) {
30
28
  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
29
  },
32
- extract,
30
+ extract (raw, body, _search, _data, _cfg, blocks) {
31
+ return sectionsFromBlocks(blocks !== null && blocks !== void 0 ? blocks : parse(body), raw, body);
32
+ },
33
33
  async remove (db, paths) {
34
34
  if (paths.length === 0) return;
35
35
  await db.runBatch('DELETE FROM sections WHERE "path" = ?', paths.map((p)=>[
@@ -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 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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/sections.ts"],"sourcesContent":["import type { Block } from '../chunk/index.ts';\nimport { estimateTokens, parse } from '../chunk/index.ts';\nimport { countLines } from '../scan/frontmatter.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), CJK-aware.\n\nexport interface Section {\n level: number;\n heading: string;\n startLine: number;\n endLine: number;\n tokens: number;\n}\n\n// Heading blocks parse() already found (mdast/CommonMark fences), offset back onto the raw file;\n// a section runs to just before the next heading, or EOF.\nfunction sectionsFromBlocks(blocks: Block[], raw: string, body: string): Section[] {\n const rawLines = raw.split('\\n');\n const offset = rawLines.length - countLines(body);\n const found: Section[] = blocks.filter((b) => b.type === 'heading').map((b) => ({ level: b.depth ?? 1, heading: (b.text ?? '').trim(), startLine: b.startLine + offset, endLine: rawLines.length, tokens: 0 }));\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 = rawLines.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(raw, body, _search, _data, _cfg, blocks) {\n return sectionsFromBlocks(blocks ?? parse(body), raw, body);\n },\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","parse","countLines","sectionsFromBlocks","blocks","raw","body","rawLines","split","offset","length","found","filter","b","type","map","level","depth","heading","text","trim","startLine","endLine","tokens","s","slice","join","Math","ceil","sections","name","schema","db","exec","extract","_search","_data","_cfg","remove","paths","runBatch","p","store","docs","rows","path","extracted","forEach","idx","push"],"mappings":"AACA,SAASA,cAAc,EAAEC,KAAK,QAAQ,oBAAoB;AAC1D,SAASC,UAAU,QAAQ,yBAAyB;AAcpD,iGAAiG;AACjG,0DAA0D;AAC1D,SAASC,mBAAmBC,MAAe,EAAEC,GAAW,EAAEC,IAAY;IACpE,MAAMC,WAAWF,IAAIG,KAAK,CAAC;IAC3B,MAAMC,SAASF,SAASG,MAAM,GAAGR,WAAWI;IAC5C,MAAMK,QAAmBP,OAAOQ,MAAM,CAAC,CAACC,IAAMA,EAAEC,IAAI,KAAK,WAAWC,GAAG,CAAC,CAACF;YAAgBA,UAAwBA;eAAjC;YAAEG,KAAK,GAAEH,WAAAA,EAAEI,KAAK,cAAPJ,sBAAAA,WAAW;YAAGK,SAAS,EAACL,UAAAA,EAAEM,IAAI,cAANN,qBAAAA,UAAU,IAAIO,IAAI;YAAIC,WAAWR,EAAEQ,SAAS,GAAGZ;YAAQa,SAASf,SAASG,MAAM;YAAEa,QAAQ;QAAE;;IAC5M,IAAK,IAAIC,IAAI,GAAGA,IAAIb,MAAMD,MAAM,EAAEc,IAAK;QACrC,IAAIA,IAAI,IAAIb,MAAMD,MAAM,EAAEC,KAAK,CAACa,EAAE,CAACF,OAAO,GAAGX,KAAK,CAACa,IAAI,EAAE,CAACH,SAAS,GAAG;QACtE,MAAMF,OAAOZ,SAASkB,KAAK,CAACd,KAAK,CAACa,EAAE,CAACH,SAAS,GAAG,GAAGV,KAAK,CAACa,EAAE,CAACF,OAAO,EAAEI,IAAI,CAAC;QAC3Ef,KAAK,CAACa,EAAE,CAACD,MAAM,GAAGI,KAAKC,IAAI,CAAC5B,eAAemB;IAC7C;IACA,OAAOR;AACT;AAEA,OAAO,MAAMkB,WAAoB;IAC/BC,MAAM;IACN,MAAMC,QAAOC,EAAE;QACb,MAAMA,GAAGC,IAAI,CAAC,CAAC,4KAA4K,CAAC;IAC9L;IACAC,SAAQ7B,GAAG,EAAEC,IAAI,EAAE6B,OAAO,EAAEC,KAAK,EAAEC,IAAI,EAAEjC,MAAM;QAC7C,OAAOD,mBAAmBC,mBAAAA,oBAAAA,SAAUH,MAAMK,OAAOD,KAAKC;IACxD;IACA,MAAMgC,QAAON,EAAE,EAAEO,KAAK;QACpB,IAAIA,MAAM7B,MAAM,KAAK,GAAG;QACxB,MAAMsB,GAAGQ,QAAQ,CACf,yCACAD,MAAMxB,GAAG,CAAC,CAAC0B,IAAM;gBAACA;aAAE;IAExB;IACA,MAAMC,OAAMV,EAAE,EAAEW,IAAI;QAClB,MAAMC,OAAoB,EAAE;QAC5B,KAAK,MAAM,EAAEC,IAAI,EAAEC,SAAS,EAAE,IAAIH,KAAM,AAACG,UAAwBC,OAAO,CAAC,CAACvB,GAAGwB,MAAQJ,KAAKK,IAAI,CAAC;gBAACJ;gBAAMG;gBAAKxB,EAAER,KAAK;gBAAEQ,EAAEN,OAAO;gBAAEM,EAAEH,SAAS;gBAAEG,EAAEF,OAAO;gBAAEE,EAAED,MAAM;aAAC;QAChK,IAAIqB,KAAKlC,MAAM,KAAK,GAAG;QACvB,MAAMsB,GAAGQ,QAAQ,CAAC,iHAAiHI;IACrI;AACF,EAAE"}
@@ -1,7 +1,6 @@
1
1
  import { maskRegions } from './fences.js';
2
2
  // tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline
3
- // #tags from the prose, deduplicated, source not distinguished. Nested tags store full
4
- // (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` is how a caller matches the parent too.
3
+ // #tags, deduplicated, source not distinguished. Nested tags store full (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` matches the parent too.
5
4
  // Obsidian treats [[#Heading]] as a same-note link, not a tag.
6
5
  const WIKILINK_RE = /\[\[.*?\]\]/g; // to the first ]], so a heading holding a lone ] still masks
7
6
  // Obsidian doesn't read tags inside HTML markup.
@@ -32,9 +31,8 @@ function frontmatterTags(data) {
32
31
  }
33
32
  return found;
34
33
  }
35
- // #tag tokens outside fenced code blocks, inline code spans, wikilinks, HTML tags, HTML blocks,
36
- // <!-- --> comments, and link destinations. maskRegions() handles all of those region-level
37
- // concerns (and code spans, masked once there); this loop only does per-line tag extraction.
34
+ // #tag tokens outside the regions maskRegions() masks (fences, code spans, wikilinks, HTML,
35
+ // comments, link destinations); this loop only does per-line tag extraction.
38
36
  function inlineTags(body) {
39
37
  const found = [];
40
38
  for (const line of maskRegions(body).split('\n')){
@@ -65,9 +63,8 @@ export const tags = {
65
63
  await db.exec('CREATE INDEX IF NOT EXISTS tags_tag ON tags(tag)');
66
64
  },
67
65
  extract,
68
- // Blanket clear-then-reinsert (store() below always writes the full found list back), so
69
- // remove() covers both a vanished file and a reparsed one that already had rows -- no
70
- // per-file NOT IN diffing, which cannot be expressed as one statement shared across files.
66
+ // Blanket clear-then-reinsert (store() always writes the full found list back), so remove()
67
+ // covers vanished and reparsed files without per-file NOT IN diffing, which cannot be one statement shared across files.
71
68
  async remove (db, paths) {
72
69
  if (paths.length === 0) return;
73
70
  await db.runBatch('DELETE FROM tags WHERE "path" = ?', paths.map((p)=>[