mdcontext 0.1.0 → 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 (251) hide show
  1. package/.changeset/config.json +9 -9
  2. package/.claude/settings.local.json +25 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +85 -0
  5. package/CONTRIBUTING.md +186 -0
  6. package/NOTES/NOTES +44 -0
  7. package/README.md +206 -3
  8. package/biome.json +1 -1
  9. package/dist/chunk-23UPXDNL.js +3044 -0
  10. package/dist/chunk-2W7MO2DL.js +1366 -0
  11. package/dist/chunk-3NUAZGMA.js +1689 -0
  12. package/dist/chunk-7TOWB2XB.js +366 -0
  13. package/dist/chunk-7XOTOADQ.js +3065 -0
  14. package/dist/chunk-AH2PDM2K.js +3042 -0
  15. package/dist/chunk-BNXWSZ63.js +3742 -0
  16. package/dist/chunk-BTL5DJVU.js +3222 -0
  17. package/dist/chunk-HDHYG7E4.js +104 -0
  18. package/dist/chunk-HLR4KZBP.js +3234 -0
  19. package/dist/chunk-IP3FRFEB.js +1045 -0
  20. package/dist/chunk-KHU56VDO.js +3042 -0
  21. package/dist/chunk-KRYIFLQR.js +85 -89
  22. package/dist/chunk-LBSDNLEM.js +287 -0
  23. package/dist/chunk-MNTQ7HCP.js +2643 -0
  24. package/dist/chunk-MUJELQQ6.js +1387 -0
  25. package/dist/chunk-MXJGMSLV.js +2199 -0
  26. package/dist/chunk-N6QJGC3Z.js +2636 -0
  27. package/dist/chunk-OBELGBPM.js +1713 -0
  28. package/dist/chunk-OT7R5XTA.js +3192 -0
  29. package/dist/chunk-P7X4RA2T.js +106 -0
  30. package/dist/chunk-PIDUQNC2.js +3185 -0
  31. package/dist/chunk-POGCDIH4.js +3187 -0
  32. package/dist/chunk-PSIEOQGZ.js +3043 -0
  33. package/dist/chunk-PVRT3IHA.js +3238 -0
  34. package/dist/chunk-QNN4TT23.js +1430 -0
  35. package/dist/chunk-RE3R45RJ.js +3042 -0
  36. package/dist/chunk-S7E6TFX6.js +718 -657
  37. package/dist/chunk-SG6GLU4U.js +1378 -0
  38. package/dist/chunk-SJCDV2ST.js +274 -0
  39. package/dist/chunk-SYE5XLF3.js +104 -0
  40. package/dist/chunk-T5VLYBZD.js +103 -0
  41. package/dist/chunk-TOQB7VWU.js +3238 -0
  42. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  43. package/dist/chunk-VVTGZNBT.js +1533 -1423
  44. package/dist/chunk-W7Q4RFEV.js +104 -0
  45. package/dist/chunk-XTYYVRLO.js +3190 -0
  46. package/dist/chunk-Y6MDYVJD.js +3063 -0
  47. package/dist/cli/main.js +4072 -629
  48. package/dist/index.d.ts +420 -33
  49. package/dist/index.js +8 -15
  50. package/dist/mcp/server.js +103 -7
  51. package/dist/schema-BAWSG7KY.js +22 -0
  52. package/dist/schema-E3QUPL26.js +20 -0
  53. package/dist/schema-EHL7WUT6.js +20 -0
  54. package/docs/019-USAGE.md +44 -5
  55. package/docs/020-current-implementation.md +8 -8
  56. package/docs/021-DOGFOODING-FINDINGS.md +1 -1
  57. package/docs/CONFIG.md +1123 -0
  58. package/docs/ERRORS.md +383 -0
  59. package/docs/summarization.md +320 -0
  60. package/justfile +40 -0
  61. package/package.json +39 -33
  62. package/research/INDEX.md +315 -0
  63. package/research/code-review/README.md +90 -0
  64. package/research/code-review/cli-error-handling-review.md +979 -0
  65. package/research/code-review/code-review-validation-report.md +464 -0
  66. package/research/code-review/main-ts-review.md +1128 -0
  67. package/research/config-docs/SUMMARY.md +357 -0
  68. package/research/config-docs/TEST-RESULTS.md +776 -0
  69. package/research/config-docs/TODO.md +542 -0
  70. package/research/config-docs/analysis.md +744 -0
  71. package/research/config-docs/fix-validation.md +502 -0
  72. package/research/config-docs/help-audit.md +264 -0
  73. package/research/config-docs/help-system-analysis.md +890 -0
  74. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  75. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  76. package/research/issue-review.md +603 -0
  77. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  78. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  79. package/research/llm-summarization/anthropic-2026.md +367 -0
  80. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  81. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  82. package/research/llm-summarization/openai-2026.md +473 -0
  83. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  84. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  85. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  86. package/research/llm-summarization/prototype-results.md +56 -0
  87. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  88. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  89. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  90. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  91. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  92. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  93. package/research/mdcontext-pudding/02-search.md +970 -0
  94. package/research/mdcontext-pudding/03-context.md +779 -0
  95. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  96. package/research/mdcontext-pudding/04-tree.md +704 -0
  97. package/research/mdcontext-pudding/05-config.md +1038 -0
  98. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  99. package/research/mdcontext-pudding/06-links.md +679 -0
  100. package/research/mdcontext-pudding/07-stats.md +693 -0
  101. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  102. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  103. package/research/mdcontext-pudding/README.md +168 -0
  104. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  105. package/research/research-quality-review.md +834 -0
  106. package/research/semantic-search/embedding-text-analysis.md +156 -0
  107. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  108. package/research/semantic-search/query-processing-analysis.md +207 -0
  109. package/research/semantic-search/root-cause-and-solution.md +114 -0
  110. package/research/semantic-search/threshold-validation-report.md +69 -0
  111. package/research/semantic-search/vector-search-analysis.md +63 -0
  112. package/research/test-path-issues.md +276 -0
  113. package/review/ALP-76/1-error-type-design.md +962 -0
  114. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  115. package/review/ALP-76/3-error-presentation.md +624 -0
  116. package/review/ALP-76/4-test-coverage.md +625 -0
  117. package/review/ALP-76/5-migration-completeness.md +440 -0
  118. package/review/ALP-76/6-effect-best-practices.md +755 -0
  119. package/scripts/apply-branch-protection.sh +47 -0
  120. package/scripts/branch-protection-templates.json +79 -0
  121. package/scripts/prototype-summarization.ts +346 -0
  122. package/scripts/rebuild-hnswlib.js +32 -37
  123. package/scripts/setup-branch-protection.sh +64 -0
  124. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  125. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  126. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  127. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  128. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  129. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  130. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  131. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  132. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  133. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  134. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  135. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  136. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  137. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  138. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  139. package/src/cli/argv-preprocessor.test.ts +2 -2
  140. package/src/cli/cli.test.ts +230 -33
  141. package/src/cli/commands/config-cmd.ts +642 -0
  142. package/src/cli/commands/context.ts +97 -9
  143. package/src/cli/commands/duplicates.ts +122 -0
  144. package/src/cli/commands/embeddings.ts +529 -0
  145. package/src/cli/commands/index-cmd.ts +210 -30
  146. package/src/cli/commands/index.ts +3 -0
  147. package/src/cli/commands/search.ts +894 -64
  148. package/src/cli/commands/stats.ts +3 -0
  149. package/src/cli/commands/tree.ts +26 -5
  150. package/src/cli/config-layer.ts +176 -0
  151. package/src/cli/error-handler.test.ts +235 -0
  152. package/src/cli/error-handler.ts +655 -0
  153. package/src/cli/flag-schemas.ts +66 -0
  154. package/src/cli/help.ts +209 -7
  155. package/src/cli/main.ts +348 -58
  156. package/src/cli/options.ts +10 -0
  157. package/src/cli/shared-error-handling.ts +199 -0
  158. package/src/cli/utils.ts +150 -17
  159. package/src/config/file-provider.test.ts +320 -0
  160. package/src/config/file-provider.ts +273 -0
  161. package/src/config/index.ts +72 -0
  162. package/src/config/integration.test.ts +667 -0
  163. package/src/config/precedence.test.ts +277 -0
  164. package/src/config/precedence.ts +451 -0
  165. package/src/config/schema.test.ts +414 -0
  166. package/src/config/schema.ts +603 -0
  167. package/src/config/service.test.ts +320 -0
  168. package/src/config/service.ts +243 -0
  169. package/src/config/testing.test.ts +264 -0
  170. package/src/config/testing.ts +110 -0
  171. package/src/core/types.ts +6 -33
  172. package/src/duplicates/detector.test.ts +183 -0
  173. package/src/duplicates/detector.ts +414 -0
  174. package/src/duplicates/index.ts +18 -0
  175. package/src/embeddings/embedding-namespace.test.ts +300 -0
  176. package/src/embeddings/embedding-namespace.ts +947 -0
  177. package/src/embeddings/heading-boost.test.ts +222 -0
  178. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  179. package/src/embeddings/hyde.test.ts +272 -0
  180. package/src/embeddings/hyde.ts +264 -0
  181. package/src/embeddings/index.ts +2 -0
  182. package/src/embeddings/openai-provider.ts +332 -83
  183. package/src/embeddings/pricing.json +22 -0
  184. package/src/embeddings/provider-constants.ts +204 -0
  185. package/src/embeddings/provider-errors.test.ts +967 -0
  186. package/src/embeddings/provider-errors.ts +565 -0
  187. package/src/embeddings/provider-factory.test.ts +240 -0
  188. package/src/embeddings/provider-factory.ts +225 -0
  189. package/src/embeddings/provider-integration.test.ts +788 -0
  190. package/src/embeddings/query-preprocessing.test.ts +187 -0
  191. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  192. package/src/embeddings/semantic-search.ts +780 -93
  193. package/src/embeddings/types.ts +293 -16
  194. package/src/embeddings/vector-store.ts +486 -77
  195. package/src/embeddings/voyage-provider.ts +313 -0
  196. package/src/errors/errors.test.ts +845 -0
  197. package/src/errors/index.ts +533 -0
  198. package/src/index/ignore-patterns.test.ts +354 -0
  199. package/src/index/ignore-patterns.ts +305 -0
  200. package/src/index/indexer.ts +286 -48
  201. package/src/index/storage.ts +94 -30
  202. package/src/index/types.ts +40 -2
  203. package/src/index/watcher.ts +67 -9
  204. package/src/index.ts +22 -0
  205. package/src/integration/search-keyword.test.ts +678 -0
  206. package/src/mcp/server.ts +135 -6
  207. package/src/parser/parser.ts +18 -19
  208. package/src/parser/section-filter.test.ts +277 -0
  209. package/src/parser/section-filter.ts +125 -3
  210. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  211. package/src/search/bm25-store.ts +366 -0
  212. package/src/search/cross-encoder.test.ts +253 -0
  213. package/src/search/cross-encoder.ts +406 -0
  214. package/src/search/fuzzy-search.test.ts +419 -0
  215. package/src/search/fuzzy-search.ts +273 -0
  216. package/src/search/hybrid-search.ts +448 -0
  217. package/src/search/path-matcher.test.ts +276 -0
  218. package/src/search/path-matcher.ts +33 -0
  219. package/src/search/searcher.test.ts +99 -1
  220. package/src/search/searcher.ts +189 -67
  221. package/src/search/wink-bm25.d.ts +30 -0
  222. package/src/summarization/cli-providers/claude.ts +202 -0
  223. package/src/summarization/cli-providers/detection.test.ts +273 -0
  224. package/src/summarization/cli-providers/detection.ts +118 -0
  225. package/src/summarization/cli-providers/index.ts +8 -0
  226. package/src/summarization/cost.test.ts +139 -0
  227. package/src/summarization/cost.ts +102 -0
  228. package/src/summarization/error-handler.test.ts +127 -0
  229. package/src/summarization/error-handler.ts +111 -0
  230. package/src/summarization/index.ts +102 -0
  231. package/src/summarization/pipeline.test.ts +498 -0
  232. package/src/summarization/pipeline.ts +231 -0
  233. package/src/summarization/prompts.test.ts +269 -0
  234. package/src/summarization/prompts.ts +133 -0
  235. package/src/summarization/provider-factory.test.ts +396 -0
  236. package/src/summarization/provider-factory.ts +178 -0
  237. package/src/summarization/types.ts +184 -0
  238. package/src/summarize/summarizer.ts +104 -35
  239. package/src/types/huggingface-transformers.d.ts +66 -0
  240. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  241. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  242. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  243. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
  244. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
  245. package/tests/integration/embed-index.test.ts +712 -0
  246. package/tests/integration/search-context.test.ts +469 -0
  247. package/tests/integration/search-semantic.test.ts +522 -0
  248. package/vitest.config.ts +1 -6
  249. package/AGENTS.md +0 -46
  250. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  251. package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
@@ -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
+ }
package/src/core/types.ts CHANGED
@@ -84,6 +84,12 @@ export interface MdCodeBlock {
84
84
  // Error Types
85
85
  // ============================================================================
86
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
+ */
87
93
  export interface ParseError {
88
94
  readonly _tag: 'ParseError'
89
95
  readonly message: string
@@ -91,19 +97,6 @@ export interface ParseError {
91
97
  readonly column?: number | undefined
92
98
  }
93
99
 
94
- export interface IoError {
95
- readonly _tag: 'IoError'
96
- readonly message: string
97
- readonly path: string
98
- readonly cause?: unknown
99
- }
100
-
101
- export interface IndexError {
102
- readonly _tag: 'IndexError'
103
- readonly cause: 'DiskFull' | 'Permission' | 'Corrupted' | 'Unknown'
104
- readonly message: string
105
- }
106
-
107
100
  // ============================================================================
108
101
  // Constructor Functions
109
102
  // ============================================================================
@@ -118,23 +111,3 @@ export const ParseError = (
118
111
  line,
119
112
  column,
120
113
  })
121
-
122
- export const IoError = (
123
- message: string,
124
- path: string,
125
- cause?: unknown,
126
- ): IoError => ({
127
- _tag: 'IoError',
128
- message,
129
- path,
130
- cause,
131
- })
132
-
133
- export const IndexError = (
134
- cause: IndexError['cause'],
135
- message: string,
136
- ): IndexError => ({
137
- _tag: 'IndexError',
138
- cause,
139
- message,
140
- })
@@ -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
+ })