Files
2026-05-27 23:29:53 +01:00

12 KiB

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:

    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:

    // 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/<pkg> (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<Uint8Array> | AsyncIterable<Uint8Array> (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

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

# 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).