mdcontext 0.0.1 → 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/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/BACKLOG.md +338 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +434 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- 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 +88 -0
- 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 +803 -0
- 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 +1629 -0
- 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.d.ts +1 -0
- package/dist/cli/main.js +5458 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +79 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +472 -0
- 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 +625 -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/CONFIG.md +1123 -0
- package/docs/DESIGN.md +439 -0
- package/docs/ERRORS.md +383 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/summarization.md +320 -0
- package/docs/test-links.md +9 -0
- package/justfile +40 -0
- package/package.json +74 -9
- package/pnpm-workspace.yaml +5 -0
- 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-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/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/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/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-error-analysis.md +521 -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/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/research-quality-review.md +834 -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/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/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/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 +58 -0
- 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 +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +627 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +285 -0
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +480 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +1281 -0
- package/src/cli/commands/stats.ts +149 -0
- package/src/cli/commands/tree.ts +128 -0
- 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 +341 -0
- package/src/cli/help.ts +588 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +435 -0
- package/src/cli/options.ts +41 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +259 -0
- 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/index.ts +1 -0
- package/src/core/types.ts +113 -0
- 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 +10 -0
- package/src/embeddings/openai-provider.ts +414 -0
- 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 +1270 -0
- package/src/embeddings/types.ts +359 -0
- package/src/embeddings/vector-store.ts +708 -0
- 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/index.ts +4 -0
- package/src/index/indexer.ts +684 -0
- package/src/index/storage.ts +260 -0
- package/src/index/types.ts +147 -0
- package/src/index/watcher.ts +189 -0
- package/src/index.ts +30 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +612 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +394 -0
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +392 -0
- 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/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +280 -0
- package/src/search/searcher.ts +724 -0
- 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/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 +597 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/types/huggingface-transformers.d.ts +66 -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/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -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 +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -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/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/tsconfig.json +26 -0
- package/vitest.config.ts +16 -0
- package/vitest.setup.ts +12 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for path-matcher utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { matchPath } from './path-matcher.js'
|
|
7
|
+
|
|
8
|
+
describe('path-matcher', () => {
|
|
9
|
+
describe('matchPath', () => {
|
|
10
|
+
describe('basic patterns', () => {
|
|
11
|
+
it('matches exact paths', () => {
|
|
12
|
+
expect(matchPath('docs/readme.md', 'docs/readme.md')).toBe(true)
|
|
13
|
+
expect(matchPath('src/index.ts', 'src/index.ts')).toBe(true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('does not match different paths', () => {
|
|
17
|
+
expect(matchPath('docs/readme.md', 'src/readme.md')).toBe(false)
|
|
18
|
+
expect(matchPath('docs/readme.md', 'docs/other.md')).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('is case-insensitive', () => {
|
|
22
|
+
expect(matchPath('docs/README.md', 'docs/readme.md')).toBe(true)
|
|
23
|
+
expect(matchPath('DOCS/readme.md', 'docs/readme.md')).toBe(true)
|
|
24
|
+
expect(matchPath('docs/readme.MD', 'docs/readme.md')).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('asterisk wildcard (*)', () => {
|
|
29
|
+
it('matches any characters within filename', () => {
|
|
30
|
+
expect(matchPath('docs/readme.md', 'docs/*.md')).toBe(true)
|
|
31
|
+
expect(matchPath('docs/guide.md', 'docs/*.md')).toBe(true)
|
|
32
|
+
expect(matchPath('docs/api-reference.md', 'docs/*.md')).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('matches empty string with asterisk', () => {
|
|
36
|
+
expect(matchPath('docs/.md', 'docs/*.md')).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('matches patterns at start of path', () => {
|
|
40
|
+
expect(matchPath('src/index.ts', '*/index.ts')).toBe(true)
|
|
41
|
+
expect(matchPath('lib/index.ts', '*/index.ts')).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('matches patterns in middle of path', () => {
|
|
45
|
+
expect(matchPath('src/utils/index.ts', 'src/*/index.ts')).toBe(true)
|
|
46
|
+
expect(matchPath('src/helpers/index.ts', 'src/*/index.ts')).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('matches multiple wildcards', () => {
|
|
50
|
+
expect(matchPath('src/utils/test.ts', '*/*/*.ts')).toBe(true)
|
|
51
|
+
expect(matchPath('a/b/c.ts', '*/*/*.ts')).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('single asterisk does NOT match directory separators', () => {
|
|
55
|
+
// Standard glob semantics: * matches within a segment only
|
|
56
|
+
expect(matchPath('file.md', '*.md')).toBe(true)
|
|
57
|
+
expect(matchPath('dir/file.md', '*.md')).toBe(false) // * doesn't match /
|
|
58
|
+
expect(matchPath('deeply/nested/path/file.md', '*')).toBe(false)
|
|
59
|
+
expect(matchPath('a/b/c.ts', '*.ts')).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does not match nested paths with single asterisk', () => {
|
|
63
|
+
expect(matchPath('docs/nested/api.md', 'docs/*.md')).toBe(false)
|
|
64
|
+
expect(matchPath('src/sub/file.ts', 'src/*.ts')).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('double asterisk wildcard (**)', () => {
|
|
69
|
+
it('matches across directory separators', () => {
|
|
70
|
+
expect(matchPath('deeply/nested/path/file.md', '**')).toBe(true)
|
|
71
|
+
expect(matchPath('a/b/c.ts', '**.ts')).toBe(true)
|
|
72
|
+
expect(matchPath('a/b/c.ts', '**/*.ts')).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('matches nested paths recursively', () => {
|
|
76
|
+
expect(matchPath('docs/nested/api.md', 'docs/**/*.md')).toBe(true)
|
|
77
|
+
expect(matchPath('docs/deeply/nested/file.md', 'docs/**/*.md')).toBe(
|
|
78
|
+
true,
|
|
79
|
+
)
|
|
80
|
+
expect(matchPath('src/a/b/c/file.ts', 'src/**/*.ts')).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('matches at beginning of pattern', () => {
|
|
84
|
+
expect(matchPath('any/path/to/file.md', '**/*.md')).toBe(true)
|
|
85
|
+
// Note: **/*.md requires at least one /; for root files use *.md or **.md
|
|
86
|
+
expect(matchPath('file.md', '**.md')).toBe(true)
|
|
87
|
+
expect(matchPath('file.md', '*.md')).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('question mark wildcard (?)', () => {
|
|
92
|
+
it('matches exactly one character', () => {
|
|
93
|
+
expect(matchPath('file1.md', 'file?.md')).toBe(true)
|
|
94
|
+
expect(matchPath('fileA.md', 'file?.md')).toBe(true)
|
|
95
|
+
expect(matchPath('file-.md', 'file?.md')).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('does not match zero characters', () => {
|
|
99
|
+
expect(matchPath('file.md', 'file?.md')).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('does not match multiple characters', () => {
|
|
103
|
+
expect(matchPath('file12.md', 'file?.md')).toBe(false)
|
|
104
|
+
expect(matchPath('fileABC.md', 'file?.md')).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('matches multiple question marks', () => {
|
|
108
|
+
expect(matchPath('file12.md', 'file??.md')).toBe(true)
|
|
109
|
+
expect(matchPath('fileAB.md', 'file??.md')).toBe(true)
|
|
110
|
+
expect(matchPath('file1.md', 'file??.md')).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('can be combined with asterisk', () => {
|
|
114
|
+
expect(matchPath('v1/readme.md', 'v?/*.md')).toBe(true)
|
|
115
|
+
expect(matchPath('v2/guide.md', 'v?/*.md')).toBe(true)
|
|
116
|
+
expect(matchPath('v10/readme.md', 'v?/*.md')).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('does not match directory separators', () => {
|
|
120
|
+
expect(matchPath('a/b', 'a?b')).toBe(false) // ? should not match /
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('dot handling', () => {
|
|
125
|
+
it('treats dot as literal character', () => {
|
|
126
|
+
expect(matchPath('file.md', 'file.md')).toBe(true)
|
|
127
|
+
expect(matchPath('fileXmd', 'file.md')).toBe(false)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('escapes dots in patterns correctly', () => {
|
|
131
|
+
expect(matchPath('src.utils.index.ts', 'src.utils.index.ts')).toBe(true)
|
|
132
|
+
expect(matchPath('srcXutilsXindexXts', 'src.utils.index.ts')).toBe(
|
|
133
|
+
false,
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('matches file extensions correctly', () => {
|
|
138
|
+
expect(matchPath('readme.md', '*.md')).toBe(true)
|
|
139
|
+
expect(matchPath('readme.markdown', '*.md')).toBe(false)
|
|
140
|
+
expect(matchPath('readmeXmd', '*.md')).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('special regex characters', () => {
|
|
145
|
+
it('handles paths with special characters', () => {
|
|
146
|
+
// The path-matcher now escapes all regex special chars
|
|
147
|
+
expect(matchPath('file.test.md', 'file.test.md')).toBe(true)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('handles patterns with multiple dots', () => {
|
|
151
|
+
expect(matchPath('package.config.json', '*.config.json')).toBe(true)
|
|
152
|
+
expect(matchPath('app.module.ts', '*.module.ts')).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('treats parentheses as literal characters', () => {
|
|
156
|
+
expect(matchPath('file(1).md', 'file(1).md')).toBe(true)
|
|
157
|
+
expect(matchPath('file1.md', 'file(1).md')).toBe(false)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('treats square brackets as literal characters', () => {
|
|
161
|
+
expect(matchPath('[ab].md', '[ab].md')).toBe(true)
|
|
162
|
+
expect(matchPath('a.md', '[ab].md')).toBe(false)
|
|
163
|
+
expect(matchPath('b.md', '[ab].md')).toBe(false)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('treats plus as literal character', () => {
|
|
167
|
+
expect(matchPath('C++.md', 'C++.md')).toBe(true)
|
|
168
|
+
expect(matchPath('C.md', 'C++.md')).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('treats caret as literal character', () => {
|
|
172
|
+
expect(matchPath('test^2.md', 'test^2.md')).toBe(true)
|
|
173
|
+
expect(matchPath('test2.md', 'test^2.md')).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('treats dollar sign as literal character', () => {
|
|
177
|
+
expect(matchPath('price$100.md', 'price$100.md')).toBe(true)
|
|
178
|
+
expect(matchPath('price100.md', 'price$100.md')).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('treats curly braces as literal characters', () => {
|
|
182
|
+
expect(matchPath('obj{}.md', 'obj{}.md')).toBe(true)
|
|
183
|
+
expect(matchPath('obj.md', 'obj{}.md')).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('treats pipe as literal character', () => {
|
|
187
|
+
expect(matchPath('a|b.md', 'a|b.md')).toBe(true)
|
|
188
|
+
expect(matchPath('a.md', 'a|b.md')).toBe(false)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('treats backslash as literal character', () => {
|
|
192
|
+
expect(matchPath('path\\file.md', 'path\\file.md')).toBe(true)
|
|
193
|
+
expect(matchPath('pathfile.md', 'path\\file.md')).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('edge cases', () => {
|
|
198
|
+
it('matches empty path with empty pattern', () => {
|
|
199
|
+
expect(matchPath('', '')).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('does not match non-empty path with empty pattern', () => {
|
|
203
|
+
expect(matchPath('file.md', '')).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('does not match empty path with non-empty pattern', () => {
|
|
207
|
+
expect(matchPath('', 'file.md')).toBe(false)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('matches only asterisk pattern', () => {
|
|
211
|
+
expect(matchPath('anything', '*')).toBe(true)
|
|
212
|
+
expect(matchPath('', '*')).toBe(true)
|
|
213
|
+
expect(matchPath('a/b/c', '*')).toBe(false) // * doesn't match /
|
|
214
|
+
expect(matchPath('a/b/c', '**')).toBe(true) // ** matches everything
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('matches only question mark pattern', () => {
|
|
218
|
+
expect(matchPath('a', '?')).toBe(true)
|
|
219
|
+
expect(matchPath('ab', '?')).toBe(false)
|
|
220
|
+
expect(matchPath('', '?')).toBe(false)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('handles very long paths', () => {
|
|
224
|
+
const longPath = `${'a/'.repeat(50)}file.md`
|
|
225
|
+
const longPattern = `${'a/'.repeat(50)}*.md`
|
|
226
|
+
expect(matchPath(longPath, longPattern)).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('handles paths with spaces', () => {
|
|
230
|
+
expect(matchPath('my docs/readme.md', 'my docs/*.md')).toBe(true)
|
|
231
|
+
expect(matchPath('path with spaces/file.md', '*/file.md')).toBe(true)
|
|
232
|
+
// Nested requires **
|
|
233
|
+
expect(matchPath('a/path with spaces/file.md', '*/file.md')).toBe(false)
|
|
234
|
+
expect(matchPath('a/path with spaces/file.md', '**/file.md')).toBe(true)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('handles unicode characters', () => {
|
|
238
|
+
expect(matchPath('docs/日本語.md', 'docs/*.md')).toBe(true)
|
|
239
|
+
expect(matchPath('文档/readme.md', '*/readme.md')).toBe(true)
|
|
240
|
+
expect(matchPath('a/文档/readme.md', '**/readme.md')).toBe(true)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('real-world patterns', () => {
|
|
245
|
+
it('matches markdown files in docs folder', () => {
|
|
246
|
+
expect(matchPath('docs/readme.md', 'docs/*.md')).toBe(true)
|
|
247
|
+
expect(matchPath('docs/api.md', 'docs/*.md')).toBe(true)
|
|
248
|
+
// * doesn't match /, use ** for nested paths
|
|
249
|
+
expect(matchPath('docs/nested/api.md', 'docs/*.md')).toBe(false)
|
|
250
|
+
expect(matchPath('docs/nested/api.md', 'docs/**/*.md')).toBe(true)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('matches typescript files in src', () => {
|
|
254
|
+
expect(matchPath('src/index.ts', 'src/*.ts')).toBe(true)
|
|
255
|
+
expect(matchPath('src/utils.ts', 'src/*.ts')).toBe(true)
|
|
256
|
+
// Nested requires **
|
|
257
|
+
expect(matchPath('src/nested/index.ts', 'src/*.ts')).toBe(false)
|
|
258
|
+
expect(matchPath('src/nested/index.ts', 'src/**/*.ts')).toBe(true)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('matches test files', () => {
|
|
262
|
+
expect(matchPath('test.spec.ts', '*.spec.ts')).toBe(true)
|
|
263
|
+
expect(matchPath('utils.test.ts', '*.test.ts')).toBe(true)
|
|
264
|
+
// Nested requires **
|
|
265
|
+
expect(matchPath('src/utils.test.ts', '*.test.ts')).toBe(false)
|
|
266
|
+
expect(matchPath('src/utils.test.ts', '**/*.test.ts')).toBe(true)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('matches config files', () => {
|
|
270
|
+
expect(matchPath('tsconfig.json', '*.json')).toBe(true)
|
|
271
|
+
expect(matchPath('package.json', 'package.json')).toBe(true)
|
|
272
|
+
expect(matchPath('.eslintrc.json', '*.json')).toBe(true)
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path matching utilities for search filtering.
|
|
3
|
+
*
|
|
4
|
+
* Simple glob-like pattern matching for document paths.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Match a file path against a glob-like pattern.
|
|
9
|
+
*
|
|
10
|
+
* Supports:
|
|
11
|
+
* - `**` matches any characters including directory separators (recursive)
|
|
12
|
+
* - `*` matches any characters except directory separators (single segment)
|
|
13
|
+
* - `?` matches exactly one character (not directory separator)
|
|
14
|
+
* - `.` is treated literally
|
|
15
|
+
*
|
|
16
|
+
* @param filePath - The file path to test
|
|
17
|
+
* @param pattern - The glob pattern (e.g., "docs/*", "src/api/*.md", "src/** /*.ts")
|
|
18
|
+
* @returns True if the path matches the pattern
|
|
19
|
+
*/
|
|
20
|
+
export const matchPath = (filePath: string, pattern: string): boolean => {
|
|
21
|
+
// Use a placeholder for ** to avoid it being processed by single * replacement
|
|
22
|
+
const DOUBLE_STAR_PLACEHOLDER = '__DOUBLE_STAR_MARKER__'
|
|
23
|
+
|
|
24
|
+
const regexPattern = pattern
|
|
25
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape all regex special chars first
|
|
26
|
+
.replace(/\*\*/g, DOUBLE_STAR_PLACEHOLDER) // Preserve ** before processing *
|
|
27
|
+
.replace(/\*/g, '[^/]*') // Single * doesn't match slashes
|
|
28
|
+
.replace(/\?/g, '[^/]') // ? matches any single non-slash char
|
|
29
|
+
.replace(new RegExp(DOUBLE_STAR_PLACEHOLDER, 'g'), '.*') // ** matches anything
|
|
30
|
+
|
|
31
|
+
const regex = new RegExp(`^${regexPattern}$`, 'i')
|
|
32
|
+
return regex.test(filePath)
|
|
33
|
+
}
|
|
@@ -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
|
+
})
|