Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-18

View File

@@ -0,0 +1,235 @@
# Design: enterprise-random-family
## Context
`@suaveplan/types` defines `Rng = { nextUint32(): number; nextFloat(): number }` but ships zero implementations. The genesis `@suaveplan/testing` package contains an inline Mulberry32 `seededRng` for test fixtures — this inline copy stays (testing packages cannot take workspace deps), but the canonical implementations live here. The games codebase runs five different hand-rolled PRNG algorithms across bingo, baccarat, rummy, minesweeper, and fruit-machine — this change provides the single canonical set they all migrate to.
Three packages with zero inter-dependency between them at the leaf level: `pseudo-random` has no workspace deps, `random` depends on `@suaveplan/types` and `@suaveplan/error`, and `noise` depends only on `@suaveplan/error`.
## Goals / Non-Goals
**Goals:**
- Ship `SeededRng` interface and four algorithm implementations (Mulberry32, SFC32, Xoshiro256++, CounterRng) with full offset/fork/checkpoint support
- Ship the full high-level random utilities surface: strings, IDs, collections, objects, game primitives, and all statistical distributions with `DistributionConfig` serializable dispatch
- Ship spatial noise functions (Perlin, Simplex, Value, Worley, fBm, Blue noise)
- All three packages at 100% coverage (foundational layer)
- Full enterprise checklist for each
**Non-Goals:**
- Cryptographic PRNGs (ChaCha20-DRBG, HMAC-DRBG) — belong in `@suaveplan/crypto-sodium`/`crypto-browser`
- Gaussian Process regression, Monte Carlo integration, statistical hypothesis testing — future `@suaveplan/stats`
- Geo random geometry (`randomPoint`, `randomPolygon`) — deferred to `@suaveplan/geo/random`
- Changing `@suaveplan/testing`'s inline Mulberry32 — testing rule holds; it stays inline
## Decisions
### Package: `@suaveplan/pseudo-random`
```text
src/
types/
seeded-rng.types.ts / seeded-rng.md # SeededRng interface + RngCheckpoint
algorithms/
mulberry32.ts / mulberry32.test.ts / mulberry32.md
sfc32.ts / sfc32.test.ts / sfc32.md
xoshiro256.ts / xoshiro256.test.ts / xoshiro256.md
counter-rng.ts / counter-rng.test.ts / counter-rng.md
seed/
normalize.ts / normalize.test.ts / normalize.md # normalizeSeed(string|number): number
utils/
shuffle.ts / shuffle.test.ts / shuffle.md # shuffle, shuffleInPlace
sample.ts / sample.test.ts / sample.md # sample, weightedSample
distributions.ts / distributions.test.ts / distributions.md # uniform int/float helpers
errors/
codes.ts / pseudo-random-error.ts / index.ts
constants/index.ts
index.ts
```
**`SeededRng` interface:**
```ts
interface SeededRng extends Rng {
readonly seed: number
readonly position: number
skip(n: number): this
reset(): this
at(offset: number): SeededRng
fork(id: number | string): SeededRng
checkpoint(): RngCheckpoint
restore(checkpoint: RngCheckpoint): this
}
```
**Algorithm selection guide** (exported as docs, not enforced):
- `Mulberry32Rng` — 32-bit state, ~2^32 period. Best for trivial use cases, minimal memory footprint.
- `Sfc32Rng` — 128-bit state, period >2^127. Best default for games and simulations.
- `Xoshiro256Rng` — 256-bit state, period 2^256-1, supports fixed-size jump (2^128 and 2^192 steps). Best when independent parallel streams are needed at scale.
- `CounterRng` — splitmix64-based hash counter. O(1) seek to any position. Best for direct-address use cases like bingo ticket generation.
**Seed normalization** — `normalizeSeed(seed: string | number): number`:
- String seeds: splitmix64 mixing over char codes (replaces djb2/FNV-1a used in bingo)
- Numeric seeds: splitmix64 single-round mixing to avalanche input bias
- Rejects NaN, Infinity, non-safe-integers
**`fork(id)`** — derives a child seed via `splitmix64(parentSeed ^ hash(id))`. Child streams are statistically independent from the parent and from each other. Critical for game use cases:
```ts
const master = new Sfc32Rng(gameSeed)
const weather = master.fork('weather')
const enemies = master.fork('enemies')
const loot = master.fork('loot')
```
**`at(offset)`** — for linear-burn algorithms (Mulberry32, SFC32): constructs a new instance at `seed` and burns `offset` values at construction time (O(offset), cost paid once). For CounterRng: O(1), directly sets the counter.
**Offset validation** — no hard cap (the bingo 1,000,000 limit was app-specific). Throws `PRNG.OFFSET.OUT_OF_RANGE` only for negative values.
### Package: `@suaveplan/random`
```text
src/
source/
crypto-rng.ts / crypto-rng.test.ts / crypto-rng.md # cryptoRng(): Rng
default-rng.ts / default-rng.test.ts / default-rng.md # getDefaultRng / setDefaultRng
bytes/
random-bytes.ts / random-bytes.test.ts / random-bytes.md
strings/
random-string.ts / random-string.test.ts / random-string.md # presets + custom charset
ids/
uuid.ts / uuid.test.ts / uuid.md # randomUUID (v4+v7), randomULID, randomNanoid
numbers/
random-int.ts / random-int.test.ts / random-int.md
random-float.ts / random-float.test.ts / random-float.md
collections/
shuffle.ts / shuffle.test.ts / shuffle.md
sample.ts / sample.test.ts / sample.md
choice.ts / choice.test.ts / choice.md
weighted.ts / weighted.test.ts / weighted.md # weightedChoice, weightedSample
objects/
random-key.ts / random-key.test.ts / random-key.md
random-value.ts / ...
random-entry.ts / ...
random-subset.ts / ...
game/
dice.ts / dice.test.ts / dice.md # coin, dice, roll
distributions/
continuous/
normal.ts / normal.test.ts / normal.md
log-normal.ts / ...
exponential.ts / ...
gamma.ts / ...
beta.ts / ...
pareto.ts / ...
weibull.ts / ...
cauchy.ts / ...
logistic.ts / ...
discrete/
bernoulli.ts / bernoulli.test.ts / bernoulli.md
geometric.ts / ...
binomial.ts / ...
poisson.ts / ...
sampler.ts / sampler.test.ts / sampler.md # createSampler, sample, DistributionConfig
errors/
codes.ts / random-error.ts / index.ts
constants/index.ts
index.ts
```
**`cryptoRng()` factory** — returns an object implementing `@suaveplan/types`'s `Rng` interface backed by `crypto.getRandomValues`:
```ts
{
nextUint32: () => { const buf = new Uint32Array(1); crypto.getRandomValues(buf); return buf[0] },
nextFloat: function() { return this.nextUint32() / 0x1_0000_0000 }
}
```
This is the **only** callsite for `crypto.getRandomValues` in `@suaveplan/random`. Guarded by `tests/no-disallowed.test.ts`.
**Default source** — module-level mutable (non-exported) singleton. `getDefaultRng()` returns it; `setDefaultRng(rng)` replaces it (test hook). Default is `cryptoRng()`.
**String presets** (`randomString(length, { type, rng? })`):
| type | charset |
|---|---|
| `hex` | `0-9a-f` |
| `base64` | A-Z, a-z, 0-9, +, / |
| `base64url` | A-Z, a-z, 0-9, -, _ |
| `url-safe` | A-Z, a-z, 0-9, -, _, . |
| `numeric` | `0-9` |
| `alphanumeric` | A-Z, a-z, 0-9 |
| `distinguishable` | `CDEHKMPRTUWXY012458` (no I/O/0/1 confusion) |
| `ascii-printable` | all printable ASCII 0x20-0x7E |
Custom charset via `{ characters: string }` option.
**UUID v7** — time-ordered, 48-bit millisecond timestamp in high bits, 74 random bits. Monotonically increasing within the same millisecond via sequence counter.
**`DistributionConfig` tagged union** — every distribution has a named-parameter config object. The `type` discriminant selects the algorithm; all other fields are the algorithm's parameters. `createSampler(config, rng?)` returns `() => number`. `sample(config, rng?)` is a one-shot convenience. Config objects are plain JSON-serializable data — `rng` is always external.
**Implementation notes for distributions:**
- Normal: Box-Muller transform
- LogNormal: `exp(normal(mu, sigma))`
- Gamma: Marsaglia-Tsang method (k ≥ 1); Ahrens-Dieter for k < 1
- Beta: ratio of two Gamma samples
- Poisson: Knuth algorithm for λ ≤ 30; rejection method for larger λ
- All discrete distributions use `nextUint32()` not `nextFloat()` for the base draw
### Package: `@suaveplan/noise`
```text
src/
perlin/
perlin.ts / perlin.test.ts / perlin.md # perlin2, perlin3 with seed
simplex/
simplex.ts / simplex.test.ts / simplex.md # simplex2, simplex3, simplex4
value/
value.ts / value.test.ts / value.md # value2, value3
worley/
worley.ts / worley.test.ts / worley.md # worley2, worley3, WorleyMode (F1|F2|F1-F2)
fbm/
fbm.ts / fbm.test.ts / fbm.md # fbm compositor, FbmOptions
blue/
blue.ts / blue.test.ts / blue.md # blueNoiseSample, Poisson disk sampling
errors/
codes.ts / noise-error.ts / index.ts
constants/index.ts
index.ts
```
All noise functions accept a `seed: number` parameter and return values normalised to `[-1, 1]` (except Worley which returns `[0, 1]` distances). No dependency on `@suaveplan/pseudo-random` — noise uses its own internal seeded hash permutation tables, which is the standard noise implementation pattern.
**fBm compositor:**
```ts
fbm(noiseFn, x, y, {
octaves: 6,
lacunarity: 2.0,
gain: 0.5,
seed: 0
}): number
```
Composes any 2D noise function with octave summation.
**Blue noise** — Bridson's Poisson disk sampling algorithm:
```ts
blueNoiseSample(width, height, minDistance, { seed?, rng? }): [number, number][]
```
## Risks / Trade-offs
| Risk | Mitigation |
|---|---|
| `at(offset)` is O(offset) for Mulberry32/SFC32 | Document clearly; `CounterRng` is the O(1) alternative; no silent perf traps |
| `setDefaultRng` is global mutable state | Documented as test-only; tests restore previous value with `afterEach`; not exported from subpath exports |
| `fork` child streams not proven independent | Use `normalizeSeed` mixing + test with fast-check that `fork('a')` and `fork('b')` produce uncorrelated sequences across 10k samples |
| Simplex noise patent (expired 2022) | US patent 6,867,776 expired April 2022 — safe to use |
| Blue noise is O(n) output size | Cap at 100k points; throw `NOISE.BLUE.SAMPLE_LIMIT_EXCEEDED` above |

View File

@@ -0,0 +1,41 @@
# Change: enterprise-random-family
## Why
Randomness is used in 15+ places across the genesis and games codebases with five different hand-rolled PRNG algorithms (Mulberry32, SFC32, LCG, Park-Miller, xorshift128+) and direct `Math.random()` calls in game logic. There is no canonical seeded PRNG with offset/fork/checkpoint support, no statistical distribution library, and no unified string/ID generation surface. `@suaveplan/types` defines the `Rng` interface but no implementations ship. This change delivers the full randomness stack in three focused packages.
## What Changes
### New packages (3)
- **`@suaveplan/pseudo-random`** — deterministic seeded PRNG implementations conforming to `@suaveplan/types`'s `Rng` interface. Algorithms: Mulberry32 (32-bit, ultra-light), SFC32 (128-bit, high-quality), Xoshiro256++ (256-bit, supports jump), CounterRng (hash-based, O(1) seek to any position). Extended `SeededRng` interface adds `skip(n)`, `at(offset)`, `fork(id)`, `checkpoint()`, `restore(state)`. Seed normalization via splitmix64 mixing (not djb2/FNV-1a). Fisher-Yates shuffle, weighted choice, and distribution helpers all accept an `rng?` parameter. Zero dependency on `crypto.getRandomValues`.
- **`@suaveplan/random`** — high-level random utilities backed by a CSPRNG default. Provides: pluggable global source (`getDefaultRng`/`setDefaultRng`), `cryptoRng()` factory (WebCrypto-backed `Rng`), string/ID generation (`randomString` with 8 character-set presets, `randomUUID` v4+v7, `randomULID`, `randomNanoid`), collection operations (`shuffle`, `sample`, `choice`, `weightedChoice`, `weightedSample`), object/record operations (`randomKey`, `randomValue`, `randomEntry`, `randomSubset`), game primitives (`coin`, `dice`, `roll`), and statistical distributions (Normal, LogNormal, Exponential, Gamma, Beta, Pareto, Weibull, Cauchy, Logistic, Bernoulli, Geometric, Binomial, Poisson) plus a unified `createSampler(config: DistributionConfig, rng?)` dispatcher for data-driven/serializable distribution selection. All functions accept optional `rng?: Rng`.
- **`@suaveplan/noise`** — spatial field functions for procedural generation. Perlin (2D/3D), Simplex (2D/3D/4D), Value noise (2D/3D), Worley/cell noise (2D/3D), Fractional Brownian Motion (fBm) compositor, and Blue noise sampler. All seeded. These are deterministic field functions, not PRNGs — they belong in a separate package from `@suaveplan/pseudo-random`.
### Modified Capabilities
- **`@suaveplan/geo`** (future) — will receive a `random` subpath (`@suaveplan/geo/random`) exposing `randomPosition`, `randomPoint`, `randomPolygon`, `randomLineString` with bbox constraints. Not in scope for this change; noted for coordination.
## Capabilities
### New Capabilities
- `seeded-prng`: `SeededRng` interface + Mulberry32, SFC32, Xoshiro256++, CounterRng implementations with offset/fork/checkpoint. Consolidates hand-rolled PRNGs from bingo, baccarat, rummy, minesweeper, fruit-machine.
- `random-utilities`: CSPRNG-backed general utilities — strings, IDs, collections, objects, game primitives, all injectable.
- `statistical-distributions`: Full d3-random-equivalent distribution suite with `DistributionConfig` tagged-union for serializable dispatch.
- `noise-functions`: Spatial/procedural noise field functions (Perlin, Simplex, Value, Worley, fBm, Blue noise).
### Modified Capabilities
None — no existing genesis specs exist for these capabilities yet.
## Impact
- **Dependency order**: `pseudo-random` (no workspace deps) → `random` (depends on `@suaveplan/types`, `@suaveplan/error`) → `noise` (independent, `@suaveplan/error` only)
- **`@suaveplan/pseudo-random` and `@suaveplan/random` are parallel-shippable** — `noise` has no dependency on either
- **Consolidates 5 hand-rolled PRNGs** across games codebase — migration path: replace with `Mulberry32Rng` or `Sfc32Rng` from `@suaveplan/pseudo-random`
- **`@suaveplan/testing` `seededRng`** — currently ships its own Mulberry32. After `@suaveplan/pseudo-random` ships, `@suaveplan/testing` can re-export from there (but must not add a workspace dep — the testing rule holds; the existing inline implementation stays)
- **`@suaveplan/crypto` `ByteSource`** — crypto's byte-level injection type is distinct from `Rng` in `@suaveplan/types` and is not affected by this change
- **Non-goals**: Cryptographic PRNGs (ChaCha20-DRBG, Fortuna) — those belong in `@suaveplan/crypto-sodium`/`crypto-browser`. Gaussian Process regression, Monte Carlo methods — future `@suaveplan/stats`. Geo random geometry — deferred to `@suaveplan/geo`.

View File

@@ -0,0 +1,58 @@
# Capability: noise-functions
## ADDED Requirements
### Package identity
- REQ-NOISE-001: Package name `@suaveplan/noise`, `packages/universal/noise`.
- REQ-NOISE-002: Runtime deps: `@suaveplan/error`, `tslib`. Zero PRNG deps — noise uses its own internal seeded hash permutation tables.
- REQ-NOISE-003: `"sideEffects": false`.
### Output contract
- REQ-NOISE-010: All noise functions return values normalised to `[-1, 1]` except Worley which returns `[0, 1]` distance values.
- REQ-NOISE-011: All noise functions accept `seed: number` as first parameter. Same seed + same coordinates always returns same value.
### Perlin noise
- REQ-NOISE-020: Exports `perlin2(seed: number, x: number, y: number): number`.
- REQ-NOISE-021: Exports `perlin3(seed: number, x: number, y: number, z: number): number`.
### Simplex noise
- REQ-NOISE-030: Exports `simplex2(seed: number, x: number, y: number): number`.
- REQ-NOISE-031: Exports `simplex3(seed: number, x: number, y: number, z: number): number`.
- REQ-NOISE-032: Exports `simplex4(seed: number, x: number, y: number, z: number, w: number): number`.
### Value noise
- REQ-NOISE-040: Exports `value2(seed: number, x: number, y: number): number`.
- REQ-NOISE-041: Exports `value3(seed: number, x: number, y: number, z: number): number`.
### Worley / cell noise
- REQ-NOISE-050: Exports `WorleyMode = "F1" | "F2" | "F1-F2"`.
- REQ-NOISE-051: Exports `worley2(seed: number, x: number, y: number, mode?: WorleyMode): number`. Default mode `"F1"`.
- REQ-NOISE-052: Exports `worley3(seed: number, x: number, y: number, z: number, mode?: WorleyMode): number`.
### Fractional Brownian Motion
- REQ-NOISE-060: Exports `FbmOptions = { octaves?: number; lacunarity?: number; gain?: number }`. Defaults: `octaves=6, lacunarity=2.0, gain=0.5`.
- REQ-NOISE-061: Exports `fbm2(noiseFn: (seed, x, y) => number, seed: number, x: number, y: number, opts?: FbmOptions): number`.
- REQ-NOISE-062: Exports `fbm3(noiseFn: (seed, x, y, z) => number, seed: number, x: number, y: number, z: number, opts?: FbmOptions): number`.
### Blue noise / Poisson disk sampling
- REQ-NOISE-070: Exports `blueNoiseSample(width: number, height: number, minDistance: number, opts?: { seed?: number; rng?: Rng; maxSamples?: number }): [number, number][]`.
- REQ-NOISE-071: Uses Bridson's algorithm. Default `maxSamples = 100_000`; throws `NoiseError("NOISE.BLUE.SAMPLE_LIMIT_EXCEEDED")` if exceeded.
### Error surface
- REQ-NOISE-080: Exports `NoiseError extends SuaveplanError<NoiseCode>`.
- REQ-NOISE-081: Codes: `NOISE.BLUE.SAMPLE_LIMIT_EXCEEDED`, `NOISE.FBM.INVALID_OCTAVES`.
### Quality
- REQ-NOISE-090: 100% statement + branch coverage.
- REQ-NOISE-091: fast-check: determinism (same seed+coords → same output), output range `[-1,1]` (or `[0,1]` for Worley).
- REQ-NOISE-092: mitata benchmarks: each function at 1k and 100k evaluations.

View File

@@ -0,0 +1,69 @@
# Capability: random-utilities
## ADDED Requirements
### Package identity
- REQ-RAND-001: Package name `@suaveplan/random`, `packages/universal/random`.
- REQ-RAND-002: Runtime deps: `@suaveplan/types`, `@suaveplan/error`, `tslib`, `zod`. Zero crypto-specific deps.
- REQ-RAND-003: `"sideEffects": false`. Subpath exports per feature module.
### CSPRNG source
- REQ-RAND-010: Exports `cryptoRng(): Rng` — factory returning a WebCrypto-backed `Rng` object: `{ nextUint32, nextFloat }`. `crypto.getRandomValues` is the **only** callsite in this package, located exclusively in `src/source/crypto-rng.ts`.
- REQ-RAND-011: Exports `getDefaultRng(): Rng` and `setDefaultRng(rng: Rng): void`. Default is `cryptoRng()` at module init.
- REQ-RAND-012: `setDefaultRng` is documented as a test hook. Tests must restore the previous value in `afterEach`.
### Bytes and numbers
- REQ-RAND-020: Exports `randomBytes(n: number, rng?: Rng): Uint8Array` — fills via `nextUint32()` calls.
- REQ-RAND-021: Exports `randomInt(min: number, max: number, rng?: Rng): number` — uniform integer in `[min, max]` inclusive. Throws if `min > max`.
- REQ-RAND-022: Exports `randomFloat(rng?: Rng): number` — uniform float in `[0, 1)`.
### String generation
- REQ-RAND-030: Exports `randomString(length: number, opts?: { type?: StringType; characters?: string; rng?: Rng }): string`.
- REQ-RAND-031: `StringType = "hex" | "base64" | "base64url" | "url-safe" | "numeric" | "alphanumeric" | "distinguishable" | "ascii-printable"`.
- REQ-RAND-032: `distinguishable` charset: `CDEHKMPRTUWXY012458` (no visually ambiguous characters).
- REQ-RAND-033: `type` and `characters` are mutually exclusive; providing both throws `RandomError("RANDOM.STRING.INVALID_OPTIONS")`.
### ID generation
- REQ-RAND-040: Exports `randomUUID(rng?: Rng): string` — RFC 4122 v4 UUID.
- REQ-RAND-041: Exports `randomUUIDv7(rng?: Rng): string` — time-ordered UUID v7: 48-bit ms timestamp + 74 random bits. Monotonically increasing within the same millisecond via sequence counter.
- REQ-RAND-042: Exports `randomULID(rng?: Rng): string` — Crockford Base32 encoded ULID.
- REQ-RAND-043: Exports `randomNanoid(size?: number, rng?: Rng): string` — URL-safe, default 21 characters.
### Collection operations
- REQ-RAND-050: Exports `shuffle<T>(array: T[], rng?: Rng): T[]` — Fisher-Yates, immutable.
- REQ-RAND-051: Exports `sample<T>(array: T[], n: number, rng?: Rng): T[]` — n distinct elements.
- REQ-RAND-052: Exports `choice<T>(array: T[], rng?: Rng): T` — single random element.
- REQ-RAND-053: Exports `weightedChoice<T>(items: T[], weights: number[], rng?: Rng): T`.
- REQ-RAND-054: Exports `weightedSample<T>(items: T[], weights: number[], n: number, rng?: Rng): T[]` — weighted sampling without replacement.
- REQ-RAND-055: All collection functions throw `RandomError("RANDOM.COLLECTION.EMPTY")` on empty input.
### Object/Record operations
- REQ-RAND-060: Exports `randomKey<T extends object>(obj: T, rng?: Rng): keyof T`.
- REQ-RAND-061: Exports `randomValue<T extends object>(obj: T, rng?: Rng): T[keyof T]`.
- REQ-RAND-062: Exports `randomEntry<T extends object>(obj: T, rng?: Rng): [keyof T, T[keyof T]]`.
- REQ-RAND-063: Exports `randomSubset<T extends object>(obj: T, n: number, rng?: Rng): Partial<T>`.
- REQ-RAND-064: All object functions throw `RandomError("RANDOM.OBJECT.EMPTY")` on empty object.
### Game primitives
- REQ-RAND-070: Exports `coin(p?: number, rng?: Rng): boolean` — Bernoulli trial. Default `p = 0.5`. Throws if `p` outside `[0, 1]`.
- REQ-RAND-071: Exports `dice(sides: number, rng?: Rng): number` — uniform integer in `[1, sides]`.
- REQ-RAND-072: Exports `roll(count: number, sides: number, rng?: Rng): number` — sum of `count` dice.
### Error surface
- REQ-RAND-080: Exports `RandomError extends SuaveplanError<RandomCode>`.
- REQ-RAND-081: Codes: `RANDOM.STRING.INVALID_OPTIONS`, `RANDOM.COLLECTION.EMPTY`, `RANDOM.COLLECTION.INSUFFICIENT`, `RANDOM.OBJECT.EMPTY`, `RANDOM.GAME.INVALID_SIDES`, `RANDOM.GAME.INVALID_PROBABILITY`.
### Quality
- REQ-RAND-090: 100% statement + branch coverage.
- REQ-RAND-091: `tests/no-disallowed.test.ts`: no `Math.random`, `Date.now`, `crypto.getRandomValues` outside `src/source/crypto-rng.ts`.
- REQ-RAND-092: fast-check: all collection operations preserve input length constraints; UUID/ULID format invariants.

View File

@@ -0,0 +1,71 @@
# Capability: seeded-prng
## ADDED Requirements
### Package identity
- REQ-PRNG-001: Package name `@suaveplan/pseudo-random`, `packages/universal/pseudo-random`.
- REQ-PRNG-002: Runtime deps: `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`. Zero crypto deps, zero browser deps.
- REQ-PRNG-003: `"sideEffects": false`.
### SeededRng interface
- REQ-PRNG-010: Exports `SeededRng` interface extending `Rng` from `@suaveplan/types`: `{ readonly seed: number; readonly position: number; skip(n: number): this; reset(): this; at(offset: number): SeededRng; fork(id: number | string): SeededRng; checkpoint(): RngCheckpoint; restore(checkpoint: RngCheckpoint): this }`.
- REQ-PRNG-011: Exports `RngCheckpoint = { seed: number; position: number; state: unknown }` — serialisable to JSON.
### Seed normalisation
- REQ-PRNG-020: Exports `normalizeSeed(seed: string | number): number`.
- REQ-PRNG-021: String seeds: splitmix64 mixing over UTF-16 char codes. Produces stable 32-bit integer.
- REQ-PRNG-022: Numeric seeds: single splitmix64 round to avalanche input bias. Rejects NaN, Infinity, non-safe-integers with `PseudoRandomError("PRNG.SEED.INVALID")`.
- REQ-PRNG-023: Zero input produces a non-zero seed (avoids degenerate PRNG state).
### Mulberry32Rng
- REQ-PRNG-030: Exports `Mulberry32Rng` implementing `SeededRng`. 32-bit state.
- REQ-PRNG-031: `nextUint32()` uses the standard Mulberry32 algorithm: `a = (a + 0x6d2b79f5) >>> 0; ...`.
- REQ-PRNG-032: Byte-identical output to the inline implementation in `@suaveplan/testing/context`.
### Sfc32Rng
- REQ-PRNG-040: Exports `Sfc32Rng` implementing `SeededRng`. 128-bit state (4×uint32).
- REQ-PRNG-041: State initialised with 12 warmup rounds from `seedToSFC32State`.
- REQ-PRNG-042: Byte-identical output to `BulletproofPRNG` (SFC32 mode) in the bingo app for same seed.
### Xoshiro256Rng
- REQ-PRNG-050: Exports `Xoshiro256Rng` implementing `SeededRng`. 256-bit state (4×uint64 via BigInt or two uint32 pairs).
- REQ-PRNG-051: Uses xoshiro256++ algorithm.
- REQ-PRNG-052: `skip(2**128)` and `skip(2**192)` use the algebraic jump polynomials (O(log n)); other `skip(n)` uses linear burn.
### CounterRng
- REQ-PRNG-060: Exports `CounterRng` implementing `SeededRng`. Counter-based via splitmix64 hash.
- REQ-PRNG-061: `at(offset)` is O(1) — directly sets the counter.
- REQ-PRNG-062: `nextUint32()` = `splitmix64(seed XOR counter++)` truncated to 32 bits.
### Shared SeededRng behaviour
- REQ-PRNG-070: `fork(id)` derives child seed via `normalizeSeed(parentSeed XOR hash(id))`. Children are statistically independent.
- REQ-PRNG-071: `checkpoint()` returns a plain object that `JSON.stringify` round-trips without loss.
- REQ-PRNG-072: `restore(checkpoint)` resets the instance to the checkpointed state and returns `this`.
- REQ-PRNG-073: `at(offset)` returns a **new instance** at the given offset (does not mutate the original).
### Utilities
- REQ-PRNG-080: Exports `shuffle<T>(array: T[], rng?: Rng): T[]` — Fisher-Yates, returns new array.
- REQ-PRNG-081: Exports `shuffleInPlace<T>(array: T[], rng?: Rng): T[]` — Fisher-Yates, mutates.
- REQ-PRNG-082: Exports `sample<T>(array: T[], n: number, rng?: Rng): T[]` — n distinct elements without replacement.
- REQ-PRNG-083: Exports `weightedChoice<T>(items: T[], weights: number[], rng?: Rng): T`.
### Error surface
- REQ-PRNG-090: Exports `PseudoRandomError extends SuaveplanError<PseudoRandomCode>`.
- REQ-PRNG-091: Codes: `PRNG.SEED.INVALID`, `PRNG.OFFSET.OUT_OF_RANGE`, `PRNG.SAMPLE.INSUFFICIENT_ITEMS`, `PRNG.CHECKPOINT.INVALID`.
### Quality
- REQ-PRNG-100: 100% statement + branch coverage.
- REQ-PRNG-101: fast-check: determinism, fork independence (10k samples, correlation < 0.01), checkpoint round-trip.
- REQ-PRNG-102: mitata benchmarks: `nextUint32` throughput for each algorithm.
- REQ-PRNG-103: `Sfc32Rng` output verified against known vectors from bingo test suite.

View File

@@ -0,0 +1,39 @@
# Capability: statistical-distributions
## ADDED Requirements
### Location
- REQ-DIST-001: All distribution functions live in `@suaveplan/random` under `src/distributions/`.
### Continuous distributions
- REQ-DIST-010: Exports `normal(mu?: number, sigma?: number, rng?: Rng): () => number` — Gaussian via Box-Muller. Defaults `mu=0, sigma=1`.
- REQ-DIST-011: Exports `logNormal(mu?: number, sigma?: number, rng?: Rng): () => number` — `exp(normal(mu, sigma))`.
- REQ-DIST-012: Exports `exponential(lambda: number, rng?: Rng): () => number`. Throws if `lambda <= 0`.
- REQ-DIST-013: Exports `gamma(k: number, theta?: number, rng?: Rng): () => number` — Marsaglia-Tsang for k≥1, Ahrens-Dieter for k<1. Default `theta=1`.
- REQ-DIST-014: Exports `beta(alpha: number, betaParam: number, rng?: Rng): () => number` — ratio of two Gamma samples.
- REQ-DIST-015: Exports `pareto(alpha: number, rng?: Rng): () => number`. Throws if `alpha <= 0`.
- REQ-DIST-016: Exports `weibull(k: number, a?: number, b?: number, rng?: Rng): () => number`. Defaults `a=0, b=1`.
- REQ-DIST-017: Exports `cauchy(a?: number, b?: number, rng?: Rng): () => number`. Defaults `a=0, b=1`.
- REQ-DIST-018: Exports `logistic(a?: number, b?: number, rng?: Rng): () => number`. Defaults `a=0, b=1`.
### Discrete distributions
- REQ-DIST-020: Exports `bernoulli(p: number, rng?: Rng): () => 0 | 1`. Throws if `p` outside `[0, 1]`.
- REQ-DIST-021: Exports `geometric(p: number, rng?: Rng): () => number`. Throws if `p` outside `(0, 1]`.
- REQ-DIST-022: Exports `binomial(n: number, p: number, rng?: Rng): () => number`. Throws if `n < 0` or `p` outside `[0, 1]`.
- REQ-DIST-023: Exports `poisson(lambda: number, rng?: Rng): () => number` — Knuth for λ≤30, rejection method above. Throws if `lambda <= 0`.
### Unified dispatcher
- REQ-DIST-030: Exports `DistributionConfig` — discriminated union: `{ type: 'normal'; mu?: number; sigma?: number } | { type: 'exponential'; lambda: number } | ...` (one member per distribution).
- REQ-DIST-031: Exports `createSampler(config: DistributionConfig, rng?: Rng): () => number` — dispatches to the appropriate distribution factory.
- REQ-DIST-032: Exports `sample(config: DistributionConfig, rng?: Rng): number` — one-shot convenience: `createSampler(config, rng)()`.
- REQ-DIST-033: `DistributionConfig` is a plain JSON-serialisable type. `rng` is always passed externally, never embedded in config.
- REQ-DIST-034: `createSampler` throws `RandomError("RANDOM.DISTRIBUTION.UNKNOWN_TYPE")` for unrecognised `type` values (runtime guard for JS consumers without TypeScript).
### Quality
- REQ-DIST-040: fast-check: for each distribution, generate 10k samples and verify mean/variance are within 5% of theoretical values at standard parameters.
- REQ-DIST-041: `DistributionConfig` round-trips through `JSON.parse(JSON.stringify(config))` without loss for all distribution types.

View File

@@ -0,0 +1,80 @@
> Closed 2026-04-19 — all tasks landed.
# Tasks: enterprise-random-family
## Pre-work
- [x] 1. Add catalog entries to root `package.json` (if not already present from crypto-family): no new external deps needed for pseudo-random or random; noise needs none either
## Wave 1 — @suaveplan/pseudo-random (no workspace deps beyond error + types)
- [x] 2. Scaffold `packages/universal/pseudo-random`: `package.json` (deps: `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`), `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts` (100% thresholds)
- [x] 3. `errors/`: `PseudoRandomError`, `PRNG_CODES`
- [x] 4. `seed/normalize.ts` + test + md: `normalizeSeed(seed: string | number): number` — splitmix64 mixing, rejects NaN/Infinity/non-safe-int
- [x] 5. `types/seeded-rng.types.ts` + md: `SeededRng` interface, `RngCheckpoint`
- [x] 6. `algorithms/mulberry32.ts` + test + md: implements `SeededRng`; byte-identical to `@suaveplan/testing` inline impl
- [x] 7. `algorithms/sfc32.ts` + test + md: 128-bit state, 12 warmup rounds; byte-identical to bingo `BulletproofPRNG` (SFC32 mode)
- [x] 8. `algorithms/xoshiro256.ts` + test + md: 256-bit state, algebraic jump for 2^128 and 2^192
- [x] 9. `algorithms/counter-rng.ts` + test + md: splitmix64 hash counter, O(1) `at(offset)`
- [x] 10. Verify all `SeededRng` shared behaviours: `fork`, `checkpoint`/`restore`, `at` returns new instance, `skip` mutates self
- [x] 11. `utils/shuffle.ts` + test + md: `shuffle`, `shuffleInPlace`
- [x] 12. `utils/sample.ts` + test + md: `sample`, `weightedChoice`
- [x] 13. `constants/index.ts`; barrel `src/index.ts`; subpath exports in `package.json`
- [x] 14. fast-check property tests: determinism, fork independence (10k samples, correlation < 0.01), checkpoint round-trip
- [x] 15. mitata benchmarks: `nextUint32` throughput for each algorithm
- [x] 16. `tests/no-disallowed.test.ts` (no `Math.random`, `crypto.getRandomValues` in `src/`); `tests/tree-shake.test.ts`
- [x] 17. `README.md`
- [x] 18. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/pseudo-random --force` → green at 100%
- [x] 19. `bun scripts/publish.ts --package @suaveplan/pseudo-random`
## Wave 2 — @suaveplan/random (depends on types + error; pseudo-random optional peer)
- [x] 20. Scaffold `packages/universal/random`: `package.json` (deps: `@suaveplan/types`, `@suaveplan/error`, `tslib`, `zod`), configs, 100% thresholds
- [x] 21. `errors/`: `RandomError`, `RANDOM_CODES`
- [x] 22. `source/crypto-rng.ts` + test + md: `cryptoRng()` factory — single `crypto.getRandomValues` callsite
- [x] 23. `source/default-rng.ts` + test + md: `getDefaultRng`, `setDefaultRng`
- [x] 24. `bytes/random-bytes.ts` + test + md
- [x] 25. `numbers/random-int.ts` + test + md; `numbers/random-float.ts` + test + md
- [x] 26. `strings/random-string.ts` + test + md: all 8 presets + custom charset, mutual-exclusion validation
- [x] 27. `ids/uuid.ts` + test + md: v4 + v7 (monotonic sequence counter within same ms)
- [x] 28. `ids/uuid.ts` (cont.): `randomULID`, `randomNanoid`
- [x] 29. `collections/shuffle.ts` + `sample.ts` + `choice.ts` + `weighted.ts` + tests + mds
- [x] 30. `objects/random-key.ts` + `random-value.ts` + `random-entry.ts` + `random-subset.ts` + tests + mds
- [x] 31. `game/dice.ts` + test + md: `coin`, `dice`, `roll`
- [x] 32. `distributions/continuous/normal.ts` + test + md (Box-Muller)
- [x] 33. `distributions/continuous/log-normal.ts`, `exponential.ts`, `gamma.ts` (Marsaglia-Tsang + Ahrens-Dieter), `beta.ts` + tests + mds
- [x] 34. `distributions/continuous/pareto.ts`, `weibull.ts`, `cauchy.ts`, `logistic.ts` + tests + mds
- [x] 35. `distributions/discrete/bernoulli.ts`, `geometric.ts`, `binomial.ts`, `poisson.ts` (Knuth + rejection) + tests + mds
- [x] 36. `distributions/sampler.ts` + test + md: `DistributionConfig`, `createSampler`, `sample`
- [x] 37. fast-check: mean/variance within 5% of theoretical values for each distribution at 10k samples
- [x] 38. `DistributionConfig` JSON round-trip test for all distribution types
- [x] 39. `constants/index.ts`; barrel `src/index.ts`; subpath exports
- [x] 40. `tests/no-disallowed.test.ts` (no `Math.random`, `crypto.getRandomValues` outside `src/source/crypto-rng.ts`); `tests/tree-shake.test.ts`
- [x] 41. mitata benchmarks: `randomInt`, `shuffle(1000)`, `normal` sampler throughput
- [x] 42. `README.md`
- [x] 43. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/random --force` → green at 100%
- [x] 44. `bun scripts/publish.ts --package @suaveplan/random`
## Wave 3 — @suaveplan/noise (independent, only needs error)
- [x] 45. Scaffold `packages/universal/noise`: `package.json` (deps: `@suaveplan/error`, `tslib`), configs, 100% thresholds
- [x] 46. `errors/`: `NoiseError`, `NOISE_CODES`
- [x] 47. `perlin/perlin.ts` + test + md: `perlin2`, `perlin3`
- [x] 48. `simplex/simplex.ts` + test + md: `simplex2`, `simplex3`, `simplex4`
- [x] 49. `value/value.ts` + test + md: `value2`, `value3`
- [x] 50. `worley/worley.ts` + test + md: `worley2`, `worley3`, `WorleyMode`
- [x] 51. `fbm/fbm.ts` + test + md: `fbm2`, `fbm3`, `FbmOptions`
- [x] 52. `blue/blue.ts` + test + md: `blueNoiseSample`, Bridson's algorithm, `maxSamples` guard
- [x] 53. `constants/index.ts`; barrel `src/index.ts`; subpath exports
- [x] 54. fast-check: determinism + output range `[-1,1]` (or `[0,1]` for Worley) for all functions
- [x] 55. mitata benchmarks: each function at 1k and 100k evaluations
- [x] 56. `tests/tree-shake.test.ts`
- [x] 57. `README.md`
- [x] 58. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/noise --force` → green at 100%
- [x] 59. `bun scripts/publish.ts --package @suaveplan/noise`
## Close-out
- [x] 60. Update `CLAUDE.md` package listing and counts for all 3 new packages
- [x] 61. Update `PACKAGES.yml` / `STRUCTURE.yml`
- [x] 62. Archive `openspec/changes/enterprise-random-family/` → `openspec/archive/2026-04-18-enterprise-random-family/`