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,131 @@
# Utils completion — Uint8Array helpers (countries / languages / safe-json placed elsewhere)
**Package**: `@suaveplan/utils`
**Status**: PROPOSED
**Type**: spec-conformance + feature
## Why
The 2026-05-02 audit of `packages/core/utils/src` against `../suaveplan/packages/universal/utils/src` flagged four legacy modules that genesis appears to lack: `countries/`, `languages/`, `json/safe-json.ts`, and `uint/`. Closer inspection shows three of the four are not gaps in `@suaveplan/utils` at all — they were intentionally hoisted into dedicated genesis packages (`@suaveplan/locale-data`, `@suaveplan/json`). The fourth, `uint/`, was dropped without a replacement and is the genuine gap.
The legacy `uint/` module (340 L) is misnamed — it is a Uint8Array / encoding helper bag covering hex/base64/UTF-8 round-trips, format auto-detection, UUID interop, equality, concatenation, and slicing. Crypto-utils already covers hex / base64 / concat / xor / equality for cryptographic call sites, but those helpers are deliberately scoped to the crypto plugin (no UUID dependency, no auto-detection). Application-level callers — drag-and-drop file readers, clipboard handlers, query-string parsers — need a dependency-light Uint8Array module they can pull from `@suaveplan/utils` without dragging the entire crypto stack. Without it, every consumer reimplements `bytesToHex` / `hexToBytes`, drift is inevitable, and we accumulate the kind of subtle off-by-one bugs the legacy harness already pinned tests for.
Hoisting `countries`, `languages`, and `safe-json` to dedicated packages was the right call (each has its own catalog, schemas, and OTel surface), but the parity table for `utils-completion` must say so explicitly so the next agent doesn't accidentally re-introduce them under `utils/` and create two copies that drift.
## What changes
### `@suaveplan/utils` (this change)
- ADDED: `uint8Array/` feature module under `packages/core/utils/src/uint8-array/` — encoding-format conversion + Uint8Array helpers tier appropriate for application-level (non-cryptographic) call sites.
- ADDED: `bytesToHex(bytes)` / `hexToBytes(hex)` — strict hex validation (rejects empty string, odd-length non-prefixed input, non-hex characters via opt-in strict mode).
- ADDED: `bytesToBase64(bytes)` / `base64ToBytes(base64)` — uses `btoa` / `atob` in browser, `Buffer` fallback in Node.
- ADDED: `utf8ToBytes(str)` / `bytesToUtf8(bytes)` — `TextEncoder` / `TextDecoder` round-trip.
- ADDED: `detectInputFormat(input)` — heuristic format auto-detection for `"hex" | "utf8" | "uint8array"`.
- ADDED: `validateInputTypeFormat(input, format)` — runtime check that the JS type matches the declared format.
- ADDED: `inputToUint8Array(input, format)` — universal coerce-to-bytes entry point.
- ADDED: `formatOutput(bytes, format)` — universal format-from-bytes entry point.
- ADDED: `convertDataFormat(input, outputFormat, inputFormat?)` — round-trip convertor with optional auto-detection.
- ADDED: `uint8ArrayToUuid(bytes)` / `uuidToUint8Array(uuid)` — RFC 4122 16-byte ↔ canonical string interop.
- ADDED: `generateUuid()` — UUID v4 emission backed by an injectable `Rng` from `@suaveplan/types`; defaults to `crypto.getRandomValues`. Genesis policy bans module-scope `Math.random()`.
- ADDED: `uint8ArraysEqual(a, b)` — non-constant-time equality (the constant-time variant lives in `@suaveplan/crypto-utils::constant-time`; documented in `uint8-array.md` to prevent misuse in cryptographic contexts).
- ADDED: `concatUint8Arrays(...arrays)` — accepts both varargs and a single array argument; throws on non-Uint8Array elements via `UtilsError` (`UINT8_ARRAY.INVALID_ARGUMENT`).
- ADDED: `sliceUint8Array(data, splitAt)` — returns `[head, tail]` tuple; throws on out-of-bounds via `UtilsError` (`UINT8_ARRAY.OUT_OF_BOUNDS`).
- ADDED: `DataFormat` and `InputType` types under `uint8-array.types.ts`.
- ADDED: `uint8-array.schemas.ts` — Zod schemas for `DataFormat` and `convertDataFormat` options.
- ADDED: subpath export `./uint8-array` in `package.json::exports` for tree-shaking.
### Out-of-scope mappings (NOT changed in this proposal — documented for parity)
The 2026-05-02 audit specifically asked whether `countries`, `languages`, and `safe-json` belong in `utils`. The answer is **no**, they live elsewhere; the parity checklist below is the source of truth so this question is not re-litigated.
| Capability | Upstream location | Genesis location | Status |
|---|---|---|---|
| Country code/name registry | `packages/universal/utils/src/countries/` (3021 L) | `packages/i18n/locale-data/src/countries/` | PRESENT-ELSEWHERE |
| Language code registry | `packages/universal/utils/src/languages/` (474 L) | `packages/i18n/locale-data/src/languages/` | PRESENT-ELSEWHERE |
| Safe JSON parse / stringify (proto-pollution guard) | `packages/universal/utils/src/json/safe-json.ts` (385 L) | `packages/data/json/src/safe-json/` | PRESENT-ELSEWHERE |
| Uint8Array / encoding helpers | `packages/universal/utils/src/uint/uint.ts` (340 L) | NONE | ADDED (this change) |
## Out of scope
- **Cross-publishing `countries` / `languages` / `safe-json` from `@suaveplan/utils`.** Re-exporting them from `utils` would re-introduce duplicate publish surface and break tree-shaking for non-i18n consumers. Callers wanting locale data import `@suaveplan/locale-data`; callers wanting safe JSON import `@suaveplan/json`. The entries in the parity table above are PRESENT-ELSEWHERE, not delegated.
- **Constant-time `uint8ArraysEqual`.** The cryptographic-grade comparison stays in `@suaveplan/crypto-utils::constant-time` to keep the dependency direction one-way (utils → no crypto). The `uint8-array.md` doc cross-links to it.
- **Streaming / chunked encoding.** Application-level callers wanting backpressure-aware streaming use `@suaveplan/streams` (not yet on genesis); when it lands, a thin `streamHexEncode` adapter can re-export from there. No streaming work in this change.
- **`InputType = ArrayBuffer` overload.** Legacy accepts `ArrayBuffer` indirectly via a private detector. Genesis narrows the public type to `string | Uint8Array`; callers with `ArrayBuffer` should `new Uint8Array(buf)` before passing. Documented in `uint8-array.md`.
- **UUID v1 / v3 / v5 / v7.** Only v4 generation is in scope. Other versions belong in a future `@suaveplan/uuid` package or are obtainable from `crypto.randomUUID` + format conversion already.
## Parity Checklist
Every legacy export traced, with disposition.
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `bytesToHex` | ✅ | ✅ | Verbatim port; default permissive (matches legacy), strict mode opt-in. |
| `hexToBytes` | ✅ | ✅ | Throws on empty input + non-hex; matches legacy error message. |
| `bytesToBase64` | ✅ | ✅ | `btoa` browser / `Buffer` Node fallback. |
| `base64ToBytes` | ✅ | ✅ | `atob` browser / `Buffer` Node fallback. |
| `utf8ToBytes` / `bytesToUtf8` | ✅ | ✅ | Native `TextEncoder` / `TextDecoder`. |
| `_detectFormat` (private) | ✅ | 🚫 | Replaced by `detectInputFormat` (the public version). Internal helper not needed once `ArrayBuffer` overload is dropped. |
| `detectInputFormat` | ✅ | ✅ | Public API stays. |
| `validateInputTypeFormat` | ✅ | ✅ | Throws via `UtilsError` instead of plain `Error`. |
| `inputToUint8Array` | ✅ | ✅ | Verbatim. |
| `formatOutput` | ✅ | ✅ | Verbatim. |
| `convertDataFormat` | ✅ | ✅ | Verbatim. |
| `uint8ArrayToUuid` | ✅ | ✅ | Backed by `uuid` catalog. |
| `uuidToUint8Array` | ✅ | ✅ | Backed by `uuid` catalog. |
| `generateUuid` | ✅ | ⬆️ | Enhanced: injectable `Rng`, defaults to `crypto.getRandomValues`. Genesis bans module-scope `Math.random()`. |
| `uint8ArraysEqual` | ✅ | ✅ | Non-constant-time (matches legacy). Crypto-grade comparison delegated to `@suaveplan/crypto-utils`. |
| `concatUint8Arrays` | ✅ | ✅ | Same dual signature (varargs OR single array). `UtilsError` instead of plain `Error`. |
| `sliceUint8Array` | ✅ | ✅ | `UtilsError` instead of plain `Error`. |
| `DataFormat` / `InputType` types | ✅ | ✅ | Co-located in `uint8-array.types.ts`. |
| `countries/*` (3021 L) | ✅ | 🚫 | PRESENT-ELSEWHERE: `@suaveplan/locale-data::countries`. Cross-publishing rejected (see Out of scope). |
| `languages/*` (474 L) | ✅ | 🚫 | PRESENT-ELSEWHERE: `@suaveplan/locale-data::languages`. Cross-publishing rejected. |
| `json/safe-json.ts` (385 L) | ✅ | 🚫 | PRESENT-ELSEWHERE: `@suaveplan/json::safe-json`. Cross-publishing rejected. |
## Source mapping
This change derives from `../suaveplan/packages/universal/utils/src/uint/`. There is no `enterprise/utils.md` for the encoding sub-feature — source mapping cites upstream source directly.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `bytesToHex` / `hexToBytes` | `packages/universal/utils/src/uint/uint.ts` | L20-L58 | Verbatim; rewire `Error` → `UtilsError` with stable code. |
| `bytesToBase64` / `base64ToBytes` | `packages/universal/utils/src/uint/uint.ts` | L62-L91 | Verbatim. |
| `utf8ToBytes` / `bytesToUtf8` | `packages/universal/utils/src/uint/uint.ts` | L95-L105 | Verbatim. |
| `detectInputFormat` | `packages/universal/utils/src/uint/uint.ts` | L134-L149 | Verbatim. |
| `validateInputTypeFormat` | `packages/universal/utils/src/uint/uint.ts` | L154-L170 | Verbatim; `UtilsError` instead of `Error`. |
| `inputToUint8Array` | `packages/universal/utils/src/uint/uint.ts` | L175-L194 | Verbatim. |
| `formatOutput` | `packages/universal/utils/src/uint/uint.ts` | L199-L216 | Verbatim. |
| `convertDataFormat` | `packages/universal/utils/src/uint/uint.ts` | L225-L252 | Verbatim. |
| `uint8ArrayToUuid` / `uuidToUint8Array` | `packages/universal/utils/src/uint/uint.ts` | L257-L271 | Verbatim; backed by `uuid` catalog dep. |
| `generateUuid` | `packages/universal/utils/src/uint/uint.ts` | L275-L278 | Enhanced: injectable `Rng`. |
| `uint8ArraysEqual` | `packages/universal/utils/src/uint/uint.ts` | L283-L289 | Verbatim. |
| `concatUint8Arrays` | `packages/universal/utils/src/uint/uint.ts` | L294-L322 | Verbatim. |
| `sliceUint8Array` | `packages/universal/utils/src/uint/uint.ts` | L330-L340 | Verbatim. |
| `DataFormat` / `InputType` types | `packages/universal/utils/src/uint/uint.types.ts` | L1-L2 | Verbatim. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `UtilsError` codes for `UINT8_ARRAY.INVALID_HEX`, `UINT8_ARRAY.INVALID_FORMAT`, `UINT8_ARRAY.INVALID_ARGUMENT`, `UINT8_ARRAY.OUT_OF_BOUNDS` | Genesis-wide policy that every thrown error route through `@suaveplan/error` (CLAUDE.md §Workflow rule 2). Legacy throws plain `Error`. |
| Injectable `Rng` for `generateUuid` | Genesis-wide determinism rule; module-scope `Math.random()` is banned. |
| Zod schemas for `DataFormat` and `convertDataFormat` options | Genesis-wide policy (CLAUDE.md "Zod schemas wired at every declared boundary"). |
| Module renamed from `uint/` to `uint8-array/` | The legacy name suggested typed-integer constructors (`uint8(n)`/`uint16(n)`/...) which it never delivered. The genesis name describes what the module actually does. The 2026-05-02 audit explicitly flagged this confusion. |
## Verification approach
- `src/uint8-array/uint8-array.test.ts` — every helper round-trip; error scenarios for invalid hex, invalid format, out-of-bounds slice; equality false on length mismatch; concat with mixed input shapes; UUID 16-byte validation.
- `src/uint8-array/uint8-array.spec-conformance.test.ts` — `UtilsError` codes thrown from production paths; injected `Rng` used by `generateUuid`; Zod schemas reject invalid formats; subpath `./uint8-array` resolves and tree-shakes.
## Impact
### Affected dependents
`@suaveplan/utils` currently has zero downstream packages on genesis (verified by `bunx turbo ls --filter=...@suaveplan/utils`). This is purely additive surface area — the change cannot break anything that doesn't yet exist.
- No dependents (verified by `grep -rln "@suaveplan/utils" /home/ubuntu/web/genesis/packages/ --include=package.json` returning only `packages/core/utils/package.json` itself).
When new consumers land, they import `@suaveplan/utils/uint8-array` or `@suaveplan/utils` (root barrel). Existing imports of `@suaveplan/utils/debug`, `./object`, etc. continue to work — this change adds a subpath, never removes one.
## Migration impact
None — additive only. No public surface is removed or renamed.

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

View File

@@ -0,0 +1,73 @@
# Tasks — Utils completion (Uint8Array helpers)
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
## 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.
## Audit Note (2026-05-02)
Status: **DEFERRED**. The entire `uint8-array/` feature module is missing from `packages/core/utils/src/` (existing dirs: `debug`, `email`, `errors`, `number`, `object`, `platform`, `random`, `sound`, `string`, `types`, `validate`, `validation`). 0% of work is done. All tasks below remain accurate.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/packages/universal/utils/src/uint/uint.ts` end-to-end
- [ ] 1.2 Read `openspec/specs/utils/spec.md`
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
- [ ] 1.6 Draft `specs/utils/spec.md` from `openspec/templates/spec-delta.md`
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
- [ ] 1.8 Confirm parity table: PRESENT-ELSEWHERE for `countries` / `languages` / `safe-json` is documented and not silently re-introduced
## 2. Implementation
- [ ] 2.1 Add `uuid` to root `package.json::catalog` if not already present; reference via `catalog:` from `@suaveplan/utils`
- [ ] 2.2 Add `UtilsError` codes (`UINT8_ARRAY.INVALID_HEX`, `INVALID_FORMAT`, `INVALID_ARGUMENT`, `OUT_OF_BOUNDS`) under `src/errors/`
- [ ] 2.3 Create `src/uint8-array/` feature module with `uint8-array.ts`, `uint8-array.types.ts`, `uint8-array.schemas.ts`, `uint8-array.md`, `index.ts`
- [ ] 2.4 Implement `bytesToHex` / `hexToBytes` / `bytesToBase64` / `base64ToBytes` / `utf8ToBytes` / `bytesToUtf8` per spec delta
- [ ] 2.5 Implement `detectInputFormat`, `validateInputTypeFormat`, `inputToUint8Array`, `formatOutput`, `convertDataFormat`
- [ ] 2.6 Implement `uint8ArrayToUuid`, `uuidToUint8Array`, `generateUuid` (with injectable `Rng` from `@suaveplan/types`)
- [ ] 2.7 Implement `uint8ArraysEqual`, `concatUint8Arrays`, `sliceUint8Array`
- [ ] 2.8 Add subpath export `./uint8-array` to `package.json::exports`
- [ ] 2.9 Re-export the public surface from the root barrel `src/index.ts`
## 3. Tests
- [ ] 3.1 Unit tests at `src/uint8-array/uint8-array.test.ts` covering every helper, every error path, and every round-trip
- [ ] 3.2 Spec-conformance tests at `src/uint8-array/uint8-array.spec-conformance.test.ts` pinning `UtilsError` codes, injectable `Rng` consumption, Zod schema enforcement, subpath export resolution
- [ ] 3.3 Property-based tests via `fast-check` for `bytesToHex` ↔ `hexToBytes` and `bytesToBase64` ↔ `base64ToBytes` round-trips
- [ ] 3.4 Every `it(...)` carries `{ timeout: 5000 }`
## 4. Documentation
- [ ] 4.1 Sibling `uint8-array.md` (≥200 words, basic + advanced examples, full API reference, cross-link to `@suaveplan/crypto-utils::constant-time` for cryptographic equality)
- [ ] 4.2 README links `./uint8-array/uint8-array.md` under `## Modules`
- [ ] 4.3 Update `CLAUDE.md` `@suaveplan/utils` entry to mention the new `uint8Array` capability
## 5. Pre-archive verification (the 5-gate triple)
- [ ] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/utils` — green
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/utils` — 100/100/100/100
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/utils` — exit 0, no `error` findings
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/utils` — green
## 6. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 6.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim (`DataFormat` union exact-type pinned)
- [ ] 6.2 Zod schemas wired at every declared boundary (`DataFormat`, `convertDataFormat` options)
- [ ] 6.3 Every error code in `error-codes.ts` thrown by at least one production path
- [ ] 6.4 Strict-mode behaviors tested (e.g. permissive vs strict hex parsing)
- [ ] 6.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
- [ ] 6.6 Lifecycle dedup — N/A (no caches in this module)
- [ ] 6.7 No retroactive tick — every box above reflects work done in this change
## 7. Archive
- [ ] 7.1 Fold spec delta into `openspec/specs/utils/spec.md`
- [ ] 7.2 Move `openspec/changes/utils-completion/` → `openspec/archive/<yyyy-mm-dd>-utils-completion/`
- [ ] 7.3 Refresh `MANIFEST.md` if dep set changed (catalog `uuid` add)