Files
suaveplan/openspec/changes/barcode-batch-generator/specs/barcode/spec.md
2026-05-27 23:29:53 +01:00

9.2 KiB

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<BatchBarcodeSummary> 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<BarcodeCode>())

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"