@shanepadgett/tau-agent 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -1,217 +0,0 @@
1
- import { dirname, join, relative, resolve, sep } from "node:path";
2
- import type { FileDepResolver } from "../adapter.ts";
3
- import { externalId, firstExistingFile, internalPaths, isWithin } from "./file-dep-util.ts";
4
-
5
- type JvmFileDepOptions = {
6
- /** File extensions to match, e.g. .java / .kt */
7
- extensions: readonly string[];
8
- /** Leading packages always treated as external. */
9
- externalPrefixes: readonly string[];
10
- /** Extra source-root suffixes under scope (relative). */
11
- sourceRootSuffixes: readonly string[];
12
- };
13
-
14
- function stripImportNoise(specifier: string): { path: string; wildcard: boolean } {
15
- let s = specifier.trim().replace(/;$/u, "");
16
- s = s.replace(/^static\s+/u, "");
17
- const wildcard = s.endsWith(".*");
18
- if (wildcard) {
19
- // Keep full package path — do not peel segments (all-lowercase packages).
20
- return { path: s.slice(0, -2).trim(), wildcard: true };
21
- }
22
- // Static/member imports: pkg.Type.member or pkg.Type.Companion.member → peel trailing
23
- // non-type segments (lowercase start, or Companion/INSTANCE).
24
- const parts = s.split(".").filter((part) => part.length > 0);
25
- while (parts.length > 1) {
26
- const last = parts[parts.length - 1];
27
- if (last === undefined) break;
28
- if (last === "Companion" || last === "INSTANCE" || last === "Default") {
29
- parts.pop();
30
- continue;
31
- }
32
- // Java/Kotlin type names are typically UpperCamel; members start lower.
33
- const start = last.charAt(0);
34
- if (start !== "" && start === start.toLowerCase() && start !== start.toUpperCase()) {
35
- parts.pop();
36
- continue;
37
- }
38
- break;
39
- }
40
- return { path: parts.join("."), wildcard: false };
41
- }
42
-
43
- function isExternalPackage(path: string, prefixes: readonly string[]): boolean {
44
- for (const prefix of prefixes) {
45
- if (path === prefix || path.startsWith(`${prefix}.`)) return true;
46
- }
47
- return false;
48
- }
49
-
50
- function segmentsOf(dotted: string): string[] {
51
- return dotted.split(".").filter((part) => part.length > 0);
52
- }
53
-
54
- /** Infer source root by peeling package segments off dirname(fromPath). */
55
- function inferredSourceRoot(fromPath: string, packageSegments: readonly string[]): string | undefined {
56
- if (packageSegments.length === 0) return dirname(fromPath);
57
- const dir = resolve(dirname(fromPath));
58
- const parts = dir.split(sep);
59
- if (parts.length < packageSegments.length) return undefined;
60
- const tail = parts.slice(parts.length - packageSegments.length);
61
- if (tail.join("/") !== packageSegments.join("/") && tail.join(sep) !== packageSegments.join(sep)) {
62
- // compare case-sensitive path segments
63
- for (let i = 0; i < packageSegments.length; i += 1) {
64
- if (tail[i] !== packageSegments[i]) return undefined;
65
- }
66
- }
67
- return parts.slice(0, parts.length - packageSegments.length).join(sep) || sep;
68
- }
69
-
70
- /** Resolve a type import (`pkg.Type`) to 0–1 files under a source root. No package dumps. */
71
- async function resolveTypeAgainstRoot(
72
- host: Parameters<FileDepResolver>[2],
73
- sourceRoot: string,
74
- segments: readonly string[],
75
- extensions: readonly string[],
76
- ): Promise<string | undefined> {
77
- if (!isWithin(host.scopeRoot, sourceRoot) && resolve(sourceRoot) !== resolve(host.scopeRoot)) {
78
- return undefined;
79
- }
80
- if (segments.length === 0) return undefined;
81
- const typeName = segments[segments.length - 1];
82
- const pkg = segments.slice(0, -1);
83
- if (typeName === undefined) return undefined;
84
- const dir = pkg.length === 0 ? sourceRoot : join(sourceRoot, ...pkg);
85
- const candidates = extensions.map((ext) => join(dir, `${typeName}${ext}`));
86
- const file = await firstExistingFile(host, candidates);
87
- if (file !== undefined && isWithin(host.scopeRoot, file)) return file;
88
- return undefined;
89
- }
90
-
91
- function sourceRootsFor(
92
- host: Parameters<FileDepResolver>[2],
93
- fromPath: string,
94
- importSegments: readonly string[],
95
- suffixes: readonly string[],
96
- ): string[] {
97
- const roots = new Set<string>();
98
- // Peel one segment as possible type from fromPath package guess via path
99
- const fromDir = dirname(fromPath);
100
- const relToScope = relative(host.scopeRoot, fromDir);
101
- if (!relToScope.startsWith("..") && !isAbsoluteRel(relToScope)) {
102
- // try every ancestor as potential source root
103
- const parts = relToScope.length === 0 ? [] : relToScope.split(sep);
104
- for (let i = 0; i <= parts.length; i += 1) {
105
- roots.add(i === 0 ? host.scopeRoot : join(host.scopeRoot, ...parts.slice(0, i)));
106
- }
107
- }
108
- const inferred = inferredSourceRoot(fromPath, guessPackageSegments(fromPath, host.scopeRoot));
109
- if (inferred !== undefined) roots.add(inferred);
110
-
111
- for (const suffix of suffixes) {
112
- roots.add(join(host.scopeRoot, ...suffix.split("/").filter((p) => p.length > 0)));
113
- }
114
-
115
- // Prefer roots that could contain the import path
116
- void importSegments;
117
- return [...roots];
118
- }
119
-
120
- function isAbsoluteRel(rel: string): boolean {
121
- return rel.length > 0 && (rel.startsWith(sep) || /^[A-Za-z]:[\\/]/u.test(rel));
122
- }
123
-
124
- function guessPackageSegments(fromPath: string, scopeRoot: string): string[] {
125
- const rel = relative(scopeRoot, dirname(fromPath));
126
- if (rel.startsWith("..") || rel.length === 0) return [];
127
- const parts = rel.split(sep);
128
- // Drop common source root prefixes
129
- const dropPrefixes = [
130
- ["src", "main", "java"],
131
- ["src", "main", "kotlin"],
132
- ["src", "commonMain", "kotlin"],
133
- ["src", "commonMain", "java"],
134
- ["src", "jvmMain", "kotlin"],
135
- ["src", "test", "java"],
136
- ["src", "test", "kotlin"],
137
- ["src"],
138
- ];
139
- for (const prefix of dropPrefixes) {
140
- if (parts.length >= prefix.length && prefix.every((p, i) => parts[i] === p)) {
141
- return parts.slice(prefix.length);
142
- }
143
- }
144
- // guava layout: guava/src/com/...
145
- const comAt = parts.indexOf("com");
146
- const orgAt = parts.indexOf("org");
147
- const netAt = parts.indexOf("net");
148
- const idx = [comAt, orgAt, netAt].filter((v) => v >= 0).sort((a, b) => a - b)[0];
149
- if (idx !== undefined) return parts.slice(idx);
150
- return parts;
151
- }
152
-
153
- function createJvmFileDepResolver(options: JvmFileDepOptions): FileDepResolver {
154
- return async (fromPath, specifier, host, signal) => {
155
- signal.throwIfAborted();
156
- const raw = specifier
157
- .trim()
158
- .replace(/;$/u, "")
159
- .replace(/^static\s+/u, "");
160
- const { path, wildcard } = stripImportNoise(specifier);
161
- if (path.length === 0) return { kind: "unresolved" };
162
- if (isExternalPackage(path, options.externalPrefixes)) return externalId(path);
163
-
164
- // Wildcards / bare packages are package ids — never dump every file in the dir.
165
- if (wildcard) return externalId(path);
166
-
167
- const segments = segmentsOf(path);
168
- const packageOnly =
169
- segments.length > 0 &&
170
- segments.every((seg) => {
171
- const c = seg.charAt(0);
172
- return c === c.toLowerCase() && c !== c.toUpperCase();
173
- });
174
- // Top-level function/property import (okio.buffer) peels to package-only segments.
175
- const rawParts = raw
176
- .replace(/\.\*$/u, "")
177
- .split(".")
178
- .filter((part) => part.length > 0);
179
- const lastRaw = rawParts[rawParts.length - 1];
180
- const memberLike =
181
- lastRaw !== undefined &&
182
- lastRaw !== "Companion" &&
183
- lastRaw.charAt(0) === lastRaw.charAt(0).toLowerCase() &&
184
- lastRaw.charAt(0) !== lastRaw.charAt(0).toUpperCase();
185
- if (packageOnly) {
186
- return externalId(memberLike ? raw.replace(/\.\*$/u, "") : path);
187
- }
188
-
189
- const roots = sourceRootsFor(host, fromPath, segments, options.sourceRootSuffixes);
190
- for (const root of roots) {
191
- signal.throwIfAborted();
192
- const file = await resolveTypeAgainstRoot(host, root, segments, options.extensions);
193
- if (file !== undefined) return internalPaths([file]);
194
- }
195
- return externalId(path);
196
- };
197
- }
198
-
199
- export const resolveJavaFileDep = createJvmFileDepResolver({
200
- extensions: [".java"],
201
- externalPrefixes: ["java", "javax", "jakarta", "jdk", "sun"],
202
- sourceRootSuffixes: ["src/main/java", "src"],
203
- });
204
-
205
- export const resolveKotlinFileDep = createJvmFileDepResolver({
206
- // .kts is often Gradle — not a package type file.
207
- extensions: [".kt", ".java"],
208
- externalPrefixes: ["java", "javax", "jakarta", "kotlin", "kotlinx"],
209
- sourceRootSuffixes: [
210
- "src/main/kotlin",
211
- "src/main/java",
212
- "src/commonMain/kotlin",
213
- "src/commonMain/java",
214
- "src/jvmMain/kotlin",
215
- "src",
216
- ],
217
- });
@@ -1,175 +0,0 @@
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
- };