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
@@ -7,6 +7,12 @@ import * as fs from 'node:fs/promises'
7
7
  import * as path from 'node:path'
8
8
  import { Effect } from 'effect'
9
9
 
10
+ import {
11
+ DirectoryCreateError,
12
+ FileReadError,
13
+ FileWriteError,
14
+ IndexCorruptedError,
15
+ } from '../errors/index.js'
10
16
  import type {
11
17
  DocumentIndex,
12
18
  IndexConfig,
@@ -19,35 +25,82 @@ import { getIndexPaths, INDEX_VERSION } from './types.js'
19
25
  // File System Helpers
20
26
  // ============================================================================
21
27
 
22
- const ensureDir = (dirPath: string): Effect.Effect<void, Error> =>
28
+ const ensureDir = (
29
+ dirPath: string,
30
+ ): Effect.Effect<void, DirectoryCreateError> =>
23
31
  Effect.tryPromise({
24
32
  try: () => fs.mkdir(dirPath, { recursive: true }),
25
- catch: (e) => new Error(`Failed to create directory ${dirPath}: ${e}`),
33
+ catch: (e) =>
34
+ new DirectoryCreateError({
35
+ path: dirPath,
36
+ message: e instanceof Error ? e.message : String(e),
37
+ cause: e,
38
+ }),
26
39
  }).pipe(Effect.map(() => undefined))
27
40
 
28
- const readJsonFile = <T>(filePath: string): Effect.Effect<T | null, Error> =>
29
- Effect.tryPromise({
30
- try: async () => {
31
- try {
32
- const content = await fs.readFile(filePath, 'utf-8')
33
- return JSON.parse(content) as T
34
- } catch {
35
- return null
36
- }
37
- },
38
- catch: (e) => new Error(`Failed to read ${filePath}: ${e}`),
41
+ const readJsonFile = <T>(
42
+ filePath: string,
43
+ ): Effect.Effect<T | null, FileReadError | IndexCorruptedError> =>
44
+ Effect.gen(function* () {
45
+ // Try to read file content
46
+ const contentResult = yield* Effect.tryPromise({
47
+ try: () => fs.readFile(filePath, 'utf-8'),
48
+ catch: (e) => {
49
+ // File not found is not an error - return null
50
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
51
+ return { notFound: true as const }
52
+ }
53
+ return new FileReadError({
54
+ path: filePath,
55
+ message: e instanceof Error ? e.message : String(e),
56
+ cause: e,
57
+ })
58
+ },
59
+ }).pipe(
60
+ Effect.map((content) =>
61
+ typeof content === 'string' ? { content } : content,
62
+ ),
63
+ // Note: catchAll here filters out "file not found" as expected case (returns null),
64
+ // while other errors are re-thrown to propagate as typed FileReadError
65
+ Effect.catchAll((e) =>
66
+ e && 'notFound' in e
67
+ ? Effect.succeed({ notFound: true as const })
68
+ : Effect.fail(e),
69
+ ),
70
+ )
71
+
72
+ // Handle not found
73
+ if ('notFound' in contentResult) {
74
+ return null
75
+ }
76
+
77
+ // Parse JSON - corrupted files should fail with IndexCorruptedError
78
+ return yield* Effect.try({
79
+ try: () => JSON.parse(contentResult.content) as T,
80
+ catch: (e) =>
81
+ new IndexCorruptedError({
82
+ path: filePath,
83
+ reason: 'InvalidJson',
84
+ details: e instanceof Error ? e.message : String(e),
85
+ }),
86
+ })
39
87
  })
40
88
 
41
89
  const writeJsonFile = <T>(
42
90
  filePath: string,
43
91
  data: T,
44
- ): Effect.Effect<void, Error> =>
92
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
45
93
  Effect.gen(function* () {
46
94
  const dir = path.dirname(filePath)
47
95
  yield* ensureDir(dir)
48
96
  yield* Effect.tryPromise({
49
97
  try: () => fs.writeFile(filePath, JSON.stringify(data, null, 2)),
50
- catch: (e) => new Error(`Failed to write ${filePath}: ${e}`),
98
+ catch: (e) =>
99
+ new FileWriteError({
100
+ path: filePath,
101
+ message: e instanceof Error ? e.message : String(e),
102
+ cause: e,
103
+ }),
51
104
  })
52
105
  })
53
106
 
@@ -75,7 +128,10 @@ export const createStorage = (rootPath: string): IndexStorage => ({
75
128
 
76
129
  export const initializeIndex = (
77
130
  storage: IndexStorage,
78
- ): Effect.Effect<void, Error> =>
131
+ ): Effect.Effect<
132
+ void,
133
+ DirectoryCreateError | FileReadError | FileWriteError | IndexCorruptedError
134
+ > =>
79
135
  Effect.gen(function* () {
80
136
  yield* ensureDir(storage.paths.root)
81
137
  yield* ensureDir(storage.paths.parsed)
@@ -102,13 +158,13 @@ export const initializeIndex = (
102
158
 
103
159
  export const loadConfig = (
104
160
  storage: IndexStorage,
105
- ): Effect.Effect<IndexConfig | null, Error> =>
161
+ ): Effect.Effect<IndexConfig | null, FileReadError | IndexCorruptedError> =>
106
162
  readJsonFile<IndexConfig>(storage.paths.config)
107
163
 
108
164
  export const saveConfig = (
109
165
  storage: IndexStorage,
110
166
  config: IndexConfig,
111
- ): Effect.Effect<void, Error> =>
167
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
112
168
  writeJsonFile(storage.paths.config, {
113
169
  ...config,
114
170
  updatedAt: new Date().toISOString(),
@@ -120,13 +176,14 @@ export const saveConfig = (
120
176
 
121
177
  export const loadDocumentIndex = (
122
178
  storage: IndexStorage,
123
- ): Effect.Effect<DocumentIndex | null, Error> =>
179
+ ): Effect.Effect<DocumentIndex | null, FileReadError | IndexCorruptedError> =>
124
180
  readJsonFile<DocumentIndex>(storage.paths.documents)
125
181
 
126
182
  export const saveDocumentIndex = (
127
183
  storage: IndexStorage,
128
184
  index: DocumentIndex,
129
- ): Effect.Effect<void, Error> => writeJsonFile(storage.paths.documents, index)
185
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
186
+ writeJsonFile(storage.paths.documents, index)
130
187
 
131
188
  export const createEmptyDocumentIndex = (rootPath: string): DocumentIndex => ({
132
189
  version: INDEX_VERSION,
@@ -140,19 +197,20 @@ export const createEmptyDocumentIndex = (rootPath: string): DocumentIndex => ({
140
197
 
141
198
  export const loadSectionIndex = (
142
199
  storage: IndexStorage,
143
- ): Effect.Effect<SectionIndex | null, Error> =>
200
+ ): Effect.Effect<SectionIndex | null, FileReadError | IndexCorruptedError> =>
144
201
  readJsonFile<SectionIndex>(storage.paths.sections)
145
202
 
146
203
  export const saveSectionIndex = (
147
204
  storage: IndexStorage,
148
205
  index: SectionIndex,
149
- ): Effect.Effect<void, Error> => writeJsonFile(storage.paths.sections, index)
206
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
207
+ writeJsonFile(storage.paths.sections, index)
150
208
 
151
209
  export const createEmptySectionIndex = (): SectionIndex => ({
152
210
  version: INDEX_VERSION,
153
211
  sections: {},
154
- byHeading: {},
155
- byDocument: {},
212
+ byHeading: Object.create(null),
213
+ byDocument: Object.create(null),
156
214
  })
157
215
 
158
216
  // ============================================================================
@@ -161,18 +219,19 @@ export const createEmptySectionIndex = (): SectionIndex => ({
161
219
 
162
220
  export const loadLinkIndex = (
163
221
  storage: IndexStorage,
164
- ): Effect.Effect<LinkIndex | null, Error> =>
222
+ ): Effect.Effect<LinkIndex | null, FileReadError | IndexCorruptedError> =>
165
223
  readJsonFile<LinkIndex>(storage.paths.links)
166
224
 
167
225
  export const saveLinkIndex = (
168
226
  storage: IndexStorage,
169
227
  index: LinkIndex,
170
- ): Effect.Effect<void, Error> => writeJsonFile(storage.paths.links, index)
228
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
229
+ writeJsonFile(storage.paths.links, index)
171
230
 
172
231
  export const createEmptyLinkIndex = (): LinkIndex => ({
173
232
  version: INDEX_VERSION,
174
- forward: {},
175
- backward: {},
233
+ forward: Object.create(null),
234
+ backward: Object.create(null),
176
235
  broken: [],
177
236
  })
178
237
 
@@ -182,7 +241,7 @@ export const createEmptyLinkIndex = (): LinkIndex => ({
182
241
 
183
242
  export const indexExists = (
184
243
  storage: IndexStorage,
185
- ): Effect.Effect<boolean, Error> =>
244
+ ): Effect.Effect<boolean, FileReadError> =>
186
245
  Effect.tryPromise({
187
246
  try: async () => {
188
247
  try {
@@ -192,5 +251,10 @@ export const indexExists = (
192
251
  return false
193
252
  }
194
253
  },
195
- catch: (e) => new Error(`Failed to check index existence: ${e}`),
254
+ catch: (e) =>
255
+ new FileReadError({
256
+ path: storage.paths.config,
257
+ message: e instanceof Error ? e.message : String(e),
258
+ cause: e,
259
+ }),
196
260
  })
@@ -75,6 +75,35 @@ export interface LinkIndex {
75
75
  // Index Result
76
76
  // ============================================================================
77
77
 
78
+ /**
79
+ * Reason why a file was skipped during indexing
80
+ */
81
+ export type SkipReason =
82
+ | 'unchanged' // File hash and mtime unchanged
83
+ | 'excluded' // Matches exclude pattern
84
+ | 'hidden' // Hidden file or directory
85
+ | 'not-markdown' // Not a markdown file
86
+ | 'binary' // Binary file detected
87
+ | 'oversized' // File too large
88
+
89
+ /**
90
+ * Information about a skipped file
91
+ */
92
+ export interface SkippedFile {
93
+ readonly path: string
94
+ readonly reason: SkipReason
95
+ }
96
+
97
+ /**
98
+ * Summary of skipped files by reason
99
+ */
100
+ export interface SkipSummary {
101
+ readonly unchanged: number
102
+ readonly excluded: number
103
+ readonly hidden: number
104
+ readonly total: number
105
+ }
106
+
78
107
  export interface IndexResult {
79
108
  readonly documentsIndexed: number
80
109
  readonly sectionsIndexed: number
@@ -83,10 +112,19 @@ export interface IndexResult {
83
112
  readonly totalSections: number
84
113
  readonly totalLinks: number
85
114
  readonly duration: number
86
- readonly errors: readonly IndexBuildError[]
115
+ /** Non-fatal file processing errors (files that couldn't be indexed) */
116
+ readonly errors: readonly FileProcessingError[]
117
+ readonly skipped: SkipSummary
87
118
  }
88
119
 
89
- export interface IndexBuildError {
120
+ /**
121
+ * Non-fatal error during file processing in index build.
122
+ * These are collected and reported but don't stop the build.
123
+ *
124
+ * Note: This is distinct from IndexBuildError in errors/index.ts,
125
+ * which is a TaggedError for fatal build failures.
126
+ */
127
+ export interface FileProcessingError {
90
128
  readonly path: string
91
129
  readonly message: string
92
130
  }
@@ -1,14 +1,53 @@
1
1
  /**
2
2
  * File watcher for automatic re-indexing
3
+ *
4
+ * ## Why Not Effect Streams?
5
+ *
6
+ * We evaluated using Effect Streams (ALP-101) but decided the current approach is better:
7
+ *
8
+ * 1. **chokidar is battle-tested** - Handles OS-specific quirks (FSEvents on macOS,
9
+ * inotify on Linux, ReadDirectoryChangesW on Windows)
10
+ *
11
+ * 2. **Debouncing handles backpressure** - The 300ms debounce already batches rapid
12
+ * changes, so Stream backpressure isn't needed
13
+ *
14
+ * 3. **Simple use case** - File change → rebuild index. No complex transformations
15
+ * or compositions that would benefit from Stream operators
16
+ *
17
+ * 4. **Already Effect-based** - The setup/teardown is wrapped in Effect for proper
18
+ * error handling, and we use typed errors (WatchError, IndexBuildError)
19
+ *
20
+ * If future requirements need more sophisticated event processing (filtering by
21
+ * content type, incremental updates, event replay), reconsider Streams then.
3
22
  */
4
23
 
5
24
  import * as path from 'node:path'
6
25
  import { watch } from 'chokidar'
7
26
  import { Effect } from 'effect'
8
27
 
28
+ import {
29
+ type DirectoryCreateError,
30
+ type DirectoryWalkError,
31
+ type FileReadError,
32
+ type FileWriteError,
33
+ type IndexCorruptedError,
34
+ WatchError,
35
+ } from '../errors/index.js'
36
+ import { getChokidarIgnorePatterns } from './ignore-patterns.js'
9
37
  import { buildIndex, type IndexOptions } from './indexer.js'
10
38
  import { createStorage, indexExists } from './storage.js'
11
39
 
40
+ /**
41
+ * Union of errors that can occur during watch operations
42
+ */
43
+ export type WatchDirectoryError =
44
+ | WatchError
45
+ | DirectoryWalkError
46
+ | DirectoryCreateError
47
+ | FileReadError
48
+ | FileWriteError
49
+ | IndexCorruptedError
50
+
12
51
  // ============================================================================
13
52
  // Watcher Types
14
53
  // ============================================================================
@@ -19,7 +58,11 @@ export interface WatcherOptions extends IndexOptions {
19
58
  documentsIndexed: number
20
59
  duration: number
21
60
  }) => void
22
- readonly onError?: (error: Error) => void
61
+ readonly onError?: (error: WatchError) => void
62
+ /** Whether to honor .gitignore for file watching (default: true) */
63
+ readonly honorGitignore?: boolean
64
+ /** Whether to honor .mdcontextignore for file watching (default: true) */
65
+ readonly honorMdcontextignore?: boolean
23
66
  }
24
67
 
25
68
  export interface Watcher {
@@ -36,7 +79,7 @@ const isMarkdownFile = (filePath: string): boolean =>
36
79
  export const watchDirectory = (
37
80
  rootPath: string,
38
81
  options: WatcherOptions = {},
39
- ): Effect.Effect<Watcher, Error> =>
82
+ ): Effect.Effect<Watcher, WatchDirectoryError> =>
40
83
  Effect.gen(function* () {
41
84
  const resolvedRoot = path.resolve(rootPath)
42
85
  const storage = createStorage(resolvedRoot)
@@ -77,18 +120,28 @@ export const watchDirectory = (
77
120
  })
78
121
  } catch (error) {
79
122
  options.onError?.(
80
- error instanceof Error ? error : new Error(String(error)),
123
+ new WatchError({
124
+ path: resolvedRoot,
125
+ message:
126
+ error instanceof Error ? error.message : 'Index rebuild failed',
127
+ cause: error,
128
+ }),
81
129
  )
82
130
  }
83
131
  }, debounceMs)
84
132
  }
85
133
 
86
- // Set up chokidar watcher
134
+ // Build ignore patterns for chokidar
135
+ const ignorePatterns = yield* getChokidarIgnorePatterns({
136
+ rootPath: resolvedRoot,
137
+ cliPatterns: options.exclude,
138
+ honorGitignore: options.honorGitignore ?? true,
139
+ honorMdcontextignore: options.honorMdcontextignore ?? true,
140
+ })
141
+
142
+ // Set up chokidar watcher with dynamic ignore patterns
87
143
  const watcher = watch(resolvedRoot, {
88
- ignored: [
89
- /(^|[/\\])\../, // Ignore dotfiles
90
- '**/node_modules/**',
91
- ],
144
+ ignored: ignorePatterns,
92
145
  persistent: true,
93
146
  ignoreInitial: true,
94
147
  })
@@ -116,7 +169,12 @@ export const watchDirectory = (
116
169
 
117
170
  watcher.on('error', (error: unknown) => {
118
171
  options.onError?.(
119
- error instanceof Error ? error : new Error(String(error)),
172
+ new WatchError({
173
+ path: resolvedRoot,
174
+ message:
175
+ error instanceof Error ? error.message : 'File watcher error',
176
+ cause: error,
177
+ }),
120
178
  )
121
179
  })
122
180
 
package/src/index.ts CHANGED
@@ -2,7 +2,29 @@
2
2
  * mdcontext - Token-efficient markdown analysis for LLMs
3
3
  */
4
4
 
5
+ // Config utilities for user config files
6
+ export type { PartialMdContextConfig } from './config/service.js'
5
7
  export * from './core/index.js'
6
8
  export * from './index/index.js'
7
9
  export * from './parser/index.js'
8
10
  export * from './utils/index.js'
11
+
12
+ /**
13
+ * Type-safe configuration helper for mdcontext.config.ts files.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { defineConfig } from 'mdcontext'
18
+ *
19
+ * export default defineConfig({
20
+ * index: {
21
+ * maxDepth: 5,
22
+ * },
23
+ * })
24
+ * ```
25
+ */
26
+ export const defineConfig = <
27
+ T extends import('./config/service.js').PartialMdContextConfig,
28
+ >(
29
+ config: T,
30
+ ): T => config