@williamthorsen/toolbelt.numbers 7.0.0 → 7.0.2

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
- /** @noformat @generated. Do not edit. Compiled by rdy. */
1
+ /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.30.0";
3
+ export const __readyupVersion = "0.34.0";
4
4
 
5
5
 
6
6
  // ../adoption/src/conventions/path-predicates.ts
@@ -48,13 +48,14 @@ import { defineRdyKit } from "readyup";
48
48
  import {
49
49
  buildFindingReport,
50
50
  countPackageUsage,
51
- discoverWorkspaces,
52
51
  readTrackedSources
53
52
  } from "readyup/check-utils";
54
53
  var NOT_A_REPO = "the project is not a git working tree, and these checks read the files git tracks";
55
- var SELF = "this project publishes the package these checks are for";
54
+ var NOTHING_TO_REPORT = { findings: [] };
56
55
  function defineAdoptionKit(spec) {
56
+ assertCheckIdsAreUnique();
57
57
  const cache = {};
58
+ const adoptedPackage = { exportNames: spec.exportNames, packageName: spec.packageName };
58
59
  return defineRdyKit({
59
60
  description: spec.description,
60
61
  defaultSeverity: "warn",
@@ -63,14 +64,27 @@ function defineAdoptionKit(spec) {
63
64
  name: "adoption",
64
65
  checks: spec.checks.map((check) => ({
65
66
  name: check.name,
67
+ id: check.id,
66
68
  ...check.severity !== void 0 && { severity: check.severity },
67
- skip: skipUnlessProjectIsAccountable,
69
+ skip: skipUnlessProjectHoldsSources,
68
70
  check: () => reportKinds(check.kinds),
69
71
  fix: check.fix
70
72
  }))
71
73
  }
72
74
  ]
73
75
  });
76
+ function assertCheckIdsAreUnique() {
77
+ const seen = /* @__PURE__ */ new Set();
78
+ const duplicated = /* @__PURE__ */ new Set();
79
+ for (const { id } of spec.checks) {
80
+ if (seen.has(id)) duplicated.add(id);
81
+ seen.add(id);
82
+ }
83
+ if (duplicated.size > 0) {
84
+ const ids = [...duplicated].toSorted().join(", ");
85
+ throw new Error(`${spec.packageName}'s kit gives one id to more than one check: ${ids}`);
86
+ }
87
+ }
74
88
  function loadSummary() {
75
89
  cache.summary ??= readProject();
76
90
  return cache.summary;
@@ -79,203 +93,26 @@ function defineAdoptionKit(spec) {
79
93
  const sources = await readTrackedSources(spec.pathFilter);
80
94
  if (sources === void 0) return void 0;
81
95
  return {
82
- adoptedCount: countPackageUsage(sources, {
83
- exportNames: spec.exportNames,
84
- packageName: spec.packageName
85
- }),
96
+ adoptedCount: countPackageUsage(sources, adoptedPackage),
86
97
  findings: sources.flatMap((source) => spec.detect(source.text).map((site) => ({ ...site, path: source.path }))),
87
- sourceCount: sources.length
98
+ sources
88
99
  };
89
100
  }
90
101
  async function reportKinds(kinds) {
91
102
  const summary = await loadSummary();
92
- if (summary === void 0) return { ok: true };
103
+ if (summary === void 0) return NOTHING_TO_REPORT;
93
104
  return buildFindingReport({
94
105
  adoptedCount: summary.adoptedCount,
95
106
  findings: summary.findings,
107
+ ownImplementation: { ...adoptedPackage, sources: summary.sources },
96
108
  shouldReport: (finding) => kinds.includes(finding.kind)
97
109
  });
98
110
  }
99
- async function skipUnlessProjectIsAccountable() {
100
- if (discoverWorkspaces().some((workspace) => workspace.name === spec.packageName)) return SELF;
111
+ async function skipUnlessProjectHoldsSources() {
101
112
  const summary = await loadSummary();
102
113
  if (summary === void 0) return NOT_A_REPO;
103
- return summary.sourceCount === 0 ? spec.noSourcesReason : false;
104
- }
105
- }
106
-
107
- // ../adoption/src/portable/blankNonCode.ts
108
- var REGEX_PRECEDERS = /* @__PURE__ */ new Set([
109
- "!",
110
- "%",
111
- "&",
112
- "(",
113
- "*",
114
- "+",
115
- ",",
116
- "-",
117
- ":",
118
- ";",
119
- "=",
120
- ">",
121
- "?",
122
- "[",
123
- "^",
124
- "{",
125
- "|",
126
- "~"
127
- ]);
128
- var EXPRESSION_KEYWORDS2 = /* @__PURE__ */ new Set([
129
- "await",
130
- "case",
131
- "delete",
132
- "do",
133
- "else",
134
- "in",
135
- "instanceof",
136
- "new",
137
- "of",
138
- "return",
139
- "typeof",
140
- "void",
141
- "yield"
142
- ]);
143
- var WORD_CHAR = /[\w$]/;
144
- function blankNonCode(source) {
145
- const scan = { out: source.split(""), source };
146
- const start = source.startsWith("#!") ? blankSpan(scan, 0, findLineEnd(source, 0)) : 0;
147
- scanCode(scan, start, false);
148
- return scan.out.join("");
149
- }
150
- function blankQuoted(scan, start, quote) {
151
- const { source } = scan;
152
- let index = start + 1;
153
- while (index < source.length) {
154
- const char = source[index];
155
- if (char === "\\") {
156
- index += 2;
157
- continue;
158
- }
159
- if (char === "\n") break;
160
- if (char === quote) return blankSpan(scan, start + 1, index) + 1;
161
- index += 1;
162
- }
163
- return start + 1;
164
- }
165
- function blankSpan(scan, from, to) {
166
- for (let index = from; index < to; index += 1) {
167
- const char = scan.source[index];
168
- if (char !== "\n" && char !== "\r") scan.out[index] = " ";
169
- }
170
- return to;
171
- }
172
- function blankTemplate(scan, start) {
173
- const { source } = scan;
174
- let index = start + 1;
175
- let textStart = index;
176
- while (index < source.length) {
177
- const char = source[index];
178
- if (char === "\\") {
179
- index += 2;
180
- continue;
181
- }
182
- if (char === "`") return blankSpan(scan, textStart, index) + 1;
183
- if (char === "$" && source[index + 1] === "{") {
184
- blankSpan(scan, textStart, index);
185
- const close = scanCode(scan, index + 2, true);
186
- index = close < source.length ? close + 1 : close;
187
- textStart = index;
188
- continue;
189
- }
190
- index += 1;
191
- }
192
- return blankSpan(scan, textStart, source.length);
193
- }
194
- function findBlockCommentEnd(source, from) {
195
- const end = source.indexOf("*/", from + 2);
196
- return end === -1 ? source.length : end + 2;
197
- }
198
- function findLineEnd(source, from) {
199
- const end = source.indexOf("\n", from);
200
- return end === -1 ? source.length : end;
201
- }
202
- function findRegexEnd(source, start) {
203
- let index = start + 1;
204
- let isInClass = false;
205
- while (index < source.length) {
206
- const char = source[index];
207
- if (char === "\\") {
208
- index += 2;
209
- continue;
210
- }
211
- if (char === "\n") return void 0;
212
- if (char === "[") isInClass = true;
213
- else if (char === "]") isInClass = false;
214
- else if (char === "/" && !isInClass) return index + 1;
215
- index += 1;
216
- }
217
- return void 0;
218
- }
219
- function findWordEnd(source, from) {
220
- let index = from;
221
- while (index < source.length && WORD_CHAR.test(source[index] ?? "")) index += 1;
222
- return index;
223
- }
224
- function scanCode(scan, from, isInterpolation) {
225
- const { source } = scan;
226
- let previousToken = "";
227
- let braceDepth = 0;
228
- let index = from;
229
- while (index < source.length) {
230
- const char = source[index] ?? "";
231
- const next = source[index + 1];
232
- if (char === "/" && next === "/") {
233
- index = blankSpan(scan, index, findLineEnd(source, index));
234
- continue;
235
- }
236
- if (char === "/" && next === "*") {
237
- index = blankSpan(scan, index, findBlockCommentEnd(source, index));
238
- continue;
239
- }
240
- if (char === "'" || char === '"') {
241
- index = blankQuoted(scan, index, char);
242
- previousToken = char;
243
- continue;
244
- }
245
- if (char === "`") {
246
- index = blankTemplate(scan, index);
247
- previousToken = char;
248
- continue;
249
- }
250
- if (char === "/" && startsRegex(previousToken)) {
251
- const end = findRegexEnd(source, index);
252
- if (end !== void 0) {
253
- blankSpan(scan, index + 1, end - 1);
254
- index = end;
255
- previousToken = "/";
256
- continue;
257
- }
258
- }
259
- if (isInterpolation && char === "{") braceDepth += 1;
260
- else if (isInterpolation && char === "}") {
261
- if (braceDepth === 0) return index;
262
- braceDepth -= 1;
263
- }
264
- if (WORD_CHAR.test(char)) {
265
- const end = findWordEnd(source, index);
266
- previousToken = source.slice(index, end);
267
- index = end;
268
- continue;
269
- }
270
- if (!/\s/.test(char)) previousToken = char;
271
- index += 1;
114
+ return summary.sources.length === 0 ? spec.noSourcesReason : false;
272
115
  }
273
- return index;
274
- }
275
- function startsRegex(previousToken) {
276
- if (previousToken === "") return true;
277
- if (previousToken.length === 1) return REGEX_PRECEDERS.has(previousToken);
278
- return EXPRESSION_KEYWORDS2.has(previousToken);
279
116
  }
280
117
 
281
118
  // ../adoption/src/portable/condenseWhitespace.ts
@@ -283,15 +120,6 @@ function condenseWhitespace(text) {
283
120
  return text.replaceAll(/\s+/g, " ");
284
121
  }
285
122
 
286
- // ../adoption/src/portable/getLineAtOffset.ts
287
- function getLineAtOffset(source, offset) {
288
- let line = 1;
289
- for (let index = 0; index < offset; index += 1) {
290
- if (source[index] === "\n") line += 1;
291
- }
292
- return line;
293
- }
294
-
295
123
  // ../adoption/src/portable/readBalancedGroup.ts
296
124
  var PARENTHESES = { close: ")", open: "(" };
297
125
  function readBalancedGroup(source, from, delimiters) {
@@ -316,6 +144,9 @@ function readAnchoredWindow(source, offset, lengths) {
316
144
  };
317
145
  }
318
146
 
147
+ // ../adoption/src/mod.ts
148
+ import { blankNonCode, getLineAtOffset } from "readyup/check-utils";
149
+
319
150
  // src/readiness/adoptedExports.ts
320
151
  var ADOPTED_EXPORTS = [
321
152
  "clamp",
@@ -448,18 +279,21 @@ var default_default = defineAdoptionKit({
448
279
  checks: [
449
280
  {
450
281
  name: "No source clamps a value by hand",
282
+ id: "no-hand-rolled-clamp",
451
283
  kinds: ["clamp-nest"],
452
284
  severity: "recommend",
453
285
  fix: `Replace each expression named above with clamp from ${PACKAGE_NAME}/candidate, called as clamp(value, { min, max }). It is not a silent substitution: clamp throws a RangeError on a reversed range or a NaN bound, where the nested Math calls return a value for both. Reference: ${README_URL}`
454
286
  },
455
287
  {
456
288
  name: "No source rounds to decimal places by hand",
289
+ id: "no-hand-rolled-round",
457
290
  kinds: ["round-scale"],
458
291
  severity: "recommend",
459
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}`
460
293
  },
461
294
  {
462
295
  name: "No source derives a random integer by hand",
296
+ id: "no-hand-rolled-random-integer",
463
297
  kinds: ["random-integer"],
464
298
  severity: "recommend",
465
299
  fix: `Replace each expression named above with pickInteger from ${PACKAGE_NAME}/candidate, which also takes a seed. Mind the bound: Math.floor(Math.random() * N) stops at N - 1, where pickInteger's max is inclusive, so the replacement is pickInteger({ max: N - 1 }). A site indexing an array is left to toolbelt.arrays, whose pickItem covers it. Reference: ${README_URL}`
@@ -19,30 +19,20 @@
19
19
  "path": "../../adoption/src/conventions/site-handoffs.ts"
20
20
  },
21
21
  {
22
- "hash": "737a39d2",
22
+ "hash": "781c3f53",
23
23
  "kind": "module",
24
24
  "path": "../../adoption/src/kits/defineAdoptionKit.ts"
25
25
  },
26
26
  {
27
- "hash": "29115f53",
27
+ "hash": "6e5a8dda",
28
28
  "kind": "module",
29
29
  "path": "../../adoption/src/mod.ts"
30
30
  },
31
- {
32
- "hash": "6617674b",
33
- "kind": "module",
34
- "path": "../../adoption/src/portable/blankNonCode.ts"
35
- },
36
31
  {
37
32
  "hash": "e8d0444d",
38
33
  "kind": "module",
39
34
  "path": "../../adoption/src/portable/condenseWhitespace.ts"
40
35
  },
41
- {
42
- "hash": "4da99027",
43
- "kind": "module",
44
- "path": "../../adoption/src/portable/getLineAtOffset.ts"
45
- },
46
36
  {
47
37
  "hash": "b6d99ce7",
48
38
  "kind": "module",
@@ -59,7 +49,7 @@
59
49
  "path": "../../adoption/src/portable/readBalancedGroup.ts"
60
50
  },
61
51
  {
62
- "hash": "824d5042",
52
+ "hash": "e6df5682",
63
53
  "kind": "module",
64
54
  "path": "kits/default.ts"
65
55
  },
@@ -91,10 +81,10 @@
91
81
  ],
92
82
  "name": "default",
93
83
  "path": "kits/default.js",
94
- "readyupVersion": "0.30.0",
84
+ "readyupVersion": "0.34.0",
95
85
  "source": "kits/default.ts",
96
- "sourceHash": "824d5042",
97
- "targetHash": "8fa7ed41"
86
+ "sourceHash": "e6df5682",
87
+ "targetHash": "44b320aa"
98
88
  }
99
89
  ]
100
90
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 7.0.2 — 2026-08-28
6
+
7
+ ### Refactoring
8
+
9
+ - Upgrade eslint-config-typescript to v12.0.1 and satisfy its new rules (#236)
10
+
11
+ Upgrades `@williamthorsen/eslint-config-typescript` to v12 and fixes violations surfaced by the new rules banning unpublished barrels and floating disposables.
12
+
13
+ ### Dependencies
14
+
15
+ - Upgrade all deps to latest version
16
+ - Upgrade deps to latest version
17
+ - Upgrade all deps to latest version
18
+
19
+ ## 7.0.1 — 2026-08-24
20
+
21
+ ### Bug fixes
22
+
23
+ - Stop the adoption kits from blanking code after `++`, `!`, and keyword-named members (#208)
24
+
25
+ Fixes an issue where the `errors`, `numbers`, and `vitest` adoption kits stopped reading a line's code after a postfix `++`, a non-null `!`, or a property spelled like a keyword. The source blanker each kit runs before its anchor scan read the `/` that followed as the opening of a regular expression and blanked to the line's next `/`, so a replaceable idiom written after one of the three went unreported.
26
+
27
+ `@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.
28
+
29
+ ### Internal
30
+
31
+ - Migrate the adoption kits onto FindingOutcome and add check ids (#217)
32
+
33
+ Upgrades `readyup` to 0.32.0 and migrates `defineAdoptionKit` onto the `FindingOutcome` that `buildFindingReport` returns as of 0.31.0.
34
+
35
+ Gives every adoption check an `id`, which a consumer's `rdy-ignore` pragma names to suppress that one check; a pragma naming none still silences every check on the line. The kits of `@williamthorsen/toolbelt.errors`, `@williamthorsen/toolbelt.numbers`, and `@williamthorsen/toolbelt.vitest` each declare ids, and `defineAdoptionKit` refuses a kit that gives one id to two checks.
36
+
37
+ - Hold this repo to its own adoption kits and exempt each package's implementation (#218)
38
+
39
+ Removes the exemption that kept this repository out of its own adoption kits. `rdy run --packages` now covers this repo like any other project.
40
+
41
+ Each package's own implementation stays exempt, but that exemption now covers the function alone rather than the whole repository.
42
+
5
43
  ## 7.0.0 — 2026-08-21
6
44
 
7
45
  ### Features
package/README.md CHANGED
@@ -2,27 +2,7 @@
2
2
 
3
3
  Utility functions for working with numbers.
4
4
 
5
- <!-- section:release-notes -->
6
- ## Release notes — v7.0.0 (2026-08-21)
7
-
8
- ### Features
9
-
10
- - 🚨 **Breaking:** Promote clamp to the candidate tier with a stricter bounds contract (#185)
11
-
12
- Promotes `clamp` to `@williamthorsen/toolbelt.numbers/candidate` and tightens its bounds contract: A `NaN` bound now throws a `RangeError`, where it previously returned `NaN` silently. A `NaN` value still passes through. Publishes the bounds type as `ClampBounds`, whose optional properties admit `undefined`, so a bound that may be absent typechecks under `exactOptionalPropertyTypes`.
13
-
14
- Migration: `clamp` is no longer exported from the `/draft` subpath; consumers import it from `/candidate`.
15
-
16
- - Add a ReadyUp adoption kit to `numbers` (#188)
17
-
18
- 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.
19
-
20
- ### Bug fixes
21
-
22
- - Blank comments and literals before a detector reads a source (#191)
23
-
24
- 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.
25
- <!-- /section:release-notes -->
5
+ <!-- section:release-notes --><!-- /section:release-notes -->
26
6
 
27
7
  ## Installation
28
8
 
@@ -93,6 +73,13 @@ A random integer used as an array subscript is left alone. That site belongs to
93
73
 
94
74
  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.
95
75
 
76
+ 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:
77
+
78
+ ```ts
79
+ // rdy-ignore-next-line toolbelt.numbers/no-hand-rolled-clamp -- a reversed range is unreachable here
80
+ const bounded = Math.max(min, Math.min(max, value));
81
+ ```
82
+
96
83
  Add the package to `.config/readyup.config.ts` to include it in a routine sweep:
97
84
 
98
85
  ```ts
@@ -1,3 +1,4 @@
1
+ export type { Seed } from '../internal/evaluateSeed.js';
1
2
  export { clamp, type ClampBounds } from './clamp.js';
2
3
  export { generateRandom } from './generateRandom.js';
3
4
  export { isIntegerString, safeParseInteger } from './integer-string.js';
@@ -6,4 +7,3 @@ export { pickInteger } from './pickInteger.js';
6
7
  export { round } from './round.js';
7
8
  export { scale } from './scale.js';
8
9
  export { IntSeededRng, SeededRng } from './seeded-rng.js';
9
- export * from './types.js';
@@ -6,4 +6,3 @@ export { pickInteger } from "./pickInteger.js";
6
6
  export { round } from "./round.js";
7
7
  export { scale } from "./scale.js";
8
8
  export { IntSeededRng, SeededRng } from "./seeded-rng.js";
9
- export * from "./types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/toolbelt.numbers",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
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
  "esbuild": "0.28.2",
50
- "readyup": "0.30.0",
50
+ "readyup": "0.34.0",
51
51
  "@williamthorsen/toolbelt.adoption": "0.1.0",
52
- "@williamthorsen/toolbelt.testing": "0.3.0"
52
+ "@williamthorsen/toolbelt.testing": "0.5.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=24.0.0"
@@ -1 +0,0 @@
1
- export type { Seed } from '../internal/evaluateSeed.js';
@@ -1 +0,0 @@
1
- export {};