@shanepadgett/tau-agent 0.27.0 → 0.28.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/extensions/context/sync.ts +1 -9
- package/extensions/context-pruning/prune.ts +2 -2
- package/extensions/explore/README.md +13 -21
- package/extensions/explore/ast/adapter.ts +97 -0
- package/extensions/explore/ast/engine.ts +470 -0
- package/extensions/explore/ast/format/ast-search.ts +97 -0
- package/extensions/explore/ast/format/composite.ts +26 -0
- package/extensions/explore/ast/format/context.ts +66 -0
- package/extensions/explore/ast/format/deps.ts +86 -0
- package/extensions/explore/ast/format/discover.ts +85 -0
- package/extensions/explore/ast/format/impact.ts +105 -0
- package/extensions/explore/ast/format/outline.ts +66 -0
- package/extensions/explore/ast/format/relationships.ts +81 -0
- package/extensions/explore/ast/format/show.ts +38 -0
- package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
- package/extensions/explore/ast/grammars/manifest.json +41 -0
- package/extensions/explore/ast/grammars/manifest.ts +99 -0
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/grammars/swift.wasm +0 -0
- package/extensions/explore/ast/graph/file-graph.ts +360 -0
- package/extensions/explore/ast/graph/relationships.ts +597 -0
- package/extensions/explore/ast/identity.ts +119 -0
- package/extensions/explore/ast/ir.ts +130 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
- package/extensions/explore/ast/languages/csharp.ts +493 -0
- package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
- package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
- package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
- package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
- package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
- package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
- package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
- package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
- package/extensions/explore/ast/languages/go.ts +545 -0
- package/extensions/explore/ast/languages/java.ts +412 -0
- package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
- package/extensions/explore/ast/languages/kotlin.ts +611 -0
- package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
- package/extensions/explore/ast/languages/odin.ts +407 -0
- package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
- package/extensions/explore/ast/languages/rust.ts +506 -0
- package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
- package/extensions/explore/ast/languages/swift.ts +466 -0
- package/extensions/explore/ast/languages/tree.ts +220 -0
- package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
- package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
- package/extensions/explore/ast/languages/typescript.ts +716 -0
- package/extensions/explore/ast/markdown.ts +145 -0
- package/extensions/explore/ast/package-surface.ts +42 -0
- package/extensions/explore/ast/queries/ast-search.ts +306 -0
- package/extensions/explore/ast/queries/composite-target.ts +77 -0
- package/extensions/explore/ast/queries/context.ts +406 -0
- package/extensions/explore/ast/queries/discover.ts +427 -0
- package/extensions/explore/ast/queries/impact.ts +250 -0
- package/extensions/explore/ast/queries/outline.ts +203 -0
- package/extensions/explore/ast/queries/show.ts +262 -0
- package/extensions/explore/ast/query.ts +48 -0
- package/extensions/explore/ast/read/hook.ts +125 -0
- package/extensions/explore/ast/read/policy.ts +55 -0
- package/extensions/explore/ast/registry.ts +120 -0
- package/extensions/explore/ast/scan.ts +76 -0
- package/extensions/explore/ast/slice.ts +31 -0
- package/extensions/explore/ast/tools/ast-search.ts +138 -0
- package/extensions/explore/ast/tools/context.ts +126 -0
- package/extensions/explore/ast/tools/deps.ts +72 -0
- package/extensions/explore/ast/tools/discover.ts +208 -0
- package/extensions/explore/ast/tools/impact.ts +135 -0
- package/extensions/explore/ast/tools/outline.ts +200 -0
- package/extensions/explore/ast/tools/relationships.ts +160 -0
- package/extensions/explore/ast/tools/render.ts +121 -0
- package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
- package/extensions/explore/ast/tools/show.ts +116 -0
- package/extensions/explore/guidance.ts +52 -0
- package/extensions/explore/index.ts +82 -92
- package/extensions/explore/read/autoread.ts +118 -0
- package/extensions/explore/settings.ts +49 -31
- package/extensions/explore/traverse.ts +254 -91
- package/extensions/handoff/index.ts +1 -1
- package/extensions/publish/index.ts +18 -85
- package/extensions/publish/publish-ui.ts +0 -4
- package/extensions/soul/README.md +7 -4
- package/extensions/soul/index.ts +11 -5
- package/extensions/soul/prompt.ts +31 -13
- package/extensions/soul/settings.ts +7 -2
- package/extensions/subagent/agents/context-sync.md +6 -10
- package/extensions/subagent/agents/review.md +24 -55
- package/extensions/subagent/agents/scout.md +38 -115
- package/extensions/subagent/run.ts +1 -1
- package/extensions/tau-help/help.md +2 -4
- package/package.json +6 -4
- package/schemas/tau.schema.json +35 -30
- package/{extensions/explore → shared}/autoread.ts +21 -7
- package/shared/events.ts +1 -1
- package/shared/full-file-knowledge.ts +49 -0
- package/extensions/explore/ast-guidance.ts +0 -67
- package/extensions/explore/ast-languages.ts +0 -61
- package/extensions/explore/ast-tools.ts +0 -1994
- package/extensions/explore/ast-worker.ts +0 -1025
- package/extensions/explore/find.ts +0 -147
- package/extensions/explore/full-file-knowledge.ts +0 -234
- package/extensions/explore/grep.ts +0 -749
- package/extensions/explore/limits.ts +0 -22
- package/extensions/explore/ls.ts +0 -113
- package/extensions/explore/orientation-state.ts +0 -280
- package/extensions/explore/path-display.ts +0 -41
- package/extensions/explore/path-tree.ts +0 -150
- package/extensions/explore/read-cache.ts +0 -273
- package/extensions/explore/read-snapshots.ts +0 -57
- package/extensions/explore/read-stats-panel.ts +0 -176
- package/extensions/explore/read-stats.ts +0 -138
- package/extensions/explore/read.ts +0 -372
- package/extensions/explore/result.ts +0 -29
- package/native-bin/darwin-arm64/tau-ast +0 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../../traverse.ts";
|
|
4
|
+
import type { ExploreEngine } from "../engine.ts";
|
|
5
|
+
import type { Candidate, Resolution } from "../identity.ts";
|
|
6
|
+
import { resolveTarget } from "../identity.ts";
|
|
7
|
+
import {
|
|
8
|
+
isCallableLike,
|
|
9
|
+
isTypeLike,
|
|
10
|
+
type CallSite,
|
|
11
|
+
type Decl,
|
|
12
|
+
type DeclKind,
|
|
13
|
+
type FileIr,
|
|
14
|
+
type ImportRef,
|
|
15
|
+
} from "../ir.ts";
|
|
16
|
+
import { walkDecls } from "../query.ts";
|
|
17
|
+
import { scanSources } from "../scan.ts";
|
|
18
|
+
import { signatureText } from "../slice.ts";
|
|
19
|
+
import type { ExploreFileGraph, FileGraphEdge } from "./file-graph.ts";
|
|
20
|
+
|
|
21
|
+
export type RelationshipOp = "callers" | "callees" | "references" | "implementations";
|
|
22
|
+
|
|
23
|
+
export type RelationshipSiteKind =
|
|
24
|
+
| "call"
|
|
25
|
+
| "construct"
|
|
26
|
+
| "macro"
|
|
27
|
+
| "super"
|
|
28
|
+
| "import"
|
|
29
|
+
| "reExport"
|
|
30
|
+
| "implementation"
|
|
31
|
+
| "override"
|
|
32
|
+
| "reference";
|
|
33
|
+
|
|
34
|
+
export type RelationshipCertainty = "exact" | "inferred" | "ambiguous";
|
|
35
|
+
|
|
36
|
+
export type RelationshipSite = {
|
|
37
|
+
path: string;
|
|
38
|
+
line: number;
|
|
39
|
+
kind: RelationshipSiteKind;
|
|
40
|
+
name: string;
|
|
41
|
+
preview: string;
|
|
42
|
+
certainty: RelationshipCertainty;
|
|
43
|
+
competitors: Candidate[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type RelationshipTarget = {
|
|
47
|
+
path: string;
|
|
48
|
+
name: string;
|
|
49
|
+
qualifiedName: string;
|
|
50
|
+
kind: DeclKind;
|
|
51
|
+
startLine: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type RelationshipQueryResult =
|
|
55
|
+
| {
|
|
56
|
+
kind: "resolved";
|
|
57
|
+
target: RelationshipTarget;
|
|
58
|
+
sites: RelationshipSite[];
|
|
59
|
+
resultLimit: number;
|
|
60
|
+
resultLimitReached: boolean;
|
|
61
|
+
parseDegraded: boolean;
|
|
62
|
+
}
|
|
63
|
+
| { kind: "candidates"; candidates: Candidate[] }
|
|
64
|
+
| { kind: "notFound" }
|
|
65
|
+
| { kind: "error"; message: string };
|
|
66
|
+
|
|
67
|
+
type DeclRef = {
|
|
68
|
+
path: string;
|
|
69
|
+
decl: Decl;
|
|
70
|
+
ir: FileIr;
|
|
71
|
+
source: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type FileBundle = {
|
|
75
|
+
path: string;
|
|
76
|
+
ir: FileIr;
|
|
77
|
+
source: string;
|
|
78
|
+
localByName: Map<string, DeclRef[]>;
|
|
79
|
+
importByLocal: Map<string, ImportRef>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type EdgeHit = {
|
|
83
|
+
from: DeclRef;
|
|
84
|
+
site: CallSite;
|
|
85
|
+
targets: DeclRef[];
|
|
86
|
+
certainty: RelationshipCertainty;
|
|
87
|
+
competitors: Candidate[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const MAX_COMPETITORS = 10;
|
|
91
|
+
|
|
92
|
+
function ownerName(qualifiedName: string): string {
|
|
93
|
+
const dot = qualifiedName.lastIndexOf(".");
|
|
94
|
+
if (dot <= 0) return "";
|
|
95
|
+
return qualifiedName.slice(0, dot).split(".").pop() ?? "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sameDecl(a: DeclRef, b: DeclRef): boolean {
|
|
99
|
+
return a.path === b.path && a.decl.startOffset === b.decl.startOffset && a.decl.name === b.decl.name;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Same callable name in same file — overloads bind as one symbol for edge matching. */
|
|
103
|
+
function sameOverloadGroup(a: DeclRef, b: DeclRef): boolean {
|
|
104
|
+
return (
|
|
105
|
+
a.path === b.path && a.decl.name === b.decl.name && isCallableLike(a.decl.kind) && isCallableLike(b.decl.kind)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function allSameName(refs: readonly DeclRef[]): boolean {
|
|
110
|
+
const first = refs[0];
|
|
111
|
+
if (first === undefined) return false;
|
|
112
|
+
return refs.every((ref) => ref.decl.name === first.decl.name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toCandidate(ref: DeclRef): Candidate {
|
|
116
|
+
return {
|
|
117
|
+
path: ref.path,
|
|
118
|
+
name: ref.decl.name,
|
|
119
|
+
qualifiedName: ref.decl.qualifiedName,
|
|
120
|
+
kind: ref.decl.kind,
|
|
121
|
+
startLine: ref.decl.startLine,
|
|
122
|
+
endLine: ref.decl.endLine,
|
|
123
|
+
signature: signatureText(ref.decl, ref.source),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function linePreview(source: string, line: number): string {
|
|
128
|
+
if (line < 1) return "";
|
|
129
|
+
let current = 1;
|
|
130
|
+
let start = 0;
|
|
131
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
132
|
+
const code = source.charCodeAt(i);
|
|
133
|
+
if (code === 10) {
|
|
134
|
+
if (current === line) return source.slice(start, i).trim();
|
|
135
|
+
current += 1;
|
|
136
|
+
start = i + 1;
|
|
137
|
+
} else if (code === 13) {
|
|
138
|
+
const end = i;
|
|
139
|
+
if (source.charCodeAt(i + 1) === 10) i += 1;
|
|
140
|
+
if (current === line) return source.slice(start, end).trim();
|
|
141
|
+
current += 1;
|
|
142
|
+
start = i + 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (current === line) return source.slice(start).trim();
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectDeclRefs(path: string, ir: FileIr, source: string): DeclRef[] {
|
|
150
|
+
const out: DeclRef[] = [];
|
|
151
|
+
walkDecls(ir.decls, (decl) => {
|
|
152
|
+
out.push({ path, decl, ir, source });
|
|
153
|
+
});
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildBundle(path: string, ir: FileIr, source: string): FileBundle {
|
|
158
|
+
const refs = collectDeclRefs(path, ir, source);
|
|
159
|
+
const localByName = new Map<string, DeclRef[]>();
|
|
160
|
+
for (const ref of refs) {
|
|
161
|
+
const list = localByName.get(ref.decl.name);
|
|
162
|
+
if (list === undefined) localByName.set(ref.decl.name, [ref]);
|
|
163
|
+
else list.push(ref);
|
|
164
|
+
}
|
|
165
|
+
const importByLocal = new Map<string, ImportRef>();
|
|
166
|
+
for (const imp of ir.imports) {
|
|
167
|
+
for (const binding of imp.bindings) {
|
|
168
|
+
importByLocal.set(binding.local, imp);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { path, ir, source, localByName, importByLocal };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function internalTargets(edges: readonly FileGraphEdge[]): Set<string> {
|
|
175
|
+
const out = new Set<string>();
|
|
176
|
+
for (const edge of edges) {
|
|
177
|
+
if (edge.kind === "internal") out.add(edge.to);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function pickByDeps(
|
|
183
|
+
candidates: readonly DeclRef[],
|
|
184
|
+
depPaths: ReadonlySet<string>,
|
|
185
|
+
): { targets: DeclRef[]; certainty: RelationshipCertainty; competitors: Candidate[] } {
|
|
186
|
+
const inClosure = candidates.filter((ref) => depPaths.has(ref.path));
|
|
187
|
+
if (inClosure.length === 1) {
|
|
188
|
+
const only = inClosure[0];
|
|
189
|
+
if (only !== undefined) return { targets: [only], certainty: "exact", competitors: [] };
|
|
190
|
+
}
|
|
191
|
+
if (inClosure.length > 1) {
|
|
192
|
+
// Overloads of one name bind together; mixed names stay ambiguous.
|
|
193
|
+
if (allSameName(inClosure)) return { targets: [...inClosure], certainty: "exact", competitors: [] };
|
|
194
|
+
return {
|
|
195
|
+
targets: [],
|
|
196
|
+
certainty: "ambiguous",
|
|
197
|
+
competitors: inClosure.slice(0, MAX_COMPETITORS).map(toCandidate),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (candidates.length === 1) {
|
|
201
|
+
const only = candidates[0];
|
|
202
|
+
if (only !== undefined) return { targets: [only], certainty: "inferred", competitors: [] };
|
|
203
|
+
}
|
|
204
|
+
if (candidates.length > 1) {
|
|
205
|
+
if (allSameName(candidates)) return { targets: [...candidates], certainty: "inferred", competitors: [] };
|
|
206
|
+
return {
|
|
207
|
+
targets: [],
|
|
208
|
+
certainty: "ambiguous",
|
|
209
|
+
competitors: candidates.slice(0, MAX_COMPETITORS).map(toCandidate),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { targets: [], certainty: "inferred", competitors: [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveCallName(
|
|
216
|
+
name: string,
|
|
217
|
+
receiver: string,
|
|
218
|
+
caller: FileBundle,
|
|
219
|
+
globalByName: Map<string, DeclRef[]>,
|
|
220
|
+
callerDeps: ReadonlySet<string>,
|
|
221
|
+
): { targets: DeclRef[]; certainty: RelationshipCertainty; competitors: Candidate[] } {
|
|
222
|
+
const local = caller.localByName.get(name);
|
|
223
|
+
if (local !== undefined && local.length === 1) {
|
|
224
|
+
const only = local[0];
|
|
225
|
+
if (only !== undefined) return { targets: [only], certainty: "exact", competitors: [] };
|
|
226
|
+
}
|
|
227
|
+
if (local !== undefined && local.length > 1) {
|
|
228
|
+
if (allSameName(local)) return { targets: [...local], certainty: "exact", competitors: [] };
|
|
229
|
+
return {
|
|
230
|
+
targets: [],
|
|
231
|
+
certainty: "ambiguous",
|
|
232
|
+
competitors: local.slice(0, MAX_COMPETITORS).map(toCandidate),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (caller.importByLocal.has(name)) {
|
|
237
|
+
return pickByDeps(globalByName.get(name) ?? [], callerDeps);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const global = globalByName.get(name) ?? [];
|
|
241
|
+
// Receiver-bearing: never unique-global Exact (blocks obj.foo → random foo).
|
|
242
|
+
if (receiver.length > 0) return pickByDeps(global, callerDeps);
|
|
243
|
+
if (global.length === 1) {
|
|
244
|
+
const only = global[0];
|
|
245
|
+
if (only !== undefined) return { targets: [only], certainty: "exact", competitors: [] };
|
|
246
|
+
}
|
|
247
|
+
if (global.length > 1 && allSameName(global)) {
|
|
248
|
+
// Same-name overloads across files still need dep evidence for Exact.
|
|
249
|
+
return pickByDeps(global, callerDeps);
|
|
250
|
+
}
|
|
251
|
+
return pickByDeps(global, callerDeps);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function siteKindFromCall(site: CallSite): RelationshipSiteKind {
|
|
255
|
+
if (site.kind === "construct") return "construct";
|
|
256
|
+
if (site.kind === "macro") return "macro";
|
|
257
|
+
if (site.kind === "super") return "super";
|
|
258
|
+
return "call";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function makeSite(
|
|
262
|
+
path: string,
|
|
263
|
+
source: string,
|
|
264
|
+
line: number,
|
|
265
|
+
kind: RelationshipSiteKind,
|
|
266
|
+
name: string,
|
|
267
|
+
certainty: RelationshipCertainty,
|
|
268
|
+
competitors: Candidate[],
|
|
269
|
+
): RelationshipSite {
|
|
270
|
+
return {
|
|
271
|
+
path,
|
|
272
|
+
line,
|
|
273
|
+
kind,
|
|
274
|
+
name,
|
|
275
|
+
preview: linePreview(source, line),
|
|
276
|
+
certainty,
|
|
277
|
+
competitors,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sortSites(sites: RelationshipSite[]): RelationshipSite[] {
|
|
282
|
+
return [...sites].sort((a, b) => {
|
|
283
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
284
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
285
|
+
if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
|
|
286
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function dedupeSites(sites: readonly RelationshipSite[]): RelationshipSite[] {
|
|
291
|
+
const seen = new Set<string>();
|
|
292
|
+
const out: RelationshipSite[] = [];
|
|
293
|
+
for (const site of sites) {
|
|
294
|
+
const key = `${site.path}|${site.line}|${site.kind}|${site.name}|${site.certainty}`;
|
|
295
|
+
if (seen.has(key)) continue;
|
|
296
|
+
seen.add(key);
|
|
297
|
+
out.push(site);
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function pathIsDirectory(path: string): Promise<boolean> {
|
|
303
|
+
try {
|
|
304
|
+
return (await lstat(path)).isDirectory();
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isWithinScope(scopeDir: string, filePath: string): boolean {
|
|
311
|
+
const rel = relative(resolve(scopeDir), resolve(filePath));
|
|
312
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function edgeHitsTarget(edge: EdgeHit, target: DeclRef): boolean {
|
|
316
|
+
if (edge.targets.some((ref) => sameDecl(ref, target) || sameOverloadGroup(ref, target))) return true;
|
|
317
|
+
if (edge.site.name !== target.decl.name) return false;
|
|
318
|
+
return edge.competitors.some((c) => c.path === target.path && c.name === target.decl.name);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function pushEdge(sites: RelationshipSite[], edge: EdgeHit): void {
|
|
322
|
+
sites.push(
|
|
323
|
+
makeSite(
|
|
324
|
+
edge.from.path,
|
|
325
|
+
edge.from.source,
|
|
326
|
+
edge.site.line,
|
|
327
|
+
siteKindFromCall(edge.site),
|
|
328
|
+
edge.site.name,
|
|
329
|
+
edge.certainty,
|
|
330
|
+
edge.competitors,
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export type RelationshipQueryArgs = {
|
|
336
|
+
engine: ExploreEngine;
|
|
337
|
+
graph: ExploreFileGraph;
|
|
338
|
+
scopePath: string;
|
|
339
|
+
op: RelationshipOp;
|
|
340
|
+
targetPath: string | undefined;
|
|
341
|
+
name: string;
|
|
342
|
+
line: number | undefined;
|
|
343
|
+
resultLimit: number;
|
|
344
|
+
signal: AbortSignal;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* One pipeline for callers / callees / references / implementations.
|
|
349
|
+
* Uses adapter-filled CallSite / bases / import bindings on IR — no language ids here.
|
|
350
|
+
*/
|
|
351
|
+
export async function queryRelationships(args: RelationshipQueryArgs): Promise<RelationshipQueryResult> {
|
|
352
|
+
const { engine, graph, op, name, line, resultLimit, signal } = args;
|
|
353
|
+
let scopeDir: string;
|
|
354
|
+
try {
|
|
355
|
+
scopeDir = resolveExplorePath(engine.cwd, args.scopePath);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { kind: "error", message: pathResolutionError(error, args.scopePath).message };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!(await pathIsDirectory(scopeDir))) {
|
|
361
|
+
return {
|
|
362
|
+
kind: "error",
|
|
363
|
+
message: `Scope path must be a directory: ${formatPathForDisplay(scopeDir, engine.cwd)}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const resolution: Resolution = await resolveTarget(engine, scopeDir, { path: args.targetPath, name, line }, signal);
|
|
368
|
+
if (resolution.kind === "candidates") return { kind: "candidates", candidates: resolution.candidates };
|
|
369
|
+
if (resolution.kind === "notFound") return { kind: "notFound" };
|
|
370
|
+
|
|
371
|
+
const target: DeclRef = {
|
|
372
|
+
path: resolution.path,
|
|
373
|
+
decl: resolution.decl,
|
|
374
|
+
ir: resolution.ir,
|
|
375
|
+
source: resolution.source,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (!isWithinScope(scopeDir, target.path)) {
|
|
379
|
+
return {
|
|
380
|
+
kind: "error",
|
|
381
|
+
message: `Declaration is outside scope: ${formatPathForDisplay(target.path, engine.cwd)}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const targetAdapter = engine.registry.adapterForPath(target.path);
|
|
386
|
+
if (targetAdapter === undefined || !targetAdapter.capabilities.callEdges) {
|
|
387
|
+
return {
|
|
388
|
+
kind: "error",
|
|
389
|
+
message: `Relationship edges unavailable for ${targetAdapter?.id ?? "this language"}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const bundles: FileBundle[] = [];
|
|
394
|
+
const globalByName = new Map<string, DeclRef[]>();
|
|
395
|
+
let parseDegraded = resolution.ir.parseDegraded;
|
|
396
|
+
|
|
397
|
+
const scan = scanSources({ engine, cwd: engine.cwd, root: scopeDir, signal });
|
|
398
|
+
for await (const file of scan) {
|
|
399
|
+
signal.throwIfAborted();
|
|
400
|
+
const adapter = engine.registry.adapterForPath(file.ir.path);
|
|
401
|
+
if (adapter === undefined || !adapter.capabilities.callEdges) continue;
|
|
402
|
+
if (file.ir.parseDegraded) parseDegraded = true;
|
|
403
|
+
bundles.push(buildBundle(file.ir.path, file.ir, file.source));
|
|
404
|
+
for (const ref of collectDeclRefs(file.ir.path, file.ir, file.source)) {
|
|
405
|
+
const list = globalByName.get(ref.decl.name);
|
|
406
|
+
if (list === undefined) globalByName.set(ref.decl.name, [ref]);
|
|
407
|
+
else list.push(ref);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const depCache = new Map<string, Set<string>>();
|
|
412
|
+
const depsOf = async (path: string): Promise<Set<string>> => {
|
|
413
|
+
const cached = depCache.get(path);
|
|
414
|
+
if (cached !== undefined) return cached;
|
|
415
|
+
const edges = await graph.forwardEdges(path, signal);
|
|
416
|
+
const set = internalTargets(edges);
|
|
417
|
+
depCache.set(path, set);
|
|
418
|
+
return set;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const edges: EdgeHit[] = [];
|
|
422
|
+
for (const bundle of bundles) {
|
|
423
|
+
signal.throwIfAborted();
|
|
424
|
+
const deps = await depsOf(bundle.path);
|
|
425
|
+
for (const from of collectDeclRefs(bundle.path, bundle.ir, bundle.source)) {
|
|
426
|
+
for (const site of from.decl.calls) {
|
|
427
|
+
const resolved = resolveCallName(site.name, site.receiver, bundle, globalByName, deps);
|
|
428
|
+
edges.push({
|
|
429
|
+
from,
|
|
430
|
+
site,
|
|
431
|
+
targets: resolved.targets,
|
|
432
|
+
certainty: resolved.certainty,
|
|
433
|
+
competitors: resolved.competitors,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const sites: RelationshipSite[] = [];
|
|
440
|
+
|
|
441
|
+
const heritageSites = async (baseName: string, siteKind: RelationshipSiteKind): Promise<void> => {
|
|
442
|
+
for (const bundle of bundles) {
|
|
443
|
+
for (const ref of collectDeclRefs(bundle.path, bundle.ir, bundle.source)) {
|
|
444
|
+
if (!ref.decl.bases.includes(baseName)) continue;
|
|
445
|
+
if (!isTypeLike(ref.decl.kind)) continue;
|
|
446
|
+
// Never list the base type as its own implementor.
|
|
447
|
+
if (sameDecl(ref, target) || (ref.decl.name === baseName && ref.path === target.path)) continue;
|
|
448
|
+
const deps = await depsOf(ref.path);
|
|
449
|
+
const candidates = globalByName.get(baseName) ?? [];
|
|
450
|
+
const certainty =
|
|
451
|
+
ref.path === target.path || deps.has(target.path)
|
|
452
|
+
? "exact"
|
|
453
|
+
: candidates.length === 1
|
|
454
|
+
? "inferred"
|
|
455
|
+
: "ambiguous";
|
|
456
|
+
sites.push(
|
|
457
|
+
makeSite(
|
|
458
|
+
ref.path,
|
|
459
|
+
ref.source,
|
|
460
|
+
ref.decl.startLine,
|
|
461
|
+
siteKind,
|
|
462
|
+
baseName,
|
|
463
|
+
certainty,
|
|
464
|
+
certainty === "ambiguous" ? candidates.slice(0, MAX_COMPETITORS).map(toCandidate) : [],
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
if (op === "callers") {
|
|
472
|
+
if (isCallableLike(target.decl.kind) || !isTypeLike(target.decl.kind)) {
|
|
473
|
+
for (const edge of edges) {
|
|
474
|
+
if (edgeHitsTarget(edge, target)) pushEdge(sites, edge);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (isTypeLike(target.decl.kind)) {
|
|
478
|
+
for (const edge of edges) {
|
|
479
|
+
// construct + bare TypeName() call (common when grammar has no separate construct node).
|
|
480
|
+
if (edge.site.kind !== "construct" && edge.site.kind !== "call") continue;
|
|
481
|
+
if (edge.site.name !== target.decl.name) continue;
|
|
482
|
+
if (
|
|
483
|
+
edgeHitsTarget(edge, target) ||
|
|
484
|
+
(globalByName.get(target.decl.name) ?? []).some((r) => sameDecl(r, target))
|
|
485
|
+
) {
|
|
486
|
+
if (edge.targets.length === 0 && edge.competitors.length === 0) {
|
|
487
|
+
const n = (globalByName.get(target.decl.name) ?? []).length;
|
|
488
|
+
sites.push(
|
|
489
|
+
makeSite(
|
|
490
|
+
edge.from.path,
|
|
491
|
+
edge.from.source,
|
|
492
|
+
edge.site.line,
|
|
493
|
+
"construct",
|
|
494
|
+
edge.site.name,
|
|
495
|
+
n === 1 ? "inferred" : "ambiguous",
|
|
496
|
+
n > 1
|
|
497
|
+
? (globalByName.get(target.decl.name) ?? []).slice(0, MAX_COMPETITORS).map(toCandidate)
|
|
498
|
+
: [],
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
} else {
|
|
502
|
+
pushEdge(sites, edge);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
await heritageSites(target.decl.name, "implementation");
|
|
507
|
+
}
|
|
508
|
+
} else if (op === "callees") {
|
|
509
|
+
if (isCallableLike(target.decl.kind) || !isTypeLike(target.decl.kind)) {
|
|
510
|
+
for (const edge of edges) {
|
|
511
|
+
if (sameDecl(edge.from, target)) pushEdge(sites, edge);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (isTypeLike(target.decl.kind)) {
|
|
515
|
+
const deps = await depsOf(target.path);
|
|
516
|
+
for (const base of target.decl.bases) {
|
|
517
|
+
const picked = pickByDeps(globalByName.get(base) ?? [], deps);
|
|
518
|
+
sites.push(
|
|
519
|
+
makeSite(
|
|
520
|
+
target.path,
|
|
521
|
+
target.source,
|
|
522
|
+
target.decl.startLine,
|
|
523
|
+
"implementation",
|
|
524
|
+
base,
|
|
525
|
+
picked.targets.length === 1
|
|
526
|
+
? picked.certainty
|
|
527
|
+
: (globalByName.get(base) ?? []).length === 1
|
|
528
|
+
? "exact"
|
|
529
|
+
: picked.certainty,
|
|
530
|
+
picked.competitors,
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
} else if (op === "references") {
|
|
536
|
+
for (const edge of edges) {
|
|
537
|
+
if (edgeHitsTarget(edge, target)) pushEdge(sites, edge);
|
|
538
|
+
}
|
|
539
|
+
for (const bundle of bundles) {
|
|
540
|
+
for (const imp of bundle.ir.imports) {
|
|
541
|
+
for (const binding of imp.bindings) {
|
|
542
|
+
const remote = binding.imported.length > 0 ? binding.imported : binding.local;
|
|
543
|
+
if (binding.local !== target.decl.name && remote !== target.decl.name) continue;
|
|
544
|
+
const deps = await depsOf(bundle.path);
|
|
545
|
+
const certainty = deps.has(target.path) || bundle.path === target.path ? "exact" : "inferred";
|
|
546
|
+
sites.push(makeSite(bundle.path, bundle.source, imp.startLine, "import", binding.local, certainty, []));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
await heritageSites(target.decl.name, "implementation");
|
|
551
|
+
} else {
|
|
552
|
+
// implementations
|
|
553
|
+
if (isTypeLike(target.decl.kind)) {
|
|
554
|
+
await heritageSites(target.decl.name, "implementation");
|
|
555
|
+
}
|
|
556
|
+
if (isCallableLike(target.decl.kind)) {
|
|
557
|
+
const owner = ownerName(target.decl.qualifiedName);
|
|
558
|
+
if (owner.length > 0) {
|
|
559
|
+
for (const bundle of bundles) {
|
|
560
|
+
for (const typeRef of collectDeclRefs(bundle.path, bundle.ir, bundle.source)) {
|
|
561
|
+
if (!isTypeLike(typeRef.decl.kind)) continue;
|
|
562
|
+
if (!typeRef.decl.bases.includes(owner)) continue;
|
|
563
|
+
for (const child of typeRef.decl.children) {
|
|
564
|
+
if (child.name !== target.decl.name || !isCallableLike(child.kind)) continue;
|
|
565
|
+
if (
|
|
566
|
+
sameDecl({ path: typeRef.path, decl: child, ir: typeRef.ir, source: typeRef.source }, target)
|
|
567
|
+
) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
sites.push(
|
|
571
|
+
makeSite(typeRef.path, typeRef.source, child.startLine, "override", child.name, "inferred", []),
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const sorted = sortSites(dedupeSites(sites));
|
|
581
|
+
const limited = sorted.slice(0, resultLimit);
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
kind: "resolved",
|
|
585
|
+
target: {
|
|
586
|
+
path: target.path,
|
|
587
|
+
name: target.decl.name,
|
|
588
|
+
qualifiedName: target.decl.qualifiedName,
|
|
589
|
+
kind: target.decl.kind,
|
|
590
|
+
startLine: target.decl.startLine,
|
|
591
|
+
},
|
|
592
|
+
sites: limited,
|
|
593
|
+
resultLimit,
|
|
594
|
+
resultLimitReached: sorted.length > resultLimit,
|
|
595
|
+
parseDegraded,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ExploreEngine } from "./engine.ts";
|
|
2
|
+
import type { Decl, DeclKind, FileIr } from "./ir.ts";
|
|
3
|
+
import { findTargets } from "./query.ts";
|
|
4
|
+
import { scanSources } from "./scan.ts";
|
|
5
|
+
import { signatureText } from "./slice.ts";
|
|
6
|
+
|
|
7
|
+
/** Symbol target — path and line optional; name required (may be dotted). */
|
|
8
|
+
export type Target = {
|
|
9
|
+
path?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** One ambiguous hit — signature is source byte slice, no body. */
|
|
15
|
+
export type Candidate = {
|
|
16
|
+
path: string;
|
|
17
|
+
name: string;
|
|
18
|
+
qualifiedName: string;
|
|
19
|
+
kind: DeclKind;
|
|
20
|
+
startLine: number;
|
|
21
|
+
endLine: number;
|
|
22
|
+
signature: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type Resolution =
|
|
26
|
+
| { kind: "resolved"; decl: Decl; path: string; ir: FileIr; source: string }
|
|
27
|
+
| { kind: "candidates"; candidates: Candidate[] }
|
|
28
|
+
| { kind: "notFound" };
|
|
29
|
+
|
|
30
|
+
/** Max candidates returned on ambiguity. Scan stops once this many matches are collected. */
|
|
31
|
+
const MAX_CANDIDATES = 10;
|
|
32
|
+
|
|
33
|
+
type Match = {
|
|
34
|
+
decl: Decl;
|
|
35
|
+
path: string;
|
|
36
|
+
ir: FileIr;
|
|
37
|
+
source: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function pathPresent(path: string | undefined): path is string {
|
|
41
|
+
return path !== undefined && path.length > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toCandidate(match: Match): Candidate {
|
|
45
|
+
return {
|
|
46
|
+
path: match.path,
|
|
47
|
+
name: match.decl.name,
|
|
48
|
+
qualifiedName: match.decl.qualifiedName,
|
|
49
|
+
kind: match.decl.kind,
|
|
50
|
+
startLine: match.decl.startLine,
|
|
51
|
+
endLine: match.decl.endLine,
|
|
52
|
+
signature: signatureText(match.decl, match.source),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pushMatches(matches: Match[], ir: FileIr, source: string, name: string, line: number | undefined): void {
|
|
57
|
+
for (const decl of findTargets(ir.decls, name, line)) {
|
|
58
|
+
matches.push({ decl, path: ir.path, ir, source });
|
|
59
|
+
if (matches.length >= MAX_CANDIDATES) return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve `{ path?, name, line? }` under `scopeDir` to one Decl, a bounded candidate
|
|
65
|
+
* list, or notFound. Sole identity entry point for symbol-targeted tools.
|
|
66
|
+
*
|
|
67
|
+
* - `path` set → that file's IR only.
|
|
68
|
+
* - `path` absent → budgeted `scanSources` of `scopeDir`.
|
|
69
|
+
* - `line` set → keep decls whose inclusive `startLine..endLine` covers it.
|
|
70
|
+
* - Multiple matches → candidates (never a silent pick). Zero → notFound.
|
|
71
|
+
* - Resolved carries the same bytes used to build the IR (no second skew window).
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveTarget(
|
|
74
|
+
engine: ExploreEngine,
|
|
75
|
+
scopeDir: string,
|
|
76
|
+
target: Target,
|
|
77
|
+
signal: AbortSignal,
|
|
78
|
+
): Promise<Resolution> {
|
|
79
|
+
signal.throwIfAborted();
|
|
80
|
+
|
|
81
|
+
const matches: Match[] = [];
|
|
82
|
+
|
|
83
|
+
if (pathPresent(target.path)) {
|
|
84
|
+
const source = await engine.sourceForFile(target.path);
|
|
85
|
+
signal.throwIfAborted();
|
|
86
|
+
pushMatches(matches, source.ir, source.source, target.name, target.line);
|
|
87
|
+
} else {
|
|
88
|
+
const scan = scanSources({
|
|
89
|
+
engine,
|
|
90
|
+
cwd: engine.cwd,
|
|
91
|
+
root: scopeDir,
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
for await (const source of scan) {
|
|
95
|
+
signal.throwIfAborted();
|
|
96
|
+
pushMatches(matches, source.ir, source.source, target.name, target.line);
|
|
97
|
+
if (matches.length >= MAX_CANDIDATES) break;
|
|
98
|
+
}
|
|
99
|
+
signal.throwIfAborted();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (matches.length === 0) return { kind: "notFound" };
|
|
103
|
+
|
|
104
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
105
|
+
if (only !== undefined) {
|
|
106
|
+
return {
|
|
107
|
+
kind: "resolved",
|
|
108
|
+
decl: only.decl,
|
|
109
|
+
path: only.path,
|
|
110
|
+
ir: only.ir,
|
|
111
|
+
source: only.source,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
kind: "candidates",
|
|
117
|
+
candidates: matches.slice(0, MAX_CANDIDATES).map(toCandidate),
|
|
118
|
+
};
|
|
119
|
+
}
|