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.
- package/.changeset/config.json +9 -9
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +206 -3
- package/biome.json +1 -1
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +85 -89
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +718 -657
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1533 -1423
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.js +4072 -629
- package/dist/index.d.ts +420 -33
- package/dist/index.js +8 -15
- package/dist/mcp/server.js +103 -7
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +44 -5
- package/docs/020-current-implementation.md +8 -8
- package/docs/021-DOGFOODING-FINDINGS.md +1 -1
- package/docs/CONFIG.md +1123 -0
- package/docs/ERRORS.md +383 -0
- package/docs/summarization.md +320 -0
- package/justfile +40 -0
- package/package.json +39 -33
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +32 -37
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +2 -2
- package/src/cli/cli.test.ts +230 -33
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +97 -9
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +210 -30
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/search.ts +894 -64
- package/src/cli/commands/stats.ts +3 -0
- package/src/cli/commands/tree.ts +26 -5
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +66 -0
- package/src/cli/help.ts +209 -7
- package/src/cli/main.ts +348 -58
- package/src/cli/options.ts +10 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/utils.ts +150 -17
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/types.ts +6 -33
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +2 -0
- package/src/embeddings/openai-provider.ts +332 -83
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +780 -93
- package/src/embeddings/types.ts +293 -16
- package/src/embeddings/vector-store.ts +486 -77
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/indexer.ts +286 -48
- package/src/index/storage.ts +94 -30
- package/src/index/types.ts +40 -2
- package/src/index/watcher.ts +67 -9
- package/src/index.ts +22 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +135 -6
- package/src/parser/parser.ts +18 -19
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +125 -3
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/searcher.test.ts +99 -1
- package/src/search/searcher.ts +189 -67
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/summarizer.ts +104 -35
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/vitest.config.ts +1 -6
- package/AGENTS.md +0 -46
- package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
package/src/index/storage.ts
CHANGED
|
@@ -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 = (
|
|
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) =>
|
|
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>(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
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) =>
|
|
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<
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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) =>
|
|
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
|
})
|
package/src/index/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/index/watcher.ts
CHANGED
|
@@ -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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|