jev-agent-tools 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -0
  4. package/docs/design.md +187 -0
  5. package/docs/tools/jev_ask.md +92 -0
  6. package/docs/tools/jev_ask_files.md +80 -0
  7. package/docs/tools/jev_check_diff.md +77 -0
  8. package/docs/tools/jev_find_files.md +59 -0
  9. package/docs/tools/jev_locate_in_file.md +51 -0
  10. package/docs/tools/jev_select_tests.md +59 -0
  11. package/package.json +85 -0
  12. package/rules/jev-ask.md +4 -0
  13. package/src/adapters/analysis-context.ts +100 -0
  14. package/src/adapters/ask-files.ts +236 -0
  15. package/src/adapters/ask-proof.ts +202 -0
  16. package/src/adapters/ask-syntax.ts +463 -0
  17. package/src/adapters/command.ts +240 -0
  18. package/src/adapters/docs.ts +222 -0
  19. package/src/adapters/files.ts +411 -0
  20. package/src/adapters/find.ts +151 -0
  21. package/src/adapters/git-base.ts +32 -0
  22. package/src/adapters/git-inventory.ts +94 -0
  23. package/src/adapters/git.ts +525 -0
  24. package/src/adapters/locate-file.ts +197 -0
  25. package/src/adapters/output-lines.ts +50 -0
  26. package/src/adapters/risk-callers.ts +525 -0
  27. package/src/adapters/runner-version.ts +102 -0
  28. package/src/adapters/syntax.ts +229 -0
  29. package/src/adapters/test-inventory.ts +168 -0
  30. package/src/adapters/usage.ts +21 -0
  31. package/src/adapters/utf8.ts +57 -0
  32. package/src/constants.ts +109 -0
  33. package/src/core/ask-closure.ts +419 -0
  34. package/src/core/ask-proof.ts +32 -0
  35. package/src/core/ask-references.ts +249 -0
  36. package/src/core/asks.ts +616 -0
  37. package/src/core/batches.ts +83 -0
  38. package/src/core/command-output.ts +249 -0
  39. package/src/core/diff.ts +226 -0
  40. package/src/core/docs.ts +399 -0
  41. package/src/core/find.ts +157 -0
  42. package/src/core/git.ts +5 -0
  43. package/src/core/import-boundaries.ts +102 -0
  44. package/src/core/imports.ts +691 -0
  45. package/src/core/integrity.ts +64 -0
  46. package/src/core/lexical.ts +130 -0
  47. package/src/core/locate.ts +213 -0
  48. package/src/core/output.ts +264 -0
  49. package/src/core/pointer.ts +51 -0
  50. package/src/core/risk-callers.ts +1270 -0
  51. package/src/core/runner-version.ts +66 -0
  52. package/src/core/sections.ts +269 -0
  53. package/src/core/state.ts +53 -0
  54. package/src/core/syntax.ts +8 -0
  55. package/src/core/test-commands.ts +430 -0
  56. package/src/core/test-coverage.ts +103 -0
  57. package/src/core/test-discovery.ts +1695 -0
  58. package/src/core/test-evidence.ts +649 -0
  59. package/src/core/test-state.ts +99 -0
  60. package/src/core/truncate.ts +14 -0
  61. package/src/core/units.ts +531 -0
  62. package/src/describe.ts +26 -0
  63. package/src/guide.ts +42 -0
  64. package/src/host.ts +22 -0
  65. package/src/index.ts +40 -0
  66. package/src/jev/client.ts +505 -0
  67. package/src/jev/pool.ts +60 -0
  68. package/src/jev/types.ts +60 -0
  69. package/src/presets/docs.ts +85 -0
  70. package/src/presets/risk.ts +263 -0
  71. package/src/presets/spec.ts +111 -0
  72. package/src/presets/witnesses.ts +313 -0
  73. package/src/render.ts +45 -0
  74. package/src/result.ts +4 -0
  75. package/src/run-end.ts +157 -0
  76. package/src/runtime.ts +16 -0
  77. package/src/session.ts +106 -0
  78. package/src/texts/ask-files.ts +2 -0
  79. package/src/texts/ask.ts +4 -0
  80. package/src/texts/check-diff.ts +24 -0
  81. package/src/texts/configuration.ts +2 -0
  82. package/src/texts/find.ts +14 -0
  83. package/src/texts/guide.ts +16 -0
  84. package/src/texts/locate.ts +10 -0
  85. package/src/texts/run-end.ts +13 -0
  86. package/src/texts/select-tests.ts +3 -0
  87. package/src/tools/ask-files.ts +263 -0
  88. package/src/tools/ask-schema.ts +116 -0
  89. package/src/tools/ask.ts +925 -0
  90. package/src/tools/check-diff.ts +510 -0
  91. package/src/tools/docs-check.ts +399 -0
  92. package/src/tools/find.ts +529 -0
  93. package/src/tools/locate.ts +369 -0
  94. package/src/tools/select-tests.ts +746 -0
  95. package/src/tools/spec-check.ts +210 -0
@@ -0,0 +1,399 @@
1
+ import { createAnalysisContext } from "../adapters/analysis-context.ts";
2
+ import {
3
+ collectDocsInventory,
4
+ docsDeclarationSearch,
5
+ } from "../adapters/docs.ts";
6
+ import { collectUnits } from "../adapters/git.ts";
7
+ import { resolveBase } from "../adapters/git-base.ts";
8
+ import { shareGitInventory } from "../adapters/git-inventory.ts";
9
+ import {
10
+ CHOICE_MAX_OPTIONS,
11
+ DOCS_COLLECT_BUDGET_MS,
12
+ DOCS_DISPLAY_MAX_SECTIONS,
13
+ DOCS_MAX_SECTIONS,
14
+ STATE_MAX_CHARS,
15
+ } from "../constants.ts";
16
+ import { collectDocsCandidates } from "../core/docs.ts";
17
+ import {
18
+ type AnswerInput,
19
+ type BudgetRefusal,
20
+ buildEnvelope,
21
+ type Envelope,
22
+ type Limitation,
23
+ } from "../core/output.ts";
24
+ import type { Judgment } from "../jev/types.ts";
25
+ import {
26
+ type DocsFinding,
27
+ prepareDocsCheck,
28
+ readDocsJudgment,
29
+ } from "../presets/docs.ts";
30
+ import type { ToolDependencies } from "../runtime.ts";
31
+ import { NOT_CONFIGURED } from "../texts/configuration.ts";
32
+
33
+ export interface DocsCheckInput {
34
+ cwd: string;
35
+ base?: string;
36
+ signal?: AbortSignal;
37
+ budgetMs?: number;
38
+ maxCalls?: number;
39
+ }
40
+ export interface DocsCheckResult {
41
+ ok: boolean;
42
+ status:
43
+ | "completed"
44
+ | "collect_budget_reached"
45
+ | "budget_exceeded"
46
+ | "refused";
47
+ envelope: Envelope;
48
+ judgments: Judgment[];
49
+ findings: DocsFinding[];
50
+ unjudged?: { count: number; sections: readonly string[]; truncated: boolean };
51
+ collection?: {
52
+ kind: "collection_budget";
53
+ count: number;
54
+ sections: readonly string[];
55
+ truncated: boolean;
56
+ };
57
+ }
58
+ /** Shared judgment seam: no guide delivery, rendering, or session answer recording. */
59
+ export async function runDocsCheck(
60
+ deps: ToolDependencies,
61
+ input: DocsCheckInput,
62
+ ): Promise<DocsCheckResult> {
63
+ const started = performance.now();
64
+ deps = { ...deps, exec: shareGitInventory(deps.exec) };
65
+ const timeout =
66
+ input.budgetMs === undefined
67
+ ? undefined
68
+ : AbortSignal.timeout(Math.max(0, Math.ceil(input.budgetMs)));
69
+ const signals = [input.signal, timeout].filter(
70
+ (value): value is AbortSignal => value !== undefined,
71
+ );
72
+ const signal = signals.length ? AbortSignal.any(signals) : undefined;
73
+ const judgments: Judgment[] = [];
74
+ const findings: DocsFinding[] = [];
75
+ const unchecked: string[] = [];
76
+ let unjudged: DocsCheckResult["unjudged"];
77
+ let collection: DocsCheckResult["collection"];
78
+ const limitations: Limitation[] = [
79
+ {
80
+ fact: "Measured D22 limit: 2/45 documentation obligations found across 180 partial got/zod commits.",
81
+ next: "Also review missing documentation: this check evaluates existing sentences, not documentation completeness.",
82
+ },
83
+ ];
84
+ let budget: BudgetRefusal | undefined;
85
+ let sent = 0;
86
+ let emptyBase: string | undefined;
87
+ const finish = (refusal?: string): DocsCheckResult => {
88
+ if (unchecked.length > 5) {
89
+ unjudged = {
90
+ count: unchecked.length,
91
+ sections: unchecked.slice(0, DOCS_MAX_SECTIONS),
92
+ truncated: unchecked.length > DOCS_MAX_SECTIONS,
93
+ };
94
+ limitations.push({
95
+ fact: `${unchecked.length} matching sections unjudged.`,
96
+ next: "Narrow the diff with base=… or rerun on the relevant documentation.",
97
+ });
98
+ }
99
+ const answers: AnswerInput[] = findings.map((finding) => ({
100
+ label: `${finding.section.path}:${finding.section.start}-${finding.section.end} § ${finding.section.heading} — ${finding.sentence ? JSON.stringify(finding.sentence.text) : "sentence not identified"} — ${finding.band === "verdict" ? "now false" : "needs checking"} after ${finding.units.map((unit) => `${unit.name} (${unit.file})`).join(", ")}`,
101
+ value: { head: "now_false", p: finding.probability },
102
+ band: finding.band,
103
+ reason: finding.reason,
104
+ }));
105
+ const envelope = buildEnvelope({
106
+ answers,
107
+ limitations:
108
+ emptyBase !== undefined && !refusal
109
+ ? [
110
+ ...limitations,
111
+ {
112
+ fact: `no changed units against ${emptyBase}`,
113
+ next: "nothing judged; pass base= or check the working directory",
114
+ },
115
+ ]
116
+ : limitations,
117
+ ...(collection || unjudged
118
+ ? {
119
+ lines: [
120
+ ...(collection
121
+ ? [
122
+ {
123
+ type: "collection" as const,
124
+ title:
125
+ "anchored documentation unexplored; directly cited sections were checked",
126
+ count: collection.count,
127
+ items: collection.sections.slice(
128
+ 0,
129
+ DOCS_DISPLAY_MAX_SECTIONS,
130
+ ),
131
+ next: "Read these sections; narrow the diff with base=… to target their dependency.",
132
+ },
133
+ ]
134
+ : []),
135
+ ...(unjudged
136
+ ? [
137
+ {
138
+ type: "collection" as const,
139
+ title: "matching documentation unjudged",
140
+ count: unjudged.count,
141
+ items: unjudged.sections.slice(
142
+ 0,
143
+ DOCS_DISPLAY_MAX_SECTIONS,
144
+ ),
145
+ next: "Read these sections or rerun on the relevant documentation.",
146
+ },
147
+ ]
148
+ : []),
149
+ ],
150
+ }
151
+ : {}),
152
+ unchecked: unjudged ? [] : unchecked,
153
+ budget,
154
+ refusal,
155
+ ...(!refusal &&
156
+ !budget &&
157
+ !collection &&
158
+ !unchecked.length &&
159
+ !findings.length &&
160
+ emptyBase === undefined
161
+ ? {
162
+ lines: [
163
+ {
164
+ type: "list" as const,
165
+ title: "docs: no stale sentences reported",
166
+ items: [],
167
+ },
168
+ ],
169
+ }
170
+ : {}),
171
+ yield: {
172
+ calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
173
+ questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
174
+ costUsd: judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0),
175
+ cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
176
+ cacheRequests: judgments.reduce(
177
+ (n, j) => n + (j.cacheRequests ?? 0),
178
+ 0,
179
+ ),
180
+ elapsedMs: performance.now() - started,
181
+ },
182
+ });
183
+ return {
184
+ ok: !refusal,
185
+ status: timeout?.aborted
186
+ ? "budget_exceeded"
187
+ : refusal
188
+ ? "refused"
189
+ : collection
190
+ ? "collect_budget_reached"
191
+ : "completed",
192
+ envelope,
193
+ judgments,
194
+ findings,
195
+ ...(collection ? { collection } : {}),
196
+ ...(unjudged ? { unjudged } : {}),
197
+ };
198
+ };
199
+ const client = deps.client;
200
+ if (!client) return finish(NOT_CONFIGURED);
201
+ const sessionRefusal = deps.runtime.session.refusal();
202
+ if (sessionRefusal) return finish(sessionRefusal);
203
+ try {
204
+ const comparison = await resolveBase(
205
+ deps.exec,
206
+ input.cwd,
207
+ input.base,
208
+ signal,
209
+ );
210
+ if (!comparison.ok) return finish(comparison.error);
211
+ const base = comparison.base;
212
+ const analysis = await createAnalysisContext();
213
+ const [collected, inventory] = await Promise.all([
214
+ collectUnits(
215
+ deps.exec,
216
+ { cwd: input.cwd, base, signal },
217
+ analysis.parser,
218
+ ),
219
+ collectDocsInventory(deps.exec, input.cwd, signal),
220
+ ]);
221
+ if (!collected.ok) return finish(collected.error);
222
+ if (!inventory.ok) return finish(inventory.error);
223
+ for (const limit of collected.limits)
224
+ limitations.push({
225
+ fact: `${limit.file} : ${limit.kind}`,
226
+ next: "Read the complete change before concluding.",
227
+ });
228
+ if (!collected.units.length) {
229
+ emptyBase = base;
230
+ return finish();
231
+ }
232
+ const collectDeadline = performance.now() + DOCS_COLLECT_BUDGET_MS;
233
+ const candidates = await collectDocsCandidates(
234
+ inventory.files,
235
+ collected.units,
236
+ {
237
+ known: inventory.tracked,
238
+ read: inventory.read,
239
+ declarations: docsDeclarationSearch(deps.exec, inventory, signal),
240
+ changedFiles: collected.files,
241
+ lexical: analysis,
242
+ shouldStop: () =>
243
+ performance.now() >= collectDeadline || !!signal?.aborted,
244
+ },
245
+ );
246
+ for (const limit of inventory.limits)
247
+ limitations.push({
248
+ fact: `${limit.path} : ${limit.reason}`,
249
+ next: "Read the omitted file; its documentation or imports are unchecked.",
250
+ });
251
+ for (const limit of candidates.limits) {
252
+ if (limit.kind === "collection_budget") {
253
+ const changedParts = collected.units.map((unit) =>
254
+ unit.file.split("/"),
255
+ );
256
+ const sections = (limit.sections ?? [])
257
+ .map((section) => {
258
+ const parts = (section.split(" § ")[0] ?? "").split("/");
259
+ const proximity = changedParts.reduce((best, changed) => {
260
+ let common = 0;
261
+ while (
262
+ common < parts.length &&
263
+ common < changed.length &&
264
+ parts[common] === changed[common]
265
+ )
266
+ common++;
267
+ return Math.max(best, common);
268
+ }, 0);
269
+ return { section, proximity };
270
+ })
271
+ .sort((a, b) => b.proximity - a.proximity);
272
+ collection = {
273
+ kind: limit.kind,
274
+ count: sections.length,
275
+ sections: sections
276
+ .slice(0, DOCS_MAX_SECTIONS)
277
+ .map((item) => item.section),
278
+ truncated: sections.length > DOCS_MAX_SECTIONS,
279
+ };
280
+ } else
281
+ limitations.push({
282
+ fact: `${limit.path} : ${limit.reason}`,
283
+ next: "Read the unresolved dependency; closure follows static imports only.",
284
+ });
285
+ }
286
+ for (const omitted of candidates.omitted)
287
+ unchecked.push(
288
+ `${omitted.path} § ${omitted.heading} (DOCS_MAX_SECTIONS)`,
289
+ );
290
+ await Promise.all(
291
+ candidates.candidates.map(async (candidate) => {
292
+ const label = `${candidate.path} § ${candidate.heading}`;
293
+ if (
294
+ candidate.units.some(
295
+ (unit) => unit.before === null && unit.after === null,
296
+ )
297
+ ) {
298
+ unchecked.push(`${label} (changed source unavailable)`);
299
+ return;
300
+ }
301
+ if (budget?.kind === "session") {
302
+ unchecked.push(`${label} (${budget.message})`);
303
+ return;
304
+ }
305
+ const prepared = prepareDocsCheck(candidate);
306
+ if (
307
+ JSON.stringify(prepared.state).length > STATE_MAX_CHARS ||
308
+ candidate.sentences.length + 1 > CHOICE_MAX_OPTIONS
309
+ ) {
310
+ unchecked.push(`${label} (state or pointer too large)`);
311
+ return;
312
+ }
313
+ if (signal?.aborted) {
314
+ unchecked.push(`${label} (budget exceeded or canceled)`);
315
+ return;
316
+ }
317
+ const judgment = await client.judge(
318
+ prepared.state,
319
+ prepared.questions,
320
+ {
321
+ signal,
322
+ beforeRequest(questionCount) {
323
+ if (budget?.kind === "session")
324
+ return { ok: false, error: budget.message };
325
+ if (signal?.aborted)
326
+ return {
327
+ ok: false,
328
+ error: "Budget exceeded or call canceled.",
329
+ };
330
+ if (input.maxCalls !== undefined && sent >= input.maxCalls) {
331
+ budget = {
332
+ kind: "max_calls",
333
+ message: `max_calls=${input.maxCalls} reached`,
334
+ };
335
+ return { ok: false, error: budget.message };
336
+ }
337
+ const admitted = deps.runtime.session.admit(questionCount);
338
+ if (!admitted.ok)
339
+ budget = { kind: "session", message: admitted.error };
340
+ else sent++;
341
+ return admitted;
342
+ },
343
+ onUsage: (usage) => deps.runtime.session.recordUsage(usage),
344
+ },
345
+ );
346
+ judgments.push(judgment);
347
+ if (!judgment.ok) {
348
+ unchecked.push(`${label} (${judgment.error})`);
349
+ return;
350
+ }
351
+ if (
352
+ !judgment.answers.status ||
353
+ !judgment.answers.sentence ||
354
+ Object.values(judgment.answers).some(
355
+ (answer) => answer.type === "unjudged",
356
+ )
357
+ ) {
358
+ unchecked.push(`${label} (unjudged question)`);
359
+ const finding = readDocsJudgment(candidate, judgment.answers);
360
+ if (finding)
361
+ findings.push({
362
+ ...finding,
363
+ band: "unsure",
364
+ reason: "unjudged question: read the section",
365
+ });
366
+ return;
367
+ }
368
+ const finding = readDocsJudgment(candidate, judgment.answers);
369
+ if (finding) {
370
+ const incomplete = candidate.units.some((unit) =>
371
+ collected.limits.some((limit) => limit.file === unit.file),
372
+ );
373
+ findings.push(
374
+ incomplete
375
+ ? {
376
+ ...finding,
377
+ band: "unsure",
378
+ reason: "incomplete change evidence: read the omitted pieces",
379
+ }
380
+ : finding,
381
+ );
382
+ }
383
+ }),
384
+ );
385
+ findings.sort(
386
+ (a, b) =>
387
+ a.section.path.localeCompare(b.section.path) ||
388
+ a.section.start - b.section.start,
389
+ );
390
+ unchecked.sort();
391
+ return finish();
392
+ } catch (error) {
393
+ if (signal?.aborted)
394
+ return finish(
395
+ timeout?.aborted ? "Docs budget exceeded." : "Docs check canceled.",
396
+ );
397
+ throw error;
398
+ }
399
+ }