Files
suaveplan/openspec/changes/compress-svg-completion/proposal.md
2026-05-27 23:29:53 +01:00

92 lines
11 KiB
Markdown

# Compress-SVG completion
**Package**: `@suaveplan/compress-svg`
**Status**: PROPOSED
**Type**: spec-conformance + feature
## Why
The genesis port of `@suaveplan/compress-svg` ships only the per-pass SVGO-style plugin core (`parser/svg`, `core/compress-svg`, `plugins/*`, `presets`, `types/common`) and a small set of in-place optimisation passes (cleanup-attrs, collapse-groups, convert-path-data, convert-transforms, mangle-ids, remove-comments, remove-doctype, remove-metadata). The legacy package at `../suaveplan/packages/universal/compress-svg/src` ships a substantially larger surface that is required by downstream tooling: a high-level `SvgOptimizer` façade, a structural-flattening pass (`SvgStructuralOptimizer`), cross-file content-addressed deduplication (`SvgDeduplicator`), a standalone `PathTokenizer` module, a CLI bin (`suaveplan-svg-optimize`), a binary archive format (`SvgPacker` + `SvgUnpacker` + `SvgArchiveBuilder` + `SvgArchiveUnpacker` totalling ~3 472 lines), and a browser unpacker (`SvgUnpackerBrowser` + `ArchiveUnpacker` ~1 592 lines) that consumes the archive without Node.js APIs.
Genesis policy is "no half-baked or mediocre libraries" (CLAUDE.md §Workflow rule 3) and "Preserve the full public surface during rewrites … never trim on 'probably unused' grounds" (CLAUDE.md §Workflow rule 5). The audit on 2026-05-02 confirmed every one of the seven gaps below by file-listing diff between `/home/ubuntu/web/suaveplan/packages/universal/compress-svg/src` and `/home/ubuntu/web/genesis/packages/docs/compress-svg/src`. This change closes those gaps in one coordinated pass so the package matches its upstream contract before any consumer wires it up.
## What changes
### `@suaveplan/compress-svg`
- ADDED: `SvgOptimizer` class — orchestrator that runs the configured plugin pipeline and surfaces a typed `OptimizationResult` (`{ originalBytes, optimizedBytes, pluginsApplied, warnings, durationMs, ratio }`); accepts a `SvgOptimizerConfig` selecting plugins, presets, and the dictionary/structural/dedup pre-passes; exposes `optimize(svg)` and `optimizeBatch(svgs)`.
- ADDED: `SvgStructuralOptimizer` class — flatten redundant `<g>` wrappers (single-child groups with no styling), collapse identical attribute sets, hoist common attributes to a parent `<g>`, merge consecutive `<path>` elements with identical fill/stroke/style, returns a `StructuralOptimizationResult` with per-rule counts.
- ADDED: `SvgDeduplicator` class — cross-file content-addressed deduplication; SHA-256 hashes the canonicalised inner content of repeating subtrees; emits `<symbol id="…">` definitions in a hoisted `<defs>` and replaces duplicates with `<use href="#…"/>`; returns a `DeduplicationReport` (`{ candidatesScanned, duplicatesFound, bytesSaved, symbolMap }`).
- ADDED: `tokenizePath(d)` and `formatTokens(tokens)` — standalone path tokeniser that emits a typed `PathToken[]` stream (`{ command, args, absolute }`) and round-trips back to the original `d` string.
- ADDED: `SvgPacker` and `SvgUnpacker` — binary archive format with version header, `SvgArchiveManifest`, and content-addressed payload table; pack/unpack by string SVG name → bytes; archive layout matches legacy on-disk format byte-for-byte for cross-tool compatibility.
- ADDED: `ArchiveUnpacker` and `SvgUnpackerBrowser` — browser-only unpackers that read the binary archive without Node.js APIs (`Buffer`, `fs`, `zlib`); use Web Streams and `globalThis.crypto`; ship under the `./browser` subpath export.
- ADDED: `bin/suaveplan-svg-optimize` — CLI command that takes glob patterns and writes optimised SVGs (in-place, to a directory, or to a binary archive); flags: `--in`, `--out`, `--preset`, `--archive`, `--dedupe`, `--structural`, `--dry-run`, `--quiet`, `--json`.
- ADDED: subpath exports `./optimizer`, `./structural`, `./deduplicator`, `./path-tokenizer`, `./archive`, `./browser`, `./cli`.
- ADDED: error codes registered on `@suaveplan/error` under `COMPRESS_SVG.*`: `OPTIMIZER_PIPELINE_FAILED`, `STRUCTURAL_INVALID_TREE`, `DEDUPE_HASH_COLLISION`, `PATH_TOKEN_PARSE_FAILED`, `ARCHIVE_VERSION_MISMATCH`, `ARCHIVE_CHECKSUM_FAILED`, `ARCHIVE_PAYLOAD_NOT_FOUND`, `BROWSER_UNSUPPORTED_ENVIRONMENT`, `CLI_INVALID_GLOB`.
- MODIFIED: `package.json` `bin` field — adds `suaveplan-svg-optimize`. `exports` map gains the seven new subpaths.
## Out of scope
- **Server-side rasterisation to PNG/JPG** — belongs in `@suaveplan/compress-image`; rasterisation requires a different dependency surface (`sharp` adapter or canvas) and the SVG package must remain a pure-string transformer.
- **Sprite-sheet generation** — separate concern (atlas packing, layout, viewBox calc); deferred to a future `@suaveplan/svg-sprite` package if needed.
- **Lossy colour quantisation beyond the existing plugin set** — the existing `cleanup-attrs` + `convert-path-data` passes already handle precision; introducing palette quantisation would require a colour-distance algorithm and is not what callers ask for.
- **Drop-in SVGO-plugin compatibility** — the genesis plugin model is intentionally simpler than upstream SVGO; mapping arbitrary SVGO plugins is its own change.
## Source mapping
This change derives from `../suaveplan/packages/universal/compress-svg/src`. There is no dedicated `enterprise/compress-svg.md` audit doc upstream, so the source mapping cites file:line ranges directly.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `SvgOptimizer` façade | `optimizer/SvgOptimizer.ts` | L1-L483 | Verbatim port; replace Node `zlib` with injectable `CompressorAdapter` for browser-safe build |
| `OptimizationResult` and `SvgDictionary` types | `optimizer/SvgOptimizer.types.ts` | L1-L27 | Verbatim |
| `SvgStructuralOptimizer` | `structural/SvgStructuralOptimizer.ts` | L1-L458 | Verbatim |
| `SvgStructuralOptimizer` types | `structural/SvgStructuralOptimizer.types.ts` | (full file) | Verbatim |
| `SvgDeduplicator` (cross-file dedup, `<use>` references, `DeduplicationReport`) | `deduplicator/SvgDeduplicator.ts` | L1-L398 | Verbatim port; replace `crypto.createHash('sha256')` with `@suaveplan/crypto-hash` `sha256` for browser/server parity |
| `tokenizePath` / `formatTokens` / `PathToken` | `PathTokenizer.ts` | L1-L559 | Verbatim |
| `SvgPacker` / `SvgUnpacker` (binary archive) | `SvgPacker.ts` | L1-L2931 | Verbatim port; the on-disk layout matches legacy byte-for-byte for cross-tool compatibility |
| `SvgArchiveBuilder` | `archive/SvgArchiveBuilder.ts` | L1-L177 | Verbatim |
| `SvgArchiveUnpacker` | `archive/SvgArchiveUnpacker.ts` | L1-L182 | Verbatim |
| `ArchiveUnpacker` (browser) | `browser/SvgArchiveUnpackerBrowser.ts` | L1-L440 | Verbatim |
| `SvgUnpackerBrowser` | `browser/SvgUnpackerBrowser.ts` | L1-L1152 | Verbatim |
| `suaveplan-svg-optimize` CLI bin | `cli.ts` | L1-L271 | Verbatim |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `COMPRESS_SVG.*` error codes registered on `@suaveplan/error` | Genesis-wide policy that every thrown error route through `@suaveplan/error` (CLAUDE.md §Workflow rule 2). Upstream throws plain `Error`. |
| Replace Node `zlib` with injectable `CompressorAdapter` in `SvgOptimizer` | Genesis ships universal-tier packages where browser tier matters; upstream binds to Node `zlib` directly. The adapter keeps the API identical while enabling the `./browser` subpath build. |
| Replace `node:crypto` `createHash` with `@suaveplan/crypto-hash` `sha256` in `SvgDeduplicator` | Same browser/server parity reason; keeps a single SHA-256 implementation across the monorepo. |
| `./optimizer`, `./structural`, `./deduplicator`, `./path-tokenizer`, `./archive`, `./browser`, `./cli` subpath exports | Genesis-wide subpath-per-feature rule (AGENTS.md §6). Upstream uses a single barrel; tree-shaking parity demands subpath splits. |
## Verification approach
Each spec delta requirement carries a `**Verified by:**` line pointing at concrete `it(...)` titles. The test files that will exist after this change ships:
- `packages/docs/compress-svg/src/optimizer/svg-optimizer.test.ts` — pipeline orchestration, plugin selection, `OptimizationResult` shape, batch mode
- `packages/docs/compress-svg/src/optimizer/svg-optimizer.spec-conformance.test.ts` — injectable `CompressorAdapter` round-trip; pipeline-failed error code; OTel histogram
- `packages/docs/compress-svg/src/structural/svg-structural-optimizer.test.ts` — flatten single-child `<g>`; collapse identical attribute sets; hoist common attrs; merge consecutive paths
- `packages/docs/compress-svg/src/structural/svg-structural-optimizer.spec-conformance.test.ts` — invalid-tree error code; per-rule counter accuracy
- `packages/docs/compress-svg/src/deduplicator/svg-deduplicator.test.ts` — cross-file dedup, `<symbol>`/`<use>` emission, hash-collision rejection
- `packages/docs/compress-svg/src/deduplicator/svg-deduplicator.spec-conformance.test.ts` — `DeduplicationReport` shape; injected hash function honoured; collision error code
- `packages/docs/compress-svg/src/path-tokenizer/path-tokenizer.test.ts` — every SVG path command (`MmLlHhVvCcSsQqTtAaZz`); round-trip property test
- `packages/docs/compress-svg/src/path-tokenizer/path-tokenizer.spec-conformance.test.ts` — malformed-path error code; absolute/relative flag preserved
- `packages/docs/compress-svg/src/archive/svg-packer.test.ts` — binary header, manifest, payload table, content-addressed dedup, version round-trip
- `packages/docs/compress-svg/src/archive/svg-packer.spec-conformance.test.ts` — version-mismatch error; checksum-failed error; payload-not-found error
- `packages/docs/compress-svg/src/browser/svg-unpacker-browser.test.ts` — browser unpacker round-trip with the same fixtures as `svg-packer.test.ts`
- `packages/docs/compress-svg/src/browser/svg-unpacker-browser.spec-conformance.test.ts` — Node `Buffer`/`fs`/`zlib` not referenced (static import scan); `BROWSER_UNSUPPORTED_ENVIRONMENT` thrown when `globalThis.crypto` is missing
- `packages/docs/compress-svg/src/cli/cli.test.ts` — argument parsing, glob expansion, in-place vs out-dir vs archive modes, dry-run, JSON reporter
- `packages/docs/compress-svg/src/cli/cli.spec-conformance.test.ts` — invalid-glob error code; exit codes match POSIX conventions
- `tests/dep-graph.test.ts` — no Node.js API import on the `./browser` subpath; subpath exports resolve
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/compress-svg` to enumerate. Result of `grep -rl "@suaveplan/compress-svg" /home/ubuntu/web/genesis/packages/ --include="package.json"` on 2026-05-02 returned only the package itself (`packages/docs/compress-svg/package.json`). No in-repo dependents.
- No dependents (verified by `grep -rl "@suaveplan/compress-svg" /home/ubuntu/web/genesis/packages/ --include="package.json"` returning only the package's own `package.json`).
This change is **additive only** — every existing export is preserved, and the new exports land under new subpaths (`./optimizer`, `./structural`, `./deduplicator`, `./path-tokenizer`, `./archive`, `./browser`, `./cli`) plus root-level re-exports. The CLI bin is a new entry; existing users who depend on the per-pass plugin core continue to compile unchanged.