Files
suaveplan/openspec/archive/2026-05-17-split-font-compress/specs/font-compress/spec.md
2026-05-27 23:29:53 +01:00

110 lines
6.3 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# 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`