@mrclrchtr/supi-code-intelligence 0.2.0 → 1.1.3

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 (29) hide show
  1. package/README.md +31 -187
  2. package/node_modules/@mrclrchtr/supi-core/package.json +8 -4
  3. package/node_modules/@mrclrchtr/supi-lsp/README.md +40 -86
  4. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +8 -4
  5. package/node_modules/@mrclrchtr/supi-lsp/package.json +15 -5
  6. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +11 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-stale-resync.ts +47 -0
  9. package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +1 -1
  10. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +38 -70
  11. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +20 -29
  12. package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +9 -0
  13. package/package.json +14 -8
  14. package/src/actions/affected-action.ts +153 -24
  15. package/src/actions/callees-action.ts +17 -0
  16. package/src/actions/callers-action.ts +178 -111
  17. package/src/actions/implementations-action.ts +18 -0
  18. package/src/actions/pattern-action.ts +167 -7
  19. package/src/brief-focused.ts +189 -9
  20. package/src/code-intelligence.ts +10 -2
  21. package/src/git-context.ts +11 -0
  22. package/src/guidance.ts +11 -8
  23. package/src/pattern-structured.ts +196 -0
  24. package/src/prioritization-signals.ts +188 -0
  25. package/src/resolve-target.ts +11 -3
  26. package/src/semantic-action-helpers.ts +28 -0
  27. package/src/target-resolution.ts +215 -0
  28. package/src/tool-actions.ts +8 -0
  29. 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
- const maxResults = params.maxResults ?? 5;
40
- const lspState = getSessionLspService(cwd);
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
- if (target.name) {
66
- const result = formatHeuristicCallers(target.name, params, cwd);
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: "heuristic",
57
+ confidence: result.confidence,
69
58
  scope: params.path ?? null,
70
- candidateCount: result.matchCount,
71
- omittedCount: 0,
72
- nextQueries: ["Enable LSP for `semantic` caller accuracy"],
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
- function partitionRefs(
94
- refs: Array<{ uri: string; range: { start: { line: number; character: number } } }>,
95
- cwd: string,
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
- function groupRefsByFile(
110
- refs: Array<{ uri: string; range: { start: { line: number; character: number } } }>,
111
- cwd: string,
112
- ): Map<string, number[]> {
113
- const byFile = new Map<string, number[]>();
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 formatSemanticCallers(
125
- refs: Array<{ uri: string; range: { start: { line: number; character: number } } }>,
126
- name: string | null,
113
+ async function executeFileLevelCallers(
114
+ targetGroup: ResolvedTargetGroup,
115
+ params: ActionParams,
127
116
  cwd: string,
128
- maxResults: number,
129
- ): string {
130
- const { project: projectRefs, external: externalRefs } = partitionRefs(refs, cwd);
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 of \`${name ?? "symbol"}\``);
131
+ lines.push(`# Callers in \`${targetGroup.displayName}\``);
134
132
  lines.push("");
135
133
  lines.push(
136
- `**${projectRefs.length} reference${projectRefs.length !== 1 ? "s" : ""}** (semantic)`,
134
+ `**${targetGroup.targets.length} exported target${targetGroup.targets.length !== 1 ? "s" : ""}** | **${uniqueRefs.length} reference${uniqueRefs.length !== 1 ? "s" : ""}** (${confidence})`,
137
135
  );
138
- if (externalRefs.length > 0) {
139
- const suffix =
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 byFile = groupRefsByFile(projectRefs, cwd);
148
-
149
- let shown = 0;
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 (byFile.size > maxResults) {
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
- return lines.join("\n");
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
- function formatHeuristicCallers(
174
- symbol: string,
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
- ): { content: string; matchCount: number } {
178
- const maxResults = params.maxResults ?? 8;
179
- const scopePath = params.path ? normalizePath(params.path, cwd) : cwd;
180
- const pattern = `\\b${escapeRegex(symbol)}\\b`;
181
- const matches = runRipgrep(pattern, scopePath, cwd, { maxMatches: maxResults * 3 });
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 (matches.length === 0) {
184
- return { content: `No references found for \`${symbol}\` (heuristic).`, matchCount: 0 };
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(`# Callers of \`${symbol}\` (heuristic)`);
227
+ lines.push(`# ${title}`);
189
228
  lines.push("");
190
229
  lines.push(
191
- `**${matches.length} match${matches.length > 1 ? "es" : ""}** — text-search hints, not semantic references`,
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
- const byFile = groupByFile(matches);
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, fileMatches] of byFile) {
245
+ for (const [file, locations] of byFile) {
198
246
  if (shown >= maxResults) break;
199
247
  lines.push(`### ${file}`);
200
- for (const m of fileMatches.slice(0, 3)) {
201
- lines.push(`- L${m.line}: \`${m.text.slice(0, 100)}\``);
248
+ for (const loc of locations.slice(0, 5)) {
249
+ lines.push(`- L${loc}`);
202
250
  }
203
- if (fileMatches.length > 3) {
204
- lines.push(`- _+${fileMatches.length - 3} more_`);
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(`_+${byFile.size - maxResults} more files omitted._`);
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
- return { content: lines.join("\n"), matchCount: matches.length };
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; 18 is acceptable for the use case
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
- let content: string;
82
- if (params.summary) {
83
- content = formatPatternSummary(params.pattern, relScope, matches, maxResults);
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;