@shanepadgett/tau-agent 0.33.1 → 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 +122 -82
  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
@@ -0,0 +1,135 @@
1
+ import { resolve } from "node:path";
2
+ import type { FileDepResolver } from "../adapter.ts";
3
+ import { buildDottedIndex, resolveDottedFile, resolveDottedPackage, type HeadScan } from "./dotted-index.ts";
4
+ import { externalId, internalPaths, isWithin, packageDirectory } from "./file-dep-util.ts";
5
+
6
+ /**
7
+ * Java / Kotlin file deps. Real multi-module builds put module B's sources
8
+ * anywhere, so resolution goes through a package-path index rather than a fixed
9
+ * list of source-root suffixes: a type import resolves to the declaring file, a
10
+ * wildcard or top-level member import to the package directory.
11
+ */
12
+
13
+ type JvmFileDepOptions = {
14
+ /** File extensions to match, e.g. .java / .kt */
15
+ extensions: readonly string[];
16
+ /** Leading packages always treated as external. */
17
+ externalPrefixes: readonly string[];
18
+ /** Top-level declaration keywords, in this language's syntax. */
19
+ declarationPattern: RegExp;
20
+ memoPrefix: string;
21
+ };
22
+
23
+ const PACKAGE_LINE = /^[ \t]*package\s+([\w.]+)\s*;?[ \t]*$/mu;
24
+
25
+ function scanJvmHead(head: string, declarationPattern: RegExp): HeadScan {
26
+ const pkg = PACKAGE_LINE.exec(head)?.[1] ?? "";
27
+ if (pkg.length === 0) return { packages: [], names: [] };
28
+ const names: HeadScan["names"] = [];
29
+ // Fresh lastIndex per scan: the pattern is shared across files.
30
+ const pattern = new RegExp(declarationPattern.source, declarationPattern.flags);
31
+ for (const match of head.matchAll(pattern)) {
32
+ const name = match[2];
33
+ if (name !== undefined) names.push({ pkg, name });
34
+ }
35
+ return { packages: [pkg], names };
36
+ }
37
+
38
+ function startsLower(text: string): boolean {
39
+ const first = text.charAt(0);
40
+ return first.length > 0 && first === first.toLowerCase() && first !== first.toUpperCase();
41
+ }
42
+
43
+ /**
44
+ * Split an import into the dotted path plus how it should resolve.
45
+ * Package segments are lowercase and type segments capitalized, so the first
46
+ * capitalized segment ends a type import; an import with none names a package
47
+ * member (Kotlin top-level fun/val) and resolves to the package.
48
+ */
49
+ function classifyImport(specifier: string): { path: string; packageLike: boolean } {
50
+ let s = specifier.trim().replace(/;$/u, "");
51
+ s = s.replace(/^static\s+/u, "");
52
+ if (s.endsWith(".*")) {
53
+ // Keep the full package path — do not peel segments (all-lowercase packages).
54
+ return { path: s.slice(0, -2).trim(), packageLike: true };
55
+ }
56
+ const parts = s.split(".").filter((part) => part.length > 0);
57
+ const typeAt = parts.findIndex((part) => !startsLower(part));
58
+ // pkg.Type.member / pkg.Type.Companion.member all resolve through pkg.Type.
59
+ if (typeAt >= 0) return { path: parts.slice(0, typeAt + 1).join("."), packageLike: false };
60
+ if (parts.length <= 1) return { path: parts.join("."), packageLike: false };
61
+ return { path: parts.slice(0, -1).join("."), packageLike: true };
62
+ }
63
+
64
+ function isExternalPackage(path: string, prefixes: readonly string[]): boolean {
65
+ for (const prefix of prefixes) {
66
+ if (path === prefix || path.startsWith(`${prefix}.`)) return true;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ function createJvmFileDepResolver(options: JvmFileDepOptions): FileDepResolver {
72
+ return async (fromPath, specifier, host, signal) => {
73
+ signal.throwIfAborted();
74
+ const { path, packageLike } = classifyImport(specifier);
75
+ if (path.length === 0) return { kind: "unresolved" };
76
+ if (isExternalPackage(path, options.externalPrefixes)) return externalId(path);
77
+
78
+ const index = await buildDottedIndex(
79
+ host,
80
+ options.memoPrefix,
81
+ options.extensions,
82
+ (head) => scanJvmHead(head, options.declarationPattern),
83
+ signal,
84
+ );
85
+
86
+ if (packageLike) {
87
+ const pkg = resolveDottedPackage(index, fromPath, path);
88
+ if (pkg === undefined || !isWithin(host.scopeRoot, pkg.dir)) return externalId(path);
89
+ return packageDirectory(path, pkg.dir, pkg.fileCount);
90
+ }
91
+
92
+ const file = resolveDottedFile(index, fromPath, path);
93
+ if (file !== undefined && isWithin(host.scopeRoot, file) && host.ownsPath(file)) {
94
+ if (resolve(file) === resolve(fromPath)) return { kind: "unresolved" };
95
+ return internalPaths([file]);
96
+ }
97
+ // No declaring file, but the package may still be in the repo: generated
98
+ // sources (Android `R`, Apollo fragments) and declarations below the head
99
+ // window land here, and the package they live in is the real dependency.
100
+ const owningPackage = path.split(".").slice(0, -1).join(".");
101
+ const pkg = owningPackage.length === 0 ? undefined : resolveDottedPackage(index, fromPath, owningPackage);
102
+ if (pkg === undefined || !isWithin(host.scopeRoot, pkg.dir)) return externalId(path);
103
+ return packageDirectory(owningPackage, pkg.dir, pkg.fileCount);
104
+ };
105
+ }
106
+
107
+ const MODIFIERS =
108
+ "public|protected|private|static|final|abstract|sealed|non-sealed|strictfp|default|internal|open|data|value|annotation|enum|expect|actual|external|inline|suspend|const|lateinit|companion|operator|infix|tailrec";
109
+
110
+ /** `@Anno`s and modifiers, then the keyword and the declared name. */
111
+ const JAVA_DECLARATION = new RegExp(
112
+ `^[ \\t]*(?:@[\\w.]+(?:\\([^)]*\\))?[ \\t]*)*(?:(?:${MODIFIERS})[ \\t]+)*(class|interface|enum|record|@interface)\\s+([A-Za-z_]\\w*)`,
113
+ "gmu",
114
+ );
115
+
116
+ /** Kotlin also indexes top-level fun/val/typealias; the optional dotted part is an extension receiver. */
117
+ const KOTLIN_DECLARATION = new RegExp(
118
+ `^[ \\t]*(?:@[\\w.]+(?:\\([^)]*\\))?[ \\t]*)*(?:(?:${MODIFIERS})[ \\t]+)*(class|interface|object|fun|val|var|typealias)\\s+(?:<[^>]*>\\s+)?(?:[A-Za-z_][\\w.]*\\.)?([A-Za-z_]\\w*)`,
119
+ "gmu",
120
+ );
121
+
122
+ export const resolveJavaFileDep = createJvmFileDepResolver({
123
+ extensions: [".java"],
124
+ externalPrefixes: ["java", "javax", "jakarta", "jdk", "sun"],
125
+ declarationPattern: JAVA_DECLARATION,
126
+ memoPrefix: "java-dotted",
127
+ });
128
+
129
+ export const resolveKotlinFileDep = createJvmFileDepResolver({
130
+ // .kts is often Gradle — not a package type file.
131
+ extensions: [".kt", ".java"],
132
+ externalPrefixes: ["java", "javax", "jakarta", "kotlin", "kotlinx"],
133
+ declarationPattern: KOTLIN_DECLARATION,
134
+ memoPrefix: "kotlin-dotted",
135
+ });
@@ -3,17 +3,15 @@ import type { ExtractResult, GrammarAdapter, LanguageCapabilities } from "../ada
3
3
  import { resolveKotlinFileDep } from "./jvm-file-deps.ts";
4
4
  import type { CallSite, Decl, ImportRef, Visibility } from "../ir.ts";
5
5
  import {
6
- applyDoc,
7
6
  callSite,
7
+ type DocSpan,
8
8
  docSpanBefore,
9
9
  docSpanTrailingChild,
10
- endLine,
11
- finishDecl,
10
+ createLeafBuilder,
12
11
  nameText,
13
12
  qualify,
14
13
  startLine,
15
14
  walkBodyNodes,
16
- type DocSpan,
17
15
  } from "./tree.ts";
18
16
 
19
17
  const CAPABILITIES: LanguageCapabilities = {
@@ -22,6 +20,7 @@ const CAPABILITIES: LanguageCapabilities = {
22
20
  fileDeps: true,
23
21
  callEdges: true,
24
22
  packageSurface: false,
23
+ structuralImplements: false,
25
24
  };
26
25
 
27
26
  const IMPORT_NOISE = new Set([
@@ -282,40 +281,7 @@ function functionBody(node: Node): Node | null {
282
281
  return body ?? null;
283
282
  }
284
283
 
285
- function leaf(
286
- node: Node,
287
- owner: string,
288
- kind: Decl["kind"],
289
- name: string,
290
- visibility: Visibility,
291
- body: Node | null,
292
- children: Decl[],
293
- doc: DocSpan | undefined,
294
- ): Decl | undefined {
295
- if (name.length === 0) return undefined;
296
- const callable = kind === "function" || kind === "method" || kind === "constructor";
297
- const typeLike =
298
- kind === "class" || kind === "interface" || kind === "enum" || kind === "struct" || kind === "object";
299
- const decl = finishDecl(
300
- {
301
- kind,
302
- name,
303
- qualifiedName: qualify(owner, name),
304
- startLine: startLine(node),
305
- endLine: endLine(node),
306
- startOffset: node.startIndex,
307
- endOffset: node.endIndex,
308
- visibility,
309
- exported: visibility === "public" || visibility === "internal",
310
- children,
311
- calls: callable ? extractCalls(body) : [],
312
- bases: typeLike ? heritageBases(node) : [],
313
- },
314
- body,
315
- );
316
- if (body === null) decl.signatureEndOffset = node.endIndex;
317
- return applyDoc(decl, doc);
318
- }
284
+ const leaf = createLeafBuilder(extractCalls, heritageBases);
319
285
 
320
286
  function primaryConstructorProperties(ctor: Node, owner: string): Decl[] {
321
287
  const out: Decl[] = [];
@@ -595,7 +561,7 @@ function extractKotlin(tree: Tree, source: string): ExtractResult {
595
561
  for (const node of root.children) {
596
562
  if (node.isNamed || node.isError) decls.push(...declsFromNode(node, "", source));
597
563
  }
598
- return { decls: rehomeOrphanMembers(decls), imports: collectImports(root) };
564
+ return { decls: rehomeOrphanMembers(decls), imports: collectImports(root), fileCalls: extractCalls(root) };
599
565
  }
600
566
 
601
567
  export function kotlinAdapter(): GrammarAdapter {
@@ -27,9 +27,9 @@ async function resolveModuleChain(
27
27
  const candidates = await moduleFileCandidates(dir, name);
28
28
  const file = await firstExistingFile(host, candidates);
29
29
  if (file === undefined) {
30
- // Last segment may be an item in the parent module — keep parent file.
31
- if (i === segments.length - 1 && lastFile !== undefined) return lastFile;
32
- return undefined;
30
+ // Remaining segments are items inside the deepest module that exists
31
+ // (inline `mod`, re-export, cfg-gated shim). Never drop the edge silently.
32
+ return lastFile;
33
33
  }
34
34
  lastFile = file;
35
35
  // Next modules live beside foo.rs → foo/ or inside foo/ for mod.rs
@@ -23,6 +23,7 @@ const CAPABILITIES: LanguageCapabilities = {
23
23
  fileDeps: true,
24
24
  callEdges: true,
25
25
  packageSurface: false,
26
+ structuralImplements: false,
26
27
  };
27
28
 
28
29
  const IMPORT_NOISE = new Set([
@@ -393,32 +394,39 @@ function declsFromItem(
393
394
  return decl === undefined ? [] : [decl];
394
395
  }
395
396
  case IMPL_ITEM: {
396
- // Methods become siblings under the target type name (inherent + trait impls).
397
397
  const target = rustTypeName(field(node, "type"));
398
398
  const body = field(node, "body");
399
399
  if (body === null || body.type !== DECLARATION_LIST || target.length === 0) return [];
400
400
  const methodOwner = owner.length === 0 ? target : qualify(owner, target);
401
401
  const methods = walkDeclList(body, owner, source, methodOwner, true);
402
- // Marker decl so `implementations Trait` finds this impl via bases (trait name).
403
402
  const trait = rustTypeName(field(node, "trait"));
404
- if (trait.length === 0) return methods;
405
- const marker = finishDecl(
403
+ // The impl block is the container its methods belong to: named for what it
404
+ // is, so it never shadows the target type, and holding its own methods.
405
+ const name = trait.length === 0 ? `impl ${target}` : `impl ${trait} for ${target}`;
406
+ // An impl block carries no visibility modifier; its surface is its methods'.
407
+ const visibility: Visibility = methods.some((method) => method.visibility === "public")
408
+ ? "public"
409
+ : methods.some((method) => method.visibility === "internal")
410
+ ? "internal"
411
+ : "private";
412
+ const container = finishDecl(
406
413
  {
407
414
  kind: "class",
408
- name: target,
409
- qualifiedName: methodOwner,
415
+ name,
416
+ qualifiedName: name,
410
417
  startLine: node.startPosition.row + 1,
411
418
  endLine: endLine(node),
412
419
  startOffset: node.startIndex,
413
420
  endOffset: node.endIndex,
414
- visibility: "public",
415
- exported: true,
416
- children: [],
417
- bases: [trait],
421
+ visibility,
422
+ exported: rustExported(visibility),
423
+ children: methods,
424
+ // `implementations Trait` finds this impl through the trait base.
425
+ bases: trait.length === 0 ? [] : [trait],
418
426
  },
419
- null,
427
+ body,
420
428
  );
421
- return [marker, ...methods];
429
+ return [applyDoc(container, doc)];
422
430
  }
423
431
  case MOD_ITEM: {
424
432
  const name = nameText(field(node, "name"));
@@ -490,7 +498,7 @@ function extractRust(tree: Tree, source: string): ExtractResult {
490
498
  }
491
499
  const imports: ImportRef[] = [];
492
500
  collectImports(root, imports);
493
- return { decls, imports };
501
+ return { decls, imports, fileCalls: extractCalls(root) };
494
502
  }
495
503
 
496
504
  export function rustAdapter(): GrammarAdapter {
@@ -1,6 +1,12 @@
1
- import { basename, join, resolve } from "node:path";
1
+ import { join, resolve, sep } from "node:path";
2
2
  import type { FileDepResolver } from "../adapter.ts";
3
- import { collectFilesWithExtensions, externalId, internalPaths, isWithin } from "./file-dep-util.ts";
3
+ import {
4
+ collectFilesWithExtensions,
5
+ externalId,
6
+ isWithin,
7
+ packageDirectory,
8
+ pickClosestPath,
9
+ } from "./file-dep-util.ts";
4
10
 
5
11
  async function findModuleDirs(
6
12
  host: Parameters<FileDepResolver>[2],
@@ -37,23 +43,24 @@ async function findModuleDirs(
37
43
  return hits;
38
44
  }
39
45
 
40
- export const resolveSwiftFileDep: FileDepResolver = async (_fromPath, specifier, host, signal) => {
46
+ /**
47
+ * `import SomeModule` names a module, not a file list: expanding it to every
48
+ * file in the module directory made `deps` and `impact` unusable on real corpora.
49
+ * One package edge for the module directory instead.
50
+ */
51
+ export const resolveSwiftFileDep: FileDepResolver = async (fromPath, specifier, host, signal) => {
41
52
  signal.throwIfAborted();
42
53
  const moduleName = specifier.trim().split(/\s+/u)[0] ?? "";
43
54
  if (moduleName.length === 0) return { kind: "unresolved" };
44
55
 
45
56
  // Skip obvious system modules when no local dir exists — still try local first.
46
57
  const dirs = await findModuleDirs(host, moduleName, signal);
47
- const files: string[] = [];
48
- for (const dir of dirs) {
49
- if (!isWithin(host.scopeRoot, dir)) continue;
50
- const found = await collectFilesWithExtensions(host, dir, [".swift"], signal, 500);
51
- for (const file of found) {
52
- // Don't treat the module root listing as importing itself only — include all.
53
- if (basename(file) === moduleName) continue;
54
- files.push(file);
55
- }
56
- }
57
- if (files.length === 0) return externalId(moduleName);
58
- return internalPaths(files);
58
+ const inScope = dirs.filter((dir) => isWithin(host.scopeRoot, dir));
59
+ const dir = pickClosestPath(inScope, fromPath);
60
+ if (dir === undefined) return externalId(moduleName);
61
+ const files = await collectFilesWithExtensions(host, dir, [".swift"], signal, 500);
62
+ // Package.swift is the manifest, not module source.
63
+ const sources = files.filter((file) => !file.endsWith(`${sep}Package.swift`));
64
+ if (sources.length === 0) return externalId(moduleName);
65
+ return packageDirectory(moduleName, dir, sources.length);
59
66
  };
@@ -3,17 +3,15 @@ import type { ExtractResult, GrammarAdapter, LanguageCapabilities } from "../ada
3
3
  import { resolveSwiftFileDep } from "./swift-file-deps.ts";
4
4
  import type { CallSite, Decl, ImportRef, Visibility } from "../ir.ts";
5
5
  import {
6
- applyDoc,
7
6
  callSite,
7
+ type DocSpan,
8
8
  docSpanBefore,
9
- endLine,
10
9
  field,
11
- finishDecl,
10
+ createLeafBuilder,
12
11
  nameText,
13
12
  qualify,
14
13
  startLine,
15
14
  walkBodyNodes,
16
- type DocSpan,
17
15
  } from "./tree.ts";
18
16
 
19
17
  const CAPABILITIES: LanguageCapabilities = {
@@ -22,6 +20,7 @@ const CAPABILITIES: LanguageCapabilities = {
22
20
  fileDeps: true,
23
21
  callEdges: true,
24
22
  packageSurface: false,
23
+ structuralImplements: false,
25
24
  };
26
25
 
27
26
  const IMPORT_NOISE = new Set([
@@ -242,40 +241,7 @@ function bodyOf(node: Node): Node | null {
242
241
  );
243
242
  }
244
243
 
245
- function leaf(
246
- node: Node,
247
- owner: string,
248
- kind: Decl["kind"],
249
- name: string,
250
- visibility: Visibility,
251
- body: Node | null,
252
- children: Decl[],
253
- doc: DocSpan | undefined,
254
- ): Decl | undefined {
255
- if (name.length === 0) return undefined;
256
- const callable = kind === "function" || kind === "method" || kind === "constructor";
257
- const typeLike =
258
- kind === "class" || kind === "interface" || kind === "enum" || kind === "struct" || kind === "object";
259
- const decl = finishDecl(
260
- {
261
- kind,
262
- name,
263
- qualifiedName: qualify(owner, name),
264
- startLine: startLine(node),
265
- endLine: endLine(node),
266
- startOffset: node.startIndex,
267
- endOffset: node.endIndex,
268
- visibility,
269
- exported: visibility === "public",
270
- children,
271
- calls: callable ? extractCalls(body) : [],
272
- bases: typeLike ? heritageBases(node) : [],
273
- },
274
- body,
275
- );
276
- if (body === null) decl.signatureEndOffset = node.endIndex;
277
- return applyDoc(decl, doc);
278
- }
244
+ const leaf = createLeafBuilder(extractCalls, heritageBases);
279
245
 
280
246
  function classKind(kindText: string): Decl["kind"] {
281
247
  if (kindText === "struct") return "struct";
@@ -450,7 +416,11 @@ function extractSwift(tree: Tree, source: string): ExtractResult {
450
416
  }
451
417
  decls.push(...declsFromNode(node, "", source));
452
418
  }
453
- return { decls: mergeExtensionMembers(decls, extensions), imports: collectImports(root) };
419
+ return {
420
+ decls: mergeExtensionMembers(decls, extensions),
421
+ imports: collectImports(root),
422
+ fileCalls: extractCalls(root),
423
+ };
454
424
  }
455
425
 
456
426
  export function swiftAdapter(): GrammarAdapter {
@@ -186,6 +186,53 @@ export function callSite(
186
186
  return { name, receiver, line, startOffset, endOffset, kind };
187
187
  }
188
188
 
189
+ const CALLABLE_KINDS = new Set<Decl["kind"]>(["function", "method", "constructor"]);
190
+ const TYPE_LIKE_KINDS = new Set<Decl["kind"]>(["class", "interface", "enum", "struct", "object"]);
191
+
192
+ export type LeafBuilder = (
193
+ node: Node,
194
+ owner: string,
195
+ kind: Decl["kind"],
196
+ name: string,
197
+ visibility: Decl["visibility"],
198
+ body: Node | null,
199
+ children: Decl[],
200
+ doc: DocSpan | undefined,
201
+ ) => Decl | undefined;
202
+
203
+ /**
204
+ * Builds one leaf decl per grammar adapter: calls come from callable bodies,
205
+ * bases from type-like heritage, and the span closes on the signature when the
206
+ * node has no body.
207
+ */
208
+ export function createLeafBuilder(
209
+ extractCalls: (body: Node | null) => CallSite[],
210
+ heritageBases: (node: Node) => string[],
211
+ ): LeafBuilder {
212
+ return (node, owner, kind, name, visibility, body, children, doc) => {
213
+ if (name.length === 0) return undefined;
214
+ const decl = finishDecl(
215
+ {
216
+ kind,
217
+ name,
218
+ qualifiedName: qualify(owner, name),
219
+ startLine: startLine(node),
220
+ endLine: endLine(node),
221
+ startOffset: node.startIndex,
222
+ endOffset: node.endIndex,
223
+ visibility,
224
+ exported: visibility === "public",
225
+ children,
226
+ calls: CALLABLE_KINDS.has(kind) ? extractCalls(body) : [],
227
+ bases: TYPE_LIKE_KINDS.has(kind) ? heritageBases(node) : [],
228
+ },
229
+ body,
230
+ );
231
+ if (body === null) decl.signatureEndOffset = node.endIndex;
232
+ return applyDoc(decl, doc);
233
+ };
234
+ }
235
+
189
236
  /** Build a top-level-ish decl from a grammar node span + optional body. */
190
237
  export function declFromNode(
191
238
  node: Node,
@@ -239,6 +239,111 @@ async function resolveViaPaths(
239
239
  return undefined;
240
240
  }
241
241
 
242
+ /** package name → package root, from the scope root's npm workspaces globs. */
243
+ type WorkspaceMap = Map<string, string>;
244
+
245
+ async function readManifest(path: string): Promise<Record<string, unknown> | undefined> {
246
+ const json = await loadTsconfigFile(path);
247
+ return json;
248
+ }
249
+
250
+ function workspaceGlobs(manifest: Record<string, unknown>): string[] {
251
+ const value = manifest.workspaces;
252
+ if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string");
253
+ const packages = asRecord(value)?.packages;
254
+ if (Array.isArray(packages)) return packages.filter((entry): entry is string => typeof entry === "string");
255
+ return [];
256
+ }
257
+
258
+ /**
259
+ * Workspace packages resolve through an npm symlink the resolver must not follow:
260
+ * a `/node_modules/` path fails `host.ownsPath` and would duplicate the real file.
261
+ * Expand the globs against the scope root instead.
262
+ */
263
+ async function loadWorkspacePackages(host: FileDepHost, signal: AbortSignal): Promise<WorkspaceMap> {
264
+ const key = `ts-workspaces:${host.scopeRoot}`;
265
+ const cached = host.memo.get(key);
266
+ if (cached !== undefined) return cached as WorkspaceMap;
267
+
268
+ const map: WorkspaceMap = new Map();
269
+ const root = await readManifest(join(host.scopeRoot, "package.json"));
270
+ for (const glob of root === undefined ? [] : workspaceGlobs(root)) {
271
+ signal.throwIfAborted();
272
+ const star = glob.indexOf("*");
273
+ const dirs: string[] = [];
274
+ if (star === -1) {
275
+ dirs.push(resolve(host.scopeRoot, glob));
276
+ } else {
277
+ const parent = resolve(host.scopeRoot, glob.slice(0, star).replace(/\/$/u, ""));
278
+ for (const name of await host.readDir(parent)) {
279
+ if (name.startsWith(".")) continue;
280
+ dirs.push(join(parent, name));
281
+ }
282
+ }
283
+ for (const dir of dirs) {
284
+ const manifest = await readManifest(join(dir, "package.json"));
285
+ const name = manifest?.name;
286
+ if (typeof name === "string" && name.length > 0) map.set(name, dir);
287
+ }
288
+ }
289
+
290
+ host.memo.set(key, map);
291
+ return map;
292
+ }
293
+
294
+ function entryTargets(manifest: Record<string, unknown>): string[] {
295
+ const out: string[] = [];
296
+ const exports = manifest.exports;
297
+ if (typeof exports === "string") out.push(exports);
298
+ const dot = asRecord(exports)?.["."];
299
+ if (typeof dot === "string") out.push(dot);
300
+ const conditions = asRecord(dot);
301
+ if (conditions !== undefined) {
302
+ for (const field of ["types", "import", "module", "default"] as const) {
303
+ const value = conditions[field];
304
+ if (typeof value === "string") out.push(value);
305
+ }
306
+ }
307
+ for (const field of ["types", "module", "main"] as const) {
308
+ const value = manifest[field];
309
+ if (typeof value === "string") out.push(value);
310
+ }
311
+ out.push("src/index.ts", "index.ts");
312
+ return out;
313
+ }
314
+
315
+ async function resolveWorkspacePackage(
316
+ host: FileDepHost,
317
+ specifier: string,
318
+ signal: AbortSignal,
319
+ ): Promise<string | undefined> {
320
+ const workspaces = await loadWorkspacePackages(host, signal);
321
+ if (workspaces.size === 0) return undefined;
322
+
323
+ let matched: { name: string; dir: string } | undefined;
324
+ for (const [name, dir] of workspaces) {
325
+ if (specifier !== name && !specifier.startsWith(`${name}/`)) continue;
326
+ if (matched === undefined || name.length > matched.name.length) matched = { name, dir };
327
+ }
328
+ if (matched === undefined) return undefined;
329
+
330
+ if (specifier.length > matched.name.length) {
331
+ const subpath = specifier.slice(matched.name.length + 1);
332
+ return (
333
+ (await resolveExistingSource(host, resolve(matched.dir, subpath))) ??
334
+ (await resolveExistingSource(host, resolve(matched.dir, "src", subpath)))
335
+ );
336
+ }
337
+
338
+ const manifest = await readManifest(join(matched.dir, "package.json"));
339
+ if (manifest === undefined) return undefined;
340
+ for (const target of entryTargets(manifest)) {
341
+ const file = await resolveExistingSource(host, resolve(matched.dir, target));
342
+ if (file !== undefined) return file;
343
+ }
344
+ return undefined;
345
+ }
346
+
242
347
  export const resolveTypescriptFileDep: FileDepResolver = async (fromPath, specifier, host, signal) => {
243
348
  signal.throwIfAborted();
244
349
  const trimmed = specifier.trim();
@@ -255,5 +360,8 @@ export const resolveTypescriptFileDep: FileDepResolver = async (fromPath, specif
255
360
  const aliased = await resolveViaPaths(host, fromPath, trimmed, signal);
256
361
  if (aliased !== undefined) return internalPaths([aliased]);
257
362
 
363
+ const workspace = await resolveWorkspacePackage(host, trimmed, signal);
364
+ if (workspace !== undefined && isWithin(host.scopeRoot, workspace)) return internalPaths([workspace]);
365
+
258
366
  return externalId(packageNameFromBareSpecifier(trimmed));
259
367
  };
@@ -3,7 +3,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node
3
3
  import type { Decl } from "../ir.ts";
4
4
  import type { FileSource } from "../engine.ts";
5
5
  import { packageDeclKey, type PackageSurfaceHost, type PackageSurfaceResolver } from "../package-surface.ts";
6
- import { formatPathForDisplay } from "../../traverse.ts";
6
+ import { formatPathForDisplay } from "../traverse.ts";
7
7
 
8
8
  const RE_EXPORT_DEPTH = 4;
9
9
  const TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".mtsx"] as const;
@@ -40,6 +40,7 @@ async function fileExists(path: string): Promise<boolean> {
40
40
  async function findPackageJson(startDir: string, stopDir: string): Promise<string | undefined> {
41
41
  let dir = resolve(startDir);
42
42
  const stop = resolve(stopDir);
43
+ const bounded = isWithin(stop, dir);
43
44
  for (;;) {
44
45
  const candidate = join(dir, "package.json");
45
46
  try {
@@ -47,10 +48,10 @@ async function findPackageJson(startDir: string, stopDir: string): Promise<strin
47
48
  } catch {
48
49
  // walk up
49
50
  }
50
- if (dir === stop) return undefined;
51
+ if (bounded && dir === stop) return undefined;
51
52
  const parent = dirname(dir);
52
53
  if (parent === dir) return undefined;
53
- if (!isWithin(stop, parent) && parent !== stop) return undefined;
54
+ if (bounded && !isWithin(stop, parent) && parent !== stop) return undefined;
54
55
  dir = parent;
55
56
  }
56
57
  }
@@ -25,6 +25,7 @@ const CAPABILITIES: LanguageCapabilities = {
25
25
  fileDeps: true,
26
26
  callEdges: true,
27
27
  packageSurface: true,
28
+ structuralImplements: false,
28
29
  };
29
30
 
30
31
  /** Keywords/types skipped when matching imports to a decl slice. */
@@ -153,10 +154,7 @@ const EXPORT_SPECIFIER = "export_specifier";
153
154
 
154
155
  const NESTED_CALLABLE = new Set([
155
156
  FUNCTION_DECLARATION,
156
- FUNCTION_EXPRESSION,
157
157
  GENERATOR_FUNCTION_DECLARATION,
158
- ARROW_FUNCTION,
159
- METHOD_DEFINITION,
160
158
  CLASS_DECLARATION,
161
159
  ABSTRACT_CLASS_DECLARATION,
162
160
  CLASS,
@@ -548,7 +546,7 @@ function variableDecls(node: Node, owner: string, exported: boolean, doc: DocSpa
548
546
  endOffset: spanNode.endIndex,
549
547
  visibility: "public",
550
548
  exported,
551
- calls: isFn ? extractCalls(body) : [],
549
+ calls: isFn ? extractCalls(body) : value === null ? [] : extractCalls(value),
552
550
  },
553
551
  body,
554
552
  );
@@ -725,7 +723,7 @@ function extractTypeScript(tree: Tree, source: string): ExtractResult {
725
723
  const decls = walkStatementList(top, "", source, false);
726
724
  const imports: ImportRef[] = [];
727
725
  collectImportsFrom(root, imports);
728
- return { decls, imports };
726
+ return { decls, imports, fileCalls: extractCalls(root) };
729
727
  }
730
728
 
731
729
  function makeAdapter(id: "typescript" | "tsx", extensions: readonly string[]): GrammarAdapter {