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,264 @@
1
+ /**
2
+ * Testing Utilities Unit Tests
3
+ *
4
+ * Tests for the config testing utilities that make it easy to test
5
+ * code that depends on ConfigService.
6
+ */
7
+
8
+ import { Effect } from 'effect'
9
+ import { describe, expect, it } from 'vitest'
10
+ import { defaultConfig } from './schema.js'
11
+ import { ConfigService } from './service.js'
12
+ import {
13
+ runWithConfig,
14
+ runWithConfigSync,
15
+ TestConfigLayer,
16
+ withTestConfig,
17
+ } from './testing.js'
18
+
19
+ describe('Testing Utilities', () => {
20
+ describe('TestConfigLayer', () => {
21
+ it('should provide default configuration values', async () => {
22
+ const program = Effect.gen(function* () {
23
+ return yield* ConfigService
24
+ })
25
+
26
+ const result = await Effect.runPromise(
27
+ program.pipe(Effect.provide(TestConfigLayer)),
28
+ )
29
+
30
+ expect(result).toEqual(defaultConfig)
31
+ })
32
+
33
+ it('should be suitable for most tests', async () => {
34
+ const program = Effect.gen(function* () {
35
+ const config = yield* ConfigService
36
+ return config.index.maxDepth
37
+ })
38
+
39
+ const result = await Effect.runPromise(
40
+ program.pipe(Effect.provide(TestConfigLayer)),
41
+ )
42
+
43
+ expect(result).toBe(10)
44
+ })
45
+ })
46
+
47
+ describe('withTestConfig', () => {
48
+ it('should override specific values', async () => {
49
+ const layer = withTestConfig({
50
+ index: { maxDepth: 5 },
51
+ })
52
+
53
+ const program = Effect.gen(function* () {
54
+ const config = yield* ConfigService
55
+ return config.index.maxDepth
56
+ })
57
+
58
+ const result = await Effect.runPromise(
59
+ program.pipe(Effect.provide(layer)),
60
+ )
61
+
62
+ expect(result).toBe(5)
63
+ })
64
+
65
+ it('should preserve defaults for unspecified values', async () => {
66
+ const layer = withTestConfig({
67
+ index: { maxDepth: 5 },
68
+ })
69
+
70
+ const program = Effect.gen(function* () {
71
+ const config = yield* ConfigService
72
+ return {
73
+ maxDepth: config.index.maxDepth,
74
+ excludePatterns: config.index.excludePatterns,
75
+ defaultLimit: config.search.defaultLimit,
76
+ }
77
+ })
78
+
79
+ const result = await Effect.runPromise(
80
+ program.pipe(Effect.provide(layer)),
81
+ )
82
+
83
+ expect(result.maxDepth).toBe(5)
84
+ expect(result.excludePatterns).toEqual([
85
+ 'node_modules',
86
+ '.git',
87
+ 'dist',
88
+ 'build',
89
+ ])
90
+ expect(result.defaultLimit).toBe(10)
91
+ })
92
+
93
+ it('should allow overriding multiple sections', async () => {
94
+ const layer = withTestConfig({
95
+ index: { maxDepth: 5 },
96
+ output: { debug: true, verbose: true },
97
+ search: { defaultLimit: 20 },
98
+ })
99
+
100
+ const program = Effect.gen(function* () {
101
+ const config = yield* ConfigService
102
+ return {
103
+ maxDepth: config.index.maxDepth,
104
+ debug: config.output.debug,
105
+ verbose: config.output.verbose,
106
+ defaultLimit: config.search.defaultLimit,
107
+ }
108
+ })
109
+
110
+ const result = await Effect.runPromise(
111
+ program.pipe(Effect.provide(layer)),
112
+ )
113
+
114
+ expect(result.maxDepth).toBe(5)
115
+ expect(result.debug).toBe(true)
116
+ expect(result.verbose).toBe(true)
117
+ expect(result.defaultLimit).toBe(20)
118
+ })
119
+ })
120
+
121
+ describe('runWithConfig', () => {
122
+ it('should run effect with default config when no overrides provided', async () => {
123
+ const program = Effect.gen(function* () {
124
+ const config = yield* ConfigService
125
+ return config.index.maxDepth
126
+ })
127
+
128
+ const result = await runWithConfig(program)
129
+
130
+ expect(result).toBe(10)
131
+ })
132
+
133
+ it('should run effect with custom config overrides', async () => {
134
+ const program = Effect.gen(function* () {
135
+ const config = yield* ConfigService
136
+ return config.index.maxDepth
137
+ })
138
+
139
+ const result = await runWithConfig(program, { index: { maxDepth: 5 } })
140
+
141
+ expect(result).toBe(5)
142
+ })
143
+
144
+ it('should work with complex effects', async () => {
145
+ const program = Effect.gen(function* () {
146
+ const config = yield* ConfigService
147
+ return config.index.maxDepth > 5 ? 'deep' : 'shallow'
148
+ })
149
+
150
+ const deepResult = await runWithConfig(program, {
151
+ index: { maxDepth: 10 },
152
+ })
153
+ const shallowResult = await runWithConfig(program, {
154
+ index: { maxDepth: 3 },
155
+ })
156
+
157
+ expect(deepResult).toBe('deep')
158
+ expect(shallowResult).toBe('shallow')
159
+ })
160
+ })
161
+
162
+ describe('runWithConfigSync', () => {
163
+ it('should run effect synchronously with default config', () => {
164
+ const program = Effect.gen(function* () {
165
+ const config = yield* ConfigService
166
+ return config.index.maxDepth
167
+ })
168
+
169
+ const result = runWithConfigSync(program)
170
+
171
+ expect(result).toBe(10)
172
+ })
173
+
174
+ it('should run effect synchronously with custom config', () => {
175
+ const program = Effect.gen(function* () {
176
+ const config = yield* ConfigService
177
+ return config.index.maxDepth
178
+ })
179
+
180
+ const result = runWithConfigSync(program, { index: { maxDepth: 5 } })
181
+
182
+ expect(result).toBe(5)
183
+ })
184
+
185
+ it('should work with pure computations', () => {
186
+ const program = Effect.gen(function* () {
187
+ const config = yield* ConfigService
188
+ const depth = config.index.maxDepth
189
+ const limit = config.search.defaultLimit
190
+ return depth * limit
191
+ })
192
+
193
+ const result = runWithConfigSync(program, {
194
+ index: { maxDepth: 5 },
195
+ search: { defaultLimit: 20 },
196
+ })
197
+
198
+ expect(result).toBe(100)
199
+ })
200
+ })
201
+
202
+ describe('real-world usage patterns', () => {
203
+ it('should enable testing services that depend on config', async () => {
204
+ const indexService = Effect.gen(function* () {
205
+ const config = yield* ConfigService
206
+ return {
207
+ shouldIndex: (depth: number) => depth <= config.index.maxDepth,
208
+ patterns: config.index.excludePatterns,
209
+ }
210
+ })
211
+
212
+ const testLayer = withTestConfig({ index: { maxDepth: 3 } })
213
+
214
+ const service = await Effect.runPromise(
215
+ indexService.pipe(Effect.provide(testLayer)),
216
+ )
217
+
218
+ expect(service.shouldIndex(2)).toBe(true)
219
+ expect(service.shouldIndex(3)).toBe(true)
220
+ expect(service.shouldIndex(4)).toBe(false)
221
+ })
222
+
223
+ it('should support parameterized tests', async () => {
224
+ const program = Effect.gen(function* () {
225
+ const config = yield* ConfigService
226
+ return config.output.format
227
+ })
228
+
229
+ const textResult = await runWithConfig(program, {
230
+ output: { format: 'text' },
231
+ })
232
+ const jsonResult = await runWithConfig(program, {
233
+ output: { format: 'json' },
234
+ })
235
+
236
+ expect(textResult).toBe('text')
237
+ expect(jsonResult).toBe('json')
238
+ })
239
+
240
+ it('should allow testing error conditions', async () => {
241
+ const validateConfig = Effect.gen(function* () {
242
+ const config = yield* ConfigService
243
+ if (
244
+ config.search.minSimilarity < 0 ||
245
+ config.search.minSimilarity > 1
246
+ ) {
247
+ return yield* Effect.fail('Invalid similarity range')
248
+ }
249
+ return config.search.minSimilarity
250
+ })
251
+
252
+ const validResult = await runWithConfig(validateConfig, {
253
+ search: { minSimilarity: 0.5 },
254
+ })
255
+ expect(validResult).toBe(0.5)
256
+
257
+ const invalidLayer = withTestConfig({ search: { minSimilarity: 1.5 } })
258
+ const invalidProgram = validateConfig.pipe(Effect.provide(invalidLayer))
259
+ const invalidResult = await Effect.runPromiseExit(invalidProgram)
260
+
261
+ expect(invalidResult._tag).toBe('Failure')
262
+ })
263
+ })
264
+ })
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Configuration Testing Utilities
3
+ *
4
+ * Provides helpers for testing code that depends on ConfigService.
5
+ * Use these utilities to create isolated test environments without
6
+ * environment pollution.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * ```typescript
11
+ * import { TestConfigLayer, withTestConfig } from './config/testing.js'
12
+ *
13
+ * // Use default test config
14
+ * const result = await Effect.runPromise(
15
+ * myProgram.pipe(Effect.provide(TestConfigLayer))
16
+ * )
17
+ *
18
+ * // Override specific values
19
+ * const result = await Effect.runPromise(
20
+ * myProgram.pipe(Effect.provide(withTestConfig({
21
+ * index: { maxDepth: 5 }
22
+ * })))
23
+ * )
24
+ * ```
25
+ */
26
+
27
+ import { Effect, Layer } from 'effect'
28
+ import { defaultConfig } from './schema.js'
29
+ import {
30
+ ConfigService,
31
+ makeConfigLayerPartial,
32
+ type PartialMdContextConfig,
33
+ } from './service.js'
34
+
35
+ /**
36
+ * Default test configuration layer.
37
+ * Uses all default values - suitable for most tests.
38
+ */
39
+ export const TestConfigLayer: Layer.Layer<ConfigService> = Layer.succeed(
40
+ ConfigService,
41
+ defaultConfig,
42
+ )
43
+
44
+ /**
45
+ * Create a test config layer with specific overrides.
46
+ *
47
+ * @param overrides - Partial config to merge with defaults
48
+ * @returns Layer providing ConfigService with merged config
49
+ *
50
+ * @example
51
+ * const layer = withTestConfig({
52
+ * index: { maxDepth: 5 },
53
+ * output: { debug: true }
54
+ * })
55
+ */
56
+ export const withTestConfig = (
57
+ overrides: PartialMdContextConfig,
58
+ ): Layer.Layer<ConfigService> => makeConfigLayerPartial(overrides)
59
+
60
+ /**
61
+ * Run an Effect with a specific configuration.
62
+ *
63
+ * Convenience function that provides the config layer and runs the effect.
64
+ *
65
+ * @param effect - The Effect to run
66
+ * @param config - Optional partial config overrides
67
+ * @returns Promise with the effect result
68
+ *
69
+ * @example
70
+ * const result = await runWithConfig(
71
+ * Effect.gen(function* () {
72
+ * const config = yield* ConfigService
73
+ * return config.index.maxDepth
74
+ * }),
75
+ * { index: { maxDepth: 5 } }
76
+ * )
77
+ * // result === 5
78
+ */
79
+ export const runWithConfig = <A, E>(
80
+ effect: Effect.Effect<A, E, ConfigService>,
81
+ config?: PartialMdContextConfig,
82
+ ): Promise<A> => {
83
+ const layer = config ? withTestConfig(config) : TestConfigLayer
84
+ return Effect.runPromise(effect.pipe(Effect.provide(layer)))
85
+ }
86
+
87
+ /**
88
+ * Run an Effect with a specific configuration synchronously.
89
+ *
90
+ * @param effect - The Effect to run
91
+ * @param config - Optional partial config overrides
92
+ * @returns The effect result
93
+ *
94
+ * @example
95
+ * const result = runWithConfigSync(
96
+ * Effect.gen(function* () {
97
+ * const config = yield* ConfigService
98
+ * return config.index.maxDepth
99
+ * }),
100
+ * { index: { maxDepth: 5 } }
101
+ * )
102
+ * // result === 5
103
+ */
104
+ export const runWithConfigSync = <A, E>(
105
+ effect: Effect.Effect<A, E, ConfigService>,
106
+ config?: PartialMdContextConfig,
107
+ ): A => {
108
+ const layer = config ? withTestConfig(config) : TestConfigLayer
109
+ return Effect.runSync(effect.pipe(Effect.provide(layer)))
110
+ }
@@ -0,0 +1 @@
1
+ export * from './types.js'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Core data types for mdcontext
3
+ * Based on DESIGN.md specifications
4
+ */
5
+
6
+ // ============================================================================
7
+ // Document Types
8
+ // ============================================================================
9
+
10
+ export interface MdDocument {
11
+ readonly id: string
12
+ readonly path: string
13
+ readonly title: string
14
+ readonly frontmatter: Record<string, unknown>
15
+ readonly sections: readonly MdSection[]
16
+ readonly links: readonly MdLink[]
17
+ readonly codeBlocks: readonly MdCodeBlock[]
18
+ readonly metadata: DocumentMetadata
19
+ }
20
+
21
+ export interface DocumentMetadata {
22
+ readonly wordCount: number
23
+ readonly tokenCount: number
24
+ readonly headingCount: number
25
+ readonly linkCount: number
26
+ readonly codeBlockCount: number
27
+ readonly lastModified: Date
28
+ readonly indexedAt: Date
29
+ }
30
+
31
+ // ============================================================================
32
+ // Section Types
33
+ // ============================================================================
34
+
35
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
36
+
37
+ export interface MdSection {
38
+ readonly id: string
39
+ readonly heading: string
40
+ readonly level: HeadingLevel
41
+ readonly content: string
42
+ readonly plainText: string
43
+ readonly startLine: number
44
+ readonly endLine: number
45
+ readonly children: readonly MdSection[]
46
+ readonly metadata: SectionMetadata
47
+ }
48
+
49
+ export interface SectionMetadata {
50
+ readonly wordCount: number
51
+ readonly tokenCount: number
52
+ readonly hasCode: boolean
53
+ readonly hasList: boolean
54
+ readonly hasTable: boolean
55
+ }
56
+
57
+ // ============================================================================
58
+ // Link Types
59
+ // ============================================================================
60
+
61
+ export type LinkType = 'internal' | 'external' | 'image'
62
+
63
+ export interface MdLink {
64
+ readonly type: LinkType
65
+ readonly href: string
66
+ readonly text: string
67
+ readonly sectionId: string
68
+ readonly line: number
69
+ }
70
+
71
+ // ============================================================================
72
+ // Code Block Types
73
+ // ============================================================================
74
+
75
+ export interface MdCodeBlock {
76
+ readonly language: string | null
77
+ readonly content: string
78
+ readonly sectionId: string
79
+ readonly startLine: number
80
+ readonly endLine: number
81
+ }
82
+
83
+ // ============================================================================
84
+ // Error Types
85
+ // ============================================================================
86
+
87
+ /**
88
+ * Parse error from markdown parsing
89
+ *
90
+ * Note: This interface is used by parser.ts. For the TaggedError version
91
+ * that works with Effect's error handling, see src/errors/index.ts ParseError.
92
+ */
93
+ export interface ParseError {
94
+ readonly _tag: 'ParseError'
95
+ readonly message: string
96
+ readonly line?: number | undefined
97
+ readonly column?: number | undefined
98
+ }
99
+
100
+ // ============================================================================
101
+ // Constructor Functions
102
+ // ============================================================================
103
+
104
+ export const ParseError = (
105
+ message: string,
106
+ line?: number,
107
+ column?: number,
108
+ ): ParseError => ({
109
+ _tag: 'ParseError',
110
+ message,
111
+ line,
112
+ column,
113
+ })
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Tests for duplicate content detection
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import {
7
+ collapseDuplicates,
8
+ type DuplicateGroup,
9
+ type DuplicateSectionInfo,
10
+ } from './detector.js'
11
+
12
+ // ============================================================================
13
+ // Test Data
14
+ // ============================================================================
15
+
16
+ const makeSectionInfo = (
17
+ id: string,
18
+ path: string,
19
+ heading: string,
20
+ ): DuplicateSectionInfo => ({
21
+ sectionId: id,
22
+ documentPath: path,
23
+ heading,
24
+ startLine: 1,
25
+ endLine: 10,
26
+ tokenCount: 100,
27
+ })
28
+
29
+ const makeGroup = (
30
+ primary: DuplicateSectionInfo,
31
+ duplicates: DuplicateSectionInfo[],
32
+ ): DuplicateGroup => ({
33
+ primary,
34
+ duplicates,
35
+ method: 'exact',
36
+ similarity: 1.0,
37
+ })
38
+
39
+ // ============================================================================
40
+ // collapseDuplicates Tests
41
+ // ============================================================================
42
+
43
+ describe('collapseDuplicates', () => {
44
+ it('returns all results when no duplicate groups', () => {
45
+ const results = [
46
+ { sectionId: 'a', documentPath: 'doc1.md', score: 0.9 },
47
+ { sectionId: 'b', documentPath: 'doc2.md', score: 0.8 },
48
+ { sectionId: 'c', documentPath: 'doc3.md', score: 0.7 },
49
+ ]
50
+ const groups: DuplicateGroup[] = []
51
+
52
+ const collapsed = collapseDuplicates(results, groups)
53
+
54
+ expect(collapsed.length).toBe(3)
55
+ expect(collapsed[0]?.result.sectionId).toBe('a')
56
+ expect(collapsed[0]?.duplicateCount).toBe(0)
57
+ expect(collapsed[1]?.result.sectionId).toBe('b')
58
+ expect(collapsed[2]?.result.sectionId).toBe('c')
59
+ })
60
+
61
+ it('collapses duplicates and keeps primary', () => {
62
+ const section1 = makeSectionInfo('a', 'doc1.md', 'Section A')
63
+ const section2 = makeSectionInfo('b', 'doc2.md', 'Section A (copy)')
64
+
65
+ const results = [
66
+ { sectionId: 'a', documentPath: 'doc1.md', score: 0.9 },
67
+ { sectionId: 'b', documentPath: 'doc2.md', score: 0.8 },
68
+ ]
69
+ const groups = [makeGroup(section1, [section2])]
70
+
71
+ const collapsed = collapseDuplicates(results, groups)
72
+
73
+ expect(collapsed.length).toBe(1)
74
+ expect(collapsed[0]?.result.sectionId).toBe('a')
75
+ expect(collapsed[0]?.duplicateCount).toBe(1)
76
+ })
77
+
78
+ it('collapses when duplicate appears first', () => {
79
+ const section1 = makeSectionInfo('a', 'doc1.md', 'Section A')
80
+ const section2 = makeSectionInfo('b', 'doc2.md', 'Section A (copy)')
81
+
82
+ // Duplicate appears first in results
83
+ const results = [
84
+ { sectionId: 'b', documentPath: 'doc2.md', score: 0.9 },
85
+ { sectionId: 'a', documentPath: 'doc1.md', score: 0.8 },
86
+ ]
87
+ const groups = [makeGroup(section1, [section2])]
88
+
89
+ const collapsed = collapseDuplicates(results, groups)
90
+
91
+ // Should keep the first result (b), not the primary (a)
92
+ expect(collapsed.length).toBe(1)
93
+ expect(collapsed[0]?.result.sectionId).toBe('b')
94
+ expect(collapsed[0]?.duplicateCount).toBe(1)
95
+ })
96
+
97
+ it('includes duplicate locations when showLocations is true', () => {
98
+ const section1 = makeSectionInfo('a', 'doc1.md', 'Section A')
99
+ const section2 = makeSectionInfo('b', 'doc2.md', 'Section A (copy)')
100
+ const section3 = makeSectionInfo('c', 'doc3.md', 'Section A (copy 2)')
101
+
102
+ const results = [{ sectionId: 'a', documentPath: 'doc1.md', score: 0.9 }]
103
+ const groups = [makeGroup(section1, [section2, section3])]
104
+
105
+ const collapsed = collapseDuplicates(results, groups, {
106
+ showLocations: true,
107
+ })
108
+
109
+ expect(collapsed.length).toBe(1)
110
+ expect(collapsed[0]?.duplicateCount).toBe(2)
111
+ expect(collapsed[0]?.duplicateLocations).toBeDefined()
112
+ expect(collapsed[0]?.duplicateLocations?.length).toBe(2)
113
+ expect(collapsed[0]?.duplicateLocations?.[0]?.documentPath).toBe('doc2.md')
114
+ expect(collapsed[0]?.duplicateLocations?.[1]?.documentPath).toBe('doc3.md')
115
+ })
116
+
117
+ it('respects maxLocations option', () => {
118
+ const section1 = makeSectionInfo('a', 'doc1.md', 'Section A')
119
+ const section2 = makeSectionInfo('b', 'doc2.md', 'Copy 1')
120
+ const section3 = makeSectionInfo('c', 'doc3.md', 'Copy 2')
121
+ const section4 = makeSectionInfo('d', 'doc4.md', 'Copy 3')
122
+ const section5 = makeSectionInfo('e', 'doc5.md', 'Copy 4')
123
+
124
+ const results = [{ sectionId: 'a', documentPath: 'doc1.md', score: 0.9 }]
125
+ const groups = [
126
+ makeGroup(section1, [section2, section3, section4, section5]),
127
+ ]
128
+
129
+ const collapsed = collapseDuplicates(results, groups, {
130
+ showLocations: true,
131
+ maxLocations: 2,
132
+ })
133
+
134
+ expect(collapsed[0]?.duplicateCount).toBe(4)
135
+ expect(collapsed[0]?.duplicateLocations?.length).toBe(2)
136
+ })
137
+
138
+ it('handles multiple duplicate groups', () => {
139
+ const sectionA1 = makeSectionInfo('a1', 'doc1.md', 'Section A')
140
+ const sectionA2 = makeSectionInfo('a2', 'doc2.md', 'Section A copy')
141
+ const sectionB1 = makeSectionInfo('b1', 'doc3.md', 'Section B')
142
+ const sectionB2 = makeSectionInfo('b2', 'doc4.md', 'Section B copy')
143
+
144
+ const results = [
145
+ { sectionId: 'a1', documentPath: 'doc1.md', score: 0.9 },
146
+ { sectionId: 'b1', documentPath: 'doc3.md', score: 0.8 },
147
+ { sectionId: 'a2', documentPath: 'doc2.md', score: 0.7 },
148
+ { sectionId: 'b2', documentPath: 'doc4.md', score: 0.6 },
149
+ ]
150
+ const groups = [
151
+ makeGroup(sectionA1, [sectionA2]),
152
+ makeGroup(sectionB1, [sectionB2]),
153
+ ]
154
+
155
+ const collapsed = collapseDuplicates(results, groups)
156
+
157
+ expect(collapsed.length).toBe(2)
158
+ expect(collapsed[0]?.result.sectionId).toBe('a1')
159
+ expect(collapsed[0]?.duplicateCount).toBe(1)
160
+ expect(collapsed[1]?.result.sectionId).toBe('b1')
161
+ expect(collapsed[1]?.duplicateCount).toBe(1)
162
+ })
163
+
164
+ it('handles empty results', () => {
165
+ const collapsed = collapseDuplicates([], [])
166
+ expect(collapsed.length).toBe(0)
167
+ })
168
+
169
+ it('does not include locations when showLocations is false', () => {
170
+ const section1 = makeSectionInfo('a', 'doc1.md', 'Section A')
171
+ const section2 = makeSectionInfo('b', 'doc2.md', 'Section A copy')
172
+
173
+ const results = [{ sectionId: 'a', documentPath: 'doc1.md', score: 0.9 }]
174
+ const groups = [makeGroup(section1, [section2])]
175
+
176
+ const collapsed = collapseDuplicates(results, groups, {
177
+ showLocations: false,
178
+ })
179
+
180
+ expect(collapsed[0]?.duplicateCount).toBe(1)
181
+ expect(collapsed[0]?.duplicateLocations).toBeUndefined()
182
+ })
183
+ })