# Utils Migration Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Land four new genesis packages — `@suaveplan/bytes`, `@suaveplan/json`, `@suaveplan/locale-data`, and `@suaveplan/utils` — by migrating and rewriting the relevant modules from `@suaveplan/utils` on main, then open a follow-up OpenSpec to refactor `@suaveplan/crypto-utils` to consume `@suaveplan/bytes`. **Architecture:** Each package is an independent leaf (or near-leaf) following the genesis enterprise pattern established by `@suaveplan/pseudo-random`: `SuaveplanError` + `defineCodes` from `@suaveplan/error`, Zod schemas, OTel via `@opentelemetry/api`, fast-check property tests, mitata benchmarks, 100% Istanbul coverage, one subpath export per feature module. Build in sequence: bytes → json → locale-data → utils (all share the same dep set; none block the next, but bytes ships first to unblock the later crypto-utils refactor). **Tech Stack:** Bun, TypeScript, Vite + vite-plugin-dts, Vitest + istanbul, fast-check, mitata, Zod, `@opentelemetry/api`, `@suaveplan/error`, `@suaveplan/types` **Reference package:** `universal/random/pseudo-random` — copy its `package.json`, `vite.config.ts`, `vitest.config.ts`, `tsconfig.lib.json` and adapt (package name, cache dir, externals). Error class pattern: `universal/random/pseudo-random/src/errors/pseudo-random-error.ts`. **Source material:** Original implementations at `/mnt/s/web/suaveplan/packages/universal/utils/src/` — read each file before implementing its genesis equivalent. --- ## Chunk 1: `@suaveplan/bytes` **Path:** `universal/primitives/bytes` ### File map ```text universal/primitives/bytes/ ├── package.json ├── tsconfig.lib.json ├── vite.config.ts ├── vitest.config.ts ├── README.md └── src/ ├── index.ts # root barrel ├── types/ │ ├── index.ts │ └── types.ts # InputFormat, OutputFormat, BytesInput ├── errors/ │ ├── index.ts │ └── bytes-error.ts # BytesError + BYTES_CODES ├── encoding/ │ ├── index.ts │ ├── encoding.ts # bytesToHex, hexToBytes, bytesToBase64, base64ToBytes, bytesToUtf8, utf8ToBytes │ ├── encoding.test.ts │ ├── encoding.schemas.ts │ └── encoding.md ├── uint8/ │ ├── index.ts │ ├── uint8.ts # concatUint8Arrays, sliceUint8Array, uint8ArraysEqual, inputToUint8Array, detectInputFormat, validateInputTypeFormat, formatOutput, convertDataFormat │ ├── uint8.test.ts │ ├── uint8.schemas.ts │ └── uint8.md └── uuid/ ├── index.ts ├── uuid.ts # uint8ArrayToUuid, uuidToUint8Array, generateUuid ├── uuid.test.ts ├── uuid.schemas.ts └── uuid.md ``` --- ### Task 1.1: OpenSpec change + package scaffold - [ ] Create `openspec/changes/enterprise-bytes/proposal.md` (why: canonical byte encoding package, unblocks crypto-utils refactor; what: encoding + uint8 + uuid modules; impact: new package, follow-up to update crypto-utils) - [ ] Create `openspec/changes/enterprise-bytes/tasks.md` (link to this plan) - [ ] Create `universal/primitives/bytes/` directory - [ ] Copy `universal/random/pseudo-random/package.json` → `universal/primitives/bytes/package.json`, then edit: - `name`: `"@suaveplan/bytes"` - `description`: `"Byte encoding, format conversion, and UUID utilities"` - Keep `@suaveplan/error workspace:^` and `@suaveplan/types workspace:^` in dependencies - Add `@opentelemetry/api`: `"catalog:observability"` to dependencies - Exports: `"."`, `"./encoding"`, `"./uint8"`, `"./uuid"`, `"./errors"`, `"./schemas"` - [ ] Copy `universal/random/pseudo-random/tsconfig.lib.json` → `universal/primitives/bytes/tsconfig.lib.json` (no changes needed) - [ ] Copy `universal/random/pseudo-random/vite.config.ts` → `universal/primitives/bytes/vite.config.ts`; update `cacheDir` to `"../../../node_modules/.vite/packages/bytes"`; add `@opentelemetry/api`, `@suaveplan/error`, `@suaveplan/types` to the `external` array - [ ] Copy `universal/random/pseudo-random/vitest.config.ts` → `universal/primitives/bytes/vitest.config.ts`; update coverage thresholds to **100% on all four** (`statements: 100, branches: 100, functions: 100, lines: 100`); remove `setupFiles` if present (bytes has no polyfill needs) - [ ] Add `"universal/primitives/bytes"` to the root `package.json` workspaces array - [ ] Run `bun install` to register the new workspace - [ ] Commit: `"chore(bytes): scaffold package"` --- ### Task 1.2: Error class + types - [ ] Read `universal/random/pseudo-random/src/errors/pseudo-random-error.ts` for the pattern - [ ] Create `src/errors/bytes-error.ts`: ```ts import { defineCodes, SuaveplanError, type SuaveplanErrorOptions } from "@suaveplan/error"; export const BYTES_CODES = defineCodes("BYTES", [ "ENCODE.INVALID_HEX", "ENCODE.INVALID_BASE64", "ENCODE.INVALID_UTF8", "UINT8.INVALID_INPUT", "UINT8.FORMAT_MISMATCH", "UUID.INVALID_LENGTH", "UUID.INVALID_FORMAT", ] as const); export type BytesCode = (typeof BYTES_CODES)[keyof typeof BYTES_CODES]; export class BytesError extends SuaveplanError { // biome-ignore lint/complexity/noUselessConstructor: widens visibility public constructor( code: BytesCode, message: string, options?: SuaveplanErrorOptions ) { super(code, message, options); } } ``` - [ ] Create `src/errors/index.ts` (barrel re-exporting `BytesError`, `BYTES_CODES`, `BytesCode`) - [ ] Create `src/types/types.ts`: ```ts export type InputFormat = "hex" | "base64" | "utf8" | "uint8array"; export type OutputFormat = "hex" | "base64" | "utf8" | "uint8array"; export type BytesInput = string | Uint8Array; ``` - [ ] Create `src/types/index.ts` (barrel) - [ ] Create `src/index.ts` (top-level barrel re-exporting from all feature modules) - [ ] Commit: `"feat(bytes): add error class and types"` --- ### Task 1.3: `encoding` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/uint/uint.ts` for the original implementation - [ ] Create `src/encoding/encoding.test.ts` with failing tests: ```ts import { describe, expect, it } from "vitest"; import { base64ToBytes, bytesToBase64, bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from "./encoding.js"; describe("bytesToHex / hexToBytes", () => { it("round-trips arbitrary bytes", { timeout: 5000 }, () => { const input = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); expect(hexToBytes(bytesToHex(input))).toEqual(input); }); it("bytesToHex produces lowercase hex", { timeout: 5000 }, () => { expect(bytesToHex(new Uint8Array([255]))).toBe("ff"); }); it("hexToBytes rejects odd-length string", { timeout: 5000 }, () => { expect(() => hexToBytes("abc")).toThrow(); }); it("hexToBytes rejects non-hex chars", { timeout: 5000 }, () => { expect(() => hexToBytes("zz")).toThrow(); }); }); describe("bytesToBase64 / base64ToBytes", () => { it("round-trips arbitrary bytes", { timeout: 5000 }, () => { const input = new Uint8Array([1, 2, 3, 4, 5]); expect(base64ToBytes(bytesToBase64(input))).toEqual(input); }); it("rejects invalid base64", { timeout: 5000 }, () => { expect(() => base64ToBytes("not!valid@base64")).toThrow(); }); }); describe("bytesToUtf8 / utf8ToBytes", () => { it("round-trips ASCII", { timeout: 5000 }, () => { const text = "Hello, world!"; expect(bytesToUtf8(utf8ToBytes(text))).toBe(text); }); it("round-trips multibyte characters", { timeout: 5000 }, () => { const text = "こんにちは"; expect(bytesToUtf8(utf8ToBytes(text))).toBe(text); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect FAIL (no implementation) - [ ] Create `src/encoding/encoding.ts` implementing all 6 functions using native `TextEncoder`/`TextDecoder` and `btoa`/`atob`; throw `BytesError` with appropriate codes on invalid input; add OTel span per function - [ ] Create `src/encoding/encoding.schemas.ts` (Zod schemas for hex string, base64 string, utf8 string) - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect PASS - [ ] Create `src/encoding/index.ts` (barrel) - [ ] Create `src/encoding/encoding.md` (≥200 words: Purpose, Features, Basic Example, Advanced Example, API Reference, Implementation Notes) — must be created alongside the implementation, not deferred - [ ] Commit: `"feat(bytes): add encoding module (hex/base64/utf8)"` --- ### Task 1.4: `uint8` module (TDD) - [ ] Create `src/uint8/uint8.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { concatUint8Arrays, convertDataFormat, detectInputFormat, sliceUint8Array, uint8ArraysEqual } from "./uint8.js"; describe("concatUint8Arrays", () => { it("concatenates multiple arrays", { timeout: 5000 }, () => { const a = new Uint8Array([1, 2]); const b = new Uint8Array([3, 4]); const c = new Uint8Array([5]); expect(concatUint8Arrays(a, b, c)).toEqual(new Uint8Array([1, 2, 3, 4, 5])); }); it("handles empty arrays", { timeout: 5000 }, () => { expect(concatUint8Arrays(new Uint8Array(), new Uint8Array([1]))).toEqual(new Uint8Array([1])); }); }); describe("sliceUint8Array", () => { it("slices correctly", { timeout: 5000 }, () => { const arr = new Uint8Array([1, 2, 3, 4, 5]); expect(sliceUint8Array(arr, 1, 3)).toEqual(new Uint8Array([2, 3])); }); }); describe("uint8ArraysEqual", () => { it("returns true for equal arrays", { timeout: 5000 }, () => { expect(uint8ArraysEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true); }); it("returns false for different arrays", { timeout: 5000 }, () => { expect(uint8ArraysEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]))).toBe(false); }); it("returns false for different lengths", { timeout: 5000 }, () => { expect(uint8ArraysEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3]))).toBe(false); }); }); describe("detectInputFormat", () => { it("detects Uint8Array", { timeout: 5000 }, () => { expect(detectInputFormat(new Uint8Array())).toBe("uint8array"); }); it("detects hex string", { timeout: 5000 }, () => { expect(detectInputFormat("48656c6c6f")).toBe("hex"); }); }); describe("convertDataFormat", () => { it("converts hex to utf8", { timeout: 5000 }, () => { expect(convertDataFormat("48656c6c6f", "utf8", "hex")).toBe("Hello"); }); it("converts utf8 to hex", { timeout: 5000 }, () => { expect(convertDataFormat("Hello", "hex", "utf8")).toBe("48656c6c6f"); }); it("converts Uint8Array to hex", { timeout: 5000 }, () => { expect(convertDataFormat(new Uint8Array([72, 101, 108, 108, 111]), "hex")).toBe("48656c6c6f"); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect FAIL - [ ] Implement `src/uint8/uint8.ts` (all 8 exports + OTel spans; use encoding module internally) - [ ] Create `src/uint8/uint8.schemas.ts` and `src/uint8/index.ts` - [ ] Create `src/uint8/uint8.md` (≥200 words — same structure as encoding.md) - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect PASS - [ ] Commit: `"feat(bytes): add uint8 module"` --- ### Task 1.5: `uuid` module (TDD) - [ ] Create `src/uuid/uuid.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { generateUuid, uint8ArrayToUuid, uuidToUint8Array } from "./uuid.js"; describe("uuid round-trip", () => { it("round-trips a known UUID", { timeout: 5000 }, () => { const uuid = "550e8400-e29b-41d4-a716-446655440000"; expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid); }); it("uuidToUint8Array produces 16 bytes", { timeout: 5000 }, () => { expect(uuidToUint8Array("550e8400-e29b-41d4-a716-446655440000")).toHaveLength(16); }); it("throws on invalid UUID", { timeout: 5000 }, () => { expect(() => uuidToUint8Array("not-a-uuid")).toThrow(); }); }); describe("generateUuid", () => { it("generates a valid UUID v4 string", { timeout: 5000 }, () => { const uuid = generateUuid(); expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); }); it("generates unique values", { timeout: 5000 }, () => { expect(generateUuid()).not.toBe(generateUuid()); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect FAIL - [ ] Implement `src/uuid/uuid.ts` using native `crypto.randomUUID()` for `generateUuid`; parse/serialise with hex arithmetic (no external `uuid` package) - [ ] Create `src/uuid/uuid.schemas.ts` (Zod UUID string schema) and `src/uuid/index.ts` - [ ] Create `src/uuid/uuid.md` (≥200 words — same structure as encoding.md) - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect PASS - [ ] Commit: `"feat(bytes): add uuid module"` --- ### Task 1.6: Property tests (fast-check) - [ ] Add fast-check property tests to `src/encoding/encoding.test.ts`: ```ts import fc from "fast-check"; it("hex round-trip for arbitrary bytes", { timeout: 30000 }, () => { fc.assert(fc.property(fc.uint8Array(), (bytes) => { expect(hexToBytes(bytesToHex(bytes))).toEqual(bytes); })); }); it("base64 round-trip for arbitrary bytes", { timeout: 30000 }, () => { fc.assert(fc.property(fc.uint8Array(), (bytes) => { expect(base64ToBytes(bytesToBase64(bytes))).toEqual(bytes); })); }); it("utf8 round-trip for arbitrary strings", { timeout: 30000 }, () => { fc.assert(fc.property(fc.string(), (str) => { expect(bytesToUtf8(utf8ToBytes(str))).toBe(str); })); }); ``` - [ ] Add to `src/uint8/uint8.test.ts`: ```ts it("concatUint8Arrays associativity", { timeout: 30000 }, () => { fc.assert(fc.property(fc.uint8Array(), fc.uint8Array(), fc.uint8Array(), (a, b, c) => { const left = concatUint8Arrays(concatUint8Arrays(a, b), c); const right = concatUint8Arrays(a, concatUint8Arrays(b, c)); expect(left).toEqual(right); })); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/bytes` — expect PASS - [ ] Commit: `"test(bytes): add fast-check property tests"` --- ### Task 1.7: Benchmarks - [ ] Create `src/__benchmarks__/encoding.bench.ts`: ```ts import { bench, run } from "mitata"; import { bytesToBase64, bytesToHex, hexToBytes } from "../encoding/encoding.js"; const small = new Uint8Array(32); const large = new Uint8Array(65536); crypto.getRandomValues(small); crypto.getRandomValues(large); bench("bytesToHex (32 bytes)", () => bytesToHex(small)); bench("bytesToHex (64 KB)", () => bytesToHex(large)); bench("hexToBytes (32 bytes)", () => hexToBytes(bytesToHex(small))); bench("bytesToBase64 (32 bytes)", () => bytesToBase64(small)); bench("bytesToBase64 (64 KB)", () => bytesToBase64(large)); await run(); ``` - [ ] Run `bun src/__benchmarks__/encoding.bench.ts` to capture baselines; paste output into `README.md` benchmarks section - [ ] Commit: `"bench(bytes): add encoding benchmarks"` --- ### Task 1.8: README + docs check > Module `.md` files are created alongside each TDD task (1.3–1.5). This task adds the package-level README and verifies 100% docs coverage. - [ ] Create `README.md` at `universal/primitives/bytes/README.md` with: description, install, quick-start, Modules table linking each `.md`, Benchmarks section - [ ] Run `bun run docs:check --filter=@suaveplan/bytes` — must be 100%; fix any gaps before continuing - [ ] Commit: `"docs(bytes): add README"` --- ### Task 1.9: Pre-publish gate + publish - [ ] Run `bunx turbo lint typecheck test --filter=@suaveplan/bytes` — must be green - [ ] Run `bunx turbo test:coverage --filter=@suaveplan/bytes` — must be 100% all thresholds - [ ] Fix any issues before continuing - [ ] Run `bun scripts/publish.ts --package @suaveplan/bytes` - [ ] Archive: move `openspec/changes/enterprise-bytes/` → `openspec/archive/2026-04-20-enterprise-bytes/`; update `openspec/specs/bytes/spec.md` with final capability description - [ ] Commit: `"release(bytes): v0.0.0 — publish to Verdaccio"` --- ## Chunk 2: `@suaveplan/json` **Path:** `universal/data/json` ### File map ```text universal/data/json/ ├── package.json ├── tsconfig.lib.json ├── vite.config.ts ├── vitest.config.ts ├── README.md └── src/ ├── index.ts ├── types/ │ ├── index.ts │ └── types.ts # ParseOptions, StringifyOptions ├── errors/ │ ├── index.ts │ └── json-error.ts # JsonError + JSON_CODES └── safe-json/ ├── index.ts ├── safe-json.ts # safeParse, safeStringify, tryParse, isValidJSON, getStringifySize ├── safe-json.test.ts ├── safe-json.schemas.ts └── safe-json.md ``` --- ### Task 2.1: OpenSpec + scaffold - [ ] Create `openspec/changes/enterprise-json/proposal.md` + `tasks.md` - [ ] Scaffold `universal/data/json/` — copy configs from `universal/primitives/bytes/` and adapt (name `@suaveplan/json`, exports: `"."`, `"./safe-json"`, `"./errors"`) - [ ] Register workspace in root `package.json`, run `bun install` - [ ] Commit: `"chore(json): scaffold package"` --- ### Task 2.2: Error class + types - [ ] Create `src/errors/json-error.ts`: ```ts import { defineCodes, SuaveplanError, type SuaveplanErrorOptions } from "@suaveplan/error"; export const JSON_CODES = defineCodes("JSON", [ "PARSE.FAILED", "PARSE.PROTOTYPE_POLLUTION", "STRINGIFY.CIRCULAR", "STRINGIFY.FAILED", ] as const); export type JsonCode = (typeof JSON_CODES)[keyof typeof JSON_CODES]; export class JsonError extends SuaveplanError { // biome-ignore lint/complexity/noUselessConstructor: widens visibility public constructor( code: JsonCode, message: string, options?: SuaveplanErrorOptions ) { super(code, message, options); } } ``` - [ ] Create `src/types/types.ts`: ```ts export interface ParseOptions { /** Keys to reject (default: ["__proto__", "constructor", "prototype"]) */ forbiddenKeys?: string[]; /** Max nesting depth before throwing (default: 50) */ maxDepth?: number; /** AbortSignal to cancel large-payload parsing */ signal?: AbortSignal; } export interface StringifyOptions { space?: number; /** AbortSignal to cancel large-payload serialisation */ signal?: AbortSignal; /** Max circular reference depth before throwing (default: 100) */ maxDepth?: number; } ``` - [ ] Create barrels for `errors/` and `types/` - [ ] Commit: `"feat(json): add error class and types"` --- ### Task 2.3: `safe-json` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/json/safe-json.ts` for original implementation - [ ] Create `src/safe-json/safe-json.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { getStringifySize, isValidJSON, JsonError, safeParse, safeStringify, tryParse } from "./safe-json.js"; describe("safeParse", () => { it("parses valid JSON", { timeout: 5000 }, () => { expect(safeParse('{"a":1}')).toEqual({ a: 1 }); }); it("rejects __proto__ key", { timeout: 5000 }, () => { expect(() => safeParse('{"__proto__":{"x":1}}')).toThrow(JsonError); }); it("rejects constructor key", { timeout: 5000 }, () => { expect(() => safeParse('{"constructor":{"x":1}}')).toThrow(JsonError); }); it("throws on invalid JSON", { timeout: 5000 }, () => { expect(() => safeParse("not json")).toThrow(JsonError); }); it("allows custom forbidden keys", { timeout: 5000 }, () => { // With empty forbiddenKeys, __proto__ is allowed expect(() => safeParse('{"__proto__":{}}', { forbiddenKeys: [] })).not.toThrow(); }); }); describe("safeStringify", () => { it("serialises a plain object", { timeout: 5000 }, () => { expect(safeStringify({ a: 1 })).toBe('{"a":1}'); }); it("throws on circular reference", { timeout: 5000 }, () => { const obj: Record = {}; obj.self = obj; expect(() => safeStringify(obj)).toThrow(JsonError); }); }); describe("tryParse", () => { it("returns undefined on invalid JSON", { timeout: 5000 }, () => { expect(tryParse("bad")).toBeUndefined(); }); it("returns parsed value on valid JSON", { timeout: 5000 }, () => { expect(tryParse("42")).toBe(42); }); }); describe("isValidJSON", () => { it("returns true for valid JSON", { timeout: 5000 }, () => { expect(isValidJSON('{"x":1}')).toBe(true); }); it("returns false for invalid JSON", { timeout: 5000 }, () => { expect(isValidJSON("not json")).toBe(false); }); }); describe("getStringifySize", () => { it("returns byte size of serialised value", { timeout: 5000 }, () => { const value = { hello: "world" }; const size = getStringifySize(value); expect(size).toBe(new TextEncoder().encode(JSON.stringify(value)).byteLength); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/json` — expect FAIL - [ ] Implement `src/safe-json/safe-json.ts`: `safeParse` uses a reviver to detect forbidden keys; `safeStringify` tracks seen objects to detect circular refs; `tryParse` wraps `safeParse` in try/catch; `isValidJSON` wraps `tryParse`; `getStringifySize` uses `TextEncoder.encode(JSON.stringify(v)).byteLength`; add OTel span on `safeParse` and `safeStringify` - [ ] Create `src/safe-json/safe-json.schemas.ts` and `src/safe-json/index.ts` - [ ] Run `bunx turbo test --filter=@suaveplan/json` — expect PASS - [ ] Commit: `"feat(json): implement safe-json module"` --- ### Task 2.4: Property tests - [ ] Add to `src/safe-json/safe-json.test.ts`: ```ts import fc from "fast-check"; it("safeParse round-trips any JSON-serialisable value", { timeout: 30000 }, () => { fc.assert(fc.property(fc.jsonValue(), (value) => { expect(safeParse(JSON.stringify(value))).toEqual(value); })); }); it("safeStringify + safeParse is identity for clean objects", { timeout: 30000 }, () => { // Use only objects without forbidden keys fc.assert(fc.property( fc.record({ a: fc.integer(), b: fc.string() }), (obj) => { expect(safeParse(safeStringify(obj))).toEqual(obj); } )); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/json` — expect PASS - [ ] Commit: `"test(json): add fast-check property tests"` --- ### Task 2.5: Benchmarks + docs + pre-publish - [ ] Create `src/__benchmarks__/safe-json.bench.ts` benchmarking `safeParse` and `safeStringify` vs native `JSON.parse`/`JSON.stringify` for small, medium, and large payloads; paste baselines into README - [ ] Create `src/safe-json/safe-json.md` (≥200 words) - [ ] Create `README.md` - [ ] Run `bun run docs:check --filter=@suaveplan/json` — must be 100% - [ ] Run `bunx turbo lint typecheck test --filter=@suaveplan/json` — must be green - [ ] Run `bunx turbo test:coverage --filter=@suaveplan/json` — must be 100% - [ ] Run `bun scripts/publish.ts --package @suaveplan/json` - [ ] Archive `openspec/changes/enterprise-json/` → `openspec/archive/2026-04-20-enterprise-json/` - [ ] Commit: `"release(json): v0.0.0 — publish to Verdaccio"` --- ## Chunk 3: `@suaveplan/locale-data` **Path:** `universal/i18n/locale-data` ### File map ```text universal/i18n/locale-data/ ├── package.json ├── tsconfig.lib.json ├── vite.config.ts ├── vitest.config.ts ├── README.md └── src/ ├── index.ts ├── types/ │ ├── index.ts │ └── types.ts # CountryItem, LanguageItem ├── errors/ │ ├── index.ts │ └── locale-data-error.ts # LocaleDataError + LOCALE_DATA_CODES ├── countries/ │ ├── index.ts │ ├── countries.ts # getCountry, getAllCountries, getCountriesByContinent, getCountriesByCurrency, country records │ ├── countries.test.ts │ ├── countries.schemas.ts │ └── countries.md └── languages/ ├── index.ts ├── languages.ts # getLanguages, getLanguage, getRtlLanguages, isRtlLanguage, language records ├── languages.test.ts ├── languages.schemas.ts └── languages.md ``` > **Note:** The raw country and language data is bundled directly in `countries.ts` and `languages.ts` (not split into separate `.data.ts` files) since it's static and tree-shaken at the subpath export level. If either file exceeds the 300-line hard limit, split data into a sibling `countries.data.ts` / `languages.data.ts`. --- ### Task 3.1: OpenSpec + scaffold - [ ] Create `openspec/changes/enterprise-locale-data/proposal.md` + `tasks.md` - [ ] Scaffold `universal/i18n/locale-data/` — copy configs, adapt (name `@suaveplan/locale-data`, exports: `"."`, `"./countries"`, `"./languages"`, `"./errors"`) - [ ] Keep `@opentelemetry/api: "catalog:observability"` in deps — add a counter on `getCountry` and `getLanguage` calls (not-found vs. found) so consumers can observe lookup patterns. Deps: `@suaveplan/error`, `@suaveplan/types`, `zod`, `tslib`, `@opentelemetry/api`. - [ ] Register workspace + `bun install` - [ ] Commit: `"chore(locale-data): scaffold package"` --- ### Task 3.2: Error class + types - [ ] Create `src/errors/locale-data-error.ts`: ```ts import { defineCodes, SuaveplanError, type SuaveplanErrorOptions } from "@suaveplan/error"; export const LOCALE_DATA_CODES = defineCodes("LOCALE_DATA", [ "COUNTRY.NOT_FOUND", "LANGUAGE.NOT_FOUND", ] as const); export type LocaleDataCode = (typeof LOCALE_DATA_CODES)[keyof typeof LOCALE_DATA_CODES]; export class LocaleDataError extends SuaveplanError { // biome-ignore lint/complexity/noUselessConstructor: widens visibility public constructor( code: LocaleDataCode, message: string, options?: SuaveplanErrorOptions ) { super(code, message, options); } } ``` - [ ] Create `src/types/types.ts`: ```ts import z from "zod"; export const CountryItemSchema = z.object({ name: z.string(), native: z.string(), phone: z.string(), continent: z.string(), capital: z.string(), currency: z.string(), languages: z.array(z.string()), emoji: z.string(), emojiU: z.string(), }); export type CountryItem = z.infer; export const LanguageItemSchema = z.object({ name: z.string(), native: z.string(), }); export type LanguageItem = z.infer; ``` - [ ] Create barrels, commit: `"feat(locale-data): add error class and types"` --- ### Task 3.3: `countries` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/countries/countries.ts` — copy all country records (this is static ISO data, no rewriting needed; verify each has the correct `CountryItem` shape) - [ ] Create `src/countries/countries.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { getAllCountries, getCountriesByContinent, getCountriesByCurrency, getCountry } from "./countries.js"; describe("getCountry", () => { it("returns a known country by ISO code", { timeout: 5000 }, () => { const us = getCountry("US"); expect(us.name).toBe("United States"); expect(us.phone).toBe("1"); }); it("throws LocaleDataError for unknown code", { timeout: 5000 }, () => { expect(() => getCountry("XX")).toThrow(); }); }); describe("getAllCountries", () => { it("returns all countries as a record", { timeout: 5000 }, () => { const all = getAllCountries(); expect(Object.keys(all).length).toBeGreaterThan(200); expect(all["GB"].name).toBe("United Kingdom"); }); }); describe("getCountriesByContinent", () => { it("returns only European countries for EU", { timeout: 5000 }, () => { const eu = getCountriesByContinent("EU"); expect(eu.every((c) => c.continent === "EU")).toBe(true); expect(eu.length).toBeGreaterThan(30); }); }); describe("getCountriesByCurrency", () => { it("returns countries using EUR", { timeout: 5000 }, () => { const eur = getCountriesByCurrency("EUR"); expect(eur.some((c) => c.name === "Germany")).toBe(true); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/locale-data` — expect FAIL - [ ] Create `src/countries/countries.ts` with all country records from the source + the 4 lookup functions (`getCountry`, `getAllCountries`, `getCountriesByContinent`, `getCountriesByCurrency`); throw `LocaleDataError` with `LOCALE_DATA_CODES.COUNTRY.NOT_FOUND` for unknown codes - [ ] Create `src/countries/countries.schemas.ts` (re-export `CountryItemSchema`, define `CountryCodeSchema = z.string().length(2).toUpperCase()`) - [ ] Create `src/countries/index.ts` - [ ] Run `bunx turbo test --filter=@suaveplan/locale-data` — expect PASS - [ ] Commit: `"feat(locale-data): add countries module"` --- ### Task 3.4: `languages` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/languages/languages.ts` — copy all language + RTL records - [ ] Create `src/languages/languages.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { getLanguage, getLanguages, getRtlLanguages, isRtlLanguage } from "./languages.js"; describe("getLanguages", () => { it("returns all languages", { timeout: 5000 }, () => { const langs = getLanguages(); expect(Object.keys(langs).length).toBeGreaterThan(100); expect(langs["en"].name).toBe("English"); }); }); describe("getLanguage", () => { it("returns a known language", { timeout: 5000 }, () => { expect(getLanguage("fr").name).toBe("French"); }); it("throws for unknown language code", { timeout: 5000 }, () => { expect(() => getLanguage("xx")).toThrow(); }); }); describe("getRtlLanguages", () => { it("includes Arabic, Hebrew, Persian, Urdu", { timeout: 5000 }, () => { const rtl = getRtlLanguages(); for (const code of ["ar", "he", "fa", "ur"]) { expect(rtl).toContain(code); } }); it("all returned codes exist in language map", { timeout: 5000 }, () => { const all = getLanguages(); for (const code of getRtlLanguages()) { expect(code in all).toBe(true); } }); }); describe("isRtlLanguage", () => { it("returns true for Arabic", { timeout: 5000 }, () => { expect(isRtlLanguage("ar")).toBe(true); }); it("returns false for English", { timeout: 5000 }, () => { expect(isRtlLanguage("en")).toBe(false); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/locale-data` — expect FAIL - [ ] Implement `src/languages/languages.ts` with all records + 4 functions; throw `LocaleDataError` with `LOCALE_DATA_CODES.LANGUAGE.NOT_FOUND` for unknown codes - [ ] Create `src/languages/languages.schemas.ts`, `src/languages/index.ts` - [ ] Run `bunx turbo test --filter=@suaveplan/locale-data` — expect PASS - [ ] Commit: `"feat(locale-data): add languages module"` --- ### Task 3.5: Docs + pre-publish - [ ] Create `src/countries/countries.md` (≥200 words) - [ ] Create `src/languages/languages.md` (≥200 words) - [ ] Create `README.md` - [ ] Run `bun run docs:check --filter=@suaveplan/locale-data` — 100% - [ ] Run `bunx turbo lint typecheck test --filter=@suaveplan/locale-data` — green - [ ] Run `bunx turbo test:coverage --filter=@suaveplan/locale-data` — 100% - [ ] Run `bun scripts/publish.ts --package @suaveplan/locale-data` - [ ] Archive `openspec/changes/enterprise-locale-data/` → `openspec/archive/2026-04-20-enterprise-locale-data/` - [ ] Commit: `"release(locale-data): v0.0.0 — publish to Verdaccio"` --- ## Chunk 4: `@suaveplan/utils` (part 1 — scaffold + modules 1–5) **Path:** `universal/data/utils` ### File map ```text universal/data/utils/ ├── package.json ├── tsconfig.lib.json ├── vite.config.ts ├── vitest.config.ts ├── README.md └── src/ ├── index.ts ├── types/ │ ├── index.ts │ └── types.ts # PseudoRandomOptions, SizeLimit, etc. ├── errors/ │ ├── index.ts │ └── utils-error.ts # UtilsError + UTILS_CODES ├── debug/ │ ├── index.ts │ ├── debug.ts │ ├── debug.test.ts │ └── debug.md ├── object/ │ ├── index.ts │ ├── object.ts │ ├── object.test.ts │ ├── object.schemas.ts │ └── object.md ├── platform/ │ ├── index.ts │ ├── platform.ts │ ├── platform.test.ts │ ├── platform.schemas.ts │ └── platform.md ├── number/ │ ├── index.ts │ ├── number.ts │ ├── number.test.ts │ ├── number.schemas.ts │ └── number.md ├── random/ │ ├── index.ts │ ├── pseudo.ts │ ├── pseudo.test.ts │ ├── pseudo.schemas.ts │ └── pseudo.md ├── email/ │ ├── index.ts │ ├── email.ts │ ├── email.test.ts │ ├── email.schemas.ts │ └── email.md ├── sound/ │ ├── index.ts │ ├── sound.ts │ ├── sound.test.ts │ ├── sound.schemas.ts │ └── sound.md ├── string/ │ ├── index.ts │ ├── string.ts │ ├── string.test.ts │ ├── string.schemas.ts │ └── string.md ├── validate/ │ ├── index.ts │ ├── validate.ts │ ├── validate.test.ts │ └── validate.md └── validation/ ├── index.ts ├── size.ts ├── size.test.ts ├── size.schemas.ts └── size.md ``` Exports map in `package.json`: `"."`, `"./debug"`, `"./object"`, `"./platform"`, `"./number"`, `"./random"`, `"./email"`, `"./sound"`, `"./string"`, `"./validate"`, `"./validation"`, `"./errors"` --- ### Task 4.1: OpenSpec + scaffold - [ ] Create `openspec/changes/enterprise-utils/proposal.md` + `tasks.md` - [ ] Scaffold `universal/data/utils/` — copy configs from bytes, adapt (name `@suaveplan/utils`, no `@suaveplan/types` dep unless needed by types.ts, add `@opentelemetry/api`) - [ ] Register workspace + `bun install` - [ ] Commit: `"chore(utils): scaffold package"` --- ### Task 4.2: Error class - [ ] Create `src/errors/utils-error.ts`: ```ts import { defineCodes, SuaveplanError, type SuaveplanErrorOptions } from "@suaveplan/error"; export const UTILS_CODES = defineCodes("UTILS", [ "OBJECT.PROTOTYPE_POLLUTION", "OBJECT.CIRCULAR", "NUMBER.OUT_OF_RANGE", "RANDOM.EMPTY_ARRAY", "RANDOM.INDEX_OUT_OF_BOUNDS", "VALIDATION.SIZE_EXCEEDED", "VALIDATION.PARSE_FAILED", "PLATFORM.UNAVAILABLE", "SOUND.NO_VARIATIONS", ] as const); export type UtilsCode = (typeof UTILS_CODES)[keyof typeof UTILS_CODES]; export class UtilsError extends SuaveplanError { // biome-ignore lint/complexity/noUselessConstructor: widens visibility public constructor( code: UtilsCode, message: string, options?: SuaveplanErrorOptions ) { super(code, message, options); } } ``` - [ ] Create `src/errors/index.ts` - [ ] Commit: `"feat(utils): add error class"` --- ### Task 4.3: `debug` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/debug/debug.ts` for original implementation - [ ] Create `src/debug/debug.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { createDebug } from "./debug.js"; describe("createDebug", () => { it("logs unique messages only", { timeout: 5000 }, () => { const logs: unknown[][] = []; const dbg = createDebug({ log: (...args) => logs.push(args) }); dbg.log("hello"); dbg.log("hello"); // duplicate — suppressed dbg.log("world"); expect(logs).toHaveLength(2); expect(logs[0]).toEqual(["hello"]); expect(logs[1]).toEqual(["world"]); }); it("evicts oldest entry when buffer is full", { timeout: 5000 }, () => { const logs: unknown[][] = []; const dbg = createDebug({ log: (...args) => logs.push(args), bufferSize: 3 }); dbg.log("a"); dbg.log("b"); dbg.log("c"); dbg.log("d"); // evicts "a" dbg.log("a"); // "a" is no longer in buffer — should log expect(logs).toHaveLength(5); }); it("exports a default debug instance", { timeout: 5000 }, async () => { const { debug } = await import("./debug.js"); expect(typeof debug.log).toBe("function"); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/debug/debug.ts`: - Export `createDebug({ log?, warn?, error?, bufferSize? })` that wraps a rolling circular buffer of message signatures; delegate to console if no override provided; default `bufferSize: 10` - Export a module-level `debug` singleton (`createDebug()`) - **No OTel** — debug is an instrumentation primitive itself; adding spans would be circular - [ ] Create `src/debug/index.ts` - [ ] Create `src/debug/debug.md` (≥200 words: Purpose, Features, Basic Example, Advanced Example, API Reference, Implementation Notes) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add debug module"` --- ### Task 4.4: `object` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/object/object.ts` - [ ] Create `src/object/object.test.ts` (cover all 15 exports: `isObject`, `isPlainObject`, `isObjectOrArray`, `isEmpty`, `deepClone`, `deepMerge`, `deepMergeAll`, `pick`, `omit`, `keepKeys`, `removeKeys`, `flattenObject`, `getNestedValue`, `setNestedValue`, `hasPath`): ```ts import { describe, expect, it } from "vitest"; import { deepClone, deepMerge, deepMergeAll, flattenObject, getNestedValue, hasPath, isEmpty, isObject, isObjectOrArray, isPlainObject, keepKeys, omit, pick, removeKeys, setNestedValue } from "./object.js"; describe("isObject", () => { it("returns true for plain objects", { timeout: 5000 }, () => { expect(isObject({})).toBe(true); expect(isObject({ a: 1 })).toBe(true); }); it("returns false for null, arrays, primitives", { timeout: 5000 }, () => { expect(isObject(null)).toBe(false); expect(isObject([])).toBe(false); expect(isObject(42)).toBe(false); }); }); describe("deepMerge", () => { it("merges nested objects", { timeout: 5000 }, () => { const a = { x: { y: 1, z: 2 } }; const b = { x: { y: 99 } }; expect(deepMerge(a, b)).toEqual({ x: { y: 99, z: 2 } }); }); it("does not mutate inputs", { timeout: 5000 }, () => { const a = { x: 1 }; const b = { y: 2 }; deepMerge(a, b); expect(a).toEqual({ x: 1 }); }); }); describe("pick / omit", () => { it("pick returns only specified keys", { timeout: 5000 }, () => { expect(pick({ a: 1, b: 2, c: 3 }, ["a", "c"])).toEqual({ a: 1, c: 3 }); }); it("omit removes specified keys", { timeout: 5000 }, () => { expect(omit({ a: 1, b: 2, c: 3 }, ["b"])).toEqual({ a: 1, c: 3 }); }); }); describe("flattenObject", () => { it("flattens nested object with dot notation", { timeout: 5000 }, () => { expect(flattenObject({ a: { b: { c: 1 } } })).toEqual({ "a.b.c": 1 }); }); }); describe("getNestedValue / setNestedValue / hasPath", () => { it("gets a deeply nested value", { timeout: 5000 }, () => { expect(getNestedValue({ a: { b: { c: 42 } } }, "a.b.c")).toBe(42); }); it("returns undefined for missing path", { timeout: 5000 }, () => { expect(getNestedValue({ a: 1 }, "a.b.c")).toBeUndefined(); }); it("sets a deeply nested value", { timeout: 5000 }, () => { const obj = { a: { b: 1 } }; setNestedValue(obj, "a.b", 99); expect(obj.a.b).toBe(99); }); it("hasPath returns true when path exists", { timeout: 5000 }, () => { expect(hasPath({ a: { b: 1 } }, "a.b")).toBe(true); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/object/object.ts` with all 15 functions; no `any` — use precise generics; throw `UtilsError` with `UTILS_CODES.OBJECT.PROTOTYPE_POLLUTION` if a deep merge would pollute `__proto__`; add OTel span on `deepMerge` and `deepClone` (these are the hot paths) - [ ] Create `src/object/object.schemas.ts` and `src/object/index.ts` - [ ] Create `src/object/object.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add object module"` --- ### Task 4.5: `platform` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/platform/platform.ts` - [ ] Create `src/platform/platform.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { createPlatform, getPlatform, isBun, isBrowser, isNode } from "./platform.js"; describe("platform detection (Node/Bun environment)", () => { it("isNode returns true in this test runner", { timeout: 5000 }, () => { expect(isNode()).toBe(true); }); it("isBrowser returns false in this test runner", { timeout: 5000 }, () => { expect(isBrowser()).toBe(false); }); it("getPlatform returns 'bun' or 'node'", { timeout: 5000 }, () => { expect(["bun", "node"]).toContain(getPlatform()); }); }); describe("createPlatform (injectable adapter)", () => { it("accepts a mock adapter", { timeout: 5000 }, () => { const p = createPlatform({ getMemoryUsage: () => ({ heapUsed: 100, heapTotal: 200 }), getCpuUsage: () => ({ user: 10, system: 5 }), getPlatform: () => "browser", }); expect(p.getMemoryUsage().heapUsed).toBe(100); expect(p.getPlatform()).toBe("browser"); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/platform/platform.ts`: injectable adapter pattern (`createPlatform`); default adapter reads from `process.memoryUsage()` in Node/Bun and `performance.memory` in browser; `getPlatform`, `isNode`, `isBrowser`, `isBun` as convenience exports using the default adapter; throw `UtilsError` with `UTILS_CODES.PLATFORM.UNAVAILABLE` when an API is absent - [ ] Create `src/platform/platform.schemas.ts` (Zod schemas for `MemoryUsage`, `CpuUsage`), `src/platform/index.ts` - [ ] Create `src/platform/platform.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add platform module"` --- ### Task 4.6: `number` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/number/number.ts` - [ ] Create `src/number/number.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { isInFloat16Range, isInFloat32Range, max16, max32, min16, min32, packFloat16, packFloat32, packFloat64, unpackFloat16, unpackFloat32, unpackFloat64 } from "./number.js"; describe("Float16 round-trip", () => { it("packs and unpacks 0, 1, -1, 0.5, min16, max16", { timeout: 5000 }, () => { for (const v of [0, 1, -1, 0.5, min16, max16]) { expect(unpackFloat16(packFloat16(v))).toBeCloseTo(v, 3); } }); it("handles NaN, Infinity, -Infinity", { timeout: 5000 }, () => { expect(unpackFloat16(packFloat16(Number.NaN))).toBeNaN(); expect(unpackFloat16(packFloat16(Infinity))).toBe(Infinity); expect(unpackFloat16(packFloat16(-Infinity))).toBe(-Infinity); }); }); describe("Float32/64 round-trip", () => { it("packFloat32/unpackFloat32 is lossless within float32 range", { timeout: 5000 }, () => { const v = 3.14; expect(unpackFloat32(packFloat32(v))).toBeCloseTo(v, 5); }); it("packFloat64/unpackFloat64 is lossless", { timeout: 5000 }, () => { const v = Math.PI; expect(unpackFloat64(packFloat64(v))).toBe(v); }); }); describe("range guards", () => { it("isInFloat16Range returns false for out-of-range values", { timeout: 5000 }, () => { expect(isInFloat16Range(max32)).toBe(false); }); it("isInFloat32Range returns false for out-of-range values", { timeout: 5000 }, () => { expect(isInFloat32Range(Number.MAX_VALUE)).toBe(false); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/number/number.ts` using `DataView`-based bit manipulation (same approach as original); add `isInFloat16Range`, `isInFloat32Range`, `isInFloat64Range`, min/max constants for all three; OTel histogram on `packFloat16/32/64` call count - [ ] Create `src/number/number.schemas.ts`, `src/number/index.ts` - [ ] Create `src/number/number.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add number (float encoding) module"` --- ### Task 4.7: `random` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/random/pseudo.ts` - [ ] Create `src/random/pseudo.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { pseudoRandom } from "./pseudo.js"; describe("pseudoRandom", () => { it("returns a deterministic result for the same seed", { timeout: 5000 }, () => { const arr = ["a", "b", "c"]; expect(pseudoRandom(arr, 0, 0.42)).toBe(pseudoRandom(arr, 0, 0.42)); }); it("throws for empty array", { timeout: 5000 }, () => { expect(() => pseudoRandom([], 0)).toThrow(); }); it("throws for out-of-bounds index", { timeout: 5000 }, () => { expect(() => pseudoRandom(["a"], 1)).toThrow(); expect(() => pseudoRandom(["a"], -1)).toThrow(); }); it("produces different results for different seeds", { timeout: 5000 }, () => { const arr = ["a", "b", "c", "d", "e", "f", "g", "h"]; const results = new Set(Array.from({ length: 10 }, (_, i) => pseudoRandom(arr, 0, i / 10))); expect(results.size).toBeGreaterThan(1); }); it("is type-safe — returns T for T[]", { timeout: 5000 }, () => { const result: number = pseudoRandom([1, 2, 3], 0); expect(typeof result).toBe("number"); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/random/pseudo.ts`: `pseudoRandom(arr: T[], index: number, seed = Math.random()): T`; throw `UtilsError` with `UTILS_CODES.RANDOM.EMPTY_ARRAY` / `RANDOM.INDEX_OUT_OF_BOUNDS`; injectable `rng` option for determinism in tests - [ ] Create `src/random/pseudo.schemas.ts`, `src/random/index.ts` - [ ] Create `src/random/pseudo.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add random/pseudo module"` --- ## Chunk 5: `@suaveplan/utils` (part 2 — modules 6–10, property tests, benchmarks, publish) ### Task 5.1: `email` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/email/email.ts` - [ ] Create `src/email/email.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { normalizeEmail } from "./email.js"; describe("normalizeEmail", () => { it("strips dots and plus alias from Gmail", { timeout: 5000 }, () => { expect(normalizeEmail("test.user+alias@gmail.com")).toBe("testuser@gmail.com"); }); it("strips dash alias from Yahoo", { timeout: 5000 }, () => { expect(normalizeEmail("test.user-alias@yahoo.com")).toBe("test.user@yahoo.com"); }); it("strips plus alias from Outlook", { timeout: 5000 }, () => { expect(normalizeEmail("user+alias@outlook.com")).toBe("user@outlook.com"); }); it("strips dots and plus for generic domains", { timeout: 5000 }, () => { expect(normalizeEmail("t.e.s.t+x@example.com")).toBe("test@example.com"); }); it("returns input unchanged for malformed email (no @)", { timeout: 5000 }, () => { expect(normalizeEmail("notanemail")).toBe("notanemail"); }); it("handles empty local part after stripping gracefully", { timeout: 5000 }, () => { expect(normalizeEmail("+alias@gmail.com")).toBe("+alias@gmail.com"); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/email/email.ts` using the provider constants from the original (GOOGLE_DOMAINS, YAHOO_DOMAINS, MICROSOFT_DOMAINS); no external deps - [ ] Create `src/email/email.schemas.ts` (Zod email string schema), `src/email/index.ts` - [ ] Create `src/email/email.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add email normalizer module"` --- ### Task 5.2: `sound` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/sound/sound.ts` and `sound.types.ts` - [ ] Create `src/sound/sound.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { convertSprites, getSoundKey, parseSoundVariations } from "./sound.js"; describe("parseSoundVariations", () => { it("groups sprite keys by base name", { timeout: 5000 }, () => { const sprites = { footstep_1: [0, 100] as [number, number], footstep_2: [100, 100] as [number, number], jump_1: [200, 150] as [number, number], }; expect(parseSoundVariations(sprites)).toEqual({ footstep: [1, 2], jump: [1] }); }); it("ignores keys without underscore+number suffix", { timeout: 5000 }, () => { expect(parseSoundVariations({ ambient: [0, 1000] as [number, number] })).toEqual({}); }); }); describe("getSoundKey", () => { it("returns a key that exists in the variations map", { timeout: 5000 }, () => { const variations = { footstep: [1, 2, 3] }; const key = getSoundKey("footstep", variations); expect(key).toMatch(/^footstep_[123]$/); }); it("throws UtilsError for unknown sound name", { timeout: 5000 }, () => { expect(() => getSoundKey("unknown", { footstep: [1] })).toThrow(); }); }); describe("convertSprites", () => { it("converts howler sprite format to variation map", { timeout: 5000 }, () => { const sprites = { step_1: [0, 100] as [number, number], step_2: [100, 100] as [number, number] }; expect(convertSprites(sprites)).toEqual({ step: [1, 2] }); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/sound/sound.ts`; preserve `SoundSprites` type from original; throw `UtilsError` with `UTILS_CODES.SOUND.NO_VARIATIONS` when a sound name has no entries - [ ] Create `src/sound/sound.schemas.ts`, `src/sound/index.ts` - [ ] Create `src/sound/sound.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add sound sprites module"` --- ### Task 5.3: `string` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/string/string.ts` (only `generateId` and `parseTextWithLinks` — `parseTemplate` is scrapped) - [ ] Create `src/string/string.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { generateId, parseTextWithLinks } from "./string.js"; describe("generateId", () => { it("returns a non-empty string", { timeout: 5000 }, () => { expect(typeof generateId()).toBe("string"); expect(generateId().length).toBeGreaterThan(0); }); it("generates unique values", { timeout: 5000 }, () => { const ids = new Set(Array.from({ length: 100 }, generateId)); expect(ids.size).toBe(100); }); it("accepts a custom length", { timeout: 5000 }, () => { expect(generateId({ length: 8 })).toHaveLength(8); }); }); describe("parseTextWithLinks", () => { it("extracts URLs from plain text", { timeout: 5000 }, () => { const result = parseTextWithLinks("Visit https://example.com for info"); expect(result).toContainEqual(expect.objectContaining({ url: "https://example.com" })); }); it("returns empty array for text with no links", { timeout: 5000 }, () => { expect(parseTextWithLinks("no links here")).toEqual([]); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/string/string.ts`; `generateId` uses `crypto.randomUUID()` internally and optionally trims to requested length; `parseTextWithLinks` uses a URL regex to find and annotate links in text - [ ] Create `src/string/string.schemas.ts`, `src/string/index.ts` - [ ] Create `src/string/string.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add string module (generateId, parseTextWithLinks)"` --- ### Task 5.4: `validate` module (TDD) - [ ] Create `src/validate/validate.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { isHex } from "./validate.js"; describe("isHex", () => { it("returns true for even-length hex strings", { timeout: 5000 }, () => { expect(isHex("a1b2c3")).toBe(true); expect(isHex("ff")).toBe(true); expect(isHex("")).toBe(false); // empty string is not valid hex }); it("returns false for odd-length strings", { timeout: 5000 }, () => { expect(isHex("abc")).toBe(false); }); it("returns false for non-hex characters", { timeout: 5000 }, () => { expect(isHex("gg")).toBe(false); }); it("returns false for non-strings", { timeout: 5000 }, () => { expect(isHex(123)).toBe(false); expect(isHex(null)).toBe(false); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/validate/validate.ts`: `isHex = (i: unknown): i is string => typeof i === "string" && /^([A-Fa-f0-9]{2})+$/.test(i)` - [ ] Create `src/validate/index.ts` - [ ] Create `src/validate/validate.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add validate/isHex module"` --- ### Task 5.5: `validation/size` module (TDD) - [ ] Read `/mnt/s/web/suaveplan/packages/universal/utils/src/validation/size.ts` - [ ] Create `src/validation/size.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { DEFAULT_MAX_MESSAGE_SIZE, formatSize, getDataSize, isWithinSizeLimit, parseSize, SIZE_LIMITS, truncateToSize, validateSize } from "./size.js"; describe("SIZE_LIMITS", () => { it("SMALL is 10 KB", { timeout: 5000 }, () => expect(SIZE_LIMITS.SMALL).toBe(10 * 1024)); it("LARGE is 1 MB", { timeout: 5000 }, () => expect(SIZE_LIMITS.LARGE).toBe(1024 * 1024)); }); describe("getDataSize", () => { it("returns byte length of ASCII string", { timeout: 5000 }, () => expect(getDataSize("hello")).toBe(5)); it("returns byte length of ArrayBuffer", { timeout: 5000 }, () => { expect(getDataSize(new ArrayBuffer(100))).toBe(100); }); it("handles multibyte characters", { timeout: 5000 }, () => { expect(getDataSize("こ")).toBeGreaterThan(1); }); }); describe("validateSize", () => { it("does not throw when within limit", { timeout: 5000 }, () => { expect(() => validateSize("hello", 100)).not.toThrow(); }); it("throws UtilsError when exceeding limit", { timeout: 5000 }, () => { expect(() => validateSize("hello", 3)).toThrow(); }); }); describe("formatSize", () => { it("formats bytes as human-readable", { timeout: 5000 }, () => { expect(formatSize(1024)).toBe("1.00 KB"); expect(formatSize(1024 * 1024)).toBe("1.00 MB"); }); }); describe("parseSize", () => { it("parses '1KB' to 1024", { timeout: 5000 }, () => expect(parseSize("1KB")).toBe(1024)); it("parses '1.5MB'", { timeout: 5000 }, () => expect(parseSize("1.5MB")).toBe(1.5 * 1024 * 1024)); }); describe("truncateToSize", () => { it("truncates string to byte limit", { timeout: 5000 }, () => { const result = truncateToSize("Hello, world!", 5); expect(getDataSize(result)).toBeLessThanOrEqual(5); }); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect FAIL - [ ] Implement `src/validation/size.ts`; throw `UtilsError` with `UTILS_CODES.VALIDATION.SIZE_EXCEEDED`; use `TextEncoder` for string byte measurement; export `SIZE_LIMITS`, `DEFAULT_MAX_MESSAGE_SIZE`; add OTel counter on `validateSize` calls - [ ] Create `src/validation/size.schemas.ts` (Zod schema for size limit options), `src/validation/index.ts` - [ ] Create `src/validation/size.md` (≥200 words) - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"feat(utils): add validation/size module"` --- ### Task 5.6: Property tests (fast-check) Add property tests to the relevant modules: - [ ] `src/object/object.test.ts` — deepMerge idempotency, deepClone produces structurally equal but distinct object, pick/omit are complementary: ```ts import fc from "fast-check"; it("deepMerge(a, {}) is structurally equal to a", { timeout: 30000 }, () => { fc.assert(fc.property(fc.object(), (a) => { expect(deepMerge(a, {})).toEqual(a); })); }); it("pick(obj, keys) + omit(obj, keys) together cover all keys", { timeout: 30000 }, () => { fc.assert(fc.property(fc.object(), fc.array(fc.string()), (obj, keys) => { const picked = pick(obj, keys as Array); const omitted = omit(obj, keys as Array); const merged = { ...picked, ...omitted }; expect(Object.keys(merged).sort()).toEqual(Object.keys(obj).sort()); })); }); ``` - [ ] `src/number/number.test.ts` — float32/64 round-trip for arbitrary in-range values: ```ts it("packFloat32 round-trips in-range values", { timeout: 30000 }, () => { fc.assert(fc.property(fc.float({ noNaN: true, noDefaultInfinity: true }), (v) => { expect(unpackFloat32(packFloat32(v))).toBeCloseTo(v, 4); })); }); ``` - [ ] `src/random/pseudo.test.ts` — always returns an element of the array: ```ts it("always returns an element that exists in the array", { timeout: 30000 }, () => { fc.assert(fc.property( fc.array(fc.integer(), { minLength: 1 }), fc.nat(), fc.double({ min: 0, max: 1, noNaN: true }), (arr, rawIndex, seed) => { const index = rawIndex % arr.length; expect(arr).toContain(pseudoRandom(arr, index, seed)); } )); }); ``` - [ ] Run `bunx turbo test --filter=@suaveplan/utils` — expect PASS - [ ] Commit: `"test(utils): add fast-check property tests"` --- ### Task 5.7: Benchmarks - [ ] Create `src/__benchmarks__/object.bench.ts` benchmarking `deepMerge` and `deepClone` for small/medium objects - [ ] Create `src/__benchmarks__/number.bench.ts` benchmarking `packFloat16/32/64` throughput - [ ] Run each and paste baselines into README - [ ] Commit: `"bench(utils): add object and number benchmarks"` --- ### Task 5.8: README + docs check > Module `.md` files are created inline alongside each TDD task (4.3–5.5). This task adds the package-level README and verifies 100% docs coverage. - [ ] Create `README.md` at `universal/data/utils/README.md` with: description, install, quick-start, Modules table (linking each `.md`), Benchmarks section - [ ] Run `bun run docs:check --filter=@suaveplan/utils` — must be 100%; fix any gaps before continuing - [ ] Commit: `"docs(utils): add README"` --- ### Task 5.9: Pre-publish gate + publish - [ ] Run `bunx turbo lint typecheck test --filter=@suaveplan/utils` — must be green - [ ] Run `bunx turbo test:coverage --filter=@suaveplan/utils` — must be 100% all thresholds - [ ] Fix any lint, type, or coverage issues before continuing - [ ] Run `bun scripts/publish.ts --package @suaveplan/utils` - [ ] Archive `openspec/changes/enterprise-utils/` → `openspec/archive/2026-04-20-enterprise-utils/`; write `openspec/specs/utils/spec.md` - [ ] Commit: `"release(utils): v0.0.0 — publish to Verdaccio"` --- ### Task 5.10: Open follow-up OpenSpec for `crypto-utils` refactor - [ ] Create `openspec/changes/crypto-utils-bytes-refactor/proposal.md`: - **Why:** `@suaveplan/crypto-utils` duplicates hex/base64/utf8/bytes/concat encoding that now lives canonically in `@suaveplan/bytes`. Deduplication reduces maintenance surface and ensures consistent behaviour. - **What:** Add `@suaveplan/bytes` as a dep to `crypto-utils`; replace duplicate encoding utilities with re-exports or direct imports from `@suaveplan/bytes`. `base58` stays in `crypto-utils` (blockchain-specific). - **Impact:** `crypto-utils` gains a new `workspace:^` dep; its exported surface is preserved (re-export or wrapping); consumers see no API change. - [ ] Create `openspec/changes/crypto-utils-bytes-refactor/tasks.md` (stub — implementation is a separate session) - [ ] Commit: `"chore: open crypto-utils-bytes-refactor OpenSpec change"` --- ## Summary | Package | Domain path | Chunks | |---------|-------------|--------| | `@suaveplan/bytes` | `universal/primitives/bytes` | 1 | | `@suaveplan/json` | `universal/data/json` | 2 | | `@suaveplan/locale-data` | `universal/i18n/locale-data` | 3 | | `@suaveplan/utils` | `universal/data/utils` | 4–5 | | crypto-utils follow-up | OpenSpec change only | 5.10 | **All dependencies already exist in genesis — no blocking work required before starting.**