adjointly 0.0.0-stage → 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # adjointly
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ac53b8a: First release: reverse mode on a typed-array tape, forward mode, and `plain` evaluation of one
8
+ function written against `Arithmetic`; the primitives, `sum`/`dot`/`logsumexp`/`softmax`, the
9
+ `gradient`/`jvp`/`evaluate` conveniences, and `finiteDifference`/`checkGradient`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexey Feldgendler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,346 @@
1
- # Temporary Holding Version
1
+ # adjointly
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Automatic differentiation for scalar `float64` programs in TypeScript. Write a function once,
4
+ against a small arithmetic interface, and get from the same code:
5
+
6
+ - its **value**, on plain numbers, at full speed;
7
+ - its **gradient** with respect to every input, by **reverse mode** (one backward sweep over a
8
+ tape stored in typed arrays, whatever the number of inputs);
9
+ - its **directional derivative** along any direction, by **forward mode**.
10
+
11
+ Derivatives are exact to floating-point rounding, not finite-difference approximations. Ordinary
12
+ control flow (loops, branches, early returns) just works. No runtime dependencies, ESM only,
13
+ Node 22 or later (or any modern browser or bundler).
14
+
15
+ > **Status:** 0.x: the API may still change before 1.0. **License:** [MIT](LICENSE).
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ npm install adjointly
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import { type Arithmetic, evaluate, gradient, jvp } from 'adjointly';
27
+
28
+ // f(x, y) = sin(x) * e^y, written once against the arithmetic interface
29
+ function f<T>(A: Arithmetic<T>, [x, y]: readonly [T, T]): T {
30
+ return A.mul(A.sin(x), A.exp(y));
31
+ }
32
+
33
+ evaluate(f, [1, 0]); //=> sin(1)
34
+ gradient(f, [1, 0]); //=> { value: sin(1), gradient: [cos(1), sin(1)] }
35
+ jvp(f, [1, 0], [1, 1]); //=> { value: sin(1), derivative: cos(1) + sin(1) }
36
+ ```
37
+
38
+ Typing the inputs as a tuple, `readonly [T, T]`, gives each destructured input the type `T`, even
39
+ under `noUncheckedIndexedAccess`; a point written as a literal (`[1, 0]`) is checked against it.
40
+
41
+ ## Concepts
42
+
43
+ **The arithmetic interface.** A differentiable function takes an `Arithmetic<T>` (here `A`) and
44
+ values of an opaque type `T`, and does every operation through `A`: `A.add(x, y)`, `A.exp(x)`.
45
+ `T` is `number` when the function is evaluated plainly, and an index into storage when it is being
46
+ differentiated. Numbers enter through `A.constant(c)`, or as the plain-number operand of `addc`,
47
+ `mulc` and the like; values leave through `A.value(x)`.
48
+
49
+ **The tape (reverse mode).** A `Tape` records each operation as a node: its value, the one or two
50
+ nodes it was computed from, and the _local partial derivatives_ with respect to them, computed on
51
+ the spot (for `y = x * z` those are `z` and `x`). That forward pass is the evaluation itself.
52
+
53
+ **Adjoints.** The adjoint of a node is the derivative of the output with respect to that node.
54
+ The output's own adjoint is 1. Walking the tape backwards, each node adds _partial × its own
55
+ adjoint_ into each parent's adjoint (the chain rule). When the walk reaches the inputs, their
56
+ adjoints are the gradient. One backward sweep costs a small constant times one evaluation, no
57
+ matter how many inputs there are.
58
+
59
+ **Forward mode.** `Forward` instead carries a _tangent_ beside every value: the derivative along
60
+ one direction `v` fixed when the inputs are created. Each operation computes its tangent from its
61
+ arguments' tangents as it runs, so one pass gives `∇f · v` with no tape to walk back.
62
+
63
+ **Which to use.**
64
+
65
+ | You have | You want | Use |
66
+ | ---------------------------------------------- | ------------------------------------------- | -------------------------------- |
67
+ | many inputs, one output (a loss, a likelihood) | the gradient | reverse mode: `gradient`, `Tape` |
68
+ | few inputs, or one direction | a directional derivative, a Jacobian column | forward mode: `jvp`, `Forward` |
69
+ | several outputs | a weighted combination of their gradients | `Tape.vjp` |
70
+ | no derivative | the value, fast | `evaluate`, `plain` |
71
+
72
+ A full gradient by forward mode costs one pass per input; by reverse mode, one pass and one sweep.
73
+
74
+ **Control flow.** Branch on values with `A.value(x)`, loop as long as you like, return early.
75
+ The tape records the operations that actually ran, so the derivative is that of the branch taken:
76
+
77
+ ```ts
78
+ import { type Arithmetic, gradient } from 'adjointly';
79
+
80
+ function hinge<T>(A: Arithmetic<T>, [x]: readonly [T]): T {
81
+ return A.value(x) > 1 ? A.constant(0) : A.csub(1, x);
82
+ }
83
+ gradient(hinge, [0.5]).gradient; //=> [-1]
84
+ gradient(hinge, [2]).gradient; //=> [0]
85
+ gradient(hinge, [1]).gradient; //=> [-1]
86
+ ```
87
+
88
+ At a branch boundary (`x = 1` above) you get the one-sided derivative of whichever branch the
89
+ comparison chose. Where the function is continuous and smooth across the boundary that is the true
90
+ derivative; where it has a kink, it is one of the one-sided derivatives, determined by the
91
+ comparison's `>` versus `>=`.
92
+
93
+ More in [docs/concepts.md](docs/concepts.md).
94
+
95
+ ## API overview
96
+
97
+ Every export is documented in its TSDoc, with an example that is run as a test. By topic:
98
+
99
+ **Writing functions**
100
+
101
+ - `interface Arithmetic<T>`: `constant(x: number): T`, `value(a: T): number`, and one method per
102
+ primitive: `neg, add, sub, mul, div, recip, square, sqrt, exp, expm1, log, log1p, pow, abs,
103
+ min, max, sin, cos, tanh, sigmoid, softplus`; and, for a plain-number operand `c`,
104
+ `addc(a, c), subc(a, c), csub(c, a), mulc(a, c), divc(a, c), cdiv(c, a), powc(a, c)`: the
105
+ binary primitive with a constant, in one tape node instead of two.
106
+ - `type ScalarFunction<X = readonly number[]> = <T>(A: Arithmetic<T>, xs: Inputs<T, X>) => T`:
107
+ what the conveniences take. `Inputs<T, X>` is one `T` per number of the point `X`: a tuple for a
108
+ literal point such as `[1, 0]`, `readonly T[]` for a `number[]`.
109
+ - `const plain: Arithmetic<number>`: values only.
110
+ - Composites, generic over any `Arithmetic`: `sum(A, xs)`, `dot(A, xs, ys)`,
111
+ `logsumexp(A, xs)`, `softmax(A, xs): T[]`.
112
+
113
+ **One-call conveniences**
114
+
115
+ - `evaluate(f, at): number`
116
+ - `gradient(f, at, tape?): { value: number; gradient: number[] }`
117
+ - `jvp(f, at, direction, forward?): { value: number; derivative: number }`
118
+
119
+ **Reverse mode: `class Tape implements Arithmetic<Var>`**
120
+
121
+ - `new Tape({ capacity?, debug? })`; `size`, `capacity`
122
+ - `input(x): Var`, `inputs(xs): Var[]` (a tuple for a literal), `constant(x): Var`, `value(v): number`
123
+ - `backward(output, seed = 1)`, `vjp(outputs, weights)`
124
+ - `adjoint(v): number`, `adjoints(vars, out?): Float64Array`,
125
+ `gradient(output, inputs, out?): Float64Array`
126
+ - `clear()`, `mark(): Mark`, `rewind(mark)`
127
+
128
+ **Forward mode: `class Forward implements Arithmetic<Dual>`**
129
+
130
+ - `new Forward({ capacity?, debug? })`; `size`
131
+ - `input(x, tangent): Dual`, `constant(x): Dual`, `value(d): number`, `tangent(d): number`
132
+ - `clear()`
133
+
134
+ **Checking your own functions**
135
+
136
+ - `finiteDifference(f, at, h = DEFAULT_STEP): number[]`
137
+ - `checkGradient(f, at, { h?, rtol?, atol? })` returns
138
+ `{ ok, value, analytic, numeric, worst: { index, analytic, numeric, error, allowed, roundoff } }`
139
+
140
+ **The primitive table**
141
+
142
+ - `primitives`: each primitive's `value` and local partials (`deriv`, or `da` and `db`);
143
+ `unaryNames`, `binaryNames`.
144
+
145
+ The full reference, recipes and pitfalls are in [docs/](docs/README.md).
146
+
147
+ ### Using a tape directly
148
+
149
+ The conveniences allocate a little per call. In an inner loop, keep one `Tape` and reuse it:
150
+
151
+ ```ts
152
+ import { Tape } from 'adjointly';
153
+
154
+ const tape = new Tape();
155
+ const out = new Float64Array(2);
156
+ let w: [number, number] = [0, 0];
157
+ for (let step = 0; step < 200; step++) {
158
+ tape.clear();
159
+ const [a, b] = tape.inputs(w);
160
+ // loss = (a - 3)^2 + (b + 1)^2
161
+ const loss = tape.add(tape.square(tape.subc(a, 3)), tape.square(tape.addc(b, 1)));
162
+ tape.gradient(loss, [a, b], out);
163
+ w = [w[0] - 0.1 * out[0]!, w[1] - 0.1 * out[1]!];
164
+ }
165
+ w; //=> ~[3, -1]
166
+ ```
167
+
168
+ `mark()` and `rewind()` keep a shared prefix (parameters, precomputed features) on the tape while
169
+ discarding everything after it; `vjp` seeds several outputs at once. See
170
+ [docs/recipes.md](docs/recipes.md).
171
+
172
+ ## A worked example
173
+
174
+ [examples/production-line/](https://github.com/feldgendler/adjointly/tree/main/examples/production-line) schedules maintenance on a simulated production line, minute by minute over a shift. One
175
+ forward and one backward pass give the sensitivity of profit to every station-minute's
176
+ maintenance decision, and that gradient estimates the effect of every candidate change to the
177
+ schedule. A local search that tries the changes in that order gets most of the way faster than one
178
+ that tries them at random (three quarters of the best gain in about a third of the work, gradient
179
+ passes counted),
180
+ but is slower to finish and costlier overall: near an optimum the first-order estimate no longer
181
+ ranks the moves. Forward mode gives the sensitivity to the price. Run it with `npm run example`.
182
+
183
+ ## Subgradient conventions
184
+
185
+ Where a primitive has no derivative, it uses a fixed, documented one:
186
+
187
+ | Primitive | Where | Derivative used |
188
+ | ------------------------ | ------------------- | ----------------------------------------------------------------------- |
189
+ | `abs(x)` | `x = 0` | `0` |
190
+ | `max(a, b)`, `min(a, b)` | `a = b` | all to the first argument: `∂/∂a = 1`, `∂/∂b = 0` |
191
+ | `pow(a, b)` | `a ≤ 0` | `∂/∂b = 0` (the real power is not differentiable in the exponent there) |
192
+ | `pow(a, b)` | `b = 0` | `∂/∂a = 0` (so `0^0` does not give `0 · ∞`) |
193
+ | `sqrt(x)` | `x = 0` or `x = -0` | `+∞`, the true one-sided limit |
194
+ | `max(a, b)`, `min(a, b)` | `a` or `b` is `NaN` | `NaN` for both, like `abs(NaN)` |
195
+
196
+ One rule completes the picture, the **zero rule**: every term of the chain rule is a local partial
197
+ times an incoming adjoint (reverse mode) or tangent (forward mode), and if either factor is exactly
198
+ 0 the term is 0, even when the other is infinite or `NaN`. So an infinite partial on a path that
199
+ does not matter (a `sqrt(0)` multiplied by zero, the untaken side of a `max`, an input the
200
+ direction does not move) never turns into `0 · ∞ = NaN`, and both modes give the same answer.
201
+ The rule looks at factors, not sums, so it cannot see a cancellation: `sqrt(x - x)` has derivative
202
+ 0 in forward mode and `NaN` in reverse mode, which multiplies the infinite partial into each term
203
+ before they cancel. [docs/concepts.md](docs/concepts.md#the-zero-rule) has the details.
204
+
205
+ ## Numerical stability
206
+
207
+ - `sigmoid` and `softplus` are primitives, evaluated without overflow for any argument, with
208
+ derivatives that keep full relative precision in the tails. Build them from `exp` and `log`
209
+ yourself and `softplus(800)` is `Infinity`.
210
+ - `tanh`'s derivative is computed from `exp(-2|x|)`, not as `1 - tanh(x)^2`, which cancels to 0
211
+ in the tails.
212
+ - `expm1` and `log1p` are accurate near `0`, where `exp(x) - 1` and `log(1 + x)` lose every digit.
213
+ - `logsumexp` and `softmax` subtract the largest input (as a constant) before exponentiating. The
214
+ shift cancels exactly in the math, so the gradient is exact, and nothing overflows.
215
+ - `square(x)` is one node and exact; prefer it to `pow(x, constant(2))`, whose exponent derivative
216
+ needs `log(x)`.
217
+ - Derivatives are those of the floating-point program you wrote. A formula that cancels
218
+ catastrophically in value usually does in its derivative too.
219
+
220
+ ## Limitations
221
+
222
+ - Scalars only: no tensors, broadcasting or vectorised kernels. A vector is an array of `T`.
223
+ - First derivatives only: no Hessians or higher order (the tape cannot be differentiated).
224
+ - No GPU or WASM backend; this is plain JavaScript on `Float64Array`s.
225
+ - Not symbolic: the tape is a record of one evaluation, valid at one point.
226
+ - A `Var` is only checked by the type system, not at run time: using one from another tape, or
227
+ after `clear()` or `rewind()` past it, silently reads the wrong node. `new Tape({ debug: true })`
228
+ checks every operand and throws instead, at some cost in speed.
229
+ - A `Var` is a number at run time, so `typeof x === 'number'` cannot tell it from a number; see
230
+ rule 1 of "For AI agents" below.
231
+ - Operations on values outside a primitive's domain produce whatever IEEE arithmetic does
232
+ (`log(-1)` is `NaN`), in value and derivative alike.
233
+
234
+ ## Performance
235
+
236
+ A tape node is six typed-array slots (value, two parent indices, two partials and its adjoint:
237
+ 40 bytes). The arrays double when full, so once a tape has grown to the size of a computation and
238
+ is reused with `clear()` or `rewind()`, recording allocates nothing. The backward sweep is one tight loop over those arrays.
239
+ `npm run bench` times two workloads (a small neural network's loss over 1000 samples, and a
240
+ transcendental-heavy function of 20 000 inputs, both about 1.5 to 2 × 10⁵ nodes). On an
241
+ Apple-silicon laptop with Node 26, relative to evaluating the same function on plain numbers
242
+ (the median of several runs; individual runs vary by ±15%):
243
+
244
+ | | record the tape | record + backward (full gradient) | forward mode (one direction) |
245
+ | ------------------------------ | --------------- | --------------------------------- | ---------------------------- |
246
+ | network (81 inputs) | 1.8× | 2.5× | 1.6× |
247
+ | transcendental (20 000 inputs) | 1.7× | 2.2× | 1.6× |
248
+
249
+ A full gradient costs about two and a half plain evaluations, whatever the number of inputs.
250
+
251
+ Fixed numbers are worth passing as plain operands. A recurrence of 20 000 steps whose operands are
252
+ mostly fixed coefficients (the `recurrence` workloads) records 300 000 nodes when every number is
253
+ a `constant()` and 200 000 with `mulc`/`subc`. On a GitHub runner its full gradient took 3.2 ms
254
+ instead of 4.3 ms, recording alone 2.6 ms instead of 3.3, and forward mode 2.3 instead of 2.9:
255
+ the same values and gradient, a quarter less time.
256
+
257
+ ## For AI agents
258
+
259
+ Rules for writing code that this library differentiates correctly. They are short because each
260
+ one is absolute.
261
+
262
+ 1. **Never tell a variable from a plain number by `typeof x === 'number'` or by its value.** A `Var`
263
+ is a number at run time and passes that test, so code that lifts "numbers" into constants that
264
+ way turns every variable into a constant, and every gradient is silently 0. Keep variables and
265
+ numbers in separate places, and give numbers to `addc`, `mulc` and the other plain-number
266
+ operations. When gradients look wrong, run once with `new Tape({ debug: true })`, which throws at
267
+ the first operand that is not a live variable of that tape, and at a variable where a number
268
+ belongs.
269
+ 2. **Route every operation on a differentiable value through the arithmetic object.**
270
+ `A.add(x, y)`, never `x + y`; `A.exp(x)`, never `Math.exp(x)`. `Var` and `Dual` are numbers
271
+ underneath, so `x + y` type-checks and silently adds two _indices_. There is no runtime check.
272
+ 3. **Write the function generic in `T`**, as `function f<T>(A: Arithmetic<T>, xs: readonly T[]): T`,
273
+ and run it with `plain`, a `Tape` or a `Forward`. Do not write a separate plain version; the
274
+ generic one evaluated with `plain` gives bit-for-bit the same value.
275
+ 4. **Numbers go in through `A.constant(c)`, or as the number operand of `addc`, `subc`, `csub`,
276
+ `mulc`, `divc`, `cdiv`, `powc`.** Data, hyperparameters, literals: all constants, with derivative
277
+ zero. Prefer the number-operand operations: `A.mulc(x, 2)` is one tape node,
278
+ `A.mul(x, A.constant(2))` two, with the same value and derivative.
279
+ 5. **Numbers come out through `A.value(x)`, and they are constants from then on.**
280
+ `A.constant(A.value(x))` cuts the derivative: `A.mul(x, A.constant(A.value(x)))` has derivative
281
+ `x`, not `2x`. Use `A.value` only to branch, to log, or to return a result.
282
+ 6. **Branching on `A.value(x)` is fine; the gradient is that of the branch taken.** At the exact
283
+ boundary it is the taken branch's one-sided derivative. Use `A.abs`, `A.max`, `A.min` for kinks
284
+ rather than hand-written branches, so the documented subgradient applies.
285
+ 7. **Use the stable primitives**: `A.sigmoid`, `A.softplus`, `A.log1p`, `A.expm1`, `A.square`,
286
+ and the composites `logsumexp` and `softmax`. Never assemble them from `exp` and `log`.
287
+ 8. **Keep domains explicit.** `A.log`, `A.sqrt`, `A.div`, `A.pow` with a non-positive base give
288
+ `NaN` or `Infinity` outside their domain, in value and gradient. Clamp or reparametrise first
289
+ (`A.softplus(raw)` for a positive parameter).
290
+ 9. **In hot loops, reuse one `Tape`** and call `clear()` (or `rewind(mark)`) per evaluation; never
291
+ construct a tape per evaluation. Pass an output `Float64Array` to `tape.gradient`.
292
+ 10. **A `Var` dies with its tape's contents.** After `clear()`, or `rewind(m)` to before it was
293
+ made, a `Var` refers to nothing (or to a newer node). Keep no `Var`s across evaluations except
294
+ those made before a `mark()` you rewind to.
295
+ 11. **Verify with `checkGradient(f, at)`** at a few random points away from kinks and branch
296
+ boundaries, in a test, whenever you write a nontrivial function.
297
+ 12. **Give a fixed number of inputs a tuple type**, `[x, y]: readonly [T, T]` (or
298
+ `ScalarFunction<[number, number]>`), so they need no `!` under `noUncheckedIndexedAccess`; with
299
+ a `readonly T[]` of any length, index with `!` and annotate an accumulator seeded from it:
300
+ `let s: T = xs[0]!`.
301
+
302
+ More: [docs/pitfalls.md](docs/pitfalls.md), and [llms.txt](llms.txt) for a compact summary.
303
+
304
+ ## Design notes
305
+
306
+ - **Plain-number operands: `addc`, `subc`, `csub`, `mulc`, `divc`, `cdiv`, `powc`.** In real code
307
+ most operands of a recurrence are fixed parameters and data, and spelling each with
308
+ `constant()` doubles the tape. The `c` marks the side the constant is on, and the arguments are
309
+ in the order of the expression: `subc(a, c)` is `a - c`, `csub(c, a)` is `c - a`. Short names
310
+ rather than `scale` or `shift`, because subtraction and division need both orders and a power
311
+ needs one, and a uniform suffix covers them all. Each is the binary primitive with one operand
312
+ fixed, computed from that primitive's table entry, so its value and derivative are bit for bit
313
+ those of the constant spelling. There is no `minc`/`maxc`: use `max(x, constant(c))`.
314
+
315
+ - **One function, three arithmetics.** The alternatives, operator overloading through wrapper
316
+ objects or a separate tracing API, either allocate an object per operation or make users keep
317
+ two versions of their code. An interface generic in `T` costs one method call per operation and
318
+ keeps a single source.
319
+ - **Branded indices.** `Var` is `number & { brand }`: no allocation, and passing a raw number where
320
+ a variable belongs is a compile error. The brand cannot tell two tapes apart; that check would
321
+ cost a field per variable.
322
+ - **Partials at record time.** Each node stores its local partials as it is recorded, so the
323
+ backward sweep is two multiply-adds per node with no dispatch on the operation. A missing
324
+ argument (both of a leaf's, the second of a unary node's) points at a sink slot with a zero
325
+ partial, so the sweep needs no test for it; leaves are still skipped, for speed.
326
+ - **One table of primitives** (`primitives`) holds every value formula and every partial. `plain`
327
+ uses its functions directly; `Tape` and `Forward` have one method per primitive, each calling its
328
+ own table entry. A single generic recording helper taking the table entry as an argument was
329
+ about 2× slower, because its call sites see every primitive and V8 cannot inline them.
330
+ - **The zero rule** (a zero partial or a zero adjoint or tangent makes the term zero) costs a
331
+ comparison per term and removes the `0 · ∞ = NaN` failures of unused singular paths, in both
332
+ modes alike, so they agree wherever their different groupings of the chain rule cannot matter.
333
+ - **The composites need no rules of their own.** `logsumexp` and `softmax` are recorded as ordinary
334
+ primitives; the max-shift is a constant, which is exact because the shift cancels.
335
+ - **`gradient()` returns `number[]`, `Tape.gradient()` a `Float64Array`.** The convenience is for
336
+ readability and serialisation; the tape method writes into a buffer you can reuse.
337
+ - **`Forward` has `clear()` but no `mark()`/`rewind()`.** Forward mode keeps nothing that a later
338
+ evaluation could reuse except inputs, which are cheap to recreate.
339
+
340
+ ## Development
341
+
342
+ See [AGENTS.md](AGENTS.md) for the layout, the invariants and the gates. In short: `npm run check`
343
+ runs formatting, lint, type checking, tests with a 100% coverage gate, the build, publint, attw and
344
+ the API report check; `npm run mutate` runs mutation testing; `npm run bench` times the workloads,
345
+ and `npm run bench:ab` compares them with another checkout, as CI does for every pull request.
346
+ Changes that affect users get a changeset (`npx changeset`).