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