Initial Commit
This commit is contained in:
@@ -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`
|
||||
Reference in New Issue
Block a user