@zanii/blackbox 0.22.0 → 0.23.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/README.md +1 -0
- package/dist/analysis/accepted.d.ts +26 -0
- package/dist/analysis/accepted.js +61 -0
- package/dist/analysis/detectors.js +12 -9
- package/dist/analysis/faults.js +71 -0
- package/dist/analysis/grounding.js +5 -9
- package/dist/analysis/guard.d.ts +72 -0
- package/dist/analysis/guard.js +240 -0
- package/dist/analysis/hallucination.js +2 -2
- package/dist/analysis/index.d.ts +21 -1
- package/dist/analysis/index.js +101 -19
- package/dist/analysis/injection.d.ts +2 -1
- package/dist/analysis/injection.js +16 -16
- package/dist/analysis/landing.js +60 -14
- package/dist/analysis/plan-checks.d.ts +55 -0
- package/dist/analysis/plan-checks.js +748 -0
- package/dist/analysis/taxonomy.js +1 -0
- package/dist/analysis/waste.js +2 -3
- package/dist/approvals/index.d.ts +12 -3
- package/dist/approvals/index.js +36 -10
- package/dist/authority/index.js +3 -3
- package/dist/aviation/index.d.ts +422 -0
- package/dist/aviation/index.js +1014 -0
- package/dist/aviation/qrh.d.ts +4 -0
- package/dist/aviation/qrh.js +171 -0
- package/dist/budgets/index.d.ts +63 -0
- package/dist/budgets/index.js +161 -0
- package/dist/cli.js +172 -24
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.js +26 -2
- package/dist/cost/batch.d.ts +26 -0
- package/dist/cost/batch.js +173 -0
- package/dist/cost/index.d.ts +21 -0
- package/dist/cost/index.js +115 -30
- package/dist/cv/index.js +1 -1
- package/dist/duty/index.js +2 -1
- package/dist/evals/index.d.ts +233 -0
- package/dist/evals/index.js +488 -0
- package/dist/federation/index.d.ts +9 -0
- package/dist/federation/index.js +8 -1
- package/dist/finance/index.js +3 -4
- package/dist/fleet/index.d.ts +25 -0
- package/dist/fleet/index.js +57 -17
- package/dist/fleet/insights.js +4 -1
- package/dist/health/index.js +2 -2
- package/dist/hooks/ai-sdk.d.ts +17 -19
- package/dist/hooks/ai-sdk.js +13 -7
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/mastra.d.ts +26 -0
- package/dist/hooks/mastra.js +63 -0
- package/dist/index.d.ts +22 -9
- package/dist/index.js +23 -9
- package/dist/mcp-requests/index.d.ts +26 -0
- package/dist/mcp-requests/index.js +154 -0
- package/dist/mcp-server/index.d.ts +12 -0
- package/dist/mcp-server/index.js +84 -2
- package/dist/mcp-wrap/index.js +8 -4
- package/dist/otlp/index.d.ts +155 -0
- package/dist/otlp/index.js +468 -0
- package/dist/otlp/protobuf.d.ts +7 -0
- package/dist/otlp/protobuf.js +314 -0
- package/dist/packs/index.d.ts +2 -0
- package/dist/packs/index.js +5 -1
- package/dist/pdf/arabic.d.ts +45 -0
- package/dist/pdf/arabic.js +373 -0
- package/dist/pdf/index.d.ts +77 -0
- package/dist/pdf/index.js +673 -0
- package/dist/policy/index.d.ts +0 -4
- package/dist/policy/index.js +13 -5
- package/dist/realtime/index.d.ts +33 -0
- package/dist/realtime/index.js +204 -0
- package/dist/reconcile/record.d.ts +12 -0
- package/dist/reconcile/record.js +114 -65
- package/dist/session/index.d.ts +43 -0
- package/dist/session/index.js +83 -0
- package/dist/undo/index.js +3 -3
- package/dist/verify/anchor.d.ts +5 -1
- package/dist/verify/anchor.js +13 -4
- package/dist/verify/envelope.d.ts +1 -1
- package/dist/verify/envelope.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.js +2 -2
- package/dist/warehouse/index.d.ts +62 -0
- package/dist/warehouse/index.js +142 -0
- package/package.json +6 -3
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// Flight-plan checks on arguments and output (spec/findings.md §5a): a portable regular expression
|
|
2
|
+
// (a Pike VM: linear time, the same answer in both SDKs), a JSON path, a JSON Schema subset, and the
|
|
3
|
+
// `tool_args` / `answer` predicates built on them. Mirrors analysis/plan_checks.py.
|
|
4
|
+
import { canonical, isObj } from "../reconcile/shared.js";
|
|
5
|
+
const MAX_PATTERN = 256;
|
|
6
|
+
const MAX_PROGRAM = 1000;
|
|
7
|
+
const MAX_REPEAT = 100;
|
|
8
|
+
/** Text longer than this (in code points) isn't matched (cannot_verify). */
|
|
9
|
+
export const MAX_MATCH_TEXT = 100_000;
|
|
10
|
+
const isDigit = (c) => c >= "0" && c <= "9";
|
|
11
|
+
const isWord = (c) => (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || isDigit(c) || c === "_";
|
|
12
|
+
const isSpace = (c) => " \t\n\r\f\v".includes(c) && c.length === 1;
|
|
13
|
+
const CLASSES = {
|
|
14
|
+
d: isDigit,
|
|
15
|
+
D: (c) => !isDigit(c),
|
|
16
|
+
w: isWord,
|
|
17
|
+
W: (c) => !isWord(c),
|
|
18
|
+
s: isSpace,
|
|
19
|
+
S: (c) => !isSpace(c),
|
|
20
|
+
};
|
|
21
|
+
const CONTROL = { n: "\n", r: "\r", t: "\t" };
|
|
22
|
+
const SYNTAX = "\\^$.|?*+()[]{}/-";
|
|
23
|
+
const cp = (c) => c.codePointAt(0);
|
|
24
|
+
class PatternError extends Error {
|
|
25
|
+
}
|
|
26
|
+
/** Parses and compiles a pattern, or says why it isn't one (spec/findings.md §5a.1). */
|
|
27
|
+
export function compilePattern(source) {
|
|
28
|
+
if (typeof source !== "string")
|
|
29
|
+
return "a pattern must be a string";
|
|
30
|
+
const s = Array.from(source);
|
|
31
|
+
if (s.length === 0 || s.length > MAX_PATTERN)
|
|
32
|
+
return "a pattern must be 1-256 characters";
|
|
33
|
+
let i = 0;
|
|
34
|
+
const fail = (why) => {
|
|
35
|
+
throw new PatternError(why);
|
|
36
|
+
};
|
|
37
|
+
const escaped = (inClass) => {
|
|
38
|
+
const c = s[i++];
|
|
39
|
+
if (c === undefined)
|
|
40
|
+
return fail("a pattern can't end with \\");
|
|
41
|
+
if (CLASSES[c])
|
|
42
|
+
return CLASSES[c];
|
|
43
|
+
if (CONTROL[c])
|
|
44
|
+
return CONTROL[c];
|
|
45
|
+
if (!inClass && (c === "b" || c === "B"))
|
|
46
|
+
return c;
|
|
47
|
+
if (SYNTAX.includes(c))
|
|
48
|
+
return c;
|
|
49
|
+
return fail("unsupported escape");
|
|
50
|
+
};
|
|
51
|
+
const klass = () => {
|
|
52
|
+
// after "["
|
|
53
|
+
let neg = false;
|
|
54
|
+
if (s[i] === "^") {
|
|
55
|
+
neg = true;
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
const tests = [];
|
|
59
|
+
let first = true;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const c = s[i];
|
|
62
|
+
if (c === undefined)
|
|
63
|
+
fail("unterminated character class");
|
|
64
|
+
if (c === "]") {
|
|
65
|
+
if (first)
|
|
66
|
+
fail("empty character class");
|
|
67
|
+
i++;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
first = false;
|
|
71
|
+
if (c === "[")
|
|
72
|
+
fail("unescaped [ in a character class");
|
|
73
|
+
let lo;
|
|
74
|
+
i++;
|
|
75
|
+
lo = c === "\\" ? escaped(true) : c;
|
|
76
|
+
if (typeof lo === "string" && s[i] === "-" && s[i + 1] !== undefined && s[i + 1] !== "]") {
|
|
77
|
+
i++;
|
|
78
|
+
const d = s[i++];
|
|
79
|
+
const hi = d === "\\" ? escaped(true) : d === "[" ? fail("unescaped [ in a character class") : d;
|
|
80
|
+
if (typeof hi !== "string" || cp(hi) < cp(lo))
|
|
81
|
+
fail("bad character range");
|
|
82
|
+
const a = cp(lo);
|
|
83
|
+
const b = cp(hi);
|
|
84
|
+
tests.push((x) => cp(x) >= a && cp(x) <= b);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof lo === "string") {
|
|
88
|
+
const ch = lo;
|
|
89
|
+
lo = (x) => x === ch;
|
|
90
|
+
}
|
|
91
|
+
tests.push(lo);
|
|
92
|
+
}
|
|
93
|
+
return neg ? (x) => !tests.some((t) => t(x)) : (x) => tests.some((t) => t(x));
|
|
94
|
+
};
|
|
95
|
+
const count = () => {
|
|
96
|
+
let n = "";
|
|
97
|
+
while (s[i] !== undefined && isDigit(s[i]))
|
|
98
|
+
n += s[i++];
|
|
99
|
+
if (n.length === 0 || n.length > 3)
|
|
100
|
+
return null;
|
|
101
|
+
return Number(n);
|
|
102
|
+
};
|
|
103
|
+
const alt = () => {
|
|
104
|
+
const items = [cat()];
|
|
105
|
+
while (s[i] === "|") {
|
|
106
|
+
i++;
|
|
107
|
+
items.push(cat());
|
|
108
|
+
}
|
|
109
|
+
return items.length === 1 ? items[0] : { t: "alt", items };
|
|
110
|
+
};
|
|
111
|
+
const cat = () => {
|
|
112
|
+
const items = [];
|
|
113
|
+
for (;;) {
|
|
114
|
+
const c = s[i];
|
|
115
|
+
if (c === undefined || c === "|" || c === ")")
|
|
116
|
+
break;
|
|
117
|
+
let node;
|
|
118
|
+
i++;
|
|
119
|
+
if (c === "(") {
|
|
120
|
+
if (s[i] === "?") {
|
|
121
|
+
if (s[i + 1] !== ":")
|
|
122
|
+
fail("unsupported group");
|
|
123
|
+
i += 2;
|
|
124
|
+
}
|
|
125
|
+
node = alt();
|
|
126
|
+
if (s[i] !== ")")
|
|
127
|
+
fail("unbalanced parentheses");
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
else if (c === "[")
|
|
131
|
+
node = { t: "char", test: klass() };
|
|
132
|
+
else if (c === ".")
|
|
133
|
+
node = { t: "char", test: (x) => x !== "\n" };
|
|
134
|
+
else if (c === "^")
|
|
135
|
+
node = { t: "assert", kind: "bol" };
|
|
136
|
+
else if (c === "$")
|
|
137
|
+
node = { t: "assert", kind: "eol" };
|
|
138
|
+
else if (c === "\\") {
|
|
139
|
+
const e = escaped(false);
|
|
140
|
+
if (e === "b" || e === "B")
|
|
141
|
+
node = { t: "assert", kind: e === "b" ? "word" : "nonword" };
|
|
142
|
+
else if (typeof e === "string")
|
|
143
|
+
node = { t: "char", test: (x) => x === e };
|
|
144
|
+
else
|
|
145
|
+
node = { t: "char", test: e };
|
|
146
|
+
}
|
|
147
|
+
else if ("*+?{".includes(c))
|
|
148
|
+
return fail("nothing to repeat");
|
|
149
|
+
else if (c === "]" || c === "}")
|
|
150
|
+
return fail("unescaped ] or }");
|
|
151
|
+
else
|
|
152
|
+
node = { t: "char", test: (x) => x === c };
|
|
153
|
+
// a quantifier
|
|
154
|
+
const q = s[i];
|
|
155
|
+
if (q === "*" || q === "+" || q === "?" || q === "{") {
|
|
156
|
+
if (node.t === "assert")
|
|
157
|
+
fail("nothing to repeat");
|
|
158
|
+
i++;
|
|
159
|
+
let min = 0;
|
|
160
|
+
let max = null;
|
|
161
|
+
if (q === "+")
|
|
162
|
+
min = 1;
|
|
163
|
+
else if (q === "?")
|
|
164
|
+
max = 1;
|
|
165
|
+
else if (q === "{") {
|
|
166
|
+
const a = count();
|
|
167
|
+
if (a === null)
|
|
168
|
+
fail("bad repetition");
|
|
169
|
+
min = a;
|
|
170
|
+
max = min;
|
|
171
|
+
if (s[i] === ",") {
|
|
172
|
+
i++;
|
|
173
|
+
max = s[i] === "}" ? null : count();
|
|
174
|
+
if (max === null && s[i] !== "}")
|
|
175
|
+
fail("bad repetition");
|
|
176
|
+
}
|
|
177
|
+
if (s[i] !== "}")
|
|
178
|
+
fail("bad repetition");
|
|
179
|
+
i++;
|
|
180
|
+
if (min > MAX_REPEAT || (max !== null && (max > MAX_REPEAT || max < min)))
|
|
181
|
+
fail("bad repetition");
|
|
182
|
+
}
|
|
183
|
+
if (s[i] === "?")
|
|
184
|
+
i++; // lazy: the same answer for a yes/no match
|
|
185
|
+
const again = s[i];
|
|
186
|
+
if (again !== undefined && "*+?{".includes(again))
|
|
187
|
+
fail("nothing to repeat");
|
|
188
|
+
node = { t: "rep", node, min, max };
|
|
189
|
+
}
|
|
190
|
+
items.push(node);
|
|
191
|
+
}
|
|
192
|
+
return items.length === 1 ? items[0] : { t: "cat", items };
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
const tree = alt();
|
|
196
|
+
if (i < s.length)
|
|
197
|
+
fail("unbalanced parentheses");
|
|
198
|
+
const prog = [];
|
|
199
|
+
const emit = (n) => {
|
|
200
|
+
if (prog.length > MAX_PROGRAM)
|
|
201
|
+
fail("pattern too large");
|
|
202
|
+
if (n.t === "char")
|
|
203
|
+
prog.push({ op: "char", test: n.test });
|
|
204
|
+
else if (n.t === "assert")
|
|
205
|
+
prog.push({ op: "assert", kind: n.kind });
|
|
206
|
+
else if (n.t === "cat")
|
|
207
|
+
for (const x of n.items)
|
|
208
|
+
emit(x);
|
|
209
|
+
else if (n.t === "alt") {
|
|
210
|
+
const jumps = [];
|
|
211
|
+
n.items.forEach((x, k) => {
|
|
212
|
+
if (k < n.items.length - 1) {
|
|
213
|
+
const split = { op: "split", x: prog.length + 1, y: 0 };
|
|
214
|
+
prog.push(split);
|
|
215
|
+
emit(x);
|
|
216
|
+
const j = { op: "jmp", x: 0 };
|
|
217
|
+
prog.push(j);
|
|
218
|
+
jumps.push(j);
|
|
219
|
+
split.y = prog.length;
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
emit(x);
|
|
223
|
+
});
|
|
224
|
+
for (const j of jumps)
|
|
225
|
+
j.x = prog.length;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
for (let k = 0; k < n.min; k++)
|
|
229
|
+
emit(n.node);
|
|
230
|
+
if (n.max === null) {
|
|
231
|
+
const split = { op: "split", x: prog.length + 1, y: 0 };
|
|
232
|
+
prog.push(split);
|
|
233
|
+
emit(n.node);
|
|
234
|
+
prog.push({ op: "jmp", x: prog.indexOf(split) });
|
|
235
|
+
split.y = prog.length;
|
|
236
|
+
}
|
|
237
|
+
else
|
|
238
|
+
for (let k = n.min; k < n.max; k++) {
|
|
239
|
+
const split = { op: "split", x: prog.length + 1, y: 0 };
|
|
240
|
+
prog.push(split);
|
|
241
|
+
emit(n.node);
|
|
242
|
+
split.y = prog.length;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
emit(tree);
|
|
247
|
+
prog.push({ op: "match" });
|
|
248
|
+
if (prog.length > MAX_PROGRAM)
|
|
249
|
+
fail("pattern too large");
|
|
250
|
+
return { prog };
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
if (e instanceof PatternError)
|
|
254
|
+
return e.message;
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Whether the pattern matches anywhere in the text (a search, not a full match). */
|
|
259
|
+
export function patternMatches(p, text) {
|
|
260
|
+
const chars = Array.from(text);
|
|
261
|
+
const n = chars.length;
|
|
262
|
+
const { prog } = p;
|
|
263
|
+
const mark = new Int32Array(prog.length).fill(-1);
|
|
264
|
+
const add = (list, pc, pos) => {
|
|
265
|
+
const stack = [pc];
|
|
266
|
+
while (stack.length) {
|
|
267
|
+
const at = stack.pop();
|
|
268
|
+
if (mark[at] === pos)
|
|
269
|
+
continue;
|
|
270
|
+
mark[at] = pos;
|
|
271
|
+
const inst = prog[at];
|
|
272
|
+
if (inst.op === "match")
|
|
273
|
+
return true;
|
|
274
|
+
if (inst.op === "jmp")
|
|
275
|
+
stack.push(inst.x);
|
|
276
|
+
else if (inst.op === "split")
|
|
277
|
+
stack.push(inst.y, inst.x);
|
|
278
|
+
else if (inst.op === "char")
|
|
279
|
+
list.push(at);
|
|
280
|
+
else {
|
|
281
|
+
const before = pos > 0 && isWord(chars[pos - 1]);
|
|
282
|
+
const after = pos < n && isWord(chars[pos]);
|
|
283
|
+
const ok = inst.kind === "bol"
|
|
284
|
+
? pos === 0
|
|
285
|
+
: inst.kind === "eol"
|
|
286
|
+
? pos === n
|
|
287
|
+
: inst.kind === "word"
|
|
288
|
+
? before !== after
|
|
289
|
+
: before === after;
|
|
290
|
+
if (ok)
|
|
291
|
+
stack.push(at + 1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
};
|
|
296
|
+
let list = [];
|
|
297
|
+
for (let pos = 0;; pos++) {
|
|
298
|
+
if (add(list, 0, pos))
|
|
299
|
+
return true;
|
|
300
|
+
if (pos === n)
|
|
301
|
+
return false;
|
|
302
|
+
const c = chars[pos];
|
|
303
|
+
const next = [];
|
|
304
|
+
for (const at of list) {
|
|
305
|
+
const inst = prog[at];
|
|
306
|
+
if (inst.test(c) && add(next, at + 1, pos + 1))
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
list = next;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// ---------------------------------------------------------------- JSON values
|
|
313
|
+
const PATH = /^\$(?:\.[A-Za-z_][A-Za-z0-9_]*|\[[0-9]{1,4}\])*$/;
|
|
314
|
+
const ABSENT = Symbol("absent");
|
|
315
|
+
/** The value at a JSON path (`$`, `.name`, `[index]`), or ABSENT. */
|
|
316
|
+
function at(value, path) {
|
|
317
|
+
let v = value;
|
|
318
|
+
for (const m of path.slice(1).matchAll(/\.([A-Za-z_][A-Za-z0-9_]*)|\[([0-9]+)\]/g)) {
|
|
319
|
+
if (m[1] !== undefined) {
|
|
320
|
+
if (!isObj(v) || !Object.hasOwn(v, m[1]))
|
|
321
|
+
return ABSENT;
|
|
322
|
+
v = v[m[1]];
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const k = Number(m[2]);
|
|
326
|
+
if (!Array.isArray(v) || k >= v.length)
|
|
327
|
+
return ABSENT;
|
|
328
|
+
v = v[k];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return v;
|
|
332
|
+
}
|
|
333
|
+
const isInt = (v) => typeof v === "number" && Number.isSafeInteger(v);
|
|
334
|
+
const isScalar = (v) => v === null ||
|
|
335
|
+
typeof v === "boolean" ||
|
|
336
|
+
isInt(v) ||
|
|
337
|
+
(typeof v === "string" && Array.from(v).length <= 1000);
|
|
338
|
+
/** Deep JSON equality (a number equals a number of the same value; true is never 1). */
|
|
339
|
+
function jsonEq(a, b) {
|
|
340
|
+
if (typeof a === "number" || typeof b === "number")
|
|
341
|
+
return a === b;
|
|
342
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
343
|
+
return (Array.isArray(a) &&
|
|
344
|
+
Array.isArray(b) &&
|
|
345
|
+
a.length === b.length &&
|
|
346
|
+
a.every((x, k) => jsonEq(x, b[k])));
|
|
347
|
+
if (isObj(a) || isObj(b)) {
|
|
348
|
+
if (!isObj(a) || !isObj(b))
|
|
349
|
+
return false;
|
|
350
|
+
const ka = Object.keys(a).sort();
|
|
351
|
+
const kb = Object.keys(b).sort();
|
|
352
|
+
return canonical(ka) === canonical(kb) && ka.every((k) => jsonEq(a[k], b[k]));
|
|
353
|
+
}
|
|
354
|
+
return a === b;
|
|
355
|
+
}
|
|
356
|
+
/** Every number in it is a safe integer (no floats in a hashed object). */
|
|
357
|
+
function integersOnly(v) {
|
|
358
|
+
if (typeof v === "number")
|
|
359
|
+
return Number.isSafeInteger(v);
|
|
360
|
+
if (Array.isArray(v))
|
|
361
|
+
return v.every(integersOnly);
|
|
362
|
+
if (isObj(v))
|
|
363
|
+
return Object.values(v).every(integersOnly);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
function depthOf(v) {
|
|
367
|
+
if (Array.isArray(v))
|
|
368
|
+
return 1 + Math.max(0, ...v.map(depthOf));
|
|
369
|
+
if (isObj(v))
|
|
370
|
+
return 1 + Math.max(0, ...Object.values(v).map(depthOf));
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
/** A value in integer milli-units (spec/findings.md §5a.2), or null. */
|
|
374
|
+
export function milliOf(v) {
|
|
375
|
+
let text;
|
|
376
|
+
if (typeof v === "number") {
|
|
377
|
+
if (!Number.isFinite(v))
|
|
378
|
+
return null;
|
|
379
|
+
if (Number.isInteger(v)) {
|
|
380
|
+
const m = v * 1000;
|
|
381
|
+
return Number.isSafeInteger(v) && Number.isSafeInteger(m) ? m : null;
|
|
382
|
+
}
|
|
383
|
+
if (Math.abs(v) >= 1e15)
|
|
384
|
+
return null;
|
|
385
|
+
text = String(v);
|
|
386
|
+
}
|
|
387
|
+
else if (typeof v === "string")
|
|
388
|
+
text = v;
|
|
389
|
+
else
|
|
390
|
+
return null;
|
|
391
|
+
const m = /^(-?)([0-9]{1,15})(?:\.([0-9]{1,3}))?$/.exec(text);
|
|
392
|
+
if (!m)
|
|
393
|
+
return null;
|
|
394
|
+
const n = Number(m[2]) * 1000 + Number((m[3] ?? "").padEnd(3, "0"));
|
|
395
|
+
return m[1] ? -n : n;
|
|
396
|
+
}
|
|
397
|
+
// ---------------------------------------------------------------- the schema subset (§5a.3)
|
|
398
|
+
const TYPES = ["object", "array", "string", "integer", "number", "boolean", "null"];
|
|
399
|
+
const SCHEMA_KEYS = [
|
|
400
|
+
"type",
|
|
401
|
+
"enum",
|
|
402
|
+
"const",
|
|
403
|
+
"required",
|
|
404
|
+
"properties",
|
|
405
|
+
"additionalProperties",
|
|
406
|
+
"items",
|
|
407
|
+
"minItems",
|
|
408
|
+
"maxItems",
|
|
409
|
+
"minLength",
|
|
410
|
+
"maxLength",
|
|
411
|
+
"minimum",
|
|
412
|
+
"maximum",
|
|
413
|
+
"pattern",
|
|
414
|
+
"title",
|
|
415
|
+
"description",
|
|
416
|
+
];
|
|
417
|
+
/** Why a schema is outside the subset, or undefined. */
|
|
418
|
+
export function checkSchema(schema) {
|
|
419
|
+
if (!integersOnly(schema))
|
|
420
|
+
return "numbers must be integers";
|
|
421
|
+
if (depthOf(schema) > 16)
|
|
422
|
+
return "nested more than 16 levels";
|
|
423
|
+
if (new TextEncoder().encode(canonical(schema)).length > 8192)
|
|
424
|
+
return "larger than 8192 bytes";
|
|
425
|
+
const walk = (x) => {
|
|
426
|
+
if (!isObj(x))
|
|
427
|
+
return "a schema must be an object";
|
|
428
|
+
for (const k of Object.keys(x).sort()) {
|
|
429
|
+
const v = x[k];
|
|
430
|
+
if (!SCHEMA_KEYS.includes(k))
|
|
431
|
+
return `unsupported keyword ${k}`;
|
|
432
|
+
const bad = `bad value for ${k}`;
|
|
433
|
+
if (k === "type") {
|
|
434
|
+
const list = Array.isArray(v) ? v : [v];
|
|
435
|
+
if (list.length === 0 ||
|
|
436
|
+
!list.every((t) => typeof t === "string" && TYPES.includes(t)) ||
|
|
437
|
+
new Set(list).size !== list.length)
|
|
438
|
+
return bad;
|
|
439
|
+
}
|
|
440
|
+
else if (k === "enum") {
|
|
441
|
+
if (!Array.isArray(v) || v.length === 0 || v.length > 100)
|
|
442
|
+
return bad;
|
|
443
|
+
}
|
|
444
|
+
else if (k === "required") {
|
|
445
|
+
if (!Array.isArray(v) || v.length > 100 || !v.every((r) => typeof r === "string"))
|
|
446
|
+
return bad;
|
|
447
|
+
}
|
|
448
|
+
else if (k === "properties") {
|
|
449
|
+
if (!isObj(v) || Object.keys(v).length > 100)
|
|
450
|
+
return bad;
|
|
451
|
+
for (const name of Object.keys(v).sort()) {
|
|
452
|
+
const e = walk(v[name]);
|
|
453
|
+
if (e)
|
|
454
|
+
return e;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (k === "additionalProperties") {
|
|
458
|
+
if (typeof v !== "boolean") {
|
|
459
|
+
const e = walk(v);
|
|
460
|
+
if (e)
|
|
461
|
+
return e;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else if (k === "items") {
|
|
465
|
+
const e = walk(v);
|
|
466
|
+
if (e)
|
|
467
|
+
return e;
|
|
468
|
+
}
|
|
469
|
+
else if (["minItems", "maxItems", "minLength", "maxLength"].includes(k)) {
|
|
470
|
+
if (!isInt(v) || v < 0)
|
|
471
|
+
return bad;
|
|
472
|
+
}
|
|
473
|
+
else if (k === "minimum" || k === "maximum") {
|
|
474
|
+
if (!isInt(v))
|
|
475
|
+
return bad;
|
|
476
|
+
}
|
|
477
|
+
else if (k === "pattern") {
|
|
478
|
+
const p = compilePattern(v);
|
|
479
|
+
if (typeof p === "string")
|
|
480
|
+
return `pattern: ${p}`;
|
|
481
|
+
}
|
|
482
|
+
else if (k === "title" || k === "description") {
|
|
483
|
+
if (typeof v !== "string")
|
|
484
|
+
return bad;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
};
|
|
489
|
+
return walk(schema);
|
|
490
|
+
}
|
|
491
|
+
const typeOf = (v) => v === null
|
|
492
|
+
? ["null"]
|
|
493
|
+
: typeof v === "boolean"
|
|
494
|
+
? ["boolean"]
|
|
495
|
+
: typeof v === "number"
|
|
496
|
+
? Number.isInteger(v)
|
|
497
|
+
? ["integer", "number"]
|
|
498
|
+
: ["number"]
|
|
499
|
+
: typeof v === "string"
|
|
500
|
+
? ["string"]
|
|
501
|
+
: Array.isArray(v)
|
|
502
|
+
? ["array"]
|
|
503
|
+
: ["object"];
|
|
504
|
+
/** The first place a value breaks a (checked) schema, or undefined (spec/findings.md §5a.3). */
|
|
505
|
+
export function schemaError(value, schema, path = "$") {
|
|
506
|
+
const s = schema;
|
|
507
|
+
const types = typeOf(value);
|
|
508
|
+
if (s.type !== undefined) {
|
|
509
|
+
const want = (Array.isArray(s.type) ? s.type : [s.type]);
|
|
510
|
+
if (!want.some((t) => types.includes(t)))
|
|
511
|
+
return `${path}: expected ${want.join(" or ")}`;
|
|
512
|
+
}
|
|
513
|
+
if ("const" in s && !jsonEq(value, s.const))
|
|
514
|
+
return `${path}: not the expected value`;
|
|
515
|
+
if (Array.isArray(s.enum) && !s.enum.some((x) => jsonEq(value, x)))
|
|
516
|
+
return `${path}: not one of the allowed values`;
|
|
517
|
+
if (typeof value === "string") {
|
|
518
|
+
const n = Array.from(value).length;
|
|
519
|
+
if (isInt(s.minLength) && n < s.minLength)
|
|
520
|
+
return `${path}: shorter than ${s.minLength} characters`;
|
|
521
|
+
if (isInt(s.maxLength) && n > s.maxLength)
|
|
522
|
+
return `${path}: longer than ${s.maxLength} characters`;
|
|
523
|
+
if (typeof s.pattern === "string") {
|
|
524
|
+
const p = compilePattern(s.pattern);
|
|
525
|
+
if (n > MAX_MATCH_TEXT || !patternMatches(p, value))
|
|
526
|
+
return `${path}: doesn't match the pattern`;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (typeof value === "number") {
|
|
530
|
+
if (isInt(s.minimum) && value < s.minimum)
|
|
531
|
+
return `${path}: less than ${s.minimum}`;
|
|
532
|
+
if (isInt(s.maximum) && value > s.maximum)
|
|
533
|
+
return `${path}: more than ${s.maximum}`;
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(value)) {
|
|
536
|
+
if (isInt(s.minItems) && value.length < s.minItems)
|
|
537
|
+
return `${path}: fewer than ${s.minItems} items`;
|
|
538
|
+
if (isInt(s.maxItems) && value.length > s.maxItems)
|
|
539
|
+
return `${path}: more than ${s.maxItems} items`;
|
|
540
|
+
if (s.items !== undefined)
|
|
541
|
+
for (let k = 0; k < value.length; k++) {
|
|
542
|
+
const e = schemaError(value[k], s.items, `${path}[${k}]`);
|
|
543
|
+
if (e)
|
|
544
|
+
return e;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (isObj(value)) {
|
|
548
|
+
if (Array.isArray(s.required))
|
|
549
|
+
for (const r of s.required)
|
|
550
|
+
if (!Object.hasOwn(value, r))
|
|
551
|
+
return `${path}: missing ${r}`;
|
|
552
|
+
const props = isObj(s.properties) ? s.properties : {};
|
|
553
|
+
for (const k of Object.keys(props).sort())
|
|
554
|
+
if (Object.hasOwn(value, k)) {
|
|
555
|
+
const e = schemaError(value[k], props[k], `${path}.${k}`);
|
|
556
|
+
if (e)
|
|
557
|
+
return e;
|
|
558
|
+
}
|
|
559
|
+
if (s.additionalProperties !== undefined && s.additionalProperties !== true)
|
|
560
|
+
for (const k of Object.keys(value).sort()) {
|
|
561
|
+
if (Object.hasOwn(props, k))
|
|
562
|
+
continue;
|
|
563
|
+
if (s.additionalProperties === false)
|
|
564
|
+
return `${path}: unexpected property ${k}`;
|
|
565
|
+
const e = schemaError(value[k], s.additionalProperties, `${path}.${k}`);
|
|
566
|
+
if (e)
|
|
567
|
+
return e;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
// ---------------------------------------------------------------- the predicates (§5a)
|
|
573
|
+
const OPS = ["equals", "in", "matches", "range", "exists"];
|
|
574
|
+
const ANSWER_OPS = ["contains", "not_contains", "matches", "schema"];
|
|
575
|
+
/** Why a `tool_args` or `answer` predicate is invalid, or undefined. */
|
|
576
|
+
export function checkPredicate(p) {
|
|
577
|
+
if (!isObj(p) || Object.keys(p).length !== 1)
|
|
578
|
+
return "a predicate is an object with one key";
|
|
579
|
+
const [key, arg] = Object.entries(p)[0];
|
|
580
|
+
if (!integersOnly(arg))
|
|
581
|
+
return "numbers in a predicate must be integers";
|
|
582
|
+
if (key === "tool_args") {
|
|
583
|
+
if (!isObj(arg))
|
|
584
|
+
return "tool_args must be an object";
|
|
585
|
+
const extra = Object.keys(arg).filter((k) => !["tool", "path", "for", ...OPS].includes(k));
|
|
586
|
+
if (extra.length)
|
|
587
|
+
return `tool_args has unknown fields: ${extra.sort().join(", ")}`;
|
|
588
|
+
if (typeof arg.tool !== "string" || arg.tool.length === 0 || arg.tool.length > 128)
|
|
589
|
+
return "tool_args.tool must be a tool name";
|
|
590
|
+
if (arg.path !== undefined &&
|
|
591
|
+
!(typeof arg.path === "string" && arg.path.length <= 256 && PATH.test(arg.path)))
|
|
592
|
+
return "tool_args.path must be a JSON path like $.a.b[0]";
|
|
593
|
+
if (arg.for !== undefined && arg.for !== "some" && arg.for !== "every")
|
|
594
|
+
return 'tool_args.for must be "some" or "every"';
|
|
595
|
+
const ops = OPS.filter((k) => k in arg);
|
|
596
|
+
if (ops.length !== 1)
|
|
597
|
+
return "tool_args needs exactly one of equals, in, matches, range, exists";
|
|
598
|
+
const v = arg[ops[0]];
|
|
599
|
+
if (ops[0] === "equals" && !isScalar(v))
|
|
600
|
+
return "tool_args.equals must be a string, an integer, true, false or null";
|
|
601
|
+
if (ops[0] === "in" &&
|
|
602
|
+
!(Array.isArray(v) && v.length > 0 && v.length <= 100 && v.every(isScalar)))
|
|
603
|
+
return "tool_args.in must be 1-100 strings, integers, booleans or nulls";
|
|
604
|
+
if (ops[0] === "matches") {
|
|
605
|
+
const c = compilePattern(v);
|
|
606
|
+
if (typeof c === "string")
|
|
607
|
+
return `tool_args.matches: ${c}`;
|
|
608
|
+
}
|
|
609
|
+
if (ops[0] === "range") {
|
|
610
|
+
const r = v;
|
|
611
|
+
if (!isObj(r) ||
|
|
612
|
+
Object.keys(r).some((k) => !["min", "max", "unit"].includes(k)) ||
|
|
613
|
+
(r.min === undefined && r.max === undefined) ||
|
|
614
|
+
(r.min !== undefined && !isInt(r.min)) ||
|
|
615
|
+
(r.max !== undefined && !isInt(r.max)) ||
|
|
616
|
+
(isInt(r.min) && isInt(r.max) && r.min > r.max) ||
|
|
617
|
+
(r.unit !== undefined && r.unit !== "milli"))
|
|
618
|
+
return 'tool_args.range must be {min?, max?, unit?: "milli"} with integer bounds, min <= max';
|
|
619
|
+
}
|
|
620
|
+
if (ops[0] === "exists" && typeof v !== "boolean")
|
|
621
|
+
return "tool_args.exists must be true or false";
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
if (key === "answer") {
|
|
625
|
+
const ops = isObj(arg) ? Object.keys(arg) : [];
|
|
626
|
+
if (!isObj(arg) || ops.length !== 1 || !ANSWER_OPS.includes(ops[0]))
|
|
627
|
+
return "answer must be an object with exactly one of contains, not_contains, matches, schema";
|
|
628
|
+
const op = ops[0];
|
|
629
|
+
const v = arg[op];
|
|
630
|
+
if (op === "contains" || op === "not_contains") {
|
|
631
|
+
if (typeof v !== "string" || v.length === 0 || Array.from(v).length > 1000)
|
|
632
|
+
return `answer.${op} must be a string of 1-1000 characters`;
|
|
633
|
+
}
|
|
634
|
+
else if (op === "matches") {
|
|
635
|
+
const c = compilePattern(v);
|
|
636
|
+
if (typeof c === "string")
|
|
637
|
+
return `answer.matches: ${c}`;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
const e = checkSchema(v);
|
|
641
|
+
if (e)
|
|
642
|
+
return `answer.schema: ${e}`;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
return "unsupported predicate";
|
|
647
|
+
}
|
|
648
|
+
const verdict = (result, gap) => (gap ? { result, gap } : { result });
|
|
649
|
+
/** Whether one value satisfies a (checked) tool_args operation. */
|
|
650
|
+
function satisfies(arg, value) {
|
|
651
|
+
if ("exists" in arg)
|
|
652
|
+
return (value !== ABSENT) === arg.exists;
|
|
653
|
+
if (value === ABSENT)
|
|
654
|
+
return false;
|
|
655
|
+
if ("equals" in arg)
|
|
656
|
+
return isScalarValue(value) && jsonEq(value, arg.equals);
|
|
657
|
+
if ("in" in arg)
|
|
658
|
+
return isScalarValue(value) && arg.in.some((x) => jsonEq(value, x));
|
|
659
|
+
if ("matches" in arg) {
|
|
660
|
+
if (typeof value !== "string" || Array.from(value).length > MAX_MATCH_TEXT)
|
|
661
|
+
return false;
|
|
662
|
+
return patternMatches(compilePattern(arg.matches), value);
|
|
663
|
+
}
|
|
664
|
+
const r = arg.range;
|
|
665
|
+
const n = r.unit === "milli"
|
|
666
|
+
? milliOf(value)
|
|
667
|
+
: typeof value === "number" && Number.isSafeInteger(value)
|
|
668
|
+
? value
|
|
669
|
+
: null;
|
|
670
|
+
return n !== null && (r.min === undefined || n >= r.min) && (r.max === undefined || n <= r.max);
|
|
671
|
+
}
|
|
672
|
+
const isScalarValue = (v) => v === null || typeof v === "boolean" || typeof v === "string" || typeof v === "number";
|
|
673
|
+
/** A `tool_args` predicate over the tool calls before `before` (spec/findings.md §5a.2). */
|
|
674
|
+
export function judgeArgs(arg, uses, before) {
|
|
675
|
+
const tool = arg.tool;
|
|
676
|
+
const path = typeof arg.path === "string" ? arg.path : "$";
|
|
677
|
+
const named = uses.filter((u) => u.seq < before && u.tool === tool);
|
|
678
|
+
const judged = named.map((u) => (u.hasArgs ? satisfies(arg, at(u.args, path)) : null));
|
|
679
|
+
if (arg.for === "every") {
|
|
680
|
+
const k = judged.indexOf(false);
|
|
681
|
+
if (k >= 0)
|
|
682
|
+
return verdict("fail", `${tool} was called with arguments outside the plan at ${path} (seq ${named[k].seq})`);
|
|
683
|
+
if (judged.includes(null))
|
|
684
|
+
return verdict("cannot_verify", `${tool}'s arguments aren't on the record`);
|
|
685
|
+
return verdict("pass");
|
|
686
|
+
}
|
|
687
|
+
if (!named.length)
|
|
688
|
+
return verdict("fail", `${tool} was never called`);
|
|
689
|
+
if (judged.includes(true))
|
|
690
|
+
return verdict("pass");
|
|
691
|
+
if (judged.includes(null))
|
|
692
|
+
return verdict("cannot_verify", `${tool}'s arguments aren't on the record`);
|
|
693
|
+
return verdict("fail", `no ${tool} call had arguments matching the plan at ${path}`);
|
|
694
|
+
}
|
|
695
|
+
const TRIM = " \t\n\r";
|
|
696
|
+
function trim(s) {
|
|
697
|
+
let a = 0;
|
|
698
|
+
let b = s.length;
|
|
699
|
+
while (a < b && TRIM.includes(s[a]))
|
|
700
|
+
a++;
|
|
701
|
+
while (b > a && TRIM.includes(s[b - 1]))
|
|
702
|
+
b--;
|
|
703
|
+
return s.slice(a, b);
|
|
704
|
+
}
|
|
705
|
+
/** The JSON an answer holds: the whole text, or one ``` fence around it; ABSENT when it isn't JSON. */
|
|
706
|
+
export function answerJson(text) {
|
|
707
|
+
let t = trim(text);
|
|
708
|
+
if (t.startsWith("```")) {
|
|
709
|
+
const nl = t.indexOf("\n");
|
|
710
|
+
t = nl < 0 ? "" : t.slice(nl + 1);
|
|
711
|
+
t = trim(t);
|
|
712
|
+
if (t.endsWith("```"))
|
|
713
|
+
t = trim(t.slice(0, -3));
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
return JSON.parse(t);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return ABSENT;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/** An `answer` predicate over the last final answer before `before` (spec/findings.md §5a.4). */
|
|
723
|
+
export function judgeAnswer(arg, answers, before) {
|
|
724
|
+
const last = [...answers].reverse().find((a) => a.seq < before);
|
|
725
|
+
if (!last)
|
|
726
|
+
return verdict("cannot_verify", "no final answer on the record before the claim");
|
|
727
|
+
const text = last.text;
|
|
728
|
+
if (typeof arg.contains === "string")
|
|
729
|
+
return text.includes(arg.contains)
|
|
730
|
+
? verdict("pass")
|
|
731
|
+
: verdict("fail", "the answer doesn't contain the expected text");
|
|
732
|
+
if (typeof arg.not_contains === "string")
|
|
733
|
+
return text.includes(arg.not_contains)
|
|
734
|
+
? verdict("fail", "the answer contains text the plan rules out")
|
|
735
|
+
: verdict("pass");
|
|
736
|
+
if (typeof arg.matches === "string") {
|
|
737
|
+
if (Array.from(text).length > MAX_MATCH_TEXT)
|
|
738
|
+
return verdict("cannot_verify", "the answer is too long to match");
|
|
739
|
+
return patternMatches(compilePattern(arg.matches), text)
|
|
740
|
+
? verdict("pass")
|
|
741
|
+
: verdict("fail", "the answer doesn't match the pattern");
|
|
742
|
+
}
|
|
743
|
+
const value = answerJson(text);
|
|
744
|
+
if (value === ABSENT)
|
|
745
|
+
return verdict("fail", "the answer isn't JSON");
|
|
746
|
+
const e = schemaError(value, arg.schema);
|
|
747
|
+
return e ? verdict("fail", `the answer doesn't match the schema (${e})`) : verdict("pass");
|
|
748
|
+
}
|