@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.
- package/README.md +31 -187
- package/node_modules/@mrclrchtr/supi-core/package.json +8 -4
- package/node_modules/@mrclrchtr/supi-lsp/README.md +40 -86
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +8 -4
- package/node_modules/@mrclrchtr/supi-lsp/package.json +15 -5
- 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/README.md +38 -70
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +20 -29
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +9 -0
- package/package.json +14 -8
- package/src/actions/affected-action.ts +153 -24
- package/src/actions/callees-action.ts +17 -0
- package/src/actions/callers-action.ts +178 -111
- package/src/actions/implementations-action.ts +18 -0
- package/src/actions/pattern-action.ts +167 -7
- package/src/brief-focused.ts +189 -9
- package/src/code-intelligence.ts +10 -2
- package/src/git-context.ts +11 -0
- package/src/guidance.ts +11 -8
- package/src/pattern-structured.ts +196 -0
- package/src/prioritization-signals.ts +188 -0
- package/src/resolve-target.ts +11 -3
- package/src/semantic-action-helpers.ts +28 -0
- package/src/target-resolution.ts +215 -0
- package/src/tool-actions.ts +8 -0
- package/src/types.ts +4 -0
|
@@ -1,97 +1,65 @@
|
|
|
1
1
|
# @mrclrchtr/supi-tree-sitter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Structural code analysis for PI — your agent parses code, not just text.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## What you get
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
pi install npm:@mrclrchtr/supi-tree-sitter
|
|
11
|
-
```
|
|
9
|
+
### See code structure
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Extract functions, classes, interfaces, and methods from any file. The agent knows what lives where without reading every line.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
### Trace call chains
|
|
16
14
|
|
|
17
|
-
|
|
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
|
-
|
|
17
|
+
### 14 languages
|
|
22
18
|
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
25
|
+
```bash
|
|
26
|
+
pi install npm:@mrclrchtr/supi-tree-sitter
|
|
27
|
+
```
|
|
39
28
|
|
|
40
|
-
##
|
|
29
|
+
## Quick look
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
The agent gets a `tree_sitter` tool with these actions:
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
## For extension developers
|
|
54
44
|
|
|
55
|
-
|
|
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(
|
|
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
|
-
|
|
52
|
+
// Check if a file is parseable
|
|
53
|
+
const result = await session.canParse("src/index.ts");
|
|
86
54
|
|
|
87
|
-
|
|
55
|
+
// Get structural outline
|
|
56
|
+
const outline = await session.outline("src/index.ts");
|
|
88
57
|
|
|
89
|
-
|
|
58
|
+
// Trace outgoing calls from a position
|
|
59
|
+
const callees = await session.calleesAt("src/index.ts", 42, 10);
|
|
90
60
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
42
|
-
"@
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
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": "
|
|
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": "
|
|
23
|
-
"@mrclrchtr/supi-lsp": "
|
|
24
|
-
"@mrclrchtr/supi-tree-sitter": "
|
|
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
|
-
"
|
|
37
|
-
"@
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|