@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +45 -0
  4. package/README.md +403 -0
  5. package/SECURITY.md +98 -0
  6. package/SPEC.md +467 -0
  7. package/dist/adapter/index.cjs +411 -0
  8. package/dist/adapter/index.d.cts +29 -0
  9. package/dist/adapter/index.d.ts +29 -0
  10. package/dist/adapter/index.js +404 -0
  11. package/dist/audit/index.cjs +82 -0
  12. package/dist/audit/index.d.cts +40 -0
  13. package/dist/audit/index.d.ts +40 -0
  14. package/dist/audit/index.js +77 -0
  15. package/dist/bayesfactor/index.cjs +152 -0
  16. package/dist/bayesfactor/index.d.cts +15 -0
  17. package/dist/bayesfactor/index.d.ts +15 -0
  18. package/dist/bayesfactor/index.js +149 -0
  19. package/dist/beta/index.cjs +180 -0
  20. package/dist/beta/index.d.cts +45 -0
  21. package/dist/beta/index.d.ts +45 -0
  22. package/dist/beta/index.js +178 -0
  23. package/dist/calibration/index.cjs +339 -0
  24. package/dist/calibration/index.d.cts +53 -0
  25. package/dist/calibration/index.d.ts +53 -0
  26. package/dist/calibration/index.js +333 -0
  27. package/dist/cli/index.cjs +968 -0
  28. package/dist/cli/index.d.cts +1 -0
  29. package/dist/cli/index.d.ts +1 -0
  30. package/dist/cli/index.js +966 -0
  31. package/dist/dimensions/index.cjs +106 -0
  32. package/dist/dimensions/index.d.cts +33 -0
  33. package/dist/dimensions/index.d.ts +33 -0
  34. package/dist/dimensions/index.js +104 -0
  35. package/dist/edge/index.cjs +1141 -0
  36. package/dist/edge/index.d.cts +12 -0
  37. package/dist/edge/index.d.ts +12 -0
  38. package/dist/edge/index.js +1109 -0
  39. package/dist/gate/index.cjs +803 -0
  40. package/dist/gate/index.d.cts +77 -0
  41. package/dist/gate/index.d.ts +77 -0
  42. package/dist/gate/index.js +799 -0
  43. package/dist/hypothesis/index.cjs +268 -0
  44. package/dist/hypothesis/index.d.cts +38 -0
  45. package/dist/hypothesis/index.d.ts +38 -0
  46. package/dist/hypothesis/index.js +266 -0
  47. package/dist/index.cjs +1141 -0
  48. package/dist/index.d.cts +29 -0
  49. package/dist/index.d.ts +29 -0
  50. package/dist/index.js +1109 -0
  51. package/dist/likelihood/index.cjs +137 -0
  52. package/dist/likelihood/index.d.cts +23 -0
  53. package/dist/likelihood/index.d.ts +23 -0
  54. package/dist/likelihood/index.js +132 -0
  55. package/dist/node/index.cjs +1282 -0
  56. package/dist/node/index.d.cts +24 -0
  57. package/dist/node/index.d.ts +24 -0
  58. package/dist/node/index.js +1246 -0
  59. package/dist/policy/index.cjs +88 -0
  60. package/dist/policy/index.d.cts +11 -0
  61. package/dist/policy/index.d.ts +11 -0
  62. package/dist/policy/index.js +85 -0
  63. package/dist/types-bMjn1j4e.d.cts +159 -0
  64. package/dist/types-bMjn1j4e.d.ts +159 -0
  65. package/package.json +142 -0
@@ -0,0 +1,968 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+
7
+ // src/canonical.ts
8
+ function canonicalize(value) {
9
+ return serialize(value);
10
+ }
11
+ function serialize(value) {
12
+ if (value === null) {
13
+ return "null";
14
+ }
15
+ const kind = typeof value;
16
+ if (kind === "number") {
17
+ return Number.isFinite(value) ? String(value) : "null";
18
+ }
19
+ if (kind === "boolean") {
20
+ return value ? "true" : "false";
21
+ }
22
+ if (kind === "string") {
23
+ return JSON.stringify(value);
24
+ }
25
+ if (Array.isArray(value)) {
26
+ return `[${value.map((item) => serialize(item)).join(",")}]`;
27
+ }
28
+ if (kind === "object") {
29
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
30
+ const body = entries.map(([key, v]) => `${JSON.stringify(key)}:${serialize(v)}`).join(",");
31
+ return `{${body}}`;
32
+ }
33
+ return "null";
34
+ }
35
+
36
+ // src/audit/index.ts
37
+ var GENESIS_HASH = "0".repeat(64);
38
+ async function sha256Hex(input) {
39
+ const bytes = new TextEncoder().encode(input);
40
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
41
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
42
+ }
43
+ function recordOf(index, payload, previousHash, timestamp) {
44
+ return timestamp !== void 0 ? { index, timestamp, payload, previousHash } : { index, payload, previousHash };
45
+ }
46
+ async function verifyChain(entries) {
47
+ for (let i = 0; i < entries.length; i++) {
48
+ const entry = entries[i];
49
+ const expectedPrevious = i === 0 ? GENESIS_HASH : entries[i - 1].hash;
50
+ if (entry.index !== i || entry.previousHash !== expectedPrevious) {
51
+ return { valid: false, brokenAt: i };
52
+ }
53
+ const record = recordOf(entry.index, entry.payload, entry.previousHash, entry.timestamp);
54
+ const recomputed = await sha256Hex(canonicalize(record));
55
+ if (recomputed !== entry.hash) {
56
+ return { valid: false, brokenAt: i };
57
+ }
58
+ }
59
+ return { valid: true };
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
+
379
+ // src/beta/index.ts
380
+ var VARIANCE_FLOOR = 1e-9;
381
+ var CONCENTRATION_FLOOR = 1e-3;
382
+ var BetaModel = class _BetaModel {
383
+ priorA;
384
+ priorB;
385
+ count = 0;
386
+ sum = 0;
387
+ sumSquares = 0;
388
+ constructor(options = {}) {
389
+ const prior = options.prior ?? { a: 1, b: 1 };
390
+ invariant(
391
+ prior.a > 0 && prior.b > 0 && Number.isFinite(prior.a) && Number.isFinite(prior.b),
392
+ "INVALID_CONFIG",
393
+ `prior Beta parameters must be positive and finite, got a=${prior.a}, b=${prior.b}`
394
+ );
395
+ this.priorA = prior.a;
396
+ this.priorB = prior.b;
397
+ }
398
+ /** Build a model from labeled scores in one pass. */
399
+ static fromSamples(samples, options = {}) {
400
+ const model = new _BetaModel(options);
401
+ for (const s of samples) {
402
+ model.observe(s);
403
+ }
404
+ return model;
405
+ }
406
+ /** Restore a model from a snapshot. */
407
+ static fromSnapshot(snapshot) {
408
+ const model = new _BetaModel({ prior: snapshot.prior });
409
+ invariant(
410
+ snapshot.count >= 0 && Number.isFinite(snapshot.sum) && Number.isFinite(snapshot.sumSquares),
411
+ "INVALID_SNAPSHOT",
412
+ "snapshot fields must be finite and non-negative"
413
+ );
414
+ model.count = snapshot.count;
415
+ model.sum = snapshot.sum;
416
+ model.sumSquares = snapshot.sumSquares;
417
+ return model;
418
+ }
419
+ /** Number of real observations folded into this model so far. */
420
+ get observations() {
421
+ return this.count;
422
+ }
423
+ /** Fold a single score in [0, 1] into the model, updating its calibration online. */
424
+ observe(score) {
425
+ invariant(
426
+ Number.isFinite(score) && score >= 0 && score <= 1,
427
+ "INVALID_SCORE",
428
+ `score must be a finite number in [0, 1], got ${score}`
429
+ );
430
+ this.count += 1;
431
+ this.sum += score;
432
+ this.sumSquares += score * score;
433
+ return this;
434
+ }
435
+ /**
436
+ * The fitted Beta parameters. The prior is mixed in as `priorA + priorB` pseudo-observations with
437
+ * the prior's own mean and variance, then a and b come from method of moments on the blend, so a
438
+ * cold model returns its prior and a warm model is data-driven.
439
+ */
440
+ params() {
441
+ const priorStrength = this.priorA + this.priorB;
442
+ const priorMean = this.priorA / priorStrength;
443
+ const priorVariance = priorMean * (1 - priorMean) / (priorStrength + 1);
444
+ const totalCount = this.count + priorStrength;
445
+ const totalSum = this.sum + priorStrength * priorMean;
446
+ const totalSumSquares = this.sumSquares + priorStrength * (priorVariance + priorMean * priorMean);
447
+ const mean = clamp(totalSum / totalCount, VARIANCE_FLOOR, 1 - VARIANCE_FLOOR);
448
+ const rawVariance = totalSumSquares / totalCount - mean * mean;
449
+ const maxVariance = mean * (1 - mean);
450
+ const variance = clamp(rawVariance, VARIANCE_FLOOR, maxVariance * (1 - VARIANCE_FLOOR));
451
+ const concentration = Math.max(mean * (1 - mean) / variance - 1, CONCENTRATION_FLOOR);
452
+ const a = Math.max(mean * concentration, CONCENTRATION_FLOOR);
453
+ const b = Math.max((1 - mean) * concentration, CONCENTRATION_FLOOR);
454
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
455
+ throw new BayesOutputGateError("NUMERIC", "Beta fit produced non-finite parameters");
456
+ }
457
+ return { a, b };
458
+ }
459
+ /** Mean of the fitted Beta. */
460
+ mean() {
461
+ const { a, b } = this.params();
462
+ return a / (a + b);
463
+ }
464
+ /** Log-density of an observed score under the fitted Beta. */
465
+ logDensity(score) {
466
+ const { a, b } = this.params();
467
+ return betaLogDensity(score, a, b);
468
+ }
469
+ /** A serializable snapshot of the current state. */
470
+ snapshot() {
471
+ return {
472
+ count: this.count,
473
+ sum: this.sum,
474
+ sumSquares: this.sumSquares,
475
+ prior: { a: this.priorA, b: this.priorB }
476
+ };
477
+ }
478
+ };
479
+
480
+ // src/hypothesis/index.ts
481
+ var HypothesisManager = class {
482
+ entries = /* @__PURE__ */ new Map();
483
+ defaultWeight;
484
+ defaultPriorHigh;
485
+ defaultPriorLow;
486
+ configured = /* @__PURE__ */ new Map();
487
+ constructor(options = {}) {
488
+ this.defaultWeight = options.defaultWeight ?? 1;
489
+ this.defaultPriorHigh = options.defaultPriorHigh ?? { a: 2, b: 1 };
490
+ this.defaultPriorLow = options.defaultPriorLow ?? { a: 1, b: 2 };
491
+ invariant(
492
+ Number.isFinite(this.defaultWeight) && this.defaultWeight >= 0,
493
+ "INVALID_CONFIG",
494
+ `defaultWeight must be a non-negative finite number, got ${this.defaultWeight}`
495
+ );
496
+ for (const config of options.dimensions ?? []) {
497
+ this.configured.set(config.dimension, config);
498
+ this.ensure(config.dimension);
499
+ }
500
+ }
501
+ ensure(dimension) {
502
+ const existing = this.entries.get(dimension);
503
+ if (existing) {
504
+ return existing;
505
+ }
506
+ const config = this.configured.get(dimension);
507
+ const entry = {
508
+ high: new BetaModel({ prior: config?.priorHigh ?? this.defaultPriorHigh }),
509
+ low: new BetaModel({ prior: config?.priorLow ?? this.defaultPriorLow }),
510
+ weight: config?.weight ?? this.defaultWeight
511
+ };
512
+ invariant(
513
+ Number.isFinite(entry.weight) && entry.weight >= 0,
514
+ "INVALID_CONFIG",
515
+ `weight for "${dimension}" must be a non-negative finite number, got ${entry.weight}`
516
+ );
517
+ this.entries.set(dimension, entry);
518
+ return entry;
519
+ }
520
+ /** Fold one labeled output into the high or low model of each scored dimension. */
521
+ observe(observation) {
522
+ invariant(
523
+ observation.label === "high" || observation.label === "low",
524
+ "INVALID_OBSERVATION",
525
+ `observation label must be "high" or "low", got ${String(observation.label)}`
526
+ );
527
+ for (const score of observation.scores) {
528
+ const entry = this.ensure(score.dimension);
529
+ const model = observation.label === "high" ? entry.high : entry.low;
530
+ model.observe(score.value);
531
+ }
532
+ return this;
533
+ }
534
+ /** Fold a batch of labeled observations. */
535
+ fit(observations) {
536
+ for (const observation of observations) {
537
+ this.observe(observation);
538
+ }
539
+ return this;
540
+ }
541
+ /** The dimension names this manager currently tracks. */
542
+ get dimensions() {
543
+ return [...this.entries.keys()];
544
+ }
545
+ /** Snapshot the current calibrated models as the dimension models the Bayes Factor consumes. */
546
+ models() {
547
+ const out = [];
548
+ for (const [dimension, entry] of this.entries) {
549
+ out.push({
550
+ dimension,
551
+ high: entry.high.params(),
552
+ low: entry.low.params(),
553
+ weight: entry.weight
554
+ });
555
+ }
556
+ return out;
557
+ }
558
+ /** Number of labeled observations folded into a dimension under one hypothesis. */
559
+ observationCount(dimension, kind) {
560
+ const entry = this.entries.get(dimension);
561
+ if (!entry) {
562
+ return 0;
563
+ }
564
+ return (kind === "high" ? entry.high : entry.low).observations;
565
+ }
566
+ };
567
+ function decide(logBayesFactor, policy) {
568
+ {
569
+ invariant(
570
+ policy.passAbove >= 1,
571
+ "INVALID_CONFIG",
572
+ `passAbove must be >= 1, got ${policy.passAbove}`
573
+ );
574
+ invariant(
575
+ policy.failBelow > 0 && policy.failBelow <= 1,
576
+ "INVALID_CONFIG",
577
+ `failBelow must be in (0, 1], got ${policy.failBelow}`
578
+ );
579
+ invariant(
580
+ policy.passAbove > policy.failBelow,
581
+ "INVALID_CONFIG",
582
+ "passAbove must be greater than failBelow"
583
+ );
584
+ const bayesFactor2 = Math.exp(logBayesFactor);
585
+ const action2 = bayesFactor2 >= policy.passAbove ? "pass" : bayesFactor2 <= policy.failBelow ? "fail" : "escalate";
586
+ return { action: action2, rationale: "bayes-factor" };
587
+ }
588
+ }
589
+
590
+ // src/gate/index.ts
591
+ function toDecision(factor, policy, guards) {
592
+ const base = {
593
+ action: policy.action,
594
+ bayesFactor: factor.bayesFactor,
595
+ logBayesFactor: factor.logBayesFactor,
596
+ strength: factor.strength,
597
+ rationale: policy.rationale,
598
+ contributions: factor.contributions,
599
+ ...policy.posteriorHighQuality !== void 0 ? { posteriorHighQuality: policy.posteriorHighQuality } : {},
600
+ ...policy.expectedLoss !== void 0 ? { expectedLoss: policy.expectedLoss } : {}
601
+ };
602
+ {
603
+ return base;
604
+ }
605
+ }
606
+ function evaluate(scores, models, policy, guards) {
607
+ const factor = bayesFactor(scores, models);
608
+ const decision = decide(factor.logBayesFactor, policy);
609
+ return toDecision(factor, decision);
610
+ }
611
+ function readTextFile(path) {
612
+ try {
613
+ return fs.readFileSync(path, "utf8");
614
+ } catch (error) {
615
+ throw new BayesOutputGateError(
616
+ "INVALID_CONFIG",
617
+ `cannot read file ${path}: ${error.message}`
618
+ );
619
+ }
620
+ }
621
+ function readJsonFile(path) {
622
+ const text = readTextFile(path);
623
+ try {
624
+ return JSON.parse(text);
625
+ } catch {
626
+ throw new BayesOutputGateError("INVALID_CONFIG", `invalid JSON in ${path}`);
627
+ }
628
+ }
629
+ function parseScoreRow(value, where) {
630
+ invariant(
631
+ typeof value === "object" && value !== null && !Array.isArray(value),
632
+ "INVALID_SCORE",
633
+ `${where} must be an object`
634
+ );
635
+ const row = value;
636
+ const dimension = row["dimension"];
637
+ const score = row["value"];
638
+ invariant(
639
+ typeof dimension === "string" && dimension.length > 0,
640
+ "INVALID_SCORE",
641
+ `${where} dimension must be a non-empty string`
642
+ );
643
+ invariant(
644
+ typeof score === "number" && Number.isFinite(score) && score >= 0 && score <= 1,
645
+ "INVALID_SCORE",
646
+ `${where} value must be a finite number in [0, 1]`
647
+ );
648
+ return { dimension, value: score };
649
+ }
650
+ function parseScoreVector(row, where) {
651
+ if (Array.isArray(row)) {
652
+ return row.map((score, j) => parseScoreRow(score, `${where} score ${j}`));
653
+ }
654
+ invariant(
655
+ typeof row === "object" && row !== null,
656
+ "INVALID_SCORE",
657
+ `${where} must be an array or an object map`
658
+ );
659
+ return Object.entries(row).map(([dimension, value]) => {
660
+ invariant(
661
+ typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1,
662
+ "INVALID_SCORE",
663
+ `${where} value for "${dimension}" must be a finite number in [0, 1]`
664
+ );
665
+ return { dimension, value };
666
+ });
667
+ }
668
+ function loadLabeledObservations(path) {
669
+ const data = readJsonFile(path);
670
+ invariant(
671
+ Array.isArray(data),
672
+ "INVALID_OBSERVATION",
673
+ `${path} must contain a JSON array of labeled observations`
674
+ );
675
+ return data.map((row, i) => {
676
+ invariant(
677
+ typeof row === "object" && row !== null,
678
+ "INVALID_OBSERVATION",
679
+ `observation ${i} must be an object`
680
+ );
681
+ const record = row;
682
+ const label = record["label"];
683
+ invariant(
684
+ label === "high" || label === "low",
685
+ "INVALID_OBSERVATION",
686
+ `observation ${i} label must be "high" or "low"`
687
+ );
688
+ invariant(
689
+ Array.isArray(record["scores"]),
690
+ "INVALID_SCORE",
691
+ `observation ${i} scores must be an array`
692
+ );
693
+ const scores = record["scores"].map(
694
+ (score, j) => parseScoreRow(score, `observation ${i} score ${j}`)
695
+ );
696
+ return { scores, label };
697
+ });
698
+ }
699
+ function parseCsvScores(text) {
700
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
701
+ invariant(lines.length >= 2, "INVALID_SCORE", "CSV needs a header row and at least one data row");
702
+ const header = lines[0].split(",").map((cell) => cell.trim());
703
+ invariant(
704
+ header.every((name) => name.length > 0),
705
+ "INVALID_SCORE",
706
+ "CSV header dimensions must be non-empty"
707
+ );
708
+ const out = [];
709
+ for (let i = 1; i < lines.length; i++) {
710
+ const cells = lines[i].split(",").map((cell) => cell.trim());
711
+ invariant(
712
+ cells.length === header.length,
713
+ "INVALID_SCORE",
714
+ `CSV row ${i} has ${cells.length} cells, expected ${header.length}`
715
+ );
716
+ const vector = header.map((dimension, k) => {
717
+ const value = Number(cells[k]);
718
+ invariant(
719
+ Number.isFinite(value) && value >= 0 && value <= 1,
720
+ "INVALID_SCORE",
721
+ `CSV row ${i} value for "${dimension}" must be a finite number in [0, 1]`
722
+ );
723
+ return { dimension, value };
724
+ });
725
+ out.push(vector);
726
+ }
727
+ return out;
728
+ }
729
+ function loadScores(path$1) {
730
+ if (path.extname(path$1).toLowerCase() === ".csv") {
731
+ return parseCsvScores(readTextFile(path$1));
732
+ }
733
+ const data = readJsonFile(path$1);
734
+ invariant(
735
+ Array.isArray(data),
736
+ "INVALID_SCORE",
737
+ `${path$1} must contain a JSON array of score vectors`
738
+ );
739
+ return data.map((row, i) => parseScoreVector(row, `entry ${i}`));
740
+ }
741
+
742
+ // src/cli/commands.ts
743
+ var VERSION = "1.0.0";
744
+ var HELP = `bayesoutputgate <command> [options]
745
+
746
+ Commands:
747
+ gate <history.json> <scores.json> fit from labeled history, decide each output
748
+ bayes-factor <models.json> <scores.json> compute the Bayes Factor for each output
749
+ calibrate <history.json> fit, then report goodness-of-fit and dependence
750
+ audit-verify <chain.json> verify a sealed decision audit chain
751
+
752
+ Options:
753
+ --pass-above <n> Bayes Factor pass threshold for gate (default 10)
754
+ --fail-below <n> Bayes Factor fail threshold for gate (default 0.1)
755
+ --json print machine-readable JSON
756
+ --help, -h show this help
757
+ --version, -v show the version
758
+
759
+ Exit codes: 0 ok, 2 usage or input error, 30 fail decision, 40 escalate decision, 20 broken chain`;
760
+ function parseArgs(argv) {
761
+ const flags = /* @__PURE__ */ new Map();
762
+ const positionals = [];
763
+ const valued = /* @__PURE__ */ new Set(["pass-above", "fail-below"]);
764
+ for (let i = 0; i < argv.length; i++) {
765
+ const token = argv[i];
766
+ if (token.startsWith("--")) {
767
+ const key = token.slice(2);
768
+ const next = argv[i + 1];
769
+ if (valued.has(key) && next !== void 0 && !next.startsWith("--")) {
770
+ flags.set(key, next);
771
+ i++;
772
+ } else {
773
+ flags.set(key, true);
774
+ }
775
+ } else if (token.startsWith("-")) {
776
+ flags.set(token.slice(1), true);
777
+ } else {
778
+ positionals.push(token);
779
+ }
780
+ }
781
+ return { command: positionals[0], positionals: positionals.slice(1), flags };
782
+ }
783
+ function numberFlag(flags, key, fallback) {
784
+ const raw = flags.get(key);
785
+ if (raw === void 0 || raw === true) {
786
+ return fallback;
787
+ }
788
+ const value = Number(raw);
789
+ if (!Number.isFinite(value)) {
790
+ throw new BayesOutputGateError("INVALID_CONFIG", `--${key} must be a number, got ${raw}`);
791
+ }
792
+ return value;
793
+ }
794
+ function worstAction(actions) {
795
+ if (actions.includes("fail")) return "fail";
796
+ if (actions.includes("escalate")) return "escalate";
797
+ return "pass";
798
+ }
799
+ function exitForAction(action) {
800
+ return action === "fail" ? 30 : action === "escalate" ? 40 : 0;
801
+ }
802
+ async function run(argv, io2) {
803
+ const { command, positionals, flags } = parseArgs(argv);
804
+ if (flags.has("version") || flags.has("v")) {
805
+ io2.write(VERSION);
806
+ return 0;
807
+ }
808
+ if (flags.has("help") || flags.has("h") || command === void 0) {
809
+ io2.write(HELP);
810
+ return 0;
811
+ }
812
+ const json = flags.has("json");
813
+ try {
814
+ switch (command) {
815
+ case "gate":
816
+ return gateCommand(positionals, flags, json, io2);
817
+ case "bayes-factor":
818
+ return bayesFactorCommand(positionals, json, io2);
819
+ case "calibrate":
820
+ return calibrateCommand(positionals, json, io2);
821
+ case "audit-verify":
822
+ return await auditVerifyCommand(positionals, json, io2);
823
+ default:
824
+ io2.error(`unknown command: ${command}`);
825
+ io2.write(HELP);
826
+ return 2;
827
+ }
828
+ } catch (error) {
829
+ if (error instanceof BayesOutputGateError) {
830
+ io2.error(`${error.code}: ${error.message}`);
831
+ } else {
832
+ io2.error(`error: ${error.message ?? String(error)}`);
833
+ }
834
+ return 2;
835
+ }
836
+ }
837
+ function requirePositionals(positionals, n, usage) {
838
+ if (positionals.length < n) {
839
+ throw new BayesOutputGateError("INVALID_CONFIG", `missing argument, usage: ${usage}`);
840
+ }
841
+ }
842
+ function gateCommand(positionals, flags, json, io2) {
843
+ requirePositionals(positionals, 2, "bayesoutputgate gate <history.json> <scores.json>");
844
+ const policy = {
845
+ passAbove: numberFlag(flags, "pass-above", 10),
846
+ failBelow: numberFlag(flags, "fail-below", 0.1)
847
+ };
848
+ const manager = new HypothesisManager();
849
+ manager.fit(loadLabeledObservations(positionals[0]));
850
+ const models = manager.models();
851
+ const vectors = loadScores(positionals[1]);
852
+ const decisions = vectors.map((vector) => evaluate(vector, models, policy));
853
+ if (json) {
854
+ io2.write(JSON.stringify(decisions, null, 2));
855
+ } else {
856
+ decisions.forEach((decision, i) => {
857
+ io2.write(
858
+ `output ${i}: ${decision.action.toUpperCase()} (Bayes Factor ${decision.bayesFactor.toFixed(3)}, ${decision.strength})`
859
+ );
860
+ });
861
+ }
862
+ return exitForAction(worstAction(decisions.map((decision) => decision.action)));
863
+ }
864
+ function bayesFactorCommand(positionals, json, io2) {
865
+ requirePositionals(positionals, 2, "bayesoutputgate bayes-factor <models.json> <scores.json>");
866
+ const models = readJsonFile(positionals[0]);
867
+ if (!Array.isArray(models)) {
868
+ throw new BayesOutputGateError(
869
+ "INVALID_CONFIG",
870
+ "models file must be a JSON array of dimension models"
871
+ );
872
+ }
873
+ const vectors = loadScores(positionals[1]);
874
+ const results = vectors.map((vector) => bayesFactor(vector, models));
875
+ if (json) {
876
+ io2.write(JSON.stringify(results, null, 2));
877
+ } else {
878
+ results.forEach((result, i) => {
879
+ io2.write(
880
+ `output ${i}: Bayes Factor ${result.bayesFactor.toFixed(3)} (${result.strength}, favors ${result.favored})`
881
+ );
882
+ });
883
+ }
884
+ return 0;
885
+ }
886
+ function calibrateCommand(positionals, json, io2) {
887
+ requirePositionals(positionals, 1, "bayesoutputgate calibrate <history.json>");
888
+ const observations = loadLabeledObservations(positionals[0]);
889
+ const manager = new HypothesisManager();
890
+ manager.fit(observations);
891
+ const models = manager.models();
892
+ const report = models.map((model) => {
893
+ const highScores = scoresFor(observations, model.dimension, "high");
894
+ const lowScores = scoresFor(observations, model.dimension, "low");
895
+ return {
896
+ dimension: model.dimension,
897
+ high: highScores.length >= 5 ? goodnessOfFit(highScores, model.high) : null,
898
+ low: lowScores.length >= 5 ? goodnessOfFit(lowScores, model.low) : null
899
+ };
900
+ });
901
+ const vectors = observations.map((observation) => observation.scores);
902
+ const dependence = vectors.length >= 2 ? dependenceDiagnostic(vectors) : null;
903
+ if (json) {
904
+ io2.write(JSON.stringify({ goodnessOfFit: report, dependence }, null, 2));
905
+ } else {
906
+ for (const row of report) {
907
+ const highTag = row.high ? row.high.adequate ? "adequate" : "INADEQUATE" : "insufficient";
908
+ const lowTag = row.low ? row.low.adequate ? "adequate" : "INADEQUATE" : "insufficient";
909
+ io2.write(`dimension ${row.dimension}: high fit ${highTag}, low fit ${lowTag}`);
910
+ }
911
+ if (dependence) {
912
+ io2.write(
913
+ `dimension independence: ${dependence.independenceAssumptionSafe ? "safe" : "AT RISK"} (max abs correlation ${dependence.maxAbsCorrelation.toFixed(3)})`
914
+ );
915
+ }
916
+ }
917
+ return 0;
918
+ }
919
+ async function auditVerifyCommand(positionals, json, io2) {
920
+ requirePositionals(positionals, 1, "bayesoutputgate audit-verify <chain.json>");
921
+ const data = readJsonFile(positionals[0]);
922
+ if (!Array.isArray(data)) {
923
+ throw new BayesOutputGateError(
924
+ "INVALID_SNAPSHOT",
925
+ "audit chain file must be a JSON array of entries"
926
+ );
927
+ }
928
+ const result = await verifyChain(data);
929
+ if (json) {
930
+ io2.write(JSON.stringify(result, null, 2));
931
+ } else {
932
+ io2.write(result.valid ? "audit chain valid" : `audit chain BROKEN at entry ${result.brokenAt}`);
933
+ }
934
+ return result.valid ? 0 : 20;
935
+ }
936
+ function scoresFor(observations, dimension, label) {
937
+ const out = [];
938
+ for (const observation of observations) {
939
+ if (observation.label !== label) {
940
+ continue;
941
+ }
942
+ for (const score of observation.scores) {
943
+ if (score.dimension === dimension) {
944
+ out.push(score.value);
945
+ }
946
+ }
947
+ }
948
+ return out;
949
+ }
950
+
951
+ // src/cli/index.ts
952
+ var io = {
953
+ write: (line) => {
954
+ process.stdout.write(`${line}
955
+ `);
956
+ },
957
+ error: (line) => {
958
+ process.stderr.write(`${line}
959
+ `);
960
+ }
961
+ };
962
+ run(process.argv.slice(2), io).then((code) => {
963
+ process.exitCode = code;
964
+ }).catch((error) => {
965
+ process.stderr.write(`${error.message ?? String(error)}
966
+ `);
967
+ process.exitCode = 2;
968
+ });