@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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getSessionLspService } from "@mrclrchtr/supi-lsp";
|
|
4
|
+
|
|
5
|
+
export interface PrioritySignalsSummary {
|
|
6
|
+
diagnosticsCount: number;
|
|
7
|
+
lowCoverageCount: number;
|
|
8
|
+
unusedCount: number;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface LoadedSignals {
|
|
13
|
+
diagnostics: Array<{ file: string; total: number; errors: number; warnings: number }>;
|
|
14
|
+
coverageByFile: Map<string, number>;
|
|
15
|
+
unusedFiles: Set<string>;
|
|
16
|
+
unusedExports: Array<{ file: string; name: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function summarizePrioritySignalsForFiles(
|
|
20
|
+
cwd: string,
|
|
21
|
+
files: Iterable<string>,
|
|
22
|
+
): PrioritySignalsSummary | null {
|
|
23
|
+
const loaded = loadPrioritizationSignals(cwd);
|
|
24
|
+
const relevantFiles = new Set([...files].map((file) => path.resolve(cwd, file)));
|
|
25
|
+
if (relevantFiles.size === 0) return null;
|
|
26
|
+
|
|
27
|
+
const warnings: string[] = [];
|
|
28
|
+
|
|
29
|
+
const matchingDiagnostics = loaded.diagnostics.filter((entry) =>
|
|
30
|
+
relevantFiles.has(path.resolve(entry.file)),
|
|
31
|
+
);
|
|
32
|
+
const diagnosticsCount = matchingDiagnostics.reduce((sum, entry) => sum + entry.total, 0);
|
|
33
|
+
for (const entry of matchingDiagnostics.slice(0, 3)) {
|
|
34
|
+
warnings.push(
|
|
35
|
+
`Diagnostics: \`${path.relative(cwd, entry.file)}\` (${entry.total} total${entry.errors > 0 ? `, ${entry.errors} errors` : ""}${entry.warnings > 0 ? `, ${entry.warnings} warnings` : ""})`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lowCoverage = [...loaded.coverageByFile.entries()]
|
|
40
|
+
.filter(([file, pct]) => relevantFiles.has(path.resolve(file)) && pct < 50)
|
|
41
|
+
.sort((a, b) => a[1] - b[1]);
|
|
42
|
+
for (const [file, pct] of lowCoverage.slice(0, 3)) {
|
|
43
|
+
warnings.push(`Low coverage: \`${path.relative(cwd, file)}\` (${pct.toFixed(0)}%)`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const unusedFileMatches = [...loaded.unusedFiles]
|
|
47
|
+
.filter((file) => relevantFiles.has(path.resolve(file)))
|
|
48
|
+
.sort((a, b) => a.localeCompare(b));
|
|
49
|
+
for (const file of unusedFileMatches.slice(0, 3)) {
|
|
50
|
+
warnings.push(`Unused file: \`${path.relative(cwd, file)}\``);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const unusedExportMatches = loaded.unusedExports
|
|
54
|
+
.filter((entry) => relevantFiles.has(path.resolve(entry.file)))
|
|
55
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
56
|
+
for (const entry of unusedExportMatches.slice(0, 3)) {
|
|
57
|
+
warnings.push(`Unused export: \`${entry.name}\` in \`${path.relative(cwd, entry.file)}\``);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const summary: PrioritySignalsSummary = {
|
|
61
|
+
diagnosticsCount,
|
|
62
|
+
lowCoverageCount: lowCoverage.length,
|
|
63
|
+
unusedCount: unusedFileMatches.length + unusedExportMatches.length,
|
|
64
|
+
warnings,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return summary.diagnosticsCount > 0 || summary.lowCoverageCount > 0 || summary.unusedCount > 0
|
|
68
|
+
? summary
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function loadPrioritizationSignals(cwd: string): LoadedSignals {
|
|
73
|
+
return {
|
|
74
|
+
diagnostics: loadDiagnostics(cwd),
|
|
75
|
+
coverageByFile: loadCoverageSummary(cwd),
|
|
76
|
+
unusedFiles: loadUnusedFiles(cwd),
|
|
77
|
+
unusedExports: loadUnusedExports(cwd),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function appendPrioritySignalsSection(
|
|
82
|
+
lines: string[],
|
|
83
|
+
summary: PrioritySignalsSummary | null,
|
|
84
|
+
): void {
|
|
85
|
+
if (!summary || summary.warnings.length === 0) return;
|
|
86
|
+
lines.push("## Priority Signals");
|
|
87
|
+
for (const warning of summary.warnings.slice(0, 6)) {
|
|
88
|
+
lines.push(`- ${warning}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadDiagnostics(
|
|
94
|
+
cwd: string,
|
|
95
|
+
): Array<{ file: string; total: number; errors: number; warnings: number }> {
|
|
96
|
+
const lspState = getSessionLspService(cwd);
|
|
97
|
+
if (lspState.kind !== "ready") return [];
|
|
98
|
+
|
|
99
|
+
return lspState.service.getOutstandingDiagnosticSummary(2).map((entry) => ({
|
|
100
|
+
file: path.resolve(cwd, entry.file),
|
|
101
|
+
total: entry.total,
|
|
102
|
+
errors: entry.errors,
|
|
103
|
+
warnings: entry.warnings,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadCoverageSummary(cwd: string): Map<string, number> {
|
|
108
|
+
const coveragePath = path.join(cwd, "coverage", "coverage-summary.json");
|
|
109
|
+
if (!fs.existsSync(coveragePath)) return new Map();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(fs.readFileSync(coveragePath, "utf-8")) as Record<string, unknown>;
|
|
113
|
+
const map = new Map<string, number>();
|
|
114
|
+
for (const [file, value] of Object.entries(parsed)) {
|
|
115
|
+
if (file === "total" || typeof value !== "object" || value === null) continue;
|
|
116
|
+
const linesPct = getPct(value, "lines");
|
|
117
|
+
const statementsPct = getPct(value, "statements");
|
|
118
|
+
const pct = Math.min(linesPct ?? 100, statementsPct ?? 100);
|
|
119
|
+
map.set(path.resolve(cwd, file), pct);
|
|
120
|
+
}
|
|
121
|
+
return map;
|
|
122
|
+
} catch {
|
|
123
|
+
return new Map();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loadUnusedFiles(cwd: string): Set<string> {
|
|
128
|
+
const parsed = loadKnipJson(cwd);
|
|
129
|
+
const values = Array.isArray(parsed?.files) ? parsed.files : [];
|
|
130
|
+
const files = values
|
|
131
|
+
.map((value) => toPathString(value))
|
|
132
|
+
.filter((value): value is string => Boolean(value))
|
|
133
|
+
.map((file) => path.resolve(cwd, file));
|
|
134
|
+
return new Set(files);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadUnusedExports(cwd: string): Array<{ file: string; name: string }> {
|
|
138
|
+
const parsed = loadKnipJson(cwd);
|
|
139
|
+
const values = Array.isArray(parsed?.exports) ? parsed.exports : [];
|
|
140
|
+
const exports: Array<{ file: string; name: string }> = [];
|
|
141
|
+
|
|
142
|
+
for (const value of values) {
|
|
143
|
+
if (typeof value === "string") {
|
|
144
|
+
exports.push({ file: path.resolve(cwd, value), name: path.basename(value) });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (typeof value !== "object" || value === null) continue;
|
|
148
|
+
const record = value as Record<string, unknown>;
|
|
149
|
+
const file = toPathString(record.file) ?? toPathString(record.path);
|
|
150
|
+
const name = typeof record.name === "string" ? record.name : null;
|
|
151
|
+
if (file && name) {
|
|
152
|
+
exports.push({ file: path.resolve(cwd, file), name });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return exports;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loadKnipJson(cwd: string): Record<string, unknown> | null {
|
|
160
|
+
const knipPath = path.join(cwd, "knip.json");
|
|
161
|
+
if (!fs.existsSync(knipPath)) return null;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(fs.readFileSync(knipPath, "utf-8"));
|
|
164
|
+
return typeof parsed === "object" && parsed !== null
|
|
165
|
+
? (parsed as Record<string, unknown>)
|
|
166
|
+
: null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getPct(value: object, key: string): number | null {
|
|
173
|
+
const candidate = (value as Record<string, unknown>)[key];
|
|
174
|
+
if (typeof candidate !== "object" || candidate === null) return null;
|
|
175
|
+
const pct = (candidate as Record<string, unknown>).pct;
|
|
176
|
+
return typeof pct === "number" ? pct : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function toPathString(value: unknown): string | null {
|
|
180
|
+
if (typeof value === "string") return value;
|
|
181
|
+
if (typeof value !== "object" || value === null) return null;
|
|
182
|
+
const record = value as Record<string, unknown>;
|
|
183
|
+
return typeof record.file === "string"
|
|
184
|
+
? record.file
|
|
185
|
+
: typeof record.path === "string"
|
|
186
|
+
? record.path
|
|
187
|
+
: null;
|
|
188
|
+
}
|
package/src/resolve-target.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ResolvedTarget,
|
|
3
|
+
type ResolvedTargetGroup,
|
|
3
4
|
resolveAnchoredTarget,
|
|
5
|
+
resolveFileTargetGroup,
|
|
4
6
|
resolveSymbolTarget,
|
|
5
7
|
} from "./target-resolution.ts";
|
|
6
8
|
import type { ActionParams } from "./tool-actions.ts";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
* Resolve a target from action params. Returns either a
|
|
11
|
+
* Resolve a target from action params. Returns either a target, a file-level target group,
|
|
10
12
|
* or a string error/disambiguation message to return directly.
|
|
11
13
|
*/
|
|
12
14
|
export async function resolveTarget(
|
|
13
15
|
params: ActionParams,
|
|
14
16
|
cwd: string,
|
|
15
|
-
): Promise<ResolvedTarget | string> {
|
|
17
|
+
): Promise<ResolvedTarget | ResolvedTargetGroup | string> {
|
|
16
18
|
if (!params.file && !params.symbol) {
|
|
17
19
|
return "**Error:** Semantic actions require either anchored coordinates (`file`, `line`, `character`) or a `symbol` for discovery.";
|
|
18
20
|
}
|
|
@@ -22,7 +24,7 @@ export async function resolveTarget(
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (params.file && !params.symbol) {
|
|
25
|
-
return
|
|
27
|
+
return resolveByFile(params.file, cwd);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
if (params.symbol) {
|
|
@@ -44,6 +46,12 @@ function resolveAnchored(
|
|
|
44
46
|
return "**Error:** Unexpected disambiguation for anchored target.";
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
async function resolveByFile(file: string, cwd: string): Promise<ResolvedTargetGroup | string> {
|
|
50
|
+
const result = await resolveFileTargetGroup(file, cwd);
|
|
51
|
+
if (result.kind === "error") return result.message;
|
|
52
|
+
return result.group;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
async function resolveBySymbol(
|
|
48
56
|
params: ActionParams,
|
|
49
57
|
cwd: string,
|
package/src/search-helpers.ts
CHANGED
|
@@ -136,6 +136,15 @@ function buildRipgrepArgs(
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function handleRipgrepError(err: unknown, filterLowSignal: boolean): RipgrepRunResult {
|
|
139
|
+
// ENOENT means rg is not installed — surface a clear error instead of silent empty results
|
|
140
|
+
if (isCodeError(err, "ENOENT")) {
|
|
141
|
+
return {
|
|
142
|
+
matches: [],
|
|
143
|
+
error:
|
|
144
|
+
"ripgrep (rg) is not available. Install it (e.g., `apt install ripgrep` or `brew install ripgrep`).",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
if (!isExecError(err)) {
|
|
140
149
|
return { matches: [] };
|
|
141
150
|
}
|
|
@@ -158,6 +167,15 @@ function isExecError(err: unknown): err is { status: number; stdout?: unknown; s
|
|
|
158
167
|
return typeof err === "object" && err !== null && "status" in err;
|
|
159
168
|
}
|
|
160
169
|
|
|
170
|
+
function isCodeError(err: unknown, code: string): boolean {
|
|
171
|
+
return (
|
|
172
|
+
typeof err === "object" &&
|
|
173
|
+
err !== null &&
|
|
174
|
+
"code" in err &&
|
|
175
|
+
(err as { code: unknown }).code === code
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
161
179
|
interface RawRgEvent {
|
|
162
180
|
type: string;
|
|
163
181
|
data?: {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResolvedTarget, ResolvedTargetGroup } from "./target-resolution.ts";
|
|
2
|
+
import type { ConfidenceMode } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
interface FileLineRef {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isResolvedTargetGroup(
|
|
10
|
+
target: ResolvedTarget | ResolvedTargetGroup,
|
|
11
|
+
): target is ResolvedTargetGroup {
|
|
12
|
+
return "targets" in target;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function highestConfidence(confidences: ConfidenceMode[]): ConfidenceMode {
|
|
16
|
+
if (confidences.includes("semantic")) return "semantic";
|
|
17
|
+
if (confidences.includes("structural")) return "structural";
|
|
18
|
+
if (confidences.includes("heuristic")) return "heuristic";
|
|
19
|
+
return "unavailable";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function dedupeFileLineRefs<T extends FileLineRef>(refs: T[]): T[] {
|
|
23
|
+
const deduped = new Map<string, T>();
|
|
24
|
+
for (const ref of refs) {
|
|
25
|
+
deduped.set(`${ref.file}:${ref.line}`, ref);
|
|
26
|
+
}
|
|
27
|
+
return [...deduped.values()];
|
|
28
|
+
}
|
package/src/target-resolution.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Target resolution — resolve symbol references to concrete file positions
|
|
2
2
|
// for semantic actions (callers, callees, implementations, affected).
|
|
3
|
+
// biome-ignore-all lint/nursery/noExcessiveLinesPerFile: anchored, symbol, and file-surface target resolution intentionally live together to share resolution helpers
|
|
3
4
|
|
|
4
5
|
import * as fs from "node:fs";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
import { isWithinOrEqual } from "@mrclrchtr/supi-core";
|
|
7
8
|
import { getSessionLspService, type Position, type SessionLspService } from "@mrclrchtr/supi-lsp";
|
|
9
|
+
import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
|
|
8
10
|
import { escapeRegex, normalizePath } from "./search-helpers.ts";
|
|
11
|
+
import { highestConfidence } from "./semantic-action-helpers.ts";
|
|
9
12
|
import type { ConfidenceMode, DisambiguationCandidate } from "./types.ts";
|
|
10
13
|
|
|
11
14
|
export interface ResolvedTarget {
|
|
@@ -20,6 +23,13 @@ export interface ResolvedTarget {
|
|
|
20
23
|
confidence: ConfidenceMode;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
export interface ResolvedTargetGroup {
|
|
27
|
+
file: string;
|
|
28
|
+
displayName: string;
|
|
29
|
+
targets: ResolvedTarget[];
|
|
30
|
+
confidence: ConfidenceMode;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export type TargetResolutionResult =
|
|
24
34
|
| { kind: "resolved"; target: ResolvedTarget }
|
|
25
35
|
| { kind: "disambiguation"; candidates: DisambiguationCandidate[]; omittedCount: number }
|
|
@@ -73,6 +83,52 @@ export function resolveAnchoredTarget(
|
|
|
73
83
|
};
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a file-only request into a group of actionable targets.
|
|
88
|
+
* Prefers LSP document symbols when available and falls back to Tree-sitter export discovery.
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveFileTargetGroup(
|
|
91
|
+
file: string,
|
|
92
|
+
cwd: string,
|
|
93
|
+
): Promise<{ kind: "resolved"; group: ResolvedTargetGroup } | { kind: "error"; message: string }> {
|
|
94
|
+
const resolvedFile = normalizePath(file, cwd);
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(resolvedFile)) {
|
|
97
|
+
return { kind: "error", message: `File not found: \`${file}\`` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isBinaryFile(resolvedFile)) {
|
|
101
|
+
return {
|
|
102
|
+
kind: "error",
|
|
103
|
+
message: `File type not supported for semantic analysis: \`${file}\`. Try \`code_intel pattern\` for text search.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const relPath = path.relative(cwd, resolvedFile);
|
|
108
|
+
const structuralTargets = await resolveFileTargetsViaTreeSitter(relPath, resolvedFile, cwd);
|
|
109
|
+
const lspTargets = await resolveFileTargetsViaLsp(resolvedFile, cwd, structuralTargets);
|
|
110
|
+
const targets = lspTargets ?? structuralTargets;
|
|
111
|
+
|
|
112
|
+
if (!targets || targets.length === 0) {
|
|
113
|
+
return {
|
|
114
|
+
kind: "error",
|
|
115
|
+
message:
|
|
116
|
+
`**Error:** File-level semantic exploration is not available for \`${file}\`. ` +
|
|
117
|
+
"Provide `line` and `character`, or a `symbol` for discovery.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
kind: "resolved",
|
|
123
|
+
group: {
|
|
124
|
+
file: resolvedFile,
|
|
125
|
+
displayName: relPath,
|
|
126
|
+
targets,
|
|
127
|
+
confidence: highestConfidence(targets.map((target) => target.confidence)),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
76
132
|
/**
|
|
77
133
|
* Resolve a target from symbol discovery — finds matching declarations.
|
|
78
134
|
* Uses LSP workspace symbols when available, falls back to Tree-sitter/text search.
|
|
@@ -271,6 +327,165 @@ async function resolveSymbolViaSearch(
|
|
|
271
327
|
}
|
|
272
328
|
}
|
|
273
329
|
|
|
330
|
+
async function resolveFileTargetsViaLsp(
|
|
331
|
+
resolvedFile: string,
|
|
332
|
+
cwd: string,
|
|
333
|
+
structuralTargets: ResolvedTarget[] | null,
|
|
334
|
+
): Promise<ResolvedTarget[] | null> {
|
|
335
|
+
const lspState = getSessionLspService(cwd);
|
|
336
|
+
if (lspState.kind !== "ready") return null;
|
|
337
|
+
|
|
338
|
+
const symbols = await lspState.service.documentSymbols(resolvedFile);
|
|
339
|
+
if (!symbols || symbols.length === 0) {
|
|
340
|
+
return structuralTargets;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const topLevel = flattenDocumentSymbols(symbols)
|
|
344
|
+
.filter((symbol) => !symbol.container)
|
|
345
|
+
.map((symbol) =>
|
|
346
|
+
createResolvedTarget({
|
|
347
|
+
file: resolvedFile,
|
|
348
|
+
line: symbol.line,
|
|
349
|
+
character: symbol.character,
|
|
350
|
+
name: symbol.name,
|
|
351
|
+
kind: symbol.kind,
|
|
352
|
+
confidence: "semantic",
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (topLevel.length === 0) {
|
|
357
|
+
return structuralTargets;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!structuralTargets || structuralTargets.length === 0) {
|
|
361
|
+
return dedupeTargets(topLevel);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const matched = structuralTargets.map((target) => {
|
|
365
|
+
const byName = topLevel.find((candidate) => candidate.name === target.name);
|
|
366
|
+
return byName ?? target;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return dedupeTargets(matched);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function resolveFileTargetsViaTreeSitter(
|
|
373
|
+
relPath: string,
|
|
374
|
+
resolvedFile: string,
|
|
375
|
+
cwd: string,
|
|
376
|
+
): Promise<ResolvedTarget[] | null> {
|
|
377
|
+
let tsSession: ReturnType<typeof createTreeSitterSession> | null = null;
|
|
378
|
+
try {
|
|
379
|
+
tsSession = createTreeSitterSession(cwd);
|
|
380
|
+
const exportsResult = await tsSession.exports(relPath);
|
|
381
|
+
if (exportsResult.kind !== "success" || exportsResult.data.length === 0) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return dedupeTargets(
|
|
386
|
+
exportsResult.data.map((record) =>
|
|
387
|
+
createResolvedTarget({
|
|
388
|
+
file: resolvedFile,
|
|
389
|
+
line: record.range.startLine,
|
|
390
|
+
character: record.range.startCharacter,
|
|
391
|
+
name: record.name,
|
|
392
|
+
kind: record.kind,
|
|
393
|
+
confidence: "structural",
|
|
394
|
+
}),
|
|
395
|
+
),
|
|
396
|
+
);
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
} finally {
|
|
400
|
+
tsSession?.dispose();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function flattenDocumentSymbols(
|
|
405
|
+
symbols: Array<{
|
|
406
|
+
name: string;
|
|
407
|
+
kind: number;
|
|
408
|
+
selectionRange?: { start: { line: number; character: number } };
|
|
409
|
+
location?: { range: { start: { line: number; character: number } } };
|
|
410
|
+
children?: Array<unknown>;
|
|
411
|
+
}>,
|
|
412
|
+
container: string | null = null,
|
|
413
|
+
): Array<{
|
|
414
|
+
name: string;
|
|
415
|
+
kind: string;
|
|
416
|
+
line: number;
|
|
417
|
+
character: number;
|
|
418
|
+
container: string | null;
|
|
419
|
+
}> {
|
|
420
|
+
const flattened: Array<{
|
|
421
|
+
name: string;
|
|
422
|
+
kind: string;
|
|
423
|
+
line: number;
|
|
424
|
+
character: number;
|
|
425
|
+
container: string | null;
|
|
426
|
+
}> = [];
|
|
427
|
+
|
|
428
|
+
for (const symbol of symbols) {
|
|
429
|
+
const start = symbol.selectionRange?.start ?? symbol.location?.range.start;
|
|
430
|
+
if (!start) continue;
|
|
431
|
+
|
|
432
|
+
flattened.push({
|
|
433
|
+
name: symbol.name,
|
|
434
|
+
kind: symbolKindName(symbol.kind),
|
|
435
|
+
line: start.line + 1,
|
|
436
|
+
character: start.character + 1,
|
|
437
|
+
container,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (Array.isArray(symbol.children) && symbol.children.length > 0) {
|
|
441
|
+
flattened.push(
|
|
442
|
+
...flattenDocumentSymbols(
|
|
443
|
+
symbol.children as Array<{
|
|
444
|
+
name: string;
|
|
445
|
+
kind: number;
|
|
446
|
+
selectionRange?: { start: { line: number; character: number } };
|
|
447
|
+
location?: { range: { start: { line: number; character: number } } };
|
|
448
|
+
children?: Array<unknown>;
|
|
449
|
+
}>,
|
|
450
|
+
symbol.name,
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return flattened;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function createResolvedTarget(input: {
|
|
460
|
+
file: string;
|
|
461
|
+
line: number;
|
|
462
|
+
character: number;
|
|
463
|
+
name: string;
|
|
464
|
+
kind: string | null;
|
|
465
|
+
confidence: ConfidenceMode;
|
|
466
|
+
}): ResolvedTarget {
|
|
467
|
+
return {
|
|
468
|
+
file: input.file,
|
|
469
|
+
position: toZeroBased(input.line, input.character),
|
|
470
|
+
displayLine: input.line,
|
|
471
|
+
displayCharacter: input.character,
|
|
472
|
+
name: input.name,
|
|
473
|
+
kind: input.kind,
|
|
474
|
+
confidence: input.confidence,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function dedupeTargets(targets: ResolvedTarget[]): ResolvedTarget[] {
|
|
479
|
+
const deduped = new Map<string, ResolvedTarget>();
|
|
480
|
+
for (const target of targets) {
|
|
481
|
+
const key = `${target.name ?? ""}:${target.displayLine}:${target.displayCharacter}`;
|
|
482
|
+
if (!deduped.has(key)) {
|
|
483
|
+
deduped.set(key, target);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return [...deduped.values()];
|
|
487
|
+
}
|
|
488
|
+
|
|
274
489
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
275
490
|
|
|
276
491
|
function mapCandidateToDisambiguation(
|
package/src/tool-actions.ts
CHANGED
|
@@ -105,5 +105,13 @@ function validateParams(params: ActionParams, cwd: string): string | null {
|
|
|
105
105
|
return "**Error:** `line` and `character` require `file`.";
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
if (
|
|
109
|
+
params.action === "pattern" &&
|
|
110
|
+
params.kind &&
|
|
111
|
+
!new Set(["definition", "export", "import"]).has(params.kind)
|
|
112
|
+
) {
|
|
113
|
+
return "**Error:** `pattern` action `kind` must be one of `definition`, `export`, or `import`.";
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
return null;
|
|
109
117
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Shared types for code intelligence tool results and metadata.
|
|
2
2
|
|
|
3
|
+
import type { PrioritySignalsSummary } from "./prioritization-signals.ts";
|
|
4
|
+
|
|
3
5
|
/** Confidence vocabulary for result labeling. */
|
|
4
6
|
export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavailable";
|
|
5
7
|
|
|
@@ -12,6 +14,7 @@ export interface BriefDetails {
|
|
|
12
14
|
dependencySummary: { moduleCount: number; edgeCount: number } | null;
|
|
13
15
|
omittedCount: number;
|
|
14
16
|
nextQueries: string[];
|
|
17
|
+
prioritySignals?: PrioritySignalsSummary | null;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/** Structured details metadata for relationship and pattern results. */
|
|
@@ -33,6 +36,7 @@ export interface AffectedDetails {
|
|
|
33
36
|
likelyTests: string[];
|
|
34
37
|
omittedCount: number;
|
|
35
38
|
nextQueries: string[];
|
|
39
|
+
prioritySignals?: PrioritySignalsSummary | null;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/** Disambiguation candidate for ambiguous symbol resolution. */
|