ollama-intern-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/SECURITY.md +22 -0
  4. package/dist/corpus/chunker.d.ts +63 -0
  5. package/dist/corpus/chunker.d.ts.map +1 -0
  6. package/dist/corpus/chunker.js +230 -0
  7. package/dist/corpus/chunker.js.map +1 -0
  8. package/dist/corpus/fusion.d.ts +67 -0
  9. package/dist/corpus/fusion.d.ts.map +1 -0
  10. package/dist/corpus/fusion.js +76 -0
  11. package/dist/corpus/fusion.js.map +1 -0
  12. package/dist/corpus/indexer.d.ts +33 -0
  13. package/dist/corpus/indexer.d.ts.map +1 -0
  14. package/dist/corpus/indexer.js +201 -0
  15. package/dist/corpus/indexer.js.map +1 -0
  16. package/dist/corpus/lexical.d.ts +95 -0
  17. package/dist/corpus/lexical.d.ts.map +1 -0
  18. package/dist/corpus/lexical.js +227 -0
  19. package/dist/corpus/lexical.js.map +1 -0
  20. package/dist/corpus/manifest.d.ts +29 -0
  21. package/dist/corpus/manifest.d.ts.map +1 -0
  22. package/dist/corpus/manifest.js +45 -0
  23. package/dist/corpus/manifest.js.map +1 -0
  24. package/dist/corpus/refresh.d.ts +54 -0
  25. package/dist/corpus/refresh.d.ts.map +1 -0
  26. package/dist/corpus/refresh.js +155 -0
  27. package/dist/corpus/refresh.js.map +1 -0
  28. package/dist/corpus/searcher.d.ts +48 -0
  29. package/dist/corpus/searcher.d.ts.map +1 -0
  30. package/dist/corpus/searcher.js +131 -0
  31. package/dist/corpus/searcher.js.map +1 -0
  32. package/dist/corpus/storage.d.ts +56 -0
  33. package/dist/corpus/storage.d.ts.map +1 -0
  34. package/dist/corpus/storage.js +81 -0
  35. package/dist/corpus/storage.js.map +1 -0
  36. package/dist/coverage.d.ts +38 -0
  37. package/dist/coverage.d.ts.map +1 -0
  38. package/dist/coverage.js +117 -0
  39. package/dist/coverage.js.map +1 -0
  40. package/dist/embedMath.d.ts +22 -0
  41. package/dist/embedMath.d.ts.map +1 -0
  42. package/dist/embedMath.js +34 -0
  43. package/dist/embedMath.js.map +1 -0
  44. package/dist/envelope.d.ts +53 -0
  45. package/dist/envelope.d.ts.map +1 -0
  46. package/dist/envelope.js +32 -0
  47. package/dist/envelope.js.map +1 -0
  48. package/dist/errors.d.ts +20 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +30 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/evals/retrieval.d.ts +68 -0
  53. package/dist/evals/retrieval.d.ts.map +1 -0
  54. package/dist/evals/retrieval.js +173 -0
  55. package/dist/evals/retrieval.js.map +1 -0
  56. package/dist/guardrails/citations.d.ts +28 -0
  57. package/dist/guardrails/citations.d.ts.map +1 -0
  58. package/dist/guardrails/citations.js +45 -0
  59. package/dist/guardrails/citations.js.map +1 -0
  60. package/dist/guardrails/compileCheck.d.ts +20 -0
  61. package/dist/guardrails/compileCheck.d.ts.map +1 -0
  62. package/dist/guardrails/compileCheck.js +96 -0
  63. package/dist/guardrails/compileCheck.js.map +1 -0
  64. package/dist/guardrails/confidence.d.ts +21 -0
  65. package/dist/guardrails/confidence.d.ts.map +1 -0
  66. package/dist/guardrails/confidence.js +18 -0
  67. package/dist/guardrails/confidence.js.map +1 -0
  68. package/dist/guardrails/timeouts.d.ts +35 -0
  69. package/dist/guardrails/timeouts.d.ts.map +1 -0
  70. package/dist/guardrails/timeouts.js +58 -0
  71. package/dist/guardrails/timeouts.js.map +1 -0
  72. package/dist/guardrails/writeConfirm.d.ts +25 -0
  73. package/dist/guardrails/writeConfirm.d.ts.map +1 -0
  74. package/dist/guardrails/writeConfirm.js +35 -0
  75. package/dist/guardrails/writeConfirm.js.map +1 -0
  76. package/dist/index.d.ts +12 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +153 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/observability.d.ts +66 -0
  81. package/dist/observability.d.ts.map +1 -0
  82. package/dist/observability.js +51 -0
  83. package/dist/observability.js.map +1 -0
  84. package/dist/ollama.d.ts +105 -0
  85. package/dist/ollama.d.ts.map +1 -0
  86. package/dist/ollama.js +109 -0
  87. package/dist/ollama.js.map +1 -0
  88. package/dist/prewarm.d.ts +28 -0
  89. package/dist/prewarm.d.ts.map +1 -0
  90. package/dist/prewarm.js +69 -0
  91. package/dist/prewarm.js.map +1 -0
  92. package/dist/profiles.d.ts +54 -0
  93. package/dist/profiles.d.ts.map +1 -0
  94. package/dist/profiles.js +95 -0
  95. package/dist/profiles.js.map +1 -0
  96. package/dist/protectedPaths.d.ts +33 -0
  97. package/dist/protectedPaths.d.ts.map +1 -0
  98. package/dist/protectedPaths.js +54 -0
  99. package/dist/protectedPaths.js.map +1 -0
  100. package/dist/runContext.d.ts +21 -0
  101. package/dist/runContext.d.ts.map +1 -0
  102. package/dist/runContext.js +9 -0
  103. package/dist/runContext.js.map +1 -0
  104. package/dist/semaphore.d.ts +16 -0
  105. package/dist/semaphore.d.ts.map +1 -0
  106. package/dist/semaphore.js +37 -0
  107. package/dist/semaphore.js.map +1 -0
  108. package/dist/sources.d.ts +19 -0
  109. package/dist/sources.d.ts.map +1 -0
  110. package/dist/sources.js +41 -0
  111. package/dist/sources.js.map +1 -0
  112. package/dist/tiers.d.ts +39 -0
  113. package/dist/tiers.d.ts.map +1 -0
  114. package/dist/tiers.js +44 -0
  115. package/dist/tiers.js.map +1 -0
  116. package/dist/tools/artifactDiff.d.ts +66 -0
  117. package/dist/tools/artifactDiff.d.ts.map +1 -0
  118. package/dist/tools/artifactDiff.js +66 -0
  119. package/dist/tools/artifactDiff.js.map +1 -0
  120. package/dist/tools/artifactExportToPath.d.ts +38 -0
  121. package/dist/tools/artifactExportToPath.d.ts.map +1 -0
  122. package/dist/tools/artifactExportToPath.js +51 -0
  123. package/dist/tools/artifactExportToPath.js.map +1 -0
  124. package/dist/tools/artifactList.d.ts +60 -0
  125. package/dist/tools/artifactList.d.ts.map +1 -0
  126. package/dist/tools/artifactList.js +107 -0
  127. package/dist/tools/artifactList.js.map +1 -0
  128. package/dist/tools/artifactRead.d.ts +43 -0
  129. package/dist/tools/artifactRead.d.ts.map +1 -0
  130. package/dist/tools/artifactRead.js +83 -0
  131. package/dist/tools/artifactRead.js.map +1 -0
  132. package/dist/tools/artifactSnippets.d.ts +56 -0
  133. package/dist/tools/artifactSnippets.d.ts.map +1 -0
  134. package/dist/tools/artifactSnippets.js +97 -0
  135. package/dist/tools/artifactSnippets.js.map +1 -0
  136. package/dist/tools/artifacts/diff.d.ts +161 -0
  137. package/dist/tools/artifacts/diff.d.ts.map +1 -0
  138. package/dist/tools/artifacts/diff.js +267 -0
  139. package/dist/tools/artifacts/diff.js.map +1 -0
  140. package/dist/tools/artifacts/export.d.ts +43 -0
  141. package/dist/tools/artifacts/export.d.ts.map +1 -0
  142. package/dist/tools/artifacts/export.js +113 -0
  143. package/dist/tools/artifacts/export.js.map +1 -0
  144. package/dist/tools/artifacts/scan.d.ts +86 -0
  145. package/dist/tools/artifacts/scan.d.ts.map +1 -0
  146. package/dist/tools/artifacts/scan.js +251 -0
  147. package/dist/tools/artifacts/scan.js.map +1 -0
  148. package/dist/tools/artifacts/snippets.d.ts +42 -0
  149. package/dist/tools/artifacts/snippets.d.ts.map +1 -0
  150. package/dist/tools/artifacts/snippets.js +165 -0
  151. package/dist/tools/artifacts/snippets.js.map +1 -0
  152. package/dist/tools/batch.d.ts +65 -0
  153. package/dist/tools/batch.d.ts.map +1 -0
  154. package/dist/tools/batch.js +117 -0
  155. package/dist/tools/batch.js.map +1 -0
  156. package/dist/tools/briefs/common.d.ts +65 -0
  157. package/dist/tools/briefs/common.d.ts.map +1 -0
  158. package/dist/tools/briefs/common.js +128 -0
  159. package/dist/tools/briefs/common.js.map +1 -0
  160. package/dist/tools/briefs/evidence.d.ts +40 -0
  161. package/dist/tools/briefs/evidence.d.ts.map +1 -0
  162. package/dist/tools/briefs/evidence.js +103 -0
  163. package/dist/tools/briefs/evidence.js.map +1 -0
  164. package/dist/tools/changeBrief.d.ts +85 -0
  165. package/dist/tools/changeBrief.d.ts.map +1 -0
  166. package/dist/tools/changeBrief.js +228 -0
  167. package/dist/tools/changeBrief.js.map +1 -0
  168. package/dist/tools/chat.d.ts +42 -0
  169. package/dist/tools/chat.d.ts.map +1 -0
  170. package/dist/tools/chat.js +49 -0
  171. package/dist/tools/chat.js.map +1 -0
  172. package/dist/tools/classify.d.ts +56 -0
  173. package/dist/tools/classify.d.ts.map +1 -0
  174. package/dist/tools/classify.js +103 -0
  175. package/dist/tools/classify.js.map +1 -0
  176. package/dist/tools/corpusAnswer.d.ts +79 -0
  177. package/dist/tools/corpusAnswer.d.ts.map +1 -0
  178. package/dist/tools/corpusAnswer.js +259 -0
  179. package/dist/tools/corpusAnswer.js.map +1 -0
  180. package/dist/tools/corpusIndex.d.ts +30 -0
  181. package/dist/tools/corpusIndex.d.ts.map +1 -0
  182. package/dist/tools/corpusIndex.js +67 -0
  183. package/dist/tools/corpusIndex.js.map +1 -0
  184. package/dist/tools/corpusList.d.ts +19 -0
  185. package/dist/tools/corpusList.d.ts.map +1 -0
  186. package/dist/tools/corpusList.js +34 -0
  187. package/dist/tools/corpusList.js.map +1 -0
  188. package/dist/tools/corpusRefresh.d.ts +29 -0
  189. package/dist/tools/corpusRefresh.d.ts.map +1 -0
  190. package/dist/tools/corpusRefresh.js +58 -0
  191. package/dist/tools/corpusRefresh.js.map +1 -0
  192. package/dist/tools/corpusSearch.d.ts +44 -0
  193. package/dist/tools/corpusSearch.d.ts.map +1 -0
  194. package/dist/tools/corpusSearch.js +80 -0
  195. package/dist/tools/corpusSearch.js.map +1 -0
  196. package/dist/tools/draft.d.ts +38 -0
  197. package/dist/tools/draft.d.ts.map +1 -0
  198. package/dist/tools/draft.js +72 -0
  199. package/dist/tools/draft.js.map +1 -0
  200. package/dist/tools/embed.d.ts +27 -0
  201. package/dist/tools/embed.d.ts.map +1 -0
  202. package/dist/tools/embed.js +41 -0
  203. package/dist/tools/embed.js.map +1 -0
  204. package/dist/tools/embedSearch.d.ts +58 -0
  205. package/dist/tools/embedSearch.d.ts.map +1 -0
  206. package/dist/tools/embedSearch.js +76 -0
  207. package/dist/tools/embedSearch.js.map +1 -0
  208. package/dist/tools/extract.d.ts +58 -0
  209. package/dist/tools/extract.d.ts.map +1 -0
  210. package/dist/tools/extract.js +93 -0
  211. package/dist/tools/extract.js.map +1 -0
  212. package/dist/tools/incidentBrief.d.ts +81 -0
  213. package/dist/tools/incidentBrief.d.ts.map +1 -0
  214. package/dist/tools/incidentBrief.js +252 -0
  215. package/dist/tools/incidentBrief.js.map +1 -0
  216. package/dist/tools/packs/changePack.d.ts +166 -0
  217. package/dist/tools/packs/changePack.d.ts.map +1 -0
  218. package/dist/tools/packs/changePack.js +470 -0
  219. package/dist/tools/packs/changePack.js.map +1 -0
  220. package/dist/tools/packs/incidentPack.d.ts +125 -0
  221. package/dist/tools/packs/incidentPack.d.ts.map +1 -0
  222. package/dist/tools/packs/incidentPack.js +354 -0
  223. package/dist/tools/packs/incidentPack.js.map +1 -0
  224. package/dist/tools/packs/repoPack.d.ts +207 -0
  225. package/dist/tools/packs/repoPack.d.ts.map +1 -0
  226. package/dist/tools/packs/repoPack.js +456 -0
  227. package/dist/tools/packs/repoPack.js.map +1 -0
  228. package/dist/tools/repoBrief.d.ts +81 -0
  229. package/dist/tools/repoBrief.d.ts.map +1 -0
  230. package/dist/tools/repoBrief.js +213 -0
  231. package/dist/tools/repoBrief.js.map +1 -0
  232. package/dist/tools/research.d.ts +45 -0
  233. package/dist/tools/research.d.ts.map +1 -0
  234. package/dist/tools/research.js +100 -0
  235. package/dist/tools/research.js.map +1 -0
  236. package/dist/tools/runner.d.ts +24 -0
  237. package/dist/tools/runner.d.ts.map +1 -0
  238. package/dist/tools/runner.js +47 -0
  239. package/dist/tools/runner.js.map +1 -0
  240. package/dist/tools/summarizeDeep.d.ts +49 -0
  241. package/dist/tools/summarizeDeep.d.ts.map +1 -0
  242. package/dist/tools/summarizeDeep.js +109 -0
  243. package/dist/tools/summarizeDeep.js.map +1 -0
  244. package/dist/tools/summarizeFast.d.ts +28 -0
  245. package/dist/tools/summarizeFast.d.ts.map +1 -0
  246. package/dist/tools/summarizeFast.js +43 -0
  247. package/dist/tools/summarizeFast.js.map +1 -0
  248. package/dist/tools/triageLogs.d.ts +51 -0
  249. package/dist/tools/triageLogs.d.ts.map +1 -0
  250. package/dist/tools/triageLogs.js +96 -0
  251. package/dist/tools/triageLogs.js.map +1 -0
  252. package/dist/version.d.ts +2 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +2 -0
  255. package/dist/version.js.map +1 -0
  256. package/package.json +60 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fusion.d.ts","sourceRoot":"","sources":["../../src/corpus/fusion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,MAAM;IACrB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,GAAE,MAAW,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQhF;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAErE;AAED,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sEAAsE;IACtE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mEAAmE;IACnE,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AASD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,cAAc,EAAE,EACxB,GAAG,EAAE,gBAAgB,EACrB,IAAI,GAAE,gBAAqB,GAC1B,cAAc,EAAE,CAelB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Score fusion primitives — pure, deterministic, no I/O.
3
+ *
4
+ * RRF (Reciprocal Rank Fusion) combines multiple ranked lists into one.
5
+ * It is the standard fusion strategy in modern hybrid retrieval because
6
+ * it needs no score calibration — only ranks — so BM25 and cosine can
7
+ * be safely combined without learning a joint normalization.
8
+ *
9
+ * `applyFactBoost` is the "fact mode" reranker: a dominant multiplier
10
+ * for exact-substring matches plus a secondary multiplier for short
11
+ * chunks. Boost, never filter — chunks without a match keep their
12
+ * fused score and stay in the result so a near-miss query never
13
+ * collapses to empty.
14
+ */
15
+ /**
16
+ * Reciprocal Rank Fusion. Score for doc d =
17
+ * sum over input lists i: weight_i / (K + rank_i(d))
18
+ * where K is a smoothing constant (default 60 — the canonical value from
19
+ * Cormack et al. 2009). A doc that appears in only one list is still
20
+ * scored; a doc absent from every list never appears in the output.
21
+ */
22
+ export function rrfFuse(lists, k = 60) {
23
+ const scores = new Map();
24
+ for (const { ranked, weight = 1 } of lists) {
25
+ for (const r of ranked) {
26
+ scores.set(r.chunkId, (scores.get(r.chunkId) ?? 0) + weight / (k + r.rank));
27
+ }
28
+ }
29
+ return scores;
30
+ }
31
+ /**
32
+ * Convert a score-sorted list into a rank list. Ties are broken by the
33
+ * order the scores arrive — callers are responsible for passing in a
34
+ * deterministically sorted list. Ranks are 1-indexed as RRF expects.
35
+ */
36
+ export function toRanked(scored) {
37
+ return scored.map((s, i) => ({ chunkId: s.chunkId, rank: i + 1 }));
38
+ }
39
+ const DEFAULT_FACT_BOOST = {
40
+ exactSubstringMultiplier: 2.5,
41
+ shortChunkMaxMultiplier: 1.15,
42
+ shortChunkFloorChars: 200,
43
+ shortChunkCeilingChars: 1600,
44
+ };
45
+ /**
46
+ * Apply the fact-mode reranker. Returns a new list; input is not mutated.
47
+ * Dominant boost = exact substring match (multiplier ≈ 2.5x by default).
48
+ * Secondary boost = short-chunk preference (up to ≈ 1.15x, decays to 1.0).
49
+ * Non-matching chunks keep their fused score unchanged and stay in the list.
50
+ */
51
+ export function applyFactBoost(scored, ctx, opts = {}) {
52
+ const cfg = { ...DEFAULT_FACT_BOOST, ...opts };
53
+ const q = ctx.query.toLowerCase();
54
+ const hasQuery = q.length > 0;
55
+ const boosted = [];
56
+ for (const s of scored) {
57
+ const text = ctx.chunkText.get(s.chunkId) ?? "";
58
+ let mult = 1;
59
+ if (hasQuery && text.toLowerCase().includes(q)) {
60
+ mult *= cfg.exactSubstringMultiplier;
61
+ }
62
+ mult *= shortChunkMultiplier(text.length, cfg);
63
+ boosted.push({ chunkId: s.chunkId, score: s.score * mult });
64
+ }
65
+ return boosted;
66
+ }
67
+ function shortChunkMultiplier(len, cfg) {
68
+ if (len <= cfg.shortChunkFloorChars)
69
+ return cfg.shortChunkMaxMultiplier;
70
+ if (len >= cfg.shortChunkCeilingChars)
71
+ return 1.0;
72
+ const range = cfg.shortChunkCeilingChars - cfg.shortChunkFloorChars;
73
+ const delta = cfg.shortChunkMaxMultiplier - 1.0;
74
+ return cfg.shortChunkMaxMultiplier - (delta * (len - cfg.shortChunkFloorChars) / range);
75
+ }
76
+ //# sourceMappingURL=fusion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fusion.js","sourceRoot":"","sources":["../../src/corpus/fusion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,KAAmB,EAAE,IAAY,EAAE;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,IAAI,KAAK,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAkC;IACzD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AACrE,CAAC;AAaD,MAAM,kBAAkB,GAA+B;IACrD,wBAAwB,EAAE,GAAG;IAC7B,uBAAuB,EAAE,IAAI;IAC7B,oBAAoB,EAAE,GAAG;IACzB,sBAAsB,EAAE,IAAI;CAC7B,CAAC;AAYF;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAwB,EACxB,GAAqB,EACrB,OAAyB,EAAE;IAE3B,MAAM,GAAG,GAAG,EAAE,GAAG,kBAAkB,EAAE,GAAG,IAAI,EAAE,CAAC;IAC/C,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAChD,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,IAAI,IAAI,GAAG,CAAC,wBAAwB,CAAC;QACvC,CAAC;QACD,IAAI,IAAI,oBAAoB,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,oBAAoB,CAC3B,GAAW,EACX,GAA+B;IAE/B,IAAI,GAAG,IAAI,GAAG,CAAC,oBAAoB;QAAE,OAAO,GAAG,CAAC,uBAAuB,CAAC;IACxE,IAAI,GAAG,IAAI,GAAG,CAAC,sBAAsB;QAAE,OAAO,GAAG,CAAC;IAClD,MAAM,KAAK,GAAG,GAAG,CAAC,sBAAsB,GAAG,GAAG,CAAC,oBAAoB,CAAC;IACpE,MAAM,KAAK,GAAG,GAAG,CAAC,uBAAuB,GAAG,GAAG,CAAC;IAChD,OAAO,GAAG,CAAC,uBAAuB,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,oBAAoB,CAAC,GAAG,KAAK,CAAC,CAAC;AAC1F,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Corpus indexer — read files, chunk, embed in batches, persist.
3
+ *
4
+ * Idempotent by file hash: if a path's sha256 matches what's already in
5
+ * the corpus under the same model_version, chunks for that path are
6
+ * reused verbatim (no re-embedding). Files no longer in the input set
7
+ * are dropped. New or changed files are embedded fresh.
8
+ *
9
+ * This means `index` can be called repeatedly in daily use without
10
+ * burning the embed tier on unchanged content.
11
+ */
12
+ import type { OllamaClient } from "../ollama.js";
13
+ export interface IndexParams {
14
+ name: string;
15
+ paths: string[];
16
+ model: string;
17
+ chunk_chars?: number;
18
+ chunk_overlap?: number;
19
+ client: OllamaClient;
20
+ }
21
+ export interface IndexReport {
22
+ name: string;
23
+ model_version: string;
24
+ documents: number;
25
+ chunks: number;
26
+ total_chars: number;
27
+ reused_chunks: number;
28
+ newly_embedded_chunks: number;
29
+ dropped_files: string[];
30
+ elapsed_ms: number;
31
+ }
32
+ export declare function indexCorpus(params: IndexParams): Promise<IndexReport>;
33
+ //# sourceMappingURL=indexer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../../src/corpus/indexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAQjD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AASD,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAoM3E"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Corpus indexer — read files, chunk, embed in batches, persist.
3
+ *
4
+ * Idempotent by file hash: if a path's sha256 matches what's already in
5
+ * the corpus under the same model_version, chunks for that path are
6
+ * reused verbatim (no re-embedding). Files no longer in the input set
7
+ * are dropped. New or changed files are embedded fresh.
8
+ *
9
+ * This means `index` can be called repeatedly in daily use without
10
+ * burning the embed tier on unchanged content.
11
+ */
12
+ import { readFile, stat } from "node:fs/promises";
13
+ import { resolve } from "node:path";
14
+ import { createHash } from "node:crypto";
15
+ import { chunkDocument, DEFAULT_CHUNK } from "./chunker.js";
16
+ import { CORPUS_SCHEMA_VERSION, loadCorpus, saveCorpus } from "./storage.js";
17
+ import { MANIFEST_SCHEMA_VERSION, loadManifest, saveManifest } from "./manifest.js";
18
+ import { InternError } from "../errors.js";
19
+ const EMBED_BATCH = 64;
20
+ async function sha256File(path) {
21
+ const content = await readFile(path, "utf8");
22
+ const hash = "sha256:" + createHash("sha256").update(content).digest("hex");
23
+ const st = await stat(path);
24
+ return { hash, mtime: st.mtime.toISOString(), content };
25
+ }
26
+ export async function indexCorpus(params) {
27
+ const t0 = Date.now();
28
+ const opts = {
29
+ chunk_chars: params.chunk_chars ?? DEFAULT_CHUNK.chunk_chars,
30
+ chunk_overlap: params.chunk_overlap ?? DEFAULT_CHUNK.chunk_overlap,
31
+ };
32
+ // Load existing corpus (if any) to reuse unchanged chunks.
33
+ // An out-of-date schema version throws SCHEMA_INVALID from loadCorpus —
34
+ // treat that as "no existing corpus" so this very call can rewrite the
35
+ // file fresh under the current schema (which is the whole point of
36
+ // re-indexing after a bump).
37
+ let existing = null;
38
+ try {
39
+ existing = await loadCorpus(params.name);
40
+ }
41
+ catch (err) {
42
+ if (err instanceof InternError && err.code === "SCHEMA_INVALID") {
43
+ existing = null;
44
+ }
45
+ else {
46
+ throw err;
47
+ }
48
+ }
49
+ const reusable = new Map();
50
+ if (existing && existing.model_version === params.model) {
51
+ for (const c of existing.chunks) {
52
+ const key = `${c.path}::${c.file_hash}`;
53
+ const arr = reusable.get(key) ?? [];
54
+ arr.push(c);
55
+ reusable.set(key, arr);
56
+ }
57
+ }
58
+ const allChunks = [];
59
+ const titles = {};
60
+ let reusedCount = 0;
61
+ let newlyEmbeddedCount = 0;
62
+ let totalChars = 0;
63
+ const seenPaths = new Set();
64
+ // Preserve titles from reusable files (they were captured at previous index).
65
+ if (existing) {
66
+ for (const [p, t] of Object.entries(existing.titles ?? {})) {
67
+ titles[p] = t;
68
+ }
69
+ }
70
+ // Pass 1: read + hash every input, reuse where possible, collect chunks to embed.
71
+ const toEmbedTexts = [];
72
+ const toEmbedMeta = [];
73
+ for (const rawPath of params.paths) {
74
+ const absPath = resolve(rawPath);
75
+ seenPaths.add(absPath);
76
+ let fileInfo;
77
+ try {
78
+ fileInfo = await sha256File(absPath);
79
+ }
80
+ catch (err) {
81
+ throw new InternError("SOURCE_PATH_NOT_FOUND", `Cannot read input file: ${rawPath} — ${err.message}`, "Check the path exists and is readable.", false);
82
+ }
83
+ totalChars += fileInfo.content.length;
84
+ const reuseKey = `${absPath}::${fileInfo.hash}`;
85
+ const reused = reusable.get(reuseKey);
86
+ if (reused && reused.length > 0) {
87
+ allChunks.push(...reused);
88
+ reusedCount += reused.length;
89
+ continue;
90
+ }
91
+ // Fresh chunking for this file — heading-aware.
92
+ const { title, chunks } = chunkDocument(fileInfo.content, opts);
93
+ titles[absPath] = title;
94
+ for (const ck of chunks) {
95
+ toEmbedMeta.push({
96
+ path: absPath,
97
+ file_hash: fileInfo.hash,
98
+ file_mtime: fileInfo.mtime,
99
+ chunk_index: ck.index,
100
+ char_start: ck.char_start,
101
+ char_end: ck.char_end,
102
+ text: ck.text,
103
+ heading_path: ck.heading_path,
104
+ chunk_type: ck.chunk_type,
105
+ });
106
+ toEmbedTexts.push(ck.text);
107
+ }
108
+ }
109
+ // Pass 2: embed everything that needs embedding, in batches.
110
+ if (toEmbedTexts.length > 0) {
111
+ for (let i = 0; i < toEmbedTexts.length; i += EMBED_BATCH) {
112
+ const batch = toEmbedTexts.slice(i, i + EMBED_BATCH);
113
+ const resp = await params.client.embed({ model: params.model, input: batch });
114
+ if (resp.embeddings.length !== batch.length) {
115
+ throw new Error(`Embed returned ${resp.embeddings.length} vectors for ${batch.length} inputs`);
116
+ }
117
+ for (let j = 0; j < batch.length; j++) {
118
+ const meta = toEmbedMeta[i + j];
119
+ allChunks.push({
120
+ id: `${params.name}-${allChunks.length.toString(16).padStart(6, "0")}`,
121
+ path: meta.path,
122
+ file_hash: meta.file_hash,
123
+ file_mtime: meta.file_mtime,
124
+ chunk_index: meta.chunk_index,
125
+ char_start: meta.char_start,
126
+ char_end: meta.char_end,
127
+ text: meta.text,
128
+ vector: resp.embeddings[j],
129
+ heading_path: meta.heading_path,
130
+ chunk_type: meta.chunk_type,
131
+ });
132
+ newlyEmbeddedCount += 1;
133
+ }
134
+ }
135
+ }
136
+ // Drop files that were in the old corpus but not in the input.
137
+ const droppedFiles = [];
138
+ if (existing) {
139
+ const previousPaths = new Set(existing.chunks.map((c) => c.path));
140
+ for (const p of previousPaths) {
141
+ if (!seenPaths.has(p))
142
+ droppedFiles.push(p);
143
+ }
144
+ }
145
+ for (const p of droppedFiles)
146
+ delete titles[p];
147
+ // Scope titles to paths actually present in this index.
148
+ const livingTitles = {};
149
+ for (const c of allChunks) {
150
+ if (c.path in titles)
151
+ livingTitles[c.path] = titles[c.path];
152
+ else if (!(c.path in livingTitles))
153
+ livingTitles[c.path] = null;
154
+ }
155
+ const corpus = {
156
+ schema_version: CORPUS_SCHEMA_VERSION,
157
+ name: params.name,
158
+ model_version: params.model,
159
+ model_digest: null,
160
+ indexed_at: new Date().toISOString(),
161
+ chunk_chars: opts.chunk_chars,
162
+ chunk_overlap: opts.chunk_overlap,
163
+ stats: {
164
+ documents: new Set(allChunks.map((c) => c.path)).size,
165
+ chunks: allChunks.length,
166
+ total_chars: totalChars,
167
+ },
168
+ titles: livingTitles,
169
+ chunks: allChunks,
170
+ };
171
+ await saveCorpus(corpus);
172
+ // Write the manifest alongside the corpus. The corpus is "reality";
173
+ // the manifest is "intent" — what the caller declared should be here.
174
+ // Refresh later reconciles the two.
175
+ const manifestPaths = [...seenPaths].sort();
176
+ const prevManifest = await loadManifest(params.name).catch(() => null);
177
+ const now = new Date().toISOString();
178
+ const manifest = {
179
+ schema_version: MANIFEST_SCHEMA_VERSION,
180
+ name: params.name,
181
+ paths: manifestPaths,
182
+ embed_model: params.model,
183
+ chunk_chars: opts.chunk_chars,
184
+ chunk_overlap: opts.chunk_overlap,
185
+ created_at: prevManifest?.created_at ?? now,
186
+ updated_at: now,
187
+ };
188
+ await saveManifest(manifest);
189
+ return {
190
+ name: corpus.name,
191
+ model_version: corpus.model_version,
192
+ documents: corpus.stats.documents,
193
+ chunks: corpus.stats.chunks,
194
+ total_chars: corpus.stats.total_chars,
195
+ reused_chunks: reusedCount,
196
+ newly_embedded_chunks: newlyEmbeddedCount,
197
+ dropped_files: droppedFiles,
198
+ elapsed_ms: Date.now() - t0,
199
+ };
200
+ }
201
+ //# sourceMappingURL=indexer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexer.js","sourceRoot":"","sources":["../../src/corpus/indexer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Lexical index + BM25 scorer — the "keyword truth" rail.
3
+ *
4
+ * This file is deliberately narrow: it produces one trustworthy primitive,
5
+ * `scoreLexical(query, index)`, that returns deterministic field-aware
6
+ * BM25 scores over v2 chunks. No fusion, no modes, no synthesis — slice 3
7
+ * layers those on top.
8
+ *
9
+ * Four fields are scored independently and then combined into a single
10
+ * `score`, but the per-field breakdown is preserved on every result so
11
+ * slice 3 can recombine them (e.g. title_path mode) without reworking
12
+ * this layer.
13
+ *
14
+ * Field identity matters:
15
+ * - title : the file's first H1 (if any)
16
+ * - heading_path : the breadcrumb stack for the chunk's section
17
+ * - path : file path tokens (dirs, base name, extension)
18
+ * - body : the chunk text itself
19
+ *
20
+ * Determinism guarantees:
21
+ * - Sort by score desc, then (path asc, chunk_index asc) as tie-break.
22
+ * - Zero-score chunks are omitted.
23
+ * - Tokenizer is pure: same input → same tokens, independent of locale.
24
+ * - Stopword set is a fixed list in this file (no env lookup).
25
+ */
26
+ import type { CorpusChunk } from "./storage.js";
27
+ export declare const DEFAULT_FIELD_WEIGHTS: {
28
+ readonly title: 3;
29
+ readonly heading: 2;
30
+ readonly path: 1.5;
31
+ readonly body: 1;
32
+ };
33
+ export declare function isStopword(term: string): boolean;
34
+ /**
35
+ * Generic tokenizer for body/title/heading text.
36
+ * Lowercases, splits on non-alphanumeric, drops stopwords and empties.
37
+ * No stemming, no fuzzy matching — those are explicit slice-3+ decisions.
38
+ */
39
+ export declare function tokenize(text: string): string[];
40
+ /**
41
+ * Path tokenizer — same splitter as body, but runs the full path through
42
+ * so dirs, base name stem, and extension each become searchable tokens.
43
+ * Absolute-path prefixes (drive letters, leading slashes) contribute
44
+ * nothing once split, which is the right behavior.
45
+ */
46
+ export declare function tokenizePath(path: string): string[];
47
+ /**
48
+ * Heading tokenizer — flattens the heading_path breadcrumb into tokens.
49
+ * Deeper headings contribute the same as shallower ones; weighting by
50
+ * depth is a slice-3+ concern.
51
+ */
52
+ export declare function tokenizeHeadings(headingPath: string[]): string[];
53
+ export type FieldName = "body" | "heading" | "title" | "path";
54
+ interface LexicalDoc {
55
+ chunkId: string;
56
+ chunkIndex: number;
57
+ path: string;
58
+ tokens: Record<FieldName, Map<string, number>>;
59
+ len: Record<FieldName, number>;
60
+ }
61
+ export interface LexicalIndex {
62
+ chunkCount: number;
63
+ avg: Record<FieldName, number>;
64
+ /** Document frequency per field: term -> number of chunks whose field contains it. */
65
+ df: Record<FieldName, Map<string, number>>;
66
+ docs: LexicalDoc[];
67
+ }
68
+ export interface LexicalScore {
69
+ chunkId: string;
70
+ chunkIndex: number;
71
+ path: string;
72
+ score: number;
73
+ fieldScores: Record<FieldName, number>;
74
+ matchedTerms: string[];
75
+ }
76
+ /**
77
+ * Build the in-memory lexical index from the chunks of a loaded corpus.
78
+ * Idempotent and deterministic: same input corpus → identical index.
79
+ */
80
+ export declare function buildLexicalIndex(chunks: CorpusChunk[], titles: Record<string, string | null>): LexicalIndex;
81
+ export interface ScoreLexicalOptions {
82
+ weights?: Partial<Record<FieldName, number>>;
83
+ /** Override the query tokenizer. Default = tokenize(). */
84
+ queryTokens?: string[];
85
+ }
86
+ /**
87
+ * Score every chunk in the index against `query`. Returns only chunks
88
+ * with score > 0, sorted score desc, (path asc, chunk_index asc) tie-break.
89
+ *
90
+ * Field scores are preserved on every result so slice 3 can recombine
91
+ * without rescoring.
92
+ */
93
+ export declare function scoreLexical(query: string, index: LexicalIndex, opts?: ScoreLexicalOptions): LexicalScore[];
94
+ export {};
95
+ //# sourceMappingURL=lexical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexical.d.ts","sourceRoot":"","sources":["../../src/corpus/lexical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAShD,eAAO,MAAM,qBAAqB;;;;;CAKxB,CAAC;AAcX,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW/C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAGhE;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE9D,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,GAAG,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC/B,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,IAAI,EAAE,UAAU,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EAAE,EACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GACpC,YAAY,CAqDd;AAkCD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7C,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,YAAY,EACnB,IAAI,GAAE,mBAAwB,GAC7B,YAAY,EAAE,CA0ChB"}
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Lexical index + BM25 scorer — the "keyword truth" rail.
3
+ *
4
+ * This file is deliberately narrow: it produces one trustworthy primitive,
5
+ * `scoreLexical(query, index)`, that returns deterministic field-aware
6
+ * BM25 scores over v2 chunks. No fusion, no modes, no synthesis — slice 3
7
+ * layers those on top.
8
+ *
9
+ * Four fields are scored independently and then combined into a single
10
+ * `score`, but the per-field breakdown is preserved on every result so
11
+ * slice 3 can recombine them (e.g. title_path mode) without reworking
12
+ * this layer.
13
+ *
14
+ * Field identity matters:
15
+ * - title : the file's first H1 (if any)
16
+ * - heading_path : the breadcrumb stack for the chunk's section
17
+ * - path : file path tokens (dirs, base name, extension)
18
+ * - body : the chunk text itself
19
+ *
20
+ * Determinism guarantees:
21
+ * - Sort by score desc, then (path asc, chunk_index asc) as tie-break.
22
+ * - Zero-score chunks are omitted.
23
+ * - Tokenizer is pure: same input → same tokens, independent of locale.
24
+ * - Stopword set is a fixed list in this file (no env lookup).
25
+ */
26
+ // BM25 constants — classic Robertson/Walker defaults.
27
+ const BM25_K1 = 1.2;
28
+ const BM25_B = 0.75;
29
+ // Default per-field weights. Metadata matches outweigh body because a
30
+ // term in the title almost always means "this document is about that
31
+ // thing" while a term in the body can be incidental.
32
+ export const DEFAULT_FIELD_WEIGHTS = {
33
+ title: 3.0,
34
+ heading: 2.0,
35
+ path: 1.5,
36
+ body: 1.0,
37
+ };
38
+ // Small, conservative English stopword list. Applied at BOTH index and
39
+ // query time so IDF statistics stay consistent. Kept short on purpose —
40
+ // overly aggressive stopword removal eats legitimate technical terms.
41
+ const STOPWORDS = new Set([
42
+ "a", "an", "and", "are", "as", "at", "be", "been", "being", "but", "by",
43
+ "do", "does", "did", "for", "from", "had", "has", "have", "he", "her",
44
+ "him", "his", "i", "if", "in", "into", "is", "it", "its", "me", "my",
45
+ "of", "on", "or", "our", "she", "so", "than", "that", "the", "their",
46
+ "them", "these", "they", "this", "those", "to", "us", "was", "we", "were",
47
+ "will", "with", "you", "your",
48
+ ]);
49
+ export function isStopword(term) {
50
+ return STOPWORDS.has(term);
51
+ }
52
+ /**
53
+ * Generic tokenizer for body/title/heading text.
54
+ * Lowercases, splits on non-alphanumeric, drops stopwords and empties.
55
+ * No stemming, no fuzzy matching — those are explicit slice-3+ decisions.
56
+ */
57
+ export function tokenize(text) {
58
+ if (!text)
59
+ return [];
60
+ const lower = text.toLowerCase();
61
+ const raw = lower.split(/[^a-z0-9]+/);
62
+ const out = [];
63
+ for (const t of raw) {
64
+ if (t.length === 0)
65
+ continue;
66
+ if (STOPWORDS.has(t))
67
+ continue;
68
+ out.push(t);
69
+ }
70
+ return out;
71
+ }
72
+ /**
73
+ * Path tokenizer — same splitter as body, but runs the full path through
74
+ * so dirs, base name stem, and extension each become searchable tokens.
75
+ * Absolute-path prefixes (drive letters, leading slashes) contribute
76
+ * nothing once split, which is the right behavior.
77
+ */
78
+ export function tokenizePath(path) {
79
+ return tokenize(path);
80
+ }
81
+ /**
82
+ * Heading tokenizer — flattens the heading_path breadcrumb into tokens.
83
+ * Deeper headings contribute the same as shallower ones; weighting by
84
+ * depth is a slice-3+ concern.
85
+ */
86
+ export function tokenizeHeadings(headingPath) {
87
+ const joined = headingPath.join(" ");
88
+ return tokenize(joined);
89
+ }
90
+ function countTokens(tokens) {
91
+ const m = new Map();
92
+ for (const t of tokens)
93
+ m.set(t, (m.get(t) ?? 0) + 1);
94
+ return m;
95
+ }
96
+ /**
97
+ * Build the in-memory lexical index from the chunks of a loaded corpus.
98
+ * Idempotent and deterministic: same input corpus → identical index.
99
+ */
100
+ export function buildLexicalIndex(chunks, titles) {
101
+ const docs = [];
102
+ const df = {
103
+ body: new Map(),
104
+ heading: new Map(),
105
+ title: new Map(),
106
+ path: new Map(),
107
+ };
108
+ const totals = { body: 0, heading: 0, title: 0, path: 0 };
109
+ for (const c of chunks) {
110
+ const bodyTokens = tokenize(c.text);
111
+ const headingTokens = tokenizeHeadings(c.heading_path);
112
+ const title = titles[c.path] ?? "";
113
+ const titleTokens = tokenize(title);
114
+ const pathTokens = tokenizePath(c.path);
115
+ const doc = {
116
+ chunkId: c.id,
117
+ chunkIndex: c.chunk_index,
118
+ path: c.path,
119
+ tokens: {
120
+ body: countTokens(bodyTokens),
121
+ heading: countTokens(headingTokens),
122
+ title: countTokens(titleTokens),
123
+ path: countTokens(pathTokens),
124
+ },
125
+ len: {
126
+ body: bodyTokens.length,
127
+ heading: headingTokens.length,
128
+ title: titleTokens.length,
129
+ path: pathTokens.length,
130
+ },
131
+ };
132
+ docs.push(doc);
133
+ for (const field of ["body", "heading", "title", "path"]) {
134
+ totals[field] += doc.len[field];
135
+ for (const term of doc.tokens[field].keys()) {
136
+ df[field].set(term, (df[field].get(term) ?? 0) + 1);
137
+ }
138
+ }
139
+ }
140
+ const n = docs.length;
141
+ const avg = {
142
+ body: n === 0 ? 0 : totals.body / n,
143
+ heading: n === 0 ? 0 : totals.heading / n,
144
+ title: n === 0 ? 0 : totals.title / n,
145
+ path: n === 0 ? 0 : totals.path / n,
146
+ };
147
+ return { chunkCount: n, avg, df, docs };
148
+ }
149
+ /**
150
+ * BM25+ IDF variant: ln((N - df + 0.5) / (df + 0.5) + 1).
151
+ * The +1 inside the log keeps IDF non-negative for very common terms.
152
+ */
153
+ function bm25Idf(n, df) {
154
+ return Math.log((n - df + 0.5) / (df + 0.5) + 1);
155
+ }
156
+ function bm25FieldScore(queryTerms, doc, field, index) {
157
+ const avgLen = index.avg[field];
158
+ const docLen = doc.len[field];
159
+ let score = 0;
160
+ const matched = [];
161
+ if (docLen === 0)
162
+ return { score: 0, matched };
163
+ for (const term of queryTerms) {
164
+ const tf = doc.tokens[field].get(term) ?? 0;
165
+ if (tf === 0)
166
+ continue;
167
+ const df = index.df[field].get(term) ?? 0;
168
+ const idf = bm25Idf(index.chunkCount, df);
169
+ const normLen = avgLen === 0 ? 1 : docLen / avgLen;
170
+ const denom = tf + BM25_K1 * (1 - BM25_B + BM25_B * normLen);
171
+ score += idf * ((tf * (BM25_K1 + 1)) / denom);
172
+ matched.push(term);
173
+ }
174
+ return { score, matched };
175
+ }
176
+ /**
177
+ * Score every chunk in the index against `query`. Returns only chunks
178
+ * with score > 0, sorted score desc, (path asc, chunk_index asc) tie-break.
179
+ *
180
+ * Field scores are preserved on every result so slice 3 can recombine
181
+ * without rescoring.
182
+ */
183
+ export function scoreLexical(query, index, opts = {}) {
184
+ const queryTerms = opts.queryTokens ?? tokenize(query);
185
+ if (queryTerms.length === 0 || index.chunkCount === 0)
186
+ return [];
187
+ const weights = {
188
+ title: opts.weights?.title ?? DEFAULT_FIELD_WEIGHTS.title,
189
+ heading: opts.weights?.heading ?? DEFAULT_FIELD_WEIGHTS.heading,
190
+ path: opts.weights?.path ?? DEFAULT_FIELD_WEIGHTS.path,
191
+ body: opts.weights?.body ?? DEFAULT_FIELD_WEIGHTS.body,
192
+ };
193
+ const results = [];
194
+ for (const doc of index.docs) {
195
+ const fieldScores = { body: 0, heading: 0, title: 0, path: 0 };
196
+ const matchedSet = new Set();
197
+ for (const field of ["body", "heading", "title", "path"]) {
198
+ const { score, matched } = bm25FieldScore(queryTerms, doc, field, index);
199
+ fieldScores[field] = score;
200
+ for (const m of matched)
201
+ matchedSet.add(m);
202
+ }
203
+ const combined = weights.title * fieldScores.title +
204
+ weights.heading * fieldScores.heading +
205
+ weights.path * fieldScores.path +
206
+ weights.body * fieldScores.body;
207
+ if (combined <= 0)
208
+ continue;
209
+ results.push({
210
+ chunkId: doc.chunkId,
211
+ chunkIndex: doc.chunkIndex,
212
+ path: doc.path,
213
+ score: combined,
214
+ fieldScores,
215
+ matchedTerms: [...matchedSet].sort(),
216
+ });
217
+ }
218
+ results.sort((a, b) => {
219
+ if (b.score !== a.score)
220
+ return b.score - a.score;
221
+ if (a.path !== b.path)
222
+ return a.path < b.path ? -1 : 1;
223
+ return a.chunkIndex - b.chunkIndex;
224
+ });
225
+ return results;
226
+ }
227
+ //# sourceMappingURL=lexical.js.map