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

6.3 KiB

Capability Spec Delta: font-compress (font-compress)

Package: @suaveplan/font-compress Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time. No exceptions.

ADDED Requirements

Requirement: Packer-only public surface

The package SHALL expose BinaryFontPacker class + createBinaryFontPacker() factory + FONT_COMPRESS_CODES registry + FontCompressError class, and SHALL NOT expose any unpacker functionality. BinaryFontUnpacker and createBinaryFontUnpacker are NOT exported from this package — they live in the sibling @suaveplan/font-decompress.

Scenario: root barrel exposes only the packer

  • WHEN a consumer imports @suaveplan/font-compress
  • THEN BinaryFontPacker, createBinaryFontPacker, FONT_COMPRESS_CODES, and FontCompressError are present; BinaryFontUnpacker is undefined

Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"root barrel exposes packer only"


Requirement: FONT_COMPRESS_CODES trimmed registry

The package SHALL declare an error-code registry via defineCodes("FONT_COMPRESS", [...]) containing exactly PACK_FAILED and INVALID_DATA. UNPACK_FAILED is NOT in this registry — it lives in @suaveplan/font-decompress as FONT_DECOMPRESS.UNPACK_FAILED. Every pack-side error path SHALL throw a FontCompressError carrying one of the two retained codes.

Scenario: every kept code is reachable

  • WHEN the test suite exercises a successful pack and a malformed-input pack (e.g. missing familyName)
  • THEN the malformed path raises FontCompressError whose code matches FONT_COMPRESS.INVALID_DATA or FONT_COMPRESS.PACK_FAILED

Scenario: UNPACK_FAILED removed from registry

  • WHEN the test suite asserts on Object.keys(FONT_COMPRESS_CODES).sort()
  • THEN the array is exactly ["INVALID_DATA", "PACK_FAILED"] (sorted lexicographically)

Verified by: src/error-codes.test.ts::"every kept FONT_COMPRESS.* code is reachable" AND src/error-codes.test.ts::"UNPACK_FAILED removed from registry post-split"


Requirement: BinaryFontPacker public API

The package SHALL provide class BinaryFontPacker with method pack(font: TypefaceFont): Uint8Array and a createBinaryFontPacker(): BinaryFontPacker factory function. TypefaceFont is imported from @suaveplan/font-convert. The class SHALL emit the BTF wire format consumed by @suaveplan/font-decompress::BinaryFontUnpacker.

Scenario: round-trip TypefaceFont → BTF → TypefaceFont

  • WHEN a known-vector TypefaceFont is passed to BinaryFontPacker.pack(...) and the resulting bytes are then unpacked via BinaryFontUnpacker.unpack(...) (from @suaveplan/font-decompress)
  • THEN the round-trip preserves familyName, cssFontWeight, cssFontStyle, boundingBox, metrics, and every glyph entry (within the dictionary-encoding tolerance documented in the implementation)

Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"round-trip preserves TypefaceFont shape via font-decompress unpacker"


Requirement: encodeString utility

The package SHALL provide an encodeString(value: string): Uint8Array helper used by the packer to emit length-prefixed UTF-8 strings to the BTF header. The function SHALL emit the bytes in the format [uint32 length][utf8 bytes]. decodeString is NOT in this package — it lives in @suaveplan/font-decompress.

Scenario: emits length-prefixed UTF-8

  • WHEN encodeString("Inter Variable") is called
  • THEN the returned Uint8Array is 14 (uint32 length, little-endian) followed by the UTF-8 bytes of "Inter Variable"

Scenario: decodeString export removed

  • WHEN import { decodeString } from "@suaveplan/font-compress" is attempted at type-check time
  • THEN TypeScript reports decodeString is not exported (the test asserts the runtime absence via import * as ns from ...; expect(ns).not.toHaveProperty("decodeString"))

Verified by: src/utils/encode-string.test.ts::"emits length-prefixed UTF-8" AND src/utils/encode-string.test.ts::"decodeString export removed post-split"


Requirement: Universal-tier purity

The package SHALL ship as a universal-tier package — no imports from node:*, bun:*, or the bare specifiers fs/path/stream/net/http/crypto in any file under src/. package.json SHALL NOT declare a "browser" field, SHALL NOT pin engines.node or engines.bun, and SHALL declare "sideEffects": false.

Scenario: no platform imports in src/

  • WHEN the CI step grep -rnE "from ['\"](node:|bun:|fs|path|stream|net|http)['\"]" packages/fonts/font-compress/src/ runs
  • THEN the command produces zero matches

Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"tier purity: no node/bun imports"


Requirement: Subpath exports trimmed

package.json SHALL declare subpath exports for . (root barrel), ./error-codes, ./constants, ./utils/encode-string, ./utils/parse-path, ./packer/binary-font-packer. The ./unpacker/binary-font-unpacker subpath is removed (no longer present in dist).

Scenario: unpacker subpath removed

  • WHEN a consumer attempts import { BinaryFontUnpacker } from "@suaveplan/font-compress/unpacker/binary-font-unpacker"
  • THEN the import fails to resolve (the subpath is no longer in package.json exports and the source files are no longer present)

Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"unpacker subpath export removed"


Requirement: Banned patterns absent from src/

The package SHALL ship with zero matches of @deprecated, TODO, FIXME, ts-ignore, ts-expect-error, it.skip, test.skip, describe.skip, .only, raw any, or console.log in any .ts or .tsx file under src/. Zero // biome-ignore suppression comments. All test files import runner primitives from @suaveplan/testing/runner (rule §7).

Scenario: clean static-analysis grep

  • WHEN the CI step grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\\.skip\\|\\.only\\|\\bany\\b\\|console\\.log" packages/fonts/font-compress/src/ --include="*.ts" runs
  • THEN the command produces zero matches

Verified by: repo-wide CI grep gate + bun scripts/lint/check-test-runner-imports.ts