Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

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

View File

@@ -0,0 +1,220 @@
# 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).

View File

@@ -0,0 +1,47 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: crypto-core (crypto-core)
**Package**: `@suaveplan/crypto-core`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## MODIFIED Requirements
### Requirement: ByteSource and Rng are distinct contracts
`@suaveplan/crypto-core` SHALL declare `ByteSource` as the package's callable byte producer (`type ByteSource = (n: number) => Uint8Array`), exported from `src/primitives/random-bytes/random-bytes.ts`. The package SHALL NOT alias `Rng` to `ByteSource` or to any callable shape — the canonical `Rng` (from `@suaveplan/types`) is an object with `nextUint32` / `nextFloat`, fundamentally different from a callable byte producer. The previous `type Rng = ByteSource` alias and the `defaultRng: ByteSource = defaultByteSource` constant SHALL both be removed.
`@suaveplan/crypto-core` MAY re-export the canonical `Rng` from `@suaveplan/types` under the name `Rng` as a back-compat forwarder for one release cycle, with a prose comment naming the canonical import path and the removal change ID (`remove-deprecated-rng-aliases`). The forwarder MUST live at `src/primitives/random-bytes/random-bytes.ts` and be re-surfaced through the three barrels (`src/primitives/random-bytes/index.ts`, `src/primitives/index.ts`, `src/index.ts`). The `defaultRng` re-export SHALL NOT survive in any barrel — consumers use `defaultByteSource` directly.
The `RandomBytesOptions` shape SHALL drop the legacy `rng?: ByteSource` field; only `byteSource?: ByteSource` remains. The internal selector simplifies from `options.byteSource ?? options.rng ?? defaultByteSource` to `options.byteSource ?? defaultByteSource`. Repo-internal grep confirms zero callsites pass `{ rng: ... }` to `randomBytes` today.
`@suaveplan/crypto-core` SHALL preserve every other `ByteSource`-typed surface verbatim — engine constructors (`AeadEngine`, `SignEngine`, `AgreeEngine`, `SealedBoxEngine`, `AuthBoxEngine`), the `byteSource` injection on `generateKeypair` (ed25519, ecdsaP256, x25519), and the `defaultByteSource` constant. None of these reference `Rng`; they reference `ByteSource` by its real name.
**Verified by:** `packages/crypto/crypto-core/src/primitives/random-bytes/random-bytes.ts` (no `type Rng = ByteSource`; no `defaultRng`; presence of `export type { Rng } from "@suaveplan/types"` back-compat re-export) AND `packages/crypto/crypto-core/src/primitives/random-bytes/random-bytes.test.ts::"randomBytes accepts byteSource option"` AND `packages/crypto/crypto-core/src/primitives/random-bytes/random-bytes.test.ts::"randomBytes rejects negative or non-integer n"` AND `bunx turbo typecheck test --filter=@suaveplan/crypto-core` (green with zero assertion edits) AND `bunx turbo test --filter=...@suaveplan/crypto-core` (every consumer green) AND repo-internal grep `grep -rn "defaultRng\b\|\.rng:\|, rng:" packages/crypto/crypto-core/src/ --include="*.ts"` returns zero results outside the documented back-compat re-export comment.
#### Scenario: Rng re-export forwards canonical
- **WHEN** a consumer writes `import type { Rng } from "@suaveplan/crypto-core"`
- **THEN** the imported type is structurally identical to `import type { Rng } from "@suaveplan/types"` (an object, NOT a callable)
#### Scenario: ByteSource remains the callable contract
- **WHEN** a consumer writes `import type { ByteSource } from "@suaveplan/crypto-core"`
- **THEN** the imported type is `(n: number) => Uint8Array`
#### Scenario: defaultRng is removed
- **WHEN** a consumer writes `import { defaultRng } from "@suaveplan/crypto-core"`
- **THEN** `tsc` rejects with "has no exported member `defaultRng`"
#### Scenario: RandomBytesOptions.rng field is removed
- **WHEN** a consumer calls `randomBytes(32, { rng: customSource })`
- **THEN** `tsc` rejects with "Object literal may only specify known properties, and `rng` does not exist in type `RandomBytesOptions`"
#### Scenario: engines preserve ByteSource injection
- **WHEN** a consumer calls `ed25519.generateKeypair({ byteSource: customSource })`
- **THEN** the keypair is generated using `customSource(32)` (behavior unchanged)

View File

@@ -0,0 +1,31 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: crypto (crypto)
**Package**: `@suaveplan/crypto`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## MODIFIED Requirements
### Requirement: Top-level barrel forwards canonical Rng
`@suaveplan/crypto/src/index.ts` SHALL re-export `Rng` from `@suaveplan/crypto-core` — which in turn forwards the canonical `Rng` from `@suaveplan/types`. The legacy `defaultRng` re-export SHALL be dropped; consumers use `defaultByteSource` (preserved under its real name) when they need the default callable byte producer. The `ByteSource` re-export SHALL remain unchanged — every existing consumer of `import { ByteSource } from "@suaveplan/crypto"` continues to work.
**Verified by:** `packages/crypto/crypto/src/index.ts` (presence of `Rng` re-export; absence of `defaultRng` re-export; presence of `ByteSource`, `defaultByteSource`, `randomBytes`, `RandomBytesOptions` re-exports) AND `bunx turbo typecheck test --filter=@suaveplan/crypto` (green with zero assertion edits) AND repo-internal grep `grep -rn "defaultRng\b" packages/crypto/crypto/src/ --include="*.ts"` returns zero results.
#### Scenario: Rng re-export forwards canonical
- **WHEN** a consumer writes `import type { Rng } from "@suaveplan/crypto"`
- **THEN** the imported type is structurally identical to `import type { Rng } from "@suaveplan/types"`
#### Scenario: defaultRng re-export is removed
- **WHEN** a consumer writes `import { defaultRng } from "@suaveplan/crypto"`
- **THEN** `tsc` rejects with "has no exported member `defaultRng`"
#### Scenario: defaultByteSource remains available
- **WHEN** a consumer writes `import { defaultByteSource } from "@suaveplan/crypto"`
- **THEN** `tsc` accepts AND `defaultByteSource(32).length === 32`

View File

@@ -0,0 +1,28 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: mime (mime)
**Package**: `@suaveplan/mime`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## MODIFIED Requirements
### Requirement: Stream byte source uses a distinct name
The file-scope helper type at `src/detection/from-stream.ts` (currently `type ByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>`) SHALL be renamed to `StreamByteSource`. The rename removes a cross-package name collision with `@suaveplan/crypto-core`'s and `@suaveplan/crypto-utils`'s `ByteSource` callable type, which is a distinct concept (`(n: number) => Uint8Array`). The local `StreamByteSource` remains file-scope only (not exported from `src/detection/from-stream.ts`); this change has zero external-consumer impact.
`@suaveplan/mime` SHALL NOT declare a top-level `Rng`-shaped type — it has no need for a random-number source and does not appear in the unification audit's "structurally compatible / incompatible" categorisation.
**Verified by:** `packages/data/mime/src/detection/from-stream.ts` (declaration `type StreamByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>` and parameter `source: StreamByteSource`) AND `packages/data/mime/src/detection/from-stream.test.ts` (test names unchanged; runtime unaffected) AND `bunx turbo typecheck test --filter=@suaveplan/mime` (green with zero assertion edits) AND repo-internal grep `grep -rn "ByteSource" packages/data/mime/src/ --include="*.ts"` returns only `StreamByteSource` matches.
#### Scenario: rename is internal-only
- **WHEN** a consumer imports anything from `@suaveplan/mime`
- **THEN** no breaking change is observable — `StreamByteSource` was never exported
#### Scenario: detectFromStream signature unchanged at the public boundary
- **WHEN** a consumer calls `detectFromStream(readableStream)`
- **THEN** the call typechecks and behaviour is unchanged (the parameter is structurally identical to before — only the local alias name changed)

View File

@@ -0,0 +1,36 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: testing (testing)
**Package**: `@suaveplan/testing`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## MODIFIED Requirements
### Requirement: Rng surface re-exports the canonical type
`@suaveplan/testing` SHALL re-export the canonical `Rng` interface from `@suaveplan/types` rather than declaring a local `interface Rng`. The re-export lives at `src/context/context.ts` and is named `Rng`, preserving the existing consumer import path (`import type { Rng } from "@suaveplan/testing"`) for one release cycle. The local `cryptoRng` and `seededRng(seed)` factories SHALL continue to satisfy the canonical type — they provide the required `nextUint32` / `nextFloat` members and omit the optional `nextBytes` / `nextIntInRange` members. The package SHALL add `@suaveplan/types` to `dependencies` as `workspace:^`; this is the sole `@suaveplan/*` runtime dependency permitted for testing-infrastructure packages under CLAUDE.md rule 8, and is allowed because `@suaveplan/types` is types-only and emits zero runtime (verified by the byte-size guard).
**Verified by:** `packages/testing/testing/src/context/context.ts` (re-export line `export type { Rng } from "@suaveplan/types"`) AND `packages/testing/testing/src/context/context.test.ts::"cryptoRng satisfies canonical Rng"` AND `packages/testing/testing/src/context/context.test.ts::"seededRng satisfies canonical Rng"` AND `packages/testing/testing/package.json` `dependencies` contains `"@suaveplan/types": "workspace:^"` AND `bunx turbo test --filter=...@suaveplan/testing` (foundation dependent sweep).
#### Scenario: local Rng re-export forwards canonical
- **WHEN** a consumer writes `import type { Rng } from "@suaveplan/testing"`
- **THEN** the imported type is structurally identical to `import type { Rng } from "@suaveplan/types"`
#### Scenario: cryptoRng satisfies canonical
- **WHEN** `const r: import("@suaveplan/types").Rng = cryptoRng`
- **THEN** `tsc` accepts (cryptoRng provides nextUint32 + nextFloat; optional members omitted)
#### Scenario: seededRng satisfies canonical
- **WHEN** `const r: import("@suaveplan/types").Rng = seededRng(42)`
- **THEN** `tsc` accepts AND `r.nextUint32()` is deterministic given seed 42
#### Scenario: byte-size guard remains green
- **WHEN** the testing-infra byte-size guard runs after `@suaveplan/types` is added as a dependency
- **THEN** the guard exits zero (because `@suaveplan/types` emits zero runtime bytes — it is a types-only package)

View File

@@ -0,0 +1,49 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: types (types)
**Package**: `@suaveplan/types`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## MODIFIED Requirements
### Requirement: Rng is the canonical random-source contract
The `Rng` interface SHALL declare `nextUint32(): number` and `nextFloat(): number` as REQUIRED members, and `nextBytes(n: number): Uint8Array` and `nextIntInRange(min: number, maxExclusive: number): number` as OPTIONAL members. Implementations SHOULD provide the optional members when efficient (CSPRNGs typically expose `getRandomValues` directly; deterministic seeded RNGs can derive both from `nextUint32`). Consumers needing them MUST narrow on presence (`if (rng.nextBytes)`). `nextIntInRange` is contractually expected to use unbiased rejection sampling, not the `% range` shortcut.
`@suaveplan/types` SHALL be the **single canonical home** for the `Rng` interface across the genesis monorepo. No other `@suaveplan/*` package MAY declare its own top-level `Rng` interface or `type Rng = ...` alias that differs structurally from the canonical export. Packages that need a narrower or wider random-source shape MUST either:
1. consume the canonical `Rng` directly via `import type { Rng } from "@suaveplan/types"`, OR
2. define a sub-interface that `extends Rng` (e.g. `@suaveplan/pseudo-random`'s `SeededRng extends Rng`), OR
3. define a distinct concept under a non-`Rng` name (e.g. `ByteSource` for a callable byte producer `(n: number) => Uint8Array`, since that is a different contract from a uniform-numeric RNG).
Packages MAY re-export the canonical `Rng` under the name `Rng` as a backwards-compatibility forwarder during a documented deprecation window; such a re-export MUST carry a prose comment naming the canonical import path and the removal change ID. The JSDoc `@deprecated` tag is banned per CLAUDE.md §2.
**Verified by:** `packages/core/types/src/rng.ts` (canonical interface declaration) AND `packages/core/types/src/rng.md` (single-source-of-truth narrative) AND repo-wide `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`, `packages/random/pseudo-random/src/types/seeded-rng.types.ts` (legitimate `SeededRng extends Rng`), and the documented back-compat re-export forwarder lines in `crypto-core/random-bytes`, `crypto-core/index`, `crypto-core/primitives/index`, `crypto-core/primitives/random-bytes/index`, `crypto/index`, and `testing/context`.
#### Scenario: nextBytes is optional
- **WHEN** a consumer declares `const r: Rng = { nextUint32: () => 0, nextFloat: () => 0 }`
- **THEN** `tsc` accepts (no `nextBytes` provided)
#### Scenario: nextIntInRange signature
- **WHEN** an implementation provides `nextIntInRange(min, maxExclusive): number`
- **THEN** `tsc` accepts AND `nextIntInRange(0, 10)` returns `number`
#### Scenario: canonical Rng is the only authoritative declaration
- **WHEN** a developer runs `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` across the genesis monorepo
- **THEN** the only authoritative `Rng` declaration is `packages/core/types/src/rng.ts`, the only legitimate extension is `packages/random/pseudo-random/src/types/seeded-rng.types.ts`'s `SeededRng extends Rng`, and any additional matches are documented back-compat re-export forwarder lines (`export type { Rng } from "@suaveplan/types";`) — never an independent interface body or a structurally-incompatible `type Rng = ...` alias
#### Scenario: sub-interface extension is permitted
- **WHEN** a downstream package defines `interface BoundedRng extends Rng { nextIntInRange(min: number, maxExclusive: number): number }` to require the optional member
- **THEN** `tsc` accepts AND any value of type `BoundedRng` is assignable to `Rng`
#### Scenario: distinct-concept names are preserved
- **WHEN** a package needs a callable byte producer `(n: number) => Uint8Array`
- **THEN** it MUST declare that type under a distinct name (e.g. `ByteSource`) and SHALL NOT alias it as `Rng`, because the canonical `Rng` is an object with `nextUint32`/`nextFloat`, not a callable

View File

@@ -0,0 +1,169 @@
# Tasks: complete-rng-unification
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 0. Pre-flight (mandatory before any edit)
- [ ] 0.1 Confirm `bunx turbo typecheck test --filter=@suaveplan/types` green at HEAD (baseline).
- [ ] 0.2 Confirm `bunx turbo typecheck test --filter=@suaveplan/testing` green at HEAD (baseline).
- [ ] 0.3 Confirm `bunx turbo typecheck test --filter=@suaveplan/crypto-core` green at HEAD (baseline).
- [ ] 0.4 Confirm `bunx turbo typecheck test --filter=@suaveplan/crypto` green at HEAD (baseline).
- [ ] 0.5 Confirm `bunx turbo typecheck test --filter=@suaveplan/mime` green at HEAD (baseline).
- [ ] 0.6 Re-run the exhaustive grep documented in `proposal.md` § Audit and confirm zero NEW matches have appeared since this proposal was drafted.
## 1. `@suaveplan/types` — spec-only tightening
- [ ] 1.1 Update `packages/core/types/src/rng.md` to add a "Single source of truth" section noting that `@suaveplan/types` is the only canonical home for `Rng` across the monorepo and the permitted forms of downstream redeclaration (consume-direct, extend, or distinct-concept-under-distinct-name).
- [ ] 1.2 No code change to `packages/core/types/src/rng.ts` (shape is already correct).
- [ ] 1.3 `bunx turbo typecheck test --filter=@suaveplan/types` — green.
- [ ] 1.4 `bunx turbo test --filter=...@suaveplan/types` — foundation dependent sweep green (mandatory per §13 step 10).
## 2. `@suaveplan/testing` — migrate Rng interface to re-export
- [ ] 2.1 Locate local declaration: `packages/testing/testing/src/context/context.ts` line 45 (`export interface Rng { nextUint32(): number; nextFloat(): number }`).
- [ ] 2.2 Confirm structural compatibility — required members are an exact subset of canonical; canonical adds two OPTIONAL members which existing `cryptoRng` / `seededRng` may omit.
- [ ] 2.3 Add `"@suaveplan/types": "workspace:^"` to `packages/testing/testing/package.json` `dependencies` (rule 8 exception: testing-infra may depend on `@suaveplan/types` for types-only — confirm byte-size guard still passes).
- [ ] 2.4 Replace `export interface Rng { ... }` with:
```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";
```
- [ ] 2.5 Confirm `cryptoRng: Rng = { ... }` and `seededRng(seed): Rng` still typecheck against the canonical (they provide only the required members).
- [ ] 2.6 Update sibling `context.md` API reference: note `Rng` is now a re-export of canonical; cross-link to `@suaveplan/types`.
- [ ] 2.7 No barrel edits needed — `src/index.ts:61` `export type { Clock, Rng } from "./context/context.js"` continues to forward correctly.
- [ ] 2.8 `bunx turbo typecheck --filter=@suaveplan/testing` — green.
- [ ] 2.9 `bunx turbo test --filter=@suaveplan/testing` — green, zero assertion edits.
- [ ] 2.10 `bunx turbo test:coverage --filter=@suaveplan/testing` — 100/100/100/100 preserved.
- [ ] 2.11 Dependent sweep: `bunx turbo test --filter=...@suaveplan/testing` — every test in the monorepo flows through `@suaveplan/testing`, must be green.
## 3. `@suaveplan/crypto-core` — drop misleading Rng alias + defaultRng
- [ ] 3.1 Locate local declarations in `packages/crypto/crypto-core/src/primitives/random-bytes/random-bytes.ts`:
- line 9: `export type ByteSource = (n: number) => Uint8Array;` — KEEP.
- line 12: `export type Rng = ByteSource;` — REMOVE.
- line 19: `export const defaultByteSource: ByteSource = ...` — KEEP.
- line 36: `export const defaultRng: ByteSource = defaultByteSource;` — REMOVE.
- line 38–41: `RandomBytesOptions { byteSource?: ByteSource; rng?: ByteSource }` — drop the `rng` field.
- line 62: `const source = options.byteSource ?? options.rng ?? defaultByteSource;` — simplify to `options.byteSource ?? defaultByteSource`.
- [ ] 3.2 Add back-compat re-export at the bottom of `random-bytes.ts`:
```ts
// Back-compat re-export of @suaveplan/types' canonical Rng. Prefer importing
// Rng from @suaveplan/types directly. This forwarder exists for one release
// cycle — the legacy local `type Rng = ByteSource` alias was structurally
// incompatible with canonical (callable vs object) and has been removed.
// The follow-up `remove-deprecated-rng-aliases` change will delete this line.
export type { Rng } from "@suaveplan/types";
```
- [ ] 3.3 Confirm `@suaveplan/types` is in `packages/crypto/crypto-core/package.json` `dependencies` (it already is per `MANIFEST.md`).
- [ ] 3.4 Update barrel `packages/crypto/crypto-core/src/primitives/random-bytes/index.ts`:
- keep `type ByteSource`, `defaultByteSource`, `type RandomBytesOptions`, `randomBytes`.
- keep `type Rng` (now forwards canonical via the back-compat re-export).
- drop `defaultRng`.
- [ ] 3.5 Update barrel `packages/crypto/crypto-core/src/primitives/index.ts` — same: drop `defaultRng`, keep `Rng` forwarding canonical.
- [ ] 3.6 Update barrel `packages/crypto/crypto-core/src/index.ts` — same: drop `defaultRng`, keep `Rng` forwarding canonical.
- [ ] 3.7 Update sibling `random-bytes.md`: drop `Rng` and `defaultRng` from local API reference; add "Implementation Notes" section explaining the type-class distinction (canonical `Rng` is an object; `ByteSource` is a callable; the local `Rng` is now a back-compat re-export of the canonical, not the legacy callable alias).
- [ ] 3.8 Repo-internal callsite check — confirm no `crypto-core` test or impl file uses `defaultRng` or `RandomBytesOptions.rng`:
```bash
grep -rn "defaultRng\b\|\.rng:\|, rng:" packages/crypto/crypto-core/src/ --include="*.ts"
```
- [ ] 3.9 `bunx turbo typecheck --filter=@suaveplan/crypto-core` — green.
- [ ] 3.10 `bunx turbo test --filter=@suaveplan/crypto-core` — green, zero assertion edits.
- [ ] 3.11 `bunx turbo test:coverage --filter=@suaveplan/crypto-core` — 100/100/100/100 preserved.
- [ ] 3.12 Dependent sweep: `bunx turbo test --filter=...@suaveplan/crypto-core` — every consumer (`crypto`, `crypto-rsa`, `crypto-vault`, `crypto-keys`, `vault`, `vault-sync`, `webauthn`, `crypto-cli`, `crypto-bin`, `crypto-sodium`, `crypto-browser`) green.
## 4. `@suaveplan/crypto` — update forwarder barrel
- [ ] 4.1 Locate `Rng` and `defaultRng` re-exports in `packages/crypto/crypto/src/index.ts` (currently re-exported from `@suaveplan/crypto-core` block).
- [ ] 4.2 Keep the `Rng` re-export (it now forwards canonical via `crypto-core`'s back-compat re-export).
- [ ] 4.3 Drop the `defaultRng` re-export.
- [ ] 4.4 Repo-internal callsite check — confirm no `crypto` test or impl file uses `defaultRng`:
```bash
grep -rn "defaultRng\b" packages/crypto/crypto/src/ --include="*.ts"
```
- [ ] 4.5 If any `.md` in `packages/crypto/crypto/src/` mentions `Rng` or `defaultRng`, update to reference `@suaveplan/types` (canonical) and `defaultByteSource` (correct name for the dropped alias).
- [ ] 4.6 `bunx turbo typecheck --filter=@suaveplan/crypto` — green.
- [ ] 4.7 `bunx turbo test --filter=@suaveplan/crypto` — green.
- [ ] 4.8 `bunx turbo test:coverage --filter=@suaveplan/crypto` — 100/100/100/100 preserved.
## 5. `@suaveplan/mime` — rename file-scope ByteSource
- [ ] 5.1 Locate `packages/data/mime/src/detection/from-stream.ts` line 19: `type ByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>`.
- [ ] 5.2 Rename to `StreamByteSource`; update the single reference at line 30 (`source: StreamByteSource`).
- [ ] 5.3 Confirm the type is NOT exported from `from-stream.ts` (it is `type`, not `export type`) — name collision is internal only, but renaming eliminates future confusion.
- [ ] 5.4 Update sibling `from-stream.md` if it mentions `ByteSource`.
- [ ] 5.5 Confirm `from-stream-reader.ts` (which uses `ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>` inline) is unaffected.
- [ ] 5.6 `bunx turbo typecheck --filter=@suaveplan/mime` — green.
- [ ] 5.7 `bunx turbo test --filter=@suaveplan/mime` — green.
- [ ] 5.8 `bunx turbo test:coverage --filter=@suaveplan/mime` — 100/100/100/100 preserved.
## 6. Spec deltas
- [ ] 6.1 Author `specs/types/spec.md` delta with `## MODIFIED Requirements` block on the canonical-Rng requirement, adding the single-source-of-truth clause.
- [ ] 6.2 Author `specs/testing/spec.md` delta with `## MODIFIED Requirements` block documenting `Rng` is now a back-compat re-export.
- [ ] 6.3 Author `specs/crypto-core/spec.md` delta with `## MODIFIED Requirements` documenting: `Rng` is canonical re-export; `defaultRng` is removed; `RandomBytesOptions.rng` field is removed; `ByteSource` is preserved under its real name (callable byte producer, distinct from `Rng`).
- [ ] 6.4 Author `specs/crypto/spec.md` delta with `## MODIFIED Requirements` documenting the forwarder barrel update.
- [ ] 6.5 Author `specs/mime/spec.md` delta with `## MODIFIED Requirements` documenting the `ByteSource` → `StreamByteSource` rename.
- [ ] 6.6 Every requirement carries `**Verified by:**` line at authoring time.
## 7. Repo-wide verification gate
- [ ] 7.1 Repo-wide grep returns exactly the documented set:
```bash
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
```
Allowed hits:
- `packages/core/types/src/rng.ts` (canonical declaration)
- `packages/random/pseudo-random/src/types/seeded-rng.types.ts` (legitimate `SeededRng extends Rng`)
- back-compat `export type { Rng } from "@suaveplan/types"` forwarder lines in `crypto-core/random-bytes`, `crypto-core/primitives/index`, `crypto-core/primitives/random-bytes/index`, `crypto-core/index`, `crypto/index`, `testing/context`
- [ ] 7.2 Repo-wide `ByteSource` grep — confirm zero unintended renames; only the `mime` file-scope occurrence has changed.
- [ ] 7.3 Repo-wide `defaultRng` grep returns zero hits in genesis source (it remains in `@suaveplan/crypto-rsa/src/encrypt/default-rng.ts` as a `function defaultRng(): Rng` — that is correct usage, not the dropped alias).
- [ ] 7.4 `bunx turbo lint typecheck --filter=...` (full repo) — green.
## 8. Doc-drift + behaviour-change propagation
- [ ] 8.1 Per CLAUDE.md §4a: every `.ts` edited has its sibling `.md` updated in the same change set (covered by tasks 1.1, 2.6, 3.7, 4.5, 5.4).
- [ ] 8.2 Per CLAUDE.md §13a: the `## Migration` note is authored in `proposal.md` (already done) — verify the search command works and produces the expected callsites.
- [ ] 8.3 Update root `MIGRATION.md` with the per-package migration note (canonical `Rng` import path, `defaultRng` → `defaultByteSource`, `RandomBytesOptions.rng` → `byteSource`).
- [ ] 8.4 Repo-wide consumer scan:
```bash
grep -rln "from ['\"]@suaveplan/\(crypto-core\|crypto\|testing\)['\"]" packages/ \
| xargs grep -l "Rng\b" 2>/dev/null
```
Every hit either continues to compile via the back-compat forwarder OR is updated to canonical import in this change set.
## 9. Root-doc refresh + markdownlint
- [ ] 9.1 No package add/remove; `MANIFEST.md` dep graph gains one edge (`@suaveplan/testing → @suaveplan/types`). Run `bun scripts/folder-reorg/generate-manifest.ts`.
- [ ] 9.2 `STATUS.md` unchanged (no new pending → completed transitions).
- [ ] 9.3 `SUMMARY.md` — update `@suaveplan/types`, `@suaveplan/testing`, `@suaveplan/crypto-core`, `@suaveplan/crypto` paragraphs to mention the canonical `Rng` clause and the back-compat forwarder.
- [ ] 9.4 `MIGRATION.md` — updated per task 8.3.
- [ ] 9.5 `bunx markdownlint-cli2 --fix` — green.
## 10. Fold + archive
- [ ] 10.1 Fold `specs/types/spec.md` delta into `openspec/specs/types/spec.md`.
- [ ] 10.2 Fold `specs/testing/spec.md` delta into `openspec/specs/testing/spec.md`.
- [ ] 10.3 Fold `specs/crypto-core/spec.md` delta into `openspec/specs/crypto-core/spec.md`.
- [ ] 10.4 Fold `specs/crypto/spec.md` delta into `openspec/specs/crypto/spec.md`.
- [ ] 10.5 Fold `specs/mime/spec.md` delta into `openspec/specs/mime/spec.md`.
- [ ] 10.6 Move `openspec/changes/complete-rng-unification/` → `openspec/archive/<yyyy-mm-dd>-complete-rng-unification/`.
- [ ] 10.7 Open the follow-up `remove-deprecated-rng-aliases` change to track removal of the back-compat re-export forwarders after one release cycle.