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,3 +1,8 @@
1
+ import {
2
+ type EvidenceContext,
3
+ resolveEvidenceContext,
4
+ withEvidenceContext,
5
+ } from "../adapters/evidence-context.ts";
1
6
  import { collectFiles } from "../adapters/files.ts";
2
7
  import { collectUnits } from "../adapters/git.ts";
3
8
  import { resolveBase } from "../adapters/git-base.ts";
@@ -13,6 +18,11 @@ import {
13
18
  type Envelope,
14
19
  type Limitation,
15
20
  } from "../core/output.ts";
21
+ import {
22
+ type Cause,
23
+ known,
24
+ type ResultReportV1,
25
+ } from "../core/result-report.ts";
16
26
  import type { Judgment } from "../jev/types.ts";
17
27
  import {
18
28
  prepareSpecCheck,
@@ -21,12 +31,14 @@ import {
21
31
  } from "../presets/spec.ts";
22
32
  import type { ToolDependencies } from "../runtime.ts";
23
33
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
34
+ import { ReviewReport, reportMetrics } from "./review-report.ts";
24
35
 
25
36
  export interface SpecCheckResult {
26
37
  ok: boolean;
27
38
  envelope: Envelope;
28
39
  judgments: Judgment[];
29
40
  findings: SpecFinding[];
41
+ result: ResultReportV1;
30
42
  }
31
43
  export async function runSpecCheck(
32
44
  deps: ToolDependencies,
@@ -36,9 +48,21 @@ export async function runSpecCheck(
36
48
  specPath?: string;
37
49
  maxCalls?: number;
38
50
  signal?: AbortSignal;
51
+ evidenceContext?: EvidenceContext;
39
52
  },
40
53
  ): Promise<SpecCheckResult> {
41
54
  const started = performance.now();
55
+ const report = new ReviewReport();
56
+ const admission = input.evidenceContext
57
+ ? undefined
58
+ : await resolveEvidenceContext(input.cwd, undefined, {
59
+ exec: deps.exec,
60
+ signal: input.signal,
61
+ origin: deps.evidenceOrigin,
62
+ });
63
+ const evidenceContext = input.evidenceContext ?? admission?.context;
64
+ if (!evidenceContext) throw new Error("Evidence context was not established");
65
+ evidenceContext.requestedBase = input.base ?? "HEAD";
42
66
  const judgments: Judgment[] = [];
43
67
  const findings: SpecFinding[] = [];
44
68
  const limitations: Limitation[] = [];
@@ -47,7 +71,10 @@ export async function runSpecCheck(
47
71
  let sent = 0;
48
72
  let incomplete = false;
49
73
  let emptyBase: string | undefined;
50
- const finish = (refusal?: string): SpecCheckResult => {
74
+ const finish = (
75
+ refusal?: string,
76
+ cause: Cause = "internal_error",
77
+ ): SpecCheckResult => {
51
78
  const answers: AnswerInput[] = findings.map((finding) => ({
52
79
  label:
53
80
  finding.kind === "requirement"
@@ -81,7 +108,9 @@ export async function runSpecCheck(
81
108
  !budget &&
82
109
  !unchecked.length &&
83
110
  !findings.length &&
84
- emptyBase === undefined
111
+ emptyBase === undefined &&
112
+ report.items.size > 0 &&
113
+ [...report.items.values()].every((item) => item.treatment === "judged")
85
114
  ? {
86
115
  lines: [
87
116
  {
@@ -95,7 +124,10 @@ export async function runSpecCheck(
95
124
  yield: {
96
125
  calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
97
126
  questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
98
- costUsd: judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0),
127
+ costUsd:
128
+ judgments.length && judgments.every((j) => j.usage !== undefined)
129
+ ? judgments.reduce((n, j) => n + (j.usage?.costUsd ?? 0), 0)
130
+ : undefined,
99
131
  cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
100
132
  cacheRequests: judgments.reduce(
101
133
  (n, j) => n + (j.cacheRequests ?? 0),
@@ -104,48 +136,134 @@ export async function runSpecCheck(
104
136
  elapsedMs: performance.now() - started,
105
137
  },
106
138
  });
107
- return { ok: !refusal, envelope, judgments, findings };
139
+ if (emptyBase !== undefined)
140
+ report.diagnose(
141
+ "no_changed_units",
142
+ `No changed units against ${emptyBase}`,
143
+ emptyBase,
144
+ [],
145
+ false,
146
+ );
147
+ if (refusal) {
148
+ report.refusal = cause !== "not_configured";
149
+ report.diagnose(cause, refusal, input.specPath);
150
+ }
151
+ const result = report.build(
152
+ "jev_check_diff",
153
+ evidenceContext,
154
+ reportMetrics(envelope),
155
+ );
156
+ return { ok: !refusal, envelope, judgments, findings, result };
108
157
  };
109
158
  if (!input.specPath)
110
- return finish("spec_path required: no specification to check against.");
111
- if (!deps.client) return finish(NOT_CONFIGURED);
159
+ return finish(
160
+ "spec_path required: no specification to check against.",
161
+ "missing_required",
162
+ );
163
+ if (!deps.client) return finish(NOT_CONFIGURED, "not_configured");
112
164
  const comparison = await resolveBase(
113
165
  deps.exec,
114
166
  input.cwd,
115
167
  input.base,
116
168
  input.signal,
117
169
  );
118
- if (!comparison.ok) return finish(comparison.error);
170
+ if (!comparison.ok)
171
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
119
172
  const base = comparison.base;
173
+ evidenceContext.resolvedBase = base;
120
174
  const root = await deps.exec("git", ["rev-parse", "--show-toplevel"], {
121
175
  cwd: input.cwd,
122
176
  timeout: TIMEOUT_MS,
123
177
  signal: input.signal,
124
178
  });
125
- if (root.code || root.killed) return finish("Repository root not found.");
179
+ if (root.code || root.killed)
180
+ return finish(
181
+ "Repository root not found.",
182
+ root.killed ? "cancelled" : "git_failure",
183
+ );
126
184
  const cwd = root.stdout.trim();
185
+ if (evidenceContext.effectiveRoot) evidenceContext.effectiveRoot.path = cwd;
127
186
  const [collected, specification] = await Promise.all([
128
187
  collectUnits(deps.exec, { cwd, base, signal: input.signal }),
129
188
  collectFiles(cwd, [input.specPath], input.signal, { exec: deps.exec }),
130
189
  ]);
131
- if (!collected.ok) return finish(collected.error);
132
- if (!specification.ok) return finish(specification.error);
190
+ if (!collected.ok)
191
+ return finish(collected.error, collected.cause ?? "git_failure");
192
+ if (!specification.ok)
193
+ return finish(
194
+ specification.error,
195
+ specification.cause ?? "file_unavailable",
196
+ );
133
197
  const text = Object.values(specification.files)[0];
134
- if (!text) return finish("The specification is empty: nothing to check.");
198
+ if (!text)
199
+ return finish(
200
+ "The specification is empty: nothing to check.",
201
+ "empty_required",
202
+ );
135
203
  for (const limit of collected.limits)
136
204
  limitations.push({
137
205
  fact: `${limit.file} : ${limit.kind}`,
138
206
  next: "Read the complete change before concluding.",
139
207
  });
140
- const units = collected.units.filter((unit) => {
141
- if (unit.before !== null || unit.after !== null) return true;
208
+ const unavailableUnits = collected.units.filter(
209
+ (unit) => unit.before === null && unit.after === null,
210
+ );
211
+ const units = collected.units.filter(
212
+ (unit) => unit.before !== null || unit.after !== null,
213
+ );
214
+ for (const unit of unavailableUnits)
142
215
  unchecked.push(`${unit.file} ${unit.name} (changed source unavailable)`);
143
- return false;
144
- });
145
216
  const prepared = prepareSpecCheck(text, units);
217
+ for (const requirement of prepared.requirements)
218
+ report.expect(`spec:${requirement.id}`, requirement.label, "requirement");
219
+ report.expect("spec:drift", "Specification drift pointer", "pointer");
220
+ report.inventories.push({
221
+ id: "spec-requirements",
222
+ kind: "sections",
223
+ rules: ["### REQ- headings in the supplied specification"],
224
+ restrictions: [input.specPath],
225
+ discovered: known(prepared.requirements.length),
226
+ considered: known(prepared.requirements.length),
227
+ scopeRestricted: true,
228
+ criteria: [],
229
+ });
230
+ report.inventories.push({
231
+ id: "changed-units",
232
+ kind: "units",
233
+ rules: ["Changed source units against resolved base"],
234
+ restrictions: [],
235
+ discovered: known(collected.units.length),
236
+ considered: known(units.length),
237
+ scopeRestricted: false,
238
+ criteria: [],
239
+ });
240
+ if (!collected.units.length && prepared.requirements.length)
241
+ report.items.clear();
242
+ const reportIds = [...report.items.keys()];
243
+ for (const unit of unavailableUnits)
244
+ report.diagnose(
245
+ "binary_or_non_utf8",
246
+ "Changed source unavailable",
247
+ unit.file,
248
+ reportIds,
249
+ units.length === 0,
250
+ [`${unit.file} ${unit.name}`],
251
+ );
252
+ for (const limit of collected.limits)
253
+ report.diagnose(
254
+ limit.kind === "secret_pattern" ? "secret_pattern" : "collection_omitted",
255
+ limit.kind,
256
+ limit.file,
257
+ reportIds,
258
+ units.length === 0,
259
+ ["unit" in limit ? `${limit.file} ${limit.unit}` : limit.file],
260
+ );
261
+ report.missingWork =
262
+ unavailableUnits.length > 0 || collected.limits.length > 0;
146
263
  if (!prepared.requirements.length)
147
264
  return finish(
148
265
  "No ### REQ-… requirements in the specification: nothing to check.",
266
+ "missing_required",
149
267
  );
150
268
  if (!collected.units.length) {
151
269
  emptyBase = base;
@@ -159,23 +277,35 @@ export async function runSpecCheck(
159
277
  "drift (changed source unavailable)",
160
278
  );
161
279
  }
162
- if (prepared.tableWarning)
280
+ if (prepared.tableWarning) {
281
+ report.diagnose(
282
+ "unsupported_syntax",
283
+ "Specification Markdown table interpretation is uncalibrated",
284
+ input.specPath,
285
+ [],
286
+ false,
287
+ );
163
288
  limitations.push({
164
289
  fact: "The specification contains a Markdown table.",
165
290
  next: "Read the table requirements: their interpretation is uncalibrated.",
166
291
  });
167
- incomplete = collected.limits.length > 0;
292
+ }
293
+ incomplete = report.missingWork;
294
+ const state = withEvidenceContext(prepared.state, evidenceContext);
168
295
  if (
169
296
  units.length + 1 > CHOICE_MAX_OPTIONS ||
170
- JSON.stringify(prepared.state).length > STATE_MAX_CHARS
297
+ JSON.stringify(state).length > STATE_MAX_CHARS
171
298
  )
172
299
  return finish(
173
300
  `State or spec pointer exceeds the limits; compare against a closer base (STATE_MAX_CHARS=${STATE_MAX_CHARS}).`,
301
+ "evidence_too_large",
174
302
  );
175
303
  if (!units.length) return finish();
176
- const judgment = await deps.client.judge(prepared.state, prepared.questions, {
304
+ const judgment = await deps.client.judge(state, prepared.questions, {
177
305
  signal: input.signal,
178
306
  ...deps.runtime.session.requestGate(),
307
+ admissionCause: () =>
308
+ budget?.kind === "session" ? "session_budget" : "call_budget",
179
309
  beforeRequest(questionCount) {
180
310
  if (input.maxCalls !== undefined && sent >= input.maxCalls) {
181
311
  budget = {
@@ -192,6 +322,50 @@ export async function runSpecCheck(
192
322
  onUsage: (usage) => deps.runtime.session.recordUsage(usage),
193
323
  });
194
324
  judgments.push(judgment);
325
+ if (!judgment.ok)
326
+ report.failure(
327
+ judgment,
328
+ reportIds,
329
+ budget
330
+ ? budget.kind === "session"
331
+ ? "session_budget"
332
+ : "call_budget"
333
+ : undefined,
334
+ );
335
+ else {
336
+ for (const requirement of prepared.requirements)
337
+ report.answer(
338
+ `spec:${requirement.id}`,
339
+ judgment.answers[requirement.id],
340
+ { band: incomplete ? "unsure" : "verdict" },
341
+ );
342
+ const driftAnswer = judgment.answers.drift;
343
+ const selectedUnit =
344
+ driftAnswer?.type === "choice"
345
+ ? units.find((unit) => unit.id === driftAnswer.choice)
346
+ : undefined;
347
+ const pointer = report.items.get("spec:drift");
348
+ if (pointer && selectedUnit)
349
+ report.items.set("spec:drift", {
350
+ ...pointer,
351
+ label: `${selectedUnit.file} ${selectedUnit.name} specification drift`,
352
+ });
353
+ const selectedProbability =
354
+ driftAnswer?.type === "choice"
355
+ ? driftAnswer.probabilities[driftAnswer.choice]
356
+ : undefined;
357
+ report.answer("spec:drift", driftAnswer, {
358
+ band: incomplete ? "unsure" : "verdict",
359
+ ...(driftAnswer?.type === "choice" && selectedProbability !== undefined
360
+ ? {
361
+ value: {
362
+ head: driftAnswer.choice,
363
+ p: selectedProbability,
364
+ },
365
+ }
366
+ : {}),
367
+ });
368
+ }
195
369
  if (!judgment.ok) {
196
370
  unchecked.push(
197
371
  ...prepared.requirements.map((req) => `${req.label} (${judgment.error})`),