codebase-context 1.6.1 → 1.7.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.
Files changed (149) hide show
  1. package/README.md +142 -46
  2. package/dist/analyzers/angular/index.d.ts +4 -0
  3. package/dist/analyzers/angular/index.d.ts.map +1 -1
  4. package/dist/analyzers/angular/index.js +51 -10
  5. package/dist/analyzers/angular/index.js.map +1 -1
  6. package/dist/analyzers/generic/index.d.ts +1 -0
  7. package/dist/analyzers/generic/index.d.ts.map +1 -1
  8. package/dist/analyzers/generic/index.js +89 -10
  9. package/dist/analyzers/generic/index.js.map +1 -1
  10. package/dist/cli.d.ts +8 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +352 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/constants/codebase-context.d.ts +13 -0
  15. package/dist/constants/codebase-context.d.ts.map +1 -1
  16. package/dist/constants/codebase-context.js +13 -0
  17. package/dist/constants/codebase-context.js.map +1 -1
  18. package/dist/core/index-meta.d.ts +24 -0
  19. package/dist/core/index-meta.d.ts.map +1 -0
  20. package/dist/core/index-meta.js +204 -0
  21. package/dist/core/index-meta.js.map +1 -0
  22. package/dist/core/indexer.d.ts +0 -5
  23. package/dist/core/indexer.d.ts.map +1 -1
  24. package/dist/core/indexer.js +265 -60
  25. package/dist/core/indexer.js.map +1 -1
  26. package/dist/core/search.d.ts +1 -0
  27. package/dist/core/search.d.ts.map +1 -1
  28. package/dist/core/search.js +60 -5
  29. package/dist/core/search.js.map +1 -1
  30. package/dist/core/symbol-references.d.ts +21 -0
  31. package/dist/core/symbol-references.d.ts.map +1 -0
  32. package/dist/core/symbol-references.js +91 -0
  33. package/dist/core/symbol-references.js.map +1 -0
  34. package/dist/eval/harness.d.ts +5 -0
  35. package/dist/eval/harness.d.ts.map +1 -0
  36. package/dist/eval/harness.js +153 -0
  37. package/dist/eval/harness.js.map +1 -0
  38. package/dist/eval/types.d.ts +59 -0
  39. package/dist/eval/types.d.ts.map +1 -0
  40. package/dist/eval/types.js +2 -0
  41. package/dist/eval/types.js.map +1 -0
  42. package/dist/grammars/manifest.d.ts +26 -0
  43. package/dist/grammars/manifest.d.ts.map +1 -0
  44. package/dist/grammars/manifest.js +63 -0
  45. package/dist/grammars/manifest.js.map +1 -0
  46. package/dist/index.d.ts +12 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +146 -1231
  49. package/dist/index.js.map +1 -1
  50. package/dist/memory/store.d.ts +3 -0
  51. package/dist/memory/store.d.ts.map +1 -1
  52. package/dist/memory/store.js +9 -0
  53. package/dist/memory/store.js.map +1 -1
  54. package/dist/patterns/semantics.d.ts +6 -0
  55. package/dist/patterns/semantics.d.ts.map +1 -1
  56. package/dist/patterns/semantics.js +22 -6
  57. package/dist/patterns/semantics.js.map +1 -1
  58. package/dist/preflight/evidence-lock.d.ts +8 -0
  59. package/dist/preflight/evidence-lock.d.ts.map +1 -1
  60. package/dist/preflight/evidence-lock.js +49 -3
  61. package/dist/preflight/evidence-lock.js.map +1 -1
  62. package/dist/storage/lancedb.d.ts +9 -1
  63. package/dist/storage/lancedb.d.ts.map +1 -1
  64. package/dist/storage/lancedb.js +26 -8
  65. package/dist/storage/lancedb.js.map +1 -1
  66. package/dist/tools/detect-circular-dependencies.d.ts +5 -0
  67. package/dist/tools/detect-circular-dependencies.d.ts.map +1 -0
  68. package/dist/tools/detect-circular-dependencies.js +117 -0
  69. package/dist/tools/detect-circular-dependencies.js.map +1 -0
  70. package/dist/tools/get-codebase-metadata.d.ts +5 -0
  71. package/dist/tools/get-codebase-metadata.d.ts.map +1 -0
  72. package/dist/tools/get-codebase-metadata.js +53 -0
  73. package/dist/tools/get-codebase-metadata.js.map +1 -0
  74. package/dist/tools/get-component-usage.d.ts +5 -0
  75. package/dist/tools/get-component-usage.d.ts.map +1 -0
  76. package/dist/tools/get-component-usage.js +83 -0
  77. package/dist/tools/get-component-usage.js.map +1 -0
  78. package/dist/tools/get-indexing-status.d.ts +5 -0
  79. package/dist/tools/get-indexing-status.d.ts.map +1 -0
  80. package/dist/tools/get-indexing-status.js +44 -0
  81. package/dist/tools/get-indexing-status.js.map +1 -0
  82. package/dist/tools/get-memory.d.ts +5 -0
  83. package/dist/tools/get-memory.d.ts.map +1 -0
  84. package/dist/tools/get-memory.js +89 -0
  85. package/dist/tools/get-memory.js.map +1 -0
  86. package/dist/tools/get-style-guide.d.ts +5 -0
  87. package/dist/tools/get-style-guide.d.ts.map +1 -0
  88. package/dist/tools/get-style-guide.js +151 -0
  89. package/dist/tools/get-style-guide.js.map +1 -0
  90. package/dist/tools/get-symbol-references.d.ts +5 -0
  91. package/dist/tools/get-symbol-references.d.ts.map +1 -0
  92. package/dist/tools/get-symbol-references.js +70 -0
  93. package/dist/tools/get-symbol-references.js.map +1 -0
  94. package/dist/tools/get-team-patterns.d.ts +5 -0
  95. package/dist/tools/get-team-patterns.d.ts.map +1 -0
  96. package/dist/tools/get-team-patterns.js +131 -0
  97. package/dist/tools/get-team-patterns.js.map +1 -0
  98. package/dist/tools/index.d.ts +6 -0
  99. package/dist/tools/index.d.ts.map +1 -0
  100. package/dist/tools/index.js +41 -0
  101. package/dist/tools/index.js.map +1 -0
  102. package/dist/tools/refresh-index.d.ts +5 -0
  103. package/dist/tools/refresh-index.d.ts.map +1 -0
  104. package/dist/tools/refresh-index.js +40 -0
  105. package/dist/tools/refresh-index.js.map +1 -0
  106. package/dist/tools/remember.d.ts +5 -0
  107. package/dist/tools/remember.d.ts.map +1 -0
  108. package/dist/tools/remember.js +101 -0
  109. package/dist/tools/remember.js.map +1 -0
  110. package/dist/tools/search-codebase.d.ts +5 -0
  111. package/dist/tools/search-codebase.d.ts.map +1 -0
  112. package/dist/tools/search-codebase.js +638 -0
  113. package/dist/tools/search-codebase.js.map +1 -0
  114. package/dist/tools/types.d.ts +31 -0
  115. package/dist/tools/types.d.ts.map +1 -0
  116. package/dist/tools/types.js +2 -0
  117. package/dist/tools/types.js.map +1 -0
  118. package/dist/types/index.d.ts +8 -0
  119. package/dist/types/index.d.ts.map +1 -1
  120. package/dist/utils/ast-chunker.d.ts +71 -0
  121. package/dist/utils/ast-chunker.d.ts.map +1 -0
  122. package/dist/utils/ast-chunker.js +453 -0
  123. package/dist/utils/ast-chunker.js.map +1 -0
  124. package/dist/utils/chunking.d.ts.map +1 -1
  125. package/dist/utils/chunking.js +10 -3
  126. package/dist/utils/chunking.js.map +1 -1
  127. package/dist/utils/dependency-detection.d.ts +2 -1
  128. package/dist/utils/dependency-detection.d.ts.map +1 -1
  129. package/dist/utils/dependency-detection.js.map +1 -1
  130. package/dist/utils/language-detection.d.ts.map +1 -1
  131. package/dist/utils/language-detection.js +20 -0
  132. package/dist/utils/language-detection.js.map +1 -1
  133. package/dist/utils/tree-sitter.d.ts +17 -0
  134. package/dist/utils/tree-sitter.d.ts.map +1 -0
  135. package/dist/utils/tree-sitter.js +311 -0
  136. package/dist/utils/tree-sitter.js.map +1 -0
  137. package/docs/capabilities.md +131 -37
  138. package/grammars/.gitkeep +0 -0
  139. package/grammars/tree-sitter-c.wasm +0 -0
  140. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  141. package/grammars/tree-sitter-cpp.wasm +0 -0
  142. package/grammars/tree-sitter-go.wasm +0 -0
  143. package/grammars/tree-sitter-java.wasm +0 -0
  144. package/grammars/tree-sitter-javascript.wasm +0 -0
  145. package/grammars/tree-sitter-python.wasm +0 -0
  146. package/grammars/tree-sitter-rust.wasm +0 -0
  147. package/grammars/tree-sitter-tsx.wasm +0 -0
  148. package/grammars/tree-sitter-typescript.wasm +0 -0
  149. package/package.json +12 -5
@@ -0,0 +1,638 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import { CodebaseSearcher } from '../core/search.js';
5
+ import { buildEvidenceLock } from '../preflight/evidence-lock.js';
6
+ import { shouldIncludePatternConflictCategory } from '../preflight/query-scope.js';
7
+ import { isComplementaryPatternConflict, shouldSkipLegacyTestingFrameworkCategory } from '../patterns/semantics.js';
8
+ import { assessSearchQuality } from '../core/search-quality.js';
9
+ import { IndexCorruptedError } from '../errors/index.js';
10
+ import { readMemoriesFile, withConfidence } from '../memory/store.js';
11
+ import { InternalFileGraph } from '../utils/usage-tracker.js';
12
+ import { RELATIONSHIPS_FILENAME } from '../constants/codebase-context.js';
13
+ export const definition = {
14
+ name: 'search_codebase',
15
+ description: 'Search the indexed codebase. Returns ranked results and a searchQuality confidence summary. ' +
16
+ 'IMPORTANT: Pass the intent="edit"|"refactor"|"migrate" to get preflight: edit readiness check with evidence gating.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ query: {
21
+ type: 'string',
22
+ description: 'Natural language search query'
23
+ },
24
+ intent: {
25
+ type: 'string',
26
+ enum: ['explore', 'edit', 'refactor', 'migrate'],
27
+ description: 'Optional. Use "edit", "refactor", or "migrate" to get the full preflight card before making changes.'
28
+ },
29
+ limit: {
30
+ type: 'number',
31
+ description: 'Maximum number of results to return (default: 5)',
32
+ default: 5
33
+ },
34
+ includeSnippets: {
35
+ type: 'boolean',
36
+ description: 'Include code snippets in results (default: false). If you need code, prefer read_file instead.',
37
+ default: false
38
+ },
39
+ filters: {
40
+ type: 'object',
41
+ description: 'Optional filters',
42
+ properties: {
43
+ framework: {
44
+ type: 'string',
45
+ description: 'Filter by framework (angular, react, vue)'
46
+ },
47
+ language: {
48
+ type: 'string',
49
+ description: 'Filter by programming language'
50
+ },
51
+ componentType: {
52
+ type: 'string',
53
+ description: 'Filter by component type (component, service, directive, etc.)'
54
+ },
55
+ layer: {
56
+ type: 'string',
57
+ description: 'Filter by architectural layer (presentation, business, data, state, core, shared)'
58
+ },
59
+ tags: {
60
+ type: 'array',
61
+ items: { type: 'string' },
62
+ description: 'Filter by tags'
63
+ }
64
+ }
65
+ }
66
+ },
67
+ required: ['query']
68
+ }
69
+ };
70
+ export async function handle(args, ctx) {
71
+ const { query, limit, filters, intent, includeSnippets } = args;
72
+ const queryStr = typeof query === 'string' ? query.trim() : '';
73
+ if (!queryStr) {
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: JSON.stringify({
79
+ status: 'error',
80
+ errorCode: 'invalid_params',
81
+ message: "Invalid params: 'query' is required and must be a non-empty string.",
82
+ hint: "Provide a query like 'how are routes configured' or 'AlbumApiService'."
83
+ }, null, 2)
84
+ }
85
+ ],
86
+ isError: true
87
+ };
88
+ }
89
+ if (ctx.indexState.status === 'indexing') {
90
+ return {
91
+ content: [
92
+ {
93
+ type: 'text',
94
+ text: JSON.stringify({
95
+ status: 'indexing',
96
+ message: 'Index is still being built. Retry in a moment.',
97
+ progress: ctx.indexState.indexer?.getProgress()
98
+ }, null, 2)
99
+ }
100
+ ]
101
+ };
102
+ }
103
+ if (ctx.indexState.status === 'error') {
104
+ return {
105
+ content: [
106
+ {
107
+ type: 'text',
108
+ text: JSON.stringify({
109
+ status: 'error',
110
+ message: `Indexing failed: ${ctx.indexState.error}`
111
+ }, null, 2)
112
+ }
113
+ ]
114
+ };
115
+ }
116
+ const searcher = new CodebaseSearcher(ctx.rootPath);
117
+ let results;
118
+ const searchProfile = intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent) ? intent : 'explore';
119
+ try {
120
+ results = await searcher.search(queryStr, limit || 5, filters, {
121
+ profile: searchProfile
122
+ });
123
+ }
124
+ catch (error) {
125
+ if (error instanceof IndexCorruptedError) {
126
+ console.error('[Auto-Heal] Index corrupted. Triggering full re-index...');
127
+ await ctx.performIndexing();
128
+ if (ctx.indexState.status === 'ready') {
129
+ console.error('[Auto-Heal] Success. Retrying search...');
130
+ const freshSearcher = new CodebaseSearcher(ctx.rootPath);
131
+ try {
132
+ results = await freshSearcher.search(queryStr, limit || 5, filters, {
133
+ profile: searchProfile
134
+ });
135
+ }
136
+ catch (retryError) {
137
+ return {
138
+ content: [
139
+ {
140
+ type: 'text',
141
+ text: JSON.stringify({
142
+ status: 'error',
143
+ message: `Auto-heal retry failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`
144
+ }, null, 2)
145
+ }
146
+ ]
147
+ };
148
+ }
149
+ }
150
+ else {
151
+ return {
152
+ content: [
153
+ {
154
+ type: 'text',
155
+ text: JSON.stringify({
156
+ status: 'error',
157
+ message: `Auto-heal failed: Indexing ended with status '${ctx.indexState.status}'`,
158
+ error: ctx.indexState.error
159
+ }, null, 2)
160
+ }
161
+ ]
162
+ };
163
+ }
164
+ }
165
+ else {
166
+ throw error; // Propagate unexpected errors
167
+ }
168
+ }
169
+ // Load memories for keyword matching, enriched with confidence
170
+ const allMemories = await readMemoriesFile(ctx.paths.memory);
171
+ const allMemoriesWithConf = withConfidence(allMemories);
172
+ const queryTerms = queryStr.toLowerCase().split(/\s+/).filter(Boolean);
173
+ const relatedMemories = allMemoriesWithConf
174
+ .filter((m) => {
175
+ const searchText = `${m.memory} ${m.reason}`.toLowerCase();
176
+ return queryTerms.some((term) => searchText.includes(term));
177
+ })
178
+ .sort((a, b) => b.effectiveConfidence - a.effectiveConfidence);
179
+ // Load intelligence data for enrichment (all intents, not just preflight)
180
+ let intelligence = null;
181
+ try {
182
+ const intelligenceContent = await fs.readFile(ctx.paths.intelligence, 'utf-8');
183
+ intelligence = JSON.parse(intelligenceContent);
184
+ }
185
+ catch {
186
+ /* graceful degradation — intelligence file may not exist yet */
187
+ }
188
+ // Load relationships sidecar (preferred over intelligence.internalFileGraph)
189
+ let relationships = null;
190
+ try {
191
+ const relationshipsPath = path.join(path.dirname(ctx.paths.intelligence), RELATIONSHIPS_FILENAME);
192
+ const relationshipsContent = await fs.readFile(relationshipsPath, 'utf-8');
193
+ relationships = JSON.parse(relationshipsContent);
194
+ }
195
+ catch {
196
+ /* graceful degradation — relationships sidecar may not exist yet */
197
+ }
198
+ // Helper to get imports graph from relationships sidecar (preferred) or intelligence
199
+ function getImportsGraph() {
200
+ if (relationships?.graph?.imports) {
201
+ return relationships.graph.imports;
202
+ }
203
+ if (intelligence?.internalFileGraph?.imports) {
204
+ return intelligence.internalFileGraph.imports;
205
+ }
206
+ return null;
207
+ }
208
+ function computeIndexConfidence() {
209
+ let confidence = 'stale';
210
+ if (intelligence?.generatedAt) {
211
+ const indexAge = Date.now() - new Date(intelligence.generatedAt).getTime();
212
+ const hoursOld = indexAge / (1000 * 60 * 60);
213
+ if (hoursOld < 24) {
214
+ confidence = 'fresh';
215
+ }
216
+ else if (hoursOld < 168) {
217
+ confidence = 'aging';
218
+ }
219
+ }
220
+ return confidence;
221
+ }
222
+ // Cheap impact breadth estimate from the import graph (used for risk assessment).
223
+ function computeImpactCandidates(resultPaths) {
224
+ const impactCandidates = [];
225
+ const allImports = getImportsGraph();
226
+ if (!allImports)
227
+ return impactCandidates;
228
+ for (const [file, deps] of Object.entries(allImports)) {
229
+ if (deps.some((dep) => resultPaths.some((rp) => dep.endsWith(rp) || rp.endsWith(dep)))) {
230
+ if (!resultPaths.some((rp) => file.endsWith(rp) || rp.endsWith(file))) {
231
+ impactCandidates.push(file);
232
+ }
233
+ }
234
+ }
235
+ return impactCandidates;
236
+ }
237
+ // Build reverse import map from relationships sidecar (preferred) or intelligence graph
238
+ const reverseImports = new Map();
239
+ const importsGraph = getImportsGraph();
240
+ if (importsGraph) {
241
+ for (const [file, deps] of Object.entries(importsGraph)) {
242
+ for (const dep of deps) {
243
+ if (!reverseImports.has(dep))
244
+ reverseImports.set(dep, []);
245
+ reverseImports.get(dep).push(file);
246
+ }
247
+ }
248
+ }
249
+ function buildRelationshipHints(result) {
250
+ const rPath = result.filePath;
251
+ // Graph keys are relative paths with forward slashes; normalize for comparison
252
+ const rPathNorm = path.relative(ctx.rootPath, rPath).replace(/\\/g, '/') || rPath.replace(/\\/g, '/');
253
+ // importedBy: files that import this result (reverse lookup), collect with counts
254
+ const importedByMap = new Map();
255
+ for (const [dep, importers] of reverseImports) {
256
+ if (dep === rPathNorm || dep.endsWith(rPathNorm) || rPathNorm.endsWith(dep)) {
257
+ for (const importer of importers) {
258
+ importedByMap.set(importer, (importedByMap.get(importer) || 0) + 1);
259
+ }
260
+ }
261
+ }
262
+ // testedIn: heuristic — same basename with .spec/.test extension
263
+ const testedIn = [];
264
+ const baseName = path.basename(rPathNorm).replace(/\.[^.]+$/, '');
265
+ if (importsGraph) {
266
+ for (const file of Object.keys(importsGraph)) {
267
+ const fileBase = path.basename(file);
268
+ if ((fileBase.includes('.spec.') || fileBase.includes('.test.')) &&
269
+ fileBase.startsWith(baseName)) {
270
+ testedIn.push(file);
271
+ }
272
+ }
273
+ }
274
+ // Build condensed relationships
275
+ const condensedRel = {};
276
+ if (importedByMap.size > 0) {
277
+ condensedRel.importedByCount = importedByMap.size;
278
+ }
279
+ if (testedIn.length > 0) {
280
+ condensedRel.hasTests = true;
281
+ }
282
+ // Build hints object with capped arrays
283
+ const hintsObj = {};
284
+ // Rank importers by count descending, cap at 3
285
+ if (importedByMap.size > 0) {
286
+ const sortedCallers = Array.from(importedByMap.entries())
287
+ .sort((a, b) => b[1] - a[1])
288
+ .slice(0, 3)
289
+ .map(([file]) => file);
290
+ hintsObj.callers = sortedCallers;
291
+ hintsObj.consumers = sortedCallers; // Same data, different label
292
+ }
293
+ // Cap tests at 3
294
+ if (testedIn.length > 0) {
295
+ hintsObj.tests = testedIn.slice(0, 3);
296
+ }
297
+ // Return both condensed and hints (hints only included if non-empty)
298
+ const output = {};
299
+ if (Object.keys(condensedRel).length > 0) {
300
+ output.relationships = condensedRel;
301
+ }
302
+ if (Object.keys(hintsObj).length > 0) {
303
+ output.hints = hintsObj;
304
+ }
305
+ return output;
306
+ }
307
+ const searchQuality = assessSearchQuality(query, results);
308
+ // Always-on edit preflight (lite): do not require intent and keep payload small.
309
+ let editPreflight = undefined;
310
+ if (intelligence && (!intent || intent === 'explore')) {
311
+ try {
312
+ const resultPaths = results.map((r) => r.filePath);
313
+ const impactCandidates = computeImpactCandidates(resultPaths);
314
+ // Use existing pattern intelligence for evidenceLock scoring, but keep the output payload lite.
315
+ const preferredPatternsForEvidence = [];
316
+ const patterns = intelligence.patterns || {};
317
+ for (const [_, data] of Object.entries(patterns)) {
318
+ if (data.primary) {
319
+ const p = data.primary;
320
+ if (p.trend === 'Rising' || p.trend === 'Stable') {
321
+ preferredPatternsForEvidence.push({
322
+ pattern: p.name,
323
+ ...(p.canonicalExample && { example: p.canonicalExample.file })
324
+ });
325
+ }
326
+ }
327
+ }
328
+ let riskLevel = 'low';
329
+ if (impactCandidates.length > 10) {
330
+ riskLevel = 'high';
331
+ }
332
+ else if (impactCandidates.length > 3) {
333
+ riskLevel = 'medium';
334
+ }
335
+ editPreflight = {
336
+ mode: 'lite',
337
+ riskLevel,
338
+ confidence: computeIndexConfidence(),
339
+ evidenceLock: buildEvidenceLock({
340
+ results,
341
+ preferredPatterns: preferredPatternsForEvidence.slice(0, 5),
342
+ relatedMemories,
343
+ failureWarnings: [],
344
+ patternConflicts: [],
345
+ searchQualityStatus: searchQuality.status
346
+ })
347
+ };
348
+ }
349
+ catch {
350
+ // editPreflight is best-effort - never fail search over it
351
+ }
352
+ }
353
+ // Compose preflight card for edit/refactor/migrate intents
354
+ let preflight = undefined;
355
+ const preflightIntents = ['edit', 'refactor', 'migrate'];
356
+ if (intent && preflightIntents.includes(intent) && intelligence) {
357
+ try {
358
+ // --- Avoid / Prefer patterns ---
359
+ const avoidPatternsList = [];
360
+ const preferredPatternsList = [];
361
+ const patterns = intelligence.patterns || {};
362
+ for (const [category, data] of Object.entries(patterns)) {
363
+ // Primary pattern = preferred if Rising or Stable
364
+ if (data.primary) {
365
+ const p = data.primary;
366
+ if (p.trend === 'Rising' || p.trend === 'Stable') {
367
+ preferredPatternsList.push({
368
+ pattern: p.name,
369
+ category,
370
+ adoption: p.frequency,
371
+ trend: p.trend,
372
+ guidance: p.guidance,
373
+ ...(p.canonicalExample && { example: p.canonicalExample.file })
374
+ });
375
+ }
376
+ }
377
+ // Also-detected patterns that are Declining = avoid
378
+ if (data.alsoDetected) {
379
+ for (const alt of data.alsoDetected) {
380
+ if (alt.trend === 'Declining') {
381
+ avoidPatternsList.push({
382
+ pattern: alt.name,
383
+ category,
384
+ adoption: alt.frequency,
385
+ trend: 'Declining',
386
+ guidance: alt.guidance
387
+ });
388
+ }
389
+ }
390
+ }
391
+ }
392
+ // --- Impact candidates (files importing the result files) ---
393
+ const resultPaths = results.map((r) => r.filePath);
394
+ const impactCandidates = computeImpactCandidates(resultPaths);
395
+ // PREF-02: Compute impact coverage (callers of result files that appear in results)
396
+ const callerFiles = resultPaths.flatMap((p) => {
397
+ const importers = [];
398
+ for (const [dep, importerList] of reverseImports) {
399
+ if (dep.endsWith(p) || p.endsWith(dep)) {
400
+ importers.push(...importerList);
401
+ }
402
+ }
403
+ return importers;
404
+ });
405
+ const uniqueCallers = new Set(callerFiles);
406
+ const callersCovered = Array.from(uniqueCallers).filter((f) => resultPaths.some((rp) => f.endsWith(rp) || rp.endsWith(f))).length;
407
+ const callersTotal = uniqueCallers.size;
408
+ const impactCoverage = callersTotal > 0 ? { covered: callersCovered, total: callersTotal } : undefined;
409
+ // --- Risk level (based on circular deps + impact breadth) ---
410
+ //TODO: Review this risk level calculation
411
+ let _riskLevel = 'low';
412
+ let cycleCount = 0;
413
+ const graphDataSource = relationships?.graph || intelligence?.internalFileGraph;
414
+ if (graphDataSource) {
415
+ try {
416
+ const graph = InternalFileGraph.fromJSON(graphDataSource, ctx.rootPath);
417
+ // Use directory prefixes as scope (not full file paths)
418
+ // findCycles(scope) filters files by startsWith, so a full path would only match itself
419
+ const scopes = new Set(resultPaths.map((rp) => {
420
+ const lastSlash = rp.lastIndexOf('/');
421
+ return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp;
422
+ }));
423
+ for (const scope of scopes) {
424
+ const cycles = graph.findCycles(scope);
425
+ cycleCount += cycles.length;
426
+ }
427
+ }
428
+ catch {
429
+ // Graph reconstruction failed — skip cycle check
430
+ }
431
+ }
432
+ if (cycleCount > 0 || impactCandidates.length > 10) {
433
+ _riskLevel = 'high';
434
+ }
435
+ else if (impactCandidates.length > 3) {
436
+ _riskLevel = 'medium';
437
+ }
438
+ // --- Golden files (exemplar code) ---
439
+ const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g) => ({
440
+ file: g.file,
441
+ score: g.score
442
+ }));
443
+ // --- Confidence (index freshness) ---
444
+ // TODO: Review this confidence calculation
445
+ //const confidence = computeIndexConfidence();
446
+ // --- Failure memories (1.5x relevance boost) ---
447
+ const failureWarnings = relatedMemories
448
+ .filter((m) => m.type === 'failure' && !m.stale)
449
+ .map((m) => ({
450
+ memory: m.memory,
451
+ reason: m.reason,
452
+ confidence: m.effectiveConfidence
453
+ }))
454
+ .slice(0, 3);
455
+ const preferredPatternsForOutput = preferredPatternsList.slice(0, 5);
456
+ const avoidPatternsForOutput = avoidPatternsList.slice(0, 5);
457
+ // --- Pattern conflicts (split decisions within categories) ---
458
+ const patternConflicts = [];
459
+ const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary);
460
+ for (const [cat, data] of Object.entries(patterns)) {
461
+ if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns))
462
+ continue;
463
+ if (!shouldIncludePatternConflictCategory(cat, query))
464
+ continue;
465
+ if (!data.primary || !data.alsoDetected?.length)
466
+ continue;
467
+ const primaryFreq = parseFloat(data.primary.frequency) || 100;
468
+ if (primaryFreq >= 80)
469
+ continue;
470
+ for (const alt of data.alsoDetected) {
471
+ const altFreq = parseFloat(alt.frequency) || 0;
472
+ if (altFreq >= 20) {
473
+ if (isComplementaryPatternConflict(cat, data.primary.name, alt.name))
474
+ continue;
475
+ if (hasUnitTestFramework && cat === 'testingFramework')
476
+ continue;
477
+ patternConflicts.push({
478
+ category: cat,
479
+ primary: { name: data.primary.name, adoption: data.primary.frequency },
480
+ alternative: { name: alt.name, adoption: alt.frequency }
481
+ });
482
+ }
483
+ }
484
+ }
485
+ const evidenceLock = buildEvidenceLock({
486
+ results,
487
+ preferredPatterns: preferredPatternsForOutput,
488
+ relatedMemories,
489
+ failureWarnings,
490
+ patternConflicts,
491
+ searchQualityStatus: searchQuality.status,
492
+ impactCoverage
493
+ });
494
+ const decisionCard = {
495
+ ready: evidenceLock.readyToEdit
496
+ };
497
+ // Add nextAction if not ready
498
+ if (!decisionCard.ready && evidenceLock.nextAction) {
499
+ decisionCard.nextAction = evidenceLock.nextAction;
500
+ }
501
+ // Add warnings from failure memories (capped at 3)
502
+ if (failureWarnings.length > 0) {
503
+ decisionCard.warnings = failureWarnings.slice(0, 3).map((w) => w.memory);
504
+ }
505
+ // Add patterns (do/avoid, capped at 3 each, with adoption %)
506
+ const doPatterns = preferredPatternsForOutput
507
+ .slice(0, 3)
508
+ .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''}`);
509
+ const avoidPatterns = avoidPatternsForOutput
510
+ .slice(0, 3)
511
+ .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''} (declining)`);
512
+ if (doPatterns.length > 0 || avoidPatterns.length > 0) {
513
+ decisionCard.patterns = {
514
+ ...(doPatterns.length > 0 && { do: doPatterns }),
515
+ ...(avoidPatterns.length > 0 && { avoid: avoidPatterns })
516
+ };
517
+ }
518
+ // Add bestExample (top 1 golden file)
519
+ if (goldenFiles.length > 0) {
520
+ decisionCard.bestExample = `${goldenFiles[0].file}`;
521
+ }
522
+ // Add impact (coverage + top 3 files)
523
+ if (impactCoverage || impactCandidates.length > 0) {
524
+ const impactObj = {};
525
+ if (impactCoverage) {
526
+ impactObj.coverage = `${impactCoverage.covered}/${impactCoverage.total} callers in results`;
527
+ }
528
+ if (impactCandidates.length > 0) {
529
+ impactObj.files = impactCandidates.slice(0, 3);
530
+ }
531
+ if (Object.keys(impactObj).length > 0) {
532
+ decisionCard.impact = impactObj;
533
+ }
534
+ }
535
+ // Add whatWouldHelp from evidenceLock
536
+ if (evidenceLock.whatWouldHelp && evidenceLock.whatWouldHelp.length > 0) {
537
+ decisionCard.whatWouldHelp = evidenceLock.whatWouldHelp;
538
+ }
539
+ preflight = decisionCard;
540
+ }
541
+ catch {
542
+ // Preflight construction failed — skip preflight, don't fail the search
543
+ }
544
+ }
545
+ // For edit/refactor/migrate: return clean decision card.
546
+ // For explore or lite-only: return lightweight { ready, reason }.
547
+ let preflightPayload;
548
+ if (preflight) {
549
+ // preflight is already a clean decision card (DecisionCard type)
550
+ preflightPayload = preflight;
551
+ }
552
+ else if (editPreflight) {
553
+ // Lite preflight for explore intent
554
+ const el = editPreflight.evidenceLock;
555
+ preflightPayload = {
556
+ ready: el?.readyToEdit ?? false,
557
+ ...(el && !el.readyToEdit && el.nextAction && { reason: el.nextAction })
558
+ };
559
+ }
560
+ // Helper: Build scope header for symbol-aware chunks (SEARCH-02)
561
+ function buildScopeHeader(metadata) {
562
+ // Try symbolPath first (most reliable for AST-based symbols)
563
+ if (metadata?.symbolPath && Array.isArray(metadata.symbolPath)) {
564
+ return metadata.symbolPath.join('.');
565
+ }
566
+ // Fallback: className + functionName
567
+ if (metadata?.className && metadata?.functionName) {
568
+ return `${metadata.className}.${metadata.functionName}`;
569
+ }
570
+ // Class only
571
+ if (metadata?.className) {
572
+ return metadata.className;
573
+ }
574
+ // Function only
575
+ if (metadata?.functionName) {
576
+ return metadata.functionName;
577
+ }
578
+ // component chunk fallback (component or pipe name)
579
+ if (metadata?.componentName) {
580
+ return metadata.componentName;
581
+ }
582
+ return null;
583
+ }
584
+ function enrichSnippetWithScope(snippet, metadata) {
585
+ if (!snippet)
586
+ return undefined;
587
+ const scopeHeader = buildScopeHeader(metadata);
588
+ if (scopeHeader) {
589
+ return `// ${scopeHeader}\n${snippet}`;
590
+ }
591
+ return snippet;
592
+ }
593
+ return {
594
+ content: [
595
+ {
596
+ type: 'text',
597
+ text: JSON.stringify({
598
+ status: 'success',
599
+ searchQuality: {
600
+ status: searchQuality.status,
601
+ confidence: searchQuality.confidence,
602
+ ...(searchQuality.status === 'low_confidence' &&
603
+ searchQuality.nextSteps?.[0] && {
604
+ hint: searchQuality.nextSteps[0]
605
+ })
606
+ },
607
+ ...(preflightPayload && { preflight: preflightPayload }),
608
+ results: results.map((r) => {
609
+ const relationshipsAndHints = buildRelationshipHints(r);
610
+ const enrichedSnippet = includeSnippets
611
+ ? enrichSnippetWithScope(r.snippet, r.metadata)
612
+ : undefined;
613
+ return {
614
+ file: `${r.filePath}:${r.startLine}-${r.endLine}`,
615
+ summary: r.summary,
616
+ score: Math.round(r.score * 100) / 100,
617
+ ...(r.componentType && r.layer && { type: `${r.componentType}:${r.layer}` }),
618
+ ...(r.trend && r.trend !== 'Stable' && { trend: r.trend }),
619
+ ...(r.patternWarning && { patternWarning: r.patternWarning }),
620
+ ...(relationshipsAndHints.relationships && {
621
+ relationships: relationshipsAndHints.relationships
622
+ }),
623
+ ...(relationshipsAndHints.hints && { hints: relationshipsAndHints.hints }),
624
+ ...(enrichedSnippet && { snippet: enrichedSnippet })
625
+ };
626
+ }),
627
+ totalResults: results.length,
628
+ ...(relatedMemories.length > 0 && {
629
+ relatedMemories: relatedMemories
630
+ .slice(0, 3)
631
+ .map((m) => `${m.memory} (${m.effectiveConfidence})`)
632
+ })
633
+ }, null, 2)
634
+ }
635
+ ]
636
+ };
637
+ }
638
+ //# sourceMappingURL=search-codebase.js.map