Files
suaveplan/openspec/archive/2026-05-25-enterprise-pdf/proposal.md
2026-05-27 23:29:53 +01:00

5.2 KiB
Raw Blame History

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.