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,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
|
+
}
|