Initial Commit
This commit is contained in:
154
openspec/changes/utils-completion/specs/utils/spec.md
Normal file
154
openspec/changes/utils-completion/specs/utils/spec.md
Normal file
@@ -0,0 +1,154 @@
|
||||
<!-- 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 -->
|
||||
Reference in New Issue
Block a user