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

8.1 KiB

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"