Initial Commit
This commit is contained in:
151
openspec/archive/2026-05-17-split-font-compress/design.md
Normal file
151
openspec/archive/2026-05-17-split-font-compress/design.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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.
|
||||
100
openspec/archive/2026-05-17-split-font-compress/proposal.md
Normal file
100
openspec/archive/2026-05-17-split-font-compress/proposal.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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<FontDecompressErrorCode>`.
|
||||
- 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.
|
||||
@@ -0,0 +1,109 @@
|
||||
<!-- 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`
|
||||
@@ -0,0 +1,101 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: font-decompress (font-decompress)
|
||||
|
||||
**Package**: `@suaveplan/font-decompress`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED 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`
|
||||
72
openspec/archive/2026-05-17-split-font-compress/tasks.md
Normal file
72
openspec/archive/2026-05-17-split-font-compress/tasks.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Tasks — split-font-compress
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `openspec/AGENTS.md` §0, §0a, §0c, §6a, §7, §13, §14
|
||||
- [x] 1.2 Audit current `packages/fonts/font-compress/src/` — every TS file enumerated; cross-module imports mapped
|
||||
- [x] 1.3 Audit current consumers monorepo-wide — verified zero TS importers of `@suaveplan/font-compress`
|
||||
- [x] 1.4 Draft `proposal.md` covering scope, source mapping, parity, affected dependents
|
||||
- [x] 1.5 Draft `design.md` covering file layout for both packages, constant duplication, error-code split
|
||||
- [x] 1.6 Draft `specs/font-decompress/spec.md` — new capability, post-split shape
|
||||
- [x] 1.7 Draft `specs/font-compress/spec.md` — first canonical, post-split shape
|
||||
- [x] 1.8 Every requirement carries `**Verified by:**` line at draft time
|
||||
- [x] 1.9 `openspec validate split-font-compress` clean
|
||||
|
||||
## 2. Create @suaveplan/font-decompress
|
||||
|
||||
- [x] 2.1 `mkdir -p packages/fonts/font-decompress/{src/{constants,utils,unpacker/binary-font-unpacker},tests/setup}`
|
||||
- [x] 2.2 Write `package.json` v0.0.0 with subpath exports and deps
|
||||
- [x] 2.3 Write `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `tests/setup/polyfills.ts`, `README.md` (≥ 200 words, all required sections)
|
||||
- [x] 2.4 Copy `src/constants/index.ts` verbatim from font-compress
|
||||
- [x] 2.5 Author `src/error-codes.ts` — `FONT_DECOMPRESS_CODES = { UNPACK_FAILED, INVALID_DATA }`, `FontDecompressError extends SuaveplanError<FontDecompressErrorCode>`
|
||||
- [x] 2.6 Author `src/error-codes.test.ts` — every code reachable, `cause` chain preserved
|
||||
- [x] 2.7 Author `src/error-codes.md`
|
||||
- [x] 2.8 Move `decodeString` from font-compress's `utils/encode-string.ts` to new `src/utils/decode-string.ts` (separate file per CLAUDE.md per-feature layout)
|
||||
- [x] 2.9 Author `src/utils/decode-string.test.ts` — round-trip with known vectors
|
||||
- [x] 2.10 Author `src/utils/decode-string.md`
|
||||
- [x] 2.11 Move `binary-font-unpacker.ts` (+ test, + .md, + index.ts) from font-compress's `unpacker/binary-font-unpacker/` to new `src/unpacker/binary-font-unpacker/`
|
||||
- [x] 2.12 Update internal imports in the moved files to point at the new package-local error registry and constants module
|
||||
- [x] 2.13 Author `src/index.ts` root barrel with explicit named exports
|
||||
- [x] 2.14 `bun install` to register the new workspace
|
||||
- [x] 2.15 `bunx biome check --write --unsafe src/` clean
|
||||
- [x] 2.16 `bunx tsc --noEmit -p tsconfig.lib.json` clean
|
||||
- [x] 2.17 `bunx vitest run --coverage` with 100/100/100/100 Istanbul thresholds clean
|
||||
- [x] 2.18 `bun run ../../../scripts/docs-check.ts .` 100%
|
||||
|
||||
## 3. Trim @suaveplan/font-compress
|
||||
|
||||
- [x] 3.1 Delete `src/unpacker/` directory in font-compress
|
||||
- [x] 3.2 Trim `src/utils/encode-string.ts` to `encodeString` only; update test file accordingly; update sibling `.md`
|
||||
- [x] 3.3 Trim `src/error-codes.ts` — remove `UNPACK_FAILED` from `FONT_COMPRESS_CODES`. Update the error-codes test and `.md`.
|
||||
- [x] 3.4 Remove the `BinaryFontUnpacker` + `createBinaryFontUnpacker` exports from `src/index.ts`
|
||||
- [x] 3.5 Remove the `./unpacker/binary-font-unpacker` subpath export from `package.json`
|
||||
- [x] 3.6 Update `README.md` to reflect packer-only scope (≥ 200 words, all required sections)
|
||||
- [x] 3.7 `bunx biome check --write --unsafe src/` clean
|
||||
- [x] 3.8 `bunx tsc --noEmit -p tsconfig.lib.json` clean
|
||||
- [x] 3.9 `bunx vitest run --coverage` with 100/100/100/100 Istanbul thresholds clean
|
||||
- [x] 3.10 `bun run ../../../scripts/docs-check.ts .` 100%
|
||||
|
||||
## 4. Cross-package verification
|
||||
|
||||
- [x] 4.1 `bunx turbo lint typecheck test --filter=@suaveplan/font-compress` green
|
||||
- [x] 4.2 `bunx turbo lint typecheck test --filter=@suaveplan/font-decompress` green
|
||||
- [x] 4.3 `bunx turbo test:coverage --filter=@suaveplan/font-compress` 100/100/100/100
|
||||
- [x] 4.4 `bunx turbo test:coverage --filter=@suaveplan/font-decompress` 100/100/100/100
|
||||
- [x] 4.5 `bunx turbo build --filter=@suaveplan/font-compress` succeeds; dist contains the trimmed subpath set
|
||||
- [x] 4.6 `bunx turbo build --filter=@suaveplan/font-decompress` succeeds; dist contains advertised subpaths
|
||||
- [x] 4.7 `grep -rE "from ['\"](vitest|bun:test)['\"]" packages/fonts/font-compress/src/ packages/fonts/font-decompress/src/` → zero matches (rule §7)
|
||||
- [x] 4.8 `grep -rE "^export \*" packages/fonts/font-compress/src/ packages/fonts/font-decompress/src/` → only root barrels and exempt forms (rule §6a)
|
||||
- [x] 4.9 `grep -rE "from ['\"](node:|bun:|fs|path|stream|net|http)['\"]" packages/fonts/font-compress/src/ packages/fonts/font-decompress/src/` → zero matches (universal tier)
|
||||
- [x] 4.10 `grep -rE "@deprecated|TODO|FIXME|ts-ignore|ts-expect-error|\\.skip|\\.only|\\bany\\b|console\\.log" packages/fonts/font-compress/src/ packages/fonts/font-decompress/src/ --include="*.ts"` → zero matches
|
||||
|
||||
## 5. Fold + archive
|
||||
|
||||
- [x] 5.1 Fold `specs/font-compress/spec.md` into `openspec/specs/font-compress/spec.md` (new canonical for an existing package — converts ADDED → Requirements, prepends Purpose)
|
||||
- [x] 5.2 Fold `specs/font-decompress/spec.md` into `openspec/specs/font-decompress/spec.md` (new canonical for the new package)
|
||||
- [x] 5.3 `openspec validate font-compress` and `openspec validate font-decompress` both clean
|
||||
- [x] 5.4 Move `openspec/changes/split-font-compress/` → `openspec/archive/$(date +%Y-%m-%d)-split-font-compress/`
|
||||
- [x] 5.5 `bun scripts/folder-reorg/generate-manifest.ts` — `MANIFEST.md` reflects both packages
|
||||
- [x] 5.6 `STATUS.md` rows updated: `@suaveplan/font-compress` description trimmed to packer-only; `@suaveplan/font-decompress` flipped from `–` to `✅`
|
||||
- [x] 5.7 Commit per CLAUDE.md style; no push without explicit user approval
|
||||
Reference in New Issue
Block a user