@shanepadgett/tau-agent 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +130 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +716 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +31 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -0,0 +1,159 @@
1
+ import { basename, dirname, join, relative, sep } from "node:path";
2
+ import type { FileDepResolver } from "../adapter.ts";
3
+ import { externalId, findAncestorFile, firstExistingFile, internalPaths, isWithin } from "./file-dep-util.ts";
4
+
5
+ function splitRustPath(specifier: string): string[] {
6
+ return specifier
7
+ .replace(/\{[\s\S]*\}$/u, "") // drop use-group braces for v1 leading path
8
+ .split("::")
9
+ .map((part) => part.trim())
10
+ .filter((part) => part.length > 0 && part !== "self");
11
+ }
12
+
13
+ async function moduleFileCandidates(dir: string, name: string): Promise<string[]> {
14
+ return [join(dir, `${name}.rs`), join(dir, name, "mod.rs")];
15
+ }
16
+
17
+ async function resolveModuleChain(
18
+ host: Parameters<FileDepResolver>[2],
19
+ startDir: string,
20
+ segments: readonly string[],
21
+ ): Promise<string | undefined> {
22
+ let dir = startDir;
23
+ let lastFile: string | undefined;
24
+ for (let i = 0; i < segments.length; i += 1) {
25
+ const name = segments[i];
26
+ if (name === undefined) break;
27
+ const candidates = await moduleFileCandidates(dir, name);
28
+ const file = await firstExistingFile(host, candidates);
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;
33
+ }
34
+ lastFile = file;
35
+ // Next modules live beside foo.rs → foo/ or inside foo/ for mod.rs
36
+ if (basename(file) === "mod.rs") dir = dirname(file);
37
+ else dir = join(dirname(file), name);
38
+ }
39
+ return lastFile;
40
+ }
41
+
42
+ function currentModuleDir(fromPath: string): string {
43
+ const base = dirname(fromPath);
44
+ const file = basename(fromPath);
45
+ if (file === "lib.rs" || file === "main.rs" || file === "mod.rs") return base;
46
+ // foo.rs → treat nested mods under foo/
47
+ const stem = file.endsWith(".rs") ? file.slice(0, -3) : file;
48
+ const nested = join(base, stem);
49
+ return nested;
50
+ }
51
+
52
+ async function crateSrcRoot(host: Parameters<FileDepResolver>[2], crateRoot: string): Promise<string> {
53
+ if (
54
+ (await host.isFile(join(crateRoot, "src", "lib.rs"))) ||
55
+ (await host.isFile(join(crateRoot, "src", "main.rs")))
56
+ ) {
57
+ return join(crateRoot, "src");
58
+ }
59
+ return crateRoot;
60
+ }
61
+
62
+ function modulePathFromFile(fromPath: string, crateRoot: string): string[] {
63
+ const rel = relative(crateRoot, fromPath);
64
+ if (rel.startsWith("..") || rel.length === 0) return [];
65
+ const parts = rel.split(sep);
66
+ const last = parts.at(-1);
67
+ if (last === undefined) return [];
68
+ if (last === "lib.rs" || last === "main.rs") return parts.slice(0, -1);
69
+ if (last === "mod.rs") return parts.slice(0, -1);
70
+ if (last.endsWith(".rs")) {
71
+ parts[parts.length - 1] = last.slice(0, -3);
72
+ return parts;
73
+ }
74
+ return parts;
75
+ }
76
+
77
+ export const resolveRustFileDep: FileDepResolver = async (fromPath, specifier, host, signal) => {
78
+ signal.throwIfAborted();
79
+ const trimmed = specifier.trim().replace(/;$/u, "");
80
+ if (trimmed.length === 0) return { kind: "unresolved" };
81
+
82
+ const cargo = await findAncestorFile(host, dirname(fromPath), host.scopeRoot, "Cargo.toml");
83
+ const crateRoot = cargo !== undefined ? dirname(cargo) : host.scopeRoot;
84
+
85
+ // External mod declaration: "mod name"
86
+ const modDecl = /^mod\s+([A-Za-z_][\w]*)$/u.exec(trimmed);
87
+ if (modDecl !== null) {
88
+ const name = modDecl[1];
89
+ if (name === undefined) return { kind: "unresolved" };
90
+ const dir =
91
+ basename(fromPath) === "mod.rs" || basename(fromPath) === "lib.rs" || basename(fromPath) === "main.rs"
92
+ ? dirname(fromPath)
93
+ : join(dirname(fromPath), basename(fromPath).replace(/\.rs$/u, ""));
94
+ // Also try beside the file (common for lib.rs siblings)
95
+ const candidates = [
96
+ ...(await moduleFileCandidates(dirname(fromPath), name)),
97
+ ...(await moduleFileCandidates(dir, name)),
98
+ ];
99
+ const file = await firstExistingFile(host, candidates);
100
+ if (file === undefined || !isWithin(host.scopeRoot, file)) return { kind: "unresolved" };
101
+ return internalPaths([file]);
102
+ }
103
+
104
+ const segments = splitRustPath(trimmed);
105
+ if (segments.length === 0) return { kind: "unresolved" };
106
+
107
+ const head = segments[0];
108
+ if (head === undefined) return { kind: "unresolved" };
109
+
110
+ if (head === "crate") {
111
+ const rest = segments.slice(1);
112
+ const srcRoot = await crateSrcRoot(host, crateRoot);
113
+ if (rest.length === 0) {
114
+ const rootFile = await firstExistingFile(host, [join(srcRoot, "lib.rs"), join(srcRoot, "main.rs")]);
115
+ return rootFile === undefined ? { kind: "unresolved" } : internalPaths([rootFile]);
116
+ }
117
+ const file = await resolveModuleChain(host, srcRoot, rest);
118
+ if (file === undefined || !isWithin(host.scopeRoot, file)) return { kind: "unresolved" };
119
+ return internalPaths([file]);
120
+ }
121
+
122
+ if (head === "super") {
123
+ const modPath = modulePathFromFile(fromPath, crateRoot);
124
+ const up = segments.filter((s) => s === "super").length;
125
+ const after = segments.filter((s) => s !== "super");
126
+ const basePath = modPath.slice(0, Math.max(0, modPath.length - up));
127
+ const srcRoot = await crateSrcRoot(host, crateRoot);
128
+ const startDir = basePath.length === 0 ? srcRoot : join(srcRoot, ...basePath);
129
+ if (after.length === 0) {
130
+ // parent module file
131
+ if (basePath.length === 0) {
132
+ const rootFile = await firstExistingFile(host, [join(srcRoot, "lib.rs"), join(srcRoot, "main.rs")]);
133
+ return rootFile === undefined ? { kind: "unresolved" } : internalPaths([rootFile]);
134
+ }
135
+ const parentName = basePath[basePath.length - 1];
136
+ const parentDir = join(srcRoot, ...basePath.slice(0, -1));
137
+ if (parentName === undefined) return { kind: "unresolved" };
138
+ const file = await firstExistingFile(host, await moduleFileCandidates(parentDir, parentName));
139
+ return file === undefined ? { kind: "unresolved" } : internalPaths([file]);
140
+ }
141
+ const file = await resolveModuleChain(host, startDir, after);
142
+ if (file === undefined || !isWithin(host.scopeRoot, file)) return { kind: "unresolved" };
143
+ return internalPaths([file]);
144
+ }
145
+
146
+ // Relative from current module (self:: already stripped)
147
+ if (head !== "self") {
148
+ // Could be crate-local module from current dir, or external crate.
149
+ const local = await resolveModuleChain(host, currentModuleDir(fromPath), segments);
150
+ if (local !== undefined && isWithin(host.scopeRoot, local)) return internalPaths([local]);
151
+ // Try from src root as absolute module path without crate::
152
+ const srcRoot = await crateSrcRoot(host, crateRoot);
153
+ const fromRoot = await resolveModuleChain(host, srcRoot, segments);
154
+ if (fromRoot !== undefined && isWithin(host.scopeRoot, fromRoot)) return internalPaths([fromRoot]);
155
+ return externalId(head);
156
+ }
157
+
158
+ return { kind: "unresolved" };
159
+ };
@@ -0,0 +1,506 @@
1
+ import type { Node, Tree } from "web-tree-sitter";
2
+ import type { ExtractResult, GrammarAdapter, LanguageCapabilities } from "../adapter.ts";
3
+ import { resolveRustFileDep } from "./rust-file-deps.ts";
4
+ import type { CallSite, Decl, ImportRef, Visibility } from "../ir.ts";
5
+ import {
6
+ applyDoc,
7
+ callSite,
8
+ docSpanBefore,
9
+ endLine,
10
+ field,
11
+ finishDecl,
12
+ nameText,
13
+ qualify,
14
+ spanThroughPrevious,
15
+ startLine,
16
+ walkBodyNodes,
17
+ type DocSpan,
18
+ } from "./tree.ts";
19
+
20
+ const CAPABILITIES: LanguageCapabilities = {
21
+ shape: true,
22
+ search: true,
23
+ fileDeps: true,
24
+ callEdges: true,
25
+ packageSurface: false,
26
+ };
27
+
28
+ const IMPORT_NOISE = new Set([
29
+ "as",
30
+ "async",
31
+ "await",
32
+ "break",
33
+ "const",
34
+ "continue",
35
+ "crate",
36
+ "dyn",
37
+ "else",
38
+ "enum",
39
+ "extern",
40
+ "false",
41
+ "fn",
42
+ "for",
43
+ "if",
44
+ "impl",
45
+ "in",
46
+ "let",
47
+ "loop",
48
+ "match",
49
+ "mod",
50
+ "move",
51
+ "mut",
52
+ "pub",
53
+ "ref",
54
+ "return",
55
+ "self",
56
+ "Self",
57
+ "static",
58
+ "struct",
59
+ "super",
60
+ "trait",
61
+ "true",
62
+ "type",
63
+ "unsafe",
64
+ "use",
65
+ "where",
66
+ "while",
67
+ ]);
68
+
69
+ const EXTENSIONS = [".rs"] as const;
70
+
71
+ const FUNCTION_ITEM = "function_item";
72
+ const FUNCTION_SIGNATURE_ITEM = "function_signature_item";
73
+ const STRUCT_ITEM = "struct_item";
74
+ const ENUM_ITEM = "enum_item";
75
+ const TRAIT_ITEM = "trait_item";
76
+ const IMPL_ITEM = "impl_item";
77
+ const MOD_ITEM = "mod_item";
78
+ const CONST_ITEM = "const_item";
79
+ const STATIC_ITEM = "static_item";
80
+ const TYPE_ITEM = "type_item";
81
+ const UNION_ITEM = "union_item";
82
+ const ASSOCIATED_TYPE = "associated_type";
83
+ const USE_DECLARATION = "use_declaration";
84
+ const ATTRIBUTE_ITEM = "attribute_item";
85
+ const VISIBILITY_MODIFIER = "visibility_modifier";
86
+ const FIELD_DECLARATION = "field_declaration";
87
+ const FIELD_DECLARATION_LIST = "field_declaration_list";
88
+ const ENUM_VARIANT = "enum_variant";
89
+ const ENUM_VARIANT_LIST = "enum_variant_list";
90
+ const DECLARATION_LIST = "declaration_list";
91
+ const TYPE_IDENTIFIER = "type_identifier";
92
+ const GENERIC_TYPE = "generic_type";
93
+ const SCOPED_TYPE_IDENTIFIER = "scoped_type_identifier";
94
+ const LINE_COMMENT = "line_comment";
95
+ const BLOCK_COMMENT = "block_comment";
96
+
97
+ const CALL_EXPRESSION = "call_expression";
98
+ const MACRO_INVOCATION = "macro_invocation";
99
+ const STRUCT_EXPRESSION = "struct_expression";
100
+ const IDENTIFIER = "identifier";
101
+ const FIELD_IDENTIFIER = "field_identifier";
102
+ const SCOPED_IDENTIFIER = "scoped_identifier";
103
+
104
+ const NESTED_SCOPE = new Set([
105
+ FUNCTION_ITEM,
106
+ FUNCTION_SIGNATURE_ITEM,
107
+ STRUCT_ITEM,
108
+ ENUM_ITEM,
109
+ TRAIT_ITEM,
110
+ IMPL_ITEM,
111
+ MOD_ITEM,
112
+ ]);
113
+
114
+ function bareCallee(node: Node): { name: string; receiver: string; leaf: Node } | undefined {
115
+ if (node.type === IDENTIFIER || node.type === FIELD_IDENTIFIER) {
116
+ return { name: node.text, receiver: "", leaf: node };
117
+ }
118
+ if (node.type === SCOPED_IDENTIFIER || node.type === "field_expression") {
119
+ const nameNode = field(node, "name") ?? field(node, "field");
120
+ const scope = field(node, "path") ?? field(node, "value");
121
+ if (nameNode === null) return undefined;
122
+ return { name: nameNode.text, receiver: scope === null ? "" : scope.text, leaf: nameNode };
123
+ }
124
+ return undefined;
125
+ }
126
+
127
+ function extractCalls(body: Node | null): CallSite[] {
128
+ if (body === null) return [];
129
+ const out: CallSite[] = [];
130
+ walkBodyNodes(
131
+ body,
132
+ (n) => NESTED_SCOPE.has(n.type),
133
+ (node) => {
134
+ if (node.type === CALL_EXPRESSION) {
135
+ const fn = field(node, "function");
136
+ if (fn === null) return;
137
+ const bare = bareCallee(fn);
138
+ if (bare === undefined) return;
139
+ const site = callSite(
140
+ bare.name,
141
+ startLine(bare.leaf),
142
+ bare.leaf.startIndex,
143
+ bare.leaf.endIndex,
144
+ "call",
145
+ bare.receiver,
146
+ );
147
+ if (site !== undefined) out.push(site);
148
+ return;
149
+ }
150
+ if (node.type === MACRO_INVOCATION) {
151
+ const mac = field(node, "macro") ?? node.namedChildren[0];
152
+ if (mac === undefined || mac === null) return;
153
+ const bare =
154
+ bareCallee(mac) ?? (mac.type === IDENTIFIER ? { name: mac.text, receiver: "", leaf: mac } : undefined);
155
+ if (bare === undefined) return;
156
+ const site = callSite(
157
+ bare.name,
158
+ startLine(bare.leaf),
159
+ bare.leaf.startIndex,
160
+ bare.leaf.endIndex,
161
+ "macro",
162
+ bare.receiver,
163
+ );
164
+ if (site !== undefined) out.push(site);
165
+ return;
166
+ }
167
+ if (node.type === STRUCT_EXPRESSION) {
168
+ const nameNode = field(node, "name");
169
+ if (nameNode === null) return;
170
+ const bare =
171
+ bareCallee(nameNode) ??
172
+ (nameNode.type === TYPE_IDENTIFIER ? { name: nameNode.text, receiver: "", leaf: nameNode } : undefined);
173
+ if (bare === undefined) return;
174
+ const site = callSite(
175
+ bare.name,
176
+ startLine(bare.leaf),
177
+ bare.leaf.startIndex,
178
+ bare.leaf.endIndex,
179
+ "construct",
180
+ );
181
+ if (site !== undefined) out.push(site);
182
+ }
183
+ },
184
+ );
185
+ return out;
186
+ }
187
+
188
+ function traitBase(node: Node): string[] {
189
+ // impl Trait for Type → trait is base of Type for implementation search of Trait
190
+ const trait = field(node, "trait");
191
+ if (trait === null) return [];
192
+ const name = rustTypeName(trait);
193
+ return name.length === 0 ? [] : [name];
194
+ }
195
+
196
+ const COMMENT_TYPES = [LINE_COMMENT, BLOCK_COMMENT] as const;
197
+ const ATTR_TYPES = [ATTRIBUTE_ITEM] as const;
198
+
199
+ function rustDoc(node: Node, source: string): DocSpan | undefined {
200
+ return docSpanBefore(node, source, ATTR_TYPES, COMMENT_TYPES);
201
+ }
202
+
203
+ function rustVisibility(node: Node): Visibility {
204
+ for (const child of node.namedChildren) {
205
+ if (child.type !== VISIBILITY_MODIFIER) continue;
206
+ const text = child.text;
207
+ if (text === "pub") return "public";
208
+ // pub(crate), pub(super), pub(in path)
209
+ return "internal";
210
+ }
211
+ return "private";
212
+ }
213
+
214
+ function rustExported(visibility: Visibility): boolean {
215
+ return visibility === "public" || visibility === "internal";
216
+ }
217
+
218
+ /** Base type name for impl targets (strip path/generics/pointers). */
219
+ function rustTypeName(node: Node | null): string {
220
+ if (node === null) return "";
221
+ if (node.type === TYPE_IDENTIFIER) return node.text;
222
+ if (node.type === GENERIC_TYPE) return rustTypeName(field(node, "type"));
223
+ if (node.type === SCOPED_TYPE_IDENTIFIER) {
224
+ const name = field(node, "name");
225
+ if (name !== null) return name.text;
226
+ }
227
+ for (let i = node.namedChildren.length - 1; i >= 0; i -= 1) {
228
+ const child = node.namedChildren[i];
229
+ if (child === undefined) continue;
230
+ const inner = rustTypeName(child);
231
+ if (inner.length > 0) return inner;
232
+ }
233
+ return "";
234
+ }
235
+
236
+ function withAttrSpan(
237
+ node: Node,
238
+ owner: string,
239
+ kind: Decl["kind"],
240
+ name: string,
241
+ visibility: Visibility,
242
+ body: Node | null,
243
+ children: Decl[],
244
+ doc: DocSpan | undefined,
245
+ ): Decl | undefined {
246
+ if (name.length === 0) return undefined;
247
+ const span = spanThroughPrevious(node, ATTR_TYPES);
248
+ const callable = kind === "function" || kind === "method" || kind === "constructor";
249
+ const decl = finishDecl(
250
+ {
251
+ kind,
252
+ name,
253
+ qualifiedName: qualify(owner, name),
254
+ startLine: span.startLine,
255
+ endLine: endLine(node),
256
+ startOffset: span.startOffset,
257
+ endOffset: node.endIndex,
258
+ visibility,
259
+ exported: rustExported(visibility),
260
+ children,
261
+ calls: callable ? extractCalls(body) : [],
262
+ bases: node.type === IMPL_ITEM ? traitBase(node) : [],
263
+ },
264
+ body,
265
+ );
266
+ if (body === null) decl.signatureEndOffset = node.endIndex;
267
+ return applyDoc(decl, doc);
268
+ }
269
+
270
+ function structFields(list: Node, owner: string): Decl[] {
271
+ const out: Decl[] = [];
272
+ for (const node of list.namedChildren) {
273
+ if (node.type !== FIELD_DECLARATION) continue;
274
+ const name = nameText(field(node, "name"));
275
+ if (name.length === 0) continue;
276
+ const visibility = rustVisibility(node);
277
+ out.push({
278
+ kind: "field",
279
+ name,
280
+ qualifiedName: qualify(owner, name),
281
+ startLine: node.startPosition.row + 1,
282
+ endLine: endLine(node),
283
+ startOffset: node.startIndex,
284
+ endOffset: node.endIndex,
285
+ signatureEndOffset: node.endIndex,
286
+ visibility,
287
+ exported: rustExported(visibility),
288
+ children: [],
289
+ calls: [],
290
+ bases: [],
291
+ });
292
+ }
293
+ return out;
294
+ }
295
+
296
+ function enumVariants(list: Node, owner: string): Decl[] {
297
+ const out: Decl[] = [];
298
+ for (const node of list.namedChildren) {
299
+ if (node.type !== ENUM_VARIANT) continue;
300
+ const name = nameText(field(node, "name"));
301
+ if (name.length === 0) continue;
302
+ out.push({
303
+ kind: "enumMember",
304
+ name,
305
+ qualifiedName: qualify(owner, name),
306
+ startLine: node.startPosition.row + 1,
307
+ endLine: endLine(node),
308
+ startOffset: node.startIndex,
309
+ endOffset: node.endIndex,
310
+ signatureEndOffset: node.endIndex,
311
+ visibility: "public",
312
+ exported: true,
313
+ children: [],
314
+ calls: [],
315
+ bases: [],
316
+ });
317
+ }
318
+ return out;
319
+ }
320
+
321
+ function walkDeclList(
322
+ list: Node,
323
+ owner: string,
324
+ source: string,
325
+ intoImplTarget: string,
326
+ // Trait bodies mark fn items as methods; modules keep free functions.
327
+ methodsInOwner: boolean,
328
+ ): Decl[] {
329
+ const out: Decl[] = [];
330
+ for (const node of list.namedChildren) {
331
+ out.push(...declsFromItem(node, owner, source, intoImplTarget, methodsInOwner));
332
+ }
333
+ return out;
334
+ }
335
+
336
+ function declsFromItem(
337
+ node: Node,
338
+ owner: string,
339
+ source: string,
340
+ intoImplTarget: string,
341
+ methodsInOwner: boolean,
342
+ ): Decl[] {
343
+ if (node.isError || node.type === "ERROR") {
344
+ const out: Decl[] = [];
345
+ for (const child of node.children) {
346
+ if (child.isNamed || child.isError) {
347
+ out.push(...declsFromItem(child, owner, source, intoImplTarget, methodsInOwner));
348
+ }
349
+ }
350
+ return out;
351
+ }
352
+
353
+ const doc = rustDoc(node, source);
354
+ const visibility = rustVisibility(node);
355
+
356
+ switch (node.type) {
357
+ case FUNCTION_ITEM:
358
+ case FUNCTION_SIGNATURE_ITEM: {
359
+ const name = nameText(field(node, "name"));
360
+ const body = field(node, "body");
361
+ // Impl methods re-home under the impl target; trait methods keep the trait owner.
362
+ const methodOwner = intoImplTarget.length > 0 ? intoImplTarget : owner;
363
+ const asMethod = intoImplTarget.length > 0 || methodsInOwner;
364
+ const kind = asMethod ? "method" : "function";
365
+ const decl = withAttrSpan(node, methodOwner, kind, name, visibility, body, [], doc);
366
+ return decl === undefined ? [] : [decl];
367
+ }
368
+ case STRUCT_ITEM:
369
+ case UNION_ITEM: {
370
+ // Union maps to struct — shared vocabulary has no union kind.
371
+ const name = nameText(field(node, "name"));
372
+ const body = field(node, "body");
373
+ const qn = qualify(owner, name);
374
+ const children = body !== null && body.type === FIELD_DECLARATION_LIST ? structFields(body, qn) : [];
375
+ const decl = withAttrSpan(node, owner, "struct", name, visibility, body, children, doc);
376
+ return decl === undefined ? [] : [decl];
377
+ }
378
+ case ENUM_ITEM: {
379
+ const name = nameText(field(node, "name"));
380
+ const body = field(node, "body");
381
+ const qn = qualify(owner, name);
382
+ const children = body !== null && body.type === ENUM_VARIANT_LIST ? enumVariants(body, qn) : [];
383
+ const decl = withAttrSpan(node, owner, "enum", name, visibility, body, children, doc);
384
+ return decl === undefined ? [] : [decl];
385
+ }
386
+ case TRAIT_ITEM: {
387
+ const name = nameText(field(node, "name"));
388
+ const body = field(node, "body");
389
+ const qn = qualify(owner, name);
390
+ const children =
391
+ body !== null && body.type === DECLARATION_LIST ? walkDeclList(body, qn, source, "", true) : [];
392
+ const decl = withAttrSpan(node, owner, "interface", name, visibility, body, children, doc);
393
+ return decl === undefined ? [] : [decl];
394
+ }
395
+ case IMPL_ITEM: {
396
+ // Methods become siblings under the target type name (inherent + trait impls).
397
+ const target = rustTypeName(field(node, "type"));
398
+ const body = field(node, "body");
399
+ if (body === null || body.type !== DECLARATION_LIST || target.length === 0) return [];
400
+ const methodOwner = owner.length === 0 ? target : qualify(owner, target);
401
+ const methods = walkDeclList(body, owner, source, methodOwner, true);
402
+ // Marker decl so `implementations Trait` finds this impl via bases (trait name).
403
+ const trait = rustTypeName(field(node, "trait"));
404
+ if (trait.length === 0) return methods;
405
+ const marker = finishDecl(
406
+ {
407
+ kind: "class",
408
+ name: target,
409
+ qualifiedName: methodOwner,
410
+ startLine: node.startPosition.row + 1,
411
+ endLine: endLine(node),
412
+ startOffset: node.startIndex,
413
+ endOffset: node.endIndex,
414
+ visibility: "public",
415
+ exported: true,
416
+ children: [],
417
+ bases: [trait],
418
+ },
419
+ null,
420
+ );
421
+ return [marker, ...methods];
422
+ }
423
+ case MOD_ITEM: {
424
+ const name = nameText(field(node, "name"));
425
+ const body = field(node, "body");
426
+ const qn = qualify(owner, name);
427
+ const children =
428
+ body !== null && body.type === DECLARATION_LIST ? walkDeclList(body, qn, source, "", false) : [];
429
+ const decl = withAttrSpan(node, owner, "module", name, visibility, body, children, doc);
430
+ return decl === undefined ? [] : [decl];
431
+ }
432
+ case CONST_ITEM:
433
+ case STATIC_ITEM: {
434
+ const name = nameText(field(node, "name"));
435
+ const decl = withAttrSpan(node, owner, "constant", name, visibility, null, [], doc);
436
+ return decl === undefined ? [] : [decl];
437
+ }
438
+ case TYPE_ITEM:
439
+ case ASSOCIATED_TYPE: {
440
+ const name = nameText(field(node, "name"));
441
+ const decl = withAttrSpan(node, owner, "typeAlias", name, visibility, null, [], doc);
442
+ return decl === undefined ? [] : [decl];
443
+ }
444
+ default:
445
+ return [];
446
+ }
447
+ }
448
+
449
+ function useSpecifier(node: Node): string {
450
+ const argument = field(node, "argument");
451
+ return argument === null ? node.text.replace(/^use\s+/, "").replace(/;$/, "") : argument.text;
452
+ }
453
+
454
+ function collectImports(root: Node, imports: ImportRef[]): void {
455
+ for (const node of root.namedChildren) {
456
+ if (node.type === USE_DECLARATION) {
457
+ imports.push({
458
+ specifier: useSpecifier(node),
459
+ startLine: node.startPosition.row + 1,
460
+ startOffset: node.startIndex,
461
+ endOffset: node.endIndex,
462
+ bindings: [],
463
+ });
464
+ continue;
465
+ }
466
+ if (node.type === MOD_ITEM) {
467
+ const body = field(node, "body");
468
+ if (body !== null) {
469
+ collectImports(body, imports);
470
+ continue;
471
+ }
472
+ const name = nameText(field(node, "name"));
473
+ if (name.length === 0) continue;
474
+ imports.push({
475
+ specifier: `mod ${name}`,
476
+ startLine: node.startPosition.row + 1,
477
+ startOffset: node.startIndex,
478
+ endOffset: node.endIndex,
479
+ bindings: [],
480
+ });
481
+ }
482
+ }
483
+ }
484
+
485
+ function extractRust(tree: Tree, source: string): ExtractResult {
486
+ const root = tree.rootNode;
487
+ const decls: Decl[] = [];
488
+ for (const node of root.children) {
489
+ if (node.isNamed || node.isError) decls.push(...declsFromItem(node, "", source, "", false));
490
+ }
491
+ const imports: ImportRef[] = [];
492
+ collectImports(root, imports);
493
+ return { decls, imports };
494
+ }
495
+
496
+ export function rustAdapter(): GrammarAdapter {
497
+ return {
498
+ mode: "grammar",
499
+ id: "rust",
500
+ extensions: EXTENSIONS,
501
+ capabilities: CAPABILITIES,
502
+ importNoiseIdentifiers: IMPORT_NOISE,
503
+ extract: extractRust,
504
+ resolveFileDep: resolveRustFileDep,
505
+ };
506
+ }