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.
- package/README.md +142 -46
- package/dist/analyzers/angular/index.d.ts +4 -0
- package/dist/analyzers/angular/index.d.ts.map +1 -1
- package/dist/analyzers/angular/index.js +51 -10
- package/dist/analyzers/angular/index.js.map +1 -1
- package/dist/analyzers/generic/index.d.ts +1 -0
- package/dist/analyzers/generic/index.d.ts.map +1 -1
- package/dist/analyzers/generic/index.js +89 -10
- package/dist/analyzers/generic/index.js.map +1 -1
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +352 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants/codebase-context.d.ts +13 -0
- package/dist/constants/codebase-context.d.ts.map +1 -1
- package/dist/constants/codebase-context.js +13 -0
- package/dist/constants/codebase-context.js.map +1 -1
- package/dist/core/index-meta.d.ts +24 -0
- package/dist/core/index-meta.d.ts.map +1 -0
- package/dist/core/index-meta.js +204 -0
- package/dist/core/index-meta.js.map +1 -0
- package/dist/core/indexer.d.ts +0 -5
- package/dist/core/indexer.d.ts.map +1 -1
- package/dist/core/indexer.js +265 -60
- package/dist/core/indexer.js.map +1 -1
- package/dist/core/search.d.ts +1 -0
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/search.js +60 -5
- package/dist/core/search.js.map +1 -1
- package/dist/core/symbol-references.d.ts +21 -0
- package/dist/core/symbol-references.d.ts.map +1 -0
- package/dist/core/symbol-references.js +91 -0
- package/dist/core/symbol-references.js.map +1 -0
- package/dist/eval/harness.d.ts +5 -0
- package/dist/eval/harness.d.ts.map +1 -0
- package/dist/eval/harness.js +153 -0
- package/dist/eval/harness.js.map +1 -0
- package/dist/eval/types.d.ts +59 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/eval/types.js +2 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/grammars/manifest.d.ts +26 -0
- package/dist/grammars/manifest.d.ts.map +1 -0
- package/dist/grammars/manifest.js +63 -0
- package/dist/grammars/manifest.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -1231
- package/dist/index.js.map +1 -1
- package/dist/memory/store.d.ts +3 -0
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +9 -0
- package/dist/memory/store.js.map +1 -1
- package/dist/patterns/semantics.d.ts +6 -0
- package/dist/patterns/semantics.d.ts.map +1 -1
- package/dist/patterns/semantics.js +22 -6
- package/dist/patterns/semantics.js.map +1 -1
- package/dist/preflight/evidence-lock.d.ts +8 -0
- package/dist/preflight/evidence-lock.d.ts.map +1 -1
- package/dist/preflight/evidence-lock.js +49 -3
- package/dist/preflight/evidence-lock.js.map +1 -1
- package/dist/storage/lancedb.d.ts +9 -1
- package/dist/storage/lancedb.d.ts.map +1 -1
- package/dist/storage/lancedb.js +26 -8
- package/dist/storage/lancedb.js.map +1 -1
- package/dist/tools/detect-circular-dependencies.d.ts +5 -0
- package/dist/tools/detect-circular-dependencies.d.ts.map +1 -0
- package/dist/tools/detect-circular-dependencies.js +117 -0
- package/dist/tools/detect-circular-dependencies.js.map +1 -0
- package/dist/tools/get-codebase-metadata.d.ts +5 -0
- package/dist/tools/get-codebase-metadata.d.ts.map +1 -0
- package/dist/tools/get-codebase-metadata.js +53 -0
- package/dist/tools/get-codebase-metadata.js.map +1 -0
- package/dist/tools/get-component-usage.d.ts +5 -0
- package/dist/tools/get-component-usage.d.ts.map +1 -0
- package/dist/tools/get-component-usage.js +83 -0
- package/dist/tools/get-component-usage.js.map +1 -0
- package/dist/tools/get-indexing-status.d.ts +5 -0
- package/dist/tools/get-indexing-status.d.ts.map +1 -0
- package/dist/tools/get-indexing-status.js +44 -0
- package/dist/tools/get-indexing-status.js.map +1 -0
- package/dist/tools/get-memory.d.ts +5 -0
- package/dist/tools/get-memory.d.ts.map +1 -0
- package/dist/tools/get-memory.js +89 -0
- package/dist/tools/get-memory.js.map +1 -0
- package/dist/tools/get-style-guide.d.ts +5 -0
- package/dist/tools/get-style-guide.d.ts.map +1 -0
- package/dist/tools/get-style-guide.js +151 -0
- package/dist/tools/get-style-guide.js.map +1 -0
- package/dist/tools/get-symbol-references.d.ts +5 -0
- package/dist/tools/get-symbol-references.d.ts.map +1 -0
- package/dist/tools/get-symbol-references.js +70 -0
- package/dist/tools/get-symbol-references.js.map +1 -0
- package/dist/tools/get-team-patterns.d.ts +5 -0
- package/dist/tools/get-team-patterns.d.ts.map +1 -0
- package/dist/tools/get-team-patterns.js +131 -0
- package/dist/tools/get-team-patterns.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +41 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/refresh-index.d.ts +5 -0
- package/dist/tools/refresh-index.d.ts.map +1 -0
- package/dist/tools/refresh-index.js +40 -0
- package/dist/tools/refresh-index.js.map +1 -0
- package/dist/tools/remember.d.ts +5 -0
- package/dist/tools/remember.d.ts.map +1 -0
- package/dist/tools/remember.js +101 -0
- package/dist/tools/remember.js.map +1 -0
- package/dist/tools/search-codebase.d.ts +5 -0
- package/dist/tools/search-codebase.d.ts.map +1 -0
- package/dist/tools/search-codebase.js +638 -0
- package/dist/tools/search-codebase.js.map +1 -0
- package/dist/tools/types.d.ts +31 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/ast-chunker.d.ts +71 -0
- package/dist/utils/ast-chunker.d.ts.map +1 -0
- package/dist/utils/ast-chunker.js +453 -0
- package/dist/utils/ast-chunker.js.map +1 -0
- package/dist/utils/chunking.d.ts.map +1 -1
- package/dist/utils/chunking.js +10 -3
- package/dist/utils/chunking.js.map +1 -1
- package/dist/utils/dependency-detection.d.ts +2 -1
- package/dist/utils/dependency-detection.d.ts.map +1 -1
- package/dist/utils/dependency-detection.js.map +1 -1
- package/dist/utils/language-detection.d.ts.map +1 -1
- package/dist/utils/language-detection.js +20 -0
- package/dist/utils/language-detection.js.map +1 -1
- package/dist/utils/tree-sitter.d.ts +17 -0
- package/dist/utils/tree-sitter.d.ts.map +1 -0
- package/dist/utils/tree-sitter.js +311 -0
- package/dist/utils/tree-sitter.js.map +1 -0
- package/docs/capabilities.md +131 -37
- package/grammars/.gitkeep +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- 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
|