@williamthorsen/toolbelt.numbers 7.1.0 → 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.35.0";
3
+ export const __readyupVersion = "0.36.0";
4
4
 
5
5
 
6
6
  // ../adoption/src/conventions/path-predicates.ts
@@ -56,6 +56,11 @@ 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
 
@@ -9,22 +9,22 @@
9
9
  "esbuildVersion": "0.28.2",
10
10
  "inputs": [
11
11
  {
12
- "hash": "b9007d48",
12
+ "hash": "1dc83619",
13
13
  "kind": "module",
14
14
  "path": "../../adoption/src/conventions/path-predicates.ts"
15
15
  },
16
16
  {
17
- "hash": "84c99907",
17
+ "hash": "0237a871",
18
18
  "kind": "module",
19
19
  "path": "../../adoption/src/conventions/site-handoffs.ts"
20
20
  },
21
21
  {
22
- "hash": "3372fb44",
22
+ "hash": "8fb83c2d",
23
23
  "kind": "module",
24
24
  "path": "../../adoption/src/kits/defineAdoptionKit.ts"
25
25
  },
26
26
  {
27
- "hash": "dee2a3a3",
27
+ "hash": "10596562",
28
28
  "kind": "module",
29
29
  "path": "../../adoption/src/mod.ts"
30
30
  },
@@ -34,17 +34,27 @@
34
34
  "path": "../../adoption/src/portable/condenseWhitespace.ts"
35
35
  },
36
36
  {
37
- "hash": "15a78157",
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": "1d2261fb",
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
  },
@@ -86,10 +96,10 @@
86
96
  ],
87
97
  "name": "default",
88
98
  "path": "kits/default.js",
89
- "readyupVersion": "0.35.0",
99
+ "readyupVersion": "0.36.0",
90
100
  "source": "kits/default.ts",
91
101
  "sourceHash": "24f6e6db",
92
- "targetHash": "cee2ac4e"
102
+ "targetHash": "b1603ae6"
93
103
  }
94
104
  ]
95
105
  }
package/CHANGELOG.md CHANGED
@@ -2,9 +2,77 @@
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
+
5
73
  ## 7.1.0 — 2026-09-06
6
74
 
7
- ### Features
75
+ ### 🎉 Features
8
76
 
9
77
  - Add a ReadyUp adoption kit (#253)
10
78
 
@@ -14,12 +82,7 @@ All notable changes to this project will be documented in this file.
14
82
 
15
83
  - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.async` that recommends the use of `delay` to replace a hand-rolled sleep.
16
84
 
17
- ### Dependencies
18
-
19
- - Upgrade all deps to latest version
20
- - Upgrade all deps to latest version
21
-
22
- ### Documentation
85
+ ### 📚 Documentation
23
86
 
24
87
  - Document the generated-source exemption in the adoption kits (#251)
25
88
 
@@ -47,7 +110,7 @@ All notable changes to this project will be documented in this file.
47
110
 
48
111
  ## 7.0.3 — 2026-08-30
49
112
 
50
- ### Bug fixes
113
+ ### 🐛 Bug fixes
51
114
 
52
115
  - Reject a check declaring a kind its detector never produces (#246)
53
116
 
@@ -55,21 +118,15 @@ All notable changes to this project will be documented in this file.
55
118
 
56
119
  ## 7.0.2 — 2026-08-28
57
120
 
58
- ### Refactoring
121
+ ### ♻️ Refactoring
59
122
 
60
123
  - Upgrade eslint-config-typescript to v12.0.1 and satisfy its new rules (#236)
61
124
 
62
125
  Upgrades `@williamthorsen/eslint-config-typescript` to v12 and fixes violations surfaced by the new rules banning unpublished barrels and floating disposables.
63
126
 
64
- ### Dependencies
65
-
66
- - Upgrade all deps to latest version
67
- - Upgrade deps to latest version
68
- - Upgrade all deps to latest version
69
-
70
127
  ## 7.0.1 — 2026-08-24
71
128
 
72
- ### Bug fixes
129
+ ### 🐛 Bug fixes
73
130
 
74
131
  - Stop the adoption kits from blanking code after `++`, `!`, and keyword-named members (#208)
75
132
 
@@ -77,7 +134,7 @@ All notable changes to this project will be documented in this file.
77
134
 
78
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.
79
136
 
80
- ### Internal
137
+ ### 🏗️ Internal features
81
138
 
82
139
  - Migrate the adoption kits onto FindingOutcome and add check ids (#217)
83
140
 
@@ -93,7 +150,7 @@ All notable changes to this project will be documented in this file.
93
150
 
94
151
  ## 7.0.0 — 2026-08-21
95
152
 
96
- ### Features
153
+ ### 🎉 Features
97
154
 
98
155
  - 🚨 **Breaking:** Promote clamp to the candidate tier with a stricter bounds contract (#185)
99
156
 
@@ -105,25 +162,21 @@ All notable changes to this project will be documented in this file.
105
162
 
106
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.
107
164
 
108
- ### Bug fixes
165
+ ### 🐛 Bug fixes
109
166
 
110
167
  - Blank comments and literals before a detector reads a source (#191)
111
168
 
112
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.
113
170
 
114
- ### Refactoring
171
+ ### ♻️ Refactoring
115
172
 
116
173
  - Break the workspace dependency cycle by making `adoption` a leaf (#195)
117
174
 
118
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.
119
176
 
120
- ### Dependencies
121
-
122
- - Upgrade all deps to latest version
123
-
124
177
  ## 6.0.1 — 2026-08-13
125
178
 
126
- ### Tooling
179
+ ### ⚙️ Tooling
127
180
 
128
181
  - Remove redundant .gitignore files
129
182
  - Populate manifest metadata and adopt a pnpm catalog (#140)
@@ -132,13 +185,13 @@ All notable changes to this project will be documented in this file.
132
185
 
133
186
  ## 6.0.0 — 2026-08-12
134
187
 
135
- ### Features
188
+ ### 🎉 Features
136
189
 
137
190
  - 🚨 **Breaking:** Rename get* functions by return kind and verb specificity (#119)
138
191
 
139
192
  Renames thirteen functions across various packages to align with a consistent naming pattern.
140
193
 
141
- ### Refactoring
194
+ ### ♻️ Refactoring
142
195
 
143
196
  - Stop shipping test-only and dead support modules (#117)
144
197
 
@@ -150,17 +203,13 @@ All notable changes to this project will be documented in this file.
150
203
 
151
204
  ## 5.0.0 — 2026-08-08
152
205
 
153
- ### Features
206
+ ### 🎉 Features
154
207
 
155
208
  - 🚨 **Breaking:** Spawn seeded number generator of same subclass (#84)
156
209
 
157
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.
158
211
 
159
- - Use underscore separator at 4 digits or more
160
-
161
- 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.
162
-
163
- ### Refactoring
212
+ ### ♻️ Refactoring
164
213
 
165
214
  - Fixes violations surfaced by newly active lint rules (#84)
166
215
  - Fix slug punctuation and require safe integers (#86)
@@ -169,9 +218,7 @@ All notable changes to this project will be documented in this file.
169
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.
170
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.
171
220
 
172
- - Fix lint
173
-
174
- ### Tooling
221
+ ### ⚙️ Tooling
175
222
 
176
223
  - Migrate Vitest configs to the nmr projects model (#73)
177
224
 
@@ -189,35 +236,31 @@ All notable changes to this project will be documented in this file.
189
236
 
190
237
  All packages now have identical compiler settings, using the settings from the `@williamthorsen/tsconfig` base config without modification.
191
238
 
192
- ### Dependencies
193
-
194
- - Upgrade all deps to latest version
195
-
196
239
  ## 4.3.8 — 2026-07-27
197
240
 
198
- ### Tooling
241
+ ### ⚙️ Tooling
199
242
 
200
243
  - Normalize Vitest, and lint configs
201
244
 
202
- ### Documentation
245
+ ### 📚 Documentation
203
246
 
204
247
  - Change license to ISC
205
248
 
206
249
  ## 4.3.7 — 2026-07-24
207
250
 
208
- ### Tooling
251
+ ### ⚙️ Tooling
209
252
 
210
253
  - Configure release-kit & repo labels
211
254
 
212
255
  ## 4.3.6 — 2026-07-20
213
256
 
214
- ### Bug fixes
257
+ ### 🐛 Bug fixes
215
258
 
216
259
  - Add repository field to package manifests for npm provenance (#65)
217
260
 
218
261
  Fixes an issue that prevented every package from publishing to npm. Each package now links to its source repository from its npm page.
219
262
 
220
- ### Dependencies
263
+ ### 📦 Dependencies
221
264
 
222
265
  - Upgrade ESLint packages and migrate to TypeScript 6 (#67)
223
266
 
@@ -225,7 +268,7 @@ All notable changes to this project will be documented in this file.
225
268
 
226
269
  ## 4.3.5 — 2026-07-20
227
270
 
228
- ### Tooling
271
+ ### ⚙️ Tooling
229
272
 
230
273
  - Migrate to the nmr toolchain and resolve dependency vulnerabilities (#45)
231
274
 
@@ -233,37 +276,13 @@ All notable changes to this project will be documented in this file.
233
276
 
234
277
  ## 4.3.4 — 2026-03-19
235
278
 
236
- ### Formatting
279
+ ### 🎨 Formatting
237
280
 
238
281
  - Format changelogs
239
282
 
240
283
  ## 4.3.1 — 2026-03-10
241
284
 
242
- ### Features
243
-
244
- - Add string functions
245
- - Add number functions
246
- - Add integer string functions
247
- - Caller can specify fallback value for safeParseInteger
248
- - Allow error as safeParseInteger fallback
249
- - Add safeParseNumber
250
-
251
- ### Refactoring
252
-
253
- - Rename functions
254
-
255
- ### Tests
256
-
257
- - Adapt Deno tests to Vitest
258
-
259
- ### Tooling
260
-
261
- - Scaffold the numbers workspace
262
- - Enable incremental type generation
263
- - Rename publish script to avoid recursion
264
- - Change package registry from github to npmjs
265
-
266
- ### Dependencies
285
+ ### 📦 Dependencies
267
286
 
268
287
  - Adapt to dependency upgrades and bump Node engine to >=24 (#8)
269
288
 
@@ -305,8 +324,4 @@ All notable changes to this project will be documented in this file.
305
324
 
306
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.
307
326
 
308
- ### Documentation
309
-
310
- - Fix lint
311
-
312
327
  <!-- Generated by release-kit. Do not edit this file. Use .meta/changelog-overrides.json to override entries. -->
package/README.md CHANGED
@@ -1,19 +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.1.0 (2026-09-06)
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.
7
32
 
8
- ### Features
33
+ ### 🐛 Bug fixes
9
34
 
10
- - Add a ReadyUp adoption kit (#253)
35
+ - Stop reading each segment of a probe's path as a separately probed name (#320)
11
36
 
12
- 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`.
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`.
13
38
 
14
- - Add a ReadyUp adoption kit reporting hand-rolled sleeps (#303)
39
+ - Make pickInteger draw once per call and keep seeded draws below 1 (#329)
15
40
 
16
- - Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.async` that recommends the use of `delay` to replace a hand-rolled sleep.
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.
17
44
  <!-- /section:release-notes -->
18
45
 
19
46
  ## Installation
@@ -61,7 +88,7 @@ round(3.14159, 2); // 3.14
61
88
  pickInteger(params?: { min?: number; max?: number; seed?: Seed }): number;
62
89
  ```
63
90
 
64
- 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.
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.
65
92
 
66
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 })`.
67
94
 
@@ -1,6 +1,10 @@
1
1
  import { type Seed } from '../internal/evaluateSeed.js';
2
2
  /**
3
3
  * Returns a scaled random number in the range [min, max).
4
+ *
5
+ * @category Number
6
+ * @experimental
7
+ * @stage candidate
4
8
  */
5
9
  export declare function generateRandom(options?: Options): number;
6
10
  interface Options {
@@ -2,6 +2,12 @@ import type { Seed } from '../internal/evaluateSeed.js';
2
2
  /**
3
3
  * Returns a random integer between the bounds inclusive.
4
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
5
11
  */
6
12
  export declare function pickInteger(params?: Params): number;
7
13
  interface Params {
@@ -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
  }
@@ -2,5 +2,9 @@
2
2
  * Returns the number rounded to the given number of decimal places, or 0 if no nDecimalPlaces is specified.
3
3
  * @param value
4
4
  * @param nDecimalPlaces
5
+ *
6
+ * @category Number
7
+ * @experimental
8
+ * @stage candidate
5
9
  */
6
10
  export declare function round(value: number, nDecimalPlaces?: number): number;
@@ -1,7 +1,19 @@
1
1
  /**
2
2
  * Scales a number from one range to another.
3
+ *
4
+ * @category Number
5
+ * @experimental
6
+ * @stage candidate
3
7
  */
4
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
+ */
5
17
  export declare function scaleInt(value: number, toRange: Range, fromRange?: Partial<IntegerRange>): number;
6
18
  interface Range {
7
19
  min: 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/toolbelt.numbers",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "Utility functions for working with numbers",
5
5
  "keywords": [
6
6
  "esm",
@@ -47,9 +47,9 @@
47
47
  ],
48
48
  "devDependencies": {
49
49
  "@williamthorsen/toolbelt.adoption": "0.1.0",
50
- "@williamthorsen/toolbelt.testing": "0.5.1",
50
+ "@williamthorsen/toolbelt.testing": "0.6.0",
51
51
  "esbuild": "0.28.2",
52
- "readyup": "0.35.0"
52
+ "readyup": "0.36.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=24.0.0"