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,267 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { privateStorage, } from "./adapters/private-storage.js";
7
+ import { createJevClient } from "./jev/client.js";
8
+ const fields = ["url", "apiKey", "model"];
9
+ const defaults = { url: "", apiKey: "", model: "openjev" };
10
+ const storageError = () => new Error("Cannot read or save Jev configuration. Check that the configuration directory and config.json are owned by you, private, regular storage without symlinks, and contain valid configuration JSON.");
11
+ function nonempty(value) {
12
+ return value !== undefined && value.trim().length > 0;
13
+ }
14
+ function validate(values) {
15
+ let validUrl = false;
16
+ let insecureTransport = false;
17
+ try {
18
+ const url = new URL(values.url);
19
+ const host = url.hostname.toLowerCase();
20
+ const loopback = host === "127.0.0.1" ||
21
+ host === "localhost" ||
22
+ host === "::1" ||
23
+ host === "[::1]";
24
+ const wellFormed = /^https?:\/\//i.test(values.url) &&
25
+ (url.protocol === "http:" || url.protocol === "https:") &&
26
+ url.hostname !== "" &&
27
+ url.username === "" &&
28
+ url.password === "";
29
+ insecureTransport = wellFormed && url.protocol === "http:" && !loopback;
30
+ validUrl = wellFormed && (url.protocol === "https:" || loopback);
31
+ }
32
+ catch {
33
+ // Keep user-controlled URL and credentials out of error messages.
34
+ }
35
+ if (insecureTransport) {
36
+ throw new Error("Jev configuration refuses plain http: to a non-loopback host; use https: or an http: loopback URL (127.0.0.1, ::1 or localhost).");
37
+ }
38
+ if (!validUrl ||
39
+ !nonempty(values.apiKey) ||
40
+ /[^\x20-\x7e\x80-\xff]/.test(values.apiKey) ||
41
+ !nonempty(values.model)) {
42
+ throw new Error("Jev configuration requires a full https: URL (or http: to 127.0.0.1, ::1 or localhost) without embedded credentials, a nonempty HTTP-header-compatible API key, and a nonempty model.");
43
+ }
44
+ }
45
+ /** Effective configuration and its client move together, only after a save succeeds. */
46
+ export class ConfigController {
47
+ environment = {};
48
+ flags = {};
49
+ saved = {};
50
+ session = {};
51
+ directory;
52
+ storage;
53
+ initialization;
54
+ currentClient;
55
+ constructor(options = {}) {
56
+ const env = options.env ?? process.env;
57
+ this.storage = options.storage ?? privateStorage();
58
+ for (const [field, name] of [
59
+ ["url", "JEV_TOOLS_URL"],
60
+ ["apiKey", "JEV_TOOLS_API_KEY"],
61
+ ["model", "JEV_TOOLS_MODEL"],
62
+ ]) {
63
+ if (nonempty(env[name]))
64
+ this.environment[field] = env[name];
65
+ }
66
+ const base = nonempty(env.XDG_CONFIG_HOME) && isAbsolute(env.XDG_CONFIG_HOME)
67
+ ? env.XDG_CONFIG_HOME
68
+ : join(homedir(), ".config");
69
+ this.directory = resolve(options.directory ?? join(base, "jev-agent-tools"));
70
+ if (nonempty(this.environment.url) && nonempty(this.environment.apiKey)) {
71
+ const effective = this.values();
72
+ validate(effective);
73
+ this.currentClient = createJevClient(effective);
74
+ }
75
+ }
76
+ get client() {
77
+ return this.currentClient;
78
+ }
79
+ initialize(flags) {
80
+ if (this.initialization)
81
+ return this.initialization;
82
+ this.initialization = this.load(flags).catch((error) => {
83
+ this.initialization = undefined;
84
+ throw error;
85
+ });
86
+ return this.initialization;
87
+ }
88
+ values() {
89
+ return {
90
+ ...defaults,
91
+ ...this.saved,
92
+ ...this.session,
93
+ ...this.flags,
94
+ ...this.environment,
95
+ };
96
+ }
97
+ locked(field) {
98
+ return (this.environment[field] !== undefined || this.flags[field] !== undefined);
99
+ }
100
+ async apply(values, persist) {
101
+ if (!this.initialization) {
102
+ throw new Error("Initialize Jev configuration before applying changes.");
103
+ }
104
+ await this.initialization;
105
+ const session = { ...this.session };
106
+ for (const field of fields) {
107
+ if (!this.locked(field))
108
+ session[field] = values[field];
109
+ }
110
+ const effective = {
111
+ ...defaults,
112
+ ...this.saved,
113
+ ...session,
114
+ ...this.flags,
115
+ ...this.environment,
116
+ };
117
+ validate(effective);
118
+ const saved = { ...this.saved, ...session };
119
+ if (persist)
120
+ await this.save(saved);
121
+ const previous = this.values();
122
+ this.session = session;
123
+ if (persist)
124
+ this.saved = saved;
125
+ if (fields.some((field) => previous[field] !== effective[field])) {
126
+ this.currentClient?.clearCache();
127
+ this.currentClient = createJevClient(effective);
128
+ }
129
+ else {
130
+ this.currentClient ??= createJevClient(effective);
131
+ }
132
+ }
133
+ async load(flags) {
134
+ const saved = await this.readSaved();
135
+ const supplied = {};
136
+ if (nonempty(flags.url))
137
+ supplied.url = flags.url;
138
+ if (nonempty(flags.model))
139
+ supplied.model = flags.model;
140
+ const effective = {
141
+ ...defaults,
142
+ ...saved,
143
+ ...supplied,
144
+ ...this.environment,
145
+ };
146
+ if (nonempty(effective.url) && nonempty(effective.apiKey)) {
147
+ validate(effective);
148
+ this.currentClient?.clearCache();
149
+ this.currentClient = createJevClient(effective);
150
+ }
151
+ this.saved = saved;
152
+ this.flags = supplied;
153
+ }
154
+ async checkDirectory() {
155
+ // Check ancestors too: recursive mkdir and path-based reads must not follow symlinks.
156
+ // Privacy of the directory itself is checked with its file in readSaved,
157
+ // using the operating system's own permission model.
158
+ let path = this.directory;
159
+ let exists = true;
160
+ while (true) {
161
+ try {
162
+ const stat = await lstat(path);
163
+ if (!stat.isDirectory() || stat.isSymbolicLink())
164
+ throw storageError();
165
+ }
166
+ catch (error) {
167
+ if (error?.code !== "ENOENT")
168
+ throw storageError();
169
+ if (path === this.directory)
170
+ exists = false;
171
+ }
172
+ const parent = dirname(path);
173
+ if (parent === path)
174
+ break;
175
+ path = parent;
176
+ }
177
+ return exists;
178
+ }
179
+ async readSaved() {
180
+ try {
181
+ if (!(await this.checkDirectory()))
182
+ return {};
183
+ const directory = {
184
+ path: this.directory,
185
+ stat: await lstat(this.directory),
186
+ };
187
+ const path = join(this.directory, "config.json");
188
+ try {
189
+ const stat = await lstat(path);
190
+ if (!stat.isFile() || stat.isSymbolicLink())
191
+ throw storageError();
192
+ }
193
+ catch (error) {
194
+ if (error?.code === "ENOENT") {
195
+ if (!(await this.storage.isPrivate([directory])))
196
+ throw storageError();
197
+ return {};
198
+ }
199
+ throw error;
200
+ }
201
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
202
+ try {
203
+ const stat = await file.stat();
204
+ if (!stat.isFile() ||
205
+ stat.nlink !== 1 ||
206
+ !(await this.storage.isPrivate([directory, { path, stat }]))) {
207
+ throw storageError();
208
+ }
209
+ const value = JSON.parse(await file.readFile("utf8"));
210
+ if (value === null ||
211
+ typeof value !== "object" ||
212
+ Array.isArray(value) ||
213
+ Object.entries(value).some(([key, entry]) => !fields.some((field) => field === key) ||
214
+ typeof entry !== "string")) {
215
+ throw storageError();
216
+ }
217
+ return value;
218
+ }
219
+ finally {
220
+ await file.close();
221
+ }
222
+ }
223
+ catch {
224
+ throw storageError();
225
+ }
226
+ }
227
+ async save(values) {
228
+ let temporary;
229
+ try {
230
+ const existed = await this.checkDirectory();
231
+ if (!existed) {
232
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
233
+ }
234
+ // Windows ignores mkdir's mode, so restrict the ACL before any secret is
235
+ // written: always for a new directory, and for an existing one only while
236
+ // it holds no configuration (e.g. left behind by an earlier failed save).
237
+ // Existing configuration is never re-permissioned; readSaved refuses it.
238
+ if (!existed ||
239
+ !(await lstat(join(this.directory, "config.json")).then(() => true, () => false)))
240
+ await this.storage.restrictDirectory(this.directory);
241
+ // Refuse to replace malformed, insecure or newly introduced storage.
242
+ await this.readSaved();
243
+ temporary = join(this.directory, `.config-${randomUUID()}.tmp`);
244
+ const file = await open(temporary, constants.O_WRONLY |
245
+ constants.O_CREAT |
246
+ constants.O_EXCL |
247
+ constants.O_NOFOLLOW, 0o600);
248
+ try {
249
+ await file.writeFile(`${JSON.stringify(values, null, 2)}\n`, "utf8");
250
+ await file.sync();
251
+ }
252
+ finally {
253
+ await file.close();
254
+ }
255
+ await this.readSaved();
256
+ await rename(temporary, join(this.directory, "config.json"));
257
+ temporary = undefined;
258
+ }
259
+ catch {
260
+ throw storageError();
261
+ }
262
+ finally {
263
+ if (temporary)
264
+ await unlink(temporary).catch(() => { });
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,140 @@
1
+ export const STATE_MAX_CHARS = 80_000;
2
+ export const TEST_SOURCE_MAX_BYTES = STATE_MAX_CHARS * 16;
3
+ export const SELECT_FILE_SHARE = 0.8;
4
+ export const ASK_NOTE_MAX_CHARS = 8_000;
5
+ export const ASK_MAX_FILES = 20;
6
+ export const ASK_TIMEOUT_S = 60;
7
+ export const ASK_TIMEOUT_MAX_S = 300;
8
+ export const PROCESS_KILL_GRACE_MS = 2_000;
9
+ export const PROCESS_PIPE_GRACE_MS = 1_000;
10
+ export const PROCESS_TREE_TIMEOUT_MS = 5_000;
11
+ export const MCP_SHUTDOWN_FLUSH_TIMEOUT_MS = 1_000;
12
+ export const PRIVATE_STORAGE_TIMEOUT_MS = 20_000;
13
+ export const MCP_VALIDATION_MAX_ERRORS = 5;
14
+ export const MCP_PACKAGE_REPLY_TIMEOUT_MS = 20_000;
15
+ export const MCP_PACKAGE_EXIT_TIMEOUT_MS = 10_000;
16
+ export const MCP_PACKAGE_KILL_TIMEOUT_MS = 2_000;
17
+ export const MCP_PACKAGE_INSTALL_TIMEOUT_MS = 300_000;
18
+ export const MCP_PACKAGE_STDERR_MAX_CHARS = 2_000;
19
+ export const OUTPUT_REPEAT_MIN = 5;
20
+ export const OUTPUT_CHUNK_CHARS = 2_500;
21
+ export const OUTPUT_FIND_MAX_CALLS = 40;
22
+ export const OUTPUT_HEAD_SHARE = 0.05;
23
+ export const OUTPUT_TAIL_SHARE = 0.15;
24
+ // Unmeasured admission bound for each command output file; never truncate it silently.
25
+ export const OUTPUT_FILE_MAX_BYTES = 64 * 1024 * 1024;
26
+ export const OUTPUT_LINE_MAX_CHARS = 8_192;
27
+ export const OUTPUT_SHAPE_MAX_COUNT = 2_048;
28
+ export const OUTPUT_FAILURE_WINDOW_LINES = 20;
29
+ export const CHOICE_MAX_OPTIONS = 255;
30
+ export const MAX_FILES = 255;
31
+ export const FILE_MAX_KB = STATE_MAX_CHARS / 1_000;
32
+ export const SCORE_MIN_LEVELS = 2;
33
+ export const SCORE_MAX_LEVELS = 10;
34
+ export const TIMEOUT_MS = 10_000;
35
+ export const UNIT_MAX_CHARS = 6_000;
36
+ export const UNIT_CONTEXT_LINES = 8;
37
+ export const SELECT_MIN = 0.5;
38
+ export const BAND_BOOL_GRAY_A = 0.2;
39
+ export const BAND_BOOL_YES_MIN = 1 - BAND_BOOL_GRAY_A;
40
+ export const ORDER_REVERSE_BELOW = 0.85;
41
+ export const ORDER_DISAGREE_MIN = 0.1;
42
+ export const BAND_CHOICE_VERDICT_MIN = ORDER_REVERSE_BELOW;
43
+ export const CANNOT_TELL_MIN = 0.3;
44
+ export const REQUEST_MAX_TOKENS = 60_000;
45
+ export const RATE_PER_SECOND = 8;
46
+ export const CONCURRENCY = 8;
47
+ export const REQUEST_ATTEMPTS = 3;
48
+ export const RETRY_BASE_MS = 500;
49
+ export const RETRY_MAX_MS = 5_000;
50
+ export const HTTP_ERROR_MAX_CHARS = 300;
51
+ export const QUESTION_TOKENS = 1 / 3.3;
52
+ export const STATE_TOKENS = 0.283;
53
+ export const REQUEST_BASE_TOKENS = 456;
54
+ export const FLAG_MIN = 0.7;
55
+ export const DOCS_CHECK_MIN = 0.2;
56
+ export const DOCS_MAX_SECTIONS = 40;
57
+ export const HOOK_BUDGET_MS = 15_000;
58
+ /** Progressive collection time bound, not a coverage or wall-time guarantee. */
59
+ export const DOCS_COLLECT_BUDGET_MS = 750;
60
+ export const DOCS_NAME_MAX_FILES = 32;
61
+ /** Initial per-anchor traversal bound; shared edge cache remains unbounded. */
62
+ export const DOCS_ANCHOR_MAX_FILES = 32;
63
+ export const WITNESS_LURE_MAX = 0.15;
64
+ /** Small coverage decoys only; risk and large-witness policies remain unchanged. */
65
+ export const COVERAGE_WITNESS_LURE_MAX = 0.2;
66
+ export const WITNESS_LARGE_LURE_MAX = 0.25;
67
+ export const WITNESS_LARGE_CHARS = 13_000;
68
+ export const WITNESS_ETALON_MIN = 0.7;
69
+ export const WITNESS_AUTO_MIN_CELLS = 8;
70
+ export const CLOSURE_MAX_CHARS = 20_000;
71
+ export const CALLER_DISPLAY_MAX_SPANS = 8;
72
+ export const LIMIT_DISPLAY_MAX_PATHS = 4;
73
+ export const DOCS_DISPLAY_MAX_SECTIONS = 5;
74
+ export const FIND_NAME_CANDIDATES = 128;
75
+ export const FIND_NAME_BATCH = 64;
76
+ export const FIND_FILES_READ = 20;
77
+ export const FIND_READ_PER_CALL = 5;
78
+ export const FIND_EXCERPT_CHARS = 3_000;
79
+ export const FIND_POINTER_EXCERPT_CHARS = 1_500;
80
+ export const FIND_POINTER_MAX = 8;
81
+ export const FIND_NAME_GUARD_MIN = 0.9;
82
+ export const FIND_CONFIRM_MIN = 0.8;
83
+ export const FIND_GOAL_MIN_WORDS = 5;
84
+ export const FIND_QUICK_CANDIDATES = 32;
85
+ export const FIND_QUICK_READ = 8;
86
+ export const FIND_THOROUGH_CANDIDATES = 256;
87
+ export const FIND_THOROUGH_READ = 40;
88
+ export const FIND_EXCERPT_HEAD_CHARS = 1_000;
89
+ export const FIND_CONTENT_MIN = 0.5;
90
+ export const FIND_NAME_GUARD_TOP = 3;
91
+ export const FIND_PATH_WEIGHT = 3;
92
+ export const FIND_GREP_PAGE_SIZE = 1024;
93
+ export const FIND_READ_CHUNK_BYTES = 16_384;
94
+ export const LOCATE_MIN_KB = 19;
95
+ export const LOCATE_VERDICT_MIN = 0.7;
96
+ export const LOCATE_GRAY_MIN = 0.4;
97
+ export const LOCATE_SHRINK_TOP = 3;
98
+ export const LOCATE_SECTION_MAX_LINES = 150;
99
+ export const LOCATE_SECTION_MIN_LINES = 8;
100
+ export const LOCATE_WINDOW_LINES = 80;
101
+ export const LOCATE_LABEL_MAX_CHARS = 120;
102
+ export const LOCATE_WHOLE_MAX_CHARS = STATE_MAX_CHARS * 4;
103
+ export const LOCATE_WHOLE_MAX_BYTES = LOCATE_WHOLE_MAX_CHARS * 4;
104
+ export const LOCATE_READ_BUFFER_BYTES = 64 * 1024;
105
+ // Unmeasured engineering bound: leaves room for multi-window refinement and JSON metadata.
106
+ export const LOCATE_WINDOW_MAX_SERIALIZED_CHARS = STATE_MAX_CHARS / 4;
107
+ // Keep SHA-256 IDs plus options below Windows' command-line limit.
108
+ export const GIT_BLOB_BATCH_SIZE = 400;
109
+ export const RUNNER_PACKAGE_MAX_BYTES = 64 * 1024;
110
+ // ADR 0001: allowed local dependencies, including erased type imports.
111
+ export const IMPORT_LAYERS = {
112
+ core: [
113
+ "core",
114
+ "constants.ts",
115
+ "result.ts",
116
+ "result-types.ts",
117
+ "jev/types.ts",
118
+ ],
119
+ presets: [
120
+ "presets",
121
+ "core",
122
+ "constants.ts",
123
+ "result.ts",
124
+ "result-types.ts",
125
+ "jev/types.ts",
126
+ ],
127
+ adapters: [
128
+ "adapters",
129
+ "core",
130
+ "constants.ts",
131
+ "result.ts",
132
+ "result-types.ts",
133
+ "jev/types.ts",
134
+ ],
135
+ jev: ["jev", "core", "constants.ts", "result.ts", "result-types.ts"],
136
+ texts: ["texts", "constants.ts", "presets"],
137
+ "constants.ts": [],
138
+ "result.ts": ["result-types.ts"],
139
+ "result-types.ts": [],
140
+ };
@@ -0,0 +1,282 @@
1
+ import { posix } from "node:path";
2
+ import { CLOSURE_MAX_CHARS, STATE_MAX_CHARS, UNIT_MAX_CHARS, } from "../constants.js";
3
+ import { truncate } from "./truncate.js";
4
+ export function buildAskClosure({ graph, syntax, paths, intentions, state, }) {
5
+ const limitations = [];
6
+ const dependencyLimit = (path, dependency, cause) => {
7
+ const nonRelative = /^(?:unresolved|ambiguous) import$/.test(cause) &&
8
+ dependency.length > 0 &&
9
+ !dependency.startsWith(".") &&
10
+ !dependency.startsWith("/") &&
11
+ !dependency.startsWith("sys.path");
12
+ return {
13
+ path,
14
+ dependency,
15
+ cause: `${cause}${nonRelative ? " (external or standard-library, or unresolved repository alias)" : ""}`,
16
+ fact: `${path} : ${dependency}, ${cause}${nonRelative ? "; external or standard-library dependency (or unresolved repository alias), not admitted as a repository file" : ""}`,
17
+ next: nonRelative
18
+ ? "Provide the dependency contract in state or a repository-local implementation in paths; do not read outside the repository."
19
+ : "Provide the dependency explicitly in paths.",
20
+ };
21
+ };
22
+ const relevant = new Set(paths);
23
+ const candidates = new Map();
24
+ const originalFiles = state.files &&
25
+ typeof state.files === "object" &&
26
+ !Array.isArray(state.files)
27
+ ? state.files
28
+ : {};
29
+ const contained = (path) => paths.includes(path) || Object.hasOwn(originalFiles, path);
30
+ const declarations = (path, name) => syntax.declarations.filter((item) => item.path === path &&
31
+ (name === "*" ||
32
+ item.name === name ||
33
+ item.name.startsWith(`${name}.`)));
34
+ const mentioned = (name) => intentions.split(/[^\p{L}\p{N}_$.]+/u).includes(name);
35
+ const add = (declaration, direct, depth, typeOnly = false) => {
36
+ if (contained(declaration.path))
37
+ return;
38
+ const changed = declaration.before !== undefined &&
39
+ declaration.before !== declaration.text;
40
+ const priority = mentioned(declaration.name) || direct ? 0 : changed ? 1 : 2;
41
+ const key = `${declaration.path} (${declaration.name})`;
42
+ const previous = candidates.get(key);
43
+ if (!previous ||
44
+ priority < previous.priority ||
45
+ (priority === previous.priority &&
46
+ (Number(typeOnly) < Number(previous.typeOnly) ||
47
+ (typeOnly === previous.typeOnly && depth < previous.depth))))
48
+ candidates.set(key, { declaration, priority, depth, typeOnly });
49
+ };
50
+ const target = (binding) => {
51
+ if (!binding.specifier)
52
+ return binding.path;
53
+ const edges = graph.edges.get(binding.path) ?? [];
54
+ const python = binding.path.endsWith(".py");
55
+ let base;
56
+ if (python) {
57
+ const relative = binding.specifier.match(/^\.+/)?.[0].length ?? 0;
58
+ base = relative
59
+ ? posix.join(posix.dirname(binding.path), ...Array.from({ length: relative - 1 }, () => ".."), binding.specifier.slice(relative).replaceAll(".", "/"))
60
+ : binding.specifier.replaceAll(".", "/");
61
+ }
62
+ else
63
+ base =
64
+ binding.specifier.startsWith(".") || binding.data
65
+ ? posix.join(posix.dirname(binding.path), binding.specifier)
66
+ : binding.specifier;
67
+ base = posix.normalize(base);
68
+ const alternatives = python
69
+ ? [
70
+ base,
71
+ `${base}.py`,
72
+ `${base}/__init__.py`,
73
+ `${base}/${binding.imported}.py`,
74
+ ]
75
+ : [
76
+ base,
77
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"].map((extension) => base.replace(/\.js$/, "") + extension),
78
+ ...["ts", "tsx", "js", "jsx"].map((extension) => `${base}/index.${extension}`),
79
+ ];
80
+ const matches = edges.filter((edge) => alternatives.some((alternative) => edge === alternative ||
81
+ (!binding.specifier.startsWith(".") &&
82
+ edge.endsWith(`/${alternative}`))));
83
+ if (matches.length === 1)
84
+ return matches[0];
85
+ limitations.push(dependencyLimit(binding.path, binding.specifier, matches.length > 1 ? "ambiguous import" : "unresolved import"));
86
+ return undefined;
87
+ };
88
+ const resolve = (path, name, direct, depth, visited, typeOnly = false) => {
89
+ relevant.add(path);
90
+ const key = `${path}:${name}`;
91
+ if (visited.has(key))
92
+ return;
93
+ visited.add(key);
94
+ const found = declarations(path, name);
95
+ for (const declaration of found)
96
+ add(declaration, direct, depth, typeOnly);
97
+ const exports = syntax.bindings.filter((item) => item.path === path &&
98
+ item.reexport &&
99
+ (item.local === name || item.local === "*" || name === "*"));
100
+ if (!found.length && !exports.length && !contained(path))
101
+ limitations.push({
102
+ fact: `${path} (${name}) : used declaration not found`,
103
+ next: "Provide the declaration explicitly in paths.",
104
+ });
105
+ for (const binding of exports) {
106
+ const destination = target(binding);
107
+ if (destination)
108
+ resolve(destination, binding.imported === "*" ? name : binding.imported, direct, depth, visited, typeOnly);
109
+ }
110
+ };
111
+ for (const path of paths) {
112
+ const used = syntax.uses?.get(path) ??
113
+ syntax.declarations
114
+ .filter((item) => item.path === path)
115
+ .flatMap((item) => item.usedNames);
116
+ for (const binding of syntax.bindings.filter((item) => item.path === path &&
117
+ (!item.reexport || path.endsWith("__init__.py")) &&
118
+ (item.used ?? used.includes(item.local)))) {
119
+ const destination = target(binding);
120
+ if (!destination)
121
+ continue;
122
+ if (binding.data) {
123
+ relevant.add(destination);
124
+ const source = syntax.sources?.find((item) => item.path === destination);
125
+ if (source)
126
+ add({
127
+ path: destination,
128
+ name: "data",
129
+ text: source.text,
130
+ ...(syntax.before && Object.hasOwn(syntax.before, destination)
131
+ ? { before: syntax.before[destination] }
132
+ : {}),
133
+ usedNames: [],
134
+ }, true, 1);
135
+ else
136
+ limitations.push({
137
+ fact: `${destination} : data missing from the inventory`,
138
+ next: "Provide this file in paths.",
139
+ });
140
+ }
141
+ else if (binding.imported === "*" && binding.local !== "*") {
142
+ const members = used.filter((item) => item.startsWith(`${binding.local}.`));
143
+ for (const name of members)
144
+ resolve(destination, name.slice(binding.local.length + 1), true, 1, new Set(), binding.valueMembers
145
+ ? !binding.valueMembers.includes(name)
146
+ : binding.typeOnly);
147
+ if (!members.length)
148
+ limitations.push({
149
+ fact: `${path} : namespace ${binding.local} used without a statically resolved member`,
150
+ next: "Provide the namespace declarations explicitly.",
151
+ });
152
+ }
153
+ else
154
+ resolve(destination, binding.imported, true, 1, new Set(), binding.typeOnly);
155
+ }
156
+ if (path.endsWith(".py") &&
157
+ /(^|\/)(test[^/]*|[^/]*_test)\.py$/.test(path)) {
158
+ const fixtures = syntax.declarations.filter((item) => item.fixture &&
159
+ posix.basename(item.path) === "conftest.py" &&
160
+ (posix.dirname(item.path) === "." ||
161
+ path.startsWith(`${posix.dirname(item.path)}/`)));
162
+ // Nearest fixture shadows ancestors, as in pytest.
163
+ fixtures.sort((a, b) => b.path.length - a.path.length);
164
+ const selected = new Set();
165
+ for (const fixture of fixtures) {
166
+ if (selected.has(fixture.name) ||
167
+ (!fixture.autouse && !used.includes(fixture.name)))
168
+ continue;
169
+ selected.add(fixture.name);
170
+ relevant.add(fixture.path);
171
+ add(fixture, true, 1);
172
+ for (const name of fixture.usedNames)
173
+ for (const helper of declarations(fixture.path, name))
174
+ add(helper, false, 1);
175
+ for (const binding of syntax.bindings.filter((item) => item.path === fixture.path &&
176
+ fixture.usedNames.includes(item.local))) {
177
+ const destination = target(binding);
178
+ if (destination)
179
+ resolve(destination, binding.imported, false, 1, new Set());
180
+ }
181
+ }
182
+ }
183
+ }
184
+ for (const limit of syntax.limits)
185
+ if ([...relevant].some((path) => limit.fact.includes(path)))
186
+ limitations.push(limit);
187
+ for (const limit of graph.limits)
188
+ if (relevant.has(limit.path))
189
+ limitations.push(dependencyLimit(limit.path, limit.specifier, `${limit.kind} import`));
190
+ const ordered = [...candidates.values()].sort((a, b) => a.priority - b.priority ||
191
+ Number(a.typeOnly) - Number(b.typeOnly) ||
192
+ Number(b.declaration.before !== undefined &&
193
+ b.declaration.before !== b.declaration.text) -
194
+ Number(a.declaration.before !== undefined &&
195
+ a.declaration.before !== a.declaration.text) ||
196
+ a.depth - b.depth ||
197
+ a.declaration.path.localeCompare(b.declaration.path) ||
198
+ a.declaration.name.localeCompare(b.declaration.name));
199
+ const existing = state.closure &&
200
+ typeof state.closure === "object" &&
201
+ !Array.isArray(state.closure)
202
+ ? state.closure
203
+ : {};
204
+ const closure = { ...existing };
205
+ let nextState = state;
206
+ let budget = JSON.stringify(existing).length - 2;
207
+ const added = [];
208
+ const omitted = [];
209
+ for (const { declaration } of ordered) {
210
+ const { path, name } = declaration;
211
+ const key = `${path} (${name})`;
212
+ const paired = declaration.before !== undefined &&
213
+ declaration.before !== declaration.text;
214
+ if (Object.hasOwn(closure, key))
215
+ continue;
216
+ const beforeBudget = !paired || declaration.before == null
217
+ ? 0
218
+ : declaration.before.length + declaration.text.length > UNIT_MAX_CHARS
219
+ ? Math.floor(UNIT_MAX_CHARS / 2)
220
+ : declaration.before.length;
221
+ const before = !paired
222
+ ? undefined
223
+ : declaration.before == null
224
+ ? declaration.before
225
+ : truncate(declaration.before, beforeBudget);
226
+ const after = truncate(declaration.text, UNIT_MAX_CHARS - (before?.length ?? 0));
227
+ const sliced = after.length < declaration.text.length ||
228
+ (paired && (before?.length ?? 0) < (declaration.before?.length ?? 0));
229
+ const value = !paired
230
+ ? after
231
+ : { before: before ?? null, after: declaration.deleted ? null : after };
232
+ const chars = after.length + (before?.length ?? 0);
233
+ const contribution = JSON.stringify(key).length +
234
+ 1 +
235
+ JSON.stringify(value).length +
236
+ (Object.keys(closure).length ? 1 : 0);
237
+ const candidateState = { ...state, closure: { ...closure, [key]: value } };
238
+ const reason = budget + contribution > CLOSURE_MAX_CHARS
239
+ ? "closure budget exceeded"
240
+ : JSON.stringify(candidateState).length > STATE_MAX_CHARS
241
+ ? "serialized state limit exceeded"
242
+ : undefined;
243
+ if (reason) {
244
+ omitted.push({ path, name, reason });
245
+ limitations.push({
246
+ fact: `${key} : declaration omitted, ${reason}`,
247
+ next: "Provide this declaration in a smaller call.",
248
+ });
249
+ continue;
250
+ }
251
+ closure[key] = value;
252
+ nextState = candidateState;
253
+ budget += contribution;
254
+ added.push({ path, name, chars });
255
+ if (sliced)
256
+ limitations.push({
257
+ fact: `${key} : sliced declaration (${chars} characters)`,
258
+ next: "Read the complete declaration before drawing conclusions about an omitted part.",
259
+ });
260
+ }
261
+ limitations.push({
262
+ fact: "Import closure only, semantic depth 1; files without an import link are not checked.",
263
+ next: "Provide configuration, documentation or data dependencies not linked by imports in paths.",
264
+ });
265
+ const seen = new Map();
266
+ for (const limit of limitations) {
267
+ const key = JSON.stringify(limit.path
268
+ ? [limit.path, limit.cause, limit.dependency]
269
+ : [limit.fact, limit.next]);
270
+ const previous = seen.get(key);
271
+ if (previous) {
272
+ if (limit.cause === "dynamic import") {
273
+ previous.count = (previous.count ?? 1) + 1;
274
+ previous.fact = `${limit.fact} (${previous.count} occurrences)`;
275
+ }
276
+ }
277
+ else
278
+ seen.set(key, { ...limit });
279
+ }
280
+ const unique = [...seen.values()];
281
+ return { state: nextState, added, omitted, limitations: unique };
282
+ }
@@ -0,0 +1 @@
1
+ export {};