# Design — split-font-compress Non-normative. File layout for the two packages after the split, the rationale for duplicating the BTF wire-format constants, and the boundary between pack and unpack error registries. ## Post-split file layout ### `packages/fonts/font-decompress/` ```text packages/fonts/font-decompress/ package.json # @suaveplan/font-decompress v0.0.0, sideEffects: false tsconfig.json tsconfig.lib.json vite.config.ts vitest.config.ts README.md src/ index.ts # explicit named re-exports constants/ index.ts # CMD_M, CMD_L, CMD_B, CMD_Q, CMD_Z, CMD_COORD_COUNTS, METRICS_COUNT error-codes.ts # FONT_DECOMPRESS_CODES + FontDecompressError error-codes.test.ts error-codes.md utils/ decode-string.ts # decodeString(data, offset) decode-string.test.ts decode-string.md unpacker/ binary-font-unpacker/ binary-font-unpacker.ts # BinaryFontUnpacker class + createBinaryFontUnpacker factory binary-font-unpacker.test.ts binary-font-unpacker.md index.ts types/ index.ts # re-exports TypefaceFont, PathCommand from @suaveplan/font-convert ``` ### `packages/fonts/font-compress/` (post-trim) ```text packages/fonts/font-compress/ package.json # unchanged version, subpath exports trimmed README.md # updated to reflect packer-only scope src/ index.ts # drops BinaryFontUnpacker / createBinaryFontUnpacker exports constants/ index.ts # unchanged (CMD_*, CMD_COORD_COUNTS, METRICS_COUNT) error-codes.ts # FONT_COMPRESS_CODES trimmed: { PACK_FAILED, INVALID_DATA } utils/ encode-string.ts # encodeString only; decodeString removed parse-path.ts # unchanged packer/ binary-font-packer/ binary-font-packer.ts binary-font-packer.test.ts binary-font-packer.md index.ts # NOTE: src/unpacker/ removed entirely ``` ## Why duplicate the BTF constants rather than introducing a shared package The BTF wire-format constants are 19 lines of magic numbers (`CMD_M = 0`, `CMD_L = 1`, etc.). Three alternatives were considered: 1. **Duplicate (chosen).** Each package keeps its own copy of `constants/index.ts`. Trivially low maintenance; if the format ever changes, both packages get updated in the same commit (which is when wire-format changes happen anyway — they are coordinated breaking events). 2. **Move to a third `@suaveplan/font-typeface-format` package.** Adds a coordinate package with three constants and no functional behaviour. Costs a `workspace:^` dep on both packages plus another row in `MANIFEST.md` for marginal benefit. 3. **Make `font-compress` depend on `font-decompress` for the constants.** Asymmetric — compress conceptually generates what decompress reads, so a forward dep would invert the natural composition. Rejected. Option 1 is the cleanest. The duplication is auditable in one place per package, both files are byte-identical, and `bun scripts/folder-reorg/generate-manifest.ts` confirms each package's dep count stays minimal. ## Error registry split `font-compress` originally shipped three codes: `PACK_FAILED`, `UNPACK_FAILED`, `INVALID_DATA`. Post-split: | Code | font-compress | font-decompress | Notes | |---|---|---|---| | `PACK_FAILED` | ✅ kept | — | Thrown by `BinaryFontPacker.pack` only | | `UNPACK_FAILED` | ❌ removed | ✅ added | Thrown by `BinaryFontUnpacker.unpack` only | | `INVALID_DATA` | ✅ kept | ✅ added | Both pack and unpack throw it on boundary-validation failures; namespaces keep the codes distinct | The namespaces (`FONT_COMPRESS` vs `FONT_DECOMPRESS`) ensure that even codes with the same reason string are programmatically distinct: - `FONT_COMPRESS.INVALID_DATA` — pack input was malformed - `FONT_DECOMPRESS.INVALID_DATA` — unpack input was malformed Consumers dispatching on the error code see different strings; consumers using `err instanceof FontCompressError` vs `err instanceof FontDecompressError` see different classes. ## Public-surface diffs ### `font-compress/src/index.ts` (post-split) ```ts export type { FontCompressErrorCode } from "./error-codes.js"; export { FONT_COMPRESS_CODES, FontCompressError } from "./error-codes.js"; export { BinaryFontPacker, createBinaryFontPacker, } from "./packer/binary-font-packer/index.js"; // REMOVED: BinaryFontUnpacker + createBinaryFontUnpacker exports. ``` ### `font-decompress/src/index.ts` (new) ```ts export type { FontDecompressErrorCode } from "./error-codes.js"; export { FONT_DECOMPRESS_CODES, FontDecompressError } from "./error-codes.js"; export { BinaryFontUnpacker, createBinaryFontUnpacker, } from "./unpacker/binary-font-unpacker/index.js"; export type { PathCommand, TypefaceFont } from "@suaveplan/font-convert"; ``` The convenience re-export of `TypefaceFont` and `PathCommand` from `@suaveplan/font-convert` lets consumers type their `unpack(...)` return value without taking an extra dep — but it is a single named re-export of types only, which CLAUDE.md rule §4 permits (the ban is on convenience re-exports of external runtime deps, not on types). Actually — rule §4 says "NEVER re-export external deps for convenience — import directly from source." That applies here too. Removing the convenience re-export; consumers import `TypefaceFont` directly from `@suaveplan/font-convert`. The package's own types module just declares its public shapes. Revised `font-decompress/src/index.ts`: ```ts export type { FontDecompressErrorCode } from "./error-codes.js"; export { FONT_DECOMPRESS_CODES, FontDecompressError } from "./error-codes.js"; export { BinaryFontUnpacker, createBinaryFontUnpacker, } from "./unpacker/binary-font-unpacker/index.js"; ``` No type re-export. `TypefaceFont` and `PathCommand` come from `@suaveplan/font-convert` directly. ## Test coverage strategy - `font-decompress` inherits the unpacker's existing 100% test file. The tests get moved verbatim alongside the implementation; only the import paths update. New tests cover the package's own `error-codes.ts` (every code reachable) and `decode-string.ts` (byte-level boundary cases). - `font-compress` keeps its packer test file untouched. Its `error-codes.test.ts` is regenerated to assert the trimmed registry (no `UNPACK_FAILED`). The `encode-string.test.ts` is trimmed to drop the `decodeString` cases that move to `font-decompress`. ## Build/dist verification Both packages run their gates independently: ```bash bunx turbo lint typecheck test --filter=@suaveplan/font-compress bunx turbo lint typecheck test --filter=@suaveplan/font-decompress bunx turbo test:coverage --filter=@suaveplan/font-compress bunx turbo test:coverage --filter=@suaveplan/font-decompress bun run docs:check --filter=@suaveplan/font-compress bun run docs:check --filter=@suaveplan/font-decompress bunx turbo build --filter=@suaveplan/font-compress bunx turbo build --filter=@suaveplan/font-decompress ``` All eight must pass before archive. Coverage thresholds are 100% Istanbul on both packages.