mdcontext 0.0.1 → 0.2.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 (337) hide show
  1. package/.changeset/README.md +28 -0
  2. package/.changeset/config.json +11 -0
  3. package/.claude/settings.local.json +25 -0
  4. package/.github/workflows/ci.yml +83 -0
  5. package/.github/workflows/claude-code-review.yml +44 -0
  6. package/.github/workflows/claude.yml +85 -0
  7. package/.github/workflows/release.yml +113 -0
  8. package/.tldrignore +112 -0
  9. package/BACKLOG.md +338 -0
  10. package/CONTRIBUTING.md +186 -0
  11. package/NOTES/NOTES +44 -0
  12. package/README.md +434 -11
  13. package/biome.json +36 -0
  14. package/cspell.config.yaml +14 -0
  15. package/dist/chunk-23UPXDNL.js +3044 -0
  16. package/dist/chunk-2W7MO2DL.js +1366 -0
  17. package/dist/chunk-3NUAZGMA.js +1689 -0
  18. package/dist/chunk-7TOWB2XB.js +366 -0
  19. package/dist/chunk-7XOTOADQ.js +3065 -0
  20. package/dist/chunk-AH2PDM2K.js +3042 -0
  21. package/dist/chunk-BNXWSZ63.js +3742 -0
  22. package/dist/chunk-BTL5DJVU.js +3222 -0
  23. package/dist/chunk-HDHYG7E4.js +104 -0
  24. package/dist/chunk-HLR4KZBP.js +3234 -0
  25. package/dist/chunk-IP3FRFEB.js +1045 -0
  26. package/dist/chunk-KHU56VDO.js +3042 -0
  27. package/dist/chunk-KRYIFLQR.js +88 -0
  28. package/dist/chunk-LBSDNLEM.js +287 -0
  29. package/dist/chunk-MNTQ7HCP.js +2643 -0
  30. package/dist/chunk-MUJELQQ6.js +1387 -0
  31. package/dist/chunk-MXJGMSLV.js +2199 -0
  32. package/dist/chunk-N6QJGC3Z.js +2636 -0
  33. package/dist/chunk-OBELGBPM.js +1713 -0
  34. package/dist/chunk-OT7R5XTA.js +3192 -0
  35. package/dist/chunk-P7X4RA2T.js +106 -0
  36. package/dist/chunk-PIDUQNC2.js +3185 -0
  37. package/dist/chunk-POGCDIH4.js +3187 -0
  38. package/dist/chunk-PSIEOQGZ.js +3043 -0
  39. package/dist/chunk-PVRT3IHA.js +3238 -0
  40. package/dist/chunk-QNN4TT23.js +1430 -0
  41. package/dist/chunk-RE3R45RJ.js +3042 -0
  42. package/dist/chunk-S7E6TFX6.js +803 -0
  43. package/dist/chunk-SG6GLU4U.js +1378 -0
  44. package/dist/chunk-SJCDV2ST.js +274 -0
  45. package/dist/chunk-SYE5XLF3.js +104 -0
  46. package/dist/chunk-T5VLYBZD.js +103 -0
  47. package/dist/chunk-TOQB7VWU.js +3238 -0
  48. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  49. package/dist/chunk-VVTGZNBT.js +1629 -0
  50. package/dist/chunk-W7Q4RFEV.js +104 -0
  51. package/dist/chunk-XTYYVRLO.js +3190 -0
  52. package/dist/chunk-Y6MDYVJD.js +3063 -0
  53. package/dist/cli/main.d.ts +1 -0
  54. package/dist/cli/main.js +5458 -0
  55. package/dist/index.d.ts +653 -0
  56. package/dist/index.js +79 -0
  57. package/dist/mcp/server.d.ts +1 -0
  58. package/dist/mcp/server.js +472 -0
  59. package/dist/schema-BAWSG7KY.js +22 -0
  60. package/dist/schema-E3QUPL26.js +20 -0
  61. package/dist/schema-EHL7WUT6.js +20 -0
  62. package/docs/019-USAGE.md +625 -0
  63. package/docs/020-current-implementation.md +364 -0
  64. package/docs/021-DOGFOODING-FINDINGS.md +175 -0
  65. package/docs/BACKLOG.md +80 -0
  66. package/docs/CONFIG.md +1123 -0
  67. package/docs/DESIGN.md +439 -0
  68. package/docs/ERRORS.md +383 -0
  69. package/docs/PROJECT.md +88 -0
  70. package/docs/ROADMAP.md +407 -0
  71. package/docs/summarization.md +320 -0
  72. package/docs/test-links.md +9 -0
  73. package/justfile +40 -0
  74. package/package.json +74 -9
  75. package/pnpm-workspace.yaml +5 -0
  76. package/research/INDEX.md +315 -0
  77. package/research/code-review/README.md +90 -0
  78. package/research/code-review/cli-error-handling-review.md +979 -0
  79. package/research/code-review/code-review-validation-report.md +464 -0
  80. package/research/code-review/main-ts-review.md +1128 -0
  81. package/research/config-analysis/01-current-implementation.md +470 -0
  82. package/research/config-analysis/02-strategy-recommendation.md +428 -0
  83. package/research/config-analysis/03-task-candidates.md +715 -0
  84. package/research/config-analysis/033-research-configuration-management.md +828 -0
  85. package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
  86. package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
  87. package/research/config-docs/SUMMARY.md +357 -0
  88. package/research/config-docs/TEST-RESULTS.md +776 -0
  89. package/research/config-docs/TODO.md +542 -0
  90. package/research/config-docs/analysis.md +744 -0
  91. package/research/config-docs/fix-validation.md +502 -0
  92. package/research/config-docs/help-audit.md +264 -0
  93. package/research/config-docs/help-system-analysis.md +890 -0
  94. package/research/dogfood/consolidated-tool-evaluation.md +373 -0
  95. package/research/dogfood/strategy-a/a-synthesis.md +184 -0
  96. package/research/dogfood/strategy-a/a1-docs.md +226 -0
  97. package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
  98. package/research/dogfood/strategy-a/a3-llm.md +164 -0
  99. package/research/dogfood/strategy-b/b-synthesis.md +228 -0
  100. package/research/dogfood/strategy-b/b1-architecture.md +207 -0
  101. package/research/dogfood/strategy-b/b2-gaps.md +258 -0
  102. package/research/dogfood/strategy-b/b3-workflows.md +250 -0
  103. package/research/dogfood/strategy-c/c-synthesis.md +451 -0
  104. package/research/dogfood/strategy-c/c1-explorer.md +192 -0
  105. package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
  106. package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
  107. package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
  108. package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
  109. package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
  110. package/research/effect-cli-error-handling.md +845 -0
  111. package/research/effect-errors-as-values.md +943 -0
  112. package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
  113. package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
  114. package/research/errors-task-analysis/embeddings-analysis.md +709 -0
  115. package/research/errors-task-analysis/index-search-analysis.md +812 -0
  116. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  117. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  118. package/research/issue-review.md +603 -0
  119. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  120. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  121. package/research/llm-summarization/anthropic-2026.md +367 -0
  122. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  123. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  124. package/research/llm-summarization/openai-2026.md +473 -0
  125. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  126. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  127. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  128. package/research/llm-summarization/prototype-results.md +56 -0
  129. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  130. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  131. package/research/mdcontext-error-analysis.md +521 -0
  132. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  133. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  134. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  135. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  136. package/research/mdcontext-pudding/02-search.md +970 -0
  137. package/research/mdcontext-pudding/03-context.md +779 -0
  138. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  139. package/research/mdcontext-pudding/04-tree.md +704 -0
  140. package/research/mdcontext-pudding/05-config.md +1038 -0
  141. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  142. package/research/mdcontext-pudding/06-links.md +679 -0
  143. package/research/mdcontext-pudding/07-stats.md +693 -0
  144. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  145. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  146. package/research/mdcontext-pudding/README.md +168 -0
  147. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  148. package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
  149. package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
  150. package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
  151. package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
  152. package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
  153. package/research/research-quality-review.md +834 -0
  154. package/research/semantic-search/002-research-embedding-models.md +490 -0
  155. package/research/semantic-search/003-research-rag-alternatives.md +523 -0
  156. package/research/semantic-search/004-research-vector-search.md +841 -0
  157. package/research/semantic-search/032-research-semantic-search.md +427 -0
  158. package/research/semantic-search/embedding-text-analysis.md +156 -0
  159. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  160. package/research/semantic-search/query-processing-analysis.md +207 -0
  161. package/research/semantic-search/root-cause-and-solution.md +114 -0
  162. package/research/semantic-search/threshold-validation-report.md +69 -0
  163. package/research/semantic-search/vector-search-analysis.md +63 -0
  164. package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
  165. package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
  166. package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
  167. package/research/task-management-2026/03-lightweight-file-based.md +567 -0
  168. package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
  169. package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
  170. package/research/task-management-2026/linear/02-api-integrations.md +930 -0
  171. package/research/task-management-2026/linear/03-ai-features.md +368 -0
  172. package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
  173. package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
  174. package/research/test-path-issues.md +276 -0
  175. package/review/ALP-76/1-error-type-design.md +962 -0
  176. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  177. package/review/ALP-76/3-error-presentation.md +624 -0
  178. package/review/ALP-76/4-test-coverage.md +625 -0
  179. package/review/ALP-76/5-migration-completeness.md +440 -0
  180. package/review/ALP-76/6-effect-best-practices.md +755 -0
  181. package/scripts/apply-branch-protection.sh +47 -0
  182. package/scripts/branch-protection-templates.json +79 -0
  183. package/scripts/prototype-summarization.ts +346 -0
  184. package/scripts/rebuild-hnswlib.js +58 -0
  185. package/scripts/setup-branch-protection.sh +64 -0
  186. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  187. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  188. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  189. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  190. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  191. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  192. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  193. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  194. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  195. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  196. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  197. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  198. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  199. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  200. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  201. package/src/cli/argv-preprocessor.test.ts +210 -0
  202. package/src/cli/argv-preprocessor.ts +202 -0
  203. package/src/cli/cli.test.ts +627 -0
  204. package/src/cli/commands/backlinks.ts +54 -0
  205. package/src/cli/commands/config-cmd.ts +642 -0
  206. package/src/cli/commands/context.ts +285 -0
  207. package/src/cli/commands/duplicates.ts +122 -0
  208. package/src/cli/commands/embeddings.ts +529 -0
  209. package/src/cli/commands/index-cmd.ts +480 -0
  210. package/src/cli/commands/index.ts +16 -0
  211. package/src/cli/commands/links.ts +52 -0
  212. package/src/cli/commands/search.ts +1281 -0
  213. package/src/cli/commands/stats.ts +149 -0
  214. package/src/cli/commands/tree.ts +128 -0
  215. package/src/cli/config-layer.ts +176 -0
  216. package/src/cli/error-handler.test.ts +235 -0
  217. package/src/cli/error-handler.ts +655 -0
  218. package/src/cli/flag-schemas.ts +341 -0
  219. package/src/cli/help.ts +588 -0
  220. package/src/cli/index.ts +9 -0
  221. package/src/cli/main.ts +435 -0
  222. package/src/cli/options.ts +41 -0
  223. package/src/cli/shared-error-handling.ts +199 -0
  224. package/src/cli/typo-suggester.test.ts +105 -0
  225. package/src/cli/typo-suggester.ts +130 -0
  226. package/src/cli/utils.ts +259 -0
  227. package/src/config/file-provider.test.ts +320 -0
  228. package/src/config/file-provider.ts +273 -0
  229. package/src/config/index.ts +72 -0
  230. package/src/config/integration.test.ts +667 -0
  231. package/src/config/precedence.test.ts +277 -0
  232. package/src/config/precedence.ts +451 -0
  233. package/src/config/schema.test.ts +414 -0
  234. package/src/config/schema.ts +603 -0
  235. package/src/config/service.test.ts +320 -0
  236. package/src/config/service.ts +243 -0
  237. package/src/config/testing.test.ts +264 -0
  238. package/src/config/testing.ts +110 -0
  239. package/src/core/index.ts +1 -0
  240. package/src/core/types.ts +113 -0
  241. package/src/duplicates/detector.test.ts +183 -0
  242. package/src/duplicates/detector.ts +414 -0
  243. package/src/duplicates/index.ts +18 -0
  244. package/src/embeddings/embedding-namespace.test.ts +300 -0
  245. package/src/embeddings/embedding-namespace.ts +947 -0
  246. package/src/embeddings/heading-boost.test.ts +222 -0
  247. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  248. package/src/embeddings/hyde.test.ts +272 -0
  249. package/src/embeddings/hyde.ts +264 -0
  250. package/src/embeddings/index.ts +10 -0
  251. package/src/embeddings/openai-provider.ts +414 -0
  252. package/src/embeddings/pricing.json +22 -0
  253. package/src/embeddings/provider-constants.ts +204 -0
  254. package/src/embeddings/provider-errors.test.ts +967 -0
  255. package/src/embeddings/provider-errors.ts +565 -0
  256. package/src/embeddings/provider-factory.test.ts +240 -0
  257. package/src/embeddings/provider-factory.ts +225 -0
  258. package/src/embeddings/provider-integration.test.ts +788 -0
  259. package/src/embeddings/query-preprocessing.test.ts +187 -0
  260. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  261. package/src/embeddings/semantic-search.ts +1270 -0
  262. package/src/embeddings/types.ts +359 -0
  263. package/src/embeddings/vector-store.ts +708 -0
  264. package/src/embeddings/voyage-provider.ts +313 -0
  265. package/src/errors/errors.test.ts +845 -0
  266. package/src/errors/index.ts +533 -0
  267. package/src/index/ignore-patterns.test.ts +354 -0
  268. package/src/index/ignore-patterns.ts +305 -0
  269. package/src/index/index.ts +4 -0
  270. package/src/index/indexer.ts +684 -0
  271. package/src/index/storage.ts +260 -0
  272. package/src/index/types.ts +147 -0
  273. package/src/index/watcher.ts +189 -0
  274. package/src/index.ts +30 -0
  275. package/src/integration/search-keyword.test.ts +678 -0
  276. package/src/mcp/server.ts +612 -0
  277. package/src/parser/index.ts +1 -0
  278. package/src/parser/parser.test.ts +291 -0
  279. package/src/parser/parser.ts +394 -0
  280. package/src/parser/section-filter.test.ts +277 -0
  281. package/src/parser/section-filter.ts +392 -0
  282. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  283. package/src/search/bm25-store.ts +366 -0
  284. package/src/search/cross-encoder.test.ts +253 -0
  285. package/src/search/cross-encoder.ts +406 -0
  286. package/src/search/fuzzy-search.test.ts +419 -0
  287. package/src/search/fuzzy-search.ts +273 -0
  288. package/src/search/hybrid-search.ts +448 -0
  289. package/src/search/path-matcher.test.ts +276 -0
  290. package/src/search/path-matcher.ts +33 -0
  291. package/src/search/query-parser.test.ts +260 -0
  292. package/src/search/query-parser.ts +319 -0
  293. package/src/search/searcher.test.ts +280 -0
  294. package/src/search/searcher.ts +724 -0
  295. package/src/search/wink-bm25.d.ts +30 -0
  296. package/src/summarization/cli-providers/claude.ts +202 -0
  297. package/src/summarization/cli-providers/detection.test.ts +273 -0
  298. package/src/summarization/cli-providers/detection.ts +118 -0
  299. package/src/summarization/cli-providers/index.ts +8 -0
  300. package/src/summarization/cost.test.ts +139 -0
  301. package/src/summarization/cost.ts +102 -0
  302. package/src/summarization/error-handler.test.ts +127 -0
  303. package/src/summarization/error-handler.ts +111 -0
  304. package/src/summarization/index.ts +102 -0
  305. package/src/summarization/pipeline.test.ts +498 -0
  306. package/src/summarization/pipeline.ts +231 -0
  307. package/src/summarization/prompts.test.ts +269 -0
  308. package/src/summarization/prompts.ts +133 -0
  309. package/src/summarization/provider-factory.test.ts +396 -0
  310. package/src/summarization/provider-factory.ts +178 -0
  311. package/src/summarization/types.ts +184 -0
  312. package/src/summarize/budget-bugs.test.ts +620 -0
  313. package/src/summarize/formatters.ts +419 -0
  314. package/src/summarize/index.ts +20 -0
  315. package/src/summarize/summarizer.test.ts +275 -0
  316. package/src/summarize/summarizer.ts +597 -0
  317. package/src/summarize/verify-bugs.test.ts +238 -0
  318. package/src/types/huggingface-transformers.d.ts +66 -0
  319. package/src/utils/index.ts +1 -0
  320. package/src/utils/tokens.test.ts +142 -0
  321. package/src/utils/tokens.ts +186 -0
  322. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  323. package/tests/fixtures/cli/.mdcontext/config.json +8 -0
  324. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  325. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  326. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
  327. package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
  328. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
  329. package/tests/fixtures/cli/README.md +9 -0
  330. package/tests/fixtures/cli/api-reference.md +11 -0
  331. package/tests/fixtures/cli/getting-started.md +11 -0
  332. package/tests/integration/embed-index.test.ts +712 -0
  333. package/tests/integration/search-context.test.ts +469 -0
  334. package/tests/integration/search-semantic.test.ts +522 -0
  335. package/tsconfig.json +26 -0
  336. package/vitest.config.ts +16 -0
  337. package/vitest.setup.ts +12 -0
@@ -0,0 +1,724 @@
1
+ /**
2
+ * Keyword search for mdcontext
3
+ */
4
+
5
+ import * as fs from 'node:fs/promises'
6
+ import * as path from 'node:path'
7
+ import { Effect } from 'effect'
8
+
9
+ import {
10
+ DocumentNotFoundError,
11
+ type FileReadError,
12
+ type IndexCorruptedError,
13
+ IndexNotFoundError,
14
+ } from '../errors/index.js'
15
+ import {
16
+ createStorage,
17
+ loadDocumentIndex,
18
+ loadSectionIndex,
19
+ } from '../index/storage.js'
20
+ import type { DocumentEntry, SectionEntry } from '../index/types.js'
21
+ import {
22
+ buildFuzzyHighlightPattern,
23
+ findMatchesInLine,
24
+ type MatchOptions,
25
+ matchesWithOptions,
26
+ } from './fuzzy-search.js'
27
+ import { matchPath } from './path-matcher.js'
28
+ import {
29
+ buildHighlightPattern,
30
+ evaluateQuery,
31
+ isAdvancedQuery,
32
+ type ParsedQuery,
33
+ parseQuery,
34
+ } from './query-parser.js'
35
+
36
+ // ============================================================================
37
+ // Search Options
38
+ // ============================================================================
39
+
40
+ export interface SearchOptions {
41
+ /** Filter by heading pattern (regex) */
42
+ readonly heading?: string | undefined
43
+ /** Search within section content (regex) */
44
+ readonly content?: string | undefined
45
+ /** Filter by file path pattern (glob-like) */
46
+ readonly pathPattern?: string | undefined
47
+ /** Only sections with code blocks */
48
+ readonly hasCode?: boolean | undefined
49
+ /** Only sections with lists */
50
+ readonly hasList?: boolean | undefined
51
+ /** Only sections with tables */
52
+ readonly hasTable?: boolean | undefined
53
+ /** Minimum heading level */
54
+ readonly minLevel?: number | undefined
55
+ /** Maximum heading level */
56
+ readonly maxLevel?: number | undefined
57
+ /** Maximum results */
58
+ readonly limit?: number | undefined
59
+ /** Lines of context before matches */
60
+ readonly contextBefore?: number | undefined
61
+ /** Lines of context after matches */
62
+ readonly contextAfter?: number | undefined
63
+ /** Enable fuzzy matching with typo tolerance */
64
+ readonly fuzzy?: boolean | undefined
65
+ /** Max edit distance for fuzzy matching (default: 2) */
66
+ readonly fuzzyDistance?: number | undefined
67
+ /** Enable word stemming (fail matches failure, failed, etc.) */
68
+ readonly stem?: boolean | undefined
69
+ }
70
+
71
+ export interface ContentMatch {
72
+ /** The line number where match was found (1-based) */
73
+ readonly lineNumber: number
74
+ /** The matching line text */
75
+ readonly line: string
76
+ /** Snippet showing match context (lines before and after) */
77
+ readonly snippet: string
78
+ /** Context lines with their line numbers (for JSON output) */
79
+ readonly contextLines?: readonly ContextLine[]
80
+ }
81
+
82
+ export interface ContextLine {
83
+ /** The line number (1-based) */
84
+ readonly lineNumber: number
85
+ /** The line text */
86
+ readonly line: string
87
+ /** Whether this is the matching line */
88
+ readonly isMatch: boolean
89
+ }
90
+
91
+ export interface SearchResult {
92
+ readonly section: SectionEntry
93
+ readonly document: DocumentEntry
94
+ readonly sectionContent?: string
95
+ /** Matches found within the content (when content search is used) */
96
+ readonly matches?: readonly ContentMatch[]
97
+ }
98
+
99
+ // ============================================================================
100
+ // Search Implementation
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Search for sections by metadata (heading, path, content flags).
105
+ *
106
+ * @param rootPath - Root directory containing indexed markdown files
107
+ * @param options - Search filters (heading, path pattern, code/list/table flags)
108
+ * @returns Matching sections
109
+ *
110
+ * @throws FileReadError - Cannot read index files
111
+ * @throws IndexCorruptedError - Index files are corrupted
112
+ */
113
+ export const search = (
114
+ rootPath: string,
115
+ options: SearchOptions = {},
116
+ ): Effect.Effect<
117
+ readonly SearchResult[],
118
+ FileReadError | IndexCorruptedError
119
+ > =>
120
+ Effect.gen(function* () {
121
+ const storage = createStorage(rootPath)
122
+
123
+ const docIndex = yield* loadDocumentIndex(storage)
124
+ const sectionIndex = yield* loadSectionIndex(storage)
125
+
126
+ if (!docIndex || !sectionIndex) {
127
+ return []
128
+ }
129
+
130
+ const results: SearchResult[] = []
131
+ const headingRegex = options.heading
132
+ ? new RegExp(options.heading, 'i')
133
+ : null
134
+
135
+ for (const section of Object.values(sectionIndex.sections)) {
136
+ // Filter by heading pattern
137
+ if (headingRegex && !headingRegex.test(section.heading)) {
138
+ continue
139
+ }
140
+
141
+ // Filter by path pattern
142
+ if (
143
+ options.pathPattern &&
144
+ !matchPath(section.documentPath, options.pathPattern)
145
+ ) {
146
+ continue
147
+ }
148
+
149
+ // Filter by code blocks
150
+ if (
151
+ options.hasCode !== undefined &&
152
+ section.hasCode !== options.hasCode
153
+ ) {
154
+ continue
155
+ }
156
+
157
+ // Filter by lists
158
+ if (
159
+ options.hasList !== undefined &&
160
+ section.hasList !== options.hasList
161
+ ) {
162
+ continue
163
+ }
164
+
165
+ // Filter by tables
166
+ if (
167
+ options.hasTable !== undefined &&
168
+ section.hasTable !== options.hasTable
169
+ ) {
170
+ continue
171
+ }
172
+
173
+ // Filter by level range
174
+ if (options.minLevel !== undefined && section.level < options.minLevel) {
175
+ continue
176
+ }
177
+
178
+ if (options.maxLevel !== undefined && section.level > options.maxLevel) {
179
+ continue
180
+ }
181
+
182
+ const document = docIndex.documents[section.documentPath]
183
+ if (document) {
184
+ results.push({ section, document })
185
+ }
186
+
187
+ // Check limit
188
+ if (options.limit !== undefined && results.length >= options.limit) {
189
+ break
190
+ }
191
+ }
192
+
193
+ return results
194
+ })
195
+
196
+ // ============================================================================
197
+ // Content Search Implementation
198
+ // ============================================================================
199
+
200
+ /**
201
+ * Search within section content.
202
+ * Supports boolean operators (AND, OR, NOT) and quoted phrases.
203
+ * Falls back to regex for simple patterns.
204
+ *
205
+ * @param rootPath - Root directory containing indexed markdown files
206
+ * @param options - Search options including content pattern
207
+ * @returns Matching sections with match highlights
208
+ *
209
+ * @throws FileReadError - Cannot read index or source files
210
+ * @throws IndexCorruptedError - Index files are corrupted
211
+ */
212
+ export const searchContent = (
213
+ rootPath: string,
214
+ options: SearchOptions = {},
215
+ ): Effect.Effect<
216
+ readonly SearchResult[],
217
+ FileReadError | IndexCorruptedError
218
+ > =>
219
+ Effect.gen(function* () {
220
+ const storage = createStorage(rootPath)
221
+
222
+ const docIndex = yield* loadDocumentIndex(storage)
223
+ const sectionIndex = yield* loadSectionIndex(storage)
224
+
225
+ if (!docIndex || !sectionIndex) {
226
+ return []
227
+ }
228
+
229
+ // Parse content query - use boolean parser if advanced, else regex
230
+ let parsedQuery: ParsedQuery | null = null
231
+ let contentRegex: RegExp | null = null
232
+ let highlightRegex: RegExp | null = null
233
+
234
+ // Configure fuzzy/stem matching options
235
+ const matchOptions: MatchOptions = {
236
+ stem: options.stem,
237
+ fuzzyDistance: options.fuzzy ? (options.fuzzyDistance ?? 2) : undefined,
238
+ }
239
+ const useFuzzyOrStem = options.fuzzy || options.stem
240
+
241
+ if (options.content) {
242
+ if (isAdvancedQuery(options.content)) {
243
+ parsedQuery = parseQuery(options.content)
244
+ if (parsedQuery) {
245
+ if (useFuzzyOrStem) {
246
+ highlightRegex = buildFuzzyHighlightPattern(
247
+ options.content,
248
+ matchOptions,
249
+ )
250
+ } else {
251
+ highlightRegex = buildHighlightPattern(parsedQuery)
252
+ }
253
+ }
254
+ } else {
255
+ // Simple search - use regex for exact match, or fuzzy/stem matching
256
+ if (!useFuzzyOrStem) {
257
+ contentRegex = new RegExp(options.content, 'gi')
258
+ highlightRegex = contentRegex
259
+ } else {
260
+ // For fuzzy/stem mode, build a highlight pattern
261
+ highlightRegex = buildFuzzyHighlightPattern(
262
+ options.content,
263
+ matchOptions,
264
+ )
265
+ }
266
+ }
267
+ }
268
+
269
+ const headingRegex = options.heading
270
+ ? new RegExp(options.heading, 'i')
271
+ : null
272
+
273
+ const results: SearchResult[] = []
274
+
275
+ // Group sections by document for efficient file reading
276
+ const sectionsByDoc: Record<string, SectionEntry[]> = {}
277
+ for (const section of Object.values(sectionIndex.sections)) {
278
+ const docSections = sectionsByDoc[section.documentPath]
279
+ if (docSections) {
280
+ docSections.push(section)
281
+ } else {
282
+ sectionsByDoc[section.documentPath] = [section]
283
+ }
284
+ }
285
+
286
+ // Process each document
287
+ for (const [docPath, sections] of Object.entries(sectionsByDoc)) {
288
+ // Apply path filter early
289
+ if (options.pathPattern && !matchPath(docPath, options.pathPattern)) {
290
+ continue
291
+ }
292
+
293
+ const document = docIndex.documents[docPath]
294
+ if (!document) continue
295
+
296
+ // Load file content for content search
297
+ let fileContent: string | null = null
298
+ let fileLines: string[] = []
299
+
300
+ // Need to load file if we have any content matching to do:
301
+ // - parsedQuery: boolean query evaluation
302
+ // - contentRegex: regex matching
303
+ // - useFuzzyOrStem: fuzzy/stem matching
304
+ if (parsedQuery || contentRegex || (useFuzzyOrStem && options.content)) {
305
+ const filePath = path.join(storage.rootPath, docPath)
306
+ try {
307
+ fileContent = yield* Effect.promise(() =>
308
+ fs.readFile(filePath, 'utf-8'),
309
+ )
310
+ fileLines = fileContent.split('\n')
311
+ } catch {
312
+ continue // Skip files that can't be read
313
+ }
314
+ }
315
+
316
+ for (const section of sections) {
317
+ // Apply heading filter
318
+ if (headingRegex && !headingRegex.test(section.heading)) {
319
+ continue
320
+ }
321
+
322
+ // Apply other filters
323
+ if (
324
+ options.hasCode !== undefined &&
325
+ section.hasCode !== options.hasCode
326
+ ) {
327
+ continue
328
+ }
329
+ if (
330
+ options.hasList !== undefined &&
331
+ section.hasList !== options.hasList
332
+ ) {
333
+ continue
334
+ }
335
+ if (
336
+ options.hasTable !== undefined &&
337
+ section.hasTable !== options.hasTable
338
+ ) {
339
+ continue
340
+ }
341
+ if (
342
+ options.minLevel !== undefined &&
343
+ section.level < options.minLevel
344
+ ) {
345
+ continue
346
+ }
347
+ if (
348
+ options.maxLevel !== undefined &&
349
+ section.level > options.maxLevel
350
+ ) {
351
+ continue
352
+ }
353
+
354
+ // Content search
355
+ if ((parsedQuery || contentRegex || useFuzzyOrStem) && fileContent) {
356
+ const sectionLines = fileLines.slice(
357
+ section.startLine - 1,
358
+ section.endLine,
359
+ )
360
+ const sectionContent = sectionLines.join('\n')
361
+
362
+ // For boolean queries, evaluate against entire section content
363
+ if (parsedQuery) {
364
+ if (!evaluateQuery(parsedQuery.ast, sectionContent)) {
365
+ continue // Section doesn't match query
366
+ }
367
+ }
368
+
369
+ // For fuzzy/stem mode without boolean query, check section content
370
+ if (useFuzzyOrStem && !parsedQuery && options.content) {
371
+ if (
372
+ !matchesWithOptions(options.content, sectionContent, matchOptions)
373
+ ) {
374
+ continue // Section doesn't match with fuzzy/stem
375
+ }
376
+ }
377
+
378
+ // Find individual line matches for highlighting
379
+ const matches: ContentMatch[] = []
380
+ const searchRegex = contentRegex || highlightRegex
381
+
382
+ // Use configurable context lines (default to 1 if not specified)
383
+ const contextBefore = options.contextBefore ?? 1
384
+ const contextAfter = options.contextAfter ?? 1
385
+
386
+ // Get query words for fuzzy/stem matching
387
+ const queryWords = options.content
388
+ ? options.content
389
+ .toLowerCase()
390
+ .split(/\W+/)
391
+ .filter((w) => w.length > 0)
392
+ : []
393
+
394
+ for (let i = 0; i < sectionLines.length; i++) {
395
+ const line = sectionLines[i]
396
+ if (!line) continue
397
+
398
+ let isMatch = false
399
+
400
+ // Check with regex for exact match mode
401
+ if (searchRegex) {
402
+ if (searchRegex.test(line)) {
403
+ isMatch = true
404
+ }
405
+ // Reset regex lastIndex for next test
406
+ searchRegex.lastIndex = 0
407
+ }
408
+
409
+ // Check with fuzzy/stem matching
410
+ if (!isMatch && useFuzzyOrStem && queryWords.length > 0) {
411
+ const lineMatches = findMatchesInLine(
412
+ queryWords,
413
+ line,
414
+ matchOptions,
415
+ )
416
+ if (lineMatches.length > 0) {
417
+ isMatch = true
418
+ }
419
+ }
420
+
421
+ if (isMatch) {
422
+ const absoluteLineNum = section.startLine + i
423
+
424
+ // Create snippet with configurable context
425
+ const snippetStart = Math.max(0, i - contextBefore)
426
+ const snippetEnd = Math.min(
427
+ sectionLines.length,
428
+ i + contextAfter + 1,
429
+ )
430
+ const snippetLines = sectionLines.slice(snippetStart, snippetEnd)
431
+ const snippet = snippetLines.join('\n')
432
+
433
+ // Build context lines array for JSON output
434
+ const contextLines: ContextLine[] = []
435
+ for (let j = snippetStart; j < snippetEnd; j++) {
436
+ const ctxLine = sectionLines[j]
437
+ if (ctxLine !== undefined) {
438
+ contextLines.push({
439
+ lineNumber: section.startLine + j,
440
+ line: ctxLine,
441
+ isMatch: j === i,
442
+ })
443
+ }
444
+ }
445
+
446
+ matches.push({
447
+ lineNumber: absoluteLineNum,
448
+ line: line,
449
+ snippet,
450
+ contextLines,
451
+ })
452
+ }
453
+ }
454
+
455
+ // For boolean queries, include section even without line-level matches
456
+ // (the section matched as a whole)
457
+ if (parsedQuery || matches.length > 0) {
458
+ const result: SearchResult = {
459
+ section,
460
+ document,
461
+ sectionContent,
462
+ }
463
+ if (matches.length > 0) {
464
+ results.push({ ...result, matches })
465
+ } else {
466
+ results.push(result)
467
+ }
468
+
469
+ if (
470
+ options.limit !== undefined &&
471
+ results.length >= options.limit
472
+ ) {
473
+ return results
474
+ }
475
+ }
476
+ } else if (!parsedQuery && !contentRegex && !useFuzzyOrStem) {
477
+ // No content search, heading-only search
478
+ results.push({ section, document })
479
+
480
+ if (options.limit !== undefined && results.length >= options.limit) {
481
+ return results
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ return results
488
+ })
489
+
490
+ // ============================================================================
491
+ // Search with Content (legacy, uses heading-only search)
492
+ // ============================================================================
493
+
494
+ /**
495
+ * Search for sections by metadata and include section content.
496
+ *
497
+ * @param rootPath - Root directory containing indexed markdown files
498
+ * @param options - Search filters
499
+ * @returns Matching sections with content
500
+ *
501
+ * @throws FileReadError - Cannot read index or source files
502
+ * @throws IndexCorruptedError - Index files are corrupted
503
+ */
504
+ export const searchWithContent = (
505
+ rootPath: string,
506
+ options: SearchOptions = {},
507
+ ): Effect.Effect<
508
+ readonly SearchResult[],
509
+ FileReadError | IndexCorruptedError
510
+ > =>
511
+ Effect.gen(function* () {
512
+ const storage = createStorage(rootPath)
513
+ const results = yield* search(rootPath, options)
514
+
515
+ const resultsWithContent: SearchResult[] = []
516
+
517
+ for (const result of results) {
518
+ const filePath = path.join(storage.rootPath, result.section.documentPath)
519
+
520
+ try {
521
+ const fileContent = yield* Effect.promise(() =>
522
+ fs.readFile(filePath, 'utf-8'),
523
+ )
524
+
525
+ const lines = fileContent.split('\n')
526
+ const sectionContent = lines
527
+ .slice(result.section.startLine - 1, result.section.endLine)
528
+ .join('\n')
529
+
530
+ resultsWithContent.push({
531
+ ...result,
532
+ sectionContent,
533
+ })
534
+ } catch {
535
+ // If file can't be read, include result without content
536
+ resultsWithContent.push(result)
537
+ }
538
+ }
539
+
540
+ return resultsWithContent
541
+ })
542
+
543
+ // ============================================================================
544
+ // Context Generation
545
+ // ============================================================================
546
+
547
+ export interface ContextOptions {
548
+ /** Maximum tokens to include */
549
+ readonly maxTokens?: number | undefined
550
+ /** Include section content */
551
+ readonly includeContent?: boolean | undefined
552
+ /** Compression level: brief, summary, full */
553
+ readonly level?: 'brief' | 'summary' | 'full' | undefined
554
+ }
555
+
556
+ export interface DocumentContext {
557
+ readonly path: string
558
+ readonly title: string
559
+ readonly totalTokens: number
560
+ readonly includedTokens: number
561
+ readonly sections: readonly SectionContext[]
562
+ }
563
+
564
+ export interface SectionContext {
565
+ readonly heading: string
566
+ readonly level: number
567
+ readonly tokens: number
568
+ readonly content?: string | undefined
569
+ readonly hasCode: boolean
570
+ readonly hasList: boolean
571
+ readonly hasTable: boolean
572
+ }
573
+
574
+ /**
575
+ * Get context information for a document.
576
+ *
577
+ * @param rootPath - Root directory containing indexed markdown files
578
+ * @param filePath - Path to the document
579
+ * @param options - Context options (max tokens, include content)
580
+ * @returns Document context with sections
581
+ *
582
+ * @throws IndexNotFoundError - Index doesn't exist
583
+ * @throws DocumentNotFoundError - Document not in index
584
+ * @throws FileReadError - Cannot read index or source files
585
+ * @throws IndexCorruptedError - Index files are corrupted
586
+ */
587
+ export const getContext = (
588
+ rootPath: string,
589
+ filePath: string,
590
+ options: ContextOptions = {},
591
+ ): Effect.Effect<
592
+ DocumentContext,
593
+ | IndexNotFoundError
594
+ | DocumentNotFoundError
595
+ | FileReadError
596
+ | IndexCorruptedError
597
+ > =>
598
+ Effect.gen(function* () {
599
+ const storage = createStorage(rootPath)
600
+ const resolvedFile = path.resolve(filePath)
601
+ const relativePath = path.relative(storage.rootPath, resolvedFile)
602
+
603
+ const docIndex = yield* loadDocumentIndex(storage)
604
+ const sectionIndex = yield* loadSectionIndex(storage)
605
+
606
+ if (!docIndex || !sectionIndex) {
607
+ return yield* Effect.fail(
608
+ new IndexNotFoundError({ path: storage.rootPath }),
609
+ )
610
+ }
611
+
612
+ const document = docIndex.documents[relativePath]
613
+ if (!document) {
614
+ return yield* Effect.fail(
615
+ new DocumentNotFoundError({
616
+ path: relativePath,
617
+ indexPath: storage.rootPath,
618
+ }),
619
+ )
620
+ }
621
+
622
+ // Get sections for this document
623
+ const sectionIds = sectionIndex.byDocument[document.id] ?? []
624
+ const sections: SectionContext[] = []
625
+ let includedTokens = 0
626
+ const maxTokens = options.maxTokens ?? Infinity
627
+ const includeContent = options.includeContent ?? options.level === 'full'
628
+
629
+ // Read file content if needed
630
+ let fileContent: string | null = null
631
+ if (includeContent) {
632
+ try {
633
+ fileContent = yield* Effect.promise(() =>
634
+ fs.readFile(resolvedFile, 'utf-8'),
635
+ )
636
+ } catch {
637
+ // Continue without content
638
+ }
639
+ }
640
+
641
+ const fileLines = fileContent?.split('\n') ?? []
642
+
643
+ for (const sectionId of sectionIds) {
644
+ const section = sectionIndex.sections[sectionId]
645
+ if (!section) continue
646
+
647
+ // Check token budget
648
+ if (includedTokens + section.tokenCount > maxTokens) {
649
+ // Include brief info only if we're over budget
650
+ if (options.level === 'brief') continue
651
+
652
+ sections.push({
653
+ heading: section.heading,
654
+ level: section.level,
655
+ tokens: section.tokenCount,
656
+ hasCode: section.hasCode,
657
+ hasList: section.hasList,
658
+ hasTable: section.hasTable,
659
+ })
660
+ continue
661
+ }
662
+
663
+ includedTokens += section.tokenCount
664
+
665
+ let content: string | undefined
666
+ if (includeContent && fileContent) {
667
+ content = fileLines
668
+ .slice(section.startLine - 1, section.endLine)
669
+ .join('\n')
670
+ }
671
+
672
+ sections.push({
673
+ heading: section.heading,
674
+ level: section.level,
675
+ tokens: section.tokenCount,
676
+ content,
677
+ hasCode: section.hasCode,
678
+ hasList: section.hasList,
679
+ hasTable: section.hasTable,
680
+ })
681
+ }
682
+
683
+ return {
684
+ path: relativePath,
685
+ title: document.title,
686
+ totalTokens: document.tokenCount,
687
+ includedTokens,
688
+ sections,
689
+ }
690
+ })
691
+
692
+ // ============================================================================
693
+ // LLM-Ready Output
694
+ // ============================================================================
695
+
696
+ export const formatContextForLLM = (context: DocumentContext): string => {
697
+ const lines: string[] = []
698
+
699
+ lines.push(`# ${context.title}`)
700
+ lines.push(`Path: ${context.path}`)
701
+ lines.push(`Tokens: ${context.includedTokens}/${context.totalTokens}`)
702
+ lines.push('')
703
+
704
+ for (const section of context.sections) {
705
+ const prefix = '#'.repeat(section.level)
706
+ const meta: string[] = []
707
+ if (section.hasCode) meta.push('code')
708
+ if (section.hasList) meta.push('list')
709
+ if (section.hasTable) meta.push('table')
710
+
711
+ const metaStr = meta.length > 0 ? ` [${meta.join(', ')}]` : ''
712
+ lines.push(
713
+ `${prefix} ${section.heading}${metaStr} (${section.tokens} tokens)`,
714
+ )
715
+
716
+ if (section.content) {
717
+ lines.push('')
718
+ lines.push(section.content)
719
+ lines.push('')
720
+ }
721
+ }
722
+
723
+ return lines.join('\n')
724
+ }