Initial Commit
This commit is contained in:
180
openspec/changes/complete-rng-unification/design.md
Normal file
180
openspec/changes/complete-rng-unification/design.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# 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/<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
|
||||
|
||||
```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).
|
||||
Reference in New Issue
Block a user