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,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).

View 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 -->

View 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`