@zanii/blackbox 0.21.2 → 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/actions/index.d.ts +29 -0
- package/dist/actions/index.js +114 -0
- package/dist/admission/index.d.ts +30 -0
- package/dist/admission/index.js +106 -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 +199 -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/evidence/index.d.ts +9 -0
- package/dist/evidence/index.js +50 -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 +24 -9
- package/dist/index.js +25 -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 +14 -0
- package/dist/reconcile/record.js +161 -66
- 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/chain.d.ts +4 -0
- package/dist/verify/chain.js +4 -3
- 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,488 @@
|
|
|
1
|
+
// Datasets, scorers and eval runs (spec/evals.md §1-§3), prompt versions and experiments (§4-§5).
|
|
2
|
+
// Pure functions: the servers score and compare with them, the CLI builds datasets with them, and
|
|
3
|
+
// spec/vectors/evals.json pins them. Mirrors evals.py.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { schemaProblem } from "../analysis/hallucination.js";
|
|
6
|
+
import { successRange, systemPromptHash } from "../analysis/learning.js";
|
|
7
|
+
import { portableRegexError } from "../policy/index.js";
|
|
8
|
+
import { canonical } from "../reconcile/shared.js";
|
|
9
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
|
+
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
11
|
+
export const SCORERS = [
|
|
12
|
+
"exact",
|
|
13
|
+
"contains",
|
|
14
|
+
"regex",
|
|
15
|
+
"json_schema",
|
|
16
|
+
"numeric",
|
|
17
|
+
"tool_called",
|
|
18
|
+
"tool_not_called",
|
|
19
|
+
"findings_absent",
|
|
20
|
+
"judge",
|
|
21
|
+
];
|
|
22
|
+
export const CASE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
23
|
+
const TOOL = /^[A-Za-z0-9._:/-]{1,128}$/;
|
|
24
|
+
const CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
25
|
+
const DECIMAL = /^-?[0-9]+(?:\.[0-9]+)?$/;
|
|
26
|
+
const SESSION_ID = /^ses_[A-Za-z0-9]{16,64}$/;
|
|
27
|
+
const CASE_FIELDS = ["id", "input", "expected", "rubric", "tags", "scorer"];
|
|
28
|
+
const MAX_CASES = 1000;
|
|
29
|
+
const MAX_TEXT = 100_000;
|
|
30
|
+
/** The first number in `v` that isn't a safe integer, as a path, or null (no floats are hashed). */
|
|
31
|
+
function floatAt(v, path) {
|
|
32
|
+
if (typeof v === "number")
|
|
33
|
+
return Number.isSafeInteger(v) ? null : path;
|
|
34
|
+
if (Array.isArray(v)) {
|
|
35
|
+
for (const [i, x] of v.entries()) {
|
|
36
|
+
const p = floatAt(x, `${path}[${i}]`);
|
|
37
|
+
if (p)
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (isObj(v))
|
|
42
|
+
for (const [k, x] of Object.entries(v)) {
|
|
43
|
+
const p = floatAt(x, `${path}.${k}`);
|
|
44
|
+
if (p)
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** §2: a scorer, checked: the normalised scorer, or what's wrong with it. */
|
|
50
|
+
export function checkScorer(v, at = "scorer") {
|
|
51
|
+
if (!isObj(v) || !SCORERS.includes(v.kind))
|
|
52
|
+
return `${at}.kind must be one of ${SCORERS.join(", ")}`;
|
|
53
|
+
const kind = v.kind;
|
|
54
|
+
if (kind === "regex") {
|
|
55
|
+
const p = v.pattern;
|
|
56
|
+
if (typeof p !== "string" || p.length < 1 || p.length > 500)
|
|
57
|
+
return `${at}.pattern must be a regular expression of 1-500 characters`;
|
|
58
|
+
const unportable = portableRegexError(p);
|
|
59
|
+
if (unportable)
|
|
60
|
+
return `${at}.pattern uses syntax JavaScript and Python don't share (${unportable})`;
|
|
61
|
+
try {
|
|
62
|
+
new RegExp(p);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return `${at}.pattern is not a valid regular expression`;
|
|
66
|
+
}
|
|
67
|
+
return { kind, pattern: p };
|
|
68
|
+
}
|
|
69
|
+
if (kind === "json_schema") {
|
|
70
|
+
if (!isObj(v.schema))
|
|
71
|
+
return `${at}.schema must be a JSON Schema object`;
|
|
72
|
+
return { kind, schema: v.schema };
|
|
73
|
+
}
|
|
74
|
+
if (kind === "numeric") {
|
|
75
|
+
const t = v.tolerance_milli ?? 0;
|
|
76
|
+
if (!(typeof t === "number" && Number.isSafeInteger(t) && t >= 0))
|
|
77
|
+
return `${at}.tolerance_milli must be an integer of at least 0`;
|
|
78
|
+
return { kind, tolerance_milli: t };
|
|
79
|
+
}
|
|
80
|
+
if (kind === "tool_called" || kind === "tool_not_called") {
|
|
81
|
+
if (typeof v.tool !== "string" || !TOOL.test(v.tool))
|
|
82
|
+
return `${at}.tool must be a tool name`;
|
|
83
|
+
return { kind, tool: v.tool };
|
|
84
|
+
}
|
|
85
|
+
if (kind === "findings_absent") {
|
|
86
|
+
const c = v.codes;
|
|
87
|
+
if (!Array.isArray(c) ||
|
|
88
|
+
c.length > 50 ||
|
|
89
|
+
!c.every((x) => typeof x === "string" && CODE.test(x)))
|
|
90
|
+
return `${at}.codes must be at most 50 finding codes`;
|
|
91
|
+
return { kind, codes: [...new Set(c)].sort() };
|
|
92
|
+
}
|
|
93
|
+
return { kind };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* §1: a dataset, checked: `{name, cases}` with every case normalised (an id, tags, its scorer or
|
|
97
|
+
* the dataset's), or the first problem. Numbers must be integers: decimals are written as strings.
|
|
98
|
+
*/
|
|
99
|
+
export function checkDataset(v) {
|
|
100
|
+
if (!isObj(v))
|
|
101
|
+
return "the dataset must be a JSON object";
|
|
102
|
+
const name = v.name;
|
|
103
|
+
if (typeof name !== "string" || name.length < 1 || name.length > 128)
|
|
104
|
+
return "name must be 1-128 characters";
|
|
105
|
+
let fallback;
|
|
106
|
+
if (v.scorer !== undefined) {
|
|
107
|
+
const float = floatAt(v.scorer, "scorer");
|
|
108
|
+
if (float)
|
|
109
|
+
return `${float} is a number that isn't an integer (write decimals as strings)`;
|
|
110
|
+
const s = checkScorer(v.scorer);
|
|
111
|
+
if (typeof s === "string")
|
|
112
|
+
return s;
|
|
113
|
+
fallback = s;
|
|
114
|
+
}
|
|
115
|
+
const raw = v.cases;
|
|
116
|
+
if (!Array.isArray(raw) || raw.length < 1 || raw.length > MAX_CASES)
|
|
117
|
+
return `cases must be a list of 1-${MAX_CASES} cases`;
|
|
118
|
+
const cases = [];
|
|
119
|
+
const ids = new Set();
|
|
120
|
+
for (const [i, c] of raw.entries()) {
|
|
121
|
+
const at = `cases[${i}]`;
|
|
122
|
+
if (!isObj(c))
|
|
123
|
+
return `${at} must be an object`;
|
|
124
|
+
const extra = Object.keys(c).find((k) => !CASE_FIELDS.includes(k));
|
|
125
|
+
if (extra !== undefined)
|
|
126
|
+
return `${at}.${extra} is not a case field`;
|
|
127
|
+
const id = c.id === undefined ? `case-${i + 1}` : c.id;
|
|
128
|
+
if (typeof id !== "string" || !CASE_ID.test(id))
|
|
129
|
+
return `${at}.id must be 1-128 of A-Z a-z 0-9 . _ : -`;
|
|
130
|
+
if (ids.has(id))
|
|
131
|
+
return `${at}.id repeats "${id}"`;
|
|
132
|
+
ids.add(id);
|
|
133
|
+
if (c.input === undefined)
|
|
134
|
+
return `${at}.input is required`;
|
|
135
|
+
const float = floatAt(c, at);
|
|
136
|
+
if (float)
|
|
137
|
+
return `${float} is a number that isn't an integer (write decimals as strings)`;
|
|
138
|
+
const expected = c.expected;
|
|
139
|
+
if (expected !== undefined &&
|
|
140
|
+
!(typeof expected === "string" && expected.length <= MAX_TEXT) &&
|
|
141
|
+
typeof expected !== "number")
|
|
142
|
+
return `${at}.expected must be a string of at most ${MAX_TEXT} characters or an integer`;
|
|
143
|
+
const rubric = c.rubric;
|
|
144
|
+
if (rubric !== undefined &&
|
|
145
|
+
(typeof rubric !== "string" || rubric.length < 1 || rubric.length > 2000))
|
|
146
|
+
return `${at}.rubric must be 1-2000 characters`;
|
|
147
|
+
const tags = c.tags === undefined ? [] : c.tags;
|
|
148
|
+
if (!Array.isArray(tags) ||
|
|
149
|
+
tags.length > 20 ||
|
|
150
|
+
!tags.every((t) => typeof t === "string" && t.length >= 1 && t.length <= 64))
|
|
151
|
+
return `${at}.tags must be at most 20 tags of 1-64 characters`;
|
|
152
|
+
let scorer = fallback;
|
|
153
|
+
if (c.scorer !== undefined) {
|
|
154
|
+
const s = checkScorer(c.scorer, `${at}.scorer`);
|
|
155
|
+
if (typeof s === "string")
|
|
156
|
+
return s;
|
|
157
|
+
scorer = s;
|
|
158
|
+
}
|
|
159
|
+
if (!scorer)
|
|
160
|
+
return `${at}.scorer is required (or give the dataset a scorer)`;
|
|
161
|
+
if (["exact", "contains", "numeric"].includes(scorer.kind) && expected === undefined)
|
|
162
|
+
return `${at} needs expected for ${scorer.kind}`;
|
|
163
|
+
if (scorer.kind === "numeric" &&
|
|
164
|
+
!(typeof expected === "number" || DECIMAL.test(String(expected))))
|
|
165
|
+
return `${at}.expected must be a number for numeric (an integer, or a decimal as a string)`;
|
|
166
|
+
if (scorer.kind === "judge" && rubric === undefined)
|
|
167
|
+
return `${at} needs a rubric for judge`;
|
|
168
|
+
cases.push({
|
|
169
|
+
id,
|
|
170
|
+
input: c.input,
|
|
171
|
+
...(expected !== undefined ? { expected: expected } : {}),
|
|
172
|
+
...(rubric !== undefined ? { rubric: rubric } : {}),
|
|
173
|
+
tags: tags,
|
|
174
|
+
scorer,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return { name, cases };
|
|
178
|
+
}
|
|
179
|
+
/** §1: what names a dataset's content exactly: `sha256:` over the canonical JSON of {name, cases}. */
|
|
180
|
+
export function datasetHash(d) {
|
|
181
|
+
return `sha256:${sha256(canonical({ name: d.name, cases: d.cases }))}`;
|
|
182
|
+
}
|
|
183
|
+
/** §1: a JSON Lines file of cases: one object per non-empty line, or the first line that isn't. */
|
|
184
|
+
export function parseCasesJsonl(text) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const [i, raw] of text.split("\n").entries()) {
|
|
187
|
+
const line = raw.replace(/\r$/, "");
|
|
188
|
+
if (!line.trim())
|
|
189
|
+
continue;
|
|
190
|
+
try {
|
|
191
|
+
out.push(JSON.parse(line));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return `line ${i + 1}: not JSON`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
/** §1: one case per saved scenario (spec/replay.md §3): a fork of the incident, passing when none
|
|
200
|
+
* of its findings comes back. */
|
|
201
|
+
export function casesFromScenarios(scenarios) {
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const [i, s] of scenarios.entries()) {
|
|
204
|
+
if (!isObj(s) || s.v !== 1 || typeof s.source !== "string" || !SESSION_ID.test(s.source))
|
|
205
|
+
return `scenarios[${i}] is not a scenario (spec/replay.md §3)`;
|
|
206
|
+
const expect = isObj(s.expect) && isObj(s.expect.finding_codes) ? s.expect.finding_codes : {};
|
|
207
|
+
out.push({
|
|
208
|
+
id: s.source,
|
|
209
|
+
input: { replay_of: s.source },
|
|
210
|
+
tags: ["scenario"],
|
|
211
|
+
scorer: { kind: "findings_absent", codes: Object.keys(expect).sort() },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** §1: one case per recorded session: a fork of it, expected to answer what it answered then. */
|
|
217
|
+
export function casesFromSessions(items) {
|
|
218
|
+
return items.map((s) => ({
|
|
219
|
+
id: s.session_id,
|
|
220
|
+
input: { replay_of: s.session_id },
|
|
221
|
+
expected: s.answer,
|
|
222
|
+
tags: ["session"],
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
const trimWs = (s) => s.replace(/^[ \t\n\r]+|[ \t\n\r]+$/g, "");
|
|
226
|
+
const NUMBER = /-?[0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?/;
|
|
227
|
+
const FENCE = /^```[A-Za-z]*\n([\s\S]*?)\n?```$/;
|
|
228
|
+
/** A decimal as integer milli-units, the digits past the third decimal dropped. */
|
|
229
|
+
export function toMilli(s) {
|
|
230
|
+
const t = s.replace(/,/g, "");
|
|
231
|
+
const neg = t.startsWith("-");
|
|
232
|
+
const [whole, frac = ""] = (neg ? t.slice(1) : t).split(".");
|
|
233
|
+
const v = BigInt(whole) * 1000n + BigInt(`${frac}000`.slice(0, 3));
|
|
234
|
+
return neg ? -v : v;
|
|
235
|
+
}
|
|
236
|
+
/** §2: one case scored against what its run left. An outage of the judge is never a pass. */
|
|
237
|
+
export function scoreCase(c, o) {
|
|
238
|
+
const s = c.scorer;
|
|
239
|
+
const ok = (pass, yes, no) => ({
|
|
240
|
+
pass,
|
|
241
|
+
reason: pass ? yes : no,
|
|
242
|
+
});
|
|
243
|
+
if (s.kind === "tool_called" || s.kind === "tool_not_called") {
|
|
244
|
+
const called = (o.tools[s.tool] ?? 0) > 0;
|
|
245
|
+
const yes = `${s.tool} was called`;
|
|
246
|
+
const no = `${s.tool} was never called`;
|
|
247
|
+
return s.kind === "tool_called" ? ok(called, yes, no) : ok(!called, no, yes);
|
|
248
|
+
}
|
|
249
|
+
if (s.kind === "findings_absent") {
|
|
250
|
+
const back = (s.codes ?? []).filter((code) => (o.finding_codes[code] ?? 0) > 0);
|
|
251
|
+
return ok(back.length === 0, "none of the findings", `recurred: ${back.join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
if (o.answer === null)
|
|
254
|
+
return { pass: false, reason: "no answer" };
|
|
255
|
+
if (s.kind === "judge") {
|
|
256
|
+
if (o.judge === undefined)
|
|
257
|
+
return { pass: false, reason: "the judge wasn't asked", outage: true };
|
|
258
|
+
if (o.judge === null)
|
|
259
|
+
return { pass: false, reason: "the judge didn't answer", outage: true };
|
|
260
|
+
const failed = o.judge.steps.filter((x) => !x.pass).length;
|
|
261
|
+
if (!o.judge.steps.length)
|
|
262
|
+
return { pass: false, reason: "the judge gave no steps" };
|
|
263
|
+
return ok(failed === 0, "passed every step", `failed ${failed} of ${o.judge.steps.length} steps`);
|
|
264
|
+
}
|
|
265
|
+
const answer = trimWs(o.answer);
|
|
266
|
+
const expected = c.expected === undefined ? "" : String(c.expected);
|
|
267
|
+
if (s.kind === "exact")
|
|
268
|
+
return ok(answer === trimWs(expected), "matches", "differs from expected");
|
|
269
|
+
if (s.kind === "contains")
|
|
270
|
+
return ok(answer.includes(expected), "contains expected", "doesn't contain expected");
|
|
271
|
+
if (s.kind === "regex")
|
|
272
|
+
return ok(new RegExp(s.pattern).test(o.answer), "matches the pattern", "doesn't match the pattern");
|
|
273
|
+
if (s.kind === "json_schema") {
|
|
274
|
+
let value;
|
|
275
|
+
try {
|
|
276
|
+
value = JSON.parse(FENCE.exec(answer)?.[1] ?? answer);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return { pass: false, reason: "not JSON" };
|
|
280
|
+
}
|
|
281
|
+
const problem = schemaProblem(s.schema, value);
|
|
282
|
+
return ok(problem === null, "matches the schema", `schema: ${problem}`);
|
|
283
|
+
}
|
|
284
|
+
// numeric
|
|
285
|
+
const found = NUMBER.exec(answer)?.[0];
|
|
286
|
+
if (found === undefined)
|
|
287
|
+
return { pass: false, reason: "no number in the answer" };
|
|
288
|
+
const diff = toMilli(found) - toMilli(expected);
|
|
289
|
+
const off = diff < 0n ? -diff : diff;
|
|
290
|
+
return ok(off <= BigInt(s.tolerance_milli ?? 0), "within tolerance", `off by ${off} milli`);
|
|
291
|
+
}
|
|
292
|
+
const ppm = (n, d) => (d ? Math.floor((n * 1_000_000) / d) : 0);
|
|
293
|
+
/** §3: a run's totals: passes with the 95% range, outages, cost, and per tag. */
|
|
294
|
+
export function runSummary(results) {
|
|
295
|
+
const total = results.length;
|
|
296
|
+
const passed = results.filter((r) => r.pass).length;
|
|
297
|
+
const cost = results.reduce((n, r) => n + r.cost_micro_usd, 0);
|
|
298
|
+
const [low, high] = successRange(passed, total);
|
|
299
|
+
const tags = new Map();
|
|
300
|
+
for (const r of results)
|
|
301
|
+
for (const t of new Set(r.tags ?? [])) {
|
|
302
|
+
const x = tags.get(t) ?? { total: 0, passed: 0 };
|
|
303
|
+
x.total++;
|
|
304
|
+
if (r.pass)
|
|
305
|
+
x.passed++;
|
|
306
|
+
tags.set(t, x);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
total,
|
|
310
|
+
passed,
|
|
311
|
+
failed: total - passed,
|
|
312
|
+
outages: results.filter((r) => r.outage === true).length,
|
|
313
|
+
pass_ppm: ppm(passed, total),
|
|
314
|
+
pass_low_ppm: low,
|
|
315
|
+
pass_high_ppm: high,
|
|
316
|
+
cost_micro_usd: cost,
|
|
317
|
+
cost_per_case_micro_usd: total ? Math.floor(cost / total) : 0,
|
|
318
|
+
tags: Object.fromEntries([...tags.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** spec/fleet.md §9's rule: only a difference outside both 95% ranges counts. */
|
|
322
|
+
function versus(now, before) {
|
|
323
|
+
return now.low > before.high ? "better" : now.high < before.low ? "worse" : "unclear";
|
|
324
|
+
}
|
|
325
|
+
/** §3: run `b` against run `a`: better, worse or unclear, and the cases each fixed or broke. */
|
|
326
|
+
export function compareRuns(a, b) {
|
|
327
|
+
const before = new Map(a.results.map((r) => [r.case, r.pass]));
|
|
328
|
+
const fixed = [];
|
|
329
|
+
const broken = [];
|
|
330
|
+
for (const r of b.results) {
|
|
331
|
+
const was = before.get(r.case);
|
|
332
|
+
if (was === false && r.pass)
|
|
333
|
+
fixed.push(r.case);
|
|
334
|
+
if (was === true && !r.pass)
|
|
335
|
+
broken.push(r.case);
|
|
336
|
+
}
|
|
337
|
+
const range = (s) => ({ low: s.pass_low_ppm, high: s.pass_high_ppm });
|
|
338
|
+
return {
|
|
339
|
+
a: a.id,
|
|
340
|
+
b: b.id,
|
|
341
|
+
same_dataset: a.dataset.hash === b.dataset.hash,
|
|
342
|
+
verdict: versus(range(b.summary), range(a.summary)),
|
|
343
|
+
pass_ppm: { a: a.summary.pass_ppm, b: b.summary.pass_ppm },
|
|
344
|
+
pass_range_ppm: {
|
|
345
|
+
a: [a.summary.pass_low_ppm, a.summary.pass_high_ppm],
|
|
346
|
+
b: [b.summary.pass_low_ppm, b.summary.pass_high_ppm],
|
|
347
|
+
},
|
|
348
|
+
fixed: fixed.sort(),
|
|
349
|
+
broken: broken.sort(),
|
|
350
|
+
cost_per_case_delta_micro_usd: b.summary.cost_per_case_micro_usd - a.summary.cost_per_case_micro_usd,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------- prompts and experiments (§4-§5)
|
|
354
|
+
export const PROMPT_NAME = /^[A-Za-z0-9._-]{1,128}$/;
|
|
355
|
+
const ARM_NAME = /^[a-z0-9_-]{1,64}$/;
|
|
356
|
+
/** §4: a prompt text's version hash: the gateway's `system_sha256` of a request whose system
|
|
357
|
+
* prompt is this text (an Anthropic `system` string, or a Chat Completions system message). */
|
|
358
|
+
export function promptSha256(text) {
|
|
359
|
+
return systemPromptHash({ system: text });
|
|
360
|
+
}
|
|
361
|
+
/** §5: the arm a session falls in: the first 8 hex of sha256(`<experiment>:<session>`) mod 1000,
|
|
362
|
+
* walked through the arms' per-mille shares in order. The same session always gets the same arm. */
|
|
363
|
+
export function assignArm(experimentId, sessionId, arms) {
|
|
364
|
+
const bucket = Number.parseInt(sha256(`${experimentId}:${sessionId}`).slice(0, 8), 16) % 1000;
|
|
365
|
+
let at = 0;
|
|
366
|
+
for (const a of arms) {
|
|
367
|
+
at += a.share_permille;
|
|
368
|
+
if (bucket < at)
|
|
369
|
+
return a.name;
|
|
370
|
+
}
|
|
371
|
+
return arms[arms.length - 1].name;
|
|
372
|
+
}
|
|
373
|
+
/** §5: an experiment, checked against its prompt's versions: `{name, prompt, arms}`, or why not. */
|
|
374
|
+
export function checkExperiment(v, versions) {
|
|
375
|
+
if (!isObj(v))
|
|
376
|
+
return "the experiment must be a JSON object";
|
|
377
|
+
if (typeof v.name !== "string" || v.name.length < 1 || v.name.length > 128)
|
|
378
|
+
return "name must be 1-128 characters";
|
|
379
|
+
if (typeof v.prompt !== "string" || !PROMPT_NAME.test(v.prompt))
|
|
380
|
+
return "prompt must be a prompt's name";
|
|
381
|
+
if (!Array.isArray(v.arms) || v.arms.length < 2 || v.arms.length > 10)
|
|
382
|
+
return "arms must be a list of 2-10 arms";
|
|
383
|
+
const arms = [];
|
|
384
|
+
for (const [i, a] of v.arms.entries()) {
|
|
385
|
+
const at = `arms[${i}]`;
|
|
386
|
+
if (!isObj(a))
|
|
387
|
+
return `${at} must be {name, version, share_permille}`;
|
|
388
|
+
if (typeof a.name !== "string" || !ARM_NAME.test(a.name))
|
|
389
|
+
return `${at}.name must be 1-64 of a-z 0-9 _ -`;
|
|
390
|
+
if (arms.some((x) => x.name === a.name))
|
|
391
|
+
return `${at}.name repeats "${a.name}"`;
|
|
392
|
+
if (!(typeof a.version === "number" && versions.includes(a.version)))
|
|
393
|
+
return `${at}.version must be a version of ${v.prompt}`;
|
|
394
|
+
const share = a.share_permille;
|
|
395
|
+
if (!(typeof share === "number" && Number.isSafeInteger(share) && share >= 0 && share <= 1000))
|
|
396
|
+
return `${at}.share_permille must be an integer 0-1000`;
|
|
397
|
+
arms.push({ name: a.name, version: a.version, share_permille: share });
|
|
398
|
+
}
|
|
399
|
+
if (arms.reduce((n, a) => n + a.share_permille, 0) !== 1000)
|
|
400
|
+
return "the arms' share_permille must add up to 1000";
|
|
401
|
+
return { name: v.name, prompt: v.prompt, arms };
|
|
402
|
+
}
|
|
403
|
+
/** §4: how a group of runs did: success with the 95% range, spend per run, findings. */
|
|
404
|
+
export function armStats(runs) {
|
|
405
|
+
const wins = runs.filter((s) => s.outcome === "success").length;
|
|
406
|
+
const [low, high] = successRange(wins, runs.length);
|
|
407
|
+
const findings = { advisory: 0, caution: 0, warning: 0 };
|
|
408
|
+
const codes = {};
|
|
409
|
+
for (const s of runs) {
|
|
410
|
+
for (const k of ["advisory", "caution", "warning"])
|
|
411
|
+
findings[k] += s.findings?.[k] ?? 0;
|
|
412
|
+
for (const [c, n] of Object.entries(s.finding_codes ?? {}))
|
|
413
|
+
codes[c] = (codes[c] ?? 0) + n;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
sessions: runs.length,
|
|
417
|
+
success: wins,
|
|
418
|
+
success_ppm: ppm(wins, runs.length),
|
|
419
|
+
success_low_ppm: low,
|
|
420
|
+
success_high_ppm: high,
|
|
421
|
+
spend_per_run_micro_usd: runs.length
|
|
422
|
+
? Math.floor(runs.reduce((n, s) => n + s.spend_micro_usd, 0) / runs.length)
|
|
423
|
+
: 0,
|
|
424
|
+
findings,
|
|
425
|
+
finding_codes: Object.fromEntries(Object.keys(codes)
|
|
426
|
+
.sort()
|
|
427
|
+
.map((c) => [c, codes[c]])),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* §4: each version of a prompt, from closed sessions' summaries: the runs that declared it (an
|
|
432
|
+
* experiment's arm or the SDK's `promptVersion`), and the runs that declared none whose first
|
|
433
|
+
* request's system prompt hash is the version's. Oldest version first, each against the one before.
|
|
434
|
+
*/
|
|
435
|
+
export function promptResults(summaries, prompt) {
|
|
436
|
+
let matched = 0;
|
|
437
|
+
const groups = new Map(prompt.versions.map((v) => [v.version, { declared: [], detected: [] }]));
|
|
438
|
+
for (const s of summaries) {
|
|
439
|
+
if (!s.closed)
|
|
440
|
+
continue;
|
|
441
|
+
const pv = s.prompt_version;
|
|
442
|
+
if (pv) {
|
|
443
|
+
const g = pv.name === prompt.name ? groups.get(pv.version) : undefined;
|
|
444
|
+
if (g) {
|
|
445
|
+
g.declared.push(s);
|
|
446
|
+
matched++;
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const v = prompt.versions.find((x) => x.sha256 === s.version?.prompt);
|
|
451
|
+
if (v) {
|
|
452
|
+
groups.get(v.version)?.detected.push(s);
|
|
453
|
+
matched++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const versions = prompt.versions.map((v) => {
|
|
457
|
+
const g = groups.get(v.version);
|
|
458
|
+
return {
|
|
459
|
+
version: v.version,
|
|
460
|
+
sha256: v.sha256,
|
|
461
|
+
declared: g.declared.length,
|
|
462
|
+
detected: g.detected.length,
|
|
463
|
+
...armStats([...g.declared, ...g.detected]),
|
|
464
|
+
vs_previous: null,
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
for (let i = 1; i < versions.length; i++) {
|
|
468
|
+
const [p, v] = [versions[i - 1], versions[i]];
|
|
469
|
+
v.vs_previous = versus({ low: v.success_low_ppm, high: v.success_high_ppm }, { low: p.success_low_ppm, high: p.success_high_ppm });
|
|
470
|
+
}
|
|
471
|
+
return { name: prompt.name, sessions: matched, versions };
|
|
472
|
+
}
|
|
473
|
+
/** §5: each arm of an experiment, from the closed sessions it assigned, against the first arm. */
|
|
474
|
+
export function experimentResults(summaries, experiment) {
|
|
475
|
+
const mine = summaries.filter((s) => s.closed && s.experiment?.id === experiment.id);
|
|
476
|
+
const arms = experiment.arms.map((a) => ({
|
|
477
|
+
name: a.name,
|
|
478
|
+
version: a.version,
|
|
479
|
+
share_permille: a.share_permille,
|
|
480
|
+
...armStats(mine.filter((s) => s.experiment?.arm === a.name)),
|
|
481
|
+
vs_control: null,
|
|
482
|
+
}));
|
|
483
|
+
const control = arms[0];
|
|
484
|
+
if (control)
|
|
485
|
+
for (const a of arms.slice(1))
|
|
486
|
+
a.vs_control = versus({ low: a.success_low_ppm, high: a.success_high_ppm }, { low: control.success_low_ppm, high: control.success_high_ppm });
|
|
487
|
+
return { id: experiment.id, sessions: mine.length, arms };
|
|
488
|
+
}
|
package/dist/evidence/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { actionsPage } from "../actions/index.ts";
|
|
1
2
|
import type { Bundle, BundleReport } from "../verify/index.ts";
|
|
2
3
|
type Text = {
|
|
3
4
|
en: string;
|
|
@@ -7,7 +8,10 @@ export interface EvidencePackOptions {
|
|
|
7
8
|
generated_at: string;
|
|
8
9
|
bundle_sha256: string;
|
|
9
10
|
proofs: "checked" | "not_checked";
|
|
11
|
+
/** §7a: an `actionsPage`, when the bodies are at hand: each action with the reasoning before it. */
|
|
12
|
+
actions?: ActionsPage;
|
|
10
13
|
}
|
|
14
|
+
type ActionsPage = ReturnType<typeof actionsPage>;
|
|
11
15
|
/** spec/investigations.md §7. */
|
|
12
16
|
export declare function evidencePack(bundle: Bundle, report: BundleReport, opts: EvidencePackOptions): {
|
|
13
17
|
v: 1;
|
|
@@ -53,6 +57,11 @@ export declare function evidencePack(bundle: Bundle, report: BundleReport, opts:
|
|
|
53
57
|
reproduce: string[];
|
|
54
58
|
generated_at: string;
|
|
55
59
|
bundle_sha256: string;
|
|
60
|
+
actions?: {
|
|
61
|
+
total: number;
|
|
62
|
+
capped: boolean;
|
|
63
|
+
actions: import("../actions/index.ts").Action[];
|
|
64
|
+
};
|
|
56
65
|
};
|
|
57
66
|
export type EvidencePack = ReturnType<typeof evidencePack>;
|
|
58
67
|
/** The Markdown copy (spec/investigations.md §7): `en`, `ar` or `both`. */
|
package/dist/evidence/index.js
CHANGED
|
@@ -97,6 +97,7 @@ export function evidencePack(bundle, report, opts) {
|
|
|
97
97
|
reproduce: REPRODUCE,
|
|
98
98
|
generated_at: opts.generated_at,
|
|
99
99
|
bundle_sha256: opts.bundle_sha256,
|
|
100
|
+
...(opts.actions ? { actions: opts.actions } : {}),
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
// U+2067 … U+2069 isolate the Arabic paragraphs, so they read right to left in any viewer.
|
|
@@ -147,5 +148,54 @@ export function renderEvidencePack(pack, lang = "both") {
|
|
|
147
148
|
...pack.reproduce,
|
|
148
149
|
"```",
|
|
149
150
|
"",
|
|
151
|
+
...actionsMd(pack.actions, label),
|
|
150
152
|
].join("\n");
|
|
151
153
|
}
|
|
154
|
+
const KIND = {
|
|
155
|
+
text: ["the model's own reasoning", "تفكير النموذج نفسه"],
|
|
156
|
+
summary: ["the provider's summary of it", "ملخّص المزوّد له"],
|
|
157
|
+
encrypted: ["recorded, but encrypted by the provider", "مسجّل لكنه مشفّر من المزوّد"],
|
|
158
|
+
none: ["none recorded", "لم يُسجَّل شيء"],
|
|
159
|
+
};
|
|
160
|
+
const quote = (text) => text
|
|
161
|
+
.split(/\r?\n|\r/)
|
|
162
|
+
.map((l) => ` > ${l}`)
|
|
163
|
+
.join("\n") || ">";
|
|
164
|
+
/** Python's json.dumps(…, ensure_ascii=False, sort_keys=True): keys sorted, ", " and ": ". */
|
|
165
|
+
function pyJson(v) {
|
|
166
|
+
if (Array.isArray(v))
|
|
167
|
+
return `[${v.map(pyJson).join(", ")}]`;
|
|
168
|
+
if (v && typeof v === "object")
|
|
169
|
+
return `{${Object.keys(v)
|
|
170
|
+
.sort()
|
|
171
|
+
.map((k) => `${JSON.stringify(k)}: ${pyJson(v[k])}`)
|
|
172
|
+
.join(", ")}}`;
|
|
173
|
+
return JSON.stringify(v) ?? "null";
|
|
174
|
+
}
|
|
175
|
+
/** §7a: each action, then the reasoning recorded before it. */
|
|
176
|
+
function actionsMd(page, label) {
|
|
177
|
+
if (!page)
|
|
178
|
+
return [];
|
|
179
|
+
const out = [
|
|
180
|
+
`## ${label("Actions and the reasoning before each", "الإجراءات والتفكير الذي سبق كلًّا منها")}`,
|
|
181
|
+
"",
|
|
182
|
+
];
|
|
183
|
+
if (!page.actions.length)
|
|
184
|
+
out.push(label("No actions recorded.", "لم تُسجَّل أي إجراءات."), "");
|
|
185
|
+
page.actions.forEach((a, i) => {
|
|
186
|
+
const all = pyJson(a.args);
|
|
187
|
+
const points = Array.from(all);
|
|
188
|
+
const args = points.length <= 300 ? all : `${points.slice(0, 300).join("")}…`;
|
|
189
|
+
const [en, ar] = KIND[a.reasoning.kind] ?? KIND.none;
|
|
190
|
+
out.push(`${i + 1}. **\`${a.tool}\`** · ${label("event", "الحدث")} ${a.seq} · \`${args}\``, "", ` ${label("Reasoning", "التفكير")}: ${label(en, ar)}`, "");
|
|
191
|
+
if (a.reasoning.text)
|
|
192
|
+
out.push(quote(a.reasoning.text), "");
|
|
193
|
+
if (a.said)
|
|
194
|
+
out.push(` ${label("It wrote", "كتب")}:`, "", quote(a.said), "");
|
|
195
|
+
});
|
|
196
|
+
if (page.capped) {
|
|
197
|
+
const n = page.actions.length;
|
|
198
|
+
out.push(label(`The first ${n} of ${page.total} actions.`, `أول ${n} من أصل ${page.total} إجراءات.`), "");
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
@@ -8,6 +8,15 @@ export declare function cosignLedgerSth(sth: SignedTreeHead, o: {
|
|
|
8
8
|
witnessed_log_id: string;
|
|
9
9
|
cosignature: SignedTreeHead;
|
|
10
10
|
};
|
|
11
|
+
/** spec/anchoring.md §15: `cosignLedgerSth` with a key held outside the process. */
|
|
12
|
+
export declare function cosignLedgerSthWith(sth: SignedTreeHead, o: {
|
|
13
|
+
did: string;
|
|
14
|
+
sign: (message: Uint8Array) => Promise<Uint8Array>;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
v: number;
|
|
17
|
+
witnessed_log_id: string;
|
|
18
|
+
cosignature: SignedTreeHead;
|
|
19
|
+
}>;
|
|
11
20
|
/** Zanii's check: a valid co-signature over the same (size, root) of a valid tree head. */
|
|
12
21
|
export declare function verifyLedgerCosignature(c: unknown, sth: SignedTreeHead): boolean;
|
|
13
22
|
/** Zanii's equivocation proof: one log key, one size, two roots. Throws unless it really is one. */
|
package/dist/federation/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// co-signs the ledger's signed tree heads it checked, and gossip that turns a split view into an
|
|
3
3
|
// equivocation proof (@zanii/witness, @zanii/federation). Pure; mirrors
|
|
4
4
|
// sdks/python/src/zanii_blackbox/federation.py; pinned by spec/vectors/federation.json.
|
|
5
|
-
import { createSTH, verifySTH } from "@zanii/core";
|
|
5
|
+
import { createSTH, createSTHAsync, verifySTH } from "@zanii/core";
|
|
6
6
|
const valid = (sth) => {
|
|
7
7
|
try {
|
|
8
8
|
return typeof sth === "object" && sth !== null && verifySTH(sth);
|
|
@@ -18,6 +18,13 @@ export function cosignLedgerSth(sth, o) {
|
|
|
18
18
|
const cosignature = createSTH({ logId: o.did, size: sth.size, root: sth.root, ts: sth.ts }, o.privateKey);
|
|
19
19
|
return { v: 1, witnessed_log_id: sth.log_id, cosignature };
|
|
20
20
|
}
|
|
21
|
+
/** spec/anchoring.md §15: `cosignLedgerSth` with a key held outside the process. */
|
|
22
|
+
export async function cosignLedgerSthWith(sth, o) {
|
|
23
|
+
if (!valid(sth))
|
|
24
|
+
throw new Error("refusing to co-sign: the signed tree head doesn't verify");
|
|
25
|
+
const cosignature = await createSTHAsync({ logId: o.did, size: sth.size, root: sth.root, ts: sth.ts }, o.sign);
|
|
26
|
+
return { v: 1, witnessed_log_id: sth.log_id, cosignature };
|
|
27
|
+
}
|
|
21
28
|
/** Zanii's check: a valid co-signature over the same (size, root) of a valid tree head. */
|
|
22
29
|
export function verifyLedgerCosignature(c, sth) {
|
|
23
30
|
const p = (c ?? {});
|
package/dist/finance/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// (@zanii/kyb), and the findings over them. Pure; mirrors sdks/python/src/zanii_blackbox/finance.py;
|
|
4
4
|
// pinned by spec/vectors/finance.json.
|
|
5
5
|
import { canonicalBytes, publicKeyFromDid } from "@zanii/core";
|
|
6
|
+
import { envelopesOf } from "../reconcile/record.js";
|
|
6
7
|
import { ed25519Sign, ed25519Verify } from "../transparency/index.js";
|
|
7
8
|
/** Minor-unit precision: Zanii's table, plus the Gulf's currencies. */
|
|
8
9
|
export const CURRENCIES = {
|
|
@@ -219,8 +220,7 @@ export function financeFindings(lines, bodies, closing = false) {
|
|
|
219
220
|
const screened = new Map();
|
|
220
221
|
const prepared = [];
|
|
221
222
|
const handedOff = new Set();
|
|
222
|
-
for (const
|
|
223
|
-
const e = JSON.parse(l);
|
|
223
|
+
for (const e of envelopesOf(lines)) {
|
|
224
224
|
if (e.kind === "session.close")
|
|
225
225
|
closed = true;
|
|
226
226
|
if (e.kind !== "sdk.event")
|
|
@@ -287,8 +287,7 @@ export function financeFindings(lines, bodies, closing = false) {
|
|
|
287
287
|
export function paymentsOf(lines, bodies) {
|
|
288
288
|
const payments = [];
|
|
289
289
|
const totals = new Map();
|
|
290
|
-
for (const
|
|
291
|
-
const e = JSON.parse(l);
|
|
290
|
+
for (const e of envelopesOf(lines)) {
|
|
292
291
|
if (e.kind !== "sdk.event" || e.meta.type !== "payment")
|
|
293
292
|
continue;
|
|
294
293
|
const body = bodies(e.body_hash);
|