Initial Commit
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user