# Capability Spec Delta: barcode (barcode-batch-generator) **Package**: `@suaveplan/barcode` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.) ## ADDED Requirements ### Requirement: BatchBarcodeGenerator with per-item failure isolation The package SHALL expose a `generateBatch(requests, options?): Promise` async function and an equivalent `BatchBarcodeGenerator` class. The function SHALL produce one `BatchBarcodeResult` per input request in input order. A failure on any single request — encoder rejection, validator rejection, or any thrown exception inside the per-item path — SHALL NOT short-circuit the batch; the failure SHALL surface as `{ ok: false, error: BarcodeError }` for that index while every other request continues to generate. Successful items SHALL surface as `{ ok: true, result: BarcodeResult }`. The summary SHALL include `total`, `succeeded`, `failed`, `durationMs` (from injected `Clock`), and the full `results` array. #### Scenario: every request succeeds - **WHEN** `generateBatch` is called with five valid `Code 128` requests - **THEN** `summary.total === 5` AND `summary.succeeded === 5` AND `summary.failed === 0` AND every `results[i].ok === true` AND `results[i].result.symbology === "code128"` #### Scenario: one request throws — others still succeed - **WHEN** `generateBatch` is called with three valid Code 128 requests and one EAN-13 request whose value `"abc"` violates EAN-13's numeric-only rule - **THEN** `summary.succeeded === 3` AND `summary.failed === 1` AND `results[3].ok === false` AND `results[3].error` is a `BarcodeError` with code from `BARCODE_CODES` AND the three Code 128 results are present and valid #### Scenario: input order is preserved - **WHEN** `generateBatch` is called with `concurrency: 8` over 16 mixed-symbology requests - **THEN** `results[i].index === i` for every `i` AND `results.length === 16` regardless of completion order **Verified by:** `src/core/batch/batch-generator.test.ts::"generateBatch returns one result per input in input order"` AND `src/core/batch/batch-generator.test.ts::"generateBatch isolates per-item failures and continues"` AND `src/core/batch/batch-generator.test.ts::"generateBatch preserves input order under concurrency 8"` --- ### Requirement: Concurrency control with safe clamping The package SHALL accept an optional `concurrency` option on `BatchBarcodeOptions`. The default SHALL be `1` (sequential). The value SHALL be clamped to the inclusive range `[1, 64]` — values below `1` are treated as `1`; values above `64` are treated as `64`. Concurrent execution SHALL still preserve input-order results. #### Scenario: default is sequential - **WHEN** `generateBatch(requests)` is called without `options.concurrency` - **THEN** at most one barcode is being generated at any time AND `summary.results` is in input order #### Scenario: concurrency clamps to upper bound - **WHEN** `generateBatch(requests, { concurrency: 1000 })` is called - **THEN** the internal worker pool is sized at 64 (verified by counting active in-flight promises via the `onItem` callback) AND no error is thrown for the out-of-range input #### Scenario: concurrency clamps to lower bound - **WHEN** `generateBatch(requests, { concurrency: 0 })` is called - **THEN** execution is sequential (effective concurrency 1) AND no error is thrown **Verified by:** `src/core/batch/batch-generator.test.ts::"generateBatch defaults to sequential execution"` AND `src/core/batch/batch-generator.test.ts::"generateBatch clamps concurrency above 64 to 64"` AND `src/core/batch/batch-generator.test.ts::"generateBatch clamps concurrency below 1 to 1"` --- ### Requirement: AbortSignal cancels the batch The package SHALL accept an optional `AbortSignal` on `BatchBarcodeOptions`. When the signal fires before the batch resolves, the returned promise SHALL reject with a `BarcodeError` whose `code === "BARCODE.BATCH_ABORTED"`. In-flight per-item generations MAY complete or be discarded; queued items SHALL NOT be started. The error SHALL carry the original `AbortSignal.reason` as `cause` if present. #### Scenario: pre-fired signal aborts immediately - **WHEN** `generateBatch(requests, { signal })` is called with an already-aborted `AbortSignal` - **THEN** the returned promise rejects with `BarcodeError` AND `error.code === "BARCODE.BATCH_ABORTED"` AND no item is generated #### Scenario: signal fires mid-batch - **WHEN** `generateBatch(requests, { signal })` is called with 100 requests at concurrency 1, and `signal.abort()` is called after 10 items - **THEN** the returned promise rejects with `BarcodeError` of code `"BARCODE.BATCH_ABORTED"` AND fewer than 100 items were generated **Verified by:** `src/core/batch/batch-generator.test.ts::"generateBatch rejects immediately when signal is pre-aborted"` AND `src/core/batch/batch-generator.test.ts::"generateBatch rejects mid-batch when signal fires"` AND `src/core/batch/batch-generator.spec-conformance.test.ts::"abort path emits BARCODE.BATCH_ABORTED with chained cause"` --- ### Requirement: onItem progress callback emits coded results The package SHALL accept an optional `onItem: (result: BatchBarcodeResult) => void` callback on `BatchBarcodeOptions`. The callback SHALL fire exactly once per completed request, with the same `BatchBarcodeResult` that ends up in `summary.results`. The callback SHALL fire in completion order (not input order), so concurrent batches MAY observe out-of-order callbacks even though the final summary is sorted. #### Scenario: onItem fires once per request - **WHEN** `generateBatch` is called with 20 requests and an `onItem` spy - **THEN** the spy is called 20 times AND every emitted result has the same identity as the corresponding `summary.results[i]` #### Scenario: onItem callback throwing does not break the batch - **WHEN** the `onItem` callback throws for one item - **THEN** the batch still resolves normally AND the throw is captured into the OTel span as a non-fatal event **Verified by:** `src/core/batch/batch-generator.test.ts::"onItem fires exactly once per completed request"` AND `src/core/batch/batch-generator.spec-conformance.test.ts::"onItem callback errors are isolated from the batch"` --- ### Requirement: Deterministic durationMs via injectable Clock The package SHALL accept an injectable `Clock` (`@suaveplan/types::Clock`) on the `BatchBarcodeGenerator` constructor. `durationMs` on `BatchBarcodeSummary` SHALL be computed as `clock.now() - startedAt`. The free `generateBatch` function SHALL use a default real clock; tests SHALL be able to inject a deterministic clock for stable assertions. #### Scenario: injected clock yields deterministic duration - **WHEN** a `BatchBarcodeGenerator` is constructed with a `FakeClock` that advances by 7 ms after each `now()` call, and `generate` is invoked over 4 requests - **THEN** `summary.durationMs === 7` (start-to-finish span of one tick) — exact value pinned by the fake clock #### Scenario: no module-scope Date.now / performance.now - **WHEN** the `batch-generator.ts` source is grepped for `Date.now(`, `performance.now(`, `Math.random(` - **THEN** zero matches are found at module scope (only the constructor-default factory may resolve `performance.now`, and that factory lives behind the `Clock` interface) **Verified by:** `src/core/batch/batch-generator.spec-conformance.test.ts::"injected FakeClock yields deterministic durationMs"` AND `src/core/batch/batch-generator.spec-conformance.test.ts::"batch-generator source has no module-scope Date.now or Math.random"` --- ### Requirement: BARCODE.BATCH_ABORTED registered in BARCODE_CODES The package SHALL register `BARCODE.BATCH_ABORTED` in `BARCODE_CODES` (the stable error-code registry). The code SHALL be the only error thrown from the abort path; per-item failures continue to use the existing per-symbology codes. #### Scenario: code is registered - **WHEN** `BARCODE_CODES` is enumerated - **THEN** the array includes `"BARCODE.BATCH_ABORTED"` #### Scenario: code is the documented type - **WHEN** the `BarcodeCode` exported type is inspected - **THEN** `"BARCODE.BATCH_ABORTED"` is one of its members (verified by `expectTypeOf<"BARCODE.BATCH_ABORTED">().toMatchTypeOf()`) **Verified by:** `src/errors/barcode-error.test.ts::"BARCODE_CODES includes BATCH_ABORTED"` AND `src/core/batch/batch-generator.spec-conformance.test.ts::"BarcodeCode union includes BATCH_ABORTED"` --- ### Requirement: ./batch subpath export and tree-shakeability The package SHALL expose `./batch` as a tree-shakeable subpath in `package.json` `exports`, mapping to `dist/core/batch/batch-generator.js`. Importing only `generateBatch` SHALL NOT pull in the entire `core/encoder` graph for callers that don't use it. #### Scenario: subpath resolves to the same function as the root export - **WHEN** a consumer imports `{ generateBatch } from "@suaveplan/barcode/batch"` and `{ generateBatch as fromRoot } from "@suaveplan/barcode"` - **THEN** the two references are identity-equal **Verified by:** `src/core/batch/batch-generator.test.ts::"./batch subpath and root export resolve to the same function"`