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,354 @@
1
+ /**
2
+ * Unit tests for ignore patterns module
3
+ *
4
+ * Tests verify:
5
+ * - Pattern matching (globs, negation, comments, directory-only)
6
+ * - Precedence (CLI > .mdcontextignore > .gitignore > defaults)
7
+ * - Edge cases (missing files, empty files, invalid patterns)
8
+ */
9
+
10
+ import * as fs from 'node:fs/promises'
11
+ import * as os from 'node:os'
12
+ import * as path from 'node:path'
13
+ import { Effect } from 'effect'
14
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
15
+ import {
16
+ createFilterFunction,
17
+ createIgnoreFilter,
18
+ DEFAULT_IGNORE_PATTERNS,
19
+ getChokidarIgnorePatterns,
20
+ shouldIgnore,
21
+ } from './ignore-patterns.js'
22
+
23
+ describe('Ignore Patterns Module', () => {
24
+ let testDir: string
25
+
26
+ beforeEach(async () => {
27
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mdcontext-ignore-test-'))
28
+ })
29
+
30
+ afterEach(async () => {
31
+ await fs.rm(testDir, { recursive: true, force: true })
32
+ })
33
+
34
+ // ==========================================================================
35
+ // Pattern Matching
36
+ // ==========================================================================
37
+
38
+ describe('Pattern Matching', () => {
39
+ it('matches glob patterns (*.log)', async () => {
40
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.log\n')
41
+
42
+ const result = await Effect.runPromise(
43
+ createIgnoreFilter({ rootPath: testDir }),
44
+ )
45
+
46
+ expect(shouldIgnore('debug.log', result.filter)).toBe(true)
47
+ expect(shouldIgnore('app.log', result.filter)).toBe(true)
48
+ expect(shouldIgnore('app.txt', result.filter)).toBe(false)
49
+ })
50
+
51
+ it('matches deep glob patterns (**/*.tmp)', async () => {
52
+ await fs.writeFile(path.join(testDir, '.gitignore'), '**/*.tmp\n')
53
+
54
+ const result = await Effect.runPromise(
55
+ createIgnoreFilter({ rootPath: testDir }),
56
+ )
57
+
58
+ expect(shouldIgnore('file.tmp', result.filter)).toBe(true)
59
+ expect(shouldIgnore('src/file.tmp', result.filter)).toBe(true)
60
+ expect(shouldIgnore('src/deep/file.tmp', result.filter)).toBe(true)
61
+ expect(shouldIgnore('src/file.md', result.filter)).toBe(false)
62
+ })
63
+
64
+ it('handles negation patterns (!important.log)', async () => {
65
+ await fs.writeFile(
66
+ path.join(testDir, '.gitignore'),
67
+ '*.log\n!important.log\n',
68
+ )
69
+
70
+ const result = await Effect.runPromise(
71
+ createIgnoreFilter({ rootPath: testDir }),
72
+ )
73
+
74
+ expect(shouldIgnore('debug.log', result.filter)).toBe(true)
75
+ expect(shouldIgnore('important.log', result.filter)).toBe(false)
76
+ })
77
+
78
+ it('ignores comments (# ignore this)', async () => {
79
+ await fs.writeFile(
80
+ path.join(testDir, '.gitignore'),
81
+ '# This is a comment\n*.log\n# Another comment\n',
82
+ )
83
+
84
+ const result = await Effect.runPromise(
85
+ createIgnoreFilter({ rootPath: testDir }),
86
+ )
87
+
88
+ // Comments should not affect pattern matching
89
+ expect(shouldIgnore('debug.log', result.filter)).toBe(true)
90
+ expect(shouldIgnore('# This is a comment', result.filter)).toBe(false)
91
+ })
92
+
93
+ it('matches directory-only patterns (node_modules/)', async () => {
94
+ await fs.writeFile(path.join(testDir, '.gitignore'), 'build/\n')
95
+
96
+ const result = await Effect.runPromise(
97
+ createIgnoreFilter({ rootPath: testDir }),
98
+ )
99
+
100
+ expect(shouldIgnore('build/output.js', result.filter)).toBe(true)
101
+ expect(shouldIgnore('build', result.filter)).toBe(true)
102
+ })
103
+ })
104
+
105
+ // ==========================================================================
106
+ // Precedence
107
+ // ==========================================================================
108
+
109
+ describe('Precedence', () => {
110
+ it('CLI patterns override .mdcontextignore', async () => {
111
+ await fs.writeFile(
112
+ path.join(testDir, '.mdcontextignore'),
113
+ '*.md\n!important.md\n',
114
+ )
115
+
116
+ const result = await Effect.runPromise(
117
+ createIgnoreFilter({
118
+ rootPath: testDir,
119
+ cliPatterns: ['important.md'], // CLI says ignore it
120
+ }),
121
+ )
122
+
123
+ // CLI takes precedence - important.md should be ignored
124
+ expect(shouldIgnore('important.md', result.filter)).toBe(true)
125
+ })
126
+
127
+ it('.mdcontextignore overrides .gitignore', async () => {
128
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.md\n')
129
+ await fs.writeFile(
130
+ path.join(testDir, '.mdcontextignore'),
131
+ '!README.md\n', // Allow README.md
132
+ )
133
+
134
+ const result = await Effect.runPromise(
135
+ createIgnoreFilter({ rootPath: testDir }),
136
+ )
137
+
138
+ // .mdcontextignore negation should override .gitignore
139
+ expect(shouldIgnore('README.md', result.filter)).toBe(false)
140
+ expect(shouldIgnore('other.md', result.filter)).toBe(true)
141
+ })
142
+
143
+ it('later rules override earlier (gitignore behavior)', async () => {
144
+ await fs.writeFile(
145
+ path.join(testDir, '.gitignore'),
146
+ '*.md\n!important.md\nimportant.md\n', // Re-ignore important.md
147
+ )
148
+
149
+ const result = await Effect.runPromise(
150
+ createIgnoreFilter({ rootPath: testDir }),
151
+ )
152
+
153
+ // Last rule wins - important.md should be ignored again
154
+ expect(shouldIgnore('important.md', result.filter)).toBe(true)
155
+ })
156
+ })
157
+
158
+ // ==========================================================================
159
+ // Edge Cases
160
+ // ==========================================================================
161
+
162
+ describe('Edge Cases', () => {
163
+ it('handles missing .gitignore gracefully', async () => {
164
+ // No .gitignore file exists
165
+ const result = await Effect.runPromise(
166
+ createIgnoreFilter({ rootPath: testDir }),
167
+ )
168
+
169
+ // Should still work with defaults
170
+ expect(result.filter).toBeDefined()
171
+ expect(result.sources).not.toContain('.gitignore')
172
+ expect(shouldIgnore('node_modules/pkg/file.js', result.filter)).toBe(true)
173
+ })
174
+
175
+ it('handles missing .mdcontextignore gracefully', async () => {
176
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.log\n')
177
+ // No .mdcontextignore file exists
178
+
179
+ const result = await Effect.runPromise(
180
+ createIgnoreFilter({ rootPath: testDir }),
181
+ )
182
+
183
+ expect(result.sources).toContain('.gitignore')
184
+ expect(result.sources).not.toContain('.mdcontextignore')
185
+ })
186
+
187
+ it('handles empty files', async () => {
188
+ await fs.writeFile(path.join(testDir, '.gitignore'), '')
189
+ await fs.writeFile(path.join(testDir, '.mdcontextignore'), ' \n\n')
190
+
191
+ const result = await Effect.runPromise(
192
+ createIgnoreFilter({ rootPath: testDir }),
193
+ )
194
+
195
+ // Should work with just defaults
196
+ expect(result.filter).toBeDefined()
197
+ expect(result.sources).not.toContain('.gitignore')
198
+ expect(result.sources).not.toContain('.mdcontextignore')
199
+ })
200
+
201
+ it('handles whitespace-only patterns (skipped)', async () => {
202
+ await fs.writeFile(
203
+ path.join(testDir, '.gitignore'),
204
+ ' \n\n*.log\n \n',
205
+ )
206
+
207
+ const result = await Effect.runPromise(
208
+ createIgnoreFilter({ rootPath: testDir }),
209
+ )
210
+
211
+ // Should skip whitespace lines
212
+ expect(shouldIgnore('debug.log', result.filter)).toBe(true)
213
+ })
214
+ })
215
+
216
+ // ==========================================================================
217
+ // Default Patterns
218
+ // ==========================================================================
219
+
220
+ describe('Default Patterns', () => {
221
+ it('includes node_modules in defaults', () => {
222
+ expect(DEFAULT_IGNORE_PATTERNS).toContain('node_modules')
223
+ })
224
+
225
+ it('includes .git in defaults', () => {
226
+ expect(DEFAULT_IGNORE_PATTERNS).toContain('.git')
227
+ })
228
+
229
+ it('includes dist in defaults', () => {
230
+ expect(DEFAULT_IGNORE_PATTERNS).toContain('dist')
231
+ })
232
+
233
+ it('includes build in defaults', () => {
234
+ expect(DEFAULT_IGNORE_PATTERNS).toContain('build')
235
+ })
236
+
237
+ it('applies defaults without any ignore files', async () => {
238
+ const result = await Effect.runPromise(
239
+ createIgnoreFilter({ rootPath: testDir }),
240
+ )
241
+
242
+ expect(shouldIgnore('node_modules/pkg/index.js', result.filter)).toBe(
243
+ true,
244
+ )
245
+ expect(shouldIgnore('dist/bundle.js', result.filter)).toBe(true)
246
+ expect(shouldIgnore('build/output.js', result.filter)).toBe(true)
247
+ expect(shouldIgnore('.git/config', result.filter)).toBe(true)
248
+ })
249
+ })
250
+
251
+ // ==========================================================================
252
+ // Honor Flags
253
+ // ==========================================================================
254
+
255
+ describe('Honor Flags', () => {
256
+ it('respects honorGitignore=false', async () => {
257
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.secret\n')
258
+
259
+ const result = await Effect.runPromise(
260
+ createIgnoreFilter({
261
+ rootPath: testDir,
262
+ honorGitignore: false,
263
+ }),
264
+ )
265
+
266
+ // .gitignore patterns should not be applied
267
+ expect(shouldIgnore('password.secret', result.filter)).toBe(false)
268
+ expect(result.sources).not.toContain('.gitignore')
269
+ })
270
+
271
+ it('respects honorMdcontextignore=false', async () => {
272
+ await fs.writeFile(path.join(testDir, '.mdcontextignore'), 'drafts/\n')
273
+
274
+ const result = await Effect.runPromise(
275
+ createIgnoreFilter({
276
+ rootPath: testDir,
277
+ honorMdcontextignore: false,
278
+ }),
279
+ )
280
+
281
+ // .mdcontextignore patterns should not be applied
282
+ expect(shouldIgnore('drafts/doc.md', result.filter)).toBe(false)
283
+ expect(result.sources).not.toContain('.mdcontextignore')
284
+ })
285
+ })
286
+
287
+ // ==========================================================================
288
+ // Filter Function
289
+ // ==========================================================================
290
+
291
+ describe('createFilterFunction', () => {
292
+ it('creates a function suitable for Array.filter', async () => {
293
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.log\n')
294
+
295
+ const result = await Effect.runPromise(
296
+ createIgnoreFilter({ rootPath: testDir }),
297
+ )
298
+
299
+ const filterFn = createFilterFunction(result.filter)
300
+ const files = ['app.md', 'debug.log', 'src/index.ts', 'error.log']
301
+ const filtered = files.filter(filterFn)
302
+
303
+ expect(filtered).toEqual(['app.md', 'src/index.ts'])
304
+ })
305
+ })
306
+
307
+ // ==========================================================================
308
+ // Chokidar Patterns
309
+ // ==========================================================================
310
+
311
+ describe('getChokidarIgnorePatterns', () => {
312
+ it('returns patterns suitable for chokidar', async () => {
313
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.log\n')
314
+
315
+ const patterns = await Effect.runPromise(
316
+ getChokidarIgnorePatterns({ rootPath: testDir }),
317
+ )
318
+
319
+ expect(patterns.length).toBeGreaterThan(0)
320
+ // Should include a regex string for dotfiles
321
+ expect(patterns.some((p) => typeof p === 'string')).toBe(true)
322
+ })
323
+
324
+ it('includes default patterns', async () => {
325
+ const patterns = await Effect.runPromise(
326
+ getChokidarIgnorePatterns({ rootPath: testDir }),
327
+ )
328
+
329
+ // Should have patterns for node_modules, etc.
330
+ expect(patterns.some((p) => p.includes('node_modules'))).toBe(true)
331
+ })
332
+ })
333
+
334
+ // ==========================================================================
335
+ // Pattern Count
336
+ // ==========================================================================
337
+
338
+ describe('Pattern Count', () => {
339
+ it('counts patterns from all sources', async () => {
340
+ await fs.writeFile(path.join(testDir, '.gitignore'), '*.log\n*.tmp\n')
341
+ await fs.writeFile(path.join(testDir, '.mdcontextignore'), 'drafts/\n')
342
+
343
+ const result = await Effect.runPromise(
344
+ createIgnoreFilter({
345
+ rootPath: testDir,
346
+ cliPatterns: ['*.bak'],
347
+ }),
348
+ )
349
+
350
+ // 4 defaults + 2 gitignore + 1 mdcontextignore + 1 CLI = 8
351
+ expect(result.patternCount).toBe(8)
352
+ })
353
+ })
354
+ })
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Ignore Pattern Support Module
3
+ *
4
+ * Provides .gitignore and .mdcontextignore support using the battle-tested `ignore` npm package.
5
+ * Implements the following precedence (highest to lowest):
6
+ *
7
+ * 1. CLI --exclude flag
8
+ * 2. MDCONTEXT_INDEX_EXCLUDEPATTERNS env var
9
+ * 3. Config file excludePatterns
10
+ * 4. .mdcontextignore file
11
+ * 5. .gitignore file
12
+ * 6. Built-in defaults: ['node_modules', '.git', 'dist', 'build']
13
+ */
14
+
15
+ import * as fs from 'node:fs/promises'
16
+ import * as path from 'node:path'
17
+ import { Effect } from 'effect'
18
+ import ignore, { type Ignore } from 'ignore'
19
+
20
+ // ============================================================================
21
+ // Types
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Options for building the ignore filter
26
+ */
27
+ export interface IgnoreOptions {
28
+ /** Root directory to search for ignore files */
29
+ readonly rootPath: string
30
+ /** CLI/config exclude patterns (highest priority) */
31
+ readonly cliPatterns?: readonly string[] | undefined
32
+ /** Whether to honor .gitignore (default: true) */
33
+ readonly honorGitignore?: boolean | undefined
34
+ /** Whether to honor .mdcontextignore (default: true) */
35
+ readonly honorMdcontextignore?: boolean | undefined
36
+ }
37
+
38
+ /**
39
+ * Result of loading ignore patterns
40
+ */
41
+ export interface IgnoreFilterResult {
42
+ /** The ignore filter instance */
43
+ readonly filter: Ignore
44
+ /** Source files that were loaded */
45
+ readonly sources: readonly string[]
46
+ /** Total number of patterns loaded */
47
+ readonly patternCount: number
48
+ }
49
+
50
+ // ============================================================================
51
+ // Constants
52
+ // ============================================================================
53
+
54
+ /**
55
+ * Default patterns always applied (lowest priority)
56
+ */
57
+ export const DEFAULT_IGNORE_PATTERNS: readonly string[] = [
58
+ 'node_modules',
59
+ '.git',
60
+ 'dist',
61
+ 'build',
62
+ ]
63
+
64
+ // ============================================================================
65
+ // File Loading
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Try to read an ignore file, returning empty string if it doesn't exist or is unreadable.
70
+ */
71
+ const tryReadIgnoreFile = (filePath: string): Effect.Effect<string, never> =>
72
+ Effect.tryPromise({
73
+ try: () => fs.readFile(filePath, 'utf-8'),
74
+ catch: () => '',
75
+ }).pipe(Effect.catchAll(() => Effect.succeed('')))
76
+
77
+ /**
78
+ * Parse ignore file contents, filtering out empty lines and comments.
79
+ * Returns the number of valid patterns found.
80
+ */
81
+ const countPatterns = (content: string): number => {
82
+ if (!content.trim()) return 0
83
+ return content.split('\n').filter((line) => {
84
+ const trimmed = line.trim()
85
+ return trimmed.length > 0 && !trimmed.startsWith('#')
86
+ }).length
87
+ }
88
+
89
+ // ============================================================================
90
+ // Main API
91
+ // ============================================================================
92
+
93
+ /**
94
+ * Create an ignore filter with proper precedence.
95
+ *
96
+ * Loads patterns from (in order, lower priority first):
97
+ * 1. Built-in defaults
98
+ * 2. .gitignore (if exists and honorGitignore is true)
99
+ * 3. .mdcontextignore (if exists and honorMdcontextignore is true)
100
+ * 4. CLI/config patterns (highest priority)
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const result = yield* createIgnoreFilter({
105
+ * rootPath: '/my/project',
106
+ * cliPatterns: ['*.log', 'temp/'],
107
+ * })
108
+ *
109
+ * // Check if a file should be ignored
110
+ * if (result.filter.ignores('node_modules/package/file.md')) {
111
+ * // Skip this file
112
+ * }
113
+ *
114
+ * // Or filter an array of paths
115
+ * const includedFiles = files.filter(result.filter.createFilter())
116
+ * ```
117
+ */
118
+ export const createIgnoreFilter = (
119
+ options: IgnoreOptions,
120
+ ): Effect.Effect<IgnoreFilterResult, never> =>
121
+ Effect.gen(function* () {
122
+ const {
123
+ rootPath,
124
+ cliPatterns = [],
125
+ honorGitignore = true,
126
+ honorMdcontextignore = true,
127
+ } = options
128
+
129
+ const ig = ignore()
130
+ const sources: string[] = []
131
+ let patternCount = 0
132
+
133
+ // 1. Add defaults (lowest priority)
134
+ ig.add(DEFAULT_IGNORE_PATTERNS as string[])
135
+ patternCount += DEFAULT_IGNORE_PATTERNS.length
136
+
137
+ // 2. Load .gitignore if enabled
138
+ if (honorGitignore) {
139
+ const gitignorePath = path.join(rootPath, '.gitignore')
140
+ const gitignoreContent = yield* tryReadIgnoreFile(gitignorePath)
141
+ if (gitignoreContent.trim()) {
142
+ ig.add(gitignoreContent)
143
+ const count = countPatterns(gitignoreContent)
144
+ patternCount += count
145
+ sources.push('.gitignore')
146
+ }
147
+ }
148
+
149
+ // 3. Load .mdcontextignore if enabled
150
+ if (honorMdcontextignore) {
151
+ const mdcontextignorePath = path.join(rootPath, '.mdcontextignore')
152
+ const mdcontextignoreContent =
153
+ yield* tryReadIgnoreFile(mdcontextignorePath)
154
+ if (mdcontextignoreContent.trim()) {
155
+ ig.add(mdcontextignoreContent)
156
+ const count = countPatterns(mdcontextignoreContent)
157
+ patternCount += count
158
+ sources.push('.mdcontextignore')
159
+ }
160
+ }
161
+
162
+ // 4. Add CLI/config patterns (highest priority)
163
+ if (cliPatterns.length > 0) {
164
+ ig.add(cliPatterns as string[])
165
+ patternCount += cliPatterns.length
166
+ sources.push('CLI/config')
167
+ }
168
+
169
+ return {
170
+ filter: ig,
171
+ sources,
172
+ patternCount,
173
+ }
174
+ })
175
+
176
+ /**
177
+ * Check if a path should be ignored.
178
+ *
179
+ * This is a convenience wrapper around createIgnoreFilter for single-path checks.
180
+ * For checking multiple paths, prefer creating the filter once and reusing it.
181
+ *
182
+ * @param relativePath - Path relative to root (e.g., 'src/foo/bar.md')
183
+ * @param filter - The ignore filter instance
184
+ * @returns true if the path should be ignored
185
+ */
186
+ export const shouldIgnore = (relativePath: string, filter: Ignore): boolean => {
187
+ // The ignore package requires paths without leading slash
188
+ const normalized = relativePath.replace(/^\//, '')
189
+ return filter.ignores(normalized)
190
+ }
191
+
192
+ /**
193
+ * Create a filter function suitable for Array.filter().
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const result = yield* createIgnoreFilter({ rootPath })
198
+ * const filterFn = createFilterFunction(result.filter)
199
+ * const includedFiles = files.filter(filterFn)
200
+ * ```
201
+ */
202
+ export const createFilterFunction = (
203
+ filter: Ignore,
204
+ ): ((relativePath: string) => boolean) => {
205
+ const innerFilter = filter.createFilter()
206
+ return (relativePath: string) => {
207
+ // The ignore package's createFilter returns true for non-ignored files
208
+ const normalized = relativePath.replace(/^\//, '')
209
+ return innerFilter(normalized)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Get ignore patterns as an array of strings for chokidar.
215
+ *
216
+ * Chokidar uses anymatch which accepts globs, so we convert
217
+ * the ignore patterns to glob format.
218
+ *
219
+ * @param options - Ignore options
220
+ * @returns Array of patterns suitable for chokidar's `ignored` option
221
+ */
222
+ export const getChokidarIgnorePatterns = (
223
+ options: IgnoreOptions,
224
+ ): Effect.Effect<string[], never> =>
225
+ Effect.gen(function* () {
226
+ const {
227
+ rootPath,
228
+ cliPatterns = [],
229
+ honorGitignore = true,
230
+ honorMdcontextignore = true,
231
+ } = options
232
+
233
+ const patterns: string[] = []
234
+
235
+ // Always ignore dotfiles (chokidar regex format)
236
+ patterns.push(/(^|[/\\])\./.source)
237
+
238
+ // Add defaults
239
+ for (const p of DEFAULT_IGNORE_PATTERNS) {
240
+ patterns.push(`**/${p}/**`)
241
+ }
242
+
243
+ // Load .gitignore patterns
244
+ if (honorGitignore) {
245
+ const gitignorePath = path.join(rootPath, '.gitignore')
246
+ const content = yield* tryReadIgnoreFile(gitignorePath)
247
+ if (content.trim()) {
248
+ const parsed = parseIgnoreFile(content)
249
+ for (const p of parsed) {
250
+ patterns.push(convertToGlob(p))
251
+ }
252
+ }
253
+ }
254
+
255
+ // Load .mdcontextignore patterns
256
+ if (honorMdcontextignore) {
257
+ const mdcontextignorePath = path.join(rootPath, '.mdcontextignore')
258
+ const content = yield* tryReadIgnoreFile(mdcontextignorePath)
259
+ if (content.trim()) {
260
+ const parsed = parseIgnoreFile(content)
261
+ for (const p of parsed) {
262
+ patterns.push(convertToGlob(p))
263
+ }
264
+ }
265
+ }
266
+
267
+ // Add CLI patterns
268
+ for (const p of cliPatterns) {
269
+ patterns.push(convertToGlob(p))
270
+ }
271
+
272
+ return patterns
273
+ })
274
+
275
+ // ============================================================================
276
+ // Helpers
277
+ // ============================================================================
278
+
279
+ /**
280
+ * Parse ignore file content into individual patterns
281
+ */
282
+ const parseIgnoreFile = (content: string): string[] => {
283
+ return content
284
+ .split('\n')
285
+ .map((line) => line.trim())
286
+ .filter((line) => line.length > 0 && !line.startsWith('#'))
287
+ }
288
+
289
+ /**
290
+ * Convert a gitignore pattern to glob format for chokidar
291
+ */
292
+ const convertToGlob = (pattern: string): string => {
293
+ // Negation patterns - keep as is for now (chokidar handles them differently)
294
+ if (pattern.startsWith('!')) {
295
+ return pattern
296
+ }
297
+
298
+ // Already a glob pattern
299
+ if (pattern.includes('*') || pattern.includes('/')) {
300
+ return pattern.startsWith('/') ? pattern.slice(1) : `**/${pattern}`
301
+ }
302
+
303
+ // Simple name - match anywhere
304
+ return `**/${pattern}/**`
305
+ }
@@ -0,0 +1,4 @@
1
+ export * from './indexer.js'
2
+ export * from './storage.js'
3
+ export * from './types.js'
4
+ export * from './watcher.js'