Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# Design — @suaveplan/pdf
## Package placement
`packages/devtools/docs/pdf/` per `scripts/folder-reorg/assignment.yml`
line 261 (`docs` plugin under the `devtools` super-category). Server-tier:
the writer uses `TextEncoder`/`TextDecoder` (universally available in
Node + Bun), no DOM APIs.
## Tier
Server-tier. Runs in Node ≥ 18 and Bun ≥ 1.0. The React preview component
(Chunk E) carries React as an **optional** peer dependency so server-only
consumers do not pay the cost.
## External runtime deps
Chunk A only:
- `@suaveplan/error` — `defineCodes`, `SuaveplanError` (workspace).
- `@suaveplan/opentelemetry` — `createCounter`, `createHistogram`
(workspace).
- `@suaveplan/types` — `Clock` for injectable timing in later chunks
(workspace).
- `@opentelemetry/api` — `metrics`, `trace` namespaces (catalog).
- `zod` — boundary validation (catalog).
- `tslib` — runtime helpers (catalog).
No new third-party deps are introduced. Upstream uses zero npm deps for
PDF generation (hand-rolled 1.4 writer); genesis preserves that property.
## Why hand-roll a PDF writer instead of `pdfkit`
`pdfkit` is ~600 KB unpacked, pulls in `crypto-js`, `fontkit`, and PNG/JPEG
streams it does not always need, and ships with CommonJS-only entry
points that complicate ESM bundling. The upstream writer is ~200 LOC,
produces valid PDF 1.4, and round-trips through every PDF reader we have
tested (`pdf-parse`, `pdfjs-dist`, Chrome, Safari, Preview, Adobe Reader).
Genesis preserves the hand-rolled approach because it is the smaller and
more auditable surface.
## Error model
Single `PdfError extends SuaveplanError<PdfErrorCode>` with the stable
`PDF.*` code registry. Every public function raises `PdfError` only —
never a plain `Error` — so consumers can `instanceof PdfError` + switch
on `err.code`.
Code coverage in Chunk A is `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`. The later codes are declared up front so
Chunks B–E do not have to expand the registry mid-flight (which would
break consumers that pin to `PdfErrorCode`).
## Observability
Six instruments on the `@suaveplan/pdf` meter, all created at module
load time via the `@suaveplan/opentelemetry` helpers:
- `pdf_documents_rendered_total` — counter tagged `success` (`true`/`false`).
- `pdf_pages_emitted_total` — counter.
- `pdf_render_errors_total` — counter tagged `code`.
- `pdf_validation_failures_total` — counter tagged `code` (Zod parse fails).
- `pdf_render_duration_ms` — histogram (ms).
- `pdf_document_size_bytes` — histogram (bytes).
A `createPdfObservability(opts?)` factory returns a struct so consumers
can inject a custom `Meter` / `Tracer` (used in tests to assert recorded
values).
## Zod boundary
`schemas/` declares Zod schemas matching every public type:
`UnitSchema = z.enum(["pt", "mm", "cm", "in", "px"])`,
`HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/)`,
`RgbColorSchema = z.object({ r: z.number().min(0).max(255), … })`,
`MarginsSchema`, `PageConfigSchema`, `DocumentMetadataSchema`.
Parse failures are converted to `PdfError(PDF_CODES.INVALID_*)` with the
Zod issue as `cause` and the issue path as `context.path`.
## AbortSignal
Reserved in the public API of every async function in later chunks
(`renderDocument({ signal })`). Chunk A has no async surface so the
contract is documented but not exercised.
## Test strategy
- `units/` and `colors/` — exhaustive table-driven tests covering every
branch (positive + negative + edge values).
- `error-codes/` — verifies every code in `PDF_CODES` is reachable from a
real call site (`PdfError` constructor + a regression test that walks
the union).
- `schemas/` — Zod parse pass / fail per schema.
- `telemetry/` — asserts the six instruments are created and `record*`
helpers fire on the supplied meter.
- All tests use `@suaveplan/testing/runner` per rule 11.
## File-size discipline
Every implementation file ≤ 250 LOC soft, ≤ 300 LOC hard. Tests ≤ 500 LOC.
The hand-rolled `pdf-writer` (Chunk B) is the largest single file; it
will be split into `pdf-writer-objects.ts`, `pdf-writer-xref.ts`, and
`pdf-writer.ts` if it crosses 300 LOC.
## Chunking rationale
The full surface is ~30 source files + ~30 test files + ~30 docs at
≥ 200 words each, plus real PDF round-trip verification using
`pdf-parse`. That exceeds a single agent session's reliable working
capacity at 100 / 100 / 100 / 100 coverage. Splitting along the natural
dependency lines (utilities → writer → elements → layout → templates)
keeps every chunk independently verifiable and lets each one ship to
green before the next begins.
## Migration path
Chunks B–E follow as separate OpenSpec changes (`enterprise-pdf-writer`,
`enterprise-pdf-elements`, `enterprise-pdf-layout`,
`enterprise-pdf-templates`). The unshipped chunks are listed in
`tasks.md` with their dependency edges so the next agent can pick up
without re-reading the upstream source.