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
@@ -1,16 +1,31 @@
1
1
  import { createAnalysisContext } from "../adapters/analysis-context.js";
2
2
  import { collectDocsInventory, docsDeclarationSearch, } from "../adapters/docs.js";
3
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
3
4
  import { collectUnits } from "../adapters/git.js";
4
5
  import { resolveBase } from "../adapters/git-base.js";
5
6
  import { shareGitInventory } from "../adapters/git-inventory.js";
6
- import { CHOICE_MAX_OPTIONS, DOCS_COLLECT_BUDGET_MS, DOCS_DISPLAY_MAX_SECTIONS, DOCS_MAX_SECTIONS, STATE_MAX_CHARS, } from "../constants.js";
7
+ import { CHOICE_MAX_OPTIONS, DOCS_COLLECT_BUDGET_MS, DOCS_DISPLAY_MAX_SECTIONS, DOCS_MAX_SECTIONS, STATE_MAX_CHARS, TIMEOUT_MS, } from "../constants.js";
7
8
  import { collectDocsCandidates } from "../core/docs.js";
8
9
  import { buildEnvelope, } from "../core/output.js";
10
+ import { known, } from "../core/result-report.js";
9
11
  import { prepareDocsCheck, readDocsJudgment, } from "../presets/docs.js";
10
12
  import { NOT_CONFIGURED } from "../texts/configuration.js";
13
+ import { ReviewReport, reportMetrics } from "./review-report.js";
11
14
  /** Shared judgment seam: no guide delivery, rendering, or session answer recording. */
12
15
  export async function runDocsCheck(deps, input) {
13
16
  const started = performance.now();
17
+ const report = new ReviewReport();
18
+ const admission = input.evidenceContext
19
+ ? undefined
20
+ : await resolveEvidenceContext(input.cwd, undefined, {
21
+ exec: deps.exec,
22
+ signal: input.signal,
23
+ origin: deps.evidenceOrigin,
24
+ });
25
+ const evidenceContext = input.evidenceContext ?? admission?.context;
26
+ if (!evidenceContext)
27
+ throw new Error("Evidence context was not established");
28
+ evidenceContext.requestedBase = input.base ?? "HEAD";
14
29
  deps = { ...deps, exec: shareGitInventory(deps.exec) };
15
30
  const timeout = input.budgetMs === undefined
16
31
  ? undefined
@@ -31,7 +46,7 @@ export async function runDocsCheck(deps, input) {
31
46
  let budget;
32
47
  let sent = 0;
33
48
  let emptyBase;
34
- const finish = (refusal) => {
49
+ const finish = (refusal, cause = "internal_error") => {
35
50
  if (unchecked.length > 5) {
36
51
  unjudged = {
37
52
  count: unchecked.length,
@@ -96,7 +111,9 @@ export async function runDocsCheck(deps, input) {
96
111
  !collection &&
97
112
  !unchecked.length &&
98
113
  !findings.length &&
99
- emptyBase === undefined
114
+ emptyBase === undefined &&
115
+ report.items.size > 0 &&
116
+ [...report.items.values()].every((item) => item.treatment === "judged")
100
117
  ? {
101
118
  lines: [
102
119
  {
@@ -110,12 +127,27 @@ export async function runDocsCheck(deps, input) {
110
127
  yield: {
111
128
  calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
112
129
  questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
113
- costUsd: judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0),
130
+ costUsd: judgments.length && judgments.every((j) => j.usage !== undefined)
131
+ ? judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0)
132
+ : undefined,
114
133
  cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
115
134
  cacheRequests: judgments.reduce((n, j) => n + (j.cacheRequests ?? 0), 0),
116
135
  elapsedMs: performance.now() - started,
117
136
  },
118
137
  });
138
+ report.diagnose("collection_omitted", "Existing documentation sentences only; missing documentation is not exhaustively detected (measured 2/45 obligations across 180 partial commits).", undefined, [], false);
139
+ if (emptyBase !== undefined)
140
+ report.diagnose("no_changed_units", `No changed units against ${emptyBase}`, emptyBase, [], false);
141
+ if (!refusal && emptyBase === undefined && !report.items.size)
142
+ report.diagnose("collection_empty", "No matching documentation sections were collected; this does not establish documentation completeness.", undefined, [], false);
143
+ if (refusal) {
144
+ report.refusal =
145
+ cause === "invalid_base" ||
146
+ cause === "invalid_root" ||
147
+ cause === "git_failure";
148
+ report.diagnose(cause, refusal);
149
+ }
150
+ const result = report.build("jev_check_diff", evidenceContext, reportMetrics(envelope));
119
151
  return {
120
152
  ok: !refusal,
121
153
  status: timeout?.aborted
@@ -128,37 +160,58 @@ export async function runDocsCheck(deps, input) {
128
160
  envelope,
129
161
  judgments,
130
162
  findings,
163
+ result,
131
164
  ...(collection ? { collection } : {}),
132
165
  ...(unjudged ? { unjudged } : {}),
133
166
  };
134
167
  };
135
168
  const client = deps.client;
136
169
  if (!client)
137
- return finish(NOT_CONFIGURED);
170
+ return finish(NOT_CONFIGURED, "not_configured");
138
171
  const sessionRefusal = deps.runtime.session.refusal();
139
172
  if (sessionRefusal)
140
- return finish(sessionRefusal);
173
+ return finish(sessionRefusal, "session_budget");
141
174
  try {
142
175
  const comparison = await resolveBase(deps.exec, input.cwd, input.base, signal);
143
176
  if (!comparison.ok)
144
- return finish(comparison.error);
177
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
145
178
  const base = comparison.base;
179
+ evidenceContext.resolvedBase = base;
146
180
  const analysis = await createAnalysisContext();
147
181
  const [collected, inventory] = await Promise.all([
148
182
  collectUnits(deps.exec, { cwd: input.cwd, base, signal }, analysis.parser),
149
183
  collectDocsInventory(deps.exec, input.cwd, signal),
150
184
  ]);
151
185
  if (!collected.ok)
152
- return finish(collected.error);
186
+ return finish(collected.error, collected.cause ?? "git_failure");
153
187
  if (!inventory.ok)
154
- return finish(inventory.error);
155
- for (const limit of collected.limits)
188
+ return finish(inventory.error, inventory.cause ?? "file_unavailable");
189
+ const repositoryRoot = await deps.exec("git", ["rev-parse", "--show-toplevel"], { cwd: input.cwd, timeout: TIMEOUT_MS, signal });
190
+ if (!repositoryRoot.code &&
191
+ !repositoryRoot.killed &&
192
+ evidenceContext.effectiveRoot)
193
+ evidenceContext.effectiveRoot.path = repositoryRoot.stdout.trim();
194
+ for (const limit of collected.limits) {
195
+ report.diagnose(limit.kind === "secret_pattern"
196
+ ? "secret_pattern"
197
+ : "collection_omitted", limit.kind, limit.file);
156
198
  limitations.push({
157
199
  fact: `${limit.file} : ${limit.kind}`,
158
200
  next: "Read the complete change before concluding.",
159
201
  });
202
+ }
160
203
  if (!collected.units.length) {
161
204
  emptyBase = base;
205
+ report.inventories.push({
206
+ id: "changed-units",
207
+ kind: "units",
208
+ rules: ["Changed source units against resolved base"],
209
+ restrictions: [],
210
+ discovered: known(0),
211
+ considered: known(0),
212
+ scopeRestricted: false,
213
+ criteria: [],
214
+ });
162
215
  return finish();
163
216
  }
164
217
  const collectDeadline = performance.now() + DOCS_COLLECT_BUDGET_MS;
@@ -170,6 +223,38 @@ export async function runDocsCheck(deps, input) {
170
223
  lexical: analysis,
171
224
  shouldStop: () => performance.now() >= collectDeadline || !!signal?.aborted,
172
225
  });
226
+ report.inventories.push({
227
+ id: "docs-sections",
228
+ kind: "sections",
229
+ rules: [
230
+ "Tracked Markdown sections referring to changed declarations or static import closure",
231
+ `At most ${DOCS_MAX_SECTIONS} admitted sections`,
232
+ ],
233
+ restrictions: [],
234
+ discovered: known(candidates.candidates.length + candidates.omitted.length),
235
+ considered: known(candidates.candidates.length),
236
+ scopeRestricted: false,
237
+ criteria: [],
238
+ });
239
+ for (const candidate of candidates.candidates)
240
+ report.expect(`docs:${candidate.path}:${candidate.start}`, `${candidate.path} § ${candidate.heading}`, "section");
241
+ for (const candidate of candidates.omitted) {
242
+ const id = `docs:${candidate.path}:${candidate.start}`;
243
+ report.expect(id, `${candidate.path} § ${candidate.heading}`, "section");
244
+ report.diagnose("collection_omitted", "Section exceeds DOCS_MAX_SECTIONS", candidate.path, [id]);
245
+ }
246
+ for (const limit of candidates.limits) {
247
+ if (limit.kind === "collection_budget") {
248
+ report.missingWork = true;
249
+ report.diagnose("collection_omitted", limit.reason, limit.path, [], true, [...(limit.sections ?? [])]);
250
+ }
251
+ else
252
+ report.diagnose("dynamic_dependency", limit.reason, limit.path, [], false);
253
+ }
254
+ for (const limit of inventory.limits)
255
+ report.diagnose(limit.cause === "secret_pattern"
256
+ ? "secret_pattern"
257
+ : "collection_omitted", limit.reason, limit.path);
173
258
  for (const limit of inventory.limits)
174
259
  limitations.push({
175
260
  fact: `${limit.path} : ${limit.reason}`,
@@ -211,27 +296,35 @@ export async function runDocsCheck(deps, input) {
211
296
  unchecked.push(`${omitted.path} § ${omitted.heading} (DOCS_MAX_SECTIONS)`);
212
297
  await Promise.all(candidates.candidates.map(async (candidate) => {
213
298
  const label = `${candidate.path} § ${candidate.heading}`;
299
+ const reportId = `docs:${candidate.path}:${candidate.start}`;
214
300
  if (candidate.units.some((unit) => unit.before === null && unit.after === null)) {
215
301
  unchecked.push(`${label} (changed source unavailable)`);
302
+ report.diagnose("binary_or_non_utf8", "Changed source unavailable", label, [reportId]);
216
303
  return;
217
304
  }
218
305
  if (budget?.kind === "session") {
219
306
  unchecked.push(`${label} (${budget.message})`);
307
+ report.diagnose("session_budget", budget.message, label, [reportId]);
220
308
  return;
221
309
  }
222
310
  const prepared = prepareDocsCheck(candidate);
223
- if (JSON.stringify(prepared.state).length > STATE_MAX_CHARS ||
311
+ const state = withEvidenceContext(prepared.state, evidenceContext);
312
+ if (JSON.stringify(state).length > STATE_MAX_CHARS ||
224
313
  candidate.sentences.length + 1 > CHOICE_MAX_OPTIONS) {
225
314
  unchecked.push(`${label} (state or pointer too large)`);
315
+ report.diagnose("evidence_too_large", "Section state or sentence pointer exceeds limits", label, [reportId]);
226
316
  return;
227
317
  }
228
318
  if (signal?.aborted) {
229
319
  unchecked.push(`${label} (budget exceeded or canceled)`);
320
+ report.diagnose("cancelled", "Collection budget exceeded or check canceled", label, [reportId]);
230
321
  return;
231
322
  }
232
- const judgment = await client.judge(prepared.state, prepared.questions, {
323
+ const judgment = await client.judge(state, prepared.questions, {
233
324
  signal,
325
+ groups: [["status", "sentence"]],
234
326
  ...deps.runtime.session.requestGate(),
327
+ admissionCause: () => budget?.kind === "session" ? "session_budget" : "call_budget",
235
328
  beforeRequest(questionCount) {
236
329
  if (budget?.kind === "session")
237
330
  return { ok: false, error: budget.message };
@@ -257,6 +350,22 @@ export async function runDocsCheck(deps, input) {
257
350
  onUsage: (usage) => deps.runtime.session.recordUsage(usage),
258
351
  });
259
352
  judgments.push(judgment);
353
+ if (!judgment.ok)
354
+ report.failure(judgment, [reportId]);
355
+ else {
356
+ const status = judgment.answers.status;
357
+ const pointer = judgment.answers.sentence;
358
+ const finding = readDocsJudgment(candidate, judgment.answers);
359
+ const item = report.items.get(reportId);
360
+ if (finding && item)
361
+ item.label = `${label} — ${finding.sentence ? JSON.stringify(finding.sentence.text) : "sentence not identified"} — after ${finding.units.map((unit) => `${unit.name} (${unit.file})`).join(", ")}`;
362
+ report.answer(reportId, status, { band: finding?.band ?? "verdict", reason: finding?.reason }, [
363
+ pointer ?? {
364
+ type: "unjudged",
365
+ reason: "Required sentence pointer missing",
366
+ },
367
+ ]);
368
+ }
260
369
  if (!judgment.ok) {
261
370
  unchecked.push(`${label} (${judgment.error})`);
262
371
  return;
@@ -293,7 +402,7 @@ export async function runDocsCheck(deps, input) {
293
402
  }
294
403
  catch (error) {
295
404
  if (signal?.aborted)
296
- return finish(timeout?.aborted ? "Docs budget exceeded." : "Docs check canceled.");
405
+ return finish(timeout?.aborted ? "Docs budget exceeded." : "Docs check canceled.", "cancelled");
297
406
  throw error;
298
407
  }
299
408
  }