sweet-search 0.0.1 → 2.3.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/LICENSE +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +587 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- package/scripts/verify-runtime.js +176 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary Ranker Module
|
|
3
|
+
*
|
|
4
|
+
* Scores and classifies mined vocabulary terms for cache pre-warming.
|
|
5
|
+
* Two ranking streams:
|
|
6
|
+
*
|
|
7
|
+
* Stream A: Identifier Ranking (BM25 + PageRank + heuristic multipliers)
|
|
8
|
+
* → Produces scored, classified identifiers for lexical + semantic warmup
|
|
9
|
+
*
|
|
10
|
+
* Stream B: Community Phrase Ranking (c-TF-IDF scores + PageRank boost)
|
|
11
|
+
* → Produces NL query phrases with question variants for semantic warmup
|
|
12
|
+
*
|
|
13
|
+
* Pure computation module — no I/O, no external dependencies.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Constants
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* BM25 tuning constants for prewarm vocabulary ranking only.
|
|
22
|
+
* NOTE: These are NOT used for live FTS5 search. SQLite FTS5 uses its own
|
|
23
|
+
* built-in BM25 defaults (k1=1.2, b=0.75). Column weights are configured
|
|
24
|
+
* via bm25() arguments in graph-search.js. Do not add custom rescoring
|
|
25
|
+
* unless benchmark data proves a real win — column weights and lexical
|
|
26
|
+
* normalization matter far more than k1/b tuning.
|
|
27
|
+
*/
|
|
28
|
+
const K1 = 1.5; // Higher saturation for code (many repeated identifiers)
|
|
29
|
+
const B = 0.5; // Lower length penalty (code file sizes vary widely)
|
|
30
|
+
|
|
31
|
+
/** Heuristic multipliers applied on top of BM25 score */
|
|
32
|
+
const MULTIPLIERS = {
|
|
33
|
+
exports: 3.0, // Public API surface
|
|
34
|
+
pagerank: 2.5, // High code-graph connectivity (PageRank > 0.1)
|
|
35
|
+
filepath: 2.0, // Appears in file/directory names
|
|
36
|
+
crossfile: 1.5, // Appears in multiple files
|
|
37
|
+
named: 1.2, // camelCase / PascalCase named entity
|
|
38
|
+
dependency: 1.0, // Appears in dependency names
|
|
39
|
+
string: 0.8, // String literal / error message
|
|
40
|
+
private: 0.3, // Single occurrence, private scope
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Depth presets: { identifiers, phrases } caps */
|
|
44
|
+
const DEPTH_CUTOFFS = {
|
|
45
|
+
light: { identifiers: 200, phrases: 100 },
|
|
46
|
+
medium: { identifiers: 1000, phrases: 500 },
|
|
47
|
+
deep: { identifiers: 2000, phrases: 1000 },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Default phrases-per-community limit */
|
|
51
|
+
const DEFAULT_PHRASES_PER_COMMUNITY = 15;
|
|
52
|
+
|
|
53
|
+
/** Question templates for semantic warmup phrases */
|
|
54
|
+
const QUESTION_TEMPLATES = [
|
|
55
|
+
phrase => `how does ${phrase} work`,
|
|
56
|
+
phrase => `where is ${phrase}`,
|
|
57
|
+
phrase => `what handles ${phrase}`,
|
|
58
|
+
phrase => `find ${phrase}`,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Pattern Detection (for classifyWarmMode)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
const RE_PASCAL_CASE = /^[A-Z][a-z]+(?:[A-Z][a-z]+)+$/;
|
|
66
|
+
const RE_SCREAMING_SNAKE = /^[A-Z][A-Z0-9_]+$/;
|
|
67
|
+
const RE_CAMEL_CASE = /^[a-z]+(?:[A-Z][a-z]+)+$/;
|
|
68
|
+
const RE_SNAKE_CASE = /^[a-z][a-z0-9_]+$/;
|
|
69
|
+
const RE_DOTTED_PATH = /^[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+$/;
|
|
70
|
+
|
|
71
|
+
// Code-like tokens: contain uppercase transitions, underscores, dots, or digits mixed with letters
|
|
72
|
+
const RE_CODE_TOKEN = /[A-Z]{2}|[a-z][A-Z]|_|\.\w|[a-z]\d|\d[a-z]/;
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// BM25 Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute IDF-like rarity score for a term given total bucket count and frequency.
|
|
80
|
+
* NOTE: In prewarm mining this is source-bucket rarity (import/export/graph-entity...)
|
|
81
|
+
* unless caller supplies a real `options.totalFiles`.
|
|
82
|
+
* @param {number} totalDocs - N (total buckets, or total files if provided)
|
|
83
|
+
* @param {number} docFreq - df (bucket frequency)
|
|
84
|
+
* @returns {number} IDF score
|
|
85
|
+
*/
|
|
86
|
+
function computeIDF(totalDocs, docFreq) {
|
|
87
|
+
return Math.log((totalDocs) / (1 + docFreq));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compute BM25 score for a single term.
|
|
92
|
+
* @param {number} tf - term frequency (occurrences across mining)
|
|
93
|
+
* @param {number} idf - precomputed IDF
|
|
94
|
+
* @param {number} docLen - approximate document length (source count)
|
|
95
|
+
* @param {number} avgDocLen - average document length
|
|
96
|
+
* @returns {number} BM25 score
|
|
97
|
+
*/
|
|
98
|
+
function bm25Score(tf, idf, docLen, avgDocLen) {
|
|
99
|
+
const numerator = tf * (K1 + 1);
|
|
100
|
+
const denominator = tf + K1 * (1 - B + B * (docLen / avgDocLen));
|
|
101
|
+
return idf * (numerator / denominator);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Heuristic Multiplier Computation
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Determine the best heuristic multiplier for a term based on its sources/metadata.
|
|
110
|
+
* @param {object} termInfo - { term, score, source, type, sources }
|
|
111
|
+
* @param {number} pageRankScore - PageRank score for this term (0 if absent)
|
|
112
|
+
* @returns {number} combined heuristic multiplier
|
|
113
|
+
*/
|
|
114
|
+
function computeHeuristicMultiplier(termInfo, pageRankScore) {
|
|
115
|
+
const sources = termInfo.sources || (termInfo.source ? [termInfo.source] : []);
|
|
116
|
+
const type = termInfo.type || '';
|
|
117
|
+
|
|
118
|
+
// Start with 1.0 (neutral)
|
|
119
|
+
let multiplier = 1.0;
|
|
120
|
+
|
|
121
|
+
// Exports / public API
|
|
122
|
+
if (type === 'export' || type === 'exports' || sources.includes('exports')) {
|
|
123
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.exports);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// High PageRank connectivity
|
|
127
|
+
if (pageRankScore > 0.1) {
|
|
128
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.pagerank);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// File/directory name
|
|
132
|
+
if (type === 'filepath' || sources.includes('filepath') || sources.includes('filename')) {
|
|
133
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.filepath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cross-file (appears in multiple files)
|
|
137
|
+
if (sources.length > 1 || type === 'crossfile') {
|
|
138
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.crossfile);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Named entity (camelCase / PascalCase)
|
|
142
|
+
if (RE_CAMEL_CASE.test(termInfo.term) || RE_PASCAL_CASE.test(termInfo.term)) {
|
|
143
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.named);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Dependency name
|
|
147
|
+
if (type === 'dependency' || sources.includes('dependency')) {
|
|
148
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.dependency);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// String literal / error message
|
|
152
|
+
if (type === 'string' || type === 'error' || sources.includes('string')) {
|
|
153
|
+
multiplier = Math.max(multiplier, MULTIPLIERS.string);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Single occurrence, private
|
|
157
|
+
const totalOccurrences = termInfo.score || 1;
|
|
158
|
+
if (totalOccurrences <= 1 && (type === 'private' || sources.length <= 1)) {
|
|
159
|
+
// Only apply private penalty if no stronger signal
|
|
160
|
+
if (multiplier <= 1.0) {
|
|
161
|
+
multiplier = MULTIPLIERS.private;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return multiplier;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Warm Mode Classification
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Classify how a term should be pre-warmed in the embedding cache.
|
|
174
|
+
*
|
|
175
|
+
* - "lexical": exact FTS5 match is sufficient (code identifiers, paths)
|
|
176
|
+
* - "semantic": needs embedding (natural language phrases)
|
|
177
|
+
* - "both": high-value hub entity worth caching both ways
|
|
178
|
+
*
|
|
179
|
+
* @param {string} term - the vocabulary term
|
|
180
|
+
* @param {number} [pageRankScore=0] - optional PageRank score for the term
|
|
181
|
+
* @returns {'lexical' | 'semantic' | 'both'}
|
|
182
|
+
*/
|
|
183
|
+
export function classifyWarmMode(term, pageRankScore = 0) {
|
|
184
|
+
// High-PageRank hub entity → cache both lexical and semantic
|
|
185
|
+
if (pageRankScore > 0.1) return 'both';
|
|
186
|
+
|
|
187
|
+
// PascalCase (e.g., UserService, AuthController)
|
|
188
|
+
if (RE_PASCAL_CASE.test(term)) return 'lexical';
|
|
189
|
+
|
|
190
|
+
// SCREAMING_SNAKE (e.g., MAX_RETRIES, DEFAULT_TIMEOUT)
|
|
191
|
+
if (RE_SCREAMING_SNAKE.test(term)) return 'lexical';
|
|
192
|
+
|
|
193
|
+
// Dotted path (e.g., config.database.host)
|
|
194
|
+
if (RE_DOTTED_PATH.test(term)) return 'lexical';
|
|
195
|
+
|
|
196
|
+
// snake_case single identifier
|
|
197
|
+
if (RE_SNAKE_CASE.test(term)) return 'lexical';
|
|
198
|
+
|
|
199
|
+
// camelCase
|
|
200
|
+
if (RE_CAMEL_CASE.test(term)) return 'lexical';
|
|
201
|
+
|
|
202
|
+
// Tokenize by whitespace
|
|
203
|
+
const tokens = term.split(/\s+/).filter(t => t.length > 0);
|
|
204
|
+
|
|
205
|
+
// Short terms (≤2 tokens) that look like code → lexical
|
|
206
|
+
if (tokens.length <= 2) {
|
|
207
|
+
// If any token looks code-like, treat as lexical
|
|
208
|
+
if (tokens.some(t => RE_CODE_TOKEN.test(t))) return 'lexical';
|
|
209
|
+
// Single-word terms default to lexical (likely an identifier)
|
|
210
|
+
if (tokens.length === 1) return 'lexical';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// NL phrase: 3+ tokens, mostly lowercase with no code tokens
|
|
214
|
+
if (tokens.length >= 3) {
|
|
215
|
+
const codeTokenCount = tokens.filter(t => RE_CODE_TOKEN.test(t)).length;
|
|
216
|
+
if (codeTokenCount === 0) return 'semantic';
|
|
217
|
+
// Mix of NL and code → both
|
|
218
|
+
if (codeTokenCount <= 1) return 'both';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Default: lexical (safer for code search)
|
|
222
|
+
return 'lexical';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Stream A: Identifier Ranking
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Rank identifiers using BM25 + PageRank + heuristic multipliers.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<{term: string, score: number, source: string, type: string, sources?: string[]}>} minedTerms
|
|
233
|
+
* Output from vocab-miner.js
|
|
234
|
+
* @param {Map<string, number>} pageRankScores
|
|
235
|
+
* Entity name → PageRank score (from repo-map.js)
|
|
236
|
+
* @param {object} [options]
|
|
237
|
+
* @param {'light'|'medium'|'deep'} [options.depth='medium']
|
|
238
|
+
* @param {number} [options.topN] - explicit cap (overrides depth)
|
|
239
|
+
* @param {number} [options.totalFiles] - total files in codebase (for IDF; estimated if absent)
|
|
240
|
+
* @returns {{ identifiers: Array<{term: string, score: number, warm_mode: string, sources: string[], type: string}>, totalMined: number }}
|
|
241
|
+
*/
|
|
242
|
+
export function rankIdentifiers(minedTerms, pageRankScores, options = {}) {
|
|
243
|
+
const depth = options.depth || 'medium';
|
|
244
|
+
const cutoff = options.topN || DEPTH_CUTOFFS[depth]?.identifiers || DEPTH_CUTOFFS.medium.identifiers;
|
|
245
|
+
|
|
246
|
+
if (!minedTerms || minedTerms.length === 0) {
|
|
247
|
+
return { identifiers: [], totalMined: 0 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Aggregate terms: group duplicates, accumulate tf & sources ---
|
|
251
|
+
const termMap = new Map(); // term → { tf, sources: Set, type, docFreq }
|
|
252
|
+
for (const entry of minedTerms) {
|
|
253
|
+
const key = entry.term;
|
|
254
|
+
if (!termMap.has(key)) {
|
|
255
|
+
termMap.set(key, {
|
|
256
|
+
term: key,
|
|
257
|
+
tf: 0,
|
|
258
|
+
sources: new Set(),
|
|
259
|
+
type: entry.type || '',
|
|
260
|
+
docFreq: 0,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const agg = termMap.get(key);
|
|
264
|
+
agg.tf += (entry.score || 1);
|
|
265
|
+
if (entry.source) agg.sources.add(entry.source);
|
|
266
|
+
if (entry.sources) {
|
|
267
|
+
for (const s of entry.sources) agg.sources.add(s);
|
|
268
|
+
}
|
|
269
|
+
// Approximate docFreq from unique sources
|
|
270
|
+
agg.docFreq = agg.sources.size;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- BM25 prerequisites ---
|
|
274
|
+
const totalFiles = options.totalFiles || estimateTotalSourceBuckets(termMap);
|
|
275
|
+
const terms = [...termMap.values()];
|
|
276
|
+
|
|
277
|
+
// Average "document length" ≈ average source count per term
|
|
278
|
+
const totalDocLen = terms.reduce((sum, t) => sum + t.sources.size, 0);
|
|
279
|
+
const avgDocLen = terms.length > 0 ? totalDocLen / terms.length : 1;
|
|
280
|
+
|
|
281
|
+
// Pre-compute IDFs
|
|
282
|
+
for (const t of terms) {
|
|
283
|
+
t.idf = computeIDF(totalFiles, t.docFreq);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --- Score each term ---
|
|
287
|
+
const scored = [];
|
|
288
|
+
for (const t of terms) {
|
|
289
|
+
const prScore = pageRankScores?.get(t.term) || 0;
|
|
290
|
+
const bm25 = bm25Score(t.tf, t.idf, t.sources.size, avgDocLen);
|
|
291
|
+
const heuristic = computeHeuristicMultiplier(
|
|
292
|
+
{ term: t.term, score: t.tf, sources: [...t.sources], type: t.type },
|
|
293
|
+
prScore
|
|
294
|
+
);
|
|
295
|
+
const finalScore = bm25 * heuristic * (1 + prScore);
|
|
296
|
+
const warmMode = classifyWarmMode(t.term, prScore);
|
|
297
|
+
|
|
298
|
+
scored.push({
|
|
299
|
+
term: t.term,
|
|
300
|
+
score: finalScore,
|
|
301
|
+
warm_mode: warmMode,
|
|
302
|
+
sources: [...t.sources],
|
|
303
|
+
type: t.type,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- Sort descending by score, then take top-N ---
|
|
308
|
+
scored.sort((a, b) => b.score - a.score);
|
|
309
|
+
const identifiers = scored.slice(0, cutoff);
|
|
310
|
+
|
|
311
|
+
return { identifiers, totalMined: minedTerms.length };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Estimate total source-bucket count from term source diversity.
|
|
316
|
+
* This is an approximation used when real file counts are unavailable.
|
|
317
|
+
* Pass `options.totalFiles` to rankIdentifiers() for true file-based BM25 IDF.
|
|
318
|
+
* @param {Map} termMap
|
|
319
|
+
* @returns {number}
|
|
320
|
+
*/
|
|
321
|
+
function estimateTotalSourceBuckets(termMap) {
|
|
322
|
+
const allSources = new Set();
|
|
323
|
+
for (const t of termMap.values()) {
|
|
324
|
+
for (const s of t.sources) allSources.add(s);
|
|
325
|
+
}
|
|
326
|
+
// Source-type buckets are small but stable; keep floor at 1.
|
|
327
|
+
return Math.max(allSources.size, 1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Stream B: Community Phrase Ranking
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Rank community phrases using c-TF-IDF scores + PageRank boost.
|
|
336
|
+
*
|
|
337
|
+
* @param {Array<{communityId: number, phrases: Array<{text: string, score: number, type?: string}>}>} communityPhrases
|
|
338
|
+
* Output from vocab-miner.js
|
|
339
|
+
* @param {Map<string, number>} pageRankScores
|
|
340
|
+
* Entity name → PageRank score
|
|
341
|
+
* @param {object} [options]
|
|
342
|
+
* @param {number} [options.maxPhrasesPerCommunity] - K per community (default 15)
|
|
343
|
+
* @param {number} [options.maxTotalPhrases] - total cap (default from depth)
|
|
344
|
+
* @param {'light'|'medium'|'deep'} [options.depth='medium']
|
|
345
|
+
* @returns {{ phrases: Array<{phrase: string, communityId: number, score: number, variants: string[]}>, totalMined: number }}
|
|
346
|
+
*/
|
|
347
|
+
export function rankCommunityPhrases(communityPhrases, pageRankScores, options = {}) {
|
|
348
|
+
const depth = options.depth || 'medium';
|
|
349
|
+
const maxPerCommunity = options.maxPhrasesPerCommunity || DEFAULT_PHRASES_PER_COMMUNITY;
|
|
350
|
+
const maxTotal = options.maxTotalPhrases || DEPTH_CUTOFFS[depth]?.phrases || DEPTH_CUTOFFS.medium.phrases;
|
|
351
|
+
|
|
352
|
+
if (!communityPhrases || communityPhrases.length === 0) {
|
|
353
|
+
return { phrases: [], totalMined: 0 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Pre-filter high-PR entities ONCE before the phrase loop (O(N*M) → O(N+M)).
|
|
357
|
+
// Build a pre-lowercased array so each phrase only iterates the small filtered set.
|
|
358
|
+
const highPrEntities = []; // { nameLower: string, score: number }
|
|
359
|
+
if (pageRankScores) {
|
|
360
|
+
for (const [entity, prScore] of pageRankScores) {
|
|
361
|
+
if (prScore > 0.01) {
|
|
362
|
+
highPrEntities.push({ nameLower: entity.toLowerCase(), score: prScore });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let totalMined = 0;
|
|
368
|
+
const allPhrases = []; // { phrase, communityId, score }
|
|
369
|
+
|
|
370
|
+
for (const community of communityPhrases) {
|
|
371
|
+
const cid = community.communityId;
|
|
372
|
+
const phrases = community.phrases || [];
|
|
373
|
+
totalMined += phrases.length;
|
|
374
|
+
|
|
375
|
+
// Score each phrase: base c-TF-IDF score + PageRank boost for entity mentions
|
|
376
|
+
const scored = phrases.map(p => {
|
|
377
|
+
let boost = 0;
|
|
378
|
+
if (highPrEntities.length > 0) {
|
|
379
|
+
// Check if phrase text contains any high-PageRank entity name.
|
|
380
|
+
// Uses pre-lowercased filtered set — avoids repeated toLowerCase() on all N entries.
|
|
381
|
+
const textLower = p.text.toLowerCase();
|
|
382
|
+
for (const { nameLower, score: prScore } of highPrEntities) {
|
|
383
|
+
if (textLower.includes(nameLower)) {
|
|
384
|
+
boost = Math.max(boost, prScore);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
phrase: p.text,
|
|
390
|
+
communityId: cid,
|
|
391
|
+
score: (p.score || 0) * (1 + boost),
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Sort by score descending, take top-K per community
|
|
396
|
+
scored.sort((a, b) => b.score - a.score);
|
|
397
|
+
allPhrases.push(...scored.slice(0, maxPerCommunity));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Deduplicate across communities: keep highest-scoring occurrence
|
|
401
|
+
const deduped = deduplicatePhrases(allPhrases);
|
|
402
|
+
|
|
403
|
+
// Sort globally by score, take top-N
|
|
404
|
+
deduped.sort((a, b) => b.score - a.score);
|
|
405
|
+
const topPhrases = deduped.slice(0, maxTotal);
|
|
406
|
+
|
|
407
|
+
// Generate question variants
|
|
408
|
+
const withVariants = topPhrases.map(p => ({
|
|
409
|
+
phrase: p.phrase,
|
|
410
|
+
communityId: p.communityId,
|
|
411
|
+
score: p.score,
|
|
412
|
+
variants: QUESTION_TEMPLATES.map(fn => fn(p.phrase)),
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
return { phrases: withVariants, totalMined };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Deduplicate phrases: when the same phrase appears in multiple communities,
|
|
420
|
+
* keep only the highest-scoring occurrence.
|
|
421
|
+
* @param {Array<{phrase: string, communityId: number, score: number}>} phrases
|
|
422
|
+
* @returns {Array<{phrase: string, communityId: number, score: number}>}
|
|
423
|
+
*/
|
|
424
|
+
function deduplicatePhrases(phrases) {
|
|
425
|
+
const best = new Map(); // normalized phrase → best entry
|
|
426
|
+
for (const p of phrases) {
|
|
427
|
+
const key = p.phrase.toLowerCase().trim();
|
|
428
|
+
const existing = best.get(key);
|
|
429
|
+
if (!existing || p.score > existing.score) {
|
|
430
|
+
best.set(key, p);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return [...best.values()];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// Combined Ranking
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Run both ranking streams and return unified result.
|
|
442
|
+
*
|
|
443
|
+
* @param {Array} minedTerms - from vocab-miner.js
|
|
444
|
+
* @param {Array} communityPhrases - from vocab-miner.js
|
|
445
|
+
* @param {Map<string, number>} pageRankScores - from repo-map.js
|
|
446
|
+
* @param {object} [options]
|
|
447
|
+
* @param {'light'|'medium'|'deep'} [options.depth='medium']
|
|
448
|
+
* @param {number} [options.topN] - explicit identifier cap
|
|
449
|
+
* @param {number} [options.maxPhrasesPerCommunity]
|
|
450
|
+
* @param {number} [options.maxTotalPhrases]
|
|
451
|
+
* @param {number} [options.totalFiles]
|
|
452
|
+
* @returns {{ identifiers: Array, phrases: Array }}
|
|
453
|
+
*/
|
|
454
|
+
export function rankAll(minedTerms, communityPhrases, pageRankScores, options = {}) {
|
|
455
|
+
const { identifiers, totalMined: idTotal } = rankIdentifiers(
|
|
456
|
+
minedTerms, pageRankScores, options
|
|
457
|
+
);
|
|
458
|
+
const { phrases, totalMined: phTotal } = rankCommunityPhrases(
|
|
459
|
+
communityPhrases, pageRankScores, options
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
identifiers,
|
|
464
|
+
phrases,
|
|
465
|
+
stats: {
|
|
466
|
+
totalIdentifiersMined: idTotal,
|
|
467
|
+
totalPhrasesMined: phTotal,
|
|
468
|
+
identifiersReturned: identifiers.length,
|
|
469
|
+
phrasesReturned: phrases.length,
|
|
470
|
+
depth: options.depth || 'medium',
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
// Exports (for testing internals)
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
export const _internals = {
|
|
480
|
+
computeIDF,
|
|
481
|
+
bm25Score,
|
|
482
|
+
computeHeuristicMultiplier,
|
|
483
|
+
deduplicatePhrases,
|
|
484
|
+
estimateTotalSourceBuckets,
|
|
485
|
+
// Backward compatibility export name
|
|
486
|
+
estimateTotalFiles: estimateTotalSourceBuckets,
|
|
487
|
+
MULTIPLIERS,
|
|
488
|
+
DEPTH_CUTOFFS,
|
|
489
|
+
QUESTION_TEMPLATES,
|
|
490
|
+
K1,
|
|
491
|
+
B,
|
|
492
|
+
};
|