jev-agent-tools 0.1.4 → 0.2.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.
- package/CHANGELOG.md +70 -1
- package/CONTRIBUTING.md +40 -0
- package/README.md +42 -9
- package/SECURITY.md +27 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +189 -0
- package/dist/adapters/ask-proof.js +144 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +181 -0
- package/dist/adapters/docs.js +172 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +293 -0
- package/dist/adapters/find.js +122 -0
- package/dist/adapters/git-base.js +26 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +439 -0
- package/dist/adapters/locate-file.js +159 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +98 -0
- package/dist/adapters/risk-callers.js +426 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +76 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +131 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +257 -0
- package/dist/constants.js +119 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +194 -0
- package/dist/core/asks.js +436 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +120 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +44 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +434 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +187 -0
- package/dist/mcp/tools.js +116 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +42 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +1 -0
- package/dist/texts/ask.js +2 -0
- package/dist/texts/check-diff.js +17 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +14 -0
- package/dist/texts/guide.js +16 -0
- package/dist/texts/locate.js +10 -0
- package/dist/texts/select-tests.js +2 -0
- package/dist/tools/ask-files.js +217 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +686 -0
- package/dist/tools/check-diff.js +402 -0
- package/dist/tools/docs-check.js +299 -0
- package/dist/tools/find.js +389 -0
- package/dist/tools/locate.js +303 -0
- package/dist/tools/select-tests.js +567 -0
- package/dist/tools/spec-check.js +166 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +91 -0
- package/docs/design.md +3 -3
- package/docs/mcp.md +231 -0
- package/package.json +19 -4
- package/server.json +57 -0
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +7 -4
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/private-storage.ts +143 -0
- package/src/adapters/risk-callers.ts +4 -2
- package/src/adapters/shell.ts +97 -0
- package/src/configuration.ts +39 -12
- package/src/constants.ts +11 -0
- package/src/core/command-output.ts +17 -1
- package/src/host.ts +11 -0
- package/src/jev/client.ts +12 -0
- package/src/jev/types.ts +6 -0
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +282 -0
- package/src/mcp/tools.ts +166 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/tools/ask-files.ts +5 -7
- package/src/tools/ask.ts +26 -22
- package/src/tools/check-diff.ts +8 -5
- package/src/tools/docs-check.ts +1 -0
- package/src/tools/find.ts +5 -2
- package/src/tools/locate.ts +5 -8
- package/src/tools/select-tests.ts +7 -4
- package/src/tools/spec-check.ts +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { createAnalysisContext } from "../adapters/analysis-context.js";
|
|
4
|
+
import { canonicalPath } from "../adapters/canonical-path.js";
|
|
5
|
+
import { collectUnits } from "../adapters/git.js";
|
|
6
|
+
import { resolveBase } from "../adapters/git-base.js";
|
|
7
|
+
import { shareGitInventory } from "../adapters/git-inventory.js";
|
|
8
|
+
import { collectRiskCallers } from "../adapters/risk-callers.js";
|
|
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";
|
|
11
|
+
import { prepareBatches } from "../core/batches.js";
|
|
12
|
+
import { isTestFile } from "../core/diff.js";
|
|
13
|
+
import { buildEnvelope, } from "../core/output.js";
|
|
14
|
+
import { evaluateWitnessHealth, prepareRiskMatrix, prepareRiskSeverity, } from "../presets/risk.js";
|
|
15
|
+
import { renderEnvelope } from "../render.js";
|
|
16
|
+
import { CHECK_DIFF_DESCRIPTION } from "../texts/check-diff.js";
|
|
17
|
+
import { NOT_CONFIGURED } from "../texts/configuration.js";
|
|
18
|
+
import { runDocsCheck } from "./docs-check.js";
|
|
19
|
+
import { runSpecCheck } from "./spec-check.js";
|
|
20
|
+
export const checkDiffParameters = Type.Object({
|
|
21
|
+
check: Type.Union([
|
|
22
|
+
Type.Literal("risk"),
|
|
23
|
+
Type.Literal("docs"),
|
|
24
|
+
Type.Literal("spec"),
|
|
25
|
+
]),
|
|
26
|
+
base: Type.Optional(Type.String({ minLength: 1 })),
|
|
27
|
+
spec_path: Type.Optional(Type.String({ minLength: 1 })),
|
|
28
|
+
witnesses: Type.Optional(Type.Union([
|
|
29
|
+
Type.Literal("off"),
|
|
30
|
+
Type.Literal("auto"),
|
|
31
|
+
Type.Literal("on"),
|
|
32
|
+
])),
|
|
33
|
+
dimensions: Type.Optional(Type.Record(Type.String(), Type.String({ minLength: 1 }))),
|
|
34
|
+
only: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
35
|
+
max_calls: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
36
|
+
}, { additionalProperties: false });
|
|
37
|
+
function unitLabel(unit) {
|
|
38
|
+
const range = unit.afterRange ?? unit.beforeRange;
|
|
39
|
+
const fingerprint = createHash("sha256")
|
|
40
|
+
.update(JSON.stringify([unit.file, unit.name, unit.before, unit.after]))
|
|
41
|
+
.digest("hex")
|
|
42
|
+
.slice(0, 8);
|
|
43
|
+
return `${unit.file}${range ? `:${range.start}-${range.end}` : ""} ${unit.name} [${fingerprint}]`;
|
|
44
|
+
}
|
|
45
|
+
export function createCheckDiffTool(dependencies) {
|
|
46
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
47
|
+
return {
|
|
48
|
+
name: "jev_check_diff",
|
|
49
|
+
label: "Jev check diff",
|
|
50
|
+
description: CHECK_DIFF_DESCRIPTION,
|
|
51
|
+
parameters: checkDiffParameters,
|
|
52
|
+
...(host.isOmp
|
|
53
|
+
? { approval: "read", loadMode: "essential" }
|
|
54
|
+
: {
|
|
55
|
+
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
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
async execute(_id, args, signal, _update, ctx) {
|
|
61
|
+
const client = dependencies.client;
|
|
62
|
+
const cwd = await canonicalPath(ctx.cwd);
|
|
63
|
+
const exec = shareGitInventory(execute);
|
|
64
|
+
if (args.check === "docs" || args.check === "spec") {
|
|
65
|
+
const deps = { client, host, runtime, exec };
|
|
66
|
+
const result = args.check === "docs"
|
|
67
|
+
? await runDocsCheck(deps, {
|
|
68
|
+
cwd: cwd,
|
|
69
|
+
base: args.base,
|
|
70
|
+
signal,
|
|
71
|
+
maxCalls: args.max_calls,
|
|
72
|
+
})
|
|
73
|
+
: await runSpecCheck(deps, {
|
|
74
|
+
cwd: cwd,
|
|
75
|
+
base: args.base,
|
|
76
|
+
specPath: args.spec_path,
|
|
77
|
+
signal,
|
|
78
|
+
maxCalls: args.max_calls,
|
|
79
|
+
});
|
|
80
|
+
runtime.session.record(result.envelope);
|
|
81
|
+
runtime.guide.deliver(ctx);
|
|
82
|
+
const usage = result.judgments.reduce((sum, judgment) => ({
|
|
83
|
+
inputTokens: sum.inputTokens + (judgment.usage?.inputTokens ?? 0),
|
|
84
|
+
costUsd: sum.costUsd + (judgment.usage?.costUsd ?? 0),
|
|
85
|
+
}), { inputTokens: 0, costUsd: 0 });
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{ type: "text", text: renderEnvelope(result.envelope) },
|
|
89
|
+
],
|
|
90
|
+
details: { ...result, callerProofs: [] },
|
|
91
|
+
...hostUsage(host.isOmp, usage),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const started = performance.now();
|
|
95
|
+
const judgments = [];
|
|
96
|
+
const callerProofs = [];
|
|
97
|
+
const answers = [];
|
|
98
|
+
const unitOrder = new Map();
|
|
99
|
+
const limitations = [];
|
|
100
|
+
const unchecked = [];
|
|
101
|
+
let budget;
|
|
102
|
+
let sent = 0;
|
|
103
|
+
let matrixHealthy = true;
|
|
104
|
+
let emptyBase;
|
|
105
|
+
const finish = (refusal) => {
|
|
106
|
+
answers.sort((a, b) => (unitOrder.get(a) ?? "").localeCompare(unitOrder.get(b) ?? ""));
|
|
107
|
+
const uniqueLimitations = [
|
|
108
|
+
...new Map(limitations.map((limit) => [`${limit.fact}\0${limit.next}`, limit])).values(),
|
|
109
|
+
];
|
|
110
|
+
const usage = judgments.reduce((sum, value) => ({
|
|
111
|
+
inputTokens: sum.inputTokens + (value.usage?.inputTokens ?? 0),
|
|
112
|
+
costUsd: sum.costUsd + (value.usage?.costUsd ?? 0),
|
|
113
|
+
}), { inputTokens: 0, costUsd: 0 });
|
|
114
|
+
const noFindings = !refusal && matrixHealthy && !answers.length && !unchecked.length;
|
|
115
|
+
const envelope = buildEnvelope({
|
|
116
|
+
answers,
|
|
117
|
+
limitations: emptyBase !== undefined && !refusal
|
|
118
|
+
? [
|
|
119
|
+
...uniqueLimitations,
|
|
120
|
+
{
|
|
121
|
+
fact: `no changed units against ${emptyBase}`,
|
|
122
|
+
next: "nothing judged; pass base= or check the working directory",
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
: uniqueLimitations,
|
|
126
|
+
unchecked: [...new Set(unchecked)],
|
|
127
|
+
budget,
|
|
128
|
+
...(refusal ? { refusal } : {}),
|
|
129
|
+
...(noFindings && emptyBase === undefined
|
|
130
|
+
? {
|
|
131
|
+
lines: [
|
|
132
|
+
{
|
|
133
|
+
type: "list",
|
|
134
|
+
title: "risk: no findings",
|
|
135
|
+
items: [],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
140
|
+
yield: {
|
|
141
|
+
calls: judgments.reduce((n, j) => n + (j.calls ?? 0), 0),
|
|
142
|
+
questions: judgments.reduce((n, j) => n + (j.questions ?? 0), 0),
|
|
143
|
+
costUsd: usage.costUsd,
|
|
144
|
+
cacheHits: judgments.reduce((n, j) => n + (j.cacheHits ?? 0), 0),
|
|
145
|
+
cacheRequests: judgments.reduce((n, j) => n + (j.cacheRequests ?? 0), 0),
|
|
146
|
+
elapsedMs: performance.now() - started,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
runtime.session.record(envelope);
|
|
150
|
+
runtime.guide.deliver(ctx);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: renderEnvelope(envelope) }],
|
|
153
|
+
details: {
|
|
154
|
+
ok: !refusal,
|
|
155
|
+
envelope,
|
|
156
|
+
judgments,
|
|
157
|
+
callerProofs,
|
|
158
|
+
},
|
|
159
|
+
...hostUsage(host.isOmp, usage),
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
if (!client)
|
|
163
|
+
return finish(NOT_CONFIGURED);
|
|
164
|
+
const comparison = await resolveBase(exec, cwd, args.base, signal);
|
|
165
|
+
if (!comparison.ok)
|
|
166
|
+
return finish(comparison.error);
|
|
167
|
+
const base = comparison.base;
|
|
168
|
+
const analysis = await createAnalysisContext();
|
|
169
|
+
const collected = await collectUnits(exec, {
|
|
170
|
+
cwd: cwd,
|
|
171
|
+
base,
|
|
172
|
+
signal,
|
|
173
|
+
}, analysis.parser);
|
|
174
|
+
if (!collected.ok)
|
|
175
|
+
return finish(collected.error);
|
|
176
|
+
for (const limit of collected.limits)
|
|
177
|
+
limitations.push({
|
|
178
|
+
fact: `${limit.file}: ${limit.kind}`,
|
|
179
|
+
next: "read the complete before/after source or rerun with base= a nearer ref",
|
|
180
|
+
});
|
|
181
|
+
const readableUnits = collected.units.filter((unit) => {
|
|
182
|
+
if (unit.before !== null || unit.after !== null)
|
|
183
|
+
return true;
|
|
184
|
+
unchecked.push(`${unitLabel(unit)} source unavailable`);
|
|
185
|
+
return false;
|
|
186
|
+
});
|
|
187
|
+
if (!collected.units.length) {
|
|
188
|
+
emptyBase = base;
|
|
189
|
+
return finish();
|
|
190
|
+
}
|
|
191
|
+
if (!readableUnits.length)
|
|
192
|
+
return finish();
|
|
193
|
+
const matrix = prepareRiskMatrix(readableUnits, {
|
|
194
|
+
...args,
|
|
195
|
+
testFilesPresent: collected.files.some((file) => isTestFile(file.path)),
|
|
196
|
+
});
|
|
197
|
+
if (!matrix.ok)
|
|
198
|
+
return finish(matrix.error);
|
|
199
|
+
const prepared = matrix;
|
|
200
|
+
for (const warning of prepared.warnings)
|
|
201
|
+
limitations.push(warning);
|
|
202
|
+
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`);
|
|
204
|
+
const batches = prepareBatches(prepared.state, prepared.questions, {
|
|
205
|
+
groups: prepared.groups,
|
|
206
|
+
witnesses: prepared.witnessQuestionIds,
|
|
207
|
+
});
|
|
208
|
+
if (!batches.ok)
|
|
209
|
+
return finish(batches.error);
|
|
210
|
+
let reservedMatrixCalls = batches.batches.length;
|
|
211
|
+
const optionsFor = (matrixRequest = false) => ({
|
|
212
|
+
signal,
|
|
213
|
+
...runtime.session.requestGate(),
|
|
214
|
+
beforeRequest(questionCount) {
|
|
215
|
+
if (matrixRequest && reservedMatrixCalls > 0)
|
|
216
|
+
reservedMatrixCalls--;
|
|
217
|
+
const limit = args.max_calls === undefined
|
|
218
|
+
? undefined
|
|
219
|
+
: args.max_calls - (matrixRequest ? 0 : reservedMatrixCalls);
|
|
220
|
+
if (limit !== undefined && sent >= limit) {
|
|
221
|
+
budget = {
|
|
222
|
+
kind: "max_calls",
|
|
223
|
+
message: `max_calls=${args.max_calls} reached`,
|
|
224
|
+
};
|
|
225
|
+
return { ok: false, error: budget.message };
|
|
226
|
+
}
|
|
227
|
+
const admitted = runtime.session.admit(questionCount);
|
|
228
|
+
if (!admitted.ok) {
|
|
229
|
+
budget = { kind: "session", message: admitted.error };
|
|
230
|
+
return admitted;
|
|
231
|
+
}
|
|
232
|
+
sent++;
|
|
233
|
+
return admitted;
|
|
234
|
+
},
|
|
235
|
+
onUsage: (usage) => runtime.session.recordUsage(usage),
|
|
236
|
+
});
|
|
237
|
+
const judge = async (state, questions, extra, matrixRequest = false) => {
|
|
238
|
+
const result = await client.judge(state, questions, {
|
|
239
|
+
...optionsFor(matrixRequest),
|
|
240
|
+
...extra,
|
|
241
|
+
});
|
|
242
|
+
judgments.push(result);
|
|
243
|
+
return result;
|
|
244
|
+
};
|
|
245
|
+
const local = prepared.dimensions.some((dimension) => dimension.name === "reliability")
|
|
246
|
+
? await collectRiskCallers(exec, { cwd: cwd, base, signal }, collected.units, collected.files, analysis.parser)
|
|
247
|
+
: { proofs: [], limits: [] };
|
|
248
|
+
const uncheckedCallers = new Set();
|
|
249
|
+
for (const limit of local.limits) {
|
|
250
|
+
limitations.push({
|
|
251
|
+
fact: `${limit.unitId}: ${limit.reason} — ${limit.paths.join(" + ")}`,
|
|
252
|
+
next: "read the named caller/provider pieces or supply an observation via jev_ask",
|
|
253
|
+
});
|
|
254
|
+
if (limit.kind !== "dynamic_access_uncovered" &&
|
|
255
|
+
!uncheckedCallers.has(limit.unitId)) {
|
|
256
|
+
uncheckedCallers.add(limit.unitId);
|
|
257
|
+
unchecked.push(`${limit.unitId} local caller ${limit.paths.join(" + ")}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const matrixPromise = judge(prepared.state, prepared.questions, { groups: prepared.groups, witnesses: prepared.witnessQuestionIds }, true).finally(() => {
|
|
261
|
+
reservedMatrixCalls = 0;
|
|
262
|
+
});
|
|
263
|
+
const localPromises = local.proofs.map(async (proof) => {
|
|
264
|
+
if (args.max_calls !== undefined &&
|
|
265
|
+
sent + reservedMatrixCalls >= args.max_calls)
|
|
266
|
+
await matrixPromise;
|
|
267
|
+
return {
|
|
268
|
+
proof,
|
|
269
|
+
result: await judge(proof.state, { caller: proof.question }),
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
const [result, locals] = await Promise.all([
|
|
273
|
+
matrixPromise,
|
|
274
|
+
Promise.all(localPromises),
|
|
275
|
+
]);
|
|
276
|
+
const health = result.ok
|
|
277
|
+
? evaluateWitnessHealth(prepared, result)
|
|
278
|
+
: {
|
|
279
|
+
healthy: false,
|
|
280
|
+
controls: [],
|
|
281
|
+
unhealthyQuestionIds: new Map(),
|
|
282
|
+
};
|
|
283
|
+
matrixHealthy = health.healthy;
|
|
284
|
+
const decoys = new Set(prepared.witnesses
|
|
285
|
+
.filter((witness) => witness.expected === "no")
|
|
286
|
+
.map((witness) => witness.unitId)).size;
|
|
287
|
+
const references = new Set(prepared.witnesses
|
|
288
|
+
.filter((witness) => witness.expected === "yes")
|
|
289
|
+
.map((witness) => witness.unitId)).size;
|
|
290
|
+
const witnessStatus = health.healthy
|
|
291
|
+
? "healthy"
|
|
292
|
+
: !result.ok ||
|
|
293
|
+
health.controls.some((control) => control.fact.includes("unavailable"))
|
|
294
|
+
? "unavailable"
|
|
295
|
+
: "unhealthy";
|
|
296
|
+
limitations.push({
|
|
297
|
+
fact: prepared.witnesses.length
|
|
298
|
+
? `risk matrix: ${decoys} decoys and ${references} references ${witnessStatus}`
|
|
299
|
+
: "risk matrix: witnesses disabled",
|
|
300
|
+
next: "read the findings and stated limits",
|
|
301
|
+
});
|
|
302
|
+
limitations.push(...health.controls);
|
|
303
|
+
if (!result.ok)
|
|
304
|
+
limitations.push({
|
|
305
|
+
fact: `risk matrix: ${result.error}`,
|
|
306
|
+
next: "check Jev configuration and availability, then retry",
|
|
307
|
+
});
|
|
308
|
+
const severity = [];
|
|
309
|
+
for (const cell of prepared.cells) {
|
|
310
|
+
const unit = collected.units.find((u) => u.id === cell.unitId);
|
|
311
|
+
if (!unit)
|
|
312
|
+
continue;
|
|
313
|
+
const answer = result.ok ? result.answers[cell.id] : undefined;
|
|
314
|
+
if (answer?.type !== "bool") {
|
|
315
|
+
unchecked.push(`${unitLabel(unit)} ${cell.dimension}`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (answer.p < FLAG_MIN)
|
|
319
|
+
continue;
|
|
320
|
+
const reason = health.unhealthyQuestionIds.get(cell.id);
|
|
321
|
+
const line = {
|
|
322
|
+
label: `${unitLabel(unit)} ${cell.dimension}${reason ? ` — ${reason}` : ""}`,
|
|
323
|
+
value: { head: "risk", p: answer.p },
|
|
324
|
+
band: reason ? "unsure" : "verdict",
|
|
325
|
+
uncalibrated: cell.uncalibrated,
|
|
326
|
+
};
|
|
327
|
+
answers.push(line);
|
|
328
|
+
unitOrder.set(line, unit.id);
|
|
329
|
+
if (!cell.uncalibrated)
|
|
330
|
+
severity.push({ unit, dimension: cell.dimension, lines: [line] });
|
|
331
|
+
}
|
|
332
|
+
for (const { proof, result: localResult } of locals) {
|
|
333
|
+
callerProofs.push(proof);
|
|
334
|
+
const unit = collected.units.find((u) => u.id === proof.unitId);
|
|
335
|
+
if (!unit)
|
|
336
|
+
continue;
|
|
337
|
+
const answer = localResult.ok ? localResult.answers.caller : undefined;
|
|
338
|
+
if (answer?.type !== "choice") {
|
|
339
|
+
unchecked.push(`${unitLabel(unit)} local caller`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const p = answer.probabilities.new_failure ?? 0;
|
|
343
|
+
const missing = (answer.probabilities.cannot_tell ?? 0) >= CANNOT_TELL_MIN;
|
|
344
|
+
if (!missing && p < BAND_BOOL_GRAY_A)
|
|
345
|
+
continue;
|
|
346
|
+
const merged = [];
|
|
347
|
+
for (const span of [...proof.paths].sort((a, b) => a.path.localeCompare(b.path) || a.start - b.start || a.end - b.end)) {
|
|
348
|
+
const previous = merged.at(-1);
|
|
349
|
+
if (previous &&
|
|
350
|
+
previous.path === span.path &&
|
|
351
|
+
span.start <= previous.end + 1)
|
|
352
|
+
previous.end = Math.max(previous.end, span.end);
|
|
353
|
+
else
|
|
354
|
+
merged.push({ ...span });
|
|
355
|
+
}
|
|
356
|
+
const omitted = Math.max(0, merged.length - CALLER_DISPLAY_MAX_SPANS);
|
|
357
|
+
const passages = merged
|
|
358
|
+
.slice(0, CALLER_DISPLAY_MAX_SPANS)
|
|
359
|
+
.map((span) => `${span.path}:${span.start}-${span.end}`)
|
|
360
|
+
.join(" + ");
|
|
361
|
+
const line = {
|
|
362
|
+
label: `${unitLabel(unit)} reliability — static code only; ${passages}${omitted ? ` (+${omitted} passages)` : ""}`,
|
|
363
|
+
value: {
|
|
364
|
+
head: missing ? "cannot_tell" : "new_failure",
|
|
365
|
+
p: missing ? (answer.probabilities.cannot_tell ?? 0) : p,
|
|
366
|
+
},
|
|
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",
|
|
369
|
+
};
|
|
370
|
+
answers.push(line);
|
|
371
|
+
unitOrder.set(line, unit.id);
|
|
372
|
+
if (!missing && p >= FLAG_MIN) {
|
|
373
|
+
const existing = severity.find((item) => item.unit.id === unit.id && item.dimension === "reliability");
|
|
374
|
+
if (existing) {
|
|
375
|
+
existing.evidence = proof.state;
|
|
376
|
+
existing.lines.push(line);
|
|
377
|
+
}
|
|
378
|
+
else
|
|
379
|
+
severity.push({
|
|
380
|
+
unit,
|
|
381
|
+
dimension: "reliability",
|
|
382
|
+
lines: [line],
|
|
383
|
+
evidence: proof.state,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
await Promise.all(severity.map(async (item) => {
|
|
388
|
+
const request = prepareRiskSeverity(item.unit, item.dimension, item.evidence?.callerEvidence);
|
|
389
|
+
const value = await judge(request.state, request.questions);
|
|
390
|
+
const answer = value.ok ? value.answers.severity : undefined;
|
|
391
|
+
if (answer?.type === "score") {
|
|
392
|
+
const suffix = ` · severity ${answer.score.toFixed(1)}/3`;
|
|
393
|
+
for (const line of item.lines)
|
|
394
|
+
line.label += suffix;
|
|
395
|
+
}
|
|
396
|
+
else
|
|
397
|
+
unchecked.push(`${unitLabel(item.unit)} ${item.dimension} severity`);
|
|
398
|
+
}));
|
|
399
|
+
return finish();
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|