@plebeiantech/lat.md 0.12.3-fork.17 → 0.12.3-fork.20

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 (244) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/agent-invocation.d.ts +6 -0
  3. package/dist/src/cli/agent-invocation.js +23 -0
  4. package/dist/src/cli/fork-instructions.js +1 -1
  5. package/dist/src/cli/fork-scaffold.js +2 -2
  6. package/dist/src/cli/graph.d.ts +1 -1
  7. package/dist/src/cli/hook.js +11 -11
  8. package/dist/src/cli/index.js +41 -59
  9. package/dist/src/cli/init-preferences.js +5 -4
  10. package/dist/src/cli/init.d.ts +1 -1
  11. package/dist/src/cli/init.js +54 -45
  12. package/dist/src/cli/reindex.d.ts +1 -1
  13. package/dist/src/cli/reindex.js +2 -2
  14. package/dist/src/cli/search.d.ts +4 -4
  15. package/dist/src/cli/search.js +41 -28
  16. package/dist/src/cli/ui-build-server.d.ts +1 -1
  17. package/dist/src/cli/ui-build.d.ts +1 -1
  18. package/dist/src/cli/ui.d.ts +1 -1
  19. package/dist/src/fork/antigravity-hook.js +6 -6
  20. package/dist/src/fork/antigravity-init.d.ts +2 -2
  21. package/dist/src/fork/antigravity-init.js +2 -2
  22. package/dist/src/graph-export.js +7 -7
  23. package/dist/src/knowledge/index.js +2 -2
  24. package/dist/src/mcp/server.js +8 -8
  25. package/dist/src/search/cache.d.ts +1 -1
  26. package/dist/src/search/cache.js +40 -7
  27. package/dist/src/search/chunks.d.ts +2 -2
  28. package/dist/src/search/db.d.ts +5 -4
  29. package/dist/src/search/db.js +36 -40
  30. package/dist/src/search/embedder.js +1 -1
  31. package/dist/src/search/index.d.ts +1 -1
  32. package/dist/src/search/index.js +2 -2
  33. package/dist/src/search/lock.d.ts +4 -1
  34. package/dist/src/search/lock.js +30 -8
  35. package/dist/src/search/query.d.ts +5 -2
  36. package/dist/src/search/query.js +55 -38
  37. package/dist/src/search/search.d.ts +3 -1
  38. package/dist/src/search/search.js +21 -12
  39. package/dist/src/search/types.d.ts +2 -18
  40. package/dist/src/view/client/assets/{GraphView-BWlRdQM4.js → GraphView-Dij8mAA_.js} +1 -1
  41. package/dist/src/view/client/assets/{MarkdownEditor-MW-Ck1O0.js → MarkdownEditor-88Wbt0M1.js} +1 -1
  42. package/dist/src/view/client/assets/{abnfDiagram-VCTEODGH-Bn3RshMV.js → abnfDiagram-VCTEODGH-y0DlYa8E.js} +1 -1
  43. package/dist/src/view/client/assets/{arc-En9tdFQF.js → arc-FlMu8dnO.js} +1 -1
  44. package/dist/src/view/client/assets/{architectureDiagram-5GKGNRK7-B-9J1VAM.js → architectureDiagram-5GKGNRK7-DA_uWZW1.js} +1 -1
  45. package/dist/src/view/client/assets/{blockDiagram-I7D4REHJ-QlznwalP.js → blockDiagram-I7D4REHJ-BHQ5LU85.js} +1 -1
  46. package/dist/src/view/client/assets/{c4Diagram-7LVT6UL2-Bp2y8B4_.js → c4Diagram-7LVT6UL2-BRucKCt2.js} +1 -1
  47. package/dist/src/view/client/assets/channel-BgDNA_Ay.js +1 -0
  48. package/dist/src/view/client/assets/{chunk-2Q5K7J3B-CnBKL-lm.js → chunk-2Q5K7J3B-BJPcFivt.js} +1 -1
  49. package/dist/src/view/client/assets/{chunk-5VM5RSS4-LLYMi_n6.js → chunk-5VM5RSS4-v2q99bGz.js} +1 -1
  50. package/dist/src/view/client/assets/{chunk-F27PBJKO-VIeO7VF0.js → chunk-F27PBJKO-CcHOuJCs.js} +1 -1
  51. package/dist/src/view/client/assets/{chunk-IMKFNOWR-D5i67DVD.js → chunk-IMKFNOWR-DNxYuJNq.js} +1 -1
  52. package/dist/src/view/client/assets/{chunk-JWPE2WC7-D4qAtoL3.js → chunk-JWPE2WC7-C0eHKvLW.js} +1 -1
  53. package/dist/src/view/client/assets/{chunk-POPQ4Y6H-BMzOyiW_.js → chunk-POPQ4Y6H-57CWp2Ve.js} +1 -1
  54. package/dist/src/view/client/assets/{chunk-SVP7TREG-DWy5nbfp.js → chunk-SVP7TREG-BqcoxIJw.js} +1 -1
  55. package/dist/src/view/client/assets/{chunk-TICWLB2K-CVqCANFB.js → chunk-TICWLB2K-DRUJR5zH.js} +1 -1
  56. package/dist/src/view/client/assets/{chunk-XXDRQBXY-Bp23h6Tf.js → chunk-XXDRQBXY-Dez0oRoM.js} +1 -1
  57. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-CFZ_Sgh5.js +1 -0
  58. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-CFZ_Sgh5.js +1 -0
  59. package/dist/src/view/client/assets/{cose-bilkent-JH36ORCC-BCw7fCC2.js → cose-bilkent-JH36ORCC-BHtud6Hg.js} +1 -1
  60. package/dist/src/view/client/assets/{cynefin-OW5HDTMX--lYN4N9m.js → cynefin-OW5HDTMX-BjBW8VGk.js} +1 -1
  61. package/dist/src/view/client/assets/{cynefinDiagram-5FMLGOSQ-9sK5EV7v.js → cynefinDiagram-5FMLGOSQ-DwGuD7oF.js} +1 -1
  62. package/dist/src/view/client/assets/{dagre-GXQ25YYZ-CoMdEFCq.js → dagre-GXQ25YYZ-C5zSlGK9.js} +1 -1
  63. package/dist/src/view/client/assets/{diagram-S7CK7UJ4-D13dyWdd.js → diagram-S7CK7UJ4-C_NhA3bj.js} +1 -1
  64. package/dist/src/view/client/assets/{diagram-UQ7AKVKN-PdwLPPPA.js → diagram-UQ7AKVKN-az7QPYNb.js} +1 -1
  65. package/dist/src/view/client/assets/{diagram-VSXAHHWV-CtWWa_A7.js → diagram-VSXAHHWV-BRvDuM8_.js} +1 -1
  66. package/dist/src/view/client/assets/{diagram-VX7I27RA-BeCYcSU-.js → diagram-VX7I27RA-BWLgF6S-.js} +1 -1
  67. package/dist/src/view/client/assets/{diagram-Z3DM3KII-DiWabOBM.js → diagram-Z3DM3KII-DV3d6mL5.js} +1 -1
  68. package/dist/src/view/client/assets/{ebnfDiagram-PWID7BFC-CZQ3MD_K.js → ebnfDiagram-PWID7BFC-yUXrEAvK.js} +1 -1
  69. package/dist/src/view/client/assets/{erDiagram-RLTQ6QDP-B1AhXIrP.js → erDiagram-RLTQ6QDP-D35FN1Pt.js} +1 -1
  70. package/dist/src/view/client/assets/{flowDiagram-HODETNUW-Bhr0JM6P.js → flowDiagram-HODETNUW-BciZqsjT.js} +1 -1
  71. package/dist/src/view/client/assets/{ganttDiagram-EL5Y4UJY-CbP8iAkg.js → ganttDiagram-EL5Y4UJY-BysPGgXS.js} +1 -1
  72. package/dist/src/view/client/assets/{gitGraphDiagram-WWUBYQGX-B9uuGdLF.js → gitGraphDiagram-WWUBYQGX-C3DdJ5xG.js} +1 -1
  73. package/dist/src/view/client/assets/index-CJokoRSp.js +13 -0
  74. package/dist/src/view/client/assets/{infoDiagram-27XIBGKW-BrfwQ_Cq.js → infoDiagram-27XIBGKW-D3HZrDRb.js} +1 -1
  75. package/dist/src/view/client/assets/{ishikawaDiagram-5VMMS53U-CHtJ36re.js → ishikawaDiagram-5VMMS53U-BXyVIXTU.js} +1 -1
  76. package/dist/src/view/client/assets/{journeyDiagram-3NMN7TZE-BChcgX1s.js → journeyDiagram-3NMN7TZE-D4-1NxVU.js} +1 -1
  77. package/dist/src/view/client/assets/{kanban-definition-UXKFOSKX-BnwFsKiB.js → kanban-definition-UXKFOSKX-4Gu_5yBA.js} +1 -1
  78. package/dist/src/view/client/assets/{layout-Dnd0pLdb.js → layout-Jf1K3TMM.js} +1 -1
  79. package/dist/src/view/client/assets/{linear-CFhW2ozn.js → linear-DWfwzUT_.js} +1 -1
  80. package/dist/src/view/client/assets/{mermaid.core--OP6lXtW.js → mermaid.core-DMx_DAl8.js} +5 -5
  81. package/dist/src/view/client/assets/{mindmap-definition-YA3MSWOX-D51y2zbm.js → mindmap-definition-YA3MSWOX-8pxY_RWi.js} +1 -1
  82. package/dist/src/view/client/assets/{pegDiagram-XKGWAZYB--zyQJDvI.js → pegDiagram-XKGWAZYB-DNAw_O5t.js} +1 -1
  83. package/dist/src/view/client/assets/{pieDiagram-E7YTZNPT-Cj8mvpJy.js → pieDiagram-E7YTZNPT-uuvRCDkC.js} +1 -1
  84. package/dist/src/view/client/assets/{quadrantDiagram-AXDQQJYC-qvhJrnBf.js → quadrantDiagram-AXDQQJYC-BER2O4Aq.js} +1 -1
  85. package/dist/src/view/client/assets/{railroadDiagram-O6MQD6OU-CtWvxpSo.js → railroadDiagram-O6MQD6OU-CxEsJ_0n.js} +1 -1
  86. package/dist/src/view/client/assets/{requirementDiagram-BXWQKSXE-CBxen_KG.js → requirementDiagram-BXWQKSXE-yS9rspca.js} +1 -1
  87. package/dist/src/view/client/assets/{sankeyDiagram-P5KCCOFB-B_fUkmfc.js → sankeyDiagram-P5KCCOFB-Bqt70a2j.js} +1 -1
  88. package/dist/src/view/client/assets/{sequenceDiagram-WJ2MYXX4-Dclv4p68.js → sequenceDiagram-WJ2MYXX4-BvOOEJNr.js} +1 -1
  89. package/dist/src/view/client/assets/{sizeCapture-INFHLROL-GfG0sMTZ.js → sizeCapture-INFHLROL-CvO_Pjjo.js} +1 -1
  90. package/dist/src/view/client/assets/{stateDiagram-D77RDMKH-YQWPnzxU.js → stateDiagram-D77RDMKH-KLmVOZkq.js} +1 -1
  91. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-KCS8Mdzu.js +1 -0
  92. package/dist/src/view/client/assets/{swimlanes-42K2YHIH-5yiCveVS.js → swimlanes-42K2YHIH-DC1r4_sq.js} +1 -1
  93. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-B2TcAwis.js +8 -0
  94. package/dist/src/view/client/assets/{timeline-definition-24CTP7MA-BvAbB5Zs.js → timeline-definition-24CTP7MA-ChXLOuBL.js} +1 -1
  95. package/dist/src/view/client/assets/{vennDiagram-4TSXK5OY-guC-r60n.js → vennDiagram-4TSXK5OY-D9_h25-Y.js} +1 -1
  96. package/dist/src/view/client/assets/{wardleyDiagram-VM6X3IG4-C0yXcU2x.js → wardleyDiagram-VM6X3IG4-CsQJGGW0.js} +1 -1
  97. package/dist/src/view/client/assets/{xychartDiagram-S5SC5T6Z-RPmkPNfK.js → xychartDiagram-S5SC5T6Z-DGMuPMlm.js} +1 -1
  98. package/dist/src/view/client/index.html +1 -1
  99. package/dist/src/view/diagnostics.d.ts +2 -2
  100. package/dist/src/view/diagnostics.js +5 -5
  101. package/dist/src/view/external-document-tree.js +1 -1
  102. package/dist/src/view/git-diff.js +1 -1
  103. package/dist/src/view/git.js +1 -1
  104. package/dist/src/view/graph.d.ts +2 -2
  105. package/dist/src/view/graph.js +2 -2
  106. package/dist/src/view/markdown.js +1 -1
  107. package/dist/src/view/preindexed-search.d.ts +1 -1
  108. package/dist/src/view/protocol.d.ts +2 -18
  109. package/dist/src/view/publication.d.ts +2 -0
  110. package/dist/src/view/publication.js +78 -0
  111. package/dist/src/view/references.d.ts +3 -3
  112. package/dist/src/view/references.js +3 -3
  113. package/dist/src/view/repository.d.ts +6 -6
  114. package/dist/src/view/repository.js +28 -14
  115. package/dist/src/view/request-security.d.ts +5 -0
  116. package/dist/src/view/request-security.js +46 -0
  117. package/dist/src/view/search-result.d.ts +1 -1
  118. package/dist/src/view/search-result.js +1 -1
  119. package/dist/src/view/search.d.ts +1 -1
  120. package/dist/src/view/server-build.d.ts +2 -2
  121. package/dist/src/view/server-build.js +2 -2
  122. package/dist/src/view/server-deployment.d.ts +1 -2
  123. package/dist/src/view/server-deployment.js +6 -46
  124. package/dist/src/view/server-index-worker.d.ts +1 -1
  125. package/dist/src/view/server.d.ts +1 -1
  126. package/dist/src/view/server.js +13 -3
  127. package/dist/src/view/source-target.js +2 -2
  128. package/dist/src/view/static-build.d.ts +1 -1
  129. package/dist/src/view/static-build.js +49 -17
  130. package/dist/src/view/store.d.ts +4 -2
  131. package/dist/src/view/store.js +31 -19
  132. package/dist/src/view/table-of-contents.d.ts +1 -1
  133. package/dist/src/view/table-of-contents.js +1 -1
  134. package/dist/src/view/vercel-server-build.d.ts +1 -1
  135. package/package.json +19 -29
  136. package/templates/AGENTS.md +2 -2
  137. package/templates/cursor-rules.md +2 -2
  138. package/templates/opencode-plugin.ts +9 -9
  139. package/templates/pi-extension.ts +11 -11
  140. package/templates/skill/SKILL.md +2 -1
  141. package/dist/src/cli/check-context.d.ts +0 -56
  142. package/dist/src/cli/check-context.js +0 -215
  143. package/dist/src/cli/check-coverage.d.ts +0 -2
  144. package/dist/src/cli/check-coverage.js +0 -75
  145. package/dist/src/cli/check-frontmatter.d.ts +0 -12
  146. package/dist/src/cli/check-frontmatter.js +0 -44
  147. package/dist/src/cli/check-mode.d.ts +0 -21
  148. package/dist/src/cli/check-mode.js +0 -394
  149. package/dist/src/cli/check-status.d.ts +0 -74
  150. package/dist/src/cli/check-status.js +0 -217
  151. package/dist/src/cli/check.d.ts +0 -52
  152. package/dist/src/cli/check.js +0 -834
  153. package/dist/src/cli/context.d.ts +0 -11
  154. package/dist/src/cli/context.js +0 -58
  155. package/dist/src/cli/expand.d.ts +0 -7
  156. package/dist/src/cli/expand.js +0 -141
  157. package/dist/src/cli/external.d.ts +0 -11
  158. package/dist/src/cli/external.js +0 -185
  159. package/dist/src/cli/gen-index.d.ts +0 -45
  160. package/dist/src/cli/gen-index.js +0 -235
  161. package/dist/src/cli/link-scheme.d.ts +0 -54
  162. package/dist/src/cli/link-scheme.js +0 -101
  163. package/dist/src/cli/locate.d.ts +0 -2
  164. package/dist/src/cli/locate.js +0 -18
  165. package/dist/src/cli/refs.d.ts +0 -23
  166. package/dist/src/cli/refs.js +0 -274
  167. package/dist/src/cli/section.d.ts +0 -45
  168. package/dist/src/cli/section.js +0 -328
  169. package/dist/src/cli/select-menu.d.ts +0 -13
  170. package/dist/src/cli/select-menu.js +0 -100
  171. package/dist/src/code-refs.d.ts +0 -29
  172. package/dist/src/code-refs.js +0 -479
  173. package/dist/src/config.d.ts +0 -29
  174. package/dist/src/config.js +0 -83
  175. package/dist/src/context.d.ts +0 -26
  176. package/dist/src/context.js +0 -11
  177. package/dist/src/document-formats.d.ts +0 -7
  178. package/dist/src/document-formats.js +0 -39
  179. package/dist/src/extensions/alert-marker.d.ts +0 -25
  180. package/dist/src/extensions/alert-marker.js +0 -55
  181. package/dist/src/extensions/wiki-link/from-markdown.d.ts +0 -5
  182. package/dist/src/extensions/wiki-link/from-markdown.js +0 -34
  183. package/dist/src/extensions/wiki-link/index.d.ts +0 -4
  184. package/dist/src/extensions/wiki-link/index.js +0 -3
  185. package/dist/src/extensions/wiki-link/syntax.d.ts +0 -14
  186. package/dist/src/extensions/wiki-link/syntax.js +0 -121
  187. package/dist/src/extensions/wiki-link/to-markdown.d.ts +0 -5
  188. package/dist/src/extensions/wiki-link/to-markdown.js +0 -28
  189. package/dist/src/extensions/wiki-link/types.d.ts +0 -34
  190. package/dist/src/extensions/wiki-link/types.js +0 -1
  191. package/dist/src/external-documents.d.ts +0 -56
  192. package/dist/src/external-documents.js +0 -404
  193. package/dist/src/external-sources.d.ts +0 -136
  194. package/dist/src/external-sources.js +0 -1098
  195. package/dist/src/fork/frontmatter-fields.d.ts +0 -36
  196. package/dist/src/fork/frontmatter-fields.js +0 -23
  197. package/dist/src/format.d.ts +0 -16
  198. package/dist/src/format.js +0 -76
  199. package/dist/src/init-version.d.ts +0 -10
  200. package/dist/src/init-version.js +0 -49
  201. package/dist/src/lattice-model.d.ts +0 -84
  202. package/dist/src/lattice-model.js +0 -467
  203. package/dist/src/lattice.d.ts +0 -14
  204. package/dist/src/lattice.js +0 -353
  205. package/dist/src/markdown-analysis-cache.d.ts +0 -20
  206. package/dist/src/markdown-analysis-cache.js +0 -139
  207. package/dist/src/markdown-analysis-worker.d.ts +0 -16
  208. package/dist/src/markdown-analysis-worker.js +0 -33
  209. package/dist/src/markdown-analysis.d.ts +0 -61
  210. package/dist/src/markdown-analysis.js +0 -169
  211. package/dist/src/markdown-analyzer-loader.d.ts +0 -4
  212. package/dist/src/markdown-analyzer-loader.js +0 -13
  213. package/dist/src/markdown-validation.d.ts +0 -25
  214. package/dist/src/markdown-validation.js +0 -104
  215. package/dist/src/parser-cache.d.ts +0 -17
  216. package/dist/src/parser-cache.js +0 -65
  217. package/dist/src/parser-import.d.ts +0 -8
  218. package/dist/src/parser-import.js +0 -1
  219. package/dist/src/parser.d.ts +0 -3
  220. package/dist/src/parser.js +0 -93
  221. package/dist/src/path.d.ts +0 -2
  222. package/dist/src/path.js +0 -4
  223. package/dist/src/profiler.d.ts +0 -16
  224. package/dist/src/profiler.js +0 -81
  225. package/dist/src/project-analysis.d.ts +0 -59
  226. package/dist/src/project-analysis.js +0 -233
  227. package/dist/src/project-discovery.d.ts +0 -6
  228. package/dist/src/project-discovery.js +0 -30
  229. package/dist/src/repository-path.d.ts +0 -11
  230. package/dist/src/repository-path.js +0 -52
  231. package/dist/src/source-formats.d.ts +0 -7
  232. package/dist/src/source-formats.js +0 -30
  233. package/dist/src/source-parser.d.ts +0 -67
  234. package/dist/src/source-parser.js +0 -1749
  235. package/dist/src/untrusted.d.ts +0 -18
  236. package/dist/src/untrusted.js +0 -93
  237. package/dist/src/view/client/assets/channel-uy0fFvcW.js +0 -1
  238. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-DbSEqjA5.js +0 -1
  239. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-DbSEqjA5.js +0 -1
  240. package/dist/src/view/client/assets/index-nBmbYfun.js +0 -13
  241. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-gUj6Hzgl.js +0 -1
  242. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DR7JhrxL.js +0 -8
  243. package/dist/src/walk.d.ts +0 -20
  244. package/dist/src/walk.js +0 -121
@@ -1,6 +1,6 @@
1
- import { copyFile, mkdir, rename, rm } from 'node:fs/promises';
1
+ import { copyFile, mkdir, rename, rm, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { acquireSearchLock } from './lock.js';
3
+ import { acquireSearchLock, withSearchAccess } from './lock.js';
4
4
  import { SearchDb, hasIndex, INDEX_FILE, ensureMeta, getStoredModel, } from './db.js';
5
5
  /** Retry transient Windows handles without deleting the usable index first. */
6
6
  async function withFileRetry(work) {
@@ -11,7 +11,7 @@ async function withFileRetry(work) {
11
11
  }
12
12
  catch (error) {
13
13
  const code = error.code;
14
- // Windows may briefly retain a staging handle or a reader's copy handle.
14
+ // Windows may briefly retain a staging handle or a reader handle.
15
15
  if (process.platform !== 'win32' ||
16
16
  attempt >= 9 ||
17
17
  !['EBUSY', 'EPERM', 'EACCES'].includes(code ?? ''))
@@ -24,7 +24,7 @@ async function removeStaging(path) {
24
24
  for (const suffix of ['', '-wal', '-shm', '-journal'])
25
25
  await withFileRetry(() => rm(path + suffix, { force: true }));
26
26
  }
27
- /** Build a private staging database; readers only open snapshots of search.db. */
27
+ /** Build a private staging database; access locks protect copies and publication of search.db. */
28
28
  export async function writeIndex(latDir, cacheDir, rebuild, work) {
29
29
  const dir = cacheDir ?? join(latDir, '.cache');
30
30
  await mkdir(dir, { recursive: true });
@@ -36,8 +36,19 @@ export async function writeIndex(latDir, cacheDir, rebuild, work) {
36
36
  // A crashed writer may have left a partial database and journal behind.
37
37
  await removeStaging(path);
38
38
  const existing = hasIndex(dir);
39
- if (existing && !rebuild)
40
- await copyFile(activePath, path);
39
+ if (existing && !rebuild) {
40
+ await withSearchAccess(dir, 'exclusive', async () => {
41
+ // Recover/checkpoint a WAL left by a process that died during a query.
42
+ const source = new SearchDb(activePath);
43
+ try {
44
+ await source.checkpoint();
45
+ }
46
+ finally {
47
+ await source.close();
48
+ }
49
+ await copyFile(activePath, path);
50
+ });
51
+ }
41
52
  db = new SearchDb(path);
42
53
  const model = existing && !rebuild ? await getStoredModel(db) : null;
43
54
  await ensureMeta(db);
@@ -52,7 +63,29 @@ export async function writeIndex(latDir, cacheDir, rebuild, work) {
52
63
  await db.close();
53
64
  db = undefined;
54
65
  if (!unchanged)
55
- await withFileRetry(() => rename(path, activePath));
66
+ await withSearchAccess(dir, 'exclusive', async () => {
67
+ // Recover old sidecars before replacing their database, including after a crash.
68
+ if (existing) {
69
+ const wal = await stat(activePath + '-wal').catch((error) => {
70
+ if (error.code !== 'ENOENT')
71
+ throw error;
72
+ return undefined;
73
+ });
74
+ // A fresh rebuild must also repair an unreadable database with no WAL.
75
+ if (wal && wal.size > 0) {
76
+ const source = new SearchDb(activePath);
77
+ try {
78
+ await source.checkpoint();
79
+ }
80
+ finally {
81
+ await source.close();
82
+ }
83
+ }
84
+ for (const suffix of ['-wal', '-shm', '-journal'])
85
+ await withFileRetry(() => rm(activePath + suffix, { force: true }));
86
+ }
87
+ await withFileRetry(() => rename(path, activePath));
88
+ });
56
89
  return result;
57
90
  }
58
91
  finally {
@@ -1,7 +1,7 @@
1
1
  import type { SourceSpan } from './types.js';
2
2
  import type { Embedder } from './embedder.js';
3
- import type { MarkdownFileAnalysis } from '../markdown-analysis.js';
4
- import type { Section } from '../lattice-model.js';
3
+ import type { MarkdownFileAnalysis } from '@plebeiantech/lat.md-core/markdown-analysis';
4
+ import type { Section } from '@plebeiantech/lat.md-core/lattice-model';
5
5
  export declare const CHUNK_POLICY = "owned-blocks-v1";
6
6
  export type Passage = {
7
7
  id: string;
@@ -4,11 +4,12 @@ export declare function hasIndex(cacheDir: string): boolean;
4
4
  /** Small SQL adapter; callers never depend on a libSQL connection. */
5
5
  export declare class SearchDb {
6
6
  readonly path: string;
7
- private readonly snapshot;
7
+ private readonly published;
8
8
  private connection;
9
- constructor(path: string, snapshot?: boolean);
10
- private snapshotDir;
9
+ private release;
10
+ constructor(path: string, published?: boolean);
11
11
  private get;
12
+ private connect;
12
13
  execute(statement: string | {
13
14
  sql: string;
14
15
  args?: any[];
@@ -18,7 +19,7 @@ export declare class SearchDb {
18
19
  close(): Promise<void>;
19
20
  checkpoint(): Promise<void>;
20
21
  }
21
- export declare function openDb(latDir: string, requestedCacheDir?: string, readOnly?: boolean): SearchDb;
22
+ export declare function openDb(latDir: string, requestedCacheDir?: string, published?: boolean): SearchDb;
22
23
  export declare function ensureMeta(db: SearchDb): Promise<void>;
23
24
  export declare function getStoredModel(db: SearchDb): Promise<string | null>;
24
25
  export declare function setStoredModel(db: SearchDb, value: string): Promise<void>;
@@ -1,8 +1,7 @@
1
1
  import { connect } from '@tursodatabase/database';
2
- import { existsSync, mkdirSync, mkdtempSync, copyFileSync } from 'node:fs';
3
- import { rm } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { acquireSearchAccess } from './lock.js';
6
5
  export const INDEX_FILE = 'search.db';
7
6
  export const CREATE_PASSAGE_FTS = "CREATE INDEX IF NOT EXISTS chunks_fts ON lexical_chunks USING fts(body,heading,path) WITH (tokenizer='whitespace',weights='body=1.0,heading=2.0,path=0.5')";
8
7
  export function hasIndex(cacheDir) {
@@ -11,26 +10,31 @@ export function hasIndex(cacheDir) {
11
10
  /** Small SQL adapter; callers never depend on a libSQL connection. */
12
11
  export class SearchDb {
13
12
  path;
14
- snapshot;
13
+ published;
15
14
  connection;
16
- constructor(path, snapshot = false) {
15
+ release;
16
+ constructor(path, published = false) {
17
17
  this.path = path;
18
- this.snapshot = snapshot;
18
+ this.published = published;
19
19
  }
20
- snapshotDir;
21
20
  get() {
22
- if (this.connection)
23
- return this.connection;
24
- let path = this.path;
25
- if (this.snapshot) {
26
- this.snapshotDir = mkdtempSync(join(tmpdir(), 'lat-search-reader-'));
27
- path = join(this.snapshotDir, 'index.db');
28
- copyFileSync(this.path, path);
21
+ return (this.connection ??= this.connect());
22
+ }
23
+ async connect() {
24
+ // Turso 0.7.2 FTS needs writable connections and exclusive process access.
25
+ if (this.published)
26
+ this.release = await acquireSearchAccess(dirname(this.path), 'exclusive');
27
+ try {
28
+ return await connect(this.path, {
29
+ experimental: ['index_method'],
30
+ timeout: 10000,
31
+ });
32
+ }
33
+ catch (error) {
34
+ await this.release?.();
35
+ this.release = undefined;
36
+ throw error;
29
37
  }
30
- return (this.connection = connect(path, {
31
- experimental: ['index_method'],
32
- timeout: 10000,
33
- }));
34
38
  }
35
39
  async execute(statement) {
36
40
  const { sql, args = [] } = typeof statement === 'string' ? { sql: statement } : statement;
@@ -47,39 +51,31 @@ export class SearchDb {
47
51
  try {
48
52
  if (this.connection) {
49
53
  const db = await this.connection;
50
- await db.close();
51
- this.connection = undefined;
52
- }
53
- }
54
- finally {
55
- if (this.snapshotDir) {
56
54
  try {
57
- await rm(this.snapshotDir, {
58
- recursive: true,
59
- force: true,
60
- maxRetries: 3,
61
- retryDelay: 20,
62
- });
55
+ // Leave a self-contained file for incremental copies and publication.
56
+ if (this.published)
57
+ await this.checkpoint();
63
58
  }
64
- catch (error) {
65
- const code = error.code;
66
- if (process.platform !== 'win32' ||
67
- !['EBUSY', 'EPERM', 'ENOTEMPTY'].includes(code ?? ''))
68
- throw error;
69
- // Native handles can outlive close on Windows; this is only an owned temp copy.
59
+ finally {
60
+ await db.close();
70
61
  }
71
- this.snapshotDir = undefined;
72
62
  }
73
63
  }
64
+ finally {
65
+ this.connection = undefined;
66
+ const release = this.release;
67
+ this.release = undefined;
68
+ await release?.();
69
+ }
74
70
  }
75
71
  async checkpoint() {
76
72
  await this.execute('PRAGMA wal_checkpoint(TRUNCATE)');
77
73
  }
78
74
  }
79
- export function openDb(latDir, requestedCacheDir, readOnly = false) {
75
+ export function openDb(latDir, requestedCacheDir, published = false) {
80
76
  const cacheDir = requestedCacheDir ?? join(latDir, '.cache');
81
77
  mkdirSync(cacheDir, { recursive: true });
82
- return new SearchDb(join(cacheDir, INDEX_FILE), readOnly);
78
+ return new SearchDb(join(cacheDir, INDEX_FILE), published);
83
79
  }
84
80
  export async function ensureMeta(db) {
85
81
  await db.execute('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
@@ -1,6 +1,6 @@
1
1
  import { createEmbedder, EmbeddingAuthError, } from '@lat.md/embed';
2
2
  import minilm from '@lat.md/embed-minilm-fp16';
3
- import { getLlmKey, getRepoEmbedding } from '../config.js';
3
+ import { getLlmKey, getRepoEmbedding } from '@plebeiantech/lat.md-core/config';
4
4
  export { EmbeddingAuthError };
5
5
  const defaultCreateSearchEngine = (key) => key ? createEmbedder({ key }) : createEmbedder({ model: minilm });
6
6
  /** `meta.embedding_model` value for an embedder, e.g. `local:minilm-l6-v2:384`. */
@@ -1,5 +1,5 @@
1
1
  import { type SearchDb } from './db.js';
2
- import { type MarkdownProjectAnalysis } from '../project-analysis.js';
2
+ import { type MarkdownProjectAnalysis } from '@plebeiantech/lat.md-core/project-analysis';
3
3
  import type { Embedder } from './embedder.js';
4
4
  export declare function projectFingerprint(project: MarkdownProjectAnalysis): string;
5
5
  export type IndexStats = {
@@ -1,8 +1,8 @@
1
- import { PARSER_CACHE_VERSION } from '../parser-cache.js';
1
+ import { PARSER_CACHE_VERSION } from '@plebeiantech/lat.md-core/parser-cache';
2
2
  import { synchronizeLexical } from './lexical.js';
3
3
  import { dirname } from 'node:path';
4
4
  import { CREATE_PASSAGE_FTS } from './db.js';
5
- import { analyzeMarkdownProject, } from '../project-analysis.js';
5
+ import { analyzeMarkdownProject, } from '@plebeiantech/lat.md-core/project-analysis';
6
6
  import { chunkFile, digest, embeddingFingerprint } from './chunks.js';
7
7
  export function projectFingerprint(project) {
8
8
  return digest(`parser:${PARSER_CACHE_VERSION}\n` +
@@ -1,2 +1,5 @@
1
- /** The persistent file must never be unlinked: all writers lock the same inode. */
1
+ export type LockMode = 'shared' | 'exclusive';
2
2
  export declare function acquireSearchLock(cacheDir: string, timeoutMs?: number): Promise<() => Promise<void>>;
3
+ /** A turnstile prevents new readers bypassing a writer waiting for readers to drain. */
4
+ export declare function acquireSearchAccess(cacheDir: string, mode: LockMode, timeoutMs?: number): Promise<() => Promise<void>>;
5
+ export declare function withSearchAccess<T>(cacheDir: string, mode: LockMode, work: () => Promise<T>): Promise<T>;
@@ -1,19 +1,17 @@
1
1
  import { open } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { setTimeout } from 'node:timers/promises';
4
- /** The persistent file must never be unlinked: all writers lock the same inode. */
5
- export async function acquireSearchLock(cacheDir, timeoutMs = 120_000) {
4
+ /** Lock files stay in place so every process locks the same inode. */
5
+ async function acquireFileLock(path, mode, deadline, message) {
6
6
  const { tryLock } = await import('fs-native-extensions');
7
- const handle = await open(join(cacheDir, 'search-write.lock'), 'a+');
8
- const deadline = performance.now() + timeoutMs;
7
+ const handle = await open(path, 'a+');
9
8
  try {
10
- while (!tryLock(handle.fd)) {
9
+ while (!tryLock(handle.fd, { shared: mode === 'shared' })) {
11
10
  const remaining = deadline - performance.now();
12
11
  if (remaining <= 0)
13
- throw new Error('Search index writer is busy; retry shortly.');
14
- await setTimeout(Math.min(100, remaining));
12
+ throw new Error(message);
13
+ await setTimeout(Math.min(10, remaining));
15
14
  }
16
- // Closing releases the kernel lock, including on abnormal process exit.
17
15
  return () => handle.close();
18
16
  }
19
17
  catch (error) {
@@ -21,3 +19,27 @@ export async function acquireSearchLock(cacheDir, timeoutMs = 120_000) {
21
19
  throw error;
22
20
  }
23
21
  }
22
+ export function acquireSearchLock(cacheDir, timeoutMs = 120_000) {
23
+ return acquireFileLock(join(cacheDir, 'search-write.lock'), 'exclusive', performance.now() + timeoutMs, 'Search index writer is busy; retry shortly.');
24
+ }
25
+ /** A turnstile prevents new readers bypassing a writer waiting for readers to drain. */
26
+ export async function acquireSearchAccess(cacheDir, mode, timeoutMs = 120_000) {
27
+ const deadline = performance.now() + timeoutMs;
28
+ const message = 'Search database is busy; retry shortly.';
29
+ const releaseGate = await acquireFileLock(join(cacheDir, 'search-access-gate.lock'), 'exclusive', deadline, message);
30
+ try {
31
+ return await acquireFileLock(join(cacheDir, 'search-access.lock'), mode, deadline, message);
32
+ }
33
+ finally {
34
+ await releaseGate();
35
+ }
36
+ }
37
+ export async function withSearchAccess(cacheDir, mode, work) {
38
+ const release = await acquireSearchAccess(cacheDir, mode);
39
+ try {
40
+ return await work();
41
+ }
42
+ finally {
43
+ await release();
44
+ }
45
+ }
@@ -1,4 +1,5 @@
1
- import type { Section, SectionMatch } from '../lattice-model.js';
1
+ import type { Section, SectionMatch } from '@plebeiantech/lat.md-core/lattice-model';
2
+ import { type SearchDb } from './db.js';
2
3
  import { type CreateSearchEngine } from './embedder.js';
3
4
  import { type SearchResult } from './search.js';
4
5
  export type IndexedSearchResult = {
@@ -11,7 +12,7 @@ export type IndexedSearchSession = {
11
12
  };
12
13
  /** Resolve scored index rows against one already-analyzed project snapshot. */
13
14
  export declare function resolveSearchMatches(results: readonly SearchResult[], sectionById: ReadonlyMap<string, Section>): SectionMatch[];
14
- /** Open one reusable database and embedder for a sequence of index queries. */
15
+ /** Reuse an embedder with scoped database access for a sequence of index queries. */
15
16
  export declare function openIndexedSearchSession(latDir: string, options?: {
16
17
  cacheDir?: string;
17
18
  createSearchEngine?: CreateSearchEngine;
@@ -21,3 +22,5 @@ export declare function searchIndexedSections(latDir: string, query: string, lim
21
22
  cacheDir?: string;
22
23
  minSimilarity?: number;
23
24
  }): Promise<IndexedSearchResult>;
25
+ export declare function indexMetadata(db: SearchDb): Promise<Map<string, string>>;
26
+ export declare function sameIndexMetadata(a: Map<string, string>, b: Map<string, string>): boolean;
@@ -3,9 +3,9 @@ import { ReindexRequiredError } from './embedder.js';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { hasIndex } from './db.js';
6
- import { closeDb, getStoredModel, openDb } from './db.js';
6
+ import { closeDb, openDb } from './db.js';
7
7
  import { embedderForIndex } from './embedder.js';
8
- import { searchSections } from './search.js';
8
+ import { searchSections, prepareSearchQuery, } from './search.js';
9
9
  /** Resolve scored index rows against one already-analyzed project snapshot. */
10
10
  export function resolveSearchMatches(results, sectionById) {
11
11
  return results.flatMap((result) => {
@@ -27,48 +27,56 @@ export function resolveSearchMatches(results, sectionById) {
27
27
  : [];
28
28
  });
29
29
  }
30
- /** Open one reusable database and embedder for a sequence of index queries. */
30
+ /** Reuse an embedder with scoped database access for a sequence of index queries. */
31
31
  export async function openIndexedSearchSession(latDir, options = {}) {
32
32
  const cacheDir = options.cacheDir ?? join(latDir, '.cache');
33
33
  if (!existsSync(cacheDir) || !hasIndex(cacheDir))
34
34
  return { search: async () => [], close: async () => { } };
35
- const db = openDb(latDir, options.cacheDir, true);
36
- let closed = false;
37
- const close = async () => {
38
- if (closed)
39
- return;
40
- closed = true;
41
- await closeDb(db);
35
+ const readMetadata = async () => {
36
+ const db = openDb(latDir, options.cacheDir, true);
37
+ try {
38
+ return await indexMetadata(db);
39
+ }
40
+ finally {
41
+ await closeDb(db);
42
+ }
42
43
  };
43
- try {
44
- const stored = await getStoredModel(db);
45
- if (stored === null) {
46
- return {
47
- search: async () => {
48
- if (closed)
49
- throw new Error('Search session is closed');
44
+ const initial = await readMetadata();
45
+ const stored = initial.get('embedding_model') ?? null;
46
+ if (stored && initial.get('lexical_version') !== LEXICAL_VERSION)
47
+ throw new ReindexRequiredError('Search lexical index changed; run lat search to update it.');
48
+ const embedder = stored
49
+ ? await embedderForIndex(stored, latDir, options.createSearchEngine)
50
+ : null;
51
+ let closed = false;
52
+ const active = new Set();
53
+ return {
54
+ search(query, limit, minSimilarity) {
55
+ if (closed)
56
+ return Promise.reject(new Error('Search session is closed'));
57
+ const operation = (async () => {
58
+ if (!embedder)
50
59
  return [];
51
- },
52
- close,
53
- };
54
- }
55
- const lexicalVersion = (await db.execute("SELECT value FROM meta WHERE key='lexical_version'")).rows[0]?.value;
56
- if (lexicalVersion !== LEXICAL_VERSION)
57
- throw new ReindexRequiredError('Search lexical index changed; run lat search to update it.');
58
- const embedder = await embedderForIndex(stored, latDir, options.createSearchEngine);
59
- return {
60
- async search(query, limit, minSimilarity) {
61
- if (closed)
62
- throw new Error('Search session is closed');
63
- return searchSections(db, query, embedder, limit, minSimilarity);
64
- },
65
- close,
66
- };
67
- }
68
- catch (error) {
69
- await close();
70
- throw error;
71
- }
60
+ const vector = await prepareSearchQuery(query, embedder, limit, minSimilarity);
61
+ const db = openDb(latDir, options.cacheDir, true);
62
+ try {
63
+ if (!sameIndexMetadata(initial, await indexMetadata(db)))
64
+ throw new ReindexRequiredError('Search index changed; reopen the search session to refresh its documents and model.');
65
+ return await searchSections(db, query, embedder, limit, minSimilarity, vector);
66
+ }
67
+ finally {
68
+ await closeDb(db);
69
+ }
70
+ })();
71
+ active.add(operation);
72
+ void operation.finally(() => active.delete(operation)).catch(() => { });
73
+ return operation;
74
+ },
75
+ async close() {
76
+ closed = true;
77
+ await Promise.allSettled([...active]);
78
+ },
79
+ };
72
80
  }
73
81
  /** Query a finished index and resolve its ids from precomputed section data. */
74
82
  export async function searchIndexedSections(latDir, query, limit, sectionById, options = {}) {
@@ -84,3 +92,12 @@ export async function searchIndexedSections(latDir, query, limit, sectionById, o
84
92
  await session.close();
85
93
  }
86
94
  }
95
+ export async function indexMetadata(db) {
96
+ return new Map((await db.execute('SELECT key,value FROM meta')).rows.map((row) => [
97
+ row.key,
98
+ row.value,
99
+ ]));
100
+ }
101
+ export function sameIndexMetadata(a, b) {
102
+ return (a.size === b.size && [...a].every(([key, value]) => b.get(key) === value));
103
+ }
@@ -14,4 +14,6 @@ export declare function collapse(candidates: Candidate[]): Map<string, Candidate
14
14
  rank: number;
15
15
  }>;
16
16
  export declare function literalFtsQuery(query: string): string;
17
- export declare function searchSections(db: SearchDb, query: string, embedder: Embedder, limit?: number, minSimilarity?: number): Promise<SearchResult[]>;
17
+ export declare function searchSections(db: SearchDb, query: string, embedder: Embedder, limit?: number, minSimilarity?: number, preparedVector?: number[]): Promise<SearchResult[]>;
18
+ /** Prepare embeddings before acquiring a database access lock. */
19
+ export declare function prepareSearchQuery(query: string, embedder: Embedder, limit?: number, minSimilarity?: number): Promise<number[]>;
@@ -24,21 +24,12 @@ export function literalFtsQuery(query) {
24
24
  .map((t) => `"${t.replaceAll('"', '\\"')}"`)
25
25
  .join(' OR ');
26
26
  }
27
- export async function searchSections(db, query, embedder, limit = DEFAULT_SEARCH_LIMIT, minSimilarity = DEFAULT_MIN_SIMILARITY) {
27
+ export async function searchSections(db, query, embedder, limit = DEFAULT_SEARCH_LIMIT, minSimilarity = DEFAULT_MIN_SIMILARITY, preparedVector) {
28
28
  query = query.trim();
29
29
  if (!query)
30
30
  return [];
31
- if (!Number.isInteger(limit) || limit < 1)
32
- throw new Error('limit must be a positive integer');
33
- if (!Number.isFinite(minSimilarity) || minSimilarity < 0 || minSimilarity > 1)
34
- throw new Error('min-similarity must be a number from 0 to 1');
35
- if (embedder.countTokens(query) > embedder.maxInputTokens)
36
- throw new Error('Search query exceeds the embedding model token limit; shorten the query.');
37
- const [vector] = await embedder.embed([query]);
38
- if (!vector ||
39
- vector.length !== embedder.dimensions ||
40
- vector.some((n) => !Number.isFinite(n)))
41
- throw new Error('Embedding backend returned an invalid query vector');
31
+ const vector = preparedVector ??
32
+ (await prepareSearchQuery(query, embedder, limit, minSimilarity));
42
33
  const vectorJson = JSON.stringify(vector), fts = literalFtsQuery(query);
43
34
  const target = Math.max(50, limit), cap = 10 * target;
44
35
  const owners = new Map((await db.execute('SELECT id,section_id FROM chunks')).rows.map((r) => [
@@ -155,3 +146,21 @@ export async function searchSections(db, query, embedder, limit = DEFAULT_SEARCH
155
146
  }
156
147
  return results;
157
148
  }
149
+ /** Prepare embeddings before acquiring a database access lock. */
150
+ export async function prepareSearchQuery(query, embedder, limit = DEFAULT_SEARCH_LIMIT, minSimilarity = DEFAULT_MIN_SIMILARITY) {
151
+ query = query.trim();
152
+ if (!query)
153
+ return [];
154
+ if (!Number.isInteger(limit) || limit < 1)
155
+ throw new Error('limit must be a positive integer');
156
+ if (!Number.isFinite(minSimilarity) || minSimilarity < 0 || minSimilarity > 1)
157
+ throw new Error('min-similarity must be a number from 0 to 1');
158
+ if (embedder.countTokens(query) > embedder.maxInputTokens)
159
+ throw new Error('Search query exceeds the embedding model token limit; shorten the query.');
160
+ const [vector] = await embedder.embed([query]);
161
+ if (!vector ||
162
+ vector.length !== embedder.dimensions ||
163
+ vector.some((n) => !Number.isFinite(n)))
164
+ throw new Error('Embedding backend returned an invalid query vector');
165
+ return vector;
166
+ }
@@ -1,21 +1,5 @@
1
- export type SourceSpan = {
2
- start: number;
3
- end: number;
4
- startLine: number;
5
- endLine: number;
6
- };
7
- export type SearchEvidence = {
8
- chunkId: string;
9
- text: string;
10
- spans: SourceSpan[];
11
- channel: 'lexical' | 'semantic';
12
- };
13
- export type SearchDiagnostics = {
14
- lexicalCapped: boolean;
15
- semanticCapped: boolean;
16
- lexicalCandidates: number;
17
- semanticCandidates: number;
18
- };
1
+ import type { SearchEvidence, SearchDiagnostics } from '@plebeiantech/lat.md-core/search-metadata';
2
+ export type { SourceSpan, SearchEvidence, SearchDiagnostics, } from '@plebeiantech/lat.md-core/search-metadata';
19
3
  export type SearchResult = {
20
4
  id: string;
21
5
  file: string;