jev-agent-tools 0.1.3 → 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.
Files changed (135) hide show
  1. package/CHANGELOG.md +88 -3
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +64 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +294 -0
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host-tui.d.ts +14 -0
  117. package/src/host.ts +11 -0
  118. package/src/index.ts +13 -5
  119. package/src/jev/client.ts +12 -0
  120. package/src/jev/types.ts +6 -0
  121. package/src/mcp/main.ts +135 -0
  122. package/src/mcp/protocol.ts +282 -0
  123. package/src/mcp/tools.ts +166 -0
  124. package/src/secret-input.ts +222 -0
  125. package/src/session.ts +59 -0
  126. package/src/setup.ts +170 -0
  127. package/src/texts/configuration.ts +1 -1
  128. package/src/tools/ask-files.ts +8 -13
  129. package/src/tools/ask.ts +29 -28
  130. package/src/tools/check-diff.ts +11 -11
  131. package/src/tools/docs-check.ts +1 -0
  132. package/src/tools/find.ts +8 -8
  133. package/src/tools/locate.ts +8 -9
  134. package/src/tools/select-tests.ts +10 -10
  135. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,436 @@
1
+ import { BAND_BOOL_YES_MIN, BAND_CHOICE_VERDICT_MIN, CANNOT_TELL_MIN, CHOICE_MAX_OPTIONS, ORDER_DISAGREE_MIN, ORDER_REVERSE_BELOW, SCORE_MAX_LEVELS, SCORE_MIN_LEVELS, } from "../constants.js";
2
+ import { isRecord } from "../result.js";
3
+ const gap = "If a document, file, requirement or output that the question refers to is missing or empty in the state, choose cannot_tell.";
4
+ const other = "None of these substantive options applies.";
5
+ const cannot = "The state does not contain the evidence needed to decide.";
6
+ const bool = (text) => ({
7
+ type: "bool",
8
+ instructions: text,
9
+ criteria: {
10
+ true: "The statement is shown true.",
11
+ false: "The statement is not shown true.",
12
+ },
13
+ });
14
+ const text = (v) => typeof v === "string" && v.trim().length > 0;
15
+ const record = (v) => isRecord(v) && Object.keys(v).length > 0 && Object.values(v).every(text);
16
+ function hash(s) {
17
+ let h = 2166136261;
18
+ for (let i = 0; i < s.length; i++)
19
+ h = Math.imul(h ^ s.charCodeAt(i), 16777619);
20
+ return h >>> 0;
21
+ }
22
+ function canonical(criteria) {
23
+ const first = ["none", "not_addressed"].filter((k) => k in criteria);
24
+ const last = ["other", "cannot_tell"].filter((k) => k in criteria);
25
+ const middle = Object.keys(criteria)
26
+ .filter((k) => !first.includes(k) && !last.includes(k))
27
+ .sort((a, b) => hash(a) - hash(b) || a.localeCompare(b));
28
+ return Object.fromEntries([...first, ...middle, ...last].map((k) => [k, criteria[k] ?? ""]));
29
+ }
30
+ function reverse(q) {
31
+ const keys = Object.keys(q.criteria);
32
+ const middle = keys
33
+ .filter((k) => !["none", "not_addressed", "other", "cannot_tell"].includes(k))
34
+ .reverse();
35
+ let index = 0;
36
+ return {
37
+ ...q,
38
+ criteria: Object.fromEntries(keys.map((k) => {
39
+ const key = ["none", "not_addressed", "other", "cannot_tell"].includes(k)
40
+ ? k
41
+ : (middle[index++] ?? k);
42
+ return [key, q.criteria[key] ?? ""];
43
+ })),
44
+ };
45
+ }
46
+ export function compileAsks(input, { surface }) {
47
+ if (typeof input === "string") {
48
+ try {
49
+ input = JSON.parse(input);
50
+ }
51
+ catch {
52
+ return {
53
+ ok: false,
54
+ error: 'Invalid asks JSON. Example: {"intent":"verify","claims":{"c1":"The file validates tokens"}}',
55
+ };
56
+ }
57
+ }
58
+ const values = Array.isArray(input) ? input : [input];
59
+ const plan = {
60
+ asks: [],
61
+ questions: {},
62
+ groups: [],
63
+ readings: [],
64
+ warnings: [],
65
+ };
66
+ const fail = () => ({
67
+ ok: false,
68
+ error: 'Invalid intention or missing field. Example: {"intent":"verify","claims":{"c1":"The file validates tokens"}}',
69
+ });
70
+ if (!values.length)
71
+ return fail();
72
+ let questionCount = 0;
73
+ function lint(id, s, claim = false) {
74
+ if (claim && /\b(not|never|no longer|without)\b|;|\band\b/i.test(s))
75
+ plan.warnings.push({
76
+ fact: `${id} is negated or compound (sent as written)`,
77
+ next: "use one positive fact when possible",
78
+ });
79
+ if (/\b(how many|how much|count|number of)\b|\b(exactly|at least|at most|more than|fewer than)\s+\d+|\b\d+\s+(times|lines|calls|occurrences|files)\b|\d{4}-\d{2}-\d{2}|\b(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,4}\b|\b\d{1,4}\s+(january|february|march|april|may|june|july|august|september|october|november|december)\b|\b(before|after|since|until)\s+\d{4}/i.test(s))
80
+ plan.warnings.push({
81
+ fact: `${id} asks for a count or a date: Jev does not count`,
82
+ next: "use grep or code",
83
+ });
84
+ }
85
+ for (const [index, value] of values.entries()) {
86
+ if (!isRecord(value) ||
87
+ typeof value.intent !== "string" ||
88
+ (value.about !== undefined && !text(value.about)))
89
+ return fail();
90
+ const allowed = {
91
+ verify: ["claims"],
92
+ classify: ["categories", "pick", "by"],
93
+ decide: ["hypotheses"],
94
+ rate: ["dimension", "levels"],
95
+ locate: ["target", "among", "count", "attribution"],
96
+ free: ["question"],
97
+ };
98
+ if (Object.keys(value).some((k) => ![
99
+ "intent",
100
+ "about",
101
+ ...(allowed[String(value.intent)] ?? []),
102
+ ].includes(k)))
103
+ return fail();
104
+ const about = typeof value.about === "string"
105
+ ? value.about
106
+ : surface === "files"
107
+ ? "content"
108
+ : "the state";
109
+ const group = [];
110
+ const add = (q) => {
111
+ const id = `q${++questionCount}`;
112
+ plan.questions[id] = q;
113
+ group.push(id);
114
+ return id;
115
+ };
116
+ const choice = (instructions, criteria, fixed = false) => ({
117
+ type: "choice",
118
+ instructions: surface === "ask" ? `${instructions}\n${gap}` : instructions,
119
+ criteria: fixed
120
+ ? criteria
121
+ : canonical({
122
+ ...criteria,
123
+ ...(surface === "ask" ? { cannot_tell: cannot } : {}),
124
+ }),
125
+ });
126
+ const reading = (id, label, kind = "plain", extra = {}) => {
127
+ const match = about.match(/^files\["(.+)"\]$/);
128
+ const missing = about === "output"
129
+ ? "add the missing output through command"
130
+ : match
131
+ ? `read ${match[1]} and add it to paths`
132
+ : ["state", "the state"].includes(about)
133
+ ? "complete the state note"
134
+ : `add decisive evidence for ${about} to paths`;
135
+ plan.readings.push({
136
+ id,
137
+ askIndex: index,
138
+ label,
139
+ kind,
140
+ uncalibrated: kind !== "verify",
141
+ missing,
142
+ ...extra,
143
+ });
144
+ plan.groups.push(group.splice(0));
145
+ };
146
+ if (value.intent === "verify") {
147
+ if (!record(value.claims) ||
148
+ Object.values(value.claims).some((s) => s.trim().endsWith("?")))
149
+ return fail();
150
+ for (const [claim, s] of Object.entries(value.claims)) {
151
+ lint(claim, s, true);
152
+ const label = `${claim} ${JSON.stringify(s)}`;
153
+ if (surface === "files") {
154
+ reading(add(bool(`Is the following statement true of ${about}: ${s}`)), label, "verify");
155
+ continue;
156
+ }
157
+ const q = choice(`For ${about}, which issue describes this exact statement: ${s}`, {
158
+ not_addressed: "The state does not address this statement.",
159
+ holds: "The state shows this statement holds.",
160
+ contradicted: "The state shows this statement is contradicted.",
161
+ cannot_tell: cannot,
162
+ }, true);
163
+ const id = add(q);
164
+ const twin = add(reverse(q));
165
+ const check = add(bool(`Is the following statement true of ${about}: ${s}`));
166
+ reading(id, label, "verify", { twin, controls: { exact: check } });
167
+ }
168
+ }
169
+ else if (value.intent === "decide" || value.intent === "classify") {
170
+ const criteria = value.intent === "decide" ? value.hypotheses : value.categories;
171
+ if (record(criteria)) {
172
+ const reserved = Object.keys(criteria).find((key) => ["none", "other", "cannot_tell", "not_addressed"].includes(key));
173
+ if (reserved)
174
+ return {
175
+ ok: false,
176
+ error: `${value.intent === "classify" ? "categories" : "hypotheses"}.${reserved} is reserved; omit it${reserved === "other" ? ", the compiler adds other automatically" : ", use a non-reserved option name"}. Example: {"intent":"classify","categories":{"validation":"Validates arguments"},"pick":"one"}`,
177
+ };
178
+ }
179
+ if (!record(criteria) ||
180
+ (value.pick !== undefined &&
181
+ !["one", "many"].includes(String(value.pick))))
182
+ return fail();
183
+ const sorted = canonical(criteria);
184
+ for (const [k, s] of Object.entries(sorted))
185
+ lint(k, s);
186
+ if (value.intent === "classify" && value.pick === "many") {
187
+ for (const [k, s] of Object.entries(sorted))
188
+ reading(add(bool(`For ${about}: ${s}`)), `${k} ${JSON.stringify(s)}`);
189
+ }
190
+ else {
191
+ const id = add(choice(`Which option holds for ${about}${text(value.by) ? ` by ${value.by}` : ""}?`, { ...sorted, other }));
192
+ const controls = {};
193
+ if (value.intent === "decide")
194
+ for (const [k, s] of Object.entries(sorted))
195
+ controls[k] = add(bool(`Is the following statement true of ${about}: ${s}`));
196
+ reading(id, `${value.intent}${index + 1}`, value.intent === "decide" ? "decide" : "plain", { controls, options: sorted });
197
+ }
198
+ }
199
+ else if (value.intent === "rate") {
200
+ if (!text(value.dimension) ||
201
+ !Array.isArray(value.levels) ||
202
+ value.levels.length < SCORE_MIN_LEVELS ||
203
+ value.levels.length > SCORE_MAX_LEVELS ||
204
+ !value.levels.every((s) => text(s) &&
205
+ !/^\s*(\d+(\.\d+)?|low|medium|moderate|high)\s*$/i.test(s)))
206
+ return fail();
207
+ for (const [i, s] of value.levels.entries())
208
+ lint(`rate${index + 1}.${i + 1}`, s);
209
+ reading(add({
210
+ type: "score",
211
+ instructions: `Rate ${value.dimension} of ${about}`,
212
+ criteria: value.levels,
213
+ }), value.dimension, "plain", { levels: value.levels });
214
+ }
215
+ else if (value.intent === "locate") {
216
+ if (surface !== "ask" ||
217
+ !text(value.target) ||
218
+ !Array.isArray(value.among) ||
219
+ !value.among.length ||
220
+ !value.among.every(text) ||
221
+ new Set(value.among).size !== value.among.length ||
222
+ (value.count !== undefined &&
223
+ !["one", "many"].includes(String(value.count))) ||
224
+ (value.attribution !== undefined &&
225
+ typeof value.attribution !== "boolean"))
226
+ return fail();
227
+ if (value.count === "many")
228
+ for (const candidate of value.among)
229
+ reading(add(bool(`Does ${candidate} in ${about} ${value.target}?`)), `${candidate} ${JSON.stringify(value.target)}`);
230
+ else
231
+ reading(add(choice(`Which candidate in ${about} ${value.target}?`, {
232
+ none: "No candidate matches.",
233
+ ...Object.fromEntries(value.among.map((k) => [
234
+ k,
235
+ `Candidate ${k} matches the target.`,
236
+ ])),
237
+ })), value.target, "plain", { attributable: value.attribution === true, among: value.among });
238
+ if (value.attribution && value.count === "many")
239
+ plan.warnings.push({
240
+ fact: "attribution applies only to locate count: one",
241
+ next: "use count: one for an ablation, or inspect each matrix match",
242
+ });
243
+ }
244
+ else if (value.intent === "free") {
245
+ const q = value.question;
246
+ if (!isRecord(q) ||
247
+ !text(q.instructions) ||
248
+ Object.keys(q).some((k) => !["type", "instructions", "criteria"].includes(k)))
249
+ return fail();
250
+ lint(`free${index + 1}`, q.instructions, true);
251
+ if (q.type === "bool") {
252
+ if (q.criteria !== undefined &&
253
+ (!isRecord(q.criteria) ||
254
+ Object.entries(q.criteria).some(([k, v]) => !["true", "false"].includes(k) || typeof v !== "string")))
255
+ return fail();
256
+ reading(add(q), `free${index + 1} ${JSON.stringify(q.instructions)}`);
257
+ }
258
+ else if (q.type === "choice") {
259
+ if (!record(q.criteria))
260
+ return fail();
261
+ const criteria = { ...q.criteria };
262
+ if (!("other" in criteria)) {
263
+ criteria.other = other;
264
+ plan.warnings.push({
265
+ fact: `free${index + 1}: other added`,
266
+ next: "cover all possible outcomes",
267
+ });
268
+ }
269
+ reading(add(choice(q.instructions, criteria)), `free${index + 1} ${JSON.stringify(q.instructions)}`);
270
+ }
271
+ else if (q.type === "score") {
272
+ if (!Array.isArray(q.criteria) ||
273
+ q.criteria.length < SCORE_MIN_LEVELS ||
274
+ q.criteria.length > SCORE_MAX_LEVELS ||
275
+ !q.criteria.every(text))
276
+ return fail();
277
+ reading(add(q), `free${index + 1} ${JSON.stringify(q.instructions)}`);
278
+ }
279
+ else
280
+ return fail();
281
+ }
282
+ else
283
+ return fail();
284
+ plan.asks.push(value);
285
+ }
286
+ if (Object.values(plan.questions).some((q) => q.type === "choice" &&
287
+ Object.keys(q.criteria).length > CHOICE_MAX_OPTIONS))
288
+ return fail();
289
+ return { ok: true, ...plan };
290
+ }
291
+ function probabilities(a) {
292
+ return a?.type === "choice" ? a.probabilities : undefined;
293
+ }
294
+ function merged(p, verify) {
295
+ return verify
296
+ ? {
297
+ holds: p.holds ?? 0,
298
+ contradicted: p.contradicted ?? 0,
299
+ not_addressed: (p.not_addressed ?? 0) + (p.cannot_tell ?? 0),
300
+ }
301
+ : p;
302
+ }
303
+ function top(p) {
304
+ return Object.keys(p).sort((a, b) => (p[b] ?? 0) - (p[a] ?? 0))[0] ?? "other";
305
+ }
306
+ export function reverseQuestions(plan, answers) {
307
+ const questions = {};
308
+ for (const r of plan.readings) {
309
+ const q = plan.questions[r.id];
310
+ const p = probabilities(answers[r.id]);
311
+ if (!r.twin &&
312
+ q?.type === "choice" &&
313
+ p &&
314
+ Math.max(...Object.values(p)) < ORDER_REVERSE_BELOW)
315
+ questions[r.id] = reverse(q);
316
+ }
317
+ return questions;
318
+ }
319
+ export function readAsks(plan, answers, reversed = {}) {
320
+ const result = [];
321
+ for (const r of plan.readings) {
322
+ const answer = answers[r.id];
323
+ const members = [
324
+ [r.id, r.kind === "verify" && r.twin ? "issues" : "judgment"],
325
+ ...(r.twin ? [[r.twin, "twin"]] : []),
326
+ ...Object.entries(r.controls ?? {}).map(([name, id]) => [id, `${name} bool`]),
327
+ ];
328
+ const missing = members.filter(([id]) => !answers[id] || answers[id]?.type === "unjudged");
329
+ if (missing.length) {
330
+ const reason = missing
331
+ .map(([id, name]) => {
332
+ const a = answers[id];
333
+ return `${name} unjudged: ${a?.type === "unjudged" ? a.reason : "missing answer"}`;
334
+ })
335
+ .join("; ");
336
+ result.push({
337
+ label: r.label,
338
+ value: { head: "unjudged", p: 0 },
339
+ band: "unsure",
340
+ reason: `${reason}; ${r.missing}`,
341
+ uncalibrated: r.uncalibrated,
342
+ });
343
+ continue;
344
+ }
345
+ if (!answer || answer.type === "unjudged")
346
+ continue;
347
+ if (answer.type !== "choice") {
348
+ const level = answer.type === "score"
349
+ ? r.levels?.[Math.round(answer.score)]
350
+ : undefined;
351
+ result.push({
352
+ label: level ? `${r.label} ${JSON.stringify(level)}` : r.label,
353
+ answer,
354
+ uncalibrated: r.uncalibrated,
355
+ });
356
+ continue;
357
+ }
358
+ const original = answer.probabilities;
359
+ const second = probabilities(r.twin ? answers[r.twin] : reversed[r.id]);
360
+ const p = second
361
+ ? Object.fromEntries(Object.keys(original).map((k) => [
362
+ k,
363
+ ((original[k] ?? 0) + (second[k] ?? 0)) / 2,
364
+ ]))
365
+ : original;
366
+ const values = merged(p, r.kind === "verify");
367
+ let head = top(values);
368
+ let mass = values[head] ?? 0;
369
+ let band = (p.cannot_tell ?? 0) >= CANNOT_TELL_MIN
370
+ ? "abstain"
371
+ : mass >= BAND_CHOICE_VERDICT_MIN
372
+ ? "verdict"
373
+ : "unsure";
374
+ if (band === "abstain") {
375
+ head = "cannot_tell";
376
+ mass = p.cannot_tell ?? 0;
377
+ }
378
+ let reason;
379
+ const exact = r.controls?.exact ? answers[r.controls.exact] : undefined;
380
+ if (r.kind === "verify" &&
381
+ exact?.type === "bool" &&
382
+ ((head === "holds" && exact.p < 0.5) ||
383
+ (head === "contradicted" && exact.p >= 0.5) ||
384
+ (["not_addressed", "cannot_tell"].includes(head) &&
385
+ exact.p >= BAND_BOOL_YES_MIN))) {
386
+ band = "unsure";
387
+ reason = `scope/evidence disagreement: issues ${head} ${mass.toFixed(2)}, bool ${exact.p >= 0.5 ? "yes" : "no"} ${exact.p.toFixed(2)}; no verdict: ${r.missing}`;
388
+ }
389
+ if (r.kind === "decide") {
390
+ const entries = Object.entries(r.controls ?? {});
391
+ const conflict = entries.find(([k, id]) => {
392
+ const a = answers[id];
393
+ return (a?.type === "bool" &&
394
+ ((k === head && a.p < 0.5) ||
395
+ (["other", "cannot_tell"].includes(head) &&
396
+ a.p >= BAND_BOOL_YES_MIN)));
397
+ });
398
+ if (conflict) {
399
+ const a = answers[conflict[1]];
400
+ if (a?.type === "bool") {
401
+ band = "unsure";
402
+ reason = `scope/evidence disagreement: choice ${head} ${mass.toFixed(2)}, ${conflict[0]} ${JSON.stringify(r.options?.[conflict[0]])} bool ${a.p >= 0.5 ? "yes" : "no"} ${a.p.toFixed(2)}; no verdict: ${r.missing}`;
403
+ }
404
+ }
405
+ }
406
+ if (Object.values(r.controls ?? {}).some((id) => answers[id]?.type !== "bool")) {
407
+ band = "unsure";
408
+ reason =
409
+ "coherence control unjudged; no verdict: rerun with decisive evidence";
410
+ }
411
+ if (!r.twin && Object.hasOwn(reversed, r.id) && !second) {
412
+ band = "unsure";
413
+ reason = `reverse-order control unjudged; no verdict: ${r.missing}`;
414
+ }
415
+ const firstMerged = merged(original, r.kind === "verify");
416
+ const secondMerged = second
417
+ ? merged(second, r.kind === "verify")
418
+ : undefined;
419
+ const compared = top(values);
420
+ result.push({
421
+ label: r.options?.[head]
422
+ ? `${r.label} ${JSON.stringify(r.options[head])}`
423
+ : r.label,
424
+ answer: { ...answer, probabilities: p, choice: head },
425
+ value: { head, p: mass },
426
+ band,
427
+ reason,
428
+ missing: r.missing,
429
+ uncalibrated: r.uncalibrated,
430
+ orderDependent: !!secondMerged &&
431
+ Math.abs((firstMerged[compared] ?? 0) - (secondMerged[compared] ?? 0)) >
432
+ ORDER_DISAGREE_MIN,
433
+ });
434
+ }
435
+ return result;
436
+ }
@@ -0,0 +1,65 @@
1
+ import { QUESTION_TOKENS, REQUEST_BASE_TOKENS, REQUEST_MAX_TOKENS, STATE_TOKENS, } from "../constants.js";
2
+ /** Partition whole question groups; an oversized singleton is still sent. */
3
+ export function prepareBatches(state, questions, options) {
4
+ const witnesses = new Set(options.witnesses ?? []);
5
+ const seen = new Set();
6
+ for (const id of options.witnesses ?? []) {
7
+ if (!Object.hasOwn(questions, id) || seen.has(id))
8
+ return { ok: false, error: `Invalid witness question id: ${id}` };
9
+ seen.add(id);
10
+ }
11
+ const groups = [];
12
+ for (const group of options.groups ?? []) {
13
+ if (!group.length)
14
+ return { ok: false, error: "Question groups must not be empty." };
15
+ for (const id of group) {
16
+ if (!Object.hasOwn(questions, id) || seen.has(id))
17
+ return {
18
+ ok: false,
19
+ error: `Invalid or repeated grouped question id: ${id}`,
20
+ };
21
+ seen.add(id);
22
+ }
23
+ groups.push([...group]);
24
+ }
25
+ for (const id of Object.keys(questions))
26
+ if (!seen.has(id))
27
+ groups.push([id]);
28
+ if (!groups.length && witnesses.size)
29
+ return {
30
+ ok: false,
31
+ error: "Witnesses require at least one judgment group.",
32
+ };
33
+ const costs = Object.fromEntries(Object.entries(questions).map(([id, question]) => [
34
+ id,
35
+ JSON.stringify(question).length * QUESTION_TOKENS,
36
+ ]));
37
+ const base = REQUEST_BASE_TOKENS +
38
+ JSON.stringify(state).length * STATE_TOKENS +
39
+ [...witnesses].reduce((sum, id) => sum + (costs[id] ?? 0), 0);
40
+ const batches = [];
41
+ let batch = [];
42
+ let tokens = base;
43
+ for (const group of groups) {
44
+ const cost = group.reduce((sum, id) => sum + (costs[id] ?? 0), 0);
45
+ if (batch.length && tokens + cost > REQUEST_MAX_TOKENS) {
46
+ batches.push(batch);
47
+ batch = [];
48
+ tokens = base;
49
+ }
50
+ batch.push(group);
51
+ tokens += cost;
52
+ }
53
+ if (batch.length)
54
+ batches.push(batch);
55
+ return { ok: true, batches };
56
+ }
57
+ export function splitGroups(groups) {
58
+ if (groups.length < 2)
59
+ return {
60
+ ok: false,
61
+ error: "Jev max_tokens_exceeded: indivisible question group refused.",
62
+ };
63
+ const middle = Math.ceil(groups.length / 2);
64
+ return { ok: true, halves: [groups.slice(0, middle), groups.slice(middle)] };
65
+ }