# Split @suaveplan/font-compress into compress + decompress packages **Packages**: `@suaveplan/font-compress` (modified), `@suaveplan/font-decompress` (new) **Status**: PROPOSED **Type**: refactor / package-split - **Design**: ./design.md - **Spec deltas**: ./specs/font-compress/spec.md, ./specs/font-decompress/spec.md ## Why `@suaveplan/font-compress` currently ships both `BinaryFontPacker` and `BinaryFontUnpacker` from one package — pack and unpack are two distinct concerns wedged together in v0.0.0. The pending `@suaveplan/font-decompress` row in `STATUS.md` exists because consumers reasonably expect a decompress-only package: build-time tooling packs fonts once and ships them, while runtime renderers (browsers, R3F scenes, headless test environments) only need to decompress. A renderer pulling the full `font-compress` payload to use one class drags pack-side code, parse-path utilities, and dictionary builders it never executes. Splitting eliminates that. Genesis has zero monorepo consumers of `@suaveplan/font-compress` today — verified via `grep -rE "from ['\"]@suaveplan/font-compress" packages --include='*.ts'` — so the split has no internal blast radius. External consumers (assuming there are any pre-publish) get a small migration: import `BinaryFontUnpacker` / `createBinaryFontUnpacker` from `@suaveplan/font-decompress` instead of `@suaveplan/font-compress`. The unpacker's public API is preserved bit-for-bit; only the import path changes. This is also the pattern the no-deferrals memory captures: the existing `font-compress` package shipped at 0.0.0 with the decompress half folded in, leaving the named `font-decompress` row pending — a half-completed identity. The split lands BOTH packages at their full functional surface in one OpenSpec change rather than leaving one in `STATUS.md ➖`. ## What changes ### `@suaveplan/font-decompress` (new package at `packages/fonts/font-decompress/`) - ADDED: package `@suaveplan/font-decompress` v0.0.0, universal-tier, `"sideEffects": false`. - ADDED: `BinaryFontUnpacker` class + `createBinaryFontUnpacker()` factory. Public API is bit-identical to the class currently exported by `@suaveplan/font-compress` — same `unpack(data: Uint8Array): TypefaceFont` signature, same wire-format expectations, same `TypefaceFont` return shape from `@suaveplan/font-convert`. - ADDED: `FONT_DECOMPRESS_CODES` registry via `defineCodes("FONT_DECOMPRESS", [...])` with codes `UNPACK_FAILED` and `INVALID_DATA`. `FontDecompressError extends SuaveplanError`. - ADDED: `decodeString(data, offset)` utility — moved from `font-compress`'s `utils/encode-string.ts`. The encode-side helper stays in `font-compress`. - ADDED: BTF format constants module (duplicated from `font-compress` — see design.md). Both packages share the wire-format constants; duplicating 18 lines of magic numbers avoids a third "fonts-shared" package and keeps each package self-contained. - ADDED: Sibling `.md` docs for every implementation file. - ADDED: Subpath exports — `.`, `./unpacker/binary-font-unpacker`, `./error-codes`, `./utils/decode-string`, `./constants`, `./types/index` (re-exports `TypefaceFont` and `PathCommand` from `@suaveplan/font-convert` for caller convenience). - ADDED: Dependencies: `@suaveplan/error`, `@suaveplan/font-convert`, `tslib`. DevDeps: `@suaveplan/testing`, `vitest`, `vite`, `vite-plugin-dts`, `@vitest/coverage-istanbul`, `fast-check`, `typescript`, `@types/node`. ### `@suaveplan/font-compress` (modified, stays at v0.0.0) - REMOVED: `BinaryFontUnpacker` class and `createBinaryFontUnpacker` factory from the public surface. Their source files move to `@suaveplan/font-decompress`. - REMOVED: `src/unpacker/` directory in its entirety. - MODIFIED: `src/utils/encode-string.ts` — `decodeString` moves to `@suaveplan/font-decompress`. The file keeps `encodeString` only. - MODIFIED: `FONT_COMPRESS_CODES` registry — `UNPACK_FAILED` removed (no longer thrown by any path in this package). `PACK_FAILED` and `INVALID_DATA` retained. - MODIFIED: Subpath exports — `./unpacker/binary-font-unpacker` removed. - MODIFIED: Public surface declaration in `README.md` and `src/index.ts`. - The packer surface (`BinaryFontPacker`, `createBinaryFontPacker`), packer error codes, `parsePath` utility, `constants/`, and `FontCompressError` (with the trimmed code registry) remain in `font-compress`. ### Cross-cutting - ADDED: New canonical capability `font-decompress` at `openspec/specs/font-decompress/spec.md` (created at fold time). - MODIFIED: New canonical capability `font-compress` at `openspec/specs/font-compress/spec.md` — first canonical spec for the package, capturing the post-split shape. - MODIFIED: `STATUS.md` — flip `@suaveplan/font-decompress` from `–` to `✅`; update `@suaveplan/font-compress` description to reflect packer-only scope. - MODIFIED: `MANIFEST.md` — regenerated via `bun scripts/folder-reorg/generate-manifest.ts`. Both packages appear; `font-compress` dep count drops by one (decodeString removed); `font-decompress` lands with the same dep graph as `font-compress` plus tslib. ## Out of scope - **WOFF / WOFF2 / EOT decompression.** Upstream `@suaveplan/font-decompress` was BTF-format-only (Typeface.js binary), and that's what the package's name has come to mean inside genesis. WOFF/WOFF2 decoding lives in `@suaveplan/font-convert` already; consumers compose the two when they need to go from `.woff2` bytes through `TypefaceFont` to a rendered glyph. Adding WOFF/WOFF2 to `font-decompress` would duplicate `font-convert`. Reason: orthogonal capability, already covered. - **A third `@suaveplan/font-typeface` types-only package.** The shared `TypefaceFont` and `PathCommand` types already live in `@suaveplan/font-convert`; both packages depend on it. Introducing a third package adds a coordinate without enabling anything new. Reason: existing dep already solves the type-sharing problem. - **Bumping `@suaveplan/font-compress` past v0.0.0.** CLAUDE.md says packages stay at `0.1.0` until graduation to `1.0.0`. The split is a v0.0.0-to-v0.0.0 refactor; no version bump, no CHANGELOG entry. External consumers (none in this monorepo) discover the import-path change via the new spec and the npm registry's package listing. Reason: established versioning policy. ## Source mapping Both packages are sourced from the existing `packages/fonts/font-compress/` tree. Specifically: | Source file (current) | Destination | |---|---| | `packages/fonts/font-compress/src/unpacker/binary-font-unpacker/binary-font-unpacker.ts` | `packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/binary-font-unpacker.ts` | | `packages/fonts/font-compress/src/unpacker/binary-font-unpacker/index.ts` | `packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/index.ts` | | `packages/fonts/font-compress/src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts` | `packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts` | | `packages/fonts/font-compress/src/unpacker/binary-font-unpacker/binary-font-unpacker.md` | `packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/binary-font-unpacker.md` | | `decodeString` from `src/utils/encode-string.ts` | new `packages/fonts/font-decompress/src/utils/decode-string.ts` | | `src/constants/index.ts` (duplicated) | `packages/fonts/font-decompress/src/constants/index.ts` (verbatim copy; 19 lines of format magic) | | `UNPACK_FAILED` and `INVALID_DATA` from `src/error-codes.ts` | new `packages/fonts/font-decompress/src/error-codes.ts` (own namespace `FONT_DECOMPRESS`) | No bit-level changes to algorithm logic. The unpacker's class body, the `decodeString` helper, and the constants file move verbatim. Imports are updated to point at the new package-local error registry and constants module. ### Reference docs consulted - ✅ `packages/fonts/font-compress/src/` — full read of every TS file (unpacker, packer, utils, constants, error-codes, index) - ✅ `packages/fonts/font-compress/package.json` — exports + deps - ✅ `../suaveplan/packages/universal/font-decompress/` — upstream comparison; confirmed BTF-only scope and verified the upstream class is a stripped-down version of what genesis already ships - ✅ `STATUS.md` rows for `@suaveplan/font-compress` and `@suaveplan/font-decompress` - ✅ Monorepo grep for `@suaveplan/font-compress` consumers — zero TS importers; only docs reference it ## Impact ### Affected dependents None inside the monorepo (verified). External consumers (if any pre-publish) must rewrite imports of `BinaryFontUnpacker` / `createBinaryFontUnpacker` / the `unpacker/` subpath to point at `@suaveplan/font-decompress` instead of `@suaveplan/font-compress`. The class signature and behaviour are unchanged. ### Risk - **Low.** Pure code-relocation; algorithm logic untouched. The unpacker's existing 100% test coverage carries forward to the new package along with the source files. The packer's tests in `font-compress` stay there. New tests verify the moved-in error registry and the trimmed packer-side error registry. - **Wire-format constants duplicated.** If the BTF format ever changes, both packages need updates in lockstep. Documented in design.md; not a problem in practice because the format is locked at v0.0.0. ### Migration notes Single import-path rewrite for any external consumer of the unpacker: ```ts // Before import { BinaryFontUnpacker, createBinaryFontUnpacker } from "@suaveplan/font-compress"; // After import { BinaryFontUnpacker, createBinaryFontUnpacker } from "@suaveplan/font-decompress"; ``` Error code consumers that programmatically dispatch on `FONT_COMPRESS.UNPACK_FAILED` rewrite to `FONT_DECOMPRESS.UNPACK_FAILED`. Note the namespace change in addition to the package change.