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,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for keyword search context flags
|
|
3
|
+
*
|
|
4
|
+
* Tests the -C, -A, -B flags that show lines of context around matches.
|
|
5
|
+
* This test specifically validates that context lines are properly included
|
|
6
|
+
* in search results, which is the reported bug.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs/promises'
|
|
10
|
+
import * as path from 'node:path'
|
|
11
|
+
import { Effect } from 'effect'
|
|
12
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
13
|
+
import { buildIndex } from '../../src/index/indexer.js'
|
|
14
|
+
import { searchContent } from '../../src/search/searcher.js'
|
|
15
|
+
|
|
16
|
+
const TEST_DIR = path.join(process.cwd(), 'tests', 'fixtures', 'context-search')
|
|
17
|
+
|
|
18
|
+
const runEffect = <A, E>(effect: Effect.Effect<A, E>) =>
|
|
19
|
+
Effect.runPromise(effect)
|
|
20
|
+
|
|
21
|
+
describe('keyword search context flags', () => {
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
await fs.mkdir(TEST_DIR, { recursive: true })
|
|
24
|
+
|
|
25
|
+
await fs.writeFile(
|
|
26
|
+
path.join(TEST_DIR, 'example.md'),
|
|
27
|
+
`# Test Document
|
|
28
|
+
|
|
29
|
+
## Section One
|
|
30
|
+
|
|
31
|
+
Line 1: This is the first line
|
|
32
|
+
Line 2: This contains the TARGET word
|
|
33
|
+
Line 3: This is the third line
|
|
34
|
+
Line 4: This is the fourth line
|
|
35
|
+
Line 5: This is the fifth line
|
|
36
|
+
|
|
37
|
+
## Section Two
|
|
38
|
+
|
|
39
|
+
Line A: Before the match
|
|
40
|
+
Line B: Before the match closer
|
|
41
|
+
Line C: Here is another TARGET occurrence
|
|
42
|
+
Line D: After the match closer
|
|
43
|
+
Line E: After the match
|
|
44
|
+
|
|
45
|
+
## Section Three
|
|
46
|
+
|
|
47
|
+
Line X: Only one line above
|
|
48
|
+
Line Y: This has TARGET in it
|
|
49
|
+
Line Z: Only one line below
|
|
50
|
+
|
|
51
|
+
## Section Four
|
|
52
|
+
|
|
53
|
+
Line 1: First
|
|
54
|
+
Line 2: Second TARGET here
|
|
55
|
+
Line 3: Third
|
|
56
|
+
Line 4: Fourth
|
|
57
|
+
Line 5: Fifth TARGET here
|
|
58
|
+
Line 6: Sixth
|
|
59
|
+
`,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
await fs.writeFile(
|
|
63
|
+
path.join(TEST_DIR, 'multiline.md'),
|
|
64
|
+
`# Multiline Test
|
|
65
|
+
|
|
66
|
+
## Code Section
|
|
67
|
+
|
|
68
|
+
\`\`\`typescript
|
|
69
|
+
function hello() {
|
|
70
|
+
console.log("TARGET");
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
Text after code.
|
|
76
|
+
|
|
77
|
+
## List Section
|
|
78
|
+
|
|
79
|
+
- Item 1
|
|
80
|
+
- Item with TARGET
|
|
81
|
+
- Item 3
|
|
82
|
+
|
|
83
|
+
End of list.
|
|
84
|
+
`,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const shouldRebuild = process.env.REBUILD_TEST_INDEX === 'true'
|
|
88
|
+
await runEffect(buildIndex(TEST_DIR, { force: shouldRebuild }))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
afterAll(async () => {
|
|
92
|
+
await fs.rm(TEST_DIR, { recursive: true, force: true })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('-C flag (context around matches)', () => {
|
|
96
|
+
it('should include context lines when -C is specified', async () => {
|
|
97
|
+
const results = await runEffect(
|
|
98
|
+
searchContent(TEST_DIR, {
|
|
99
|
+
content: 'TARGET',
|
|
100
|
+
contextBefore: 2,
|
|
101
|
+
contextAfter: 2,
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(results.length).toBeGreaterThan(0)
|
|
106
|
+
|
|
107
|
+
const firstResult = results[0]
|
|
108
|
+
expect(firstResult?.matches).toBeDefined()
|
|
109
|
+
expect(firstResult!.matches!.length).toBeGreaterThan(0)
|
|
110
|
+
|
|
111
|
+
const firstMatch = firstResult!.matches![0]
|
|
112
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
113
|
+
expect(firstMatch!.contextLines!.length).toBeGreaterThan(1)
|
|
114
|
+
|
|
115
|
+
const matchingLine = firstMatch!.contextLines!.find((ctx) => ctx.isMatch)
|
|
116
|
+
expect(matchingLine).toBeDefined()
|
|
117
|
+
expect(matchingLine?.line).toContain('TARGET')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should show exactly the requested number of context lines', async () => {
|
|
121
|
+
const results = await runEffect(
|
|
122
|
+
searchContent(TEST_DIR, {
|
|
123
|
+
content: 'TARGET',
|
|
124
|
+
pathPattern: 'example.md',
|
|
125
|
+
contextBefore: 2,
|
|
126
|
+
contextAfter: 2,
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(results.length).toBeGreaterThan(0)
|
|
131
|
+
|
|
132
|
+
for (const result of results) {
|
|
133
|
+
if (!result.matches) continue
|
|
134
|
+
|
|
135
|
+
for (const match of result.matches) {
|
|
136
|
+
expect(match.contextLines).toBeDefined()
|
|
137
|
+
|
|
138
|
+
const matchIndex = match.contextLines!.findIndex((ctx) => ctx.isMatch)
|
|
139
|
+
expect(matchIndex).toBeGreaterThanOrEqual(0)
|
|
140
|
+
|
|
141
|
+
const linesBefore = matchIndex
|
|
142
|
+
const linesAfter = match.contextLines!.length - matchIndex - 1
|
|
143
|
+
|
|
144
|
+
expect(linesBefore).toBeLessThanOrEqual(2)
|
|
145
|
+
expect(linesAfter).toBeLessThanOrEqual(2)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should work with -C 0 (no context)', async () => {
|
|
151
|
+
const results = await runEffect(
|
|
152
|
+
searchContent(TEST_DIR, {
|
|
153
|
+
content: 'TARGET',
|
|
154
|
+
pathPattern: 'example.md',
|
|
155
|
+
contextBefore: 0,
|
|
156
|
+
contextAfter: 0,
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
expect(results.length).toBeGreaterThan(0)
|
|
161
|
+
|
|
162
|
+
const firstResult = results[0]
|
|
163
|
+
expect(firstResult?.matches).toBeDefined()
|
|
164
|
+
|
|
165
|
+
const firstMatch = firstResult!.matches![0]
|
|
166
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
167
|
+
expect(firstMatch!.contextLines!.length).toBe(1)
|
|
168
|
+
expect(firstMatch!.contextLines![0]?.isMatch).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should work with large context values', async () => {
|
|
172
|
+
const results = await runEffect(
|
|
173
|
+
searchContent(TEST_DIR, {
|
|
174
|
+
content: 'TARGET',
|
|
175
|
+
pathPattern: 'example.md',
|
|
176
|
+
contextBefore: 10,
|
|
177
|
+
contextAfter: 10,
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expect(results.length).toBeGreaterThan(0)
|
|
182
|
+
|
|
183
|
+
const firstResult = results[0]
|
|
184
|
+
expect(firstResult?.matches).toBeDefined()
|
|
185
|
+
expect(firstResult!.matches![0]?.contextLines).toBeDefined()
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('-B flag (before context)', () => {
|
|
190
|
+
it('should include only lines before match when -B is specified', async () => {
|
|
191
|
+
const results = await runEffect(
|
|
192
|
+
searchContent(TEST_DIR, {
|
|
193
|
+
content: 'TARGET',
|
|
194
|
+
pathPattern: 'example.md',
|
|
195
|
+
contextBefore: 3,
|
|
196
|
+
contextAfter: 0,
|
|
197
|
+
}),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
expect(results.length).toBeGreaterThan(0)
|
|
201
|
+
|
|
202
|
+
const firstResult = results[0]
|
|
203
|
+
const firstMatch = firstResult!.matches![0]
|
|
204
|
+
|
|
205
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
206
|
+
|
|
207
|
+
const matchIndex = firstMatch!.contextLines!.findIndex(
|
|
208
|
+
(ctx) => ctx.isMatch,
|
|
209
|
+
)
|
|
210
|
+
const linesAfter = firstMatch!.contextLines!.length - matchIndex - 1
|
|
211
|
+
|
|
212
|
+
expect(matchIndex).toBeGreaterThan(0)
|
|
213
|
+
expect(linesAfter).toBe(0)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('-A flag (after context)', () => {
|
|
218
|
+
it('should include only lines after match when -A is specified', async () => {
|
|
219
|
+
const results = await runEffect(
|
|
220
|
+
searchContent(TEST_DIR, {
|
|
221
|
+
content: 'TARGET',
|
|
222
|
+
pathPattern: 'example.md',
|
|
223
|
+
contextBefore: 0,
|
|
224
|
+
contextAfter: 3,
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
expect(results.length).toBeGreaterThan(0)
|
|
229
|
+
|
|
230
|
+
const firstResult = results[0]
|
|
231
|
+
const firstMatch = firstResult!.matches![0]
|
|
232
|
+
|
|
233
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
234
|
+
|
|
235
|
+
const matchIndex = firstMatch!.contextLines!.findIndex(
|
|
236
|
+
(ctx) => ctx.isMatch,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
expect(matchIndex).toBe(0)
|
|
240
|
+
expect(firstMatch!.contextLines!.length).toBeGreaterThan(1)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('context line content validation', () => {
|
|
245
|
+
it('should preserve exact line content in context', async () => {
|
|
246
|
+
const results = await runEffect(
|
|
247
|
+
searchContent(TEST_DIR, {
|
|
248
|
+
content: 'TARGET',
|
|
249
|
+
pathPattern: 'example.md',
|
|
250
|
+
contextBefore: 1,
|
|
251
|
+
contextAfter: 1,
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
expect(results.length).toBeGreaterThan(0)
|
|
256
|
+
|
|
257
|
+
for (const result of results) {
|
|
258
|
+
if (!result.matches) continue
|
|
259
|
+
|
|
260
|
+
for (const match of result.matches) {
|
|
261
|
+
expect(match.contextLines).toBeDefined()
|
|
262
|
+
|
|
263
|
+
for (const ctx of match.contextLines!) {
|
|
264
|
+
expect(ctx.line).toBeTruthy()
|
|
265
|
+
expect(typeof ctx.line).toBe('string')
|
|
266
|
+
expect(ctx.lineNumber).toBeGreaterThan(0)
|
|
267
|
+
expect(typeof ctx.isMatch).toBe('boolean')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const matchingLine = match.contextLines!.find((ctx) => ctx.isMatch)
|
|
271
|
+
expect(matchingLine).toBeDefined()
|
|
272
|
+
expect(matchingLine?.line).toContain('TARGET')
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should have correct line numbers in context', async () => {
|
|
278
|
+
const results = await runEffect(
|
|
279
|
+
searchContent(TEST_DIR, {
|
|
280
|
+
content: 'TARGET',
|
|
281
|
+
pathPattern: 'example.md',
|
|
282
|
+
contextBefore: 2,
|
|
283
|
+
contextAfter: 2,
|
|
284
|
+
}),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
expect(results.length).toBeGreaterThan(0)
|
|
288
|
+
|
|
289
|
+
for (const result of results) {
|
|
290
|
+
if (!result.matches) continue
|
|
291
|
+
|
|
292
|
+
for (const match of result.matches) {
|
|
293
|
+
expect(match.contextLines).toBeDefined()
|
|
294
|
+
|
|
295
|
+
for (let i = 1; i < match.contextLines!.length; i++) {
|
|
296
|
+
const prevLine = match.contextLines![i - 1]
|
|
297
|
+
const currLine = match.contextLines![i]
|
|
298
|
+
|
|
299
|
+
expect(currLine?.lineNumber).toBe((prevLine?.lineNumber ?? 0) + 1)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe('edge cases', () => {
|
|
307
|
+
it('should handle match at start of section (limited before context)', async () => {
|
|
308
|
+
const results = await runEffect(
|
|
309
|
+
searchContent(TEST_DIR, {
|
|
310
|
+
content: 'Only one line above',
|
|
311
|
+
contextBefore: 5,
|
|
312
|
+
contextAfter: 1,
|
|
313
|
+
}),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
expect(results.length).toBeGreaterThan(0)
|
|
317
|
+
|
|
318
|
+
const firstResult = results[0]
|
|
319
|
+
const firstMatch = firstResult!.matches![0]
|
|
320
|
+
|
|
321
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
322
|
+
|
|
323
|
+
const matchIndex = firstMatch!.contextLines!.findIndex(
|
|
324
|
+
(ctx) => ctx.isMatch,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
expect(matchIndex).toBeLessThan(5)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should handle match at end of section (limited after context)', async () => {
|
|
331
|
+
const results = await runEffect(
|
|
332
|
+
searchContent(TEST_DIR, {
|
|
333
|
+
content: 'Only one line below',
|
|
334
|
+
contextBefore: 1,
|
|
335
|
+
contextAfter: 5,
|
|
336
|
+
}),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
expect(results.length).toBeGreaterThan(0)
|
|
340
|
+
|
|
341
|
+
const firstResult = results[0]
|
|
342
|
+
const firstMatch = firstResult!.matches![0]
|
|
343
|
+
|
|
344
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
345
|
+
|
|
346
|
+
const matchIndex = firstMatch!.contextLines!.findIndex(
|
|
347
|
+
(ctx) => ctx.isMatch,
|
|
348
|
+
)
|
|
349
|
+
const linesAfter = firstMatch!.contextLines!.length - matchIndex - 1
|
|
350
|
+
|
|
351
|
+
expect(linesAfter).toBeLessThan(5)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should handle multiple matches in same section', async () => {
|
|
355
|
+
const results = await runEffect(
|
|
356
|
+
searchContent(TEST_DIR, {
|
|
357
|
+
content: 'TARGET',
|
|
358
|
+
pathPattern: 'example.md',
|
|
359
|
+
contextBefore: 1,
|
|
360
|
+
contextAfter: 1,
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
const sectionFour = results.find((r) =>
|
|
365
|
+
r.section.heading.includes('Section Four'),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
expect(sectionFour).toBeDefined()
|
|
369
|
+
expect(sectionFour!.matches!.length).toBeGreaterThanOrEqual(2)
|
|
370
|
+
|
|
371
|
+
for (const match of sectionFour!.matches!) {
|
|
372
|
+
expect(match.contextLines).toBeDefined()
|
|
373
|
+
const matchingLine = match.contextLines!.find((ctx) => ctx.isMatch)
|
|
374
|
+
expect(matchingLine?.line).toContain('TARGET')
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
describe('default context behavior', () => {
|
|
380
|
+
it('should use default context of 1 when not specified', async () => {
|
|
381
|
+
const results = await runEffect(
|
|
382
|
+
searchContent(TEST_DIR, {
|
|
383
|
+
content: 'TARGET',
|
|
384
|
+
pathPattern: 'example.md',
|
|
385
|
+
}),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
expect(results.length).toBeGreaterThan(0)
|
|
389
|
+
|
|
390
|
+
const firstResult = results[0]
|
|
391
|
+
const firstMatch = firstResult!.matches![0]
|
|
392
|
+
|
|
393
|
+
expect(firstMatch?.contextLines).toBeDefined()
|
|
394
|
+
|
|
395
|
+
const matchIndex = firstMatch!.contextLines!.findIndex(
|
|
396
|
+
(ctx) => ctx.isMatch,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
expect(matchIndex).toBeGreaterThanOrEqual(0)
|
|
400
|
+
expect(matchIndex).toBeLessThanOrEqual(1)
|
|
401
|
+
|
|
402
|
+
const linesAfter = firstMatch!.contextLines!.length - matchIndex - 1
|
|
403
|
+
expect(linesAfter).toBeLessThanOrEqual(1)
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('context with code blocks', () => {
|
|
408
|
+
it('should include context lines in code blocks', async () => {
|
|
409
|
+
const results = await runEffect(
|
|
410
|
+
searchContent(TEST_DIR, {
|
|
411
|
+
content: 'TARGET',
|
|
412
|
+
pathPattern: 'multiline.md',
|
|
413
|
+
contextBefore: 2,
|
|
414
|
+
contextAfter: 2,
|
|
415
|
+
}),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
expect(results.length).toBeGreaterThan(0)
|
|
419
|
+
|
|
420
|
+
const codeResult = results.find((r) => r.section.heading.includes('Code'))
|
|
421
|
+
|
|
422
|
+
if (codeResult?.matches && codeResult.matches.length > 0) {
|
|
423
|
+
const match = codeResult.matches[0]
|
|
424
|
+
expect(match?.contextLines).toBeDefined()
|
|
425
|
+
expect(match!.contextLines!.length).toBeGreaterThan(1)
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
describe('CRITICAL: -C flag must show context (regression test)', () => {
|
|
431
|
+
it('MUST FAIL if context lines are missing when -C is specified', async () => {
|
|
432
|
+
const results = await runEffect(
|
|
433
|
+
searchContent(TEST_DIR, {
|
|
434
|
+
content: 'TARGET',
|
|
435
|
+
pathPattern: 'example.md',
|
|
436
|
+
contextBefore: 2,
|
|
437
|
+
contextAfter: 2,
|
|
438
|
+
}),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
expect(results.length).toBeGreaterThan(0)
|
|
442
|
+
|
|
443
|
+
for (const result of results) {
|
|
444
|
+
expect(result.matches).toBeDefined()
|
|
445
|
+
expect(result.matches!.length).toBeGreaterThan(0)
|
|
446
|
+
|
|
447
|
+
for (const match of result.matches!) {
|
|
448
|
+
expect(match.contextLines).toBeDefined()
|
|
449
|
+
expect(
|
|
450
|
+
match.contextLines!.length,
|
|
451
|
+
`Context lines must be present when contextBefore/contextAfter is specified. ` +
|
|
452
|
+
`Expected context around match at line ${match.lineNumber} but got no context lines.`,
|
|
453
|
+
).toBeGreaterThan(1)
|
|
454
|
+
|
|
455
|
+
const matchingLine = match.contextLines!.find((ctx) => ctx.isMatch)
|
|
456
|
+
expect(
|
|
457
|
+
matchingLine,
|
|
458
|
+
'At least one context line should be marked as the matching line',
|
|
459
|
+
).toBeDefined()
|
|
460
|
+
|
|
461
|
+
expect(
|
|
462
|
+
matchingLine?.line,
|
|
463
|
+
'The matching line should contain the search term',
|
|
464
|
+
).toContain('TARGET')
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
})
|