# Design: complete-rng-unification ## Goal A single canonical `Rng` interface across every `@suaveplan/*` package, with zero runtime behaviour change and a one-release-cycle deprecation window for the `Rng` aliases that are being phased out. The audit is **exhaustive** — every match from the mandated repo-wide grep is categorised and either migrated, preserved with justification (subtype-extension or distinct namespace), or renamed to remove a misleading collision. ## Strategy 1. **Structural conformance over re-declaration.** The canonical `Rng` in `@suaveplan/types/src/rng.ts` is the contract: ```ts export interface Rng { nextUint32(): number; nextFloat(): number; nextBytes?(n: number): Uint8Array; nextIntInRange?(min: number, maxExclusive: number): number; } ``` Local types that exactly match (or are a subset of) this shape become deprecated re-exports of the canonical. Local types that diverge structurally (object vs callable) are dropped under the misleading name and kept under their real name (`ByteSource`). 2. **Deprecated forwarders, not deletions.** Per CLAUDE.md §2 the JSDoc `@deprecated` tag is banned. The aliases are kept as named re-exports with prose comments — and the `MIGRATION.md` root-level doc is updated with the search command and recommended replacement, per §13a. Comment example: ```ts // Back-compat re-export of @suaveplan/types' canonical Rng. Prefer // importing Rng from @suaveplan/types directly; this forwarder will // be removed in the follow-up `remove-deprecated-rng-aliases` change. export type { Rng } from "@suaveplan/types"; ``` 3. **Type-only migration.** No runtime behaviour changes. Every package's full test suite must remain green with zero assertion edits before and after the swap. Verified per package with `bunx turbo typecheck test --filter=@suaveplan/` (CLAUDE.md rule 1: never `tsc` directly). 4. **Foundation dependent-sweeps are non-negotiable.** Per CLAUDE.md §13 step 10 and §14a, `types` and `testing` are foundation packages; `bunx turbo test --filter=...@suaveplan/types` and `...@suaveplan/testing` MUST both run green before archival. ## Per-package categorisation | # | Package | Local symbol | Local shape | Canonical shape | Verdict | Action | |---|---|---|---|---|---|---| | 1 | `@suaveplan/types` | `Rng` | object `{nextUint32; nextFloat; nextBytes?; nextIntInRange?}` | — (canonical) | — | None — source of truth. | | 2 | `@suaveplan/testing` | `interface Rng { nextUint32; nextFloat }` | object, exact subset of canonical | object | **Compatible** (canonical adds two OPTIONAL members) | Replace with re-export of canonical. `cryptoRng` / `seededRng` continue to typecheck against canonical — they implement the required members and omit the optional ones. | | 3 | `@suaveplan/crypto-core` | `type Rng = ByteSource` where `ByteSource = (n: number) => Uint8Array` | **callable** `(n) => Uint8Array` | **object** with `nextUint32`/`nextFloat` | **Incompatible** — structurally distinct contracts | Remove the misleading `Rng` alias. Keep `ByteSource` under its real name. Add a back-compat re-export of canonical `Rng` so any external code that imported `crypto-core`'s `Rng` keeps compiling, but now sees the canonical (object) shape. **Any consumer that was using `crypto-core`'s `Rng` as a callable will fail typecheck — this is intentional and flagged in `proposal.md` §Migration.** | | 4 | `@suaveplan/crypto-core` | `defaultRng = defaultByteSource` (a `ByteSource`) | callable | object | **Incompatible** | Remove. It was a deprecation-shaped alias of `defaultByteSource` — consumers should use `defaultByteSource` directly. Repo-wide grep confirms zero internal callsites use the `defaultRng` re-export. | | 5 | `@suaveplan/crypto-core` | `RandomBytesOptions { byteSource?: ByteSource; rng?: ByteSource }` | both are callable; `rng` is a legacy alias of `byteSource` | — (not an `Rng` declaration; misleading field name) | **Field rename / drop** | Remove the `rng` field. Internal selector becomes `options.byteSource ?? defaultByteSource`. Repo-wide grep confirms zero callsites pass `{ rng: ... }` to `randomBytes`. | | 6 | `@suaveplan/crypto` | re-exports `Rng`, `defaultRng` from `crypto-core` | inherits from #3, #4 | object | inherits | Update `src/index.ts` to forward the canonical `Rng` via `crypto-core`'s new back-compat re-export; drop the `defaultRng` re-export. | | 7 | `@suaveplan/crypto-utils` | `type ByteSource = (n: number) => Uint8Array` (no `Rng` alias) | callable | — (no `Rng` declared) | **Clean** | No change. `ByteSource` stays under its real name as the package's callable byte producer. Spec delta on `crypto-core` documents the type-class distinction so future agents do not re-introduce the `type Rng = ByteSource` alias. | | 8 | `@suaveplan/mime` | `type ByteSource = ReadableStream \| AsyncIterable` (file-scope, not exported) | union of stream types | — (different concept) | **Name collision, distinct concept** | Rename to `StreamByteSource` to remove the cross-package collision with `@suaveplan/crypto-{utils,core}`'s `ByteSource`. File-scope only — zero external consumers. | | 9 | `@suaveplan/crypto-rsa` | `function defaultRng(): Rng` | object (imports canonical `Rng` from `@suaveplan/types`) | object | **Already compliant** — reference pattern | No change. This is the shape every future `defaultRng()` factory should follow when it sits outside `@suaveplan/types`. | | 10 | `@suaveplan/pseudo-random` | `interface SeededRng extends Rng` (from `@suaveplan/types`) | object, **extends** canonical | object | **Subtype extension** — explicitly permitted by the spec clause | No change. Reference pattern for any future `*Rng` interface that needs more members than canonical. | | 11 | `@suaveplan/events-schema` | `interface SchemaRng` | schema-registry abbreviation (Rng = "Registry") | — (not an RNG) | **Distinct namespace; misleading name** | No change in this OpenSpec change scope. Documented here so future audits don't re-flag it; a future rename (`SchemaRegistryBinding` or similar) is out of scope. | ### Files this change edits ```text packages/core/types/src/rng.ts # comment-only: tighten "canonical" wording packages/core/types/src/rng.md # add "Single source of truth" section packages/testing/testing/src/context/context.ts # swap interface for re-export packages/testing/testing/src/context/context.md # update API reference packages/testing/testing/package.json # add @suaveplan/types: workspace:^ packages/crypto/crypto-core/src/primitives/random-bytes/random-bytes.ts # drop Rng alias, drop defaultRng, drop rng field, add canonical re-export packages/crypto/crypto-core/src/primitives/random-bytes/random-bytes.md # update API reference packages/crypto/crypto-core/src/primitives/random-bytes/index.ts # drop defaultRng from barrel; Rng forwards canonical packages/crypto/crypto-core/src/primitives/index.ts # same packages/crypto/crypto-core/src/index.ts # same packages/crypto/crypto/src/index.ts # drop defaultRng; Rng forwards canonical packages/data/mime/src/detection/from-stream.ts # rename ByteSource → StreamByteSource packages/data/mime/src/detection/from-stream.md # update API reference MIGRATION.md # root-level migration note per §13a ``` ## Why `crypto-core`'s old `type Rng = ByteSource` was wrong The canonical `Rng` is an **object** with `nextUint32` / `nextFloat` (uniform numeric source). `ByteSource` is a **function** `(n) => Uint8Array` (callable byte producer — key material, nonces, AEAD entropy, salts). These are fundamentally different shapes and use cases. Aliasing one as the other meant: 1. A consumer importing `Rng` from `crypto-core` got a callable; importing the same name from `@suaveplan/types` got an object. The two were silently incompatible at the typesystem level. 2. Cross-package code that needed both (e.g. `crypto-rsa/default-rng.ts`) ended up disambiguating via import-path discipline, not via the type itself — fragile. The fix splits the names by their real contracts: `Rng` is always the object; `ByteSource` is always the callable. ## Back-compat strategy For one release cycle (until the follow-up removal change archives): - `@suaveplan/crypto-core/random-bytes` exports a back-compat `Rng` re-export pointing at `@suaveplan/types`. The local `type Rng = ByteSource` alias is **removed** (keeping both shapes under the same name would break typecheck). - `@suaveplan/crypto-core/index`, `@suaveplan/crypto-core/primitives/index`, `@suaveplan/crypto-core/primitives/random-bytes/index` forward the same re-export. - `@suaveplan/crypto/index` forwards the same re-export. - `@suaveplan/testing/context` re-exports canonical `Rng`. The local `interface Rng` declaration is removed. - `defaultRng` is **not** preserved as a back-compat re-export anywhere (it was incorrectly typed — `defaultRng: ByteSource` cannot satisfy canonical `Rng`). Internal audit confirms zero callsites; callers used `defaultByteSource` directly. The deprecation prose comment names the canonical import path and the removal change ID (`remove-deprecated-rng-aliases`). ## Removal follow-up (out of scope here) A separate OpenSpec change titled `remove-deprecated-rng-aliases` (opened at least one release cycle after this change archives) will: 1. Delete the `Rng` re-export from `@suaveplan/crypto-core/random-bytes`. 2. Delete the `Rng` re-export from `@suaveplan/crypto-core/index`, `primitives/index`, `random-bytes/index`. 3. Delete the `Rng` re-export from `@suaveplan/crypto/index`. 4. Delete the `Rng` re-export from `@suaveplan/testing/context`. 5. Run dependent sweeps and update any caught consumers. This is tracked as a follow-up — not a phase-2 split (per CLAUDE.md §2b this change is complete on its own surface). ## Error codes No new error codes. The migration is structural-types-only; no new throw sites, no new error paths. ## Verification commands ```bash # Per affected package — full pre-publish gate bunx turbo lint typecheck test --filter=@suaveplan/types bunx turbo lint typecheck test --filter=@suaveplan/testing bunx turbo lint typecheck test --filter=@suaveplan/crypto-core bunx turbo lint typecheck test --filter=@suaveplan/crypto bunx turbo lint typecheck test --filter=@suaveplan/mime # Foundation dependent sweeps (mandatory for types and testing) bunx turbo test --filter=...@suaveplan/types bunx turbo test --filter=...@suaveplan/testing bunx turbo test --filter=...@suaveplan/crypto-core # Repo-wide grep — only the canonical declaration, the legitimate # SeededRng extension, and the documented back-compat re-export # forwarders should remain grep -rln "interface Rng\b\|type Rng\s*=\|class Rng\b\|RngLike\b\|RandomSource\b" \ packages/ --include="*.ts" --exclude-dir=dist --exclude-dir=node_modules ``` ## Pre-flight cross-check Mandatory reads completed: 1. `openspec/AGENTS.md` — spec-delta format, dependent-sweep requirements, archival flow. 2. `openspec/project.md` — Bun + Turborepo conventions; rule 8 (testing-infra → only `@suaveplan/types` permitted). 3. `openspec/changes/_superseded-narrow-deferrals-2026-05-17/unify-rng-across-packages/` — prior 3-package draft; this draft extends to every package. 4. `packages/core/types/src/rng.ts` — canonical shape verified. 5. Exhaustive grep across `packages/` for `interface Rng\b|type Rng\s*=|class Rng\b|RngLike\b|RandomSource\b|ByteSource\b` — all 30+ source-file matches inspected and categorised in the table above. Zero matches for `RngLike` or `RandomSource` in genesis (those identifiers exist only in upstream `../suaveplan/` and are not migrated here).