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,7 +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 { canonicalPath } from "../adapters/canonical-path.ts";
4
+ import {
5
+ type EvidenceContext,
6
+ resolveEvidenceContext,
7
+ withEvidenceContext,
8
+ } from "../adapters/evidence-context.ts";
5
9
  import { collectUnits } from "../adapters/git.ts";
6
10
  import { resolveBase } from "../adapters/git-base.ts";
7
11
  import { shareGitInventory } from "../adapters/git-inventory.ts";
@@ -13,6 +17,7 @@ import {
13
17
  CANNOT_TELL_MIN,
14
18
  FLAG_MIN,
15
19
  STATE_MAX_CHARS,
20
+ TIMEOUT_MS,
16
21
  } from "../constants.ts";
17
22
  import { prepareBatches } from "../core/batches.ts";
18
23
  import { isTestFile } from "../core/diff.ts";
@@ -23,6 +28,11 @@ import {
23
28
  type Envelope,
24
29
  type Limitation,
25
30
  } from "../core/output.ts";
31
+ import {
32
+ type Cause,
33
+ known,
34
+ type ResultReportV1,
35
+ } from "../core/result-report.ts";
26
36
  import type { CallerProof, EvidenceSpan } from "../core/risk-callers.ts";
27
37
  import type { EvidenceUnit } from "../core/units.ts";
28
38
  import type { GuideContext } from "../guide.ts";
@@ -38,15 +48,18 @@ import {
38
48
  prepareRiskMatrix,
39
49
  prepareRiskSeverity,
40
50
  } from "../presets/risk.ts";
41
- import { renderEnvelope } from "../render.ts";
51
+ import { renderResultReport } from "../render.ts";
42
52
  import type { ToolDependencies } from "../runtime.ts";
43
53
  import { CHECK_DIFF_DESCRIPTION } from "../texts/check-diff.ts";
44
54
  import { NOT_CONFIGURED } from "../texts/configuration.ts";
55
+ import { CHECK_DIFF_GUIDELINE } from "../texts/instructions.ts";
45
56
  import { runDocsCheck } from "./docs-check.ts";
57
+ import { controlsFor, ReviewReport, reportMetrics } from "./review-report.ts";
46
58
  import { runSpecCheck } from "./spec-check.ts";
47
59
 
48
60
  export const checkDiffParameters = Type.Object(
49
61
  {
62
+ root: Type.Optional(Type.String()),
50
63
  check: Type.Union([
51
64
  Type.Literal("risk"),
52
65
  Type.Literal("docs"),
@@ -72,6 +85,8 @@ export const checkDiffParameters = Type.Object(
72
85
  export type CheckDiffArgs = Static<typeof checkDiffParameters>;
73
86
  export interface RiskDetails {
74
87
  ok: boolean;
88
+ evidenceContext: EvidenceContext;
89
+ result: ResultReportV1;
75
90
  envelope: Envelope;
76
91
  judgments: Judgment[];
77
92
  callerProofs: CallerProof[];
@@ -96,9 +111,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
96
111
  : {
97
112
  promptSnippet:
98
113
  "Check the uncommitted diff for risky changes, stale docs or spec drift",
99
- promptGuidelines: [
100
- "jev_check_diff: run it once before you report done, not on a half-written diff",
101
- ],
114
+ promptGuidelines: [CHECK_DIFF_GUIDELINE],
102
115
  }),
103
116
  async execute(
104
117
  _id: string,
@@ -108,8 +121,53 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
108
121
  ctx: { cwd: string } & GuideContext,
109
122
  ) {
110
123
  const client = dependencies.client;
111
- const cwd = await canonicalPath(ctx.cwd);
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;
112
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";
113
171
  if (args.check === "docs" || args.check === "spec") {
114
172
  const deps = { client, host, runtime, exec };
115
173
  const result =
@@ -119,6 +177,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
119
177
  base: args.base,
120
178
  signal,
121
179
  maxCalls: args.max_calls,
180
+ evidenceContext,
122
181
  })
123
182
  : await runSpecCheck(deps, {
124
183
  cwd: cwd,
@@ -126,6 +185,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
126
185
  specPath: args.spec_path,
127
186
  signal,
128
187
  maxCalls: args.max_calls,
188
+ evidenceContext,
129
189
  });
130
190
  runtime.session.record(result.envelope);
131
191
  runtime.guide.deliver(ctx);
@@ -138,9 +198,14 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
138
198
  );
139
199
  return {
140
200
  content: [
141
- { 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
+ },
142
207
  ],
143
- details: { ...result, callerProofs: [] },
208
+ details: { ...result, callerProofs: [], evidenceContext },
144
209
  ...hostUsage(host.isOmp, usage),
145
210
  };
146
211
  }
@@ -155,7 +220,7 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
155
220
  let sent = 0;
156
221
  let matrixHealthy = true;
157
222
  let emptyBase: string | undefined;
158
- const finish = (refusal?: string) => {
223
+ const finish = (refusal?: string, cause?: Cause) => {
159
224
  answers.sort((a, b) =>
160
225
  (unitOrder.get(a) ?? "").localeCompare(unitOrder.get(b) ?? ""),
161
226
  );
@@ -172,7 +237,14 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
172
237
  { inputTokens: 0, costUsd: 0 },
173
238
  );
174
239
  const noFindings =
175
- !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
+ );
176
248
  const envelope = buildEnvelope({
177
249
  answers,
178
250
  limitations:
@@ -202,7 +274,11 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
202
274
  yield: {
203
275
  calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
204
276
  questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
205
- costUsd: usage.costUsd,
277
+ costUsd:
278
+ judgments.every((j) => j.usage !== undefined) &&
279
+ judgments.length > 0
280
+ ? usage.costUsd
281
+ : undefined,
206
282
  cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
207
283
  cacheRequests: judgments.reduce(
208
284
  (n, j) => n + (j.cacheRequests ?? 0),
@@ -211,11 +287,38 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
211
287
  elapsedMs: performance.now() - started,
212
288
  },
213
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
+ );
214
310
  runtime.session.record(envelope);
215
311
  runtime.guide.deliver(ctx);
216
312
  return {
217
- content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
313
+ content: [
314
+ {
315
+ type: "text" as const,
316
+ text: renderResultReport(result, { details: envelope }),
317
+ },
318
+ ],
218
319
  details: {
320
+ evidenceContext,
321
+ result,
219
322
  ok: !refusal,
220
323
  envelope,
221
324
  judgments,
@@ -224,9 +327,11 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
224
327
  ...hostUsage(host.isOmp, usage),
225
328
  };
226
329
  };
227
- if (!client) return finish(NOT_CONFIGURED);
330
+ if (!client) return finish(NOT_CONFIGURED, "not_configured");
228
331
  const comparison = await resolveBase(exec, cwd, args.base, signal);
229
- if (!comparison.ok) return finish(comparison.error);
332
+ if (!comparison.ok)
333
+ return finish(comparison.error, comparison.cause ?? "invalid_base");
334
+ evidenceContext.resolvedBase = comparison.base;
230
335
  const base = comparison.base;
231
336
  const analysis = await createAnalysisContext();
232
337
  const collected = await collectUnits(
@@ -238,7 +343,41 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
238
343
  },
239
344
  analysis.parser,
240
345
  );
241
- 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
+ );
242
381
  for (const limit of collected.limits)
243
382
  limitations.push({
244
383
  fact: `${limit.file}: ${limit.kind}`,
@@ -247,6 +386,12 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
247
386
  const readableUnits = collected.units.filter((unit) => {
248
387
  if (unit.before !== null || unit.after !== null) return true;
249
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
+ );
250
395
  return false;
251
396
  });
252
397
  if (!collected.units.length) {
@@ -258,22 +403,43 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
258
403
  ...args,
259
404
  testFilesPresent: collected.files.some((file) => isTestFile(file.path)),
260
405
  });
261
- if (!matrix.ok) return finish(matrix.error);
406
+ if (!matrix.ok) return finish(matrix.error, "invalid_arguments");
262
407
  const prepared = matrix;
263
- 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
+ }
264
427
  if (JSON.stringify(prepared.state).length > STATE_MAX_CHARS)
265
428
  return finish(
266
429
  `risk state exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}; rerun with base= a nearer ref`,
430
+ "evidence_too_large",
267
431
  );
268
432
  const batches = prepareBatches(prepared.state, prepared.questions, {
269
433
  groups: prepared.groups,
270
434
  witnesses: prepared.witnessQuestionIds,
271
435
  });
272
- if (!batches.ok) return finish(batches.error);
436
+ if (!batches.ok) return finish(batches.error, "group_too_large");
273
437
  let reservedMatrixCalls = batches.batches.length;
274
438
  const optionsFor = (matrixRequest = false): JudgmentOptions => ({
275
439
  signal,
276
440
  ...runtime.session.requestGate(),
441
+ admissionCause: () =>
442
+ budget?.kind === "session" ? "session_budget" : "call_budget",
277
443
  beforeRequest(questionCount) {
278
444
  if (matrixRequest && reservedMatrixCalls > 0) reservedMatrixCalls--;
279
445
  const limit =
@@ -303,6 +469,12 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
303
469
  extra?: JudgmentOptions,
304
470
  matrixRequest = false,
305
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
+ };
306
478
  const result = await client.judge(state, questions, {
307
479
  ...optionsFor(matrixRequest),
308
480
  ...extra,
@@ -323,6 +495,23 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
323
495
  : { proofs: [], limits: [] };
324
496
  const uncheckedCallers = new Set<string>();
325
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
+ );
326
515
  limitations.push({
327
516
  fact: `${limit.unitId}: ${limit.reason} — ${limit.paths.join(" + ")}`,
328
517
  next: "read the named caller/provider pieces or supply an observation via jev_ask",
@@ -368,6 +557,53 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
368
557
  unhealthyQuestionIds: new Map<string, string>(),
369
558
  };
370
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
+ }
371
607
  const decoys = new Set(
372
608
  prepared.witnesses
373
609
  .filter((witness) => witness.expected === "no")
@@ -428,16 +664,36 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
428
664
  for (const { proof, result: localResult } of locals) {
429
665
  callerProofs.push(proof);
430
666
  const unit = collected.units.find((u) => u.id === proof.unitId);
431
- if (!unit) continue;
432
- 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;
433
686
  if (answer?.type !== "choice") {
434
- unchecked.push(`${unitLabel(unit)} local caller`);
687
+ report.answer(reportId, answer);
688
+ unchecked.push(
689
+ `${unit ? unitLabel(unit) : proof.unitId} local caller`,
690
+ );
435
691
  continue;
436
692
  }
437
693
  const p = answer.probabilities.new_failure ?? 0;
438
694
  const missing =
439
695
  (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN;
440
- if (!missing && p < BAND_BOOL_GRAY_A) continue;
696
+ const negative = !missing && p < BAND_BOOL_GRAY_A;
441
697
  const merged: EvidenceSpan[] = [];
442
698
  for (const span of [...proof.paths].sort(
443
699
  (a, b) =>
@@ -458,15 +714,45 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
458
714
  .map((span) => `${span.path}:${span.start}-${span.end}`)
459
715
  .join(" + ");
460
716
  const line: AnswerInput = {
461
- 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)` : ""}`,
462
718
  value: {
463
- head: missing ? "cannot_tell" : "new_failure",
464
- 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,
465
729
  },
466
- band: missing ? "abstain" : p >= FLAG_MIN ? "verdict" : "unsure",
467
- missing:
468
- "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.",
469
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;
470
756
  answers.push(line);
471
757
  unitOrder.set(line, unit.id);
472
758
  if (!missing && p >= FLAG_MIN) {
@@ -494,6 +780,23 @@ export function createCheckDiffTool(dependencies: ToolDependencies) {
494
780
  item.evidence?.callerEvidence,
495
781
  );
496
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
+ );
497
800
  const answer = value.ok ? value.answers.severity : undefined;
498
801
  if (answer?.type === "score") {
499
802
  const suffix = ` · severity ${answer.score.toFixed(1)}/3`;