@rulvar/evals 1.18.0 → 1.20.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 (2) hide show
  1. package/dist/index.js +52 -12
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -20,27 +20,67 @@ import { createHash } from "node:crypto";
20
20
  * invocation and is never persisted.
21
21
  *
22
22
  * Accounting is integer micro-USD and conservative at the
23
- * representation boundary (v1.17.0 review P1-4): the cap converts DOWN
24
- * (floor), every debit converts UP (ceil), and a cap below one
25
- * micro-USD is rejected outright, so for any admitted sequence the sum
26
- * of the ORIGINAL ceilings can never exceed maxTotalUsd and no
27
- * positive ceiling ever debits zero. Amounts that are integer
23
+ * representation boundary (v1.17.0 review P1-4, hardened by the
24
+ * v1.18.0 review P1-4 and the v1.19.0 review P1-3): the cap converts
25
+ * DOWN (floor), every debit converts UP (ceil), only ULP-scale
26
+ * representation noise may snap to the neighboring integer, amounts at
27
+ * or above 2^49 micro-USD (about $563M, where the noise window would
28
+ * reach half a micro and the nearest integer would stop being unique)
29
+ * are rejected, and a cap below one micro-USD is rejected outright.
30
+ *
31
+ * The exact input interpretation: every USD amount is a JavaScript
32
+ * double, and the envelope interprets a double within SNAP_ULPS ULPs
33
+ * of an integer micro value AS that integer; anything farther rounds
34
+ * in the conservative direction. Under that interpretation the sum of
35
+ * admitted ceilings can never exceed maxTotalUsd, and no positive
36
+ * ceiling ever debits zero. The honest raw-double bound: each admitted
37
+ * amount's double may sit below its interpreted integer by at most the
38
+ * noise window (4 ULPs, always under half a micro in-domain), so the
39
+ * sum of raw doubles can exceed the cap by at most half a micro-USD
40
+ * per admitted amount; distinguishing finer than that is impossible in
41
+ * double precision at the top of the domain. Amounts that are integer
28
42
  * micro-USD up to float noise stay exact, so 0.1 + 0.2 against a 0.3
29
43
  * envelope is a fit, not a float rejection.
30
44
  */
31
45
  const MICRO = 1e6;
32
46
  /**
33
- * Relative tolerance for float noise around an integer micro-USD
34
- * amount: 0.3 * 1e6 is 299999.99999999994 and MUST count as 300000,
35
- * while a genuinely sub-micro 0.4 must not.
47
+ * Directed-rounding snap window, in ULPs of the product `usd * 1e6`:
48
+ * only genuine IEEE-754 representation noise may snap to the neighboring
49
+ * integer (0.3 * 1e6 is 299999.99999999994, about 6e-11 away, and MUST
50
+ * count as 300000), while a genuinely sub-micro fraction like 0.4 must
51
+ * round in the conservative direction. The window scales with the ULP,
52
+ * never with a relative fraction of the amount: the v1.17.0 fix used a
53
+ * 1e-6 RELATIVE tolerance, which already reaches half a micro-USD at
54
+ * $0.50 and turned directed rounding into round-to-nearest, admitting
55
+ * aggregates above the cap (v1.18.0 review P1-4).
36
56
  */
37
- const MICRO_NOISE = 1e-6;
57
+ const SNAP_ULPS = 4;
38
58
  function microOf(usd, direction) {
39
59
  const raw = usd * MICRO;
40
60
  const nearest = Math.round(raw);
41
- if (Math.abs(raw - nearest) <= MICRO_NOISE * Math.max(1, Math.abs(nearest))) return nearest;
61
+ if (Math.abs(raw - nearest) <= SNAP_ULPS * Math.abs(raw) * Number.EPSILON) return nearest;
42
62
  return direction === "floor" ? Math.floor(raw) : Math.ceil(raw);
43
63
  }
64
+ /**
65
+ * The accounting domain is integer micro-USD strictly below 2^49
66
+ * (at most $562,949,953.421311). Two limits meet there. Outside the
67
+ * safe integer range the arithmetic silently degrades: a
68
+ * Number.MAX_VALUE cap overflowed to Infinity and admitted EVERYTHING
69
+ * with remainingUsd NaN (v1.18.0 review P1-4). Well before that, from
70
+ * 2^49 micro upward, the SNAP_ULPS window reaches half a micro-USD:
71
+ * the nearest integer stops being unique and the snap absorbs genuine
72
+ * sub-micro remainders, so a ceil debit could round DOWN and admit an
73
+ * aggregate whose raw doubles sum above the cap by more than the
74
+ * documented noise bound (v1.19.0 review P1-3). Below 2^49 the window
75
+ * stays strictly under half a micro, the snap target is unique, and
76
+ * only representation noise is absorbed. Out-of-domain amounts are
77
+ * rejected up front.
78
+ */
79
+ const MAX_MICRO_EXCLUSIVE = 2 ** 49;
80
+ function requireDomainMicro(micro, what, usd) {
81
+ if (!Number.isSafeInteger(micro) || micro >= MAX_MICRO_EXCLUSIVE) throw new ConfigError(`${what} ${String(usd)} USD is outside the micro-USD accounting domain (at most \$562949953.421311)`);
82
+ return micro;
83
+ }
44
84
  /** Thrown when authorizing a run's ceiling would exceed the envelope. */
45
85
  var SweepBudgetError = class extends Error {
46
86
  /** What was about to start, e.g. `eval target 'sweep-math'`. */
@@ -71,7 +111,7 @@ var SpendEnvelope = class {
71
111
  constructor(maxTotalUsd) {
72
112
  if (!Number.isFinite(maxTotalUsd) || maxTotalUsd <= 0) throw new ConfigError(`SpendEnvelope maxTotalUsd must be a positive finite number, got ${String(maxTotalUsd)}`);
73
113
  this.maxTotalUsd = maxTotalUsd;
74
- this.maxMicroUsd = microOf(maxTotalUsd, "floor");
114
+ this.maxMicroUsd = requireDomainMicro(microOf(maxTotalUsd, "floor"), "SpendEnvelope maxTotalUsd", maxTotalUsd);
75
115
  if (this.maxMicroUsd < 1) throw new ConfigError(`SpendEnvelope maxTotalUsd ${String(maxTotalUsd)} is below the 1 micro-USD accounting granularity (\$0.000001); such an envelope could never admit a run`);
76
116
  }
77
117
  /** Total authorized so far (debit-only; never decreases). */
@@ -89,7 +129,7 @@ var SpendEnvelope = class {
89
129
  */
90
130
  authorize(ceilingUsd, runLabel) {
91
131
  if (ceilingUsd === void 0 || !Number.isFinite(ceilingUsd) || ceilingUsd <= 0) throw new ConfigError(`the spend envelope requires an explicit positive per-run ceiling for ${runLabel}; got ${String(ceilingUsd)} (an unbounded run under an aggregate envelope would be unaccountable)`);
92
- const micro = Math.max(1, microOf(ceilingUsd, "ceil"));
132
+ const micro = requireDomainMicro(Math.max(1, microOf(ceilingUsd, "ceil")), `per-run ceiling for ${runLabel}`, ceilingUsd);
93
133
  if (this.authorizedMicroUsd + micro > this.maxMicroUsd) throw new SweepBudgetError(runLabel, ceilingUsd, this.authorizedUsd, this.maxTotalUsd);
94
134
  this.authorizedMicroUsd += micro;
95
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/evals",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Rulvar evals: eval cases, golden outputs, rubric and judge graders, matrix sweeps, canary fingerprint.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,8 +22,8 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.18.0",
26
- "@rulvar/testing": "1.18.0"
25
+ "@rulvar/core": "1.20.0",
26
+ "@rulvar/testing": "1.20.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.0",