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.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/AGENTS.md +46 -0
- package/BACKLOG.md +338 -0
- package/README.md +231 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-KRYIFLQR.js +92 -0
- package/dist/chunk-S7E6TFX6.js +742 -0
- package/dist/chunk-VVTGZNBT.js +1519 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +2015 -0
- package/dist/index.d.ts +266 -0
- package/dist/index.js +86 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +376 -0
- package/docs/019-USAGE.md +586 -0
- package/docs/020-current-implementation.md +364 -0
- package/docs/021-DOGFOODING-FINDINGS.md +175 -0
- package/docs/BACKLOG.md +80 -0
- package/docs/DESIGN.md +439 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/test-links.md +9 -0
- package/package.json +69 -10
- package/pnpm-workspace.yaml +5 -0
- package/research/config-analysis/01-current-implementation.md +470 -0
- package/research/config-analysis/02-strategy-recommendation.md +428 -0
- package/research/config-analysis/03-task-candidates.md +715 -0
- package/research/config-analysis/033-research-configuration-management.md +828 -0
- package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
- package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
- package/research/dogfood/consolidated-tool-evaluation.md +373 -0
- package/research/dogfood/strategy-a/a-synthesis.md +184 -0
- package/research/dogfood/strategy-a/a1-docs.md +226 -0
- package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
- package/research/dogfood/strategy-a/a3-llm.md +164 -0
- package/research/dogfood/strategy-b/b-synthesis.md +228 -0
- package/research/dogfood/strategy-b/b1-architecture.md +207 -0
- package/research/dogfood/strategy-b/b2-gaps.md +258 -0
- package/research/dogfood/strategy-b/b3-workflows.md +250 -0
- package/research/dogfood/strategy-c/c-synthesis.md +451 -0
- package/research/dogfood/strategy-c/c1-explorer.md +192 -0
- package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
- package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
- package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
- package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
- package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
- package/research/effect-cli-error-handling.md +845 -0
- package/research/effect-errors-as-values.md +943 -0
- package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
- package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
- package/research/errors-task-analysis/embeddings-analysis.md +709 -0
- package/research/errors-task-analysis/index-search-analysis.md +812 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
- package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
- package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
- package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
- package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
- package/research/semantic-search/002-research-embedding-models.md +490 -0
- package/research/semantic-search/003-research-rag-alternatives.md +523 -0
- package/research/semantic-search/004-research-vector-search.md +841 -0
- package/research/semantic-search/032-research-semantic-search.md +427 -0
- package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
- package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
- package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
- package/research/task-management-2026/03-lightweight-file-based.md +567 -0
- package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
- package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
- package/research/task-management-2026/linear/02-api-integrations.md +930 -0
- package/research/task-management-2026/linear/03-ai-features.md +368 -0
- package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
- package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
- package/scripts/rebuild-hnswlib.js +63 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +430 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/context.ts +197 -0
- package/src/cli/commands/index-cmd.ts +300 -0
- package/src/cli/commands/index.ts +13 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +451 -0
- package/src/cli/commands/stats.ts +146 -0
- package/src/cli/commands/tree.ts +107 -0
- package/src/cli/flag-schemas.ts +275 -0
- package/src/cli/help.ts +386 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +145 -0
- package/src/cli/options.ts +31 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +126 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +140 -0
- package/src/embeddings/index.ts +8 -0
- package/src/embeddings/openai-provider.ts +165 -0
- package/src/embeddings/semantic-search.ts +583 -0
- package/src/embeddings/types.ts +82 -0
- package/src/embeddings/vector-store.ts +299 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +446 -0
- package/src/index/storage.ts +196 -0
- package/src/index/types.ts +109 -0
- package/src/index/watcher.ts +131 -0
- package/src/index.ts +8 -0
- package/src/mcp/server.ts +483 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +395 -0
- package/src/parser/section-filter.ts +270 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +182 -0
- package/src/search/searcher.ts +602 -0
- package/src/summarize/budget-bugs.test.ts +620 -0
- package/src/summarize/formatters.ts +419 -0
- package/src/summarize/index.ts +20 -0
- package/src/summarize/summarizer.test.ts +275 -0
- package/src/summarize/summarizer.ts +528 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tokens.test.ts +142 -0
- package/src/utils/tokens.ts +186 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +233 -0
- package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/vectors.meta.json +1264 -0
- package/tests/fixtures/cli/README.md +9 -0
- package/tests/fixtures/cli/api-reference.md +11 -0
- package/tests/fixtures/cli/getting-started.md +11 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +21 -0
- 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
|
+
})
|