@mark1russell7/rigor-bench 0.2.20 → 0.2.22
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 +50 -1
- package/build/code-stamp.d.ts +66 -0
- package/build/code-stamp.d.ts.map +1 -0
- package/build/code-stamp.js +215 -0
- package/build/code-stamp.js.map +1 -0
- package/build/index.d.ts +10 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +22 -8
- package/build/index.js.map +1 -1
- package/build/supervise.js +1 -1
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ v.comparison; // the cases that gain and dro
|
|
|
34
34
|
| Function | What it gives |
|
|
35
35
|
| --- | --- |
|
|
36
36
|
| `firstArm(round, offset)` | The arm that runs first. The order turns each round, and an odd offset starts with arm `b`. |
|
|
37
|
-
| `armOrder(arms, round)` | The order of any count of arms in a round. Each pair of arms has each order within 1 after each count of rounds. |
|
|
37
|
+
| `armOrder(arms, round)` | The order of any count of arms in a round. Each pair of arms has each order within 1 after each count of rounds. A report that keeps no `first` for each pair states the order that it used, so that a reader can make `first` again. |
|
|
38
38
|
| `checkPairedTime(record)` | The record, or a `RangeError` for a time that is not a finite number above 0. |
|
|
39
39
|
| `caseRatios(records)` | The ratio of each case. It refuses 2 records of one round, and an order that did not turn. |
|
|
40
40
|
| `orderSize(records)` | The count of records per first arm, the order effect, and the amount that unequal counts can move a mean. This size is a guide, because a verdict reads a median. |
|
|
@@ -69,6 +69,8 @@ v.comparison; // the cases that gain and dro
|
|
|
69
69
|
| `childLine(stdout)` | From `/fresh`: the last JSON line of a child, with its time `ms`, its `hash` and the reason of a skip. |
|
|
70
70
|
| `pairOrder(pairs, round, caseIndex)` | From `/fresh`: the order of the pairs of one case in one round, and the first arm of each pair. |
|
|
71
71
|
| `freshRun(options)` | From `/fresh`: the records of each pair, with each arm in its own fresh child process. |
|
|
72
|
+
| `codeStamp(options)` | From `/code-stamp`: the stamp of the code of a run, from the code that its entries reach, the lock closure of its engines and its data files. |
|
|
73
|
+
| `lockClosure(text, importer, names)` | From `/code-stamp`: each package that an engine pulls in, from a pnpm lock file. |
|
|
72
74
|
| `superviseCases(options)` | From `/supervise`: each case in a child process, held to a time budget, with a hard stop and a resume. |
|
|
73
75
|
| `caseProgress(file)` | From `/supervise`: the writer of the progress file of a child. |
|
|
74
76
|
| `startSupervisedProcess(argv, options)` | From `/supervise`: a child process that `superviseCases` can stop with its tree. |
|
|
@@ -310,6 +312,7 @@ const claim = winClaim(check, "fill", "my-entry");
|
|
|
310
312
|
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.
|
|
311
313
|
|
|
312
314
|
- **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`.
|
|
315
|
+
- **Floors:** a rule can state `floors`, one number for each group. The noise of a measure can differ by group, so one number for all groups removes real gains or passes noise. A comparison then needs a size over the larger floor of its two groups, and `floor` gives that size. A group with no floor reads `resolution`. A group with neither makes the check throw.
|
|
313
316
|
- **A control wins** when no real entry beats it. A tie counts for the control.
|
|
314
317
|
- **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.
|
|
315
318
|
- **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.
|
|
@@ -399,6 +402,52 @@ A run of many jobs sets `roundOffset` to `job * rounds`. Then the merged records
|
|
|
399
402
|
|
|
400
403
|
A rotation of N arms turns the order of two arms only when they are N/2 apart in the rotation. For 4 arms, two arms 1 apart come in the order 3:1 over 4 rounds. Thus each pair of `freshRun` has its own two children, and the order of each pair turns each round.
|
|
401
404
|
|
|
405
|
+
## The stamp of the code of a run
|
|
406
|
+
|
|
407
|
+
The entry `@mark1russell7/rigor-bench/code-stamp` is for Node only. It uses the optional peer `esbuild`. Add `esbuild` to the dependencies of the caller, so that its lock file pins the version.
|
|
408
|
+
|
|
409
|
+
A run keeps a saved record when the stamp of the record equals the stamp of the run. A version number is a poor stamp. A release that changes no result throws away each record, and a change with no release keeps wrong records. The function `codeStamp` hashes what can change a result.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { codeStamp } from "@mark1russell7/rigor-bench/code-stamp";
|
|
413
|
+
|
|
414
|
+
const stamp = await codeStamp({
|
|
415
|
+
cwd: "packages/bench-of-my-repo",
|
|
416
|
+
entries: ["src/runner.ts", "src/cover.ts"],
|
|
417
|
+
engines: { names: ["my-engine"], lockFile: "../../pnpm-lock.yaml", importer: "packages/bench-of-my-repo" },
|
|
418
|
+
dataFiles: ["corpus/corpus.json"],
|
|
419
|
+
reads: ["readFileSync"],
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
| Part of `stamp.parts` | What it reads | A change that moves it |
|
|
424
|
+
| --- | --- | --- |
|
|
425
|
+
| `code` | The bundle of the entries after a tree shake, with the engines outside | A token that the entries reach, a new export of an entry, a rename |
|
|
426
|
+
| `engines` | The version and the integrity of each package in the lock closure of each engine | An engine version, a transitive package of an engine |
|
|
427
|
+
| `data` | The name and the hash of each file in `dataFiles` | A byte or the name of such a file |
|
|
428
|
+
| `bundler` | The version of `esbuild`. This part is not in the hash | A new version in the lock file of the caller |
|
|
429
|
+
| `node` | The Node major | Another major, which can change a float or a sort |
|
|
430
|
+
|
|
431
|
+
These changes keep the stamp: a comment, blank space, line ends, a type and a version in a package file. An export that the entries do not reach and another folder for the same files also keep it.
|
|
432
|
+
|
|
433
|
+
The same tree gives the same stamp on Linux and on Windows. A slash in an option can have either direction. The hash of a text data file reads each line end as LF, and a file with a NUL byte hashes as its bytes. So a parser that reads a line end as data can read another result with an equal stamp.
|
|
434
|
+
|
|
435
|
+
The stamp holds no computer. Each record keeps the computer as a field, because a time depends on it and a result does not.
|
|
436
|
+
|
|
437
|
+
### What a bundle cannot see
|
|
438
|
+
|
|
439
|
+
- **A file that the code reads when it runs.** The bundle holds no such file. So the function looks for the words of `CODE_STAMP_READ_WORDS` in the bundle, for example `readFileSync`, `fetch` and `process.env`. A word that the option `reads` does not state refuses the stamp, and so does a stated word that the bundle does not hold. List each file that such a read opens in `dataFiles`.
|
|
440
|
+
- **The name of a file that a program makes at run time.** A word tells that a read exists, not which file it opens. The list in `dataFiles` is the statement of the caller.
|
|
441
|
+
- **The code below an engine.** An engine stays outside the bundle, so `lockClosure` hashes the resolved closure of the engine from the lock file. The name of the engine alone is not sufficient. It reads pnpm lock files of version 9 and refuses another version.
|
|
442
|
+
|
|
443
|
+
A workspace package is not an engine: `lockClosure` refuses it, because its code enters the bundle.
|
|
444
|
+
|
|
445
|
+
### The limits of the stamp
|
|
446
|
+
|
|
447
|
+
- A rename of an identifier moves the stamp, and so does the rename of an entry file. This gives a false rerun, not a false keep.
|
|
448
|
+
- A top-level statement with an effect stays in the bundle when its module has no `"sideEffects": false`. A change of that statement moves the stamp, also when no result depends on it.
|
|
449
|
+
- A new version of the bundler that gives an equal bundle keeps the stamp and the records. A new version that gives another bundle moves `code`, so it keeps no record, also when no result can change. This gives a false rerun, not a false keep. The field `bundler` of `parts` tells the cause. A caller with long runs holds automatic updates of the bundler in its own update settings. It merges an update when its next full run starts. The read of 2026-11-05 stamped 2 real entries (17 inputs) with 8 versions of the bundler, 0.27.2 to 0.28.2. Each of the 7 steps gave an equal bundle, so 0 of 7 steps gave a false rerun. The true rate is at most approximately 35 % at 95 %, and this read is of one tree only. Also, esbuild 0.27.4 crashed on a failed build of an entry in that read. That version is outside the peer range, so the read does not count it as a false rerun.
|
|
450
|
+
|
|
402
451
|
## Run cases with a time budget
|
|
403
452
|
|
|
404
453
|
The entry `@mark1russell7/rigor-bench/supervise` starts child processes, so it is for Node only.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The engines of a run, which stay outside the bundle. The lock file gives what each engine pulls in. */
|
|
2
|
+
export interface CodeStampEngines {
|
|
3
|
+
/** The package names. A subpath of a name also stays outside the bundle. */
|
|
4
|
+
readonly names: readonly string[];
|
|
5
|
+
/** The path of the `pnpm-lock.yaml`, from `cwd`. */
|
|
6
|
+
readonly lockFile: string;
|
|
7
|
+
/** The key of the package in `importers` of the lock file, for example `packages/joint`. */
|
|
8
|
+
readonly importer: string;
|
|
9
|
+
}
|
|
10
|
+
/** The inputs of `codeStamp`. Each relative path starts at `cwd`. */
|
|
11
|
+
export interface CodeStampOptions {
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
/** The source files that a run starts from. Each export of these files stays in the bundle, so a new export of an entry moves the stamp. */
|
|
14
|
+
readonly entries: readonly string[];
|
|
15
|
+
readonly engines?: CodeStampEngines;
|
|
16
|
+
/**
|
|
17
|
+
* The files that the code reads when it runs, for example a corpus or a table. A bundle cannot see them, so each has its own hash.
|
|
18
|
+
* The hash of a text file reads each line end as LF, so a checkout with CRLF gives the same stamp. A file with a NUL byte hashes as its bytes.
|
|
19
|
+
*/
|
|
20
|
+
readonly dataFiles?: readonly string[];
|
|
21
|
+
/** The words of `CODE_STAMP_READ_WORDS` that the caller expects in the bundle. A difference in either direction refuses the stamp. */
|
|
22
|
+
readonly reads?: readonly string[];
|
|
23
|
+
/** The Node major of the stamp. The default is the major of this process. */
|
|
24
|
+
readonly node?: string;
|
|
25
|
+
}
|
|
26
|
+
/** One engine of a stamp: the version in the lock file, and each package of its closure with its peers. */
|
|
27
|
+
export interface EngineClosure {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly version: string;
|
|
30
|
+
readonly closure: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/** The hash of each part of a stamp, so that a change of the stamp names its cause. */
|
|
33
|
+
export interface CodeStampParts {
|
|
34
|
+
readonly code: string;
|
|
35
|
+
readonly engines: string;
|
|
36
|
+
readonly data: string;
|
|
37
|
+
/** The name and the version of the bundler, for example `esbuild@0.28.2`. It tells the cause of a change of `code`, and it is not in the hash. */
|
|
38
|
+
readonly bundler: string;
|
|
39
|
+
readonly node: string;
|
|
40
|
+
}
|
|
41
|
+
/** The result of `codeStamp`. Only `hash` and `parts` are the stamp. The other fields tell what went in. */
|
|
42
|
+
export interface CodeStamp {
|
|
43
|
+
readonly hash: string;
|
|
44
|
+
readonly parts: CodeStampParts;
|
|
45
|
+
/** The source files that put at least one byte in the bundle, with paths from `cwd`. */
|
|
46
|
+
readonly inputs: readonly string[];
|
|
47
|
+
/** The words of `CODE_STAMP_READ_WORDS` in the bundle. */
|
|
48
|
+
readonly reads: readonly string[];
|
|
49
|
+
readonly engines: readonly EngineClosure[];
|
|
50
|
+
}
|
|
51
|
+
/** The words in a bundle that show an input from outside the bundle. A word inside a text also counts, so the scan can only say too much. */
|
|
52
|
+
export declare const CODE_STAMP_READ_WORDS: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* This function gives the closure of each engine from the text of a pnpm lock file of version 9, in the order of `names`.
|
|
55
|
+
* A closure holds each package that the engine pulls in, with its peers, in sorted order.
|
|
56
|
+
* It refuses another lock version, an unknown importer or name, and a workspace package, which belongs in the bundle.
|
|
57
|
+
*/
|
|
58
|
+
export declare function lockClosure(lockText: string, importer: string, names: readonly string[]): readonly EngineClosure[];
|
|
59
|
+
/**
|
|
60
|
+
* This function gives the stamp of the code of a run. Only the options, the source files and the data files decide it, so another folder gives the same stamp.
|
|
61
|
+
* A change in comments, blank space, line ends or types alone keeps the stamp. A new export that the entries do not reach also keeps it.
|
|
62
|
+
* A change in a token that the entries reach moves `parts.code`. Identifiers stay as they are, so a rename also moves it. This gives a false rerun, not a false keep.
|
|
63
|
+
* A bundle cannot see a file that the code reads when it runs. So a word of `CODE_STAMP_READ_WORDS` that `reads` does not state refuses the stamp, and `dataFiles` lists the files.
|
|
64
|
+
*/
|
|
65
|
+
export declare function codeStamp(options: CodeStampOptions): Promise<CodeStamp>;
|
|
66
|
+
//# sourceMappingURL=code-stamp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-stamp.d.ts","names":[],"sources":["../src/code-stamp.ts"],"mappings":";iBAeiB;;WAEN;;WAEA;;WAEA;;;iBAIM;WACN;;WAEA;WACA,UAAU;;;;;WAKV;;WAEA;;WAEA;;;iBAIM;WACN;WACA;WACA;;;iBAIM;WACN;WACA;WACA;;WAEA;WACA;;;iBAIM;WACN;WACA,OAAO;;WAEP;;WAEA;WACA,kBAAkB;;;qBAIhB;;;;;;wBA0FG,YAAY,kBAAkB,kBAAkB,oCAAoC;;;;;;;wBA4D9E,UAAU,SAAS,mBAAmB,QAAQ"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
// src/read-words.ts
|
|
5
|
+
/** The test of a word against the text of a bundle. This file is internal, so its name is not in the public API. */
|
|
6
|
+
const isWordCharacter = (character) => character !== void 0 && /\w/.test(character);
|
|
7
|
+
/**
|
|
8
|
+
* This function tells if a text holds a word. Each character of the word is literal, so no character of the word needs an escape.
|
|
9
|
+
* A word edge that is a word character must also be a word boundary in the text.
|
|
10
|
+
* So `fetch` is not in `fetcher`, and `import(` is not in `reimport(`.
|
|
11
|
+
* An empty word is in no text.
|
|
12
|
+
*/
|
|
13
|
+
function hasWord(text, word) {
|
|
14
|
+
if (word === "") return false;
|
|
15
|
+
const startEdge = isWordCharacter(word[0]);
|
|
16
|
+
const endEdge = isWordCharacter(word.at(-1));
|
|
17
|
+
for (let at = text.indexOf(word); at !== -1; at = text.indexOf(word, at + 1)) {
|
|
18
|
+
const startOk = !startEdge || !isWordCharacter(text[at - 1]);
|
|
19
|
+
const endOk = !endEdge || !isWordCharacter(text[at + word.length]);
|
|
20
|
+
if (startOk && endOk) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// src/code-stamp.ts
|
|
25
|
+
/**
|
|
26
|
+
* The stamp of the code of a run, for Node only. A run keeps a saved record when the stamp of the record equals the stamp of the run.
|
|
27
|
+
* A version number is a poor stamp. A release that changes no result throws away each record. A change that has no release keeps wrong records.
|
|
28
|
+
* This entry hashes what can change a result. These are the code that the entries reach after a tree shake and the lock closure of the engines.
|
|
29
|
+
* The stated data files and the Node major are also in the hash.
|
|
30
|
+
* The bundler only makes the text that the hash reads, and no run uses that text. So its version is a part of the stamp and is not in the hash. A new version that gives another bundle moves `code`.
|
|
31
|
+
* The computer is no part of the stamp. Each record keeps it as a field.
|
|
32
|
+
* The bundler is the optional peer `esbuild`, so a caller that only reads verdicts installs no native binary.
|
|
33
|
+
*/
|
|
34
|
+
/** The words in a bundle that show an input from outside the bundle. A word inside a text also counts, so the scan can only say too much. */
|
|
35
|
+
const CODE_STAMP_READ_WORDS = [
|
|
36
|
+
"readFile",
|
|
37
|
+
"readFileSync",
|
|
38
|
+
"createReadStream",
|
|
39
|
+
"readdir",
|
|
40
|
+
"readdirSync",
|
|
41
|
+
"opendir",
|
|
42
|
+
"fetch",
|
|
43
|
+
"WebAssembly",
|
|
44
|
+
"require",
|
|
45
|
+
"process.env",
|
|
46
|
+
"import.meta.url",
|
|
47
|
+
"import("
|
|
48
|
+
];
|
|
49
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
50
|
+
const unquote = (text) => text.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
|
|
51
|
+
/** The first colon outside quotes ends a key. For example, the key `'@a/b@1.0.0'` holds no colon outside its quotes. */
|
|
52
|
+
function splitKey(line) {
|
|
53
|
+
const body = line.trim();
|
|
54
|
+
if (body.startsWith("'") || body.startsWith("\"")) {
|
|
55
|
+
const end = body.indexOf(body[0], 1);
|
|
56
|
+
return [body.slice(1, end), body.slice(end + 1).replace(/^:/, "").trim()];
|
|
57
|
+
}
|
|
58
|
+
const colon = body.indexOf(":");
|
|
59
|
+
return colon < 0 ? [body, ""] : [body.slice(0, colon), body.slice(colon + 1).trim()];
|
|
60
|
+
}
|
|
61
|
+
const indentOf = (line) => line.length - line.trimStart().length;
|
|
62
|
+
/** A value in `dependencies` of a snapshot is a version with peers, or a whole key when the name is an alias. */
|
|
63
|
+
function keyOfDependency(name, value) {
|
|
64
|
+
return /^[^\d(].*@\d/.test(value) ? value : `${name}@${value}`;
|
|
65
|
+
}
|
|
66
|
+
/** This function reads one document of a pnpm lock file. It reads only lines that the closure needs. */
|
|
67
|
+
function readLockDocument(lines) {
|
|
68
|
+
const doc = {
|
|
69
|
+
importers: /* @__PURE__ */ new Map(),
|
|
70
|
+
packages: /* @__PURE__ */ new Map(),
|
|
71
|
+
snapshots: /* @__PURE__ */ new Map()
|
|
72
|
+
};
|
|
73
|
+
let section = "";
|
|
74
|
+
let top = "";
|
|
75
|
+
let group = "";
|
|
76
|
+
let name = "";
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
if (line.trim() === "") continue;
|
|
79
|
+
const indent = indentOf(line);
|
|
80
|
+
const [key, value] = splitKey(line);
|
|
81
|
+
if (indent === 0) {
|
|
82
|
+
section = key;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (indent === 2) {
|
|
86
|
+
top = key;
|
|
87
|
+
group = "";
|
|
88
|
+
if (section === "importers") doc.importers.set(top, /* @__PURE__ */ new Map());
|
|
89
|
+
if (section === "snapshots") doc.snapshots.set(top, []);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (section === "packages" && indent === 4 && key === "resolution") doc.packages.set(top, value);
|
|
93
|
+
if (section === "snapshots") {
|
|
94
|
+
if (indent === 4) group = key;
|
|
95
|
+
else if (indent === 6 && (group === "dependencies" || group === "optionalDependencies")) doc.snapshots.get(top).push(keyOfDependency(key, unquote(value)));
|
|
96
|
+
}
|
|
97
|
+
if (section === "importers") {
|
|
98
|
+
if (indent === 4) group = key;
|
|
99
|
+
else if (indent === 6) name = key;
|
|
100
|
+
else if (indent === 8 && key === "version") doc.importers.get(top).set(name, unquote(value));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return doc;
|
|
104
|
+
}
|
|
105
|
+
const withoutPeers = (key) => key.replace(/\(.*$/, "");
|
|
106
|
+
/**
|
|
107
|
+
* This function gives the closure of each engine from the text of a pnpm lock file of version 9, in the order of `names`.
|
|
108
|
+
* A closure holds each package that the engine pulls in, with its peers, in sorted order.
|
|
109
|
+
* It refuses another lock version, an unknown importer or name, and a workspace package, which belongs in the bundle.
|
|
110
|
+
*/
|
|
111
|
+
function lockClosure(lockText, importer, names) {
|
|
112
|
+
const lines = (lockText.split(/\r?\n---\r?\n|^---\r?\n/).filter((text) => text.includes("importers:")).at(-1) ?? "").split(/\r?\n/);
|
|
113
|
+
const version = lines.find((line) => line.startsWith("lockfileVersion:"));
|
|
114
|
+
if (version === void 0 || !/^9\./.test(unquote(version.slice(16)))) throw new Error(`The lock file has the version line ${version ?? "(none)"}. This reader knows only lock version 9.`);
|
|
115
|
+
const doc = readLockDocument(lines);
|
|
116
|
+
const packages = doc.importers.get(importer);
|
|
117
|
+
if (packages === void 0) throw new Error(`The lock file has no importer ${importer}.`);
|
|
118
|
+
return names.map((name) => {
|
|
119
|
+
const resolved = packages.get(name);
|
|
120
|
+
if (resolved === void 0) throw new Error(`The importer ${importer} has no package ${name} in the lock file.`);
|
|
121
|
+
if (resolved.startsWith("link:")) throw new Error(`The engine ${name} is a workspace package (${resolved}). It has no lock closure, so leave it out of the engines and let it enter the bundle.`);
|
|
122
|
+
const start = keyOfDependency(name, resolved);
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
const stack = [start];
|
|
125
|
+
while (stack.length > 0) {
|
|
126
|
+
const key = stack.pop();
|
|
127
|
+
if (seen.has(key)) continue;
|
|
128
|
+
const dependencies = doc.snapshots.get(key);
|
|
129
|
+
if (dependencies === void 0) throw new Error(`The lock file has no snapshot ${key}, which ${name} pulls in.`);
|
|
130
|
+
seen.add(key);
|
|
131
|
+
stack.push(...dependencies);
|
|
132
|
+
}
|
|
133
|
+
const closure = [...seen].sort().map((key) => `${key} ${doc.packages.get(withoutPeers(key)) ?? ""}`);
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
version: withoutPeers(resolved),
|
|
137
|
+
closure
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function loadBundler() {
|
|
142
|
+
try {
|
|
143
|
+
return await import("esbuild");
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
throw new Error("The function codeStamp uses the optional peer esbuild. Add esbuild to the dependencies of the caller, so that its lock file pins the version.", { cause });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const forwardSlashes = (path) => path.split("\\").join("/");
|
|
149
|
+
const fromCwd = (cwd, path) => forwardSlashes(relative(cwd, resolve(cwd, forwardSlashes(path))));
|
|
150
|
+
/** This function gives the bytes that a data file hashes as. Git uses the same test for a binary file: a NUL byte. */
|
|
151
|
+
function dataBytes(bytes) {
|
|
152
|
+
return bytes.includes(0) ? bytes : Buffer.from(bytes.toString("latin1").replaceAll("\r\n", "\n"), "latin1");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* This function gives the stamp of the code of a run. Only the options, the source files and the data files decide it, so another folder gives the same stamp.
|
|
156
|
+
* A change in comments, blank space, line ends or types alone keeps the stamp. A new export that the entries do not reach also keeps it.
|
|
157
|
+
* A change in a token that the entries reach moves `parts.code`. Identifiers stay as they are, so a rename also moves it. This gives a false rerun, not a false keep.
|
|
158
|
+
* A bundle cannot see a file that the code reads when it runs. So a word of `CODE_STAMP_READ_WORDS` that `reads` does not state refuses the stamp, and `dataFiles` lists the files.
|
|
159
|
+
*/
|
|
160
|
+
async function codeStamp(options) {
|
|
161
|
+
const cwd = resolve(options.cwd);
|
|
162
|
+
const entries = [...new Set(options.entries.map((entry) => fromCwd(cwd, entry)))].sort();
|
|
163
|
+
if (entries.length === 0) throw new Error("A code stamp needs at least one entry.");
|
|
164
|
+
const outside = entries.find((entry) => entry.startsWith("..") || isAbsolute(entry));
|
|
165
|
+
if (outside !== void 0) throw new Error(`The entry ${outside} is outside ${cwd}. Another folder would give another stamp.`);
|
|
166
|
+
const bundler = await loadBundler();
|
|
167
|
+
const result = await bundler.build({
|
|
168
|
+
stdin: {
|
|
169
|
+
contents: entries.map((entry, i) => `export * as e${i} from "./${entry}";`).join("\n"),
|
|
170
|
+
resolveDir: cwd,
|
|
171
|
+
loader: "ts"
|
|
172
|
+
},
|
|
173
|
+
absWorkingDir: cwd,
|
|
174
|
+
bundle: true,
|
|
175
|
+
format: "esm",
|
|
176
|
+
platform: "node",
|
|
177
|
+
write: false,
|
|
178
|
+
outfile: "stamp.js",
|
|
179
|
+
treeShaking: true,
|
|
180
|
+
minifyWhitespace: true,
|
|
181
|
+
legalComments: "none",
|
|
182
|
+
metafile: true,
|
|
183
|
+
logLevel: "silent",
|
|
184
|
+
tsconfigRaw: {},
|
|
185
|
+
external: [...options.engines?.names ?? []]
|
|
186
|
+
});
|
|
187
|
+
const text = (result.outputFiles ?? []).map((file) => file.text).join("\n");
|
|
188
|
+
const reads = CODE_STAMP_READ_WORDS.filter((word) => hasWord(text, word));
|
|
189
|
+
const stated = [...new Set(options.reads ?? [])];
|
|
190
|
+
const notStated = reads.filter((word) => !stated.includes(word));
|
|
191
|
+
const notFound = stated.filter((word) => !reads.includes(word));
|
|
192
|
+
if (notStated.length > 0 || notFound.length > 0) throw new Error(`The reads of the bundle differ from the option reads. In the bundle and not stated: ${notStated.join(", ") || "(none)"}. Stated and not in the bundle: ${notFound.join(", ") || "(none)"}. List each file that such a read opens in dataFiles.`);
|
|
193
|
+
const inputs = /* @__PURE__ */ new Set();
|
|
194
|
+
for (const output of Object.values(result.metafile?.outputs ?? {})) for (const [input, facts] of Object.entries(output.inputs)) if (facts.bytesInOutput > 0 && input !== "<stdin>") inputs.add(forwardSlashes(input));
|
|
195
|
+
const engines = options.engines === void 0 ? [] : lockClosure(readFileSync(resolve(cwd, options.engines.lockFile), "utf8"), options.engines.importer, options.engines.names);
|
|
196
|
+
const data = [...new Set((options.dataFiles ?? []).map((file) => fromCwd(cwd, file)))].sort().map((file) => `${file} ${sha256(dataBytes(readFileSync(resolve(cwd, file))))}`);
|
|
197
|
+
const parts = {
|
|
198
|
+
code: sha256(text),
|
|
199
|
+
engines: sha256(JSON.stringify(engines)),
|
|
200
|
+
data: sha256(JSON.stringify(data)),
|
|
201
|
+
bundler: `esbuild@${bundler.version}`,
|
|
202
|
+
node: options.node ?? process.versions.node.split(".")[0]
|
|
203
|
+
};
|
|
204
|
+
const { bundler: _version, ...hashed } = parts;
|
|
205
|
+
return {
|
|
206
|
+
hash: sha256(JSON.stringify(hashed)),
|
|
207
|
+
parts,
|
|
208
|
+
inputs: [...inputs].sort(),
|
|
209
|
+
reads,
|
|
210
|
+
engines
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export { CODE_STAMP_READ_WORDS, codeStamp, lockClosure };
|
|
214
|
+
|
|
215
|
+
//# sourceMappingURL=code-stamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-stamp.js","names":[],"sources":["../src/read-words.ts","../src/code-stamp.ts"],"sourcesContent":["/** The test of a word against the text of a bundle. This file is internal, so its name is not in the public API. */\n\nconst isWordCharacter = (character: string | undefined): boolean => character !== undefined && /\\w/.test(character);\n\n/**\n * This function tells if a text holds a word. Each character of the word is literal, so no character of the word needs an escape.\n * A word edge that is a word character must also be a word boundary in the text.\n * So `fetch` is not in `fetcher`, and `import(` is not in `reimport(`.\n * An empty word is in no text.\n */\nexport function hasWord(text: string, word: string): boolean {\n if (word === \"\") return false;\n const startEdge = isWordCharacter(word[0]);\n const endEdge = isWordCharacter(word.at(-1));\n for (let at = text.indexOf(word); at !== -1; at = text.indexOf(word, at + 1)) {\n const startOk = !startEdge || !isWordCharacter(text[at - 1]);\n const endOk = !endEdge || !isWordCharacter(text[at + word.length]);\n if (startOk && endOk) return true;\n }\n return false;\n}\n","/**\n * The stamp of the code of a run, for Node only. A run keeps a saved record when the stamp of the record equals the stamp of the run.\n * A version number is a poor stamp. A release that changes no result throws away each record. A change that has no release keeps wrong records.\n * This entry hashes what can change a result. These are the code that the entries reach after a tree shake and the lock closure of the engines.\n * The stated data files and the Node major are also in the hash.\n * The bundler only makes the text that the hash reads, and no run uses that text. So its version is a part of the stamp and is not in the hash. A new version that gives another bundle moves `code`.\n * The computer is no part of the stamp. Each record keeps it as a field.\n * The bundler is the optional peer `esbuild`, so a caller that only reads verdicts installs no native binary.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport { hasWord } from \"./read-words.ts\";\n\n/** The engines of a run, which stay outside the bundle. The lock file gives what each engine pulls in. */\nexport interface CodeStampEngines {\n /** The package names. A subpath of a name also stays outside the bundle. */\n readonly names: readonly string[];\n /** The path of the `pnpm-lock.yaml`, from `cwd`. */\n readonly lockFile: string;\n /** The key of the package in `importers` of the lock file, for example `packages/joint`. */\n readonly importer: string;\n}\n\n/** The inputs of `codeStamp`. Each relative path starts at `cwd`. */\nexport interface CodeStampOptions {\n readonly cwd: string;\n /** The source files that a run starts from. Each export of these files stays in the bundle, so a new export of an entry moves the stamp. */\n readonly entries: readonly string[];\n readonly engines?: CodeStampEngines;\n /**\n * The files that the code reads when it runs, for example a corpus or a table. A bundle cannot see them, so each has its own hash.\n * The hash of a text file reads each line end as LF, so a checkout with CRLF gives the same stamp. A file with a NUL byte hashes as its bytes.\n */\n readonly dataFiles?: readonly string[];\n /** The words of `CODE_STAMP_READ_WORDS` that the caller expects in the bundle. A difference in either direction refuses the stamp. */\n readonly reads?: readonly string[];\n /** The Node major of the stamp. The default is the major of this process. */\n readonly node?: string;\n}\n\n/** One engine of a stamp: the version in the lock file, and each package of its closure with its peers. */\nexport interface EngineClosure {\n readonly name: string;\n readonly version: string;\n readonly closure: readonly string[];\n}\n\n/** The hash of each part of a stamp, so that a change of the stamp names its cause. */\nexport interface CodeStampParts {\n readonly code: string;\n readonly engines: string;\n readonly data: string;\n /** The name and the version of the bundler, for example `esbuild@0.28.2`. It tells the cause of a change of `code`, and it is not in the hash. */\n readonly bundler: string;\n readonly node: string;\n}\n\n/** The result of `codeStamp`. Only `hash` and `parts` are the stamp. The other fields tell what went in. */\nexport interface CodeStamp {\n readonly hash: string;\n readonly parts: CodeStampParts;\n /** The source files that put at least one byte in the bundle, with paths from `cwd`. */\n readonly inputs: readonly string[];\n /** The words of `CODE_STAMP_READ_WORDS` in the bundle. */\n readonly reads: readonly string[];\n readonly engines: readonly EngineClosure[];\n}\n\n/** The words in a bundle that show an input from outside the bundle. A word inside a text also counts, so the scan can only say too much. */\nexport const CODE_STAMP_READ_WORDS: readonly string[] = [\n \"readFile\",\n \"readFileSync\",\n \"createReadStream\",\n \"readdir\",\n \"readdirSync\",\n \"opendir\",\n \"fetch\",\n \"WebAssembly\",\n \"require\",\n \"process.env\",\n \"import.meta.url\",\n \"import(\",\n];\n\nconst sha256 = (text: string | Uint8Array): string => createHash(\"sha256\").update(text).digest(\"hex\");\n\n/** A lock document, read to the three maps that a closure needs. */\ninterface LockDocument {\n /** The key of each importer, then the name and the version of each of its packages. */\n readonly importers: Map<string, Map<string, string>>;\n /** The resolution line of each package, with no peers in the key. */\n readonly packages: Map<string, string>;\n /** The key of each snapshot, with the keys of its dependencies. */\n readonly snapshots: Map<string, string[]>;\n}\n\nconst unquote = (text: string): string => text.trim().replace(/^'(.*)'$/, \"$1\").replace(/^\"(.*)\"$/, \"$1\");\n\n/** The first colon outside quotes ends a key. For example, the key `'@a/b@1.0.0'` holds no colon outside its quotes. */\nfunction splitKey(line: string): readonly [string, string] {\n const body = line.trim();\n if (body.startsWith(\"'\") || body.startsWith('\"')) {\n const end = body.indexOf(body[0]!, 1);\n return [body.slice(1, end), body.slice(end + 1).replace(/^:/, \"\").trim()];\n }\n const colon = body.indexOf(\":\");\n return colon < 0 ? [body, \"\"] : [body.slice(0, colon), body.slice(colon + 1).trim()];\n}\n\nconst indentOf = (line: string): number => line.length - line.trimStart().length;\n\n/** A value in `dependencies` of a snapshot is a version with peers, or a whole key when the name is an alias. */\nfunction keyOfDependency(name: string, value: string): string {\n return /^[^\\d(].*@\\d/.test(value) ? value : `${name}@${value}`;\n}\n\n/** This function reads one document of a pnpm lock file. It reads only lines that the closure needs. */\nfunction readLockDocument(lines: readonly string[]): LockDocument {\n const doc: LockDocument = { importers: new Map(), packages: new Map(), snapshots: new Map() };\n let section = \"\";\n let top = \"\";\n let group = \"\";\n let name = \"\";\n for (const line of lines) {\n if (line.trim() === \"\") continue;\n const indent = indentOf(line);\n const [key, value] = splitKey(line);\n if (indent === 0) {\n section = key;\n continue;\n }\n if (indent === 2) {\n top = key;\n group = \"\";\n if (section === \"importers\") doc.importers.set(top, new Map());\n if (section === \"snapshots\") doc.snapshots.set(top, []);\n continue;\n }\n if (section === \"packages\" && indent === 4 && key === \"resolution\") doc.packages.set(top, value);\n if (section === \"snapshots\") {\n if (indent === 4) group = key;\n else if (indent === 6 && (group === \"dependencies\" || group === \"optionalDependencies\")) doc.snapshots.get(top)!.push(keyOfDependency(key, unquote(value)));\n }\n if (section === \"importers\") {\n if (indent === 4) group = key;\n else if (indent === 6) name = key;\n else if (indent === 8 && key === \"version\") doc.importers.get(top)!.set(name, unquote(value));\n }\n }\n return doc;\n}\n\nconst withoutPeers = (key: string): string => key.replace(/\\(.*$/, \"\");\n\n/**\n * This function gives the closure of each engine from the text of a pnpm lock file of version 9, in the order of `names`.\n * A closure holds each package that the engine pulls in, with its peers, in sorted order.\n * It refuses another lock version, an unknown importer or name, and a workspace package, which belongs in the bundle.\n */\nexport function lockClosure(lockText: string, importer: string, names: readonly string[]): readonly EngineClosure[] {\n const documents = lockText.split(/\\r?\\n---\\r?\\n|^---\\r?\\n/).filter((text) => text.includes(\"importers:\"));\n const lines = (documents.at(-1) ?? \"\").split(/\\r?\\n/);\n const version = lines.find((line) => line.startsWith(\"lockfileVersion:\"));\n if (version === undefined || !/^9\\./.test(unquote(version.slice(\"lockfileVersion:\".length)))) throw new Error(`The lock file has the version line ${version ?? \"(none)\"}. This reader knows only lock version 9.`);\n const doc = readLockDocument(lines);\n const packages = doc.importers.get(importer);\n if (packages === undefined) throw new Error(`The lock file has no importer ${importer}.`);\n return names.map((name) => {\n const resolved = packages.get(name);\n if (resolved === undefined) throw new Error(`The importer ${importer} has no package ${name} in the lock file.`);\n if (resolved.startsWith(\"link:\")) throw new Error(`The engine ${name} is a workspace package (${resolved}). It has no lock closure, so leave it out of the engines and let it enter the bundle.`);\n const start = keyOfDependency(name, resolved);\n const seen = new Set<string>();\n const stack = [start];\n while (stack.length > 0) {\n const key = stack.pop()!;\n if (seen.has(key)) continue;\n const dependencies = doc.snapshots.get(key);\n if (dependencies === undefined) throw new Error(`The lock file has no snapshot ${key}, which ${name} pulls in.`);\n seen.add(key);\n stack.push(...dependencies);\n }\n const closure = [...seen].sort().map((key) => `${key} ${doc.packages.get(withoutPeers(key)) ?? \"\"}`);\n return { name, version: withoutPeers(resolved), closure };\n });\n}\n\n/** The part of a bundler that this entry uses. */\ninterface Bundler {\n readonly version: string;\n build(options: Record<string, unknown>): Promise<{\n readonly outputFiles?: readonly { readonly text: string }[];\n readonly metafile?: { readonly outputs: Record<string, { readonly inputs: Record<string, { readonly bytesInOutput: number }> }> };\n }>;\n}\n\nasync function loadBundler(): Promise<Bundler> {\n try {\n return (await import(\"esbuild\")) as unknown as Bundler;\n } catch (cause) {\n throw new Error(\"The function codeStamp uses the optional peer esbuild. Add esbuild to the dependencies of the caller, so that its lock file pins the version.\", { cause });\n }\n}\n\nconst forwardSlashes = (path: string): string => path.split(\"\\\\\").join(\"/\");\n\nconst fromCwd = (cwd: string, path: string): string => forwardSlashes(relative(cwd, resolve(cwd, forwardSlashes(path))));\n\n/** This function gives the bytes that a data file hashes as. Git uses the same test for a binary file: a NUL byte. */\nfunction dataBytes(bytes: Buffer): Buffer {\n return bytes.includes(0) ? bytes : Buffer.from(bytes.toString(\"latin1\").replaceAll(\"\\r\\n\", \"\\n\"), \"latin1\");\n}\n\n/**\n * This function gives the stamp of the code of a run. Only the options, the source files and the data files decide it, so another folder gives the same stamp.\n * A change in comments, blank space, line ends or types alone keeps the stamp. A new export that the entries do not reach also keeps it.\n * A change in a token that the entries reach moves `parts.code`. Identifiers stay as they are, so a rename also moves it. This gives a false rerun, not a false keep.\n * A bundle cannot see a file that the code reads when it runs. So a word of `CODE_STAMP_READ_WORDS` that `reads` does not state refuses the stamp, and `dataFiles` lists the files.\n */\nexport async function codeStamp(options: CodeStampOptions): Promise<CodeStamp> {\n const cwd = resolve(options.cwd);\n const entries = [...new Set(options.entries.map((entry) => fromCwd(cwd, entry)))].sort();\n if (entries.length === 0) throw new Error(\"A code stamp needs at least one entry.\");\n const outside = entries.find((entry) => entry.startsWith(\"..\") || isAbsolute(entry));\n if (outside !== undefined) throw new Error(`The entry ${outside} is outside ${cwd}. Another folder would give another stamp.`);\n const bundler = await loadBundler();\n const result = await bundler.build({\n stdin: { contents: entries.map((entry, i) => `export * as e${i} from \"./${entry}\";`).join(\"\\n\"), resolveDir: cwd, loader: \"ts\" },\n absWorkingDir: cwd,\n bundle: true,\n format: \"esm\",\n platform: \"node\",\n write: false,\n outfile: \"stamp.js\",\n treeShaking: true,\n minifyWhitespace: true,\n legalComments: \"none\",\n metafile: true,\n logLevel: \"silent\",\n // The tsconfig file of a package is not an input: another compiler option in that file does not move the stamp.\n tsconfigRaw: {},\n external: [...(options.engines?.names ?? [])],\n });\n const text = (result.outputFiles ?? []).map((file) => file.text).join(\"\\n\");\n const reads = CODE_STAMP_READ_WORDS.filter((word) => hasWord(text, word));\n const stated = [...new Set(options.reads ?? [])];\n const notStated = reads.filter((word) => !stated.includes(word));\n const notFound = stated.filter((word) => !reads.includes(word));\n if (notStated.length > 0 || notFound.length > 0) {\n throw new Error(\n `The reads of the bundle differ from the option reads. In the bundle and not stated: ${notStated.join(\", \") || \"(none)\"}. Stated and not in the bundle: ${notFound.join(\", \") || \"(none)\"}. List each file that such a read opens in dataFiles.`,\n );\n }\n const inputs = new Set<string>();\n for (const output of Object.values(result.metafile?.outputs ?? {})) {\n for (const [input, facts] of Object.entries(output.inputs)) if (facts.bytesInOutput > 0 && input !== \"<stdin>\") inputs.add(forwardSlashes(input));\n }\n const engines = options.engines === undefined ? [] : lockClosure(readFileSync(resolve(cwd, options.engines.lockFile), \"utf8\"), options.engines.importer, options.engines.names);\n const data = [...new Set((options.dataFiles ?? []).map((file) => fromCwd(cwd, file)))].sort().map((file) => `${file} ${sha256(dataBytes(readFileSync(resolve(cwd, file))))}`);\n const parts: CodeStampParts = {\n code: sha256(text),\n engines: sha256(JSON.stringify(engines)),\n data: sha256(JSON.stringify(data)),\n bundler: `esbuild@${bundler.version}`,\n node: options.node ?? process.versions.node.split(\".\")[0]!,\n };\n const { bundler: _version, ...hashed } = parts;\n return { hash: sha256(JSON.stringify(hashed)), parts, inputs: [...inputs].sort(), reads, engines };\n}\n"],"mappings":";;;;;AAEA,MAAM,mBAAmB,cAA2C,cAAc,KAAA,KAAa,KAAK,KAAK,SAAS;;;;;;;AAQlH,SAAgB,QAAQ,MAAc,MAAuB;CAC3D,IAAI,SAAS,IAAI,OAAO;CACxB,MAAM,YAAY,gBAAgB,KAAK,EAAE;CACzC,MAAM,UAAU,gBAAgB,KAAK,GAAG,EAAE,CAAC;CAC3C,KAAK,IAAI,KAAK,KAAK,QAAQ,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,QAAQ,MAAM,KAAK,CAAC,GAAG;EAC5E,MAAM,UAAU,CAAC,aAAa,CAAC,gBAAgB,KAAK,KAAK,EAAE;EAC3D,MAAM,QAAQ,CAAC,WAAW,CAAC,gBAAgB,KAAK,KAAK,KAAK,OAAO;EACjE,IAAI,WAAW,OAAO,OAAO;CAC/B;CACA,OAAO;AACT;;;;;;;;;;;;ACkDA,MAAa,wBAA2C;CACtD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,UAAU,SAAsC,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK;AAYpG,MAAM,WAAW,SAAyB,KAAK,KAAK,CAAC,CAAC,QAAQ,YAAY,IAAI,CAAC,CAAC,QAAQ,YAAY,IAAI;;AAGxG,SAAS,SAAS,MAAyC;CACzD,MAAM,OAAO,KAAK,KAAK;CACvB,IAAI,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,IAAG,GAAG;EAChD,MAAM,MAAM,KAAK,QAAQ,KAAK,IAAK,CAAC;EACpC,OAAO,CAAC,KAAK,MAAM,GAAG,GAAG,GAAG,KAAK,MAAM,MAAM,CAAC,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC;CAC1E;CACA,MAAM,QAAQ,KAAK,QAAQ,GAAG;CAC9B,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,MAAM,GAAG,KAAK,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;AACrF;AAEA,MAAM,YAAY,SAAyB,KAAK,SAAS,KAAK,UAAU,CAAC,CAAC;;AAG1E,SAAS,gBAAgB,MAAc,OAAuB;CAC5D,OAAO,eAAe,KAAK,KAAK,IAAI,QAAQ,GAAG,KAAK,GAAG;AACzD;;AAGA,SAAS,iBAAiB,OAAwC;CAChE,MAAM,MAAoB;EAAE,2BAAW,IAAI,IAAI;EAAG,0BAAU,IAAI,IAAI;EAAG,2BAAW,IAAI,IAAI;CAAE;CAC5F,IAAI,UAAU;CACd,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,OAAO;CACX,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,KAAK,MAAM,IAAI;EACxB,MAAM,SAAS,SAAS,IAAI;EAC5B,MAAM,CAAC,KAAK,SAAS,SAAS,IAAI;EAClC,IAAI,WAAW,GAAG;GAChB,UAAU;GACV;EACF;EACA,IAAI,WAAW,GAAG;GAChB,MAAM;GACN,QAAQ;GACR,IAAI,YAAY,aAAa,IAAI,UAAU,IAAI,qBAAK,IAAI,IAAI,CAAC;GAC7D,IAAI,YAAY,aAAa,IAAI,UAAU,IAAI,KAAK,CAAC,CAAC;GACtD;EACF;EACA,IAAI,YAAY,cAAc,WAAW,KAAK,QAAQ,cAAc,IAAI,SAAS,IAAI,KAAK,KAAK;EAC/F,IAAI,YAAY,aAAa;GAC3B,IAAI,WAAW,GAAG,QAAQ;QACrB,IAAI,WAAW,MAAM,UAAU,kBAAkB,UAAU,yBAAyB,IAAI,UAAU,IAAI,GAAG,CAAC,CAAE,KAAK,gBAAgB,KAAK,QAAQ,KAAK,CAAC,CAAC;EAC5J;EACA,IAAI,YAAY,aAAa;GAC3B,IAAI,WAAW,GAAG,QAAQ;QACrB,IAAI,WAAW,GAAG,OAAO;QACzB,IAAI,WAAW,KAAK,QAAQ,WAAW,IAAI,UAAU,IAAI,GAAG,CAAC,CAAE,IAAI,MAAM,QAAQ,KAAK,CAAC;EAC9F;CACF;CACA,OAAO;AACT;AAEA,MAAM,gBAAgB,QAAwB,IAAI,QAAQ,SAAS,EAAE;;;;;;AAOrE,SAAgB,YAAY,UAAkB,UAAkB,OAAoD;CAElH,MAAM,SADY,SAAS,MAAM,yBAAyB,CAAC,CAAC,QAAQ,SAAS,KAAK,SAAS,YAAY,CAChF,CAAC,CAAC,GAAG,EAAE,KAAK,GAAA,CAAI,MAAM,OAAO;CACpD,MAAM,UAAU,MAAM,MAAM,SAAS,KAAK,WAAW,kBAAkB,CAAC;CACxE,IAAI,YAAY,KAAA,KAAa,CAAC,OAAO,KAAK,QAAQ,QAAQ,MAAM,EAAyB,CAAC,CAAC,GAAG,MAAM,IAAI,MAAM,sCAAsC,WAAW,SAAS,yCAAyC;CACjN,MAAM,MAAM,iBAAiB,KAAK;CAClC,MAAM,WAAW,IAAI,UAAU,IAAI,QAAQ;CAC3C,IAAI,aAAa,KAAA,GAAW,MAAM,IAAI,MAAM,iCAAiC,SAAS,EAAE;CACxF,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,WAAW,SAAS,IAAI,IAAI;EAClC,IAAI,aAAa,KAAA,GAAW,MAAM,IAAI,MAAM,gBAAgB,SAAS,kBAAkB,KAAK,mBAAmB;EAC/G,IAAI,SAAS,WAAW,OAAO,GAAG,MAAM,IAAI,MAAM,cAAc,KAAK,2BAA2B,SAAS,uFAAuF;EAChM,MAAM,QAAQ,gBAAgB,MAAM,QAAQ;EAC5C,MAAM,uBAAO,IAAI,IAAY;EAC7B,MAAM,QAAQ,CAAC,KAAK;EACpB,OAAO,MAAM,SAAS,GAAG;GACvB,MAAM,MAAM,MAAM,IAAI;GACtB,IAAI,KAAK,IAAI,GAAG,GAAG;GACnB,MAAM,eAAe,IAAI,UAAU,IAAI,GAAG;GAC1C,IAAI,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,iCAAiC,IAAI,UAAU,KAAK,WAAW;GAC/G,KAAK,IAAI,GAAG;GACZ,MAAM,KAAK,GAAG,YAAY;EAC5B;EACA,MAAM,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,SAAS,IAAI,aAAa,GAAG,CAAC,KAAK,IAAI;EACnG,OAAO;GAAE;GAAM,SAAS,aAAa,QAAQ;GAAG;EAAQ;CAC1D,CAAC;AACH;AAWA,eAAe,cAAgC;CAC7C,IAAI;EACF,OAAQ,MAAM,OAAO;CACvB,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,iJAAiJ,EAAE,MAAM,CAAC;CAC5K;AACF;AAEA,MAAM,kBAAkB,SAAyB,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG;AAE1E,MAAM,WAAW,KAAa,SAAyB,eAAe,SAAS,KAAK,QAAQ,KAAK,eAAe,IAAI,CAAC,CAAC,CAAC;;AAGvH,SAAS,UAAU,OAAuB;CACxC,OAAO,MAAM,SAAS,CAAC,IAAI,QAAQ,OAAO,KAAK,MAAM,SAAS,QAAQ,CAAC,CAAC,WAAW,QAAQ,IAAI,GAAG,QAAQ;AAC5G;;;;;;;AAQA,eAAsB,UAAU,SAA+C;CAC7E,MAAM,MAAM,QAAQ,QAAQ,GAAG;CAC/B,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,QAAQ,QAAQ,KAAK,UAAU,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;CACvF,IAAI,QAAQ,WAAW,GAAG,MAAM,IAAI,MAAM,wCAAwC;CAClF,MAAM,UAAU,QAAQ,MAAM,UAAU,MAAM,WAAW,IAAI,KAAK,WAAW,KAAK,CAAC;CACnF,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,MAAM,aAAa,QAAQ,cAAc,IAAI,2CAA2C;CAC7H,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,SAAS,MAAM,QAAQ,MAAM;EACjC,OAAO;GAAE,UAAU,QAAQ,KAAK,OAAO,MAAM,gBAAgB,EAAE,WAAW,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI;GAAG,YAAY;GAAK,QAAQ;EAAK;EAC/H,eAAe;EACf,QAAQ;EACR,QAAQ;EACR,UAAU;EACV,OAAO;EACP,SAAS;EACT,aAAa;EACb,kBAAkB;EAClB,eAAe;EACf,UAAU;EACV,UAAU;EAEV,aAAa,CAAC;EACd,UAAU,CAAC,GAAI,QAAQ,SAAS,SAAS,CAAC,CAAE;CAC9C,CAAC;CACD,MAAM,QAAQ,OAAO,eAAe,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI;CAC1E,MAAM,QAAQ,sBAAsB,QAAQ,SAAS,QAAQ,MAAM,IAAI,CAAC;CACxE,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,SAAS,CAAC,CAAC,CAAC;CAC/C,MAAM,YAAY,MAAM,QAAQ,SAAS,CAAC,OAAO,SAAS,IAAI,CAAC;CAC/D,MAAM,WAAW,OAAO,QAAQ,SAAS,CAAC,MAAM,SAAS,IAAI,CAAC;CAC9D,IAAI,UAAU,SAAS,KAAK,SAAS,SAAS,GAC5C,MAAM,IAAI,MACR,uFAAuF,UAAU,KAAK,IAAI,KAAK,SAAS,kCAAkC,SAAS,KAAK,IAAI,KAAK,SAAS,sDAC5L;CAEF,MAAM,yBAAS,IAAI,IAAY;CAC/B,KAAK,MAAM,UAAU,OAAO,OAAO,OAAO,UAAU,WAAW,CAAC,CAAC,GAC/D,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,OAAO,MAAM,GAAG,IAAI,MAAM,gBAAgB,KAAK,UAAU,WAAW,OAAO,IAAI,eAAe,KAAK,CAAC;CAElJ,MAAM,UAAU,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,YAAY,aAAa,QAAQ,KAAK,QAAQ,QAAQ,QAAQ,GAAG,MAAM,GAAG,QAAQ,QAAQ,UAAU,QAAQ,QAAQ,KAAK;CAC9K,MAAM,OAAO,CAAC,GAAG,IAAI,KAAK,QAAQ,aAAa,CAAC,EAAA,CAAG,KAAK,SAAS,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,SAAS,GAAG,KAAK,GAAG,OAAO,UAAU,aAAa,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG;CAC5K,MAAM,QAAwB;EAC5B,MAAM,OAAO,IAAI;EACjB,SAAS,OAAO,KAAK,UAAU,OAAO,CAAC;EACvC,MAAM,OAAO,KAAK,UAAU,IAAI,CAAC;EACjC,SAAS,WAAW,QAAQ;EAC5B,MAAM,QAAQ,QAAQ,QAAQ,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;CACzD;CACA,MAAM,EAAE,SAAS,UAAU,GAAG,WAAW;CACzC,OAAO;EAAE,MAAM,OAAO,KAAK,UAAU,MAAM,CAAC;EAAG;EAAO,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK;EAAG;EAAO;CAAQ;AACnG"}
|
package/build/index.d.ts
CHANGED
|
@@ -1057,6 +1057,11 @@ interface MeasureRule {
|
|
|
1057
1057
|
readonly partners: readonly string[];
|
|
1058
1058
|
/** The smallest median paired difference that counts. Without it, only the test decides, and the output says so. It has no default. */
|
|
1059
1059
|
readonly resolution?: number;
|
|
1060
|
+
/**
|
|
1061
|
+
* The smallest median paired difference that counts for each group. A comparison needs a size over the larger floor of its two groups.
|
|
1062
|
+
* A group with no floor reads `resolution`. A group with neither makes the check throw, so a comparison does not fall back to the test alone.
|
|
1063
|
+
*/
|
|
1064
|
+
readonly floors?: Readonly<Record<string, number>>;
|
|
1060
1065
|
/** The reason and the date, for a rule with no controls. */
|
|
1061
1066
|
readonly noControl?: NoControl;
|
|
1062
1067
|
}
|
|
@@ -1098,7 +1103,9 @@ interface Comparison {
|
|
|
1098
1103
|
readonly tooFew: boolean;
|
|
1099
1104
|
/** This field is true when the shared keys reach `needs` and ties took them under it. So the two read as equal, not as unread. */
|
|
1100
1105
|
readonly tied: boolean;
|
|
1101
|
-
/** The
|
|
1106
|
+
/** The size that this comparison needs: the larger floor of its two groups, else the resolution. It is absent for a measure with neither. */
|
|
1107
|
+
readonly floor?: number;
|
|
1108
|
+
/** The test passes, and the median difference is over `floor` when the comparison has one. */
|
|
1102
1109
|
readonly beats: boolean;
|
|
1103
1110
|
}
|
|
1104
1111
|
/** The read of one control on one measure and one stratum. */
|
|
@@ -1161,7 +1168,7 @@ interface GuardRead {
|
|
|
1161
1168
|
interface MeasureCheck {
|
|
1162
1169
|
readonly measure: string;
|
|
1163
1170
|
readonly sense: "higher" | "lower";
|
|
1164
|
-
/** `test only`: a measure with no resolution. `test and resolution`: a measure with one. */
|
|
1171
|
+
/** `test only`: a measure with no resolution and no floors. `test and resolution`: a measure with one of these. */
|
|
1165
1172
|
readonly basis: "test only" | "test and resolution";
|
|
1166
1173
|
/** The only method of the rows. It is undefined when the rows give none. */
|
|
1167
1174
|
readonly method: string | undefined;
|
|
@@ -1265,7 +1272,7 @@ interface JointClaim {
|
|
|
1265
1272
|
* This function reads the controls and the spread of each measure of a bench.
|
|
1266
1273
|
*
|
|
1267
1274
|
* - A control wins a measure when no real entry beats it on the keys that both finished. A tie counts for the control.
|
|
1268
|
-
* - A real entry beats a control when the Holm-corrected sign test passes and the median paired difference is
|
|
1275
|
+
* - A real entry beats a control when the Holm-corrected sign test passes and the median paired difference clears the bar. The bar is the larger floor of the two, else the resolution.
|
|
1269
1276
|
* - One Holm family holds the tests of one control on one measure over all real entries and all strata.
|
|
1270
1277
|
* - One more Holm family holds the tests of one control against the real entries, with the same need and resolution. A control that beats none of them is weak.
|
|
1271
1278
|
* - One Holm family holds the tests of the spread of one measure over all pairs of real entries and all strata.
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":["Comparison"],"sources":["../src/budget.ts","../src/verdict.ts","../src/limit.ts","../src/merge.ts","../src/gate.ts","../src/control.ts","../src/ladder.ts","../src/model.ts","../src/tail.ts","../src/measures.ts","../src/power.ts","../src/rounds.ts"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":["Comparison"],"sources":["../src/budget.ts","../src/verdict.ts","../src/limit.ts","../src/merge.ts","../src/gate.ts","../src/control.ts","../src/ladder.ts","../src/model.ts","../src/tail.ts","../src/measures.ts","../src/power.ts","../src/rounds.ts"],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -1729,6 +1729,7 @@ function checkedRules(options) {
|
|
|
1729
1729
|
if (!rules.has(partner)) throw new RangeError(`checkMeasures: the partner ${JSON.stringify(partner)} of ${name} has no rule.`);
|
|
1730
1730
|
}
|
|
1731
1731
|
if (rule.resolution !== void 0 && !(Number.isFinite(rule.resolution) && rule.resolution >= 0)) throw new RangeError(`checkMeasures: the resolution of ${name} is finite and 0 or more, not ${rule.resolution}.`);
|
|
1732
|
+
for (const [group, floor] of Object.entries(rule.floors ?? {})) if (!(Number.isFinite(floor) && floor >= 0)) throw new RangeError(`checkMeasures: the floor of ${JSON.stringify(group)} on ${name} is finite and 0 or more, not ${floor}.`);
|
|
1732
1733
|
if (rule.noControl !== void 0) {
|
|
1733
1734
|
if (rule.controls.length > 0) throw new RangeError(`checkMeasures: ${name} has controls and also noControl.`);
|
|
1734
1735
|
checkedDate(`the date of ${name}`, rule.noControl.until);
|
|
@@ -1786,8 +1787,19 @@ function rawTest(better, other, stratum, groups, sense) {
|
|
|
1786
1787
|
medianDifference: median(differences)
|
|
1787
1788
|
};
|
|
1788
1789
|
}
|
|
1790
|
+
/** This function gives the size that a comparison of two groups needs. The value is undefined only for a rule with no floors and no resolution. */
|
|
1791
|
+
function floorOf(rule, a, b) {
|
|
1792
|
+
const floors = rule.floors;
|
|
1793
|
+
if (floors === void 0) return rule.resolution;
|
|
1794
|
+
const ofGroup = (group) => {
|
|
1795
|
+
const floor = Object.hasOwn(floors, group) ? floors[group] : rule.resolution;
|
|
1796
|
+
if (floor === void 0) throw new RangeError(`checkMeasures: the group ${JSON.stringify(group)} of ${JSON.stringify(rule.measure)} has no floor, and the rule has no resolution.`);
|
|
1797
|
+
return floor;
|
|
1798
|
+
};
|
|
1799
|
+
return Math.max(ofGroup(a), ofGroup(b));
|
|
1800
|
+
}
|
|
1789
1801
|
/** This function corrects one family of tests, over all strata. A test with too few keys beats nothing, even when a step of Holm passes it. */
|
|
1790
|
-
function corrected(tests, sides, alpha,
|
|
1802
|
+
function corrected(tests, sides, alpha, rule) {
|
|
1791
1803
|
if (tests.length === 0) return [];
|
|
1792
1804
|
const family = tests.length;
|
|
1793
1805
|
const needs = pairsNeeded(alpha / family, sides);
|
|
@@ -1797,7 +1809,8 @@ function corrected(tests, sides, alpha, resolution) {
|
|
|
1797
1809
|
const n = t.wins + t.losses;
|
|
1798
1810
|
const tooFew = n < needs;
|
|
1799
1811
|
const size = sides === 1 ? t.medianDifference : Math.abs(t.medianDifference);
|
|
1800
|
-
const
|
|
1812
|
+
const floor = floorOf(rule, t.better, t.other);
|
|
1813
|
+
const clears = floor === void 0 || size > floor;
|
|
1801
1814
|
return {
|
|
1802
1815
|
...t,
|
|
1803
1816
|
n,
|
|
@@ -1808,6 +1821,7 @@ function corrected(tests, sides, alpha, resolution) {
|
|
|
1808
1821
|
needs,
|
|
1809
1822
|
tooFew,
|
|
1810
1823
|
tied: tooFew && t.shared >= needs,
|
|
1824
|
+
...floor === void 0 ? {} : { floor },
|
|
1811
1825
|
beats: !tooFew && steps[i]?.reject === true && clears
|
|
1812
1826
|
};
|
|
1813
1827
|
});
|
|
@@ -1818,8 +1832,8 @@ const unread = (c) => c.tooFew && !c.tied;
|
|
|
1818
1832
|
/** The reads of one control on one measure: one for each stratum of the measure. */
|
|
1819
1833
|
function controlReads(measure, control, data, rule, real, alpha) {
|
|
1820
1834
|
if (data === void 0) return [];
|
|
1821
|
-
const comparisons = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry) => rawTest(entry, control, stratum, groups, rule.sense) ?? [])), 1, alpha, rule
|
|
1822
|
-
const attacks = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry) => rawTest(control, entry, stratum, groups, rule.sense) ?? [])), 1, alpha, rule
|
|
1835
|
+
const comparisons = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry) => rawTest(entry, control, stratum, groups, rule.sense) ?? [])), 1, alpha, rule);
|
|
1836
|
+
const attacks = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry) => rawTest(control, entry, stratum, groups, rule.sense) ?? [])), 1, alpha, rule);
|
|
1823
1837
|
return [...data.keys()].map((stratum) => {
|
|
1824
1838
|
const here = comparisons.filter((c) => c.stratum === stratum);
|
|
1825
1839
|
const against = attacks.filter((c) => c.stratum === stratum);
|
|
@@ -1838,7 +1852,7 @@ function controlReads(measure, control, data, rule, real, alpha) {
|
|
|
1838
1852
|
}
|
|
1839
1853
|
/** The read of the spread of one measure: one family over all pairs of real entries and all strata. */
|
|
1840
1854
|
function spreadReads(data, rule, real, alpha) {
|
|
1841
|
-
const comparisons = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry, i) => real.slice(i + 1).flatMap((other) => rawTest(entry, other, stratum, groups, rule.sense) ?? []))), 2, alpha, rule
|
|
1855
|
+
const comparisons = corrected([...data].flatMap(([stratum, groups]) => real.flatMap((entry, i) => real.slice(i + 1).flatMap((other) => rawTest(entry, other, stratum, groups, rule.sense) ?? []))), 2, alpha, rule);
|
|
1842
1856
|
return {
|
|
1843
1857
|
reads: [...data.keys()].map((stratum) => {
|
|
1844
1858
|
const here = comparisons.filter((c) => c.stratum === stratum);
|
|
@@ -2037,7 +2051,7 @@ function checkRule(rule, reading, asOf) {
|
|
|
2037
2051
|
});
|
|
2038
2052
|
else if (testable.length > 0 && !comparisons.some((c) => c.beats)) {
|
|
2039
2053
|
const widest = Math.max(...comparisons.map((c) => Math.abs(c.medianDifference)));
|
|
2040
|
-
const basis = rule.resolution === void 0 ? "by the test only" : `by the test and the resolution ${rule.resolution}`;
|
|
2054
|
+
const basis = rule.floors !== void 0 ? "by the test and the floors of the groups" : rule.resolution === void 0 ? "by the test only" : `by the test and the resolution ${rule.resolution}`;
|
|
2041
2055
|
findings.push({
|
|
2042
2056
|
kind: "no-spread",
|
|
2043
2057
|
measure: rule.measure,
|
|
@@ -2050,7 +2064,7 @@ function checkRule(rule, reading, asOf) {
|
|
|
2050
2064
|
check: {
|
|
2051
2065
|
measure: rule.measure,
|
|
2052
2066
|
sense: rule.sense,
|
|
2053
|
-
basis: rule.resolution === void 0 ? "test only" : "test and resolution",
|
|
2067
|
+
basis: rule.resolution === void 0 && rule.floors === void 0 ? "test only" : "test and resolution",
|
|
2054
2068
|
method: mixed ? void 0 : methods[0],
|
|
2055
2069
|
mixed,
|
|
2056
2070
|
guards,
|
|
@@ -2067,7 +2081,7 @@ function checkRule(rule, reading, asOf) {
|
|
|
2067
2081
|
* This function reads the controls and the spread of each measure of a bench.
|
|
2068
2082
|
*
|
|
2069
2083
|
* - A control wins a measure when no real entry beats it on the keys that both finished. A tie counts for the control.
|
|
2070
|
-
* - A real entry beats a control when the Holm-corrected sign test passes and the median paired difference is
|
|
2084
|
+
* - A real entry beats a control when the Holm-corrected sign test passes and the median paired difference clears the bar. The bar is the larger floor of the two, else the resolution.
|
|
2071
2085
|
* - One Holm family holds the tests of one control on one measure over all real entries and all strata.
|
|
2072
2086
|
* - One more Holm family holds the tests of one control against the real entries, with the same need and resolution. A control that beats none of them is weak.
|
|
2073
2087
|
* - One Holm family holds the tests of the spread of one measure over all pairs of real entries and all strata.
|