308 lines
25 KiB
Markdown
308 lines
25 KiB
Markdown
# @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
|