Files
suaveplan/openspec/archive/2026-05-25-enterprise-video/proposal.md
2026-05-27 23:29:53 +01:00

112 lines
7.4 KiB
Markdown

# Change: enterprise-video
- **Package**: `@suaveplan/video`
- **Status**: PROPOSED
- **Design**: ./design.md
- **Spec delta**: ./specs/video/spec.md
- **Layout**: `packages/media/video/video/`
- **Tier**: universal (no Node-only / browser-only APIs in `src/`)
- **Genesis deps**: `@suaveplan/error` shipped, `@suaveplan/events` shipped
## Why
`@suaveplan/video` is the canonical universal-tier video-metadata and
transform-planning capability on the suaveplan platform. It underpins the
pending `@suaveplan/video-thumbnails` (ffmpeg-backed thumbnail renderer) and
`@suaveplan/react-video` (browser playback) siblings, both of which read
`VideoMetadata`/`VideoTransformPipeline`/`VideoTransformResult` from this
package. Upstream ships 1517 LOC covering magic-byte format detection,
metadata extraction with optional caller-supplied probe data, resolution
presets, an evented `VideoProcessor` + fluent `VideoPipelineBuilder`, and a
15-factory typed error registry with HTTP/severity/retry metadata.
Genesis must own this capability before the siblings can land. The legacy
package uses the deprecated `BaseError` / `ErrorCodes.registerMany` registry
and the Node-style `EventEmitter<[T]>` array-tuple shape — genesis migrates
these to `SuaveplanError` + `defineCodes("VIDEO", …)` + `EventBus<TMap>` while
preserving the full public surface and adding the enterprise quality bar the
audit calls for: Zod boundary validation, OTel counters + histograms + spans,
`AbortSignal` propagation, schema-validated config, deterministic clocks,
100/100/100/100 coverage, and tier-pure imports.
## What changes
1. New package `packages/media/video/video/` at `@suaveplan/video@0.1.0`.
2. `assignment.yml` `__categories__:` row updated: `media` now includes the
`video` plugin folder (alongside `audio`/`image`/`fonts`/etc.).
3. Full upstream public surface preserved — every type, every constant, every
error factory, every method on `VideoProcessor` / `VideoPipelineBuilder`.
4. `VideoError` migrates from `BaseError<Code, Context>` to a
`SuaveplanError<VideoCode>` subclass that retains a typed `context` field;
codes move from `ErrorCodes.registerMany` to `defineCodes("VIDEO", […])`.
5. `VideoProcessor` migrates from Node-style `EventEmitter<VideoEventMap>`
(array-tuple payloads) to composing `EventBus<VideoEventMap>` (single
payload per topic, matching `@suaveplan/image`'s convention) and exposes
`on` / `once` / `off`.
6. Enterprise enhancements: Zod-validated config + pipeline boundaries, OTel
meter `@suaveplan/video` exposing 2 counters + 2 histograms + named spans,
`AbortSignal` on every public entrypoint that can do work, injectable
`Clock`.
7. Co-located `.md` docs per implementation file (≥200 words, 6 required
sections), package-root `README.md` with a Modules index.
8. Subpath exports per feature module so consumers can tree-shake.
## Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `VideoFormat` / `VideoCodec` / `AudioCodec` / `ResolutionPreset` enums | x | x | Re-expressed as Zod enums + inferred types. |
| `VideoDimensions` / `VideoMetadata` / `TrimRange` / `VideoResizeOptions` | x | x | Mirrored with Zod schemas (`VideoMetadataSchema` etc.). |
| `VideoTransformOp` discriminated union (9 variants) | x | x | Migrated to `z.discriminatedUnion` for boundary validation. |
| `VideoTransformPipeline` / `VideoTransformResult` | x | x | Schemas + inferred types. |
| `VideoEventMap` | x | up | Re-shaped from `[T]` tuples to single-payload `T` to match `EventBus<TMap>`. Semantic `metadata.extracted` / `transform.planned` / `pipeline.built` keys preserved. |
| `detectVideoFormat` (MP4/MOV/MKV/AVI/FLV/OGG magic bytes) | x | x | Byte-for-byte identical sniffer. |
| `calculateVideoResize` (preset + maintainAspectRatio + even-dimensions) | x | x | Identical algorithm; Zod-validated input. |
| `validateTrimRange` | x | x | Identical. |
| `formatDuration` / `parseDuration` round-trip | x | x | Identical. |
| `videoFormatToExtension` | x | x | Identical. |
| `RESOLUTION_PRESETS` (6 presets) | x | x | Identical. |
| `VIDEO_MIME_TYPES` (8 formats) | x | x | Identical. |
| `VideoError` typed error | x | up | Migrated to `SuaveplanError<VideoCode>`; codes namespaced via `defineCodes("VIDEO", …)`. |
| 15 error-factory functions | x | x | All preserved: `unknownVideoFormat`, `invalidVideoDimensions`, `invalidTrimRange`, `videoMetadataParseFailed`, `invalidVideoTransform`, `durationExceeded`, `unsupportedCodec`, `videoNotFound`, `videoDecodeFailed`, `videoEncodeFailed`, `videoFormatUnsupported`, `videoTooLarge`, `videoProcessingFailed`, `videoCodecUnsupported`, `videoTimeout`. |
| `VIDEO_ERROR_METADATA` HTTP/severity/retry table | x | x | Preserved as a typed const for problem+json / retry consumers. |
| `registerVideoErrorCodes` callable | x | up | Kept as a callable; genesis `defineCodes` is module-local so the function is a no-op alias returning the metadata table — same name, same shape, callers keep compiling. |
| `VideoProcessor.extractMetadata` | x | up | Adds `AbortSignal`, OTel counter + histogram + span, Zod-validated config, deterministic clock. |
| `VideoProcessor.planTransform` (9 op types) | x | up | Adds `AbortSignal`, Zod-validated pipeline, OTel counter + histogram + span. |
| `VideoProcessor.createPipeline()` | x | x | Identical. |
| `VideoPipelineBuilder` (resize/trim/rotate/flip/speed/mute/toFormat/codec/bitrate/build/apply) | x | x | Identical fluent surface. |
| `VideoProcessorConfig` (`maxDuration`, `defaultFormat`) | x | up | Zod-validated; configs that fail validation throw `VideoError(VIDEO.CONFIG_INVALID)`. |
| `pipeline.built` event | x | up | Now actually emitted from `VideoPipelineBuilder.build()` (upstream declared but never fired). |
| Side-effect `import "./error-codes.js"` registration | x | down | Genesis `defineCodes` is module-local and side-effect-free; equivalent metadata is exposed via `VIDEO_ERROR_METADATA` re-export and `registerVideoErrorCodes()` is retained as a no-op alias. |
The single `down` row (the side-effect import) is structurally replaced by
the `defineCodes` model — same callers, same outcome, no functional drop.
## Impact
### Affected dependents
`bunx turbo ls --filter=...@suaveplan/video` — package is brand-new on genesis,
so no genesis consumers exist yet. The future `@suaveplan/video-thumbnails`
(ffmpeg renderer) and `@suaveplan/react-video` (browser playback) packages
will consume this; both are still `-` (pending) in `STATUS.md` and will be
migrated in follow-up changes against this stable interface.
No dependents (verified by `bunx turbo ls --filter=...@suaveplan/video`).
## Migration
For consumers porting from the upstream `@suaveplan/video` shape:
- **VideoError**: same import path, same `err.code` string values, same
`err.message`, `err.cause`, `err.context`. Base class moved from
`BaseError` to `SuaveplanError`; `instanceof VideoError` keeps working.
- **Event listeners**: upstream listeners on Node `EventEmitter` received the
payload as the first element of a tuple (`(meta) => …` since `args[0]`
destructures the tuple); under `EventBus` the listener receives the payload
directly. Idiomatic call sites are unchanged.
- **`registerVideoErrorCodes()`**: still callable, still side-effect-safe;
registration is now driven by `defineCodes` at module-load time.
- Search command for upstream consumers (none on genesis today):
`grep -rln "@suaveplan/video" packages/ | grep -v "/dist/\|/coverage/"`.