mdcontext 0.0.1 → 0.1.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 (140) hide show
  1. package/.changeset/README.md +28 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/ci.yml +83 -0
  4. package/.github/workflows/release.yml +113 -0
  5. package/.tldrignore +112 -0
  6. package/AGENTS.md +46 -0
  7. package/BACKLOG.md +338 -0
  8. package/README.md +231 -11
  9. package/biome.json +36 -0
  10. package/cspell.config.yaml +14 -0
  11. package/dist/chunk-KRYIFLQR.js +92 -0
  12. package/dist/chunk-S7E6TFX6.js +742 -0
  13. package/dist/chunk-VVTGZNBT.js +1519 -0
  14. package/dist/cli/main.d.ts +1 -0
  15. package/dist/cli/main.js +2015 -0
  16. package/dist/index.d.ts +266 -0
  17. package/dist/index.js +86 -0
  18. package/dist/mcp/server.d.ts +1 -0
  19. package/dist/mcp/server.js +376 -0
  20. package/docs/019-USAGE.md +586 -0
  21. package/docs/020-current-implementation.md +364 -0
  22. package/docs/021-DOGFOODING-FINDINGS.md +175 -0
  23. package/docs/BACKLOG.md +80 -0
  24. package/docs/DESIGN.md +439 -0
  25. package/docs/PROJECT.md +88 -0
  26. package/docs/ROADMAP.md +407 -0
  27. package/docs/test-links.md +9 -0
  28. package/package.json +69 -10
  29. package/pnpm-workspace.yaml +5 -0
  30. package/research/config-analysis/01-current-implementation.md +470 -0
  31. package/research/config-analysis/02-strategy-recommendation.md +428 -0
  32. package/research/config-analysis/03-task-candidates.md +715 -0
  33. package/research/config-analysis/033-research-configuration-management.md +828 -0
  34. package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
  35. package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
  36. package/research/dogfood/consolidated-tool-evaluation.md +373 -0
  37. package/research/dogfood/strategy-a/a-synthesis.md +184 -0
  38. package/research/dogfood/strategy-a/a1-docs.md +226 -0
  39. package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
  40. package/research/dogfood/strategy-a/a3-llm.md +164 -0
  41. package/research/dogfood/strategy-b/b-synthesis.md +228 -0
  42. package/research/dogfood/strategy-b/b1-architecture.md +207 -0
  43. package/research/dogfood/strategy-b/b2-gaps.md +258 -0
  44. package/research/dogfood/strategy-b/b3-workflows.md +250 -0
  45. package/research/dogfood/strategy-c/c-synthesis.md +451 -0
  46. package/research/dogfood/strategy-c/c1-explorer.md +192 -0
  47. package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
  48. package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
  49. package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
  50. package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
  51. package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
  52. package/research/effect-cli-error-handling.md +845 -0
  53. package/research/effect-errors-as-values.md +943 -0
  54. package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
  55. package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
  56. package/research/errors-task-analysis/embeddings-analysis.md +709 -0
  57. package/research/errors-task-analysis/index-search-analysis.md +812 -0
  58. package/research/mdcontext-error-analysis.md +521 -0
  59. package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
  60. package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
  61. package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
  62. package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
  63. package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
  64. package/research/semantic-search/002-research-embedding-models.md +490 -0
  65. package/research/semantic-search/003-research-rag-alternatives.md +523 -0
  66. package/research/semantic-search/004-research-vector-search.md +841 -0
  67. package/research/semantic-search/032-research-semantic-search.md +427 -0
  68. package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
  69. package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
  70. package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
  71. package/research/task-management-2026/03-lightweight-file-based.md +567 -0
  72. package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
  73. package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
  74. package/research/task-management-2026/linear/02-api-integrations.md +930 -0
  75. package/research/task-management-2026/linear/03-ai-features.md +368 -0
  76. package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
  77. package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
  78. package/scripts/rebuild-hnswlib.js +63 -0
  79. package/src/cli/argv-preprocessor.test.ts +210 -0
  80. package/src/cli/argv-preprocessor.ts +202 -0
  81. package/src/cli/cli.test.ts +430 -0
  82. package/src/cli/commands/backlinks.ts +54 -0
  83. package/src/cli/commands/context.ts +197 -0
  84. package/src/cli/commands/index-cmd.ts +300 -0
  85. package/src/cli/commands/index.ts +13 -0
  86. package/src/cli/commands/links.ts +52 -0
  87. package/src/cli/commands/search.ts +451 -0
  88. package/src/cli/commands/stats.ts +146 -0
  89. package/src/cli/commands/tree.ts +107 -0
  90. package/src/cli/flag-schemas.ts +275 -0
  91. package/src/cli/help.ts +386 -0
  92. package/src/cli/index.ts +9 -0
  93. package/src/cli/main.ts +145 -0
  94. package/src/cli/options.ts +31 -0
  95. package/src/cli/typo-suggester.test.ts +105 -0
  96. package/src/cli/typo-suggester.ts +130 -0
  97. package/src/cli/utils.ts +126 -0
  98. package/src/core/index.ts +1 -0
  99. package/src/core/types.ts +140 -0
  100. package/src/embeddings/index.ts +8 -0
  101. package/src/embeddings/openai-provider.ts +165 -0
  102. package/src/embeddings/semantic-search.ts +583 -0
  103. package/src/embeddings/types.ts +82 -0
  104. package/src/embeddings/vector-store.ts +299 -0
  105. package/src/index/index.ts +4 -0
  106. package/src/index/indexer.ts +446 -0
  107. package/src/index/storage.ts +196 -0
  108. package/src/index/types.ts +109 -0
  109. package/src/index/watcher.ts +131 -0
  110. package/src/index.ts +8 -0
  111. package/src/mcp/server.ts +483 -0
  112. package/src/parser/index.ts +1 -0
  113. package/src/parser/parser.test.ts +291 -0
  114. package/src/parser/parser.ts +395 -0
  115. package/src/parser/section-filter.ts +270 -0
  116. package/src/search/query-parser.test.ts +260 -0
  117. package/src/search/query-parser.ts +319 -0
  118. package/src/search/searcher.test.ts +182 -0
  119. package/src/search/searcher.ts +602 -0
  120. package/src/summarize/budget-bugs.test.ts +620 -0
  121. package/src/summarize/formatters.ts +419 -0
  122. package/src/summarize/index.ts +20 -0
  123. package/src/summarize/summarizer.test.ts +275 -0
  124. package/src/summarize/summarizer.ts +528 -0
  125. package/src/summarize/verify-bugs.test.ts +238 -0
  126. package/src/utils/index.ts +1 -0
  127. package/src/utils/tokens.test.ts +142 -0
  128. package/src/utils/tokens.ts +186 -0
  129. package/tests/fixtures/cli/.mdcontext/config.json +8 -0
  130. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
  131. package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
  132. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +233 -0
  133. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  134. package/tests/fixtures/cli/.mdcontext/vectors.meta.json +1264 -0
  135. package/tests/fixtures/cli/README.md +9 -0
  136. package/tests/fixtures/cli/api-reference.md +11 -0
  137. package/tests/fixtures/cli/getting-started.md +11 -0
  138. package/tsconfig.json +26 -0
  139. package/vitest.config.ts +21 -0
  140. package/vitest.setup.ts +12 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Section filtering utilities for extracting specific sections from markdown documents
3
+ */
4
+
5
+ import type { MdDocument, MdSection } from '../core/types.js'
6
+
7
+ // ============================================================================
8
+ // Simple Glob Matching
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Simple glob pattern matching (supports * and ?)
13
+ */
14
+ const globMatch = (text: string, pattern: string): boolean => {
15
+ // Convert glob pattern to regex
16
+ const regexPattern = pattern
17
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except * and ?
18
+ .replace(/\*/g, '.*')
19
+ .replace(/\?/g, '.')
20
+
21
+ const regex = new RegExp(`^${regexPattern}$`, 'i')
22
+ return regex.test(text)
23
+ }
24
+
25
+ // ============================================================================
26
+ // Types
27
+ // ============================================================================
28
+
29
+ export interface SectionListItem {
30
+ readonly number: string
31
+ readonly heading: string
32
+ readonly level: number
33
+ readonly tokenCount: number
34
+ }
35
+
36
+ export interface SectionFilterOptions {
37
+ /** If true, don't include nested subsections */
38
+ readonly shallow?: boolean
39
+ }
40
+
41
+ // ============================================================================
42
+ // Section Map Building
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Build a flat list of all sections with their hierarchical numbers
47
+ * e.g., "1", "1.1", "1.2", "2", "2.1", etc.
48
+ */
49
+ export const buildSectionList = (document: MdDocument): SectionListItem[] => {
50
+ const result: SectionListItem[] = []
51
+
52
+ const processSection = (
53
+ section: MdSection,
54
+ prefix: string,
55
+ index: number,
56
+ ): void => {
57
+ const number = prefix ? `${prefix}.${index + 1}` : `${index + 1}`
58
+
59
+ result.push({
60
+ number,
61
+ heading: section.heading,
62
+ level: section.level,
63
+ tokenCount: section.metadata.tokenCount,
64
+ })
65
+
66
+ // Process children
67
+ section.children.forEach((child, i) => {
68
+ processSection(child, number, i)
69
+ })
70
+ }
71
+
72
+ document.sections.forEach((section, i) => {
73
+ processSection(section, '', i)
74
+ })
75
+
76
+ return result
77
+ }
78
+
79
+ /**
80
+ * Format section list for display
81
+ */
82
+ export const formatSectionList = (sections: SectionListItem[]): string => {
83
+ const lines: string[] = []
84
+
85
+ for (const section of sections) {
86
+ // Indent based on dots in number
87
+ const depth = (section.number.match(/\./g) || []).length
88
+ const indent = ' '.repeat(depth)
89
+ lines.push(
90
+ `${indent}${section.number}. ${section.heading} (${section.tokenCount} tokens)`,
91
+ )
92
+ }
93
+
94
+ return lines.join('\n')
95
+ }
96
+
97
+ // ============================================================================
98
+ // Section Matching
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Check if a section matches a selector (by number, exact name, or glob pattern)
103
+ */
104
+ const matchesSelector = (
105
+ section: SectionListItem,
106
+ selector: string,
107
+ ): boolean => {
108
+ // Check if it's a number match (e.g., "5.3")
109
+ if (/^[\d.]+$/.test(selector)) {
110
+ // Exact number match
111
+ return section.number === selector
112
+ }
113
+
114
+ // Check for exact heading match (case-insensitive)
115
+ if (section.heading.toLowerCase() === selector.toLowerCase()) {
116
+ return true
117
+ }
118
+
119
+ // Check for glob pattern match
120
+ if (selector.includes('*') || selector.includes('?')) {
121
+ return globMatch(section.heading, selector)
122
+ }
123
+
124
+ // Partial match (contains)
125
+ return section.heading.toLowerCase().includes(selector.toLowerCase())
126
+ }
127
+
128
+ /**
129
+ * Find all sections matching a selector
130
+ */
131
+ export const findMatchingSections = (
132
+ sectionList: SectionListItem[],
133
+ selector: string,
134
+ ): SectionListItem[] => {
135
+ return sectionList.filter((s) => matchesSelector(s, selector))
136
+ }
137
+
138
+ /**
139
+ * Get all descendant section numbers for a given section number
140
+ */
141
+ const getDescendantNumbers = (
142
+ sectionList: SectionListItem[],
143
+ parentNumber: string,
144
+ ): Set<string> => {
145
+ const result = new Set<string>()
146
+ const prefix = `${parentNumber}.`
147
+
148
+ for (const section of sectionList) {
149
+ if (section.number.startsWith(prefix)) {
150
+ result.add(section.number)
151
+ }
152
+ }
153
+
154
+ return result
155
+ }
156
+
157
+ // ============================================================================
158
+ // Section Content Extraction
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Extract content for specific sections from a document
163
+ */
164
+ export const extractSectionContent = (
165
+ document: MdDocument,
166
+ selector: string,
167
+ options: SectionFilterOptions = {},
168
+ ): {
169
+ sections: MdSection[]
170
+ matchedNumbers: string[]
171
+ } => {
172
+ const sectionList = buildSectionList(document)
173
+ const matchedSections = findMatchingSections(sectionList, selector)
174
+
175
+ if (matchedSections.length === 0) {
176
+ return { sections: [], matchedNumbers: [] }
177
+ }
178
+
179
+ // Get all section numbers to include
180
+ const numbersToInclude = new Set<string>()
181
+ const matchedNumbers: string[] = []
182
+
183
+ for (const matched of matchedSections) {
184
+ numbersToInclude.add(matched.number)
185
+ matchedNumbers.push(matched.number)
186
+
187
+ if (!options.shallow) {
188
+ // Include all descendants
189
+ const descendants = getDescendantNumbers(sectionList, matched.number)
190
+ for (const desc of descendants) {
191
+ numbersToInclude.add(desc)
192
+ }
193
+ }
194
+ }
195
+
196
+ // Build a map from section number to section for efficient lookup
197
+ const numberToSection = new Map<string, MdSection>()
198
+
199
+ const mapSections = (
200
+ sections: readonly MdSection[],
201
+ prefix: string,
202
+ ): void => {
203
+ sections.forEach((section, i) => {
204
+ const number = prefix ? `${prefix}.${i + 1}` : `${i + 1}`
205
+ numberToSection.set(number, section)
206
+ mapSections(section.children, number)
207
+ })
208
+ }
209
+
210
+ mapSections(document.sections, '')
211
+
212
+ // Extract matching sections
213
+ const extractedSections: MdSection[] = []
214
+
215
+ for (const number of matchedNumbers) {
216
+ const section = numberToSection.get(number)
217
+ if (section) {
218
+ if (options.shallow) {
219
+ // Clone without children for shallow mode
220
+ extractedSections.push({
221
+ ...section,
222
+ children: [],
223
+ })
224
+ } else {
225
+ extractedSections.push(section)
226
+ }
227
+ }
228
+ }
229
+
230
+ return { sections: extractedSections, matchedNumbers }
231
+ }
232
+
233
+ /**
234
+ * Format extracted sections as markdown content
235
+ */
236
+ export const formatExtractedSections = (sections: MdSection[]): string => {
237
+ const formatSection = (
238
+ section: MdSection,
239
+ includeChildren: boolean,
240
+ ): string => {
241
+ const lines: string[] = []
242
+
243
+ // Add heading
244
+ const headingPrefix = '#'.repeat(section.level)
245
+ lines.push(`${headingPrefix} ${section.heading}`)
246
+ lines.push('')
247
+
248
+ // Add content (strip the heading line if it starts with #)
249
+ const contentLines = section.content.split('\n')
250
+ const contentWithoutHeading = contentLines
251
+ .filter((line, i) => i > 0 || !line.startsWith('#'))
252
+ .join('\n')
253
+ .trim()
254
+
255
+ if (contentWithoutHeading) {
256
+ lines.push(contentWithoutHeading)
257
+ }
258
+
259
+ if (includeChildren) {
260
+ for (const child of section.children) {
261
+ lines.push('')
262
+ lines.push(formatSection(child, true))
263
+ }
264
+ }
265
+
266
+ return lines.join('\n')
267
+ }
268
+
269
+ return sections.map((s) => formatSection(s, true)).join('\n\n')
270
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Tests for query parser
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import {
7
+ buildHighlightPattern,
8
+ evaluateQuery,
9
+ isAdvancedQuery,
10
+ parseQuery,
11
+ type QueryNode,
12
+ } from './query-parser.js'
13
+
14
+ describe('query-parser', () => {
15
+ describe('parseQuery', () => {
16
+ it('should parse a single term', () => {
17
+ const result = parseQuery('auth')
18
+ expect(result).not.toBeNull()
19
+ expect(result!.ast).toEqual({ type: 'term', value: 'auth' })
20
+ expect(result!.terms).toEqual(['auth'])
21
+ })
22
+
23
+ it('should parse a quoted phrase', () => {
24
+ const result = parseQuery('"context resumption"')
25
+ expect(result).not.toBeNull()
26
+ expect(result!.ast).toEqual({
27
+ type: 'phrase',
28
+ value: 'context resumption',
29
+ })
30
+ expect(result!.phrases).toEqual(['context resumption'])
31
+ })
32
+
33
+ it('should parse AND operator', () => {
34
+ const result = parseQuery('auth AND criticism')
35
+ expect(result).not.toBeNull()
36
+ expect(result!.ast).toEqual({
37
+ type: 'and',
38
+ left: { type: 'term', value: 'auth' },
39
+ right: { type: 'term', value: 'criticism' },
40
+ })
41
+ })
42
+
43
+ it('should parse OR operator', () => {
44
+ const result = parseQuery('checkpoint OR gate')
45
+ expect(result).not.toBeNull()
46
+ expect(result!.ast).toEqual({
47
+ type: 'or',
48
+ left: { type: 'term', value: 'checkpoint' },
49
+ right: { type: 'term', value: 'gate' },
50
+ })
51
+ })
52
+
53
+ it('should parse NOT operator', () => {
54
+ const result = parseQuery('implementation NOT example')
55
+ expect(result).not.toBeNull()
56
+ // "implementation NOT example" is parsed as: implementation AND (NOT example)
57
+ expect(result!.ast).toEqual({
58
+ type: 'and',
59
+ left: { type: 'term', value: 'implementation' },
60
+ right: { type: 'not', operand: { type: 'term', value: 'example' } },
61
+ })
62
+ })
63
+
64
+ it('should parse grouped expressions', () => {
65
+ const result = parseQuery('auth AND (error OR bug)')
66
+ expect(result).not.toBeNull()
67
+ expect(result!.ast).toEqual({
68
+ type: 'and',
69
+ left: { type: 'term', value: 'auth' },
70
+ right: {
71
+ type: 'or',
72
+ left: { type: 'term', value: 'error' },
73
+ right: { type: 'term', value: 'bug' },
74
+ },
75
+ })
76
+ })
77
+
78
+ it('should handle case-insensitive operators', () => {
79
+ const result1 = parseQuery('auth and criticism')
80
+ const result2 = parseQuery('auth And criticism')
81
+ expect(result1!.ast).toEqual(result2!.ast)
82
+ })
83
+
84
+ it('should parse phrase combined with boolean', () => {
85
+ const result = parseQuery('"context resumption" AND drift')
86
+ expect(result).not.toBeNull()
87
+ expect(result!.ast).toEqual({
88
+ type: 'and',
89
+ left: { type: 'phrase', value: 'context resumption' },
90
+ right: { type: 'term', value: 'drift' },
91
+ })
92
+ })
93
+
94
+ it('should parse implicit AND between terms', () => {
95
+ const result = parseQuery('auth error')
96
+ expect(result).not.toBeNull()
97
+ expect(result!.ast).toEqual({
98
+ type: 'and',
99
+ left: { type: 'term', value: 'auth' },
100
+ right: { type: 'term', value: 'error' },
101
+ })
102
+ })
103
+
104
+ it('should respect operator precedence (NOT > AND > OR)', () => {
105
+ // "a OR b AND NOT c" should parse as "a OR (b AND (NOT c))"
106
+ const result = parseQuery('a OR b AND NOT c')
107
+ expect(result).not.toBeNull()
108
+ expect(result!.ast).toEqual({
109
+ type: 'or',
110
+ left: { type: 'term', value: 'a' },
111
+ right: {
112
+ type: 'and',
113
+ left: { type: 'term', value: 'b' },
114
+ right: { type: 'not', operand: { type: 'term', value: 'c' } },
115
+ },
116
+ })
117
+ })
118
+
119
+ it('should return null for empty query', () => {
120
+ expect(parseQuery('')).toBeNull()
121
+ expect(parseQuery(' ')).toBeNull()
122
+ })
123
+ })
124
+
125
+ describe('isAdvancedQuery', () => {
126
+ it('should return false for simple terms', () => {
127
+ expect(isAdvancedQuery('auth')).toBe(false)
128
+ expect(isAdvancedQuery('some term')).toBe(false)
129
+ })
130
+
131
+ it('should return true for boolean operators', () => {
132
+ expect(isAdvancedQuery('auth AND error')).toBe(true)
133
+ expect(isAdvancedQuery('auth OR error')).toBe(true)
134
+ expect(isAdvancedQuery('auth NOT error')).toBe(true)
135
+ })
136
+
137
+ it('should return true for phrases', () => {
138
+ expect(isAdvancedQuery('"exact phrase"')).toBe(true)
139
+ })
140
+
141
+ it('should return true for grouped expressions', () => {
142
+ expect(isAdvancedQuery('(a OR b)')).toBe(true)
143
+ })
144
+ })
145
+
146
+ describe('evaluateQuery', () => {
147
+ const text = 'This is about authentication errors and bug fixes'
148
+
149
+ it('should match single term', () => {
150
+ const ast: QueryNode = { type: 'term', value: 'authentication' }
151
+ expect(evaluateQuery(ast, text)).toBe(true)
152
+ })
153
+
154
+ it('should not match missing term', () => {
155
+ const ast: QueryNode = { type: 'term', value: 'security' }
156
+ expect(evaluateQuery(ast, text)).toBe(false)
157
+ })
158
+
159
+ it('should match phrase', () => {
160
+ const ast: QueryNode = { type: 'phrase', value: 'authentication errors' }
161
+ expect(evaluateQuery(ast, text)).toBe(true)
162
+ })
163
+
164
+ it('should not match partial phrase', () => {
165
+ const ast: QueryNode = { type: 'phrase', value: 'authentication bug' }
166
+ expect(evaluateQuery(ast, text)).toBe(false)
167
+ })
168
+
169
+ it('should evaluate AND correctly', () => {
170
+ const ast: QueryNode = {
171
+ type: 'and',
172
+ left: { type: 'term', value: 'authentication' },
173
+ right: { type: 'term', value: 'bug' },
174
+ }
175
+ expect(evaluateQuery(ast, text)).toBe(true)
176
+
177
+ const ast2: QueryNode = {
178
+ type: 'and',
179
+ left: { type: 'term', value: 'authentication' },
180
+ right: { type: 'term', value: 'security' },
181
+ }
182
+ expect(evaluateQuery(ast2, text)).toBe(false)
183
+ })
184
+
185
+ it('should evaluate OR correctly', () => {
186
+ const ast: QueryNode = {
187
+ type: 'or',
188
+ left: { type: 'term', value: 'authentication' },
189
+ right: { type: 'term', value: 'security' },
190
+ }
191
+ expect(evaluateQuery(ast, text)).toBe(true)
192
+
193
+ const ast2: QueryNode = {
194
+ type: 'or',
195
+ left: { type: 'term', value: 'crypto' },
196
+ right: { type: 'term', value: 'security' },
197
+ }
198
+ expect(evaluateQuery(ast2, text)).toBe(false)
199
+ })
200
+
201
+ it('should evaluate NOT correctly', () => {
202
+ const ast: QueryNode = {
203
+ type: 'not',
204
+ operand: { type: 'term', value: 'security' },
205
+ }
206
+ expect(evaluateQuery(ast, text)).toBe(true)
207
+
208
+ const ast2: QueryNode = {
209
+ type: 'not',
210
+ operand: { type: 'term', value: 'authentication' },
211
+ }
212
+ expect(evaluateQuery(ast2, text)).toBe(false)
213
+ })
214
+
215
+ it('should be case-insensitive', () => {
216
+ const ast: QueryNode = { type: 'term', value: 'AUTHENTICATION' }
217
+ expect(evaluateQuery(ast, text)).toBe(true)
218
+ })
219
+
220
+ it('should handle complex nested expressions', () => {
221
+ // (auth AND bug) OR (error AND NOT fixes)
222
+ const ast: QueryNode = {
223
+ type: 'or',
224
+ left: {
225
+ type: 'and',
226
+ left: { type: 'term', value: 'auth' },
227
+ right: { type: 'term', value: 'bug' },
228
+ },
229
+ right: {
230
+ type: 'and',
231
+ left: { type: 'term', value: 'error' },
232
+ right: { type: 'not', operand: { type: 'term', value: 'fixes' } },
233
+ },
234
+ }
235
+ expect(evaluateQuery(ast, text)).toBe(true)
236
+ })
237
+ })
238
+
239
+ describe('buildHighlightPattern', () => {
240
+ it('should create pattern from terms', () => {
241
+ const parsed = parseQuery('auth error')!
242
+ const pattern = buildHighlightPattern(parsed)
243
+ expect(pattern.test('authentication error')).toBe(true)
244
+ expect(pattern.test('no match here')).toBe(false)
245
+ })
246
+
247
+ it('should create pattern from phrases', () => {
248
+ const parsed = parseQuery('"exact phrase"')!
249
+ const pattern = buildHighlightPattern(parsed)
250
+ expect(pattern.test('this is the exact phrase here')).toBe(true)
251
+ })
252
+
253
+ it('should escape special regex characters', () => {
254
+ const parsed = parseQuery('"test.value"')!
255
+ const pattern = buildHighlightPattern(parsed)
256
+ expect(pattern.test('has test.value inside')).toBe(true)
257
+ expect(pattern.test('has testXvalue inside')).toBe(false) // . should not match any char
258
+ })
259
+ })
260
+ })