@mrclrchtr/supi-code-intelligence 0.1.0
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 +212 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +112 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/capabilities.ts +62 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +229 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +545 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +192 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config.ts +143 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/defaults.json +82 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +82 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +68 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +73 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +98 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +47 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +58 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +359 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +163 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +17 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/lsp-state.ts +82 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +470 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +34 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +139 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +39 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +46 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-types.ts +39 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +83 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +18 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +550 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/overrides.ts +173 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +197 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/renderer.ts +120 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/scanner.ts +153 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +98 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/service-registry.ts +153 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +292 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +153 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +430 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +48 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +156 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/types.ts +409 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/ui.ts +358 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +122 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/workspace-sentinels.ts +114 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +97 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +67 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/.gitkeep +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +126 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +144 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +151 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/callees.ts +343 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +108 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/exports.ts +315 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/formatting.ts +104 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/imports.ts +42 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +16 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +116 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/node-at.ts +96 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/outline.ts +287 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +237 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session.ts +112 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/structure.ts +7 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +13 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +306 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +146 -0
- package/package.json +47 -0
- package/src/actions/affected-action.ts +310 -0
- package/src/actions/brief-action.ts +242 -0
- package/src/actions/callees-action.ts +134 -0
- package/src/actions/callers-action.ts +215 -0
- package/src/actions/implementations-action.ts +190 -0
- package/src/actions/index-action.ts +187 -0
- package/src/actions/pattern-action.ts +232 -0
- package/src/architecture.ts +367 -0
- package/src/brief-focused.ts +383 -0
- package/src/brief.ts +228 -0
- package/src/code-intelligence.ts +122 -0
- package/src/git-context.ts +65 -0
- package/src/guidance.ts +39 -0
- package/src/index.ts +28 -0
- package/src/resolve-target.ts +104 -0
- package/src/search-helpers.ts +283 -0
- package/src/target-resolution.ts +368 -0
- package/src/tool-actions.ts +109 -0
- package/src/types.ts +57 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// Affected action — blast-radius analysis for a symbol change.
|
|
2
|
+
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getSessionLspService } from "@mrclrchtr/supi-lsp";
|
|
5
|
+
import { buildArchitectureModel, findModuleForPath, getDependents } from "../architecture.ts";
|
|
6
|
+
import { resolveTarget } from "../resolve-target.ts";
|
|
7
|
+
import {
|
|
8
|
+
escapeRegex,
|
|
9
|
+
filterOutDeclaration,
|
|
10
|
+
isInProjectPath,
|
|
11
|
+
normalizePath,
|
|
12
|
+
runRipgrep,
|
|
13
|
+
uriToFile,
|
|
14
|
+
} from "../search-helpers.ts";
|
|
15
|
+
import type { ActionParams } from "../tool-actions.ts";
|
|
16
|
+
import type { AffectedDetails, CodeIntelResult, ConfidenceMode } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
export async function executeAffectedAction(
|
|
19
|
+
params: ActionParams,
|
|
20
|
+
cwd: string,
|
|
21
|
+
): Promise<CodeIntelResult> {
|
|
22
|
+
const target = await resolveTarget(params, cwd);
|
|
23
|
+
if (typeof target === "string") {
|
|
24
|
+
return {
|
|
25
|
+
content: target,
|
|
26
|
+
details: {
|
|
27
|
+
type: "affected" as const,
|
|
28
|
+
data: {
|
|
29
|
+
confidence: "unavailable",
|
|
30
|
+
directCount: 0,
|
|
31
|
+
downstreamCount: 0,
|
|
32
|
+
riskLevel: "low",
|
|
33
|
+
checkNext: [],
|
|
34
|
+
likelyTests: [],
|
|
35
|
+
omittedCount: 0,
|
|
36
|
+
nextQueries: ["Provide `file`, `line`, `character` or a `symbol` to resolve the target"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const relPath = path.relative(cwd, target.file);
|
|
43
|
+
const symbolName = target.name ?? `symbol at ${relPath}:${target.displayLine}`;
|
|
44
|
+
|
|
45
|
+
const refs = await gatherReferences(target, params, cwd);
|
|
46
|
+
const model = await buildArchitectureModel(cwd);
|
|
47
|
+
const analysis = analyzeImpact(refs, model, target.name, cwd);
|
|
48
|
+
|
|
49
|
+
const content = formatAffectedOutput(symbolName, refs, analysis, params);
|
|
50
|
+
const details: AffectedDetails = {
|
|
51
|
+
confidence: analysis.confidence,
|
|
52
|
+
directCount: refs.refs.length,
|
|
53
|
+
downstreamCount: analysis.downstreamCount,
|
|
54
|
+
riskLevel: analysis.riskLevel,
|
|
55
|
+
checkNext: analysis.checkNext,
|
|
56
|
+
likelyTests: analysis.likelyTests,
|
|
57
|
+
omittedCount:
|
|
58
|
+
analysis.externalRefs + (analysis.affectedFiles.size > (params.maxResults ?? 8) ? 1 : 0),
|
|
59
|
+
nextQueries: [
|
|
60
|
+
"`code_intel brief` on the most-affected module for deeper context",
|
|
61
|
+
`\`code_intel callers\` with \`symbol: "${symbolName}"\` for grouped call-site detail`,
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
return { content, details: { type: "affected" as const, data: details } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface GatheredRef {
|
|
68
|
+
file: string;
|
|
69
|
+
line: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
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;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multi-source reference gathering with fallback logic
|
|
84
|
+
async function gatherReferences(
|
|
85
|
+
target: { file: string; position: { line: number; character: number }; name: string | null },
|
|
86
|
+
params: ActionParams,
|
|
87
|
+
cwd: string,
|
|
88
|
+
): Promise<{ refs: GatheredRef[]; confidence: ConfidenceMode; externalCount: number }> {
|
|
89
|
+
const lspState = getSessionLspService(cwd);
|
|
90
|
+
const refs: GatheredRef[] = [];
|
|
91
|
+
let externalCount = 0;
|
|
92
|
+
|
|
93
|
+
if (lspState.kind === "ready") {
|
|
94
|
+
const lspRefs = await lspState.service.references(target.file, target.position);
|
|
95
|
+
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
|
+
const filtered = filterOutDeclaration(lspRefs, target.file, target.position);
|
|
99
|
+
for (const ref of filtered) {
|
|
100
|
+
const filePath = uriToFile(ref.uri);
|
|
101
|
+
if (isInProjectPath(filePath, cwd)) {
|
|
102
|
+
refs.push({ file: path.relative(cwd, filePath), line: ref.range.start.line + 1 });
|
|
103
|
+
} else {
|
|
104
|
+
externalCount++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { refs, confidence: "semantic", externalCount };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (target.name) {
|
|
112
|
+
const scopePath = params.path ? normalizePath(params.path, cwd) : cwd;
|
|
113
|
+
const pattern = `\\b${escapeRegex(target.name)}\\b`;
|
|
114
|
+
const matches = runRipgrep(pattern, scopePath, cwd, { maxMatches: 30 });
|
|
115
|
+
for (const m of matches) {
|
|
116
|
+
refs.push({ file: m.file, line: m.line });
|
|
117
|
+
}
|
|
118
|
+
return { refs, confidence: "heuristic", externalCount: 0 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { refs, confidence: "unavailable", externalCount: 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: impact analysis with downstream module traversal
|
|
125
|
+
function analyzeImpact(
|
|
126
|
+
result: { refs: GatheredRef[]; confidence: ConfidenceMode; externalCount: number },
|
|
127
|
+
model: Awaited<ReturnType<typeof buildArchitectureModel>>,
|
|
128
|
+
symbolName: string | null,
|
|
129
|
+
cwd: string,
|
|
130
|
+
): ImpactAnalysis {
|
|
131
|
+
const affectedFiles = new Set(result.refs.map((r) => r.file));
|
|
132
|
+
const affectedModules = new Set<string>();
|
|
133
|
+
const checkNext: string[] = [];
|
|
134
|
+
let downstreamCount = 0;
|
|
135
|
+
|
|
136
|
+
if (model) {
|
|
137
|
+
for (const file of affectedFiles) {
|
|
138
|
+
const mod = findModuleForPath(model, path.resolve(cwd, file));
|
|
139
|
+
if (mod) affectedModules.add(mod.name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Transitive downstream: BFS to find all modules reachable through dependents
|
|
143
|
+
const downstreamModules = new Set<string>();
|
|
144
|
+
const queue = [...affectedModules];
|
|
145
|
+
while (queue.length > 0) {
|
|
146
|
+
const modName = queue.shift() as string;
|
|
147
|
+
for (const dep of getDependents(model, modName)) {
|
|
148
|
+
if (!affectedModules.has(dep.name) && !downstreamModules.has(dep.name)) {
|
|
149
|
+
downstreamModules.add(dep.name);
|
|
150
|
+
queue.push(dep.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
downstreamCount = downstreamModules.size;
|
|
155
|
+
|
|
156
|
+
for (const modName of [...affectedModules, ...downstreamModules].slice(0, 3)) {
|
|
157
|
+
const mod = model.modules.find((m) => m.name === modName);
|
|
158
|
+
if (mod) checkNext.push(`${mod.name.replace(/^@[^/]+\//, "")} (\`${mod.relativePath}\`)`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const likelyTests = findLikelyTests(affectedFiles, symbolName);
|
|
163
|
+
const totalRefs = result.refs.length + result.externalCount;
|
|
164
|
+
const riskLevel = assessRisk(totalRefs, affectedModules.size, downstreamCount);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
confidence: result.confidence,
|
|
168
|
+
affectedFiles,
|
|
169
|
+
affectedModules,
|
|
170
|
+
downstreamCount,
|
|
171
|
+
checkNext,
|
|
172
|
+
likelyTests,
|
|
173
|
+
riskLevel,
|
|
174
|
+
externalRefs: result.externalCount,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function findLikelyTests(affectedFiles: Set<string>, _symbolName: string | null): string[] {
|
|
179
|
+
const tests: string[] = [];
|
|
180
|
+
for (const file of affectedFiles) {
|
|
181
|
+
if (file.includes("test") || file.includes("spec") || file.includes("__tests__")) {
|
|
182
|
+
tests.push(file);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return tests.slice(0, 3);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assessRisk(
|
|
189
|
+
directCount: number,
|
|
190
|
+
moduleCount: number,
|
|
191
|
+
downstreamCount: number,
|
|
192
|
+
): "low" | "medium" | "high" {
|
|
193
|
+
if (directCount > 10 || moduleCount > 3 || downstreamCount > 1) return "high";
|
|
194
|
+
if (directCount > 3 || moduleCount > 1 || downstreamCount >= 1) return "medium";
|
|
195
|
+
return "low";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatAffectedOutput(
|
|
199
|
+
symbolName: string,
|
|
200
|
+
result: { refs: GatheredRef[]; confidence: ConfidenceMode; externalCount: number },
|
|
201
|
+
analysis: ImpactAnalysis,
|
|
202
|
+
params: ActionParams,
|
|
203
|
+
): string {
|
|
204
|
+
const totalRefs = result.refs.length + analysis.externalRefs;
|
|
205
|
+
const lines: string[] = [];
|
|
206
|
+
|
|
207
|
+
lines.push(`# Affected: \`${symbolName}\``);
|
|
208
|
+
lines.push("");
|
|
209
|
+
const refSummary =
|
|
210
|
+
analysis.externalRefs > 0
|
|
211
|
+
? `${totalRefs} refs (${result.refs.length} direct + ${analysis.externalRefs} external)`
|
|
212
|
+
: `${totalRefs} ref${totalRefs !== 1 ? "s" : ""}`;
|
|
213
|
+
lines.push(
|
|
214
|
+
`**Risk: ${analysis.riskLevel.toUpperCase()}** | ${refSummary} | ${analysis.affectedFiles.size} file${analysis.affectedFiles.size !== 1 ? "s" : ""} | ${analysis.affectedModules.size} module${analysis.affectedModules.size !== 1 ? "s" : ""} | ${analysis.downstreamCount} downstream (${analysis.confidence})`,
|
|
215
|
+
);
|
|
216
|
+
if (analysis.externalRefs > 0) {
|
|
217
|
+
lines.push(
|
|
218
|
+
`_External references are not listed individually (node_modules, .pnpm, or out-of-tree)_`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
lines.push("");
|
|
222
|
+
|
|
223
|
+
addRiskSection(lines, analysis, totalRefs);
|
|
224
|
+
addReferencesSection(lines, result.refs, params.maxResults ?? 8);
|
|
225
|
+
addCheckNextSection(lines, analysis.checkNext);
|
|
226
|
+
addTestsSection(lines, analysis.likelyTests);
|
|
227
|
+
addAffectedNextQueries(lines, symbolName, analysis);
|
|
228
|
+
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: risk level formatting with conditional branching
|
|
233
|
+
function addRiskSection(lines: string[], analysis: ImpactAnalysis, directCount: number): void {
|
|
234
|
+
lines.push("## Risk Assessment");
|
|
235
|
+
const { riskLevel, affectedFiles, affectedModules, downstreamCount } = analysis;
|
|
236
|
+
if (riskLevel === "low") {
|
|
237
|
+
lines.push(
|
|
238
|
+
`\`low\` — ${directCount} ref${directCount !== 1 ? "s" : ""}, local, no downstream dependents.`,
|
|
239
|
+
);
|
|
240
|
+
} else if (riskLevel === "medium") {
|
|
241
|
+
lines.push(
|
|
242
|
+
`\`medium\` — ${directCount} ref${directCount !== 1 ? "s" : ""} across ${affectedFiles.size} file${affectedFiles.size !== 1 ? "s" : ""}${downstreamCount > 0 ? `, ${downstreamCount} downstream` : ""}.`,
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
lines.push(
|
|
246
|
+
`\`high\` — ${directCount} ref${directCount !== 1 ? "s" : ""} across ${affectedFiles.size} file${affectedFiles.size !== 1 ? "s" : ""} in ${affectedModules.size} module${affectedModules.size !== 1 ? "s" : ""}${downstreamCount > 0 ? `, ${downstreamCount} downstream` : ""}.`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
lines.push("");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function addReferencesSection(lines: string[], refs: GatheredRef[], maxFiles: number): void {
|
|
253
|
+
if (refs.length === 0) return;
|
|
254
|
+
lines.push("## Direct References");
|
|
255
|
+
const byFile = new Map<string, number[]>();
|
|
256
|
+
for (const ref of refs) {
|
|
257
|
+
const fileLines = byFile.get(ref.file) ?? [];
|
|
258
|
+
fileLines.push(ref.line);
|
|
259
|
+
byFile.set(ref.file, fileLines);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let shown = 0;
|
|
263
|
+
for (const [file, fileLines] of byFile) {
|
|
264
|
+
if (shown >= maxFiles) break;
|
|
265
|
+
const lineStr = fileLines
|
|
266
|
+
.slice(0, 5)
|
|
267
|
+
.map((l) => `L${l}`)
|
|
268
|
+
.join(", ");
|
|
269
|
+
const extra = fileLines.length > 5 ? ` +${fileLines.length - 5} more` : "";
|
|
270
|
+
lines.push(`- \`${file}\` ${lineStr}${extra}`);
|
|
271
|
+
shown++;
|
|
272
|
+
}
|
|
273
|
+
if (byFile.size > maxFiles) {
|
|
274
|
+
lines.push(`- _+${byFile.size - maxFiles} more files omitted_`);
|
|
275
|
+
}
|
|
276
|
+
lines.push("");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function addCheckNextSection(lines: string[], checkNext: string[]): void {
|
|
280
|
+
if (checkNext.length === 0) return;
|
|
281
|
+
lines.push("## Check Next");
|
|
282
|
+
for (const item of checkNext.slice(0, 3)) {
|
|
283
|
+
lines.push(`- ${item}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push("");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function addTestsSection(lines: string[], tests: string[]): void {
|
|
289
|
+
if (tests.length === 0) return;
|
|
290
|
+
lines.push("## Likely Tests");
|
|
291
|
+
for (const t of tests.slice(0, 3)) {
|
|
292
|
+
lines.push(`- \`${t}\``);
|
|
293
|
+
}
|
|
294
|
+
lines.push("");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function addAffectedNextQueries(
|
|
298
|
+
lines: string[],
|
|
299
|
+
symbolName: string,
|
|
300
|
+
analysis: ImpactAnalysis,
|
|
301
|
+
): void {
|
|
302
|
+
lines.push("## Next");
|
|
303
|
+
if (analysis.checkNext.length > 0) {
|
|
304
|
+
lines.push("- `code_intel brief` on the most-affected module for deeper context");
|
|
305
|
+
}
|
|
306
|
+
lines.push(
|
|
307
|
+
`- \`code_intel callers\` with \`symbol: "${symbolName}"\` for grouped call-site detail`,
|
|
308
|
+
);
|
|
309
|
+
lines.push("");
|
|
310
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Brief action — architecture overviews and focused briefs.
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
|
|
6
|
+
import { buildArchitectureModel, findModuleForPath } from "../architecture.ts";
|
|
7
|
+
import { generateFocusedBrief, generateProjectBrief } from "../brief.ts";
|
|
8
|
+
import { normalizePath } from "../search-helpers.ts";
|
|
9
|
+
import type { ActionParams } from "../tool-actions.ts";
|
|
10
|
+
import type { CodeIntelResult } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
export async function executeBriefAction(
|
|
13
|
+
params: ActionParams,
|
|
14
|
+
cwd: string,
|
|
15
|
+
): Promise<CodeIntelResult> {
|
|
16
|
+
const model = await buildArchitectureModel(cwd);
|
|
17
|
+
if (!model) {
|
|
18
|
+
return {
|
|
19
|
+
content:
|
|
20
|
+
"No project structure detected. This directory has no recognizable project metadata or source files.",
|
|
21
|
+
details: {
|
|
22
|
+
type: "brief" as const,
|
|
23
|
+
data: {
|
|
24
|
+
confidence: "unavailable",
|
|
25
|
+
focusTarget: null,
|
|
26
|
+
startHere: [],
|
|
27
|
+
publicSurfaces: [],
|
|
28
|
+
dependencySummary: null,
|
|
29
|
+
omittedCount: 0,
|
|
30
|
+
nextQueries: [
|
|
31
|
+
"Add a package.json or pnpm-workspace.yaml to enable architecture analysis",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (params.file && params.line != null && params.character != null) {
|
|
39
|
+
const content = await executeAnchoredBrief(params, cwd, model);
|
|
40
|
+
const relPath = path.relative(cwd, normalizePath(params.file ?? "", cwd));
|
|
41
|
+
const mod = findModuleForPath(model, path.resolve(cwd, relPath));
|
|
42
|
+
return {
|
|
43
|
+
content,
|
|
44
|
+
details: {
|
|
45
|
+
type: "brief" as const,
|
|
46
|
+
data: {
|
|
47
|
+
confidence: "structural",
|
|
48
|
+
focusTarget: `${relPath}:${params.line}:${params.character}`,
|
|
49
|
+
startHere: [],
|
|
50
|
+
publicSurfaces: [],
|
|
51
|
+
dependencySummary: mod && model ? { moduleCount: 1, edgeCount: 0 } : null,
|
|
52
|
+
omittedCount: 0,
|
|
53
|
+
nextQueries: [
|
|
54
|
+
`\`code_intel callers\` with \`file: "${relPath}", line: ${params.line}, character: ${params.character}\` for call sites`,
|
|
55
|
+
`\`code_intel affected\` with \`file: "${relPath}", line: ${params.line}, character: ${params.character}\` for impact analysis`,
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (params.path) {
|
|
63
|
+
const result = generateFocusedBrief(model, normalizePath(params.path, cwd));
|
|
64
|
+
return { content: result.content, details: { type: "brief" as const, data: result.details } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (params.file) {
|
|
68
|
+
const result = generateFocusedBrief(model, normalizePath(params.file, cwd));
|
|
69
|
+
return { content: result.content, details: { type: "brief" as const, data: result.details } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = generateProjectBrief(model);
|
|
73
|
+
return { content: result.content, details: { type: "brief" as const, data: result.details } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function executeAnchoredBrief(
|
|
77
|
+
params: ActionParams,
|
|
78
|
+
cwd: string,
|
|
79
|
+
model: NonNullable<Awaited<ReturnType<typeof buildArchitectureModel>>>,
|
|
80
|
+
): Promise<string> {
|
|
81
|
+
const file = normalizePath(params.file ?? "", cwd);
|
|
82
|
+
const line1 = params.line ?? 1;
|
|
83
|
+
const char1 = params.character ?? 1;
|
|
84
|
+
const relPath = path.relative(cwd, file);
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(file)) {
|
|
87
|
+
return `**Error:** File not found: \`${params.file}\``;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines: string[] = [];
|
|
91
|
+
lines.push(`# Anchored Brief: ${relPath}:${line1}:${char1}`);
|
|
92
|
+
lines.push("");
|
|
93
|
+
|
|
94
|
+
await addTreeSitterContext({ lines, relPath, line1, char1, cwd });
|
|
95
|
+
addModuleContext(lines, model, file);
|
|
96
|
+
addNextQueries(lines, relPath, line1, char1);
|
|
97
|
+
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface TreeSitterContextInput {
|
|
102
|
+
lines: string[];
|
|
103
|
+
relPath: string;
|
|
104
|
+
line1: number;
|
|
105
|
+
char1: number;
|
|
106
|
+
cwd: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function addTreeSitterContext(input: TreeSitterContextInput): Promise<void> {
|
|
110
|
+
const { lines, relPath, line1, char1, cwd } = input;
|
|
111
|
+
let tsSession: ReturnType<typeof createTreeSitterSession> | null = null;
|
|
112
|
+
try {
|
|
113
|
+
tsSession = createTreeSitterSession(cwd);
|
|
114
|
+
await addNodeContext(lines, tsSession, relPath, { line: line1, char: char1 });
|
|
115
|
+
await addOutlineContext(lines, tsSession, relPath, line1);
|
|
116
|
+
await addImportsContext(lines, tsSession, relPath);
|
|
117
|
+
await addExportsContext(lines, tsSession, relPath);
|
|
118
|
+
} catch {
|
|
119
|
+
// Tree-sitter not available
|
|
120
|
+
} finally {
|
|
121
|
+
tsSession?.dispose();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function addNodeContext(
|
|
126
|
+
lines: string[],
|
|
127
|
+
ts: ReturnType<typeof createTreeSitterSession>,
|
|
128
|
+
relPath: string,
|
|
129
|
+
pos: { line: number; char: number },
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
const result = await ts.nodeAt(relPath, pos.line, pos.char);
|
|
132
|
+
if (result.kind !== "success") return;
|
|
133
|
+
const node = result.data;
|
|
134
|
+
lines.push(
|
|
135
|
+
`**Node:** \`${node.type}\` at ${relPath}:${node.range.startLine}:${node.range.startCharacter}`,
|
|
136
|
+
);
|
|
137
|
+
if (node.text && node.text.length <= 200) {
|
|
138
|
+
lines.push("```");
|
|
139
|
+
lines.push(node.text);
|
|
140
|
+
lines.push("```");
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function addOutlineContext(
|
|
146
|
+
lines: string[],
|
|
147
|
+
ts: ReturnType<typeof createTreeSitterSession>,
|
|
148
|
+
relPath: string,
|
|
149
|
+
line1: number,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const result = await ts.outline(relPath);
|
|
152
|
+
if (result.kind !== "success") return;
|
|
153
|
+
const outline = result.data;
|
|
154
|
+
|
|
155
|
+
const enclosing = outline.find(
|
|
156
|
+
(item) => item.range.startLine <= line1 && item.range.endLine >= line1,
|
|
157
|
+
);
|
|
158
|
+
if (enclosing) {
|
|
159
|
+
lines.push(`**Enclosing symbol:** \`${enclosing.name}\` (${enclosing.kind})`);
|
|
160
|
+
lines.push(`- Range: ${relPath}:${enclosing.range.startLine}–${enclosing.range.endLine}`);
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (outline.length > 0) {
|
|
165
|
+
lines.push("## File Outline");
|
|
166
|
+
const shown = outline.slice(0, 15);
|
|
167
|
+
for (const item of shown) {
|
|
168
|
+
const prefix = getOutlinePrefix(item.kind);
|
|
169
|
+
lines.push(`- ${prefix} \`${item.name}\` (${item.kind}) L${item.range.startLine}`);
|
|
170
|
+
}
|
|
171
|
+
if (outline.length > 15) {
|
|
172
|
+
lines.push(`- _+${outline.length - 15} more declarations_`);
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getOutlinePrefix(kind: string): string {
|
|
179
|
+
if (kind === "function" || kind === "method") return "ƒ";
|
|
180
|
+
if (kind === "class") return "◆";
|
|
181
|
+
return "·";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function addImportsContext(
|
|
185
|
+
lines: string[],
|
|
186
|
+
ts: ReturnType<typeof createTreeSitterSession>,
|
|
187
|
+
relPath: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const result = await ts.imports(relPath);
|
|
190
|
+
if (result.kind !== "success" || result.data.length === 0) return;
|
|
191
|
+
lines.push("## Imports");
|
|
192
|
+
const shown = result.data.slice(0, 10);
|
|
193
|
+
for (const imp of shown) {
|
|
194
|
+
lines.push(`- \`${imp.moduleSpecifier}\``);
|
|
195
|
+
}
|
|
196
|
+
if (result.data.length > 10) {
|
|
197
|
+
lines.push(`- _+${result.data.length - 10} more_`);
|
|
198
|
+
}
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function addExportsContext(
|
|
203
|
+
lines: string[],
|
|
204
|
+
ts: ReturnType<typeof createTreeSitterSession>,
|
|
205
|
+
relPath: string,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const result = await ts.exports(relPath);
|
|
208
|
+
if (result.kind !== "success" || result.data.length === 0) return;
|
|
209
|
+
lines.push("## Exports");
|
|
210
|
+
const shown = result.data.slice(0, 10);
|
|
211
|
+
for (const exp of shown) {
|
|
212
|
+
lines.push(`- \`${exp.name}\` (${exp.kind})`);
|
|
213
|
+
}
|
|
214
|
+
if (result.data.length > 10) {
|
|
215
|
+
lines.push(`- _+${result.data.length - 10} more_`);
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function addModuleContext(
|
|
221
|
+
lines: string[],
|
|
222
|
+
model: NonNullable<Awaited<ReturnType<typeof buildArchitectureModel>>>,
|
|
223
|
+
file: string,
|
|
224
|
+
): void {
|
|
225
|
+
const mod = findModuleForPath(model, file);
|
|
226
|
+
if (mod) {
|
|
227
|
+
const shortName = mod.name.replace(/^@[^/]+\//, "");
|
|
228
|
+
lines.push(`_Module: ${shortName} (\`${mod.relativePath}\`)_`);
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function addNextQueries(lines: string[], relPath: string, line1: number, char1: number): void {
|
|
234
|
+
lines.push("## Next");
|
|
235
|
+
lines.push(
|
|
236
|
+
`- \`code_intel callers\` with \`file: "${relPath}", line: ${line1}, character: ${char1}\` for call sites`,
|
|
237
|
+
);
|
|
238
|
+
lines.push(
|
|
239
|
+
`- \`code_intel affected\` with \`file: "${relPath}", line: ${line1}, character: ${char1}\` for impact analysis`,
|
|
240
|
+
);
|
|
241
|
+
lines.push("");
|
|
242
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Callees action — structural outgoing call map via Tree-sitter.
|
|
2
|
+
// Orchestrates target resolution and formatting; delegates grammar-aware
|
|
3
|
+
// extraction to @mrclrchtr/supi-tree-sitter.
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
|
|
7
|
+
import { resolveTarget } from "../resolve-target.ts";
|
|
8
|
+
import type { ActionParams } from "../tool-actions.ts";
|
|
9
|
+
import type { CodeIntelResult, SearchDetails } from "../types.ts";
|
|
10
|
+
|
|
11
|
+
export async function executeCalleesAction(
|
|
12
|
+
params: ActionParams,
|
|
13
|
+
cwd: string,
|
|
14
|
+
): Promise<CodeIntelResult> {
|
|
15
|
+
const target = await resolveTarget(params, cwd);
|
|
16
|
+
if (typeof target === "string") {
|
|
17
|
+
return {
|
|
18
|
+
content: target,
|
|
19
|
+
details: {
|
|
20
|
+
type: "search" as const,
|
|
21
|
+
data: {
|
|
22
|
+
confidence: "unavailable",
|
|
23
|
+
scope: null,
|
|
24
|
+
candidateCount: 0,
|
|
25
|
+
omittedCount: 0,
|
|
26
|
+
nextQueries: ["Provide `file`, `line`, `character` or a `symbol` to resolve the target"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const relPath = path.relative(cwd, target.file);
|
|
33
|
+
let tsSession: ReturnType<typeof createTreeSitterSession> | null = null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
tsSession = createTreeSitterSession(cwd);
|
|
37
|
+
const result = await tsSession.calleesAt(relPath, target.displayLine, target.displayCharacter);
|
|
38
|
+
|
|
39
|
+
if (result.kind !== "success") {
|
|
40
|
+
return {
|
|
41
|
+
content: noCalleesMessage(relPath, target.displayLine, target.displayCharacter),
|
|
42
|
+
details: {
|
|
43
|
+
type: "search" as const,
|
|
44
|
+
data: {
|
|
45
|
+
confidence: "unavailable",
|
|
46
|
+
scope: null,
|
|
47
|
+
candidateCount: 0,
|
|
48
|
+
omittedCount: 0,
|
|
49
|
+
nextQueries: ["Use `lsp` for type-aware analysis on this file"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { enclosingScope, callees } = result.data;
|
|
56
|
+
|
|
57
|
+
if (callees.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
content: noCalleesMessage(relPath, target.displayLine, target.displayCharacter),
|
|
60
|
+
details: {
|
|
61
|
+
type: "search" as const,
|
|
62
|
+
data: {
|
|
63
|
+
confidence: "structural",
|
|
64
|
+
scope: null,
|
|
65
|
+
candidateCount: 0,
|
|
66
|
+
omittedCount: 0,
|
|
67
|
+
nextQueries: [
|
|
68
|
+
'Use `tree_sitter` with `action: "outline"` to explore the enclosing function',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const content = formatCallees(callees, enclosingScope.name, relPath, params.maxResults ?? 8);
|
|
76
|
+
const details: SearchDetails = {
|
|
77
|
+
confidence: "structural",
|
|
78
|
+
scope: null,
|
|
79
|
+
candidateCount: callees.length,
|
|
80
|
+
omittedCount: Math.max(0, callees.length - (params.maxResults ?? 8)),
|
|
81
|
+
nextQueries: ["Use `lsp` for precise type information on callees"],
|
|
82
|
+
};
|
|
83
|
+
return { content, details: { type: "search" as const, data: details } };
|
|
84
|
+
} catch {
|
|
85
|
+
return {
|
|
86
|
+
content: noCalleesMessage(relPath, target.displayLine, target.displayCharacter),
|
|
87
|
+
details: {
|
|
88
|
+
type: "search" as const,
|
|
89
|
+
data: {
|
|
90
|
+
confidence: "unavailable",
|
|
91
|
+
scope: null,
|
|
92
|
+
candidateCount: 0,
|
|
93
|
+
omittedCount: 0,
|
|
94
|
+
nextQueries: ["Use `lsp` for type-aware analysis on this file"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
} finally {
|
|
99
|
+
tsSession?.dispose();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatCallees(
|
|
104
|
+
callees: Array<{ name: string; range: { startLine: number } }>,
|
|
105
|
+
enclosingName: string,
|
|
106
|
+
relPath: string,
|
|
107
|
+
maxResults: number,
|
|
108
|
+
): string {
|
|
109
|
+
const lines: string[] = [];
|
|
110
|
+
lines.push(`# Callees of \`${enclosingName}\` (structural)`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push(
|
|
113
|
+
`**${callees.length} outgoing call${callees.length > 1 ? "s" : ""}** from \`${enclosingName}\` in \`${relPath}\``,
|
|
114
|
+
);
|
|
115
|
+
lines.push("");
|
|
116
|
+
|
|
117
|
+
const shown = callees.slice(0, maxResults);
|
|
118
|
+
for (const c of shown) {
|
|
119
|
+
lines.push(`- \`${c.name}\` (L${c.range.startLine})`);
|
|
120
|
+
}
|
|
121
|
+
if (callees.length > maxResults) {
|
|
122
|
+
lines.push(`- _+${callees.length - maxResults} more_`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push(
|
|
126
|
+
"_Structural analysis — may include unresolved or qualified names. Use `lsp` for precise type information._",
|
|
127
|
+
);
|
|
128
|
+
lines.push("");
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function noCalleesMessage(relPath: string, line: number, char: number): string {
|
|
133
|
+
return `No callee data available for ${relPath}:${line}:${char}.\n\nUse \`tree_sitter\` with \`action: "callees"\` for structural drill-down.`;
|
|
134
|
+
}
|