jev-agent-tools 0.1.3 → 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 (135) hide show
  1. package/CHANGELOG.md +88 -3
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +64 -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 +294 -0
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host-tui.d.ts +14 -0
  117. package/src/host.ts +11 -0
  118. package/src/index.ts +13 -5
  119. package/src/jev/client.ts +12 -0
  120. package/src/jev/types.ts +6 -0
  121. package/src/mcp/main.ts +135 -0
  122. package/src/mcp/protocol.ts +282 -0
  123. package/src/mcp/tools.ts +166 -0
  124. package/src/secret-input.ts +222 -0
  125. package/src/session.ts +59 -0
  126. package/src/setup.ts +170 -0
  127. package/src/texts/configuration.ts +1 -1
  128. package/src/tools/ask-files.ts +8 -13
  129. package/src/tools/ask.ts +29 -28
  130. package/src/tools/check-diff.ts +11 -11
  131. package/src/tools/docs-check.ts +1 -0
  132. package/src/tools/find.ts +8 -8
  133. package/src/tools/locate.ts +8 -9
  134. package/src/tools/select-tests.ts +10 -10
  135. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,851 @@
1
+ import { CLOSURE_MAX_CHARS, STATE_MAX_CHARS, UNIT_MAX_CHARS, } from "../constants.js";
2
+ const question = {
3
+ type: "choice",
4
+ instructions: "Compare the concrete before caller/provider/unit combination to the complete after combination, accounting for all coordinated edits. From the source code shown (not an execution), what happens to this concrete caller? If the actual provider implementation is missing or selected from unseen configuration, choose cannot_tell.",
5
+ criteria: {
6
+ no_new_failure: "The after caller path still works: supported rename, equivalent refactor, or coordinated migration; no newly failing call is shown.",
7
+ new_failure: "The before caller succeeds, but the after caller adds a concrete exception, rejected await or missing/non-callable member on that path.",
8
+ cannot_tell: "The actual provider implementation or binding needed to establish the caller outcome is not present.",
9
+ },
10
+ };
11
+ const callableKinds = {
12
+ function_declaration: true,
13
+ generator_function_declaration: true,
14
+ function_definition: true,
15
+ method_definition: true,
16
+ arrow_function: true,
17
+ function_expression: true,
18
+ };
19
+ function nodes(root, file) {
20
+ const range = file?.ranges.get(root.id());
21
+ if (file && range)
22
+ return file.all.slice(range.start, range.end);
23
+ const result = [];
24
+ const visit = (node) => {
25
+ result.push(node);
26
+ for (const child of node.children())
27
+ visit(child);
28
+ };
29
+ visit(root);
30
+ return result;
31
+ }
32
+ function indexFile(path, root) {
33
+ const file = {
34
+ path,
35
+ root,
36
+ all: [],
37
+ errors: [],
38
+ syntax: [],
39
+ syntaxByName: new Map(),
40
+ ranges: new Map(),
41
+ scopes: new Map(),
42
+ parents: new Map(),
43
+ declarations: new Map(),
44
+ writes: new Map(),
45
+ imports: [],
46
+ identifiers: [],
47
+ calls: [],
48
+ aliases: new Map(),
49
+ memo: new Map(),
50
+ dynamic: false,
51
+ meta: false,
52
+ };
53
+ const invalid = new Set();
54
+ for (const error of root.findAll({ rule: { kind: "ERROR" } })) {
55
+ const range = error.range();
56
+ file.errors.push({
57
+ start: range.start.line + 1,
58
+ end: range.end.line + (range.end.column === 0 ? 0 : 1),
59
+ });
60
+ invalid.add(error.id());
61
+ for (const ancestor of error.ancestors())
62
+ invalid.add(ancestor.id());
63
+ }
64
+ const syntaxNode = (node, name, invalidClass, callable) => {
65
+ const range = node.range();
66
+ const entry = {
67
+ node,
68
+ name,
69
+ start: range.start.line + 1,
70
+ end: range.end.line + (range.end.column === 0 ? 0 : 1),
71
+ invalid: invalid.has(node.id()) || invalidClass,
72
+ callable,
73
+ };
74
+ file.syntax.push(entry);
75
+ const named = file.syntaxByName.get(name) ?? [];
76
+ named.push(entry);
77
+ file.syntaxByName.set(name, named);
78
+ };
79
+ const visit = (node, owner, declaratorName, invalidClass = false, indexOnly = false) => {
80
+ const kind = String(node.kind());
81
+ const callable = !!callableKinds[kind];
82
+ const classNode = kind === "class_declaration" || kind === "class_definition";
83
+ const badClass = invalidClass || (classNode && invalid.has(node.id()));
84
+ const skip = kind === "ERROR" ||
85
+ ((callable ||
86
+ /^(?:export_statement|lexical_declaration|variable_declaration|variable_declarator|class_declaration|class_definition|assignment)$/.test(kind)) &&
87
+ invalid.has(node.id()));
88
+ const syntaxKind = callable ||
89
+ classNode ||
90
+ kind === "interface_declaration" ||
91
+ kind === "type_alias_declaration" ||
92
+ kind === "variable_declarator" ||
93
+ kind === "assignment";
94
+ if (syntaxKind) {
95
+ const syntaxName = node.field("name")?.text() ?? declaratorName ?? "";
96
+ syntaxNode(node, syntaxName, badClass, callable);
97
+ }
98
+ const childDeclarator = kind === "variable_declarator"
99
+ ? node.field("name")?.text()
100
+ : declaratorName;
101
+ if (skip || indexOnly) {
102
+ for (const child of node.children())
103
+ visit(child, owner, childDeclarator, badClass, true);
104
+ return;
105
+ }
106
+ const start = file.all.length;
107
+ file.all.push(node);
108
+ const nameNode = ["variable_declarator", "assignment"].includes(kind)
109
+ ? (node.field("name") ?? node.field("left"))
110
+ : [
111
+ "function_declaration",
112
+ "function_definition",
113
+ "class_declaration",
114
+ "class_definition",
115
+ ].includes(kind)
116
+ ? node.field("name")
117
+ : undefined;
118
+ if (nameNode && owner) {
119
+ const names = file.declarations.get(owner.id()) ?? new Map();
120
+ const name = nameNode.text();
121
+ const declarations = names.get(name) ?? [];
122
+ declarations.push(node);
123
+ names.set(name, declarations);
124
+ file.declarations.set(owner.id(), names);
125
+ const value = node.field("value") ?? node.field("right");
126
+ if (value &&
127
+ ["identifier", "member_expression", "attribute"].includes(String(value.kind()))) {
128
+ const original = (value.field("property") ??
129
+ value.field("attribute") ??
130
+ value).text();
131
+ const aliases = file.aliases.get(original) ?? new Set();
132
+ aliases.add(name);
133
+ file.aliases.set(original, aliases);
134
+ }
135
+ }
136
+ if (["import_specifier", "aliased_import"].includes(kind)) {
137
+ const original = node.field("name")?.text();
138
+ const alias = node.field("alias")?.text();
139
+ if (original && alias) {
140
+ const aliases = file.aliases.get(original) ?? new Set();
141
+ aliases.add(alias);
142
+ file.aliases.set(original, aliases);
143
+ }
144
+ }
145
+ if (kind === "import_clause") {
146
+ const alias = node
147
+ .children()
148
+ .find((child) => child.kind() === "identifier")
149
+ ?.text();
150
+ if (alias) {
151
+ const aliases = file.aliases.get("default") ?? new Set();
152
+ aliases.add(alias);
153
+ file.aliases.set("default", aliases);
154
+ }
155
+ }
156
+ if ([
157
+ "assignment_expression",
158
+ "augmented_assignment",
159
+ "update_expression",
160
+ "assignment",
161
+ ].includes(kind)) {
162
+ const left = node.field("left") ?? node.field("argument");
163
+ const name = left?.text().split(/[.[]/, 1)[0];
164
+ if (name) {
165
+ const writes = file.writes.get(name) ?? new Set();
166
+ writes.add(node.id());
167
+ file.writes.set(name, writes);
168
+ }
169
+ }
170
+ if (["import_statement", "import_from_statement"].includes(kind))
171
+ file.imports.push(node);
172
+ if (kind === "identifier")
173
+ file.identifiers.push(node);
174
+ if (["call", "call_expression"].includes(kind)) {
175
+ file.calls.push(node);
176
+ const called = node.field("function")?.text();
177
+ if (called === "__import__")
178
+ file.dynamic = true;
179
+ if (["eval", "exec", "setattr", "Object.defineProperty"].includes(called ?? ""))
180
+ file.meta = true;
181
+ }
182
+ if (kind === "import" && node.parent()?.kind() === "call_expression")
183
+ file.dynamic = true;
184
+ if (callableKinds[kind] ||
185
+ ["program", "module", "statement_block", "block"].includes(kind)) {
186
+ file.parents.set(node.id(), owner);
187
+ owner = node;
188
+ }
189
+ if (owner)
190
+ file.scopes.set(node.id(), owner);
191
+ for (const child of node.children())
192
+ visit(child, owner, childDeclarator, badClass);
193
+ file.ranges.set(node.id(), { start, end: file.all.length });
194
+ };
195
+ visit(root);
196
+ return file;
197
+ }
198
+ function envelope(node) {
199
+ let current = node;
200
+ const parent = current.parent();
201
+ if (parent &&
202
+ ["variable_declarator", "assignment"].includes(String(parent.kind())) &&
203
+ (parent.field("value") ?? parent.field("right"))?.id() === current.id())
204
+ current = parent.parent() ?? parent;
205
+ if (["arrow_function", "function_expression"].includes(String(current.kind())))
206
+ current =
207
+ current
208
+ .ancestors()
209
+ .find((n) => ["lexical_declaration", "variable_declaration"].includes(String(n.kind()))) ?? current;
210
+ if (["export_statement", "decorated_definition"].includes(String(current.parent()?.kind())))
211
+ current = current.parent() ?? current;
212
+ return current;
213
+ }
214
+ function piece(file, node) {
215
+ const value = envelope(node);
216
+ const range = value.range();
217
+ return {
218
+ path: file.path,
219
+ start: range.start.line + 1,
220
+ end: range.end.line + (range.end.column === 0 ? 0 : 1),
221
+ text: value.text(),
222
+ };
223
+ }
224
+ function parameters(node) {
225
+ const params = node.field("parameters");
226
+ if (!params)
227
+ return [];
228
+ return params
229
+ .children()
230
+ .filter((n) => !["(", ")", ","].includes(n.text()))
231
+ .map((n) => n.field("pattern")?.text() ?? n.field("name")?.text() ?? n.text());
232
+ }
233
+ function functionName(node) {
234
+ return (node.field("name")?.text() ??
235
+ node
236
+ .ancestors()
237
+ .find((n) => n.kind() === "variable_declarator")
238
+ ?.field("name")
239
+ ?.text() ??
240
+ "");
241
+ }
242
+ function members(node, file) {
243
+ return nodes(node, file)
244
+ .filter((n) => ["member_expression", "attribute"].includes(String(n.kind())))
245
+ .map((n) => ({
246
+ receiver: (n.field("object") ?? n.field("value"))?.text() ?? "",
247
+ member: (n.field("property") ?? n.field("attribute"))?.text() ?? "",
248
+ }))
249
+ .filter((n) => n.receiver && n.member);
250
+ }
251
+ function resolvePath(path, specifier, files) {
252
+ const parts = path.split("/");
253
+ parts.pop();
254
+ let candidates;
255
+ if (specifier.startsWith(".")) {
256
+ const input = specifier.includes("/")
257
+ ? specifier
258
+ : specifier.replaceAll(".", "/");
259
+ for (const part of input.split("/")) {
260
+ if (!part || part === ".")
261
+ continue;
262
+ if (part === "..")
263
+ parts.pop();
264
+ else
265
+ parts.push(part);
266
+ }
267
+ candidates = [parts.join("/")];
268
+ }
269
+ else
270
+ candidates = [
271
+ specifier.replaceAll(".", "/"),
272
+ [...parts, specifier.replaceAll(".", "/")].join("/"),
273
+ ];
274
+ const sourceExtensions = {
275
+ ".js": ".ts",
276
+ ".jsx": ".tsx",
277
+ ".mjs": ".mts",
278
+ ".cjs": ".cts",
279
+ };
280
+ for (const candidate of [...candidates]) {
281
+ const extension = /\.[^./]+$/.exec(candidate)?.[0] ?? "";
282
+ const sourceExtension = sourceExtensions[extension];
283
+ if (sourceExtension)
284
+ candidates.push(candidate.slice(0, -extension.length) + sourceExtension);
285
+ }
286
+ const matches = files.filter((file) => candidates.some((base) => file.path === base ||
287
+ [
288
+ ".ts",
289
+ ".tsx",
290
+ ".js",
291
+ ".jsx",
292
+ ".mts",
293
+ ".cts",
294
+ ".mjs",
295
+ ".cjs",
296
+ ".py",
297
+ "/index.ts",
298
+ "/index.js",
299
+ "/__init__.py",
300
+ ].some((ext) => file.path === base + ext)));
301
+ return matches.length === 1 ? matches[0] : undefined;
302
+ }
303
+ function resolveIdentifier(file, name, at, files, seen = new Set()) {
304
+ const scope = file.scopes.get(at.id());
305
+ const key = `${name}:${scope?.id() ?? at.id()}`;
306
+ const cycleKey = `${file.path}:${key}`;
307
+ if (seen.has(cycleKey))
308
+ return;
309
+ if (file.memo.has(key))
310
+ return file.memo.get(key);
311
+ seen.add(cycleKey);
312
+ const resolved = resolveScopedIdentifier(file, name, scope, files, seen);
313
+ seen.delete(cycleKey);
314
+ file.memo.set(key, resolved);
315
+ return resolved;
316
+ }
317
+ function resolveScopedIdentifier(file, name, scope, files, seen) {
318
+ for (; scope; scope = file.parents.get(scope.id())) {
319
+ const candidates = file.declarations.get(scope.id())?.get(name) ?? [];
320
+ if (candidates.length > 1)
321
+ return;
322
+ const found = candidates[0];
323
+ if (found) {
324
+ if (["variable_declarator", "assignment"].includes(String(found.kind()))) {
325
+ const writes = file.writes.get(name);
326
+ if (writes && (writes.size > 1 || !writes.has(found.id())))
327
+ return;
328
+ }
329
+ const value = found.field("value") ?? found.field("right");
330
+ if (value &&
331
+ ["identifier", "member_expression", "attribute"].includes(String(value.kind())))
332
+ return resolveExpression(file, value, files, seen);
333
+ return { file, node: value ?? found };
334
+ }
335
+ if (callableKinds[String(scope.kind())] && parameters(scope).includes(name))
336
+ return;
337
+ }
338
+ for (const imp of file.imports) {
339
+ const source = imp.field("source") ?? imp.field("module_name");
340
+ if (!source)
341
+ continue;
342
+ const specifier = source.kind() === "string" ? source.text().slice(1, -1) : source.text();
343
+ let imported;
344
+ for (const binding of nodes(imp, file)) {
345
+ if (["import_specifier", "aliased_import"].includes(String(binding.kind()))) {
346
+ const original = binding.field("name");
347
+ const alias = binding.field("alias");
348
+ if ((alias ?? original)?.text() === name)
349
+ imported = original?.text();
350
+ }
351
+ if (binding.kind() === "import_clause" &&
352
+ binding
353
+ .children()
354
+ .some((n) => n.kind() === "identifier" && n.text() === name))
355
+ imported = "default";
356
+ if (imp.kind() === "import_from_statement" &&
357
+ binding.kind() === "dotted_name" &&
358
+ binding.id() !== source.id() &&
359
+ binding.text() === name)
360
+ imported = name;
361
+ }
362
+ if (!imported)
363
+ continue;
364
+ const other = resolvePath(file.path, specifier, files);
365
+ if (!other)
366
+ return;
367
+ if (imported === "default") {
368
+ const exp = other.all.find((n) => n.kind() === "export_statement" &&
369
+ n.children().some((c) => c.kind() === "default"));
370
+ const value = exp?.field("declaration") ??
371
+ exp?.field("value") ??
372
+ exp?.children().find((n) => callableKinds[String(n.kind())]);
373
+ return value ? { file: other, node: value } : undefined;
374
+ }
375
+ return resolveIdentifier(other, imported, other.root, files, seen);
376
+ }
377
+ return;
378
+ }
379
+ function resolveExpression(file, value, files, seen = new Set()) {
380
+ if (value.kind() === "identifier")
381
+ return resolveIdentifier(file, value.text(), value, files, seen);
382
+ if (!["member_expression", "attribute"].includes(String(value.kind())))
383
+ return;
384
+ const receiver = (value.field("object") ?? value.field("value"))?.text();
385
+ const member = (value.field("property") ?? value.field("attribute"))?.text();
386
+ if (!receiver || !member)
387
+ return;
388
+ for (const imp of file.imports.filter((n) => n.kind() === "import_statement")) {
389
+ const source = imp.field("source");
390
+ const namespace = nodes(imp, file).find((n) => n.kind() === "namespace_import" &&
391
+ n
392
+ .children()
393
+ .some((c) => c.kind() === "identifier" && c.text() === receiver));
394
+ const pythonAlias = nodes(imp, file).find((n) => n.kind() === "aliased_import" && n.field("alias")?.text() === receiver);
395
+ const pythonModule = nodes(imp, file).find((n) => n.kind() === "dotted_name" && n.text() === receiver);
396
+ const specifier = source
397
+ ? source.text().slice(1, -1)
398
+ : (pythonAlias?.field("name") ?? pythonModule)?.text();
399
+ if ((!namespace && !pythonAlias && !pythonModule) || !specifier)
400
+ continue;
401
+ const other = resolvePath(file.path, specifier, files);
402
+ if (other)
403
+ return resolveIdentifier(other, member, other.root, files, seen);
404
+ }
405
+ return;
406
+ }
407
+ function provider(file, value, files, seen = new Set()) {
408
+ const key = `${file.path}:${value.id()}`;
409
+ if (seen.has(key))
410
+ return;
411
+ seen.add(key);
412
+ if ([
413
+ "object",
414
+ "dictionary",
415
+ "class_declaration",
416
+ "class_definition",
417
+ "list",
418
+ "tuple",
419
+ ].includes(String(value.kind()))) {
420
+ if (nodes(value, file).some((n) => ([
421
+ "spread_element",
422
+ "dictionary_splat",
423
+ "class_heritage",
424
+ "argument_list",
425
+ ].includes(String(n.kind())) &&
426
+ n.parent()?.id() === value.id()) ||
427
+ n.field("name")?.text() === "__getattr__" ||
428
+ n.field("name")?.text() === "__getattribute__"))
429
+ return;
430
+ return { file, node: value };
431
+ }
432
+ if (value.kind() === "identifier") {
433
+ const found = resolveIdentifier(file, value.text(), value, files);
434
+ return found ? provider(found.file, found.node, files, seen) : undefined;
435
+ }
436
+ if (["new_expression", "call", "call_expression"].includes(String(value.kind()))) {
437
+ const fn = value.field("constructor") ?? value.field("function");
438
+ if (!fn)
439
+ return;
440
+ const found = resolveExpression(file, fn, files);
441
+ if (!found)
442
+ return;
443
+ if (["class_declaration", "class_definition"].includes(String(found.node.kind())))
444
+ return provider(found.file, found.node, files, seen);
445
+ if (callableKinds[String(found.node.kind())]) {
446
+ const returns = nodes(found.node, found.file).filter((n) => n.kind() === "return_statement");
447
+ if (returns.length !== 1)
448
+ return;
449
+ const returned = returns[0]
450
+ ?.children()
451
+ .find((n) => n.kind() !== "return");
452
+ if (!returned)
453
+ return;
454
+ const supplied = provider(found.file, returned, files, seen);
455
+ return supplied ? { file: found.file, node: found.node } : undefined;
456
+ }
457
+ }
458
+ return;
459
+ }
460
+ export function prepareRiskCallers(units, sources, parser, budgets = {}) {
461
+ const result = { proofs: [], limits: [] };
462
+ const parsed = {
463
+ before: [],
464
+ after: [],
465
+ };
466
+ const failures = [];
467
+ if (parser)
468
+ for (const source of sources) {
469
+ if (source.limitation)
470
+ failures.push({
471
+ path: source.path,
472
+ kind: "unreadable",
473
+ missing: source.limitation,
474
+ });
475
+ let beforeFile;
476
+ for (const side of ["before", "after"]) {
477
+ const text = source[side];
478
+ if (text === null)
479
+ continue;
480
+ const path = side === "before" ? (source.beforePath ?? source.path) : source.path;
481
+ if (side === "after" &&
482
+ beforeFile?.path === path &&
483
+ source.before === text) {
484
+ parsed.after.push({ ...beforeFile, memo: new Map() });
485
+ for (const failure of failures.filter((failure) => failure.path === path &&
486
+ failure.kind === "parse_partial" &&
487
+ failure.missing.startsWith("before ")))
488
+ failures.push({
489
+ ...failure,
490
+ missing: failure.missing.replace(/^before /, "after "),
491
+ });
492
+ continue;
493
+ }
494
+ if (!parser.supports(path)) {
495
+ failures.push({
496
+ path,
497
+ kind: "parser_absent",
498
+ missing: `${side} grammar`,
499
+ });
500
+ continue;
501
+ }
502
+ const root = parser.parse(path, text);
503
+ if (root) {
504
+ const errors = root.findAll({ rule: { kind: "ERROR" } });
505
+ if (errors.length) {
506
+ const lines = [
507
+ ...new Set(errors.map((error) => error.range().start.line + 1)),
508
+ ];
509
+ failures.push({
510
+ path,
511
+ kind: "parse_partial",
512
+ missing: `${side} syntax at lines ${lines.slice(0, 3).join(", ")}${lines.length > 3 ? "…" : ""} (${errors.length} errors)`,
513
+ });
514
+ }
515
+ const file = indexFile(path, root);
516
+ parsed[side].push(file);
517
+ if (side === "before")
518
+ beforeFile = file;
519
+ }
520
+ else
521
+ failures.push({ path, kind: "parse_failed", missing: `${side} AST` });
522
+ }
523
+ }
524
+ for (const unit of units) {
525
+ if (!/\.(?:[cm]?[jt]sx?|py)$/.test(unit.file) ||
526
+ unit.before === null ||
527
+ unit.after === null)
528
+ continue;
529
+ const limit = (kind, paths, missing) => result.limits.push({
530
+ unitId: unit.id,
531
+ kind,
532
+ paths,
533
+ missing,
534
+ reason: `${kind}: ${missing}`,
535
+ });
536
+ if (!parser?.supports(unit.file)) {
537
+ limit("parser_absent", [unit.file], "member-access AST");
538
+ continue;
539
+ }
540
+ const targetFile = parsed.before.find((f) => f.path ===
541
+ (sources.find((s) => s.path === unit.file)?.beforePath ?? unit.file));
542
+ const afterTargetFile = parsed.after.find((f) => f.path === unit.file);
543
+ const name = unit.name.split(".").at(-1);
544
+ const syntaxCandidates = (file, range) => (unit.kind === "hunk"
545
+ ? file?.syntax
546
+ : file?.syntaxByName.get(name ?? ""))?.filter((entry) => !range || (entry.start <= range.end && entry.end >= range.start)) ?? [];
547
+ const beforeCandidates = syntaxCandidates(targetFile, unit.beforeRange);
548
+ const afterCandidates = syntaxCandidates(afterTargetFile, unit.afterRange);
549
+ const intervalInvalid = (file, range) => !!range &&
550
+ !!file?.errors.some((error) => error.start <= range.end && error.end >= range.start);
551
+ if ([...beforeCandidates, ...afterCandidates].some((entry) => entry.invalid) ||
552
+ intervalInvalid(targetFile, unit.beforeRange) ||
553
+ intervalInvalid(afterTargetFile, unit.afterRange)) {
554
+ limit("parse_partial", [unit.file], "enclosing changed declaration AST");
555
+ continue;
556
+ }
557
+ const locate = (entries) => (entries.find((entry) => entry.callable) ??
558
+ entries.find((entry) => !["class_declaration", "class_definition"].includes(String(entry.node.kind()))))?.node;
559
+ const target = unit.kind === "hunk" ? undefined : locate(beforeCandidates);
560
+ const afterTarget = unit.kind === "hunk" ? undefined : locate(afterCandidates);
561
+ const oldRoot = target ??
562
+ (unit.kind === "hunk" ? targetFile?.root : undefined) ??
563
+ parser.parse(unit.file, unit.before);
564
+ const newRoot = afterTarget ??
565
+ (unit.kind === "hunk" ? afterTargetFile?.root : undefined) ??
566
+ parser.parse(unit.file, unit.after);
567
+ if (!oldRoot || !newRoot) {
568
+ limit("parse_failed", [unit.file], "complete changed declaration AST");
569
+ continue;
570
+ }
571
+ const inRange = (node, range) => {
572
+ const span = node.range();
573
+ return (!range ||
574
+ (span.start.line + 1 <= range.end &&
575
+ span.end.line + (span.end.column === 0 ? 0 : 1) >= range.start));
576
+ };
577
+ const errors = [
578
+ ...oldRoot
579
+ .findAll({ rule: { kind: "ERROR" } })
580
+ .filter((node) => target ||
581
+ oldRoot !== targetFile?.root ||
582
+ inRange(node, unit.beforeRange)),
583
+ ...newRoot
584
+ .findAll({ rule: { kind: "ERROR" } })
585
+ .filter((node) => afterTarget ||
586
+ newRoot !== afterTargetFile?.root ||
587
+ inRange(node, unit.afterRange)),
588
+ ];
589
+ if (errors.length) {
590
+ limit("parse_partial", [unit.file], `changed source interval (${errors.length} errors)`);
591
+ continue;
592
+ }
593
+ const intervalMembers = (root, file, range) => nodes(root, file)
594
+ .filter((node) => ["member_expression", "attribute"].includes(String(node.kind())) &&
595
+ (!file || unit.kind !== "hunk" || inRange(node, range)))
596
+ .flatMap((node) => members(node, file).slice(0, 1));
597
+ const beforeMembers = intervalMembers(oldRoot, target || oldRoot === targetFile?.root ? targetFile : undefined, unit.beforeRange);
598
+ const afterMembers = intervalMembers(newRoot, afterTarget || newRoot === afterTargetFile?.root
599
+ ? afterTargetFile
600
+ : undefined, unit.afterRange);
601
+ const replaced = beforeMembers.filter((old) => !afterMembers.some((next) => next.receiver === old.receiver && next.member === old.member) &&
602
+ afterMembers.some((next) => next.receiver === old.receiver &&
603
+ next.member !== old.member &&
604
+ !beforeMembers.some((previous) => previous.receiver === next.receiver &&
605
+ previous.member === next.member)));
606
+ if (!replaced.length)
607
+ continue;
608
+ for (const failure of failures)
609
+ limit(failure.kind, [failure.path], failure.missing);
610
+ if (["slice", "hunk"].includes(unit.kind)) {
611
+ limit("piece_cut", [unit.file], "complete changed declaration");
612
+ continue;
613
+ }
614
+ const combinations = [];
615
+ const spans = [];
616
+ let extra = 0;
617
+ // The declaration identity is resolved in the complete repository source above.
618
+ if (!targetFile || !target) {
619
+ limit("binding_unknown", [unit.file], "target declaration");
620
+ continue;
621
+ }
622
+ const receiverIndices = replaced.map((m) => parameters(target).indexOf(m.receiver));
623
+ // A receiver can be passed by a caller or concretely bound inside the changed declaration.
624
+ let candidates = 0;
625
+ const targetNames = new Set([functionName(target), "default"]);
626
+ for (const name of targetNames)
627
+ for (const side of [parsed.before, parsed.after])
628
+ for (const file of side)
629
+ for (const alias of file.aliases.get(name) ?? [])
630
+ targetNames.add(alias);
631
+ for (const file of parsed.before)
632
+ for (const reference of file.identifiers) {
633
+ if (!targetNames.has(reference.text()))
634
+ continue;
635
+ const parent = reference.parent();
636
+ if (!parent ||
637
+ !["arguments", "argument_list"].includes(String(parent.kind())))
638
+ continue;
639
+ const bound = resolveExpression(file, reference, parsed.before);
640
+ if (bound?.file.path === targetFile.path &&
641
+ bound.node.id() === target.id())
642
+ limit("binding_unknown", [file.path], `callback ${reference.text()} at line ${reference.range().start.line + 1}: concrete callback arguments`);
643
+ }
644
+ for (const file of parsed.before)
645
+ for (const call of file.calls) {
646
+ const fn = call.field("function");
647
+ if (!fn)
648
+ continue;
649
+ if (!targetNames.has((fn.field("property") ?? fn.field("attribute") ?? fn).text()))
650
+ continue;
651
+ const binding = resolveExpression(file, fn, parsed.before);
652
+ if (!binding ||
653
+ binding.file.path !== targetFile.path ||
654
+ binding.node.id() !== target.id()) {
655
+ if (fn.text() === functionName(target) && !binding)
656
+ limit("binding_unknown", [file.path], `candidate ${fn.text()} at line ${call.range().start.line + 1}`);
657
+ continue;
658
+ }
659
+ candidates++;
660
+ if (file.dynamic) {
661
+ limit("dynamic_import", [file.path], "candidate caller dynamic import target");
662
+ continue;
663
+ }
664
+ if (file.meta) {
665
+ limit("metaprogramming", [file.path], "candidate caller computed definitions");
666
+ continue;
667
+ }
668
+ const caller = call.ancestors().find((n) => callableKinds[String(n.kind())]) ??
669
+ call
670
+ .ancestors()
671
+ .find((n) => ["lexical_declaration", "expression_statement"].includes(String(n.kind()))) ??
672
+ call;
673
+ const callerPiece = piece(file, caller);
674
+ const afterPath = sources.find((s) => (s.beforePath ?? s.path) === file.path)?.path ??
675
+ file.path;
676
+ const afterFile = parsed.after.find((f) => f.path === afterPath);
677
+ const callerName = functionName(caller);
678
+ const afterCaller = afterFile?.all.find((n) => callerName
679
+ ? callableKinds[String(n.kind())] && functionName(n) === callerName
680
+ : n.kind() === caller.kind() &&
681
+ n.range().start.line === caller.range().start.line);
682
+ if (!afterFile || !afterCaller) {
683
+ limit("caller_missing", [file.path, afterPath], "complete after caller");
684
+ continue;
685
+ }
686
+ if (afterFile.dynamic) {
687
+ limit("dynamic_import", [afterPath], "after candidate caller dynamic import target");
688
+ continue;
689
+ }
690
+ if (afterFile.meta) {
691
+ limit("metaprogramming", [afterPath], "after candidate caller computed definitions");
692
+ continue;
693
+ }
694
+ const afterTargetPath = unit.file;
695
+ const callsAfter = nodes(afterCaller, afterFile).filter((n) => {
696
+ if (!["call", "call_expression"].includes(String(n.kind())))
697
+ return false;
698
+ const called = n.field("function");
699
+ if (!called)
700
+ return false;
701
+ if (!targetNames.has((called.field("property") ??
702
+ called.field("attribute") ??
703
+ called).text()))
704
+ return false;
705
+ const bound = resolveExpression(afterFile, called, parsed.after);
706
+ return (bound?.file.path === afterTargetPath &&
707
+ functionName(bound.node) === unit.name.split(".").at(-1));
708
+ });
709
+ const callAfter = callsAfter.length === 1 ? callsAfter[0] : undefined;
710
+ if (!callAfter) {
711
+ limit("binding_unknown", [afterPath], "unique coordinated after call");
712
+ continue;
713
+ }
714
+ const args = call
715
+ .field("arguments")
716
+ ?.children()
717
+ .filter((n) => !["(", ")", ","].includes(n.text())) ?? [];
718
+ const argsAfter = callAfter
719
+ .field("arguments")
720
+ ?.children()
721
+ .filter((n) => !["(", ")", ","].includes(n.text())) ?? [];
722
+ const providersBefore = [];
723
+ const providersAfter = [];
724
+ let valid = true;
725
+ for (let position = 0; position < receiverIndices.length; position++) {
726
+ const index = receiverIndices[position] ?? -1;
727
+ const receiver = replaced[position]?.receiver ?? "";
728
+ const localOldNode = index < 0
729
+ ? nodes(target, targetFile).find((n) => ["variable_declarator", "assignment"].includes(String(n.kind())) &&
730
+ (n.field("name") ?? n.field("left"))?.text() === receiver)
731
+ : undefined;
732
+ const localNewNode = index < 0 && afterTarget
733
+ ? nodes(afterTarget, afterTargetFile).find((n) => ["variable_declarator", "assignment"].includes(String(n.kind())) &&
734
+ (n.field("name") ?? n.field("left"))?.text() === receiver)
735
+ : undefined;
736
+ const oldArg = index < 0
737
+ ? (localOldNode?.field("value") ?? localOldNode?.field("right"))
738
+ : args[index];
739
+ const newArg = index < 0
740
+ ? (localNewNode?.field("value") ?? localNewNode?.field("right"))
741
+ : argsAfter[index];
742
+ const oldProvider = oldArg &&
743
+ provider(index < 0 ? targetFile : file, oldArg, parsed.before);
744
+ const newProvider = newArg &&
745
+ provider(index < 0 ? (afterTargetFile ?? afterFile) : afterFile, newArg, parsed.after);
746
+ if (!oldProvider || !newProvider) {
747
+ limit("provider_missing", [file.path, afterPath], `concrete before/after provider for ${index < 0 ? receiver : `argument ${index + 1}`}`);
748
+ valid = false;
749
+ break;
750
+ }
751
+ for (const supplied of [oldProvider, newProvider]) {
752
+ if (supplied.file.dynamic) {
753
+ limit("dynamic_import", [supplied.file.path], "bound provider dynamic import target");
754
+ valid = false;
755
+ }
756
+ if (supplied.file.meta) {
757
+ limit("metaprogramming", [supplied.file.path], "bound provider computed definitions");
758
+ valid = false;
759
+ }
760
+ }
761
+ if (!valid)
762
+ break;
763
+ providersBefore.push(piece(oldProvider.file, index < 0 ? target : oldProvider.node));
764
+ providersAfter.push(piece(newProvider.file, index < 0 ? (afterTarget ?? newProvider.node) : newProvider.node));
765
+ }
766
+ if (!valid)
767
+ continue;
768
+ const bindingPieces = (source, declaration) => {
769
+ const collected = source.imports.map((n) => piece(source, n));
770
+ for (const reference of nodes(declaration, source).filter((n) => n.kind() === "identifier")) {
771
+ const located = resolveIdentifier(source, reference.text(), reference, source === file ? parsed.before : parsed.after);
772
+ if (!located ||
773
+ located.node.id() === target.id() ||
774
+ callableKinds[String(located.node.kind())])
775
+ continue;
776
+ const value = piece(located.file, located.node);
777
+ if (!collected.some((p) => p.path === value.path &&
778
+ p.start === value.start &&
779
+ p.end === value.end))
780
+ collected.push(value);
781
+ }
782
+ return collected;
783
+ };
784
+ const contextBefore = bindingPieces(file, caller);
785
+ const contextAfter = bindingPieces(afterFile, afterCaller);
786
+ const pieces = [
787
+ callerPiece,
788
+ piece(afterFile, afterCaller),
789
+ ...providersBefore,
790
+ ...providersAfter,
791
+ ...contextBefore,
792
+ ...contextAfter,
793
+ ];
794
+ const oversized = pieces.find((p) => p.text.length > (budgets.piece ?? UNIT_MAX_CHARS));
795
+ if (oversized) {
796
+ limit("piece_cut", [oversized.path], `${oversized.start}-${oversized.end} complete piece`);
797
+ continue;
798
+ }
799
+ const size = pieces.reduce((sum, p) => sum + p.text.length, 0);
800
+ if (extra + size > (budgets.closure ?? CLOSURE_MAX_CHARS)) {
801
+ limit("budget", pieces.map((p) => p.path), "caller/provider closure budget");
802
+ continue;
803
+ }
804
+ extra += size;
805
+ spans.push(...pieces.map(({ path, start, end }) => ({ path, start, end })));
806
+ combinations.push({
807
+ caller: {
808
+ before: { ...callerPiece },
809
+ after: { ...piece(afterFile, afterCaller) },
810
+ bindings: {
811
+ before: contextBefore.map((p) => ({ ...p })),
812
+ after: contextAfter.map((p) => ({ ...p })),
813
+ },
814
+ },
815
+ provider: {
816
+ before: providersBefore.map((p) => ({ ...p })),
817
+ after: providersAfter.map((p) => ({ ...p })),
818
+ },
819
+ });
820
+ }
821
+ if (!candidates)
822
+ limit("caller_missing", [unit.file], "statically bound local caller");
823
+ if (!combinations.length)
824
+ continue;
825
+ const state = {
826
+ changedUnits: [{ ...unit }],
827
+ callerEvidence: {
828
+ source: "static code only; not executed",
829
+ caller: {
830
+ before: combinations.map((value) => value.caller.before),
831
+ after: combinations.map((value) => value.caller.after),
832
+ bindings: combinations.map((value) => value.caller.bindings),
833
+ },
834
+ provider: {
835
+ before: combinations.map((value) => value.provider.before),
836
+ after: combinations.map((value) => value.provider.after),
837
+ },
838
+ },
839
+ };
840
+ if (JSON.stringify(state).length > (budgets.state ?? STATE_MAX_CHARS)) {
841
+ limit("budget", spans.map((p) => p.path), "serialized caller evidence state");
842
+ continue;
843
+ }
844
+ result.proofs.push({ unitId: unit.id, state, question, paths: spans });
845
+ }
846
+ result.limits = result.limits.filter((value, index, all) => all.findIndex((other) => other.unitId === value.unitId &&
847
+ other.kind === value.kind &&
848
+ other.missing === value.missing &&
849
+ other.paths.join("\0") === value.paths.join("\0")) === index);
850
+ return result;
851
+ }