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

221 lines
16 KiB
Markdown

# 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:
```bash
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**:
```bash
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**:
```ts
// 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).