@takk/bayesoutputgate 1.0.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 +92 -0
- package/LICENSE +190 -0
- package/NOTICE +45 -0
- package/README.md +403 -0
- package/SECURITY.md +98 -0
- package/SPEC.md +467 -0
- package/dist/adapter/index.cjs +411 -0
- package/dist/adapter/index.d.cts +29 -0
- package/dist/adapter/index.d.ts +29 -0
- package/dist/adapter/index.js +404 -0
- package/dist/audit/index.cjs +82 -0
- package/dist/audit/index.d.cts +40 -0
- package/dist/audit/index.d.ts +40 -0
- package/dist/audit/index.js +77 -0
- package/dist/bayesfactor/index.cjs +152 -0
- package/dist/bayesfactor/index.d.cts +15 -0
- package/dist/bayesfactor/index.d.ts +15 -0
- package/dist/bayesfactor/index.js +149 -0
- package/dist/beta/index.cjs +180 -0
- package/dist/beta/index.d.cts +45 -0
- package/dist/beta/index.d.ts +45 -0
- package/dist/beta/index.js +178 -0
- package/dist/calibration/index.cjs +339 -0
- package/dist/calibration/index.d.cts +53 -0
- package/dist/calibration/index.d.ts +53 -0
- package/dist/calibration/index.js +333 -0
- package/dist/cli/index.cjs +968 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +966 -0
- package/dist/dimensions/index.cjs +106 -0
- package/dist/dimensions/index.d.cts +33 -0
- package/dist/dimensions/index.d.ts +33 -0
- package/dist/dimensions/index.js +104 -0
- package/dist/edge/index.cjs +1141 -0
- package/dist/edge/index.d.cts +12 -0
- package/dist/edge/index.d.ts +12 -0
- package/dist/edge/index.js +1109 -0
- package/dist/gate/index.cjs +803 -0
- package/dist/gate/index.d.cts +77 -0
- package/dist/gate/index.d.ts +77 -0
- package/dist/gate/index.js +799 -0
- package/dist/hypothesis/index.cjs +268 -0
- package/dist/hypothesis/index.d.cts +38 -0
- package/dist/hypothesis/index.d.ts +38 -0
- package/dist/hypothesis/index.js +266 -0
- package/dist/index.cjs +1141 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1109 -0
- package/dist/likelihood/index.cjs +137 -0
- package/dist/likelihood/index.d.cts +23 -0
- package/dist/likelihood/index.d.ts +23 -0
- package/dist/likelihood/index.js +132 -0
- package/dist/node/index.cjs +1282 -0
- package/dist/node/index.d.cts +24 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.js +1246 -0
- package/dist/policy/index.cjs +88 -0
- package/dist/policy/index.d.cts +11 -0
- package/dist/policy/index.d.ts +11 -0
- package/dist/policy/index.js +85 -0
- package/dist/types-bMjn1j4e.d.cts +159 -0
- package/dist/types-bMjn1j4e.d.ts +159 -0
- package/package.json +142 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
function canonicalize(value) {
|
|
3
|
+
return serialize(value);
|
|
4
|
+
}
|
|
5
|
+
function serialize(value) {
|
|
6
|
+
if (value === null) {
|
|
7
|
+
return "null";
|
|
8
|
+
}
|
|
9
|
+
const kind = typeof value;
|
|
10
|
+
if (kind === "number") {
|
|
11
|
+
return Number.isFinite(value) ? String(value) : "null";
|
|
12
|
+
}
|
|
13
|
+
if (kind === "boolean") {
|
|
14
|
+
return value ? "true" : "false";
|
|
15
|
+
}
|
|
16
|
+
if (kind === "string") {
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return `[${value.map((item) => serialize(item)).join(",")}]`;
|
|
21
|
+
}
|
|
22
|
+
if (kind === "object") {
|
|
23
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
24
|
+
const body = entries.map(([key, v]) => `${JSON.stringify(key)}:${serialize(v)}`).join(",");
|
|
25
|
+
return `{${body}}`;
|
|
26
|
+
}
|
|
27
|
+
return "null";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/audit/index.ts
|
|
31
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
32
|
+
async function sha256Hex(input) {
|
|
33
|
+
const bytes = new TextEncoder().encode(input);
|
|
34
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
35
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
36
|
+
}
|
|
37
|
+
function recordOf(index, payload, previousHash, timestamp) {
|
|
38
|
+
return timestamp !== void 0 ? { index, timestamp, payload, previousHash } : { index, payload, previousHash };
|
|
39
|
+
}
|
|
40
|
+
var AuditChain = class {
|
|
41
|
+
entries = [];
|
|
42
|
+
/** Append a payload, sealing it against the previous entry's hash. */
|
|
43
|
+
async append(payload, meta = {}) {
|
|
44
|
+
const index = this.entries.length;
|
|
45
|
+
const previousHash = index === 0 ? GENESIS_HASH : this.entries[index - 1].hash;
|
|
46
|
+
const record = recordOf(index, payload, previousHash, meta.timestamp);
|
|
47
|
+
const hash = await sha256Hex(canonicalize(record));
|
|
48
|
+
const entry = meta.timestamp !== void 0 ? { index, timestamp: meta.timestamp, payload, previousHash, hash } : { index, payload, previousHash, hash };
|
|
49
|
+
this.entries.push(entry);
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
/** A copy of the chain's entries in order. */
|
|
53
|
+
toArray() {
|
|
54
|
+
return [...this.entries];
|
|
55
|
+
}
|
|
56
|
+
/** Number of entries in the chain. */
|
|
57
|
+
get length() {
|
|
58
|
+
return this.entries.length;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/errors.ts
|
|
63
|
+
var BayesOutputGateError = class _BayesOutputGateError extends Error {
|
|
64
|
+
code;
|
|
65
|
+
constructor(code, message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "BayesOutputGateError";
|
|
68
|
+
this.code = code;
|
|
69
|
+
Object.setPrototypeOf(this, _BayesOutputGateError.prototype);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function invariant(condition, code, message) {
|
|
73
|
+
if (!condition) {
|
|
74
|
+
throw new BayesOutputGateError(code, message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/mathspecial.ts
|
|
79
|
+
var LN_SQRT_2PI = 0.9189385332046728;
|
|
80
|
+
var LANCZOS_G = 7;
|
|
81
|
+
var LANCZOS_COEFFICIENTS = [
|
|
82
|
+
0.9999999999998099,
|
|
83
|
+
676.5203681218851,
|
|
84
|
+
-1259.1392167224028,
|
|
85
|
+
771.3234287776531,
|
|
86
|
+
-176.6150291621406,
|
|
87
|
+
12.507343278686905,
|
|
88
|
+
-0.13857109526572012,
|
|
89
|
+
9984369578019572e-21,
|
|
90
|
+
15056327351493116e-23
|
|
91
|
+
];
|
|
92
|
+
function lgamma(x) {
|
|
93
|
+
if (!Number.isFinite(x) || x <= 0) {
|
|
94
|
+
throw new BayesOutputGateError(
|
|
95
|
+
"NUMERIC",
|
|
96
|
+
`lgamma requires a positive finite argument, got ${x}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const z = x - 1;
|
|
100
|
+
let acc = LANCZOS_COEFFICIENTS[0];
|
|
101
|
+
for (let i = 1; i < LANCZOS_COEFFICIENTS.length; i++) {
|
|
102
|
+
acc += LANCZOS_COEFFICIENTS[i] / (z + i);
|
|
103
|
+
}
|
|
104
|
+
const t = z + LANCZOS_G + 0.5;
|
|
105
|
+
return LN_SQRT_2PI + (z + 0.5) * Math.log(t) - t + Math.log(acc);
|
|
106
|
+
}
|
|
107
|
+
function lbeta(a, b) {
|
|
108
|
+
return lgamma(a) + lgamma(b) - lgamma(a + b);
|
|
109
|
+
}
|
|
110
|
+
function betaLogDensity(x, a, b) {
|
|
111
|
+
if (a <= 0 || b <= 0) {
|
|
112
|
+
throw new BayesOutputGateError(
|
|
113
|
+
"NUMERIC",
|
|
114
|
+
`Beta shape parameters must be positive, got a=${a}, b=${b}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (x < 0 || x > 1 || !Number.isFinite(x)) {
|
|
118
|
+
return Number.NEGATIVE_INFINITY;
|
|
119
|
+
}
|
|
120
|
+
if (x === 0) {
|
|
121
|
+
if (a < 1) return Number.POSITIVE_INFINITY;
|
|
122
|
+
if (a > 1) return Number.NEGATIVE_INFINITY;
|
|
123
|
+
return -lbeta(a, b) + (b - 1) * Math.log(1);
|
|
124
|
+
}
|
|
125
|
+
if (x === 1) {
|
|
126
|
+
if (b < 1) return Number.POSITIVE_INFINITY;
|
|
127
|
+
if (b > 1) return Number.NEGATIVE_INFINITY;
|
|
128
|
+
return -lbeta(a, b);
|
|
129
|
+
}
|
|
130
|
+
return (a - 1) * Math.log(x) + (b - 1) * Math.log1p(-x) - lbeta(a, b);
|
|
131
|
+
}
|
|
132
|
+
function clamp(x, lo, hi) {
|
|
133
|
+
if (x < lo) return lo;
|
|
134
|
+
if (x > hi) return hi;
|
|
135
|
+
return x;
|
|
136
|
+
}
|
|
137
|
+
function regularizedIncompleteBeta(x, a, b) {
|
|
138
|
+
if (a <= 0 || b <= 0) {
|
|
139
|
+
throw new BayesOutputGateError(
|
|
140
|
+
"NUMERIC",
|
|
141
|
+
`incomplete beta requires positive shapes, got a=${a}, b=${b}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (x <= 0) return 0;
|
|
145
|
+
if (x >= 1) return 1;
|
|
146
|
+
const logFront = lgamma(a + b) - lgamma(a) - lgamma(b) + a * Math.log(x) + b * Math.log1p(-x);
|
|
147
|
+
const front = Math.exp(logFront);
|
|
148
|
+
if (x < (a + 1) / (a + b + 2)) {
|
|
149
|
+
return front * betaContinuedFraction(x, a, b) / a;
|
|
150
|
+
}
|
|
151
|
+
return 1 - front * betaContinuedFraction(1 - x, b, a) / b;
|
|
152
|
+
}
|
|
153
|
+
function betaCdf(x, a, b) {
|
|
154
|
+
return regularizedIncompleteBeta(x, a, b);
|
|
155
|
+
}
|
|
156
|
+
function betaContinuedFraction(x, a, b) {
|
|
157
|
+
const maxIterations = 200;
|
|
158
|
+
const epsilon = 3e-12;
|
|
159
|
+
const tiny = 1e-300;
|
|
160
|
+
const qab = a + b;
|
|
161
|
+
const qap = a + 1;
|
|
162
|
+
const qam = a - 1;
|
|
163
|
+
let c = 1;
|
|
164
|
+
let d = 1 - qab * x / qap;
|
|
165
|
+
if (Math.abs(d) < tiny) d = tiny;
|
|
166
|
+
d = 1 / d;
|
|
167
|
+
let h = d;
|
|
168
|
+
for (let m = 1; m <= maxIterations; m++) {
|
|
169
|
+
const m2 = 2 * m;
|
|
170
|
+
let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
|
|
171
|
+
d = 1 + aa * d;
|
|
172
|
+
if (Math.abs(d) < tiny) d = tiny;
|
|
173
|
+
c = 1 + aa / c;
|
|
174
|
+
if (Math.abs(c) < tiny) c = tiny;
|
|
175
|
+
d = 1 / d;
|
|
176
|
+
h *= d * c;
|
|
177
|
+
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
|
|
178
|
+
d = 1 + aa * d;
|
|
179
|
+
if (Math.abs(d) < tiny) d = tiny;
|
|
180
|
+
c = 1 + aa / c;
|
|
181
|
+
if (Math.abs(c) < tiny) c = tiny;
|
|
182
|
+
d = 1 / d;
|
|
183
|
+
const delta = d * c;
|
|
184
|
+
h *= delta;
|
|
185
|
+
if (Math.abs(delta - 1) < epsilon) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return h;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/bayesfactor/index.ts
|
|
193
|
+
var LN_3 = Math.log(3);
|
|
194
|
+
var LN_10 = Math.log(10);
|
|
195
|
+
var LN_100 = Math.log(100);
|
|
196
|
+
var DENSITY_EPSILON = 1e-6;
|
|
197
|
+
function jeffreysStrength(logBayesFactor) {
|
|
198
|
+
const x = logBayesFactor;
|
|
199
|
+
if (x >= LN_100) return "decisive-high";
|
|
200
|
+
if (x >= LN_10) return "strong-high";
|
|
201
|
+
if (x >= LN_3) return "substantial-high";
|
|
202
|
+
if (x > -LN_3) return "inconclusive";
|
|
203
|
+
if (x > -LN_10) return "substantial-low";
|
|
204
|
+
if (x > -LN_100) return "strong-low";
|
|
205
|
+
return "decisive-low";
|
|
206
|
+
}
|
|
207
|
+
function bayesFactor(scores, models) {
|
|
208
|
+
invariant(models.length > 0, "INVALID_CONFIG", "at least one dimension model is required");
|
|
209
|
+
const map = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const s of scores) {
|
|
211
|
+
invariant(
|
|
212
|
+
Number.isFinite(s.value) && s.value >= 0 && s.value <= 1,
|
|
213
|
+
"INVALID_SCORE",
|
|
214
|
+
`score for "${s.dimension}" must be a finite number in [0, 1], got ${s.value}`
|
|
215
|
+
);
|
|
216
|
+
invariant(
|
|
217
|
+
!map.has(s.dimension),
|
|
218
|
+
"INVALID_SCORE",
|
|
219
|
+
`duplicate score for dimension "${s.dimension}"`
|
|
220
|
+
);
|
|
221
|
+
map.set(s.dimension, s.value);
|
|
222
|
+
}
|
|
223
|
+
const contributions = [];
|
|
224
|
+
let logBayesFactor = 0;
|
|
225
|
+
for (const model of models) {
|
|
226
|
+
const value = map.get(model.dimension);
|
|
227
|
+
if (value === void 0) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
invariant(
|
|
231
|
+
Number.isFinite(model.weight) && model.weight >= 0,
|
|
232
|
+
"INVALID_CONFIG",
|
|
233
|
+
`weight for "${model.dimension}" must be a non-negative finite number, got ${model.weight}`
|
|
234
|
+
);
|
|
235
|
+
const x = clamp(value, DENSITY_EPSILON, 1 - DENSITY_EPSILON);
|
|
236
|
+
const logHigh = betaLogDensity(x, model.high.a, model.high.b);
|
|
237
|
+
const logLow = betaLogDensity(x, model.low.a, model.low.b);
|
|
238
|
+
const weighted = model.weight * (logHigh - logLow);
|
|
239
|
+
logBayesFactor += weighted;
|
|
240
|
+
contributions.push({
|
|
241
|
+
dimension: model.dimension,
|
|
242
|
+
score: value,
|
|
243
|
+
logBayesFactor: weighted,
|
|
244
|
+
weight: model.weight
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
invariant(
|
|
248
|
+
contributions.length > 0,
|
|
249
|
+
"INVALID_SCORE",
|
|
250
|
+
"no scored dimension matched a configured dimension model"
|
|
251
|
+
);
|
|
252
|
+
invariant(Number.isFinite(logBayesFactor), "NUMERIC", "combined log-Bayes-Factor is not finite");
|
|
253
|
+
const strength = jeffreysStrength(logBayesFactor);
|
|
254
|
+
const favored = strength.endsWith("high") ? "high" : strength.endsWith("low") ? "low" : "inconclusive";
|
|
255
|
+
return {
|
|
256
|
+
logBayesFactor,
|
|
257
|
+
bayesFactor: Math.exp(logBayesFactor),
|
|
258
|
+
favored,
|
|
259
|
+
strength,
|
|
260
|
+
contributions
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/dimensions/index.ts
|
|
265
|
+
function dependenceDiagnostic(history, options = {}) {
|
|
266
|
+
const threshold = options.threshold ?? 0.5;
|
|
267
|
+
invariant(
|
|
268
|
+
Number.isFinite(threshold) && threshold >= 0 && threshold <= 1,
|
|
269
|
+
"INVALID_CONFIG",
|
|
270
|
+
`threshold must be in [0, 1], got ${threshold}`
|
|
271
|
+
);
|
|
272
|
+
invariant(
|
|
273
|
+
history.length >= 2,
|
|
274
|
+
"INVALID_OBSERVATION",
|
|
275
|
+
"dependence diagnostic needs at least two score vectors"
|
|
276
|
+
);
|
|
277
|
+
const dimensionSet = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const vector of history) {
|
|
279
|
+
for (const score of vector) {
|
|
280
|
+
dimensionSet.add(score.dimension);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const dimensions = [...dimensionSet].sort();
|
|
284
|
+
const rows = history.map((vector) => {
|
|
285
|
+
const map = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const score of vector) {
|
|
287
|
+
map.set(score.dimension, score.value);
|
|
288
|
+
}
|
|
289
|
+
return map;
|
|
290
|
+
});
|
|
291
|
+
const pairs = [];
|
|
292
|
+
for (let i = 0; i < dimensions.length; i++) {
|
|
293
|
+
for (let j = i + 1; j < dimensions.length; j++) {
|
|
294
|
+
const a = dimensions[i];
|
|
295
|
+
const b = dimensions[j];
|
|
296
|
+
const xs = [];
|
|
297
|
+
const ys = [];
|
|
298
|
+
for (const row of rows) {
|
|
299
|
+
const x = row.get(a);
|
|
300
|
+
const y = row.get(b);
|
|
301
|
+
if (x !== void 0 && y !== void 0) {
|
|
302
|
+
xs.push(x);
|
|
303
|
+
ys.push(y);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (xs.length >= 2) {
|
|
307
|
+
const correlation = pearson(xs, ys);
|
|
308
|
+
if (Number.isFinite(correlation)) {
|
|
309
|
+
pairs.push({ a, b, correlation, samples: xs.length });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
pairs.sort((left, right) => Math.abs(right.correlation) - Math.abs(left.correlation));
|
|
315
|
+
const maxAbsCorrelation = pairs.length > 0 ? Math.abs(pairs[0].correlation) : 0;
|
|
316
|
+
const flagged = pairs.filter((pair) => Math.abs(pair.correlation) >= threshold);
|
|
317
|
+
return {
|
|
318
|
+
dimensions,
|
|
319
|
+
pairs,
|
|
320
|
+
flagged,
|
|
321
|
+
maxAbsCorrelation,
|
|
322
|
+
independenceAssumptionSafe: flagged.length === 0
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function pearson(xs, ys) {
|
|
326
|
+
const n = xs.length;
|
|
327
|
+
let sumX = 0;
|
|
328
|
+
let sumY = 0;
|
|
329
|
+
for (let i = 0; i < n; i++) {
|
|
330
|
+
sumX += xs[i];
|
|
331
|
+
sumY += ys[i];
|
|
332
|
+
}
|
|
333
|
+
const meanX = sumX / n;
|
|
334
|
+
const meanY = sumY / n;
|
|
335
|
+
let covariance = 0;
|
|
336
|
+
let varianceX = 0;
|
|
337
|
+
let varianceY = 0;
|
|
338
|
+
for (let i = 0; i < n; i++) {
|
|
339
|
+
const dx = xs[i] - meanX;
|
|
340
|
+
const dy = ys[i] - meanY;
|
|
341
|
+
covariance += dx * dy;
|
|
342
|
+
varianceX += dx * dx;
|
|
343
|
+
varianceY += dy * dy;
|
|
344
|
+
}
|
|
345
|
+
if (varianceX === 0 || varianceY === 0) {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
return covariance / Math.sqrt(varianceX * varianceY);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/calibration/index.ts
|
|
352
|
+
function goodnessOfFit(samples, params, alpha = 0.05) {
|
|
353
|
+
invariant(
|
|
354
|
+
samples.length >= 5,
|
|
355
|
+
"INVALID_OBSERVATION",
|
|
356
|
+
"goodnessOfFit needs at least five samples"
|
|
357
|
+
);
|
|
358
|
+
invariant(params.a > 0 && params.b > 0, "INVALID_CONFIG", "Beta parameters must be positive");
|
|
359
|
+
const sorted = [...samples].sort((left, right) => left - right);
|
|
360
|
+
const n = sorted.length;
|
|
361
|
+
let ks = 0;
|
|
362
|
+
for (let i = 0; i < n; i++) {
|
|
363
|
+
const value = sorted[i];
|
|
364
|
+
invariant(
|
|
365
|
+
Number.isFinite(value) && value >= 0 && value <= 1,
|
|
366
|
+
"INVALID_SCORE",
|
|
367
|
+
`samples must be finite and in [0, 1], got ${value}`
|
|
368
|
+
);
|
|
369
|
+
const cdf = betaCdf(value, params.a, params.b);
|
|
370
|
+
const upper = (i + 1) / n - cdf;
|
|
371
|
+
const lower = cdf - i / n;
|
|
372
|
+
ks = Math.max(ks, upper, lower);
|
|
373
|
+
}
|
|
374
|
+
const coefficient = alpha <= 0.01 ? 1.628 : alpha <= 0.05 ? 1.358 : 1.224;
|
|
375
|
+
const criticalValue = coefficient / Math.sqrt(n);
|
|
376
|
+
return { ksStatistic: ks, criticalValue, adequate: ks <= criticalValue, samples: n };
|
|
377
|
+
}
|
|
378
|
+
function scoresForDimension(history, dimension, label) {
|
|
379
|
+
const out = [];
|
|
380
|
+
for (const observation of history) {
|
|
381
|
+
if (observation.label !== label) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
for (const score of observation.scores) {
|
|
385
|
+
if (score.dimension === dimension) {
|
|
386
|
+
out.push(score.value);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
function assessAssumptions(history, models, options = {}) {
|
|
393
|
+
const alpha = options.alpha ?? 0.05;
|
|
394
|
+
const minSamples = options.minSamples ?? 5;
|
|
395
|
+
const inadequateDimensions = [];
|
|
396
|
+
for (const model of models) {
|
|
397
|
+
const high = scoresForDimension(history, model.dimension, "high");
|
|
398
|
+
const low = scoresForDimension(history, model.dimension, "low");
|
|
399
|
+
const highBad = high.length >= minSamples && !goodnessOfFit(high, model.high, alpha).adequate;
|
|
400
|
+
const lowBad = low.length >= minSamples && !goodnessOfFit(low, model.low, alpha).adequate;
|
|
401
|
+
if (highBad || lowBad) {
|
|
402
|
+
inadequateDimensions.push(model.dimension);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const vectors = history.map((observation) => observation.scores);
|
|
406
|
+
let independenceAssumptionSafe = true;
|
|
407
|
+
const dependentPairs = [];
|
|
408
|
+
if (vectors.length >= 2) {
|
|
409
|
+
const diagnostic = dependenceDiagnostic(
|
|
410
|
+
vectors,
|
|
411
|
+
options.dependenceThreshold !== void 0 ? { threshold: options.dependenceThreshold } : {}
|
|
412
|
+
);
|
|
413
|
+
independenceAssumptionSafe = diagnostic.independenceAssumptionSafe;
|
|
414
|
+
for (const pair of diagnostic.flagged) {
|
|
415
|
+
dependentPairs.push(`${pair.a}~${pair.b}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
goodnessOfFitAdequate: inadequateDimensions.length === 0,
|
|
420
|
+
independenceAssumptionSafe,
|
|
421
|
+
inadequateDimensions,
|
|
422
|
+
dependentPairs
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/beta/index.ts
|
|
427
|
+
var VARIANCE_FLOOR = 1e-9;
|
|
428
|
+
var CONCENTRATION_FLOOR = 1e-3;
|
|
429
|
+
var BetaModel = class _BetaModel {
|
|
430
|
+
priorA;
|
|
431
|
+
priorB;
|
|
432
|
+
count = 0;
|
|
433
|
+
sum = 0;
|
|
434
|
+
sumSquares = 0;
|
|
435
|
+
constructor(options = {}) {
|
|
436
|
+
const prior = options.prior ?? { a: 1, b: 1 };
|
|
437
|
+
invariant(
|
|
438
|
+
prior.a > 0 && prior.b > 0 && Number.isFinite(prior.a) && Number.isFinite(prior.b),
|
|
439
|
+
"INVALID_CONFIG",
|
|
440
|
+
`prior Beta parameters must be positive and finite, got a=${prior.a}, b=${prior.b}`
|
|
441
|
+
);
|
|
442
|
+
this.priorA = prior.a;
|
|
443
|
+
this.priorB = prior.b;
|
|
444
|
+
}
|
|
445
|
+
/** Build a model from labeled scores in one pass. */
|
|
446
|
+
static fromSamples(samples, options = {}) {
|
|
447
|
+
const model = new _BetaModel(options);
|
|
448
|
+
for (const s of samples) {
|
|
449
|
+
model.observe(s);
|
|
450
|
+
}
|
|
451
|
+
return model;
|
|
452
|
+
}
|
|
453
|
+
/** Restore a model from a snapshot. */
|
|
454
|
+
static fromSnapshot(snapshot) {
|
|
455
|
+
const model = new _BetaModel({ prior: snapshot.prior });
|
|
456
|
+
invariant(
|
|
457
|
+
snapshot.count >= 0 && Number.isFinite(snapshot.sum) && Number.isFinite(snapshot.sumSquares),
|
|
458
|
+
"INVALID_SNAPSHOT",
|
|
459
|
+
"snapshot fields must be finite and non-negative"
|
|
460
|
+
);
|
|
461
|
+
model.count = snapshot.count;
|
|
462
|
+
model.sum = snapshot.sum;
|
|
463
|
+
model.sumSquares = snapshot.sumSquares;
|
|
464
|
+
return model;
|
|
465
|
+
}
|
|
466
|
+
/** Number of real observations folded into this model so far. */
|
|
467
|
+
get observations() {
|
|
468
|
+
return this.count;
|
|
469
|
+
}
|
|
470
|
+
/** Fold a single score in [0, 1] into the model, updating its calibration online. */
|
|
471
|
+
observe(score) {
|
|
472
|
+
invariant(
|
|
473
|
+
Number.isFinite(score) && score >= 0 && score <= 1,
|
|
474
|
+
"INVALID_SCORE",
|
|
475
|
+
`score must be a finite number in [0, 1], got ${score}`
|
|
476
|
+
);
|
|
477
|
+
this.count += 1;
|
|
478
|
+
this.sum += score;
|
|
479
|
+
this.sumSquares += score * score;
|
|
480
|
+
return this;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The fitted Beta parameters. The prior is mixed in as `priorA + priorB` pseudo-observations with
|
|
484
|
+
* the prior's own mean and variance, then a and b come from method of moments on the blend, so a
|
|
485
|
+
* cold model returns its prior and a warm model is data-driven.
|
|
486
|
+
*/
|
|
487
|
+
params() {
|
|
488
|
+
const priorStrength = this.priorA + this.priorB;
|
|
489
|
+
const priorMean = this.priorA / priorStrength;
|
|
490
|
+
const priorVariance = priorMean * (1 - priorMean) / (priorStrength + 1);
|
|
491
|
+
const totalCount = this.count + priorStrength;
|
|
492
|
+
const totalSum = this.sum + priorStrength * priorMean;
|
|
493
|
+
const totalSumSquares = this.sumSquares + priorStrength * (priorVariance + priorMean * priorMean);
|
|
494
|
+
const mean = clamp(totalSum / totalCount, VARIANCE_FLOOR, 1 - VARIANCE_FLOOR);
|
|
495
|
+
const rawVariance = totalSumSquares / totalCount - mean * mean;
|
|
496
|
+
const maxVariance = mean * (1 - mean);
|
|
497
|
+
const variance = clamp(rawVariance, VARIANCE_FLOOR, maxVariance * (1 - VARIANCE_FLOOR));
|
|
498
|
+
const concentration = Math.max(mean * (1 - mean) / variance - 1, CONCENTRATION_FLOOR);
|
|
499
|
+
const a = Math.max(mean * concentration, CONCENTRATION_FLOOR);
|
|
500
|
+
const b = Math.max((1 - mean) * concentration, CONCENTRATION_FLOOR);
|
|
501
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) {
|
|
502
|
+
throw new BayesOutputGateError("NUMERIC", "Beta fit produced non-finite parameters");
|
|
503
|
+
}
|
|
504
|
+
return { a, b };
|
|
505
|
+
}
|
|
506
|
+
/** Mean of the fitted Beta. */
|
|
507
|
+
mean() {
|
|
508
|
+
const { a, b } = this.params();
|
|
509
|
+
return a / (a + b);
|
|
510
|
+
}
|
|
511
|
+
/** Log-density of an observed score under the fitted Beta. */
|
|
512
|
+
logDensity(score) {
|
|
513
|
+
const { a, b } = this.params();
|
|
514
|
+
return betaLogDensity(score, a, b);
|
|
515
|
+
}
|
|
516
|
+
/** A serializable snapshot of the current state. */
|
|
517
|
+
snapshot() {
|
|
518
|
+
return {
|
|
519
|
+
count: this.count,
|
|
520
|
+
sum: this.sum,
|
|
521
|
+
sumSquares: this.sumSquares,
|
|
522
|
+
prior: { a: this.priorA, b: this.priorB }
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/hypothesis/index.ts
|
|
528
|
+
var HypothesisManager = class {
|
|
529
|
+
entries = /* @__PURE__ */ new Map();
|
|
530
|
+
defaultWeight;
|
|
531
|
+
defaultPriorHigh;
|
|
532
|
+
defaultPriorLow;
|
|
533
|
+
configured = /* @__PURE__ */ new Map();
|
|
534
|
+
constructor(options = {}) {
|
|
535
|
+
this.defaultWeight = options.defaultWeight ?? 1;
|
|
536
|
+
this.defaultPriorHigh = options.defaultPriorHigh ?? { a: 2, b: 1 };
|
|
537
|
+
this.defaultPriorLow = options.defaultPriorLow ?? { a: 1, b: 2 };
|
|
538
|
+
invariant(
|
|
539
|
+
Number.isFinite(this.defaultWeight) && this.defaultWeight >= 0,
|
|
540
|
+
"INVALID_CONFIG",
|
|
541
|
+
`defaultWeight must be a non-negative finite number, got ${this.defaultWeight}`
|
|
542
|
+
);
|
|
543
|
+
for (const config of options.dimensions ?? []) {
|
|
544
|
+
this.configured.set(config.dimension, config);
|
|
545
|
+
this.ensure(config.dimension);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
ensure(dimension) {
|
|
549
|
+
const existing = this.entries.get(dimension);
|
|
550
|
+
if (existing) {
|
|
551
|
+
return existing;
|
|
552
|
+
}
|
|
553
|
+
const config = this.configured.get(dimension);
|
|
554
|
+
const entry = {
|
|
555
|
+
high: new BetaModel({ prior: config?.priorHigh ?? this.defaultPriorHigh }),
|
|
556
|
+
low: new BetaModel({ prior: config?.priorLow ?? this.defaultPriorLow }),
|
|
557
|
+
weight: config?.weight ?? this.defaultWeight
|
|
558
|
+
};
|
|
559
|
+
invariant(
|
|
560
|
+
Number.isFinite(entry.weight) && entry.weight >= 0,
|
|
561
|
+
"INVALID_CONFIG",
|
|
562
|
+
`weight for "${dimension}" must be a non-negative finite number, got ${entry.weight}`
|
|
563
|
+
);
|
|
564
|
+
this.entries.set(dimension, entry);
|
|
565
|
+
return entry;
|
|
566
|
+
}
|
|
567
|
+
/** Fold one labeled output into the high or low model of each scored dimension. */
|
|
568
|
+
observe(observation) {
|
|
569
|
+
invariant(
|
|
570
|
+
observation.label === "high" || observation.label === "low",
|
|
571
|
+
"INVALID_OBSERVATION",
|
|
572
|
+
`observation label must be "high" or "low", got ${String(observation.label)}`
|
|
573
|
+
);
|
|
574
|
+
for (const score of observation.scores) {
|
|
575
|
+
const entry = this.ensure(score.dimension);
|
|
576
|
+
const model = observation.label === "high" ? entry.high : entry.low;
|
|
577
|
+
model.observe(score.value);
|
|
578
|
+
}
|
|
579
|
+
return this;
|
|
580
|
+
}
|
|
581
|
+
/** Fold a batch of labeled observations. */
|
|
582
|
+
fit(observations) {
|
|
583
|
+
for (const observation of observations) {
|
|
584
|
+
this.observe(observation);
|
|
585
|
+
}
|
|
586
|
+
return this;
|
|
587
|
+
}
|
|
588
|
+
/** The dimension names this manager currently tracks. */
|
|
589
|
+
get dimensions() {
|
|
590
|
+
return [...this.entries.keys()];
|
|
591
|
+
}
|
|
592
|
+
/** Snapshot the current calibrated models as the dimension models the Bayes Factor consumes. */
|
|
593
|
+
models() {
|
|
594
|
+
const out = [];
|
|
595
|
+
for (const [dimension, entry] of this.entries) {
|
|
596
|
+
out.push({
|
|
597
|
+
dimension,
|
|
598
|
+
high: entry.high.params(),
|
|
599
|
+
low: entry.low.params(),
|
|
600
|
+
weight: entry.weight
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
/** Number of labeled observations folded into a dimension under one hypothesis. */
|
|
606
|
+
observationCount(dimension, kind) {
|
|
607
|
+
const entry = this.entries.get(dimension);
|
|
608
|
+
if (!entry) {
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
return (kind === "high" ? entry.high : entry.low).observations;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/policy/index.ts
|
|
616
|
+
function posteriorHighQuality(logBayesFactor, priorHighQuality) {
|
|
617
|
+
invariant(
|
|
618
|
+
priorHighQuality > 0 && priorHighQuality < 1,
|
|
619
|
+
"INVALID_CONFIG",
|
|
620
|
+
`priorHighQuality must be in (0, 1), got ${priorHighQuality}`
|
|
621
|
+
);
|
|
622
|
+
invariant(
|
|
623
|
+
Number.isFinite(logBayesFactor),
|
|
624
|
+
"NUMERIC",
|
|
625
|
+
`logBayesFactor must be finite, got ${logBayesFactor}`
|
|
626
|
+
);
|
|
627
|
+
const logPosteriorOdds = logBayesFactor + Math.log(priorHighQuality / (1 - priorHighQuality));
|
|
628
|
+
if (logPosteriorOdds >= 0) {
|
|
629
|
+
return 1 / (1 + Math.exp(-logPosteriorOdds));
|
|
630
|
+
}
|
|
631
|
+
const odds = Math.exp(logPosteriorOdds);
|
|
632
|
+
return odds / (1 + odds);
|
|
633
|
+
}
|
|
634
|
+
function decide(logBayesFactor, policy) {
|
|
635
|
+
if (policy.kind === "bayes-factor") {
|
|
636
|
+
invariant(
|
|
637
|
+
policy.passAbove >= 1,
|
|
638
|
+
"INVALID_CONFIG",
|
|
639
|
+
`passAbove must be >= 1, got ${policy.passAbove}`
|
|
640
|
+
);
|
|
641
|
+
invariant(
|
|
642
|
+
policy.failBelow > 0 && policy.failBelow <= 1,
|
|
643
|
+
"INVALID_CONFIG",
|
|
644
|
+
`failBelow must be in (0, 1], got ${policy.failBelow}`
|
|
645
|
+
);
|
|
646
|
+
invariant(
|
|
647
|
+
policy.passAbove > policy.failBelow,
|
|
648
|
+
"INVALID_CONFIG",
|
|
649
|
+
"passAbove must be greater than failBelow"
|
|
650
|
+
);
|
|
651
|
+
const bayesFactor2 = Math.exp(logBayesFactor);
|
|
652
|
+
const action2 = bayesFactor2 >= policy.passAbove ? "pass" : bayesFactor2 <= policy.failBelow ? "fail" : "escalate";
|
|
653
|
+
return { action: action2, rationale: "bayes-factor" };
|
|
654
|
+
}
|
|
655
|
+
invariant(
|
|
656
|
+
policy.lossFalsePass >= 0 && policy.lossFalseFail >= 0 && policy.escalationCost >= 0,
|
|
657
|
+
"INVALID_CONFIG",
|
|
658
|
+
"decision-theoretic losses must be non-negative"
|
|
659
|
+
);
|
|
660
|
+
invariant(
|
|
661
|
+
Number.isFinite(policy.lossFalsePass) && Number.isFinite(policy.lossFalseFail) && Number.isFinite(policy.escalationCost),
|
|
662
|
+
"INVALID_CONFIG",
|
|
663
|
+
"decision-theoretic losses must be finite"
|
|
664
|
+
);
|
|
665
|
+
const posterior = posteriorHighQuality(logBayesFactor, policy.priorHighQuality);
|
|
666
|
+
const expectedLoss = {
|
|
667
|
+
pass: (1 - posterior) * policy.lossFalsePass,
|
|
668
|
+
fail: posterior * policy.lossFalseFail,
|
|
669
|
+
escalate: policy.escalationCost
|
|
670
|
+
};
|
|
671
|
+
let action = "pass";
|
|
672
|
+
let best = expectedLoss.pass;
|
|
673
|
+
if (expectedLoss.fail < best) {
|
|
674
|
+
action = "fail";
|
|
675
|
+
best = expectedLoss.fail;
|
|
676
|
+
}
|
|
677
|
+
if (expectedLoss.escalate < best) {
|
|
678
|
+
action = "escalate";
|
|
679
|
+
}
|
|
680
|
+
return { action, rationale: "expected-loss", posteriorHighQuality: posterior, expectedLoss };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/gate/index.ts
|
|
684
|
+
function toDecision(factor, policy, guards) {
|
|
685
|
+
const base = {
|
|
686
|
+
action: policy.action,
|
|
687
|
+
bayesFactor: factor.bayesFactor,
|
|
688
|
+
logBayesFactor: factor.logBayesFactor,
|
|
689
|
+
strength: factor.strength,
|
|
690
|
+
rationale: policy.rationale,
|
|
691
|
+
contributions: factor.contributions,
|
|
692
|
+
...policy.posteriorHighQuality !== void 0 ? { posteriorHighQuality: policy.posteriorHighQuality } : {},
|
|
693
|
+
...policy.expectedLoss !== void 0 ? { expectedLoss: policy.expectedLoss } : {}
|
|
694
|
+
};
|
|
695
|
+
const assumptions = guards?.assumptions;
|
|
696
|
+
if (assumptions === void 0) {
|
|
697
|
+
return base;
|
|
698
|
+
}
|
|
699
|
+
const goodnessOfFitViolated = guards?.requireGoodnessOfFit === true && !assumptions.goodnessOfFitAdequate;
|
|
700
|
+
const independenceViolated = guards?.requireIndependence === true && !assumptions.independenceAssumptionSafe;
|
|
701
|
+
if (goodnessOfFitViolated || independenceViolated) {
|
|
702
|
+
return { ...base, action: "escalate", rationale: "assumption-violated", assumptions };
|
|
703
|
+
}
|
|
704
|
+
return { ...base, assumptions };
|
|
705
|
+
}
|
|
706
|
+
function evaluate(scores, models, policy, guards) {
|
|
707
|
+
const factor = bayesFactor(scores, models);
|
|
708
|
+
const decision = decide(factor.logBayesFactor, policy);
|
|
709
|
+
return toDecision(factor, decision, guards);
|
|
710
|
+
}
|
|
711
|
+
var OutputGate = class {
|
|
712
|
+
models;
|
|
713
|
+
policy;
|
|
714
|
+
guards;
|
|
715
|
+
constructor(options) {
|
|
716
|
+
invariant(
|
|
717
|
+
options.models.length > 0,
|
|
718
|
+
"INVALID_CONFIG",
|
|
719
|
+
"a gate needs at least one dimension model"
|
|
720
|
+
);
|
|
721
|
+
this.models = [...options.models];
|
|
722
|
+
this.policy = options.policy;
|
|
723
|
+
this.guards = options.guards;
|
|
724
|
+
}
|
|
725
|
+
/** Evaluate one output's scores. */
|
|
726
|
+
evaluate(scores) {
|
|
727
|
+
return evaluate(scores, this.models, this.policy, this.guards);
|
|
728
|
+
}
|
|
729
|
+
/** A copy of the gate's dimension models. */
|
|
730
|
+
dimensionModels() {
|
|
731
|
+
return [...this.models];
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
var OutputGateMonitor = class {
|
|
735
|
+
manager;
|
|
736
|
+
policy;
|
|
737
|
+
chain = new AuditChain();
|
|
738
|
+
guards;
|
|
739
|
+
assumptions;
|
|
740
|
+
constructor(options) {
|
|
741
|
+
this.manager = options.manager ?? new HypothesisManager();
|
|
742
|
+
this.policy = options.policy;
|
|
743
|
+
this.guards = options.guards;
|
|
744
|
+
}
|
|
745
|
+
/** Fold one labeled observation into the calibration. Does not recompute the assumption report. */
|
|
746
|
+
observe(observation) {
|
|
747
|
+
this.manager.observe(observation);
|
|
748
|
+
return this;
|
|
749
|
+
}
|
|
750
|
+
/** Fold a batch of labeled observations, and reassess the modeling assumptions when guarded. */
|
|
751
|
+
fit(observations) {
|
|
752
|
+
const list = [...observations];
|
|
753
|
+
for (const observation of list) {
|
|
754
|
+
this.manager.observe(observation);
|
|
755
|
+
}
|
|
756
|
+
if (this.guards !== void 0 && (this.guards.requireGoodnessOfFit === true || this.guards.requireIndependence === true)) {
|
|
757
|
+
this.assumptions = assessAssumptions(list, this.manager.models(), this.guards);
|
|
758
|
+
}
|
|
759
|
+
return this;
|
|
760
|
+
}
|
|
761
|
+
/** Evaluate without recording. */
|
|
762
|
+
evaluate(scores) {
|
|
763
|
+
return evaluate(scores, this.manager.models(), this.policy, this.guardsForEvaluate());
|
|
764
|
+
}
|
|
765
|
+
guardsForEvaluate() {
|
|
766
|
+
if (this.guards === void 0) {
|
|
767
|
+
return void 0;
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
...this.assumptions !== void 0 ? { assumptions: this.assumptions } : {},
|
|
771
|
+
...this.guards.requireGoodnessOfFit !== void 0 ? { requireGoodnessOfFit: this.guards.requireGoodnessOfFit } : {},
|
|
772
|
+
...this.guards.requireIndependence !== void 0 ? { requireIndependence: this.guards.requireIndependence } : {}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/** Evaluate and seal the decision into the audit chain. */
|
|
776
|
+
async record(scores, meta = {}) {
|
|
777
|
+
const decision = this.evaluate(scores);
|
|
778
|
+
const entry = await this.chain.append(decision, meta);
|
|
779
|
+
return { decision, entry };
|
|
780
|
+
}
|
|
781
|
+
/** A copy of the audit chain. */
|
|
782
|
+
auditTrail() {
|
|
783
|
+
return this.chain.toArray();
|
|
784
|
+
}
|
|
785
|
+
/** Number of sealed decisions. */
|
|
786
|
+
get auditLength() {
|
|
787
|
+
return this.chain.length;
|
|
788
|
+
}
|
|
789
|
+
/** The most recent assumption report, present after a guarded {@link OutputGateMonitor.fit}. */
|
|
790
|
+
get assumptionReport() {
|
|
791
|
+
return this.assumptions;
|
|
792
|
+
}
|
|
793
|
+
/** The current calibrated dimension models. */
|
|
794
|
+
models() {
|
|
795
|
+
return this.manager.models();
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
export { OutputGate, OutputGateMonitor, evaluate };
|