jev-agent-tools 0.1.4 → 0.2.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 (131) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +42 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +39 -12
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host.ts +11 -0
  117. package/src/jev/client.ts +12 -0
  118. package/src/jev/types.ts +6 -0
  119. package/src/mcp/main.ts +135 -0
  120. package/src/mcp/protocol.ts +282 -0
  121. package/src/mcp/tools.ts +166 -0
  122. package/src/session.ts +59 -0
  123. package/src/setup.ts +13 -5
  124. package/src/tools/ask-files.ts +5 -7
  125. package/src/tools/ask.ts +26 -22
  126. package/src/tools/check-diff.ts +8 -5
  127. package/src/tools/docs-check.ts +1 -0
  128. package/src/tools/find.ts +5 -2
  129. package/src/tools/locate.ts +5 -8
  130. package/src/tools/select-tests.ts +7 -4
  131. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,144 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { STATE_MAX_CHARS, TIMEOUT_MS } from "../constants.js";
3
+ import { collectFiles } from "./files.js";
4
+ import { verifyGitUtf8 } from "./utf8.js";
5
+ export async function collectAskRepository(exec, cwd, base, signal) {
6
+ try {
7
+ const options = { cwd, timeout: TIMEOUT_MS, signal };
8
+ const location = await exec("git", ["rev-parse", "--show-toplevel"], options);
9
+ if (location.code !== 0 || location.killed)
10
+ return {
11
+ ok: false,
12
+ error: location.stderr || "Cannot resolve repository root.",
13
+ };
14
+ const root = location.stdout.trimEnd();
15
+ const gitOptions = { ...options, cwd: root };
16
+ const [listed, ignored, revision] = await Promise.all([
17
+ exec("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], gitOptions),
18
+ exec("git", ["ls-files", "--cached", "--ignored", "--exclude-standard", "-z"], gitOptions),
19
+ base === undefined
20
+ ? undefined
21
+ : exec("git", ["rev-parse", "--verify", "--end-of-options", `${base}^{commit}`], gitOptions),
22
+ ]);
23
+ if (listed.code !== 0 || listed.killed)
24
+ return {
25
+ ok: false,
26
+ error: listed.stderr || "Cannot list repository paths.",
27
+ };
28
+ if (revision && (revision.code !== 0 || revision.killed))
29
+ return { ok: false, error: revision.stderr || "Cannot resolve base." };
30
+ if (ignored.code !== 0 || ignored.killed)
31
+ return {
32
+ ok: false,
33
+ error: ignored.stderr || "Cannot list ignored repository paths.",
34
+ };
35
+ const known = new Set(listed.stdout.split("\0").filter(Boolean));
36
+ for (const path of ignored.stdout.split("\0"))
37
+ known.delete(path);
38
+ const baseSha = revision?.stdout.trim();
39
+ const basePaths = baseSha
40
+ ? await exec("git", ["ls-tree", "-r", "--name-only", "-z", baseSha], gitOptions)
41
+ : undefined;
42
+ if (basePaths && (basePaths.code !== 0 || basePaths.killed))
43
+ return {
44
+ ok: false,
45
+ error: basePaths.stderr || "Cannot list base paths.",
46
+ };
47
+ const beforeKnown = new Set(basePaths?.stdout.split("\0").filter(Boolean));
48
+ const reads = new Map();
49
+ const beforeReads = new Map();
50
+ return {
51
+ ok: true,
52
+ root,
53
+ known,
54
+ baseSha,
55
+ read(path) {
56
+ let pending = reads.get(path);
57
+ if (!pending) {
58
+ pending = (async () => {
59
+ if (!known.has(path))
60
+ return {
61
+ ok: false,
62
+ error: `${path}: not in repository inventory`,
63
+ };
64
+ const loaded = await collectFiles(root, [path], signal, {
65
+ allowEmpty: true,
66
+ exec,
67
+ inventory: known,
68
+ });
69
+ return loaded.ok
70
+ ? { ok: true, path, text: loaded.files[path] ?? "" }
71
+ : loaded;
72
+ })();
73
+ reads.set(path, pending);
74
+ }
75
+ return pending;
76
+ },
77
+ readBefore(path) {
78
+ let pending = beforeReads.get(path);
79
+ if (!pending) {
80
+ pending = (async () => {
81
+ if (!baseSha || !beforeKnown.has(path))
82
+ return { ok: true, text: null };
83
+ try {
84
+ const object = await exec("git", [
85
+ "rev-parse",
86
+ "--verify",
87
+ "--end-of-options",
88
+ `${baseSha}:${path}`,
89
+ ], gitOptions);
90
+ if (object.code !== 0 || object.killed)
91
+ return {
92
+ ok: false,
93
+ error: `before evidence unavailable for ${path}`,
94
+ };
95
+ const id = object.stdout.trim();
96
+ const sizeResult = await exec("git", ["cat-file", "-s", id], gitOptions);
97
+ const size = Number(sizeResult.stdout);
98
+ if (sizeResult.code !== 0 ||
99
+ sizeResult.killed ||
100
+ !Number.isFinite(size) ||
101
+ size > STATE_MAX_CHARS * 4)
102
+ return {
103
+ ok: false,
104
+ error: `before evidence unavailable for ${path}`,
105
+ };
106
+ const loaded = await exec("git", [
107
+ "show",
108
+ "--no-ext-diff",
109
+ "--no-textconv",
110
+ "--end-of-options",
111
+ id,
112
+ ], gitOptions);
113
+ if (loaded.code !== 0 ||
114
+ loaded.killed ||
115
+ loaded.stdout.length > STATE_MAX_CHARS)
116
+ return {
117
+ ok: false,
118
+ error: `before evidence unavailable for ${path}`,
119
+ };
120
+ const decoded = verifyGitUtf8(loaded.stdout, id, size);
121
+ return decoded.ok
122
+ ? decoded
123
+ : { ok: false, error: `${path}: ${decoded.error}` };
124
+ }
125
+ catch (error) {
126
+ return { ok: false, error: String(error) };
127
+ }
128
+ })();
129
+ beforeReads.set(path, pending);
130
+ }
131
+ return pending;
132
+ },
133
+ };
134
+ }
135
+ catch (error) {
136
+ return {
137
+ ok: false,
138
+ error: `Cannot collect repository proof: ${String(error)}`,
139
+ };
140
+ }
141
+ }
142
+ export function repositoryPath(root, cwd, path) {
143
+ return relative(root, resolve(cwd, path)).split("\\").join("/");
144
+ }
@@ -0,0 +1,385 @@
1
+ import { loadSyntaxRootParser } from "./syntax.js";
2
+ function nodes(root, kinds) {
3
+ const result = [];
4
+ for (const kind of kinds) {
5
+ try {
6
+ result.push(...root.findAll({ rule: { kind } }));
7
+ }
8
+ catch {
9
+ /* Node kinds differ across the supported language grammars. */
10
+ }
11
+ }
12
+ return result
13
+ .map((node) => ({ node, index: node.range().start.index }))
14
+ .sort((a, b) => a.index - b.index)
15
+ .map((entry) => entry.node);
16
+ }
17
+ function names(root, includeTypes = true) {
18
+ const result = new Set();
19
+ const excluded = nodes(root, [
20
+ "import_statement",
21
+ "import_from_statement",
22
+ "export_clause",
23
+ ]).map((node) => node.range());
24
+ const typeScopes = [];
25
+ if (includeTypes) {
26
+ for (const binder of nodes(root, [
27
+ "type_parameter",
28
+ "infer_type",
29
+ "mapped_type_clause",
30
+ ])) {
31
+ const name = binder.field("name") ??
32
+ binder.children().find((node) => node.kind() === "type_identifier");
33
+ let scope = binder.parent();
34
+ if (binder.kind() === "type_parameter")
35
+ scope = scope?.parent() ?? null;
36
+ if (binder.kind() === "infer_type") {
37
+ while (scope && scope.kind() !== "conditional_type")
38
+ scope = scope.parent();
39
+ const own = name?.range();
40
+ if (name && own)
41
+ typeScopes.push({
42
+ name: name.text(),
43
+ start: own.start.index,
44
+ end: own.end.index,
45
+ });
46
+ scope = scope?.field("consequence") ?? null;
47
+ }
48
+ if (name && scope) {
49
+ const range = scope.range();
50
+ typeScopes.push({
51
+ name: name.text(),
52
+ start: range.start.index,
53
+ end: range.end.index,
54
+ });
55
+ }
56
+ }
57
+ }
58
+ for (const node of nodes(root, [
59
+ "identifier",
60
+ "type_identifier",
61
+ "nested_type_identifier",
62
+ "member_expression",
63
+ "attribute",
64
+ ])) {
65
+ const position = node.range().start.index;
66
+ if (node.kind() === "type_identifier") {
67
+ if (!includeTypes ||
68
+ node.parent()?.kind() === "nested_type_identifier" ||
69
+ typeScopes.some((scope) => scope.name === node.text() &&
70
+ position >= scope.start &&
71
+ position < scope.end))
72
+ continue;
73
+ }
74
+ if (!includeTypes && node.kind() === "nested_type_identifier")
75
+ continue;
76
+ if (excluded.some((range) => position >= range.start.index && position < range.end.index))
77
+ continue;
78
+ if (node.kind() === "identifier" || node.kind() === "type_identifier")
79
+ result.add(node.text());
80
+ else {
81
+ const owner = node.field("object") ?? node.field("module");
82
+ const property = node.field("property") ?? node.field("attribute") ?? node.field("name");
83
+ if (owner && property)
84
+ result.add(`${owner.text()}.${property.text()}`);
85
+ }
86
+ }
87
+ return [...result];
88
+ }
89
+ function literal(node) {
90
+ if (!node || !["string", "string_literal"].includes(String(node.kind())))
91
+ return undefined;
92
+ const fragments = nodes(node, ["string_fragment", "string_content"]);
93
+ if (nodes(node, ["interpolation", "escape_sequence"]).length)
94
+ return undefined;
95
+ return fragments.map((fragment) => fragment.text()).join("");
96
+ }
97
+ /** Collect AST metadata only; repository resolution remains in the shared I8 graph. */
98
+ export async function collectProofSyntax(files, before, sourceParser) {
99
+ const declarations = [];
100
+ const bindings = [];
101
+ const limits = [];
102
+ const uses = new Map();
103
+ const parser = sourceParser ?? (await loadSyntaxRootParser());
104
+ for (const file of files) {
105
+ if (!parser?.supports(file.path)) {
106
+ limits.push({
107
+ fact: `${file.path} : unsupported language${/\.[cm]?[jt]sx?$|\.py$/.test(file.path) ? ", optional parser unavailable" : ""}`,
108
+ next: "Install the language parser or provide the required declarations.",
109
+ });
110
+ continue;
111
+ }
112
+ const python = file.path.endsWith(".py");
113
+ const parsed = parser.parseRaw(file.path, file.text);
114
+ if (!parsed.ok) {
115
+ limits.push({
116
+ fact: `${file.path}: parse_failed`,
117
+ next: "read the complete source and check its syntax",
118
+ });
119
+ continue;
120
+ }
121
+ const root = parsed.root;
122
+ const cachedParser = Object.create(parser);
123
+ cachedParser.parseRaw = () => parsed;
124
+ const syntax = cachedParser.declarations(file.path, file.text, {
125
+ descendAbove: Infinity,
126
+ });
127
+ for (const limit of syntax.limits) {
128
+ if (limit.kind === "parse_partial" && limit.declaration)
129
+ continue;
130
+ limits.push({
131
+ fact: `${file.path} : ${limit.kind}`,
132
+ next: "check the complete supplied source",
133
+ });
134
+ }
135
+ if (syntax.limits.some((limit) => limit.kind === "parse_failed"))
136
+ continue;
137
+ const usedNames = names(root);
138
+ const valueNames = names(root, false);
139
+ uses.set(file.path, usedNames);
140
+ const collectDeclarations = (tree) => {
141
+ const result = [];
142
+ for (const outer of tree.children()) {
143
+ const envelope = outer;
144
+ const node = ["export_statement", "decorated_definition"].includes(String(outer.kind()))
145
+ ? (outer
146
+ .children()
147
+ .find((child) => [
148
+ "function_declaration",
149
+ "generator_function_declaration",
150
+ "class_declaration",
151
+ "lexical_declaration",
152
+ "variable_declaration",
153
+ "function_definition",
154
+ "class_definition",
155
+ "type_alias_declaration",
156
+ "interface_declaration",
157
+ "enum_declaration",
158
+ ].includes(String(child.kind()))) ?? outer)
159
+ : outer;
160
+ if ([
161
+ "function_declaration",
162
+ "generator_function_declaration",
163
+ "class_declaration",
164
+ "function_definition",
165
+ "class_definition",
166
+ "type_alias_declaration",
167
+ "interface_declaration",
168
+ "enum_declaration",
169
+ ].includes(String(node.kind()))) {
170
+ const name = node.field("name")?.text();
171
+ if (name)
172
+ result.push({ name, text: envelope.text(), node: envelope });
173
+ }
174
+ else if (["lexical_declaration", "variable_declaration"].includes(String(node.kind()))) {
175
+ for (const variable of node
176
+ .children()
177
+ .filter((child) => child.kind() === "variable_declarator")) {
178
+ const name = variable.field("name")?.text();
179
+ if (name)
180
+ result.push({ name, text: envelope.text(), node: envelope });
181
+ }
182
+ }
183
+ else if (python && node.kind() === "expression_statement") {
184
+ const assignment = node
185
+ .children()
186
+ .find((child) => child.kind() === "assignment");
187
+ const name = assignment?.field("left")?.text();
188
+ if (name)
189
+ result.push({ name, text: envelope.text(), node: envelope });
190
+ }
191
+ }
192
+ for (const declaration of result) {
193
+ if (!declaration.node.find({ rule: { kind: "ERROR" } }))
194
+ continue;
195
+ const fact = `${file.path} : parse_partial (${declaration.name})`;
196
+ if (!limits.some((limit) => limit.fact === fact))
197
+ limits.push({
198
+ fact,
199
+ next: "check the complete supplied source declaration",
200
+ });
201
+ }
202
+ return result;
203
+ };
204
+ const old = before && Object.hasOwn(before, file.path)
205
+ ? before[file.path]
206
+ : undefined;
207
+ const oldRoot = typeof old === "string" ? parser.parseRaw(file.path, old) : undefined;
208
+ const oldDeclarations = oldRoot?.ok
209
+ ? collectDeclarations(oldRoot.root)
210
+ : [];
211
+ const currentDeclarations = collectDeclarations(root);
212
+ for (const declaration of currentDeclarations) {
213
+ const tree = declaration.node;
214
+ const decorators = nodes(tree, ["decorator"]);
215
+ declarations.push({
216
+ path: file.path,
217
+ name: declaration.name,
218
+ text: declaration.text,
219
+ ...(old !== undefined
220
+ ? {
221
+ before: oldDeclarations.find((item) => item.name === declaration.name)
222
+ ?.text ?? null,
223
+ }
224
+ : {}),
225
+ usedNames: names(tree),
226
+ fixture: python &&
227
+ decorators.some((node) => nodes(node, ["identifier"]).some((item) => item.text() === "fixture")),
228
+ autouse: python &&
229
+ decorators.some((node) => nodes(node, ["keyword_argument"]).some((item) => item.field("name")?.text() === "autouse" &&
230
+ item.field("value")?.text() === "True")),
231
+ });
232
+ }
233
+ for (const declaration of oldDeclarations) {
234
+ if (!currentDeclarations.some((item) => item.name === declaration.name))
235
+ declarations.push({
236
+ path: file.path,
237
+ name: declaration.name,
238
+ text: "",
239
+ before: declaration.text,
240
+ usedNames: names(declaration.node),
241
+ deleted: true,
242
+ });
243
+ }
244
+ const add = (local, imported, specifier, reexport, data = false) => bindings.push({
245
+ path: file.path,
246
+ local,
247
+ imported,
248
+ specifier,
249
+ reexport,
250
+ used: data ||
251
+ usedNames.includes(local) ||
252
+ usedNames.some((name) => name.startsWith(`${local}.`)),
253
+ ...(data ? { data: true } : {}),
254
+ typeOnly: !data &&
255
+ !valueNames.some((name) => name === local || name.startsWith(`${local}.`)),
256
+ valueMembers: valueNames.filter((name) => name.startsWith(`${local}.`)),
257
+ });
258
+ for (const node of nodes(root, [
259
+ "import_statement",
260
+ "import_from_statement",
261
+ "export_statement",
262
+ ])) {
263
+ if (python) {
264
+ const children = node.children();
265
+ const module = node.kind() === "import_from_statement"
266
+ ? children.find((child) => ["relative_import", "dotted_name"].includes(String(child.kind())))
267
+ : undefined;
268
+ for (const child of children) {
269
+ if (child === module)
270
+ continue;
271
+ if (child.kind() === "aliased_import") {
272
+ const original = child.field("name")?.text() ??
273
+ child
274
+ .children()
275
+ .find((item) => item.kind() === "dotted_name")
276
+ ?.text();
277
+ const alias = child.field("alias")?.text() ??
278
+ child
279
+ .children()
280
+ .filter((item) => item.kind() === "identifier")
281
+ .at(-1)
282
+ ?.text();
283
+ if (original)
284
+ add(alias ?? original, module ? original : "*", module?.text() ?? original, file.path.endsWith("__init__.py"));
285
+ }
286
+ else if (child.kind() === "dotted_name")
287
+ add(child.text().split(".")[0] ?? child.text(), module ? child.text() : "*", module?.text() ?? child.text(), file.path.endsWith("__init__.py"));
288
+ else if (child.kind() === "wildcard_import" && module)
289
+ add("*", "*", module.text(), file.path.endsWith("__init__.py"));
290
+ }
291
+ }
292
+ else {
293
+ const specifier = literal(node.field("source") ??
294
+ node.children().find((child) => child.kind() === "string"));
295
+ const reexport = node.kind() === "export_statement";
296
+ const specs = nodes(node, ["import_specifier", "export_specifier"]);
297
+ if (specifier) {
298
+ for (const spec of specs) {
299
+ const ids = spec
300
+ .children()
301
+ .filter((child) => child.kind() === "identifier");
302
+ const imported = spec.field("name")?.text() ?? ids[0]?.text();
303
+ const alias = spec.field("alias")?.text() ?? ids.at(-1)?.text();
304
+ if (imported)
305
+ add(alias ?? imported, imported, specifier, reexport);
306
+ }
307
+ const clause = node
308
+ .children()
309
+ .find((child) => child.kind() === "import_clause");
310
+ const defaultName = clause
311
+ ?.children()
312
+ .find((child) => child.kind() === "identifier");
313
+ if (defaultName)
314
+ add(defaultName.text(), "default", specifier, false);
315
+ const namespace = nodes(node, [
316
+ "namespace_import",
317
+ "namespace_export",
318
+ ])[0];
319
+ if (namespace)
320
+ add(namespace
321
+ .children()
322
+ .find((child) => child.kind() === "identifier")
323
+ ?.text() ?? "*", "*", specifier, reexport);
324
+ else if (reexport &&
325
+ !specs.length &&
326
+ node.children().some((child) => child.text() === "*"))
327
+ add("*", "*", specifier, true);
328
+ }
329
+ else if (reexport) {
330
+ if (node.children().some((child) => child.kind() === "default")) {
331
+ const declaration = node
332
+ .children()
333
+ .find((child) => child.field("name") !== null);
334
+ const name = declaration?.field("name")?.text();
335
+ if (name)
336
+ add("default", name, "", true);
337
+ }
338
+ for (const spec of specs) {
339
+ const ids = spec
340
+ .children()
341
+ .filter((child) => child.kind() === "identifier");
342
+ if (ids[0])
343
+ add(ids.at(-1)?.text() ?? ids[0].text(), ids[0].text(), "", true);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ const readAliases = bindings
349
+ .filter((binding) => binding.path === file.path && binding.imported === "readFileSync")
350
+ .map((binding) => binding.local);
351
+ for (const call of nodes(root, python ? ["call"] : ["call_expression"])) {
352
+ const fn = call.field("function");
353
+ const fnName = fn?.text() ?? "";
354
+ if (fnName === "readFileSync" ||
355
+ fnName.endsWith(".readFileSync") ||
356
+ readAliases.includes(fnName)) {
357
+ const argument = (call.field("arguments") ??
358
+ call.children().find((child) => child.kind() === "arguments"))
359
+ ?.children()
360
+ .find((child) => child.kind() === "string");
361
+ const specifier = literal(argument);
362
+ if (specifier !== undefined)
363
+ add(specifier, "*", specifier, false, true);
364
+ else
365
+ limits.push({
366
+ fact: `${file.path} : unresolved dynamic readFileSync call`,
367
+ next: "Provide the data file explicitly.",
368
+ });
369
+ }
370
+ if (["import", "__import__", "importlib.import_module"].includes(fnName))
371
+ limits.push({
372
+ fact: `${file.path} : unresolved dynamic import ${call.text()}`,
373
+ next: "Provide the dependency explicitly.",
374
+ });
375
+ }
376
+ }
377
+ return {
378
+ declarations,
379
+ bindings,
380
+ limits,
381
+ sources: files,
382
+ uses,
383
+ ...(before ? { before } : {}),
384
+ };
385
+ }
@@ -0,0 +1,17 @@
1
+ import { realpath } from "node:fs";
2
+ import { promisify } from "node:util";
3
+ const nativeRealpath = promisify(realpath.native);
4
+ /**
5
+ * Canonical form of an existing path: resolves symlinks and, on Windows,
6
+ * expands 8.3 short names (C:\Users\NAME~1) so the path relates correctly to
7
+ * `git rev-parse --show-toplevel`, which always reports the long form. Falls
8
+ * back to the input when the path cannot be resolved.
9
+ */
10
+ export async function canonicalPath(path) {
11
+ try {
12
+ return await nativeRealpath(path);
13
+ }
14
+ catch {
15
+ return path;
16
+ }
17
+ }