Files
2026-05-27 23:29:53 +01:00

71 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<BatchQrCodeSummary>` — produces one `BatchQrCodeResult` per input request in input order; per-item failures isolated.
- ADDED: `generateBatchAsync(requests, options?): AsyncGenerator<BatchQrCodeResult>` — 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<BatchQrCodeSummary>; stream(requests, options?): AsyncGenerator<BatchQrCodeResult> }` 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`.