mdcontext 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.changeset/config.json +9 -9
  2. package/.claude/settings.local.json +25 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +85 -0
  5. package/CONTRIBUTING.md +186 -0
  6. package/NOTES/NOTES +44 -0
  7. package/README.md +206 -3
  8. package/biome.json +1 -1
  9. package/dist/chunk-23UPXDNL.js +3044 -0
  10. package/dist/chunk-2W7MO2DL.js +1366 -0
  11. package/dist/chunk-3NUAZGMA.js +1689 -0
  12. package/dist/chunk-7TOWB2XB.js +366 -0
  13. package/dist/chunk-7XOTOADQ.js +3065 -0
  14. package/dist/chunk-AH2PDM2K.js +3042 -0
  15. package/dist/chunk-BNXWSZ63.js +3742 -0
  16. package/dist/chunk-BTL5DJVU.js +3222 -0
  17. package/dist/chunk-HDHYG7E4.js +104 -0
  18. package/dist/chunk-HLR4KZBP.js +3234 -0
  19. package/dist/chunk-IP3FRFEB.js +1045 -0
  20. package/dist/chunk-KHU56VDO.js +3042 -0
  21. package/dist/chunk-KRYIFLQR.js +85 -89
  22. package/dist/chunk-LBSDNLEM.js +287 -0
  23. package/dist/chunk-MNTQ7HCP.js +2643 -0
  24. package/dist/chunk-MUJELQQ6.js +1387 -0
  25. package/dist/chunk-MXJGMSLV.js +2199 -0
  26. package/dist/chunk-N6QJGC3Z.js +2636 -0
  27. package/dist/chunk-OBELGBPM.js +1713 -0
  28. package/dist/chunk-OT7R5XTA.js +3192 -0
  29. package/dist/chunk-P7X4RA2T.js +106 -0
  30. package/dist/chunk-PIDUQNC2.js +3185 -0
  31. package/dist/chunk-POGCDIH4.js +3187 -0
  32. package/dist/chunk-PSIEOQGZ.js +3043 -0
  33. package/dist/chunk-PVRT3IHA.js +3238 -0
  34. package/dist/chunk-QNN4TT23.js +1430 -0
  35. package/dist/chunk-RE3R45RJ.js +3042 -0
  36. package/dist/chunk-S7E6TFX6.js +718 -657
  37. package/dist/chunk-SG6GLU4U.js +1378 -0
  38. package/dist/chunk-SJCDV2ST.js +274 -0
  39. package/dist/chunk-SYE5XLF3.js +104 -0
  40. package/dist/chunk-T5VLYBZD.js +103 -0
  41. package/dist/chunk-TOQB7VWU.js +3238 -0
  42. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  43. package/dist/chunk-VVTGZNBT.js +1533 -1423
  44. package/dist/chunk-W7Q4RFEV.js +104 -0
  45. package/dist/chunk-XTYYVRLO.js +3190 -0
  46. package/dist/chunk-Y6MDYVJD.js +3063 -0
  47. package/dist/cli/main.js +4072 -629
  48. package/dist/index.d.ts +420 -33
  49. package/dist/index.js +8 -15
  50. package/dist/mcp/server.js +103 -7
  51. package/dist/schema-BAWSG7KY.js +22 -0
  52. package/dist/schema-E3QUPL26.js +20 -0
  53. package/dist/schema-EHL7WUT6.js +20 -0
  54. package/docs/019-USAGE.md +44 -5
  55. package/docs/020-current-implementation.md +8 -8
  56. package/docs/021-DOGFOODING-FINDINGS.md +1 -1
  57. package/docs/CONFIG.md +1123 -0
  58. package/docs/ERRORS.md +383 -0
  59. package/docs/summarization.md +320 -0
  60. package/justfile +40 -0
  61. package/package.json +39 -33
  62. package/research/INDEX.md +315 -0
  63. package/research/code-review/README.md +90 -0
  64. package/research/code-review/cli-error-handling-review.md +979 -0
  65. package/research/code-review/code-review-validation-report.md +464 -0
  66. package/research/code-review/main-ts-review.md +1128 -0
  67. package/research/config-docs/SUMMARY.md +357 -0
  68. package/research/config-docs/TEST-RESULTS.md +776 -0
  69. package/research/config-docs/TODO.md +542 -0
  70. package/research/config-docs/analysis.md +744 -0
  71. package/research/config-docs/fix-validation.md +502 -0
  72. package/research/config-docs/help-audit.md +264 -0
  73. package/research/config-docs/help-system-analysis.md +890 -0
  74. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  75. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  76. package/research/issue-review.md +603 -0
  77. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  78. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  79. package/research/llm-summarization/anthropic-2026.md +367 -0
  80. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  81. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  82. package/research/llm-summarization/openai-2026.md +473 -0
  83. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  84. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  85. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  86. package/research/llm-summarization/prototype-results.md +56 -0
  87. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  88. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  89. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  90. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  91. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  92. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  93. package/research/mdcontext-pudding/02-search.md +970 -0
  94. package/research/mdcontext-pudding/03-context.md +779 -0
  95. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  96. package/research/mdcontext-pudding/04-tree.md +704 -0
  97. package/research/mdcontext-pudding/05-config.md +1038 -0
  98. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  99. package/research/mdcontext-pudding/06-links.md +679 -0
  100. package/research/mdcontext-pudding/07-stats.md +693 -0
  101. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  102. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  103. package/research/mdcontext-pudding/README.md +168 -0
  104. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  105. package/research/research-quality-review.md +834 -0
  106. package/research/semantic-search/embedding-text-analysis.md +156 -0
  107. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  108. package/research/semantic-search/query-processing-analysis.md +207 -0
  109. package/research/semantic-search/root-cause-and-solution.md +114 -0
  110. package/research/semantic-search/threshold-validation-report.md +69 -0
  111. package/research/semantic-search/vector-search-analysis.md +63 -0
  112. package/research/test-path-issues.md +276 -0
  113. package/review/ALP-76/1-error-type-design.md +962 -0
  114. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  115. package/review/ALP-76/3-error-presentation.md +624 -0
  116. package/review/ALP-76/4-test-coverage.md +625 -0
  117. package/review/ALP-76/5-migration-completeness.md +440 -0
  118. package/review/ALP-76/6-effect-best-practices.md +755 -0
  119. package/scripts/apply-branch-protection.sh +47 -0
  120. package/scripts/branch-protection-templates.json +79 -0
  121. package/scripts/prototype-summarization.ts +346 -0
  122. package/scripts/rebuild-hnswlib.js +32 -37
  123. package/scripts/setup-branch-protection.sh +64 -0
  124. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  125. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  126. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  127. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  128. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  129. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  130. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  131. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  132. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  133. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  134. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  135. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  136. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  137. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  138. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  139. package/src/cli/argv-preprocessor.test.ts +2 -2
  140. package/src/cli/cli.test.ts +230 -33
  141. package/src/cli/commands/config-cmd.ts +642 -0
  142. package/src/cli/commands/context.ts +97 -9
  143. package/src/cli/commands/duplicates.ts +122 -0
  144. package/src/cli/commands/embeddings.ts +529 -0
  145. package/src/cli/commands/index-cmd.ts +210 -30
  146. package/src/cli/commands/index.ts +3 -0
  147. package/src/cli/commands/search.ts +894 -64
  148. package/src/cli/commands/stats.ts +3 -0
  149. package/src/cli/commands/tree.ts +26 -5
  150. package/src/cli/config-layer.ts +176 -0
  151. package/src/cli/error-handler.test.ts +235 -0
  152. package/src/cli/error-handler.ts +655 -0
  153. package/src/cli/flag-schemas.ts +66 -0
  154. package/src/cli/help.ts +209 -7
  155. package/src/cli/main.ts +348 -58
  156. package/src/cli/options.ts +10 -0
  157. package/src/cli/shared-error-handling.ts +199 -0
  158. package/src/cli/utils.ts +150 -17
  159. package/src/config/file-provider.test.ts +320 -0
  160. package/src/config/file-provider.ts +273 -0
  161. package/src/config/index.ts +72 -0
  162. package/src/config/integration.test.ts +667 -0
  163. package/src/config/precedence.test.ts +277 -0
  164. package/src/config/precedence.ts +451 -0
  165. package/src/config/schema.test.ts +414 -0
  166. package/src/config/schema.ts +603 -0
  167. package/src/config/service.test.ts +320 -0
  168. package/src/config/service.ts +243 -0
  169. package/src/config/testing.test.ts +264 -0
  170. package/src/config/testing.ts +110 -0
  171. package/src/core/types.ts +6 -33
  172. package/src/duplicates/detector.test.ts +183 -0
  173. package/src/duplicates/detector.ts +414 -0
  174. package/src/duplicates/index.ts +18 -0
  175. package/src/embeddings/embedding-namespace.test.ts +300 -0
  176. package/src/embeddings/embedding-namespace.ts +947 -0
  177. package/src/embeddings/heading-boost.test.ts +222 -0
  178. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  179. package/src/embeddings/hyde.test.ts +272 -0
  180. package/src/embeddings/hyde.ts +264 -0
  181. package/src/embeddings/index.ts +2 -0
  182. package/src/embeddings/openai-provider.ts +332 -83
  183. package/src/embeddings/pricing.json +22 -0
  184. package/src/embeddings/provider-constants.ts +204 -0
  185. package/src/embeddings/provider-errors.test.ts +967 -0
  186. package/src/embeddings/provider-errors.ts +565 -0
  187. package/src/embeddings/provider-factory.test.ts +240 -0
  188. package/src/embeddings/provider-factory.ts +225 -0
  189. package/src/embeddings/provider-integration.test.ts +788 -0
  190. package/src/embeddings/query-preprocessing.test.ts +187 -0
  191. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  192. package/src/embeddings/semantic-search.ts +780 -93
  193. package/src/embeddings/types.ts +293 -16
  194. package/src/embeddings/vector-store.ts +486 -77
  195. package/src/embeddings/voyage-provider.ts +313 -0
  196. package/src/errors/errors.test.ts +845 -0
  197. package/src/errors/index.ts +533 -0
  198. package/src/index/ignore-patterns.test.ts +354 -0
  199. package/src/index/ignore-patterns.ts +305 -0
  200. package/src/index/indexer.ts +286 -48
  201. package/src/index/storage.ts +94 -30
  202. package/src/index/types.ts +40 -2
  203. package/src/index/watcher.ts +67 -9
  204. package/src/index.ts +22 -0
  205. package/src/integration/search-keyword.test.ts +678 -0
  206. package/src/mcp/server.ts +135 -6
  207. package/src/parser/parser.ts +18 -19
  208. package/src/parser/section-filter.test.ts +277 -0
  209. package/src/parser/section-filter.ts +125 -3
  210. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  211. package/src/search/bm25-store.ts +366 -0
  212. package/src/search/cross-encoder.test.ts +253 -0
  213. package/src/search/cross-encoder.ts +406 -0
  214. package/src/search/fuzzy-search.test.ts +419 -0
  215. package/src/search/fuzzy-search.ts +273 -0
  216. package/src/search/hybrid-search.ts +448 -0
  217. package/src/search/path-matcher.test.ts +276 -0
  218. package/src/search/path-matcher.ts +33 -0
  219. package/src/search/searcher.test.ts +99 -1
  220. package/src/search/searcher.ts +189 -67
  221. package/src/search/wink-bm25.d.ts +30 -0
  222. package/src/summarization/cli-providers/claude.ts +202 -0
  223. package/src/summarization/cli-providers/detection.test.ts +273 -0
  224. package/src/summarization/cli-providers/detection.ts +118 -0
  225. package/src/summarization/cli-providers/index.ts +8 -0
  226. package/src/summarization/cost.test.ts +139 -0
  227. package/src/summarization/cost.ts +102 -0
  228. package/src/summarization/error-handler.test.ts +127 -0
  229. package/src/summarization/error-handler.ts +111 -0
  230. package/src/summarization/index.ts +102 -0
  231. package/src/summarization/pipeline.test.ts +498 -0
  232. package/src/summarization/pipeline.ts +231 -0
  233. package/src/summarization/prompts.test.ts +269 -0
  234. package/src/summarization/prompts.ts +133 -0
  235. package/src/summarization/provider-factory.test.ts +396 -0
  236. package/src/summarization/provider-factory.ts +178 -0
  237. package/src/summarization/types.ts +184 -0
  238. package/src/summarize/summarizer.ts +104 -35
  239. package/src/types/huggingface-transformers.d.ts +66 -0
  240. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  241. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  242. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  243. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
  244. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
  245. package/tests/integration/embed-index.test.ts +712 -0
  246. package/tests/integration/search-context.test.ts +469 -0
  247. package/tests/integration/search-semantic.test.ts +522 -0
  248. package/vitest.config.ts +1 -6
  249. package/AGENTS.md +0 -46
  250. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  251. package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
@@ -0,0 +1,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
+ })