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.
Files changed (131) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +292 -87
  3. package/dist/analyzers/angular/index.d.ts +1 -1
  4. package/dist/analyzers/angular/index.d.ts.map +1 -1
  5. package/dist/analyzers/angular/index.js +298 -309
  6. package/dist/analyzers/angular/index.js.map +1 -1
  7. package/dist/analyzers/generic/index.d.ts +1 -2
  8. package/dist/analyzers/generic/index.d.ts.map +1 -1
  9. package/dist/analyzers/generic/index.js +93 -60
  10. package/dist/analyzers/generic/index.js.map +1 -1
  11. package/dist/constants/codebase-context.d.ts +8 -0
  12. package/dist/constants/codebase-context.d.ts.map +1 -0
  13. package/dist/constants/codebase-context.js +10 -0
  14. package/dist/constants/codebase-context.js.map +1 -0
  15. package/dist/constants/git-patterns.d.ts +12 -0
  16. package/dist/constants/git-patterns.d.ts.map +1 -0
  17. package/dist/constants/git-patterns.js +11 -0
  18. package/dist/constants/git-patterns.js.map +1 -0
  19. package/dist/core/analyzer-registry.d.ts.map +1 -1
  20. package/dist/core/analyzer-registry.js +8 -8
  21. package/dist/core/analyzer-registry.js.map +1 -1
  22. package/dist/core/indexer.d.ts +11 -1
  23. package/dist/core/indexer.d.ts.map +1 -1
  24. package/dist/core/indexer.js +359 -157
  25. package/dist/core/indexer.js.map +1 -1
  26. package/dist/core/manifest.d.ts +39 -0
  27. package/dist/core/manifest.d.ts.map +1 -0
  28. package/dist/core/manifest.js +86 -0
  29. package/dist/core/manifest.js.map +1 -0
  30. package/dist/core/search-quality.d.ts +10 -0
  31. package/dist/core/search-quality.d.ts.map +1 -0
  32. package/dist/core/search-quality.js +64 -0
  33. package/dist/core/search-quality.js.map +1 -0
  34. package/dist/core/search.d.ts +17 -1
  35. package/dist/core/search.d.ts.map +1 -1
  36. package/dist/core/search.js +303 -104
  37. package/dist/core/search.js.map +1 -1
  38. package/dist/embeddings/openai.d.ts.map +1 -1
  39. package/dist/embeddings/openai.js +2 -2
  40. package/dist/embeddings/openai.js.map +1 -1
  41. package/dist/embeddings/transformers.d.ts +1 -1
  42. package/dist/embeddings/transformers.d.ts.map +1 -1
  43. package/dist/embeddings/transformers.js +19 -15
  44. package/dist/embeddings/transformers.js.map +1 -1
  45. package/dist/embeddings/types.d.ts +1 -1
  46. package/dist/embeddings/types.d.ts.map +1 -1
  47. package/dist/embeddings/types.js +3 -3
  48. package/dist/embeddings/types.js.map +1 -1
  49. package/dist/errors/index.d.ts +8 -0
  50. package/dist/errors/index.d.ts.map +1 -0
  51. package/dist/errors/index.js +11 -0
  52. package/dist/errors/index.js.map +1 -0
  53. package/dist/index.d.ts +7 -29
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1125 -362
  56. package/dist/index.js.map +1 -1
  57. package/dist/lib.d.ts +18 -18
  58. package/dist/lib.d.ts.map +1 -1
  59. package/dist/lib.js +23 -23
  60. package/dist/lib.js.map +1 -1
  61. package/dist/memory/git-memory.d.ts +9 -0
  62. package/dist/memory/git-memory.d.ts.map +1 -0
  63. package/dist/memory/git-memory.js +51 -0
  64. package/dist/memory/git-memory.js.map +1 -0
  65. package/dist/memory/store.d.ts +38 -0
  66. package/dist/memory/store.d.ts.map +1 -0
  67. package/dist/memory/store.js +136 -0
  68. package/dist/memory/store.js.map +1 -0
  69. package/dist/patterns/semantics.d.ts +4 -0
  70. package/dist/patterns/semantics.d.ts.map +1 -0
  71. package/dist/patterns/semantics.js +24 -0
  72. package/dist/patterns/semantics.js.map +1 -0
  73. package/dist/preflight/evidence-lock.d.ts +50 -0
  74. package/dist/preflight/evidence-lock.d.ts.map +1 -0
  75. package/dist/preflight/evidence-lock.js +130 -0
  76. package/dist/preflight/evidence-lock.js.map +1 -0
  77. package/dist/preflight/query-scope.d.ts +3 -0
  78. package/dist/preflight/query-scope.d.ts.map +1 -0
  79. package/dist/preflight/query-scope.js +40 -0
  80. package/dist/preflight/query-scope.js.map +1 -0
  81. package/dist/resources/uri.d.ts +5 -0
  82. package/dist/resources/uri.d.ts.map +1 -0
  83. package/dist/resources/uri.js +15 -0
  84. package/dist/resources/uri.js.map +1 -0
  85. package/dist/storage/lancedb.d.ts +1 -0
  86. package/dist/storage/lancedb.d.ts.map +1 -1
  87. package/dist/storage/lancedb.js +51 -34
  88. package/dist/storage/lancedb.js.map +1 -1
  89. package/dist/storage/types.d.ts +5 -0
  90. package/dist/storage/types.d.ts.map +1 -1
  91. package/dist/storage/types.js +2 -1
  92. package/dist/storage/types.js.map +1 -1
  93. package/dist/types/index.d.ts +47 -0
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/types/index.js +1 -0
  96. package/dist/types/index.js.map +1 -1
  97. package/dist/utils/chunking.d.ts.map +1 -1
  98. package/dist/utils/chunking.js +10 -9
  99. package/dist/utils/chunking.js.map +1 -1
  100. package/dist/utils/dependency-detection.d.ts +18 -0
  101. package/dist/utils/dependency-detection.d.ts.map +1 -0
  102. package/dist/utils/dependency-detection.js +102 -0
  103. package/dist/utils/dependency-detection.js.map +1 -0
  104. package/dist/utils/git-dates.d.ts +1 -0
  105. package/dist/utils/git-dates.d.ts.map +1 -1
  106. package/dist/utils/git-dates.js +23 -3
  107. package/dist/utils/git-dates.js.map +1 -1
  108. package/dist/utils/language-detection.d.ts.map +1 -1
  109. package/dist/utils/language-detection.js +69 -17
  110. package/dist/utils/language-detection.js.map +1 -1
  111. package/dist/utils/usage-tracker.d.ts +2 -2
  112. package/dist/utils/usage-tracker.d.ts.map +1 -1
  113. package/dist/utils/usage-tracker.js +67 -38
  114. package/dist/utils/usage-tracker.js.map +1 -1
  115. package/dist/utils/workspace-detection.d.ts +32 -0
  116. package/dist/utils/workspace-detection.d.ts.map +1 -0
  117. package/dist/utils/workspace-detection.js +107 -0
  118. package/dist/utils/workspace-detection.js.map +1 -0
  119. package/package.json +122 -97
  120. package/dist/core/file-watcher.d.ts +0 -63
  121. package/dist/core/file-watcher.d.ts.map +0 -1
  122. package/dist/core/file-watcher.js +0 -210
  123. package/dist/core/file-watcher.js.map +0 -1
  124. package/dist/utils/logger.d.ts +0 -36
  125. package/dist/utils/logger.d.ts.map +0 -1
  126. package/dist/utils/logger.js +0 -111
  127. package/dist/utils/logger.js.map +0 -1
  128. package/dist/utils/pattern-detector.d.ts +0 -41
  129. package/dist/utils/pattern-detector.d.ts.map +0 -1
  130. package/dist/utils/pattern-detector.js +0 -101
  131. package/dist/utils/pattern-detector.js.map +0 -1
@@ -1,18 +1,72 @@
1
1
  /**
2
2
  * Hybrid search combining semantic vector search with keyword matching
3
3
  */
4
- import Fuse from "fuse.js";
5
- import path from "path";
6
- import { promises as fs } from "fs";
7
- import { getEmbeddingProvider, } from "../embeddings/index.js";
8
- import { getStorageProvider } from "../storage/index.js";
9
- import { analyzerRegistry } from "./analyzer-registry.js";
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, ".codebase-index");
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
- console.warn("Partial initialization (keyword search only):", error);
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, ".codebase-index.json");
50
- const content = await fs.readFile(indexPath, "utf-8");
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: "content", weight: 0.4 },
55
- { name: "metadata.componentName", weight: 0.25 },
56
- { name: "filePath", weight: 0.15 },
57
- { name: "relativePath", weight: 0.15 },
58
- { name: "componentType", weight: 0.15 },
59
- { name: "layer", weight: 0.1 },
60
- { name: "tags", weight: 0.15 },
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("Keyword index load failed:", error);
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, ".codebase-intelligence.json");
80
- const content = await fs.readFile(intelligencePath, "utf-8");
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 [category, data] of Object.entries(intelligence.patterns)) {
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("Pattern intelligence load failed (will proceed without trend detection):", error);
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
- async search(query, limit = 5, filters, options = DEFAULT_SEARCH_OPTIONS) {
142
- if (!this.initialized) {
143
- await this.initialize();
144
- }
145
- const { useSemanticSearch, useKeywordSearch, semanticWeight, keywordWeight, } = {
146
- ...DEFAULT_SEARCH_OPTIONS,
147
- ...options,
148
- };
149
- const results = new Map();
150
- if (useSemanticSearch && this.embeddingProvider && this.storageProvider) {
151
- try {
152
- const vectorResults = await this.semanticSearch(query, limit * 2, filters);
153
- vectorResults.forEach((result) => {
154
- const id = result.chunk.id;
155
- const existing = results.get(id);
156
- if (existing) {
157
- existing.scores.push(result.score * (semanticWeight || 0.7));
158
- }
159
- else {
160
- results.set(id, {
161
- chunk: result.chunk,
162
- scores: [result.score * (semanticWeight || 0.7)],
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
- if (useKeywordSearch && this.fuseIndex) {
172
- try {
173
- const keywordResults = await this.keywordSearch(query, limit * 2, filters);
174
- keywordResults.forEach((result) => {
175
- const id = result.chunk.id;
176
- const existing = results.get(id);
177
- if (existing) {
178
- existing.scores.push(result.score * (keywordWeight || 0.3));
179
- }
180
- else {
181
- results.set(id, {
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
- const combinedResults = Array.from(results.entries())
193
- .map(([id, { chunk, scores }]) => {
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
- // Boost scores for Angular components with proper detection
200
- if (chunk.componentType && chunk.framework === "angular") {
201
- combinedScore = Math.min(1.0, combinedScore * 1.3);
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 !== "unknown") {
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.90; // -10% for legacy patterns
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
- return combinedResults;
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("Analyzer summary failed:", error);
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: "TypeScript",
277
- js: "JavaScript",
278
- html: "HTML template",
279
- scss: "SCSS styles",
280
- css: "CSS styles",
281
- json: "JSON config",
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 = 100) {
286
- const lines = content.split("\n");
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("\n");
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$/, "") === queryLower.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("; ") : "content match";
575
+ return reasons.length > 0 ? reasons.join('; ') : 'content match';
377
576
  }
378
577
  async getChunkCount() {
379
578
  if (this.storageProvider) {