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,307 @@
# @suaveplan/utils — Specification
**Package:** `@suaveplan/utils`
**Version at graduation:** `0.1.0`
**Layer:** Universal — zero browser/React/Node-specific APIs
**Published:** 2026-04-20. Spec heading conformance: `utils-spec-conformance` (2026-04-26).
---
## Purpose
Zero-external-dependency utility library covering debugging, object manipulation, platform detection, IEEE 754 float packing, pseudo-random selection, email normalization, audio sprite management, string utilities, hex validation, and byte-size enforcement.
## Requirements
### Requirement: Debug logger with bounded deduplicating buffer
The package SHALL export a `debug` factory and a default debug instance whose `log` / `warn` / `error` / `info` methods deduplicate identical message keys per-channel within a bounded ring buffer. When the buffer fills, the oldest entry MUST be evicted. Error objects MUST be keyed by `message`. The default sink is `console.*`. `bufferSize: 1` MUST keep only the most recent message.
#### Scenario: Identical messages are logged once per channel
- **WHEN** the same message is logged repeatedly via `log`, `warn`, `error`, or `info`
- **THEN** only one underlying console call happens per channel until the entry is evicted
#### Scenario: Buffer eviction is FIFO
- **WHEN** the buffer is full and a new unique message arrives
- **THEN** the oldest entry is evicted
#### Scenario: Console fallbacks do not crash
- **WHEN** the default console-backed debug instance is used for log / warn / error / info / multi-arg
- **THEN** no exception is thrown
#### Scenario: Error objects key on message
- **WHEN** `Error` instances are passed
- **THEN** dedup uses the `message` field, not object identity
**Verified by:** `src/debug/debug.test.ts::"logs unique messages only"` AND `src/debug/debug.test.ts::"evicts oldest entry when buffer is full"` AND `src/debug/debug.test.ts::"exports a default debug instance"` AND `src/debug/debug.test.ts::"uses console.log by default (no crash)"` AND `src/debug/debug.test.ts::"uses console.warn by default (no crash)"` AND `src/debug/debug.test.ts::"uses console.error by default (no crash)"` AND `src/debug/debug.test.ts::"uses console.info by default (no crash)"` AND `src/debug/debug.test.ts::"warn deduplicates independently in same buffer"` AND `src/debug/debug.test.ts::"error deduplicates independently in same buffer"` AND `src/debug/debug.test.ts::"info deduplicates independently in same buffer"` AND `src/debug/debug.test.ts::"handles Error objects by keying on message"` AND `src/debug/debug.test.ts::"handles multi-arg log calls"` AND `src/debug/debug.test.ts::"bufferSize=1 only keeps the most recent message"`
### Requirement: Object utilities — type guards, immutable merge/pick/omit, nested access, deep clone
The package SHALL export `isObject`, `isPlainObject`, `isObjectOrArray`, `isEmpty`, `deepMerge` (with optional `skipUndefined`), `deepMergeAll`, `pick` / `omit` / `keepKeys` / `removeKeys`, `flattenObject` (configurable separator), `getNestedValue` / `setNestedValue` / `hasPath`, and `deepClone`. `deepMerge`/`deepMergeAll` MUST throw `UtilsError(UTILS/OBJECT.PROTOTYPE_POLLUTION)` on `__proto__`/`constructor`/`prototype` keys, MUST NOT mutate inputs, and MUST replace arrays rather than merge them.
#### Scenario: Type guards classify objects correctly
- **WHEN** `isObject` / `isPlainObject` / `isObjectOrArray` / `isEmpty` are called with various values
- **THEN** they classify plain objects, arrays, null, primitives, Date and RegExp per their documented contract
#### Scenario: deepMerge composes nested objects without mutation
- **WHEN** `deepMerge(a, b)` is called with nested objects
- **THEN** the result merges nested keys AND neither `a` nor `b` is mutated
- **AND** arrays are replaced rather than concatenated
- **AND** `skipUndefined: true` does not overwrite with `undefined`; default behaviour does
#### Scenario: Prototype pollution is rejected
- **WHEN** `deepMerge` encounters a `__proto__` key
- **THEN** `UtilsError(UTILS/OBJECT.PROTOTYPE_POLLUTION)` is thrown
#### Scenario: pick / omit / flattenObject behave as documented
- **WHEN** `pick`, `omit`, `keepKeys`, `removeKeys` are called
- **THEN** only the specified keys are retained / removed; `keepKeys`/`removeKeys` are aliases for `pick`/`omit`
- **WHEN** `flattenObject` is called with or without a custom separator
- **THEN** dot-notation (or the custom separator) flattens nested keys; non-object leaves are preserved
**Verified by:** `src/object/object.test.ts::"returns true for plain objects"` AND `src/object/object.test.ts::"returns false for null, arrays, primitives"` AND `src/object/object.test.ts::"returns true for Date and RegExp (non-array objects)"` AND `src/object/object.test.ts::"returns false for non-plain objects"` AND `src/object/object.test.ts::"returns true for objects and arrays"` AND `src/object/object.test.ts::"returns false for null and primitives"` AND `src/object/object.test.ts::"returns true for empty objects"` AND `src/object/object.test.ts::"returns false for non-empty objects"` AND `src/object/object.test.ts::"merges nested objects"` AND `src/object/object.test.ts::"does not mutate inputs"` AND `src/object/object.test.ts::"replaces arrays (does not merge them)"` AND `src/object/object.test.ts::"skipUndefined does not overwrite with undefined"` AND `src/object/object.test.ts::"overwrites with undefined when skipUndefined=false (default)"` AND `src/object/object.test.ts::"throws on __proto__ key"` AND `src/object/object.test.ts::"merges multiple objects in priority order"` AND `src/object/object.test.ts::"handles empty call"` AND `src/object/object.test.ts::"pick returns only specified keys"` AND `src/object/object.test.ts::"omit removes specified keys"` AND `src/object/object.test.ts::"pick handles missing keys gracefully"` AND `src/object/object.test.ts::"keepKeys is alias for pick"` AND `src/object/object.test.ts::"removeKeys is alias for omit"` AND `src/object/object.test.ts::"flattens nested object with dot notation"` AND `src/object/object.test.ts::"preserves non-object leaf values"` AND `src/object/object.test.ts::"uses custom separator"`
### Requirement: Platform detection with injectable adapter
The package SHALL export `isBrowser`, `isNode`, `isBun`, `isEdge`, `isSSR`, `supportsWebWorkers`, `supportsServiceWorkers`, and `getPlatform()` returning `"browser" | "node" | "bun" | "edge" | "unknown"`. Memory and CPU usage MUST be exposed via an injectable adapter; missing adapter methods MUST throw `UtilsError(UTILS/PLATFORM.UNAVAILABLE)`. Partial adapters MUST fall back to defaults. `getPlatform` MUST classify `bun`, `node`, and `browser` (window+document present) correctly and return `"unknown"` otherwise.
#### Scenario: Detection works under the test runner
- **WHEN** `isNode()`, `isBrowser()`, `isBun()`, `getPlatform()` are called inside Vitest
- **THEN** node-side helpers return appropriate booleans; `getPlatform()` returns `"bun"` or `"node"`
#### Scenario: Platform adapter is injectable and partial-tolerant
- **WHEN** a mock adapter is supplied
- **THEN** memory / CPU calls delegate to it
- **WHEN** the adapter omits a method
- **THEN** the default fallback is used
- **WHEN** memory or CPU is unavailable in the underlying runtime
- **THEN** `UtilsError(UTILS/PLATFORM.UNAVAILABLE)` is thrown
#### Scenario: getPlatform classifies all five buckets
- **WHEN** environment globals indicate Bun, Node, or Browser
- **THEN** `getPlatform` returns `"bun"`, `"node"`, or `"browser"` accordingly
- **WHEN** none of the markers is present
- **THEN** the result is `"unknown"`
**Verified by:** `src/platform/platform.test.ts::"isNode returns true in this test runner"` AND `src/platform/platform.test.ts::"isBrowser returns false in this test runner"` AND `src/platform/platform.test.ts::"getPlatform returns 'bun' or 'node'"` AND `src/platform/platform.test.ts::"isBun returns boolean"` AND `src/platform/platform.test.ts::"accepts a mock adapter"` AND `src/platform/platform.test.ts::"default adapter returns real memory usage"` AND `src/platform/platform.test.ts::"default adapter returns real CPU usage"` AND `src/platform/platform.test.ts::"partial adapter falls back to defaults for missing methods"` AND `src/platform/platform.test.ts::"returns memory stats when memoryUsage is available"` AND `src/platform/platform.test.ts::"throws PLATFORM.UNAVAILABLE when memoryUsage is missing"` AND `src/platform/platform.test.ts::"returns bun when hasBun=true"` AND `src/platform/platform.test.ts::"returns node when hasNode=true"` AND `src/platform/platform.test.ts::"calls fallback when neither bun nor node"` AND `src/platform/platform.test.ts::"returns 'browser' when window and document are defined"` AND `src/platform/platform.test.ts::"returns 'unknown' when window is missing"` AND `src/platform/platform.test.ts::"returns 'unknown' when document is missing"` AND `src/platform/platform.test.ts::"uses globalThis by default (returns unknown in Node)"` AND `src/platform/platform.test.ts::"returns cpu stats when cpuUsage is available"` AND `src/platform/platform.test.ts::"throws PLATFORM.UNAVAILABLE when cpuUsage is missing"`
### Requirement: IEEE 754 float packing and range constants
The package SHALL export precision constants (`min16`, `max16`, `min32`, `max32`, `min64`, `max64`) and lossless `pack` / `unpack` helpers for IEEE 754 half / single / double precision. NaN, ±Infinity, negative zero, and subnormals MUST round-trip correctly per IEEE 754 rules. `unpackFloatN` MUST throw on invalid buffer length. `isInFloat16Range` / `isInFloat32Range` / `isInFloat64Range` MUST classify NaN/Infinity as in-range and out-of-range floats correctly.
#### Scenario: Range constants match IEEE 754 definitions
- **WHEN** `min16`, `max16`, `min32`, `max32`, `min64`, `max64` are read
- **THEN** they equal `2^-14`, 65504, `2^-126`, 32-bit max, `Number.MIN_VALUE`, `Number.MAX_VALUE` respectively
#### Scenario: pack/unpack round-trips for half / single / double
- **WHEN** any in-range float is packed and unpacked at the same precision
- **THEN** the value is recovered (subnormal positive/negative both round-trip)
- **AND** NaN, ±Infinity, ±0 are preserved at every precision
- **AND** values beyond the half-precision range saturate to ±Infinity
#### Scenario: Invalid buffer length throws
- **WHEN** `unpackFloatN` is called with the wrong-length buffer
- **THEN** an error is thrown
#### Scenario: Range predicates classify correctly
- **WHEN** `isInFloat16Range` / `isInFloat32Range` are called
- **THEN** in-range numbers return `true`, out-of-range return `false`, NaN/Infinity return `true`
**Verified by:** `src/number/number.test.ts::"min16 is 2^-14"` AND `src/number/number.test.ts::"max16 is 65504"` AND `src/number/number.test.ts::"min32 is 2^-126"` AND `src/number/number.test.ts::"max32 is the max 32-bit float"` AND `src/number/number.test.ts::"min64 is Number.MIN_VALUE"` AND `src/number/number.test.ts::"max64 is Number.MAX_VALUE"` AND `src/number/number.test.ts::"packs and unpacks 0, 1, -1, 0.5, min16, max16"` AND `src/number/number.test.ts::"handles NaN, Infinity, -Infinity"` AND `src/number/number.test.ts::"handles negative zero"` AND `src/number/number.test.ts::"handles subnormal values (< min16)"` AND `src/number/number.test.ts::"unpackFloat16 returns subnormal positive when exp=0 and mant>0"` AND `src/number/number.test.ts::"unpackFloat16 returns subnormal negative when sign=1 exp=0 mant>0"` AND `src/number/number.test.ts::"values beyond max16 become Infinity"` AND `src/number/number.test.ts::"negative large values become -Infinity"` AND `src/number/number.test.ts::"throws for invalid buffer length"` AND `src/number/number.test.ts::"packFloat32/unpackFloat32 is lossless within float32 range"` AND `src/number/number.test.ts::"packFloat64/unpackFloat64 is lossless"` AND `src/number/number.test.ts::"packFloat32 handles NaN and Infinities"` AND `src/number/number.test.ts::"packFloat64 handles NaN and Infinities"` AND `src/number/number.test.ts::"unpackFloat32 works on non-zero offset Uint8Array"` AND `src/number/number.test.ts::"isInFloat16Range returns false for out-of-range values"` AND `src/number/number.test.ts::"isInFloat16Range returns true for values in range"` AND `src/number/number.test.ts::"isInFloat16Range returns true for Infinity and NaN"` AND `src/number/number.test.ts::"isInFloat32Range returns false for out-of-range values"` AND `src/number/number.test.ts::"isInFloat32Range returns true for values in range"`
### Requirement: Pseudorandom array selection
The package SHALL export `pseudoRandom<T>(arr, index, seed?, opts?)` returning a deterministic element when seeded and a random element when only `rng` is supplied (default `Math.random`). Empty arrays MUST throw `UtilsError(UTILS/RANDOM.EMPTY_ARRAY)`; out-of-bounds indices (negative or `>= arr.length`) MUST throw `UtilsError(UTILS/RANDOM.INDEX_OUT_OF_BOUNDS)`. Output MUST always be an element of the input array.
#### Scenario: Same seed yields the same result
- **WHEN** `pseudoRandom(arr, index, seed)` is called twice with the same arguments
- **THEN** the result is identical
- **WHEN** different seeds are used
- **THEN** results may differ
#### Scenario: Empty / out-of-bounds inputs throw
- **WHEN** the array is empty
- **THEN** `UtilsError(UTILS/RANDOM.EMPTY_ARRAY)` is thrown
- **WHEN** the index is negative or `>= arr.length`
- **THEN** `UtilsError(UTILS/RANDOM.INDEX_OUT_OF_BOUNDS)` is thrown
#### Scenario: Output is always an element of the array
- **WHEN** `pseudoRandom` returns a value
- **THEN** the value is one of the input array's elements
- **AND** the function is type-safe — `T[]` produces `T`
#### Scenario: Injectable rng or Math.random fallback
- **WHEN** no seed but a custom `rng` is provided
- **THEN** the custom rng drives selection
- **WHEN** neither seed nor rng is provided
- **THEN** `Math.random` drives selection
**Verified by:** `src/random/pseudo.test.ts::"returns a deterministic result for the same seed"` AND `src/random/pseudo.test.ts::"throws for empty array"` AND `src/random/pseudo.test.ts::"throws for out-of-bounds index (too high)"` AND `src/random/pseudo.test.ts::"throws for out-of-bounds index (negative)"` AND `src/random/pseudo.test.ts::"produces different results for different seeds"` AND `src/random/pseudo.test.ts::"is type-safe — returns T for T[]"` AND `src/random/pseudo.test.ts::"uses injectable rng when no seed provided"` AND `src/random/pseudo.test.ts::"uses Math.random when neither seed nor rng provided"` AND `src/random/pseudo.test.ts::"works with a single-element array"` AND `src/random/pseudo.test.ts::"works with object arrays"` AND `src/random/pseudo.test.ts::"throws with correct error code for empty array"` AND `src/random/pseudo.test.ts::"throws with correct error code for out-of-bounds"` AND `src/random/pseudo.test.ts::"always returns an element that exists in the array"`
### Requirement: Provider-aware email normalization
The package SHALL export `normalizeEmail(email)` that strips Gmail dots and `+` aliases, Yahoo `-` aliases, Outlook `+` aliases, and `.`/`+` aliases for generic domains. Malformed emails (missing `@`) MUST be returned unchanged. Empty local-part or empty domain after stripping MUST be handled gracefully without throwing.
#### Scenario: Provider-specific stripping
- **WHEN** a Gmail address with dots and `+`-alias is normalized
- **THEN** dots and the `+`-alias suffix are removed
- **WHEN** a Yahoo `-`-alias is normalized
- **THEN** the `-`-alias suffix is removed
- **WHEN** an Outlook `+`-alias is normalized
- **THEN** the `+`-alias suffix is removed
- **WHEN** a generic-provider address with dots and `+`-alias is normalized
- **THEN** dots and the `+`-alias suffix are removed
#### Scenario: Malformed input is preserved
- **WHEN** the email lacks `@`
- **THEN** the input is returned unchanged
- **WHEN** the input is `@`-only or yields empty local/domain
- **THEN** no exception is thrown
**Verified by:** `src/email/email.test.ts::"strips dots and plus alias from Gmail"` AND `src/email/email.test.ts::"strips dash alias from Yahoo"` AND `src/email/email.test.ts::"strips plus alias from Outlook"` AND `src/email/email.test.ts::"strips dots and plus for generic domains"` AND `src/email/email.test.ts::"returns input unchanged for malformed email (no @)"` AND `src/email/email.test.ts::"handles at-sign only (empty local and domain)"` AND `src/email/email.test.ts::"handles empty local part after stripping gracefully"`
### Requirement: Sound-sprite parsing and variation selection
The package SHALL export `parseSoundVariations(sprites)` (and its alias `convertSprites`) which groups `<name>_<n>` keys by base name, ignoring keys without a numeric suffix, with empty bases (leading `_`), with whitespace-only suffixes, or with non-integer suffixes. `getSoundKey(name, variations)` MUST return a key that exists in the variations map and MUST throw `UtilsError(UTILS/SOUND.NO_VARIATIONS)` for unknown sound names.
#### Scenario: parseSoundVariations groups by numeric suffix
- **WHEN** sprite keys like `bell_1`, `bell_2` are parsed
- **THEN** they are grouped under `bell`
- **WHEN** a key lacks `_<n>`, has an empty base, has a whitespace-only suffix, or has a non-integer suffix
- **THEN** that key is ignored
#### Scenario: getSoundKey returns a valid variation or throws
- **WHEN** `getSoundKey(name, variations)` is called for a known name
- **THEN** the result is one of the variation keys
- **WHEN** the name is unknown
- **THEN** `UtilsError(UTILS/SOUND.NO_VARIATIONS)` is thrown
#### Scenario: convertSprites is an alias for parseSoundVariations
- **WHEN** `convertSprites(sprites)` is called
- **THEN** it returns the same shape as `parseSoundVariations`
**Verified by:** `src/sound/sound.test.ts::"groups sprite keys by base name"` AND `src/sound/sound.test.ts::"ignores keys without underscore+number suffix"` AND `src/sound/sound.test.ts::"ignores keys where base is empty (leading underscore)"` AND `src/sound/sound.test.ts::"ignores keys with whitespace-only suffix"` AND `src/sound/sound.test.ts::"ignores keys with non-integer suffix"` AND `src/sound/sound.test.ts::"returns a key that exists in the variations map"` AND `src/sound/sound.test.ts::"throws UtilsError for unknown sound name"` AND `src/sound/sound.test.ts::"converts howler sprite format to variation map"`
### Requirement: String utilities — id generation and link extraction
The package SHALL export `generateId(opts?)` returning a non-empty random hex identifier (length defaults to 32; configurable shorter or longer than the underlying UUID), and `parseTextWithLinks(text)` returning an array of HTTP/HTTPS URL records (`TextLink[]`).
#### Scenario: generateId returns unique non-empty strings
- **WHEN** `generateId()` is called many times
- **THEN** each call returns a non-empty string AND values are unique
#### Scenario: generateId honours custom length
- **WHEN** `generateId({ length })` is called with a length shorter or longer than 32
- **THEN** the output length matches the requested length
#### Scenario: parseTextWithLinks finds HTTP/HTTPS URLs
- **WHEN** `parseTextWithLinks(text)` is called on text containing URLs
- **THEN** each URL is returned as a `TextLink`
- **WHEN** the text has no links
- **THEN** an empty array is returned
**Verified by:** `src/string/string.test.ts::"returns a non-empty string"` AND `src/string/string.test.ts::"generates unique values"` AND `src/string/string.test.ts::"accepts a custom length shorter than base"` AND `src/string/string.test.ts::"accepts a custom length longer than 32 chars"` AND `src/string/string.test.ts::"extracts URLs from plain text"` AND `src/string/string.test.ts::"returns empty array for text with no links"`
### Requirement: Hex string validation
The package SHALL export `isHex(i)` as a type-guard returning `true` only for non-empty, even-length, hex-only strings; non-strings, odd-length strings, and strings with non-hex characters MUST return `false`.
#### Scenario: Even-length hex returns true
- **WHEN** `isHex` is called with an even-length hex string
- **THEN** the result is `true`
#### Scenario: Odd length, non-hex chars, non-strings return false
- **WHEN** `isHex` is called with an odd-length string, a string with non-hex characters, or a non-string value
- **THEN** the result is `false`
**Verified by:** `src/validate/validate.test.ts::"returns true for even-length hex strings"` AND `src/validate/validate.test.ts::"returns false for odd-length strings"` AND `src/validate/validate.test.ts::"returns false for non-hex characters"` AND `src/validate/validate.test.ts::"returns false for non-strings"`
### Requirement: Byte-size enforcement and parsing
The package SHALL export `SIZE_LIMITS` constants (including `SMALL`=10 KB, `LARGE`=1 MB), `DEFAULT_MAX_MESSAGE_SIZE` (1 MB), `getDataSize` (UTF-8 bytes for strings, byteLength for binary, 8 for numbers), `validateSize` (throws `UtilsError(UTILS/VALIDATION.SIZE_EXCEEDED)` over limit), `isWithinSizeLimit` (non-throwing boolean), `formatSize` (human-readable; `"0 Bytes"` for zero; configurable decimals), `parseSize` (throws `UtilsError(UTILS/VALIDATION.PARSE_FAILED)` on invalid format), and `truncateToSize` (binary-search string truncation).
#### Scenario: Constants match their documented values
- **WHEN** `SIZE_LIMITS.SMALL`, `SIZE_LIMITS.LARGE`, and `DEFAULT_MAX_MESSAGE_SIZE` are read
- **THEN** they equal 10 KB, 1 MB, and 1 MB respectively
#### Scenario: getDataSize handles every supported type
- **WHEN** `getDataSize` is called with an ASCII string, multibyte string, ArrayBuffer, Uint8Array, or number
- **THEN** the result is the correct byte length (8 for numbers — float64)
#### Scenario: validateSize and isWithinSizeLimit honour the default
- **WHEN** data is within the explicit or default limit
- **THEN** `validateSize` does not throw AND `isWithinSizeLimit` returns `true`
- **WHEN** data exceeds the limit
- **THEN** `validateSize` throws `UtilsError(UTILS/VALIDATION.SIZE_EXCEEDED)` AND `isWithinSizeLimit` returns `false`
#### Scenario: formatSize / parseSize are inverse on canonical inputs
- **WHEN** `formatSize(bytes, decimals?)` is called
- **THEN** the output is human-readable (`"0 Bytes"` for zero, `"1.50 KB"`, etc.) with configurable decimals
- **WHEN** `parseSize` is called with `"1KB"`, `"1.5MB"`, `"100"`, `"100B"`
- **THEN** the result is the correct byte count
- **WHEN** `parseSize` is called with an invalid format
- **THEN** `UtilsError(UTILS/VALIDATION.PARSE_FAILED)` is thrown
#### Scenario: truncateToSize is exact and idempotent on small input
- **WHEN** `truncateToSize` is called with a string longer than the limit
- **THEN** the result fits within the limit
- **WHEN** the input is already within the limit
- **THEN** the input is returned unchanged
**Verified by:** `src/validation/size.test.ts::"SMALL is 10 KB"` AND `src/validation/size.test.ts::"LARGE is 1 MB"` AND `src/validation/size.test.ts::"is 1 MB"` AND `src/validation/size.test.ts::"returns byte length of ASCII string"` AND `src/validation/size.test.ts::"returns byte length of ArrayBuffer"` AND `src/validation/size.test.ts::"handles multibyte characters"` AND `src/validation/size.test.ts::"returns byte length of Uint8Array"` AND `src/validation/size.test.ts::"returns 8 for a number (float64)"` AND `src/validation/size.test.ts::"does not throw when within limit"` AND `src/validation/size.test.ts::"throws UtilsError when exceeding limit"` AND `src/validation/size.test.ts::"uses default maxSize (1 MB) when not specified"` AND `src/validation/size.test.ts::"returns true when data is within limit"` AND `src/validation/size.test.ts::"returns false when data exceeds limit"` AND `src/validation/size.test.ts::"uses default maxSize when not specified"` AND `src/validation/size.test.ts::"formats bytes as human-readable"` AND `src/validation/size.test.ts::"returns '0 Bytes' for zero"` AND `src/validation/size.test.ts::"uses custom decimal places"` AND `src/validation/size.test.ts::"parses '1KB' to 1024"` AND `src/validation/size.test.ts::"parses '1.5MB'"` AND `src/validation/size.test.ts::"parses plain bytes '100'"` AND `src/validation/size.test.ts::"parses '100B'"` AND `src/validation/size.test.ts::"throws for invalid format"` AND `src/validation/size.test.ts::"truncates string to byte limit"` AND `src/validation/size.test.ts::"returns string unchanged if already within limit"`
## Error Codes
| Code | Description |
|------|-------------|
| `UTILS/OBJECT.PROTOTYPE_POLLUTION` | `__proto__` / constructor key detected |
| `UTILS/OBJECT.CIRCULAR` | Circular reference in deepClone |
| `UTILS/NUMBER.OUT_OF_RANGE` | Float value outside target precision range |
| `UTILS/RANDOM.EMPTY_ARRAY` | pseudoRandom called with empty array |
| `UTILS/RANDOM.INDEX_OUT_OF_BOUNDS` | index >= array.length |
| `UTILS/VALIDATION.SIZE_EXCEEDED` | Data exceeds maxSize bytes |
| `UTILS/VALIDATION.PARSE_FAILED` | Invalid size string format |
| `UTILS/PLATFORM.UNAVAILABLE` | Requested platform feature unavailable |
| `UTILS/SOUND.NO_VARIATIONS` | getSoundKey called for unknown sound name |
## Quality Gates (at ship)
- Istanbul coverage: 100% statements / branches / functions / lines
- Docs coverage: 100% (12/12 files)
- Lint: clean (biome, 0 errors)
- TypeScript: clean (noEmit)
- Tests: 169 passing