Initial Commit
This commit is contained in:
105
openspec/specs/font-decompress/spec.md
Normal file
105
openspec/specs/font-decompress/spec.md
Normal file
@@ -0,0 +1,105 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec: font-decompress
|
||||
|
||||
**Package**: `@suaveplan/font-decompress`
|
||||
**Status**: SHIPPED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/font-decompress` reverses the BTF (Binary TypeFace) wire format produced by `@suaveplan/font-compress::BinaryFontPacker`. It owns the runtime side of the BTF pipeline: build tooling packs once, then ships the compact `Uint8Array`; renderers (Three.js `FontLoader`, R3F text components, anything typeface.js-compatible) unpack on demand. The package is universal-tier — runs unchanged in Node, Bun, browsers, and Web Workers — and depends only on `@suaveplan/error` and `@suaveplan/font-convert` (for the `TypefaceFont` type).
|
||||
|
||||
## Requirements
|
||||
|
||||
### 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-decompress/src/` runs
|
||||
- **THEN** the command produces zero matches
|
||||
|
||||
**Verified by:** `src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts::"tier purity: no node/bun imports"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: FONT_DECOMPRESS_CODES error registry
|
||||
|
||||
The package SHALL declare an error-code registry via `defineCodes("FONT_DECOMPRESS", [...])` containing exactly `UNPACK_FAILED` and `INVALID_DATA`. Every public error path SHALL throw a `FontDecompressError extends SuaveplanError<FontDecompressErrorCode>` carrying one of those codes. The class SHALL NOT throw raw `Error` instances from any public method.
|
||||
|
||||
#### Scenario: every code is reachable from a real call site
|
||||
|
||||
- **WHEN** the test suite exercises malformed wire bytes (truncated header, dictionary length overrun) and a successful unpack call
|
||||
- **THEN** each path raises (or returns) values consistent with the documented contract; the `UNPACK_FAILED` and `INVALID_DATA` codes are each reachable
|
||||
|
||||
#### Scenario: SuaveplanError contract preserved
|
||||
|
||||
- **WHEN** a `FontDecompressError` is constructed with a `cause` (e.g. wrapping a `RangeError` from `DataView.getUint32`)
|
||||
- **THEN** `error.cause` is the original; `error instanceof SuaveplanError` is true
|
||||
|
||||
**Verified by:** `src/error-codes.test.ts::"every FONT_DECOMPRESS.* code is reachable from a call site"` AND `src/error-codes.test.ts::"FontDecompressError preserves cause"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: BinaryFontUnpacker public API
|
||||
|
||||
The package SHALL provide `class BinaryFontUnpacker` with method `unpack(data: Uint8Array): TypefaceFont` and a `createBinaryFontUnpacker(): BinaryFontUnpacker` factory function. `TypefaceFont` is imported from `@suaveplan/font-convert`. The class SHALL parse the BTF wire format produced by `@suaveplan/font-compress::BinaryFontPacker` and produce a `TypefaceFont` object that round-trips that pack-then-unpack pair byte-for-byte to JSON-shape equivalence.
|
||||
|
||||
#### Scenario: round-trip BTF → TypefaceFont
|
||||
|
||||
- **WHEN** a known-vector BTF buffer (produced offline by `BinaryFontPacker.pack(...)`) is passed to `unpack(...)`
|
||||
- **THEN** the resulting `TypefaceFont` has the expected `familyName`, `cssFontWeight`, `cssFontStyle`, `boundingBox`, `glyphs` (per-character `ha`/`x_min`/`x_max`/`o` path string), and metrics
|
||||
|
||||
#### Scenario: truncated buffer raises UNPACK_FAILED or INVALID_DATA
|
||||
|
||||
- **WHEN** `unpack(...)` is called on a buffer truncated mid-dictionary
|
||||
- **THEN** a `FontDecompressError` is thrown with one of the registry's codes
|
||||
|
||||
**Verified by:** `src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts::"round-trip pack/unpack preserves font shape"` AND `src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts::"truncated buffer raises FontDecompressError"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: decodeString utility
|
||||
|
||||
The package SHALL provide a `decodeString(data: Uint8Array, offset: number): { value: string; offset: number }` helper used by the unpacker to read length-prefixed UTF-8 strings from the BTF header. The function SHALL return the decoded string plus the offset of the next byte after the string. Malformed length prefixes (negative, beyond buffer end) SHALL raise `FontDecompressError` with `code === "FONT_DECOMPRESS.INVALID_DATA"`.
|
||||
|
||||
#### Scenario: round-trip a UTF-8 string
|
||||
|
||||
- **WHEN** `decodeString` is called on bytes produced by the matching `encodeString` (from `@suaveplan/font-compress`) for `"Inter Variable"`
|
||||
- **THEN** the returned `value` equals `"Inter Variable"` and `offset` points past the encoded bytes
|
||||
|
||||
#### Scenario: rejects length-overrun
|
||||
|
||||
- **WHEN** `decodeString` is called on a buffer whose length prefix exceeds the buffer length
|
||||
- **THEN** a `FontDecompressError` with `code === "FONT_DECOMPRESS.INVALID_DATA"` is thrown
|
||||
|
||||
**Verified by:** `src/utils/decode-string.test.ts::"round-trip a UTF-8 string"` AND `src/utils/decode-string.test.ts::"rejects length overrun"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports tree-shake per feature module
|
||||
|
||||
`package.json` SHALL declare subpath exports for `.` (root barrel), `./error-codes`, `./constants`, `./utils/decode-string`, `./unpacker/binary-font-unpacker`. Each subpath SHALL resolve to a built `.js` + `.d.ts` pair under `dist/`. `sideEffects` SHALL be `false`.
|
||||
|
||||
#### Scenario: subpath import resolves to feature module
|
||||
|
||||
- **WHEN** a consumer writes `import { BinaryFontUnpacker } from "@suaveplan/font-decompress/unpacker/binary-font-unpacker"`
|
||||
- **THEN** the import resolves to the dedicated unpacker module, not the root barrel
|
||||
|
||||
**Verified by:** `src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts::"subpath export resolves to unpacker module"`
|
||||
|
||||
---
|
||||
|
||||
### 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-decompress/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`
|
||||
Reference in New Issue
Block a user