@rulvar/evals 1.18.0 → 1.19.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 +36 -14
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -20,27 +20,49 @@ 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
28
- * micro-USD up to float noise stay exact, so 0.1 + 0.2 against a 0.3
29
- * envelope is a fit, not a float rejection.
23
+ * representation boundary (v1.17.0 review P1-4, hardened by the v1.18.0
24
+ * review P1-4): the cap converts DOWN (floor), every debit converts UP
25
+ * (ceil), only ULP-scale representation noise may snap to the
26
+ * neighboring integer, amounts outside the safe integer micro-USD
27
+ * domain are rejected, and a cap below one micro-USD is rejected
28
+ * outright, so for any admitted sequence the sum of the ORIGINAL
29
+ * ceilings can never exceed maxTotalUsd and no positive ceiling ever
30
+ * debits zero. Amounts that are integer micro-USD up to float noise
31
+ * stay exact, so 0.1 + 0.2 against a 0.3 envelope is a fit, not a
32
+ * float rejection.
30
33
  */
31
34
  const MICRO = 1e6;
32
35
  /**
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.
36
+ * Directed-rounding snap window, in ULPs of the product `usd * 1e6`:
37
+ * only genuine IEEE-754 representation noise may snap to the neighboring
38
+ * integer (0.3 * 1e6 is 299999.99999999994, about 6e-11 away, and MUST
39
+ * count as 300000), while a genuinely sub-micro fraction like 0.4 must
40
+ * round in the conservative direction. The window scales with the ULP,
41
+ * never with a relative fraction of the amount: the v1.17.0 fix used a
42
+ * 1e-6 RELATIVE tolerance, which already reaches half a micro-USD at
43
+ * $0.50 and turned directed rounding into round-to-nearest, admitting
44
+ * aggregates above the cap (v1.18.0 review P1-4).
36
45
  */
37
- const MICRO_NOISE = 1e-6;
46
+ const SNAP_ULPS = 4;
38
47
  function microOf(usd, direction) {
39
48
  const raw = usd * MICRO;
40
49
  const nearest = Math.round(raw);
41
- if (Math.abs(raw - nearest) <= MICRO_NOISE * Math.max(1, Math.abs(nearest))) return nearest;
50
+ if (Math.abs(raw - nearest) <= SNAP_ULPS * Math.abs(raw) * Number.EPSILON) return nearest;
42
51
  return direction === "floor" ? Math.floor(raw) : Math.ceil(raw);
43
52
  }
53
+ /**
54
+ * The accounting domain is integer micro-USD within Number's safe
55
+ * integer range (up to about $9.007e9). Outside it (Number.MAX_VALUE
56
+ * caps overflow to Infinity, huge finite amounts lose integer
57
+ * precision) the envelope arithmetic silently degrades: Infinity plus
58
+ * anything compares false against Infinity and every authorization
59
+ * would be admitted with remainingUsd NaN (v1.18.0 review P1-4), so
60
+ * out-of-domain amounts are rejected up front.
61
+ */
62
+ function requireSafeMicro(micro, what, usd) {
63
+ if (!Number.isSafeInteger(micro)) throw new ConfigError(`${what} ${String(usd)} USD is outside the safe integer micro-USD accounting domain (at most \$9007199254.740991)`);
64
+ return micro;
65
+ }
44
66
  /** Thrown when authorizing a run's ceiling would exceed the envelope. */
45
67
  var SweepBudgetError = class extends Error {
46
68
  /** What was about to start, e.g. `eval target 'sweep-math'`. */
@@ -71,7 +93,7 @@ var SpendEnvelope = class {
71
93
  constructor(maxTotalUsd) {
72
94
  if (!Number.isFinite(maxTotalUsd) || maxTotalUsd <= 0) throw new ConfigError(`SpendEnvelope maxTotalUsd must be a positive finite number, got ${String(maxTotalUsd)}`);
73
95
  this.maxTotalUsd = maxTotalUsd;
74
- this.maxMicroUsd = microOf(maxTotalUsd, "floor");
96
+ this.maxMicroUsd = requireSafeMicro(microOf(maxTotalUsd, "floor"), "SpendEnvelope maxTotalUsd", maxTotalUsd);
75
97
  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
98
  }
77
99
  /** Total authorized so far (debit-only; never decreases). */
@@ -89,7 +111,7 @@ var SpendEnvelope = class {
89
111
  */
90
112
  authorize(ceilingUsd, runLabel) {
91
113
  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"));
114
+ const micro = requireSafeMicro(Math.max(1, microOf(ceilingUsd, "ceil")), `per-run ceiling for ${runLabel}`, ceilingUsd);
93
115
  if (this.authorizedMicroUsd + micro > this.maxMicroUsd) throw new SweepBudgetError(runLabel, ceilingUsd, this.authorizedUsd, this.maxTotalUsd);
94
116
  this.authorizedMicroUsd += micro;
95
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/evals",
3
- "version": "1.18.0",
3
+ "version": "1.19.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/testing": "1.19.0",
26
+ "@rulvar/core": "1.19.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.0",