@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 +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/search-helpers.ts +18 -0
- 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
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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 (
|
|
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": "
|
|
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": "
|
|
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": "
|
|
23
|
-
"@mrclrchtr/supi-lsp": "
|
|
24
|
-
"@mrclrchtr/supi-tree-sitter": "
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|