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,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Encoder Re-ranking for Search Results
|
|
3
|
+
*
|
|
4
|
+
* Uses a cross-encoder model to re-rank search results for improved precision.
|
|
5
|
+
* Cross-encoders process query-document pairs together, capturing fine-grained
|
|
6
|
+
* relevance that bi-encoders (used in initial retrieval) may miss.
|
|
7
|
+
*
|
|
8
|
+
* Benefits:
|
|
9
|
+
* - 20-35% improvement in retrieval precision
|
|
10
|
+
* - Better handling of nuanced relevance
|
|
11
|
+
* - Opt-in to avoid latency when not needed
|
|
12
|
+
*
|
|
13
|
+
* Model: Xenova/ms-marco-MiniLM-L-6-v2 (22.7M params, 2-5ms/pair)
|
|
14
|
+
* Note: Model download is ~90MB on first use. Use initializeReranker() to pre-download.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as path from 'node:path'
|
|
18
|
+
import { Effect } from 'effect'
|
|
19
|
+
import { INDEX_DIR } from '../index/types.js'
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Dependency availability (checked at module load time)
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
let transformersAvailable: boolean | null = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if @huggingface/transformers is available.
|
|
29
|
+
* Result is cached after first check.
|
|
30
|
+
*/
|
|
31
|
+
const checkTransformersAvailable = async (): Promise<boolean> => {
|
|
32
|
+
if (transformersAvailable !== null) {
|
|
33
|
+
return transformersAvailable
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await import('@huggingface/transformers')
|
|
37
|
+
transformersAvailable = true
|
|
38
|
+
} catch {
|
|
39
|
+
transformersAvailable = false
|
|
40
|
+
}
|
|
41
|
+
return transformersAvailable
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Types
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
export interface RerankerOptions {
|
|
49
|
+
/** Number of top candidates to re-rank (default: 20) */
|
|
50
|
+
readonly topK?: number
|
|
51
|
+
/** Number of results to return after re-ranking (default: 10) */
|
|
52
|
+
readonly returnTopN?: number
|
|
53
|
+
/** Progress callback for model loading */
|
|
54
|
+
readonly onProgress?: (progress: ModelProgress) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ModelProgress {
|
|
58
|
+
readonly status: 'loading' | 'ready'
|
|
59
|
+
readonly file?: string | undefined
|
|
60
|
+
readonly progress?: number | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RerankedResult<T> {
|
|
64
|
+
readonly item: T
|
|
65
|
+
readonly rerankerScore: number
|
|
66
|
+
readonly originalRank: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Reranker {
|
|
70
|
+
/**
|
|
71
|
+
* Re-rank search results by query-document relevance.
|
|
72
|
+
*
|
|
73
|
+
* @param query - The search query
|
|
74
|
+
* @param results - Array of search results to re-rank
|
|
75
|
+
* @param getContent - Function to extract text content for each result
|
|
76
|
+
* @returns Re-ranked results with scores
|
|
77
|
+
*/
|
|
78
|
+
rerank<T>(
|
|
79
|
+
query: string,
|
|
80
|
+
results: readonly T[],
|
|
81
|
+
getContent: (item: T) => string,
|
|
82
|
+
): Promise<RerankedResult<T>[]>
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if the model is loaded and ready.
|
|
86
|
+
*/
|
|
87
|
+
isReady(): boolean
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Unload the model to free memory.
|
|
91
|
+
*/
|
|
92
|
+
unload(): void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Cross-Encoder Implementation
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
// Types for dynamically imported transformers.js
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
type AutoTokenizer = any
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
type AutoModelForSequenceClassification = any
|
|
104
|
+
|
|
105
|
+
const MODEL_ID = 'Xenova/ms-marco-MiniLM-L-6-v2'
|
|
106
|
+
|
|
107
|
+
class CrossEncoderReranker implements Reranker {
|
|
108
|
+
private model: AutoModelForSequenceClassification | null = null
|
|
109
|
+
private tokenizer: AutoTokenizer | null = null
|
|
110
|
+
private loadPromise: Promise<void> | null = null
|
|
111
|
+
private cacheDir: string | undefined
|
|
112
|
+
|
|
113
|
+
constructor(cacheDir?: string) {
|
|
114
|
+
this.cacheDir = cacheDir
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async ensureLoaded(
|
|
118
|
+
onProgress?: (progress: ModelProgress) => void,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
if (this.model && this.tokenizer) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.loadPromise) {
|
|
125
|
+
return this.loadPromise
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.loadPromise = this.load(onProgress)
|
|
129
|
+
return this.loadPromise
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async load(
|
|
133
|
+
onProgress?: (progress: ModelProgress) => void,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
onProgress?.({ status: 'loading' })
|
|
136
|
+
|
|
137
|
+
// Dynamic import to avoid bundling issues
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
139
|
+
const transformers = await import(
|
|
140
|
+
/* webpackIgnore: true */ '@huggingface/transformers'
|
|
141
|
+
)
|
|
142
|
+
const { AutoTokenizer, AutoModelForSequenceClassification, env } =
|
|
143
|
+
transformers
|
|
144
|
+
|
|
145
|
+
// Set cache directory if specified
|
|
146
|
+
if (this.cacheDir) {
|
|
147
|
+
env.cacheDir = this.cacheDir
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Load model and tokenizer
|
|
151
|
+
const progressCallback = onProgress
|
|
152
|
+
? (data: { file?: string; progress?: number }) => {
|
|
153
|
+
onProgress({
|
|
154
|
+
status: 'loading',
|
|
155
|
+
file: data.file,
|
|
156
|
+
progress: data.progress,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
: undefined
|
|
160
|
+
|
|
161
|
+
this.tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID, {
|
|
162
|
+
progress_callback: progressCallback,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
this.model = await AutoModelForSequenceClassification.from_pretrained(
|
|
166
|
+
MODEL_ID,
|
|
167
|
+
{
|
|
168
|
+
progress_callback: progressCallback,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
onProgress?.({ status: 'ready' })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async rerank<T>(
|
|
176
|
+
query: string,
|
|
177
|
+
results: readonly T[],
|
|
178
|
+
getContent: (item: T) => string,
|
|
179
|
+
): Promise<RerankedResult<T>[]> {
|
|
180
|
+
if (results.length === 0) {
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await this.ensureLoaded()
|
|
185
|
+
|
|
186
|
+
if (!this.model || !this.tokenizer) {
|
|
187
|
+
throw new Error('Failed to load cross-encoder model')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Prepare query-document pairs
|
|
191
|
+
const queries = results.map(() => query)
|
|
192
|
+
const documents = results.map((r) => getContent(r))
|
|
193
|
+
|
|
194
|
+
// Tokenize all pairs
|
|
195
|
+
const features = this.tokenizer(queries, {
|
|
196
|
+
text_pair: documents,
|
|
197
|
+
padding: true,
|
|
198
|
+
truncation: true,
|
|
199
|
+
max_length: 512,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Get relevance scores
|
|
203
|
+
const output = await this.model(features)
|
|
204
|
+
|
|
205
|
+
// Extract logits (relevance scores)
|
|
206
|
+
// The model outputs logits directly - higher = more relevant
|
|
207
|
+
const logits = output.logits.data as Float32Array
|
|
208
|
+
|
|
209
|
+
// Create results with scores
|
|
210
|
+
const scoredResults: RerankedResult<T>[] = results.map((item, idx) => ({
|
|
211
|
+
item,
|
|
212
|
+
rerankerScore: logits[idx] ?? 0,
|
|
213
|
+
originalRank: idx + 1,
|
|
214
|
+
}))
|
|
215
|
+
|
|
216
|
+
// Sort by re-ranker score (descending)
|
|
217
|
+
scoredResults.sort((a, b) => b.rerankerScore - a.rerankerScore)
|
|
218
|
+
|
|
219
|
+
return scoredResults
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
isReady(): boolean {
|
|
223
|
+
return this.model !== null && this.tokenizer !== null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Initialize (pre-download) the model with progress reporting.
|
|
228
|
+
* This is the preferred method for --rerank-init.
|
|
229
|
+
*/
|
|
230
|
+
async initialize(
|
|
231
|
+
onProgress?: (progress: ModelProgress) => void,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
await this.ensureLoaded(onProgress)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
unload(): void {
|
|
237
|
+
this.model = null
|
|
238
|
+
this.tokenizer = null
|
|
239
|
+
this.loadPromise = null
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// Singleton Instance
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
let rerankerInstance: CrossEncoderReranker | null = null
|
|
248
|
+
let rerankerCacheDir: string | undefined
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the singleton reranker instance.
|
|
252
|
+
* The model is loaded lazily on first use.
|
|
253
|
+
*
|
|
254
|
+
* Note: If cacheDir changes from a previous call, the instance is recreated.
|
|
255
|
+
* This ensures correct behavior in multi-tenant scenarios and tests.
|
|
256
|
+
*/
|
|
257
|
+
export const getReranker = (cacheDir?: string): Reranker => {
|
|
258
|
+
// Recreate instance if cacheDir has changed
|
|
259
|
+
if (rerankerInstance && rerankerCacheDir !== cacheDir) {
|
|
260
|
+
rerankerInstance.unload()
|
|
261
|
+
rerankerInstance = null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!rerankerInstance) {
|
|
265
|
+
rerankerInstance = new CrossEncoderReranker(cacheDir)
|
|
266
|
+
rerankerCacheDir = cacheDir
|
|
267
|
+
}
|
|
268
|
+
return rerankerInstance
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Unload the reranker model to free memory.
|
|
273
|
+
*/
|
|
274
|
+
export const unloadReranker = (): void => {
|
|
275
|
+
if (rerankerInstance) {
|
|
276
|
+
rerankerInstance.unload()
|
|
277
|
+
rerankerInstance = null
|
|
278
|
+
rerankerCacheDir = undefined
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Effect Wrappers
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
export class RerankerError extends Error {
|
|
287
|
+
readonly _tag = 'RerankerError'
|
|
288
|
+
constructor(
|
|
289
|
+
readonly reason:
|
|
290
|
+
| 'ModelLoadFailed'
|
|
291
|
+
| 'InferenceFailed'
|
|
292
|
+
| 'DependencyMissing',
|
|
293
|
+
message: string,
|
|
294
|
+
readonly cause?: unknown,
|
|
295
|
+
) {
|
|
296
|
+
super(message)
|
|
297
|
+
this.name = 'RerankerError'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Re-rank search results using the cross-encoder model.
|
|
303
|
+
*
|
|
304
|
+
* @param query - The search query
|
|
305
|
+
* @param results - Array of search results to re-rank
|
|
306
|
+
* @param getContent - Function to extract text content for each result
|
|
307
|
+
* @param options - Re-ranking options
|
|
308
|
+
* @returns Re-ranked results wrapped in Effect
|
|
309
|
+
*/
|
|
310
|
+
export const rerankResults = <T>(
|
|
311
|
+
query: string,
|
|
312
|
+
results: readonly T[],
|
|
313
|
+
getContent: (item: T) => string,
|
|
314
|
+
options: RerankerOptions = {},
|
|
315
|
+
): Effect.Effect<RerankedResult<T>[], RerankerError> =>
|
|
316
|
+
Effect.gen(function* () {
|
|
317
|
+
const topK = options.topK ?? 20
|
|
318
|
+
const returnTopN = options.returnTopN ?? 10
|
|
319
|
+
|
|
320
|
+
// Take top-K candidates for re-ranking
|
|
321
|
+
const candidates = results.slice(0, topK)
|
|
322
|
+
|
|
323
|
+
if (candidates.length === 0) {
|
|
324
|
+
return []
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Determine cache directory using node's process.cwd()
|
|
328
|
+
const nodeProcess = globalThis as unknown as {
|
|
329
|
+
process?: { cwd: () => string }
|
|
330
|
+
}
|
|
331
|
+
const cwd = nodeProcess.process?.cwd() ?? '.'
|
|
332
|
+
const cacheDir = options.onProgress
|
|
333
|
+
? undefined // Let user see download progress
|
|
334
|
+
: path.join(cwd, INDEX_DIR, 'models')
|
|
335
|
+
|
|
336
|
+
const reranker = getReranker(cacheDir)
|
|
337
|
+
|
|
338
|
+
// Check dependency availability before attempting re-ranking
|
|
339
|
+
const available = yield* Effect.promise(() => checkTransformersAvailable())
|
|
340
|
+
if (!available) {
|
|
341
|
+
return yield* Effect.fail(
|
|
342
|
+
new RerankerError(
|
|
343
|
+
'DependencyMissing',
|
|
344
|
+
'Cross-encoder re-ranking requires @huggingface/transformers. Install with: npm install @huggingface/transformers',
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Re-rank using cross-encoder
|
|
350
|
+
const reranked = yield* Effect.tryPromise({
|
|
351
|
+
try: () => reranker.rerank(query, candidates, getContent),
|
|
352
|
+
catch: (e) =>
|
|
353
|
+
new RerankerError(
|
|
354
|
+
'InferenceFailed',
|
|
355
|
+
`Re-ranking failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
356
|
+
e,
|
|
357
|
+
),
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Return top-N re-ranked results
|
|
361
|
+
return reranked.slice(0, returnTopN)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Check if the @huggingface/transformers package is available.
|
|
366
|
+
*/
|
|
367
|
+
export const isRerankerAvailable = (): Effect.Effect<boolean, never> =>
|
|
368
|
+
Effect.promise(() => checkTransformersAvailable())
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Initialize (pre-download) the cross-encoder model.
|
|
372
|
+
* Use this to download the model before first search to avoid latency.
|
|
373
|
+
*
|
|
374
|
+
* @param cacheDir - Optional directory to cache the model
|
|
375
|
+
* @param onProgress - Optional callback for download progress
|
|
376
|
+
* @returns Effect that completes when model is loaded
|
|
377
|
+
*/
|
|
378
|
+
export const initializeReranker = (
|
|
379
|
+
cacheDir?: string,
|
|
380
|
+
onProgress?: (progress: ModelProgress) => void,
|
|
381
|
+
): Effect.Effect<void, RerankerError> =>
|
|
382
|
+
Effect.gen(function* () {
|
|
383
|
+
// Check dependency availability
|
|
384
|
+
const available = yield* Effect.promise(() => checkTransformersAvailable())
|
|
385
|
+
if (!available) {
|
|
386
|
+
return yield* Effect.fail(
|
|
387
|
+
new RerankerError(
|
|
388
|
+
'DependencyMissing',
|
|
389
|
+
'Cross-encoder re-ranking requires @huggingface/transformers. Install with: npm install @huggingface/transformers',
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const reranker = getReranker(cacheDir) as CrossEncoderReranker
|
|
395
|
+
|
|
396
|
+
// Initialize the model with progress reporting
|
|
397
|
+
yield* Effect.tryPromise({
|
|
398
|
+
try: () => reranker.initialize(onProgress),
|
|
399
|
+
catch: (e) =>
|
|
400
|
+
new RerankerError(
|
|
401
|
+
'ModelLoadFailed',
|
|
402
|
+
`Failed to initialize model: ${e instanceof Error ? e.message : String(e)}`,
|
|
403
|
+
e,
|
|
404
|
+
),
|
|
405
|
+
})
|
|
406
|
+
})
|