@mrclrchtr/supi-code-intelligence 0.2.0 → 1.1.2
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 +16 -5
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -5
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -5
- package/node_modules/@mrclrchtr/supi-lsp/package.json +2 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +11 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-stale-resync.ts +47 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +10 -30
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +9 -0
- package/package.json +4 -9
- package/src/actions/affected-action.ts +153 -24
- package/src/actions/callees-action.ts +17 -0
- package/src/actions/callers-action.ts +178 -111
- package/src/actions/implementations-action.ts +18 -0
- package/src/actions/pattern-action.ts +167 -7
- package/src/brief-focused.ts +189 -9
- package/src/code-intelligence.ts +10 -2
- package/src/git-context.ts +11 -0
- package/src/guidance.ts +11 -8
- package/src/pattern-structured.ts +196 -0
- package/src/prioritization-signals.ts +188 -0
- package/src/resolve-target.ts +11 -3
- package/src/semantic-action-helpers.ts +28 -0
- package/src/target-resolution.ts +215 -0
- package/src/tool-actions.ts +8 -0
- package/src/types.ts +4 -0
|
@@ -6,14 +6,19 @@ import { resolveTarget } from "../resolve-target.ts";
|
|
|
6
6
|
import {
|
|
7
7
|
escapeRegex,
|
|
8
8
|
filterOutDeclaration,
|
|
9
|
-
groupByFile,
|
|
10
9
|
isInProjectPath,
|
|
11
10
|
normalizePath,
|
|
12
11
|
runRipgrep,
|
|
13
12
|
uriToFile,
|
|
14
13
|
} from "../search-helpers.ts";
|
|
14
|
+
import {
|
|
15
|
+
dedupeFileLineRefs,
|
|
16
|
+
highestConfidence,
|
|
17
|
+
isResolvedTargetGroup,
|
|
18
|
+
} from "../semantic-action-helpers.ts";
|
|
19
|
+
import type { ResolvedTarget, ResolvedTargetGroup } from "../target-resolution.ts";
|
|
15
20
|
import type { ActionParams } from "../tool-actions.ts";
|
|
16
|
-
import type { CodeIntelResult, SearchDetails } from "../types.ts";
|
|
21
|
+
import type { CodeIntelResult, ConfidenceMode, SearchDetails } from "../types.ts";
|
|
17
22
|
|
|
18
23
|
export async function executeCallersAction(
|
|
19
24
|
params: ActionParams,
|
|
@@ -36,42 +41,45 @@ export async function executeCallersAction(
|
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (lspState.kind === "ready") {
|
|
43
|
-
const refs = await lspState.service.references(target.file, target.position);
|
|
44
|
-
if (refs && refs.length > 0) {
|
|
45
|
-
// Filter out the declaration itself — LSP includes it with includeDeclaration
|
|
46
|
-
const callerRefs = filterOutDeclaration(refs, target.file, target.position);
|
|
47
|
-
if (callerRefs.length > 0) {
|
|
48
|
-
const content = formatSemanticCallers(callerRefs, target.name, cwd, maxResults);
|
|
49
|
-
const { project: projectRefs, external: externalRefs } = partitionRefs(refs, cwd);
|
|
50
|
-
const details: SearchDetails = {
|
|
51
|
-
confidence: "semantic",
|
|
52
|
-
scope: params.path ?? null,
|
|
53
|
-
candidateCount: projectRefs.length,
|
|
54
|
-
omittedCount: externalRefs.length,
|
|
55
|
-
nextQueries: [
|
|
56
|
-
"`code_intel affected` for impact analysis",
|
|
57
|
-
"`code_intel pattern` with broader scope for additional matches",
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
return { content, details: { type: "search" as const, data: details } };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
44
|
+
if (isResolvedTargetGroup(target)) {
|
|
45
|
+
return executeFileLevelCallers(target, params, cwd);
|
|
63
46
|
}
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
const result = await collectCallerRefs(target, params, cwd);
|
|
49
|
+
if (result.refs.length > 0) {
|
|
50
|
+
const content = formatTargetCallers(
|
|
51
|
+
`Callers of \`${target.name ?? "symbol"}\``,
|
|
52
|
+
result,
|
|
53
|
+
cwd,
|
|
54
|
+
params,
|
|
55
|
+
);
|
|
67
56
|
const details: SearchDetails = {
|
|
68
|
-
confidence:
|
|
57
|
+
confidence: result.confidence,
|
|
69
58
|
scope: params.path ?? null,
|
|
70
|
-
candidateCount: result.
|
|
71
|
-
omittedCount:
|
|
72
|
-
nextQueries: [
|
|
59
|
+
candidateCount: result.candidateCount,
|
|
60
|
+
omittedCount: result.externalCount,
|
|
61
|
+
nextQueries: [
|
|
62
|
+
"`code_intel affected` for impact analysis",
|
|
63
|
+
"`code_intel pattern` with broader scope for additional matches",
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
return { content, details: { type: "search" as const, data: details } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (target.name) {
|
|
70
|
+
return {
|
|
71
|
+
content: `No references found for \`${target.name}\` (${result.confidence}).`,
|
|
72
|
+
details: {
|
|
73
|
+
type: "search" as const,
|
|
74
|
+
data: {
|
|
75
|
+
confidence: result.confidence,
|
|
76
|
+
scope: params.path ?? null,
|
|
77
|
+
candidateCount: 0,
|
|
78
|
+
omittedCount: 0,
|
|
79
|
+
nextQueries: ["Enable LSP for `semantic` caller accuracy"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
73
82
|
};
|
|
74
|
-
return { content: result.content, details: { type: "search" as const, data: details } };
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
const relPath = path.relative(cwd, target.file);
|
|
@@ -90,126 +98,185 @@ export async function executeCallersAction(
|
|
|
90
98
|
};
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
): { project: typeof refs; external: typeof refs } {
|
|
97
|
-
const project: typeof refs = [];
|
|
98
|
-
const external: typeof refs = [];
|
|
99
|
-
for (const ref of refs) {
|
|
100
|
-
if (isInProjectPath(uriToFile(ref.uri), cwd)) {
|
|
101
|
-
project.push(ref);
|
|
102
|
-
} else {
|
|
103
|
-
external.push(ref);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return { project, external };
|
|
101
|
+
interface CallerRef {
|
|
102
|
+
file: string;
|
|
103
|
+
line: number;
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
refs:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for (const ref of refs) {
|
|
115
|
-
const filePath = uriToFile(ref.uri);
|
|
116
|
-
const relPath = path.relative(cwd, filePath);
|
|
117
|
-
const group = byFile.get(relPath) ?? [];
|
|
118
|
-
group.push(ref.range.start.line + 1);
|
|
119
|
-
byFile.set(relPath, group);
|
|
120
|
-
}
|
|
121
|
-
return byFile;
|
|
106
|
+
interface CallerCollection {
|
|
107
|
+
refs: CallerRef[];
|
|
108
|
+
confidence: ConfidenceMode;
|
|
109
|
+
externalCount: number;
|
|
110
|
+
candidateCount: number;
|
|
122
111
|
}
|
|
123
112
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
113
|
+
async function executeFileLevelCallers(
|
|
114
|
+
targetGroup: ResolvedTargetGroup,
|
|
115
|
+
params: ActionParams,
|
|
127
116
|
cwd: string,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
): Promise<CodeIntelResult> {
|
|
118
|
+
const perTarget = await Promise.all(
|
|
119
|
+
targetGroup.targets.map(async (target) => ({
|
|
120
|
+
target,
|
|
121
|
+
result: await collectCallerRefs(target, params, cwd),
|
|
122
|
+
})),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const withRefs = perTarget.filter((entry) => entry.result.refs.length > 0);
|
|
126
|
+
const uniqueRefs = dedupeFileLineRefs(withRefs.flatMap((entry) => entry.result.refs));
|
|
127
|
+
const confidence = highestConfidence(withRefs.map((entry) => entry.result.confidence));
|
|
128
|
+
const externalCount = withRefs.reduce((sum, entry) => sum + entry.result.externalCount, 0);
|
|
131
129
|
|
|
132
130
|
const lines: string[] = [];
|
|
133
|
-
lines.push(`# Callers
|
|
131
|
+
lines.push(`# Callers in \`${targetGroup.displayName}\``);
|
|
134
132
|
lines.push("");
|
|
135
133
|
lines.push(
|
|
136
|
-
`**${
|
|
134
|
+
`**${targetGroup.targets.length} exported target${targetGroup.targets.length !== 1 ? "s" : ""}** | **${uniqueRefs.length} reference${uniqueRefs.length !== 1 ? "s" : ""}** (${confidence})`,
|
|
137
135
|
);
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
externalRefs.length === 1
|
|
141
|
-
? "+1 external reference"
|
|
142
|
-
: `+${externalRefs.length} external references`;
|
|
143
|
-
lines.push(`_${suffix} (node_modules, .pnpm, or out-of-tree)_`);
|
|
136
|
+
if (externalCount > 0) {
|
|
137
|
+
lines.push(`_+${externalCount} external reference${externalCount !== 1 ? "s" : ""}_`);
|
|
144
138
|
}
|
|
145
139
|
lines.push("");
|
|
146
140
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (const [file, locations] of byFile) {
|
|
151
|
-
if (shown >= maxResults) break;
|
|
152
|
-
lines.push(`### ${file}`);
|
|
153
|
-
for (const loc of locations.slice(0, 5)) {
|
|
154
|
-
lines.push(`- L${loc}`);
|
|
155
|
-
}
|
|
156
|
-
if (locations.length > 5) {
|
|
157
|
-
lines.push(`- _+${locations.length - 5} more in this file_`);
|
|
158
|
-
}
|
|
141
|
+
for (const entry of withRefs) {
|
|
142
|
+
lines.push(`## \`${entry.target.name ?? "symbol"}\``);
|
|
143
|
+
addRefList(lines, entry.result.refs, cwd, params.maxResults ?? 5);
|
|
159
144
|
lines.push("");
|
|
160
|
-
shown++;
|
|
161
145
|
}
|
|
162
146
|
|
|
163
|
-
if (
|
|
164
|
-
lines.push(
|
|
165
|
-
`_+${byFile.size - maxResults} more files omitted. Narrow with \`path\` or increase \`maxResults\`._`,
|
|
166
|
-
);
|
|
147
|
+
if (withRefs.length === 0) {
|
|
148
|
+
lines.push("No caller references found for the discovered file-level targets.");
|
|
167
149
|
lines.push("");
|
|
168
150
|
}
|
|
169
151
|
|
|
170
|
-
|
|
152
|
+
const details: SearchDetails = {
|
|
153
|
+
confidence,
|
|
154
|
+
scope: params.path ?? null,
|
|
155
|
+
candidateCount: uniqueRefs.length,
|
|
156
|
+
omittedCount: externalCount,
|
|
157
|
+
nextQueries: [
|
|
158
|
+
"`code_intel affected` for impact analysis",
|
|
159
|
+
"Use `file` + coordinates to drill into one symbol precisely",
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return { content: lines.join("\n"), details: { type: "search" as const, data: details } };
|
|
171
164
|
}
|
|
172
165
|
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: LSP-first caller collection with fallback and declaration filtering is clearest in one helper
|
|
167
|
+
async function collectCallerRefs(
|
|
168
|
+
target: ResolvedTarget,
|
|
175
169
|
params: ActionParams,
|
|
176
170
|
cwd: string,
|
|
177
|
-
):
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
): Promise<CallerCollection> {
|
|
172
|
+
const lspState = getSessionLspService(cwd);
|
|
173
|
+
|
|
174
|
+
if (lspState.kind === "ready") {
|
|
175
|
+
const refs = await lspState.service.references(target.file, target.position);
|
|
176
|
+
if (refs && refs.length > 0) {
|
|
177
|
+
const filtered = filterOutDeclaration(refs, target.file, target.position);
|
|
178
|
+
const projectRefs: CallerRef[] = [];
|
|
179
|
+
let externalCount = 0;
|
|
180
|
+
for (const ref of refs) {
|
|
181
|
+
const filePath = uriToFile(ref.uri);
|
|
182
|
+
if (!isInProjectPath(filePath, cwd)) {
|
|
183
|
+
externalCount++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const ref of filtered) {
|
|
187
|
+
const filePath = uriToFile(ref.uri);
|
|
188
|
+
if (isInProjectPath(filePath, cwd)) {
|
|
189
|
+
projectRefs.push({ file: path.relative(cwd, filePath), line: ref.range.start.line + 1 });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (projectRefs.length > 0) {
|
|
193
|
+
return {
|
|
194
|
+
refs: projectRefs,
|
|
195
|
+
confidence: "semantic",
|
|
196
|
+
externalCount,
|
|
197
|
+
candidateCount: projectRefs.length,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
182
202
|
|
|
183
|
-
if (
|
|
184
|
-
return {
|
|
203
|
+
if (!target.name) {
|
|
204
|
+
return { refs: [], confidence: "unavailable", externalCount: 0, candidateCount: 0 };
|
|
185
205
|
}
|
|
186
206
|
|
|
207
|
+
const scopePath = params.path ? normalizePath(params.path, cwd) : cwd;
|
|
208
|
+
const pattern = `\\b${escapeRegex(target.name)}\\b`;
|
|
209
|
+
const matches = runRipgrep(pattern, scopePath, cwd, { maxMatches: (params.maxResults ?? 8) * 3 });
|
|
210
|
+
const refs = matches
|
|
211
|
+
.filter((match) => !isDeclarationMatch(match.file, match.line, target, cwd))
|
|
212
|
+
.map((match) => ({
|
|
213
|
+
file: path.relative(cwd, path.resolve(cwd, match.file)),
|
|
214
|
+
line: match.line,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
return { refs, confidence: "heuristic", externalCount: 0, candidateCount: refs.length };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatTargetCallers(
|
|
221
|
+
title: string,
|
|
222
|
+
result: CallerCollection,
|
|
223
|
+
cwd: string,
|
|
224
|
+
params: ActionParams,
|
|
225
|
+
): string {
|
|
187
226
|
const lines: string[] = [];
|
|
188
|
-
lines.push(`#
|
|
227
|
+
lines.push(`# ${title}`);
|
|
189
228
|
lines.push("");
|
|
190
229
|
lines.push(
|
|
191
|
-
`**${
|
|
230
|
+
`**${result.candidateCount} reference${result.candidateCount !== 1 ? "s" : ""}** (${result.confidence})`,
|
|
192
231
|
);
|
|
232
|
+
if (result.externalCount > 0) {
|
|
233
|
+
lines.push(
|
|
234
|
+
`_+${result.externalCount} external reference${result.externalCount !== 1 ? "s" : ""}_`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
193
237
|
lines.push("");
|
|
238
|
+
addRefList(lines, result.refs, cwd, params.maxResults ?? 5);
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
}
|
|
194
241
|
|
|
195
|
-
|
|
242
|
+
function addRefList(lines: string[], refs: CallerRef[], cwd: string, maxResults: number): void {
|
|
243
|
+
const byFile = groupRefsByFile(refs, cwd);
|
|
196
244
|
let shown = 0;
|
|
197
|
-
for (const [file,
|
|
245
|
+
for (const [file, locations] of byFile) {
|
|
198
246
|
if (shown >= maxResults) break;
|
|
199
247
|
lines.push(`### ${file}`);
|
|
200
|
-
for (const
|
|
201
|
-
lines.push(`- L${
|
|
248
|
+
for (const loc of locations.slice(0, 5)) {
|
|
249
|
+
lines.push(`- L${loc}`);
|
|
202
250
|
}
|
|
203
|
-
if (
|
|
204
|
-
lines.push(`- _+${
|
|
251
|
+
if (locations.length > 5) {
|
|
252
|
+
lines.push(`- _+${locations.length - 5} more in this file_`);
|
|
205
253
|
}
|
|
206
254
|
lines.push("");
|
|
207
255
|
shown++;
|
|
208
256
|
}
|
|
209
257
|
|
|
210
258
|
if (byFile.size > maxResults) {
|
|
211
|
-
lines.push(
|
|
259
|
+
lines.push(
|
|
260
|
+
`_+${byFile.size - maxResults} more files omitted. Narrow with \`path\` or increase \`maxResults\`._`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function groupRefsByFile(refs: CallerRef[], _cwd: string): Map<string, number[]> {
|
|
266
|
+
const byFile = new Map<string, number[]>();
|
|
267
|
+
for (const ref of refs) {
|
|
268
|
+
const group = byFile.get(ref.file) ?? [];
|
|
269
|
+
group.push(ref.line);
|
|
270
|
+
byFile.set(ref.file, group);
|
|
212
271
|
}
|
|
272
|
+
return byFile;
|
|
273
|
+
}
|
|
213
274
|
|
|
214
|
-
|
|
275
|
+
function isDeclarationMatch(
|
|
276
|
+
file: string,
|
|
277
|
+
line: number,
|
|
278
|
+
target: ResolvedTarget,
|
|
279
|
+
cwd: string,
|
|
280
|
+
): boolean {
|
|
281
|
+
return path.resolve(cwd, file) === target.file && line === target.displayLine;
|
|
215
282
|
}
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
runRipgrep,
|
|
11
11
|
uriToFile,
|
|
12
12
|
} from "../search-helpers.ts";
|
|
13
|
+
import { isResolvedTargetGroup } from "../semantic-action-helpers.ts";
|
|
13
14
|
import type { ActionParams } from "../tool-actions.ts";
|
|
14
15
|
import type { CodeIntelResult, SearchDetails } from "../types.ts";
|
|
15
16
|
|
|
17
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: implementation lookup has distinct semantic, unsupported-file, and heuristic branches
|
|
16
18
|
export async function executeImplementationsAction(
|
|
17
19
|
params: ActionParams,
|
|
18
20
|
cwd: string,
|
|
@@ -34,6 +36,22 @@ export async function executeImplementationsAction(
|
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
if (isResolvedTargetGroup(target)) {
|
|
40
|
+
return {
|
|
41
|
+
content: `**Error:** File-level implementation discovery is not available for \`${path.relative(cwd, target.file)}\`.\n\nProvide \`line\` and \`character\`, or a \`symbol\` for discovery.`,
|
|
42
|
+
details: {
|
|
43
|
+
type: "search" as const,
|
|
44
|
+
data: {
|
|
45
|
+
confidence: "unavailable",
|
|
46
|
+
scope: params.path ?? null,
|
|
47
|
+
candidateCount: 0,
|
|
48
|
+
omittedCount: 0,
|
|
49
|
+
nextQueries: ["Use `file` + coordinates or `symbol` for implementation lookup"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
const lspState = getSessionLspService(cwd);
|
|
38
56
|
const relPath = path.relative(cwd, target.file);
|
|
39
57
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// Pattern action — bounded, scope-aware text search.
|
|
2
|
+
// biome-ignore-all lint/nursery/noExcessiveLinesPerFile: text and structured pattern flows share one formatter and matcher pipeline
|
|
2
3
|
|
|
4
|
+
import {
|
|
5
|
+
getStructuredPatternMatches,
|
|
6
|
+
isStructuredPatternKind,
|
|
7
|
+
type StructuredMatch,
|
|
8
|
+
type StructuredPatternResult,
|
|
9
|
+
} from "../pattern-structured.ts";
|
|
3
10
|
import type { RgMatch } from "../search-helpers.ts";
|
|
4
11
|
import {
|
|
5
12
|
escapeRegex,
|
|
@@ -19,7 +26,7 @@ import type { CodeIntelResult, SearchDetails } from "../types.ts";
|
|
|
19
26
|
* patterns are surfaced as explicit user-facing errors instead of being
|
|
20
27
|
* collapsed into a misleading no-match response.
|
|
21
28
|
*/
|
|
22
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: function has multiple distinct paths (validation, regex vs literal, summary vs detailed, zero vs results) that are clearer when explicit than when split
|
|
29
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: function has multiple distinct paths (validation, regex vs literal, summary vs detailed, structured vs text, zero vs results) that are clearer when explicit than when split
|
|
23
30
|
export async function executePatternAction(
|
|
24
31
|
params: ActionParams,
|
|
25
32
|
cwd: string,
|
|
@@ -36,6 +43,63 @@ export async function executePatternAction(
|
|
|
36
43
|
const scopePath = params.path ? normalizePath(params.path, cwd) : cwd;
|
|
37
44
|
const relScope = params.path ?? ".";
|
|
38
45
|
|
|
46
|
+
if (isStructuredPatternKind(params.kind)) {
|
|
47
|
+
const structured = await getStructuredPatternMatches(
|
|
48
|
+
{ ...params, pattern: params.pattern, kind: params.kind },
|
|
49
|
+
scopePath,
|
|
50
|
+
cwd,
|
|
51
|
+
relScope,
|
|
52
|
+
);
|
|
53
|
+
if (typeof structured === "string") {
|
|
54
|
+
const errorDetails: SearchDetails = {
|
|
55
|
+
confidence: "unavailable",
|
|
56
|
+
scope: params.path ?? null,
|
|
57
|
+
candidateCount: 0,
|
|
58
|
+
omittedCount: 0,
|
|
59
|
+
nextQueries: ["Fix the regex pattern and retry"],
|
|
60
|
+
};
|
|
61
|
+
return { content: structured, details: { type: "search", data: errorDetails } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (structured) {
|
|
65
|
+
if (structured.matches.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
content: formatStructuredEmptyState(params.pattern, params.kind, relScope, structured),
|
|
68
|
+
details: {
|
|
69
|
+
type: "search",
|
|
70
|
+
data: {
|
|
71
|
+
confidence: "structural",
|
|
72
|
+
scope: params.path ?? null,
|
|
73
|
+
candidateCount: 0,
|
|
74
|
+
omittedCount: structured.omittedCount,
|
|
75
|
+
nextQueries: [
|
|
76
|
+
"Try a broader `pattern`, or omit `kind` for plain text search",
|
|
77
|
+
"Narrow `path` if the structured scan was partial",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
content: formatStructuredMatches(params.pattern, params.kind, relScope, structured),
|
|
86
|
+
details: {
|
|
87
|
+
type: "search",
|
|
88
|
+
data: {
|
|
89
|
+
confidence: "structural",
|
|
90
|
+
scope: params.path ?? null,
|
|
91
|
+
candidateCount: structured.matches.length,
|
|
92
|
+
omittedCount: structured.omittedCount,
|
|
93
|
+
nextQueries: [
|
|
94
|
+
"Omit `kind` for plain text matches",
|
|
95
|
+
"Use `summary: true` for broader textual distribution",
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
39
103
|
const matches = params.regex
|
|
40
104
|
? getRegexMatches({
|
|
41
105
|
pattern: params.pattern,
|
|
@@ -78,12 +142,9 @@ export async function executePatternAction(
|
|
|
78
142
|
};
|
|
79
143
|
}
|
|
80
144
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
content = formatPatternResults(params.pattern, relScope, matches, maxResults);
|
|
86
|
-
}
|
|
145
|
+
const content = params.summary
|
|
146
|
+
? formatPatternSummary(params.pattern, relScope, matches, maxResults)
|
|
147
|
+
: formatPatternResults(params.pattern, relScope, matches, maxResults);
|
|
87
148
|
|
|
88
149
|
const details: SearchDetails = {
|
|
89
150
|
confidence: "heuristic",
|
|
@@ -97,6 +158,105 @@ export async function executePatternAction(
|
|
|
97
158
|
return { content, details: { type: "search" as const, data: details } };
|
|
98
159
|
}
|
|
99
160
|
|
|
161
|
+
function formatStructuredEmptyState(
|
|
162
|
+
pattern: string,
|
|
163
|
+
kind: "definition" | "export" | "import",
|
|
164
|
+
relScope: string,
|
|
165
|
+
result: StructuredPatternResult,
|
|
166
|
+
): string {
|
|
167
|
+
const lines = [`No ${kind} matches found for \`${pattern}\` in \`${relScope}\`.`];
|
|
168
|
+
const partialWarning = formatPartialStructuredWarning(result);
|
|
169
|
+
if (partialWarning) {
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push(partialWarning);
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatStructuredMatches(
|
|
177
|
+
pattern: string,
|
|
178
|
+
kind: "definition" | "export" | "import",
|
|
179
|
+
relScope: string,
|
|
180
|
+
result: StructuredPatternResult,
|
|
181
|
+
): string {
|
|
182
|
+
const grouped = new Map<string, StructuredMatch[]>();
|
|
183
|
+
for (const match of result.matches) {
|
|
184
|
+
const group = grouped.get(match.file) ?? [];
|
|
185
|
+
group.push(match);
|
|
186
|
+
grouped.set(match.file, group);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const kindLabel =
|
|
190
|
+
kind === "definition" ? "Definitions" : kind === "export" ? "Exports" : "Imports";
|
|
191
|
+
const lines: string[] = [];
|
|
192
|
+
lines.push(`# Pattern ${kindLabel}: \`${pattern}\``);
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push(
|
|
195
|
+
`**${result.matches.length} match${result.matches.length !== 1 ? "es" : ""}** across **${grouped.size} file${grouped.size !== 1 ? "s" : ""}** in \`${relScope}\``,
|
|
196
|
+
);
|
|
197
|
+
const partialWarning = formatPartialStructuredWarning(result);
|
|
198
|
+
if (partialWarning) {
|
|
199
|
+
lines.push(partialWarning);
|
|
200
|
+
}
|
|
201
|
+
lines.push("");
|
|
202
|
+
|
|
203
|
+
if (kind === "definition" || kind === "export") {
|
|
204
|
+
addDuplicateSummary(lines, result.matches);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const [file, fileMatches] of grouped) {
|
|
208
|
+
lines.push(`### ${file}`);
|
|
209
|
+
for (const match of fileMatches.slice(0, 8)) {
|
|
210
|
+
lines.push(`- \`${match.name}\` (${match.kind}) L${match.line}`);
|
|
211
|
+
}
|
|
212
|
+
if (fileMatches.length > 8) {
|
|
213
|
+
lines.push(`- _+${fileMatches.length - 8} more in this file_`);
|
|
214
|
+
}
|
|
215
|
+
lines.push("");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function formatPartialStructuredWarning(result: StructuredPatternResult): string | null {
|
|
222
|
+
if (!result.partialReason || result.omittedCount <= 0) return null;
|
|
223
|
+
|
|
224
|
+
if (result.partialReason === "timeout") {
|
|
225
|
+
return `_Partial structured results — scan timed out with +${result.omittedCount} file${result.omittedCount !== 1 ? "s" : ""} omitted. Narrow \`path\` or \`pattern\` for complete coverage._`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return `_Partial structured results — +${result.omittedCount} file${result.omittedCount !== 1 ? "s" : ""} omitted after reaching the structured scan cap. Narrow \`path\` or \`pattern\` for complete coverage._`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addDuplicateSummary(lines: string[], matches: StructuredMatch[]): void {
|
|
232
|
+
const byName = new Map<string, Set<string>>();
|
|
233
|
+
for (const match of matches) {
|
|
234
|
+
const files = byName.get(match.name) ?? new Set<string>();
|
|
235
|
+
files.add(match.file);
|
|
236
|
+
byName.set(match.name, files);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const duplicates = [...byName.entries()]
|
|
240
|
+
.map(([name, files]) => ({ name, files: [...files].sort((a, b) => a.localeCompare(b)) }))
|
|
241
|
+
.filter((entry) => entry.files.length > 1)
|
|
242
|
+
.sort((a, b) => b.files.length - a.files.length || a.name.localeCompare(b.name));
|
|
243
|
+
|
|
244
|
+
if (duplicates.length === 0) return;
|
|
245
|
+
|
|
246
|
+
lines.push("## Duplicate Definitions");
|
|
247
|
+
for (const duplicate of duplicates.slice(0, 8)) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`- \`${duplicate.name}\` — defined in ${duplicate.files.length} files: ${duplicate.files
|
|
250
|
+
.map((file) => `\`${file}\``)
|
|
251
|
+
.join(", ")}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (duplicates.length > 8) {
|
|
255
|
+
lines.push(`- _+${duplicates.length - 8} more duplicates_`);
|
|
256
|
+
}
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
|
|
100
260
|
function getRegexMatches(options: {
|
|
101
261
|
pattern: string;
|
|
102
262
|
scopePath: string;
|