jev-agent-tools 0.1.4 → 0.3.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 (180) hide show
  1. package/CHANGELOG.md +106 -1
  2. package/CONTRIBUTING.md +43 -0
  3. package/README.md +58 -17
  4. package/SECURITY.md +43 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +198 -0
  7. package/dist/adapters/ask-proof.js +200 -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 +234 -0
  11. package/dist/adapters/docs.js +192 -0
  12. package/dist/adapters/evidence-context.js +119 -0
  13. package/dist/adapters/exec.js +207 -0
  14. package/dist/adapters/files.js +418 -0
  15. package/dist/adapters/find.js +150 -0
  16. package/dist/adapters/git-base.js +32 -0
  17. package/dist/adapters/git-inventory.js +71 -0
  18. package/dist/adapters/git.js +483 -0
  19. package/dist/adapters/locate-file.js +197 -0
  20. package/dist/adapters/output-lines.js +46 -0
  21. package/dist/adapters/private-storage.js +106 -0
  22. package/dist/adapters/risk-callers.js +429 -0
  23. package/dist/adapters/runner-version.js +78 -0
  24. package/dist/adapters/shell.js +92 -0
  25. package/dist/adapters/syntax.js +187 -0
  26. package/dist/adapters/test-inventory.js +139 -0
  27. package/dist/adapters/usage.js +20 -0
  28. package/dist/adapters/utf8.js +47 -0
  29. package/dist/configuration.js +267 -0
  30. package/dist/constants.js +140 -0
  31. package/dist/core/ask-closure.js +282 -0
  32. package/dist/core/ask-proof.js +1 -0
  33. package/dist/core/ask-references.js +278 -0
  34. package/dist/core/asks.js +507 -0
  35. package/dist/core/batches.js +65 -0
  36. package/dist/core/command-output.js +224 -0
  37. package/dist/core/diff.js +178 -0
  38. package/dist/core/docs.js +302 -0
  39. package/dist/core/find.js +108 -0
  40. package/dist/core/git.js +1 -0
  41. package/dist/core/imports.js +550 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/lexical.js +132 -0
  44. package/dist/core/locate.js +169 -0
  45. package/dist/core/output.js +137 -0
  46. package/dist/core/pointer.js +29 -0
  47. package/dist/core/result-report.js +302 -0
  48. package/dist/core/risk-callers.js +851 -0
  49. package/dist/core/runner-version.js +45 -0
  50. package/dist/core/secret-path.js +34 -0
  51. package/dist/core/sections.js +230 -0
  52. package/dist/core/state.js +51 -0
  53. package/dist/core/syntax.js +1 -0
  54. package/dist/core/test-commands.js +334 -0
  55. package/dist/core/test-coverage.js +74 -0
  56. package/dist/core/test-discovery.js +1382 -0
  57. package/dist/core/test-evidence.js +527 -0
  58. package/dist/core/test-state.js +81 -0
  59. package/dist/core/truncate.js +12 -0
  60. package/dist/core/units.js +349 -0
  61. package/dist/describe.js +23 -0
  62. package/dist/guide.js +33 -0
  63. package/dist/host.js +24 -0
  64. package/dist/jev/client.js +456 -0
  65. package/dist/jev/pool.js +54 -0
  66. package/dist/jev/types.js +1 -0
  67. package/dist/mcp/main.js +124 -0
  68. package/dist/mcp/protocol.js +210 -0
  69. package/dist/mcp/tools.js +129 -0
  70. package/dist/presets/docs.js +62 -0
  71. package/dist/presets/risk.js +179 -0
  72. package/dist/presets/spec.js +81 -0
  73. package/dist/presets/witnesses.js +249 -0
  74. package/dist/render.js +114 -0
  75. package/dist/report-schema.js +1356 -0
  76. package/dist/result-types.js +1 -0
  77. package/dist/result.js +3 -0
  78. package/dist/runtime.js +1 -0
  79. package/dist/session.js +147 -0
  80. package/dist/texts/ask-files.js +3 -0
  81. package/dist/texts/ask.js +4 -0
  82. package/dist/texts/check-diff.js +20 -0
  83. package/dist/texts/configuration.js +1 -0
  84. package/dist/texts/find.js +19 -0
  85. package/dist/texts/guide.js +3 -0
  86. package/dist/texts/instructions.js +72 -0
  87. package/dist/texts/locate.js +15 -0
  88. package/dist/texts/select-tests.js +4 -0
  89. package/dist/tools/ask-files.js +450 -0
  90. package/dist/tools/ask-schema.js +70 -0
  91. package/dist/tools/ask.js +1147 -0
  92. package/dist/tools/check-diff.js +594 -0
  93. package/dist/tools/docs-check.js +408 -0
  94. package/dist/tools/find.js +682 -0
  95. package/dist/tools/locate.js +602 -0
  96. package/dist/tools/review-report.js +230 -0
  97. package/dist/tools/select-tests.js +821 -0
  98. package/dist/tools/spec-check.js +263 -0
  99. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  100. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  101. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  102. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  103. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  104. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  105. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  106. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  107. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  108. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  109. package/docs/agent-instructions.md +120 -0
  110. package/docs/design.md +16 -4
  111. package/docs/mcp.md +233 -0
  112. package/docs/tools/jev_ask.md +8 -5
  113. package/docs/tools/jev_ask_files.md +2 -1
  114. package/docs/tools/jev_check_diff.md +4 -1
  115. package/docs/tools/jev_find_files.md +2 -1
  116. package/docs/tools/jev_locate_in_file.md +5 -0
  117. package/docs/tools/jev_select_tests.md +4 -1
  118. package/package.json +19 -4
  119. package/rules/jev-ask.md +22 -1
  120. package/server.json +57 -0
  121. package/src/adapters/ask-files.ts +11 -3
  122. package/src/adapters/ask-proof.ts +69 -11
  123. package/src/adapters/canonical-path.ts +18 -0
  124. package/src/adapters/command.ts +102 -36
  125. package/src/adapters/docs.ts +33 -14
  126. package/src/adapters/evidence-context.ts +169 -0
  127. package/src/adapters/exec.ts +226 -0
  128. package/src/adapters/files.ts +146 -16
  129. package/src/adapters/find.ts +37 -7
  130. package/src/adapters/git-base.ts +7 -1
  131. package/src/adapters/git.ts +61 -8
  132. package/src/adapters/locate-file.ts +51 -9
  133. package/src/adapters/private-storage.ts +155 -0
  134. package/src/adapters/risk-callers.ts +7 -2
  135. package/src/adapters/shell.ts +113 -0
  136. package/src/adapters/test-inventory.ts +12 -4
  137. package/src/configuration.ts +55 -14
  138. package/src/constants.ts +37 -5
  139. package/src/core/ask-references.ts +262 -146
  140. package/src/core/asks.ts +79 -7
  141. package/src/core/command-output.ts +17 -1
  142. package/src/core/import-boundaries.ts +8 -3
  143. package/src/core/locate.ts +8 -5
  144. package/src/core/output.ts +34 -0
  145. package/src/core/result-report.ts +410 -0
  146. package/src/core/secret-path.ts +37 -0
  147. package/src/core/state.ts +8 -1
  148. package/src/core/units.ts +3 -2
  149. package/src/host.ts +11 -0
  150. package/src/index.ts +3 -0
  151. package/src/jev/client.ts +66 -16
  152. package/src/jev/types.ts +24 -3
  153. package/src/mcp/main.ts +135 -0
  154. package/src/mcp/protocol.ts +332 -0
  155. package/src/mcp/tools.ts +179 -0
  156. package/src/render.ts +109 -0
  157. package/src/report-schema.ts +1380 -0
  158. package/src/result-types.ts +234 -0
  159. package/src/result.ts +4 -1
  160. package/src/runtime.ts +6 -0
  161. package/src/session.ts +59 -0
  162. package/src/setup.ts +13 -5
  163. package/src/texts/ask-files.ts +4 -1
  164. package/src/texts/ask.ts +8 -1
  165. package/src/texts/check-diff.ts +7 -4
  166. package/src/texts/find.ts +8 -2
  167. package/src/texts/guide.ts +8 -16
  168. package/src/texts/instructions.ts +98 -0
  169. package/src/texts/locate.ts +8 -2
  170. package/src/texts/run-end.ts +2 -2
  171. package/src/texts/select-tests.ts +4 -1
  172. package/src/tools/ask-files.ts +311 -18
  173. package/src/tools/ask.ts +722 -95
  174. package/src/tools/check-diff.ts +337 -31
  175. package/src/tools/docs-check.ts +241 -38
  176. package/src/tools/find.ts +389 -29
  177. package/src/tools/locate.ts +387 -25
  178. package/src/tools/review-report.ts +308 -0
  179. package/src/tools/select-tests.ts +484 -23
  180. package/src/tools/spec-check.ts +194 -19
@@ -0,0 +1,527 @@
1
+ import { posix } from "node:path";
2
+ import { importClosure, } from "./imports.js";
3
+ function references(node) {
4
+ const found = [];
5
+ const memberKind = node.kind() === "module" ||
6
+ node.ancestors().some((ancestor) => ancestor.kind() === "module")
7
+ ? "attribute"
8
+ : "member_expression";
9
+ for (const part of node.findAll({
10
+ rule: { any: [{ kind: "identifier" }, { kind: memberKind }] },
11
+ })) {
12
+ const kind = String(part.kind());
13
+ if (/^(member_expression|attribute)$/.test(kind)) {
14
+ const object = part.field("object") ?? part.children()[0];
15
+ const property = part.field("property") ?? part.children().at(-1);
16
+ if (object?.kind() === "identifier" &&
17
+ property &&
18
+ /identifier$/.test(String(property.kind())))
19
+ found.push({ name: object.text(), member: property.text() });
20
+ }
21
+ else {
22
+ const parent = part.parent();
23
+ if (parent &&
24
+ /^(import|export).*|^(member_expression|attribute)$/.test(String(parent.kind())))
25
+ continue;
26
+ found.push({ name: part.text() });
27
+ }
28
+ }
29
+ return found;
30
+ }
31
+ /** Builds one lazy, cached declaration index for an inventory. Unknown chains remain judgeable. */
32
+ export function prepareTestEvidence(files, graph, units, parser) {
33
+ const sources = new Map(files.map((file) => [file.path, file]));
34
+ const modules = new Map();
35
+ const dependencyCache = new Map();
36
+ const resolutionCache = new Map();
37
+ const closureCache = new Map();
38
+ const rootCache = new Map();
39
+ const declarationParents = new Map();
40
+ const declarationHits = new Map();
41
+ const reachesUnit = new Set();
42
+ const indexed = new Set();
43
+ const propagationQueue = [];
44
+ let propagationOffset = 0;
45
+ const packageDirectories = new Map();
46
+ for (const file of files) {
47
+ if (posix.basename(file.path) !== "package.json")
48
+ continue;
49
+ try {
50
+ const manifest = JSON.parse(file.text);
51
+ if (typeof manifest.name === "string") {
52
+ const directories = packageDirectories.get(manifest.name) ?? [];
53
+ directories.push(posix.dirname(file.path));
54
+ packageDirectories.set(manifest.name, directories);
55
+ }
56
+ }
57
+ catch {
58
+ // Invalid manifests cannot establish package identity.
59
+ }
60
+ }
61
+ const reachesDiff = new Set(units.map((unit) => unit.file));
62
+ const importers = new Map();
63
+ for (const [path, dependencies] of graph.edges)
64
+ for (const dependency of dependencies) {
65
+ const parents = importers.get(dependency) ?? [];
66
+ parents.push(path);
67
+ importers.set(dependency, parents);
68
+ }
69
+ const pending = [...reachesDiff];
70
+ for (const dependency of pending)
71
+ for (const path of importers.get(dependency) ?? [])
72
+ if (!reachesDiff.has(path)) {
73
+ reachesDiff.add(path);
74
+ pending.push(path);
75
+ }
76
+ const target = (path, specifier, limits) => {
77
+ const edges = graph.edges.get(path) ?? [];
78
+ let matches;
79
+ if (specifier.startsWith(".") || path.endsWith(".py")) {
80
+ let base;
81
+ if (path.endsWith(".py")) {
82
+ const dots = specifier.match(/^\.+/)?.[0].length ?? 0;
83
+ let dir = dots ? posix.dirname(path) : ".";
84
+ for (let i = 1; i < dots; i++)
85
+ dir = posix.dirname(dir);
86
+ base = posix.join(dir, specifier.slice(dots).replaceAll(".", "/"));
87
+ }
88
+ else
89
+ base = posix.normalize(posix.join(posix.dirname(path), specifier));
90
+ const stem = base.replace(/\.[cm]?[jt]sx?$/, "");
91
+ matches = edges.filter((edge) => edge === base ||
92
+ edge.replace(/\.[cm]?[jt]sx?$/, "") === stem ||
93
+ edge === `${base}.py` ||
94
+ edge === `${base}/__init__.py` ||
95
+ (/^index\.[jt]s$/.test(posix.basename(edge)) &&
96
+ posix.dirname(edge) === base));
97
+ }
98
+ else {
99
+ // Only claim a package target when its manifest identifies the graph edge.
100
+ const directories = packageDirectories.get(specifier) ?? [];
101
+ matches = edges.filter((edge) => directories.some((directory) => !posix.relative(directory, edge).startsWith("../")));
102
+ }
103
+ if (matches.length === 1)
104
+ return matches[0];
105
+ if (matches.length > 1 || (specifier.startsWith(".") && edges.length))
106
+ limits.push({
107
+ path,
108
+ kind: matches.length > 1 ? "ambiguous" : "unsupported",
109
+ specifier,
110
+ });
111
+ return undefined;
112
+ };
113
+ const getModule = (path) => {
114
+ const cached = modules.get(path);
115
+ if (cached)
116
+ return cached;
117
+ const result = {
118
+ definitions: [],
119
+ bindings: new Map(),
120
+ exports: new Map(),
121
+ stars: [],
122
+ references: [],
123
+ limits: [],
124
+ };
125
+ modules.set(path, result);
126
+ const file = sources.get(path);
127
+ if (!file) {
128
+ result.limits.push({ path, kind: "unresolved" });
129
+ return result;
130
+ }
131
+ if (!parser?.supports(path)) {
132
+ result.limits.push({ path, kind: "grammar_absent" });
133
+ return result;
134
+ }
135
+ const parsed = parser.parseRaw(path, file.text);
136
+ if (!parsed.ok) {
137
+ result.limits.push({ path, kind: "parse_failed" });
138
+ return result;
139
+ }
140
+ // Declaration extraction reuses this AST rather than invoking the grammar a second time.
141
+ const cachedParser = Object.create(parser);
142
+ cachedParser.parseRaw = () => parsed;
143
+ const declarations = /(?:[._-]test|[._-]spec)\.[cm]?[jt]sx?$|(?:^|\/)test[^/]*\.py$/.test(path)
144
+ ? {
145
+ declarations: [],
146
+ limits: parsed.root.find({ rule: { kind: "ERROR" } })
147
+ ? [{ kind: "parse_partial" }]
148
+ : [],
149
+ }
150
+ : cachedParser.declarations(path, file.text, {
151
+ descendAbove: Infinity,
152
+ });
153
+ result.limits.push(...declarations.limits.map((limit) => ({ ...limit, path })));
154
+ if (declarations.limits.some((limit) => limit.kind === "parse_failed")) {
155
+ return result;
156
+ }
157
+ const lines = file.text.split("\n");
158
+ const children = parsed.root.children().map((node) => ({
159
+ node,
160
+ start: node.range().start.line + 1,
161
+ end: node.range().end.line + (node.range().end.column ? 1 : 0),
162
+ refs: references(node),
163
+ }));
164
+ for (const declaration of declarations.declarations) {
165
+ const refs = [];
166
+ for (const child of children) {
167
+ if (child.start >= declaration.start && child.end <= declaration.end)
168
+ refs.push(...child.refs);
169
+ }
170
+ result.definitions.push({
171
+ file: path,
172
+ declaration,
173
+ text: lines.slice(declaration.start - 1, declaration.end).join("\n"),
174
+ references: refs,
175
+ });
176
+ if (declaration.exported || path.endsWith(".py"))
177
+ result.exports.set(declaration.name, [
178
+ { target: path, name: declaration.name },
179
+ ]);
180
+ }
181
+ const addExport = (name, binding) => {
182
+ result.exports.set(name, [...(result.exports.get(name) ?? []), binding]);
183
+ };
184
+ for (const { node, refs } of children) {
185
+ const kind = String(node.kind());
186
+ if (kind === "import_statement" && !path.endsWith(".py")) {
187
+ const string = node
188
+ .children()
189
+ .find((child) => child.kind() === "string");
190
+ if (!string)
191
+ continue;
192
+ const specifier = string.text().slice(1, -1);
193
+ const destination = target(path, specifier, result.limits);
194
+ if (!destination)
195
+ continue;
196
+ const clause = node
197
+ .children()
198
+ .find((child) => child.kind() === "import_clause");
199
+ if (!clause)
200
+ continue;
201
+ for (const child of clause.children()) {
202
+ if (child.kind() === "identifier")
203
+ result.bindings.set(child.text(), {
204
+ target: destination,
205
+ name: "default",
206
+ });
207
+ if (child.kind() === "namespace_import")
208
+ result.bindings.set(child.children().at(-1)?.text() ?? "", {
209
+ target: destination,
210
+ name: "*",
211
+ });
212
+ if (child.kind() === "named_imports")
213
+ for (const spec of child
214
+ .children()
215
+ .filter((part) => part.kind() === "import_specifier")) {
216
+ const names = spec
217
+ .children()
218
+ .filter((part) => part.kind() === "identifier");
219
+ result.bindings.set(names.at(-1)?.text() ?? "", {
220
+ target: destination,
221
+ name: names[0]?.text() ?? "",
222
+ });
223
+ }
224
+ }
225
+ }
226
+ else if (kind === "export_statement") {
227
+ const string = node
228
+ .children()
229
+ .find((child) => child.kind() === "string");
230
+ const destination = string
231
+ ? target(path, string.text().slice(1, -1), result.limits)
232
+ : path;
233
+ if (!destination)
234
+ continue;
235
+ const clause = node
236
+ .children()
237
+ .find((child) => child.kind() === "export_clause");
238
+ if (clause)
239
+ for (const spec of clause
240
+ .children()
241
+ .filter((part) => part.kind() === "export_specifier")) {
242
+ const names = spec
243
+ .children()
244
+ .filter((part) => part.kind() === "identifier");
245
+ addExport(names.at(-1)?.text() ?? "", {
246
+ target: destination,
247
+ name: names[0]?.text() ?? "",
248
+ });
249
+ }
250
+ else if (node.children().some((child) => child.kind() === "*"))
251
+ result.stars.push(destination);
252
+ else if (node.children().some((child) => child.kind() === "default")) {
253
+ const definition = result.definitions.find((def) => def.declaration.start <= node.range().start.line + 1 &&
254
+ def.declaration.end >= node.range().end.line + 1);
255
+ const identifier = node
256
+ .children()
257
+ .find((child) => child.kind() === "identifier");
258
+ if (definition || identifier)
259
+ addExport("default", {
260
+ target: path,
261
+ name: definition?.declaration.name ?? identifier?.text() ?? "",
262
+ });
263
+ else
264
+ result.limits.push({
265
+ path,
266
+ kind: "unsupported",
267
+ specifier: "default export expression",
268
+ });
269
+ }
270
+ }
271
+ else if (path.endsWith(".py") &&
272
+ /^(import_from_statement|import_statement)$/.test(kind)) {
273
+ const children = node.children();
274
+ const from = kind === "import_from_statement";
275
+ const moduleNode = from
276
+ ? children.find((child) => /^(dotted_name|relative_import)$/.test(String(child.kind())))
277
+ : undefined;
278
+ const module = moduleNode?.text() ?? "";
279
+ for (const child of children) {
280
+ if (!/^(aliased_import|dotted_name)$/.test(String(child.kind())) ||
281
+ child === moduleNode)
282
+ continue;
283
+ const names = child.kind() === "aliased_import"
284
+ ? child
285
+ .children()
286
+ .filter((part) => /^(identifier|dotted_name)$/.test(String(part.kind())))
287
+ .map((part) => part.text())
288
+ : [child.text()];
289
+ const destination = target(path, from ? module : (names[0] ?? ""), result.limits);
290
+ if (destination)
291
+ result.bindings.set(names.at(-1) ?? "", {
292
+ target: destination,
293
+ name: from ? (names[0] ?? "") : "*",
294
+ });
295
+ }
296
+ }
297
+ else
298
+ result.references.push(...refs);
299
+ }
300
+ return result;
301
+ };
302
+ return (entry) => {
303
+ const starts = [entry.path];
304
+ if (entry.framework === "pytest")
305
+ for (const file of files)
306
+ if (posix.basename(file.path) === "conftest.py" &&
307
+ (posix.dirname(file.path) === "." ||
308
+ entry.path.startsWith(`${posix.dirname(file.path)}/`)))
309
+ starts.push(file.path);
310
+ const closures = starts.map((path) => {
311
+ let closure = closureCache.get(path);
312
+ if (!closure) {
313
+ closure = importClosure(graph, path);
314
+ closureCache.set(path, closure);
315
+ }
316
+ return closure;
317
+ });
318
+ const paths = [...new Set(closures.flatMap((closure) => closure.paths))];
319
+ const limits = closures.flatMap((closure) => closure.limits);
320
+ // Missing lexical edges are already uncertain; AST analysis cannot invent a path.
321
+ if (!units.length || !starts.some((path) => reachesDiff.has(path))) {
322
+ return {
323
+ paths,
324
+ units: limits.length ? units : [],
325
+ imports: [],
326
+ usedBy: new Map(),
327
+ limits,
328
+ uncertain: limits.length > 0,
329
+ };
330
+ }
331
+ const resolveUncached = (path, name, exported, seen = new Set()) => {
332
+ if (!reachesDiff.has(path))
333
+ return [];
334
+ const key = `${path}:${name}:${exported}`;
335
+ if (seen.has(key))
336
+ return [];
337
+ const next = new Set(seen).add(key);
338
+ const module = getModule(path);
339
+ if (!exported) {
340
+ const local = module.definitions.filter((def) => def.declaration.name === name);
341
+ if (local.length)
342
+ return local;
343
+ const binding = module.bindings.get(name);
344
+ return binding && binding.name !== "*"
345
+ ? resolve(binding.target, binding.name, true, next)
346
+ : [];
347
+ }
348
+ if (module.stars.some((star) => !reachesDiff.has(star))) {
349
+ limits.push({ path, kind: "ambiguous", specifier: name });
350
+ }
351
+ const bindings = module.exports.get(name);
352
+ const found = bindings
353
+ ? bindings.flatMap((binding) => binding.target === path && binding.name === name
354
+ ? module.definitions.filter((def) => def.declaration.name === name)
355
+ : resolve(binding.target, binding.name, binding.target !== path, next))
356
+ : name === "default"
357
+ ? []
358
+ : module.stars.flatMap((star) => resolve(star, name, true, next));
359
+ const unique = [...new Set(found)];
360
+ if (unique.length > 1)
361
+ limits.push({ path, kind: "ambiguous", specifier: name });
362
+ return unique;
363
+ };
364
+ const resolve = (path, name, exported, seen = new Set()) => {
365
+ if (seen.size)
366
+ return resolveUncached(path, name, exported, seen);
367
+ const key = `${path}:${name}:${exported}`;
368
+ const cached = resolutionCache.get(key);
369
+ if (cached) {
370
+ limits.push(...cached.limits);
371
+ return cached.definitions;
372
+ }
373
+ const limitStart = limits.length;
374
+ const definitions = resolveUncached(path, name, exported, seen);
375
+ resolutionCache.set(key, {
376
+ definitions,
377
+ limits: limits.slice(limitStart),
378
+ });
379
+ return definitions;
380
+ };
381
+ const dependencies = (path, refs) => refs.flatMap((ref) => {
382
+ const module = getModule(path);
383
+ const binding = module.bindings.get(ref.name);
384
+ const resolveImported = (name) => {
385
+ if (!binding)
386
+ return [];
387
+ if (!reachesDiff.has(binding.target))
388
+ return [];
389
+ const found = resolve(binding.target, name, true);
390
+ if (!found.length)
391
+ limits.push({
392
+ path,
393
+ kind: "unresolved",
394
+ specifier: `${ref.name}${ref.member ? `.${ref.member}` : ""}`,
395
+ });
396
+ return found;
397
+ };
398
+ if (ref.member && binding) {
399
+ if (binding.name === "*")
400
+ return resolveImported(ref.member);
401
+ limits.push({
402
+ path,
403
+ kind: "unsupported",
404
+ specifier: `${ref.name}.${ref.member}`,
405
+ });
406
+ return resolveImported(binding.name);
407
+ }
408
+ if (binding?.name === "*") {
409
+ limits.push({ path, kind: "unsupported", specifier: ref.name });
410
+ return [];
411
+ }
412
+ return binding
413
+ ? resolveImported(binding.name)
414
+ : resolve(path, ref.name, false);
415
+ });
416
+ const reachable = new Set();
417
+ const visit = (definition) => {
418
+ if (reachable.has(definition))
419
+ return;
420
+ reachable.add(definition);
421
+ let cached = dependencyCache.get(definition);
422
+ if (!cached) {
423
+ const limitStart = limits.length;
424
+ const definitions = [
425
+ ...new Set(dependencies(definition.file, definition.references)),
426
+ ].filter((dep) => dep !== definition);
427
+ cached = { definitions, limits: limits.slice(limitStart) };
428
+ dependencyCache.set(definition, cached);
429
+ }
430
+ else
431
+ limits.push(...cached.limits);
432
+ const deps = cached.definitions;
433
+ if (!indexed.has(definition)) {
434
+ indexed.add(definition);
435
+ const matched = units.filter((unit) => unit.file === definition.file &&
436
+ (unit.name === definition.declaration.name ||
437
+ (unit.afterRange &&
438
+ unit.afterRange.start <= definition.declaration.end &&
439
+ unit.afterRange.end >= definition.declaration.start)));
440
+ if (matched.length)
441
+ declarationHits.set(definition, matched);
442
+ for (const dependency of deps) {
443
+ const parents = declarationParents.get(dependency) ?? new Set();
444
+ parents.add(definition);
445
+ declarationParents.set(dependency, parents);
446
+ }
447
+ if ((matched.length ||
448
+ deps.some((dependency) => reachesUnit.has(dependency))) &&
449
+ !reachesUnit.has(definition)) {
450
+ reachesUnit.add(definition);
451
+ propagationQueue.push(definition);
452
+ }
453
+ }
454
+ for (const dep of deps)
455
+ visit(dep);
456
+ };
457
+ for (const start of starts) {
458
+ let roots = rootCache.get(start);
459
+ if (!roots) {
460
+ const module = getModule(start);
461
+ const limitStart = limits.length;
462
+ roots = {
463
+ definitions: [
464
+ ...dependencies(start, module.references),
465
+ ...module.definitions,
466
+ ],
467
+ limits: limits.slice(limitStart),
468
+ };
469
+ rootCache.set(start, roots);
470
+ }
471
+ else
472
+ limits.push(...roots.limits);
473
+ for (const definition of roots.definitions)
474
+ visit(definition);
475
+ }
476
+ while (propagationOffset < propagationQueue.length) {
477
+ const dependency = propagationQueue[propagationOffset++];
478
+ if (!dependency)
479
+ continue;
480
+ for (const parent of declarationParents.get(dependency) ?? []) {
481
+ if (reachesUnit.has(parent))
482
+ continue;
483
+ reachesUnit.add(parent);
484
+ propagationQueue.push(parent);
485
+ }
486
+ }
487
+ const hits = new Map([...reachable].flatMap((definition) => {
488
+ const matched = declarationHits.get(definition);
489
+ return matched ? [[definition, matched]] : [];
490
+ }));
491
+ const relevant = new Set([...reachable].filter((definition) => reachesUnit.has(definition)));
492
+ for (const path of paths)
493
+ limits.push(...(modules.get(path)?.limits ?? []));
494
+ const uniqueLimits = [
495
+ ...new Map(limits.map((limit) => [
496
+ `${limit.path}:${limit.kind}:${limit.specifier ?? ""}`,
497
+ limit,
498
+ ])).values(),
499
+ ];
500
+ // Unknown import edges stay conservative; declaration limits affect only
501
+ // branches whose files can reach the diff through known import edges.
502
+ const uncertain = closures.some((closure) => closure.limits.length > 0) ||
503
+ uniqueLimits.some((limit) => reachesDiff.has(limit.path));
504
+ const reached = units.filter((unit) => paths.includes(unit.file) ||
505
+ closures.some((closure) => closure.limits.length));
506
+ const intermediates = [...relevant].filter((definition) => definition.file !== entry.path && !hits.has(definition));
507
+ const imports = intermediates
508
+ .filter((definition) => !units.some((unit) => unit.file === definition.file))
509
+ .map((definition) => ({ path: definition.file, text: definition.text }));
510
+ const usedBy = new Map();
511
+ for (const unit of reached)
512
+ usedBy.set(unit.id, intermediates
513
+ .filter((definition) => definition.file === unit.file)
514
+ .map((definition) => ({
515
+ path: definition.file,
516
+ text: definition.text,
517
+ })));
518
+ return {
519
+ paths,
520
+ units: reached,
521
+ imports,
522
+ usedBy,
523
+ limits: uniqueLimits,
524
+ uncertain,
525
+ };
526
+ };
527
+ }
@@ -0,0 +1,81 @@
1
+ import { STATE_MAX_CHARS } from "../constants.js";
2
+ /** Partition complete bodies with a single setup mask and additive serialized sizes. */
3
+ export function prepareTestStates(state, scenarios, maxChars = STATE_MAX_CHARS) {
4
+ if (JSON.stringify(state).length <= maxChars)
5
+ return { batches: [{ state, scenarios }], unjudged: [] };
6
+ const file = state.testFile;
7
+ if (!file ||
8
+ typeof file !== "object" ||
9
+ Array.isArray(file) ||
10
+ typeof file.text !== "string")
11
+ return {
12
+ batches: [],
13
+ unjudged: scenarios.map((scenario) => ({
14
+ scenario,
15
+ reason: "required test file absent from state",
16
+ })),
17
+ };
18
+ const source = file.text.split("\n");
19
+ const mask = new Int32Array(source.length + 1);
20
+ for (const scenario of scenarios)
21
+ if (scenario.range) {
22
+ const start = Math.max(0, scenario.range.start - 1);
23
+ const end = Math.min(source.length, scenario.range.end);
24
+ mask[start] = (mask[start] ?? 0) + 1;
25
+ mask[end] = (mask[end] ?? 0) - 1;
26
+ }
27
+ let active = 0;
28
+ const setup = `${source
29
+ .map((line, index) => {
30
+ active += mask[index] ?? 0;
31
+ return active ? "" : line;
32
+ })
33
+ .join("\n")}\n`;
34
+ const base = { ...state, testFile: { ...file, text: setup } };
35
+ const baseSize = JSON.stringify(base).length;
36
+ const batches = [];
37
+ const unjudged = [];
38
+ let group = [];
39
+ let bodies = [];
40
+ let size = baseSize;
41
+ const flush = () => {
42
+ if (group.length)
43
+ batches.push({
44
+ state: {
45
+ ...base,
46
+ testFile: { ...file, text: setup + bodies.join("\n") },
47
+ },
48
+ scenarios: group,
49
+ });
50
+ group = [];
51
+ bodies = [];
52
+ size = baseSize;
53
+ };
54
+ for (const scenario of scenarios) {
55
+ if (!scenario.range) {
56
+ unjudged.push({
57
+ scenario,
58
+ reason: "test body range unresolved; complete file exceeds state budget",
59
+ });
60
+ continue;
61
+ }
62
+ const body = source
63
+ .slice(scenario.range.start - 1, scenario.range.end)
64
+ .join("\n");
65
+ const bodySize = JSON.stringify(body).length - 2;
66
+ if (baseSize + bodySize > maxChars) {
67
+ unjudged.push({
68
+ scenario,
69
+ reason: `complete test body and required evidence exceed state budget ${maxChars}`,
70
+ });
71
+ continue;
72
+ }
73
+ if (group.length && size + 2 + bodySize > maxChars)
74
+ flush();
75
+ size += bodySize + (group.length ? 2 : 0);
76
+ group.push(scenario);
77
+ bodies.push(body);
78
+ }
79
+ flush();
80
+ return { batches, unjudged };
81
+ }
@@ -0,0 +1,12 @@
1
+ /** Keep a UTF-16 prefix without splitting a valid surrogate pair. */
2
+ export function truncate(text, maxChars) {
3
+ let end = Math.max(0, Math.min(text.length, Math.floor(maxChars)));
4
+ if (end > 0 &&
5
+ end < text.length &&
6
+ text.charCodeAt(end - 1) >= 0xd800 &&
7
+ text.charCodeAt(end - 1) <= 0xdbff &&
8
+ text.charCodeAt(end) >= 0xdc00 &&
9
+ text.charCodeAt(end) <= 0xdfff)
10
+ end--;
11
+ return text.slice(0, end);
12
+ }