codebase-context 1.2.2 → 1.5.1
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 +21 -21
- package/README.md +292 -87
- package/dist/analyzers/angular/index.d.ts +1 -1
- package/dist/analyzers/angular/index.d.ts.map +1 -1
- package/dist/analyzers/angular/index.js +298 -309
- package/dist/analyzers/angular/index.js.map +1 -1
- package/dist/analyzers/generic/index.d.ts +1 -2
- package/dist/analyzers/generic/index.d.ts.map +1 -1
- package/dist/analyzers/generic/index.js +93 -60
- package/dist/analyzers/generic/index.js.map +1 -1
- package/dist/constants/codebase-context.d.ts +8 -0
- package/dist/constants/codebase-context.d.ts.map +1 -0
- package/dist/constants/codebase-context.js +10 -0
- package/dist/constants/codebase-context.js.map +1 -0
- package/dist/constants/git-patterns.d.ts +12 -0
- package/dist/constants/git-patterns.d.ts.map +1 -0
- package/dist/constants/git-patterns.js +11 -0
- package/dist/constants/git-patterns.js.map +1 -0
- package/dist/core/analyzer-registry.d.ts.map +1 -1
- package/dist/core/analyzer-registry.js +8 -8
- package/dist/core/analyzer-registry.js.map +1 -1
- package/dist/core/indexer.d.ts +11 -1
- package/dist/core/indexer.d.ts.map +1 -1
- package/dist/core/indexer.js +359 -157
- package/dist/core/indexer.js.map +1 -1
- package/dist/core/manifest.d.ts +39 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +86 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/core/search-quality.d.ts +10 -0
- package/dist/core/search-quality.d.ts.map +1 -0
- package/dist/core/search-quality.js +64 -0
- package/dist/core/search-quality.js.map +1 -0
- package/dist/core/search.d.ts +17 -1
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/search.js +303 -104
- package/dist/core/search.js.map +1 -1
- package/dist/embeddings/openai.d.ts.map +1 -1
- package/dist/embeddings/openai.js +2 -2
- package/dist/embeddings/openai.js.map +1 -1
- package/dist/embeddings/transformers.d.ts +1 -1
- package/dist/embeddings/transformers.d.ts.map +1 -1
- package/dist/embeddings/transformers.js +19 -15
- package/dist/embeddings/transformers.js.map +1 -1
- package/dist/embeddings/types.d.ts +1 -1
- package/dist/embeddings/types.d.ts.map +1 -1
- package/dist/embeddings/types.js +3 -3
- package/dist/embeddings/types.js.map +1 -1
- package/dist/errors/index.d.ts +8 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +11 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +7 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1125 -362
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +18 -18
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +23 -23
- package/dist/lib.js.map +1 -1
- package/dist/memory/git-memory.d.ts +9 -0
- package/dist/memory/git-memory.d.ts.map +1 -0
- package/dist/memory/git-memory.js +51 -0
- package/dist/memory/git-memory.js.map +1 -0
- package/dist/memory/store.d.ts +38 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +136 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/patterns/semantics.d.ts +4 -0
- package/dist/patterns/semantics.d.ts.map +1 -0
- package/dist/patterns/semantics.js +24 -0
- package/dist/patterns/semantics.js.map +1 -0
- package/dist/preflight/evidence-lock.d.ts +50 -0
- package/dist/preflight/evidence-lock.d.ts.map +1 -0
- package/dist/preflight/evidence-lock.js +130 -0
- package/dist/preflight/evidence-lock.js.map +1 -0
- package/dist/preflight/query-scope.d.ts +3 -0
- package/dist/preflight/query-scope.d.ts.map +1 -0
- package/dist/preflight/query-scope.js +40 -0
- package/dist/preflight/query-scope.js.map +1 -0
- package/dist/resources/uri.d.ts +5 -0
- package/dist/resources/uri.d.ts.map +1 -0
- package/dist/resources/uri.js +15 -0
- package/dist/resources/uri.js.map +1 -0
- package/dist/storage/lancedb.d.ts +1 -0
- package/dist/storage/lancedb.d.ts.map +1 -1
- package/dist/storage/lancedb.js +51 -34
- package/dist/storage/lancedb.js.map +1 -1
- package/dist/storage/types.d.ts +5 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +2 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/chunking.d.ts.map +1 -1
- package/dist/utils/chunking.js +10 -9
- package/dist/utils/chunking.js.map +1 -1
- package/dist/utils/dependency-detection.d.ts +18 -0
- package/dist/utils/dependency-detection.d.ts.map +1 -0
- package/dist/utils/dependency-detection.js +102 -0
- package/dist/utils/dependency-detection.js.map +1 -0
- package/dist/utils/git-dates.d.ts +1 -0
- package/dist/utils/git-dates.d.ts.map +1 -1
- package/dist/utils/git-dates.js +23 -3
- package/dist/utils/git-dates.js.map +1 -1
- package/dist/utils/language-detection.d.ts.map +1 -1
- package/dist/utils/language-detection.js +69 -17
- package/dist/utils/language-detection.js.map +1 -1
- package/dist/utils/usage-tracker.d.ts +2 -2
- package/dist/utils/usage-tracker.d.ts.map +1 -1
- package/dist/utils/usage-tracker.js +67 -38
- package/dist/utils/usage-tracker.js.map +1 -1
- package/dist/utils/workspace-detection.d.ts +32 -0
- package/dist/utils/workspace-detection.d.ts.map +1 -0
- package/dist/utils/workspace-detection.js +107 -0
- package/dist/utils/workspace-detection.js.map +1 -0
- package/package.json +122 -97
- package/dist/core/file-watcher.d.ts +0 -63
- package/dist/core/file-watcher.d.ts.map +0 -1
- package/dist/core/file-watcher.js +0 -210
- package/dist/core/file-watcher.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -36
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -111
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/pattern-detector.d.ts +0 -41
- package/dist/utils/pattern-detector.d.ts.map +0 -1
- package/dist/utils/pattern-detector.js +0 -101
- package/dist/utils/pattern-detector.js.map +0 -1
package/dist/core/search.js
CHANGED
|
@@ -1,18 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hybrid search combining semantic vector search with keyword matching
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
+
import Fuse from 'fuse.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import { getEmbeddingProvider } from '../embeddings/index.js';
|
|
9
|
+
import { getStorageProvider } from '../storage/index.js';
|
|
10
|
+
import { analyzerRegistry } from './analyzer-registry.js';
|
|
11
|
+
import { IndexCorruptedError } from '../errors/index.js';
|
|
12
|
+
import { isTestingRelatedQuery } from '../preflight/query-scope.js';
|
|
13
|
+
import { assessSearchQuality } from './search-quality.js';
|
|
14
|
+
import { CODEBASE_CONTEXT_DIRNAME, INTELLIGENCE_FILENAME, KEYWORD_INDEX_FILENAME, VECTOR_DB_DIRNAME } from '../constants/codebase-context.js';
|
|
10
15
|
const DEFAULT_SEARCH_OPTIONS = {
|
|
11
16
|
useSemanticSearch: true,
|
|
12
17
|
useKeywordSearch: true,
|
|
13
18
|
semanticWeight: 0.7,
|
|
14
19
|
keywordWeight: 0.3,
|
|
20
|
+
profile: 'explore',
|
|
21
|
+
enableQueryExpansion: true,
|
|
22
|
+
enableLowConfidenceRescue: true,
|
|
23
|
+
candidateFloor: 30
|
|
15
24
|
};
|
|
25
|
+
const QUERY_EXPANSION_HINTS = [
|
|
26
|
+
{
|
|
27
|
+
pattern: /\b(auth|authentication|login|signin|sign-in|session|token|oauth)\b/i,
|
|
28
|
+
terms: ['auth', 'login', 'token', 'session', 'guard', 'oauth']
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /\b(route|routes|routing|router|navigate|navigation|redirect|path)\b/i,
|
|
32
|
+
terms: ['router', 'route', 'navigation', 'redirect', 'path']
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /\b(config|configuration|configure|setup|register|provider|providers|bootstrap)\b/i,
|
|
36
|
+
terms: ['config', 'setup', 'register', 'provider', 'bootstrap']
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /\b(role|roles|permission|permissions|authorization|authorisation|access)\b/i,
|
|
40
|
+
terms: ['roles', 'permissions', 'access', 'policy', 'guard']
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /\b(interceptor|middleware|request|response|http)\b/i,
|
|
44
|
+
terms: ['interceptor', 'middleware', 'http', 'request', 'response']
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /\b(theme|styles?|styling|palette|color|branding|upload)\b/i,
|
|
48
|
+
terms: ['theme', 'styles', 'palette', 'color', 'branding', 'upload']
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
const QUERY_STOP_WORDS = new Set([
|
|
52
|
+
'the',
|
|
53
|
+
'a',
|
|
54
|
+
'an',
|
|
55
|
+
'to',
|
|
56
|
+
'of',
|
|
57
|
+
'for',
|
|
58
|
+
'and',
|
|
59
|
+
'or',
|
|
60
|
+
'with',
|
|
61
|
+
'in',
|
|
62
|
+
'on',
|
|
63
|
+
'by',
|
|
64
|
+
'how',
|
|
65
|
+
'are',
|
|
66
|
+
'is',
|
|
67
|
+
'after',
|
|
68
|
+
'before'
|
|
69
|
+
]);
|
|
16
70
|
export class CodebaseSearcher {
|
|
17
71
|
rootPath;
|
|
18
72
|
storagePath;
|
|
@@ -25,7 +79,7 @@ export class CodebaseSearcher {
|
|
|
25
79
|
patternIntelligence = null;
|
|
26
80
|
constructor(rootPath) {
|
|
27
81
|
this.rootPath = rootPath;
|
|
28
|
-
this.storagePath = path.join(rootPath,
|
|
82
|
+
this.storagePath = path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, VECTOR_DB_DIRNAME);
|
|
29
83
|
}
|
|
30
84
|
async initialize() {
|
|
31
85
|
if (this.initialized)
|
|
@@ -35,38 +89,41 @@ export class CodebaseSearcher {
|
|
|
35
89
|
await this.loadPatternIntelligence();
|
|
36
90
|
this.embeddingProvider = await getEmbeddingProvider();
|
|
37
91
|
this.storageProvider = await getStorageProvider({
|
|
38
|
-
path: this.storagePath
|
|
92
|
+
path: this.storagePath
|
|
39
93
|
});
|
|
40
94
|
this.initialized = true;
|
|
41
95
|
}
|
|
42
96
|
catch (error) {
|
|
43
|
-
|
|
97
|
+
if (error instanceof IndexCorruptedError) {
|
|
98
|
+
throw error; // Propagate to handler for auto-heal
|
|
99
|
+
}
|
|
100
|
+
console.warn('Partial initialization (keyword search only):', error);
|
|
44
101
|
this.initialized = true;
|
|
45
102
|
}
|
|
46
103
|
}
|
|
47
104
|
async loadKeywordIndex() {
|
|
48
105
|
try {
|
|
49
|
-
const indexPath = path.join(this.rootPath,
|
|
50
|
-
const content = await fs.readFile(indexPath,
|
|
106
|
+
const indexPath = path.join(this.rootPath, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME);
|
|
107
|
+
const content = await fs.readFile(indexPath, 'utf-8');
|
|
51
108
|
this.chunks = JSON.parse(content);
|
|
52
109
|
this.fuseIndex = new Fuse(this.chunks, {
|
|
53
110
|
keys: [
|
|
54
|
-
{ name:
|
|
55
|
-
{ name:
|
|
56
|
-
{ name:
|
|
57
|
-
{ name:
|
|
58
|
-
{ name:
|
|
59
|
-
{ name:
|
|
60
|
-
{ name:
|
|
111
|
+
{ name: 'content', weight: 0.4 },
|
|
112
|
+
{ name: 'metadata.componentName', weight: 0.25 },
|
|
113
|
+
{ name: 'filePath', weight: 0.15 },
|
|
114
|
+
{ name: 'relativePath', weight: 0.15 },
|
|
115
|
+
{ name: 'componentType', weight: 0.15 },
|
|
116
|
+
{ name: 'layer', weight: 0.1 },
|
|
117
|
+
{ name: 'tags', weight: 0.15 }
|
|
61
118
|
],
|
|
62
119
|
includeScore: true,
|
|
63
120
|
threshold: 0.4,
|
|
64
121
|
useExtendedSearch: true,
|
|
65
|
-
ignoreLocation: true
|
|
122
|
+
ignoreLocation: true
|
|
66
123
|
});
|
|
67
124
|
}
|
|
68
125
|
catch (error) {
|
|
69
|
-
console.warn(
|
|
126
|
+
console.warn('Keyword index load failed:', error);
|
|
70
127
|
this.chunks = [];
|
|
71
128
|
this.fuseIndex = null;
|
|
72
129
|
}
|
|
@@ -76,15 +133,15 @@ export class CodebaseSearcher {
|
|
|
76
133
|
*/
|
|
77
134
|
async loadPatternIntelligence() {
|
|
78
135
|
try {
|
|
79
|
-
const intelligencePath = path.join(this.rootPath,
|
|
80
|
-
const content = await fs.readFile(intelligencePath,
|
|
136
|
+
const intelligencePath = path.join(this.rootPath, CODEBASE_CONTEXT_DIRNAME, INTELLIGENCE_FILENAME);
|
|
137
|
+
const content = await fs.readFile(intelligencePath, 'utf-8');
|
|
81
138
|
const intelligence = JSON.parse(content);
|
|
82
139
|
const decliningPatterns = new Set();
|
|
83
140
|
const risingPatterns = new Set();
|
|
84
141
|
const patternWarnings = new Map();
|
|
85
142
|
// Extract pattern indicators from intelligence data
|
|
86
143
|
if (intelligence.patterns) {
|
|
87
|
-
for (const [
|
|
144
|
+
for (const [_category, data] of Object.entries(intelligence.patterns)) {
|
|
88
145
|
const patternData = data;
|
|
89
146
|
// Track primary pattern
|
|
90
147
|
if (patternData.primary?.trend === 'Rising') {
|
|
@@ -108,7 +165,7 @@ export class CodebaseSearcher {
|
|
|
108
165
|
console.error(`[search] Loaded pattern intelligence: ${decliningPatterns.size} declining, ${risingPatterns.size} rising patterns`);
|
|
109
166
|
}
|
|
110
167
|
catch (error) {
|
|
111
|
-
console.warn(
|
|
168
|
+
console.warn('Pattern intelligence load failed (will proceed without trend detection):', error);
|
|
112
169
|
this.patternIntelligence = null;
|
|
113
170
|
}
|
|
114
171
|
}
|
|
@@ -138,79 +195,129 @@ export class CodebaseSearcher {
|
|
|
138
195
|
}
|
|
139
196
|
return { trend: 'Stable' };
|
|
140
197
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
catch (error) {
|
|
168
|
-
console.warn("Semantic search failed:", error);
|
|
198
|
+
isTestFile(filePath) {
|
|
199
|
+
const normalized = filePath.toLowerCase().replace(/\\/g, '/');
|
|
200
|
+
return (normalized.includes('.spec.') ||
|
|
201
|
+
normalized.includes('.test.') ||
|
|
202
|
+
normalized.includes('/e2e/') ||
|
|
203
|
+
normalized.includes('/__tests__/'));
|
|
204
|
+
}
|
|
205
|
+
normalizeQueryTerms(query) {
|
|
206
|
+
return query
|
|
207
|
+
.toLowerCase()
|
|
208
|
+
.split(/[^a-z0-9_]+/)
|
|
209
|
+
.filter((term) => term.length > 2 && !QUERY_STOP_WORDS.has(term));
|
|
210
|
+
}
|
|
211
|
+
buildQueryVariants(query, maxExpansions) {
|
|
212
|
+
const variants = [{ query, weight: 1 }];
|
|
213
|
+
if (maxExpansions <= 0)
|
|
214
|
+
return variants;
|
|
215
|
+
const normalized = query.toLowerCase();
|
|
216
|
+
const terms = new Set(this.normalizeQueryTerms(query));
|
|
217
|
+
for (const hint of QUERY_EXPANSION_HINTS) {
|
|
218
|
+
if (!hint.pattern.test(query))
|
|
219
|
+
continue;
|
|
220
|
+
for (const term of hint.terms) {
|
|
221
|
+
if (!normalized.includes(term)) {
|
|
222
|
+
terms.add(term);
|
|
223
|
+
}
|
|
169
224
|
}
|
|
170
225
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
chunk: result.chunk,
|
|
183
|
-
scores: [result.score * (keywordWeight || 0.3)],
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
console.warn("Keyword search failed:", error);
|
|
226
|
+
const addedTerms = Array.from(terms).filter((term) => !normalized.includes(term));
|
|
227
|
+
if (addedTerms.length === 0)
|
|
228
|
+
return variants;
|
|
229
|
+
const firstExpansion = `${query} ${addedTerms.slice(0, 6).join(' ')}`.trim();
|
|
230
|
+
if (firstExpansion !== query) {
|
|
231
|
+
variants.push({ query: firstExpansion, weight: 0.35 });
|
|
232
|
+
}
|
|
233
|
+
if (maxExpansions > 1 && addedTerms.length > 6) {
|
|
234
|
+
const secondExpansion = `${query} ${addedTerms.slice(6, 12).join(' ')}`.trim();
|
|
235
|
+
if (secondExpansion !== query) {
|
|
236
|
+
variants.push({ query: secondExpansion, weight: 0.25 });
|
|
190
237
|
}
|
|
191
238
|
}
|
|
192
|
-
|
|
193
|
-
|
|
239
|
+
return variants.slice(0, 1 + maxExpansions);
|
|
240
|
+
}
|
|
241
|
+
isCompositionRootFile(filePath) {
|
|
242
|
+
const normalized = filePath.toLowerCase().replace(/\\/g, '/');
|
|
243
|
+
const base = path.basename(normalized);
|
|
244
|
+
if (/^(main|index|bootstrap|startup)\./.test(base))
|
|
245
|
+
return true;
|
|
246
|
+
return (normalized.includes('/routes') ||
|
|
247
|
+
normalized.includes('/routing') ||
|
|
248
|
+
normalized.includes('/router') ||
|
|
249
|
+
normalized.includes('/config') ||
|
|
250
|
+
normalized.includes('/providers'));
|
|
251
|
+
}
|
|
252
|
+
queryPathTokenOverlap(filePath, query) {
|
|
253
|
+
const queryTerms = new Set(this.normalizeQueryTerms(query));
|
|
254
|
+
if (queryTerms.size === 0)
|
|
255
|
+
return 0;
|
|
256
|
+
const pathTerms = this.normalizeQueryTerms(filePath.replace(/\\/g, '/'));
|
|
257
|
+
return pathTerms.reduce((count, term) => (queryTerms.has(term) ? count + 1 : count), 0);
|
|
258
|
+
}
|
|
259
|
+
isLikelyWiringOrFlowQuery(query) {
|
|
260
|
+
return /\b(route|router|routing|navigate|navigation|redirect|auth|authentication|login|provider|register|config|configuration|interceptor|middleware)\b/i.test(query);
|
|
261
|
+
}
|
|
262
|
+
isActionOrHowQuery(query) {
|
|
263
|
+
return /\b(how|where|configure|configured|setup|register|wire|wiring|navigate|redirect|login|authenticate|copy|upload|handle|create|update|delete)\b/i.test(query);
|
|
264
|
+
}
|
|
265
|
+
isDefinitionHeavyResult(chunk) {
|
|
266
|
+
const normalizedPath = chunk.filePath.toLowerCase().replace(/\\/g, '/');
|
|
267
|
+
const componentType = (chunk.componentType || '').toLowerCase();
|
|
268
|
+
if (['type', 'interface', 'enum', 'constant'].includes(componentType))
|
|
269
|
+
return true;
|
|
270
|
+
return (normalizedPath.includes('/models/') ||
|
|
271
|
+
normalizedPath.includes('/interfaces/') ||
|
|
272
|
+
normalizedPath.includes('/types/') ||
|
|
273
|
+
normalizedPath.includes('/constants'));
|
|
274
|
+
}
|
|
275
|
+
scoreAndSortResults(query, limit, results, profile) {
|
|
276
|
+
const likelyWiringQuery = this.isLikelyWiringOrFlowQuery(query);
|
|
277
|
+
const actionQuery = this.isActionOrHowQuery(query);
|
|
278
|
+
return Array.from(results.entries())
|
|
279
|
+
.map(([_id, { chunk, scores }]) => {
|
|
194
280
|
// Calculate base combined score
|
|
195
281
|
let combinedScore = scores.reduce((sum, score) => sum + score, 0);
|
|
196
282
|
// Normalize to 0-1 range (scores are already weighted)
|
|
197
283
|
// If both semantic and keyword matched, max possible is ~1.0
|
|
198
284
|
combinedScore = Math.min(1.0, combinedScore);
|
|
199
|
-
//
|
|
200
|
-
if (chunk.componentType && chunk.
|
|
201
|
-
combinedScore = Math.min(1.0, combinedScore * 1.
|
|
285
|
+
// Slight boost when analyzer identified a concrete component type
|
|
286
|
+
if (chunk.componentType && chunk.componentType !== 'unknown') {
|
|
287
|
+
combinedScore = Math.min(1.0, combinedScore * 1.1);
|
|
202
288
|
}
|
|
203
289
|
// Boost if layer is detected
|
|
204
|
-
if (chunk.layer && chunk.layer !==
|
|
290
|
+
if (chunk.layer && chunk.layer !== 'unknown') {
|
|
205
291
|
combinedScore = Math.min(1.0, combinedScore * 1.1);
|
|
206
292
|
}
|
|
293
|
+
// Query-aware reranking to reduce noisy matches in practical workflows.
|
|
294
|
+
if (!isTestingRelatedQuery(query) && this.isTestFile(chunk.filePath)) {
|
|
295
|
+
combinedScore = combinedScore * 0.75;
|
|
296
|
+
}
|
|
297
|
+
if (actionQuery && this.isDefinitionHeavyResult(chunk)) {
|
|
298
|
+
combinedScore = combinedScore * 0.82;
|
|
299
|
+
}
|
|
300
|
+
if (actionQuery &&
|
|
301
|
+
['service', 'component', 'interceptor', 'guard', 'module', 'resolver'].includes((chunk.componentType || '').toLowerCase())) {
|
|
302
|
+
combinedScore = Math.min(1.0, combinedScore * 1.06);
|
|
303
|
+
}
|
|
304
|
+
// Light intent-aware boost for likely wiring/configuration queries.
|
|
305
|
+
if (likelyWiringQuery && profile !== 'explore') {
|
|
306
|
+
if (this.isCompositionRootFile(chunk.filePath)) {
|
|
307
|
+
combinedScore = Math.min(1.0, combinedScore * 1.12);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const pathOverlap = this.queryPathTokenOverlap(chunk.filePath, query);
|
|
311
|
+
if (pathOverlap >= 2) {
|
|
312
|
+
combinedScore = Math.min(1.0, combinedScore * 1.08);
|
|
313
|
+
}
|
|
207
314
|
// v1.2: Detect pattern trend and apply momentum boost
|
|
208
315
|
const { trend, warning } = this.detectChunkTrend(chunk);
|
|
209
316
|
if (trend === 'Rising') {
|
|
210
317
|
combinedScore = Math.min(1.0, combinedScore * 1.15); // +15% for modern patterns
|
|
211
318
|
}
|
|
212
319
|
else if (trend === 'Declining') {
|
|
213
|
-
combinedScore = combinedScore * 0.
|
|
320
|
+
combinedScore = combinedScore * 0.9; // -10% for legacy patterns
|
|
214
321
|
}
|
|
215
322
|
const summary = this.generateSummary(chunk);
|
|
216
323
|
const snippet = this.generateSnippet(chunk.content);
|
|
@@ -229,29 +336,122 @@ export class CodebaseSearcher {
|
|
|
229
336
|
metadata: chunk.metadata,
|
|
230
337
|
// v1.2: Pattern momentum awareness
|
|
231
338
|
trend,
|
|
232
|
-
patternWarning: warning
|
|
339
|
+
patternWarning: warning
|
|
233
340
|
};
|
|
234
341
|
})
|
|
235
342
|
.sort((a, b) => b.score - a.score)
|
|
236
343
|
.slice(0, limit);
|
|
237
|
-
|
|
344
|
+
}
|
|
345
|
+
pickBetterResultSet(query, primary, rescue) {
|
|
346
|
+
const primaryQuality = assessSearchQuality(query, primary);
|
|
347
|
+
const rescueQuality = assessSearchQuality(query, rescue);
|
|
348
|
+
if (rescueQuality.status === 'ok' &&
|
|
349
|
+
primaryQuality.status === 'low_confidence' &&
|
|
350
|
+
rescueQuality.confidence >= primaryQuality.confidence) {
|
|
351
|
+
return rescue;
|
|
352
|
+
}
|
|
353
|
+
if (rescueQuality.confidence >= primaryQuality.confidence + 0.05) {
|
|
354
|
+
return rescue;
|
|
355
|
+
}
|
|
356
|
+
return primary;
|
|
357
|
+
}
|
|
358
|
+
async collectHybridMatches(queryVariants, candidateLimit, filters, useSemanticSearch, useKeywordSearch, semanticWeight, keywordWeight) {
|
|
359
|
+
const results = new Map();
|
|
360
|
+
if (useSemanticSearch && this.embeddingProvider && this.storageProvider) {
|
|
361
|
+
try {
|
|
362
|
+
for (const variant of queryVariants) {
|
|
363
|
+
const vectorResults = await this.semanticSearch(variant.query, candidateLimit, filters);
|
|
364
|
+
vectorResults.forEach((result) => {
|
|
365
|
+
const id = result.chunk.id;
|
|
366
|
+
const weightedScore = result.score * semanticWeight * variant.weight;
|
|
367
|
+
const existing = results.get(id);
|
|
368
|
+
if (existing) {
|
|
369
|
+
existing.scores.push(weightedScore);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
results.set(id, {
|
|
373
|
+
chunk: result.chunk,
|
|
374
|
+
scores: [weightedScore]
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof IndexCorruptedError) {
|
|
382
|
+
throw error; // Propagate to handler for auto-heal
|
|
383
|
+
}
|
|
384
|
+
console.warn('Semantic search failed:', error);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (useKeywordSearch && this.fuseIndex) {
|
|
388
|
+
try {
|
|
389
|
+
for (const variant of queryVariants) {
|
|
390
|
+
const keywordResults = await this.keywordSearch(variant.query, candidateLimit, filters);
|
|
391
|
+
keywordResults.forEach((result) => {
|
|
392
|
+
const id = result.chunk.id;
|
|
393
|
+
const weightedScore = result.score * keywordWeight * variant.weight;
|
|
394
|
+
const existing = results.get(id);
|
|
395
|
+
if (existing) {
|
|
396
|
+
existing.scores.push(weightedScore);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
results.set(id, {
|
|
400
|
+
chunk: result.chunk,
|
|
401
|
+
scores: [weightedScore]
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
console.warn('Keyword search failed:', error);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return results;
|
|
412
|
+
}
|
|
413
|
+
async search(query, limit = 5, filters, options = DEFAULT_SEARCH_OPTIONS) {
|
|
414
|
+
if (!this.initialized) {
|
|
415
|
+
await this.initialize();
|
|
416
|
+
}
|
|
417
|
+
const { useSemanticSearch, useKeywordSearch, semanticWeight, keywordWeight, profile, enableQueryExpansion, enableLowConfidenceRescue, candidateFloor } = {
|
|
418
|
+
...DEFAULT_SEARCH_OPTIONS,
|
|
419
|
+
...options
|
|
420
|
+
};
|
|
421
|
+
const candidateLimit = Math.max(limit * 2, candidateFloor || 30);
|
|
422
|
+
const primaryVariants = this.buildQueryVariants(query, enableQueryExpansion ? 1 : 0);
|
|
423
|
+
const primaryMatches = await this.collectHybridMatches(primaryVariants, candidateLimit, filters, Boolean(useSemanticSearch), Boolean(useKeywordSearch), semanticWeight || 0.7, keywordWeight || 0.3);
|
|
424
|
+
const primaryResults = this.scoreAndSortResults(query, limit, primaryMatches, (profile || 'explore'));
|
|
425
|
+
if (!enableLowConfidenceRescue) {
|
|
426
|
+
return primaryResults;
|
|
427
|
+
}
|
|
428
|
+
const primaryQuality = assessSearchQuality(query, primaryResults);
|
|
429
|
+
if (primaryQuality.status !== 'low_confidence') {
|
|
430
|
+
return primaryResults;
|
|
431
|
+
}
|
|
432
|
+
const rescueVariants = this.buildQueryVariants(query, 2).slice(1);
|
|
433
|
+
if (rescueVariants.length === 0) {
|
|
434
|
+
return primaryResults;
|
|
435
|
+
}
|
|
436
|
+
const rescueMatches = await this.collectHybridMatches(rescueVariants.map((variant, index) => ({
|
|
437
|
+
query: variant.query,
|
|
438
|
+
weight: index === 0 ? 1 : 0.8
|
|
439
|
+
})), candidateLimit, filters, Boolean(useSemanticSearch), Boolean(useKeywordSearch), semanticWeight || 0.7, keywordWeight || 0.3);
|
|
440
|
+
const rescueResults = this.scoreAndSortResults(query, limit, rescueMatches, (profile || 'explore'));
|
|
441
|
+
return this.pickBetterResultSet(query, primaryResults, rescueResults);
|
|
238
442
|
}
|
|
239
443
|
generateSummary(chunk) {
|
|
240
|
-
const analyzer = chunk.framework
|
|
241
|
-
? analyzerRegistry.get(chunk.framework)
|
|
242
|
-
: null;
|
|
444
|
+
const analyzer = chunk.framework ? analyzerRegistry.get(chunk.framework) : null;
|
|
243
445
|
if (analyzer && analyzer.summarize) {
|
|
244
446
|
try {
|
|
245
447
|
const summary = analyzer.summarize(chunk);
|
|
246
448
|
// Only use analyzer summary if it's meaningful (not the generic fallback)
|
|
247
|
-
if (summary &&
|
|
248
|
-
!summary.startsWith("Code in ") &&
|
|
249
|
-
!summary.includes(": lines ")) {
|
|
449
|
+
if (summary && !summary.startsWith('Code in ') && !summary.includes(': lines ')) {
|
|
250
450
|
return summary;
|
|
251
451
|
}
|
|
252
452
|
}
|
|
253
453
|
catch (error) {
|
|
254
|
-
console.warn(
|
|
454
|
+
console.warn('Analyzer summary failed:', error);
|
|
255
455
|
}
|
|
256
456
|
}
|
|
257
457
|
// Enhanced generic summary
|
|
@@ -273,21 +473,21 @@ export class CodebaseSearcher {
|
|
|
273
473
|
// Last resort: describe the file type
|
|
274
474
|
const ext = path.extname(fileName).slice(1);
|
|
275
475
|
const langMap = {
|
|
276
|
-
ts:
|
|
277
|
-
js:
|
|
278
|
-
html:
|
|
279
|
-
scss:
|
|
280
|
-
css:
|
|
281
|
-
json:
|
|
476
|
+
ts: 'TypeScript',
|
|
477
|
+
js: 'JavaScript',
|
|
478
|
+
html: 'HTML template',
|
|
479
|
+
scss: 'SCSS styles',
|
|
480
|
+
css: 'CSS styles',
|
|
481
|
+
json: 'JSON config'
|
|
282
482
|
};
|
|
283
483
|
return `${langMap[ext] || ext.toUpperCase()} in ${fileName}.`;
|
|
284
484
|
}
|
|
285
|
-
generateSnippet(content, maxLines =
|
|
286
|
-
const lines = content.split(
|
|
485
|
+
generateSnippet(content, maxLines = 20) {
|
|
486
|
+
const lines = content.split('\n');
|
|
287
487
|
if (lines.length <= maxLines) {
|
|
288
488
|
return content;
|
|
289
489
|
}
|
|
290
|
-
const snippet = lines.slice(0, maxLines).join(
|
|
490
|
+
const snippet = lines.slice(0, maxLines).join('\n');
|
|
291
491
|
const remaining = lines.length - maxLines;
|
|
292
492
|
return `${snippet}\n\n... [${remaining} more lines]`;
|
|
293
493
|
}
|
|
@@ -299,7 +499,7 @@ export class CodebaseSearcher {
|
|
|
299
499
|
const results = await this.storageProvider.search(queryVector, limit, filters);
|
|
300
500
|
return results.map((r) => ({
|
|
301
501
|
chunk: r.chunk,
|
|
302
|
-
score: r.score
|
|
502
|
+
score: r.score
|
|
303
503
|
}));
|
|
304
504
|
}
|
|
305
505
|
async keywordSearch(query, limit, filters) {
|
|
@@ -310,8 +510,7 @@ export class CodebaseSearcher {
|
|
|
310
510
|
if (filters) {
|
|
311
511
|
fuseResults = fuseResults.filter((r) => {
|
|
312
512
|
const chunk = r.item;
|
|
313
|
-
if (filters.componentType &&
|
|
314
|
-
chunk.componentType !== filters.componentType) {
|
|
513
|
+
if (filters.componentType && chunk.componentType !== filters.componentType) {
|
|
315
514
|
return false;
|
|
316
515
|
}
|
|
317
516
|
if (filters.layer && chunk.layer !== filters.layer) {
|
|
@@ -339,14 +538,14 @@ export class CodebaseSearcher {
|
|
|
339
538
|
const queryLower = query.toLowerCase();
|
|
340
539
|
const fileName = path.basename(chunk.filePath).toLowerCase();
|
|
341
540
|
const relativePathLower = chunk.relativePath.toLowerCase();
|
|
342
|
-
const componentName = chunk.metadata?.componentName?.toLowerCase() ||
|
|
541
|
+
const componentName = chunk.metadata?.componentName?.toLowerCase() || '';
|
|
343
542
|
// Exact class name match
|
|
344
543
|
if (componentName && queryLower === componentName) {
|
|
345
544
|
score = Math.min(1.0, score + 0.3);
|
|
346
545
|
}
|
|
347
546
|
// Exact file name match
|
|
348
547
|
if (fileName === queryLower ||
|
|
349
|
-
fileName.replace(/\.ts$/,
|
|
548
|
+
fileName.replace(/\.ts$/, '') === queryLower.replace(/\.ts$/, '')) {
|
|
350
549
|
score = Math.min(1.0, score + 0.2);
|
|
351
550
|
}
|
|
352
551
|
// File path contains query
|
|
@@ -356,7 +555,7 @@ export class CodebaseSearcher {
|
|
|
356
555
|
}
|
|
357
556
|
return {
|
|
358
557
|
chunk,
|
|
359
|
-
score
|
|
558
|
+
score
|
|
360
559
|
};
|
|
361
560
|
});
|
|
362
561
|
}
|
|
@@ -371,9 +570,9 @@ export class CodebaseSearcher {
|
|
|
371
570
|
const queryLower = query.toLowerCase();
|
|
372
571
|
const matchingTags = (chunk.tags || []).filter((tag) => queryLower.includes(tag.toLowerCase()));
|
|
373
572
|
if (matchingTags.length > 0) {
|
|
374
|
-
reasons.push(`tags: ${matchingTags.join(
|
|
573
|
+
reasons.push(`tags: ${matchingTags.join(', ')}`);
|
|
375
574
|
}
|
|
376
|
-
return reasons.length > 0 ? reasons.join(
|
|
575
|
+
return reasons.length > 0 ? reasons.join('; ') : 'content match';
|
|
377
576
|
}
|
|
378
577
|
async getChunkCount() {
|
|
379
578
|
if (this.storageProvider) {
|