@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
@@ -1,97 +1,65 @@
1
1
  # @mrclrchtr/supi-tree-sitter
2
2
 
3
- Tree-sitter structural analysis for the [pi coding agent](https://github.com/earendil-works/pi).
3
+ Structural code analysis for PI your agent parses code, not just text.
4
4
 
5
- This package registers a `tree_sitter` tool and also exports a small TypeScript service API for other SuPi extensions. It is designed as a standalone structural-analysis substrate: it does not depend on `supi-lsp` or semantic language-server tooling, and it remains correct and useful when installed by itself.
5
+ Grep matches strings. Tree-sitter parses structure functions, classes, imports, call chains. The agent stops pattern-matching and starts understanding your code.
6
6
 
7
- ## Install
7
+ ## What you get
8
8
 
9
- ```bash
10
- pi install npm:@mrclrchtr/supi-tree-sitter
11
- ```
9
+ ### See code structure
12
10
 
13
- Standalone installs include the runtime grammar dependencies needed for the supported non-vendored languages. Kotlin and SQL use vendored WASM grammars bundled with this package.
11
+ Extract functions, classes, interfaces, and methods from any file. The agent knows what lives where without reading every line.
14
12
 
15
- It is also bundled by the full SuPi meta-package:
13
+ ### Trace call chains
16
14
 
17
- ```bash
18
- pi install npm:@mrclrchtr/supi
19
- ```
15
+ Find every function call from a given location. The agent follows the code's actual shape instead of guessing from text proximity.
20
16
 
21
- ## Supported files
17
+ ### 14 languages
22
18
 
23
- The runtime can parse these file families:
19
+ JavaScript, TypeScript, Python, Rust, Go, C, C++, Java, Kotlin, Ruby, Bash, HTML, R, SQL — get structural analysis for every project you touch.
24
20
 
25
- - **JavaScript / TypeScript**`.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, `.cjs`
26
- - **Python** — `.py`, `.pyi`
27
- - **Rust** — `.rs`
28
- - **Go** — `.go`, `.mod`
29
- - **C / C++** — `.c`, `.h`, `.cpp`, `.hpp`, `.cc`, `.cxx`, `.hxx`, `.c++`, `.h++`
30
- - **Java** — `.java`
31
- - **Kotlin** — `.kt`, `.kts`
32
- - **Ruby** — `.rb`
33
- - **Bash / Shell** — `.sh`, `.bash`, `.zsh`
34
- - **HTML** — `.html`, `.htm`, `.xhtml`
35
- - **R** — `.r`
36
- - **SQL** — `.sql`
21
+ Works standalone or alongside LSP. Grammar files are vendored no native toolchain required at install time.
22
+
23
+ ## Install
37
24
 
38
- Grammar `.wasm` files are resolved from installed package metadata for npm-shipped grammars, not from repository-relative paths.
25
+ ```bash
26
+ pi install npm:@mrclrchtr/supi-tree-sitter
27
+ ```
39
28
 
40
- ## `tree_sitter` tool
29
+ ## Quick look
41
30
 
42
- Actions:
31
+ The agent gets a `tree_sitter` tool with these actions:
43
32
 
44
- - `outline` list structural declarations such as functions, classes, interfaces, methods, and variables (**currently JavaScript / TypeScript only**)
45
- - `imports` — list import statements and module specifiers (**currently JavaScript / TypeScript only**)
46
- - `exports` list exported declarations, re-exports, and TypeScript `export =` assignments (**currently JavaScript / TypeScript only**)
47
- - `node_at` return the smallest syntax node at a 1-based `line`/`character` position, plus ancestry (all supported grammars)
48
- - `query` run a custom Tree-sitter query and return captures (all supported grammars)
49
- - `callees` find outgoing function/method calls from a position; supports all grammars with a callee query configured
33
+ | Action | What it does |
34
+ |--------|-------------|
35
+ | `outline` | List functions, classes, interfaces in a file |
36
+ | `callees` | Find all function calls from a position |
37
+ | `imports` / `exports` | See what a file imports and exports |
38
+ | `node_at` | Inspect the AST node at any line/column |
39
+ | `query` | Run a custom Tree-sitter query |
50
40
 
51
- Coordinates are 1-based and compatible with the `lsp` tool. `character` is a UTF-16 code-unit column. Relative file paths resolve from the pi session working directory.
41
+ `outline`, `imports`, and `exports` are currently JavaScript/TypeScript only. `node_at`, `query`, and `callees` work across all 14 supported languages. Coordinates are 1-based, matching the `lsp` tool convention.
52
42
 
53
- Large result sets are capped at 100 emitted items per tool response. For outlines, nested children count toward the same cap so deeply nested classes do not bypass the limit.
43
+ ## For extension developers
54
44
 
55
- ## Service API
45
+ This package exports a reusable session-scoped parsing service:
56
46
 
57
47
  ```ts
58
48
  import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
59
49
 
60
- const session = createTreeSitterSession(process.cwd());
61
- try {
62
- const parseable = await session.canParse("src/index.ts");
63
- if (parseable.kind === "success") {
64
- console.log(parseable.data.file, parseable.data.language);
65
- }
66
-
67
- const outline = await session.outline("src/index.ts");
68
- if (outline.kind === "success") {
69
- console.log(outline.data);
70
- }
71
-
72
- const callees = await session.calleesAt("src/index.ts", 1, 10);
73
- if (callees.kind === "success") {
74
- console.log(callees.data.enclosingScope.name, callees.data.callees);
75
- }
76
- } finally {
77
- session.dispose();
78
- }
79
- ```
80
-
81
- `canParse(file)` validates that a supported file can be read and parsed, then returns the resolved file path and grammar id. It does not expose the raw Tree-sitter tree; use `outline`, `query`, `imports`, `exports`, `nodeAt`, or `calleesAt` for structured results.
82
-
83
- `calleesAt(file, line, character)` extracts structural outgoing calls from the enclosing function/method scope at the given position. It returns the enclosing scope name and a deduplicated list of callees with their source ranges.
50
+ const session = createTreeSitterSession("/project");
84
51
 
85
- Exported types include `TreeSitterResult`, `TreeSitterSession`, `OutlineItem`, `ImportRecord`, `ExportRecord`, `NodeAtResult`, `QueryCapture`, `CalleesAtResult`, `SourceRange`, `GrammarId`, and `SupportedExtension`.
52
+ // Check if a file is parseable
53
+ const result = await session.canParse("src/index.ts");
86
54
 
87
- Always call `dispose()` when the session is no longer needed. The runtime lazily initializes grammars, reuses parser instances within a session, deduplicates concurrent first-use grammar initialization, and retries after initialization failures.
55
+ // Get structural outline
56
+ const outline = await session.outline("src/index.ts");
88
57
 
89
- ## Positioning
58
+ // Trace outgoing calls from a position
59
+ const callees = await session.calleesAt("src/index.ts", 42, 10);
90
60
 
91
- `supi-tree-sitter` is the structural-analysis substrate in SuPi's layered code-understanding stack:
92
-
93
- - `supi-tree-sitter` — parser-backed structural analysis (this package)
94
- - `supi-lsp` — live semantic analysis through language servers
95
- - `supi-code-intelligence` — higher-level agent-facing analysis built on top of both substrates
61
+ // Always clean up
62
+ session.dispose();
63
+ ```
96
64
 
97
- Each substrate can be installed and used independently. `supi-tree-sitter` does not require `supi-lsp` to be present, and its prompt guidance is written so it remains correct in standalone installs.
65
+ Import from the package root. No internal imports needed. Call `dispose()` when done.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "1.0.0",
3
+ "version": "1.1.3",
4
4
  "description": "SuPi Tree-sitter extension — structural AST analysis for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,14 +22,6 @@
22
22
  "scripts/*.mjs",
23
23
  "!__tests__"
24
24
  ],
25
- "scripts": {
26
- "vendor:wasm": "node scripts/vendor-wasm.mjs",
27
- "check:wasm": "node scripts/vendor-wasm.mjs --check",
28
- "generate:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs",
29
- "check:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs --check",
30
- "generate:sql-wasm": "node scripts/generate-sql-wasm.mjs",
31
- "check:sql-wasm": "node scripts/generate-sql-wasm.mjs --check"
32
- },
33
25
  "dependencies": {
34
26
  "web-tree-sitter": "^0.26.8"
35
27
  },
@@ -38,30 +30,29 @@
38
30
  "@earendil-works/pi-coding-agent": "*",
39
31
  "typebox": "*"
40
32
  },
41
- "devDependencies": {
42
- "@davisvaughan/tree-sitter-r": "1.2.0",
43
- "@derekstride/tree-sitter-sql": "0.3.11",
44
- "@types/node": "25.6.2",
45
- "tree-sitter-bash": "0.25.1",
46
- "tree-sitter-c": "0.24.1",
47
- "tree-sitter-cli": "0.26.8",
48
- "tree-sitter-cpp": "0.23.4",
49
- "tree-sitter-go": "0.25.0",
50
- "tree-sitter-html": "0.23.2",
51
- "tree-sitter-java": "0.23.5",
52
- "tree-sitter-javascript": "0.25.0",
53
- "tree-sitter-kotlin": "0.3.8",
54
- "tree-sitter-python": "0.25.0",
55
- "tree-sitter-ruby": "0.23.1",
56
- "tree-sitter-rust": "0.24.0",
57
- "tree-sitter-typescript": "0.23.2",
58
- "vitest": "4.1.5",
59
- "@mrclrchtr/supi-test-utils": "workspace:*"
33
+ "peerDependenciesMeta": {
34
+ "@earendil-works/pi-ai": {
35
+ "optional": true
36
+ },
37
+ "@earendil-works/pi-coding-agent": {
38
+ "optional": true
39
+ },
40
+ "typebox": {
41
+ "optional": true
42
+ }
60
43
  },
61
44
  "pi": {
62
45
  "extensions": [
63
46
  "./src/tree-sitter.ts"
64
47
  ]
65
48
  },
66
- "main": "src/index.ts"
49
+ "main": "src/index.ts",
50
+ "scripts": {
51
+ "vendor:wasm": "node scripts/vendor-wasm.mjs",
52
+ "check:wasm": "node scripts/vendor-wasm.mjs --check",
53
+ "generate:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs",
54
+ "check:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs --check",
55
+ "generate:sql-wasm": "node scripts/generate-sql-wasm.mjs",
56
+ "check:sql-wasm": "node scripts/generate-sql-wasm.mjs --check"
57
+ }
67
58
  }
@@ -158,6 +158,12 @@ export class TreeSitterRuntime {
158
158
  if (!queryString || queryString.trim().length === 0) {
159
159
  return { kind: "validation-error", message: "query is required and must be non-empty" };
160
160
  }
161
+ if (queryString.length > MAX_QUERY_LENGTH) {
162
+ return {
163
+ kind: "validation-error",
164
+ message: `query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`,
165
+ };
166
+ }
161
167
 
162
168
  const parseResult = await this.parseFile(filePath);
163
169
  if (parseResult.kind !== "success") return parseResult;
@@ -229,6 +235,9 @@ export class TreeSitterRuntime {
229
235
  }
230
236
  }
231
237
 
238
+ /** Max query string length to prevent ReDoS via overly complex Tree-sitter patterns. */
239
+ const MAX_QUERY_LENGTH = 10_000;
240
+
232
241
  /** Format errors with their cause chain's first message for user-facing tool output. */
233
242
  function formatError(err: unknown, fallback = "Operation failed"): string {
234
243
  if (!(err instanceof Error)) return String(err || fallback);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-intelligence",
3
- "version": "0.2.0",
3
+ "version": "1.1.3",
4
4
  "description": "SuPi Code Intelligence extension — architecture briefs, caller/callee analysis, impact assessment, and pattern search for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,9 +19,9 @@
19
19
  "src/**/*.ts"
20
20
  ],
21
21
  "dependencies": {
22
- "@mrclrchtr/supi-core": "workspace:*",
23
- "@mrclrchtr/supi-lsp": "workspace:*",
24
- "@mrclrchtr/supi-tree-sitter": "workspace:*"
22
+ "@mrclrchtr/supi-core": "1.1.3",
23
+ "@mrclrchtr/supi-lsp": "1.1.3",
24
+ "@mrclrchtr/supi-tree-sitter": "1.1.3"
25
25
  },
26
26
  "bundledDependencies": [
27
27
  "@mrclrchtr/supi-core",
@@ -33,10 +33,16 @@
33
33
  "@earendil-works/pi-coding-agent": "*",
34
34
  "typebox": "*"
35
35
  },
36
- "devDependencies": {
37
- "@types/node": "25.6.2",
38
- "vitest": "4.1.5",
39
- "@mrclrchtr/supi-test-utils": "workspace:*"
36
+ "peerDependenciesMeta": {
37
+ "@earendil-works/pi-ai": {
38
+ "optional": true
39
+ },
40
+ "@earendil-works/pi-coding-agent": {
41
+ "optional": true
42
+ },
43
+ "typebox": {
44
+ "optional": true
45
+ }
40
46
  },
41
47
  "pi": {
42
48
  "extensions": [
@@ -1,8 +1,13 @@
1
1
  // Affected action — blast-radius analysis for a symbol change.
2
+ // biome-ignore-all lint/nursery/noExcessiveLinesPerFile: file-level and single-target affected flows share helpers to keep the blast-radius logic in one place
2
3
 
3
4
  import * as path from "node:path";
4
5
  import { getSessionLspService } from "@mrclrchtr/supi-lsp";
5
6
  import { buildArchitectureModel, findModuleForPath, getDependents } from "../architecture.ts";
7
+ import {
8
+ appendPrioritySignalsSection,
9
+ summarizePrioritySignalsForFiles,
10
+ } from "../prioritization-signals.ts";
6
11
  import { resolveTarget } from "../resolve-target.ts";
7
12
  import {
8
13
  escapeRegex,
@@ -12,6 +17,12 @@ import {
12
17
  runRipgrep,
13
18
  uriToFile,
14
19
  } from "../search-helpers.ts";
20
+ import {
21
+ dedupeFileLineRefs,
22
+ highestConfidence,
23
+ isResolvedTargetGroup,
24
+ } from "../semantic-action-helpers.ts";
25
+ import type { ResolvedTarget, ResolvedTargetGroup } from "../target-resolution.ts";
15
26
  import type { ActionParams } from "../tool-actions.ts";
16
27
  import type { AffectedDetails, CodeIntelResult, ConfidenceMode } from "../types.ts";
17
28
 
@@ -39,14 +50,46 @@ export async function executeAffectedAction(
39
50
  };
40
51
  }
41
52
 
42
- const relPath = path.relative(cwd, target.file);
43
- const symbolName = target.name ?? `symbol at ${relPath}:${target.displayLine}`;
53
+ if (isResolvedTargetGroup(target)) {
54
+ return executeFileLevelAffected(target, params, cwd);
55
+ }
56
+
57
+ const symbolName =
58
+ target.name ?? `symbol at ${path.relative(cwd, target.file)}:${target.displayLine}`;
59
+ return executeSingleAffected(target, symbolName, params, cwd);
60
+ }
61
+
62
+ interface GatheredRef {
63
+ file: string;
64
+ line: number;
65
+ }
66
+
67
+ interface ImpactAnalysis {
68
+ confidence: ConfidenceMode;
69
+ affectedFiles: Set<string>;
70
+ affectedModules: Set<string>;
71
+ downstreamCount: number;
72
+ checkNext: string[];
73
+ likelyTests: string[];
74
+ riskLevel: "low" | "medium" | "high";
75
+ externalRefs: number;
76
+ }
44
77
 
78
+ async function executeSingleAffected(
79
+ target: ResolvedTarget,
80
+ symbolName: string,
81
+ params: ActionParams,
82
+ cwd: string,
83
+ ): Promise<CodeIntelResult> {
45
84
  const refs = await gatherReferences(target, params, cwd);
46
85
  const model = await buildArchitectureModel(cwd);
47
86
  const analysis = analyzeImpact(refs, model, target.name, cwd);
48
87
 
49
- const content = formatAffectedOutput(symbolName, refs, analysis, params);
88
+ const prioritySignals = summarizePrioritySignalsForFiles(
89
+ cwd,
90
+ analysis.affectedFiles.size > 0 ? [...analysis.affectedFiles] : [target.file],
91
+ );
92
+ const content = formatAffectedOutput(symbolName, refs, analysis, params, prioritySignals);
50
93
  const details: AffectedDetails = {
51
94
  confidence: analysis.confidence,
52
95
  directCount: refs.refs.length,
@@ -54,35 +97,94 @@ export async function executeAffectedAction(
54
97
  riskLevel: analysis.riskLevel,
55
98
  checkNext: analysis.checkNext,
56
99
  likelyTests: analysis.likelyTests,
57
- omittedCount:
58
- analysis.externalRefs + (analysis.affectedFiles.size > (params.maxResults ?? 8) ? 1 : 0),
100
+ omittedCount: computeOmittedCount(analysis.externalRefs, analysis.affectedFiles.size, params),
59
101
  nextQueries: [
60
102
  "`code_intel brief` on the most-affected module for deeper context",
61
103
  `\`code_intel callers\` with \`symbol: "${symbolName}"\` for grouped call-site detail`,
62
104
  ],
105
+ prioritySignals,
63
106
  };
64
107
  return { content, details: { type: "affected" as const, data: details } };
65
108
  }
66
109
 
67
- interface GatheredRef {
68
- file: string;
69
- line: number;
70
- }
110
+ async function executeFileLevelAffected(
111
+ targetGroup: ResolvedTargetGroup,
112
+ params: ActionParams,
113
+ cwd: string,
114
+ ): Promise<CodeIntelResult> {
115
+ const perTarget = await Promise.all(
116
+ targetGroup.targets.map(async (target) => ({
117
+ target,
118
+ refs: await gatherReferences(target, params, cwd),
119
+ })),
120
+ );
71
121
 
72
- interface ImpactAnalysis {
73
- confidence: ConfidenceMode;
74
- affectedFiles: Set<string>;
75
- affectedModules: Set<string>;
76
- downstreamCount: number;
77
- checkNext: string[];
78
- likelyTests: string[];
79
- riskLevel: "low" | "medium" | "high";
80
- externalRefs: number;
122
+ const combinedRefs = dedupeFileLineRefs(perTarget.flatMap((entry) => entry.refs.refs));
123
+ const combinedExternal = perTarget.reduce((sum, entry) => sum + entry.refs.externalCount, 0);
124
+ const combinedConfidence = highestConfidence(perTarget.map((entry) => entry.refs.confidence));
125
+ const model = await buildArchitectureModel(cwd);
126
+ const analysis = analyzeImpact(
127
+ { refs: combinedRefs, confidence: combinedConfidence, externalCount: combinedExternal },
128
+ model,
129
+ null,
130
+ cwd,
131
+ );
132
+
133
+ const prioritySignals = summarizePrioritySignalsForFiles(
134
+ cwd,
135
+ analysis.affectedFiles.size > 0 ? [...analysis.affectedFiles] : [targetGroup.file],
136
+ );
137
+
138
+ const lines: string[] = [];
139
+ lines.push(`# Affected: \`${targetGroup.displayName}\``);
140
+ lines.push("");
141
+ const totalRefs = combinedRefs.length + combinedExternal;
142
+ const refSummary =
143
+ combinedExternal > 0
144
+ ? `${totalRefs} refs (${combinedRefs.length} direct + ${combinedExternal} external)`
145
+ : `${totalRefs} ref${totalRefs !== 1 ? "s" : ""}`;
146
+ lines.push(formatFileLevelAffectedHeader(targetGroup.targets.length, refSummary, analysis));
147
+ lines.push("");
148
+
149
+ lines.push("## Exported Targets");
150
+ for (const entry of perTarget) {
151
+ lines.push(
152
+ `- \`${entry.target.name ?? "symbol"}\` — ${entry.refs.refs.length} ref${entry.refs.refs.length !== 1 ? "s" : ""}`,
153
+ );
154
+ }
155
+ lines.push("");
156
+
157
+ addRiskSection(lines, analysis, totalRefs);
158
+ addReferencesSection(lines, combinedRefs, params.maxResults ?? 8);
159
+ appendPrioritySignalsSection(lines, prioritySignals);
160
+ addCheckNextSection(lines, analysis.checkNext);
161
+ addTestsSection(lines, analysis.likelyTests);
162
+ lines.push("## Next");
163
+ lines.push("- `code_intel brief` on the most-affected module for deeper context");
164
+ lines.push("- Use `file` + coordinates to inspect one exported target precisely");
165
+ lines.push("");
166
+
167
+ const details: AffectedDetails = {
168
+ confidence: analysis.confidence,
169
+ directCount: combinedRefs.length,
170
+ downstreamCount: analysis.downstreamCount,
171
+ riskLevel: analysis.riskLevel,
172
+ checkNext: analysis.checkNext,
173
+ likelyTests: analysis.likelyTests,
174
+ omittedCount: computeOmittedCount(analysis.externalRefs, analysis.affectedFiles.size, params),
175
+ nextQueries: [
176
+ "`code_intel brief` on the most-affected module for deeper context",
177
+ "Use `file` + coordinates to inspect one exported target precisely",
178
+ ],
179
+ prioritySignals,
180
+ };
181
+
182
+ return { content: lines.join("\n"), details: { type: "affected" as const, data: details } };
81
183
  }
82
184
 
83
185
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multi-source reference gathering with fallback logic
84
186
  async function gatherReferences(
85
- target: { file: string; position: { line: number; character: number }; name: string | null },
187
+ target: ResolvedTarget,
86
188
  params: ActionParams,
87
189
  cwd: string,
88
190
  ): Promise<{ refs: GatheredRef[]; confidence: ConfidenceMode; externalCount: number }> {
@@ -93,8 +195,6 @@ async function gatherReferences(
93
195
  if (lspState.kind === "ready") {
94
196
  const lspRefs = await lspState.service.references(target.file, target.position);
95
197
  if (lspRefs && lspRefs.length > 0) {
96
- // Filter out the declaration itself — LSP includes it with includeDeclaration.
97
- // The declaration is the symbol being changed, not something affected by the change.
98
198
  const filtered = filterOutDeclaration(lspRefs, target.file, target.position);
99
199
  for (const ref of filtered) {
100
200
  const filePath = uriToFile(ref.uri);
@@ -113,7 +213,9 @@ async function gatherReferences(
113
213
  const pattern = `\\b${escapeRegex(target.name)}\\b`;
114
214
  const matches = runRipgrep(pattern, scopePath, cwd, { maxMatches: 30 });
115
215
  for (const m of matches) {
116
- refs.push({ file: m.file, line: m.line });
216
+ if (!isDeclarationMatch(m.file, m.line, target, cwd)) {
217
+ refs.push({ file: path.relative(cwd, path.resolve(cwd, m.file)), line: m.line });
218
+ }
117
219
  }
118
220
  return { refs, confidence: "heuristic", externalCount: 0 };
119
221
  }
@@ -139,7 +241,6 @@ function analyzeImpact(
139
241
  if (mod) affectedModules.add(mod.name);
140
242
  }
141
243
 
142
- // Transitive downstream: BFS to find all modules reachable through dependents
143
244
  const downstreamModules = new Set<string>();
144
245
  const queue = [...affectedModules];
145
246
  while (queue.length > 0) {
@@ -195,11 +296,13 @@ function assessRisk(
195
296
  return "low";
196
297
  }
197
298
 
299
+ // biome-ignore lint/complexity/useMaxParams: affected formatting keeps related inputs explicit for readability
198
300
  function formatAffectedOutput(
199
301
  symbolName: string,
200
302
  result: { refs: GatheredRef[]; confidence: ConfidenceMode; externalCount: number },
201
303
  analysis: ImpactAnalysis,
202
304
  params: ActionParams,
305
+ prioritySignals: import("../prioritization-signals.ts").PrioritySignalsSummary | null,
203
306
  ): string {
204
307
  const totalRefs = result.refs.length + analysis.externalRefs;
205
308
  const lines: string[] = [];
@@ -215,13 +318,14 @@ function formatAffectedOutput(
215
318
  );
216
319
  if (analysis.externalRefs > 0) {
217
320
  lines.push(
218
- `_External references are not listed individually (node_modules, .pnpm, or out-of-tree)_`,
321
+ "_External references are not listed individually (node_modules, .pnpm, or out-of-tree)_",
219
322
  );
220
323
  }
221
324
  lines.push("");
222
325
 
223
326
  addRiskSection(lines, analysis, totalRefs);
224
327
  addReferencesSection(lines, result.refs, params.maxResults ?? 8);
328
+ appendPrioritySignalsSection(lines, prioritySignals);
225
329
  addCheckNextSection(lines, analysis.checkNext);
226
330
  addTestsSection(lines, analysis.likelyTests);
227
331
  addAffectedNextQueries(lines, symbolName, analysis);
@@ -294,6 +398,22 @@ function addTestsSection(lines: string[], tests: string[]): void {
294
398
  lines.push("");
295
399
  }
296
400
 
401
+ function computeOmittedCount(
402
+ externalRefs: number,
403
+ affectedFileCount: number,
404
+ params: ActionParams,
405
+ ): number {
406
+ return externalRefs + Math.max(0, affectedFileCount - (params.maxResults ?? 8));
407
+ }
408
+
409
+ function formatFileLevelAffectedHeader(
410
+ targetCount: number,
411
+ refSummary: string,
412
+ analysis: ImpactAnalysis,
413
+ ): string {
414
+ return `**Risk: ${analysis.riskLevel.toUpperCase()}** | ${targetCount} exported target${targetCount !== 1 ? "s" : ""} | ${refSummary} | ${analysis.affectedFiles.size} file${analysis.affectedFiles.size !== 1 ? "s" : ""} | ${analysis.affectedModules.size} module${analysis.affectedModules.size !== 1 ? "s" : ""} | ${analysis.downstreamCount} downstream (${analysis.confidence})`;
415
+ }
416
+
297
417
  function addAffectedNextQueries(
298
418
  lines: string[],
299
419
  symbolName: string,
@@ -308,3 +428,12 @@ function addAffectedNextQueries(
308
428
  );
309
429
  lines.push("");
310
430
  }
431
+
432
+ function isDeclarationMatch(
433
+ file: string,
434
+ line: number,
435
+ target: ResolvedTarget,
436
+ cwd: string,
437
+ ): boolean {
438
+ return path.resolve(cwd, file) === target.file && line === target.displayLine;
439
+ }
@@ -5,6 +5,7 @@
5
5
  import * as path from "node:path";
6
6
  import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
7
7
  import { resolveTarget } from "../resolve-target.ts";
8
+ import { isResolvedTargetGroup } from "../semantic-action-helpers.ts";
8
9
  import type { ActionParams } from "../tool-actions.ts";
9
10
  import type { CodeIntelResult, SearchDetails } from "../types.ts";
10
11
 
@@ -29,6 +30,22 @@ export async function executeCalleesAction(
29
30
  };
30
31
  }
31
32
 
33
+ if (isResolvedTargetGroup(target)) {
34
+ return {
35
+ content: `**Error:** File-level callee discovery is not available for \`${path.relative(cwd, target.file)}\`.\n\nProvide \`line\` and \`character\`, or a \`symbol\` for discovery.`,
36
+ details: {
37
+ type: "search" as const,
38
+ data: {
39
+ confidence: "unavailable",
40
+ scope: params.path ?? null,
41
+ candidateCount: 0,
42
+ omittedCount: 0,
43
+ nextQueries: ["Use `file` + coordinates or `symbol` for callee lookup"],
44
+ },
45
+ },
46
+ };
47
+ }
48
+
32
49
  const relPath = path.relative(cwd, target.file);
33
50
  let tsSession: ReturnType<typeof createTreeSitterSession> | null = null;
34
51