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
@@ -1,6 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
3
  import { createAnalysisContext } from "../adapters/analysis-context.ts";
4
+ import {
5
+ type EvidenceContext,
6
+ resolveEvidenceContext,
7
+ withEvidenceContext,
8
+ } from "../adapters/evidence-context.ts";
4
9
  import { collectUnits } from "../adapters/git.ts";
5
10
  import { resolveBase } from "../adapters/git-base.ts";
6
11
  import { shareGitInventory } from "../adapters/git-inventory.ts";
@@ -12,6 +17,7 @@ import {
12
17
  CANNOT_TELL_MIN,
13
18
  FLAG_MIN,
14
19
  STATE_MAX_CHARS,
20
+ TIMEOUT_MS,
15
21
  } from "../constants.ts";
16
22
  import { prepareBatches } from "../core/batches.ts";
17
23
  import { isTestFile } from "../core/diff.ts";
@@ -22,6 +28,11 @@ import {
22
28
  type Envelope,
23
29
  type Limitation,
24
30
  } from "../core/output.ts";
31
+ import {
32
+ type Cause,
33
+ known,
34
+ type ResultReportV1,
35
+ } from "../core/result-report.ts";
25
36
  import type { CallerProof, EvidenceSpan } from "../core/risk-callers.ts";
26
37
  import type { EvidenceUnit } from "../core/units.ts";
27
38
  import type { GuideContext } from "../guide.ts";
@@ -37,15 +48,18 @@ import {
37
48
  prepareRiskMatrix,
38
49
  prepareRiskSeverity,
39
50
  } from "../presets/risk.ts";
40
- import { renderEnvelope } from "../render.ts";
51
+ import { renderResultReport } from "../render.ts";
41
52
  import type { ToolDependencies } from "../runtime.ts";
42
53
  import { CHECK_DIFF_DESCRIPTION } from "../texts/check-diff.ts";
43
54
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
55
+ import { CHECK_DIFF_GUIDELINE } from "../texts/instructions.ts";
44
56
  import { runDocsCheck } from "./docs-check.ts";
57
+ import { controlsFor, ReviewReport, reportMetrics } from "./review-report.ts";
45
58
  import { runSpecCheck } from "./spec-check.ts";
46
59
 
47
60
  export const checkDiffParameters = Type.Object(
48
61
  {
62
+ root: Type.Optional(Type.String()),
49
63
  check: Type.Union([
50
64
  Type.Literal("risk"),
51
65
  Type.Literal("docs"),
@@ -71,6 +85,8 @@ export const checkDiffParameters = Type.Object(
71
85
  export type CheckDiffArgs = Static<typeof checkDiffParameters>;
72
86
  export interface RiskDetails {
73
87
  ok: boolean;
88
+ evidenceContext: EvidenceContext;
89
+ result: ResultReportV1;
74
90
  envelope: Envelope;
75
91
  judgments: Judgment[];
76
92
  callerProofs: CallerProof[];
@@ -95,9 +111,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
95
111
  : {
96
112
  promptSnippet:
97
113
  "Check the uncommitted diff for risky changes, stale docs or spec drift",
98
- promptGuidelines: [
99
- "jev_check_diff: run it once before you report done, not on a half-written diff",
100
- ],
114
+ promptGuidelines: [CHECK_DIFF_GUIDELINE],
101
115
  }),
102
116
  async execute(
103
117
  _id: string,
@@ -107,23 +121,71 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
107
121
  ctx: { cwd: string } & GuideContext,
108
122
  ) {
109
123
  const client = dependencies.client;
124
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
125
+ exec: execute,
126
+ signal,
127
+ origin: dependencies.evidenceOrigin,
128
+ });
129
+ const evidenceContext = evidence.context;
130
+ const report = new ReviewReport();
131
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
110
132
  const exec = shareGitInventory(execute);
133
+ if (!evidence.ok) {
134
+ const envelope = buildEnvelope({
135
+ refusal: evidence.error,
136
+ yield: {
137
+ calls: 0,
138
+ questions: 0,
139
+ cacheHits: 0,
140
+ cacheRequests: 0,
141
+ elapsedMs: 0,
142
+ },
143
+ });
144
+ runtime.session.record(envelope);
145
+ runtime.guide.deliver(ctx);
146
+ report.refusal = true;
147
+ report.diagnose(evidence.cause, evidence.error, args.root);
148
+ const result = report.build(
149
+ "jev_check_diff",
150
+ evidenceContext,
151
+ reportMetrics(envelope),
152
+ );
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text" as const,
157
+ text: renderResultReport(result, { details: envelope }),
158
+ },
159
+ ],
160
+ details: {
161
+ ok: false,
162
+ envelope,
163
+ judgments: [],
164
+ callerProofs: [],
165
+ evidenceContext,
166
+ result,
167
+ },
168
+ };
169
+ }
170
+ evidenceContext.requestedBase = args.base ?? "HEAD";
111
171
  if (args.check === "docs" || args.check === "spec") {
112
172
  const deps = { client, host, runtime, exec };
113
173
  const result =
114
174
  args.check === "docs"
115
175
  ? await runDocsCheck(deps, {
116
- cwd: ctx.cwd,
176
+ cwd: cwd,
117
177
  base: args.base,
118
178
  signal,
119
179
  maxCalls: args.max_calls,
180
+ evidenceContext,
120
181
  })
121
182
  : await runSpecCheck(deps, {
122
- cwd: ctx.cwd,
183
+ cwd: cwd,
123
184
  base: args.base,
124
185
  specPath: args.spec_path,
125
186
  signal,
126
187
  maxCalls: args.max_calls,
188
+ evidenceContext,
127
189
  });
128
190
  runtime.session.record(result.envelope);
129
191
  runtime.guide.deliver(ctx);
@@ -136,9 +198,14 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
136
198
  );
137
199
  return {
138
200
  content: [
139
- { type: "text" as const, text: renderEnvelope(result.envelope) },
201
+ {
202
+ type: "text" as const,
203
+ text: renderResultReport(result.result, {
204
+ details: result.envelope,
205
+ }),
206
+ },
140
207
  ],
141
- details: { ...result, callerProofs: [] },
208
+ details: { ...result, callerProofs: [], evidenceContext },
142
209
  ...hostUsage(host.isOmp, usage),
143
210
  };
144
211
  }
@@ -153,7 +220,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
153
220
  let sent = 0;
154
221
  let matrixHealthy = true;
155
222
  let emptyBase: string | undefined;
156
- const finish = (refusal?: string) => {
223
+ const finish = (refusal?: string, cause?: Cause) => {
157
224
  answers.sort((a, b) =>
158
225
  (unitOrder.get(a) ?? "").localeCompare(unitOrder.get(b) ?? ""),
159
226
  );
@@ -170,7 +237,14 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
170
237
  { inputTokens: 0, costUsd: 0 },
171
238
  );
172
239
  const noFindings =
173
- !refusal && matrixHealthy && !answers.length && !unchecked.length;
240
+ !refusal &&
241
+ matrixHealthy &&
242
+ !answers.length &&
243
+ !unchecked.length &&
244
+ report.items.size > 0 &&
245
+ [...report.items.values()].every(
246
+ (item) => item.treatment === "judged",
247
+ );
174
248
  const envelope = buildEnvelope({
175
249
  answers,
176
250
  limitations:
@@ -200,7 +274,11 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
200
274
  yield: {
201
275
  calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
202
276
  questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
203
- costUsd: usage.costUsd,
277
+ costUsd:
278
+ judgments.every((j) => j.usage !== undefined) &&
279
+ judgments.length > 0
280
+ ? usage.costUsd
281
+ : undefined,
204
282
  cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
205
283
  cacheRequests: judgments.reduce(
206
284
  (n, j) => n + (j.cacheRequests ?? 0),
@@ -209,11 +287,38 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
209
287
  elapsedMs: performance.now() - started,
210
288
  },
211
289
  });
290
+ if (emptyBase !== undefined)
291
+ report.diagnose(
292
+ "no_changed_units",
293
+ `No changed units against ${emptyBase}; no risk judgment requested.`,
294
+ emptyBase,
295
+ [],
296
+ false,
297
+ );
298
+ if (
299
+ refusal &&
300
+ !report.diagnostics.some((d) => d.effect === "blocking")
301
+ ) {
302
+ report.refusal = refusal !== NOT_CONFIGURED;
303
+ report.diagnose(cause ?? "internal_error", refusal);
304
+ }
305
+ const result = report.build(
306
+ "jev_check_diff",
307
+ evidenceContext,
308
+ reportMetrics(envelope),
309
+ );
212
310
  runtime.session.record(envelope);
213
311
  runtime.guide.deliver(ctx);
214
312
  return {
215
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
313
+ content: [
314
+ {
315
+ type: "text" as const,
316
+ text: renderResultReport(result, { details: envelope }),
317
+ },
318
+ ],
216
319
  details: {
320
+ evidenceContext,
321
+ result,
217
322
  ok: !refusal,
218
323
  envelope,
219
324
  judgments,
@@ -222,21 +327,57 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
222
327
  ...hostUsage(host.isOmp, usage),
223
328
  };
224
329
  };
225
- if (!client) return finish(NOT_CONFIGURED);
226
- const comparison = await resolveBase(exec, ctx.cwd, args.base, signal);
227
- if (!comparison.ok) return finish(comparison.error);
330
+ if (!client) return finish(NOT_CONFIGURED, "not_configured");
331
+ const comparison = await resolveBase(exec, cwd, args.base, signal);
332
+ if (!comparison.ok)
333
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
334
+ evidenceContext.resolvedBase = comparison.base;
228
335
  const base = comparison.base;
229
336
  const analysis = await createAnalysisContext();
230
337
  const collected = await collectUnits(
231
338
  exec,
232
339
  {
233
- cwd: ctx.cwd,
340
+ cwd: cwd,
234
341
  base,
235
342
  signal,
236
343
  },
237
344
  analysis.parser,
238
345
  );
239
- if (!collected.ok) return finish(collected.error);
346
+ if (!collected.ok)
347
+ return finish(collected.error, collected.cause ?? "git_failure");
348
+ const repositoryRoot = await exec(
349
+ "git",
350
+ ["rev-parse", "--show-toplevel"],
351
+ { cwd, timeout: TIMEOUT_MS, signal },
352
+ );
353
+ if (
354
+ !repositoryRoot.code &&
355
+ !repositoryRoot.killed &&
356
+ evidenceContext.effectiveRoot
357
+ )
358
+ evidenceContext.effectiveRoot.path = repositoryRoot.stdout.trim();
359
+ report.inventories.push({
360
+ id: "changed-units",
361
+ kind: "units",
362
+ rules: [
363
+ "Changed source units against resolved base; tests are evidence, not risk units",
364
+ ],
365
+ restrictions: args.only ?? [],
366
+ discovered: known(collected.units.length),
367
+ considered: known(collected.units.length),
368
+ scopeRestricted: Boolean(args.only?.length),
369
+ criteria: [],
370
+ });
371
+ for (const unit of collected.units)
372
+ report.expect(`unit:${unit.id}`, unitLabel(unit), "unit");
373
+ for (const limit of collected.limits)
374
+ report.diagnose(
375
+ limit.kind === "secret_pattern"
376
+ ? "secret_pattern"
377
+ : "collection_omitted",
378
+ `${limit.file}: ${limit.kind}`,
379
+ limit.file,
380
+ );
240
381
  for (const limit of collected.limits)
241
382
  limitations.push({
242
383
  fact: `${limit.file}: ${limit.kind}`,
@@ -245,6 +386,12 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
245
386
  const readableUnits = collected.units.filter((unit) => {
246
387
  if (unit.before !== null || unit.after !== null) return true;
247
388
  unchecked.push(`${unitLabel(unit)} source unavailable`);
389
+ report.diagnose(
390
+ "binary_or_non_utf8",
391
+ "Changed source unavailable",
392
+ unit.file,
393
+ [`unit:${unit.id}`],
394
+ );
248
395
  return false;
249
396
  });
250
397
  if (!collected.units.length) {
@@ -256,21 +403,43 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
256
403
  ...args,
257
404
  testFilesPresent: collected.files.some((file) => isTestFile(file.path)),
258
405
  });
259
- if (!matrix.ok) return finish(matrix.error);
406
+ if (!matrix.ok) return finish(matrix.error, "invalid_arguments");
260
407
  const prepared = matrix;
261
- for (const warning of prepared.warnings) limitations.push(warning);
408
+ prepared.state = withEvidenceContext(prepared.state, evidenceContext);
409
+ for (const unit of readableUnits) report.items.delete(`unit:${unit.id}`);
410
+ for (const cell of prepared.cells)
411
+ report.expect(
412
+ `risk:${cell.id}`,
413
+ `${cell.unitId} ${cell.dimension}`,
414
+ "unit",
415
+ `risk:${cell.unitId}`,
416
+ );
417
+ for (const warning of prepared.warnings) {
418
+ limitations.push(warning);
419
+ report.diagnose(
420
+ "unsupported_syntax",
421
+ warning.fact,
422
+ undefined,
423
+ [],
424
+ false,
425
+ );
426
+ }
262
427
  if (JSON.stringify(prepared.state).length > STATE_MAX_CHARS)
263
428
  return finish(
264
429
  `risk state exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}; rerun with base= a nearer ref`,
430
+ "evidence_too_large",
265
431
  );
266
432
  const batches = prepareBatches(prepared.state, prepared.questions, {
267
433
  groups: prepared.groups,
268
434
  witnesses: prepared.witnessQuestionIds,
269
435
  });
270
- if (!batches.ok) return finish(batches.error);
436
+ if (!batches.ok) return finish(batches.error, "group_too_large");
271
437
  let reservedMatrixCalls = batches.batches.length;
272
438
  const optionsFor = (matrixRequest = false): JudgmentOptions => ({
273
439
  signal,
440
+ ...runtime.session.requestGate(),
441
+ admissionCause: () =>
442
+ budget?.kind === "session" ? "session_budget" : "call_budget",
274
443
  beforeRequest(questionCount) {
275
444
  if (matrixRequest && reservedMatrixCalls > 0) reservedMatrixCalls--;
276
445
  const limit =
@@ -300,6 +469,12 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
300
469
  extra?: JudgmentOptions,
301
470
  matrixRequest = false,
302
471
  ) => {
472
+ state = withEvidenceContext(state, evidenceContext);
473
+ if (JSON.stringify(state).length > STATE_MAX_CHARS)
474
+ return {
475
+ ok: false as const,
476
+ error: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
477
+ };
303
478
  const result = await client.judge(state, questions, {
304
479
  ...optionsFor(matrixRequest),
305
480
  ...extra,
@@ -312,7 +487,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
312
487
  )
313
488
  ? await collectRiskCallers(
314
489
  exec,
315
- { cwd: ctx.cwd, base, signal },
490
+ { cwd: cwd, base, signal },
316
491
  collected.units,
317
492
  collected.files,
318
493
  analysis.parser,
@@ -320,6 +495,23 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
320
495
  : { proofs: [], limits: [] };
321
496
  const uncheckedCallers = new Set<string>();
322
497
  for (const limit of local.limits) {
498
+ if (limit.kind !== "dynamic_access_uncovered") {
499
+ const id = `caller:${limit.unitId}`;
500
+ report.expect(id, `${limit.unitId} local caller`, "unit");
501
+ report.diagnose(
502
+ "collection_omitted",
503
+ limit.reason,
504
+ limit.paths.join(" + "),
505
+ [id],
506
+ );
507
+ } else
508
+ report.diagnose(
509
+ "dynamic_dependency",
510
+ limit.reason,
511
+ limit.paths.join(" + "),
512
+ [],
513
+ false,
514
+ );
323
515
  limitations.push({
324
516
  fact: `${limit.unitId}: ${limit.reason} — ${limit.paths.join(" + ")}`,
325
517
  next: "read the named caller/provider pieces or supply an observation via jev_ask",
@@ -365,6 +557,53 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
365
557
  unhealthyQuestionIds: new Map<string, string>(),
366
558
  };
367
559
  matrixHealthy = health.healthy;
560
+ for (const failure of health.controls)
561
+ report.diagnose(
562
+ health.healthy ? "conservative_widening" : "control_failure",
563
+ failure.fact,
564
+ undefined,
565
+ prepared.cells
566
+ .filter((cell) => health.unhealthyQuestionIds.has(cell.id))
567
+ .map((cell) => `risk:${cell.id}`),
568
+ false,
569
+ );
570
+ report.countControls(result, prepared.witnessQuestionIds);
571
+ for (const cell of prepared.cells) {
572
+ const rawAnswer = result.ok ? result.answers[cell.id] : undefined;
573
+ const answer =
574
+ rawAnswer?.type === "unjudged" && !rawAnswer.cause && budget
575
+ ? {
576
+ ...rawAnswer,
577
+ cause:
578
+ budget.kind === "session"
579
+ ? ("session_budget" as const)
580
+ : ("call_budget" as const),
581
+ }
582
+ : rawAnswer;
583
+ const invalid = health.unhealthyQuestionIds.get(cell.id);
584
+ if (!result.ok)
585
+ report.failure(
586
+ result,
587
+ [`risk:${cell.id}`],
588
+ budget
589
+ ? budget.kind === "session"
590
+ ? "session_budget"
591
+ : "call_budget"
592
+ : undefined,
593
+ );
594
+ else
595
+ report.answer(
596
+ `risk:${cell.id}`,
597
+ answer,
598
+ {
599
+ band: invalid ? "unsure" : "verdict",
600
+ reason: invalid,
601
+ uncalibrated: cell.uncalibrated,
602
+ },
603
+ controlsFor(cell.id, result, prepared.witnessQuestionIds),
604
+ false,
605
+ );
606
+ }
368
607
  const decoys = new Set(
369
608
  prepared.witnesses
370
609
  .filter((witness) => witness.expected === "no")
@@ -425,16 +664,36 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
425
664
  for (const { proof, result: localResult } of locals) {
426
665
  callerProofs.push(proof);
427
666
  const unit = collected.units.find((u) => u.id === proof.unitId);
428
- if (!unit) continue;
429
- const answer = localResult.ok ? localResult.answers.caller : undefined;
667
+ const reportId = `caller:${proof.unitId}`;
668
+ report.expect(
669
+ reportId,
670
+ `${unit ? unitLabel(unit) : proof.unitId} local caller`,
671
+ "unit",
672
+ );
673
+ if (!localResult.ok) {
674
+ report.failure(
675
+ localResult,
676
+ [reportId],
677
+ budget
678
+ ? budget.kind === "session"
679
+ ? "session_budget"
680
+ : "call_budget"
681
+ : undefined,
682
+ );
683
+ continue;
684
+ }
685
+ const answer = localResult.answers.caller;
430
686
  if (answer?.type !== "choice") {
431
- unchecked.push(`${unitLabel(unit)} local caller`);
687
+ report.answer(reportId, answer);
688
+ unchecked.push(
689
+ `${unit ? unitLabel(unit) : proof.unitId} local caller`,
690
+ );
432
691
  continue;
433
692
  }
434
693
  const p = answer.probabilities.new_failure ?? 0;
435
694
  const missing =
436
695
  (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN;
437
- if (!missing && p < BAND_BOOL_GRAY_A) continue;
696
+ const negative = !missing && p < BAND_BOOL_GRAY_A;
438
697
  const merged: EvidenceSpan[] = [];
439
698
  for (const span of [...proof.paths].sort(
440
699
  (a, b) =>
@@ -455,15 +714,45 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
455
714
  .map((span) => `${span.path}:${span.start}-${span.end}`)
456
715
  .join(" + ");
457
716
  const line: AnswerInput = {
458
- label: `${unitLabel(unit)} reliability — static code only; ${passages}${omitted ? ` (+${omitted} passages)` : ""}`,
717
+ label: `${unit ? unitLabel(unit) : proof.unitId} reliability — static code only; ${passages}${omitted ? ` (+${omitted} passages)` : ""}`,
459
718
  value: {
460
- head: missing ? "cannot_tell" : "new_failure",
461
- p: missing ? (answer.probabilities.cannot_tell ?? 0) : p,
719
+ head: missing
720
+ ? "cannot_tell"
721
+ : negative
722
+ ? "no_new_failure"
723
+ : "new_failure",
724
+ p: missing
725
+ ? (answer.probabilities.cannot_tell ?? 0)
726
+ : negative
727
+ ? (answer.probabilities.no_new_failure ?? 0)
728
+ : p,
462
729
  },
463
- band: missing ? "abstain" : p >= FLAG_MIN ? "verdict" : "unsure",
464
- missing:
465
- "actual provider or binding missing; supply its declaration or an observation via jev_ask",
730
+ band: missing
731
+ ? "abstain"
732
+ : negative || p >= FLAG_MIN
733
+ ? "verdict"
734
+ : "unsure",
735
+ reason: missing
736
+ ? "actual provider or binding missing; supply its declaration or an observation via jev_ask"
737
+ : negative
738
+ ? "Local caller check does not flag a new failure; static evidence does not prove every caller safe."
739
+ : p < FLAG_MIN
740
+ ? "Local caller new-failure probability is in the gray band; inspect the stated proof passages natively."
741
+ : "Local caller new-failure probability meets the finding threshold; static code evidence only.",
466
742
  };
743
+ const callerItem = report.items.get(reportId);
744
+ if (callerItem)
745
+ report.items.set(reportId, { ...callerItem, label: line.label });
746
+ report.answer(reportId, answer, line);
747
+ if (missing && line.reason)
748
+ report.diagnose(
749
+ "missing_required",
750
+ line.reason,
751
+ line.label,
752
+ [reportId],
753
+ false,
754
+ );
755
+ if (!unit || negative) continue;
467
756
  answers.push(line);
468
757
  unitOrder.set(line, unit.id);
469
758
  if (!missing && p >= FLAG_MIN) {
@@ -491,6 +780,23 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
491
780
  item.evidence?.callerEvidence,
492
781
  );
493
782
  const value = await judge(request.state, request.questions);
783
+ const reportId = `severity:${item.unit.id}:${item.dimension}`;
784
+ report.expect(
785
+ reportId,
786
+ `${item.unit.id} ${item.dimension} severity`,
787
+ "unit",
788
+ );
789
+ if (value.ok) report.answer(reportId, value.answers.severity);
790
+ else
791
+ report.failure(
792
+ value,
793
+ [reportId],
794
+ budget
795
+ ? budget.kind === "session"
796
+ ? "session_budget"
797
+ : "call_budget"
798
+ : undefined,
799
+ );
494
800
  const answer = value.ok ? value.answers.severity : undefined;
495
801
  if (answer?.type === "score") {
496
802
  const suffix = ` · severity ${answer.score.toFixed(1)}/3`;