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

106 lines
6.6 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: bitpacker-compressed (bitpacker-stub-completion)
**Package**: `@suaveplan/bitpacker-compressed`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: CompressedBitPacker subclass
The package SHALL export `CompressedBitPacker<T extends BitPackerSchema> extends BitPacker<T>`. The subclass SHALL run packed bytes through a configured `CompressionAdapter` before returning them, and SHALL run incoming bytes through `decompressData` before delegating to `BitPacker.unpack`. Compression behavior SHALL be configurable via `BitPackerConfig.compression` (`algorithm`, `options`, `enabled`); accessors `compressionAlgorithm`, `compressionOptions`, `compressionEnabled` SHALL surface and mutate the current settings.
#### Scenario: pack → decompress round-trip is byte-identical
- **WHEN** a `CompressedBitPacker` packs `{ a: 1, b: "hello" }` with `algorithm: "gzip"` and the result is decompressed and unpacked
- **THEN** the unpacked value deep-equals the input
#### Scenario: `enabled: false` skips compression entirely
- **WHEN** `compressionEnabled` is `false`
- **THEN** `pack` returns bytes byte-identical to `BitPacker.pack` (no compression header, no codec invocation)
#### Scenario: `algorithm: "none"` invokes NoCompressionAdapter
- **WHEN** `algorithm` is `"none"`
- **THEN** `pack` produces the same bytes as `BitPacker.pack` AND the OTel counter `bitpacker.compress.bytes_in` records the input length while `bitpacker.compress.bytes_out` records the same value
**Verified by:** `packages/bitpacker/bitpacker-compressed/src/compressed-bit-packer/compressed-bit-packer.test.ts::"pack-decompress round-trip is byte-identical for gzip"` AND `packages/bitpacker/bitpacker-compressed/src/compressed-bit-packer/compressed-bit-packer.test.ts::"compressionEnabled false skips codec invocation"` AND `packages/bitpacker/bitpacker-compressed/src/compressed-bit-packer/compressed-bit-packer.test.ts::"algorithm 'none' yields BitPacker.pack output"`
---
### Requirement: CompressionAdapter contract and built-in adapters
The package SHALL expose a `CompressionAdapter` interface with `compress(data, options): Promise<Uint8Array>` and `decompress(data, options): Promise<Uint8Array>`. The package SHALL ship `NoCompressionAdapter`, `GzipCompressionAdapter`, and `DeflateCompressionAdapter` matching upstream behavior (`compression.ts` L121-L336).
#### Scenario: gzip adapter round-trip
- **WHEN** `GzipCompressionAdapter` compresses 1 KiB of zeros, then decompresses
- **THEN** the output is the original 1 KiB AND the compressed length is < 50 bytes
#### Scenario: deflate adapter round-trip
- **WHEN** `DeflateCompressionAdapter` compresses random 1 KiB bytes, then decompresses
- **THEN** the decompressed output deep-equals the original
#### Scenario: NoCompressionAdapter passes bytes through
- **WHEN** `NoCompressionAdapter.compress(input)` is called
- **THEN** the result is byte-identical to the input
**Verified by:** `packages/bitpacker/bitpacker-compressed/src/compression-registry/compression-registry.test.ts::"GzipCompressionAdapter round-trip"` AND `packages/bitpacker/bitpacker-compressed/src/compression-registry/compression-registry.test.ts::"DeflateCompressionAdapter round-trip"` AND `packages/bitpacker/bitpacker-compressed/src/compression-registry/compression-registry.test.ts::"NoCompressionAdapter passes bytes through"`
---
### Requirement: CompressionRegistry replacement and unknown-algorithm error
The package SHALL expose a `CompressionRegistry` allowing callers to `register(algorithm, adapter)` and `resolve(algorithm)`. Resolving an unknown algorithm SHALL throw `BitpackerError` with code `BITPACKER.UNKNOWN_COMPRESSION_ALGORITHM` and `context.algorithm`.
#### Scenario: registering an adapter overrides the default
- **WHEN** a custom `BrotliCompressionAdapter` is registered under key `"brotli"` then `resolve("brotli")` is called
- **THEN** the registered adapter is returned; subsequent `pack` calls with `algorithm: "brotli"` route through it
#### Scenario: resolving an unknown algorithm throws coded error
- **WHEN** `resolve("zstd")` is called and no adapter is registered for that key
- **THEN** the call throws `BitpackerError` with code `BITPACKER.UNKNOWN_COMPRESSION_ALGORITHM` and `context.algorithm === "zstd"`
**Verified by:** `packages/bitpacker/bitpacker-compressed/src/compression-registry/compression-registry.spec-conformance.test.ts::"register overrides default adapter"` AND `packages/bitpacker/bitpacker-compressed/src/compression-registry/compression-registry.spec-conformance.test.ts::"resolve throws UNKNOWN_COMPRESSION_ALGORITHM for missing key"`
---
### Requirement: Compression failure surfaces coded error
When the underlying codec rejects (e.g. truncated gzip stream), the package SHALL wrap the cause and throw `BitpackerError` with code `BITPACKER.COMPRESSION_FAILED` (during compress) or `BITPACKER.DECOMPRESSION_FAILED` (during decompress); `context.algorithm` SHALL identify the codec.
#### Scenario: truncated gzip stream emits DECOMPRESSION_FAILED
- **WHEN** a half-truncated gzip-compressed buffer is passed to `decompressData(buf, "gzip")`
- **THEN** a `BitpackerError` with code `BITPACKER.DECOMPRESSION_FAILED` is thrown with the underlying codec error chained as `cause`
**Verified by:** `packages/bitpacker/bitpacker-compressed/src/compression-registry/compression-registry.spec-conformance.test.ts::"truncated gzip stream emits DECOMPRESSION_FAILED with chained cause"`
## MODIFIED Requirements
### Requirement: RLE pass-through helpers
The package SHALL keep `compressRle` / `decompressRle` as exported free functions for the bytes-only fast path; they SHALL be available both at the package root (one-release deprecation) and at the `./rle` subpath. Their behavior SHALL match the existing 61-line implementation byte-for-byte (no semantic change).
#### Scenario: existing RLE call sites still resolve from package root
- **WHEN** a consumer imports `{ compressRle } from "@suaveplan/bitpacker-compressed"`
- **THEN** the import resolves and `compressRle` produces the same bytes as before this change
#### Scenario: canonical import location is `./rle`
- **WHEN** a consumer imports `{ compressRle } from "@suaveplan/bitpacker-compressed/rle"`
- **THEN** the import resolves and the function is the same reference as the root export
**Verified by:** `packages/bitpacker/bitpacker-compressed/src/rle/rle.test.ts::"compressRle output unchanged from pre-rewrite snapshot"` AND `packages/bitpacker/bitpacker-compressed/src/rle/rle.test.ts::"./rle subpath and root export resolve to the same function"`
<!-- markdownlint-enable MD024 -->