jev-agent-tools 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -0
  4. package/docs/design.md +187 -0
  5. package/docs/tools/jev_ask.md +92 -0
  6. package/docs/tools/jev_ask_files.md +80 -0
  7. package/docs/tools/jev_check_diff.md +77 -0
  8. package/docs/tools/jev_find_files.md +59 -0
  9. package/docs/tools/jev_locate_in_file.md +51 -0
  10. package/docs/tools/jev_select_tests.md +59 -0
  11. package/package.json +85 -0
  12. package/rules/jev-ask.md +4 -0
  13. package/src/adapters/analysis-context.ts +100 -0
  14. package/src/adapters/ask-files.ts +236 -0
  15. package/src/adapters/ask-proof.ts +202 -0
  16. package/src/adapters/ask-syntax.ts +463 -0
  17. package/src/adapters/command.ts +240 -0
  18. package/src/adapters/docs.ts +222 -0
  19. package/src/adapters/files.ts +411 -0
  20. package/src/adapters/find.ts +151 -0
  21. package/src/adapters/git-base.ts +32 -0
  22. package/src/adapters/git-inventory.ts +94 -0
  23. package/src/adapters/git.ts +525 -0
  24. package/src/adapters/locate-file.ts +197 -0
  25. package/src/adapters/output-lines.ts +50 -0
  26. package/src/adapters/risk-callers.ts +525 -0
  27. package/src/adapters/runner-version.ts +102 -0
  28. package/src/adapters/syntax.ts +229 -0
  29. package/src/adapters/test-inventory.ts +168 -0
  30. package/src/adapters/usage.ts +21 -0
  31. package/src/adapters/utf8.ts +57 -0
  32. package/src/constants.ts +109 -0
  33. package/src/core/ask-closure.ts +419 -0
  34. package/src/core/ask-proof.ts +32 -0
  35. package/src/core/ask-references.ts +249 -0
  36. package/src/core/asks.ts +616 -0
  37. package/src/core/batches.ts +83 -0
  38. package/src/core/command-output.ts +249 -0
  39. package/src/core/diff.ts +226 -0
  40. package/src/core/docs.ts +399 -0
  41. package/src/core/find.ts +157 -0
  42. package/src/core/git.ts +5 -0
  43. package/src/core/import-boundaries.ts +102 -0
  44. package/src/core/imports.ts +691 -0
  45. package/src/core/integrity.ts +64 -0
  46. package/src/core/lexical.ts +130 -0
  47. package/src/core/locate.ts +213 -0
  48. package/src/core/output.ts +264 -0
  49. package/src/core/pointer.ts +51 -0
  50. package/src/core/risk-callers.ts +1270 -0
  51. package/src/core/runner-version.ts +66 -0
  52. package/src/core/sections.ts +269 -0
  53. package/src/core/state.ts +53 -0
  54. package/src/core/syntax.ts +8 -0
  55. package/src/core/test-commands.ts +430 -0
  56. package/src/core/test-coverage.ts +103 -0
  57. package/src/core/test-discovery.ts +1695 -0
  58. package/src/core/test-evidence.ts +649 -0
  59. package/src/core/test-state.ts +99 -0
  60. package/src/core/truncate.ts +14 -0
  61. package/src/core/units.ts +531 -0
  62. package/src/describe.ts +26 -0
  63. package/src/guide.ts +42 -0
  64. package/src/host.ts +22 -0
  65. package/src/index.ts +40 -0
  66. package/src/jev/client.ts +505 -0
  67. package/src/jev/pool.ts +60 -0
  68. package/src/jev/types.ts +60 -0
  69. package/src/presets/docs.ts +85 -0
  70. package/src/presets/risk.ts +263 -0
  71. package/src/presets/spec.ts +111 -0
  72. package/src/presets/witnesses.ts +313 -0
  73. package/src/render.ts +45 -0
  74. package/src/result.ts +4 -0
  75. package/src/run-end.ts +157 -0
  76. package/src/runtime.ts +16 -0
  77. package/src/session.ts +106 -0
  78. package/src/texts/ask-files.ts +2 -0
  79. package/src/texts/ask.ts +4 -0
  80. package/src/texts/check-diff.ts +24 -0
  81. package/src/texts/configuration.ts +2 -0
  82. package/src/texts/find.ts +14 -0
  83. package/src/texts/guide.ts +16 -0
  84. package/src/texts/locate.ts +10 -0
  85. package/src/texts/run-end.ts +13 -0
  86. package/src/texts/select-tests.ts +3 -0
  87. package/src/tools/ask-files.ts +263 -0
  88. package/src/tools/ask-schema.ts +116 -0
  89. package/src/tools/ask.ts +925 -0
  90. package/src/tools/check-diff.ts +510 -0
  91. package/src/tools/docs-check.ts +399 -0
  92. package/src/tools/find.ts +529 -0
  93. package/src/tools/locate.ts +369 -0
  94. package/src/tools/select-tests.ts +746 -0
  95. package/src/tools/spec-check.ts +210 -0
@@ -0,0 +1,691 @@
1
+ import { posix } from "node:path";
2
+ import { type LexicalSource, tokenize as tokens } from "./lexical.ts";
3
+
4
+ export interface ImportSource {
5
+ path: string;
6
+ text: string;
7
+ }
8
+ export type ImportLimit = {
9
+ path: string;
10
+ specifier: string;
11
+ kind: "unresolved" | "dynamic" | "outside_repo" | "ambiguous";
12
+ };
13
+ export interface ImportGraph {
14
+ edges: ReadonlyMap<string, readonly string[]>;
15
+ limits: readonly ImportLimit[];
16
+ }
17
+
18
+ function parseConfiguration(
19
+ text: string,
20
+ lexical = tokens(text, false),
21
+ ): Record<string, unknown> {
22
+ return JSON.parse(
23
+ lexical
24
+ .filter(
25
+ (token, index) =>
26
+ !(
27
+ token.value === "," &&
28
+ ["}", "]"].includes(lexical[index + 1]?.value ?? "")
29
+ ),
30
+ )
31
+ .map((token) =>
32
+ token.string ? JSON.stringify(token.value) : token.value,
33
+ )
34
+ .join(" "),
35
+ ) as Record<string, unknown>;
36
+ }
37
+
38
+ function exportTargets(value: unknown): string[] {
39
+ if (typeof value === "string") return [value];
40
+ if (!value || typeof value !== "object") return [];
41
+ return Object.values(value).flatMap(exportTargets);
42
+ }
43
+
44
+ function exportEntries(manifest: Record<string, unknown>): [string, string][] {
45
+ const value = manifest.exports;
46
+ if (value === undefined)
47
+ return exportTargets(manifest.main).map((target) => [".", target]);
48
+ if (
49
+ value &&
50
+ typeof value === "object" &&
51
+ Object.keys(value).some((key) => key.startsWith("."))
52
+ )
53
+ return Object.entries(value).flatMap(([key, target]) =>
54
+ exportTargets(target).map((text): [string, string] => [key, text]),
55
+ );
56
+ return exportTargets(value).map((target) => [".", target]);
57
+ }
58
+
59
+ function wildcardMatch(pattern: string, value: string): string | undefined {
60
+ const star = pattern.indexOf("*");
61
+ if (star < 0) return pattern === value ? "" : undefined;
62
+ const prefix = pattern.slice(0, star);
63
+ const suffix = pattern.slice(star + 1);
64
+ return value.startsWith(prefix) &&
65
+ value.endsWith(suffix) &&
66
+ value.length >= prefix.length + suffix.length
67
+ ? value.slice(prefix.length, value.length - suffix.length)
68
+ : undefined;
69
+ }
70
+
71
+ /** Discriminating literal search hints; callers still confirm each hit with the shared resolver. */
72
+ export function importSearchSpecifiers(
73
+ configuration: readonly ImportSource[],
74
+ targets: readonly string[],
75
+ ): string[] {
76
+ const patterns = new Set<string>();
77
+ for (const target of targets) {
78
+ const stem = target.replace(/\.[^./]+$/, "");
79
+ const parts = stem.split("/");
80
+ for (let index = 0; index < parts.length - 1; index++)
81
+ patterns.add(`./${parts.slice(index).join("/")}`);
82
+ patterns.add(`./${parts.at(-1)}`);
83
+ if (parts.at(-1) === "index") {
84
+ const folder = parts.slice(0, -1);
85
+ for (let index = 0; index < folder.length - 1; index++)
86
+ patterns.add(`./${folder.slice(index).join("/")}`);
87
+ if (folder.length) patterns.add(`./${folder.at(-1)}`);
88
+ }
89
+ }
90
+ for (const source of configuration) {
91
+ let parsed: Record<string, unknown>;
92
+ try {
93
+ parsed = parseConfiguration(source.text);
94
+ } catch {
95
+ continue;
96
+ }
97
+ const cwd = posix.dirname(source.path);
98
+ if (
99
+ posix.basename(source.path) === "package.json" &&
100
+ typeof parsed.name === "string"
101
+ ) {
102
+ for (const [key, value] of exportEntries(parsed)) {
103
+ if (!value.startsWith("./")) continue;
104
+ const base = posix.join(cwd, value).replace(/\.[cm]?[jt]sx?$/, "");
105
+ for (const target of targets) {
106
+ const middle = wildcardMatch(
107
+ base,
108
+ target.replace(/\.[cm]?[jt]sx?$/, ""),
109
+ );
110
+ if (middle !== undefined)
111
+ patterns.add(
112
+ parsed.name +
113
+ (key === "." ? "" : key.slice(1).replace("*", middle)),
114
+ );
115
+ }
116
+ }
117
+ }
118
+ const options = parsed.compilerOptions as
119
+ | Record<string, unknown>
120
+ | undefined;
121
+ if (!options || typeof options !== "object") continue;
122
+ const base = posix.join(
123
+ cwd,
124
+ typeof options.baseUrl === "string" ? options.baseUrl : ".",
125
+ );
126
+ if (options.paths && typeof options.paths === "object")
127
+ for (const [alias, values] of Object.entries(options.paths)) {
128
+ if (!Array.isArray(values)) continue;
129
+ for (const value of values) {
130
+ if (typeof value !== "string") continue;
131
+ const pattern = posix.join(base, value);
132
+ const star = pattern.indexOf("*");
133
+ for (const target of targets) {
134
+ for (const stem of new Set([
135
+ target.replace(/\.[cm]?[jt]sx?$/, ""),
136
+ target.replace(/\/index\.[cm]?[jt]sx?$/, ""),
137
+ ])) {
138
+ if (
139
+ star < 0
140
+ ? stem === pattern.replace(/\.[cm]?[jt]sx?$/, "")
141
+ : stem.startsWith(pattern.slice(0, star)) &&
142
+ stem.endsWith(pattern.slice(star + 1))
143
+ ) {
144
+ const middle =
145
+ star < 0
146
+ ? ""
147
+ : stem.slice(
148
+ star,
149
+ stem.length - (pattern.length - star - 1),
150
+ );
151
+ patterns.add(alias.replace("*", middle));
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ return [...patterns];
159
+ }
160
+
161
+ export function buildImportGraph(
162
+ files: readonly ImportSource[],
163
+ options: { known?: ReadonlySet<string> } = {},
164
+ ): ImportGraph {
165
+ return createImportGraphBuilder(
166
+ files,
167
+ options.known ?? new Set(files.map((file) => file.path)),
168
+ )(files);
169
+ }
170
+
171
+ /** Parse resolution configuration once, then resolve independent batches with identical semantics. */
172
+ export function createImportGraphBuilder(
173
+ configuration: readonly ImportSource[],
174
+ known: ReadonlySet<string>,
175
+ lexical?: LexicalSource,
176
+ ): (files: readonly ImportSource[]) => ImportGraph {
177
+ const cache = new Map<
178
+ string,
179
+ { text: string; edges: readonly string[]; limits: readonly ImportLimit[] }
180
+ >();
181
+ let limits: ImportLimit[] = [];
182
+ const packages = new Map<
183
+ string,
184
+ { cwd: string; manifest: Record<string, unknown> }[]
185
+ >();
186
+ const packageDirectories = new Set<string>();
187
+ for (const path of known) {
188
+ if (posix.basename(path) !== "package.json") continue;
189
+ const directory = posix.dirname(path);
190
+ const parent = posix.basename(posix.dirname(directory));
191
+ packageDirectories.add(
192
+ parent.startsWith("@")
193
+ ? `${parent}/${posix.basename(directory)}`
194
+ : posix.basename(directory),
195
+ );
196
+ }
197
+ const aliases: {
198
+ cwd: string;
199
+ base: string;
200
+ baseUrl: boolean;
201
+ paths: Record<string, unknown>;
202
+ }[] = [];
203
+ for (const source of configuration) {
204
+ if (
205
+ posix.basename(source.path) !== "package.json" &&
206
+ !/^tsconfig.*\.json$/.test(posix.basename(source.path))
207
+ )
208
+ continue;
209
+ try {
210
+ const parsed = parseConfiguration(
211
+ source.text,
212
+ lexical?.tokenize(source.path, source.text),
213
+ );
214
+ const cwd = posix.dirname(source.path);
215
+ if (
216
+ posix.basename(source.path) === "package.json" &&
217
+ typeof parsed.name === "string"
218
+ ) {
219
+ const previous = packages.get(parsed.name) ?? [];
220
+ previous.push({ cwd, manifest: parsed });
221
+ packages.set(parsed.name, previous);
222
+ } else if (
223
+ parsed.compilerOptions &&
224
+ typeof parsed.compilerOptions === "object"
225
+ ) {
226
+ const options = parsed.compilerOptions as Record<string, unknown>;
227
+ aliases.push({
228
+ cwd,
229
+ base: posix.join(
230
+ cwd,
231
+ typeof options.baseUrl === "string" ? options.baseUrl : ".",
232
+ ),
233
+ baseUrl: typeof options.baseUrl === "string",
234
+ paths:
235
+ options.paths && typeof options.paths === "object"
236
+ ? (options.paths as Record<string, unknown>)
237
+ : {},
238
+ });
239
+ }
240
+ } catch {
241
+ limits.push({
242
+ path: source.path,
243
+ specifier: "configuration",
244
+ kind: "unresolved",
245
+ });
246
+ }
247
+ }
248
+ const configurationLimits = limits;
249
+ const resolve = (
250
+ file: ImportSource,
251
+ specifier: string,
252
+ python: boolean,
253
+ ): string[] => {
254
+ if (specifier.startsWith("node:")) return [];
255
+ let base: string;
256
+ if (python) {
257
+ let dots = 0;
258
+ while (specifier[dots] === ".") dots++;
259
+ base = dots ? posix.dirname(file.path) : "";
260
+ for (let j = 1; j < dots; j++) base = posix.dirname(base);
261
+ base = posix.join(base, specifier.slice(dots).replaceAll(".", "/"));
262
+ } else {
263
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
264
+ const name = specifier.startsWith("@")
265
+ ? specifier.split("/").slice(0, 2).join("/")
266
+ : (specifier.split("/")[0] ?? "");
267
+ const workspace = packages.get(name);
268
+ if (workspace) {
269
+ if (
270
+ aliases.some(
271
+ (alias) =>
272
+ !posix.relative(alias.cwd, file.path).startsWith("../") &&
273
+ Object.keys(alias.paths).some(
274
+ (pattern) => wildcardMatch(pattern, specifier) !== undefined,
275
+ ),
276
+ )
277
+ ) {
278
+ limits.push({ path: file.path, specifier, kind: "ambiguous" });
279
+ return [];
280
+ }
281
+ if (workspace.length !== 1) {
282
+ limits.push({ path: file.path, specifier, kind: "ambiguous" });
283
+ return [];
284
+ }
285
+ const found = workspace[0];
286
+ if (!found) return [];
287
+ const { cwd, manifest } = found;
288
+ const suffix = specifier.slice(name.length);
289
+ const key = suffix ? `.${suffix}` : ".";
290
+ const entries = exportEntries(manifest);
291
+ const exact = entries.filter(([pattern]) => pattern === key);
292
+ const matching = exact.length
293
+ ? exact
294
+ : entries.filter(
295
+ ([pattern]) => wildcardMatch(pattern, key) !== undefined,
296
+ );
297
+ const resolved = new Set<string>();
298
+ for (const [pattern, target] of matching) {
299
+ if (!target.startsWith("./")) continue;
300
+ const middle = wildcardMatch(pattern, key) ?? "";
301
+ const path = posix.normalize(
302
+ posix.join(cwd, target.replace("*", middle)),
303
+ );
304
+ if (posix.relative(cwd, path).startsWith("../")) {
305
+ limits.push({ path: file.path, specifier, kind: "outside_repo" });
306
+ continue;
307
+ }
308
+ const previousLimits = limits.length;
309
+ for (const match of resolve(
310
+ file,
311
+ `./${posix.relative(posix.dirname(file.path), path)}`,
312
+ false,
313
+ ))
314
+ resolved.add(match);
315
+ limits.splice(previousLimits);
316
+ }
317
+ if (!resolved.size)
318
+ limits.push({ path: file.path, specifier, kind: "unresolved" });
319
+ return [...resolved];
320
+ } else {
321
+ const local = aliases.filter(
322
+ (alias) => !posix.relative(alias.cwd, file.path).startsWith("../"),
323
+ );
324
+ const matches: string[] = [];
325
+ for (const alias of local)
326
+ for (const [pattern, values] of Object.entries(alias.paths)) {
327
+ const star = pattern.indexOf("*");
328
+ if (
329
+ !(star < 0
330
+ ? specifier === pattern
331
+ : specifier.startsWith(pattern.slice(0, star)) &&
332
+ specifier.endsWith(pattern.slice(star + 1)))
333
+ )
334
+ continue;
335
+ if (!Array.isArray(values)) continue;
336
+ const middle =
337
+ star < 0
338
+ ? ""
339
+ : specifier.slice(
340
+ star,
341
+ specifier.length - (pattern.length - star - 1),
342
+ );
343
+ for (const value of values)
344
+ if (typeof value === "string") {
345
+ const target = posix.normalize(
346
+ posix.join(alias.base, value.replace("*", middle)),
347
+ );
348
+ if (target.startsWith("../")) {
349
+ limits.push({
350
+ path: file.path,
351
+ specifier,
352
+ kind: "outside_repo",
353
+ });
354
+ continue;
355
+ }
356
+ matches.push(
357
+ ...resolve(
358
+ file,
359
+ `./${posix.relative(posix.dirname(file.path), target)}`,
360
+ false,
361
+ ),
362
+ );
363
+ }
364
+ }
365
+ const unique = [...new Set(matches)];
366
+ if (unique.length) return unique;
367
+ if (
368
+ local.some((alias) => alias.baseUrl) ||
369
+ specifier.startsWith("@/") ||
370
+ specifier.startsWith("~/") ||
371
+ specifier.startsWith("#") ||
372
+ packageDirectories.has(name)
373
+ )
374
+ limits.push({ path: file.path, specifier, kind: "unresolved" });
375
+ return [];
376
+ }
377
+ } else
378
+ base = posix.normalize(posix.join(posix.dirname(file.path), specifier));
379
+ }
380
+ if (base.startsWith("../") || specifier.startsWith("/")) {
381
+ limits.push({ path: file.path, specifier, kind: "outside_repo" });
382
+ return [];
383
+ }
384
+ let candidates: string[];
385
+ if (python) candidates = [`${base}.py`, posix.join(base, "__init__.py")];
386
+ else if (known.has(base)) candidates = [base];
387
+ else {
388
+ const stem = base.replace(/\.[cm]?jsx?$/, "");
389
+ candidates = [
390
+ `${stem}.ts`,
391
+ `${stem}.tsx`,
392
+ `${stem}.mts`,
393
+ `${stem}.cts`,
394
+ `${base}.js`,
395
+ `${base}.ts`,
396
+ `${base}.tsx`,
397
+ posix.join(base, "index.ts"),
398
+ posix.join(base, "index.js"),
399
+ ];
400
+ }
401
+ const matches = [...new Set(candidates)].filter((path) => known.has(path));
402
+ if (matches.length !== 1) {
403
+ // Standard-library imports are external; unknown roots/plugins remain explicit.
404
+ const standard =
405
+ /^(?:__future__|sys|os|pathlib|json|re|math|typing|collections|itertools|functools|datetime|unittest|contextlib|asyncio|subprocess|tempfile|shutil|logging|inspect|importlib|dataclasses|enum|abc|io|time|copy|decimal|statistics|warnings)(?:\.|$)/;
406
+ if (!python || specifier.startsWith(".") || !standard.test(specifier))
407
+ limits.push({
408
+ path: file.path,
409
+ specifier,
410
+ kind: matches.length ? "ambiguous" : "unresolved",
411
+ });
412
+ return [];
413
+ }
414
+ return matches;
415
+ };
416
+ return (files) => {
417
+ const edges = new Map<string, readonly string[]>();
418
+ limits = [...configurationLimits];
419
+ for (const file of files) {
420
+ const cached = cache.get(file.path);
421
+ if (cached?.text === file.text) {
422
+ edges.set(file.path, cached.edges);
423
+ limits.push(...cached.limits);
424
+ continue;
425
+ }
426
+ const limitStart = limits.length;
427
+ const python = file.path.endsWith(".py");
428
+ if (!python && !/\.[cm]?[jt]sx?$/.test(file.path)) {
429
+ edges.set(file.path, []);
430
+ continue;
431
+ }
432
+ lexical?.resolved?.(file.path, file.text);
433
+ const ts = lexical
434
+ ? lexical.tokenize(file.path, file.text, python)
435
+ : tokens(file.text, python);
436
+ const targets = new Set<string>();
437
+ const add = (specifier: string) => {
438
+ for (const path of resolve(file, specifier, python)) targets.add(path);
439
+ };
440
+ const dynamic = (specifier = "") =>
441
+ limits.push({ path: file.path, specifier, kind: "dynamic" });
442
+ const readers = new Set<string>();
443
+ readers.add("readFile");
444
+ readers.add("readFileSync");
445
+ if (!python)
446
+ for (let i = 0; i < ts.length; i++) {
447
+ if (ts[i]?.value !== "import" || ts[i]?.string) continue;
448
+ let end = i + 1;
449
+ while (end < ts.length && ts[end]?.value !== ";" && !ts[end]?.string)
450
+ end++;
451
+ if (
452
+ !["node:fs", "fs", "node:fs/promises", "fs/promises"].includes(
453
+ ts[end]?.value ?? "",
454
+ )
455
+ )
456
+ continue;
457
+ for (let j = i + 1; j < end; j++)
458
+ if (["readFile", "readFileSync"].includes(ts[j]?.value ?? "")) {
459
+ const local =
460
+ ts[j + 1]?.value === "as" ? ts[j + 2]?.value : ts[j]?.value;
461
+ if (local) readers.add(local);
462
+ }
463
+ }
464
+ for (let i = 0; i < ts.length; i++) {
465
+ const t = ts[i];
466
+ if (!t || t.string) continue;
467
+ if (
468
+ python &&
469
+ t.value === "sys" &&
470
+ ts[i + 1]?.value === "." &&
471
+ ts[i + 2]?.value === "path"
472
+ )
473
+ dynamic("sys.path mutation or lookup");
474
+ if (python && t.value === "from") {
475
+ let j = i + 1;
476
+ let module = "";
477
+ while (
478
+ j < ts.length &&
479
+ ts[j]?.line === t.line &&
480
+ ts[j]?.value !== "import"
481
+ )
482
+ module += ts[j++]?.value ?? "";
483
+ if (ts[j]?.value === "import") {
484
+ if (module && !/^\.+$/.test(module)) add(module);
485
+ // `from . import module` and `from pkg import module` may expose submodules.
486
+ let k = j + 1;
487
+ while (k < ts.length && ts[k]?.line === t.line) {
488
+ const name = ts[k]?.value ?? "";
489
+ if (
490
+ /^\w+$/.test(name) &&
491
+ name !== "as" &&
492
+ ts[k - 1]?.value !== "as"
493
+ ) {
494
+ const spec = module.endsWith(".")
495
+ ? `${module}${name}`
496
+ : `${module}.${name}`;
497
+ let dots = 0;
498
+ while (spec[dots] === ".") dots++;
499
+ let base = dots ? posix.dirname(file.path) : "";
500
+ for (let level = 1; level < dots; level++)
501
+ base = posix.dirname(base);
502
+ const local = posix.join(
503
+ base,
504
+ spec.slice(dots).replaceAll(".", "/"),
505
+ );
506
+ if (
507
+ /^\.+$/.test(module) ||
508
+ known.has(`${local}.py`) ||
509
+ known.has(posix.join(local, "__init__.py"))
510
+ )
511
+ add(spec);
512
+ }
513
+ k++;
514
+ }
515
+ }
516
+ } else if (
517
+ python &&
518
+ t.value === "import" &&
519
+ ts[i - 1]?.value !== "from" &&
520
+ !ts
521
+ .slice(0, i)
522
+ .some((prev) => prev.line === t.line && prev.value === "from")
523
+ ) {
524
+ let j = i + 1;
525
+ let module = "";
526
+ while (j < ts.length && ts[j]?.line === t.line) {
527
+ if (ts[j]?.value === "as") break;
528
+ if (ts[j]?.value === ",") {
529
+ if (module) add(module);
530
+ module = "";
531
+ } else module += ts[j]?.value ?? "";
532
+ j++;
533
+ }
534
+ if (module) add(module);
535
+ } else if (!python && (t.value === "import" || t.value === "export")) {
536
+ if (ts[i + 1]?.value === "(") {
537
+ if (
538
+ ts[i + 2]?.string &&
539
+ ts[i + 3]?.value === ")" &&
540
+ ts[i + 2]?.value !== "<dynamic>"
541
+ )
542
+ add(ts[i + 2]?.value ?? "");
543
+ else dynamic();
544
+ } else if (ts[i + 1]?.string) add(ts[i + 1]?.value ?? "");
545
+ else {
546
+ for (
547
+ let j = i + 1;
548
+ j < ts.length && ts[j]?.value !== ";" && j < i + 100;
549
+ j++
550
+ ) {
551
+ if (ts[j]?.value === "from" && ts[j + 1]?.string) {
552
+ add(ts[j + 1]?.value ?? "");
553
+ break;
554
+ }
555
+ if ((ts[j]?.line ?? t.line) > t.line + 8) break;
556
+ }
557
+ }
558
+ } else if (
559
+ t.value === "URL" &&
560
+ ts[i - 1]?.value === "new" &&
561
+ ts[i + 1]?.value === "("
562
+ ) {
563
+ if (
564
+ ts[i + 2]?.string &&
565
+ ts[i + 3]?.value === "," &&
566
+ ts[i + 4]?.value === "import" &&
567
+ ts[i + 5]?.value === "." &&
568
+ ts[i + 6]?.value === "meta" &&
569
+ ts[i + 7]?.value === "." &&
570
+ ts[i + 8]?.value === "url" &&
571
+ ts[i + 9]?.value === ")"
572
+ )
573
+ add(ts[i + 2]?.value ?? "");
574
+ } else if (
575
+ (t.value === "require" || readers.has(t.value)) &&
576
+ ts[i + 1]?.value === "("
577
+ ) {
578
+ if (
579
+ ts[i + 2]?.string &&
580
+ (ts[i + 3]?.value === "," || ts[i + 3]?.value === ")") &&
581
+ ts[i + 2]?.value !== "<dynamic>"
582
+ )
583
+ add(ts[i + 2]?.value ?? "");
584
+ else if (
585
+ readers.has(t.value) &&
586
+ ts[i + 2]?.value === "new" &&
587
+ ts[i + 3]?.value === "URL" &&
588
+ ts[i + 4]?.value === "(" &&
589
+ ts[i + 5]?.string &&
590
+ ts[i + 6]?.value === ","
591
+ ) {
592
+ // The nested URL token supplies the same statically resolved edge.
593
+ } else dynamic();
594
+ } else if (
595
+ python &&
596
+ (t.value === "__import__" || t.value === "import_module") &&
597
+ ts[i + 1]?.value === "("
598
+ )
599
+ dynamic();
600
+ }
601
+ const dependencies = [...targets];
602
+ edges.set(file.path, dependencies);
603
+ cache.set(file.path, {
604
+ text: file.text,
605
+ edges: dependencies,
606
+ limits: limits.slice(limitStart),
607
+ });
608
+ }
609
+ const seenLimits = new Set<string>();
610
+ const uniqueLimits = limits.filter((limit) => {
611
+ const key = JSON.stringify([limit.path, limit.specifier, limit.kind]);
612
+ if (seenLimits.has(key)) return false;
613
+ seenLimits.add(key);
614
+ return true;
615
+ });
616
+ return { edges, limits: uniqueLimits };
617
+ };
618
+ }
619
+
620
+ export function importClosure(
621
+ graph: ImportGraph,
622
+ start: string,
623
+ options: { maxDepth?: number; stop?: (path: string) => boolean } = {},
624
+ ): { paths: readonly string[]; limits: readonly ImportLimit[] } {
625
+ const seen = new Set<string>([start]);
626
+ const queue = [{ path: start, depth: 0 }];
627
+ for (let i = 0; i < queue.length; i++) {
628
+ const item = queue[i];
629
+ if (!item) continue;
630
+ if (
631
+ item.depth >= (options.maxDepth ?? Infinity) ||
632
+ options.stop?.(item.path)
633
+ )
634
+ continue;
635
+ for (const path of graph.edges.get(item.path) ?? []) {
636
+ if (!seen.has(path)) {
637
+ seen.add(path);
638
+ queue.push({ path, depth: item.depth + 1 });
639
+ }
640
+ }
641
+ }
642
+ return {
643
+ paths: [...seen],
644
+ limits: graph.limits.filter((limit) => seen.has(limit.path)),
645
+ };
646
+ }
647
+
648
+ /** Forward closure with injected reads; configuration and reached batches share the exact resolver. */
649
+ export async function importClosureLazy(
650
+ read: (path: string) => Promise<ImportSource | undefined>,
651
+ start: string,
652
+ options: {
653
+ known: ReadonlySet<string>;
654
+ configuration?: readonly ImportSource[];
655
+ stop?: (path: string) => boolean;
656
+ cache?: Map<string, Promise<ImportGraph>>;
657
+ build?: (files: readonly ImportSource[]) => ImportGraph;
658
+ },
659
+ ): Promise<{ paths: readonly string[]; limits: readonly ImportLimit[] }> {
660
+ const build =
661
+ options.build ??
662
+ createImportGraphBuilder(options.configuration ?? [], options.known);
663
+ const cache = options.cache ?? new Map<string, Promise<ImportGraph>>();
664
+ const seen = new Set([start]);
665
+ const queue = [start];
666
+ const limits: ImportLimit[] = [];
667
+ for (let index = 0; index < queue.length; index++) {
668
+ const path = queue[index];
669
+ if (!path || options.stop?.(path)) continue;
670
+ let pending = cache.get(path);
671
+ if (!pending) {
672
+ pending = read(path).then((source) =>
673
+ source
674
+ ? build([source])
675
+ : {
676
+ edges: new Map(),
677
+ limits: [{ path, specifier: path, kind: "unresolved" as const }],
678
+ },
679
+ );
680
+ cache.set(path, pending);
681
+ }
682
+ const graph = await pending;
683
+ limits.push(...graph.limits);
684
+ for (const target of graph.edges.get(path) ?? [])
685
+ if (!seen.has(target)) {
686
+ seen.add(target);
687
+ queue.push(target);
688
+ }
689
+ }
690
+ return { paths: [...seen], limits };
691
+ }