first commit
This commit is contained in:
54
docs/foundation/random/random/src/collections/collections.md
Normal file
54
docs/foundation/random/random/src/collections/collections.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# collections/collections
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `collections` module re-presents the five core array-shaped random helpers — `shuffle`, `shuffleInPlace`, `choice`, `sample`, `weightedChoice`, `weightedSample` — with the same ergonomic ‘optional `rng?`' signature used across `@suaveplan/random`. Internally each function delegates to `@suaveplan/pseudo-random`'s unbiased implementation, so you pay no duplication tax but gain the CSPRNG-by-default convenience.
|
||||
|
||||
## Features
|
||||
|
||||
- `shuffle` / `shuveInPlace` — Fisher-Yates, preserves multiset
|
||||
- `choice` — single uniform draw
|
||||
- `sample` — unique, without replacement
|
||||
- `weightedChoice(items, weight, rng?)` — selects an item via a weight function
|
||||
- `weightedSample(items, weight, count, rng?)` — unique-without-replacement weighted draw
|
||||
- Every helper works with no `rng?` argument (uses `getDefaultRng()`)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { shuffle, choice, sample } from "@suaveplan/random/collections";
|
||||
|
||||
const deck = shuffle(["A", "K", "Q", "J"]);
|
||||
const top = choice(deck);
|
||||
const three = sample(deck, 3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { weightedSample } from "@suaveplan/random/collections";
|
||||
|
||||
const items = [
|
||||
{ id: "common", rarity: 70 },
|
||||
{ id: "rare", rarity: 25 },
|
||||
{ id: "epic", rarity: 5 },
|
||||
];
|
||||
const loot = weightedSample(items, (i) => i.rarity, 2, new Sfc32Rng("seed"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `shuffle<T>(input, rng?): T[]` — new array.
|
||||
- `shuffleInPlace<T>(arr, rng?): T[]` — mutates + returns.
|
||||
- `choice<T>(input, rng?): T` — throws `PseudoRandomError.ARG.EMPTY_ARRAY` on empty input.
|
||||
- `sample<T>(input, count, rng?): T[]` — throws on invalid count.
|
||||
- `weightedChoice<T>(items, weight, rng?): T` — throws `RandomError.ARG.EMPTY` on empty.
|
||||
- `weightedSample<T>(items, weight, count, rng?): T[]` — removes picks from the pool so every returned element is unique.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`weightedSample` maintains parallel `pool` and `weights` arrays and splices out the chosen index each round. For `n` items and `k` draws the cost is O(n·k); there is a theoretically faster reservoir algorithm (Efraimidis–Spirakis) but it is rarely worth the extra complexity at the scales this library targets.
|
||||
@@ -0,0 +1,51 @@
|
||||
# distributions/continuous
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `continuous` module implements the nine continuous probability distributions most commonly used in simulations, analytics, and game mechanics: Normal (Gaussian), LogNormal, Exponential, Gamma, Beta, Pareto, Weibull, Cauchy and Logistic. Each is a stateless function taking parameters and an optional `rng?`; together they cover the overwhelming majority of non-uniform sampling needs.
|
||||
|
||||
## Features
|
||||
|
||||
- `normal(mean, stddev, rng?)` — Box-Muller transform
|
||||
- `logNormal(mu, sigma, rng?)` — exponential of a Normal
|
||||
- `exponential(lambda, rng?)` — inverse-CDF method
|
||||
- `gamma(shape, scale, rng?)` — Marsaglia–Tsang for α ≥ 1, Ahrens-Dieter boost for α < 1
|
||||
- `beta(alpha, beta, rng?)` — two Gamma draws
|
||||
- `pareto(xm, alpha, rng?)` — inverse-CDF
|
||||
- `weibull(k, lambda, rng?)` — inverse-CDF
|
||||
- `cauchy(x0, gamma, rng?)` — tangent-transform
|
||||
- `logistic(mu, s, rng?)` — inverse-CDF
|
||||
- Every function validates positivity of scale parameters and throws `RandomError.DISTRIBUTION.INVALID_PARAM`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { normal, exponential } from "@suaveplan/random/distributions";
|
||||
|
||||
const z = normal(0, 1);
|
||||
const wait = exponential(2);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { gamma, beta } from "@suaveplan/random/distributions";
|
||||
|
||||
const rng = new Sfc32Rng("mc-sim-42");
|
||||
const serviceTime = gamma(2, 3, rng);
|
||||
const conversionRate = beta(8, 92, rng); // ~8% with informative prior
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions: `(...params, rng?): number`. Parameter positivity is validated; violations raise `RandomError.DISTRIBUTION.INVALID_PARAM`. Default parameter values match the standard form of each distribution (e.g. `normal()` returns a standard Normal).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Box-Muller discards the paired draw; this is the simpler variant of the transform and has no measurable performance cost at JS call scales.
|
||||
- The Marsaglia–Tsang Gamma sampler is the fastest acceptance-rejection method known for α ≥ 1. For α < 1 we use the boost trick `gamma(α) = gamma(α+1) · U^(1/α)`.
|
||||
- Beta uses two Gamma draws from the same source, which leaves the mean and variance exactly right without requiring a specialised sampler.
|
||||
@@ -0,0 +1,48 @@
|
||||
# distributions/discrete
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `discrete` module implements the four most commonly used discrete distributions: Bernoulli, Geometric, Binomial, and Poisson. Each is a small standalone function taking parameters and an optional `rng?`. Together they cover coin flips, first-success modelling, fixed-trial counting, and rare-event counting — the core of most statistical simulations and agent-based models.
|
||||
|
||||
## Features
|
||||
|
||||
- `bernoulli(p)` — 0/1 with probability `p`
|
||||
- `geometric(p)` — number of failures before first success
|
||||
- `binomial(n, p)` — direct Bernoulli-sum method (O(n))
|
||||
- `poisson(lambda)` — Knuth's multiplicative method for λ ≤ 30; Atkinson's rejection method for larger λ
|
||||
- All parameters are validated (range, integrality); bad inputs throw `RandomError.DISTRIBUTION.INVALID_PARAM`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { bernoulli, poisson } from "@suaveplan/random/distributions";
|
||||
|
||||
const click = bernoulli(0.02);
|
||||
const arrivals = poisson(4);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { binomial } from "@suaveplan/random/distributions";
|
||||
|
||||
const rng = new Sfc32Rng("ab-test");
|
||||
const sample = binomial(1000, 0.08, rng); // expected ~80
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `bernoulli(p, rng?): number`
|
||||
- `geometric(p, rng?): number`
|
||||
- `binomial(n, p, rng?): number`
|
||||
- `poisson(lambda, rng?): number`
|
||||
|
||||
All validate inputs (0 ≤ p ≤ 1, n a non-negative integer, λ > 0).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Knuth's Poisson algorithm is numerically stable up to λ ≈ 30 but gets slow and loses precision beyond that; crossing the threshold to Atkinson's rejection method maintains throughput for heavy-tailed rates. `binomial` uses the simple sum-of-Bernoullis method, which is O(n) per call — fine for `n ≤ 100k` in practice. For very large n with very small p, prefer `poisson(n·p)` as a fast approximation.
|
||||
@@ -0,0 +1,51 @@
|
||||
# distributions/sampler
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `sampler` module turns the eighteen distribution samplers in this package into a single data-driven surface: a tagged-union `DistributionConfig` plus the `sample(config, rng?)` dispatcher. It is the piece that makes distribution choice serialisable — configs can be persisted, A/B-tested, or passed over the wire as JSON and then reconstituted via the Zod schema.
|
||||
|
||||
## Features
|
||||
|
||||
- `DistributionConfig` — tagged union covering all nine continuous + four discrete distributions
|
||||
- `DistributionConfigSchema` — Zod schema with discriminated union, sensible defaults, positivity constraints
|
||||
- `sample(config, rng?)` — dispatch to the right underlying sampler
|
||||
- `createSampler(config, rng?): () => number` — zero-arg closure for hot loops
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sample } from "@suaveplan/random/distributions";
|
||||
|
||||
const arrival = sample({ type: "poisson", lambda: 4 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DistributionConfigSchema, createSampler } from "@suaveplan/random/distributions";
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const raw = '{"type":"gamma","shape":2,"scale":3}';
|
||||
const cfg = DistributionConfigSchema.parse(JSON.parse(raw));
|
||||
const rng = new Sfc32Rng("sim-2026-04-19");
|
||||
const draw = createSampler(cfg, rng);
|
||||
const batch = Array.from({ length: 10_000 }, () => draw());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DistributionConfig` — `{ type: "normal" | "logNormal" | ... ; ...params }`.
|
||||
- `DistributionConfigSchema` — Zod discriminated union.
|
||||
- `sample(config, rng?): number` — unknown `type` throws `RandomError.DISTRIBUTION.UNKNOWN_TYPE`.
|
||||
- `createSampler(config, rng?): () => number`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Default values in the schema match the standard-form parameters of each distribution (mean 0, scale 1, etc). Zod's `.default()` values are applied during parsing, so if a config omits optional fields they fill in without downstream code having to care. Having a Zod schema also lets callers validate user-supplied JSON without writing bespoke type guards.
|
||||
|
||||
### Distributions
|
||||
|
||||
The sampler family covers uniform (`[a, b)`), normal (Box-Muller transform), exponential (inverse CDF), and weighted-discrete via alias tables for O(1) draws over large categorical distributions. Every sampler takes an injected `Rng` so tests can pin draws against golden outputs. The alias-table constructor runs in O(n) amortised time, making it suitable for distribution rebuilds inside hot loops.
|
||||
63
docs/foundation/random/random/src/errors/random-error.md
Normal file
63
docs/foundation/random/random/src/errors/random-error.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# random-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `random-error` module exports the stable error-code registry and typed `RandomError` class used by every public function in `@suaveplan/random`. Every failure — missing CSPRNG, out-of-range argument, malformed distribution parameters, invalid charset preset — reports through the same channel so callers only ever have to switch on one error code namespace.
|
||||
|
||||
Grouping errors by a code prefix (`RANDOM.SOURCE`, `RANDOM.ARG`, `RANDOM.STRING`, `RANDOM.DISTRIBUTION`) keeps messages localisable and programmatically filterable.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable, frozen code registry via `defineCodes("RANDOM", [...])`
|
||||
- `RandomError` extends `SuaveplanError<RandomCode>` — full cause-chain, redaction, OTel-span integration
|
||||
- Coverage across source availability, argument validation, string generation and distribution validation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RandomError, RANDOM_CODES } from "@suaveplan/random/errors";
|
||||
|
||||
throw new RandomError(
|
||||
RANDOM_CODES["ARG.OUT_OF_RANGE"],
|
||||
"random.int.out_of_range",
|
||||
{ context: { min: 10, max: 5 } },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RandomError, RANDOM_CODES } from "@suaveplan/random/errors";
|
||||
|
||||
try {
|
||||
// ... call into @suaveplan/random ...
|
||||
} catch (err) {
|
||||
if (err instanceof RandomError && err.code === RANDOM_CODES["SOURCE.UNAVAILABLE"]) {
|
||||
// fall back to a SeededRng for test environments
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RANDOM_CODES` — frozen record of all error codes.
|
||||
- `RandomError(code, messageKey, options?)` — structured error carrying context, cause, traceId and clock.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Error codes intentionally avoid stuttering with the package name: a consumer writing `RANDOM_CODES["ARG.INVALID"]` gets `"RANDOM.ARG.INVALID"` on the wire. Adding codes is a strictly additive change; removing them is a breaking one.
|
||||
|
||||
### Subclasses & Codes
|
||||
|
||||
`RandomError` is the package-level base class. Subclasses cover invalid range (`min >= max`), invalid length for `randomBytes`, and weight-vector errors in the weighted-choice helper (empty vector, negative weights, NaN). Codes live under the `RANDOM` namespace.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RandomError } from "@suaveplan/random/errors/random-error";
|
||||
throw new RandomError("bad range", { code: "RANDOM.RANGE.INVALID" });
|
||||
```
|
||||
52
docs/foundation/random/random/src/game/dice.md
Normal file
52
docs/foundation/random/random/src/game/dice.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# game/dice
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `dice` module provides three small but ubiquitous primitives for game and simulation code: `coin()`, `dice(sides)`, and `roll(count, sides)`. They all build on `randomInt` so draws are unbiased; they all accept an optional `rng?` so tests can make rolls deterministic.
|
||||
|
||||
Packaging these trivially-implementable functions here keeps the game packages (rummy, bingo, baccarat, minesweeper, etc.) uniform and eliminates the temptation to sprinkle raw `Math.floor(Math.random() * …)` calls through production code.
|
||||
|
||||
## Features
|
||||
|
||||
- `coin` — fair coin flip with string results (`"heads"` or `"tails"`)
|
||||
- `dice` — single-die roll; validates `sides >= 1`
|
||||
- `roll` — multi-die roll returning the vector of individual results
|
||||
- Optional `rng?` on every function
|
||||
- Unbiased integer draws via `randomInt`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { coin, dice, roll } from "@suaveplan/random/game";
|
||||
|
||||
const flip = coin(); // "heads" | "tails"
|
||||
const d20 = dice(20); // integer in [1, 20]
|
||||
const hand = roll(5, 6); // five six-sided dice
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { roll } from "@suaveplan/random/game";
|
||||
|
||||
const rng = new Sfc32Rng("tournament-round-3");
|
||||
const damage = roll(3, 6, rng).reduce((a, b) => a + b, 0); // 3d6
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `coin(rng?): "heads" | "tails"`
|
||||
- `dice(sides, rng?): number` — throws `RandomError.ARG.OUT_OF_RANGE` on `sides < 1` or non-integer `sides`.
|
||||
- `roll(count, sides, rng?): number[]` — throws on negative/non-integer counts; `count === 0` returns `[]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`coin` is implemented via `randomInt(0, 1)` rather than `nextUint32() & 1` so the underlying rejection sampler handles any biases even though the range is a power of two. The minor overhead is irrelevant for the typical hundreds-of-calls-per-turn workload.
|
||||
|
||||
### Notation
|
||||
|
||||
`rollDice` accepts the classic tabletop notation `NdS+M` where N is the number of dice, S is the side count, and M is a fixed modifier. The parser handles `d20`, `2d6+3`, and `4d6` drop-lowest via the explicit `rollDrop` helper. Each individual roll is a fresh draw from the injected `Rng`, and the return value carries both the per-die results and the final total so tabletop UIs can render the breakdown.
|
||||
43
docs/foundation/random/random/src/ids/nanoid.md
Normal file
43
docs/foundation/random/random/src/ids/nanoid.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# ids/nanoid
|
||||
|
||||
## Purpose
|
||||
|
||||
The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character alphabet. The default 21-character output encodes ~126 bits of entropy — comparable to UUID — in a substantially shorter, more human-friendly form.
|
||||
|
||||
## Features
|
||||
|
||||
- `randomNanoid(length?, alphabet?, rng?)` — every parameter is optional
|
||||
- Default 21-character, 64-char alphabet matches the canonical `nanoid` JS library byte-for-byte
|
||||
- Unbiased character draws via `randomInt` rejection sampling
|
||||
- Typed errors on negative length or empty alphabet
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomNanoid } from "@suaveplan/random/ids";
|
||||
|
||||
const id = randomNanoid(); // 21 chars
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomNanoid } from "@suaveplan/random/ids";
|
||||
|
||||
const shortCode = randomNanoid(6, "ABCDEFGHJKMNPQRSTUVWXYZ23456789", new Mulberry32Rng("promo"));
|
||||
// 6 characters; no ambiguous 0/O/I/1/l
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NANOID_ALPHABET` — the canonical 64-character Nano ID alphabet.
|
||||
- `randomNanoid(length = 21, alphabet = NANOID_ALPHABET, rng?): string` — throws `RandomError.ARG.INVALID` on bad length or `STRING.EMPTY_CHARSET` on empty alphabet.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The reference Nano ID implementation uses a bitmask rejection loop rather than modulo rejection; here we reuse `randomInt`, which achieves the same uniformity guarantee through a slightly different mechanism. Output is byte-for-byte comparable across equal seeds when both approaches consume the same number of uint32 draws per character — which is true for alphabets whose length divides 2^32, and only slightly different (at the rejection boundary) otherwise.
|
||||
|
||||
### ID Properties
|
||||
|
||||
The default 21-character nanoid uses the 64-entry URL-safe alphabet `A–Z a–z 0–9 _ -`, giving ~126 bits of entropy — collision-resistant for any realistic workload. The alphabet and length are both overrideable, and the RNG is injected rather than sourced globally so tests can pin outputs. Internally it draws uniform `Uint32` values and rejects bytes that would bias the distribution modulo alphabet size.
|
||||
40
docs/foundation/random/random/src/ids/ulid.md
Normal file
40
docs/foundation/random/random/src/ids/ulid.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# ids/ulid
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feerasta specification: 48-bit millisecond timestamp + 80 random bits, Crockford base32 encoded to a 26-character string. ULIDs sort lexicographically by creation time, are fixed-width, and are URL-safe.
|
||||
|
||||
## Features
|
||||
|
||||
- Exactly 26 characters, Crockford base32 (`0-9A-HJKMNP-TV-Z`) — no I/L/O/U to avoid confusion
|
||||
- 48-bit timestamp prefix that sorts correctly through year 10889
|
||||
- Injectable `now?` and `rng?` for deterministic tests
|
||||
- Rejects out-of-range timestamps with typed `RandomError.ARG.OUT_OF_RANGE`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomULID } from "@suaveplan/random/ids";
|
||||
|
||||
const id = randomULID();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomULID } from "@suaveplan/random/ids";
|
||||
|
||||
const rng = new Sfc32Rng("batch-2026-04-19");
|
||||
const ids = Array.from({ length: 1000 }, () => randomULID(Date.now, rng));
|
||||
// Sorted set matches insertion order — handy for offset-paginated APIs.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomULID(now?, rng?): string` — returns a 26-character Crockford base32 ULID.
|
||||
Throws `RandomError.ARG.OUT_OF_RANGE` if `now()` returns a negative number or a value above the 48-bit millisecond ceiling (year 10889).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The timestamp is encoded by repeatedly taking `t % 32` and dividing by 32 for 10 digits, which packs 50 bits of space but only ever uses the lower 48 (the top 2 bits are always zero in the Crockford encoding). Random bits are drawn 5 at a time from `rng.nextUint32()` — a uint32 supplies six 5-bit nibbles, but we take one at a time for simplicity; this allocates 16 calls per ULID. That is cheap on both the seeded and the crypto-backed paths.
|
||||
44
docs/foundation/random/random/src/ids/uuid.md
Normal file
44
docs/foundation/random/random/src/ids/uuid.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# ids/uuid
|
||||
|
||||
## Purpose
|
||||
|
||||
The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with the version/variant nibbles set; UUIDv7 is a 48-bit timestamp prefix with a 12-bit sequence counter and 62 random bits, producing sortable, collision-resistant IDs that lexicographically order by creation time.
|
||||
|
||||
UUIDv7 is the preferred choice for new primary keys: it is time-sortable like ULID, compatible with every existing UUID tool, and produces no index hot-spots in databases.
|
||||
|
||||
## Features
|
||||
|
||||
- `randomUUID()` — RFC 9562 §5.4 version-4 random UUID
|
||||
- `randomUUIDv7()` — RFC 9562 §5.7 version-7 sortable UUID with same-millisecond monotonicity via a 12-bit sequence counter
|
||||
- Injectable `rng?` for deterministic generation in tests
|
||||
- Injectable `now?` for v7 clock control
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomUUID, randomUUIDv7 } from "@suaveplan/random/ids";
|
||||
|
||||
const correlationId = randomUUID();
|
||||
const orderId = randomUUIDv7();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomUUIDv7 } from "@suaveplan/random/ids";
|
||||
|
||||
const rng = new Mulberry32Rng("test-run");
|
||||
const fixedClock = () => 1_700_000_000_000;
|
||||
const ids = Array.from({ length: 5 }, () => randomUUIDv7(fixedClock, rng));
|
||||
// All 5 IDs share the timestamp prefix and are monotonically ordered by the sequence counter.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomUUID(rng?): string` — 122 random bits, versioned `4`, variant `10xx`.
|
||||
- `randomUUIDv7(now?, rng?): string` — 48-bit timestamp + 12-bit sequence (monotonic across identical timestamps) + 62 random bits, versioned `7`, variant `10xx`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
UUIDv7 maintains two module-level state variables: `lastV7Ts` (the last timestamp observed) and `lastV7Seq` (the rolling 12-bit counter within that timestamp). When the clock advances, `lastV7Seq` is re-seeded from `rng.nextUint32()`; within the same millisecond it simply increments. A 12-bit counter permits 4096 UUIDs per millisecond per process before collision, which comfortably exceeds realistic workloads.
|
||||
61
docs/foundation/random/random/src/numbers/random-float.md
Normal file
61
docs/foundation/random/random/src/numbers/random-float.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# numbers/random-float
|
||||
|
||||
## Purpose
|
||||
|
||||
The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`. The resolution is 32 bits (inherited from the `Rng.nextFloat()` contract), which is more than enough for every non-financial use case in the project.
|
||||
|
||||
## Features
|
||||
|
||||
- Half-open interval `[min, max)`
|
||||
- Accepts any `Rng` or uses the package default
|
||||
- Typed `RandomError` on non-finite or inverted ranges
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomFloat } from "@suaveplan/random/numbers";
|
||||
|
||||
const jitter = randomFloat(-0.1, 0.1);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomFloat } from "@suaveplan/random/numbers";
|
||||
|
||||
const rng = new Mulberry32Rng("layout");
|
||||
const x = randomFloat(0, 1920, rng);
|
||||
const y = randomFloat(0, 1080, rng);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomFloat(min, max, rng?): number` — uniform in `[min, max)`; throws
|
||||
`RandomError.ARG.INVALID` or `ARG.OUT_OF_RANGE` on bad inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The result is `min + nextFloat() * (max - min)`. Because `nextFloat()` returns `[0, 1)`, the output is strictly less than `max` regardless of rounding — you do not need to clamp the upper bound in caller code.
|
||||
|
||||
### Range Semantics
|
||||
|
||||
`randomFloat(min, max)` returns a uniformly distributed value in `[min, max)` — min inclusive, max exclusive — computed by scaling the injected `Rng`'s `nextFloat()` output (which is itself in `[0, 1)`). A `RandomError` is thrown when `min >= max` or either bound is non-finite, so callers never see silently-wrong output.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomFloat } from "@suaveplan/random/numbers/random-float";
|
||||
const temperature = randomFloat(18.5, 24.5); // Celsius, realistic room temp
|
||||
```
|
||||
|
||||
## Further Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { randomFloat } from "@suaveplan/random/numbers/random-float";
|
||||
import { createSeededRng } from "@suaveplan/pseudo-random";
|
||||
const rng = createSeededRng(42);
|
||||
const draws = Array.from({ length: 1000 }, () => randomFloat(0, 1, { rng }));
|
||||
```
|
||||
|
||||
The exclusive upper bound matches the ECMA-262 `Math.random()` convention, which every Suaveplan consumer already assumes. That exclusive convention is also what makes `randomFloat(0, n)` a safe index-into-array-of-length-n building block for the array helpers in the package.
|
||||
45
docs/foundation/random/random/src/numbers/random-int.md
Normal file
45
docs/foundation/random/random/src/numbers/random-int.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# numbers/random-int
|
||||
|
||||
## Purpose
|
||||
|
||||
The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.random() * (max - min + 1)) + min` idiom (which is slightly biased whenever the range doesn't divide `2^32`), this implementation uses rejection sampling to guarantee a perfectly uniform distribution even at the extreme edges.
|
||||
|
||||
It accepts an optional `rng?` for reproducibility — pass a `SeededRng` in tests, omit for the crypto-backed default.
|
||||
|
||||
## Features
|
||||
|
||||
- Uniform over `[min, max]` inclusive both ends
|
||||
- Rejection sampling eliminates modulo bias
|
||||
- Typed `RandomError` on out-of-range or non-integer inputs
|
||||
- Works with any `Rng` (or the crypto-backed default)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomInt } from "@suaveplan/random/numbers";
|
||||
|
||||
const d6 = randomInt(1, 6);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomInt } from "@suaveplan/random/numbers";
|
||||
|
||||
const rng = new Sfc32Rng("replay");
|
||||
const hand = Array.from({ length: 5 }, () => randomInt(1, 52, rng));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomInt(min, max, rng?): number` — returns a uniformly random integer in `[min, max]`.
|
||||
Throws `RandomError.ARG.NOT_INTEGER` / `ARG.OUT_OF_RANGE` on bad inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The rejection loop divides the uint32 space into equal-sized buckets of size `range` and rejects any draw that falls into the partial trailing bucket. The expected number of draws is `2^32 / limit`, which is never worse than `2` and typically `1.0000…`.
|
||||
|
||||
### Range Semantics
|
||||
|
||||
`randomInt(min, max)` returns a uniformly distributed integer in `[min, max)` using rejection sampling against the injected `Rng`'s `nextUint32` to avoid the modulo-bias that plagues naive `% n` implementations. Both bounds must be safe integers and `min < max` — anything else throws `RandomError`. The rejection loop has an exponentially small probability of iterating more than once.
|
||||
52
docs/foundation/random/random/src/objects/objects.md
Normal file
52
docs/foundation/random/random/src/objects/objects.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# objects/objects
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `objects` module lifts the collection-random operations onto plain records. It gives you `randomKey`, `randomValue`, `randomEntry`, and `randomSubset` — the four operations you most often want when sampling JSON-shaped configurations, feature flags, or dictionary-backed domain models.
|
||||
|
||||
## Features
|
||||
|
||||
- `randomKey(obj, rng?)` — a single `keyof T` drawn uniformly
|
||||
- `randomValue(obj, rng?)` — the value at a random key
|
||||
- `randomEntry(obj, rng?)` — `[key, value]` tuple
|
||||
- `randomSubset(obj, count, rng?)` — new object with `count` random entries
|
||||
- Every function delegates to the collection-random helpers, so draws use the same unbiased sampler
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomKey, randomSubset } from "@suaveplan/random/objects";
|
||||
|
||||
const config = { alpha: true, beta: false, gamma: true };
|
||||
const someFlag = randomKey(config);
|
||||
const subset = randomSubset(config, 2);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomEntry } from "@suaveplan/random/objects";
|
||||
|
||||
const dictionary = { apple: "red", banana: "yellow", cherry: "red" };
|
||||
const rng = new Mulberry32Rng("quiz");
|
||||
const [word, colour] = randomEntry(dictionary, rng);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomKey<T extends object>(obj, rng?): keyof T`
|
||||
- `randomValue<T extends object>(obj, rng?): T[keyof T]`
|
||||
- `randomEntry<T extends object>(obj, rng?): [keyof T, T[keyof T]]`
|
||||
- `randomSubset<T extends object>(obj, count, rng?): Partial<T>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Object.keys` is called once per operation rather than caching, because the object may be mutated between calls. If you need repeated subsets from the same object, extract `Object.keys(obj)` yourself and call `sample` directly from `@suaveplan/random/collections`.
|
||||
|
||||
### Helpers
|
||||
|
||||
`randomElement` returns a uniformly-chosen element from any non-empty array; `shuffle` performs a Fisher-Yates in-place shuffle backed by the injected `Rng`; `sample` returns k elements without replacement using reservoir sampling for stable memory on large inputs. All three throw `RandomError` on invalid input (empty array, `k > arr.length`, `k < 0`).
|
||||
48
docs/foundation/random/random/src/source/crypto-rng.md
Normal file
48
docs/foundation/random/random/src/source/crypto-rng.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# source/crypto-rng
|
||||
|
||||
## Purpose
|
||||
|
||||
The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographically secure random source. It wraps `globalThis.crypto.getRandomValues` in an `Rng` adapter (so it composes with every helper in this package) and exposes a `randomBytes(n)` factory that handles WebCrypto's 65 536-byte per-call limit by chunking.
|
||||
|
||||
Every other public function in `@suaveplan/random` draws from `cryptoRng()` by default; isolating the CSPRNG here means the `no-disallowed` test can enforce "no other file in src/ touches `crypto.getRandomValues`".
|
||||
|
||||
## Features
|
||||
|
||||
- `cryptoRng()` — returns a fresh `Rng` reading 4 random bytes per `nextUint32`
|
||||
- `randomBytes(length)` — crypto-backed buffer, auto-chunking for large lengths
|
||||
- Typed `RandomError.SOURCE.UNAVAILABLE` when WebCrypto is absent (e.g. some edge sandboxes)
|
||||
- Typed `RandomError.ARG.INVALID` when `length` is not a non-negative integer
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { cryptoRng, randomBytes } from "@suaveplan/random/source";
|
||||
|
||||
const rng = cryptoRng();
|
||||
const draw = rng.nextUint32();
|
||||
const key = randomBytes(32);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { cryptoRng, randomBytes } from "@suaveplan/random/source";
|
||||
import { shuffle } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = cryptoRng();
|
||||
const deck = shuffle(["A", "K", "Q", "J"], rng);
|
||||
|
||||
// Session token: 256-bit, URL-safe
|
||||
const token = [...randomBytes(32)]
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `cryptoRng(): Rng` — throws `RandomError.SOURCE.UNAVAILABLE` if `globalThis.crypto.getRandomValues` is missing.
|
||||
- `randomBytes(length: number): Uint8Array` — throws `RandomError.ARG.INVALID` on bad lengths. Chunks at 65 536 bytes to stay within the WebCrypto limit.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`cryptoRng` reuses a single 4-byte buffer between draws to avoid allocation pressure in hot loops. That buffer is captured in the returned object's closure, so each call to `cryptoRng()` gets an independent scratch area and the returned `Rng` is safe to use from a single call stack (the usual JS contract — do not share a single `Rng` instance across unrelated concurrent tasks that expect unique draws, because WebCrypto itself is thread-safe but the shared 4-byte buffer is not).
|
||||
49
docs/foundation/random/random/src/source/default-rng.md
Normal file
49
docs/foundation/random/random/src/source/default-rng.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# source/default-rng
|
||||
|
||||
## Purpose
|
||||
|
||||
The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to when the caller doesn't pass an explicit `rng?`. It is deliberately a single module-local variable (not a class, not a context) so the API stays ergonomic: call `randomString()` with no arguments and get a crypto-backed 16-character token.
|
||||
|
||||
Tests swap the default for a `SeededRng` to make randomness deterministic; production code leaves the default alone.
|
||||
|
||||
## Features
|
||||
|
||||
- `getDefaultRng()` — lazy initialisation, returns a stable `cryptoRng()` on first call
|
||||
- `setDefaultRng(rng)` — dependency-injection hook for tests and custom sources
|
||||
- `resetDefaultRng()` — restore the default, typically called from `afterEach`
|
||||
- Single-module state; no transitive cost when tree-shaken away
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getDefaultRng } from "@suaveplan/random/source";
|
||||
|
||||
const v = getDefaultRng().nextUint32();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { getDefaultRng, resetDefaultRng, setDefaultRng } from "@suaveplan/random/source";
|
||||
import { afterEach } from "@suaveplan/testing/runner";
|
||||
|
||||
afterEach(() => resetDefaultRng());
|
||||
|
||||
setDefaultRng(new Mulberry32Rng("test"));
|
||||
// ...every @suaveplan/random helper called with no `rng?` now draws from the seeded source
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getDefaultRng(): Rng` — returns the current default, creating a `cryptoRng()` on first access.
|
||||
- `setDefaultRng(rng: Rng): void` — replace the default.
|
||||
- `resetDefaultRng(): void` — clear the cache; next `getDefaultRng()` returns a fresh `cryptoRng()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default is created lazily rather than eagerly so that importing the package in an environment without WebCrypto doesn't throw at import time — it only throws if you actually try to draw. This matters for bundlers that preload tree-shaken modules.
|
||||
|
||||
### Default Source
|
||||
|
||||
`defaultRng` wraps `crypto.getRandomValues` from the ambient Web Crypto API into the canonical `Rng` shape from `@suaveplan/types`. It is the default injection for every helper in the package when the caller does not supply one. On platforms without `crypto.getRandomValues` (pre-Node-19 without `--experimental-global-webcrypto`, for example) the module throws at construction time rather than silently falling back to `Math.random`.
|
||||
48
docs/foundation/random/random/src/strings/random-string.md
Normal file
48
docs/foundation/random/random/src/strings/random-string.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# strings/random-string
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `random-string` module produces random strings from one of eight curated charset presets (alpha, alpha-upper, alpha-lower, alphanumeric, numeric, hex, base58, base64url) or from an arbitrary custom alphabet. Every character is drawn with an unbiased `randomInt(0, alphabet.length - 1)`, so the output distribution is perfectly uniform regardless of alphabet length.
|
||||
|
||||
It is the go-to primitive for short random IDs, temporary passwords, invite codes, and any bespoke token scheme where none of the formal ID formats (UUID / ULID / Nanoid) applies.
|
||||
|
||||
## Features
|
||||
|
||||
- Eight curated presets, including Bitcoin-style base58 (no `0/O/I/l`) and URL-safe base64
|
||||
- Custom alphabets for bespoke use cases (presets and custom are mutually exclusive)
|
||||
- Accepts `rng?` for deterministic output in tests
|
||||
- Unbiased character picking via `randomInt` rejection sampling
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomString } from "@suaveplan/random/strings";
|
||||
|
||||
const token = randomString({ length: 16, charset: "base58" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomString } from "@suaveplan/random/strings";
|
||||
|
||||
const code = randomString({
|
||||
length: 6,
|
||||
customCharset: "ABCDEFGHJKMNPQRSTUVWXYZ23456789", // no ambiguous chars
|
||||
rng: new Sfc32Rng("voucher-run-1"),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CHARSETS` — readonly record of the eight built-in charsets.
|
||||
- `CharsetPreset` — `keyof typeof CHARSETS`.
|
||||
- `randomString({ length, charset?, customCharset?, rng? }): string` — throws `RandomError.STRING.MUTUALLY_EXCLUSIVE` if both `charset` and `customCharset` are set, and `STRING.EMPTY_CHARSET` if the resolved alphabet is empty. Defaults to `alphanumeric` when no charset is specified.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
String concatenation by `+=` is slightly slower than `Array.prototype.join` for very long outputs, but for the common case (< 256 characters) the V8 rope-string optimisation makes `+=` the simplest readable choice. Because this module delegates character picking to `randomInt`, alphabet length never introduces modulo bias.
|
||||
Reference in New Issue
Block a user