@shanepadgett/tau-agent 0.27.0 → 0.28.1

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 +136 -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 +751 -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 +44 -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,175 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import type { FileDepHost, FileDepResolver } from "../adapter.ts";
3
+ import { boundedInternalPaths, externalId, firstExistingFile, internalPaths, isWithin } from "./file-dep-util.ts";
4
+
5
+ /** `collection:pkg/path` — Odin collection import. */
6
+ const COLLECTION_SPEC = /^([A-Za-z_][A-Za-z0-9_]*):(.+)$/u;
7
+
8
+ async function isDir(host: FileDepHost, path: string): Promise<boolean> {
9
+ if (!(await host.pathExists(path))) return false;
10
+ if (await host.isFile(path)) return false;
11
+ return true;
12
+ }
13
+
14
+ /** Direct `.odin` package files in a directory (not nested packages). */
15
+ async function listOdinPackageFiles(host: FileDepHost, dir: string): Promise<string[]> {
16
+ if (!(await isDir(host, dir))) return [];
17
+ const names = await host.readDir(dir);
18
+ const out: string[] = [];
19
+ for (const name of names) {
20
+ if (!name.endsWith(".odin")) continue;
21
+ const full = join(dir, name);
22
+ if ((await host.isFile(full)) && host.ownsPath(full)) out.push(full);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ /**
28
+ * Odin root under scope: directory that has both `core` and `base` collections.
29
+ * Also accepts ODIN_ROOT when that path sits inside the scope.
30
+ */
31
+ async function findOdinRoot(host: FileDepHost, fromPath: string): Promise<string | undefined> {
32
+ const key = `odin-root:${host.scopeRoot}`;
33
+ if (host.memo.has(key)) return host.memo.get(key) as string | undefined;
34
+
35
+ const envRoot = process.env.ODIN_ROOT?.trim();
36
+ if (envRoot !== undefined && envRoot.length > 0) {
37
+ const absolute = resolve(envRoot);
38
+ if (isWithin(host.scopeRoot, absolute) || absolute === resolve(host.scopeRoot)) {
39
+ if ((await isDir(host, join(absolute, "core"))) && (await isDir(host, join(absolute, "base")))) {
40
+ host.memo.set(key, absolute);
41
+ return absolute;
42
+ }
43
+ }
44
+ }
45
+
46
+ let dir = resolve(dirname(fromPath));
47
+ const stop = resolve(host.scopeRoot);
48
+ for (;;) {
49
+ if (isWithin(stop, dir) || dir === stop) {
50
+ const core = join(dir, "core");
51
+ const base = join(dir, "base");
52
+ if ((await isDir(host, core)) && (await isDir(host, base))) {
53
+ host.memo.set(key, dir);
54
+ return dir;
55
+ }
56
+ }
57
+ if (dir === stop) break;
58
+ const parent = dirname(dir);
59
+ if (parent === dir) break;
60
+ if (!isWithin(stop, parent) && parent !== stop) break;
61
+ dir = parent;
62
+ }
63
+
64
+ // Scope itself may be the Odin tree root even when fromPath walk missed.
65
+ const scopeCore = join(stop, "core");
66
+ const scopeBase = join(stop, "base");
67
+ if ((await isDir(host, scopeCore)) && (await isDir(host, scopeBase))) {
68
+ host.memo.set(key, stop);
69
+ return stop;
70
+ }
71
+
72
+ host.memo.set(key, undefined);
73
+ return undefined;
74
+ }
75
+
76
+ /**
77
+ * Candidate roots for a named collection, nearest-first.
78
+ * Project `vendor`/`shared` beside sources beat the compiler tree when both exist.
79
+ */
80
+ async function collectionRoots(host: FileDepHost, collection: string, fromPath: string): Promise<string[]> {
81
+ const key = `odin-collection:${host.scopeRoot}:${collection}`;
82
+ const cached = host.memo.get(key);
83
+ if (cached !== undefined) return cached as string[];
84
+
85
+ const found: string[] = [];
86
+ const seen = new Set<string>();
87
+ const add = async (candidate: string) => {
88
+ const absolute = resolve(candidate);
89
+ if (seen.has(absolute)) return;
90
+ if (!isWithin(host.scopeRoot, absolute) && absolute !== resolve(host.scopeRoot)) return;
91
+ if (!(await isDir(host, absolute))) return;
92
+ seen.add(absolute);
93
+ found.push(absolute);
94
+ };
95
+
96
+ // Ancestors from the importing file up to scope (project-local collections).
97
+ let dir = resolve(dirname(fromPath));
98
+ const stop = resolve(host.scopeRoot);
99
+ for (;;) {
100
+ await add(join(dir, collection));
101
+ if (dir === stop) break;
102
+ const parent = dirname(dir);
103
+ if (parent === dir) break;
104
+ if (!isWithin(stop, parent) && parent !== stop) break;
105
+ dir = parent;
106
+ }
107
+
108
+ await add(join(stop, collection));
109
+
110
+ const odinRoot = await findOdinRoot(host, fromPath);
111
+ if (odinRoot !== undefined) await add(join(odinRoot, collection));
112
+
113
+ host.memo.set(key, found);
114
+ return found;
115
+ }
116
+
117
+ async function resolveCollectionImport(
118
+ host: FileDepHost,
119
+ collection: string,
120
+ pkgPath: string,
121
+ fromPath: string,
122
+ signal: AbortSignal,
123
+ ): Promise<ReturnType<typeof internalPaths> | ReturnType<typeof externalId>> {
124
+ const id = `${collection}:${pkgPath}`;
125
+ const segments = pkgPath.split(/[/\\]/u).filter((part) => part.length > 0 && part !== "." && part !== "..");
126
+ if (segments.length === 0) return externalId(id);
127
+
128
+ const roots = await collectionRoots(host, collection, fromPath);
129
+ for (const root of roots) {
130
+ signal.throwIfAborted();
131
+ const dir = join(root, ...segments);
132
+ if (!isWithin(host.scopeRoot, dir) && dir !== resolve(host.scopeRoot)) continue;
133
+ const files = await listOdinPackageFiles(host, dir);
134
+ // Fat packages (e.g. base:runtime) stay package-id edges, not 40+ file dumps.
135
+ if (files.length > 0) return boundedInternalPaths(files, id);
136
+ }
137
+ return externalId(id);
138
+ }
139
+
140
+ async function resolveRelativeImport(
141
+ host: FileDepHost,
142
+ fromPath: string,
143
+ trimmed: string,
144
+ ): Promise<ReturnType<typeof internalPaths> | { kind: "unresolved" }> {
145
+ const absolute = resolve(dirname(fromPath), trimmed);
146
+ const candidates = [absolute, `${absolute}.odin`, join(absolute, "main.odin")];
147
+ const file = await firstExistingFile(host, candidates);
148
+ if (file !== undefined && isWithin(host.scopeRoot, file)) return internalPaths([file]);
149
+
150
+ const files = (await listOdinPackageFiles(host, absolute)).filter((path) => isWithin(host.scopeRoot, path));
151
+ if (files.length > 0) return boundedInternalPaths(files, trimmed);
152
+ return { kind: "unresolved" };
153
+ }
154
+
155
+ export const resolveOdinFileDep: FileDepResolver = async (fromPath, specifier, host, signal) => {
156
+ signal.throwIfAborted();
157
+ const trimmed = specifier.trim().replace(/^"+|"+$/gu, "");
158
+ if (trimmed.length === 0) return { kind: "unresolved" };
159
+
160
+ const collection = COLLECTION_SPEC.exec(trimmed);
161
+ if (collection !== null) {
162
+ const name = collection[1];
163
+ const pkg = collection[2];
164
+ if (name === undefined || pkg === undefined) return { kind: "unresolved" };
165
+ return resolveCollectionImport(host, name, pkg, fromPath, signal);
166
+ }
167
+
168
+ // Relative / path-like package path
169
+ if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.includes("/") || trimmed.includes("\\")) {
170
+ return resolveRelativeImport(host, fromPath, trimmed);
171
+ }
172
+
173
+ // Bare name with no collection — cannot resolve without collection config.
174
+ return externalId(trimmed);
175
+ };
@@ -0,0 +1,407 @@
1
+ import type { Node, Tree } from "web-tree-sitter";
2
+ import type { ExtractResult, GrammarAdapter, LanguageCapabilities } from "../adapter.ts";
3
+ import { resolveOdinFileDep } from "./odin-file-deps.ts";
4
+ import type { CallSite, Decl, ImportRef, Visibility } from "../ir.ts";
5
+ import {
6
+ applyDoc,
7
+ callSite,
8
+ docSpanBefore,
9
+ endLine,
10
+ finishDecl,
11
+ qualify,
12
+ startLine,
13
+ unquote,
14
+ walkBodyNodes,
15
+ type DocSpan,
16
+ } from "./tree.ts";
17
+
18
+ const CAPABILITIES: LanguageCapabilities = {
19
+ shape: true,
20
+ search: true,
21
+ fileDeps: true,
22
+ callEdges: true,
23
+ packageSurface: false,
24
+ };
25
+
26
+ const IMPORT_NOISE = new Set([
27
+ "package",
28
+ "import",
29
+ "foreign",
30
+ "proc",
31
+ "where",
32
+ "when",
33
+ "if",
34
+ "else",
35
+ "for",
36
+ "in",
37
+ "switch",
38
+ "case",
39
+ "break",
40
+ "continue",
41
+ "fallthrough",
42
+ "return",
43
+ "defer",
44
+ "using",
45
+ "struct",
46
+ "union",
47
+ "enum",
48
+ "bit_set",
49
+ "map",
50
+ "dynamic",
51
+ "cast",
52
+ "transmute",
53
+ "auto_cast",
54
+ "true",
55
+ "false",
56
+ "nil",
57
+ "context",
58
+ ]);
59
+
60
+ const EXTENSIONS = [".odin"] as const;
61
+
62
+ /**
63
+ * Node kinds from tree-sitter-odin v1.3.0 node-types + locals.scm definitions:
64
+ * procedure_declaration, overloaded_procedure_declaration → function
65
+ * struct_declaration / union_declaration / bit_field_declaration → struct (union/bit_field closest kind)
66
+ * enum_declaration → enum
67
+ * const_declaration / const_type_declaration → constant
68
+ * variable_declaration (`:=`) → variable, or function when value is `procedure`
69
+ * var_declaration (`:`) → variable
70
+ * field → struct field
71
+ * foreign_block → walk nested package-level decls
72
+ * Package-level only (plan). Nested procs inside function bodies stay out.
73
+ */
74
+ const PROCEDURE_DECLARATION = "procedure_declaration";
75
+ const OVERLOADED_PROCEDURE_DECLARATION = "overloaded_procedure_declaration";
76
+ const STRUCT_DECLARATION = "struct_declaration";
77
+ const ENUM_DECLARATION = "enum_declaration";
78
+ const UNION_DECLARATION = "union_declaration";
79
+ const BIT_FIELD_DECLARATION = "bit_field_declaration";
80
+ const CONST_DECLARATION = "const_declaration";
81
+ const CONST_TYPE_DECLARATION = "const_type_declaration";
82
+ const VAR_DECLARATION = "var_declaration";
83
+ const VARIABLE_DECLARATION = "variable_declaration";
84
+ const FOREIGN_BLOCK = "foreign_block";
85
+ const IMPORT_DECLARATION = "import_declaration";
86
+ const ATTRIBUTES = "attributes";
87
+ const ATTRIBUTE = "attribute";
88
+ const IDENTIFIER = "identifier";
89
+ const PROCEDURE = "procedure";
90
+ const FIELD = "field";
91
+ const BLOCK = "block";
92
+
93
+ const NESTED_SCOPE = new Set([
94
+ "procedure_declaration",
95
+ "overloaded_procedure_declaration",
96
+ "struct_declaration",
97
+ "enum_declaration",
98
+ ]);
99
+
100
+ function extractCalls(body: Node | null): CallSite[] {
101
+ if (body === null) return [];
102
+ const out: CallSite[] = [];
103
+ walkBodyNodes(
104
+ body,
105
+ (n) => NESTED_SCOPE.has(n.type),
106
+ (node) => {
107
+ if (node.type !== "call_expression" && node.type !== "selector_call_expression") return;
108
+ const fn = node.childForFieldName("function") ?? node.namedChildren[0];
109
+ if (fn === null || fn === undefined) return;
110
+ let leafNode = fn;
111
+ let receiver = "";
112
+ if (fn.type === "member_expression" || fn.type === "selector_expression") {
113
+ const fieldNode = fn.childForFieldName("field") ?? fn.namedChildren[fn.namedChildren.length - 1];
114
+ const obj = fn.childForFieldName("argument") ?? fn.childForFieldName("operand") ?? fn.namedChildren[0];
115
+ if (fieldNode !== null && fieldNode !== undefined) leafNode = fieldNode;
116
+ if (obj !== null && obj !== undefined) receiver = obj.text;
117
+ }
118
+ const site = callSite(
119
+ leafNode.text,
120
+ startLine(leafNode),
121
+ leafNode.startIndex,
122
+ leafNode.endIndex,
123
+ "call",
124
+ receiver,
125
+ );
126
+ if (site !== undefined) out.push(site);
127
+ },
128
+ );
129
+ return out;
130
+ }
131
+
132
+ function odinDoc(node: Node, source: string): DocSpan | undefined {
133
+ return docSpanBefore(node, source);
134
+ }
135
+
136
+ function hasPrivateAttribute(node: Node): boolean {
137
+ const attrs = node.namedChildren.find((child) => child.type === ATTRIBUTES);
138
+ if (attrs === undefined) return false;
139
+ for (const attr of attrs.namedChildren) {
140
+ if (attr.type !== ATTRIBUTE) continue;
141
+ for (const child of attr.namedChildren) {
142
+ if (child.type === IDENTIFIER && child.text === "private") return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+
148
+ function visibilityOf(node: Node): Visibility {
149
+ return hasPrivateAttribute(node) ? "private" : "public";
150
+ }
151
+
152
+ /** First non-attribute identifier child — name position for all odin decls in this grammar. */
153
+ function firstIdentifier(node: Node): string {
154
+ for (const child of node.namedChildren) {
155
+ if (child.type === ATTRIBUTES) continue;
156
+ if (child.type === IDENTIFIER) return child.text;
157
+ }
158
+ return "";
159
+ }
160
+
161
+ function procedureBody(node: Node): Node | null {
162
+ const proc = node.namedChildren.find((child) => child.type === PROCEDURE);
163
+ if (proc === undefined) return null;
164
+ // `---` foreign stubs use uninitialized, not block.
165
+ return proc.namedChildren.find((child) => child.type === BLOCK) ?? null;
166
+ }
167
+
168
+ function leaf(
169
+ node: Node,
170
+ owner: string,
171
+ kind: Decl["kind"],
172
+ name: string,
173
+ visibility: Visibility,
174
+ body: Node | null,
175
+ children: Decl[],
176
+ doc: DocSpan | undefined,
177
+ ): Decl | undefined {
178
+ if (name.length === 0) return undefined;
179
+ const callable = kind === "function" || kind === "method" || kind === "constructor";
180
+ const decl = finishDecl(
181
+ {
182
+ kind,
183
+ name,
184
+ qualifiedName: qualify(owner, name),
185
+ startLine: startLine(node),
186
+ endLine: endLine(node),
187
+ startOffset: node.startIndex,
188
+ endOffset: node.endIndex,
189
+ visibility,
190
+ exported: visibility === "public",
191
+ children,
192
+ calls: callable ? extractCalls(body) : [],
193
+ bases: [],
194
+ },
195
+ body,
196
+ );
197
+ if (body === null) decl.signatureEndOffset = node.endIndex;
198
+ return applyDoc(decl, doc);
199
+ }
200
+
201
+ /** struct_declaration children of type `field` (not `struct_field` — that is composite-literal elements). */
202
+ function structFields(node: Node, owner: string): Decl[] {
203
+ const out: Decl[] = [];
204
+ for (const child of node.namedChildren) {
205
+ if (child.type !== FIELD) continue;
206
+ const names: string[] = [];
207
+ for (const part of child.namedChildren) {
208
+ if (part.type === IDENTIFIER) names.push(part.text);
209
+ if (part.type === "type") break;
210
+ }
211
+ for (const name of names) {
212
+ // `using _: T` embedding — skip blank identifier.
213
+ if (name === "_") continue;
214
+ const decl = leaf(child, owner, "field", name, "public", null, [], undefined);
215
+ if (decl !== undefined) out.push(decl);
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+
221
+ function enumMembers(node: Node, owner: string): Decl[] {
222
+ const out: Decl[] = [];
223
+ let seenName = false;
224
+ for (const child of node.namedChildren) {
225
+ if (child.type !== IDENTIFIER) continue;
226
+ if (!seenName) {
227
+ seenName = true;
228
+ continue;
229
+ }
230
+ const decl = leaf(child, owner, "enumMember", child.text, "public", null, [], undefined);
231
+ if (decl !== undefined) out.push(decl);
232
+ }
233
+ return out;
234
+ }
235
+
236
+ /**
237
+ * bit_field_declaration flattens members as identifier/type/number triples after the backing type.
238
+ * Shape: Name :: bit_field Backing { field: T | bits, ... }
239
+ */
240
+ function bitFieldMembers(node: Node, owner: string): Decl[] {
241
+ const out: Decl[] = [];
242
+ let seenName = false;
243
+ let seenBackingType = false;
244
+ for (const child of node.namedChildren) {
245
+ if (child.type === ATTRIBUTES) continue;
246
+ if (!seenName && child.type === IDENTIFIER) {
247
+ seenName = true;
248
+ continue;
249
+ }
250
+ if (seenName && !seenBackingType && child.type === "type") {
251
+ seenBackingType = true;
252
+ continue;
253
+ }
254
+ if (!seenBackingType) continue;
255
+ if (child.type === IDENTIFIER) {
256
+ const decl = leaf(child, owner, "field", child.text, "public", null, [], undefined);
257
+ if (decl !== undefined) out.push(decl);
258
+ }
259
+ }
260
+ return out;
261
+ }
262
+
263
+ function cutSignatureBefore(node: Node, firstChild: Node | undefined, decl: Decl): void {
264
+ if (firstChild === undefined) return;
265
+ decl.signatureEndOffset = firstChild.startIndex;
266
+ decl.bodyStartOffset = firstChild.startIndex;
267
+ decl.bodyEndOffset = node.endIndex;
268
+ }
269
+
270
+ function declsFromNode(node: Node, owner: string, source: string): Decl[] {
271
+ if (node.isError || node.type === "ERROR") {
272
+ const out: Decl[] = [];
273
+ for (const child of node.children) {
274
+ if (child.isNamed || child.isError) out.push(...declsFromNode(child, owner, source));
275
+ }
276
+ return out;
277
+ }
278
+
279
+ const doc = odinDoc(node, source);
280
+ const visibility = visibilityOf(node);
281
+
282
+ switch (node.type) {
283
+ case PROCEDURE_DECLARATION:
284
+ case OVERLOADED_PROCEDURE_DECLARATION: {
285
+ const name = firstIdentifier(node);
286
+ const body = node.type === PROCEDURE_DECLARATION ? procedureBody(node) : null;
287
+ const decl = leaf(node, owner, "function", name, visibility, body, [], doc);
288
+ return decl === undefined ? [] : [decl];
289
+ }
290
+ case STRUCT_DECLARATION: {
291
+ const name = firstIdentifier(node);
292
+ const qn = qualify(owner, name);
293
+ const children = structFields(node, qn);
294
+ const firstField = node.namedChildren.find((child) => child.type === FIELD);
295
+ const decl = leaf(node, owner, "struct", name, visibility, null, children, doc);
296
+ if (decl !== undefined) cutSignatureBefore(node, firstField, decl);
297
+ return decl === undefined ? [] : [decl];
298
+ }
299
+ case UNION_DECLARATION: {
300
+ // locals.scm: union → type; shared IR uses struct as closest kind.
301
+ const name = firstIdentifier(node);
302
+ const decl = leaf(node, owner, "struct", name, visibility, null, [], doc);
303
+ return decl === undefined ? [] : [decl];
304
+ }
305
+ case BIT_FIELD_DECLARATION: {
306
+ // locals.scm: bit_field → type; shared IR uses struct.
307
+ const name = firstIdentifier(node);
308
+ const qn = qualify(owner, name);
309
+ const children = bitFieldMembers(node, qn);
310
+ const decl = leaf(node, owner, "struct", name, visibility, null, children, doc);
311
+ // Signature through backing type: first member identifier after backing `type`.
312
+ const firstMember = children[0];
313
+ if (decl !== undefined && firstMember !== undefined) {
314
+ decl.signatureEndOffset = firstMember.startOffset;
315
+ decl.bodyStartOffset = firstMember.startOffset;
316
+ decl.bodyEndOffset = node.endIndex;
317
+ }
318
+ return decl === undefined ? [] : [decl];
319
+ }
320
+ case ENUM_DECLARATION: {
321
+ const name = firstIdentifier(node);
322
+ const qn = qualify(owner, name);
323
+ const children = enumMembers(node, qn);
324
+ const decl = leaf(node, owner, "enum", name, visibility, null, children, doc);
325
+ return decl === undefined ? [] : [decl];
326
+ }
327
+ case CONST_DECLARATION:
328
+ case CONST_TYPE_DECLARATION: {
329
+ // locals.scm: const_declaration → constant; const_type_declaration → type (value const with explicit type).
330
+ const name = firstIdentifier(node);
331
+ // `_ :: runtime` package aliases are not outline decls.
332
+ if (name === "_") return [];
333
+ const decl = leaf(node, owner, "constant", name, visibility, null, [], doc);
334
+ return decl === undefined ? [] : [decl];
335
+ }
336
+ case VAR_DECLARATION: {
337
+ // `name: Type` / `name: Type = value`
338
+ const name = firstIdentifier(node);
339
+ if (name === "_") return [];
340
+ const decl = leaf(node, owner, "variable", name, visibility, null, [], doc);
341
+ return decl === undefined ? [] : [decl];
342
+ }
343
+ case VARIABLE_DECLARATION: {
344
+ // `name := value` — function when value is a procedure literal (locals.scm still marks var).
345
+ const name = firstIdentifier(node);
346
+ if (name === "_") return [];
347
+ const proc = node.namedChildren.find((child) => child.type === PROCEDURE);
348
+ if (proc !== undefined) {
349
+ const body = proc.namedChildren.find((child) => child.type === BLOCK) ?? null;
350
+ const decl = leaf(node, owner, "function", name, visibility, body, [], doc);
351
+ return decl === undefined ? [] : [decl];
352
+ }
353
+ const decl = leaf(node, owner, "variable", name, visibility, null, [], doc);
354
+ return decl === undefined ? [] : [decl];
355
+ }
356
+ case FOREIGN_BLOCK: {
357
+ // Package-level foreign { ... } — extract nested decls at same owner.
358
+ const block = node.namedChildren.find((child) => child.type === BLOCK);
359
+ if (block === undefined) return [];
360
+ const out: Decl[] = [];
361
+ for (const child of block.namedChildren) {
362
+ out.push(...declsFromNode(child, owner, source));
363
+ }
364
+ return out;
365
+ }
366
+ default:
367
+ return [];
368
+ }
369
+ }
370
+
371
+ function collectImports(root: Node): ImportRef[] {
372
+ const imports: ImportRef[] = [];
373
+ for (const node of root.namedChildren) {
374
+ if (node.type !== IMPORT_DECLARATION) continue;
375
+ const str = node.namedChildren.find((child) => child.type === "string");
376
+ const specifier = str === undefined ? node.text : unquote(str.text);
377
+ imports.push({
378
+ specifier,
379
+ startLine: startLine(node),
380
+ startOffset: node.startIndex,
381
+ endOffset: node.endIndex,
382
+ bindings: [],
383
+ });
384
+ }
385
+ return imports;
386
+ }
387
+
388
+ function extractOdin(tree: Tree, source: string): ExtractResult {
389
+ const root = tree.rootNode;
390
+ const decls: Decl[] = [];
391
+ for (const node of root.children) {
392
+ if (node.isNamed || node.isError) decls.push(...declsFromNode(node, "", source));
393
+ }
394
+ return { decls, imports: collectImports(root) };
395
+ }
396
+
397
+ export function odinAdapter(): GrammarAdapter {
398
+ return {
399
+ mode: "grammar",
400
+ id: "odin",
401
+ extensions: EXTENSIONS,
402
+ capabilities: CAPABILITIES,
403
+ importNoiseIdentifiers: IMPORT_NOISE,
404
+ extract: extractOdin,
405
+ resolveFileDep: resolveOdinFileDep,
406
+ };
407
+ }