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