jev-agent-tools 0.2.0 → 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 (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. package/src/tools/spec-check.ts +193 -19
@@ -3,6 +3,11 @@ import {
3
3
  collectDocsInventory,
4
4
  docsDeclarationSearch,
5
5
  } from "../adapters/docs.ts";
6
+ import {
7
+ type EvidenceContext,
8
+ resolveEvidenceContext,
9
+ withEvidenceContext,
10
+ } from "../adapters/evidence-context.ts";
6
11
  import { collectUnits } from "../adapters/git.ts";
7
12
  import { resolveBase } from "../adapters/git-base.ts";
8
13
  import { shareGitInventory } from "../adapters/git-inventory.ts";
@@ -12,6 +17,7 @@ import {
12
17
  DOCS_DISPLAY_MAX_SECTIONS,
13
18
  DOCS_MAX_SECTIONS,
14
19
  STATE_MAX_CHARS,
20
+ TIMEOUT_MS,
15
21
  } from "../constants.ts";
16
22
  import { collectDocsCandidates } from "../core/docs.ts";
17
23
  import {
@@ -21,6 +27,11 @@ import {
21
27
  type Envelope,
22
28
  type Limitation,
23
29
  } from "../core/output.ts";
30
+ import {
31
+ type Cause,
32
+ known,
33
+ type ResultReportV1,
34
+ } from "../core/result-report.ts";
24
35
  import type { Judgment } from "../jev/types.ts";
25
36
  import {
26
37
  type DocsFinding,
@@ -29,6 +40,7 @@ import {
29
40
  } from "../presets/docs.ts";
30
41
  import type { ToolDependencies } from "../runtime.ts";
31
42
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
43
+ import { ReviewReport, reportMetrics } from "./review-report.ts";
32
44
 
33
45
  export interface DocsCheckInput {
34
46
  cwd: string;
@@ -36,6 +48,7 @@ export interface DocsCheckInput {
36
48
  signal?: AbortSignal;
37
49
  budgetMs?: number;
38
50
  maxCalls?: number;
51
+ evidenceContext?: EvidenceContext;
39
52
  }
40
53
  export interface DocsCheckResult {
41
54
  ok: boolean;
@@ -47,6 +60,7 @@ export interface DocsCheckResult {
47
60
  envelope: Envelope;
48
61
  judgments: Judgment[];
49
62
  findings: DocsFinding[];
63
+ result: ResultReportV1;
50
64
  unjudged?: { count: number; sections: readonly string[]; truncated: boolean };
51
65
  collection?: {
52
66
  kind: "collection_budget";
@@ -61,6 +75,17 @@ export async function runDocsCheck(
61
75
  input: DocsCheckInput,
62
76
  ): Promise<DocsCheckResult> {
63
77
  const started = performance.now();
78
+ const report = new ReviewReport();
79
+ const admission = input.evidenceContext
80
+ ? undefined
81
+ : await resolveEvidenceContext(input.cwd, undefined, {
82
+ exec: deps.exec,
83
+ signal: input.signal,
84
+ origin: deps.evidenceOrigin,
85
+ });
86
+ const evidenceContext = input.evidenceContext ?? admission?.context;
87
+ if (!evidenceContext) throw new Error("Evidence context was not established");
88
+ evidenceContext.requestedBase = input.base ?? "HEAD";
64
89
  deps = { ...deps, exec: shareGitInventory(deps.exec) };
65
90
  const timeout =
66
91
  input.budgetMs === undefined
@@ -84,7 +109,10 @@ export async function runDocsCheck(
84
109
  let budget: BudgetRefusal | undefined;
85
110
  let sent = 0;
86
111
  let emptyBase: string | undefined;
87
- const finish = (refusal?: string): DocsCheckResult => {
112
+ const finish = (
113
+ refusal?: string,
114
+ cause: Cause = "internal_error",
115
+ ): DocsCheckResult => {
88
116
  if (unchecked.length > 5) {
89
117
  unjudged = {
90
118
  count: unchecked.length,
@@ -157,7 +185,9 @@ export async function runDocsCheck(
157
185
  !collection &&
158
186
  !unchecked.length &&
159
187
  !findings.length &&
160
- emptyBase === undefined
188
+ emptyBase === undefined &&
189
+ report.items.size > 0 &&
190
+ [...report.items.values()].every((item) => item.treatment === "judged")
161
191
  ? {
162
192
  lines: [
163
193
  {
@@ -171,7 +201,10 @@ export async function runDocsCheck(
171
201
  yield: {
172
202
  calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
173
203
  questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
174
- costUsd: judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0),
204
+ costUsd:
205
+ judgments.length && judgments.every((j) => j.usage !== undefined)
206
+ ? judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0)
207
+ : undefined,
175
208
  cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
176
209
  cacheRequests: judgments.reduce(
177
210
  (n, j) => n + (j.cacheRequests ?? 0),
@@ -180,6 +213,41 @@ export async function runDocsCheck(
180
213
  elapsedMs: performance.now() - started,
181
214
  },
182
215
  });
216
+ report.diagnose(
217
+ "collection_omitted",
218
+ "Existing documentation sentences only; missing documentation is not exhaustively detected (measured 2/45 obligations across 180 partial commits).",
219
+ undefined,
220
+ [],
221
+ false,
222
+ );
223
+ if (emptyBase !== undefined)
224
+ report.diagnose(
225
+ "no_changed_units",
226
+ `No changed units against ${emptyBase}`,
227
+ emptyBase,
228
+ [],
229
+ false,
230
+ );
231
+ if (!refusal && emptyBase === undefined && !report.items.size)
232
+ report.diagnose(
233
+ "collection_empty",
234
+ "No matching documentation sections were collected; this does not establish documentation completeness.",
235
+ undefined,
236
+ [],
237
+ false,
238
+ );
239
+ if (refusal) {
240
+ report.refusal =
241
+ cause === "invalid_base" ||
242
+ cause === "invalid_root" ||
243
+ cause === "git_failure";
244
+ report.diagnose(cause, refusal);
245
+ }
246
+ const result = report.build(
247
+ "jev_check_diff",
248
+ evidenceContext,
249
+ reportMetrics(envelope),
250
+ );
183
251
  return {
184
252
  ok: !refusal,
185
253
  status: timeout?.aborted
@@ -192,14 +260,15 @@ export async function runDocsCheck(
192
260
  envelope,
193
261
  judgments,
194
262
  findings,
263
+ result,
195
264
  ...(collection ? { collection } : {}),
196
265
  ...(unjudged ? { unjudged } : {}),
197
266
  };
198
267
  };
199
268
  const client = deps.client;
200
- if (!client) return finish(NOT_CONFIGURED);
269
+ if (!client) return finish(NOT_CONFIGURED, "not_configured");
201
270
  const sessionRefusal = deps.runtime.session.refusal();
202
- if (sessionRefusal) return finish(sessionRefusal);
271
+ if (sessionRefusal) return finish(sessionRefusal, "session_budget");
203
272
  try {
204
273
  const comparison = await resolveBase(
205
274
  deps.exec,
@@ -207,8 +276,10 @@ export async function runDocsCheck(
207
276
  input.base,
208
277
  signal,
209
278
  );
210
- if (!comparison.ok) return finish(comparison.error);
279
+ if (!comparison.ok)
280
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
211
281
  const base = comparison.base;
282
+ evidenceContext.resolvedBase = base;
212
283
  const analysis = await createAnalysisContext();
213
284
  const [collected, inventory] = await Promise.all([
214
285
  collectUnits(
@@ -218,15 +289,46 @@ export async function runDocsCheck(
218
289
  ),
219
290
  collectDocsInventory(deps.exec, input.cwd, signal),
220
291
  ]);
221
- if (!collected.ok) return finish(collected.error);
222
- if (!inventory.ok) return finish(inventory.error);
223
- for (const limit of collected.limits)
292
+ if (!collected.ok)
293
+ return finish(collected.error, collected.cause ?? "git_failure");
294
+ if (!inventory.ok)
295
+ return finish(inventory.error, inventory.cause ?? "file_unavailable");
296
+ const repositoryRoot = await deps.exec(
297
+ "git",
298
+ ["rev-parse", "--show-toplevel"],
299
+ { cwd: input.cwd, timeout: TIMEOUT_MS, signal },
300
+ );
301
+ if (
302
+ !repositoryRoot.code &&
303
+ !repositoryRoot.killed &&
304
+ evidenceContext.effectiveRoot
305
+ )
306
+ evidenceContext.effectiveRoot.path = repositoryRoot.stdout.trim();
307
+ for (const limit of collected.limits) {
308
+ report.diagnose(
309
+ limit.kind === "secret_pattern"
310
+ ? "secret_pattern"
311
+ : "collection_omitted",
312
+ limit.kind,
313
+ limit.file,
314
+ );
224
315
  limitations.push({
225
316
  fact: `${limit.file} : ${limit.kind}`,
226
317
  next: "Read the complete change before concluding.",
227
318
  });
319
+ }
228
320
  if (!collected.units.length) {
229
321
  emptyBase = base;
322
+ report.inventories.push({
323
+ id: "changed-units",
324
+ kind: "units",
325
+ rules: ["Changed source units against resolved base"],
326
+ restrictions: [],
327
+ discovered: known(0),
328
+ considered: known(0),
329
+ scopeRestricted: false,
330
+ criteria: [],
331
+ });
230
332
  return finish();
231
333
  }
232
334
  const collectDeadline = performance.now() + DOCS_COLLECT_BUDGET_MS;
@@ -243,6 +345,65 @@ export async function runDocsCheck(
243
345
  performance.now() >= collectDeadline || !!signal?.aborted,
244
346
  },
245
347
  );
348
+ report.inventories.push({
349
+ id: "docs-sections",
350
+ kind: "sections",
351
+ rules: [
352
+ "Tracked Markdown sections referring to changed declarations or static import closure",
353
+ `At most ${DOCS_MAX_SECTIONS} admitted sections`,
354
+ ],
355
+ restrictions: [],
356
+ discovered: known(
357
+ candidates.candidates.length + candidates.omitted.length,
358
+ ),
359
+ considered: known(candidates.candidates.length),
360
+ scopeRestricted: false,
361
+ criteria: [],
362
+ });
363
+ for (const candidate of candidates.candidates)
364
+ report.expect(
365
+ `docs:${candidate.path}:${candidate.start}`,
366
+ `${candidate.path} § ${candidate.heading}`,
367
+ "section",
368
+ );
369
+ for (const candidate of candidates.omitted) {
370
+ const id = `docs:${candidate.path}:${candidate.start}`;
371
+ report.expect(id, `${candidate.path} § ${candidate.heading}`, "section");
372
+ report.diagnose(
373
+ "collection_omitted",
374
+ "Section exceeds DOCS_MAX_SECTIONS",
375
+ candidate.path,
376
+ [id],
377
+ );
378
+ }
379
+ for (const limit of candidates.limits) {
380
+ if (limit.kind === "collection_budget") {
381
+ report.missingWork = true;
382
+ report.diagnose(
383
+ "collection_omitted",
384
+ limit.reason,
385
+ limit.path,
386
+ [],
387
+ true,
388
+ [...(limit.sections ?? [])],
389
+ );
390
+ } else
391
+ report.diagnose(
392
+ "dynamic_dependency",
393
+ limit.reason,
394
+ limit.path,
395
+ [],
396
+ false,
397
+ );
398
+ }
399
+ for (const limit of inventory.limits)
400
+ report.diagnose(
401
+ limit.cause === "secret_pattern"
402
+ ? "secret_pattern"
403
+ : "collection_omitted",
404
+ limit.reason,
405
+ limit.path,
406
+ );
246
407
  for (const limit of inventory.limits)
247
408
  limitations.push({
248
409
  fact: `${limit.path} : ${limit.reason}`,
@@ -290,61 +451,101 @@ export async function runDocsCheck(
290
451
  await Promise.all(
291
452
  candidates.candidates.map(async (candidate) => {
292
453
  const label = `${candidate.path} § ${candidate.heading}`;
454
+ const reportId = `docs:${candidate.path}:${candidate.start}`;
293
455
  if (
294
456
  candidate.units.some(
295
457
  (unit) => unit.before === null && unit.after === null,
296
458
  )
297
459
  ) {
298
460
  unchecked.push(`${label} (changed source unavailable)`);
461
+ report.diagnose(
462
+ "binary_or_non_utf8",
463
+ "Changed source unavailable",
464
+ label,
465
+ [reportId],
466
+ );
299
467
  return;
300
468
  }
301
469
  if (budget?.kind === "session") {
302
470
  unchecked.push(`${label} (${budget.message})`);
471
+ report.diagnose("session_budget", budget.message, label, [reportId]);
303
472
  return;
304
473
  }
305
474
  const prepared = prepareDocsCheck(candidate);
475
+ const state = withEvidenceContext(prepared.state, evidenceContext);
306
476
  if (
307
- JSON.stringify(prepared.state).length > STATE_MAX_CHARS ||
477
+ JSON.stringify(state).length > STATE_MAX_CHARS ||
308
478
  candidate.sentences.length + 1 > CHOICE_MAX_OPTIONS
309
479
  ) {
310
480
  unchecked.push(`${label} (state or pointer too large)`);
481
+ report.diagnose(
482
+ "evidence_too_large",
483
+ "Section state or sentence pointer exceeds limits",
484
+ label,
485
+ [reportId],
486
+ );
311
487
  return;
312
488
  }
313
489
  if (signal?.aborted) {
314
490
  unchecked.push(`${label} (budget exceeded or canceled)`);
491
+ report.diagnose(
492
+ "cancelled",
493
+ "Collection budget exceeded or check canceled",
494
+ label,
495
+ [reportId],
496
+ );
315
497
  return;
316
498
  }
317
- const judgment = await client.judge(
318
- prepared.state,
319
- prepared.questions,
320
- {
321
- signal,
322
- ...deps.runtime.session.requestGate(),
323
- beforeRequest(questionCount) {
324
- if (budget?.kind === "session")
325
- return { ok: false, error: budget.message };
326
- if (signal?.aborted)
327
- return {
328
- ok: false,
329
- error: "Budget exceeded or call canceled.",
330
- };
331
- if (input.maxCalls !== undefined && sent >= input.maxCalls) {
332
- budget = {
333
- kind: "max_calls",
334
- message: `max_calls=${input.maxCalls} reached`,
335
- };
336
- return { ok: false, error: budget.message };
337
- }
338
- const admitted = deps.runtime.session.admit(questionCount);
339
- if (!admitted.ok)
340
- budget = { kind: "session", message: admitted.error };
341
- else sent++;
342
- return admitted;
343
- },
344
- onUsage: (usage) => deps.runtime.session.recordUsage(usage),
499
+ const judgment = await client.judge(state, prepared.questions, {
500
+ signal,
501
+ groups: [["status", "sentence"]],
502
+ ...deps.runtime.session.requestGate(),
503
+ admissionCause: () =>
504
+ budget?.kind === "session" ? "session_budget" : "call_budget",
505
+ beforeRequest(questionCount) {
506
+ if (budget?.kind === "session")
507
+ return { ok: false, error: budget.message };
508
+ if (signal?.aborted)
509
+ return {
510
+ ok: false,
511
+ error: "Budget exceeded or call canceled.",
512
+ };
513
+ if (input.maxCalls !== undefined && sent >= input.maxCalls) {
514
+ budget = {
515
+ kind: "max_calls",
516
+ message: `max_calls=${input.maxCalls} reached`,
517
+ };
518
+ return { ok: false, error: budget.message };
519
+ }
520
+ const admitted = deps.runtime.session.admit(questionCount);
521
+ if (!admitted.ok)
522
+ budget = { kind: "session", message: admitted.error };
523
+ else sent++;
524
+ return admitted;
345
525
  },
346
- );
526
+ onUsage: (usage) => deps.runtime.session.recordUsage(usage),
527
+ });
347
528
  judgments.push(judgment);
529
+ if (!judgment.ok) report.failure(judgment, [reportId]);
530
+ else {
531
+ const status = judgment.answers.status;
532
+ const pointer = judgment.answers.sentence;
533
+ const finding = readDocsJudgment(candidate, judgment.answers);
534
+ const item = report.items.get(reportId);
535
+ if (finding && item)
536
+ item.label = `${label} — ${finding.sentence ? JSON.stringify(finding.sentence.text) : "sentence not identified"} — after ${finding.units.map((unit) => `${unit.name} (${unit.file})`).join(", ")}`;
537
+ report.answer(
538
+ reportId,
539
+ status,
540
+ { band: finding?.band ?? "verdict", reason: finding?.reason },
541
+ [
542
+ pointer ?? {
543
+ type: "unjudged",
544
+ reason: "Required sentence pointer missing",
545
+ },
546
+ ],
547
+ );
548
+ }
348
549
  if (!judgment.ok) {
349
550
  unchecked.push(`${label} (${judgment.error})`);
350
551
  return;
@@ -394,6 +595,7 @@ export async function runDocsCheck(
394
595
  if (signal?.aborted)
395
596
  return finish(
396
597
  timeout?.aborted ? "Docs budget exceeded." : "Docs check canceled.",
598
+ "cancelled",
397
599
  );
398
600
  throw error;
399
601
  }