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

155 lines
8.1 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: utils (utils-completion)
**Package**: `@suaveplan/utils`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
## ADDED Requirements
### Requirement: Hex / Uint8Array round-trip
The package SHALL expose `bytesToHex(bytes: Uint8Array): string` and `hexToBytes(hex: string): Uint8Array` functions that perform a stable round-trip. `hexToBytes` SHALL accept upper- and lower-case hex digits, MAY tolerate non-hex separator characters by extracting hex pairs, and SHALL throw `UtilsError(UINT8_ARRAY.INVALID_HEX)` when the input is the empty string, `null`, `undefined`, or a non-string value.
#### Scenario: round-trip preserves bytes
- **WHEN** a caller invokes `hexToBytes(bytesToHex(b))` for any `b: Uint8Array`
- **THEN** the result equals `b` byte-for-byte
#### Scenario: empty hex throws structured error
- **WHEN** a caller invokes `hexToBytes("")`
- **THEN** the call throws `UtilsError` with code `UINT8_ARRAY.INVALID_HEX`
**Verified by:** `src/uint8-array/uint8-array.test.ts::"round-trips bytes through hex"` AND `src/uint8-array/uint8-array.test.ts::"throws UtilsError when hex input is empty"` AND `src/uint8-array/uint8-array.spec-conformance.test.ts::"hex error carries UINT8_ARRAY.INVALID_HEX code"`
---
### Requirement: Base64 / Uint8Array round-trip
The package SHALL expose `bytesToBase64(bytes: Uint8Array): string` and `base64ToBytes(base64: string): Uint8Array` that round-trip cleanly across browser (`btoa` / `atob`) and Node (`Buffer`) environments without relying on a specific runtime.
#### Scenario: round-trip preserves bytes
- **WHEN** a caller invokes `base64ToBytes(bytesToBase64(b))` for any `b: Uint8Array`
- **THEN** the result equals `b` byte-for-byte
**Verified by:** `src/uint8-array/uint8-array.test.ts::"round-trips bytes through base64"` AND `src/uint8-array/uint8-array.spec-conformance.test.ts::"base64 round-trip property holds for fast-check generated buffers"`
---
### Requirement: UTF-8 / Uint8Array round-trip
The package SHALL expose `utf8ToBytes(str: string): Uint8Array` and `bytesToUtf8(bytes: Uint8Array): string` backed by native `TextEncoder` / `TextDecoder`.
#### Scenario: round-trip preserves UTF-8 strings including surrogates
- **WHEN** a caller invokes `bytesToUtf8(utf8ToBytes(s))` for `s` containing emoji and non-BMP code points
- **THEN** the result equals `s`
**Verified by:** `src/uint8-array/uint8-array.test.ts::"round-trips utf-8 strings including emoji"`
---
### Requirement: Format auto-detection
The package SHALL expose `detectInputFormat(input: string | Uint8Array): DataFormat` returning `"uint8array"` for `Uint8Array` inputs, `"hex"` for strings that match `/^(?:0x)?[\da-f]*$/i` with even length OR a `0x` prefix, and `"utf8"` otherwise. Non-string non-`Uint8Array` inputs SHALL throw `UtilsError(UINT8_ARRAY.INVALID_FORMAT)`.
#### Scenario: hex string detected
- **WHEN** a caller invokes `detectInputFormat("deadbeef")`
- **THEN** it returns `"hex"`
#### Scenario: utf-8 string detected
- **WHEN** a caller invokes `detectInputFormat("hello world")`
- **THEN** it returns `"utf8"`
#### Scenario: invalid input throws
- **WHEN** a caller invokes `detectInputFormat(42 as never)`
- **THEN** it throws `UtilsError` with code `UINT8_ARRAY.INVALID_FORMAT`
**Verified by:** `src/uint8-array/uint8-array.test.ts::"detects hex format from even-length hex string"` AND `src/uint8-array/uint8-array.test.ts::"defaults to utf-8 for non-hex strings"` AND `src/uint8-array/uint8-array.spec-conformance.test.ts::"throws UINT8_ARRAY.INVALID_FORMAT for non-string non-Uint8Array input"`
---
### Requirement: Format conversion
The package SHALL expose `convertDataFormat(input, outputFormat, inputFormat?)` that auto-detects the input format when omitted, validates the JS type matches the declared format when supplied, and produces output in the requested format. Underlying mechanics SHALL go through `inputToUint8Array` then `formatOutput`.
#### Scenario: hex string converts to base64
- **WHEN** a caller invokes `convertDataFormat("deadbeef", "base64")`
- **THEN** the result is the base64-encoded string for the bytes `[0xde, 0xad, 0xbe, 0xef]`
#### Scenario: explicit input format mismatch throws
- **WHEN** a caller invokes `convertDataFormat(new Uint8Array([1, 2]), "hex", "utf8")`
- **THEN** it throws `UtilsError` with code `UINT8_ARRAY.INVALID_FORMAT`
**Verified by:** `src/uint8-array/uint8-array.test.ts::"converts hex to base64"` AND `src/uint8-array/uint8-array.test.ts::"throws when explicit inputFormat does not match input type"`
---
### Requirement: UUID interop
The package SHALL expose `uint8ArrayToUuid(bytes: Uint8Array): string` (requires exactly 16 bytes), `uuidToUint8Array(uuid: string): Uint8Array`, and `generateUuid(options?: { rng?: Rng }): string` returning RFC 4122 v4 UUIDs. The `rng` option SHALL accept a `Rng` from `@suaveplan/types`; when omitted, the implementation SHALL use `crypto.getRandomValues`. Module-scope `Math.random()` is forbidden.
#### Scenario: 16-byte array round-trips through UUID string
- **WHEN** a caller invokes `uuidToUint8Array(uint8ArrayToUuid(b))` for any `b: Uint8Array` of length 16
- **THEN** the result equals `b`
#### Scenario: wrong-length array throws
- **WHEN** a caller invokes `uint8ArrayToUuid(new Uint8Array(15))`
- **THEN** it throws `UtilsError` with code `UINT8_ARRAY.INVALID_ARGUMENT`
#### Scenario: injected Rng is consumed deterministically
- **WHEN** a caller invokes `generateUuid({ rng: deterministicRng })` where `deterministicRng.bytes(16)` returns a fixed buffer
- **THEN** every invocation returns the same UUID string
**Verified by:** `src/uint8-array/uint8-array.test.ts::"round-trips 16-byte array through UUID string"` AND `src/uint8-array/uint8-array.test.ts::"throws when uint8ArrayToUuid receives non-16-byte array"` AND `src/uint8-array/uint8-array.spec-conformance.test.ts::"generateUuid consumes injected Rng deterministically"`
---
### Requirement: Equality, concatenation, and slicing
The package SHALL expose `uint8ArraysEqual(a, b)` (non-constant-time, documented as such), `concatUint8Arrays(...arrays)` (accepts varargs OR a single array argument; throws `UtilsError(UINT8_ARRAY.INVALID_ARGUMENT)` on non-`Uint8Array` elements), and `sliceUint8Array(data, splitAt)` returning a `[head, tail]` tuple (throws `UtilsError(UINT8_ARRAY.OUT_OF_BOUNDS)` when `splitAt < 0 || splitAt > data.length`).
#### Scenario: equality returns false on length mismatch
- **WHEN** a caller invokes `uint8ArraysEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3]))`
- **THEN** it returns `false`
#### Scenario: concat accepts both varargs and single-array forms
- **WHEN** a caller invokes `concatUint8Arrays(a, b)` AND `concatUint8Arrays([a, b])`
- **THEN** both calls return identical bytes
#### Scenario: slice out-of-bounds throws
- **WHEN** a caller invokes `sliceUint8Array(new Uint8Array(4), 5)`
- **THEN** it throws `UtilsError` with code `UINT8_ARRAY.OUT_OF_BOUNDS`
**Verified by:** `src/uint8-array/uint8-array.test.ts::"equality returns false on length mismatch"` AND `src/uint8-array/uint8-array.test.ts::"concat accepts varargs and single-array argument"` AND `src/uint8-array/uint8-array.test.ts::"slice throws when splitAt is out of bounds"` AND `src/uint8-array/uint8-array.spec-conformance.test.ts::"non-Uint8Array element triggers UINT8_ARRAY.INVALID_ARGUMENT"`
---
### Requirement: Subpath export and tree-shaking
The package SHALL expose the new module under the `./uint8-array` subpath in `package.json::exports`. Importing only `convertDataFormat` SHALL NOT pull in unrelated `@suaveplan/utils` modules (e.g. `string`, `email`).
#### Scenario: subpath import resolves
- **WHEN** a caller writes `import { convertDataFormat } from "@suaveplan/utils/uint8-array"`
- **THEN** TypeScript resolves the import and the bundled module size measured in a tree-shaking probe stays below the documented budget
**Verified by:** `src/uint8-array/uint8-array.spec-conformance.test.ts::"subpath ./uint8-array resolves and is tree-shakeable"`
<!-- markdownlint-enable MD024 -->