@mark1russell7/rigor-bench 0.2.13 → 0.2.15

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 CHANGED
@@ -42,6 +42,8 @@ v.comparison; // the cases that gain and dro
42
42
  | `pooledBand(pool, level)` | The pooled band: the larger of the floor and the A/A spread of the records of earlier runs. |
43
43
  | `caseIntervals(records, level)` | The ratio of each case with a distribution-free interval of its median over the rounds. The level 0.95 needs 6 rounds or more. |
44
44
  | `limitVerdict(candidate, aa, options)` | The verdict of the claim "the ratio of each case is at most `limit`": `within`, `beyond` or `cannot decide`. |
45
+ | `gateOf(cases)` | The gate of a list of case outcomes: `pass`, `block` or `no read`. |
46
+ | `notSlowerVerdict(jobs, options)` | The gate of the claim "not slower by more than the margin", with looks and a family level. |
45
47
  | `budgetVerdict(rounds, aa, options)` | The verdict of the claim "the median of each case is at most `limit`", for one build: `within`, `beyond` or `cannot decide`. |
46
48
  | `budgetLooksVerdict(rounds, aa, options)` | The budget verdict of a run that adds rounds to the cases with no outcome, up to a stated budget. |
47
49
  | `budgetLookSpend(looks)` | The chance of error that a list of looks spends. |
@@ -78,6 +80,31 @@ v.cases; // the interval, the bound and
78
80
 
79
81
  The verdict has no correction for many cases. Each case has its own interval at the level `caseLevel`. Thus with many cases, a verdict `beyond` can come from noise only. Set `caseLevel` higher when a bench has many cases. A correction of Holm is an option for a later version.
80
82
 
83
+ ## A gate that can fail
84
+
85
+ A check that blocks a merge needs a result that can fail. `gateOf` reads the case outcomes of a verdict, and `notSlowerVerdict` gives the claim of a speed gate.
86
+
87
+ ```ts
88
+ const gate = notSlowerVerdict(jobs, { margin: Math.log(1.015) });
89
+
90
+ gate.outcome; // "pass", "block" or "no read"
91
+ gate.cases; // "not slower", "slower" or "no read" for each case, with the facts of each job
92
+ gate.more; // the next look of each case with no outcome
93
+ ```
94
+
95
+ - The claim is "not slower by more than the margin". A one-sided bound answers it, thus a case can read `not slower` when its interval holds 1. The word `same` of `timeVerdict` asks for more.
96
+ - The outcome of each case uses the words of `LimitOutcome`: `within` is `not slower`, `beyond` is `slower`, and `cannot decide` is `no read`. Thus `gateOf` also reads `limitVerdict` and `budgetVerdict`.
97
+ - A case needs its bound in each of `jobs` jobs, by default 2. More rounds do not remove a difference between jobs.
98
+ - The claim "the bound holds in at least `r` of `J` jobs" is a partial conjunction. Its p is `(J - r + 1)` times the `r`-th smallest p of the jobs (Benjamini and Heller, 2008). With 2 jobs of 2, this p is the larger p.
99
+ - The sign test compares the median difference with the margin in each job. It uses no random numbers.
100
+ - A pair holds two records with the same first arm. A runner can give the two arms opposite orders in one round, and a difference of two orders holds the effect of the order.
101
+ - The `level` is for the family. `holm` corrects each look over all cases. Each look has an equal share of the level.
102
+ - The first look with an outcome decides a case. `GATE_LOOKS` is 12, 24 and 48 rounds.
103
+ - Take 16 cases and the default looks. The first test of a family at 12 rounds needs 12 of 12 pairs below the margin in both jobs. Thus a noisy case needs more rounds, or it stays `no read`.
104
+ - `no read` is not a pass. A caller adds the rounds of `more`, then more jobs. A job that it adds after the last look is one more look, and the level does not count it.
105
+
106
+ The level needs independent rounds. Rounds with a shared cause, for example a slow computer for one job, do not have it.
107
+
81
108
  ## A claim with a budget
82
109
 
83
110
  `limitVerdict` judges a ratio of two arms. A budget is an absolute limit on one build. An example is "the median blocking time of this page is at most 450 ms". A budget needs `budgetVerdict`:
@@ -206,6 +233,62 @@ const shards = controlFactor(shardRows);
206
233
  - The fields `low` and `high` are the quartiles of the ratios. A wide spread means that one factor does not explain the load.
207
234
  - The function refuses a group with fewer usable keys than `minPairs`. The default is 5.
208
235
 
236
+ ## Check that a trick does not buy a measure
237
+
238
+ A control is a strategy made to win one measure and nothing else. A real entry can win that measure by the same trick. So `checkMeasures` reads the controls of a bench, and `winClaim` tells a page what to show beside a win. Each project keeps its own controls.
239
+
240
+ ```ts
241
+ import { checkMeasures, winClaim } from "@mark1russell7/rigor-bench";
242
+
243
+ const check = checkMeasures(rows, {
244
+ asOf: "2026-11-05",
245
+ controls: ["only-fill"],
246
+ rules: [
247
+ { measure: "fill", sense: "higher", controls: ["only-fill"], partners: ["order"] },
248
+ { measure: "order", sense: "lower", controls: [], partners: [], noControl: { because: "The control has no code yet.", until: "2026-11-20" } },
249
+ ],
250
+ });
251
+ if (check.findings.length > 0) process.exitCode = 1;
252
+ const claim = winClaim(check, "fill", "my-entry");
253
+ ```
254
+
255
+ Each row has a `measure`, a `group`, a `key` and a `value`. The optional fields are `censored`, `method` and `stratum`. The function throws a `RangeError` for a repeat of a measure, a group and a key. The caller states how repeats reduce to one row.
256
+
257
+ - **Beats:** the Holm-corrected sign test passes. With a `resolution`, the median paired difference is also over it. Without one, the output says `test only`.
258
+ - **A control wins** when no real entry beats it. A tie counts for the control.
259
+ - **A control loses a partner** when one real entry beats it on that partner, at the level divided by the count of partners. One passed partner clears `uncaught`, so each partner is one more chance.
260
+ - **Families:** one Holm family holds one control on one measure, over all real entries and all strata. A second family holds the same tests in the other direction, and `beatsEntries` of `of` gives its count. One family holds the spread of one measure.
261
+ - **Too few:** the need comes from `signTestPower` at the level of that family, over the untied keys. So 31 real entries need 10 untied keys at the level 0.05. A test under the need beats nothing. A pair with enough keys that ties reads as equal.
262
+ - **Strata:** the keys of one stratum come from one family of inputs, so they are not independent draws. A result is about these keys.
263
+
264
+ Strata make a family larger, so each stratum needs more keys:
265
+
266
+ | The family | Tests | Sides | Untied keys for each test |
267
+ | --- | --- | --- | --- |
268
+ | 31 entries, no strata | 31 | 1 | 10 |
269
+ | 14 entries, 15 strata | 210 | 1 | 13 |
270
+ | 30 entries, 20 strata | 600 | 1 | 14 |
271
+ | The spread of 31 entries, 20 strata | 9,300 | 2 | 19 |
272
+
273
+ So a stratum of 10 keys reads `too-few` by construction. That result is true, and the check keeps it. A caller gets the pooled read when the rows have no `stratum`. A sign test needs independent signs and not equal distributions, so a read can pool the keys of different families. A bench reads pooled for its gate, and by stratum where the keys are sufficient.
274
+
275
+ Each finding fails a gate, and each one carries its tests with `n`, `ties`, `p` and the Holm result.
276
+
277
+ | Finding | Meaning |
278
+ | --- | --- |
279
+ | `uncaught` | A control wins the measure and loses no partner. The trick pays. |
280
+ | `weak-control` | A control with power beats no real entry on its measure, in any stratum. The trick buys nothing. A control in second place that beats other entries is not weak. |
281
+ | `censored-win` | A control wins with more censored keys than an entry it is read against. |
282
+ | `no-spread` | No pair of real entries separates on the measure. |
283
+ | `too-few` | No read of the controls or of the spread has enough keys. A control that wins and has a partner with too few keys also gives it. A small stratum alone gives only a row. |
284
+ | `mixed-method` | The rows mix methods, or a method and no method. The check has no read of the measure. |
285
+ | `no-rule` | The rows hold a measure with no rule. |
286
+ | `no-rows` | A rule names a measure or a control with no rows. This does not read as "not beaten". |
287
+ | `no-control` | A rule has no control and no `noControl`. |
288
+ | `control-late` | The date of `noControl` is before `asOf`. |
289
+
290
+ `winClaim(check, measure, entry, stratum)` gives `alone`, `show`, `likeControl`, `censored` and `open`. The fields are about this entry. The field `likeControl` is true when this entry does not beat a control on the measure and beats it on no partner. A win by another entry clears nothing. The field `alone` is true only when this entry itself beats each control on the measure, so a measure with no control is not `alone`.
291
+
209
292
  ## The fresh runner
210
293
 
211
294
  The entry `@mark1russell7/rigor-bench/fresh` starts child processes, so it is for Node only. The root entry stays free of `node:child_process`.