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,845 @@
1
+ /**
2
+ * Unit tests for error types
3
+ *
4
+ * Tests verify:
5
+ * - Error construction with correct _tag
6
+ * - Error code getter returns correct codes
7
+ * - Error data access (fields)
8
+ * - Cause chain preservation
9
+ * - catchTag matching by _tag
10
+ * - Dynamic message generation
11
+ */
12
+
13
+ import { Effect } from 'effect'
14
+ import { describe, expect, it } from 'vitest'
15
+ import { EXIT_CODE, formatError } from '../cli/error-handler.js'
16
+ import {
17
+ ApiKeyInvalidError,
18
+ ApiKeyMissingError,
19
+ CliValidationError,
20
+ ConfigError,
21
+ DirectoryCreateError,
22
+ DirectoryWalkError,
23
+ DocumentNotFoundError,
24
+ EmbeddingError,
25
+ EmbeddingsNotFoundError,
26
+ ErrorCode,
27
+ FileReadError,
28
+ FileWriteError,
29
+ IndexBuildError,
30
+ IndexCorruptedError,
31
+ IndexNotFoundError,
32
+ ParseError,
33
+ VectorStoreError,
34
+ WatchError,
35
+ } from './index.js'
36
+
37
+ describe('Error Types', () => {
38
+ // ==========================================================================
39
+ // File System Errors
40
+ // ==========================================================================
41
+
42
+ describe('FileReadError', () => {
43
+ it('has correct _tag for catchTag', () => {
44
+ const error = new FileReadError({
45
+ path: '/test/file.md',
46
+ message: 'ENOENT: no such file or directory',
47
+ })
48
+ expect(error._tag).toBe('FileReadError')
49
+ })
50
+
51
+ it('has correct error code', () => {
52
+ const error = new FileReadError({
53
+ path: '/test/file.md',
54
+ message: 'ENOENT',
55
+ })
56
+ expect(error.code).toBe(ErrorCode.FILE_READ)
57
+ expect(error.code).toBe('E100')
58
+ })
59
+
60
+ it('preserves error data fields', () => {
61
+ const error = new FileReadError({
62
+ path: '/test/file.md',
63
+ message: 'Permission denied',
64
+ })
65
+ expect(error.path).toBe('/test/file.md')
66
+ expect(error.message).toBe('Permission denied')
67
+ })
68
+
69
+ it('preserves cause chain', () => {
70
+ const cause = new Error('underlying error')
71
+ const error = new FileReadError({
72
+ path: '/test/file.md',
73
+ message: 'ENOENT',
74
+ cause,
75
+ })
76
+ expect(error.cause).toBe(cause)
77
+ })
78
+
79
+ it('can be caught with catchTag', async () => {
80
+ const effect = Effect.fail(
81
+ new FileReadError({ path: '/test.md', message: 'error' }),
82
+ )
83
+ const result = await Effect.runPromise(
84
+ effect.pipe(
85
+ Effect.catchTag('FileReadError', (e) => Effect.succeed(e.path)),
86
+ ),
87
+ )
88
+ expect(result).toBe('/test.md')
89
+ })
90
+ })
91
+
92
+ describe('FileWriteError', () => {
93
+ it('has correct _tag and error code', () => {
94
+ const error = new FileWriteError({
95
+ path: '/test/output.md',
96
+ message: 'EACCES',
97
+ })
98
+ expect(error._tag).toBe('FileWriteError')
99
+ expect(error.code).toBe(ErrorCode.FILE_WRITE)
100
+ expect(error.code).toBe('E101')
101
+ })
102
+
103
+ it('can be caught with catchTag', async () => {
104
+ const effect = Effect.fail(
105
+ new FileWriteError({ path: '/out.md', message: 'err' }),
106
+ )
107
+ const result = await Effect.runPromise(
108
+ effect.pipe(
109
+ Effect.catchTag('FileWriteError', (e) => Effect.succeed(e.path)),
110
+ ),
111
+ )
112
+ expect(result).toBe('/out.md')
113
+ })
114
+ })
115
+
116
+ describe('DirectoryCreateError', () => {
117
+ it('has correct _tag and error code', () => {
118
+ const error = new DirectoryCreateError({
119
+ path: '/test/dir',
120
+ message: 'EEXIST',
121
+ })
122
+ expect(error._tag).toBe('DirectoryCreateError')
123
+ expect(error.code).toBe(ErrorCode.DIRECTORY_CREATE)
124
+ expect(error.code).toBe('E102')
125
+ })
126
+ })
127
+
128
+ describe('DirectoryWalkError', () => {
129
+ it('has correct _tag and error code', () => {
130
+ const error = new DirectoryWalkError({
131
+ path: '/test/dir',
132
+ message: 'EACCES',
133
+ })
134
+ expect(error._tag).toBe('DirectoryWalkError')
135
+ expect(error.code).toBe(ErrorCode.DIRECTORY_WALK)
136
+ expect(error.code).toBe('E103')
137
+ })
138
+ })
139
+
140
+ // ==========================================================================
141
+ // Parse Errors
142
+ // ==========================================================================
143
+
144
+ describe('ParseError', () => {
145
+ it('has correct _tag and error code', () => {
146
+ const error = new ParseError({ message: 'Invalid syntax' })
147
+ expect(error._tag).toBe('ParseError')
148
+ expect(error.code).toBe(ErrorCode.PARSE)
149
+ expect(error.code).toBe('E200')
150
+ })
151
+
152
+ it('supports optional location fields', () => {
153
+ const error = new ParseError({
154
+ message: 'Unexpected token',
155
+ path: '/doc.md',
156
+ line: 10,
157
+ column: 5,
158
+ })
159
+ expect(error.path).toBe('/doc.md')
160
+ expect(error.line).toBe(10)
161
+ expect(error.column).toBe(5)
162
+ })
163
+
164
+ it('handles missing optional fields', () => {
165
+ const error = new ParseError({ message: 'error' })
166
+ expect(error.path).toBeUndefined()
167
+ expect(error.line).toBeUndefined()
168
+ expect(error.column).toBeUndefined()
169
+ })
170
+ })
171
+
172
+ // ==========================================================================
173
+ // API Key Errors
174
+ // ==========================================================================
175
+
176
+ describe('ApiKeyMissingError', () => {
177
+ it('has correct _tag and error code', () => {
178
+ const error = new ApiKeyMissingError({
179
+ provider: 'openai',
180
+ envVar: 'OPENAI_API_KEY',
181
+ })
182
+ expect(error._tag).toBe('ApiKeyMissingError')
183
+ expect(error.code).toBe(ErrorCode.API_KEY_MISSING)
184
+ expect(error.code).toBe('E300')
185
+ })
186
+
187
+ it('generates dynamic message', () => {
188
+ const error = new ApiKeyMissingError({
189
+ provider: 'openai',
190
+ envVar: 'OPENAI_API_KEY',
191
+ })
192
+ expect(error.message).toBe('OPENAI_API_KEY not set')
193
+ })
194
+
195
+ it('can be caught with catchTag', async () => {
196
+ const effect = Effect.fail(
197
+ new ApiKeyMissingError({
198
+ provider: 'openai',
199
+ envVar: 'OPENAI_API_KEY',
200
+ }),
201
+ )
202
+ const result = await Effect.runPromise(
203
+ effect.pipe(
204
+ Effect.catchTag('ApiKeyMissingError', (e) =>
205
+ Effect.succeed(e.provider),
206
+ ),
207
+ ),
208
+ )
209
+ expect(result).toBe('openai')
210
+ })
211
+ })
212
+
213
+ describe('ApiKeyInvalidError', () => {
214
+ it('has correct _tag and error code', () => {
215
+ const error = new ApiKeyInvalidError({ provider: 'openai' })
216
+ expect(error._tag).toBe('ApiKeyInvalidError')
217
+ expect(error.code).toBe(ErrorCode.API_KEY_INVALID)
218
+ expect(error.code).toBe('E301')
219
+ })
220
+
221
+ it('generates dynamic message without details', () => {
222
+ const error = new ApiKeyInvalidError({ provider: 'openai' })
223
+ expect(error.message).toBe('Invalid API key for openai')
224
+ })
225
+
226
+ it('uses details when provided', () => {
227
+ const error = new ApiKeyInvalidError({
228
+ provider: 'openai',
229
+ details: 'API key expired',
230
+ })
231
+ expect(error.message).toBe('API key expired')
232
+ })
233
+ })
234
+
235
+ // ==========================================================================
236
+ // Embedding Errors
237
+ // ==========================================================================
238
+
239
+ describe('EmbeddingError', () => {
240
+ it('has correct _tag', () => {
241
+ const error = new EmbeddingError({
242
+ reason: 'RateLimit',
243
+ message: 'Rate limit hit',
244
+ })
245
+ expect(error._tag).toBe('EmbeddingError')
246
+ })
247
+
248
+ it('returns correct code for RateLimit', () => {
249
+ const error = new EmbeddingError({
250
+ reason: 'RateLimit',
251
+ message: 'Rate limit hit',
252
+ })
253
+ expect(error.code).toBe(ErrorCode.EMBEDDING_RATE_LIMIT)
254
+ expect(error.code).toBe('E310')
255
+ })
256
+
257
+ it('returns correct code for QuotaExceeded', () => {
258
+ const error = new EmbeddingError({
259
+ reason: 'QuotaExceeded',
260
+ message: 'Quota exceeded',
261
+ })
262
+ expect(error.code).toBe(ErrorCode.EMBEDDING_QUOTA)
263
+ expect(error.code).toBe('E311')
264
+ })
265
+
266
+ it('returns correct code for Network', () => {
267
+ const error = new EmbeddingError({
268
+ reason: 'Network',
269
+ message: 'Connection failed',
270
+ })
271
+ expect(error.code).toBe(ErrorCode.EMBEDDING_NETWORK)
272
+ expect(error.code).toBe('E312')
273
+ })
274
+
275
+ it('returns correct code for ModelError', () => {
276
+ const error = new EmbeddingError({
277
+ reason: 'ModelError',
278
+ message: 'Model unavailable',
279
+ })
280
+ expect(error.code).toBe(ErrorCode.EMBEDDING_MODEL)
281
+ expect(error.code).toBe('E313')
282
+ })
283
+
284
+ it('returns correct code for Unknown', () => {
285
+ const error = new EmbeddingError({
286
+ reason: 'Unknown',
287
+ message: 'Something went wrong',
288
+ })
289
+ expect(error.code).toBe(ErrorCode.EMBEDDING_UNKNOWN)
290
+ expect(error.code).toBe('E319')
291
+ })
292
+
293
+ it('preserves optional provider field', () => {
294
+ const error = new EmbeddingError({
295
+ reason: 'RateLimit',
296
+ message: 'error',
297
+ provider: 'openai',
298
+ })
299
+ expect(error.provider).toBe('openai')
300
+ })
301
+
302
+ it('can be caught with catchTag', async () => {
303
+ const effect = Effect.fail(
304
+ new EmbeddingError({ reason: 'Network', message: 'timeout' }),
305
+ )
306
+ const result = await Effect.runPromise(
307
+ effect.pipe(
308
+ Effect.catchTag('EmbeddingError', (e) => Effect.succeed(e.reason)),
309
+ ),
310
+ )
311
+ expect(result).toBe('Network')
312
+ })
313
+ })
314
+
315
+ // ==========================================================================
316
+ // Index Errors
317
+ // ==========================================================================
318
+
319
+ describe('IndexNotFoundError', () => {
320
+ it('has correct _tag and error code', () => {
321
+ const error = new IndexNotFoundError({ path: '/.mdcontext/index.json' })
322
+ expect(error._tag).toBe('IndexNotFoundError')
323
+ expect(error.code).toBe(ErrorCode.INDEX_NOT_FOUND)
324
+ expect(error.code).toBe('E400')
325
+ })
326
+
327
+ it('generates dynamic message', () => {
328
+ const error = new IndexNotFoundError({ path: '/.mdcontext/index.json' })
329
+ expect(error.message).toBe('Index not found at /.mdcontext/index.json')
330
+ })
331
+ })
332
+
333
+ describe('IndexCorruptedError', () => {
334
+ it('has correct _tag and error code', () => {
335
+ const error = new IndexCorruptedError({
336
+ path: '/.mdcontext/index.json',
337
+ reason: 'InvalidJson',
338
+ })
339
+ expect(error._tag).toBe('IndexCorruptedError')
340
+ expect(error.code).toBe(ErrorCode.INDEX_CORRUPTED)
341
+ expect(error.code).toBe('E401')
342
+ })
343
+
344
+ it('generates dynamic message with reason', () => {
345
+ const error = new IndexCorruptedError({
346
+ path: '/index.json',
347
+ reason: 'VersionMismatch',
348
+ })
349
+ expect(error.message).toBe(
350
+ 'Index corrupted at /index.json: VersionMismatch',
351
+ )
352
+ })
353
+
354
+ it('supports optional details field', () => {
355
+ const error = new IndexCorruptedError({
356
+ path: '/index.json',
357
+ reason: 'MissingData',
358
+ details: 'documents array is missing',
359
+ })
360
+ expect(error.details).toBe('documents array is missing')
361
+ })
362
+ })
363
+
364
+ describe('IndexBuildError', () => {
365
+ it('has correct _tag and error code', () => {
366
+ const error = new IndexBuildError({
367
+ path: '/docs',
368
+ message: 'Build failed',
369
+ })
370
+ expect(error._tag).toBe('IndexBuildError')
371
+ expect(error.code).toBe(ErrorCode.INDEX_BUILD)
372
+ expect(error.code).toBe('E402')
373
+ })
374
+ })
375
+
376
+ // ==========================================================================
377
+ // Search Errors
378
+ // ==========================================================================
379
+
380
+ describe('DocumentNotFoundError', () => {
381
+ it('has correct _tag and error code', () => {
382
+ const error = new DocumentNotFoundError({ path: '/doc.md' })
383
+ expect(error._tag).toBe('DocumentNotFoundError')
384
+ expect(error.code).toBe(ErrorCode.DOCUMENT_NOT_FOUND)
385
+ expect(error.code).toBe('E500')
386
+ })
387
+
388
+ it('generates dynamic message', () => {
389
+ const error = new DocumentNotFoundError({ path: '/missing.md' })
390
+ expect(error.message).toBe('Document not found in index: /missing.md')
391
+ })
392
+
393
+ it('supports optional indexPath field', () => {
394
+ const error = new DocumentNotFoundError({
395
+ path: '/doc.md',
396
+ indexPath: '/.mdcontext/index.json',
397
+ })
398
+ expect(error.indexPath).toBe('/.mdcontext/index.json')
399
+ })
400
+ })
401
+
402
+ describe('EmbeddingsNotFoundError', () => {
403
+ it('has correct _tag and error code', () => {
404
+ const error = new EmbeddingsNotFoundError({
405
+ path: '/.mdcontext/embeddings',
406
+ })
407
+ expect(error._tag).toBe('EmbeddingsNotFoundError')
408
+ expect(error.code).toBe(ErrorCode.EMBEDDINGS_NOT_FOUND)
409
+ expect(error.code).toBe('E501')
410
+ })
411
+
412
+ it('generates dynamic message with instructions', () => {
413
+ const error = new EmbeddingsNotFoundError({ path: '/embeddings' })
414
+ expect(error.message).toBe(
415
+ "Embeddings not found at /embeddings. Run 'mdcontext index --embed' first.",
416
+ )
417
+ })
418
+ })
419
+
420
+ // ==========================================================================
421
+ // Vector Store Errors
422
+ // ==========================================================================
423
+
424
+ describe('VectorStoreError', () => {
425
+ it('has correct _tag and error code', () => {
426
+ const error = new VectorStoreError({
427
+ operation: 'init',
428
+ message: 'Failed to initialize',
429
+ })
430
+ expect(error._tag).toBe('VectorStoreError')
431
+ expect(error.code).toBe(ErrorCode.VECTOR_STORE)
432
+ expect(error.code).toBe('E600')
433
+ })
434
+
435
+ it('preserves operation field', () => {
436
+ const operations = ['init', 'add', 'search', 'save', 'load'] as const
437
+ for (const op of operations) {
438
+ const error = new VectorStoreError({ operation: op, message: 'error' })
439
+ expect(error.operation).toBe(op)
440
+ }
441
+ })
442
+ })
443
+
444
+ // ==========================================================================
445
+ // Config Errors
446
+ // ==========================================================================
447
+
448
+ describe('ConfigError', () => {
449
+ it('has correct _tag and error code', () => {
450
+ const error = new ConfigError({ message: 'Invalid config' })
451
+ expect(error._tag).toBe('ConfigError')
452
+ expect(error.code).toBe(ErrorCode.CONFIG)
453
+ expect(error.code).toBe('E700')
454
+ })
455
+
456
+ it('supports optional field', () => {
457
+ const error = new ConfigError({
458
+ field: 'embeddingProvider',
459
+ message: 'Invalid provider',
460
+ })
461
+ expect(error.field).toBe('embeddingProvider')
462
+ })
463
+
464
+ it('handles missing field', () => {
465
+ const error = new ConfigError({ message: 'error' })
466
+ expect(error.field).toBeUndefined()
467
+ })
468
+
469
+ it('supports sourceFile field', () => {
470
+ const error = new ConfigError({
471
+ message: 'Invalid value',
472
+ field: 'index.maxDepth',
473
+ sourceFile: '/path/to/mdcontext.config.json',
474
+ })
475
+ expect(error.sourceFile).toBe('/path/to/mdcontext.config.json')
476
+ })
477
+
478
+ it('supports expectedType field', () => {
479
+ const error = new ConfigError({
480
+ message: 'Invalid type',
481
+ field: 'index.maxDepth',
482
+ expectedType: 'number',
483
+ })
484
+ expect(error.expectedType).toBe('number')
485
+ })
486
+
487
+ it('supports actualValue field with string value', () => {
488
+ const error = new ConfigError({
489
+ message: 'Invalid value',
490
+ field: 'index.maxDepth',
491
+ actualValue: 'ten',
492
+ })
493
+ expect(error.actualValue).toBe('ten')
494
+ })
495
+
496
+ it('supports actualValue field with number value', () => {
497
+ const error = new ConfigError({
498
+ message: 'Invalid value',
499
+ field: 'index.maxDepth',
500
+ actualValue: -5,
501
+ })
502
+ expect(error.actualValue).toBe(-5)
503
+ })
504
+
505
+ it('supports validValues field', () => {
506
+ const error = new ConfigError({
507
+ message: 'Invalid provider',
508
+ field: 'embeddingProvider',
509
+ validValues: ['openai', 'cohere', 'local'],
510
+ })
511
+ expect(error.validValues).toEqual(['openai', 'cohere', 'local'])
512
+ })
513
+
514
+ it('supports all enhanced fields together', () => {
515
+ const error = new ConfigError({
516
+ field: 'index.maxDepth',
517
+ message: 'Value must be a positive integer',
518
+ sourceFile: '/path/to/config.json',
519
+ expectedType: 'number',
520
+ actualValue: 'ten',
521
+ validValues: ['Any positive integer'],
522
+ cause: new Error('underlying error'),
523
+ })
524
+ expect(error.field).toBe('index.maxDepth')
525
+ expect(error.message).toBe('Value must be a positive integer')
526
+ expect(error.sourceFile).toBe('/path/to/config.json')
527
+ expect(error.expectedType).toBe('number')
528
+ expect(error.actualValue).toBe('ten')
529
+ expect(error.validValues).toEqual(['Any positive integer'])
530
+ expect(error.cause).toBeInstanceOf(Error)
531
+ })
532
+
533
+ it('handles all optional fields being undefined', () => {
534
+ const error = new ConfigError({ message: 'error' })
535
+ expect(error.field).toBeUndefined()
536
+ expect(error.sourceFile).toBeUndefined()
537
+ expect(error.expectedType).toBeUndefined()
538
+ expect(error.actualValue).toBeUndefined()
539
+ expect(error.validValues).toBeUndefined()
540
+ })
541
+
542
+ it('can be caught with catchTag', async () => {
543
+ const effect = Effect.fail(
544
+ new ConfigError({
545
+ field: 'test.field',
546
+ message: 'test error',
547
+ sourceFile: '/test/config.json',
548
+ }),
549
+ )
550
+ const result = await Effect.runPromise(
551
+ effect.pipe(
552
+ Effect.catchTag('ConfigError', (e) =>
553
+ Effect.succeed({ field: e.field, sourceFile: e.sourceFile }),
554
+ ),
555
+ ),
556
+ )
557
+ expect(result).toEqual({
558
+ field: 'test.field',
559
+ sourceFile: '/test/config.json',
560
+ })
561
+ })
562
+ })
563
+
564
+ // ==========================================================================
565
+ // Watch Errors
566
+ // ==========================================================================
567
+
568
+ describe('WatchError', () => {
569
+ it('has correct _tag and error code', () => {
570
+ const error = new WatchError({
571
+ path: '/docs',
572
+ message: 'Watcher failed',
573
+ })
574
+ expect(error._tag).toBe('WatchError')
575
+ expect(error.code).toBe(ErrorCode.WATCH)
576
+ expect(error.code).toBe('E800')
577
+ })
578
+ })
579
+
580
+ // ==========================================================================
581
+ // CLI Errors
582
+ // ==========================================================================
583
+
584
+ describe('CliValidationError', () => {
585
+ it('has correct _tag and error code', () => {
586
+ const error = new CliValidationError({ message: 'Invalid argument' })
587
+ expect(error._tag).toBe('CliValidationError')
588
+ expect(error.code).toBe(ErrorCode.CLI_VALIDATION)
589
+ expect(error.code).toBe('E900')
590
+ })
591
+
592
+ it('supports optional fields', () => {
593
+ const error = new CliValidationError({
594
+ message: 'Invalid value',
595
+ argument: '--limit',
596
+ expected: 'number',
597
+ received: 'abc',
598
+ })
599
+ expect(error.argument).toBe('--limit')
600
+ expect(error.expected).toBe('number')
601
+ expect(error.received).toBe('abc')
602
+ })
603
+
604
+ it('handles missing optional fields', () => {
605
+ const error = new CliValidationError({ message: 'error' })
606
+ expect(error.argument).toBeUndefined()
607
+ expect(error.expected).toBeUndefined()
608
+ expect(error.received).toBeUndefined()
609
+ })
610
+ })
611
+
612
+ // ==========================================================================
613
+ // catchTags Integration
614
+ // ==========================================================================
615
+
616
+ describe('catchTags integration', () => {
617
+ it('can handle multiple error types with catchTags', async () => {
618
+ const program = (shouldFail: 'file' | 'api' | 'index') =>
619
+ Effect.gen(function* () {
620
+ if (shouldFail === 'file') {
621
+ yield* Effect.fail(
622
+ new FileReadError({ path: '/file.md', message: 'not found' }),
623
+ )
624
+ }
625
+ if (shouldFail === 'api') {
626
+ yield* Effect.fail(
627
+ new ApiKeyMissingError({
628
+ provider: 'openai',
629
+ envVar: 'OPENAI_API_KEY',
630
+ }),
631
+ )
632
+ }
633
+ if (shouldFail === 'index') {
634
+ yield* Effect.fail(new IndexNotFoundError({ path: '/index' }))
635
+ }
636
+ return 'success'
637
+ }).pipe(
638
+ Effect.catchTags({
639
+ FileReadError: () => Effect.succeed('file_error'),
640
+ ApiKeyMissingError: () => Effect.succeed('api_error'),
641
+ IndexNotFoundError: () => Effect.succeed('index_error'),
642
+ }),
643
+ )
644
+
645
+ expect(await Effect.runPromise(program('file'))).toBe('file_error')
646
+ expect(await Effect.runPromise(program('api'))).toBe('api_error')
647
+ expect(await Effect.runPromise(program('index'))).toBe('index_error')
648
+ })
649
+ })
650
+
651
+ // ==========================================================================
652
+ // Error Code Constants
653
+ // ==========================================================================
654
+
655
+ describe('ErrorCode constants', () => {
656
+ it('has unique codes for each error type', () => {
657
+ const codes = Object.values(ErrorCode)
658
+ const uniqueCodes = new Set(codes)
659
+ expect(uniqueCodes.size).toBe(codes.length)
660
+ })
661
+
662
+ it('follows E{category}{number} format', () => {
663
+ for (const code of Object.values(ErrorCode)) {
664
+ expect(code).toMatch(/^E[1-9]\d{2}$/)
665
+ }
666
+ })
667
+
668
+ it('groups codes by category', () => {
669
+ // File system E1xx
670
+ expect(ErrorCode.FILE_READ).toMatch(/^E1\d{2}$/)
671
+ expect(ErrorCode.FILE_WRITE).toMatch(/^E1\d{2}$/)
672
+ expect(ErrorCode.DIRECTORY_CREATE).toMatch(/^E1\d{2}$/)
673
+ expect(ErrorCode.DIRECTORY_WALK).toMatch(/^E1\d{2}$/)
674
+
675
+ // Parse E2xx
676
+ expect(ErrorCode.PARSE).toMatch(/^E2\d{2}$/)
677
+
678
+ // API E3xx
679
+ expect(ErrorCode.API_KEY_MISSING).toMatch(/^E3\d{2}$/)
680
+ expect(ErrorCode.API_KEY_INVALID).toMatch(/^E3\d{2}$/)
681
+ expect(ErrorCode.EMBEDDING_RATE_LIMIT).toMatch(/^E3\d{2}$/)
682
+
683
+ // Index E4xx
684
+ expect(ErrorCode.INDEX_NOT_FOUND).toMatch(/^E4\d{2}$/)
685
+ expect(ErrorCode.INDEX_CORRUPTED).toMatch(/^E4\d{2}$/)
686
+ expect(ErrorCode.INDEX_BUILD).toMatch(/^E4\d{2}$/)
687
+
688
+ // Search E5xx
689
+ expect(ErrorCode.DOCUMENT_NOT_FOUND).toMatch(/^E5\d{2}$/)
690
+ expect(ErrorCode.EMBEDDINGS_NOT_FOUND).toMatch(/^E5\d{2}$/)
691
+
692
+ // Vector Store E6xx
693
+ expect(ErrorCode.VECTOR_STORE).toMatch(/^E6\d{2}$/)
694
+
695
+ // Config E7xx
696
+ expect(ErrorCode.CONFIG).toMatch(/^E7\d{2}$/)
697
+
698
+ // Watch E8xx
699
+ expect(ErrorCode.WATCH).toMatch(/^E8\d{2}$/)
700
+
701
+ // CLI E9xx
702
+ expect(ErrorCode.CLI_VALIDATION).toMatch(/^E9\d{2}$/)
703
+ })
704
+ })
705
+ })
706
+
707
+ // ============================================================================
708
+ // ConfigError Formatting Tests
709
+ // ============================================================================
710
+
711
+ describe('ConfigError Formatting', () => {
712
+ it('formats basic config error with field', () => {
713
+ const error = new ConfigError({
714
+ field: 'index.maxDepth',
715
+ message: 'Value must be a positive integer',
716
+ })
717
+ const formatted = formatError(error)
718
+
719
+ expect(formatted.code).toBe('E700')
720
+ expect(formatted.message).toBe('Invalid configuration: index.maxDepth')
721
+ expect(formatted.exitCode).toBe(EXIT_CODE.USER_ERROR)
722
+ expect(formatted.suggestions).toContain('Check your config file syntax')
723
+ expect(formatted.suggestions).toContain(
724
+ "Run 'mdcontext config check' to validate configuration",
725
+ )
726
+ })
727
+
728
+ it('formats config error without field', () => {
729
+ const error = new ConfigError({
730
+ message: 'Invalid configuration format',
731
+ })
732
+ const formatted = formatError(error)
733
+
734
+ expect(formatted.message).toBe('Configuration error')
735
+ expect(formatted.details).toContain('Invalid configuration format')
736
+ })
737
+
738
+ it('formats config error with sourceFile', () => {
739
+ const error = new ConfigError({
740
+ field: 'index.maxDepth',
741
+ message: 'Invalid value',
742
+ sourceFile: '/path/to/mdcontext.config.json',
743
+ })
744
+ const formatted = formatError(error)
745
+
746
+ expect(formatted.details).toContain(
747
+ 'Source: /path/to/mdcontext.config.json',
748
+ )
749
+ })
750
+
751
+ it('formats config error with expectedType and actualValue', () => {
752
+ const error = new ConfigError({
753
+ field: 'index.maxDepth',
754
+ message: 'Type mismatch',
755
+ expectedType: 'number',
756
+ actualValue: 'ten',
757
+ })
758
+ const formatted = formatError(error)
759
+
760
+ expect(formatted.details).toContain('Expected: number')
761
+ expect(formatted.details).toContain('Got: "ten"')
762
+ })
763
+
764
+ it('formats config error with number actualValue', () => {
765
+ const error = new ConfigError({
766
+ field: 'index.maxDepth',
767
+ message: 'Value out of range',
768
+ expectedType: 'positive integer',
769
+ actualValue: -5,
770
+ })
771
+ const formatted = formatError(error)
772
+
773
+ expect(formatted.details).toContain('Expected: positive integer')
774
+ expect(formatted.details).toContain('Got: -5')
775
+ })
776
+
777
+ it('formats config error with validValues', () => {
778
+ const error = new ConfigError({
779
+ field: 'embeddingProvider',
780
+ message: 'Invalid provider',
781
+ validValues: ['openai', 'cohere', 'local'],
782
+ })
783
+ const formatted = formatError(error)
784
+
785
+ expect(formatted.details).toContain('Valid values: openai, cohere, local')
786
+ })
787
+
788
+ it('formats config error with all enhanced fields', () => {
789
+ const error = new ConfigError({
790
+ field: 'index.maxDepth',
791
+ message: 'Value must be a positive integer',
792
+ sourceFile: '/path/to/mdcontext.config.json',
793
+ expectedType: 'number',
794
+ actualValue: 'ten',
795
+ validValues: ['Any positive integer'],
796
+ })
797
+ const formatted = formatError(error)
798
+
799
+ expect(formatted.code).toBe('E700')
800
+ expect(formatted.message).toBe('Invalid configuration: index.maxDepth')
801
+ expect(formatted.details).toContain(
802
+ 'Source: /path/to/mdcontext.config.json',
803
+ )
804
+ expect(formatted.details).toContain('Expected: number')
805
+ expect(formatted.details).toContain('Got: "ten"')
806
+ expect(formatted.details).toContain('Valid values: Any positive integer')
807
+ expect(formatted.exitCode).toBe(EXIT_CODE.USER_ERROR)
808
+ })
809
+
810
+ it('includes message in details when other fields present', () => {
811
+ const error = new ConfigError({
812
+ field: 'test.field',
813
+ message: 'Technical error details',
814
+ sourceFile: '/config.json',
815
+ })
816
+ const formatted = formatError(error)
817
+
818
+ expect(formatted.details).toContain('Technical error details')
819
+ expect(formatted.details).toContain('Source: /config.json')
820
+ })
821
+
822
+ it('uses only message as details when no enhanced fields', () => {
823
+ const error = new ConfigError({
824
+ field: 'test.field',
825
+ message: 'Simple error message',
826
+ })
827
+ const formatted = formatError(error)
828
+
829
+ expect(formatted.details).toBe('Simple error message')
830
+ })
831
+
832
+ it('always includes standard suggestions', () => {
833
+ const error = new ConfigError({
834
+ message: 'Any error',
835
+ })
836
+ const formatted = formatError(error)
837
+
838
+ expect(formatted.suggestions).toBeDefined()
839
+ expect(formatted.suggestions).toHaveLength(2)
840
+ expect(formatted.suggestions).toContain('Check your config file syntax')
841
+ expect(formatted.suggestions).toContain(
842
+ "Run 'mdcontext config check' to validate configuration",
843
+ )
844
+ })
845
+ })