@mrclrchtr/supi-code-intelligence 0.1.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 CHANGED
@@ -33,12 +33,15 @@ Scopes: project (no params), package/directory (`path`), file (`file`), or ancho
33
33
 
34
34
  - Project-level brief: module listing, dependency graph, "start here" recommendations, suggested next queries
35
35
  - Focused brief (`path` or anchored symbol): stripped-down version with a single module or symbol focus
36
+ - Non-module directory briefs now recurse into descendant structure, summarize nested source files, and include lightweight public-surface / import-export summaries for JS/TS trees
36
37
  - Now includes git context (branch, dirty files, last commit) when inside a git repository
38
+ - Can surface optional priority signals such as diagnostics, low coverage, and unused-code hints when available
37
39
  - Metadata returned: `BriefDetails` with confidence, focus target, public surfaces, dependency summary
38
40
 
39
41
  ### `callers` — Find call sites for a symbol
40
42
 
41
43
  - LSP-first (references query), falls back to heuristic text search (word-boundary ripgrep)
44
+ - File-only requests now expand across discovered exported targets when possible, so you can ask for callers of a module surface without coordinates
42
45
  - Results grouped by file with ranked, contextual call sites
43
46
  - Confidence labeling: `semantic` (LSP), `heuristic` (text search)
44
47
 
@@ -60,6 +63,8 @@ Before changing exported APIs, shared helpers, config surfaces, or cross-package
60
63
  - Downstream dependents (transitive)
61
64
  - Risk level: `low` | `medium` | `high`
62
65
  - Likely test files
66
+ - File-only requests now expand across discovered exported targets when possible
67
+ - Optional priority signals highlight diagnostics, low coverage, and unused-code hints when available
63
68
  - Returns `AffectedDetails` metadata
64
69
 
65
70
  ### `index` — Factual project map
@@ -83,6 +88,9 @@ Optimized for common agent lookups:
83
88
 
84
89
  - `pattern` is treated as a **literal string by default**
85
90
  - Set `regex: true` to opt into raw ripgrep regex semantics
91
+ - Structured JS/TS searches support `kind: "definition" | "export" | "import"` to avoid regex look-around hacks when you care about declarations instead of raw text lines
92
+ - Definition/export searches include duplicate-definition summaries when the same symbol appears in multiple files
93
+ - Structured scans are capped and may return a partial-result warning when the scope is too large or times out; narrow `path` or `pattern` for complete coverage
86
94
  - Malformed regex input returns an explicit error instead of a misleading "No matches found"
87
95
  - Nearby matches in the same file deduplicate overlapping context lines to reduce token waste
88
96
  - Results grouped with file and context lines
@@ -93,6 +101,7 @@ Examples:
93
101
  ```json
94
102
  { "action": "pattern", "pattern": "sendMessage({", "path": "packages/" }
95
103
  { "action": "pattern", "pattern": "register(Settings|Config)", "path": "packages/", "regex": true }
104
+ { "action": "pattern", "pattern": "payment", "kind": "definition", "path": "src/" }
96
105
  { "action": "pattern", "pattern": "createServerFn", "summary": true }
97
106
  ```
98
107
 
@@ -139,9 +148,9 @@ For no-result and error states, `details` carries `confidence: "unavailable"` or
139
148
  `confidence: "heuristic"` with appropriately zeroed counts, so consumers always
140
149
  get structured metadata back.
141
150
 
142
- - **`brief`** → `BriefDetails` (confidence, focus target, start-here suggestions, public surfaces, dependency summary, omitted count, next queries)
151
+ - **`brief`** → `BriefDetails` (confidence, focus target, start-here suggestions, public surfaces, dependency summary, omitted count, next queries, optional priority signals)
143
152
  - **`search`** → `SearchDetails` (callers/callees/implementations/pattern: confidence, scope, candidate count, omitted count)
144
- - **`affected`** → `AffectedDetails` (direct count, downstream count, risk level, likely tests, check-next list)
153
+ - **`affected`** → `AffectedDetails` (direct count, downstream count, risk level, likely tests, check-next list, optional priority signals)
145
154
 
146
155
  ## Parameter Validation
147
156
 
@@ -149,6 +158,7 @@ The tool enforces these rules and returns explicit error messages:
149
158
 
150
159
  - `line`/`character` require `file`, not `path` — `path` is for scope/focus, `file` anchors a position
151
160
  - `file` that points to a directory is rejected — use `path` for directory scoping
161
+ - `pattern.kind` must be one of `definition`, `export`, or `import`
152
162
  - Unknown actions are rejected with a list of supported actions
153
163
 
154
164
  ## Architecture
@@ -192,9 +202,10 @@ export type { AffectedDetails, BriefDetails, CodeIntelResult, ConfidenceMode, Di
192
202
  These seven guidelines are injected into the system prompt:
193
203
 
194
204
  > - Use `code_intel brief` before editing an unfamiliar package, directory, or file to get architecture context and reduce blind reads.
195
- > - Use `code_intel affected` before changing exported APIs, shared helpers, config surfaces, or cross-package contracts to check blast radius and risk.
196
- > - Use `code_intel callers` before modifying a function to verify all call sites; use `callees` and `implementations` for dependency and interface analysis.
197
- > - Use `code_intel pattern` for bounded, scope-aware text search when the question is textual rather than semantic; it treats patterns as literal strings by default and supports `regex: true` when needed.
205
+ > - Use `code_intel affected` before changing exported APIs, shared helpers, config surfaces, or cross-package contracts to check blast radius and risk; file-only requests now expand across exported targets when possible.
206
+ > - Use `code_intel callers` before modifying a function to verify all call sites; use `callees` and `implementations` for dependency and interface analysis, and use file-only `callers` when you need the export surface of a module.
207
+ > - Use `code_intel pattern` for bounded, scope-aware text search when the question is textual rather than semantic; it treats patterns as literal strings by default, supports `regex: true`, and supports `kind: "definition" | "export" | "import"` for structured searches.
208
+ > - Use `code_intel brief` and `code_intel affected` priority signals to notice diagnostics, low coverage, or unused-code hints before editing risky files.
198
209
  > - Use `code_intel index` for a factual project map (file counts, directory structure, landmark files) when you need to orient yourself in a new codebase.
199
210
  > - After `code_intel` narrows the target, use raw `lsp` and `tree_sitter` tools for precise drill-down on exact symbols, types, or AST nodes.
200
211
  > - Do not prefer `code_intel` over direct file reads or lower-level tools for trivial, already-localized edits or exact symbol/AST drill-down tasks.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "0.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,9 +22,5 @@
22
22
  "@earendil-works/pi-coding-agent": "*",
23
23
  "@earendil-works/pi-tui": "*"
24
24
  },
25
- "devDependencies": {
26
- "@types/node": "^25.6.0",
27
- "vitest": "^4.1.4"
28
- },
29
25
  "main": "src/index.ts"
30
26
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "0.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,9 +22,5 @@
22
22
  "@earendil-works/pi-coding-agent": "*",
23
23
  "@earendil-works/pi-tui": "*"
24
24
  },
25
- "devDependencies": {
26
- "@types/node": "^25.6.0",
27
- "vitest": "^4.1.4"
28
- },
29
25
  "main": "src/index.ts"
30
26
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-lsp",
3
- "version": "0.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "SuPi LSP extension — Language Server Protocol integration for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  "!__tests__"
22
22
  ],
23
23
  "dependencies": {
24
- "@mrclrchtr/supi-core": "workspace:*"
24
+ "@mrclrchtr/supi-core": "1.1.2"
25
25
  },
26
26
  "bundledDependencies": [
27
27
  "@mrclrchtr/supi-core"
@@ -32,10 +32,6 @@
32
32
  "@earendil-works/pi-tui": "*",
33
33
  "typebox": "*"
34
34
  },
35
- "devDependencies": {
36
- "vitest": "^4.1.4",
37
- "@mrclrchtr/supi-test-utils": "workspace:*"
38
- },
39
35
  "pi": {
40
36
  "extensions": [
41
37
  "./src/lsp.ts"
@@ -31,7 +31,7 @@ export function assessStaleDiagnostics(
31
31
  };
32
32
  }
33
33
 
34
- function isLikelyStaleDiagnostic(diagnostic: Diagnostic): boolean {
34
+ export function isLikelyStaleDiagnostic(diagnostic: Diagnostic): boolean {
35
35
  if (diagnostic.severity === undefined) return false;
36
36
  if (diagnostic.code !== undefined) {
37
37
  if (typeof diagnostic.code === "number" && MODULE_RESOLUTION_CODES.has(diagnostic.code)) {
@@ -29,6 +29,7 @@ import {
29
29
  removeLspTool,
30
30
  } from "./lsp-state.ts";
31
31
  import { LspManager } from "./manager/manager.ts";
32
+ import { forceResyncStaleModuleFiles } from "./manager/manager-stale-resync.ts";
32
33
  import { registerLspAwareToolOverrides } from "./overrides.ts";
33
34
  import { registerLspMessageRenderer } from "./renderer.ts";
34
35
  import { scanMissingServers, scanProjectCapabilities, startDetectedServers } from "./scanner.ts";
@@ -305,6 +306,16 @@ function registerBehaviorHandlers(pi: ExtensionAPI, state: LspRuntimeState): voi
305
306
  // Refresh failures must not prevent agent startup
306
307
  }
307
308
  state.manager.pruneMissingFiles();
309
+
310
+ // Force re-open files with module-resolution errors to clear stale
311
+ // diagnostics that persist when the TS server caches by content hash.
312
+ // Must run before the diagnostic summary so fresh results are captured.
313
+ try {
314
+ await forceResyncStaleModuleFiles(state.manager, ctx.cwd);
315
+ } catch {
316
+ // Best-effort: don't fail the agent turn
317
+ }
318
+
308
319
  refreshProjectServers(state);
309
320
  updateLspUi(ctx, state.manager, state.inlineSeverity, state.projectServers);
310
321
 
@@ -0,0 +1,47 @@
1
+ import * as path from "node:path";
2
+ import { isLikelyStaleDiagnostic } from "../diagnostics/stale-diagnostics.ts";
3
+ import type { LspManager } from "./manager.ts";
4
+
5
+ /**
6
+ * Force re-open files with module-resolution errors (e.g., "Cannot find module")
7
+ * to trigger fresh analysis by the language server.
8
+ *
9
+ * The TypeScript server caches diagnostics by file content hash. When a new
10
+ * file is created that resolves an existing file's import, the stale diagnostic
11
+ * persists because the importing file's content hasn't changed. Re-opening the
12
+ * file (didClose + didOpen) forces the server to re-resolve all imports.
13
+ *
14
+ * Returns true if any files were re-synced.
15
+ */
16
+ export async function forceResyncStaleModuleFiles(
17
+ manager: LspManager,
18
+ cwd: string,
19
+ ): Promise<boolean> {
20
+ const outstanding = manager.getOutstandingDiagnostics(1);
21
+ const staleFiles: string[] = [];
22
+
23
+ for (const entry of outstanding) {
24
+ if (entry.diagnostics.some((d) => isLikelyStaleDiagnostic(d))) {
25
+ staleFiles.push(entry.file);
26
+ }
27
+ }
28
+
29
+ if (staleFiles.length === 0) return false;
30
+
31
+ for (const file of staleFiles) {
32
+ const filePath = path.resolve(cwd, file);
33
+ // Close the file to clear cached diagnostics and remove from openDocs
34
+ manager.closeFile(filePath);
35
+ // Re-open to force the server to re-resolve imports
36
+ await manager.ensureFileOpen(filePath);
37
+ }
38
+
39
+ // Re-sync and wait for fresh diagnostics after the re-opens
40
+ try {
41
+ await manager.refreshOpenDiagnostics({ quietMs: 300, maxWaitMs: 2000 });
42
+ } catch {
43
+ // Best-effort: don't fail the agent turn if refresh has issues
44
+ }
45
+
46
+ return true;
47
+ }
@@ -151,7 +151,7 @@ function buildLspSettingItems(
151
151
  id: "exclude",
152
152
  label: "Exclude Patterns",
153
153
  description:
154
- "Gitignore patterns to suppress LSP diagnostics (e.g. __tests__/, *.generated.ts)",
154
+ "Gitignore patterns to suppress LSP diagnostics. Edit .pi/supi/config.json → lsp.exclude (or ~/.pi/agent/supi/config.json for global). Patterns like __tests__/ exclude a directory, *.test.ts wildcards match at any depth, /dist anchors to root.",
155
155
  currentValue: settings.exclude.length > 0 ? settings.exclude.join(", ") : "none",
156
156
  submenu: (_currentValue, done) => createExcludeSubmenu(scope, cwd, settings, done),
157
157
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "0.1.0",
3
+ "version": "1.1.2",
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,18 @@
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.0",
45
- "tree-sitter-bash": "^0.23.0",
46
- "tree-sitter-c": "^0.23.0",
47
- "tree-sitter-cli": "0.22.6",
48
- "tree-sitter-cpp": "^0.23.0",
49
- "tree-sitter-go": "^0.23.0",
50
- "tree-sitter-html": "^0.23.0",
51
- "tree-sitter-java": "^0.23.0",
52
- "tree-sitter-javascript": "^0.23.0",
53
- "tree-sitter-kotlin": "^0.3.8",
54
- "tree-sitter-python": "^0.23.0",
55
- "tree-sitter-ruby": "^0.23.0",
56
- "tree-sitter-rust": "^0.23.0",
57
- "tree-sitter-typescript": "^0.23.0",
58
- "vitest": "^4.1.4",
59
- "@mrclrchtr/supi-test-utils": "workspace:*"
60
- },
61
33
  "pi": {
62
34
  "extensions": [
63
35
  "./src/tree-sitter.ts"
64
36
  ]
65
37
  },
66
- "main": "src/index.ts"
38
+ "main": "src/index.ts",
39
+ "scripts": {
40
+ "vendor:wasm": "node scripts/vendor-wasm.mjs",
41
+ "check:wasm": "node scripts/vendor-wasm.mjs --check",
42
+ "generate:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs",
43
+ "check:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs --check",
44
+ "generate:sql-wasm": "node scripts/generate-sql-wasm.mjs",
45
+ "check:sql-wasm": "node scripts/generate-sql-wasm.mjs --check"
46
+ }
67
47
  }
@@ -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.1.0",
3
+ "version": "1.1.2",
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.2",
23
+ "@mrclrchtr/supi-lsp": "1.1.2",
24
+ "@mrclrchtr/supi-tree-sitter": "1.1.2"
25
25
  },
26
26
  "bundledDependencies": [
27
27
  "@mrclrchtr/supi-core",
@@ -33,11 +33,6 @@
33
33
  "@earendil-works/pi-coding-agent": "*",
34
34
  "typebox": "*"
35
35
  },
36
- "devDependencies": {
37
- "@types/node": "^25.6.0",
38
- "vitest": "^4.1.4",
39
- "@mrclrchtr/supi-test-utils": "workspace:*"
40
- },
41
36
  "pi": {
42
37
  "extensions": [
43
38
  "./src/code-intelligence.ts"
@@ -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