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

16 KiB

OpenSpec Change: complete-rng-unification

  • Change ID: complete-rng-unification
  • Package: @suaveplan/types (canonical) + every consumer that locally redefines an Rng-shaped type
  • Design: ./design.md
  • Spec deltas:
    • ./specs/types/spec.md
    • ./specs/testing/spec.md
    • ./specs/crypto-core/spec.md
    • ./specs/crypto/spec.md
    • ./specs/mime/spec.md
  • Status: PROPOSED
  • Date: 2026-05-17
  • Supersedes: _superseded-narrow-deferrals-2026-05-17/unify-rng-across-packages — the prior draft covered only three packages and did not exhaustively audit RngLike / RandomSource / ByteSource collisions across the monorepo.

Why

The 2026-04-27 types-contract-tightening change widened the canonical Rng interface in @suaveplan/types (src/rng.ts) with two OPTIONAL members (nextBytes, nextIntInRange) and deferred migrating the packages that still declared Rng-shaped types locally. A first attempt (unify-rng-across-packages, superseded on 2026-05-17) only covered three packages and silently glossed over crypto-core's genuinely incompatible type Rng = ByteSource aliasing. The naming collision (crypto-core's Rng is a function (n: number) => Uint8Array; canonical Rng is an object with nextUint32/nextFloat) means any consumer that imported Rng from @suaveplan/crypto-core was getting a fundamentally different contract than a consumer importing Rng from @suaveplan/types.

This change closes the loop exhaustively: every genesis package surveyed in the repo-wide audit below is migrated to consume the canonical Rng (or to drop a misleading Rng alias when the local shape is structurally a distinct concept like a callable byte-source). Type-only; no runtime behaviour change.

Audit (exhaustive — every match from the mandated grep)

Repo-wide grep:

grep -rln "interface Rng\b\|type Rng\s*=\|class Rng\b\|RngLike\b\|RandomSource\b\|ByteSource\b" \
  packages/ --include="*.ts" --exclude-dir=dist --exclude-dir=node_modules

Categorised — see design.md §"Per-package categorisation" for the full table, including the reasoning for every "no action / structurally compatible / structurally incompatible" verdict:

# Package File Local symbol Verdict Action
1 @suaveplan/types src/rng.ts interface Rng CANONICAL None — source of truth.
2 @suaveplan/testing src/context/context.ts interface Rng (subset of canonical) STRUCTURALLY COMPATIBLE Replace local interface with re-export of canonical; existing cryptoRng / seededRng keep working.
3 @suaveplan/crypto-core src/primitives/random-bytes/random-bytes.ts type Rng = ByteSource (function, not object) STRUCTURALLY INCOMPATIBLE Remove the misleading Rng alias and defaultRng (which is just defaultByteSource under a wrong name); keep ByteSource and defaultByteSource under their real names; add a back-compat re-export of the canonical Rng from @suaveplan/types.
4 @suaveplan/crypto-core src/index.ts, src/primitives/index.ts, src/primitives/random-bytes/index.ts re-exports Rng / defaultRng DERIVED from #3 Update barrels — Rng now forwards canonical; defaultRng is removed (a defaultRng() factory belongs in a future @suaveplan/random-adjacent helper, not in crypto-core's primitives surface).
5 @suaveplan/crypto src/index.ts re-exports Rng / defaultRng from crypto-core DERIVED from #4 Update barrel to forward the canonical Rng; drop the defaultRng re-export.
6 @suaveplan/crypto-utils src/random/random-bytes/random-bytes.ts type ByteSource = (n: number) => Uint8Array DISTINCT CONCEPT No Rng alias declared (clean); keep ByteSource as the package's own callable byte-producer contract. Document in spec that ByteSource is intentionally NOT the canonical Rng.
7 @suaveplan/mime src/detection/from-stream.ts type ByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> DISTINCT CONCEPT, NAME COLLISION The local type ByteSource shadows @suaveplan/crypto-utils's ByteSource semantically (stream vs callable) and is file-scope only. Rename to StreamByteSource to remove the collision; not exported at package root.
8 @suaveplan/crypto-rsa src/encrypt/default-rng.ts function defaultRng(): Rng (imports canonical) ALREADY COMPLIANT No change — already uses import type { Rng } from "@suaveplan/types".
9 @suaveplan/events-schema src/core/attach-event-schemas/attach-event-schemas.types.ts interface SchemaRng DISTINCT CONCEPT (schema registry namespace, not RNG) No change — SchemaRng is a schema registry abbreviation, not a random-number generator. Documented in design.md to prevent future agent confusion.
10 @suaveplan/pseudo-random src/types/seeded-rng.types.ts interface SeededRng extends Rng STRUCTURAL EXTENSION (subtype of canonical) No change — already extends canonical correctly per the rule subtype that extends Rng.

Consumers of the local Rng re-exports that need verification post-migration:

  • @suaveplan/crypto-cli — imports ByteSource from @suaveplan/crypto (not Rng); unaffected.
  • @suaveplan/vault-sync — imports ByteSource from @suaveplan/crypto-utils; unaffected.
  • Every other @suaveplan/crypto-core consumer using ByteSource (sealed-box, sign, agree, ed25519, ecdsa-p256, x25519, aead/sign/agree-engine.types, keychain, keypair/generate, vault, mnemonic, identity): unaffected — ByteSource is preserved verbatim.

What changes

For each affected genesis package, the source files that currently declare a local Rng-shaped type are migrated to:

  1. Compatible subset (testing) — replace the local interface Rng { ... } with import type { Rng } from "@suaveplan/types"; export type { Rng }; plus a prose comment marking the local re-export as a backwards-compatibility alias.
  2. Incompatible alias (crypto-core) — remove the misleading type Rng = ByteSource (and defaultRng = defaultByteSource) entirely; the Rng name now re-exports canonical from @suaveplan/types; the callable-byte-producer concept keeps the ByteSource name (which it already had).
  3. Distinct concept under colliding name (mime) — rename the file-scope ByteSource to StreamByteSource (it has zero external consumers — never exported from from-stream.ts).
  4. Already compliant or structurally extending (crypto-rsa, pseudo-random) — no edits; documented in design as the reference pattern.
  5. Distinct namespace (events-schema/SchemaRng) — no edits; documented in design.

Add @suaveplan/types as a workspace:^ dependency where missing (specifically @suaveplan/testing, which currently has zero @suaveplan/* runtime deps but is permitted to depend on @suaveplan/types for types-only consumption per CLAUDE.md rule 8 — the byte-size guard verifies @suaveplan/types emits zero runtime).

Per-affected-package spec deltas document the canonical contract and the back-compat forwarder requirement; the types capability spec delta is the normative single-source-of-truth clause.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/types to enumerate the (large) transitive dependent set. The packages directly affected by THIS change (i.e. ones whose source files this change edits or whose tests must run green pre- and post-migration) are:

  • @suaveplan/types — [x] tests run / [x] docs reviewed / [ ] no action needed (canonical spec clause added)
  • @suaveplan/testing — [x] tests run / [x] docs reviewed / [ ] no action needed (interface → re-export)
  • @suaveplan/crypto-core — [x] tests run / [x] docs reviewed / [ ] no action needed (drop alias + drop defaultRng; preserve ByteSource)
  • @suaveplan/crypto — [x] tests run / [x] docs reviewed / [ ] no action needed (barrel forward update; drop defaultRng re-export)
  • @suaveplan/mime — [x] tests run / [x] docs reviewed / [ ] no action needed (rename file-scope ByteSource → StreamByteSource)
  • @suaveplan/crypto-rsa — [ ] tests run / [ ] docs reviewed / [x] no action needed (already canonical-compliant)
  • @suaveplan/pseudo-random — [ ] tests run / [ ] docs reviewed / [x] no action needed (SeededRng extends Rng already)
  • @suaveplan/events-schema — [ ] tests run / [ ] docs reviewed / [x] no action needed (SchemaRng is a schema-registry concept, not an RNG)
  • @suaveplan/crypto-utils — [ ] tests run / [ ] docs reviewed / [x] no action needed (declares only ByteSource, no Rng collision)

Foundation-dependent sweep for types (per §13 step 10 + §14a): bunx turbo test --filter=...@suaveplan/types MUST be green before archival. Foundation-dependent sweep for testing (bunx turbo test --filter=...@suaveplan/testing) MUST also be green — every test in the monorepo flows through @suaveplan/testing/runner per CLAUDE.md rule 11, so any structural break in Rng here cascades.

Source mapping (local → canonical)

Package Local file Local symbol Local shape Action
@suaveplan/testing src/context/context.ts:45 interface Rng { nextUint32; nextFloat } { nextUint32(): number; nextFloat(): number } Replace with export type { Rng } from "@suaveplan/types";. cryptoRng/seededRng still typecheck (canonical adds only OPTIONAL members).
@suaveplan/crypto-core src/primitives/random-bytes/random-bytes.ts:12 type Rng = ByteSource (n: number) => Uint8Array (function) Remove the alias. Remove defaultRng = defaultByteSource (line 36). Add export type { Rng } from "@suaveplan/types"; with prose comment marking it as a back-compat re-export. Keep ByteSource, defaultByteSource, randomBytes, RandomBytesOptions. Update RandomBytesOptions to drop the rng alias field (currently readonly rng?: ByteSource; — this is a legacy alias of byteSource).
@suaveplan/crypto-core src/primitives/random-bytes/index.ts barrel re-exports Rng, defaultRng Update — Rng now forwards canonical; drop defaultRng.
@suaveplan/crypto-core src/primitives/index.ts barrel re-exports Rng, defaultRng Update — same as above.
@suaveplan/crypto-core src/index.ts barrel re-exports Rng, defaultRng Update — same as above.
@suaveplan/crypto src/index.ts barrel re-exports Rng, defaultRng from crypto-core Update — forward canonical Rng; drop defaultRng.
@suaveplan/mime src/detection/from-stream.ts:19 type ByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> union of stream types (file-scope) Rename to StreamByteSource to eliminate the cross-package name collision; update the single internal reference at line 30.

Verification

  • Verified by: bunx turbo typecheck test --filter=@suaveplan/types
  • Verified by: bunx turbo typecheck test --filter=@suaveplan/testing
  • Verified by: bunx turbo typecheck test --filter=@suaveplan/crypto-core
  • Verified by: bunx turbo typecheck test --filter=@suaveplan/crypto
  • Verified by: bunx turbo typecheck test --filter=@suaveplan/mime
  • Verified by: bunx turbo test --filter=...@suaveplan/types (foundation dependent sweep)
  • Verified by: bunx turbo test --filter=...@suaveplan/testing (every test consumer in the monorepo)
  • Verified by: bunx turbo test --filter=...@suaveplan/crypto-core (every crypto-core consumer: crypto, crypto-rsa, crypto-vault, crypto-keys, vault, vault-sync, webauthn, etc.)
  • Verified by: repo-wide grep 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 returns exactly:
    • packages/core/types/src/rng.ts (canonical declaration)
    • packages/random/pseudo-random/src/types/seeded-rng.types.ts (legitimate SeededRng extends Rng subtype)
    • back-compat re-export forwarder lines (export type { Rng } from "@suaveplan/types";) in crypto-core/random-bytes, crypto-core/index, crypto/index, testing/context — never an independent interface body

Out of scope

  • Removing the back-compat re-exports. A follow-up change remove-deprecated-rng-aliases (opened at least one release cycle after this change archives) will delete the Rng re-exports from @suaveplan/crypto-core/random-bytes, @suaveplan/crypto-core/index, @suaveplan/crypto/index, and @suaveplan/testing/context, plus the defaultRng re-export already dropped here. This change ships with one cycle of grace.
  • Widening the canonical Rng. If a consumer needs additional members (e.g. nextBigInt, streaming bytes), open a separate types-contract-tightening-style change against @suaveplan/types.
  • Renaming ByteSource. crypto-utils and crypto-core both keep ByteSource under that name; the type-class distinction (callable byte producer vs uniform numeric RNG) is the right separation. mime's file-scope ByteSource is renamed only because it shadows the cross-package name without semantic overlap.
  • Replacing crypto-core's RandomBytesOptions.rng field with a deprecation cycle. The rng field on RandomBytesOptions is a legacy alias for byteSource (currently options.byteSource ?? options.rng ?? defaultByteSource). It has been a misleading shadow of canonical Rng since it accepts a ByteSource callable. This change removes it inline (no deprecation window) because keeping it would require shipping a field that takes a ByteSource under the name rng, contradicting the whole purpose of the unification. Consumers using { rng: ... } instead of { byteSource: ... } are flagged in §Migration.
  • Runtime behaviour changes. This change is type-only; the test suite must remain green pre- and post-migration with zero assertion edits.

Migration

  • Old: consumers importing Rng from @suaveplan/crypto-core, @suaveplan/crypto, or @suaveplan/testing — particularly any consumer that was relying on crypto-core's Rng being callable as rng(n: number) => Uint8Array.

  • New: import Rng from @suaveplan/types. Callable byte producers use ByteSource from @suaveplan/crypto-core or @suaveplan/crypto-utils.

  • Search command:

    grep -rln "from ['\"]@suaveplan/\(crypto-core\|crypto\|testing\)['\"]" packages/ \
      | xargs grep -l "Rng\b" 2>/dev/null
    grep -rn "RandomBytesOptions\|\.rng:\|, rng:" packages/ --include="*.ts" \
      --exclude-dir=dist --exclude-dir=node_modules \
      | grep -i "byteSource\|crypto-core\|crypto-utils"
    
  • Recommended replacement:

    // For an object Rng (nextUint32 / nextFloat / optional nextBytes / nextIntInRange):
    import type { Rng } from "@suaveplan/types";
    
    // For a callable byte producer (n: number) => Uint8Array:
    import type { ByteSource } from "@suaveplan/crypto-core"; // or @suaveplan/crypto-utils
    
  • RandomBytesOptions.rng removal: any callsite passing randomBytes(n, { rng: ... }) to @suaveplan/crypto-core or @suaveplan/crypto-utils MUST switch to { byteSource: ... }. The repo-wide audit (see design.md) confirms zero current callsites use this field — the rename is the migration.

  • defaultRng removal: @suaveplan/crypto-core and @suaveplan/crypto no longer re-export defaultRng. Use defaultByteSource (same value, correct name) instead. Audit confirms zero internal callsites use the defaultRng re-export today (it was a deprecation-shaped alias).