# QR code batch generator **Package**: `@suaveplan/qrcode` **Status**: PROPOSED **Type**: spec-conformance + feature ## Why The 2026-05-02 audit of `@suaveplan/qrcode` against legacy `packages/universal/qrcode/src/` found that genesis ships only the single-call `generate()` and the `core/*` building blocks. Legacy ships a full `batch/batch-generator.ts` exposing three flavours of batch surface — `generateBatch`, `generateBatchAsync` (async generator with concurrency), and `generateBatchWithProgress` — used by every consumer that produces many QR codes at once: ticketing, vCard imports, asset registers, and bulk loyalty-card issuance. CLAUDE.md scopes this package as "generator only" — `qr-reader`, `styled-qr` (logos / colours / shapes), and `qr-exporter` (PNG/PDF) are intentionally out of scope. The batch generator is unambiguously part of the generator surface, not any of those scoped-out caps. Its absence forces every consumer to re-implement an error-isolating loop in user code, which §0a (migration parity) bans. ## What changes - ADDED: `BatchQrCodeRequest` interface — `{ id?: string; value: string; options?: QRCodeOptions }`. Per-item `id` is optional; when omitted the request index is used as the result key. - ADDED: `BatchQrCodeResult` discriminated union — `{ id: string; index: number; ok: true; result: QRCodeResult } | { id: string; index: number; ok: false; error: QRCodeError }`. Failed items carry a coded `QRCodeError`, never a bare string. - ADDED: `BatchQrCodeOptions` interface — `{ concurrency?: number; signal?: AbortSignal; onItem?: (result: BatchQrCodeResult) => void }`. `concurrency` defaults to `1` (sequential) and is clamped to `[1, 64]`. - ADDED: `BatchQrCodeSummary` interface — `{ total: number; succeeded: number; failed: number; durationMs: number; results: BatchQrCodeResult[] }`. `durationMs` uses an injectable `Clock`. - ADDED: `generateBatch(requests, options?): Promise` — produces one `BatchQrCodeResult` per input request in input order; per-item failures isolated. - ADDED: `generateBatchAsync(requests, options?): AsyncGenerator` — async iterable that yields results as they complete (completion order, not input order); honours `concurrency` and `signal`. - ADDED: `BatchQrCodeGenerator` class — `class BatchQrCodeGenerator { constructor(deps?: { clock?: Clock }); generate(requests, options?): Promise; stream(requests, options?): AsyncGenerator }` for callers that want to inject a deterministic clock or share a generator across calls. - ADDED: subpath export `./batch` mapping to `dist/core/batch/batch-generator.js`. - ADDED: error code `QRCODE.BATCH_ABORTED` registered on `QRCODE_CODES`, thrown when the supplied `AbortSignal` fires before completion. ## Out of scope - **QR reading / decoding (qr-reader)** — explicitly out of scope per CLAUDE.md package description ("generator only"). Tracked under separate `qrcode-scan` change already in `openspec/changes/`. - **Styled QR (logos / colour gradients / module shapes — styled-qr)** — intentionally out of scope per CLAUDE.md. Would be its own package. - **Raster export (PNG / PDF — qr-exporter)** — intentionally out of scope per CLAUDE.md. The package emits SVG/matrix/terminal/dataurl only; raster export belongs in a downstream package that depends on `@suaveplan/qrcode` plus a rasteriser. - **Higher QR versions (11 – 40)** — current package supports versions 1 – 10 only. Extending the version table is its own change; batch behaviour is independent of version count and works identically once added. ## Source mapping This change derives from `../suaveplan/packages/universal/qrcode/src/batch/`. | Requirement | Source lines | Notes | |---|---|---| | `BatchQrCodeRequest` shape | `types/qr.types.ts` L94-L98 | Reworded: `data` → `value`; added optional `id`; removed `outputFormat` (legacy `BatchItem` carries it; new shape relies on `QRCodeOptions`/`QRCodeResult` having every output already) | | `BatchQrCodeResult` discriminated union | (no source line) | Legacy returns `QrCode[]` with no per-item failure mode — every failure throws and aborts the batch. New shape adds discriminated-union failure isolation (parity with `barcode-batch-generator`). | | `BatchQrCodeOptions` shape with `concurrency`, `signal`, `onItem` | `types/qr.types.ts` L103-L106 (legacy `BatchOptions`) | Extended: legacy has `onProgress: (completed, total) => void` and `concurrency`; new shape replaces `onProgress` with `onItem` (richer payload), adds `signal` | | `generateBatch` Promise-based function | `batch/batch-generator.ts` L12-L29 | Verbatim semantics for the success path; new failure-isolation behaviour replaces legacy throw-and-abort (legacy bug was that one bad input poisoned the whole batch) | | `generateBatchAsync` async-iterable function | `batch/batch-generator.ts` L35-L59 | Verbatim — yields per item, supports `concurrency` | | `BatchQrCodeGenerator` class | (no source line) | New: legacy is free functions only; class added for clock injection | ### Requirements with no source line | Requirement | Justification | |---|---| | Per-item failure isolation in `generateBatch` | Legacy throws on first per-item failure (`generator.generate` rejects the loop). Genesis-wide CLAUDE.md §Workflow rule 2 ("no half-baked libraries") plus the §0a migration-parity rule mean we keep legacy's batch surface but fix the obvious correctness bug rather than ship the broken contract. | | `BatchQrCodeGenerator` class with injectable `Clock` | Genesis-wide CLAUDE.md determinism rule (`enterprise/bitpacker.md` L40 "no module-scope `Date.now()`"). Legacy reads `Date.now()` implicitly through `performance.now`; genesis routes through `@suaveplan/types::Clock`. | | `QRCODE.BATCH_ABORTED` error code on `QRCODE_CODES` | CLAUDE.md §Workflow rule 2 — every thrown error routes through `@suaveplan/error` with a stable code. Legacy throws plain `Error` from the loop (no abort plumbed in legacy at all). | | `concurrency` clamping to `[1, 64]` | Genesis-wide guardrail — unbounded `Promise.all` over user input is a denial-of-service vector. Legacy clamps to a default of 4 but accepts any number; genesis hardens the upper bound. | | `AbortSignal` support | Legacy has none. Genesis-wide pattern (every async API in `@suaveplan/retry`, `@suaveplan/idempotency`, etc.) accepts an `AbortSignal`. | ## Verification approach - `src/core/batch/batch-generator.test.ts` — covers `BatchQrCodeRequest` validation, in-order results from `generateBatch`, completion-order from `generateBatchAsync`, per-item failure isolation, `concurrency` clamping, `onItem` callback invocation, abort-via-`AbortSignal` for both shapes. - `src/core/batch/batch-generator.spec-conformance.test.ts` — pins injectable `Clock` (deterministic `durationMs`), Zod-validated `BatchQrCodeRequest` shape, `QRCODE.BATCH_ABORTED` error code emission, OTel counter `qrcode.batch.items_total` / `qrcode.batch.items_failed` / histogram `qrcode.batch.duration_ms` recorded against an in-memory `MeterProvider`. ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/qrcode` to enumerate. As of 2026-05-02 there are no in-repo dependents: - No dependents (verified by `grep -rl "@suaveplan/qrcode" /home/ubuntu/web/genesis/packages/ --include="package.json"` returning only `packages/barcode/qrcode/package.json` itself). This change is purely additive (new exports, no signature changes to existing surface), so the absence of dependents is not a risk gate. ## Migration impact None — additive only. Existing `generate()`, the `core/*` building blocks, the renderers, and `QRCODE_CODES` are unchanged. Consumers wanting batch generation opt in by importing `generateBatch` / `generateBatchAsync` from `@suaveplan/qrcode` or `@suaveplan/qrcode/batch`.