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,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyword search for mdcontext
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs/promises'
|
|
6
|
+
import * as path from 'node:path'
|
|
7
|
+
import { Effect } from 'effect'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DocumentNotFoundError,
|
|
11
|
+
type FileReadError,
|
|
12
|
+
type IndexCorruptedError,
|
|
13
|
+
IndexNotFoundError,
|
|
14
|
+
} from '../errors/index.js'
|
|
15
|
+
import {
|
|
16
|
+
createStorage,
|
|
17
|
+
loadDocumentIndex,
|
|
18
|
+
loadSectionIndex,
|
|
19
|
+
} from '../index/storage.js'
|
|
20
|
+
import type { DocumentEntry, SectionEntry } from '../index/types.js'
|
|
21
|
+
import {
|
|
22
|
+
buildFuzzyHighlightPattern,
|
|
23
|
+
findMatchesInLine,
|
|
24
|
+
type MatchOptions,
|
|
25
|
+
matchesWithOptions,
|
|
26
|
+
} from './fuzzy-search.js'
|
|
27
|
+
import { matchPath } from './path-matcher.js'
|
|
28
|
+
import {
|
|
29
|
+
buildHighlightPattern,
|
|
30
|
+
evaluateQuery,
|
|
31
|
+
isAdvancedQuery,
|
|
32
|
+
type ParsedQuery,
|
|
33
|
+
parseQuery,
|
|
34
|
+
} from './query-parser.js'
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Search Options
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
export interface SearchOptions {
|
|
41
|
+
/** Filter by heading pattern (regex) */
|
|
42
|
+
readonly heading?: string | undefined
|
|
43
|
+
/** Search within section content (regex) */
|
|
44
|
+
readonly content?: string | undefined
|
|
45
|
+
/** Filter by file path pattern (glob-like) */
|
|
46
|
+
readonly pathPattern?: string | undefined
|
|
47
|
+
/** Only sections with code blocks */
|
|
48
|
+
readonly hasCode?: boolean | undefined
|
|
49
|
+
/** Only sections with lists */
|
|
50
|
+
readonly hasList?: boolean | undefined
|
|
51
|
+
/** Only sections with tables */
|
|
52
|
+
readonly hasTable?: boolean | undefined
|
|
53
|
+
/** Minimum heading level */
|
|
54
|
+
readonly minLevel?: number | undefined
|
|
55
|
+
/** Maximum heading level */
|
|
56
|
+
readonly maxLevel?: number | undefined
|
|
57
|
+
/** Maximum results */
|
|
58
|
+
readonly limit?: number | undefined
|
|
59
|
+
/** Lines of context before matches */
|
|
60
|
+
readonly contextBefore?: number | undefined
|
|
61
|
+
/** Lines of context after matches */
|
|
62
|
+
readonly contextAfter?: number | undefined
|
|
63
|
+
/** Enable fuzzy matching with typo tolerance */
|
|
64
|
+
readonly fuzzy?: boolean | undefined
|
|
65
|
+
/** Max edit distance for fuzzy matching (default: 2) */
|
|
66
|
+
readonly fuzzyDistance?: number | undefined
|
|
67
|
+
/** Enable word stemming (fail matches failure, failed, etc.) */
|
|
68
|
+
readonly stem?: boolean | undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ContentMatch {
|
|
72
|
+
/** The line number where match was found (1-based) */
|
|
73
|
+
readonly lineNumber: number
|
|
74
|
+
/** The matching line text */
|
|
75
|
+
readonly line: string
|
|
76
|
+
/** Snippet showing match context (lines before and after) */
|
|
77
|
+
readonly snippet: string
|
|
78
|
+
/** Context lines with their line numbers (for JSON output) */
|
|
79
|
+
readonly contextLines?: readonly ContextLine[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ContextLine {
|
|
83
|
+
/** The line number (1-based) */
|
|
84
|
+
readonly lineNumber: number
|
|
85
|
+
/** The line text */
|
|
86
|
+
readonly line: string
|
|
87
|
+
/** Whether this is the matching line */
|
|
88
|
+
readonly isMatch: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SearchResult {
|
|
92
|
+
readonly section: SectionEntry
|
|
93
|
+
readonly document: DocumentEntry
|
|
94
|
+
readonly sectionContent?: string
|
|
95
|
+
/** Matches found within the content (when content search is used) */
|
|
96
|
+
readonly matches?: readonly ContentMatch[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Search Implementation
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Search for sections by metadata (heading, path, content flags).
|
|
105
|
+
*
|
|
106
|
+
* @param rootPath - Root directory containing indexed markdown files
|
|
107
|
+
* @param options - Search filters (heading, path pattern, code/list/table flags)
|
|
108
|
+
* @returns Matching sections
|
|
109
|
+
*
|
|
110
|
+
* @throws FileReadError - Cannot read index files
|
|
111
|
+
* @throws IndexCorruptedError - Index files are corrupted
|
|
112
|
+
*/
|
|
113
|
+
export const search = (
|
|
114
|
+
rootPath: string,
|
|
115
|
+
options: SearchOptions = {},
|
|
116
|
+
): Effect.Effect<
|
|
117
|
+
readonly SearchResult[],
|
|
118
|
+
FileReadError | IndexCorruptedError
|
|
119
|
+
> =>
|
|
120
|
+
Effect.gen(function* () {
|
|
121
|
+
const storage = createStorage(rootPath)
|
|
122
|
+
|
|
123
|
+
const docIndex = yield* loadDocumentIndex(storage)
|
|
124
|
+
const sectionIndex = yield* loadSectionIndex(storage)
|
|
125
|
+
|
|
126
|
+
if (!docIndex || !sectionIndex) {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const results: SearchResult[] = []
|
|
131
|
+
const headingRegex = options.heading
|
|
132
|
+
? new RegExp(options.heading, 'i')
|
|
133
|
+
: null
|
|
134
|
+
|
|
135
|
+
for (const section of Object.values(sectionIndex.sections)) {
|
|
136
|
+
// Filter by heading pattern
|
|
137
|
+
if (headingRegex && !headingRegex.test(section.heading)) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Filter by path pattern
|
|
142
|
+
if (
|
|
143
|
+
options.pathPattern &&
|
|
144
|
+
!matchPath(section.documentPath, options.pathPattern)
|
|
145
|
+
) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Filter by code blocks
|
|
150
|
+
if (
|
|
151
|
+
options.hasCode !== undefined &&
|
|
152
|
+
section.hasCode !== options.hasCode
|
|
153
|
+
) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Filter by lists
|
|
158
|
+
if (
|
|
159
|
+
options.hasList !== undefined &&
|
|
160
|
+
section.hasList !== options.hasList
|
|
161
|
+
) {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Filter by tables
|
|
166
|
+
if (
|
|
167
|
+
options.hasTable !== undefined &&
|
|
168
|
+
section.hasTable !== options.hasTable
|
|
169
|
+
) {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Filter by level range
|
|
174
|
+
if (options.minLevel !== undefined && section.level < options.minLevel) {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.maxLevel !== undefined && section.level > options.maxLevel) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const document = docIndex.documents[section.documentPath]
|
|
183
|
+
if (document) {
|
|
184
|
+
results.push({ section, document })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check limit
|
|
188
|
+
if (options.limit !== undefined && results.length >= options.limit) {
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return results
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Content Search Implementation
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Search within section content.
|
|
202
|
+
* Supports boolean operators (AND, OR, NOT) and quoted phrases.
|
|
203
|
+
* Falls back to regex for simple patterns.
|
|
204
|
+
*
|
|
205
|
+
* @param rootPath - Root directory containing indexed markdown files
|
|
206
|
+
* @param options - Search options including content pattern
|
|
207
|
+
* @returns Matching sections with match highlights
|
|
208
|
+
*
|
|
209
|
+
* @throws FileReadError - Cannot read index or source files
|
|
210
|
+
* @throws IndexCorruptedError - Index files are corrupted
|
|
211
|
+
*/
|
|
212
|
+
export const searchContent = (
|
|
213
|
+
rootPath: string,
|
|
214
|
+
options: SearchOptions = {},
|
|
215
|
+
): Effect.Effect<
|
|
216
|
+
readonly SearchResult[],
|
|
217
|
+
FileReadError | IndexCorruptedError
|
|
218
|
+
> =>
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const storage = createStorage(rootPath)
|
|
221
|
+
|
|
222
|
+
const docIndex = yield* loadDocumentIndex(storage)
|
|
223
|
+
const sectionIndex = yield* loadSectionIndex(storage)
|
|
224
|
+
|
|
225
|
+
if (!docIndex || !sectionIndex) {
|
|
226
|
+
return []
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Parse content query - use boolean parser if advanced, else regex
|
|
230
|
+
let parsedQuery: ParsedQuery | null = null
|
|
231
|
+
let contentRegex: RegExp | null = null
|
|
232
|
+
let highlightRegex: RegExp | null = null
|
|
233
|
+
|
|
234
|
+
// Configure fuzzy/stem matching options
|
|
235
|
+
const matchOptions: MatchOptions = {
|
|
236
|
+
stem: options.stem,
|
|
237
|
+
fuzzyDistance: options.fuzzy ? (options.fuzzyDistance ?? 2) : undefined,
|
|
238
|
+
}
|
|
239
|
+
const useFuzzyOrStem = options.fuzzy || options.stem
|
|
240
|
+
|
|
241
|
+
if (options.content) {
|
|
242
|
+
if (isAdvancedQuery(options.content)) {
|
|
243
|
+
parsedQuery = parseQuery(options.content)
|
|
244
|
+
if (parsedQuery) {
|
|
245
|
+
if (useFuzzyOrStem) {
|
|
246
|
+
highlightRegex = buildFuzzyHighlightPattern(
|
|
247
|
+
options.content,
|
|
248
|
+
matchOptions,
|
|
249
|
+
)
|
|
250
|
+
} else {
|
|
251
|
+
highlightRegex = buildHighlightPattern(parsedQuery)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// Simple search - use regex for exact match, or fuzzy/stem matching
|
|
256
|
+
if (!useFuzzyOrStem) {
|
|
257
|
+
contentRegex = new RegExp(options.content, 'gi')
|
|
258
|
+
highlightRegex = contentRegex
|
|
259
|
+
} else {
|
|
260
|
+
// For fuzzy/stem mode, build a highlight pattern
|
|
261
|
+
highlightRegex = buildFuzzyHighlightPattern(
|
|
262
|
+
options.content,
|
|
263
|
+
matchOptions,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const headingRegex = options.heading
|
|
270
|
+
? new RegExp(options.heading, 'i')
|
|
271
|
+
: null
|
|
272
|
+
|
|
273
|
+
const results: SearchResult[] = []
|
|
274
|
+
|
|
275
|
+
// Group sections by document for efficient file reading
|
|
276
|
+
const sectionsByDoc: Record<string, SectionEntry[]> = {}
|
|
277
|
+
for (const section of Object.values(sectionIndex.sections)) {
|
|
278
|
+
const docSections = sectionsByDoc[section.documentPath]
|
|
279
|
+
if (docSections) {
|
|
280
|
+
docSections.push(section)
|
|
281
|
+
} else {
|
|
282
|
+
sectionsByDoc[section.documentPath] = [section]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Process each document
|
|
287
|
+
for (const [docPath, sections] of Object.entries(sectionsByDoc)) {
|
|
288
|
+
// Apply path filter early
|
|
289
|
+
if (options.pathPattern && !matchPath(docPath, options.pathPattern)) {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const document = docIndex.documents[docPath]
|
|
294
|
+
if (!document) continue
|
|
295
|
+
|
|
296
|
+
// Load file content for content search
|
|
297
|
+
let fileContent: string | null = null
|
|
298
|
+
let fileLines: string[] = []
|
|
299
|
+
|
|
300
|
+
// Need to load file if we have any content matching to do:
|
|
301
|
+
// - parsedQuery: boolean query evaluation
|
|
302
|
+
// - contentRegex: regex matching
|
|
303
|
+
// - useFuzzyOrStem: fuzzy/stem matching
|
|
304
|
+
if (parsedQuery || contentRegex || (useFuzzyOrStem && options.content)) {
|
|
305
|
+
const filePath = path.join(storage.rootPath, docPath)
|
|
306
|
+
try {
|
|
307
|
+
fileContent = yield* Effect.promise(() =>
|
|
308
|
+
fs.readFile(filePath, 'utf-8'),
|
|
309
|
+
)
|
|
310
|
+
fileLines = fileContent.split('\n')
|
|
311
|
+
} catch {
|
|
312
|
+
continue // Skip files that can't be read
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const section of sections) {
|
|
317
|
+
// Apply heading filter
|
|
318
|
+
if (headingRegex && !headingRegex.test(section.heading)) {
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Apply other filters
|
|
323
|
+
if (
|
|
324
|
+
options.hasCode !== undefined &&
|
|
325
|
+
section.hasCode !== options.hasCode
|
|
326
|
+
) {
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
options.hasList !== undefined &&
|
|
331
|
+
section.hasList !== options.hasList
|
|
332
|
+
) {
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
options.hasTable !== undefined &&
|
|
337
|
+
section.hasTable !== options.hasTable
|
|
338
|
+
) {
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
if (
|
|
342
|
+
options.minLevel !== undefined &&
|
|
343
|
+
section.level < options.minLevel
|
|
344
|
+
) {
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
if (
|
|
348
|
+
options.maxLevel !== undefined &&
|
|
349
|
+
section.level > options.maxLevel
|
|
350
|
+
) {
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Content search
|
|
355
|
+
if ((parsedQuery || contentRegex || useFuzzyOrStem) && fileContent) {
|
|
356
|
+
const sectionLines = fileLines.slice(
|
|
357
|
+
section.startLine - 1,
|
|
358
|
+
section.endLine,
|
|
359
|
+
)
|
|
360
|
+
const sectionContent = sectionLines.join('\n')
|
|
361
|
+
|
|
362
|
+
// For boolean queries, evaluate against entire section content
|
|
363
|
+
if (parsedQuery) {
|
|
364
|
+
if (!evaluateQuery(parsedQuery.ast, sectionContent)) {
|
|
365
|
+
continue // Section doesn't match query
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// For fuzzy/stem mode without boolean query, check section content
|
|
370
|
+
if (useFuzzyOrStem && !parsedQuery && options.content) {
|
|
371
|
+
if (
|
|
372
|
+
!matchesWithOptions(options.content, sectionContent, matchOptions)
|
|
373
|
+
) {
|
|
374
|
+
continue // Section doesn't match with fuzzy/stem
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Find individual line matches for highlighting
|
|
379
|
+
const matches: ContentMatch[] = []
|
|
380
|
+
const searchRegex = contentRegex || highlightRegex
|
|
381
|
+
|
|
382
|
+
// Use configurable context lines (default to 1 if not specified)
|
|
383
|
+
const contextBefore = options.contextBefore ?? 1
|
|
384
|
+
const contextAfter = options.contextAfter ?? 1
|
|
385
|
+
|
|
386
|
+
// Get query words for fuzzy/stem matching
|
|
387
|
+
const queryWords = options.content
|
|
388
|
+
? options.content
|
|
389
|
+
.toLowerCase()
|
|
390
|
+
.split(/\W+/)
|
|
391
|
+
.filter((w) => w.length > 0)
|
|
392
|
+
: []
|
|
393
|
+
|
|
394
|
+
for (let i = 0; i < sectionLines.length; i++) {
|
|
395
|
+
const line = sectionLines[i]
|
|
396
|
+
if (!line) continue
|
|
397
|
+
|
|
398
|
+
let isMatch = false
|
|
399
|
+
|
|
400
|
+
// Check with regex for exact match mode
|
|
401
|
+
if (searchRegex) {
|
|
402
|
+
if (searchRegex.test(line)) {
|
|
403
|
+
isMatch = true
|
|
404
|
+
}
|
|
405
|
+
// Reset regex lastIndex for next test
|
|
406
|
+
searchRegex.lastIndex = 0
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check with fuzzy/stem matching
|
|
410
|
+
if (!isMatch && useFuzzyOrStem && queryWords.length > 0) {
|
|
411
|
+
const lineMatches = findMatchesInLine(
|
|
412
|
+
queryWords,
|
|
413
|
+
line,
|
|
414
|
+
matchOptions,
|
|
415
|
+
)
|
|
416
|
+
if (lineMatches.length > 0) {
|
|
417
|
+
isMatch = true
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (isMatch) {
|
|
422
|
+
const absoluteLineNum = section.startLine + i
|
|
423
|
+
|
|
424
|
+
// Create snippet with configurable context
|
|
425
|
+
const snippetStart = Math.max(0, i - contextBefore)
|
|
426
|
+
const snippetEnd = Math.min(
|
|
427
|
+
sectionLines.length,
|
|
428
|
+
i + contextAfter + 1,
|
|
429
|
+
)
|
|
430
|
+
const snippetLines = sectionLines.slice(snippetStart, snippetEnd)
|
|
431
|
+
const snippet = snippetLines.join('\n')
|
|
432
|
+
|
|
433
|
+
// Build context lines array for JSON output
|
|
434
|
+
const contextLines: ContextLine[] = []
|
|
435
|
+
for (let j = snippetStart; j < snippetEnd; j++) {
|
|
436
|
+
const ctxLine = sectionLines[j]
|
|
437
|
+
if (ctxLine !== undefined) {
|
|
438
|
+
contextLines.push({
|
|
439
|
+
lineNumber: section.startLine + j,
|
|
440
|
+
line: ctxLine,
|
|
441
|
+
isMatch: j === i,
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
matches.push({
|
|
447
|
+
lineNumber: absoluteLineNum,
|
|
448
|
+
line: line,
|
|
449
|
+
snippet,
|
|
450
|
+
contextLines,
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// For boolean queries, include section even without line-level matches
|
|
456
|
+
// (the section matched as a whole)
|
|
457
|
+
if (parsedQuery || matches.length > 0) {
|
|
458
|
+
const result: SearchResult = {
|
|
459
|
+
section,
|
|
460
|
+
document,
|
|
461
|
+
sectionContent,
|
|
462
|
+
}
|
|
463
|
+
if (matches.length > 0) {
|
|
464
|
+
results.push({ ...result, matches })
|
|
465
|
+
} else {
|
|
466
|
+
results.push(result)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (
|
|
470
|
+
options.limit !== undefined &&
|
|
471
|
+
results.length >= options.limit
|
|
472
|
+
) {
|
|
473
|
+
return results
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} else if (!parsedQuery && !contentRegex && !useFuzzyOrStem) {
|
|
477
|
+
// No content search, heading-only search
|
|
478
|
+
results.push({ section, document })
|
|
479
|
+
|
|
480
|
+
if (options.limit !== undefined && results.length >= options.limit) {
|
|
481
|
+
return results
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return results
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// ============================================================================
|
|
491
|
+
// Search with Content (legacy, uses heading-only search)
|
|
492
|
+
// ============================================================================
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Search for sections by metadata and include section content.
|
|
496
|
+
*
|
|
497
|
+
* @param rootPath - Root directory containing indexed markdown files
|
|
498
|
+
* @param options - Search filters
|
|
499
|
+
* @returns Matching sections with content
|
|
500
|
+
*
|
|
501
|
+
* @throws FileReadError - Cannot read index or source files
|
|
502
|
+
* @throws IndexCorruptedError - Index files are corrupted
|
|
503
|
+
*/
|
|
504
|
+
export const searchWithContent = (
|
|
505
|
+
rootPath: string,
|
|
506
|
+
options: SearchOptions = {},
|
|
507
|
+
): Effect.Effect<
|
|
508
|
+
readonly SearchResult[],
|
|
509
|
+
FileReadError | IndexCorruptedError
|
|
510
|
+
> =>
|
|
511
|
+
Effect.gen(function* () {
|
|
512
|
+
const storage = createStorage(rootPath)
|
|
513
|
+
const results = yield* search(rootPath, options)
|
|
514
|
+
|
|
515
|
+
const resultsWithContent: SearchResult[] = []
|
|
516
|
+
|
|
517
|
+
for (const result of results) {
|
|
518
|
+
const filePath = path.join(storage.rootPath, result.section.documentPath)
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const fileContent = yield* Effect.promise(() =>
|
|
522
|
+
fs.readFile(filePath, 'utf-8'),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
const lines = fileContent.split('\n')
|
|
526
|
+
const sectionContent = lines
|
|
527
|
+
.slice(result.section.startLine - 1, result.section.endLine)
|
|
528
|
+
.join('\n')
|
|
529
|
+
|
|
530
|
+
resultsWithContent.push({
|
|
531
|
+
...result,
|
|
532
|
+
sectionContent,
|
|
533
|
+
})
|
|
534
|
+
} catch {
|
|
535
|
+
// If file can't be read, include result without content
|
|
536
|
+
resultsWithContent.push(result)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return resultsWithContent
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// ============================================================================
|
|
544
|
+
// Context Generation
|
|
545
|
+
// ============================================================================
|
|
546
|
+
|
|
547
|
+
export interface ContextOptions {
|
|
548
|
+
/** Maximum tokens to include */
|
|
549
|
+
readonly maxTokens?: number | undefined
|
|
550
|
+
/** Include section content */
|
|
551
|
+
readonly includeContent?: boolean | undefined
|
|
552
|
+
/** Compression level: brief, summary, full */
|
|
553
|
+
readonly level?: 'brief' | 'summary' | 'full' | undefined
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface DocumentContext {
|
|
557
|
+
readonly path: string
|
|
558
|
+
readonly title: string
|
|
559
|
+
readonly totalTokens: number
|
|
560
|
+
readonly includedTokens: number
|
|
561
|
+
readonly sections: readonly SectionContext[]
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export interface SectionContext {
|
|
565
|
+
readonly heading: string
|
|
566
|
+
readonly level: number
|
|
567
|
+
readonly tokens: number
|
|
568
|
+
readonly content?: string | undefined
|
|
569
|
+
readonly hasCode: boolean
|
|
570
|
+
readonly hasList: boolean
|
|
571
|
+
readonly hasTable: boolean
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get context information for a document.
|
|
576
|
+
*
|
|
577
|
+
* @param rootPath - Root directory containing indexed markdown files
|
|
578
|
+
* @param filePath - Path to the document
|
|
579
|
+
* @param options - Context options (max tokens, include content)
|
|
580
|
+
* @returns Document context with sections
|
|
581
|
+
*
|
|
582
|
+
* @throws IndexNotFoundError - Index doesn't exist
|
|
583
|
+
* @throws DocumentNotFoundError - Document not in index
|
|
584
|
+
* @throws FileReadError - Cannot read index or source files
|
|
585
|
+
* @throws IndexCorruptedError - Index files are corrupted
|
|
586
|
+
*/
|
|
587
|
+
export const getContext = (
|
|
588
|
+
rootPath: string,
|
|
589
|
+
filePath: string,
|
|
590
|
+
options: ContextOptions = {},
|
|
591
|
+
): Effect.Effect<
|
|
592
|
+
DocumentContext,
|
|
593
|
+
| IndexNotFoundError
|
|
594
|
+
| DocumentNotFoundError
|
|
595
|
+
| FileReadError
|
|
596
|
+
| IndexCorruptedError
|
|
597
|
+
> =>
|
|
598
|
+
Effect.gen(function* () {
|
|
599
|
+
const storage = createStorage(rootPath)
|
|
600
|
+
const resolvedFile = path.resolve(filePath)
|
|
601
|
+
const relativePath = path.relative(storage.rootPath, resolvedFile)
|
|
602
|
+
|
|
603
|
+
const docIndex = yield* loadDocumentIndex(storage)
|
|
604
|
+
const sectionIndex = yield* loadSectionIndex(storage)
|
|
605
|
+
|
|
606
|
+
if (!docIndex || !sectionIndex) {
|
|
607
|
+
return yield* Effect.fail(
|
|
608
|
+
new IndexNotFoundError({ path: storage.rootPath }),
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const document = docIndex.documents[relativePath]
|
|
613
|
+
if (!document) {
|
|
614
|
+
return yield* Effect.fail(
|
|
615
|
+
new DocumentNotFoundError({
|
|
616
|
+
path: relativePath,
|
|
617
|
+
indexPath: storage.rootPath,
|
|
618
|
+
}),
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Get sections for this document
|
|
623
|
+
const sectionIds = sectionIndex.byDocument[document.id] ?? []
|
|
624
|
+
const sections: SectionContext[] = []
|
|
625
|
+
let includedTokens = 0
|
|
626
|
+
const maxTokens = options.maxTokens ?? Infinity
|
|
627
|
+
const includeContent = options.includeContent ?? options.level === 'full'
|
|
628
|
+
|
|
629
|
+
// Read file content if needed
|
|
630
|
+
let fileContent: string | null = null
|
|
631
|
+
if (includeContent) {
|
|
632
|
+
try {
|
|
633
|
+
fileContent = yield* Effect.promise(() =>
|
|
634
|
+
fs.readFile(resolvedFile, 'utf-8'),
|
|
635
|
+
)
|
|
636
|
+
} catch {
|
|
637
|
+
// Continue without content
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const fileLines = fileContent?.split('\n') ?? []
|
|
642
|
+
|
|
643
|
+
for (const sectionId of sectionIds) {
|
|
644
|
+
const section = sectionIndex.sections[sectionId]
|
|
645
|
+
if (!section) continue
|
|
646
|
+
|
|
647
|
+
// Check token budget
|
|
648
|
+
if (includedTokens + section.tokenCount > maxTokens) {
|
|
649
|
+
// Include brief info only if we're over budget
|
|
650
|
+
if (options.level === 'brief') continue
|
|
651
|
+
|
|
652
|
+
sections.push({
|
|
653
|
+
heading: section.heading,
|
|
654
|
+
level: section.level,
|
|
655
|
+
tokens: section.tokenCount,
|
|
656
|
+
hasCode: section.hasCode,
|
|
657
|
+
hasList: section.hasList,
|
|
658
|
+
hasTable: section.hasTable,
|
|
659
|
+
})
|
|
660
|
+
continue
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
includedTokens += section.tokenCount
|
|
664
|
+
|
|
665
|
+
let content: string | undefined
|
|
666
|
+
if (includeContent && fileContent) {
|
|
667
|
+
content = fileLines
|
|
668
|
+
.slice(section.startLine - 1, section.endLine)
|
|
669
|
+
.join('\n')
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
sections.push({
|
|
673
|
+
heading: section.heading,
|
|
674
|
+
level: section.level,
|
|
675
|
+
tokens: section.tokenCount,
|
|
676
|
+
content,
|
|
677
|
+
hasCode: section.hasCode,
|
|
678
|
+
hasList: section.hasList,
|
|
679
|
+
hasTable: section.hasTable,
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
path: relativePath,
|
|
685
|
+
title: document.title,
|
|
686
|
+
totalTokens: document.tokenCount,
|
|
687
|
+
includedTokens,
|
|
688
|
+
sections,
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
// ============================================================================
|
|
693
|
+
// LLM-Ready Output
|
|
694
|
+
// ============================================================================
|
|
695
|
+
|
|
696
|
+
export const formatContextForLLM = (context: DocumentContext): string => {
|
|
697
|
+
const lines: string[] = []
|
|
698
|
+
|
|
699
|
+
lines.push(`# ${context.title}`)
|
|
700
|
+
lines.push(`Path: ${context.path}`)
|
|
701
|
+
lines.push(`Tokens: ${context.includedTokens}/${context.totalTokens}`)
|
|
702
|
+
lines.push('')
|
|
703
|
+
|
|
704
|
+
for (const section of context.sections) {
|
|
705
|
+
const prefix = '#'.repeat(section.level)
|
|
706
|
+
const meta: string[] = []
|
|
707
|
+
if (section.hasCode) meta.push('code')
|
|
708
|
+
if (section.hasList) meta.push('list')
|
|
709
|
+
if (section.hasTable) meta.push('table')
|
|
710
|
+
|
|
711
|
+
const metaStr = meta.length > 0 ? ` [${meta.join(', ')}]` : ''
|
|
712
|
+
lines.push(
|
|
713
|
+
`${prefix} ${section.heading}${metaStr} (${section.tokens} tokens)`,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
if (section.content) {
|
|
717
|
+
lines.push('')
|
|
718
|
+
lines.push(section.content)
|
|
719
|
+
lines.push('')
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return lines.join('\n')
|
|
724
|
+
}
|