@mark1russell7/rigor-bench 0.2.21 → 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 CHANGED
@@ -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. |
@@ -400,6 +402,52 @@ A run of many jobs sets `roundOffset` to `job * rounds`. Then the merged records
400
402
 
401
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.
402
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
+
403
451
  ## Run cases with a time budget
404
452
 
405
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":"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"}
@@ -1,6 +1,6 @@
1
1
  import { t as stopTree } from "./tree.js";
2
- import { spawn } from "node:child_process";
3
2
  import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { spawn } from "node:child_process";
4
4
  // src/supervise.ts
5
5
  /**
6
6
  * The runner of cases with a time budget. A case runs in a child process, so a hard stop works on code that does not return.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/rigor-bench",
4
- "version": "0.2.21",
4
+ "version": "0.2.22",
5
5
  "description": "The paired-time records of a bench: the order that turns each round, the ratio of each case, and the time verdict against the band of an A/A arm.",
6
6
  "keywords": [
7
7
  "benchmark",
@@ -31,6 +31,10 @@
31
31
  "types": "./build/index.d.ts",
32
32
  "import": "./build/index.js"
33
33
  },
34
+ "./code-stamp": {
35
+ "types": "./build/code-stamp.d.ts",
36
+ "import": "./build/code-stamp.js"
37
+ },
34
38
  "./fresh": {
35
39
  "types": "./build/fresh.d.ts",
36
40
  "import": "./build/fresh.js"
@@ -51,12 +55,21 @@
51
55
  "node": ">=22"
52
56
  },
53
57
  "dependencies": {
54
- "@mark1russell7/rigor-generators": "^0.1.5",
55
- "@mark1russell7/rigor-statistics": "^0.1.13"
58
+ "@mark1russell7/rigor-generators": "^0.1.6",
59
+ "@mark1russell7/rigor-statistics": "^0.1.14"
60
+ },
61
+ "peerDependencies": {
62
+ "esbuild": "^0.28.2"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "esbuild": {
66
+ "optional": true
67
+ }
56
68
  },
57
69
  "devDependencies": {
58
70
  "@mark1russell7/cue": "^2.0.0",
59
71
  "@types/node": "^26.6.5",
72
+ "esbuild": "^0.28.2",
60
73
  "tsdown": "^0.23.1",
61
74
  "typescript": "^7.0.2",
62
75
  "vitest": "^5.0.3"