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,550 @@
1
+ import { posix } from "node:path";
2
+ import { tokenize as tokens } from "./lexical.js";
3
+ function parseConfiguration(text, lexical = tokens(text, false)) {
4
+ return JSON.parse(lexical
5
+ .filter((token, index) => !(token.value === "," &&
6
+ ["}", "]"].includes(lexical[index + 1]?.value ?? "")))
7
+ .map((token) => token.string ? JSON.stringify(token.value) : token.value)
8
+ .join(" "));
9
+ }
10
+ function exportTargets(value) {
11
+ if (typeof value === "string")
12
+ return [value];
13
+ if (!value || typeof value !== "object")
14
+ return [];
15
+ return Object.values(value).flatMap(exportTargets);
16
+ }
17
+ function exportEntries(manifest) {
18
+ const value = manifest.exports;
19
+ if (value === undefined)
20
+ return exportTargets(manifest.main).map((target) => [".", target]);
21
+ if (value &&
22
+ typeof value === "object" &&
23
+ Object.keys(value).some((key) => key.startsWith(".")))
24
+ return Object.entries(value).flatMap(([key, target]) => exportTargets(target).map((text) => [key, text]));
25
+ return exportTargets(value).map((target) => [".", target]);
26
+ }
27
+ function wildcardMatch(pattern, value) {
28
+ const star = pattern.indexOf("*");
29
+ if (star < 0)
30
+ return pattern === value ? "" : undefined;
31
+ const prefix = pattern.slice(0, star);
32
+ const suffix = pattern.slice(star + 1);
33
+ return value.startsWith(prefix) &&
34
+ value.endsWith(suffix) &&
35
+ value.length >= prefix.length + suffix.length
36
+ ? value.slice(prefix.length, value.length - suffix.length)
37
+ : undefined;
38
+ }
39
+ /** Discriminating literal search hints; callers still confirm each hit with the shared resolver. */
40
+ export function importSearchSpecifiers(configuration, targets) {
41
+ const patterns = new Set();
42
+ for (const target of targets) {
43
+ const stem = target.replace(/\.[^./]+$/, "");
44
+ const parts = stem.split("/");
45
+ for (let index = 0; index < parts.length - 1; index++)
46
+ patterns.add(`./${parts.slice(index).join("/")}`);
47
+ patterns.add(`./${parts.at(-1)}`);
48
+ if (parts.at(-1) === "index") {
49
+ const folder = parts.slice(0, -1);
50
+ for (let index = 0; index < folder.length - 1; index++)
51
+ patterns.add(`./${folder.slice(index).join("/")}`);
52
+ if (folder.length)
53
+ patterns.add(`./${folder.at(-1)}`);
54
+ }
55
+ }
56
+ for (const source of configuration) {
57
+ let parsed;
58
+ try {
59
+ parsed = parseConfiguration(source.text);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ const cwd = posix.dirname(source.path);
65
+ if (posix.basename(source.path) === "package.json" &&
66
+ typeof parsed.name === "string") {
67
+ for (const [key, value] of exportEntries(parsed)) {
68
+ if (!value.startsWith("./"))
69
+ continue;
70
+ const base = posix.join(cwd, value).replace(/\.[cm]?[jt]sx?$/, "");
71
+ for (const target of targets) {
72
+ const middle = wildcardMatch(base, target.replace(/\.[cm]?[jt]sx?$/, ""));
73
+ if (middle !== undefined)
74
+ patterns.add(parsed.name +
75
+ (key === "." ? "" : key.slice(1).replace("*", middle)));
76
+ }
77
+ }
78
+ }
79
+ const options = parsed.compilerOptions;
80
+ if (!options || typeof options !== "object")
81
+ continue;
82
+ const base = posix.join(cwd, typeof options.baseUrl === "string" ? options.baseUrl : ".");
83
+ if (options.paths && typeof options.paths === "object")
84
+ for (const [alias, values] of Object.entries(options.paths)) {
85
+ if (!Array.isArray(values))
86
+ continue;
87
+ for (const value of values) {
88
+ if (typeof value !== "string")
89
+ continue;
90
+ const pattern = posix.join(base, value);
91
+ const star = pattern.indexOf("*");
92
+ for (const target of targets) {
93
+ for (const stem of new Set([
94
+ target.replace(/\.[cm]?[jt]sx?$/, ""),
95
+ target.replace(/\/index\.[cm]?[jt]sx?$/, ""),
96
+ ])) {
97
+ if (star < 0
98
+ ? stem === pattern.replace(/\.[cm]?[jt]sx?$/, "")
99
+ : stem.startsWith(pattern.slice(0, star)) &&
100
+ stem.endsWith(pattern.slice(star + 1))) {
101
+ const middle = star < 0
102
+ ? ""
103
+ : stem.slice(star, stem.length - (pattern.length - star - 1));
104
+ patterns.add(alias.replace("*", middle));
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return [...patterns];
112
+ }
113
+ export function buildImportGraph(files, options = {}) {
114
+ return createImportGraphBuilder(files, options.known ?? new Set(files.map((file) => file.path)))(files);
115
+ }
116
+ /** Parse resolution configuration once, then resolve independent batches with identical semantics. */
117
+ export function createImportGraphBuilder(configuration, known, lexical) {
118
+ const cache = new Map();
119
+ let limits = [];
120
+ const packages = new Map();
121
+ const packageDirectories = new Set();
122
+ for (const path of known) {
123
+ if (posix.basename(path) !== "package.json")
124
+ continue;
125
+ const directory = posix.dirname(path);
126
+ const parent = posix.basename(posix.dirname(directory));
127
+ packageDirectories.add(parent.startsWith("@")
128
+ ? `${parent}/${posix.basename(directory)}`
129
+ : posix.basename(directory));
130
+ }
131
+ const aliases = [];
132
+ for (const source of configuration) {
133
+ if (posix.basename(source.path) !== "package.json" &&
134
+ !/^tsconfig.*\.json$/.test(posix.basename(source.path)))
135
+ continue;
136
+ try {
137
+ const parsed = parseConfiguration(source.text, lexical?.tokenize(source.path, source.text));
138
+ const cwd = posix.dirname(source.path);
139
+ if (posix.basename(source.path) === "package.json" &&
140
+ typeof parsed.name === "string") {
141
+ const previous = packages.get(parsed.name) ?? [];
142
+ previous.push({ cwd, manifest: parsed });
143
+ packages.set(parsed.name, previous);
144
+ }
145
+ else if (parsed.compilerOptions &&
146
+ typeof parsed.compilerOptions === "object") {
147
+ const options = parsed.compilerOptions;
148
+ aliases.push({
149
+ cwd,
150
+ base: posix.join(cwd, typeof options.baseUrl === "string" ? options.baseUrl : "."),
151
+ baseUrl: typeof options.baseUrl === "string",
152
+ paths: options.paths && typeof options.paths === "object"
153
+ ? options.paths
154
+ : {},
155
+ });
156
+ }
157
+ }
158
+ catch {
159
+ limits.push({
160
+ path: source.path,
161
+ specifier: "configuration",
162
+ kind: "unresolved",
163
+ });
164
+ }
165
+ }
166
+ const configurationLimits = limits;
167
+ const resolve = (file, specifier, python) => {
168
+ if (specifier.startsWith("node:"))
169
+ return [];
170
+ let base;
171
+ if (python) {
172
+ let dots = 0;
173
+ while (specifier[dots] === ".")
174
+ dots++;
175
+ base = dots ? posix.dirname(file.path) : "";
176
+ for (let j = 1; j < dots; j++)
177
+ base = posix.dirname(base);
178
+ base = posix.join(base, specifier.slice(dots).replaceAll(".", "/"));
179
+ }
180
+ else {
181
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
182
+ const name = specifier.startsWith("@")
183
+ ? specifier.split("/").slice(0, 2).join("/")
184
+ : (specifier.split("/")[0] ?? "");
185
+ const workspace = packages.get(name);
186
+ if (workspace) {
187
+ if (aliases.some((alias) => !posix.relative(alias.cwd, file.path).startsWith("../") &&
188
+ Object.keys(alias.paths).some((pattern) => wildcardMatch(pattern, specifier) !== undefined))) {
189
+ limits.push({ path: file.path, specifier, kind: "ambiguous" });
190
+ return [];
191
+ }
192
+ if (workspace.length !== 1) {
193
+ limits.push({ path: file.path, specifier, kind: "ambiguous" });
194
+ return [];
195
+ }
196
+ const found = workspace[0];
197
+ if (!found)
198
+ return [];
199
+ const { cwd, manifest } = found;
200
+ const suffix = specifier.slice(name.length);
201
+ const key = suffix ? `.${suffix}` : ".";
202
+ const entries = exportEntries(manifest);
203
+ const exact = entries.filter(([pattern]) => pattern === key);
204
+ const matching = exact.length
205
+ ? exact
206
+ : entries.filter(([pattern]) => wildcardMatch(pattern, key) !== undefined);
207
+ const resolved = new Set();
208
+ for (const [pattern, target] of matching) {
209
+ if (!target.startsWith("./"))
210
+ continue;
211
+ const middle = wildcardMatch(pattern, key) ?? "";
212
+ const path = posix.normalize(posix.join(cwd, target.replace("*", middle)));
213
+ if (posix.relative(cwd, path).startsWith("../")) {
214
+ limits.push({ path: file.path, specifier, kind: "outside_repo" });
215
+ continue;
216
+ }
217
+ const previousLimits = limits.length;
218
+ for (const match of resolve(file, `./${posix.relative(posix.dirname(file.path), path)}`, false))
219
+ resolved.add(match);
220
+ limits.splice(previousLimits);
221
+ }
222
+ if (!resolved.size)
223
+ limits.push({ path: file.path, specifier, kind: "unresolved" });
224
+ return [...resolved];
225
+ }
226
+ else {
227
+ const local = aliases.filter((alias) => !posix.relative(alias.cwd, file.path).startsWith("../"));
228
+ const matches = [];
229
+ for (const alias of local)
230
+ for (const [pattern, values] of Object.entries(alias.paths)) {
231
+ const star = pattern.indexOf("*");
232
+ if (!(star < 0
233
+ ? specifier === pattern
234
+ : specifier.startsWith(pattern.slice(0, star)) &&
235
+ specifier.endsWith(pattern.slice(star + 1))))
236
+ continue;
237
+ if (!Array.isArray(values))
238
+ continue;
239
+ const middle = star < 0
240
+ ? ""
241
+ : specifier.slice(star, specifier.length - (pattern.length - star - 1));
242
+ for (const value of values)
243
+ if (typeof value === "string") {
244
+ const target = posix.normalize(posix.join(alias.base, value.replace("*", middle)));
245
+ if (target.startsWith("../")) {
246
+ limits.push({
247
+ path: file.path,
248
+ specifier,
249
+ kind: "outside_repo",
250
+ });
251
+ continue;
252
+ }
253
+ matches.push(...resolve(file, `./${posix.relative(posix.dirname(file.path), target)}`, false));
254
+ }
255
+ }
256
+ const unique = [...new Set(matches)];
257
+ if (unique.length)
258
+ return unique;
259
+ if (local.some((alias) => alias.baseUrl) ||
260
+ specifier.startsWith("@/") ||
261
+ specifier.startsWith("~/") ||
262
+ specifier.startsWith("#") ||
263
+ packageDirectories.has(name))
264
+ limits.push({ path: file.path, specifier, kind: "unresolved" });
265
+ return [];
266
+ }
267
+ }
268
+ else
269
+ base = posix.normalize(posix.join(posix.dirname(file.path), specifier));
270
+ }
271
+ if (base.startsWith("../") || specifier.startsWith("/")) {
272
+ limits.push({ path: file.path, specifier, kind: "outside_repo" });
273
+ return [];
274
+ }
275
+ let candidates;
276
+ if (python)
277
+ candidates = [`${base}.py`, posix.join(base, "__init__.py")];
278
+ else if (known.has(base))
279
+ candidates = [base];
280
+ else {
281
+ const stem = base.replace(/\.[cm]?jsx?$/, "");
282
+ candidates = [
283
+ `${stem}.ts`,
284
+ `${stem}.tsx`,
285
+ `${stem}.mts`,
286
+ `${stem}.cts`,
287
+ `${base}.js`,
288
+ `${base}.ts`,
289
+ `${base}.tsx`,
290
+ posix.join(base, "index.ts"),
291
+ posix.join(base, "index.js"),
292
+ ];
293
+ }
294
+ const matches = [...new Set(candidates)].filter((path) => known.has(path));
295
+ if (matches.length !== 1) {
296
+ // Standard-library imports are external; unknown roots/plugins remain explicit.
297
+ const standard = /^(?:__future__|sys|os|pathlib|json|re|math|typing|collections|itertools|functools|datetime|unittest|contextlib|asyncio|subprocess|tempfile|shutil|logging|inspect|importlib|dataclasses|enum|abc|io|time|copy|decimal|statistics|warnings)(?:\.|$)/;
298
+ if (!python || specifier.startsWith(".") || !standard.test(specifier))
299
+ limits.push({
300
+ path: file.path,
301
+ specifier,
302
+ kind: matches.length ? "ambiguous" : "unresolved",
303
+ });
304
+ return [];
305
+ }
306
+ return matches;
307
+ };
308
+ return (files) => {
309
+ const edges = new Map();
310
+ limits = [...configurationLimits];
311
+ for (const file of files) {
312
+ const cached = cache.get(file.path);
313
+ if (cached?.text === file.text) {
314
+ edges.set(file.path, cached.edges);
315
+ limits.push(...cached.limits);
316
+ continue;
317
+ }
318
+ const limitStart = limits.length;
319
+ const python = file.path.endsWith(".py");
320
+ if (!python && !/\.[cm]?[jt]sx?$/.test(file.path)) {
321
+ edges.set(file.path, []);
322
+ continue;
323
+ }
324
+ lexical?.resolved?.(file.path, file.text);
325
+ const ts = lexical
326
+ ? lexical.tokenize(file.path, file.text, python)
327
+ : tokens(file.text, python);
328
+ const targets = new Set();
329
+ const add = (specifier) => {
330
+ for (const path of resolve(file, specifier, python))
331
+ targets.add(path);
332
+ };
333
+ const dynamic = (specifier = "") => limits.push({ path: file.path, specifier, kind: "dynamic" });
334
+ const readers = new Set();
335
+ readers.add("readFile");
336
+ readers.add("readFileSync");
337
+ if (!python)
338
+ for (let i = 0; i < ts.length; i++) {
339
+ if (ts[i]?.value !== "import" || ts[i]?.string)
340
+ continue;
341
+ let end = i + 1;
342
+ while (end < ts.length && ts[end]?.value !== ";" && !ts[end]?.string)
343
+ end++;
344
+ if (!["node:fs", "fs", "node:fs/promises", "fs/promises"].includes(ts[end]?.value ?? ""))
345
+ continue;
346
+ for (let j = i + 1; j < end; j++)
347
+ if (["readFile", "readFileSync"].includes(ts[j]?.value ?? "")) {
348
+ const local = ts[j + 1]?.value === "as" ? ts[j + 2]?.value : ts[j]?.value;
349
+ if (local)
350
+ readers.add(local);
351
+ }
352
+ }
353
+ for (let i = 0; i < ts.length; i++) {
354
+ const t = ts[i];
355
+ if (!t || t.string)
356
+ continue;
357
+ if (python &&
358
+ t.value === "sys" &&
359
+ ts[i + 1]?.value === "." &&
360
+ ts[i + 2]?.value === "path")
361
+ dynamic("sys.path mutation or lookup");
362
+ if (python && t.value === "from") {
363
+ let j = i + 1;
364
+ let module = "";
365
+ while (j < ts.length &&
366
+ ts[j]?.line === t.line &&
367
+ ts[j]?.value !== "import")
368
+ module += ts[j++]?.value ?? "";
369
+ if (ts[j]?.value === "import") {
370
+ if (module && !/^\.+$/.test(module))
371
+ add(module);
372
+ // `from . import module` and `from pkg import module` may expose submodules.
373
+ let k = j + 1;
374
+ while (k < ts.length && ts[k]?.line === t.line) {
375
+ const name = ts[k]?.value ?? "";
376
+ if (/^\w+$/.test(name) &&
377
+ name !== "as" &&
378
+ ts[k - 1]?.value !== "as") {
379
+ const spec = module.endsWith(".")
380
+ ? `${module}${name}`
381
+ : `${module}.${name}`;
382
+ let dots = 0;
383
+ while (spec[dots] === ".")
384
+ dots++;
385
+ let base = dots ? posix.dirname(file.path) : "";
386
+ for (let level = 1; level < dots; level++)
387
+ base = posix.dirname(base);
388
+ const local = posix.join(base, spec.slice(dots).replaceAll(".", "/"));
389
+ if (/^\.+$/.test(module) ||
390
+ known.has(`${local}.py`) ||
391
+ known.has(posix.join(local, "__init__.py")))
392
+ add(spec);
393
+ }
394
+ k++;
395
+ }
396
+ }
397
+ }
398
+ else if (python &&
399
+ t.value === "import" &&
400
+ ts[i - 1]?.value !== "from" &&
401
+ !ts
402
+ .slice(0, i)
403
+ .some((prev) => prev.line === t.line && prev.value === "from")) {
404
+ let j = i + 1;
405
+ let module = "";
406
+ while (j < ts.length && ts[j]?.line === t.line) {
407
+ if (ts[j]?.value === "as")
408
+ break;
409
+ if (ts[j]?.value === ",") {
410
+ if (module)
411
+ add(module);
412
+ module = "";
413
+ }
414
+ else
415
+ module += ts[j]?.value ?? "";
416
+ j++;
417
+ }
418
+ if (module)
419
+ add(module);
420
+ }
421
+ else if (!python && (t.value === "import" || t.value === "export")) {
422
+ if (ts[i + 1]?.value === "(") {
423
+ if (ts[i + 2]?.string &&
424
+ ts[i + 3]?.value === ")" &&
425
+ ts[i + 2]?.value !== "<dynamic>")
426
+ add(ts[i + 2]?.value ?? "");
427
+ else
428
+ dynamic();
429
+ }
430
+ else if (ts[i + 1]?.string)
431
+ add(ts[i + 1]?.value ?? "");
432
+ else {
433
+ for (let j = i + 1; j < ts.length && ts[j]?.value !== ";" && j < i + 100; j++) {
434
+ if (ts[j]?.value === "from" && ts[j + 1]?.string) {
435
+ add(ts[j + 1]?.value ?? "");
436
+ break;
437
+ }
438
+ if ((ts[j]?.line ?? t.line) > t.line + 8)
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ else if (t.value === "URL" &&
444
+ ts[i - 1]?.value === "new" &&
445
+ ts[i + 1]?.value === "(") {
446
+ if (ts[i + 2]?.string &&
447
+ ts[i + 3]?.value === "," &&
448
+ ts[i + 4]?.value === "import" &&
449
+ ts[i + 5]?.value === "." &&
450
+ ts[i + 6]?.value === "meta" &&
451
+ ts[i + 7]?.value === "." &&
452
+ ts[i + 8]?.value === "url" &&
453
+ ts[i + 9]?.value === ")")
454
+ add(ts[i + 2]?.value ?? "");
455
+ }
456
+ else if ((t.value === "require" || readers.has(t.value)) &&
457
+ ts[i + 1]?.value === "(") {
458
+ if (ts[i + 2]?.string &&
459
+ (ts[i + 3]?.value === "," || ts[i + 3]?.value === ")") &&
460
+ ts[i + 2]?.value !== "<dynamic>")
461
+ add(ts[i + 2]?.value ?? "");
462
+ else if (readers.has(t.value) &&
463
+ ts[i + 2]?.value === "new" &&
464
+ ts[i + 3]?.value === "URL" &&
465
+ ts[i + 4]?.value === "(" &&
466
+ ts[i + 5]?.string &&
467
+ ts[i + 6]?.value === ",") {
468
+ // The nested URL token supplies the same statically resolved edge.
469
+ }
470
+ else
471
+ dynamic();
472
+ }
473
+ else if (python &&
474
+ (t.value === "__import__" || t.value === "import_module") &&
475
+ ts[i + 1]?.value === "(")
476
+ dynamic();
477
+ }
478
+ const dependencies = [...targets];
479
+ edges.set(file.path, dependencies);
480
+ cache.set(file.path, {
481
+ text: file.text,
482
+ edges: dependencies,
483
+ limits: limits.slice(limitStart),
484
+ });
485
+ }
486
+ const seenLimits = new Set();
487
+ const uniqueLimits = limits.filter((limit) => {
488
+ const key = JSON.stringify([limit.path, limit.specifier, limit.kind]);
489
+ if (seenLimits.has(key))
490
+ return false;
491
+ seenLimits.add(key);
492
+ return true;
493
+ });
494
+ return { edges, limits: uniqueLimits };
495
+ };
496
+ }
497
+ export function importClosure(graph, start, options = {}) {
498
+ const seen = new Set([start]);
499
+ const queue = [{ path: start, depth: 0 }];
500
+ for (let i = 0; i < queue.length; i++) {
501
+ const item = queue[i];
502
+ if (!item)
503
+ continue;
504
+ if (item.depth >= (options.maxDepth ?? Infinity) ||
505
+ options.stop?.(item.path))
506
+ continue;
507
+ for (const path of graph.edges.get(item.path) ?? []) {
508
+ if (!seen.has(path)) {
509
+ seen.add(path);
510
+ queue.push({ path, depth: item.depth + 1 });
511
+ }
512
+ }
513
+ }
514
+ return {
515
+ paths: [...seen],
516
+ limits: graph.limits.filter((limit) => seen.has(limit.path)),
517
+ };
518
+ }
519
+ /** Forward closure with injected reads; configuration and reached batches share the exact resolver. */
520
+ export async function importClosureLazy(read, start, options) {
521
+ const build = options.build ??
522
+ createImportGraphBuilder(options.configuration ?? [], options.known);
523
+ const cache = options.cache ?? new Map();
524
+ const seen = new Set([start]);
525
+ const queue = [start];
526
+ const limits = [];
527
+ for (let index = 0; index < queue.length; index++) {
528
+ const path = queue[index];
529
+ if (!path || options.stop?.(path))
530
+ continue;
531
+ let pending = cache.get(path);
532
+ if (!pending) {
533
+ pending = read(path).then((source) => source
534
+ ? build([source])
535
+ : {
536
+ edges: new Map(),
537
+ limits: [{ path, specifier: path, kind: "unresolved" }],
538
+ });
539
+ cache.set(path, pending);
540
+ }
541
+ const graph = await pending;
542
+ limits.push(...graph.limits);
543
+ for (const target of graph.edges.get(path) ?? [])
544
+ if (!seen.has(target)) {
545
+ seen.add(target);
546
+ queue.push(target);
547
+ }
548
+ }
549
+ return { paths: [...seen], limits };
550
+ }
@@ -0,0 +1,45 @@
1
+ export function checkIntegrity(state, asks, files = [], output) {
2
+ const controls = [];
3
+ const notes = [];
4
+ for (const file of files) {
5
+ if (file.resolvedPath !== file.insertedPath || !file.content.length)
6
+ controls.push({
7
+ fact: `integrity: wrong identity or empty file ${file.requestedPath}`,
8
+ next: `add the complete file to paths: ${file.requestedPath}`,
9
+ });
10
+ if (file.readSha256 !== file.insertedSha256)
11
+ controls.push({
12
+ fact: `integrity: fingerprint mismatch ${file.requestedPath}`,
13
+ next: `read the complete file again: ${file.requestedPath}`,
14
+ });
15
+ }
16
+ const out = output?.text ??
17
+ (typeof state.output === "string" ? state.output : undefined);
18
+ if (out !== undefined)
19
+ for (const ask of asks) {
20
+ if (ask.about !== undefined && ask.about !== "output")
21
+ continue;
22
+ const statements = ask.intent === "verify"
23
+ ? Object.values(ask.claims)
24
+ : ask.intent === "decide"
25
+ ? Object.values(ask.hypotheses)
26
+ : ask.intent === "classify"
27
+ ? Object.values(ask.categories)
28
+ : [];
29
+ for (const statement of statements)
30
+ for (const match of statement.matchAll(/`([^`]{4,})`|"([^"\n]{4,})"/g)) {
31
+ const literal = match[1] ?? match[2] ?? "";
32
+ if (!out.includes(literal))
33
+ controls.push({
34
+ fact: `integrity: cited line not in state: ${literal}`,
35
+ next: "add the cited output through command or state",
36
+ });
37
+ }
38
+ }
39
+ if (output?.truncated)
40
+ notes.push({
41
+ fact: `output truncated, ${output.omittedChars} chars omitted`,
42
+ next: "narrow command to retain decisive lines",
43
+ });
44
+ return { controls, notes };
45
+ }