Files
suaveplan/docs/superpowers/specs/2026-04-20-utils-migration-design.md
2026-05-27 23:29:53 +01:00

187 lines
7.9 KiB
Markdown

# Design: `@suaveplan/utils` Migration — genesis
**Date:** 2026-04-20
**Branch:** genesis
**Source:** `@suaveplan/utils` from main (`/mnt/s/web/suaveplan/packages/universal/utils`)
**Reference:** `/mnt/s/web/suaveplan/enterprise/utils.md`
---
## Context
The old `@suaveplan/utils` package is a 14-module grab-bag imported by 109 reverse-consumer workspaces. On genesis, we rewrite it fresh at enterprise quality and split it into four packages, each with a clear, single responsibility. Nothing is ported mechanically — every module is rewritten to meet the genesis enterprise checklist.
---
## Decision: Split into 4 packages
| Package | Domain path | Purpose |
|---------|-------------|---------|
| `@suaveplan/utils` | `universal/data/utils` | Zero-external-dep odds-and-sods utilities |
| `@suaveplan/json` | `universal/data/json` | Prototype-pollution-safe JSON parse/stringify |
| `@suaveplan/bytes` | `universal/primitives/bytes` | Byte encoding / format conversion |
| `@suaveplan/locale-data` | `universal/i18n/locale-data` | ISO country + language static data |
**Scrapped:** `string/parseTemplate` only — duplicate of `@suaveplan/templates` which already handles `{{var}}` interpolation. `string/parseTextWithLinks` is distinct (link annotation, not template interpolation) and is retained in `@suaveplan/utils`.
---
## Package 1: `@suaveplan/utils`
**Principle:** Zero external deps beyond the genesis standard (`zod`, `@opentelemetry/api`, `@suaveplan/error`, `@suaveplan/types`). Odds-and-sods utilities that are not worth a dedicated package.
### Modules
| Module | Key exports |
|--------|-------------|
| `debug` | `debug.log/warn/error` — dedup-buffered console logger (suppresses repeated messages within a rolling 10-item buffer) |
| `object` | `deepMerge`, `deepMergeAll`, `deepClone`, `pick`, `omit`, `flattenObject`, `getNestedValue`, `setNestedValue`, `hasPath`, `isEmpty`, `isObject`, `isPlainObject`, `isObjectOrArray`, `keepKeys`, `removeKeys` |
| `platform` | `getPlatform`, `getMemoryUsage`, `getCpuUsage`, `isNode`, `isBrowser`, `isBun` — injectable platform adapter |
| `number` | `packFloat16/32/64`, `unpackFloat16/32/64`, `isInFloat16/32/64Range`, `min16/32/64`, `max16/32/64` |
| `random` | `pseudoRandom<T>(arr, index, seed?)` — seeded deterministic array element selection |
| `email` | `normalizeEmail` — Gmail dots/plus, Yahoo dashes, Microsoft plus, generic normalization |
| `sound` | `parseSoundVariations`, `getSoundKey`, `convertSprites` — audio sprite key parsing + variation selection |
| `string` | `generateId`, `parseTextWithLinks` |
| `validate` | `isHex` |
| `validation` | `validateSize`, `formatSize`, `parseSize`, `truncateToSize`, `isWithinSizeLimit`, `getDataSize`, `MessageSizeError`, `SIZE_LIMITS`, `DEFAULT_MAX_MESSAGE_SIZE` |
### Dependencies
```text
@suaveplan/error workspace:^
@suaveplan/types workspace:^
zod catalog:zod
@opentelemetry/api catalog:otel-api
```
---
## Package 2: `@suaveplan/json`
**Principle:** Safe, hardened JSON operations. No external deps.
### Modules
| Module | Key exports |
|--------|-------------|
| `safe-json` | `safeParse`, `safeStringify`, `tryParse`, `isValidJSON`, `getStringifySize`, `SafeJSONError` |
### Features
- Prototype pollution guard: rejects `__proto__`, `constructor`, `prototype` keys by default (configurable)
- Circular reference detection with structured `SafeJSONError`
- `tryParse` returns `undefined` instead of throwing
- `getStringifySize` measures serialised byte length without full allocation
- All errors extend `@suaveplan/error` with stable codes (`JSON.PARSE_FAILED`, `JSON.CIRCULAR`, `JSON.PROTOTYPE_POLLUTION`)
### Dependencies
```text
@suaveplan/error workspace:^
@suaveplan/types workspace:^
zod catalog:zod
@opentelemetry/api catalog:otel-api
```
---
## Package 3: `@suaveplan/bytes`
**Principle:** Single canonical home for byte encoding and format conversion in the monorepo. `@suaveplan/crypto-utils` currently duplicates some of this; a follow-up task updates `crypto-utils` to consume `@suaveplan/bytes` instead.
### Modules
| Module | Key exports |
|--------|-------------|
| `encoding` | `bytesToHex`, `hexToBytes`, `bytesToBase64`, `base64ToBytes`, `bytesToUtf8`, `utf8ToBytes` |
| `uint8` | `concatUint8Arrays`, `sliceUint8Array`, `uint8ArraysEqual`, `inputToUint8Array`, `detectInputFormat`, `validateInputTypeFormat`, `formatOutput`, `convertDataFormat` |
| `uuid` | `uint8ArrayToUuid`, `uuidToUint8Array`, `generateUuid` — uses `crypto.randomUUID()` natively; no `uuid` npm package needed |
### Features
- Format auto-detection (`detectInputFormat`: hex, base64, utf8, Uint8Array)
- Round-trip guarantees tested via fast-check property tests
- All errors extend `@suaveplan/error` with stable codes (`BYTES.INVALID_FORMAT`, `BYTES.ENCODE_FAILED`, etc.)
### Dependencies
```text
@suaveplan/error workspace:^
@suaveplan/types workspace:^
zod catalog:zod
@opentelemetry/api catalog:otel-api
```
### Follow-up
After `@suaveplan/bytes` is published, open an OpenSpec change to update `@suaveplan/crypto-utils` to depend on `@suaveplan/bytes` and remove the duplicate hex/base64/utf8/bytes/concat encoding utilities. **`base58` stays in `crypto-utils`** — it is blockchain/crypto-specific (Bitcoin address encoding) and not a general-purpose byte encoding concern; moving it to `bytes` would introduce a crypto-domain concept into a general primitive.
---
## Package 4: `@suaveplan/locale-data`
**Principle:** Static ISO reference data. Too large and domain-specific to be an odds-and-sods utility. Feeds `@suaveplan/i18n`.
### Modules
| Module | Key exports |
|--------|-------------|
| `countries` | `CountryItem` type, per-country named exports (`AD`, `AE`, ...), `getCountry(code)`, `getAllCountries()`, `getCountriesByContinent(continent)`, `getCountriesByCurrency(currency)` |
| `languages` | `LanguageItem` type, `getLanguages()`, `getLanguage(code)`, `getRtlLanguages()`, `isRtlLanguage(code)` |
### Features
- All data typed with Zod schemas
- Lookup helpers with structured errors for unknown codes
- Zero runtime deps — data is bundled at build time, not fetched
### Dependencies
```text
@suaveplan/error workspace:^
@suaveplan/types workspace:^
zod catalog:zod
@opentelemetry/api catalog:otel-api
```
---
## Build sequence
All four packages share the same dependency set (genesis standard), so they can land in parallel or any order. Recommended sequence:
1. `@suaveplan/bytes` — unblocks `crypto-utils` refactor
2. `@suaveplan/json` — self-contained
3. `@suaveplan/locale-data` — self-contained; enables `i18n` to consume it
4. `@suaveplan/utils` — self-contained; the catch-all
---
## Enterprise checklist (applies to all four)
- [ ] 100% statement + branch + function + line coverage (Istanbul)
- [ ] 100% docs coverage (`bun run docs:check`)
- [ ] Every `.ts` implementation file has a sibling `.md` (≥200 words, simple + advanced examples, API reference)
- [ ] All public types have Zod schemas in sibling `*.schemas.ts`
- [ ] All thrown errors extend `@suaveplan/error` with stable `PACKAGE.DOMAIN.REASON` codes
- [ ] Injectable `clock`, `rng`, `signal` where applicable
- [ ] `AbortSignal` on every iterative helper
- [ ] OTel counters + histograms + spans on public entrypoints
- [ ] fast-check property tests for pure functions
- [ ] mitata benchmarks with CI regression gate
- [ ] `sideEffects: false`; one subpath export per feature module
- [ ] `@vitest/coverage-istanbul` in `devDependencies`
- [ ] OpenSpec change created, verified, archived
---
## OpenSpec changes to create
| Change ID | Scope |
|-----------|-------|
| `enterprise-bytes` | New `@suaveplan/bytes` package |
| `enterprise-json` | New `@suaveplan/json` package |
| `enterprise-locale-data` | New `@suaveplan/locale-data` package |
| `enterprise-utils` | New `@suaveplan/utils` package (genesis rewrite) |
| `crypto-utils-bytes-refactor` | Update `@suaveplan/crypto-utils` to consume `@suaveplan/bytes` (follow-up) |