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,23 +1,27 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { createAnalysisContext } from "../adapters/analysis-context.js";
4
- import { canonicalPath } from "../adapters/canonical-path.js";
4
+ import { resolveEvidenceContext, withEvidenceContext, } from "../adapters/evidence-context.js";
5
5
  import { collectUnits } from "../adapters/git.js";
6
6
  import { resolveBase } from "../adapters/git-base.js";
7
7
  import { shareGitInventory } from "../adapters/git-inventory.js";
8
8
  import { collectRiskCallers } from "../adapters/risk-callers.js";
9
9
  import { hostUsage } from "../adapters/usage.js";
10
- import { BAND_BOOL_GRAY_A, CALLER_DISPLAY_MAX_SPANS, CANNOT_TELL_MIN, FLAG_MIN, STATE_MAX_CHARS, } from "../constants.js";
10
+ import { BAND_BOOL_GRAY_A, CALLER_DISPLAY_MAX_SPANS, CANNOT_TELL_MIN, FLAG_MIN, STATE_MAX_CHARS, TIMEOUT_MS, } from "../constants.js";
11
11
  import { prepareBatches } from "../core/batches.js";
12
12
  import { isTestFile } from "../core/diff.js";
13
13
  import { buildEnvelope, } from "../core/output.js";
14
+ import { known, } from "../core/result-report.js";
14
15
  import { evaluateWitnessHealth, prepareRiskMatrix, prepareRiskSeverity, } from "../presets/risk.js";
15
- import { renderEnvelope } from "../render.js";
16
+ import { renderResultReport } from "../render.js";
16
17
  import { CHECK_DIFF_DESCRIPTION } from "../texts/check-diff.js";
17
18
  import { NOT_CONFIGURED } from "../texts/configuration.js";
19
+ import { CHECK_DIFF_GUIDELINE } from "../texts/instructions.js";
18
20
  import { runDocsCheck } from "./docs-check.js";
21
+ import { controlsFor, ReviewReport, reportMetrics } from "./review-report.js";
19
22
  import { runSpecCheck } from "./spec-check.js";
20
23
  export const checkDiffParameters = Type.Object({
24
+ root: Type.Optional(Type.String()),
21
25
  check: Type.Union([
22
26
  Type.Literal("risk"),
23
27
  Type.Literal("docs"),
@@ -53,14 +57,53 @@ export function createCheckDiffTool(dependencies) {
53
57
  ? { approval: "read", loadMode: "essential" }
54
58
  : {
55
59
  promptSnippet: "Check the uncommitted diff for risky changes, stale docs or spec drift",
56
- promptGuidelines: [
57
- "jev_check_diff: run it once before you report done, not on a half-written diff",
58
- ],
60
+ promptGuidelines: [CHECK_DIFF_GUIDELINE],
59
61
  }),
60
62
  async execute(_id, args, signal, _update, ctx) {
61
63
  const client = dependencies.client;
62
- const cwd = await canonicalPath(ctx.cwd);
64
+ const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
65
+ exec: execute,
66
+ signal,
67
+ origin: dependencies.evidenceOrigin,
68
+ });
69
+ const evidenceContext = evidence.context;
70
+ const report = new ReviewReport();
71
+ const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
63
72
  const exec = shareGitInventory(execute);
73
+ if (!evidence.ok) {
74
+ const envelope = buildEnvelope({
75
+ refusal: evidence.error,
76
+ yield: {
77
+ calls: 0,
78
+ questions: 0,
79
+ cacheHits: 0,
80
+ cacheRequests: 0,
81
+ elapsedMs: 0,
82
+ },
83
+ });
84
+ runtime.session.record(envelope);
85
+ runtime.guide.deliver(ctx);
86
+ report.refusal = true;
87
+ report.diagnose(evidence.cause, evidence.error, args.root);
88
+ const result = report.build("jev_check_diff", evidenceContext, reportMetrics(envelope));
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: renderResultReport(result, { details: envelope }),
94
+ },
95
+ ],
96
+ details: {
97
+ ok: false,
98
+ envelope,
99
+ judgments: [],
100
+ callerProofs: [],
101
+ evidenceContext,
102
+ result,
103
+ },
104
+ };
105
+ }
106
+ evidenceContext.requestedBase = args.base ?? "HEAD";
64
107
  if (args.check === "docs" || args.check === "spec") {
65
108
  const deps = { client, host, runtime, exec };
66
109
  const result = args.check === "docs"
@@ -69,6 +112,7 @@ export function createCheckDiffTool(dependencies) {
69
112
  base: args.base,
70
113
  signal,
71
114
  maxCalls: args.max_calls,
115
+ evidenceContext,
72
116
  })
73
117
  : await runSpecCheck(deps, {
74
118
  cwd: cwd,
@@ -76,6 +120,7 @@ export function createCheckDiffTool(dependencies) {
76
120
  specPath: args.spec_path,
77
121
  signal,
78
122
  maxCalls: args.max_calls,
123
+ evidenceContext,
79
124
  });
80
125
  runtime.session.record(result.envelope);
81
126
  runtime.guide.deliver(ctx);
@@ -85,9 +130,14 @@ export function createCheckDiffTool(dependencies) {
85
130
  }), { inputTokens: 0, costUsd: 0 });
86
131
  return {
87
132
  content: [
88
- { type: "text", text: renderEnvelope(result.envelope) },
133
+ {
134
+ type: "text",
135
+ text: renderResultReport(result.result, {
136
+ details: result.envelope,
137
+ }),
138
+ },
89
139
  ],
90
- details: { ...result, callerProofs: [] },
140
+ details: { ...result, callerProofs: [], evidenceContext },
91
141
  ...hostUsage(host.isOmp, usage),
92
142
  };
93
143
  }
@@ -102,7 +152,7 @@ export function createCheckDiffTool(dependencies) {
102
152
  let sent = 0;
103
153
  let matrixHealthy = true;
104
154
  let emptyBase;
105
- const finish = (refusal) => {
155
+ const finish = (refusal, cause) => {
106
156
  answers.sort((a, b) => (unitOrder.get(a) ?? "").localeCompare(unitOrder.get(b) ?? ""));
107
157
  const uniqueLimitations = [
108
158
  ...new Map(limitations.map((limit) => [`${limit.fact}\0${limit.next}`, limit])).values(),
@@ -111,7 +161,12 @@ export function createCheckDiffTool(dependencies) {
111
161
  inputTokens: sum.inputTokens + (value.usage?.inputTokens ?? 0),
112
162
  costUsd: sum.costUsd + (value.usage?.costUsd ?? 0),
113
163
  }), { inputTokens: 0, costUsd: 0 });
114
- const noFindings = !refusal && matrixHealthy && !answers.length && !unchecked.length;
164
+ const noFindings = !refusal &&
165
+ matrixHealthy &&
166
+ !answers.length &&
167
+ !unchecked.length &&
168
+ report.items.size > 0 &&
169
+ [...report.items.values()].every((item) => item.treatment === "judged");
115
170
  const envelope = buildEnvelope({
116
171
  answers,
117
172
  limitations: emptyBase !== undefined && !refusal
@@ -140,17 +195,35 @@ export function createCheckDiffTool(dependencies) {
140
195
  yield: {
141
196
  calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
142
197
  questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
143
- costUsd: usage.costUsd,
198
+ costUsd: judgments.every((j) => j.usage !== undefined) &&
199
+ judgments.length > 0
200
+ ? usage.costUsd
201
+ : undefined,
144
202
  cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
145
203
  cacheRequests: judgments.reduce((n, j) => n + (j.cacheRequests ?? 0), 0),
146
204
  elapsedMs: performance.now() - started,
147
205
  },
148
206
  });
207
+ if (emptyBase !== undefined)
208
+ report.diagnose("no_changed_units", `No changed units against ${emptyBase}; no risk judgment requested.`, emptyBase, [], false);
209
+ if (refusal &&
210
+ !report.diagnostics.some((d) => d.effect === "blocking")) {
211
+ report.refusal = refusal !== NOT_CONFIGURED;
212
+ report.diagnose(cause ?? "internal_error", refusal);
213
+ }
214
+ const result = report.build("jev_check_diff", evidenceContext, reportMetrics(envelope));
149
215
  runtime.session.record(envelope);
150
216
  runtime.guide.deliver(ctx);
151
217
  return {
152
- content: [{ type: "text", text: renderEnvelope(envelope) }],
218
+ content: [
219
+ {
220
+ type: "text",
221
+ text: renderResultReport(result, { details: envelope }),
222
+ },
223
+ ],
153
224
  details: {
225
+ evidenceContext,
226
+ result,
154
227
  ok: !refusal,
155
228
  envelope,
156
229
  judgments,
@@ -160,10 +233,11 @@ export function createCheckDiffTool(dependencies) {
160
233
  };
161
234
  };
162
235
  if (!client)
163
- return finish(NOT_CONFIGURED);
236
+ return finish(NOT_CONFIGURED, "not_configured");
164
237
  const comparison = await resolveBase(exec, cwd, args.base, signal);
165
238
  if (!comparison.ok)
166
- return finish(comparison.error);
239
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
240
+ evidenceContext.resolvedBase = comparison.base;
167
241
  const base = comparison.base;
168
242
  const analysis = await createAnalysisContext();
169
243
  const collected = await collectUnits(exec, {
@@ -172,7 +246,30 @@ export function createCheckDiffTool(dependencies) {
172
246
  signal,
173
247
  }, analysis.parser);
174
248
  if (!collected.ok)
175
- return finish(collected.error);
249
+ return finish(collected.error, collected.cause ?? "git_failure");
250
+ const repositoryRoot = await exec("git", ["rev-parse", "--show-toplevel"], { cwd, timeout: TIMEOUT_MS, signal });
251
+ if (!repositoryRoot.code &&
252
+ !repositoryRoot.killed &&
253
+ evidenceContext.effectiveRoot)
254
+ evidenceContext.effectiveRoot.path = repositoryRoot.stdout.trim();
255
+ report.inventories.push({
256
+ id: "changed-units",
257
+ kind: "units",
258
+ rules: [
259
+ "Changed source units against resolved base; tests are evidence, not risk units",
260
+ ],
261
+ restrictions: args.only ?? [],
262
+ discovered: known(collected.units.length),
263
+ considered: known(collected.units.length),
264
+ scopeRestricted: Boolean(args.only?.length),
265
+ criteria: [],
266
+ });
267
+ for (const unit of collected.units)
268
+ report.expect(`unit:${unit.id}`, unitLabel(unit), "unit");
269
+ for (const limit of collected.limits)
270
+ report.diagnose(limit.kind === "secret_pattern"
271
+ ? "secret_pattern"
272
+ : "collection_omitted", `${limit.file}: ${limit.kind}`, limit.file);
176
273
  for (const limit of collected.limits)
177
274
  limitations.push({
178
275
  fact: `${limit.file}: ${limit.kind}`,
@@ -182,6 +279,7 @@ export function createCheckDiffTool(dependencies) {
182
279
  if (unit.before !== null || unit.after !== null)
183
280
  return true;
184
281
  unchecked.push(`${unitLabel(unit)} source unavailable`);
282
+ report.diagnose("binary_or_non_utf8", "Changed source unavailable", unit.file, [`unit:${unit.id}`]);
185
283
  return false;
186
284
  });
187
285
  if (!collected.units.length) {
@@ -195,22 +293,30 @@ export function createCheckDiffTool(dependencies) {
195
293
  testFilesPresent: collected.files.some((file) => isTestFile(file.path)),
196
294
  });
197
295
  if (!matrix.ok)
198
- return finish(matrix.error);
296
+ return finish(matrix.error, "invalid_arguments");
199
297
  const prepared = matrix;
200
- for (const warning of prepared.warnings)
298
+ prepared.state = withEvidenceContext(prepared.state, evidenceContext);
299
+ for (const unit of readableUnits)
300
+ report.items.delete(`unit:${unit.id}`);
301
+ for (const cell of prepared.cells)
302
+ report.expect(`risk:${cell.id}`, `${cell.unitId} ${cell.dimension}`, "unit", `risk:${cell.unitId}`);
303
+ for (const warning of prepared.warnings) {
201
304
  limitations.push(warning);
305
+ report.diagnose("unsupported_syntax", warning.fact, undefined, [], false);
306
+ }
202
307
  if (JSON.stringify(prepared.state).length > STATE_MAX_CHARS)
203
- return finish(`risk state exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}; rerun with base= a nearer ref`);
308
+ return finish(`risk state exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}; rerun with base= a nearer ref`, "evidence_too_large");
204
309
  const batches = prepareBatches(prepared.state, prepared.questions, {
205
310
  groups: prepared.groups,
206
311
  witnesses: prepared.witnessQuestionIds,
207
312
  });
208
313
  if (!batches.ok)
209
- return finish(batches.error);
314
+ return finish(batches.error, "group_too_large");
210
315
  let reservedMatrixCalls = batches.batches.length;
211
316
  const optionsFor = (matrixRequest = false) => ({
212
317
  signal,
213
318
  ...runtime.session.requestGate(),
319
+ admissionCause: () => budget?.kind === "session" ? "session_budget" : "call_budget",
214
320
  beforeRequest(questionCount) {
215
321
  if (matrixRequest && reservedMatrixCalls > 0)
216
322
  reservedMatrixCalls--;
@@ -235,6 +341,12 @@ export function createCheckDiffTool(dependencies) {
235
341
  onUsage: (usage) => runtime.session.recordUsage(usage),
236
342
  });
237
343
  const judge = async (state, questions, extra, matrixRequest = false) => {
344
+ state = withEvidenceContext(state, evidenceContext);
345
+ if (JSON.stringify(state).length > STATE_MAX_CHARS)
346
+ return {
347
+ ok: false,
348
+ error: `required evidence exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}`,
349
+ };
238
350
  const result = await client.judge(state, questions, {
239
351
  ...optionsFor(matrixRequest),
240
352
  ...extra,
@@ -247,6 +359,13 @@ export function createCheckDiffTool(dependencies) {
247
359
  : { proofs: [], limits: [] };
248
360
  const uncheckedCallers = new Set();
249
361
  for (const limit of local.limits) {
362
+ if (limit.kind !== "dynamic_access_uncovered") {
363
+ const id = `caller:${limit.unitId}`;
364
+ report.expect(id, `${limit.unitId} local caller`, "unit");
365
+ report.diagnose("collection_omitted", limit.reason, limit.paths.join(" + "), [id]);
366
+ }
367
+ else
368
+ report.diagnose("dynamic_dependency", limit.reason, limit.paths.join(" + "), [], false);
250
369
  limitations.push({
251
370
  fact: `${limit.unitId}: ${limit.reason} — ${limit.paths.join(" + ")}`,
252
371
  next: "read the named caller/provider pieces or supply an observation via jev_ask",
@@ -281,6 +400,35 @@ export function createCheckDiffTool(dependencies) {
281
400
  unhealthyQuestionIds: new Map(),
282
401
  };
283
402
  matrixHealthy = health.healthy;
403
+ for (const failure of health.controls)
404
+ report.diagnose(health.healthy ? "conservative_widening" : "control_failure", failure.fact, undefined, prepared.cells
405
+ .filter((cell) => health.unhealthyQuestionIds.has(cell.id))
406
+ .map((cell) => `risk:${cell.id}`), false);
407
+ report.countControls(result, prepared.witnessQuestionIds);
408
+ for (const cell of prepared.cells) {
409
+ const rawAnswer = result.ok ? result.answers[cell.id] : undefined;
410
+ const answer = rawAnswer?.type === "unjudged" && !rawAnswer.cause && budget
411
+ ? {
412
+ ...rawAnswer,
413
+ cause: budget.kind === "session"
414
+ ? "session_budget"
415
+ : "call_budget",
416
+ }
417
+ : rawAnswer;
418
+ const invalid = health.unhealthyQuestionIds.get(cell.id);
419
+ if (!result.ok)
420
+ report.failure(result, [`risk:${cell.id}`], budget
421
+ ? budget.kind === "session"
422
+ ? "session_budget"
423
+ : "call_budget"
424
+ : undefined);
425
+ else
426
+ report.answer(`risk:${cell.id}`, answer, {
427
+ band: invalid ? "unsure" : "verdict",
428
+ reason: invalid,
429
+ uncalibrated: cell.uncalibrated,
430
+ }, controlsFor(cell.id, result, prepared.witnessQuestionIds), false);
431
+ }
284
432
  const decoys = new Set(prepared.witnesses
285
433
  .filter((witness) => witness.expected === "no")
286
434
  .map((witness) => witness.unitId)).size;
@@ -332,17 +480,25 @@ export function createCheckDiffTool(dependencies) {
332
480
  for (const { proof, result: localResult } of locals) {
333
481
  callerProofs.push(proof);
334
482
  const unit = collected.units.find((u) => u.id === proof.unitId);
335
- if (!unit)
483
+ const reportId = `caller:${proof.unitId}`;
484
+ report.expect(reportId, `${unit ? unitLabel(unit) : proof.unitId} local caller`, "unit");
485
+ if (!localResult.ok) {
486
+ report.failure(localResult, [reportId], budget
487
+ ? budget.kind === "session"
488
+ ? "session_budget"
489
+ : "call_budget"
490
+ : undefined);
336
491
  continue;
337
- const answer = localResult.ok ? localResult.answers.caller : undefined;
492
+ }
493
+ const answer = localResult.answers.caller;
338
494
  if (answer?.type !== "choice") {
339
- unchecked.push(`${unitLabel(unit)} local caller`);
495
+ report.answer(reportId, answer);
496
+ unchecked.push(`${unit ? unitLabel(unit) : proof.unitId} local caller`);
340
497
  continue;
341
498
  }
342
499
  const p = answer.probabilities.new_failure ?? 0;
343
500
  const missing = (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN;
344
- if (!missing && p < BAND_BOOL_GRAY_A)
345
- continue;
501
+ const negative = !missing && p < BAND_BOOL_GRAY_A;
346
502
  const merged = [];
347
503
  for (const span of [...proof.paths].sort((a, b) => a.path.localeCompare(b.path) || a.start - b.start || a.end - b.end)) {
348
504
  const previous = merged.at(-1);
@@ -359,14 +515,40 @@ export function createCheckDiffTool(dependencies) {
359
515
  .map((span) => `${span.path}:${span.start}-${span.end}`)
360
516
  .join(" + ");
361
517
  const line = {
362
- label: `${unitLabel(unit)} reliability — static code only; ${passages}${omitted ? ` (+${omitted} passages)` : ""}`,
518
+ label: `${unit ? unitLabel(unit) : proof.unitId} reliability — static code only; ${passages}${omitted ? ` (+${omitted} passages)` : ""}`,
363
519
  value: {
364
- head: missing ? "cannot_tell" : "new_failure",
365
- p: missing ? (answer.probabilities.cannot_tell ?? 0) : p,
520
+ head: missing
521
+ ? "cannot_tell"
522
+ : negative
523
+ ? "no_new_failure"
524
+ : "new_failure",
525
+ p: missing
526
+ ? (answer.probabilities.cannot_tell ?? 0)
527
+ : negative
528
+ ? (answer.probabilities.no_new_failure ?? 0)
529
+ : p,
366
530
  },
367
- band: missing ? "abstain" : p >= FLAG_MIN ? "verdict" : "unsure",
368
- missing: "actual provider or binding missing; supply its declaration or an observation via jev_ask",
531
+ band: missing
532
+ ? "abstain"
533
+ : negative || p >= FLAG_MIN
534
+ ? "verdict"
535
+ : "unsure",
536
+ reason: missing
537
+ ? "actual provider or binding missing; supply its declaration or an observation via jev_ask"
538
+ : negative
539
+ ? "Local caller check does not flag a new failure; static evidence does not prove every caller safe."
540
+ : p < FLAG_MIN
541
+ ? "Local caller new-failure probability is in the gray band; inspect the stated proof passages natively."
542
+ : "Local caller new-failure probability meets the finding threshold; static code evidence only.",
369
543
  };
544
+ const callerItem = report.items.get(reportId);
545
+ if (callerItem)
546
+ report.items.set(reportId, { ...callerItem, label: line.label });
547
+ report.answer(reportId, answer, line);
548
+ if (missing && line.reason)
549
+ report.diagnose("missing_required", line.reason, line.label, [reportId], false);
550
+ if (!unit || negative)
551
+ continue;
370
552
  answers.push(line);
371
553
  unitOrder.set(line, unit.id);
372
554
  if (!missing && p >= FLAG_MIN) {
@@ -387,6 +569,16 @@ export function createCheckDiffTool(dependencies) {
387
569
  await Promise.all(severity.map(async (item) => {
388
570
  const request = prepareRiskSeverity(item.unit, item.dimension, item.evidence?.callerEvidence);
389
571
  const value = await judge(request.state, request.questions);
572
+ const reportId = `severity:${item.unit.id}:${item.dimension}`;
573
+ report.expect(reportId, `${item.unit.id} ${item.dimension} severity`, "unit");
574
+ if (value.ok)
575
+ report.answer(reportId, value.answers.severity);
576
+ else
577
+ report.failure(value, [reportId], budget
578
+ ? budget.kind === "session"
579
+ ? "session_budget"
580
+ : "call_budget"
581
+ : undefined);
390
582
  const answer = value.ok ? value.answers.severity : undefined;
391
583
  if (answer?.type === "score") {
392
584
  const suffix = ` · severity ${answer.score.toFixed(1)}/3`;