@shanepadgett/tau-agent 0.33.0 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +130 -93
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -1,21 +1,23 @@
1
1
  import { lstat, opendir } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
- import type { FileDepHost, LanguageAdapter } from "../adapter.ts";
3
+ import type { FileDepHost, FileDepResolution, LanguageAdapter } from "../adapter.ts";
4
4
  import type { ExploreEngine } from "../engine.ts";
5
5
  import { scanSources } from "../scan.ts";
6
- import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../../traverse.ts";
6
+ import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../traverse.ts";
7
7
 
8
8
  export type FileGraphEdge =
9
9
  | { kind: "internal"; from: string; to: string }
10
+ | { kind: "package"; from: string; id: string; dir: string; fileCount: number }
10
11
  | { kind: "external"; from: string; id: string };
11
12
 
12
- export type FileDepHit = {
13
- depth: number;
14
- /** Absolute path for internal hits. */
15
- path: string | undefined;
16
- /** External package/module id. */
17
- externalId: string | undefined;
18
- };
13
+ /** One resolved dependency endpoint, independent of traversal depth. */
14
+ export type FileDepTarget =
15
+ | { kind: "internal"; path: string }
16
+ /** In-repo directory too large to list; `id` is the import as written. */
17
+ | { kind: "package"; id: string; dir: string; fileCount: number }
18
+ | { kind: "external"; id: string };
19
+
20
+ export type FileDepHit = { depth: number } & FileDepTarget;
19
21
 
20
22
  export type FileDepQueryResult = {
21
23
  seedPath: string;
@@ -23,6 +25,8 @@ export type FileDepQueryResult = {
23
25
  hits: FileDepHit[];
24
26
  resultLimit: number;
25
27
  resultLimitReached: boolean;
28
+ /** External edges dropped after the external cap (internal edges keep the full limit). */
29
+ externalOmitted: number;
26
30
  };
27
31
 
28
32
  export type ExploreFileGraph = {
@@ -44,6 +48,9 @@ type ReverseIndex = {
44
48
  byTarget: Map<string, string[]>;
45
49
  };
46
50
 
51
+ /** External ids are cheap noise in bulk; in-repo edges get the caller's full budget. */
52
+ const EXTERNAL_HIT_LIMIT = 10;
53
+
47
54
  function isWithin(parent: string, child: string): boolean {
48
55
  const rel = relative(resolve(parent), resolve(child));
49
56
  return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
@@ -112,6 +119,148 @@ function sortUnique(paths: readonly string[]): string[] {
112
119
  return [...new Set(paths.map((path) => resolve(path)))].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
113
120
  }
114
121
 
122
+ type EdgeSeen = {
123
+ internal: Set<string>;
124
+ package: Set<string>;
125
+ external: Set<string>;
126
+ };
127
+
128
+ function emptyEdgeSeen(): EdgeSeen {
129
+ return { internal: new Set(), package: new Set(), external: new Set() };
130
+ }
131
+
132
+ function appendInternalPaths(
133
+ edges: FileGraphEdge[],
134
+ from: string,
135
+ paths: readonly string[],
136
+ scopeRoot: string,
137
+ ownsPath: (path: string) => boolean,
138
+ seen: Set<string>,
139
+ ): void {
140
+ for (const target of sortUnique(paths)) {
141
+ if (!isWithin(scopeRoot, target)) continue;
142
+ if (!ownsPath(target)) continue;
143
+ if (target === from) continue;
144
+ if (seen.has(target)) continue;
145
+ seen.add(target);
146
+ edges.push({ kind: "internal", from, to: target });
147
+ }
148
+ }
149
+
150
+ function appendResolution(
151
+ edges: FileGraphEdge[],
152
+ from: string,
153
+ resolution: FileDepResolution,
154
+ scopeRoot: string,
155
+ ownsPath: (path: string) => boolean,
156
+ seen: EdgeSeen,
157
+ ): void {
158
+ if (resolution.kind === "unresolved") return;
159
+ if (resolution.kind === "external") {
160
+ if (seen.external.has(resolution.id)) return;
161
+ seen.external.add(resolution.id);
162
+ edges.push({ kind: "external", from, id: resolution.id });
163
+ return;
164
+ }
165
+ if (resolution.kind === "package") {
166
+ if (!isWithin(scopeRoot, resolution.dir) || seen.package.has(resolution.dir)) return;
167
+ seen.package.add(resolution.dir);
168
+ edges.push({
169
+ kind: "package",
170
+ from,
171
+ id: resolution.id,
172
+ dir: resolution.dir,
173
+ fileCount: resolution.fileCount,
174
+ });
175
+ return;
176
+ }
177
+ appendInternalPaths(edges, from, resolution.paths, scopeRoot, ownsPath, seen.internal);
178
+ }
179
+
180
+ function recordImporter(byTarget: Map<string, string[]>, target: string, from: string): void {
181
+ const list = byTarget.get(target);
182
+ if (list === undefined) byTarget.set(target, [from]);
183
+ else if (!list.includes(from)) list.push(from);
184
+ }
185
+
186
+ type ForwardHitState = {
187
+ hits: FileDepHit[];
188
+ resultLimitReached: boolean;
189
+ externalOmitted: number;
190
+ inRepoCount: number;
191
+ externalCount: number;
192
+ listedInternal: Set<string>;
193
+ listedPackage: Set<string>;
194
+ listedExternal: Set<string>;
195
+ };
196
+
197
+ function pushForwardEdge(
198
+ state: ForwardHitState,
199
+ edge: FileGraphEdge,
200
+ nextDepth: number,
201
+ resultLimit: number,
202
+ enqueue: (next: string) => void,
203
+ ): boolean {
204
+ if (edge.kind === "external") {
205
+ if (state.listedExternal.has(edge.id)) return true;
206
+ state.listedExternal.add(edge.id);
207
+ if (state.externalCount >= EXTERNAL_HIT_LIMIT) {
208
+ state.externalOmitted += 1;
209
+ return true;
210
+ }
211
+ state.externalCount += 1;
212
+ state.hits.push({ depth: nextDepth, kind: "external", id: edge.id });
213
+ return true;
214
+ }
215
+ if (state.inRepoCount >= resultLimit) {
216
+ state.resultLimitReached = true;
217
+ return false;
218
+ }
219
+ if (edge.kind === "package") {
220
+ if (state.listedPackage.has(edge.dir)) return true;
221
+ state.listedPackage.add(edge.dir);
222
+ state.inRepoCount += 1;
223
+ state.hits.push({
224
+ depth: nextDepth,
225
+ kind: "package",
226
+ id: edge.id,
227
+ dir: edge.dir,
228
+ fileCount: edge.fileCount,
229
+ });
230
+ return true;
231
+ }
232
+ if (state.listedInternal.has(edge.to)) return true;
233
+ state.listedInternal.add(edge.to);
234
+ state.inRepoCount += 1;
235
+ state.hits.push({ depth: nextDepth, kind: "internal", path: edge.to });
236
+ enqueue(edge.to);
237
+ return true;
238
+ }
239
+
240
+ /** Breadth-first walk bounded by depth. `expand` returns false to stop the walk. */
241
+ async function walkDepth(
242
+ seedPath: string,
243
+ depth: number,
244
+ signal: AbortSignal,
245
+ expand: (path: string, nextDepth: number, enqueue: (next: string) => void) => Promise<boolean>,
246
+ ): Promise<void> {
247
+ const queue: { path: string; depth: number }[] = [{ path: seedPath, depth: 0 }];
248
+ const visited = new Set<string>([seedPath]);
249
+ while (queue.length > 0) {
250
+ signal.throwIfAborted();
251
+ const current = queue.shift();
252
+ if (current === undefined) break;
253
+ if (current.depth >= depth) continue;
254
+ const nextDepth = current.depth + 1;
255
+ const enqueue = (next: string): void => {
256
+ if (nextDepth >= depth || visited.has(next)) return;
257
+ visited.add(next);
258
+ queue.push({ path: next, depth: nextDepth });
259
+ };
260
+ if (!(await expand(current.path, nextDepth, enqueue))) break;
261
+ }
262
+ }
263
+
115
264
  /**
116
265
  * Session file-import graph. Shared code never interprets import syntax —
117
266
  * adapters own resolveFileDep.
@@ -152,33 +301,35 @@ export function createFileGraph(engine: ExploreEngine): ExploreFileGraph {
152
301
 
153
302
  const host = makeHost(scopeRoot);
154
303
  const edges: FileGraphEdge[] = [];
155
- const seenInternal = new Set<string>();
156
- const seenExternal = new Set<string>();
157
-
304
+ const seen = emptyEdgeSeen();
158
305
  for (const imp of ir.imports) {
159
306
  signal.throwIfAborted();
160
307
  const resolution = await resolveFileDep(absolutePath, imp.specifier, host, signal);
161
- if (resolution.kind === "unresolved") continue;
162
- if (resolution.kind === "external") {
163
- if (seenExternal.has(resolution.id)) continue;
164
- seenExternal.add(resolution.id);
165
- edges.push({ kind: "external", from: absolutePath, id: resolution.id });
166
- continue;
167
- }
168
- for (const target of sortUnique(resolution.paths)) {
169
- if (!isWithin(scopeRoot, target)) continue;
170
- if (!host.ownsPath(target)) continue;
171
- if (target === absolutePath) continue;
172
- if (seenInternal.has(target)) continue;
173
- seenInternal.add(target);
174
- edges.push({ kind: "internal", from: absolutePath, to: target });
175
- }
308
+ appendResolution(edges, absolutePath, resolution, scopeRoot, host.ownsPath, seen);
176
309
  }
177
310
 
178
311
  forwardCache.set(absolutePath, { contentHash: ir.contentHash, edges });
179
312
  return edges;
180
313
  };
181
314
 
315
+ const indexForwardImporters = async (
316
+ filePath: string,
317
+ scopeRoot: string,
318
+ byTarget: Map<string, string[]>,
319
+ signal: AbortSignal,
320
+ ): Promise<void> => {
321
+ const adapter = engine.registry.adapterForPath(filePath);
322
+ if (adapter?.capabilities.fileDeps !== true || adapter.resolveFileDep === undefined) return;
323
+ try {
324
+ const edges = await loadForward(filePath, scopeRoot, signal);
325
+ for (const edge of edges) {
326
+ if (edge.kind === "internal") recordImporter(byTarget, edge.to, edge.from);
327
+ }
328
+ } catch {
329
+ // Skip files that fail resolve/parse during reverse build; keep scanning.
330
+ }
331
+ };
332
+
182
333
  const ensureReverse = async (scopeRoot: string, signal: AbortSignal): Promise<ReverseIndex> => {
183
334
  const cached = reverseCache.get(scopeRoot);
184
335
  if (cached !== undefined) return cached;
@@ -193,21 +344,7 @@ export function createFileGraph(engine: ExploreEngine): ExploreFileGraph {
193
344
  let step = await scan.next();
194
345
  while (!step.done) {
195
346
  signal.throwIfAborted();
196
- const filePath = step.value.ir.path;
197
- const adapter = engine.registry.adapterForPath(filePath);
198
- if (adapter?.capabilities.fileDeps === true && adapter.resolveFileDep !== undefined) {
199
- try {
200
- const edges = await loadForward(filePath, scopeRoot, signal);
201
- for (const edge of edges) {
202
- if (edge.kind !== "internal") continue;
203
- const list = byTarget.get(edge.to);
204
- if (list === undefined) byTarget.set(edge.to, [edge.from]);
205
- else if (!list.includes(edge.from)) list.push(edge.from);
206
- }
207
- } catch {
208
- // Skip files that fail resolve/parse during reverse build; keep scanning.
209
- }
210
- }
347
+ await indexForwardImporters(step.value.ir.path, scopeRoot, byTarget, signal);
211
348
  step = await scan.next();
212
349
  }
213
350
 
@@ -227,46 +364,33 @@ export function createFileGraph(engine: ExploreEngine): ExploreFileGraph {
227
364
  signal: AbortSignal,
228
365
  ): Promise<FileDepQueryResult> => {
229
366
  const scopeRoot = await scopeRootFor(seedPath, engine.cwd);
230
- const hits: FileDepHit[] = [];
231
- let resultLimitReached = false;
232
- const listedInternal = new Set<string>();
233
- const listedExternal = new Set<string>();
234
-
235
- const queue: { path: string; depth: number }[] = [{ path: seedPath, depth: 0 }];
236
- const visited = new Set<string>([seedPath]);
237
-
238
- while (queue.length > 0) {
239
- signal.throwIfAborted();
240
- const current = queue.shift();
241
- if (current === undefined) break;
242
- if (current.depth >= depth) continue;
243
-
244
- const edges = await loadForward(current.path, scopeRoot, signal);
245
- const nextDepth = current.depth + 1;
246
-
367
+ const state: ForwardHitState = {
368
+ hits: [],
369
+ resultLimitReached: false,
370
+ externalOmitted: 0,
371
+ inRepoCount: 0,
372
+ externalCount: 0,
373
+ listedInternal: new Set(),
374
+ listedPackage: new Set(),
375
+ listedExternal: new Set(),
376
+ };
377
+
378
+ await walkDepth(seedPath, depth, signal, async (path, nextDepth, enqueue) => {
379
+ const edges = await loadForward(path, scopeRoot, signal);
247
380
  for (const edge of edges) {
248
- if (hits.length >= resultLimit) {
249
- resultLimitReached = true;
250
- break;
251
- }
252
- if (edge.kind === "external") {
253
- if (listedExternal.has(edge.id)) continue;
254
- listedExternal.add(edge.id);
255
- hits.push({ depth: nextDepth, path: undefined, externalId: edge.id });
256
- continue;
257
- }
258
- if (listedInternal.has(edge.to)) continue;
259
- listedInternal.add(edge.to);
260
- hits.push({ depth: nextDepth, path: edge.to, externalId: undefined });
261
- if (nextDepth < depth && !visited.has(edge.to)) {
262
- visited.add(edge.to);
263
- queue.push({ path: edge.to, depth: nextDepth });
264
- }
381
+ if (!pushForwardEdge(state, edge, nextDepth, resultLimit, enqueue)) break;
265
382
  }
266
- if (resultLimitReached) break;
267
- }
383
+ return !state.resultLimitReached;
384
+ });
268
385
 
269
- return { seedPath, depth, hits, resultLimit, resultLimitReached };
386
+ return {
387
+ seedPath,
388
+ depth,
389
+ hits: state.hits,
390
+ resultLimit,
391
+ resultLimitReached: state.resultLimitReached,
392
+ externalOmitted: state.externalOmitted,
393
+ };
270
394
  };
271
395
 
272
396
  const collectReverseHits = async (
@@ -281,17 +405,8 @@ export function createFileGraph(engine: ExploreEngine): ExploreFileGraph {
281
405
  let resultLimitReached = false;
282
406
  const listed = new Set<string>();
283
407
 
284
- const queue: { path: string; depth: number }[] = [{ path: seedPath, depth: 0 }];
285
- const visited = new Set<string>([seedPath]);
286
-
287
- while (queue.length > 0) {
288
- signal.throwIfAborted();
289
- const current = queue.shift();
290
- if (current === undefined) break;
291
- if (current.depth >= depth) continue;
292
-
293
- const importers = index.byTarget.get(current.path) ?? [];
294
- const nextDepth = current.depth + 1;
408
+ await walkDepth(seedPath, depth, signal, async (path, nextDepth, enqueue) => {
409
+ const importers = index.byTarget.get(path) ?? [];
295
410
  for (const importer of importers) {
296
411
  if (hits.length >= resultLimit) {
297
412
  resultLimitReached = true;
@@ -299,16 +414,13 @@ export function createFileGraph(engine: ExploreEngine): ExploreFileGraph {
299
414
  }
300
415
  if (listed.has(importer)) continue;
301
416
  listed.add(importer);
302
- hits.push({ depth: nextDepth, path: importer, externalId: undefined });
303
- if (nextDepth < depth && !visited.has(importer)) {
304
- visited.add(importer);
305
- queue.push({ path: importer, depth: nextDepth });
306
- }
417
+ hits.push({ depth: nextDepth, kind: "internal", path: importer });
418
+ enqueue(importer);
307
419
  }
308
- if (resultLimitReached) break;
309
- }
420
+ return !resultLimitReached;
421
+ });
310
422
 
311
- return { seedPath, depth, hits, resultLimit, resultLimitReached };
423
+ return { seedPath, depth, hits, resultLimit, resultLimitReached, externalOmitted: 0 };
312
424
  };
313
425
 
314
426
  const resolveSeedFile = async (pathInput: string): Promise<string> => {