@shanepadgett/tau-agent 0.33.1 → 0.35.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 (203) 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/bash-approval/README.md +20 -0
  8. package/extensions/bash-approval/index.ts +235 -0
  9. package/extensions/bash-approval/settings.ts +24 -0
  10. package/extensions/branch/index.ts +38 -21
  11. package/extensions/cache-diagnostics/index.ts +70 -58
  12. package/extensions/checkpoint/README.md +9 -0
  13. package/extensions/checkpoint/checkpoint-budget.ts +66 -0
  14. package/extensions/checkpoint/checkpoint.ts +279 -0
  15. package/extensions/checkpoint/index.ts +102 -0
  16. package/extensions/checkpoint/messages.ts +170 -0
  17. package/extensions/checkpoint/prompt.ts +24 -0
  18. package/extensions/checkpoint/settings.ts +27 -0
  19. package/extensions/commit/commit-effort.ts +3 -3
  20. package/extensions/commit/commit-plan.ts +2 -2
  21. package/extensions/context/README.md +12 -8
  22. package/extensions/context/definitions.ts +178 -55
  23. package/extensions/context/index.ts +192 -160
  24. package/extensions/context/panel.ts +57 -37
  25. package/extensions/context/sync.ts +139 -89
  26. package/extensions/context/validation.ts +37 -25
  27. package/extensions/effort/README.md +1 -1
  28. package/extensions/effort/index.ts +3 -3
  29. package/extensions/effort/state.ts +3 -18
  30. package/extensions/explore/guidance.ts +6 -47
  31. package/extensions/explore/index.ts +38 -53
  32. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  33. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  34. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  35. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  36. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  37. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  38. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  39. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  40. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  41. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  42. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  43. package/extensions/explore/tools/target-params.ts +9 -0
  44. package/extensions/footer/index.ts +75 -56
  45. package/extensions/handoff/index.ts +8 -30
  46. package/extensions/image-gen/index.ts +46 -40
  47. package/extensions/patch/executor.ts +190 -135
  48. package/extensions/patch/matcher.ts +82 -48
  49. package/extensions/patch/parser.ts +149 -105
  50. package/extensions/patch/render.ts +32 -39
  51. package/extensions/patch/summary.ts +30 -26
  52. package/extensions/ready/README.md +7 -0
  53. package/extensions/ready/index.ts +58 -0
  54. package/extensions/ready/model.ts +128 -0
  55. package/extensions/ready/packs.ts +272 -0
  56. package/extensions/ready/render.ts +445 -0
  57. package/extensions/ready/scan.ts +760 -0
  58. package/extensions/reference/index.ts +2 -0
  59. package/extensions/reference/panel.ts +94 -31
  60. package/extensions/review/README.md +14 -6
  61. package/extensions/review/index.ts +58 -99
  62. package/extensions/review/model.ts +13 -33
  63. package/extensions/review/session.ts +9 -23
  64. package/extensions/runtime-context/context.ts +30 -26
  65. package/extensions/script-runner/README.md +2 -2
  66. package/extensions/script-runner/index.ts +122 -82
  67. package/extensions/silent-command-runner/index.ts +38 -23
  68. package/extensions/soul/README.md +2 -2
  69. package/extensions/soul/index.ts +4 -4
  70. package/extensions/soul/prompt.ts +6 -10
  71. package/extensions/soul/settings.ts +6 -3
  72. package/extensions/subagent/agents/context-sync.md +185 -33
  73. package/extensions/subagent/agents/scout.md +2 -2
  74. package/extensions/subagent/agents/web-research.md +2 -2
  75. package/extensions/subagent/agents.ts +137 -84
  76. package/extensions/subagent/cmux-dashboard.ts +39 -30
  77. package/extensions/subagent/index.ts +79 -59
  78. package/extensions/subagent/resume.ts +1 -1
  79. package/extensions/subagent/run.ts +308 -193
  80. package/extensions/subagent/runtime.ts +781 -343
  81. package/extensions/tau/index.ts +47 -38
  82. package/extensions/tau-help/help.md +20 -8
  83. package/extensions/tool-loader/README.md +4 -2
  84. package/extensions/tool-loader/index.ts +77 -52
  85. package/extensions/web/index.ts +6 -3
  86. package/extensions/web/webfetch.ts +114 -86
  87. package/package.json +3 -3
  88. package/schemas/tau.schema.json +38 -35
  89. package/shared/bounded-text-result.ts +1 -0
  90. package/shared/checkpoint-visibility.ts +9 -0
  91. package/shared/events.ts +28 -10
  92. package/shared/isolated-session.ts +72 -30
  93. package/shared/model-effort.ts +23 -22
  94. package/shared/model-fallback/index.ts +66 -41
  95. package/shared/settings/load.ts +45 -38
  96. package/shared/temporary-output-store.ts +16 -10
  97. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  98. package/{extensions/explore → src}/ast/engine.ts +54 -38
  99. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  100. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  101. package/src/ast/format/deps.ts +94 -0
  102. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  103. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  104. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  105. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  106. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  107. package/src/ast/format/targeted-result.ts +20 -0
  108. package/src/ast/grammars/c_sharp.wasm +0 -0
  109. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  110. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  111. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  112. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  113. package/{extensions/explore → src}/ast/ir.ts +6 -0
  114. package/src/ast/languages/csharp-file-deps.ts +90 -0
  115. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  116. package/src/ast/languages/dotted-index.ts +131 -0
  117. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  118. package/src/ast/languages/fixtures/sample.cs +53 -0
  119. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  120. package/src/ast/languages/fixtures/sample.go +37 -0
  121. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  122. package/src/ast/languages/fixtures/sample.java +40 -0
  123. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  124. package/src/ast/languages/fixtures/sample.kt +31 -0
  125. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  126. package/src/ast/languages/fixtures/sample.md +23 -0
  127. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  128. package/src/ast/languages/fixtures/sample.rs +54 -0
  129. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  130. package/src/ast/languages/fixtures/sample.swift +45 -0
  131. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  132. package/src/ast/languages/fixtures/sample.ts +45 -0
  133. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  134. package/src/ast/languages/fixtures/sample.tsx +36 -0
  135. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  136. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  137. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  138. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  139. package/src/ast/languages/jvm-file-deps.ts +135 -0
  140. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  141. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  142. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  143. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  144. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  145. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  146. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  147. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  148. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  149. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  150. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  151. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  152. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  153. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  154. package/src/ast/queries/impact.ts +291 -0
  155. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  156. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  157. package/{extensions/explore → src}/ast/query.ts +1 -1
  158. package/{extensions/explore → src}/ast/registry.ts +1 -2
  159. package/{extensions/explore → src}/ast/scan.ts +1 -1
  160. package/src/ast/session.ts +50 -0
  161. package/{extensions/explore → src}/ast/slice.ts +8 -0
  162. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  163. package/src/file-injection/index.ts +547 -0
  164. package/src/image-generation/client.ts +50 -31
  165. package/src/image-generation/index.ts +177 -134
  166. package/src/index.ts +2 -0
  167. package/src/tool-loading/index.ts +58 -0
  168. package/extensions/context/evidence.ts +0 -517
  169. package/extensions/context/projection.ts +0 -141
  170. package/extensions/context/state.ts +0 -30
  171. package/extensions/context/write-scope.ts +0 -109
  172. package/extensions/explore/ast/format/deps.ts +0 -86
  173. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  174. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  175. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  176. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  177. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  178. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  179. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  180. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  181. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  182. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  183. package/extensions/explore/ast/languages/odin.ts +0 -407
  184. package/extensions/explore/ast/queries/impact.ts +0 -250
  185. package/extensions/explore/outline-injection.ts +0 -151
  186. package/extensions/explore/read/autoread.ts +0 -118
  187. package/extensions/review/panel.ts +0 -128
  188. package/extensions/working-memory/README.md +0 -17
  189. package/extensions/working-memory/checkpoint.ts +0 -242
  190. package/extensions/working-memory/index.ts +0 -341
  191. package/extensions/working-memory/memory.ts +0 -235
  192. package/extensions/working-memory/render.ts +0 -185
  193. package/extensions/working-memory/settings.ts +0 -38
  194. package/extensions/working-memory/state.ts +0 -158
  195. package/shared/autoread.ts +0 -234
  196. package/shared/context-messages.ts +0 -19
  197. package/shared/outline-injection.ts +0 -56
  198. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  199. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  200. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  201. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  202. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  203. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -1,12 +1,13 @@
1
1
  import { lstat } from "node:fs/promises";
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
- import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../../traverse.ts";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../traverse.ts";
4
4
  import type { ExploreEngine } from "../engine.ts";
5
5
  import type { Candidate, Resolution } from "../identity.ts";
6
6
  import { resolveTarget } from "../identity.ts";
7
7
  import {
8
8
  isCallableLike,
9
9
  isTypeLike,
10
+ type CallKind,
10
11
  type CallSite,
11
12
  type Decl,
12
13
  type DeclKind,
@@ -15,7 +16,7 @@ import {
15
16
  } from "../ir.ts";
16
17
  import { walkDecls } from "../query.ts";
17
18
  import { scanSources } from "../scan.ts";
18
- import { signatureText } from "../slice.ts";
19
+ import { compactSignature, signatureText } from "../slice.ts";
19
20
  import type { ExploreFileGraph, FileGraphEdge } from "./file-graph.ts";
20
21
 
21
22
  export type RelationshipOp = "callers" | "callees" | "references" | "implementations";
@@ -38,6 +39,7 @@ export type RelationshipSite = {
38
39
  line: number;
39
40
  kind: RelationshipSiteKind;
40
41
  name: string;
42
+ target: { path: string; startLine: number } | undefined;
41
43
  preview: string;
42
44
  certainty: RelationshipCertainty;
43
45
  competitors: Candidate[];
@@ -102,14 +104,18 @@ function sameDecl(a: DeclRef, b: DeclRef): boolean {
102
104
  /** Same callable name in same file — overloads bind as one symbol for edge matching. */
103
105
  function sameOverloadGroup(a: DeclRef, b: DeclRef): boolean {
104
106
  return (
105
- a.path === b.path && a.decl.name === b.decl.name && isCallableLike(a.decl.kind) && isCallableLike(b.decl.kind)
107
+ a.path === b.path &&
108
+ a.decl.name === b.decl.name &&
109
+ ownerName(a.decl.qualifiedName) === ownerName(b.decl.qualifiedName) &&
110
+ isCallableLike(a.decl.kind) &&
111
+ isCallableLike(b.decl.kind)
106
112
  );
107
113
  }
108
114
 
109
- function allSameName(refs: readonly DeclRef[]): boolean {
115
+ function allSameOverloadGroup(refs: readonly DeclRef[]): boolean {
110
116
  const first = refs[0];
111
117
  if (first === undefined) return false;
112
- return refs.every((ref) => ref.decl.name === first.decl.name);
118
+ return refs.every((ref) => sameOverloadGroup(ref, first));
113
119
  }
114
120
 
115
121
  function toCandidate(ref: DeclRef): Candidate {
@@ -171,6 +177,33 @@ function buildBundle(path: string, ir: FileIr, source: string): FileBundle {
171
177
  return { path, ir, source, localByName, importByLocal };
172
178
  }
173
179
 
180
+ /**
181
+ * Owner for top-level call sites. Kept out of the IR decl tree so it never
182
+ * reaches outline, discover, or declaration counts.
183
+ */
184
+ function fileScopeRef(bundle: FileBundle): DeclRef {
185
+ return {
186
+ path: bundle.path,
187
+ ir: bundle.ir,
188
+ source: bundle.source,
189
+ decl: {
190
+ kind: "module",
191
+ name: "",
192
+ qualifiedName: "",
193
+ startLine: 1,
194
+ endLine: bundle.ir.lineCount,
195
+ startOffset: 0,
196
+ endOffset: bundle.source.length,
197
+ signatureEndOffset: 0,
198
+ visibility: "private",
199
+ exported: false,
200
+ children: [],
201
+ calls: bundle.ir.fileCalls,
202
+ bases: [],
203
+ },
204
+ };
205
+ }
206
+
174
207
  function internalTargets(edges: readonly FileGraphEdge[]): Set<string> {
175
208
  const out = new Set<string>();
176
209
  for (const edge of edges) {
@@ -179,6 +212,68 @@ function internalTargets(edges: readonly FileGraphEdge[]): Set<string> {
179
212
  return out;
180
213
  }
181
214
 
215
+ type MethodSetEntry = { ref: DeclRef | undefined; methods: Set<string> };
216
+
217
+ /** Directory-scoped type key: a Go method set spans every file in the package. */
218
+ function methodSetKey(path: string, typeName: string): string {
219
+ return `${dirname(path)}|${typeName}`;
220
+ }
221
+
222
+ function addMethodSetRef(out: Map<string, MethodSetEntry>, ref: DeclRef): void {
223
+ const key = methodSetKey(ref.path, ref.decl.name);
224
+ const existing = out.get(key);
225
+ const entry = existing ?? { ref: undefined, methods: new Set<string>() };
226
+ if (existing === undefined) out.set(key, entry);
227
+ entry.ref = ref;
228
+ for (const child of ref.decl.children) {
229
+ if (isCallableLike(child.kind)) entry.methods.add(child.name);
230
+ }
231
+ }
232
+
233
+ function addMethodSetOwner(out: Map<string, MethodSetEntry>, ref: DeclRef): void {
234
+ // Go methods are file-level decls owned through qualifiedName.
235
+ const owner = ownerName(ref.decl.qualifiedName);
236
+ if (owner.length === 0) return;
237
+ const key = methodSetKey(ref.path, owner);
238
+ const existing = out.get(key);
239
+ const entry = existing ?? { ref: undefined, methods: new Set<string>() };
240
+ if (existing === undefined) out.set(key, entry);
241
+ entry.methods.add(ref.decl.name);
242
+ }
243
+
244
+ function buildMethodSets(bundles: readonly FileBundle[]): Map<string, MethodSetEntry> {
245
+ const out = new Map<string, MethodSetEntry>();
246
+ for (const bundle of bundles) {
247
+ for (const ref of collectDeclRefs(bundle.path, bundle.ir, bundle.source)) {
248
+ if (isTypeLike(ref.decl.kind)) addMethodSetRef(out, ref);
249
+ else if (isCallableLike(ref.decl.kind)) addMethodSetOwner(out, ref);
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+
255
+ /** Interface method names, with embedded interfaces expanded transitively. */
256
+ function requiredMethods(target: DeclRef, sets: Map<string, MethodSetEntry>): Set<string> {
257
+ const required = new Set<string>();
258
+ const seen = new Set<string>();
259
+ const visit = (path: string, decl: Decl): void => {
260
+ for (const child of decl.children) {
261
+ if (isCallableLike(child.kind)) required.add(child.name);
262
+ }
263
+ for (const base of decl.bases) {
264
+ const key = methodSetKey(path, base);
265
+ if (seen.has(key)) continue;
266
+ seen.add(key);
267
+ const entry = sets.get(key);
268
+ if (entry === undefined) continue;
269
+ for (const name of entry.methods) required.add(name);
270
+ if (entry.ref !== undefined) visit(entry.ref.path, entry.ref.decl);
271
+ }
272
+ };
273
+ visit(target.path, target.decl);
274
+ return required;
275
+ }
276
+
182
277
  function pickByDeps(
183
278
  candidates: readonly DeclRef[],
184
279
  depPaths: ReadonlySet<string>,
@@ -190,7 +285,7 @@ function pickByDeps(
190
285
  }
191
286
  if (inClosure.length > 1) {
192
287
  // Overloads of one name bind together; mixed names stay ambiguous.
193
- if (allSameName(inClosure)) return { targets: [...inClosure], certainty: "exact", competitors: [] };
288
+ if (allSameOverloadGroup(inClosure)) return { targets: [...inClosure], certainty: "exact", competitors: [] };
194
289
  return {
195
290
  targets: [],
196
291
  certainty: "ambiguous",
@@ -202,7 +297,7 @@ function pickByDeps(
202
297
  if (only !== undefined) return { targets: [only], certainty: "inferred", competitors: [] };
203
298
  }
204
299
  if (candidates.length > 1) {
205
- if (allSameName(candidates)) return { targets: [...candidates], certainty: "inferred", competitors: [] };
300
+ if (allSameOverloadGroup(candidates)) return { targets: [...candidates], certainty: "inferred", competitors: [] };
206
301
  return {
207
302
  targets: [],
208
303
  certainty: "ambiguous",
@@ -212,42 +307,74 @@ function pickByDeps(
212
307
  return { targets: [], certainty: "inferred", competitors: [] };
213
308
  }
214
309
 
310
+ type ResolvedCall = { targets: DeclRef[]; certainty: RelationshipCertainty; competitors: Candidate[] };
311
+
312
+ function exactTargets(refs: readonly DeclRef[]): ResolvedCall {
313
+ return { targets: [...refs], certainty: "exact", competitors: [] };
314
+ }
315
+
316
+ function ambiguousTargets(refs: readonly DeclRef[]): ResolvedCall {
317
+ return {
318
+ targets: [],
319
+ certainty: "ambiguous",
320
+ competitors: refs.slice(0, MAX_COMPETITORS).map(toCandidate),
321
+ };
322
+ }
323
+
324
+ function resolveLocalCall(local: readonly DeclRef[]): ResolvedCall | undefined {
325
+ if (local.length === 1) {
326
+ const only = local[0];
327
+ if (only !== undefined) return exactTargets([only]);
328
+ }
329
+ if (local.length > 1) {
330
+ if (allSameOverloadGroup(local)) return exactTargets(local);
331
+ return ambiguousTargets(local);
332
+ }
333
+ return undefined;
334
+ }
335
+
215
336
  function resolveCallName(
216
337
  name: string,
217
338
  receiver: string,
339
+ kind: CallKind,
340
+ from: DeclRef,
218
341
  caller: FileBundle,
219
342
  globalByName: Map<string, DeclRef[]>,
220
343
  callerDeps: ReadonlySet<string>,
221
- ): { targets: DeclRef[]; certainty: RelationshipCertainty; competitors: Candidate[] } {
222
- const local = caller.localByName.get(name);
223
- if (local !== undefined && local.length === 1) {
224
- const only = local[0];
225
- if (only !== undefined) return { targets: [only], certainty: "exact", competitors: [] };
226
- }
227
- if (local !== undefined && local.length > 1) {
228
- if (allSameName(local)) return { targets: [...local], certainty: "exact", competitors: [] };
229
- return {
230
- targets: [],
231
- certainty: "ambiguous",
232
- competitors: local.slice(0, MAX_COMPETITORS).map(toCandidate),
233
- };
344
+ ): ResolvedCall {
345
+ const callableCandidates = (globalByName.get(name) ?? []).filter((ref) =>
346
+ kind === "construct" ? isTypeLike(ref.decl.kind) : isCallableLike(ref.decl.kind),
347
+ );
348
+ if (caller.importByLocal.has(name)) {
349
+ return pickByDeps(callableCandidates, callerDeps);
234
350
  }
235
351
 
236
- if (caller.importByLocal.has(name)) {
237
- return pickByDeps(globalByName.get(name) ?? [], callerDeps);
352
+ const local = caller.localByName
353
+ .get(name)
354
+ ?.filter((ref) => (kind === "construct" ? isTypeLike(ref.decl.kind) : isCallableLike(ref.decl.kind)));
355
+ if (receiver.length === 0 && local !== undefined) {
356
+ const resolved = resolveLocalCall(local);
357
+ if (resolved !== undefined) return resolved;
238
358
  }
239
359
 
240
- const global = globalByName.get(name) ?? [];
241
- // Receiver-bearing: never unique-global Exact (blocks obj.foo → random foo).
242
- if (receiver.length > 0) return pickByDeps(global, callerDeps);
360
+ const fromOwner = ownerName(from.decl.qualifiedName);
361
+ const global =
362
+ receiver.length === 0
363
+ ? callableCandidates.filter(
364
+ (ref) => ref.decl.kind !== "method" || ownerName(ref.decl.qualifiedName) === fromOwner,
365
+ )
366
+ : callableCandidates;
367
+ if (receiver.length > 0) {
368
+ const receiverOwner = receiver === "self" || receiver === "this" ? fromOwner : receiver.split(/[.:]/u).at(-1);
369
+ const owned = global.filter((ref) => ownerName(ref.decl.qualifiedName) === receiverOwner);
370
+ if (owned.length > 0) return pickByDeps(owned, callerDeps);
371
+ return { targets: [], certainty: "inferred", competitors: [] };
372
+ }
243
373
  if (global.length === 1) {
244
374
  const only = global[0];
245
- if (only !== undefined) return { targets: [only], certainty: "exact", competitors: [] };
246
- }
247
- if (global.length > 1 && allSameName(global)) {
248
- // Same-name overloads across files still need dep evidence for Exact.
249
- return pickByDeps(global, callerDeps);
375
+ if (only !== undefined) return exactTargets([only]);
250
376
  }
377
+ // Same-name overloads across files still need dep evidence for Exact.
251
378
  return pickByDeps(global, callerDeps);
252
379
  }
253
380
 
@@ -260,21 +387,22 @@ function siteKindFromCall(site: CallSite): RelationshipSiteKind {
260
387
 
261
388
  function makeSite(
262
389
  path: string,
263
- source: string,
264
390
  line: number,
265
391
  kind: RelationshipSiteKind,
266
392
  name: string,
267
393
  certainty: RelationshipCertainty,
268
394
  competitors: Candidate[],
395
+ preview: string,
269
396
  ): RelationshipSite {
270
397
  return {
271
398
  path,
272
399
  line,
273
400
  kind,
274
401
  name,
275
- preview: linePreview(source, line),
402
+ target: undefined,
276
403
  certainty,
277
404
  competitors,
405
+ preview,
278
406
  };
279
407
  }
280
408
 
@@ -319,17 +447,29 @@ function edgeHitsTarget(edge: EdgeHit, target: DeclRef): boolean {
319
447
  }
320
448
 
321
449
  function pushEdge(sites: RelationshipSite[], edge: EdgeHit): void {
322
- sites.push(
323
- makeSite(
324
- edge.from.path,
325
- edge.from.source,
326
- edge.site.line,
327
- siteKindFromCall(edge.site),
328
- edge.site.name,
329
- edge.certainty,
330
- edge.competitors,
331
- ),
450
+ const site = makeSite(
451
+ edge.from.path,
452
+ edge.site.line,
453
+ siteKindFromCall(edge.site),
454
+ edge.site.name,
455
+ edge.certainty,
456
+ edge.competitors,
457
+ linePreview(edge.from.source, edge.site.line),
332
458
  );
459
+ const soleTarget = edge.targets.length === 1 ? edge.targets[0] : undefined;
460
+ const receiverOwner = edge.site.receiver.split(/[.:]/u).at(-1);
461
+ // Dependency-free same-module methods can still be pinned when syntax names
462
+ // their owner. Other inferred single-name matches remain visible guesses.
463
+ const target =
464
+ soleTarget !== undefined &&
465
+ (edge.certainty === "exact" ||
466
+ edge.site.receiver === "self" ||
467
+ edge.site.receiver === "this" ||
468
+ ownerName(soleTarget.decl.qualifiedName) === receiverOwner)
469
+ ? soleTarget
470
+ : undefined;
471
+ if (target !== undefined) site.target = { path: target.path, startLine: target.decl.startLine };
472
+ sites.push(site);
333
473
  }
334
474
 
335
475
  export type RelationshipQueryArgs = {
@@ -422,9 +562,10 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
422
562
  for (const bundle of bundles) {
423
563
  signal.throwIfAborted();
424
564
  const deps = await depsOf(bundle.path);
425
- for (const from of collectDeclRefs(bundle.path, bundle.ir, bundle.source)) {
565
+ const owners = [...collectDeclRefs(bundle.path, bundle.ir, bundle.source), fileScopeRef(bundle)];
566
+ for (const from of owners) {
426
567
  for (const site of from.decl.calls) {
427
- const resolved = resolveCallName(site.name, site.receiver, bundle, globalByName, deps);
568
+ const resolved = resolveCallName(site.name, site.receiver, site.kind, from, bundle, globalByName, deps);
428
569
  edges.push({
429
570
  from,
430
571
  site,
@@ -438,32 +579,51 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
438
579
 
439
580
  const sites: RelationshipSite[] = [];
440
581
 
582
+ const heritageCertainty = (
583
+ ref: DeclRef,
584
+ deps: ReadonlySet<string>,
585
+ candidates: readonly DeclRef[],
586
+ ): RelationshipCertainty => {
587
+ // Same file and same directory are one package: no import edge exists to
588
+ // prove, so label them alike rather than implying a confidence gap.
589
+ if (ref.path === target.path || dirname(ref.path) === dirname(target.path) || deps.has(target.path)) {
590
+ return "exact";
591
+ }
592
+ return candidates.length === 1 ? "inferred" : "ambiguous";
593
+ };
594
+
595
+ const isHeritageCandidate = (ref: DeclRef, baseName: string): boolean => {
596
+ if (!ref.decl.bases.includes(baseName) || !isTypeLike(ref.decl.kind)) return false;
597
+ // Never list the base type as its own implementor.
598
+ return !(sameDecl(ref, target) || (ref.decl.name === baseName && ref.path === target.path));
599
+ };
600
+
601
+ const pushHeritageSite = async (
602
+ ref: DeclRef,
603
+ baseName: string,
604
+ siteKind: RelationshipSiteKind,
605
+ candidates: readonly DeclRef[],
606
+ ): Promise<void> => {
607
+ const certainty = heritageCertainty(ref, await depsOf(ref.path), candidates);
608
+ sites.push(
609
+ makeSite(
610
+ ref.path,
611
+ ref.decl.startLine,
612
+ siteKind,
613
+ baseName,
614
+ certainty,
615
+ certainty === "ambiguous" ? candidates.slice(0, MAX_COMPETITORS).map(toCandidate) : [],
616
+ // Declaration-shaped site: its own signature, not the annotation line above it.
617
+ compactSignature(signatureText(ref.decl, ref.source)),
618
+ ),
619
+ );
620
+ };
621
+
441
622
  const heritageSites = async (baseName: string, siteKind: RelationshipSiteKind): Promise<void> => {
623
+ const candidates = globalByName.get(baseName) ?? [];
442
624
  for (const bundle of bundles) {
443
625
  for (const ref of collectDeclRefs(bundle.path, bundle.ir, bundle.source)) {
444
- if (!ref.decl.bases.includes(baseName)) continue;
445
- if (!isTypeLike(ref.decl.kind)) continue;
446
- // Never list the base type as its own implementor.
447
- if (sameDecl(ref, target) || (ref.decl.name === baseName && ref.path === target.path)) continue;
448
- const deps = await depsOf(ref.path);
449
- const candidates = globalByName.get(baseName) ?? [];
450
- const certainty =
451
- ref.path === target.path || deps.has(target.path)
452
- ? "exact"
453
- : candidates.length === 1
454
- ? "inferred"
455
- : "ambiguous";
456
- sites.push(
457
- makeSite(
458
- ref.path,
459
- ref.source,
460
- ref.decl.startLine,
461
- siteKind,
462
- baseName,
463
- certainty,
464
- certainty === "ambiguous" ? candidates.slice(0, MAX_COMPETITORS).map(toCandidate) : [],
465
- ),
466
- );
626
+ if (isHeritageCandidate(ref, baseName)) await pushHeritageSite(ref, baseName, siteKind, candidates);
467
627
  }
468
628
  }
469
629
  };
@@ -488,7 +648,6 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
488
648
  sites.push(
489
649
  makeSite(
490
650
  edge.from.path,
491
- edge.from.source,
492
651
  edge.site.line,
493
652
  "construct",
494
653
  edge.site.name,
@@ -496,6 +655,7 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
496
655
  n > 1
497
656
  ? (globalByName.get(target.decl.name) ?? []).slice(0, MAX_COMPETITORS).map(toCandidate)
498
657
  : [],
658
+ linePreview(edge.from.source, edge.site.line),
499
659
  ),
500
660
  );
501
661
  } else {
@@ -518,7 +678,6 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
518
678
  sites.push(
519
679
  makeSite(
520
680
  target.path,
521
- target.source,
522
681
  target.decl.startLine,
523
682
  "implementation",
524
683
  base,
@@ -528,6 +687,7 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
528
687
  ? "exact"
529
688
  : picked.certainty,
530
689
  picked.competitors,
690
+ compactSignature(signatureText(target.decl, target.source)),
531
691
  ),
532
692
  );
533
693
  }
@@ -543,7 +703,17 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
543
703
  if (binding.local !== target.decl.name && remote !== target.decl.name) continue;
544
704
  const deps = await depsOf(bundle.path);
545
705
  const certainty = deps.has(target.path) || bundle.path === target.path ? "exact" : "inferred";
546
- sites.push(makeSite(bundle.path, bundle.source, imp.startLine, "import", binding.local, certainty, []));
706
+ sites.push(
707
+ makeSite(
708
+ bundle.path,
709
+ imp.startLine,
710
+ "import",
711
+ binding.local,
712
+ certainty,
713
+ [],
714
+ linePreview(bundle.source, imp.startLine),
715
+ ),
716
+ );
547
717
  }
548
718
  }
549
719
  }
@@ -552,6 +722,41 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
552
722
  // implementations
553
723
  if (isTypeLike(target.decl.kind)) {
554
724
  await heritageSites(target.decl.name, "implementation");
725
+ // Structurally typed interfaces (Go): a type implements by method set, with
726
+ // nothing naming the interface. Syntax cannot check signatures — inferred.
727
+ if (target.decl.kind === "interface" && targetAdapter.capabilities.structuralImplements) {
728
+ const sets = buildMethodSets(bundles);
729
+ const required = requiredMethods(target, sets);
730
+ const reported = new Set(sites.map((site) => `${site.path}|${site.line}`));
731
+ if (required.size > 0) {
732
+ for (const entry of sets.values()) {
733
+ const ref = entry.ref;
734
+ if (ref === undefined || sameDecl(ref, target)) continue;
735
+ if (!isTypeLike(ref.decl.kind)) continue;
736
+ if (reported.has(`${ref.path}|${ref.decl.startLine}`)) continue;
737
+ if (entry.methods.size < required.size) continue;
738
+ let covers = true;
739
+ for (const name of required) {
740
+ if (!entry.methods.has(name)) {
741
+ covers = false;
742
+ break;
743
+ }
744
+ }
745
+ if (!covers) continue;
746
+ sites.push(
747
+ makeSite(
748
+ ref.path,
749
+ ref.decl.startLine,
750
+ "implementation",
751
+ target.decl.name,
752
+ "inferred",
753
+ [],
754
+ compactSignature(signatureText(ref.decl, ref.source)),
755
+ ),
756
+ );
757
+ }
758
+ }
759
+ }
555
760
  }
556
761
  if (isCallableLike(target.decl.kind)) {
557
762
  const owner = ownerName(target.decl.qualifiedName);
@@ -568,7 +773,15 @@ export async function queryRelationships(args: RelationshipQueryArgs): Promise<R
568
773
  continue;
569
774
  }
570
775
  sites.push(
571
- makeSite(typeRef.path, typeRef.source, child.startLine, "override", child.name, "inferred", []),
776
+ makeSite(
777
+ typeRef.path,
778
+ child.startLine,
779
+ "override",
780
+ child.name,
781
+ "inferred",
782
+ [],
783
+ compactSignature(signatureText(child, typeRef.source)),
784
+ ),
572
785
  );
573
786
  }
574
787
  }
@@ -131,6 +131,12 @@ export type FileIr = {
131
131
  lineCount: number;
132
132
  decls: Decl[];
133
133
  imports: ImportRef[];
134
+ /**
135
+ * Call sites in top-level statements, owned by no declaration.
136
+ * Relationship queries read these; they are not declarations and never
137
+ * appear in outline, discover, or declaration counts.
138
+ */
139
+ fileCalls: CallSite[];
134
140
  /** True when the parse tree contained ERROR/MISSING nodes. */
135
141
  parseDegraded: boolean;
136
142
  };
@@ -0,0 +1,90 @@
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
+ * C# file deps. A `using` names a namespace, so it resolves to that namespace's
8
+ * directory as a package edge; alias usings (`using A = X.Y.Type;`) name one
9
+ * type and resolve to its file.
10
+ */
11
+
12
+ /** BCL / engine — never treat polyfills as project file edges. */
13
+ function isFrameworkNamespace(ns: string): boolean {
14
+ if (ns === "System" || ns.startsWith("System.")) return true;
15
+ if (ns === "Microsoft" || ns.startsWith("Microsoft.")) return true;
16
+ if (ns === "Windows" || ns.startsWith("Windows.")) return true;
17
+ if (ns === "UnityEngine" || ns.startsWith("UnityEngine.")) return true;
18
+ if (ns === "UnityEditor" || ns.startsWith("UnityEditor.")) return true;
19
+ if (ns === "Godot" || ns.startsWith("Godot.")) return true;
20
+ return false;
21
+ }
22
+
23
+ function normalizeUsing(specifier: string): string {
24
+ let s = specifier.trim().replace(/;$/u, "").trim();
25
+ s = s.replace(/^global\s+/iu, "").trim();
26
+ s = s.replace(/^static\s+/iu, "").trim();
27
+ // Alias: using A = X.Y.Z → resolve the target.
28
+ const eq = s.indexOf("=");
29
+ if (eq !== -1) s = s.slice(eq + 1).trim();
30
+ return s;
31
+ }
32
+
33
+ const TYPE_KEYWORDS = ["class ", "struct ", "interface ", "record ", "enum ", "delegate "] as const;
34
+
35
+ /**
36
+ * Namespace + type pairs, scanned sequentially — types bind to the nearest
37
+ * preceding namespace declaration.
38
+ */
39
+ function scanCsharpHead(head: string): HeadScan {
40
+ const names: HeadScan["names"] = [];
41
+ const packages: string[] = [];
42
+ let currentNs: string | undefined;
43
+ for (const raw of head.split("\n")) {
44
+ const line = raw.trim();
45
+ if (line.length === 0 || line.startsWith("//")) continue;
46
+
47
+ // file-scoped `namespace X;`, block `namespace X {`, or `namespace X` + brace next line
48
+ const nsMatch =
49
+ /^(?:internal\s+)?namespace\s+([A-Za-z_][\w.]*)\s*[;{]?\s*$/u.exec(line) ??
50
+ /^(?:internal\s+)?namespace\s+([A-Za-z_][\w.]*)\s*[{/]/u.exec(line);
51
+ if (nsMatch?.[1] !== undefined) {
52
+ currentNs = nsMatch[1];
53
+ packages.push(currentNs);
54
+ continue;
55
+ }
56
+ if (currentNs === undefined) continue;
57
+
58
+ for (const kw of TYPE_KEYWORDS) {
59
+ const idx = line.indexOf(kw);
60
+ if (idx === -1) continue;
61
+ if (idx > 0 && /[A-Za-z0-9_]/u.test(line.charAt(idx - 1))) continue;
62
+ const rest = line.slice(idx + kw.length).trimStart();
63
+ const name = /^([A-Za-z_][\w]*)/u.exec(rest)?.[1];
64
+ if (name !== undefined) names.push({ pkg: currentNs, name });
65
+ break;
66
+ }
67
+ }
68
+ return { packages, names };
69
+ }
70
+
71
+ export const resolveCsharpFileDep: FileDepResolver = async (fromPath, specifier, host, signal) => {
72
+ signal.throwIfAborted();
73
+ const dotted = normalizeUsing(specifier);
74
+ if (dotted.length === 0) return { kind: "unresolved" };
75
+ if (isFrameworkNamespace(dotted)) return externalId(dotted);
76
+
77
+ const index = await buildDottedIndex(host, "csharp-dotted", [".cs"], scanCsharpHead, signal);
78
+
79
+ // Namespace first: a plain `using` imports the whole namespace, not one file.
80
+ const pkg = resolveDottedPackage(index, fromPath, dotted);
81
+ if (pkg !== undefined && isWithin(host.scopeRoot, pkg.dir)) {
82
+ return packageDirectory(dotted, pkg.dir, pkg.fileCount);
83
+ }
84
+
85
+ const file = resolveDottedFile(index, fromPath, dotted);
86
+ if (file === undefined) return externalId(dotted);
87
+ if (!isWithin(host.scopeRoot, file) || !host.ownsPath(file)) return externalId(dotted);
88
+ if (resolve(file) === resolve(fromPath)) return { kind: "unresolved" };
89
+ return internalPaths([file]);
90
+ };