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

8.9 KiB

Capability Spec Delta: font-compress (font-compress-cli)

Package: @suaveplan/font-compress Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time. No exceptions. (See openspec/AGENTS.md Section 13.)

ADDED Requirements

Requirement: suaveplan-font-compress bin entry

The package SHALL register a bin entry named suaveplan-font-compress mapping to ./dist/bin/suaveplan-font-compress.js. The compiled bin script SHALL start with #!/usr/bin/env node and SHALL be executable. Invoking it from a consumer's node_modules/.bin/ SHALL run the CLI's main function with process.argv.slice(2).

Scenario: bin field declared in package.json

  • WHEN the package's package.json is parsed
  • THEN bin["suaveplan-font-compress"] === "./dist/bin/suaveplan-font-compress.js"

Scenario: shebang preserved in compiled output

  • WHEN dist/bin/suaveplan-font-compress.js is read after bunx turbo build --filter=@suaveplan/font-compress
  • THEN the first line is exactly #!/usr/bin/env node

Verified by: src/bin/suaveplan-font-compress.test.ts::"package.json bin field maps suaveplan-font-compress to dist/bin entry" AND src/bin/suaveplan-font-compress.spec-conformance.test.ts::"compiled bin starts with node shebang"


Requirement: Argument parser with help and version

The package SHALL accept the following flags:

  • positional <input>
  • --input <path> / -i <path>
  • --output <path> / -o <path>
  • --format=btf|json
  • --force / -f
  • --recursive / -r
  • --no-recursive
  • --preserve-structure
  • --no-preserve-structure
  • --help / -h
  • --version / -v

--help SHALL print usage to stdout and exit 0. --version SHALL print the package version (read from package.json) and exit 0. Unknown flags SHALL print an error to stderr and exit 2. Conflicting flags (e.g. --recursive and --no-recursive together) SHALL print an error to stderr and exit 2.

Scenario: --help short-circuits without doing FS work

  • WHEN the bin is invoked with ["--help"]
  • THEN stdout contains the string "USAGE" AND the exit code is 0 AND no FS operations are performed (verified by spy on node:fs/promises)

Scenario: --version reads from package.json

  • WHEN the bin is invoked with ["--version"]
  • THEN stdout contains the same version string as JSON.parse(readFileSync('package.json')).version AND the exit code is 0

Scenario: unknown flag exits 2

  • WHEN the bin is invoked with ["--no-such-flag"]
  • THEN stderr contains the string "Unknown option" AND the exit code is 2

Scenario: conflicting recursive flags exit 2

  • WHEN the bin is invoked with ["./fonts", "--recursive", "--no-recursive"]
  • THEN the exit code is 2 AND stderr describes the conflict

Verified by: src/bin/suaveplan-font-compress.test.ts::"--help short-circuits with exit 0" AND src/bin/suaveplan-font-compress.test.ts::"--version prints package version with exit 0" AND src/bin/suaveplan-font-compress.test.ts::"unknown flag exits 2" AND src/bin/suaveplan-font-compress.test.ts::"conflicting recursive flags exit 2"


Requirement: Input dispatcher selects convert + pack vs pack-only

The package SHALL inspect the input path's extension. For .ttf, .otf, .woff, .woff2 (and directories containing them when --recursive) the bin SHALL call @suaveplan/font-convert::FontConverter (single file) or BatchConverter (directory) to produce a TypefaceFont, then call BinaryFontPacker.pack(typefaceFont) to emit a Uint8Array, then write the .btf file. For .json inputs (a previously-converted typeface JSON) the bin SHALL skip conversion and pack directly.

Scenario: TTF input runs convert then pack

  • WHEN the bin is invoked with a single .ttf file path
  • THEN FontConverter.convert is called once AND BinaryFontPacker.pack is called once with the converter's output AND a .btf file is written next to the input (or under --output if specified)

Scenario: JSON input runs pack only

  • WHEN the bin is invoked with a single .json file path containing a valid TypefaceFont
  • THEN FontConverter.convert is not called AND BinaryFontPacker.pack is called once with the parsed JSON AND a .btf file is written

Scenario: directory input uses BatchConverter

  • WHEN the bin is invoked with a directory path containing two .ttf files at --recursive
  • THEN BatchConverter is called once with the directory AND BinaryFontPacker.pack is called twice AND two .btf files are written under --output

Verified by: src/bin/suaveplan-font-compress.test.ts::"TTF input runs convert then pack" AND src/bin/suaveplan-font-compress.test.ts::"JSON input runs pack only" AND src/bin/integration/cli-roundtrip.test.ts::"directory of TTFs produces matching BTF outputs"


Requirement: --format=json emits typeface JSON instead of BTF

The package SHALL accept --format=json to emit the converter's TypefaceFont as JSON instead of running it through BinaryFontPacker. This mode SHALL write <basename>.json files. JSON-input + --format=json SHALL be a no-op pass-through (read JSON, validate, write to output path).

Scenario: --format=json writes typeface JSON

  • WHEN the bin is invoked with a .ttf input and --format=json
  • THEN a .json file is written AND BinaryFontPacker.pack is not called AND the JSON parses as a valid TypefaceFont

Scenario: default format is btf

  • WHEN the bin is invoked without --format
  • THEN --format=btf is the effective default AND .btf files are written

Verified by: src/bin/suaveplan-font-compress.test.ts::"--format=json writes typeface JSON instead of BTF" AND src/bin/suaveplan-font-compress.test.ts::"default format is btf"


Requirement: Structured exit codes

The package SHALL return distinct exit codes:

  • 0 — all inputs processed successfully
  • 1 — one or more inputs failed during conversion or packing (per-file errors are isolated; the batch continues)
  • 2 — invalid CLI arguments (unknown flag, missing required positional, conflicting flags)
  • 3 — the input path does not exist on disk

Scenario: missing input path exits 3

  • WHEN the bin is invoked with a positional input that does not exist
  • THEN the exit code is 3 AND stderr contains the string "does not exist"

Scenario: per-file failure exits 1, not 2

  • WHEN the bin is invoked over a directory where one TTF file is corrupt and three are valid
  • THEN the exit code is 1 AND three .btf files are written AND the corrupt file's error is captured to stderr

Verified by: src/bin/suaveplan-font-compress.test.ts::"missing input path exits 3" AND src/bin/integration/cli-roundtrip.test.ts::"corrupt TTF in directory yields exit 1 with siblings still written"


Requirement: FONT_COMPRESS.CLI_INVALID_ARGS error code

The package SHALL register FONT_COMPRESS.CLI_INVALID_ARGS in FONT_COMPRESS_CODES (the stable error-code registry). Argument-parsing failures SHALL throw FontCompressError with this code (caught by main's top-level handler, which maps it to exit 2). Plain Error SHALL NOT be thrown from the CLI argument-parsing path.

Scenario: code is registered

  • WHEN FONT_COMPRESS_CODES is enumerated
  • THEN the array includes "FONT_COMPRESS.CLI_INVALID_ARGS"

Scenario: bad args throw the coded error

  • WHEN the bin is invoked with an unknown flag and the throw is intercepted (in-process driver, not subprocess)
  • THEN a FontCompressError is thrown with code === "FONT_COMPRESS.CLI_INVALID_ARGS" AND context.flag === "--no-such-flag"

Verified by: src/error-codes.test.ts::"FONT_COMPRESS_CODES includes CLI_INVALID_ARGS" AND src/bin/suaveplan-font-compress.spec-conformance.test.ts::"argument-parser throws FontCompressError with CLI_INVALID_ARGS"


Requirement: Progress reporting and summary

The package SHALL print per-file progress lines while processing a batch (one line per converted file with the family name and compression ratio) and a summary block at the end (total files, succeeded, failed, total original bytes, total compressed bytes, compression ratio, output path). Progress lines SHALL go to stdout. Errors SHALL go to stderr.

Scenario: per-file progress lines printed

  • WHEN the bin is invoked over a directory with three valid TTFs
  • THEN stdout contains three lines starting with the per-file marker (✓ or equivalent) AND each line names the family

Scenario: summary block printed at end

  • WHEN the same invocation completes
  • THEN stdout ends with a summary block containing Total fonts: 3, Converted: 3, Errors: 0 AND a compression-ratio line

Verified by: src/bin/integration/cli-roundtrip.test.ts::"per-file progress lines and summary block printed"