jev-agent-tools 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -0
  4. package/docs/design.md +187 -0
  5. package/docs/tools/jev_ask.md +92 -0
  6. package/docs/tools/jev_ask_files.md +80 -0
  7. package/docs/tools/jev_check_diff.md +77 -0
  8. package/docs/tools/jev_find_files.md +59 -0
  9. package/docs/tools/jev_locate_in_file.md +51 -0
  10. package/docs/tools/jev_select_tests.md +59 -0
  11. package/package.json +85 -0
  12. package/rules/jev-ask.md +4 -0
  13. package/src/adapters/analysis-context.ts +100 -0
  14. package/src/adapters/ask-files.ts +236 -0
  15. package/src/adapters/ask-proof.ts +202 -0
  16. package/src/adapters/ask-syntax.ts +463 -0
  17. package/src/adapters/command.ts +240 -0
  18. package/src/adapters/docs.ts +222 -0
  19. package/src/adapters/files.ts +411 -0
  20. package/src/adapters/find.ts +151 -0
  21. package/src/adapters/git-base.ts +32 -0
  22. package/src/adapters/git-inventory.ts +94 -0
  23. package/src/adapters/git.ts +525 -0
  24. package/src/adapters/locate-file.ts +197 -0
  25. package/src/adapters/output-lines.ts +50 -0
  26. package/src/adapters/risk-callers.ts +525 -0
  27. package/src/adapters/runner-version.ts +102 -0
  28. package/src/adapters/syntax.ts +229 -0
  29. package/src/adapters/test-inventory.ts +168 -0
  30. package/src/adapters/usage.ts +21 -0
  31. package/src/adapters/utf8.ts +57 -0
  32. package/src/constants.ts +109 -0
  33. package/src/core/ask-closure.ts +419 -0
  34. package/src/core/ask-proof.ts +32 -0
  35. package/src/core/ask-references.ts +249 -0
  36. package/src/core/asks.ts +616 -0
  37. package/src/core/batches.ts +83 -0
  38. package/src/core/command-output.ts +249 -0
  39. package/src/core/diff.ts +226 -0
  40. package/src/core/docs.ts +399 -0
  41. package/src/core/find.ts +157 -0
  42. package/src/core/git.ts +5 -0
  43. package/src/core/import-boundaries.ts +102 -0
  44. package/src/core/imports.ts +691 -0
  45. package/src/core/integrity.ts +64 -0
  46. package/src/core/lexical.ts +130 -0
  47. package/src/core/locate.ts +213 -0
  48. package/src/core/output.ts +264 -0
  49. package/src/core/pointer.ts +51 -0
  50. package/src/core/risk-callers.ts +1270 -0
  51. package/src/core/runner-version.ts +66 -0
  52. package/src/core/sections.ts +269 -0
  53. package/src/core/state.ts +53 -0
  54. package/src/core/syntax.ts +8 -0
  55. package/src/core/test-commands.ts +430 -0
  56. package/src/core/test-coverage.ts +103 -0
  57. package/src/core/test-discovery.ts +1695 -0
  58. package/src/core/test-evidence.ts +649 -0
  59. package/src/core/test-state.ts +99 -0
  60. package/src/core/truncate.ts +14 -0
  61. package/src/core/units.ts +531 -0
  62. package/src/describe.ts +26 -0
  63. package/src/guide.ts +42 -0
  64. package/src/host.ts +22 -0
  65. package/src/index.ts +40 -0
  66. package/src/jev/client.ts +505 -0
  67. package/src/jev/pool.ts +60 -0
  68. package/src/jev/types.ts +60 -0
  69. package/src/presets/docs.ts +85 -0
  70. package/src/presets/risk.ts +263 -0
  71. package/src/presets/spec.ts +111 -0
  72. package/src/presets/witnesses.ts +313 -0
  73. package/src/render.ts +45 -0
  74. package/src/result.ts +4 -0
  75. package/src/run-end.ts +157 -0
  76. package/src/runtime.ts +16 -0
  77. package/src/session.ts +106 -0
  78. package/src/texts/ask-files.ts +2 -0
  79. package/src/texts/ask.ts +4 -0
  80. package/src/texts/check-diff.ts +24 -0
  81. package/src/texts/configuration.ts +2 -0
  82. package/src/texts/find.ts +14 -0
  83. package/src/texts/guide.ts +16 -0
  84. package/src/texts/locate.ts +10 -0
  85. package/src/texts/run-end.ts +13 -0
  86. package/src/texts/select-tests.ts +3 -0
  87. package/src/tools/ask-files.ts +263 -0
  88. package/src/tools/ask-schema.ts +116 -0
  89. package/src/tools/ask.ts +925 -0
  90. package/src/tools/check-diff.ts +510 -0
  91. package/src/tools/docs-check.ts +399 -0
  92. package/src/tools/find.ts +529 -0
  93. package/src/tools/locate.ts +369 -0
  94. package/src/tools/select-tests.ts +746 -0
  95. package/src/tools/spec-check.ts +210 -0
@@ -0,0 +1,510 @@
1
+ import { createHash } from "node:crypto";
2
+ import { type Static, Type } from "@sinclair/typebox";
3
+ import { createAnalysisContext } from "../adapters/analysis-context.ts";
4
+ import { collectUnits } from "../adapters/git.ts";
5
+ import { resolveBase } from "../adapters/git-base.ts";
6
+ import { shareGitInventory } from "../adapters/git-inventory.ts";
7
+ import { collectRiskCallers } from "../adapters/risk-callers.ts";
8
+ import { hostUsage } from "../adapters/usage.ts";
9
+ import {
10
+ BAND_BOOL_GRAY_A,
11
+ CALLER_DISPLAY_MAX_SPANS,
12
+ CANNOT_TELL_MIN,
13
+ FLAG_MIN,
14
+ STATE_MAX_CHARS,
15
+ } from "../constants.ts";
16
+ import { prepareBatches } from "../core/batches.ts";
17
+ import { isTestFile } from "../core/diff.ts";
18
+ import {
19
+ type AnswerInput,
20
+ type BudgetRefusal,
21
+ buildEnvelope,
22
+ type Envelope,
23
+ type Limitation,
24
+ } from "../core/output.ts";
25
+ import type { CallerProof, EvidenceSpan } from "../core/risk-callers.ts";
26
+ import type { EvidenceUnit } from "../core/units.ts";
27
+ import type { GuideContext } from "../guide.ts";
28
+ import type {
29
+ Judgment,
30
+ JudgmentOptions,
31
+ Question,
32
+ State,
33
+ } from "../jev/types.ts";
34
+ import {
35
+ type BuiltinRiskDimension,
36
+ evaluateWitnessHealth,
37
+ prepareRiskMatrix,
38
+ prepareRiskSeverity,
39
+ } from "../presets/risk.ts";
40
+ import { renderEnvelope } from "../render.ts";
41
+ import type { ToolDependencies } from "../runtime.ts";
42
+ import { CHECK_DIFF_DESCRIPTION } from "../texts/check-diff.ts";
43
+ import { NOT_CONFIGURED } from "../texts/configuration.ts";
44
+ import { runDocsCheck } from "./docs-check.ts";
45
+ import { runSpecCheck } from "./spec-check.ts";
46
+
47
+ export const checkDiffParameters = Type.Object(
48
+ {
49
+ check: Type.Union([
50
+ Type.Literal("risk"),
51
+ Type.Literal("docs"),
52
+ Type.Literal("spec"),
53
+ ]),
54
+ base: Type.Optional(Type.String({ minLength: 1 })),
55
+ spec_path: Type.Optional(Type.String({ minLength: 1 })),
56
+ witnesses: Type.Optional(
57
+ Type.Union([
58
+ Type.Literal("off"),
59
+ Type.Literal("auto"),
60
+ Type.Literal("on"),
61
+ ]),
62
+ ),
63
+ dimensions: Type.Optional(
64
+ Type.Record(Type.String(), Type.String({ minLength: 1 })),
65
+ ),
66
+ only: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
67
+ max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
68
+ },
69
+ { additionalProperties: false },
70
+ );
71
+ export type CheckDiffArgs = Static<typeof checkDiffParameters>;
72
+ export interface RiskDetails {
73
+ ok: boolean;
74
+ envelope: Envelope;
75
+ judgments: Judgment[];
76
+ callerProofs: CallerProof[];
77
+ }
78
+ function unitLabel(unit: EvidenceUnit): string {
79
+ const range = unit.afterRange ?? unit.beforeRange;
80
+ const fingerprint = createHash("sha256")
81
+ .update(JSON.stringify([unit.file, unit.name, unit.before, unit.after]))
82
+ .digest("hex")
83
+ .slice(0, 8);
84
+ return `${unit.file}${range ? `:${range.start}-${range.end}` : ""} ${unit.name} [${fingerprint}]`;
85
+ }
86
+ export function createCheckDiffTool({
87
+ client,
88
+ host,
89
+ runtime,
90
+ exec: execute,
91
+ }: ToolDependencies) {
92
+ return {
93
+ name: "jev_check_diff",
94
+ label: "Jev check diff",
95
+ description: CHECK_DIFF_DESCRIPTION,
96
+ parameters: checkDiffParameters,
97
+ ...(host.isOmp
98
+ ? { approval: "read" as const, loadMode: "essential" as const }
99
+ : {
100
+ promptSnippet:
101
+ "Check the uncommitted diff for risky changes, stale docs or spec drift",
102
+ promptGuidelines: [
103
+ "jev_check_diff: run it once before you report done, not on a half-written diff",
104
+ ],
105
+ }),
106
+ async execute(
107
+ _id: string,
108
+ args: CheckDiffArgs,
109
+ signal: AbortSignal | undefined,
110
+ _update: unknown,
111
+ ctx: { cwd: string } & GuideContext,
112
+ ) {
113
+ const exec = shareGitInventory(execute);
114
+ if (args.check === "docs" || args.check === "spec") {
115
+ const deps = { client, host, runtime, exec };
116
+ const result =
117
+ args.check === "docs"
118
+ ? await runDocsCheck(deps, {
119
+ cwd: ctx.cwd,
120
+ base: args.base,
121
+ signal,
122
+ maxCalls: args.max_calls,
123
+ })
124
+ : await runSpecCheck(deps, {
125
+ cwd: ctx.cwd,
126
+ base: args.base,
127
+ specPath: args.spec_path,
128
+ signal,
129
+ maxCalls: args.max_calls,
130
+ });
131
+ runtime.session.record(result.envelope);
132
+ runtime.guide.deliver(ctx);
133
+ const usage = result.judgments.reduce(
134
+ (sum, judgment) => ({
135
+ inputTokens: sum.inputTokens + (judgment.usage?.inputTokens ?? 0),
136
+ costUsd: sum.costUsd + (judgment.usage?.costUsd ?? 0),
137
+ }),
138
+ { inputTokens: 0, costUsd: 0 },
139
+ );
140
+ return {
141
+ content: [
142
+ { type: "text" as const, text: renderEnvelope(result.envelope) },
143
+ ],
144
+ details: { ...result, callerProofs: [] },
145
+ ...hostUsage(host.isOmp, usage),
146
+ };
147
+ }
148
+ const started = performance.now();
149
+ const judgments: Judgment[] = [];
150
+ const callerProofs: CallerProof[] = [];
151
+ const answers: AnswerInput[] = [];
152
+ const unitOrder = new Map<AnswerInput, string>();
153
+ const limitations: Limitation[] = [];
154
+ const unchecked: string[] = [];
155
+ let budget: BudgetRefusal | undefined;
156
+ let sent = 0;
157
+ let matrixHealthy = true;
158
+ let emptyBase: string | undefined;
159
+ const finish = (refusal?: string) => {
160
+ answers.sort((a, b) =>
161
+ (unitOrder.get(a) ?? "").localeCompare(unitOrder.get(b) ?? ""),
162
+ );
163
+ const uniqueLimitations = [
164
+ ...new Map(
165
+ limitations.map((limit) => [`${limit.fact}\0${limit.next}`, limit]),
166
+ ).values(),
167
+ ];
168
+ const usage = judgments.reduce(
169
+ (sum, value) => ({
170
+ inputTokens: sum.inputTokens + (value.usage?.inputTokens ?? 0),
171
+ costUsd: sum.costUsd + (value.usage?.costUsd ?? 0),
172
+ }),
173
+ { inputTokens: 0, costUsd: 0 },
174
+ );
175
+ const noFindings =
176
+ !refusal && matrixHealthy && !answers.length && !unchecked.length;
177
+ const envelope = buildEnvelope({
178
+ answers,
179
+ limitations:
180
+ emptyBase !== undefined && !refusal
181
+ ? [
182
+ ...uniqueLimitations,
183
+ {
184
+ fact: `no changed units against ${emptyBase}`,
185
+ next: "nothing judged; pass base= or check the working directory",
186
+ },
187
+ ]
188
+ : uniqueLimitations,
189
+ unchecked: [...new Set(unchecked)],
190
+ budget,
191
+ ...(refusal ? { refusal } : {}),
192
+ ...(noFindings && emptyBase === undefined
193
+ ? {
194
+ lines: [
195
+ {
196
+ type: "list" as const,
197
+ title: "risk: no findings",
198
+ items: [],
199
+ },
200
+ ],
201
+ }
202
+ : {}),
203
+ yield: {
204
+ calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
205
+ questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
206
+ costUsd: usage.costUsd,
207
+ cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
208
+ cacheRequests: judgments.reduce(
209
+ (n, j) => n + (j.cacheRequests ?? 0),
210
+ 0,
211
+ ),
212
+ elapsedMs: performance.now() - started,
213
+ },
214
+ });
215
+ runtime.session.record(envelope);
216
+ runtime.guide.deliver(ctx);
217
+ return {
218
+ content: [{ type: "text" as const, text: renderEnvelope(envelope) }],
219
+ details: {
220
+ ok: !refusal,
221
+ envelope,
222
+ judgments,
223
+ callerProofs,
224
+ } satisfies RiskDetails,
225
+ ...hostUsage(host.isOmp, usage),
226
+ };
227
+ };
228
+ if (!client) return finish(NOT_CONFIGURED);
229
+ const comparison = await resolveBase(exec, ctx.cwd, args.base, signal);
230
+ if (!comparison.ok) return finish(comparison.error);
231
+ const base = comparison.base;
232
+ const analysis = await createAnalysisContext();
233
+ const collected = await collectUnits(
234
+ exec,
235
+ {
236
+ cwd: ctx.cwd,
237
+ base,
238
+ signal,
239
+ },
240
+ analysis.parser,
241
+ );
242
+ if (!collected.ok) return finish(collected.error);
243
+ for (const limit of collected.limits)
244
+ limitations.push({
245
+ fact: `${limit.file}: ${limit.kind}`,
246
+ next: "read the complete before/after source or rerun with base= a nearer ref",
247
+ });
248
+ const readableUnits = collected.units.filter((unit) => {
249
+ if (unit.before !== null || unit.after !== null) return true;
250
+ unchecked.push(`${unitLabel(unit)} source unavailable`);
251
+ return false;
252
+ });
253
+ if (!collected.units.length) {
254
+ emptyBase = base;
255
+ return finish();
256
+ }
257
+ if (!readableUnits.length) return finish();
258
+ const matrix = prepareRiskMatrix(readableUnits, {
259
+ ...args,
260
+ testFilesPresent: collected.files.some((file) => isTestFile(file.path)),
261
+ });
262
+ if (!matrix.ok) return finish(matrix.error);
263
+ const prepared = matrix;
264
+ for (const warning of prepared.warnings) limitations.push(warning);
265
+ if (JSON.stringify(prepared.state).length > STATE_MAX_CHARS)
266
+ return finish(
267
+ `risk state exceeds STATE_MAX_CHARS=${STATE_MAX_CHARS}; rerun with base= a nearer ref`,
268
+ );
269
+ const batches = prepareBatches(prepared.state, prepared.questions, {
270
+ groups: prepared.groups,
271
+ witnesses: prepared.witnessQuestionIds,
272
+ });
273
+ if (!batches.ok) return finish(batches.error);
274
+ let reservedMatrixCalls = batches.batches.length;
275
+ const optionsFor = (matrixRequest = false): JudgmentOptions => ({
276
+ signal,
277
+ beforeRequest(questionCount) {
278
+ if (matrixRequest && reservedMatrixCalls > 0) reservedMatrixCalls--;
279
+ const limit =
280
+ args.max_calls === undefined
281
+ ? undefined
282
+ : args.max_calls - (matrixRequest ? 0 : reservedMatrixCalls);
283
+ if (limit !== undefined && sent >= limit) {
284
+ budget = {
285
+ kind: "max_calls",
286
+ message: `max_calls=${args.max_calls} reached`,
287
+ };
288
+ return { ok: false, error: budget.message };
289
+ }
290
+ const admitted = runtime.session.admit(questionCount);
291
+ if (!admitted.ok) {
292
+ budget = { kind: "session", message: admitted.error };
293
+ return admitted;
294
+ }
295
+ sent++;
296
+ return admitted;
297
+ },
298
+ onUsage: (usage) => runtime.session.recordUsage(usage),
299
+ });
300
+ const judge = async (
301
+ state: State,
302
+ questions: Record<string, Question>,
303
+ extra?: JudgmentOptions,
304
+ matrixRequest = false,
305
+ ) => {
306
+ const result = await client.judge(state, questions, {
307
+ ...optionsFor(matrixRequest),
308
+ ...extra,
309
+ });
310
+ judgments.push(result);
311
+ return result;
312
+ };
313
+ const local = prepared.dimensions.some(
314
+ (dimension) => dimension.name === "reliability",
315
+ )
316
+ ? await collectRiskCallers(
317
+ exec,
318
+ { cwd: ctx.cwd, base, signal },
319
+ collected.units,
320
+ collected.files,
321
+ analysis.parser,
322
+ )
323
+ : { proofs: [], limits: [] };
324
+ const uncheckedCallers = new Set<string>();
325
+ for (const limit of local.limits) {
326
+ limitations.push({
327
+ fact: `${limit.unitId}: ${limit.reason} — ${limit.paths.join(" + ")}`,
328
+ next: "read the named caller/provider pieces or supply an observation via jev_ask",
329
+ });
330
+ if (
331
+ limit.kind !== "dynamic_access_uncovered" &&
332
+ !uncheckedCallers.has(limit.unitId)
333
+ ) {
334
+ uncheckedCallers.add(limit.unitId);
335
+ unchecked.push(
336
+ `${limit.unitId} local caller ${limit.paths.join(" + ")}`,
337
+ );
338
+ }
339
+ }
340
+ const matrixPromise = judge(
341
+ prepared.state,
342
+ prepared.questions,
343
+ { groups: prepared.groups, witnesses: prepared.witnessQuestionIds },
344
+ true,
345
+ ).finally(() => {
346
+ reservedMatrixCalls = 0;
347
+ });
348
+ const localPromises = local.proofs.map(async (proof) => {
349
+ if (
350
+ args.max_calls !== undefined &&
351
+ sent + reservedMatrixCalls >= args.max_calls
352
+ )
353
+ await matrixPromise;
354
+ return {
355
+ proof,
356
+ result: await judge(proof.state, { caller: proof.question }),
357
+ };
358
+ });
359
+ const [result, locals] = await Promise.all([
360
+ matrixPromise,
361
+ Promise.all(localPromises),
362
+ ]);
363
+ const health = result.ok
364
+ ? evaluateWitnessHealth(prepared, result)
365
+ : {
366
+ healthy: false,
367
+ controls: [],
368
+ unhealthyQuestionIds: new Map<string, string>(),
369
+ };
370
+ matrixHealthy = health.healthy;
371
+ const decoys = new Set(
372
+ prepared.witnesses
373
+ .filter((witness) => witness.expected === "no")
374
+ .map((witness) => witness.unitId),
375
+ ).size;
376
+ const references = new Set(
377
+ prepared.witnesses
378
+ .filter((witness) => witness.expected === "yes")
379
+ .map((witness) => witness.unitId),
380
+ ).size;
381
+ const witnessStatus = health.healthy
382
+ ? "healthy"
383
+ : !result.ok ||
384
+ health.controls.some((control) =>
385
+ control.fact.includes("unavailable"),
386
+ )
387
+ ? "unavailable"
388
+ : "unhealthy";
389
+ limitations.push({
390
+ fact: prepared.witnesses.length
391
+ ? `risk matrix: ${decoys} decoys and ${references} references ${witnessStatus}`
392
+ : "risk matrix: witnesses disabled",
393
+ next: "read the findings and stated limits",
394
+ });
395
+ limitations.push(...health.controls);
396
+ if (!result.ok)
397
+ limitations.push({
398
+ fact: `risk matrix: ${result.error}`,
399
+ next: "check Jev configuration and availability, then retry",
400
+ });
401
+ const severity: {
402
+ unit: EvidenceUnit;
403
+ dimension: BuiltinRiskDimension;
404
+ lines: AnswerInput[];
405
+ evidence?: State;
406
+ }[] = [];
407
+ for (const cell of prepared.cells) {
408
+ const unit = collected.units.find((u) => u.id === cell.unitId);
409
+ if (!unit) continue;
410
+ const answer = result.ok ? result.answers[cell.id] : undefined;
411
+ if (answer?.type !== "bool") {
412
+ unchecked.push(`${unitLabel(unit)} ${cell.dimension}`);
413
+ continue;
414
+ }
415
+ if (answer.p < FLAG_MIN) continue;
416
+ const reason = health.unhealthyQuestionIds.get(cell.id);
417
+ const line: AnswerInput = {
418
+ label: `${unitLabel(unit)} ${cell.dimension}${reason ? ` — ${reason}` : ""}`,
419
+ value: { head: "risk", p: answer.p },
420
+ band: reason ? "unsure" : "verdict",
421
+ uncalibrated: cell.uncalibrated,
422
+ };
423
+ answers.push(line);
424
+ unitOrder.set(line, unit.id);
425
+ if (!cell.uncalibrated)
426
+ severity.push({ unit, dimension: cell.dimension, lines: [line] });
427
+ }
428
+ for (const { proof, result: localResult } of locals) {
429
+ callerProofs.push(proof);
430
+ const unit = collected.units.find((u) => u.id === proof.unitId);
431
+ if (!unit) continue;
432
+ const answer = localResult.ok ? localResult.answers.caller : undefined;
433
+ if (answer?.type !== "choice") {
434
+ unchecked.push(`${unitLabel(unit)} local caller`);
435
+ continue;
436
+ }
437
+ const p = answer.probabilities.new_failure ?? 0;
438
+ const missing =
439
+ (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN;
440
+ if (!missing && p < BAND_BOOL_GRAY_A) continue;
441
+ const merged: EvidenceSpan[] = [];
442
+ for (const span of [...proof.paths].sort(
443
+ (a, b) =>
444
+ a.path.localeCompare(b.path) || a.start - b.start || a.end - b.end,
445
+ )) {
446
+ const previous = merged.at(-1);
447
+ if (
448
+ previous &&
449
+ previous.path === span.path &&
450
+ span.start <= previous.end + 1
451
+ )
452
+ previous.end = Math.max(previous.end, span.end);
453
+ else merged.push({ ...span });
454
+ }
455
+ const omitted = Math.max(0, merged.length - CALLER_DISPLAY_MAX_SPANS);
456
+ const passages = merged
457
+ .slice(0, CALLER_DISPLAY_MAX_SPANS)
458
+ .map((span) => `${span.path}:${span.start}-${span.end}`)
459
+ .join(" + ");
460
+ const line: AnswerInput = {
461
+ label: `${unitLabel(unit)} reliability — static code only; ${passages}${omitted ? ` (+${omitted} passages)` : ""}`,
462
+ value: {
463
+ head: missing ? "cannot_tell" : "new_failure",
464
+ p: missing ? (answer.probabilities.cannot_tell ?? 0) : p,
465
+ },
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",
469
+ };
470
+ answers.push(line);
471
+ unitOrder.set(line, unit.id);
472
+ if (!missing && p >= FLAG_MIN) {
473
+ const existing = severity.find(
474
+ (item) =>
475
+ item.unit.id === unit.id && item.dimension === "reliability",
476
+ );
477
+ if (existing) {
478
+ existing.evidence = proof.state;
479
+ existing.lines.push(line);
480
+ } else
481
+ severity.push({
482
+ unit,
483
+ dimension: "reliability",
484
+ lines: [line],
485
+ evidence: proof.state,
486
+ });
487
+ }
488
+ }
489
+ await Promise.all(
490
+ severity.map(async (item) => {
491
+ const request = prepareRiskSeverity(
492
+ item.unit,
493
+ item.dimension,
494
+ item.evidence?.callerEvidence,
495
+ );
496
+ const value = await judge(request.state, request.questions);
497
+ const answer = value.ok ? value.answers.severity : undefined;
498
+ if (answer?.type === "score") {
499
+ const suffix = ` · severity ${answer.score.toFixed(1)}/3`;
500
+ for (const line of item.lines) line.label += suffix;
501
+ } else
502
+ unchecked.push(
503
+ `${unitLabel(item.unit)} ${item.dimension} severity`,
504
+ );
505
+ }),
506
+ );
507
+ return finish();
508
+ },
509
+ };
510
+ }