107 lines
5.2 KiB
Markdown
107 lines
5.2 KiB
Markdown
# Proposal — Migrate @suaveplan/pdf to genesis (multi-chunk)
|
||
|
||
- **Design**: ./design.md
|
||
- **Spec delta**: ./specs/pdf/spec.md
|
||
- **Status**: PROPOSED (Chunk A ships in this change; Chunks B–E follow)
|
||
|
||
## Why
|
||
|
||
Upstream `@suaveplan/pdf` (`/mnt/s/web/suaveplan/packages/server/pdf/`, ~2 100
|
||
LOC implementation across eleven feature modules, ~7 300 LOC of tests) ships a
|
||
working server-tier PDF 1.4 generator built on the 14 standard PDF fonts and a
|
||
hand-rolled binary writer. It exports a fluent `DocumentBuilder`, four element
|
||
primitives (text, table, image, divider), a layout engine with cursor +
|
||
flow-block tracking, a style resolver with theme merging, three high-level
|
||
templates (invoice, receipt, report), unit + colour utilities, and a React
|
||
preview component. The genesis `enterprise/pdf.md` audit calls out the
|
||
cross-cutting gaps shared with every other migration target:
|
||
|
||
1. **No typed error registry.** Every failure path raises plain `Error`
|
||
instances (`new Error("Invalid hex color: …")`, `Unknown unit`, etc.) so
|
||
consumers cannot classify failures without parsing message strings.
|
||
2. **No OpenTelemetry surface.** PDF generation is a load-bearing primitive
|
||
(invoices, receipts, financial reports) yet emits zero counters,
|
||
histograms, or spans, leaving operators blind to throughput, latency, and
|
||
failure rates.
|
||
3. **No Zod-validated public boundary.** Hex strings, unit strings, element
|
||
props, document definitions, and template inputs are accepted as plain TS
|
||
types — invalid input surfaces as cryptic runtime errors deep in the
|
||
render pipeline.
|
||
4. **No AbortSignal support.** Rendering a large report cannot be cancelled.
|
||
5. **No injectable seams.** The PDF writer hard-codes `TextEncoder`/`Date`
|
||
so tests must rely on real wall-clock metadata.
|
||
|
||
## What
|
||
|
||
Land `@suaveplan/pdf@0.1.0` at `packages/devtools/docs/pdf/` (server-tier;
|
||
allocation confirmed in `scripts/folder-reorg/assignment.yml` line 261 under
|
||
the `docs` plugin) across five chunks. Each chunk is an independently
|
||
shippable subset with its own 100 / 100 / 100 / 100 coverage gate. The
|
||
chunks are sized so a single agent session can land each one with full
|
||
enterprise additions (error codes, Zod, OTel, AbortSignal, `.md` per source
|
||
file, real round-trip tests).
|
||
|
||
### Chunk A — Foundation (this change)
|
||
|
||
Pure utilities + the cross-cutting wiring. Zero external runtime deps beyond
|
||
`@suaveplan/error`, `@suaveplan/opentelemetry`, `@suaveplan/types`, `tslib`,
|
||
and `zod`. No PDF I/O. Easy to verify to 100 % in one pass.
|
||
|
||
- `types/` — `PageSize`, `PAGE_DIMENSIONS`, `PageOrientation`, `Margins`,
|
||
`DEFAULT_MARGINS`, `DocumentMetadata`, `PageConfig`, `DEFAULT_PAGE_CONFIG`,
|
||
`DocumentDefinition`, `PageDefinition`, `ElementNode`, `RgbColor`,
|
||
`PdfColor`, `Unit`.
|
||
- `error-codes/` — `PDF_CODES` via `defineCodes("PDF", [...])` (`INVALID_UNIT`,
|
||
`INVALID_HEX_COLOR`, `INVALID_RGB`, `INVALID_PAGE_CONFIG`,
|
||
`INVALID_DOCUMENT`, `INVALID_ELEMENT`, `INVALID_TEMPLATE_INPUT`,
|
||
`RENDER_FAILED`, `LAYOUT_OVERFLOW`, `IMAGE_DECODE_FAILED`, `FONT_UNKNOWN`,
|
||
`WRITER_DISPOSED`, `ABORTED`) + `PdfError` class.
|
||
- `schemas/` — Zod schemas (`UnitSchema`, `HexColorSchema`, `RgbColorSchema`,
|
||
`MarginsSchema`, `PageConfigSchema`, `DocumentMetadataSchema`).
|
||
- `telemetry/` — OTel counters (`pdf_documents_rendered_total`,
|
||
`pdf_pages_emitted_total`, `pdf_render_errors_total`,
|
||
`pdf_validation_failures_total`) + histograms (`pdf_render_duration_ms`,
|
||
`pdf_document_size_bytes`) on the `@suaveplan/pdf` meter, plus recording
|
||
helpers.
|
||
- `units/` — `toPoints`, `fromPoints`, `convert`, `parseUnit`, `formatUnit`
|
||
raising `PdfError(PDF_CODES.INVALID_UNIT)` on bad input.
|
||
- `colors/` — `hexToRgb`, `rgbToPdfColor`, `hexToPdfColor`, `rgbToHex`,
|
||
`lighten`, `darken`, `COLORS`, `isTransparent` raising
|
||
`PdfError(PDF_CODES.INVALID_HEX_COLOR)` on bad input.
|
||
|
||
### Chunk B — Low-level writer + content streams (next change)
|
||
|
||
- `pdf-writer/` — Real PDF 1.4 binary writer with deterministic byte output
|
||
(injectable `Clock` for metadata), font registry for the 14 standard
|
||
fonts, indirect-object + xref + trailer assembly, `escapePdfString`,
|
||
`fmt`, AbortSignal-aware `generate`.
|
||
- `content-stream/` — Encoder for text-positioning, line-drawing, and
|
||
state-save / state-restore operators.
|
||
|
||
### Chunk C — Elements
|
||
|
||
- `text-element/`, `table-element/`, `image-element/`, `divider-element/` —
|
||
each with Zod input schemas, OTel counters tagged by element kind, and
|
||
round-trip verification via `pdf-parse`.
|
||
|
||
### Chunk D — Layout, styles, document builder, renderer
|
||
|
||
- `layout-engine/`, `style-resolver/`, `document-builder/`, `renderer/` —
|
||
cursor tracking, theme merging, fluent builder, top-level
|
||
`renderDocument(definition, opts)`.
|
||
|
||
### Chunk E — Templates + React preview
|
||
|
||
- `invoice-template/`, `receipt-template/`, `report-template/`,
|
||
`pdf-preview/` (React 19 component with peer-only React dep).
|
||
|
||
## Out of scope
|
||
|
||
- PDF/UA and PDF/A profiles — tracked in a follow-up enterprise plan.
|
||
- Embedded font subsetting — handled by `@suaveplan/font-subset` /
|
||
`@suaveplan/font-compress` and wired in via a future plugin slot.
|
||
- Digital signatures — gated on `@suaveplan/jws` reaching enterprise
|
||
parity for PDF (`/Sig` dictionaries).
|
||
- HTTP plugin (`@suaveplan/api-plugin-pdf`) — separate change once Chunks
|
||
A–E land.
|