Initial Commit
This commit is contained in:
145
openspec/changes/qrcode-scan/specs/qrcode-scan/spec.md
Normal file
145
openspec/changes/qrcode-scan/specs/qrcode-scan/spec.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Capability Spec Delta: qrcode-scan
|
||||
|
||||
**Package**: `@suaveplan/qrcode` (`/scan` subpath)
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Image decode from multiple input types
|
||||
|
||||
`scanFromImageData(input)` MUST accept `ImageData`, `Uint8Array`, `Blob`, or `File`. It MUST decode the QR code matrix and return the raw payload string. If no QR code is found, `QrScanError` (code `QRCODE.DECODE_FAILED`) MUST be thrown.
|
||||
|
||||
#### Scenario: Decode from ImageData
|
||||
|
||||
- **WHEN** `scanFromImageData` receives an `ImageData` object containing a valid QR code
|
||||
- **THEN** it returns the decoded string payload
|
||||
|
||||
#### Scenario: Decode from File
|
||||
|
||||
- **WHEN** `scanFromImageData` receives a `File` object containing a valid QR code
|
||||
- **THEN** it returns the decoded string payload
|
||||
|
||||
#### Scenario: No QR code found
|
||||
|
||||
- **WHEN** `scanFromImageData` receives an image with no QR code
|
||||
- **THEN** it throws `QrScanError` with code `QRCODE.DECODE_FAILED`
|
||||
|
||||
**Verified by:** `src/scan/scan.test.ts::"decodes QR code from ImageData"` AND `src/scan/scan.test.ts::"decodes QR code from File"` AND `src/scan/scan.test.ts::"throws QrScanError when no QR code is found"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Camera stream access
|
||||
|
||||
`scanFromCameraStream(stream, options?)` MUST accept a `MediaStream` and extract video frames at a configurable interval (default 250 ms). It MUST attempt to decode a QR code from each frame. On first successful decode, it MUST return the payload and stop frame extraction. If `options.signal` aborts, it MUST reject with an abort error. If the stream ends without a successful decode, it MUST throw `QrScanError` (code `QRCODE.DECODE_FAILED`).
|
||||
|
||||
#### Scenario: Successful decode from camera stream
|
||||
|
||||
- **WHEN** `scanFromCameraStream` receives a stream where one frame contains a valid QR code
|
||||
- **THEN** it returns the payload after processing that frame
|
||||
|
||||
#### Scenario: Abort signal stops scanning
|
||||
|
||||
- **WHEN** `options.signal` aborts before a QR code is found
|
||||
- **THEN** the promise rejects with an abort error
|
||||
|
||||
#### Scenario: Stream ends without decode
|
||||
|
||||
- **WHEN** the stream ends and no frame contained a QR code
|
||||
- **THEN** it throws `QrScanError` with code `QRCODE.DECODE_FAILED`
|
||||
|
||||
**Verified by:** `src/scan/scan-camera.test.ts::"returns payload when QR code appears in stream"` AND `src/scan/scan-camera.test.ts::"rejects when abort signal fires before decode"` AND `src/scan/scan-camera.test.ts::"throws QrScanError when stream ends without finding QR code"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Injectable Clock for deterministic camera timing
|
||||
|
||||
`scanFromCameraStream` options MUST include an optional `clock?: Clock`. When provided, frame extraction intervals and timeout deadlines MUST be driven by the injected clock instead of the global time source.
|
||||
|
||||
**Verified by:** `src/scan/scan-camera.test.ts::"uses injected clock for frame intervals"` AND `src/scan/scan-camera.test.ts::"uses injected clock for stream timeout"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: TOTP URI extraction
|
||||
|
||||
`extractTotpUri(data)` MUST parse `otpauth://totp/` URIs per the de-facto Google Authenticator Key URI Format. It MUST return a `TotpConfig` with:
|
||||
|
||||
- `secret` (required) — base32-encoded secret
|
||||
- `issuer` (optional) — issuer name
|
||||
- `account` (optional) — account name
|
||||
- `algorithm` (optional, default `SHA1`) — `SHA1 | SHA256 | SHA512`
|
||||
- `digits` (optional, default `6`) — `6 | 7 | 8`
|
||||
- `period` (optional, default `30`) — seconds as integer
|
||||
|
||||
If `secret` is missing or the URI scheme is not `otpauth://totp/`, `TotpUriParseError` (code `QRCODE.TOTP_PARSE_ERROR`) MUST be thrown with `context.rawUri` populated.
|
||||
|
||||
#### Scenario: Valid TOTP URI
|
||||
|
||||
- **WHEN** `extractTotpUri` receives `"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"`
|
||||
- **THEN** it returns a `TotpConfig` with all fields correctly populated
|
||||
|
||||
#### Scenario: Missing secret
|
||||
|
||||
- **WHEN** `extractTotpUri` receives `"otpauth://totp/Example:alice@google.com?issuer=Example"`
|
||||
- **THEN** it throws `TotpUriParseError`
|
||||
|
||||
#### Scenario: Invalid scheme
|
||||
|
||||
- **WHEN** `extractTotpUri` receives `"https://example.com"`
|
||||
- **THEN** it throws `TotpUriParseError`
|
||||
|
||||
**Verified by:** `src/scan/totp-extract.test.ts::"parses valid otpauth URI into TotpConfig"` AND `src/scan/totp-extract.test.ts::"throws TotpUriParseError when secret is missing"` AND `src/scan/totp-extract.test.ts::"throws TotpUriParseError for non-otpauth scheme"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath export configuration
|
||||
|
||||
The `package.json` `exports` map MUST include an entry for `./scan/scan` so consumers can import scan functionality without pulling in generation code:
|
||||
|
||||
```json
|
||||
"./scan/scan": {
|
||||
"import": { "types": "./dist/scan/scan.d.ts", "default": "./dist/scan/scan.js" }
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: Tree-shakeable import
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/qrcode/scan/scan`
|
||||
- **THEN** only scan-related code is included in the bundle
|
||||
|
||||
**Verified by:** `tests/dep-graph.test.ts::"scan subpath is tree-shakeable and does not import generator modules"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Structured errors
|
||||
|
||||
All scan-related thrown errors MUST extend `QrScanError` which extends `QrError`. The following codes MUST be defined in the existing `qrcode` error code registry:
|
||||
|
||||
- `QRCODE.CAMERA_ACCESS_DENIED` — camera permission was denied or stream could not be obtained
|
||||
- `QRCODE.TOTP_PARSE_ERROR` — the decoded data is not a valid `otpauth://totp/` URI
|
||||
|
||||
`QrScanError`, `CameraAccessError`, and `TotpUriParseError` MUST be exported from the `./scan/scan` subpath.
|
||||
|
||||
**Verified by:** `src/scan/scan.spec-conformance.test.ts::"all scan errors extend QrScanError"` AND `src/scan/scan.spec-conformance.test.ts::"camera access error code is registered"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Observability
|
||||
|
||||
The following metrics MUST be emitted via `@opentelemetry/api` using meter name `@suaveplan/qrcode`:
|
||||
|
||||
- Counter `qrcode.scan.total` with attributes `source` (`image` / `camera`) and `outcome` (`success` / `error` / `not_found`)
|
||||
- Histogram `qrcode.scan.duration_ms` recorded on every `scanFromImageData` and `scanFromCameraStream` call
|
||||
|
||||
All instruments MUST be no-ops when no OTel metric exporter is configured.
|
||||
|
||||
**Verified by:** `src/scan/scan.spec-conformance.test.ts::"emits scan total counter with correct attributes on success"` AND `src/scan/scan.spec-conformance.test.ts::"emits scan duration histogram on every call"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 100% coverage
|
||||
|
||||
Vitest thresholds for the `@suaveplan/qrcode` package MUST remain at 100/100/100/100 after adding the scan subpath. No `.skip` or `.only` is permitted in `src/scan/`.
|
||||
|
||||
**Verified by:** `vitest.config.ts` AND `src/scan/scan.test.ts` (all branches covered)
|
||||
Reference in New Issue
Block a user