@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.
Files changed (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +130 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +716 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +31 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -0,0 +1,145 @@
1
+ import type { ExtractResult } from "./adapter.ts";
2
+ import type { Decl } from "./ir.ts";
3
+
4
+ const ATX_HEADING = /^(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/;
5
+ const OPEN_FENCE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
6
+
7
+ type LineInfo = {
8
+ /** 1-indexed. */
9
+ line: number;
10
+ text: string;
11
+ /** Offset of the first code unit of this line. */
12
+ startOffset: number;
13
+ /** Offset just past the line content (before newline, or EOF). */
14
+ endOffset: number;
15
+ /** Offset of the first code unit of the following line (or EOF). */
16
+ nextOffset: number;
17
+ };
18
+
19
+ type OpenHeading = {
20
+ decl: Decl;
21
+ depth: number;
22
+ };
23
+
24
+ function splitLines(source: string): LineInfo[] {
25
+ if (source.length === 0) {
26
+ return [{ line: 1, text: "", startOffset: 0, endOffset: 0, nextOffset: 0 }];
27
+ }
28
+
29
+ const lines: LineInfo[] = [];
30
+ let line = 1;
31
+ let i = 0;
32
+
33
+ while (i < source.length) {
34
+ const start = i;
35
+ while (i < source.length) {
36
+ const ch = source.charCodeAt(i);
37
+ if (ch === 10 || ch === 13) break;
38
+ i += 1;
39
+ }
40
+ const end = i;
41
+ let next = i;
42
+ if (next < source.length && source.charCodeAt(next) === 13) {
43
+ next += 1;
44
+ }
45
+ if (next < source.length && source.charCodeAt(next) === 10) {
46
+ next += 1;
47
+ }
48
+ lines.push({ line, text: source.slice(start, end), startOffset: start, endOffset: end, nextOffset: next });
49
+ line += 1;
50
+ i = next;
51
+ }
52
+
53
+ return lines;
54
+ }
55
+
56
+ function closeOpen(stack: OpenHeading[], depth: number, endLine: number, endOffset: number): void {
57
+ while (stack.length > 0) {
58
+ const top = stack[stack.length - 1];
59
+ if (top === undefined || top.depth < depth) break;
60
+ stack.pop();
61
+ const decl = top.decl;
62
+ decl.endLine = endLine;
63
+ decl.endOffset = endOffset;
64
+ if (decl.bodyStartOffset !== undefined) {
65
+ decl.bodyEndOffset = endOffset;
66
+ if (decl.bodyStartOffset >= endOffset) {
67
+ delete decl.bodyStartOffset;
68
+ delete decl.bodyEndOffset;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * ATX heading scanner. Fenced code (``` / ~~~, length ≥ 3, matching closer) is not heading material.
76
+ * Section range runs to the line before the next same-or-shallower heading, else EOF.
77
+ */
78
+ export function extractMarkdown(source: string): ExtractResult {
79
+ const lines = splitLines(source);
80
+ const roots: Decl[] = [];
81
+ const stack: OpenHeading[] = [];
82
+ let inFence: { char: "`" | "~"; length: number } | undefined;
83
+
84
+ for (const info of lines) {
85
+ if (inFence !== undefined) {
86
+ const close = info.text.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
87
+ const marker = close?.[1];
88
+ if (marker !== undefined && marker[0] === inFence.char && marker.length >= inFence.length) {
89
+ inFence = undefined;
90
+ }
91
+ continue;
92
+ }
93
+
94
+ const fence = info.text.match(OPEN_FENCE);
95
+ const fenceMarker = fence?.[2];
96
+ if (fenceMarker !== undefined) {
97
+ const char = fenceMarker[0] === "~" ? "~" : "`";
98
+ inFence = { char, length: fenceMarker.length };
99
+ continue;
100
+ }
101
+
102
+ const heading = info.text.match(ATX_HEADING);
103
+ if (heading === null) continue;
104
+ const hashes = heading[1];
105
+ if (hashes === undefined) continue;
106
+ const depth = hashes.length;
107
+ const name = (heading[2] ?? "").trim();
108
+ if (name.length === 0) continue;
109
+
110
+ const prev = info.line > 1 ? lines[info.line - 2] : undefined;
111
+ const closeEndLine = prev === undefined ? info.line : prev.line;
112
+ closeOpen(stack, depth, closeEndLine, info.startOffset);
113
+
114
+ const parent = stack[stack.length - 1];
115
+ const qualifiedName = parent === undefined ? name : `${parent.decl.qualifiedName}.${name}`;
116
+ const decl: Decl = {
117
+ kind: "heading",
118
+ name,
119
+ qualifiedName,
120
+ startLine: info.line,
121
+ endLine: info.line,
122
+ startOffset: info.startOffset,
123
+ endOffset: info.endOffset,
124
+ signatureEndOffset: info.endOffset,
125
+ bodyStartOffset: info.nextOffset,
126
+ bodyEndOffset: info.nextOffset,
127
+ visibility: "public",
128
+ exported: true,
129
+ children: [],
130
+ calls: [],
131
+ bases: [],
132
+ };
133
+
134
+ if (parent === undefined) roots.push(decl);
135
+ else parent.decl.children.push(decl);
136
+ stack.push({ decl, depth });
137
+ }
138
+
139
+ const last = lines[lines.length - 1];
140
+ const eofLine = last === undefined ? 1 : last.line;
141
+ const eofOffset = last === undefined ? 0 : last.nextOffset;
142
+ closeOpen(stack, 1, eofLine, eofOffset);
143
+
144
+ return { decls: roots, imports: [] };
145
+ }
@@ -0,0 +1,42 @@
1
+ import type { Decl } from "./ir.ts";
2
+ import type { FileSource } from "./engine.ts";
3
+
4
+ /** Stable key for a declaration on a package surface (path + identity spans). */
5
+ export function packageDeclKey(path: string, decl: Decl): string {
6
+ return `${path}\0${decl.qualifiedName}\0${decl.startLine}\0${decl.startOffset}`;
7
+ }
8
+
9
+ /**
10
+ * Host services for a package-surface resolver.
11
+ * Language-agnostic: resolvers never touch the engine type.
12
+ */
13
+ export type PackageSurfaceHost = {
14
+ readonly cwd: string;
15
+ sourceForFile(path: string): Promise<FileSource>;
16
+ /** True when path belongs to a language that participates in this surface capability. */
17
+ ownsPath(path: string): boolean;
18
+ };
19
+
20
+ /**
21
+ * Resolved public package surface for one language family.
22
+ * Discover matches ordinary IR decls against `declKeys` — no language branches.
23
+ */
24
+ export type PackageSurfaceGraph = {
25
+ declKeys: ReadonlySet<string>;
26
+ /** packageDeclKey → caller import/access line when known. */
27
+ accessByDecl: ReadonlyMap<string, string>;
28
+ /** Absolute defining files that hold surface decls. */
29
+ paths: readonly string[];
30
+ filesVisited: number;
31
+ };
32
+
33
+ /**
34
+ * Language-owned package surface resolution.
35
+ * `undefined` = this language has no package root at the scope (try next resolver).
36
+ * Throw only for hard failures (broken manifest, etc.).
37
+ */
38
+ export type PackageSurfaceResolver = (
39
+ scopeRoot: string,
40
+ host: PackageSurfaceHost,
41
+ signal: AbortSignal,
42
+ ) => Promise<PackageSurfaceGraph | undefined>;
@@ -0,0 +1,306 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import type { AstSearchHit, ExploreEngine } from "../engine.ts";
3
+ import type { Decl, DeclKind, FileIr } from "../ir.ts";
4
+ import { walkDecls } from "../query.ts";
5
+ import { scanSources, type ScanOutcome } from "../scan.ts";
6
+ import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../../traverse.ts";
7
+
8
+ const PATTERN_MAX_BYTES = 16 * 1024;
9
+
10
+ export type AstSearchMatch = AstSearchHit & {
11
+ path: string;
12
+ /** Innermost enclosing decl when it helps disambiguate multi-hit files. */
13
+ enclosing: { name: string; kind: DeclKind; startLine: number } | undefined;
14
+ parseDegraded: boolean;
15
+ };
16
+
17
+ export type AstSearchResult = {
18
+ matches: AstSearchMatch[];
19
+ resultLimit: number;
20
+ resultLimitReached: boolean;
21
+ languageId: string;
22
+ scan: ScanOutcome | undefined;
23
+ /** Files that failed to search (path + reason). */
24
+ fileErrors: { path: string; message: string }[];
25
+ };
26
+
27
+ function assertPattern(pattern: string): void {
28
+ if (pattern.length === 0) throw new Error("pattern must be non-empty");
29
+ const bytes = Buffer.byteLength(pattern, "utf8");
30
+ if (bytes > PATTERN_MAX_BYTES) {
31
+ throw new Error(`pattern exceeds ${PATTERN_MAX_BYTES} bytes (${bytes})`);
32
+ }
33
+ }
34
+
35
+ function resolveLanguageId(
36
+ engine: ExploreEngine,
37
+ language: string | undefined,
38
+ absolutePath: string | undefined,
39
+ isDirectory: boolean,
40
+ ): string {
41
+ if (isDirectory) {
42
+ if (language === undefined || language.length === 0) {
43
+ throw new Error("language is required for directory targets");
44
+ }
45
+ }
46
+ if (language !== undefined && language.length > 0) {
47
+ const adapter = engine.registry.adapterForId(language);
48
+ if (adapter === undefined) {
49
+ throw new Error(`Unregistered language: ${language}`);
50
+ }
51
+ if (!adapter.capabilities.search) {
52
+ throw new Error(`Language does not support structural search: ${language}`);
53
+ }
54
+ if (absolutePath !== undefined && !isDirectory) {
55
+ const byPath = engine.registry.adapterForPath(absolutePath);
56
+ if (byPath === undefined) {
57
+ throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
58
+ }
59
+ if (byPath.id !== language) {
60
+ throw new Error(`language mismatch: path is ${byPath.id}, requested ${language}`);
61
+ }
62
+ }
63
+ return language;
64
+ }
65
+ if (absolutePath === undefined) {
66
+ throw new Error("language is required");
67
+ }
68
+ const byPath = engine.registry.adapterForPath(absolutePath);
69
+ if (byPath === undefined) {
70
+ throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
71
+ }
72
+ if (!byPath.capabilities.search) {
73
+ throw new Error(`Language does not support structural search: ${byPath.id}`);
74
+ }
75
+ return byPath.id;
76
+ }
77
+
78
+ function innermostEnclosing(
79
+ ir: FileIr,
80
+ startLine: number,
81
+ endLine: number,
82
+ ): { name: string; kind: DeclKind; startLine: number } | undefined {
83
+ let best: Decl | undefined;
84
+ walkDecls(ir.decls, (decl) => {
85
+ if (decl.startLine <= startLine && decl.endLine >= endLine) {
86
+ // Prefer strictly containing named scopes over the match's own span when equal.
87
+ if (
88
+ best === undefined ||
89
+ decl.startLine > best.startLine ||
90
+ (decl.startLine === best.startLine && decl.endLine < best.endLine)
91
+ ) {
92
+ best = decl;
93
+ }
94
+ }
95
+ });
96
+ if (best === undefined) return undefined;
97
+ // Skip file-level module/package wrappers that don't disambiguate.
98
+ if (best.kind === "module" || best.kind === "package") return undefined;
99
+ // If the match is the whole decl body span, scope adds little.
100
+ if (best.startLine === startLine && best.endLine === endLine) return undefined;
101
+ return { name: best.name, kind: best.kind, startLine: best.startLine };
102
+ }
103
+
104
+ function attachEnclosing(
105
+ path: string,
106
+ hits: readonly AstSearchHit[],
107
+ ir: FileIr | undefined,
108
+ parseDegraded: boolean,
109
+ disambiguate: boolean,
110
+ ): AstSearchMatch[] {
111
+ return hits.map((hit) => ({
112
+ ...hit,
113
+ path,
114
+ parseDegraded,
115
+ enclosing: disambiguate && ir !== undefined ? innermostEnclosing(ir, hit.startLine, hit.endLine) : undefined,
116
+ }));
117
+ }
118
+
119
+ async function searchOneFile(
120
+ engine: ExploreEngine,
121
+ absolutePath: string,
122
+ languageId: string,
123
+ pattern: string,
124
+ signal: AbortSignal,
125
+ ): Promise<{ matches: AstSearchMatch[]; error: string | undefined }> {
126
+ signal.throwIfAborted();
127
+ let source: string;
128
+ try {
129
+ const bytes = await readFile(absolutePath);
130
+ source = bytes.toString("utf8");
131
+ } catch (error) {
132
+ return { matches: [], error: pathResolutionError(error, absolutePath).message };
133
+ }
134
+ signal.throwIfAborted();
135
+
136
+ let hits: AstSearchHit[];
137
+ try {
138
+ hits = await engine.searchInSource(languageId, source, pattern);
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ // Invalid pattern is global — rethrow.
142
+ if (message.startsWith("Invalid pattern:")) throw error;
143
+ return { matches: [], error: message };
144
+ }
145
+
146
+ let ir: FileIr | undefined;
147
+ let parseDegraded = false;
148
+ if (hits.length > 1) {
149
+ try {
150
+ const file = await engine.sourceForFile(absolutePath);
151
+ ir = file.ir;
152
+ parseDegraded = file.ir.parseDegraded;
153
+ } catch {
154
+ // Enclosing scope is best-effort.
155
+ }
156
+ } else if (hits.length === 1) {
157
+ // Still surface parse uncertainty when IR is cheap via cache after sourceForFile.
158
+ try {
159
+ const file = await engine.sourceForFile(absolutePath);
160
+ parseDegraded = file.ir.parseDegraded;
161
+ // Single hit: only attach enclosing when degraded parse needs trust signal? Spec:
162
+ // enclosing when disambiguating — single hit does not need it.
163
+ ir = undefined;
164
+ } catch {
165
+ // ignore
166
+ }
167
+ }
168
+
169
+ return {
170
+ matches: attachEnclosing(absolutePath, hits, ir, parseDegraded, hits.length > 1),
171
+ error: undefined,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Structural pattern search on one file or a language-filtered directory walk.
177
+ * Matcher and grammar lifecycle live on the engine; this query only walks and shapes.
178
+ */
179
+ export async function astSearch(
180
+ engine: ExploreEngine,
181
+ pathInput: string,
182
+ pattern: string,
183
+ language: string | undefined,
184
+ resultLimit: number,
185
+ signal: AbortSignal,
186
+ ): Promise<AstSearchResult> {
187
+ signal.throwIfAborted();
188
+ assertPattern(pattern);
189
+
190
+ const absolutePath = resolveExplorePath(engine.cwd, pathInput);
191
+ let stats;
192
+ try {
193
+ stats = await lstat(absolutePath);
194
+ } catch (error) {
195
+ throw pathResolutionError(error, pathInput);
196
+ }
197
+
198
+ const isDirectory = stats.isDirectory();
199
+ const languageId = resolveLanguageId(engine, language, isDirectory ? undefined : absolutePath, isDirectory);
200
+
201
+ const matches: AstSearchMatch[] = [];
202
+ const fileErrors: { path: string; message: string }[] = [];
203
+ let resultLimitReached = false;
204
+ let scan: ScanOutcome | undefined;
205
+
206
+ if (!isDirectory) {
207
+ const one = await searchOneFile(engine, absolutePath, languageId, pattern, signal);
208
+ if (one.error !== undefined) {
209
+ throw new Error(one.error);
210
+ }
211
+ if (one.matches.length > resultLimit) {
212
+ resultLimitReached = true;
213
+ matches.push(...one.matches.slice(0, resultLimit));
214
+ } else {
215
+ matches.push(...one.matches);
216
+ }
217
+ return {
218
+ matches,
219
+ resultLimit,
220
+ resultLimitReached,
221
+ languageId,
222
+ scan: undefined,
223
+ fileErrors,
224
+ };
225
+ }
226
+
227
+ // Directory: walk supported files; keep only the requested language.
228
+ const generator = scanSources({
229
+ engine,
230
+ cwd: engine.cwd,
231
+ root: absolutePath,
232
+ signal,
233
+ // scanSources already filters to registered languages; further filter below.
234
+ });
235
+
236
+ let step = await generator.next();
237
+ while (!step.done) {
238
+ signal.throwIfAborted();
239
+ const file = step.value;
240
+ if (file.ir.languageId !== languageId) {
241
+ step = await generator.next();
242
+ continue;
243
+ }
244
+
245
+ // Prefer source already loaded by scan (avoids second read for IR).
246
+ let hits: AstSearchHit[];
247
+ try {
248
+ hits = await engine.searchInSource(languageId, file.source, pattern);
249
+ } catch (error) {
250
+ const message = error instanceof Error ? error.message : String(error);
251
+ if (message.startsWith("Invalid pattern:")) throw error;
252
+ fileErrors.push({ path: file.ir.path, message });
253
+ step = await generator.next();
254
+ continue;
255
+ }
256
+
257
+ const room = resultLimit - matches.length;
258
+ if (room <= 0) {
259
+ if (hits.length > 0) resultLimitReached = true;
260
+ // Still drain walk for budget outcome? Stop early for latency.
261
+ break;
262
+ }
263
+
264
+ const limited = hits.length > room ? hits.slice(0, room) : hits;
265
+ if (hits.length > room) resultLimitReached = true;
266
+
267
+ const withScope = attachEnclosing(
268
+ file.ir.path,
269
+ limited,
270
+ hits.length > 1 ? file.ir : undefined,
271
+ file.ir.parseDegraded,
272
+ hits.length > 1,
273
+ );
274
+ matches.push(...withScope);
275
+
276
+ if (matches.length >= resultLimit) {
277
+ resultLimitReached = true;
278
+ break;
279
+ }
280
+ step = await generator.next();
281
+ }
282
+
283
+ if (signal.aborted) throw new Error("ast_search cancelled");
284
+
285
+ if (step.done) {
286
+ scan = step.value;
287
+ } else {
288
+ scan = {
289
+ limit: undefined,
290
+ filesVisited: 0,
291
+ sourceBytes: 0,
292
+ elapsedMs: 0,
293
+ filesEmitted: matches.length > 0 ? 1 : 0,
294
+ };
295
+ }
296
+ if (scan.limit === "cancelled") throw new Error("ast_search cancelled");
297
+
298
+ return {
299
+ matches,
300
+ resultLimit,
301
+ resultLimitReached,
302
+ languageId,
303
+ scan,
304
+ fileErrors,
305
+ };
306
+ }
@@ -0,0 +1,77 @@
1
+ import { pathResolutionError, resolveExplorePath } from "../../traverse.ts";
2
+ import type { ExploreEngine } from "../engine.ts";
3
+ import type { Candidate } from "../identity.ts";
4
+ import { resolveTarget } from "../identity.ts";
5
+ import { isCallableLike, isTypeLike, type Decl, type DeclKind, type FileIr } from "../ir.ts";
6
+
7
+ export type CompositeTarget = {
8
+ path: string;
9
+ name: string;
10
+ qualifiedName: string;
11
+ kind: DeclKind;
12
+ startLine: number;
13
+ };
14
+
15
+ export type CompositeResolved = {
16
+ scopeDir: string;
17
+ target: CompositeTarget;
18
+ decl: Decl;
19
+ ir: FileIr;
20
+ source: string;
21
+ parseDegraded: boolean;
22
+ };
23
+
24
+ export type CompositeResolveResult =
25
+ | { kind: "resolved"; value: CompositeResolved }
26
+ | { kind: "candidates"; candidates: Candidate[] }
27
+ | { kind: "notFound" }
28
+ | { kind: "error"; message: string };
29
+
30
+ /** Resolve one callable/type target under a directory scope for impact/context. */
31
+ export async function resolveCompositeTarget(
32
+ engine: ExploreEngine,
33
+ scopePath: string,
34
+ targetPath: string | undefined,
35
+ name: string,
36
+ line: number | undefined,
37
+ toolName: "impact" | "context",
38
+ signal: AbortSignal,
39
+ ): Promise<CompositeResolveResult> {
40
+ let scopeDir: string;
41
+ try {
42
+ scopeDir = resolveExplorePath(engine.cwd, scopePath);
43
+ } catch (error) {
44
+ return { kind: "error", message: pathResolutionError(error, scopePath).message };
45
+ }
46
+
47
+ const resolution = await resolveTarget(engine, scopeDir, { path: targetPath, name, line }, signal);
48
+ if (resolution.kind === "candidates") return { kind: "candidates", candidates: resolution.candidates };
49
+ if (resolution.kind === "notFound") return { kind: "notFound" };
50
+
51
+ const kind = resolution.decl.kind;
52
+ if (!isCallableLike(kind) && !isTypeLike(kind)) {
53
+ const alt = toolName === "impact" ? "deps/reverse_deps/references" : "show/references";
54
+ return {
55
+ kind: "error",
56
+ message: `${toolName} supports callable and type targets, not ${kind}. Use ${alt}.`,
57
+ };
58
+ }
59
+
60
+ return {
61
+ kind: "resolved",
62
+ value: {
63
+ scopeDir,
64
+ target: {
65
+ path: resolution.path,
66
+ name: resolution.decl.name,
67
+ qualifiedName: resolution.decl.qualifiedName,
68
+ kind,
69
+ startLine: resolution.decl.startLine,
70
+ },
71
+ decl: resolution.decl,
72
+ ir: resolution.ir,
73
+ source: resolution.source,
74
+ parseDegraded: resolution.ir.parseDegraded,
75
+ },
76
+ };
77
+ }