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,65 @@
# Barcode batch generator
**Package**: `@suaveplan/barcode`
**Status**: PROPOSED
**Type**: spec-conformance + feature
## Why
The 2026-05-02 audit of `@suaveplan/barcode` against legacy `packages/universal/barcode/src/` found that genesis ships only the single-call generator surface (`encode`, `renderSvg`, the per-symbology helpers, `validateBarcode`). Legacy ships a full `batch/batch-generator.ts` (`generateBatch(items, options): Promise<BatchResult>`) used by every consumer that needs to produce ten or ten-thousand barcodes in one pass — bulk-label printing, ticketing, asset registers, and inventory imports all rely on it. CLAUDE.md scopes this package as "generator only" (barcode reading is intentionally out of scope), but the batch generator is part of the generator surface, not the reader. Its absence forces every consumer to re-implement an error-isolating loop in user code, which is exactly the duplication §0a (migration parity) was written to prevent.
## What changes
- ADDED: `BatchBarcodeRequest` interface — `{ id?: string; symbology: BarcodeSymbology; value: string; options?: BarcodeOptions }`. Per-item `id` is optional; when omitted the request index is used as the result key.
- ADDED: `BatchBarcodeResult` discriminated union — `{ id: string; index: number; ok: true; result: BarcodeResult } | { id: string; index: number; ok: false; error: BarcodeError }`. Failed items carry a coded `BarcodeError`, never a bare string.
- ADDED: `BatchBarcodeOptions` interface — `{ concurrency?: number; signal?: AbortSignal; onItem?: (result: BatchBarcodeResult) => void }`. `concurrency` defaults to `1` (sequential) and is clamped to `[1, 64]`.
- ADDED: `BatchBarcodeSummary` interface — `{ total: number; succeeded: number; failed: number; durationMs: number; results: BatchBarcodeResult[] }`. `durationMs` uses an injectable `Clock` (defaults to `performance.now()` shim) so tests stay deterministic.
- ADDED: `generateBatch(requests, options?): Promise<BatchBarcodeSummary>` — produces one `BatchBarcodeResult` per input request in input order; per-item failures are isolated (a thrown encode error becomes `{ ok: false, error }`, every other item keeps generating).
- ADDED: `BatchBarcodeGenerator` class — `class BatchBarcodeGenerator { constructor(deps?: { clock?: Clock }); generate(requests, options?): Promise<BatchBarcodeSummary> }` 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 `BARCODE.BATCH_ABORTED` registered on `BARCODE_CODES`, thrown when the supplied `AbortSignal` fires before completion.
## Out of scope
- **Barcode reading / decoding** — explicitly out of scope per CLAUDE.md package description ("generator only"). Tracked separately under a future `barcode-reader-port` change if business requirements ever demand it. Not deferred for "complexity" reasons; deferred because the package contract intentionally excludes it.
- **2D symbologies (DataMatrix, PDF417, Aztec)** — not in legacy upstream either; would be its own package.
- **Streaming / async-iterable batch API** — `generateBatch` returns a single resolved summary. A streaming variant would let callers consume results as they emit (useful for very large batches, > 10 000 items). Punted because no current consumer needs it; the `onItem` callback covers the common "show progress" case.
## Source mapping
This change derives from `../suaveplan/packages/universal/barcode/src/batch/`.
| Requirement | Source lines | Notes |
|---|---|---|
| `BatchBarcodeRequest` shape | `batch/batch-generator.ts` L9-L13 | Reworded: `data` → `value`; added optional `id` |
| `BatchBarcodeResult` discriminated union | `batch/batch-generator.ts` L16-L21 | Reworded: legacy returns `{ index, result?, svg?, error?: string }`; new shape uses a discriminated union with coded `BarcodeError` instead of a string |
| `BatchBarcodeOptions` shape with `concurrency`, `signal`, `onItem` | `batch/batch-generator.ts` L24-L28 | Extended: legacy has `outputFormat` and `onProgress`; new shape replaces `onProgress` with `onItem` (richer payload), drops `outputFormat` (caller picks via `options.includeText`/SVG availability already on `BarcodeResult`), adds `concurrency` and `signal` |
| `generateBatch` function | `batch/batch-generator.ts` L42-L95 | Verbatim semantics — per-item failure isolation, sequential default, in-input-order results |
| `BatchBarcodeGenerator` class | (no source line) | New: legacy is a free function only; class is added for clock injection |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `BatchBarcodeGenerator` 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`. |
| `BARCODE.BATCH_ABORTED` error code on `BARCODE_CODES` | CLAUDE.md §Workflow rule 2 — every thrown error routes through `@suaveplan/error` with a stable code. Legacy throws a plain `Error("aborted")` from the loop. |
| `concurrency` clamping to `[1, 64]` | Genesis-wide guardrail — unbounded `Promise.all` over user input is a denial-of-service vector. Legacy ships sequential only (concurrency 1) so the clamp is additive. |
## Verification approach
- `src/core/batch/batch-generator.test.ts` — covers `BatchBarcodeRequest` validation, in-order results, per-item failure isolation, `concurrency` clamping, `onItem` callback invocation, abort-via-`AbortSignal`, sequential-vs-concurrent equivalence on success-only inputs.
- `src/core/batch/batch-generator.spec-conformance.test.ts` — pins injectable `Clock` (deterministic `durationMs`), Zod-validated `BatchBarcodeRequest` shape, `BARCODE.BATCH_ABORTED` error code emission, OTel counter `barcode.batch.items_total` / `barcode.batch.items_failed` recorded against an in-memory `MeterProvider`.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/barcode` to enumerate. As of 2026-05-02 there are no in-repo dependents:
- No dependents (verified by `grep -rl "@suaveplan/barcode" /home/ubuntu/web/genesis/packages/ --include="package.json"` returning only `packages/barcode/barcode/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 single-call surface (`encode`, `renderSvg`, `validateBarcode`, per-symbology encoders) is unchanged. Consumers wanting batch generation opt in by importing `generateBatch` from `@suaveplan/barcode` or `@suaveplan/barcode/batch`.

View File

@@ -0,0 +1,141 @@
<!-- markdownlint-disable MD024 -->
# 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"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,81 @@
# Tasks — Barcode batch generator
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/packages/universal/barcode/src/batch/batch-generator.ts` end-to-end
- [x] 1.2 Read `openspec/specs/barcode/spec.md` (does not exist yet — capability spec authored fresh in this change)
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13, 13a, 14, 14a
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [x] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
- [x] 1.6 Draft `specs/barcode/spec.md` from `openspec/templates/spec-delta.md`
- [x] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
## 2. Implementation
- [ ] 2.1 Add `src/core/batch/batch-generator.ts` exporting `BatchBarcodeRequest`, `BatchBarcodeResult`, `BatchBarcodeOptions`, `BatchBarcodeSummary`, `generateBatch`, `BatchBarcodeGenerator`
- [ ] 2.2 Add `src/core/batch/batch-generator.types.ts` with the shared interface declarations and Zod schemas (`BatchBarcodeRequestSchema`, `BatchBarcodeOptionsSchema`)
- [ ] 2.3 Wire `BARCODE.BATCH_ABORTED` into `src/errors/barcode-error.ts::BARCODE_CODES`; throw `BarcodeError` (never plain `Error`) from the abort path and from per-item failures
- [ ] 2.4 Concurrency limiter — clamp to `[1, 64]`; sequential default (concurrency 1); honour `AbortSignal` between items
- [ ] 2.5 OTel instrumentation: `barcode.batch.items_total` (counter), `barcode.batch.items_failed` (counter), `barcode.batch.duration_ms` (histogram); use the package's existing OTel meter
- [ ] 2.6 Inject `Clock` via constructor dependency on `BatchBarcodeGenerator`; `generateBatch` free function uses the default real clock
- [ ] 2.7 Add `./batch` subpath to `package.json` `exports`
- [ ] 2.8 Re-export new symbols from `src/index.ts`
## 3. Tests
- [ ] 3.1 `src/core/batch/batch-generator.test.ts` — all scenarios from spec delta (in-order results, failure isolation, concurrency clamp, `onItem` callback, abort, deterministic results across symbologies)
- [ ] 3.2 `src/core/batch/batch-generator.spec-conformance.test.ts` — injectable `Clock` deterministic `durationMs`, Zod `BatchBarcodeRequest` rejection with structured `ZodError`, `BARCODE.BATCH_ABORTED` code emission, OTel counters/histogram asserted via in-memory `MeterProvider`
- [ ] 3.3 Property test (fast-check): for any `Array<{ symbology, value }>` of length ≤ 100 where every item is a valid encoding, `generateBatch` SHALL return `succeeded === input.length` and `failed === 0`
- [ ] 3.4 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async)
## 4. Documentation
- [ ] 4.1 `src/core/batch/batch-generator.md` — sibling doc, ≥200 words, basic + advanced examples (sequential, concurrent, abort, custom clock), full API reference for every public export, implementation notes covering concurrency clamping and per-item isolation
- [ ] 4.2 README links the new co-located `.md` under `## Modules`
- [ ] 4.3 Update `CLAUDE.md` `barcode` listing to mention "batch generation with per-item failure isolation"
## 5. Pre-archive verification (the 5-gate triple)
- [ ] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/barcode` — green
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/barcode` — 100/100/100/100
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/barcode` — exit 0, no `error` findings
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/barcode` — green
## 6. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 6.1 Generics threaded; `expectTypeOf` assertions on `BatchBarcodeResult` discriminated-union narrowing
- [ ] 6.2 Zod schemas wired at the `generateBatch` and `BatchBarcodeGenerator.generate` boundary
- [ ] 6.3 Every error code in `error-codes.ts` thrown by at least one production path (`BARCODE.BATCH_ABORTED` covered by abort scenario)
- [ ] 6.4 Strict-mode behaviors tested both on and off (concurrency 1 vs > 1; `continueOnError` semantics implicit — failure isolation always on)
- [ ] 6.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
- [ ] 6.6 Lifecycle dedup tested where claimed (`BatchBarcodeGenerator.generate` is stateless across calls — verified by parallel-call test)
- [ ] 6.7 No retroactive tick — every box above reflects work done in this change
## 7. Archive
- [ ] 7.1 Fold spec delta into `openspec/specs/barcode/spec.md` (creates the canonical capability spec from scratch since none exists)
- [ ] 7.2 Move `openspec/changes/barcode-batch-generator/` → `openspec/archive/<yyyy-mm-dd>-barcode-batch-generator/`
- [ ] 7.3 Refresh `MANIFEST.md` if package set or deps changed (no change expected — purely additive feature)
---
## Audit Note (2026-05-02)
**Status**: DEFERRED.
State at audit:
- `@suaveplan/barcode` package itself is shipped and green, but the batch-generator feature has **not** been authored. No `src/core/batch/` directory exists. `find packages/barcode/barcode/src -name 'batch*'` returns no hits.
- Section 1 (spec authoring) is checked, so the proposal + spec delta exist; Sections 2–7 (implementation, tests, docs, verification, archive) are entirely unchecked.
- `BARCODE.BATCH_ABORTED` is not yet wired into `src/errors/barcode-error.ts::BARCODE_CODES`.
Resuming work: execute Sections 2–7 in order. Reference upstream `../suaveplan/packages/universal/barcode/src/batch/batch-generator.ts` (read in Section 1.1).