Initial Commit
This commit is contained in:
74
openspec/archive/2026-05-22-migrate-image/proposal.md
Normal file
74
openspec/archive/2026-05-22-migrate-image/proposal.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Migrate @suaveplan/image to genesis
|
||||
|
||||
**Package**: `@suaveplan/image`
|
||||
**Status**: PROPOSED
|
||||
**Type**: spec-conformance
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/image` exists upstream (`../suaveplan/packages/universal/image`) but has not been migrated to genesis. It provides image format detection, metadata extraction, resize math, crop validation, and transform pipeline planning — primitives consumed by `compress-image` and downstream UI packages. Migrating it unblocks full enterprise-quality parity and brings the package into compliance with genesis conventions: `defineCodes`, Zod boundaries, OTel instrumentation, explicit barrels, and 100% coverage.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `IMAGE_CODES` — stable `defineCodes("IMAGE", [...])` registry replacing upstream `ErrorCodes.registerMany` side effect.
|
||||
- ADDED: `ImageError` — extends `SuaveplanError<ImageCode>` with typed `ImageErrorContext`.
|
||||
- ADDED: Zod schemas for every public boundary (`ImageProcessorConfigSchema`, `CropRegionSchema`, `ResizeOptionsSchema`, `TransformOpSchema`, `TransformPipelineSchema`, `ImageMetadataSchema`).
|
||||
- ADDED: OTel counters (`image.metadata.extracted_total`, `image.transform.planned_total`) and histograms (`image.metadata.extract_duration_ms`, `image.transform.plan_duration_ms`) emitted from production paths.
|
||||
- ADDED: `ImageProcessor` spans for `extractMetadata` and `planTransform`.
|
||||
- ADDED: `AbortSignal` support on `extractMetadata` and `planTransform` (pre-abort short-circuit).
|
||||
- MODIFIED: `ImageProcessor` constructor now validates config via Zod and accepts optional `tracer` / `meter` from `@opentelemetry/api`.
|
||||
- MODIFIED: `PipelineBuilder` quality setter now clamps to `[1, 100]` and throws `IMAGE_CODES.DIMENSIONS_INVALID` on bad input.
|
||||
- REMOVED: Side-effect import `import "./error-codes.js"` — package is now `sideEffects: false`.
|
||||
- REMOVED: `@deprecated` JSDoc aliases (`unknownFormat`, `invalidDimensions`, `invalidCrop`, `metadataParseFailed`, `invalidTransform`) — replaced by prose-only backward-compat re-exports.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Actual pixel buffer encoding/decoding — `compress-image` and `exif` already cover compression and metadata I/O. This package remains a metadata + transform-planning library.
|
||||
- Browser-specific APIs (`HTMLImageElement`, `CanvasRenderingContext2D`) — the package is universal-tier.
|
||||
- Network fetch helpers — callers supply `Uint8Array` directly.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/image.md`.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Zero browser / Node / React dependencies | L37 | verbatim |
|
||||
| Zod schemas for public types | L38-L39 | extended: wired at every boundary |
|
||||
| OpenTelemetry instrumentation | L41-L42 | extended: counters + histograms + spans |
|
||||
| Structured errors via `@suaveplan/error` | L42 | extended: `defineCodes` + `SuaveplanError` |
|
||||
| `AbortSignal` propagation | L43 | extended: pre-abort on sync entrypoints |
|
||||
| Test coverage ≥ 95% | L50 | extended: genesis requires 100% |
|
||||
| No mocks | L53 | verbatim |
|
||||
| `sideEffects: false` | L55 | verbatim |
|
||||
| Biome clean | L58 | verbatim |
|
||||
| Error hierarchy | L61 | verbatim |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Explicit named barrel exports (no `export *`) | Genesis-wide policy per openspec/AGENTS.md §6a |
|
||||
| Test imports from `@suaveplan/testing/runner` | Genesis-wide policy per openspec/AGENTS.md §7 |
|
||||
| Sibling `.md` for every `.ts` | Genesis-wide docs policy per openspec/AGENTS.md §4 |
|
||||
| Subpath exports per feature module | Genesis-wide tree-shaking policy per openspec/AGENTS.md §6 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/error-codes/error-codes.test.ts` — covers code registry, type assertions.
|
||||
- `src/errors/errors.test.ts` — covers `ImageError` construction, context, cause, legacy alias round-trips.
|
||||
- `src/types/image-types.test.ts` — covers format detection, aspect ratio, resize math, crop validation, MIME mapping.
|
||||
- `src/processor/processor.test.ts` — covers metadata extraction, dimension parsing, size limits, transform planning, pipeline builder chaining, event emission.
|
||||
- `src/processor/processor.spec-conformance.test.ts` — pins OTel instrument emission, Zod boundary rejection, strict-mode behaviors, AbortSignal pre-abort.
|
||||
- `tests/dep-graph.test.ts` — pins zero non-catalog external deps.
|
||||
- `tests/tree-shake.test.ts` — pins `sideEffects: false`.
|
||||
|
||||
## Migration impact
|
||||
|
||||
`@suaveplan/image` has zero current consumers in genesis (it does not yet exist). Downstream packages that will consume it (`compress-image`, future `react-image`) will adopt the new API in follow-up changes. No existing code is affected.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
No dependents (verified by `bunx turbo ls --filter=...@suaveplan/image` — package does not exist on disk yet).
|
||||
221
openspec/archive/2026-05-22-migrate-image/specs/image/spec.md
Normal file
221
openspec/archive/2026-05-22-migrate-image/specs/image/spec.md
Normal file
@@ -0,0 +1,221 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: image (migrate-image)
|
||||
|
||||
**Package**: `@suaveplan/image`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Stable error code registry via defineCodes
|
||||
|
||||
The package SHALL declare a single `defineCodes("IMAGE", [...])` registry named `IMAGE_CODES` containing every error code emitted by the package. No hand-typed string literals for codes SHALL appear in `src/` or tests.
|
||||
|
||||
#### Scenario: Code values use IMAGE.* namespace
|
||||
|
||||
- **WHEN** a consumer reads `IMAGE_CODES.NOT_FOUND`
|
||||
- **THEN** the value is exactly `"IMAGE.NOT_FOUND"`
|
||||
|
||||
#### Scenario: Every code is listed in the registry
|
||||
|
||||
- **WHEN** introspecting `IMAGE_CODES`
|
||||
- **THEN** it contains `NOT_FOUND`, `DECODE_FAILED`, `ENCODE_FAILED`, `FORMAT_UNSUPPORTED`, `TOO_LARGE`, `PROCESSING_FAILED`, `DIMENSIONS_INVALID`
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"code values use IMAGE.* namespace"` AND `src/error-codes/error-codes.test.ts::"contains expected codes"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Typed error hierarchy extending SuaveplanError
|
||||
|
||||
The package SHALL export `ImageError` extending `SuaveplanError<ImageCode>` with an optional `ImageErrorContext` carrying `format`, `width`, `height`, `pixels`, `maxPixels`, and `path`. Typed factory functions SHALL exist for every failure mode.
|
||||
|
||||
#### Scenario: ImageError carries code, message, context, and cause
|
||||
|
||||
- **WHEN** constructing `new ImageError(IMAGE_CODES.FORMAT_UNSUPPORTED, "msg", { format: "heic" })`
|
||||
- **THEN** `error.code === "IMAGE.FORMAT_UNSUPPORTED"`, `error.context.format === "heic"`, and `error instanceof SuaveplanError` is true
|
||||
|
||||
#### Scenario: Factory functions produce correctly-coded errors
|
||||
|
||||
- **WHEN** calling `imageNotFound("/a.png")`, `imageDecodeFailed("png")`, `imageTooLarge(10, 5)`, `imageDimensionsInvalid(-1, 0)`
|
||||
- **THEN** each result has the matching `IMAGE.*` code and populated context
|
||||
|
||||
**Verified by:** `src/errors/errors.test.ts::"creates error with code and message"` AND `src/errors/errors.test.ts::"factory functions produce correctly-coded errors"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Magic-byte format detection
|
||||
|
||||
The package SHALL detect image formats from the first bytes of a `Uint8Array` for PNG, JPEG, GIF, BMP, TIFF (little- and big-endian), ICO, WebP, and SVG. Unknown headers SHALL return `null`.
|
||||
|
||||
#### Scenario: Detect PNG from header
|
||||
|
||||
- **WHEN** calling `detectFormat(new Uint8Array([0x89, 0x50, 0x4e, 0x47]))`
|
||||
- **THEN** the result is `"png"`
|
||||
|
||||
#### Scenario: Reject RIFF without WEBP tag
|
||||
|
||||
- **WHEN** calling `detectFormat` on a RIFF header whose bytes 8-11 are not `WEBP`
|
||||
- **THEN** the result is `null`
|
||||
|
||||
#### Scenario: Detect SVG from textual header
|
||||
|
||||
- **WHEN** calling `detectFormat` on a UTF-8 buffer containing `<svg` or `<?xml`
|
||||
- **THEN** the result is `"svg"`
|
||||
|
||||
**Verified by:** `src/types/image-types.test.ts::"detects PNG"` AND `src/types/image-types.test.ts::"rejects RIFF without WEBP tag"` AND `src/types/image-types.test.ts::"detects SVG"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Dimension parsing for supported formats
|
||||
|
||||
The package SHALL parse `width` and `height` from PNG, JPEG, GIF, and BMP headers. TIFF SHALL report format but dimensions zero (not yet parsed). Short or malformed data SHALL yield `{ width: 0, height: 0 }` without throwing.
|
||||
|
||||
#### Scenario: Parse PNG IHDR dimensions
|
||||
|
||||
- **WHEN** `ImageProcessor.extractMetadata` receives a valid PNG header
|
||||
- **THEN** `metadata.width` and `metadata.height` match the IHDR values
|
||||
|
||||
#### Scenario: Parse JPEG SOF0 dimensions after APP0
|
||||
|
||||
- **WHEN** `ImageProcessor.extractMetadata` receives JPEG with an APP0 segment before SOF0
|
||||
- **THEN** dimensions are read from the SOF0 segment
|
||||
|
||||
#### Scenario: Handle short BMP data gracefully
|
||||
|
||||
- **WHEN** `ImageProcessor.extractMetadata` receives a BMP shorter than 26 bytes
|
||||
- **THEN** `metadata.width` and `metadata.height` are `0`
|
||||
|
||||
**Verified by:** `src/processor/processor.test.ts::"extracts PNG metadata"` AND `src/processor/processor.test.ts::"handles JPEG with multiple segments before SOF"` AND `src/processor/processor.test.ts::"handles short BMP data"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Resize math with fit modes
|
||||
|
||||
The package SHALL calculate output dimensions from `ResizeOptions` supporting `cover`, `contain`, `fill`, `inside`, `outside` fits, plus `withoutEnlargement`. Omitting both `width` and `height` SHALL return original dimensions.
|
||||
|
||||
#### Scenario: Contain fit scales down to fit both axes
|
||||
|
||||
- **WHEN** `calculateResize({ width: 800, height: 600 }, { width: 200, height: 200, fit: "contain" })`
|
||||
- **THEN** result width and height are both `<= 200` and maintain aspect ratio
|
||||
|
||||
#### Scenario: WithoutEnlargement prevents upscaling
|
||||
|
||||
- **WHEN** `calculateResize({ width: 200, height: 150 }, { width: 800, withoutEnlargement: true })`
|
||||
- **THEN** result is `{ width: 200, height: 150 }`
|
||||
|
||||
**Verified by:** `src/types/image-types.test.ts::"contain fit scales down"` AND `src/types/image-types.test.ts::"withoutEnlargement prevents upscaling"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Crop region validation
|
||||
|
||||
The package SHALL reject crop regions with negative origin, zero or negative size, or bounds exceeding the source dimensions.
|
||||
|
||||
#### Scenario: Reject crop exceeding image width
|
||||
|
||||
- **WHEN** `validateCrop({ width: 800, height: 600 }, { x: 700, y: 0, width: 200, height: 100 })`
|
||||
- **THEN** the result is `false`
|
||||
|
||||
**Verified by:** `src/types/image-types.test.ts::"rejects crop exceeding width"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ImageProcessor metadata extraction with limits and events
|
||||
|
||||
The package SHALL provide `ImageProcessor` with configurable `maxPixels`, `defaultQuality`, and `defaultFormat`. `extractMetadata` SHALL emit `"metadata.extracted"` via the internal event bus and throw `IMAGE_CODES.TOO_LARGE` when pixel count exceeds the limit.
|
||||
|
||||
#### Scenario: Throw when image exceeds maxPixels
|
||||
|
||||
- **WHEN** `extractMetadata` is called on an 800×600 image with `maxPixels: 100`
|
||||
- **THEN** it throws `ImageError` with code `IMAGE.TOO_LARGE`
|
||||
|
||||
#### Scenario: Emit metadata.extracted event on success
|
||||
|
||||
- **WHEN** `extractMetadata` succeeds
|
||||
- **THEN** the `"metadata.extracted"` event fires with the metadata object
|
||||
|
||||
**Verified by:** `src/processor/processor.test.ts::"throws for image exceeding maxPixels"` AND `src/processor/processor.test.ts::"emits metadata.extracted event"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Transform pipeline planning
|
||||
|
||||
The package SHALL provide `PipelineBuilder` with a fluent API for `resize`, `crop`, `rotate`, `flip`, `quality`, and `toFormat`. `ImageProcessor.planTransform` SHALL compute final dimensions by applying operations in order and emit `"transform.planned"`.
|
||||
|
||||
#### Scenario: 90-degree rotation swaps dimensions
|
||||
|
||||
- **WHEN** planning a transform with `rotate(90)` on an 800×600 image
|
||||
- **THEN** result dimensions are 600×800
|
||||
|
||||
#### Scenario: Invalid crop region throws processing error
|
||||
|
||||
- **WHEN** planning a transform with a crop region that exceeds current dimensions
|
||||
- **THEN** it throws `ImageError` with code `IMAGE.PROCESSING_FAILED`
|
||||
|
||||
**Verified by:** `src/processor/processor.test.ts::"applies 90-degree rotation"` AND `src/processor/processor.test.ts::"throws for invalid crop"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod schema validation on public boundaries
|
||||
|
||||
The package SHALL validate `ImageProcessorConfig`, `CropRegion`, `ResizeOptions`, `TransformPipeline`, and `TransformOp` via Zod schemas at public entrypoints. Invalid input SHALL throw `IMAGE_CODES.DIMENSIONS_INVALID` (or `IMAGE_CODES.PROCESSING_FAILED` where semantically appropriate).
|
||||
|
||||
#### Scenario: Invalid config rejects negative maxPixels
|
||||
|
||||
- **WHEN** constructing `ImageProcessor({ maxPixels: -1 })`
|
||||
- **THEN** it throws `ImageError` with code `IMAGE.DIMENSIONS_INVALID`
|
||||
|
||||
#### Scenario: Invalid crop in PipelineBuilder throws
|
||||
|
||||
- **WHEN** `PipelineBuilder.crop({ x: 0, y: 0, width: -1, height: 10 })`
|
||||
- **THEN** it throws `ImageError` with code `IMAGE.DIMENSIONS_INVALID`
|
||||
|
||||
**Verified by:** `src/processor/processor.spec-conformance.test.ts::"rejects invalid config"` AND `src/processor/processor.spec-conformance.test.ts::"rejects negative crop dimensions"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry instrumentation
|
||||
|
||||
The package SHALL emit OTel counters (`image.metadata.extracted_total`, `image.transform.planned_total`) and histograms (`image.metadata.extract_duration_ms`, `image.transform.plan_duration_ms`) from production code paths. Spans SHALL wrap `extractMetadata` and `planTransform`.
|
||||
|
||||
#### Scenario: Extract metadata increments counter and records histogram
|
||||
|
||||
- **WHEN** `extractMetadata` is called with a valid image
|
||||
- **THEN** `image.metadata.extracted_total{format="png",outcome="success"}` increments by 1 and `image.metadata.extract_duration_ms` receives a non-negative observation
|
||||
|
||||
#### Scenario: Span wraps planTransform
|
||||
|
||||
- **WHEN** `planTransform` is called
|
||||
- **THEN** a span named `image.transform.plan` is started and ended
|
||||
|
||||
**Verified by:** `src/processor/processor.spec-conformance.test.ts::"increments extracted counter on success"` AND `src/processor/processor.spec-conformance.test.ts::"creates planTransform span"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AbortSignal propagation
|
||||
|
||||
The package SHALL accept an optional `AbortSignal` on async and synchronous entrypoints. A pre-aborted signal SHALL short-circuit before work begins with `IMAGE_CODES.PROCESSING_FAILED` and cause set to `AbortError`.
|
||||
|
||||
#### Scenario: Pre-aborted signal short-circuits extractMetadata
|
||||
|
||||
- **WHEN** `extractMetadata` receives `signal` that is already aborted
|
||||
- **THEN** it throws `ImageError` with code `IMAGE.PROCESSING_FAILED` and `cause.name === "AbortError"`
|
||||
|
||||
**Verified by:** `src/processor/processor.spec-conformance.test.ts::"short-circuits on pre-aborted signal"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports and sideEffects false
|
||||
|
||||
The package SHALL declare `sideEffects: false` and provide subpath exports for `./error-codes/error-codes`, `./errors/errors`, `./types/image-types`, and `./processor/processor`. Feature-module barrels SHALL use explicit named re-exports.
|
||||
|
||||
#### Scenario: Tree-shake test passes
|
||||
|
||||
- **WHEN** bundling with only `detectFormat` imported
|
||||
- **THEN** `ImageProcessor` code is not included in the output
|
||||
|
||||
**Verified by:** `tests/tree-shake.test.ts::"tree-shakes unused exports"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
63
openspec/archive/2026-05-22-migrate-image/tasks.md
Normal file
63
openspec/archive/2026-05-22-migrate-image/tasks.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Tasks — Migrate @suaveplan/image to genesis
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/image.md` end-to-end
|
||||
- [x] 1.2 Read upstream source (`../suaveplan/packages/universal/image/src/`)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
|
||||
- [x] 1.5 Fill in `## Source mapping` table
|
||||
- [x] 1.6 Draft `specs/image/spec.md` from `openspec/templates/spec-delta.md`
|
||||
- [x] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold package at `packages/media/image/image/`
|
||||
- [x] 2.2 Implement `src/error-codes/error-codes.ts` with `defineCodes`
|
||||
- [x] 2.3 Implement `src/errors/errors.ts` with `ImageError` + factories
|
||||
- [x] 2.4 Implement `src/types/image-types.ts` with Zod schemas + pure functions
|
||||
- [x] 2.5 Implement `src/processor/processor.ts` with `ImageProcessor`, `PipelineBuilder`, OTel
|
||||
- [x] 2.6 Wire root barrel `src/index.ts` with explicit named exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests at `src/error-codes/error-codes.test.ts`
|
||||
- [x] 3.2 Unit tests at `src/errors/errors.test.ts`
|
||||
- [x] 3.3 Unit tests at `src/types/image-types.test.ts`
|
||||
- [x] 3.4 Unit tests at `src/processor/processor.test.ts`
|
||||
- [x] 3.5 Spec-conformance tests at `src/processor/processor.spec-conformance.test.ts`
|
||||
- [x] 3.6 Integration tests at `tests/dep-graph.test.ts` and `tests/tree-shake.test.ts`
|
||||
- [x] 3.7 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts`
|
||||
- [x] 4.2 README with quick-start, modules, install
|
||||
- [x] 4.3 Update `CLAUDE.md` package listing
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/image` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/image` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/image` — exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/image` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim
|
||||
- [x] 6.2 Zod schemas wired at every declared boundary
|
||||
- [x] 6.3 Every error code in `error-codes.ts` thrown by at least one production path
|
||||
- [x] 6.4 Strict-mode behaviors tested both on and off
|
||||
- [x] 6.5 OTel instruments emitted from production paths and asserted with real MeterProvider
|
||||
- [x] 6.6 Lifecycle dedup tested where claimed
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/image/spec.md`
|
||||
- [x] 7.2 Move `openspec/changes/migrate-image/` → `openspec/archive/<yyyy-mm-dd>-migrate-image/`
|
||||
- [x] 7.3 Refresh `MANIFEST.md`
|
||||
- [x] 7.4 Update `STATUS.md` and `SUMMARY.md`
|
||||
Reference in New Issue
Block a user