@williamthorsen/toolbelt.numbers 7.0.3 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.34.0";
3
+ export const __readyupVersion = "0.36.0";
4
4
 
5
5
 
6
6
  // ../adoption/src/conventions/path-predicates.ts
@@ -50,12 +50,17 @@ import {
50
50
  countPackageUsage,
51
51
  readTrackedSources
52
52
  } from "readyup/check-utils";
53
- var NOT_A_REPO = "the project is not a git working tree, and these checks read the files git tracks";
53
+ var NOT_A_REPO = "the project is not a git working tree, and these checks read the files that git tracks";
54
54
  var NOTHING_TO_REPORT = { findings: [] };
55
55
  function defineAdoptionKit(spec) {
56
56
  assertCheckIdsAreUnique();
57
57
  const cache = {};
58
58
  const adoptedPackage = { exportNames: spec.exportNames, packageName: spec.packageName };
59
+ const kitScope = { noSourcesReason: spec.noSourcesReason, pathFilter: spec.pathFilter };
60
+ const pathFiltersByKind = mapPathFiltersByKind();
61
+ const sweptPathFilters = [
62
+ .../* @__PURE__ */ new Set([spec.pathFilter, ...spec.checks.map((check) => resolveScope(check).pathFilter)])
63
+ ];
59
64
  return defineRdyKit({
60
65
  description: spec.description,
61
66
  defaultSeverity: "warn",
@@ -66,7 +71,7 @@ function defineAdoptionKit(spec) {
66
71
  name: check.name,
67
72
  id: check.id,
68
73
  ...check.severity !== void 0 && { severity: check.severity },
69
- skip: skipUnlessProjectHoldsSources,
74
+ skip: () => skipUnlessProjectHoldsSources(resolveScope(check)),
70
75
  check: () => reportKinds(check.kinds),
71
76
  fix: check.fix
72
77
  }))
@@ -85,16 +90,38 @@ function defineAdoptionKit(spec) {
85
90
  throw new Error(`${spec.packageName}'s kit gives one id to more than one check: ${ids}`);
86
91
  }
87
92
  }
93
+ function isSweptPath(path) {
94
+ return sweptPathFilters.some((pathFilter) => pathFilter(path));
95
+ }
88
96
  function loadSummary() {
89
97
  cache.summary ??= readProject();
90
98
  return cache.summary;
91
99
  }
100
+ function mapPathFiltersByKind() {
101
+ const pathFilters = /* @__PURE__ */ new Map();
102
+ const conflicted = /* @__PURE__ */ new Set();
103
+ for (const check of spec.checks) {
104
+ const { pathFilter } = resolveScope(check);
105
+ for (const kind of check.kinds) {
106
+ const assigned = pathFilters.get(kind);
107
+ if (assigned !== void 0 && assigned !== pathFilter) conflicted.add(kind);
108
+ pathFilters.set(kind, pathFilter);
109
+ }
110
+ }
111
+ if (conflicted.size > 0) {
112
+ const kinds = [...conflicted].toSorted().join(", ");
113
+ throw new Error(`${spec.packageName}'s kit reads one kind through more than one path filter: ${kinds}`);
114
+ }
115
+ return pathFilters;
116
+ }
92
117
  async function readProject() {
93
- const sources = await readTrackedSources(spec.pathFilter);
118
+ const sources = await readTrackedSources(isSweptPath);
94
119
  if (sources === void 0) return void 0;
95
120
  return {
96
121
  adoptedCount: countPackageUsage(sources, adoptedPackage),
97
- findings: sources.flatMap((source) => spec.detect(source.text).map((site) => ({ ...site, path: source.path }))),
122
+ findings: sources.flatMap(
123
+ (source) => spec.detect(source.text).filter((site) => (pathFiltersByKind.get(site.kind) ?? spec.pathFilter)(source.path)).map((site) => ({ ...site, path: source.path }))
124
+ ),
98
125
  sources
99
126
  };
100
127
  }
@@ -108,10 +135,13 @@ function defineAdoptionKit(spec) {
108
135
  shouldReport: (finding) => kinds.includes(finding.kind)
109
136
  });
110
137
  }
111
- async function skipUnlessProjectHoldsSources() {
138
+ function resolveScope(check) {
139
+ return check.pathFilter === void 0 ? kitScope : { noSourcesReason: check.noSourcesReason, pathFilter: check.pathFilter };
140
+ }
141
+ async function skipUnlessProjectHoldsSources(scope) {
112
142
  const summary = await loadSummary();
113
143
  if (summary === void 0) return NOT_A_REPO;
114
- return summary.sources.length === 0 ? spec.noSourcesReason : false;
144
+ return summary.sources.some((source) => scope.pathFilter(source.path)) ? false : scope.noSourcesReason;
115
145
  }
116
146
  }
117
147
 
@@ -120,6 +150,14 @@ function condenseWhitespace(text) {
120
150
  return text.replaceAll(/\s+/g, " ");
121
151
  }
122
152
 
153
+ // ../adoption/src/portable/readAnchoredWindow.ts
154
+ function readAnchoredWindow(source, offset, lengths) {
155
+ return {
156
+ after: condenseWhitespace(source.slice(offset, offset + lengths.lookahead)),
157
+ before: condenseWhitespace(source.slice(Math.max(0, offset - lengths.lookbehind), offset))
158
+ };
159
+ }
160
+
123
161
  // ../adoption/src/portable/readBalancedGroup.ts
124
162
  var PARENTHESES = { close: ")", open: "(" };
125
163
  function readBalancedGroup(source, from, delimiters) {
@@ -136,14 +174,6 @@ function readBalancedGroup(source, from, delimiters) {
136
174
  return void 0;
137
175
  }
138
176
 
139
- // ../adoption/src/portable/readAnchoredWindow.ts
140
- function readAnchoredWindow(source, offset, lengths) {
141
- return {
142
- after: condenseWhitespace(source.slice(offset, offset + lengths.lookahead)),
143
- before: condenseWhitespace(source.slice(Math.max(0, offset - lengths.lookbehind), offset))
144
- };
145
- }
146
-
147
177
  // ../adoption/src/mod.ts
148
178
  import { blankNonCode, getLineAtOffset } from "readyup/check-utils";
149
179
 
@@ -273,8 +303,8 @@ var default_default = defineAdoptionKit({
273
303
  exportNames: ADOPTED_EXPORTS,
274
304
  noSourcesReason: "the project holds no JavaScript or TypeScript sources outside the exempt paths",
275
305
  packageName: PACKAGE_NAME,
276
- // A test computes these values deliberately, and a bootstrap wrapper's hand-rolled arithmetic is what keeps
277
- // its build-first message alive through an incomplete install.
306
+ // A test computes these values deliberately, and a bootstrap wrapper's hand-rolled arithmetic keeps its
307
+ // build-first message alive through an incomplete install.
278
308
  pathFilter: isAdoptableSource,
279
309
  checks: [
280
310
  {
@@ -289,7 +319,7 @@ var default_default = defineAdoptionKit({
289
319
  id: "no-hand-rolled-round",
290
320
  kinds: ["round-scale"],
291
321
  severity: "recommend",
292
- fix: `Replace each expression named above with round from ${PACKAGE_NAME}/candidate, called as round(value, places). The substitution is exact: round scales by the same power of ten these sites write out. Reference: ${README_URL}`
322
+ fix: `Replace each expression named above with round from ${PACKAGE_NAME}/candidate, called as round(value, places). The substitution is exact: round scales by the same power of ten that these sites write out. Reference: ${README_URL}`
293
323
  },
294
324
  {
295
325
  name: "No source derives a random integer by hand",
@@ -9,82 +9,97 @@
9
9
  "esbuildVersion": "0.28.2",
10
10
  "inputs": [
11
11
  {
12
- "hash": "9cbd3541",
12
+ "hash": "1dc83619",
13
13
  "kind": "module",
14
14
  "path": "../../adoption/src/conventions/path-predicates.ts"
15
15
  },
16
16
  {
17
- "hash": "c7d90b66",
17
+ "hash": "0237a871",
18
18
  "kind": "module",
19
19
  "path": "../../adoption/src/conventions/site-handoffs.ts"
20
20
  },
21
21
  {
22
- "hash": "3a124dad",
22
+ "hash": "8fb83c2d",
23
23
  "kind": "module",
24
24
  "path": "../../adoption/src/kits/defineAdoptionKit.ts"
25
25
  },
26
26
  {
27
- "hash": "6e5a8dda",
27
+ "hash": "10596562",
28
28
  "kind": "module",
29
29
  "path": "../../adoption/src/mod.ts"
30
30
  },
31
31
  {
32
- "hash": "e8d0444d",
32
+ "hash": "6fdaacac",
33
33
  "kind": "module",
34
34
  "path": "../../adoption/src/portable/condenseWhitespace.ts"
35
35
  },
36
36
  {
37
- "hash": "b6d99ce7",
37
+ "hash": "0661f0dd",
38
+ "kind": "module",
39
+ "path": "../../adoption/src/portable/listDirectoryAscents.ts"
40
+ },
41
+ {
42
+ "hash": "07c8358f",
38
43
  "kind": "module",
39
44
  "path": "../../adoption/src/portable/listFunctionBodies.ts"
40
45
  },
41
46
  {
42
- "hash": "4d2d8e11",
47
+ "hash": "2d9f1edb",
48
+ "kind": "module",
49
+ "path": "../../adoption/src/portable/listTemplateLiterals.ts"
50
+ },
51
+ {
52
+ "hash": "b0751395",
43
53
  "kind": "module",
44
54
  "path": "../../adoption/src/portable/readAnchoredWindow.ts"
45
55
  },
46
56
  {
47
- "hash": "498e3ca4",
57
+ "hash": "0eb1daf1",
48
58
  "kind": "module",
49
59
  "path": "../../adoption/src/portable/readBalancedGroup.ts"
50
60
  },
51
61
  {
52
- "hash": "e6df5682",
62
+ "hash": "e9427d50",
63
+ "kind": "module",
64
+ "path": "../../adoption/src/portable/readLiteral.ts"
65
+ },
66
+ {
67
+ "hash": "24f6e6db",
53
68
  "kind": "module",
54
69
  "path": "kits/default.ts"
55
70
  },
56
71
  {
57
- "hash": "f3d9105b",
72
+ "hash": "05bd292b",
58
73
  "kind": "module",
59
74
  "path": "../src/readiness/adoptedExports.ts"
60
75
  },
61
76
  {
62
- "hash": "54ee66e5",
77
+ "hash": "f14085f3",
63
78
  "kind": "module",
64
79
  "path": "../src/readiness/listClampNestLines.ts"
65
80
  },
66
81
  {
67
- "hash": "47d35285",
82
+ "hash": "eabb769f",
68
83
  "kind": "module",
69
84
  "path": "../src/readiness/listMathIdioms.ts"
70
85
  },
71
86
  {
72
- "hash": "fa0a23b7",
87
+ "hash": "827c07b2",
73
88
  "kind": "module",
74
89
  "path": "../src/readiness/listRandomIntegerLines.ts"
75
90
  },
76
91
  {
77
- "hash": "ae0ac903",
92
+ "hash": "bdcac255",
78
93
  "kind": "module",
79
94
  "path": "../src/readiness/listRoundScaleLines.ts"
80
95
  }
81
96
  ],
82
97
  "name": "default",
83
98
  "path": "kits/default.js",
84
- "readyupVersion": "0.34.0",
99
+ "readyupVersion": "0.36.0",
85
100
  "source": "kits/default.ts",
86
- "sourceHash": "e6df5682",
87
- "targetHash": "44b320aa"
101
+ "sourceHash": "24f6e6db",
102
+ "targetHash": "b1603ae6"
88
103
  }
89
104
  ]
90
105
  }
package/CHANGELOG.md CHANGED
@@ -2,9 +2,115 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 7.2.0 — 2026-09-15
6
+
7
+ ### 🎉 Features
8
+
9
+ - Add a ReadyUp adoption kit reporting hand-rolled guards (#311)
10
+
11
+ - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.guards` that reports every function whose entire body re-implements a guard published by the package.
12
+
13
+ - Add a ReadyUp adoption kit to toolbelt.filesystem (#316)
14
+
15
+ - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.filesystem` that recommends `writeAtomic` where a project writes a file to a temporary path and renames it into place, and `listDirectoryChain`, `findDirectoryChainMatch`, or `listDirectoryChainMatches` where a loop ascends to the filesystem root by `path.dirname`.
16
+ - Promotes `writeAtomic` to the candidate tier.
17
+
18
+ Migration: Change any import of `writeAtomic` from `@williamthorsen/toolbelt.filesystem/proposed` to `@williamthorsen/toolbelt.filesystem/candidate`.
19
+
20
+ - Read a directory walk's probed name through a binding or constant (#324)
21
+
22
+ - Extends the ReadyUp adoption kit in `@williamthorsen/toolbelt.packaging` to recommend `findPackageRoot` or `resolveSelfVersion` for a hand-rolled `package.json` search that checks for the file through a variable declared once inside its loop and never reassigned there, or through a string constant declared once in the same file.
23
+ - Stops the kit in `@williamthorsen/toolbelt.filesystem` from reporting such a search, which it previously treated as a generic directory walk.
24
+
25
+ - Report hand-rolled dedents in the strings adoption kit (#330)
26
+
27
+ - Adds `no-joined-line-array`, which reports an array of string or template literals that spans several lines and is joined with a newline.
28
+ - Adds `no-layout-breaking-template`, which reports an untagged template literal whose later lines drop below the indentation of the line on which it opens.
29
+
30
+ ### 🐛 Bug fixes
31
+
32
+ - Stop reading each segment of a probe's path as a separately probed name (#320)
33
+
34
+ - Fixes an issue in which `toolbelt.filesystem`'s ReadyUp adoption kit treated a loop over parent directories as a search for each directory's own `package.json` when the path checked at each level contained a `'package.json'` literal, as in `path.join(dir, 'node_modules', name, 'package.json')`, and so did not report the loop under `no-hand-rolled-directory-walk`.
35
+
36
+ - Make pickInteger draw once per call and keep seeded draws below 1 (#329)
37
+
38
+ - Fixes seeded draws that could return exactly 1, outside the documented range of [0, 1): With seed `1_000_000_856_026_238`, `pickInteger({ min: 0, max: 9, seed })` returned 10 and `pickItem` from `toolbelt.arrays` threw a `RangeError`.
39
+ - Changes only the draw that returned 1, which no `SeededRng` and no integer seed below 2³¹ could produce.
40
+ - Stops `pickInteger` from skipping its draw when `min` and `max` truncate to the same integer, which changes no return value but shifts the later values drawn from a `SeededRng` or seed function passed with such bounds.
41
+
42
+ ### ♻️ Refactoring
43
+
44
+ - Move directory-walk recognition from filesystem into packages/adoption (#318)
45
+
46
+ - Adds `listDirectoryAscents` to `packages/adoption`, which reports each directory ascent once with the names probed by its innermost loop, and reduces `filesystem`'s `listChainWalkSites` to a partition of that output, so a `toolbelt.packaging` kit can partition the same ascents without its own copy of the recognition.
47
+ - Renames the hand-off rule `isProjectRootSearch` to `isManifestSearch` and rewrites the text in `packages/adoption` and `filesystem` that credited `findProjectRoot` alone with a `package.json` walk.
48
+ - Stops `filesystem`'s kit from reporting a loop that ascends one binding around an inner loop ascending another binding and probing for `package.json`, which is the only finding changed by the move.
49
+
50
+ ### 🧪 Tests
51
+
52
+ - Move the rdy run report reader into the adoption test utilities (#323)
53
+
54
+ - Replaces the `rdy run --json` report reader copied into each of the twelve `pragma-suppression.tool.test.ts` suites with `listKitCheckReports`, a helper added to `@williamthorsen/toolbelt.adoption/test-utils` that runs a package's compiled kit over a fixture repo and returns its check reports, so a change to the shape of readyup's report needs one edit rather than twelve.
55
+ - Fixes the error thrown for a kit that does not load: Each copy discarded the load error recorded by `rdy` on the kit's entry and threw "the run reported no adoption checks", and the helper throws with `rdy`'s own message instead.
56
+
57
+ ### ⚙️ Tooling
58
+
59
+ - Remove the stale repo-local cliff.toml and normalize changelog titles (#327)
60
+
61
+ - Stops `release-kit prepare` from printing a "skipped due to grouping error(s)" warning for each releasable workspace by letting it resolve the git-cliff template bundled with release-kit, previously overridden by the root `cliff.toml`.
62
+ - Excludes commits without a ticket prefix from future changelog entries.
63
+ - Renames the section titles in every `packages/*/.meta/changelog.json`, except `Dependency updates`, to the headings of release-kit's work-type taxonomy, such as "🎉 Features" and "🏗️ Internal features", and regenerates each `CHANGELOG.md` so that release-kit orders existing and new sections by the same rule.
64
+ - Causes the next `release-kit prepare` to plan patch releases of `dstructs`, `hof`, and `sets`, which had no other commits since their last release, because the changelog commit touches every workspace.
65
+
66
+ ### 📚 Documentation
67
+
68
+ - Align prose with plain-speech doctrine and writing conventions (#306)
69
+
70
+ - Copy-edits prose across the repo: comments, test names, package READMEs, and `AGENTS.md`.
71
+ - Rewrites a few user-facing strings as well, among them `configure-project`'s help text and the errors from `parseProjectSpec`, `securityCommands`, and `hashString`.
72
+
73
+ ## 7.1.0 — 2026-09-06
74
+
75
+ ### 🎉 Features
76
+
77
+ - Add a ReadyUp adoption kit (#253)
78
+
79
+ Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.objects`. The kit recommends `Object.hasOwn` or the package's own `hasOwnProperty` in place of a call reached through `Object.prototype`, and `isRecord` or `isRecordOrArray` in place of a guard written as `typeof value === 'object' && value !== null`. Its third check warns that a comparison of two `JSON.stringify` calls is key-order dependent and should be replaced by `isEqual`.
80
+
81
+ - Add a ReadyUp adoption kit reporting hand-rolled sleeps (#303)
82
+
83
+ - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.async` that recommends the use of `delay` to replace a hand-rolled sleep.
84
+
85
+ ### 📚 Documentation
86
+
87
+ - Document the generated-source exemption in the adoption kits (#251)
88
+
89
+ Documents the generated- and vendored-source exemption in the `errors`, `numbers`, and `strings` READMEs.
90
+
91
+ - Repair reduced object relatives in passages recurring across files (#262)
92
+
93
+ Repairs the reduced object relative in the prose passages that recur across more than one file, in package READMEs, source comments, test titles, and the ReadyUp kits' check messages.
94
+
95
+ - Repair reduced object relatives in the READMEs and AGENTS.md (#263)
96
+
97
+ Repairs the reduced object relative in `AGENTS.md`, the root `README.md`, and the package READMEs.
98
+
99
+ - Repair reduced object relatives in packages/adoption (#264)
100
+
101
+ Repairs the reduced object relative in `packages/adoption`, in source comments, doc descriptions, and test titles.
102
+
103
+ - Repair reduced object relatives in the readiness modules and kits (#265)
104
+
105
+ Repairs the reduced object relative in the six kit-bearing packages' readiness modules and ReadyUp kit sources, across comments, doc descriptions, test titles, and the kits' check messages.
106
+
107
+ - Repair the repository's prose and record every rejection's ground (#290)
108
+
109
+ Applies one repo-wide `revise-prose` sweep across the repository's READMEs, `AGENTS.md`, source comments, doc descriptions, and test names.
110
+
5
111
  ## 7.0.3 — 2026-08-30
6
112
 
7
- ### Bug fixes
113
+ ### 🐛 Bug fixes
8
114
 
9
115
  - Reject a check declaring a kind its detector never produces (#246)
10
116
 
@@ -12,21 +118,15 @@ All notable changes to this project will be documented in this file.
12
118
 
13
119
  ## 7.0.2 — 2026-08-28
14
120
 
15
- ### Refactoring
121
+ ### ♻️ Refactoring
16
122
 
17
123
  - Upgrade eslint-config-typescript to v12.0.1 and satisfy its new rules (#236)
18
124
 
19
125
  Upgrades `@williamthorsen/eslint-config-typescript` to v12 and fixes violations surfaced by the new rules banning unpublished barrels and floating disposables.
20
126
 
21
- ### Dependencies
22
-
23
- - Upgrade all deps to latest version
24
- - Upgrade deps to latest version
25
- - Upgrade all deps to latest version
26
-
27
127
  ## 7.0.1 — 2026-08-24
28
128
 
29
- ### Bug fixes
129
+ ### 🐛 Bug fixes
30
130
 
31
131
  - Stop the adoption kits from blanking code after `++`, `!`, and keyword-named members (#208)
32
132
 
@@ -34,7 +134,7 @@ All notable changes to this project will be documented in this file.
34
134
 
35
135
  `@williamthorsen/toolbelt.adoption` now takes `blankNonCode` and `getLineAtOffset` from `readyup/check-utils` in place of the copies it held, which had fallen behind readyup's on those three cases.
36
136
 
37
- ### Internal
137
+ ### 🏗️ Internal features
38
138
 
39
139
  - Migrate the adoption kits onto FindingOutcome and add check ids (#217)
40
140
 
@@ -50,7 +150,7 @@ All notable changes to this project will be documented in this file.
50
150
 
51
151
  ## 7.0.0 — 2026-08-21
52
152
 
53
- ### Features
153
+ ### 🎉 Features
54
154
 
55
155
  - 🚨 **Breaking:** Promote clamp to the candidate tier with a stricter bounds contract (#185)
56
156
 
@@ -62,25 +162,21 @@ All notable changes to this project will be documented in this file.
62
162
 
63
163
  Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.numbers`. When run against a project, the kit identifies hand-rolled code that can be replaced by the package's `clamp`, `round`, or `pickInteger` functions.
64
164
 
65
- ### Bug fixes
165
+ ### 🐛 Bug fixes
66
166
 
67
167
  - Blank comments and literals before a detector reads a source (#191)
68
168
 
69
169
  Fixes the issue that the `errors`, `numbers`, and `vitest` adoption kits' detectors did not distinguish code from comments and literals, and so flagged a pattern written in a comment or a string as a candidate replacement site. Each detector now blanks every comment, string, template literal, and regular expression before its anchor scan, leaving interpolated expressions intact.
70
170
 
71
- ### Refactoring
171
+ ### ♻️ Refactoring
72
172
 
73
173
  - Break the workspace dependency cycle by making `adoption` a leaf (#195)
74
174
 
75
175
  Fixes a cyclic dependency among packages in the repo. `packages/adoption` is now a workspace leaf: It declares no workspace dependency, and its test scaffolding is held to node builtins. A new root test fails on any cycle in the workspace dependency graph.
76
176
 
77
- ### Dependencies
78
-
79
- - Upgrade all deps to latest version
80
-
81
177
  ## 6.0.1 — 2026-08-13
82
178
 
83
- ### Tooling
179
+ ### ⚙️ Tooling
84
180
 
85
181
  - Remove redundant .gitignore files
86
182
  - Populate manifest metadata and adopt a pnpm catalog (#140)
@@ -89,13 +185,13 @@ All notable changes to this project will be documented in this file.
89
185
 
90
186
  ## 6.0.0 — 2026-08-12
91
187
 
92
- ### Features
188
+ ### 🎉 Features
93
189
 
94
190
  - 🚨 **Breaking:** Rename get* functions by return kind and verb specificity (#119)
95
191
 
96
192
  Renames thirteen functions across various packages to align with a consistent naming pattern.
97
193
 
98
- ### Refactoring
194
+ ### ♻️ Refactoring
99
195
 
100
196
  - Stop shipping test-only and dead support modules (#117)
101
197
 
@@ -107,17 +203,13 @@ All notable changes to this project will be documented in this file.
107
203
 
108
204
  ## 5.0.0 — 2026-08-08
109
205
 
110
- ### Features
206
+ ### 🎉 Features
111
207
 
112
208
  - 🚨 **Breaking:** Spawn seeded number generator of same subclass (#84)
113
209
 
114
210
  A seeded generator spawned from a subclass now matches that subclass, so `IntSeededRng.withSeed` supplies the wrapped function integers where it previously supplied floats. Callers relying on values derived through that path must re-baseline. A detached reference such as `const { withSeed } = SeededRng` now throws.
115
211
 
116
- - Use underscore separator at 4 digits or more
117
-
118
- Changes the `unicorn/numeric-separators-style` rule config so that separators are consistently used in base 10 numbers, instead of exempting numbers of 5 digits or less.
119
-
120
- ### Refactoring
212
+ ### ♻️ Refactoring
121
213
 
122
214
  - Fixes violations surfaced by newly active lint rules (#84)
123
215
  - Fix slug punctuation and require safe integers (#86)
@@ -126,9 +218,7 @@ All notable changes to this project will be documented in this file.
126
218
  - Time-unit conversions, scaling range bounds, normal-distribution interval counts, and array indices in object paths now reject values too large to represent exactly instead of silently losing precision.
127
219
  - Seeded number generators now produce distinct sequences for seeds at or beyond 2^53, where adjacent seeds previously collapsed onto nearly identical output. A seed of that size saved before this release no longer reproduces the same output.
128
220
 
129
- - Fix lint
130
-
131
- ### Tooling
221
+ ### ⚙️ Tooling
132
222
 
133
223
  - Migrate Vitest configs to the nmr projects model (#73)
134
224
 
@@ -146,35 +236,31 @@ All notable changes to this project will be documented in this file.
146
236
 
147
237
  All packages now have identical compiler settings, using the settings from the `@williamthorsen/tsconfig` base config without modification.
148
238
 
149
- ### Dependencies
150
-
151
- - Upgrade all deps to latest version
152
-
153
239
  ## 4.3.8 — 2026-07-27
154
240
 
155
- ### Tooling
241
+ ### ⚙️ Tooling
156
242
 
157
243
  - Normalize Vitest, and lint configs
158
244
 
159
- ### Documentation
245
+ ### 📚 Documentation
160
246
 
161
247
  - Change license to ISC
162
248
 
163
249
  ## 4.3.7 — 2026-07-24
164
250
 
165
- ### Tooling
251
+ ### ⚙️ Tooling
166
252
 
167
253
  - Configure release-kit & repo labels
168
254
 
169
255
  ## 4.3.6 — 2026-07-20
170
256
 
171
- ### Bug fixes
257
+ ### 🐛 Bug fixes
172
258
 
173
259
  - Add repository field to package manifests for npm provenance (#65)
174
260
 
175
261
  Fixes an issue that prevented every package from publishing to npm. Each package now links to its source repository from its npm page.
176
262
 
177
- ### Dependencies
263
+ ### 📦 Dependencies
178
264
 
179
265
  - Upgrade ESLint packages and migrate to TypeScript 6 (#67)
180
266
 
@@ -182,7 +268,7 @@ All notable changes to this project will be documented in this file.
182
268
 
183
269
  ## 4.3.5 — 2026-07-20
184
270
 
185
- ### Tooling
271
+ ### ⚙️ Tooling
186
272
 
187
273
  - Migrate to the nmr toolchain and resolve dependency vulnerabilities (#45)
188
274
 
@@ -190,37 +276,13 @@ All notable changes to this project will be documented in this file.
190
276
 
191
277
  ## 4.3.4 — 2026-03-19
192
278
 
193
- ### Formatting
279
+ ### 🎨 Formatting
194
280
 
195
281
  - Format changelogs
196
282
 
197
283
  ## 4.3.1 — 2026-03-10
198
284
 
199
- ### Features
200
-
201
- - Add string functions
202
- - Add number functions
203
- - Add integer string functions
204
- - Caller can specify fallback value for safeParseInteger
205
- - Allow error as safeParseInteger fallback
206
- - Add safeParseNumber
207
-
208
- ### Refactoring
209
-
210
- - Rename functions
211
-
212
- ### Tests
213
-
214
- - Adapt Deno tests to Vitest
215
-
216
- ### Tooling
217
-
218
- - Scaffold the numbers workspace
219
- - Enable incremental type generation
220
- - Rename publish script to avoid recursion
221
- - Change package registry from github to npmjs
222
-
223
- ### Dependencies
285
+ ### 📦 Dependencies
224
286
 
225
287
  - Adapt to dependency upgrades and bump Node engine to >=24 (#8)
226
288
 
@@ -262,8 +324,4 @@ All notable changes to this project will be documented in this file.
262
324
 
263
325
  The build script used `pnpm --recursive run build` but no workspace package defines a `build` script — they all use `ws build` through the workspace script runner. Aligns with all other recursive commands.
264
326
 
265
- ### Documentation
266
-
267
- - Fix lint
268
-
269
327
  <!-- Generated by release-kit. Do not edit this file. Use .meta/changelog-overrides.json to override entries. -->
package/README.md CHANGED
@@ -1,15 +1,46 @@
1
+ <!-- readme-type: library -->
2
+
1
3
  # @williamthorsen/toolbelt.numbers
2
4
 
3
5
  Utility functions for working with numbers.
4
6
 
5
7
  <!-- section:release-notes -->
6
- ## Release notes — v7.0.3 (2026-08-30)
8
+ ## Release notes — v7.2.0 (2026-09-15)
9
+
10
+ ### 🎉 Features
11
+
12
+ - Add a ReadyUp adoption kit reporting hand-rolled guards (#311)
13
+
14
+ - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.guards` that reports every function whose entire body re-implements a guard published by the package.
15
+
16
+ - Add a ReadyUp adoption kit to toolbelt.filesystem (#316)
17
+
18
+ - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.filesystem` that recommends `writeAtomic` where a project writes a file to a temporary path and renames it into place, and `listDirectoryChain`, `findDirectoryChainMatch`, or `listDirectoryChainMatches` where a loop ascends to the filesystem root by `path.dirname`.
19
+ - Promotes `writeAtomic` to the candidate tier.
20
+
21
+ Migration: Change any import of `writeAtomic` from `@williamthorsen/toolbelt.filesystem/proposed` to `@williamthorsen/toolbelt.filesystem/candidate`.
22
+
23
+ - Read a directory walk's probed name through a binding or constant (#324)
24
+
25
+ - Extends the ReadyUp adoption kit in `@williamthorsen/toolbelt.packaging` to recommend `findPackageRoot` or `resolveSelfVersion` for a hand-rolled `package.json` search that checks for the file through a variable declared once inside its loop and never reassigned there, or through a string constant declared once in the same file.
26
+ - Stops the kit in `@williamthorsen/toolbelt.filesystem` from reporting such a search, which it previously treated as a generic directory walk.
27
+
28
+ - Report hand-rolled dedents in the strings adoption kit (#330)
29
+
30
+ - Adds `no-joined-line-array`, which reports an array of string or template literals that spans several lines and is joined with a newline.
31
+ - Adds `no-layout-breaking-template`, which reports an untagged template literal whose later lines drop below the indentation of the line on which it opens.
32
+
33
+ ### 🐛 Bug fixes
34
+
35
+ - Stop reading each segment of a probe's path as a separately probed name (#320)
7
36
 
8
- ### Bug fixes
37
+ - Fixes an issue in which `toolbelt.filesystem`'s ReadyUp adoption kit treated a loop over parent directories as a search for each directory's own `package.json` when the path checked at each level contained a `'package.json'` literal, as in `path.join(dir, 'node_modules', name, 'package.json')`, and so did not report the loop under `no-hand-rolled-directory-walk`.
9
38
 
10
- - Reject a check declaring a kind its detector never produces (#246)
39
+ - Make pickInteger draw once per call and keep seeded draws below 1 (#329)
11
40
 
12
- Fixes an issue where an adoption check could declare a kind its kit's detector never produces. `Kind` was inferred from `checks` and `detect` together, so a typo in a check's `kinds` widened `Kind` rather than failing.
41
+ - Fixes seeded draws that could return exactly 1, outside the documented range of [0, 1): With seed `1_000_000_856_026_238`, `pickInteger({ min: 0, max: 9, seed })` returned 10 and `pickItem` from `toolbelt.arrays` threw a `RangeError`.
42
+ - Changes only the draw that returned 1, which no `SeededRng` and no integer seed below 2³¹ could produce.
43
+ - Stops `pickInteger` from skipping its draw when `min` and `max` truncate to the same integer, which changes no return value but shifts the later values drawn from a `SeededRng` or seed function passed with such bounds.
13
44
  <!-- /section:release-notes -->
14
45
 
15
46
  ## Installation
@@ -28,7 +59,7 @@ Requires Node.js 24 or later.
28
59
  clamp(value: number, bounds: { min?: number; max?: number }): number;
29
60
  ```
30
61
 
31
- Returns the value constrained to the inclusive bounds; an omitted bound leaves that side unconstrained. A reversed range or a `NaN` bound throws a `RangeError`, where the `Math.max(min, Math.min(max, value))` idiom it replaces returns a value for both. A `NaN` value passes through.
62
+ Returns the value constrained to the inclusive bounds; an omitted bound leaves that side unconstrained. A reversed range or a `NaN` bound throws a `RangeError`, where the `Math.max(min, Math.min(max, value))` idiom that it replaces returns a value for both. A `NaN` value passes through.
32
63
 
33
64
  ```ts
34
65
  import { clamp } from '@williamthorsen/toolbelt.numbers/candidate';
@@ -57,9 +88,9 @@ round(3.14159, 2); // 3.14
57
88
  pickInteger(params?: { min?: number; max?: number; seed?: Seed }): number;
58
89
  ```
59
90
 
60
- Returns a random integer between the bounds, **inclusive** of both, and truncates a non-integer bound. Passing a seed makes the draw deterministic, which is what a test wants.
91
+ Returns a random integer between the bounds, **inclusive** of both, and truncates a non-integer bound. Passing a seed makes the draw deterministic, which a test wants. Each call draws exactly once, even when the bounds admit a single value.
61
92
 
62
- Mind the bound when replacing `Math.floor(Math.random() * n)`: that idiom stops at `n - 1`, so the equivalent is `pickInteger({ max: n - 1 })`.
93
+ Mind the bound when replacing `Math.floor(Math.random() * n)`: That idiom stops at `n - 1`, so the equivalent is `pickInteger({ max: n - 1 })`.
63
94
 
64
95
  ```ts
65
96
  import { pickInteger } from '@williamthorsen/toolbelt.numbers/candidate';
@@ -75,11 +106,11 @@ The package ships a ReadyUp kit, so a project that installs it can ask how far i
75
106
  rdy run --packages
76
107
  ```
77
108
 
78
- The kit reads the project's tracked sources and reports every hand-rolled clamp, decimal rounding, and random integer in them, each counted against the calls the project already makes into this package. All three report at `recommend`: they are correct code that a published utility expresses better, not defects.
109
+ The kit reads the project's tracked sources and reports every hand-rolled clamp, decimal rounding, and random integer in them, each counted against the calls that the project already makes into this package. All three report at `recommend`: They are correct code that a published utility expresses better, not defects.
79
110
 
80
111
  A random integer used as an array subscript is left alone. That site belongs to `@williamthorsen/toolbelt.arrays`, whose `pickItem` covers it, and reporting it here would mean seeing one line twice under conflicting advice.
81
112
 
82
- Bootstrap wrappers under `bin/` are exempt: such a wrapper imports only builtins so its build-first message survives an incomplete install, and importing this package there would replace that message with a module-resolution failure. Tests are exempt too, since they compute these values deliberately.
113
+ Bootstrap wrappers under `bin/` are exempt: Such a wrapper imports only builtins so its build-first message survives an incomplete install, and importing this package there would replace that message with a module-resolution failure. Tests are exempt too, since they compute these values deliberately. A source declared generated or vendored by the project in its own `.gitattributes`, under `linguist-generated` or `linguist-vendored`, is exempt as well: The sweep drops it before the kit sees it, so committed bundler output yields no advice that anyone could act on. The sweep is readyup's, so this holds on readyup 0.35.0 or later.
83
114
 
84
115
  A reviewed site is silenced by an `rdy-ignore` pragma on its own line, or `rdy-ignore-next-line` on the line above. A pragma naming a check's id suppresses that check alone; with no id it covers every check on the line. A failed check prints its id ahead of its fraction, which is the form to write:
85
116
 
@@ -1,2 +1,7 @@
1
- import type { Seed } from '../internal/evaluateSeed.js';
1
+ import { type Seed } from '../internal/evaluateSeed.js';
2
+ /**
3
+ * Returns a number generator whose output, when invoked successively, is a pseudo-random
4
+ * series of numbers that deterministically depend on the initial seed (or pseudo-random if no seed is given).
5
+ * This is not intended to be a cryptographically secure random number generator.
6
+ */
2
7
  export declare function makeRng(seed?: Seed): () => number;
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Returns the value constrained to the inclusive bounds; an omitted bound leaves that side unconstrained.
3
+ * A NaN value passes through, but a NaN bound or a min greater than max throws a RangeError.
4
+ *
5
+ * @category Number
6
+ * @experimental
7
+ * @stage candidate
8
+ */
1
9
  export declare function clamp(value: number, bounds: ClampBounds): number;
2
10
  export interface ClampBounds {
3
11
  readonly max?: number | undefined;
@@ -1,4 +1,11 @@
1
- import type { Seed } from '../internal/evaluateSeed.js';
1
+ import { type Seed } from '../internal/evaluateSeed.js';
2
+ /**
3
+ * Returns a scaled random number in the range [min, max).
4
+ *
5
+ * @category Number
6
+ * @experimental
7
+ * @stage candidate
8
+ */
2
9
  export declare function generateRandom(options?: Options): number;
3
10
  interface Options {
4
11
  max?: number | undefined;
@@ -1,4 +1,23 @@
1
1
  import type { Maybe } from '../internal/internal.types.js';
2
+ /**
3
+ * Returns true if the value is a string that represents a valid integer.
4
+ *
5
+ * @category Type Guards
6
+ * @experimental
7
+ * @stage candidate
8
+ */
2
9
  export declare function isIntegerString(value: string | null | undefined): value is string;
10
+ /**
11
+ * Attempts to parse an integer from a string and returns the parsed integer.
12
+ * If the input does not represent a valid integer, returns the fallback value or throws the fallback error.
13
+ *
14
+ * @param value - The string to parse.
15
+ * @param fallback - The value to return if parsing fails.
16
+ * @returns The parsed integer or the fallback value.
17
+ *
18
+ * @category Type Guards
19
+ * @experimental
20
+ * @stage candidate
21
+ */
3
22
  export declare function safeParseInteger(value: Maybe<string>, fallback: number | Error): number;
4
23
  export declare function safeParseInteger(value: Maybe<string>, fallback?: undefined): number | undefined;
@@ -1,4 +1,25 @@
1
1
  import type { Maybe } from '../internal/internal.types.js';
2
+ /**
3
+ * Returns true if the value is a string that represents a valid finite number.
4
+ *
5
+ * Accepts integers, decimals, and scientific notation (e.g., "1e3").
6
+ *
7
+ * @category Type Guards
8
+ * @experimental
9
+ * @stage candidate
10
+ */
2
11
  export declare function isNumericString(value: string | null | undefined): value is string;
12
+ /**
13
+ * Attempts to parse a number from a string and returns the parsed number.
14
+ * If the input does not represent a valid number, returns the fallback value or throws the fallback error.
15
+ *
16
+ * @param value - The string to parse.
17
+ * @param fallback - The value to return if parsing fails.
18
+ * @returns The parsed number or the fallback value.
19
+ *
20
+ * @category Type Guards
21
+ * @experimental
22
+ * @stage candidate
23
+ */
3
24
  export declare function safeParseNumber(value: Maybe<string>, fallback: number | Error): number;
4
25
  export declare function safeParseNumber(value: Maybe<string>, fallback?: undefined): number | undefined;
@@ -1,4 +1,14 @@
1
1
  import type { Seed } from '../internal/evaluateSeed.js';
2
+ /**
3
+ * Returns a random integer between the bounds inclusive.
4
+ * If the bounds are not integers, they are truncated to integers.
5
+ * Draws exactly once per call, even when the bounds admit a single value, so that a shared seed advances the same way
6
+ * whatever the bounds.
7
+ *
8
+ * @category Number
9
+ * @experimental
10
+ * @stage candidate
11
+ */
2
12
  export declare function pickInteger(params?: Params): number;
3
13
  interface Params {
4
14
  max?: number | undefined;
@@ -9,9 +9,6 @@ export function pickInteger(params = {}) {
9
9
  }
10
10
  const start = Math.trunc(Math.min(min, max));
11
11
  const end = Math.trunc(Math.max(min, max));
12
- if (start === end) {
13
- return start;
14
- }
15
12
  const range = end - start + 1;
16
13
  return start + Math.floor(generateRandom({ seed }) * range);
17
14
  }
@@ -1 +1,10 @@
1
+ /**
2
+ * Returns the number rounded to the given number of decimal places, or 0 if no nDecimalPlaces is specified.
3
+ * @param value
4
+ * @param nDecimalPlaces
5
+ *
6
+ * @category Number
7
+ * @experimental
8
+ * @stage candidate
9
+ */
1
10
  export declare function round(value: number, nDecimalPlaces?: number): number;
@@ -1,9 +1,27 @@
1
+ /**
2
+ * Scales a number from one range to another.
3
+ *
4
+ * @category Number
5
+ * @experimental
6
+ * @stage candidate
7
+ */
1
8
  export declare function scale(value: number, toRange: Range, fromRange?: Partial<Range>): number;
9
+ /**
10
+ * Scales a number from one range to another and rounds it to the nearest integer.
11
+ * Throws a RangeError unless the target bounds are safe integers.
12
+ *
13
+ * @category Number
14
+ * @experimental
15
+ * @stage candidate
16
+ */
2
17
  export declare function scaleInt(value: number, toRange: Range, fromRange?: Partial<IntegerRange>): number;
3
18
  interface Range {
4
19
  min: number;
5
20
  max: number;
6
21
  }
22
+ /**
23
+ * Represents a range of integers. Non-integer values should be truncated by the called function.
24
+ */
7
25
  interface IntegerRange {
8
26
  min: number;
9
27
  max: number;
@@ -1,24 +1,46 @@
1
- import type { Seed, SeededGenerator } from '../internal/evaluateSeed.js';
1
+ import { type Seed, type SeededGenerator } from '../internal/evaluateSeed.js';
2
+ /**
3
+ * Class that manages a pseudo-random number generator that behaves deterministically when given a seed.
4
+ */
2
5
  export declare class SeededRng implements SeededGenerator {
3
6
  private _seed;
4
7
  private baseSeed;
5
8
  private nIncrements;
9
+ /**
10
+ * Constructor
11
+ */
6
12
  constructor(seed?: Seed);
7
13
  get maxBase(): number;
8
14
  get rng(): () => number;
9
15
  get seed(): number;
10
16
  static evaluateSeed(seed?: Seed): number | undefined;
17
+ /**
18
+ * Creates a child from a seed without mutating the parent seed.
19
+ */
11
20
  static clone<T extends ThisConstructor<typeof SeededRng>>(this: T, seed: undefined, nIncrements?: number): undefined;
12
21
  static clone<T extends ThisConstructor<typeof SeededRng>>(this: T, seed: Seed, nIncrements?: number): This<T>;
13
22
  static clone<T extends ThisConstructor<typeof SeededRng>>(this: T, seed: Seed | undefined, nIncrements?: number): This<T> | undefined;
23
+ /**
24
+ * Clones the given seed or creates a new one if none is given.
25
+ */
14
26
  static cloneOrCreate<T extends ThisConstructor<typeof SeededRng>>(this: T, seed?: Seed, nIncrements?: number): This<T>;
15
27
  static spawn<T extends ThisConstructor<typeof SeededRng>>(this: T, seed: undefined): undefined;
16
28
  static spawn<T extends ThisConstructor<typeof SeededRng>>(this: T, seed: Seed): This<T>;
17
29
  static spawn<T extends ThisConstructor<typeof SeededRng>>(this: T, seed: Seed | undefined): This<T> | undefined;
30
+ /**
31
+ * Given a seed-accepting function and an optional seed, returns a new function that passes the seed to the function.
32
+ * The spawned generator is an instance of the class on which the method is called, so subclasses supply their
33
+ * own sequence.
34
+ */
18
35
  static withSeed<TOptions extends object, R>(fn: (options?: OptionsWithSeed<TOptions> | OptionsWithSeed<EmptyObject>) => R, seed: Seed | undefined): (options?: TOptions) => R;
19
36
  clone<T extends SeededRng>(this: T, nIncrements?: number): T;
37
+ /** Safely increments the seed by the given number of increments */
20
38
  increment(nIncrements?: number): this;
21
39
  next(n?: number): number;
40
+ /**
41
+ * Returns the next value in the pseudo-random sequence without incrementing the seed.
42
+ * For use in testing and debugging.
43
+ */
22
44
  peek(): number;
23
45
  scaleDownSeed(seed: number): number;
24
46
  scaleUpSeed(value: number): number;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Wrapper for `toIntegerSeed` that provides static properties for generating new seeds.
3
+ *
4
+ * @internal
5
+ */
1
6
  export declare const IntegerSeed: {
2
7
  max: number;
3
8
  multiplier: number;
@@ -1 +1,6 @@
1
+ /**
2
+ * Deterministically computes and returns a number in the range [0, 1) based on the input.
3
+ *
4
+ * @internal
5
+ */
1
6
  export declare function computeFakeMathRandom(seed: number): number;
@@ -11,5 +11,5 @@ export function computeFakeMathRandom(seed) {
11
11
  hash += hash << 3;
12
12
  hash ^= hash >> 11;
13
13
  hash += hash << 15;
14
- return (hash & 0x7fff_ffff) / 0x7fff_ffff;
14
+ return Math.min(hash & 0x7fff_ffff, 0x7fff_fffe) / 0x7fff_ffff;
15
15
  }
@@ -1,5 +1,18 @@
1
+ /**
2
+ * Resolves a seed to its numeric value, returning `undefined` when there is no seed.
3
+ *
4
+ * @internal
5
+ */
1
6
  export declare function evaluateSeed(seed: Seed | undefined): number | undefined;
7
+ /**
8
+ * Narrows a seed to a generator.
9
+ *
10
+ * @internal
11
+ */
2
12
  export declare function checkIsRngLike(seed: Seed | undefined): seed is SeededGenerator;
13
+ /**
14
+ * Interface describing an object that returns a sequence of numbers.
15
+ */
3
16
  export interface SeededGenerator {
4
17
  seed: number;
5
18
  next(): number;
@@ -1 +1,6 @@
1
+ /**
2
+ * Returns the sum of the given addends, wrapping around from 0 when the given max is exceeded.
3
+ *
4
+ * @internal
5
+ */
1
6
  export declare function wrapSum(max: number, ...addends: number[]): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/toolbelt.numbers",
3
- "version": "7.0.3",
3
+ "version": "7.2.0",
4
4
  "description": "Utility functions for working with numbers",
5
5
  "keywords": [
6
6
  "esm",
@@ -46,10 +46,10 @@
46
46
  "CHANGELOG.md"
47
47
  ],
48
48
  "devDependencies": {
49
- "esbuild": "0.28.2",
50
- "readyup": "0.34.0",
51
49
  "@williamthorsen/toolbelt.adoption": "0.1.0",
52
- "@williamthorsen/toolbelt.testing": "0.5.0"
50
+ "@williamthorsen/toolbelt.testing": "0.6.0",
51
+ "esbuild": "0.28.2",
52
+ "readyup": "0.36.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=24.0.0"