@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,427 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import type { ExploreEngine, FileSource } from "../engine.ts";
|
|
3
|
+
import type { Decl, DeclKind, Visibility } from "../ir.ts";
|
|
4
|
+
import { packageDeclKey, type PackageSurfaceHost } from "../package-surface.ts";
|
|
5
|
+
import { scanSources, type ScanOutcome } from "../scan.ts";
|
|
6
|
+
import { docsText, signatureText } from "../slice.ts";
|
|
7
|
+
import { pathResolutionError, resolveExplorePath } from "../../traverse.ts";
|
|
8
|
+
|
|
9
|
+
export type DiscoverSurface = "all" | "public" | "private" | "sourceExport" | "packageSurface";
|
|
10
|
+
|
|
11
|
+
export type DiscoverQuery =
|
|
12
|
+
| { kind: "exactName"; name: string }
|
|
13
|
+
| { kind: "prefixName"; name: string }
|
|
14
|
+
| { kind: "substringName"; name: string }
|
|
15
|
+
| { kind: "fuzzyName"; name: string; maxCandidates: number; maxWork: number }
|
|
16
|
+
| { kind: "declarationKind"; declarationKind: DeclKind }
|
|
17
|
+
| { kind: "documentation"; terms: string[]; maxCandidates: number; maxWork: number };
|
|
18
|
+
|
|
19
|
+
export type DiscoverCandidate = {
|
|
20
|
+
path: string;
|
|
21
|
+
name: string;
|
|
22
|
+
qualifiedName: string;
|
|
23
|
+
kind: DeclKind;
|
|
24
|
+
startLine: number;
|
|
25
|
+
endLine: number;
|
|
26
|
+
signature: string;
|
|
27
|
+
visibility: Visibility;
|
|
28
|
+
exported: boolean;
|
|
29
|
+
/** Import form when surface makes access the point. */
|
|
30
|
+
access: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type DiscoverResult = {
|
|
34
|
+
candidates: DiscoverCandidate[];
|
|
35
|
+
resultLimit: number;
|
|
36
|
+
resultLimitReached: boolean;
|
|
37
|
+
workLimitReached: boolean;
|
|
38
|
+
candidateLimitReached: boolean;
|
|
39
|
+
scan: ScanOutcome | undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ScoredCandidate = {
|
|
43
|
+
score: number;
|
|
44
|
+
candidate: DiscoverCandidate;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function nameTail(qualifiedName: string): string {
|
|
48
|
+
const i = qualifiedName.lastIndexOf(".");
|
|
49
|
+
return i === -1 ? qualifiedName : qualifiedName.slice(i + 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nameFields(decl: Decl): [string, string] {
|
|
53
|
+
return [decl.name, nameTail(decl.qualifiedName)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matchesNameQuery(kind: "exactName" | "prefixName" | "substringName", needle: string, decl: Decl): boolean {
|
|
57
|
+
for (const field of nameFields(decl)) {
|
|
58
|
+
if (kind === "exactName" && field === needle) return true;
|
|
59
|
+
if (kind === "prefixName" && field.startsWith(needle)) return true;
|
|
60
|
+
if (kind === "substringName" && field.includes(needle)) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Lower is better. undefined = no match. */
|
|
66
|
+
function fuzzyScore(query: string, text: string): number | undefined {
|
|
67
|
+
if (text.length === 0) return undefined;
|
|
68
|
+
if (text === query) return 0;
|
|
69
|
+
if (text.startsWith(query)) return 1;
|
|
70
|
+
const idx = text.indexOf(query);
|
|
71
|
+
if (idx >= 0) return 2 + idx + (text.length - query.length);
|
|
72
|
+
|
|
73
|
+
let qi = 0;
|
|
74
|
+
let gaps = 0;
|
|
75
|
+
let last = -1;
|
|
76
|
+
for (let i = 0; i < text.length && qi < query.length; i += 1) {
|
|
77
|
+
if (text[i] === query[qi]) {
|
|
78
|
+
if (last >= 0) gaps += i - last - 1;
|
|
79
|
+
last = i;
|
|
80
|
+
qi += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (qi === query.length) return 100 + gaps + (text.length - query.length);
|
|
84
|
+
|
|
85
|
+
const maxDist = Math.min(3, Math.max(1, Math.floor(query.length / 2)));
|
|
86
|
+
const dist = boundedLevenshtein(query, text, maxDist);
|
|
87
|
+
return dist === undefined ? undefined : 1000 + dist;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function boundedLevenshtein(a: string, b: string, maxDist: number): number | undefined {
|
|
91
|
+
if (Math.abs(a.length - b.length) > maxDist) return undefined;
|
|
92
|
+
const prev = Array.from({ length: b.length + 1 }, () => 0);
|
|
93
|
+
const cur = Array.from({ length: b.length + 1 }, () => 0);
|
|
94
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = j;
|
|
95
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
96
|
+
cur[0] = i;
|
|
97
|
+
let rowMin = cur[0] ?? i;
|
|
98
|
+
const ca = a.charCodeAt(i - 1);
|
|
99
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
100
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
101
|
+
const del = (prev[j] ?? maxDist + 1) + 1;
|
|
102
|
+
const ins = (cur[j - 1] ?? maxDist + 1) + 1;
|
|
103
|
+
const sub = (prev[j - 1] ?? maxDist + 1) + cost;
|
|
104
|
+
const v = Math.min(del, ins, sub);
|
|
105
|
+
cur[j] = v;
|
|
106
|
+
if (v < rowMin) rowMin = v;
|
|
107
|
+
}
|
|
108
|
+
if (rowMin > maxDist) return undefined;
|
|
109
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = cur[j] ?? 0;
|
|
110
|
+
}
|
|
111
|
+
const dist = prev[b.length] ?? maxDist + 1;
|
|
112
|
+
return dist > maxDist ? undefined : dist;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function bestFuzzyScore(query: string, decl: Decl): number | undefined {
|
|
116
|
+
let best: number | undefined;
|
|
117
|
+
for (const field of nameFields(decl)) {
|
|
118
|
+
const score = fuzzyScore(query, field);
|
|
119
|
+
if (score === undefined) continue;
|
|
120
|
+
if (best === undefined || score < best) best = score;
|
|
121
|
+
}
|
|
122
|
+
return best;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isPublicSurface(decl: Decl, inExportedOwner: boolean): boolean {
|
|
126
|
+
return decl.visibility === "public" && (decl.exported || inExportedOwner);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function matchesSurfaceFilter(
|
|
130
|
+
surface: DiscoverSurface,
|
|
131
|
+
path: string,
|
|
132
|
+
decl: Decl,
|
|
133
|
+
inExportedOwner: boolean,
|
|
134
|
+
packageKeys: ReadonlySet<string> | undefined,
|
|
135
|
+
): boolean {
|
|
136
|
+
switch (surface) {
|
|
137
|
+
case "all":
|
|
138
|
+
return true;
|
|
139
|
+
case "public":
|
|
140
|
+
return isPublicSurface(decl, inExportedOwner);
|
|
141
|
+
case "private":
|
|
142
|
+
return !isPublicSurface(decl, inExportedOwner);
|
|
143
|
+
case "sourceExport":
|
|
144
|
+
return decl.exported;
|
|
145
|
+
case "packageSurface":
|
|
146
|
+
return packageKeys !== undefined && packageKeys.has(packageDeclKey(path, decl));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function toCandidate(path: string, decl: Decl, source: string, access: string | undefined): DiscoverCandidate {
|
|
151
|
+
return {
|
|
152
|
+
path,
|
|
153
|
+
name: decl.name,
|
|
154
|
+
qualifiedName: decl.qualifiedName,
|
|
155
|
+
kind: decl.kind,
|
|
156
|
+
startLine: decl.startLine,
|
|
157
|
+
endLine: decl.endLine,
|
|
158
|
+
signature: signatureText(decl, source).replace(/\s+$/u, ""),
|
|
159
|
+
visibility: decl.visibility,
|
|
160
|
+
exported: decl.exported,
|
|
161
|
+
access,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function accessFor(surface: DiscoverSurface, packageAccess: string | undefined): string | undefined {
|
|
166
|
+
if (surface === "packageSurface") return packageAccess;
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function insertScored(heap: ScoredCandidate[], entry: ScoredCandidate, max: number): void {
|
|
171
|
+
const at = heap.findIndex((row) => entry.score < row.score);
|
|
172
|
+
if (at === -1) heap.push(entry);
|
|
173
|
+
else heap.splice(at, 0, entry);
|
|
174
|
+
if (heap.length > max) heap.length = max;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function compareCandidates(a: DiscoverCandidate, b: DiscoverCandidate): number {
|
|
178
|
+
const byPath = a.path.localeCompare(b.path);
|
|
179
|
+
if (byPath !== 0) return byPath;
|
|
180
|
+
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
181
|
+
return a.qualifiedName.localeCompare(b.qualifiedName);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type MatchBucket = {
|
|
185
|
+
plain: DiscoverCandidate[];
|
|
186
|
+
scored: ScoredCandidate[];
|
|
187
|
+
workUsed: number;
|
|
188
|
+
workLimitReached: boolean;
|
|
189
|
+
candidateLimitReached: boolean;
|
|
190
|
+
scoring: boolean;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function newBucket(query: DiscoverQuery): MatchBucket {
|
|
194
|
+
return {
|
|
195
|
+
plain: [],
|
|
196
|
+
scored: [],
|
|
197
|
+
workUsed: 0,
|
|
198
|
+
workLimitReached: false,
|
|
199
|
+
candidateLimitReached: false,
|
|
200
|
+
scoring: query.kind === "fuzzyName" || query.kind === "documentation",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function considerDecl(
|
|
205
|
+
bucket: MatchBucket,
|
|
206
|
+
query: DiscoverQuery,
|
|
207
|
+
path: string,
|
|
208
|
+
decl: Decl,
|
|
209
|
+
source: string,
|
|
210
|
+
surface: DiscoverSurface,
|
|
211
|
+
inExportedOwner: boolean,
|
|
212
|
+
packageKeys: ReadonlySet<string> | undefined,
|
|
213
|
+
packageAccess: ReadonlyMap<string, string> | undefined,
|
|
214
|
+
): void {
|
|
215
|
+
if (bucket.workLimitReached) return;
|
|
216
|
+
if (!matchesSurfaceFilter(surface, path, decl, inExportedOwner, packageKeys)) return;
|
|
217
|
+
|
|
218
|
+
const id = packageDeclKey(path, decl);
|
|
219
|
+
const access = accessFor(surface, packageAccess?.get(id));
|
|
220
|
+
|
|
221
|
+
switch (query.kind) {
|
|
222
|
+
case "exactName":
|
|
223
|
+
case "prefixName":
|
|
224
|
+
case "substringName": {
|
|
225
|
+
if (!matchesNameQuery(query.kind, query.name, decl)) return;
|
|
226
|
+
bucket.plain.push(toCandidate(path, decl, source, access));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
case "declarationKind": {
|
|
230
|
+
if (decl.kind !== query.declarationKind) return;
|
|
231
|
+
bucket.plain.push(toCandidate(path, decl, source, access));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
case "fuzzyName": {
|
|
235
|
+
bucket.workUsed += 1;
|
|
236
|
+
if (bucket.workUsed > query.maxWork) {
|
|
237
|
+
bucket.workLimitReached = true;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const score = bestFuzzyScore(query.name, decl);
|
|
241
|
+
if (score === undefined) return;
|
|
242
|
+
const candidate = toCandidate(path, decl, source, access);
|
|
243
|
+
const worst = bucket.scored[bucket.scored.length - 1];
|
|
244
|
+
if (bucket.scored.length >= query.maxCandidates && worst !== undefined && score >= worst.score) {
|
|
245
|
+
bucket.candidateLimitReached = true;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const before = bucket.scored.length;
|
|
249
|
+
insertScored(bucket.scored, { score, candidate }, query.maxCandidates);
|
|
250
|
+
if (before === query.maxCandidates) bucket.candidateLimitReached = true;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
case "documentation": {
|
|
254
|
+
if (decl.docStartOffset === undefined || decl.docEndOffset === undefined) return;
|
|
255
|
+
bucket.workUsed += 1;
|
|
256
|
+
if (bucket.workUsed > query.maxWork) {
|
|
257
|
+
bucket.workLimitReached = true;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const docs = docsText(decl, source);
|
|
261
|
+
if (docs === undefined || !query.terms.every((term) => docs.includes(term))) return;
|
|
262
|
+
if (bucket.scored.length >= query.maxCandidates) {
|
|
263
|
+
bucket.candidateLimitReached = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
bucket.scored.push({ score: bucket.scored.length, candidate: toCandidate(path, decl, source, access) });
|
|
267
|
+
if (bucket.scored.length >= query.maxCandidates) bucket.candidateLimitReached = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function walkFileDecls(
|
|
273
|
+
bucket: MatchBucket,
|
|
274
|
+
query: DiscoverQuery,
|
|
275
|
+
file: FileSource,
|
|
276
|
+
surface: DiscoverSurface,
|
|
277
|
+
packageKeys: ReadonlySet<string> | undefined,
|
|
278
|
+
packageAccess: ReadonlyMap<string, string> | undefined,
|
|
279
|
+
): void {
|
|
280
|
+
const go = (decls: readonly Decl[], inExportedOwner: boolean): void => {
|
|
281
|
+
for (const decl of decls) {
|
|
282
|
+
if (bucket.workLimitReached) return;
|
|
283
|
+
considerDecl(
|
|
284
|
+
bucket,
|
|
285
|
+
query,
|
|
286
|
+
file.ir.path,
|
|
287
|
+
decl,
|
|
288
|
+
file.source,
|
|
289
|
+
surface,
|
|
290
|
+
inExportedOwner,
|
|
291
|
+
packageKeys,
|
|
292
|
+
packageAccess,
|
|
293
|
+
);
|
|
294
|
+
go(decl.children, inExportedOwner || decl.exported);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
go(file.ir.decls, false);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function finalizeBucket(bucket: MatchBucket, resultLimit: number): Omit<DiscoverResult, "scan"> {
|
|
301
|
+
let ordered: DiscoverCandidate[];
|
|
302
|
+
if (bucket.scoring) {
|
|
303
|
+
const sorted = bucket.scored.slice().sort((a, b) => {
|
|
304
|
+
if (a.score !== b.score) return a.score - b.score;
|
|
305
|
+
return compareCandidates(a.candidate, b.candidate);
|
|
306
|
+
});
|
|
307
|
+
ordered = sorted.map((row) => row.candidate);
|
|
308
|
+
} else {
|
|
309
|
+
ordered = bucket.plain.slice().sort(compareCandidates);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
candidates: ordered.slice(0, resultLimit),
|
|
313
|
+
resultLimit,
|
|
314
|
+
resultLimitReached: ordered.length > resultLimit,
|
|
315
|
+
workLimitReached: bucket.workLimitReached,
|
|
316
|
+
candidateLimitReached: bucket.candidateLimitReached,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function pathIsDirectory(absolutePath: string, input: string): Promise<void> {
|
|
321
|
+
let stats;
|
|
322
|
+
try {
|
|
323
|
+
stats = await lstat(absolutePath);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw pathResolutionError(error, input);
|
|
326
|
+
}
|
|
327
|
+
if (!stats.isDirectory()) throw new Error("discover requires a directory scope");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function packageSurfaceHost(engine: ExploreEngine): PackageSurfaceHost {
|
|
331
|
+
return {
|
|
332
|
+
cwd: engine.cwd,
|
|
333
|
+
sourceForFile: (path) => engine.sourceForFile(path),
|
|
334
|
+
ownsPath: (path) => {
|
|
335
|
+
const adapter = engine.registry.adapterForPath(path);
|
|
336
|
+
return adapter !== undefined && adapter.capabilities.packageSurface;
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Directory-scoped declaration discovery. Language rules stay on adapters. */
|
|
342
|
+
export async function discover(
|
|
343
|
+
engine: ExploreEngine,
|
|
344
|
+
pathInput: string,
|
|
345
|
+
query: DiscoverQuery,
|
|
346
|
+
surface: DiscoverSurface,
|
|
347
|
+
resultLimit: number,
|
|
348
|
+
signal: AbortSignal,
|
|
349
|
+
): Promise<DiscoverResult> {
|
|
350
|
+
signal.throwIfAborted();
|
|
351
|
+
const absolutePath = resolveExplorePath(engine.cwd, pathInput);
|
|
352
|
+
await pathIsDirectory(absolutePath, pathInput);
|
|
353
|
+
|
|
354
|
+
const bucket = newBucket(query);
|
|
355
|
+
|
|
356
|
+
if (surface === "packageSurface") {
|
|
357
|
+
const resolvers = engine.registry.packageSurfaceResolvers();
|
|
358
|
+
if (resolvers.length === 0) {
|
|
359
|
+
throw new Error("packageSurface capability is not available");
|
|
360
|
+
}
|
|
361
|
+
const host = packageSurfaceHost(engine);
|
|
362
|
+
const packageKeys = new Set<string>();
|
|
363
|
+
const packageAccess = new Map<string, string>();
|
|
364
|
+
const paths = new Set<string>();
|
|
365
|
+
let filesVisited = 0;
|
|
366
|
+
let anyPackage = false;
|
|
367
|
+
|
|
368
|
+
for (const resolve of resolvers) {
|
|
369
|
+
signal.throwIfAborted();
|
|
370
|
+
const graph = await resolve(absolutePath, host, signal);
|
|
371
|
+
if (graph === undefined) continue;
|
|
372
|
+
anyPackage = true;
|
|
373
|
+
filesVisited += graph.filesVisited;
|
|
374
|
+
for (const key of graph.declKeys) packageKeys.add(key);
|
|
375
|
+
for (const [key, access] of graph.accessByDecl) packageAccess.set(key, access);
|
|
376
|
+
for (const path of graph.paths) paths.add(path);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!anyPackage) {
|
|
380
|
+
throw new Error("packageSurface: no package found for a supporting language near the scope path");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const path of paths) {
|
|
384
|
+
signal.throwIfAborted();
|
|
385
|
+
if (bucket.workLimitReached) break;
|
|
386
|
+
const file = await engine.sourceForFile(path);
|
|
387
|
+
walkFileDecls(bucket, query, file, "packageSurface", packageKeys, packageAccess);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const finalized = finalizeBucket(bucket, resultLimit);
|
|
391
|
+
return {
|
|
392
|
+
...finalized,
|
|
393
|
+
scan: {
|
|
394
|
+
limit: undefined,
|
|
395
|
+
filesVisited,
|
|
396
|
+
sourceBytes: 0,
|
|
397
|
+
elapsedMs: 0,
|
|
398
|
+
filesEmitted: paths.size,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const scan = scanSources({ engine, cwd: engine.cwd, root: absolutePath, signal });
|
|
404
|
+
let step = await scan.next();
|
|
405
|
+
let filesEmitted = 0;
|
|
406
|
+
while (!step.done) {
|
|
407
|
+
signal.throwIfAborted();
|
|
408
|
+
filesEmitted += 1;
|
|
409
|
+
walkFileDecls(bucket, query, step.value, surface, undefined, undefined);
|
|
410
|
+
if (bucket.workLimitReached) break;
|
|
411
|
+
step = await scan.next();
|
|
412
|
+
}
|
|
413
|
+
if (signal.aborted) throw new Error("discover cancelled");
|
|
414
|
+
const outcome: ScanOutcome = step.done
|
|
415
|
+
? step.value
|
|
416
|
+
: {
|
|
417
|
+
limit: undefined,
|
|
418
|
+
filesVisited: filesEmitted,
|
|
419
|
+
sourceBytes: 0,
|
|
420
|
+
elapsedMs: 0,
|
|
421
|
+
filesEmitted,
|
|
422
|
+
};
|
|
423
|
+
if (outcome.limit === "cancelled") throw new Error("discover cancelled");
|
|
424
|
+
|
|
425
|
+
const finalized = finalizeBucket(bucket, resultLimit);
|
|
426
|
+
return { ...finalized, scan: outcome };
|
|
427
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { ExploreEngine } from "../engine.ts";
|
|
2
|
+
import type { Candidate } from "../identity.ts";
|
|
3
|
+
import type { ExploreFileGraph, FileGraphEdge } from "../graph/file-graph.ts";
|
|
4
|
+
import {
|
|
5
|
+
queryRelationships,
|
|
6
|
+
type RelationshipCertainty,
|
|
7
|
+
type RelationshipSite,
|
|
8
|
+
type RelationshipSiteKind,
|
|
9
|
+
} from "../graph/relationships.ts";
|
|
10
|
+
import { resolveCompositeTarget, type CompositeTarget } from "./composite-target.ts";
|
|
11
|
+
|
|
12
|
+
const IMPACT_SECTION_LIMIT = 50;
|
|
13
|
+
export const IMPACT_DEPTH_DEFAULT = 2;
|
|
14
|
+
export const IMPACT_DEPTH_MAX = 5;
|
|
15
|
+
|
|
16
|
+
export type ImpactMode = "all" | "deps" | "dependents";
|
|
17
|
+
|
|
18
|
+
export type ImpactSectionId = "callees" | "fileImports" | "callers" | "fileImporters" | "transitiveDependents";
|
|
19
|
+
|
|
20
|
+
export type ImpactSiteRow = {
|
|
21
|
+
kind: "site";
|
|
22
|
+
path: string;
|
|
23
|
+
line: number;
|
|
24
|
+
siteKind: RelationshipSiteKind;
|
|
25
|
+
name: string;
|
|
26
|
+
preview: string;
|
|
27
|
+
certainty: RelationshipCertainty;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ImpactFileRow = {
|
|
31
|
+
kind: "file";
|
|
32
|
+
path: string | undefined;
|
|
33
|
+
externalId: string | undefined;
|
|
34
|
+
depth: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ImpactRow = ImpactSiteRow | ImpactFileRow;
|
|
38
|
+
|
|
39
|
+
export type ImpactSection = {
|
|
40
|
+
id: ImpactSectionId;
|
|
41
|
+
label: string;
|
|
42
|
+
rows: ImpactRow[];
|
|
43
|
+
limit: number;
|
|
44
|
+
omitted: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ImpactTarget = CompositeTarget;
|
|
48
|
+
|
|
49
|
+
export type ImpactResult =
|
|
50
|
+
| {
|
|
51
|
+
kind: "resolved";
|
|
52
|
+
target: ImpactTarget;
|
|
53
|
+
sections: ImpactSection[];
|
|
54
|
+
parseDegraded: boolean;
|
|
55
|
+
notes: string[];
|
|
56
|
+
}
|
|
57
|
+
| { kind: "candidates"; candidates: Candidate[] }
|
|
58
|
+
| { kind: "notFound" }
|
|
59
|
+
| { kind: "error"; message: string };
|
|
60
|
+
|
|
61
|
+
export type ImpactQueryArgs = {
|
|
62
|
+
engine: ExploreEngine;
|
|
63
|
+
graph: ExploreFileGraph;
|
|
64
|
+
scopePath: string;
|
|
65
|
+
targetPath: string | undefined;
|
|
66
|
+
name: string;
|
|
67
|
+
line: number | undefined;
|
|
68
|
+
depth: number;
|
|
69
|
+
mode: ImpactMode;
|
|
70
|
+
signal: AbortSignal;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function siteRows(sites: readonly RelationshipSite[]): ImpactSiteRow[] {
|
|
74
|
+
return sites.slice(0, IMPACT_SECTION_LIMIT).map((site) => ({
|
|
75
|
+
kind: "site" as const,
|
|
76
|
+
path: site.path,
|
|
77
|
+
line: site.line,
|
|
78
|
+
siteKind: site.kind,
|
|
79
|
+
name: site.name,
|
|
80
|
+
preview: site.preview,
|
|
81
|
+
certainty: site.certainty,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fileRowsFromEdges(edges: readonly FileGraphEdge[]): ImpactFileRow[] {
|
|
86
|
+
const rows: ImpactFileRow[] = [];
|
|
87
|
+
for (const edge of edges) {
|
|
88
|
+
if (rows.length >= IMPACT_SECTION_LIMIT) break;
|
|
89
|
+
if (edge.kind === "internal") {
|
|
90
|
+
rows.push({ kind: "file", path: edge.to, externalId: undefined, depth: 1 });
|
|
91
|
+
} else {
|
|
92
|
+
rows.push({ kind: "file", path: undefined, externalId: edge.id, depth: 1 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function relationshipSection(
|
|
99
|
+
args: ImpactQueryArgs,
|
|
100
|
+
op: "callees" | "callers",
|
|
101
|
+
id: ImpactSectionId,
|
|
102
|
+
label: string,
|
|
103
|
+
target: ImpactTarget,
|
|
104
|
+
): Promise<{ section: ImpactSection | undefined; parseDegraded: boolean; note: string | undefined }> {
|
|
105
|
+
const result = await queryRelationships({
|
|
106
|
+
engine: args.engine,
|
|
107
|
+
graph: args.graph,
|
|
108
|
+
scopePath: args.scopePath,
|
|
109
|
+
op,
|
|
110
|
+
targetPath: target.path,
|
|
111
|
+
name: target.name,
|
|
112
|
+
line: target.startLine,
|
|
113
|
+
resultLimit: IMPACT_SECTION_LIMIT,
|
|
114
|
+
signal: args.signal,
|
|
115
|
+
});
|
|
116
|
+
if (result.kind === "resolved") {
|
|
117
|
+
const rows = siteRows(result.sites);
|
|
118
|
+
if (rows.length === 0) return { section: undefined, parseDegraded: result.parseDegraded, note: undefined };
|
|
119
|
+
return {
|
|
120
|
+
section: {
|
|
121
|
+
id,
|
|
122
|
+
label,
|
|
123
|
+
rows,
|
|
124
|
+
limit: IMPACT_SECTION_LIMIT,
|
|
125
|
+
omitted: result.resultLimitReached || result.sites.length > IMPACT_SECTION_LIMIT,
|
|
126
|
+
},
|
|
127
|
+
parseDegraded: result.parseDegraded,
|
|
128
|
+
note: undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (result.kind === "error") {
|
|
132
|
+
return { section: undefined, parseDegraded: false, note: result.message };
|
|
133
|
+
}
|
|
134
|
+
return { section: undefined, parseDegraded: false, note: undefined };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Blast radius composite: symbol callees/callers + file imports/importers + transitive file dependents.
|
|
139
|
+
* Symbol hops are depth 1 only; `depth` applies to reverse file BFS only.
|
|
140
|
+
*/
|
|
141
|
+
export async function queryImpact(args: ImpactQueryArgs): Promise<ImpactResult> {
|
|
142
|
+
const { graph, signal } = args;
|
|
143
|
+
if (args.depth < 1) {
|
|
144
|
+
return { kind: "error", message: "depth must be >= 1" };
|
|
145
|
+
}
|
|
146
|
+
const depth = Math.min(args.depth, IMPACT_DEPTH_MAX);
|
|
147
|
+
|
|
148
|
+
const resolved = await resolveCompositeTarget(
|
|
149
|
+
args.engine,
|
|
150
|
+
args.scopePath,
|
|
151
|
+
args.targetPath,
|
|
152
|
+
args.name,
|
|
153
|
+
args.line,
|
|
154
|
+
"impact",
|
|
155
|
+
signal,
|
|
156
|
+
);
|
|
157
|
+
if (resolved.kind !== "resolved") return resolved;
|
|
158
|
+
|
|
159
|
+
const target = resolved.value.target;
|
|
160
|
+
const mode = args.mode;
|
|
161
|
+
const wantDeps = mode === "all" || mode === "deps";
|
|
162
|
+
const wantDependents = mode === "all" || mode === "dependents";
|
|
163
|
+
const sections: ImpactSection[] = [];
|
|
164
|
+
const notes: string[] = [];
|
|
165
|
+
let parseDegraded = resolved.value.parseDegraded;
|
|
166
|
+
|
|
167
|
+
if (wantDeps) {
|
|
168
|
+
const callees = await relationshipSection(args, "callees", "callees", "callees", target);
|
|
169
|
+
if (callees.section !== undefined) sections.push(callees.section);
|
|
170
|
+
if (callees.parseDegraded) parseDegraded = true;
|
|
171
|
+
if (callees.note !== undefined) notes.push(callees.note);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const edges = await graph.forwardEdges(target.path, signal);
|
|
175
|
+
const rows = fileRowsFromEdges(edges);
|
|
176
|
+
if (rows.length > 0) {
|
|
177
|
+
sections.push({
|
|
178
|
+
id: "fileImports",
|
|
179
|
+
label: "file imports",
|
|
180
|
+
rows,
|
|
181
|
+
limit: IMPACT_SECTION_LIMIT,
|
|
182
|
+
omitted: edges.length > IMPACT_SECTION_LIMIT,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
notes.push(error instanceof Error ? error.message : String(error));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (wantDependents) {
|
|
191
|
+
const callers = await relationshipSection(args, "callers", "callers", "callers", target);
|
|
192
|
+
if (callers.section !== undefined) sections.push(callers.section);
|
|
193
|
+
if (callers.parseDegraded) parseDegraded = true;
|
|
194
|
+
if (callers.note !== undefined) notes.push(callers.note);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const reverseLimit = IMPACT_SECTION_LIMIT * Math.max(depth, 1);
|
|
198
|
+
const reverse = await graph.reverseDeps(target.path, depth, reverseLimit, signal);
|
|
199
|
+
const importerHits = reverse.hits.filter((hit) => hit.depth === 1);
|
|
200
|
+
const importers = importerHits.slice(0, IMPACT_SECTION_LIMIT).map(
|
|
201
|
+
(hit): ImpactFileRow => ({
|
|
202
|
+
kind: "file",
|
|
203
|
+
path: hit.path,
|
|
204
|
+
externalId: hit.externalId,
|
|
205
|
+
depth: hit.depth,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
if (importers.length > 0) {
|
|
209
|
+
sections.push({
|
|
210
|
+
id: "fileImporters",
|
|
211
|
+
label: "file importers",
|
|
212
|
+
rows: importers,
|
|
213
|
+
limit: IMPACT_SECTION_LIMIT,
|
|
214
|
+
omitted: importerHits.length > IMPACT_SECTION_LIMIT,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (depth >= 2) {
|
|
219
|
+
const transitiveHits = reverse.hits.filter((hit) => hit.depth >= 2 && hit.depth <= depth);
|
|
220
|
+
const transitive = transitiveHits.slice(0, IMPACT_SECTION_LIMIT).map(
|
|
221
|
+
(hit): ImpactFileRow => ({
|
|
222
|
+
kind: "file",
|
|
223
|
+
path: hit.path,
|
|
224
|
+
externalId: hit.externalId,
|
|
225
|
+
depth: hit.depth,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
if (transitive.length > 0) {
|
|
229
|
+
sections.push({
|
|
230
|
+
id: "transitiveDependents",
|
|
231
|
+
label: "transitive dependents",
|
|
232
|
+
rows: transitive,
|
|
233
|
+
limit: IMPACT_SECTION_LIMIT,
|
|
234
|
+
omitted: transitiveHits.length > IMPACT_SECTION_LIMIT || reverse.resultLimitReached,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
notes.push(error instanceof Error ? error.message : String(error));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
kind: "resolved",
|
|
245
|
+
target,
|
|
246
|
+
sections,
|
|
247
|
+
parseDegraded,
|
|
248
|
+
notes: [...new Set(notes)],
|
|
249
|
+
};
|
|
250
|
+
}
|