Initial Commit
This commit is contained in:
116
openspec/archive/2026-05-25-enterprise-media/design.md
Normal file
116
openspec/archive/2026-05-25-enterprise-media/design.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Design: enterprise-media
|
||||
|
||||
## Module layout
|
||||
|
||||
```
|
||||
packages/foundation/data/media/src/
|
||||
detection/ detectMediaType, detectSignature, detectMediaTypeFromName,
|
||||
inferMimeFromSignature, defaultMimeForType
|
||||
metadata/ getMetadata, getMetadataAsync, MetadataOptions
|
||||
validation/ validateMedia, validateMediaAsync, assertValidMedia,
|
||||
ValidateOptions
|
||||
signatures/ BUILTIN_SIGNATURES, SignatureRegistry,
|
||||
defaultSignatureRegistry
|
||||
errors/ MEDIA_CODES, MEDIA_ERROR_METADATA, MediaError, asMediaError
|
||||
observability/ createMediaObservability + counter / histogram types
|
||||
types/ MediaType, MediaSignature, MediaMetadata, MediaConstraints,
|
||||
MediaResult, MediaMetadataExtractor + Zod schemas
|
||||
index.ts root barrel — explicit named re-exports
|
||||
```
|
||||
|
||||
Every feature folder is the canonical four-file pattern (`<name>.ts`,
|
||||
`<name>.test.ts`, `<name>.md`, `index.ts`). Co-located `.md` docs cover
|
||||
purpose, features, basic + advanced usage, API reference, and
|
||||
implementation notes.
|
||||
|
||||
## Detection pipeline
|
||||
|
||||
1. `detectMediaType(input, opts?)` resolves `opts.registry ??
|
||||
defaultSignatureRegistry` and delegates to `registry.detect(input)`.
|
||||
2. `detectSignature(input, opts?)` returns the matched
|
||||
`MediaSignature | null` so consumers can route on the label
|
||||
(PNG / JPEG / MP4 / …) and not just the coarse `MediaType`.
|
||||
3. `detectMediaTypeFromName(filename, opts?)` calls
|
||||
`@suaveplan/mime.getMimeType` and buckets the result by top-level
|
||||
type (`image/`, `video/`, `audio/`).
|
||||
4. `inferMimeFromSignature(sig)` maps the matched signature label to
|
||||
a canonical MIME, falling back to `defaultMimeForType(sig.type)`
|
||||
when no per-label mapping exists.
|
||||
|
||||
`SignatureRegistry` exposes `register`, `unregister`, `list`, `match`,
|
||||
and `detect`. `register` rejects empty byte arrays
|
||||
(`MEDIA.INVALID_SIGNATURE`), non-byte values
|
||||
(`MEDIA.INVALID_SIGNATURE`), and duplicate labels
|
||||
(`MEDIA.DUPLICATE_SIGNATURE`). The built-in list orders longer / more-
|
||||
specific signatures first so the first match wins.
|
||||
|
||||
## Metadata pipeline
|
||||
|
||||
`getMetadata` builds the base bundle (`{ type, mimeType, size,
|
||||
signatureLabel }`) in a private helper. `getMetadataAsync` accepts an
|
||||
optional `MediaMetadataExtractor` that receives `(input, base)` and
|
||||
returns `Partial<MediaMetadata>`; the helper merges the partial over the
|
||||
base. Extractor exceptions are wrapped in
|
||||
`MediaError(MEDIA.METADATA_EXTRACTOR_FAILED)` with the original error
|
||||
chained.
|
||||
|
||||
MIME resolution prefers the explicit `filename` field (via
|
||||
`getMimeType`) and falls back to `inferMimeFromSignature`. The base
|
||||
bundle exposes `signatureLabel` so downstream extractors can route on
|
||||
the exact format.
|
||||
|
||||
## Validation pipeline
|
||||
|
||||
`validateMedia` runs:
|
||||
|
||||
1. `MediaConstraintsSchema.safeParse(constraints)` (unless caller passes
|
||||
`skipConstraintValidation: true`). Failure raises
|
||||
`MediaError(MEDIA.INVALID_CONSTRAINTS)`.
|
||||
2. Empty-input guard: zero-byte input raises
|
||||
`MediaError(MEDIA.EMPTY_INPUT)`.
|
||||
3. `getMetadata(input, opts)` to build the bundle.
|
||||
4. `checkConstraints(metadata, constraints)` to populate the error array
|
||||
(size floors / ceilings, type / MIME allowlists, width / height
|
||||
ceilings + floors, duration ceiling).
|
||||
|
||||
`validateMediaAsync` runs the same pipeline but calls `getMetadataAsync`
|
||||
and, after constraint checks, runs `constraints.predicate(metadata,
|
||||
input)`. Predicate return values:
|
||||
|
||||
- `null` / `undefined` — pass.
|
||||
- `string` — push onto the errors array.
|
||||
- thrown — wrapped in `MediaError(MEDIA.PREDICATE_FAILED)`.
|
||||
|
||||
`assertValidMedia` calls the sync variant and throws
|
||||
`MediaError(MEDIA.VALIDATION_FAILED)` if any error is reported,
|
||||
incrementing the `media_error_total` counter with `{ code }`.
|
||||
|
||||
## Observability
|
||||
|
||||
`createMediaObservability(opts?)` builds the bundle. If
|
||||
`opts.observability` is supplied it short-circuits. Otherwise the
|
||||
function resolves `meter` and `tracer` from `opts` or the global
|
||||
`@opentelemetry/api` providers and constructs counters / histograms via
|
||||
the helpers in `@suaveplan/opentelemetry`. Every public entrypoint
|
||||
constructs a default bundle when none is supplied so calls always emit
|
||||
telemetry without requiring caller-side wiring.
|
||||
|
||||
## Error model
|
||||
|
||||
`MEDIA_CODES` is built via `defineCodes("MEDIA", [...] as const)` so the
|
||||
namespace and value strings line up with the rest of the monorepo.
|
||||
`MEDIA_ERROR_METADATA` is `Object.freeze`'d at module load.
|
||||
`MediaError` extends `SuaveplanError<MediaErrorCode>` and exposes a
|
||||
`.metadata` getter that pulls the matching record. `asMediaError(code,
|
||||
cause, fallback)` lifts unknown thrown values, preserving the original
|
||||
error as `.cause` when it's already an `Error` instance.
|
||||
|
||||
## Why split into seven feature folders
|
||||
|
||||
The package is small (~700 LOC after the enterprise rewrite) but each
|
||||
module owns a distinct concern: detection / metadata / validation are
|
||||
end-user surfaces; signatures / errors / observability are
|
||||
infrastructure shared across the three; types holds the data contracts.
|
||||
The split keeps `<file>.ts` under the 300-line hard limit and lets
|
||||
tree-shake-sensitive consumers deep-import via the subpath exports
|
||||
without dragging in the full bundle.
|
||||
89
openspec/archive/2026-05-25-enterprise-media/proposal.md
Normal file
89
openspec/archive/2026-05-25-enterprise-media/proposal.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Change: enterprise-media
|
||||
|
||||
- **Package**: `@suaveplan/media`
|
||||
- **Status**: PROPOSED
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/media/spec.md
|
||||
- **Upstream**: `/mnt/s/web/suaveplan/packages/universal/media` (1.1.0)
|
||||
- **Enterprise audit**: `/mnt/s/web/suaveplan/enterprise/media.md`
|
||||
|
||||
## Why
|
||||
|
||||
Genesis is migrating `@suaveplan/media` from the legacy
|
||||
`packages/universal/media/` checkout into the canonical
|
||||
`packages/foundation/data/media/` location alongside the already-shipped
|
||||
`@suaveplan/mime`. The upstream package shipped at 1.1.0 as a ~140-line
|
||||
universal-tier facade over four functions — `detectMediaType` (magic
|
||||
bytes), `detectMediaTypeFromName` (MIME lookup), `getMetadata`, and
|
||||
`validateMedia`. The enterprise audit (`enterprise/media.md`) flagged
|
||||
the gaps shared by every pre-enterprise package:
|
||||
|
||||
1. Hand-rolled error strings — no `@suaveplan/error` integration, no
|
||||
stable codes, no programmatic discrimination.
|
||||
2. No Zod validation of `MediaConstraints` — malformed input silently
|
||||
passes through.
|
||||
3. No OpenTelemetry instrumentation — production cannot measure
|
||||
detection / metadata / validation volume, latency, or error rate.
|
||||
4. Magic-byte signatures hard-coded inside the implementation — no
|
||||
extension point for downstream packages to add formats like AVIF,
|
||||
HEIC, or M4A.
|
||||
5. No async metadata extractor hook — width / height / duration / codec
|
||||
extraction has to happen outside the package.
|
||||
6. No custom-predicate constraint — non-trivial business rules cannot
|
||||
be expressed.
|
||||
7. No throwing assert variant for guard sites.
|
||||
8. No filename-MIME refinement when both bytes and filename are
|
||||
available.
|
||||
9. No `signatureLabel` surfacing — consumers cannot route on the exact
|
||||
matched format.
|
||||
|
||||
The greenfield rewrite addresses every gap while preserving every
|
||||
upstream public symbol so downstream consumers continue to compile
|
||||
against the same import paths.
|
||||
|
||||
## What changes
|
||||
|
||||
- **Add**: `@suaveplan/media` at version `0.1.0` under
|
||||
`packages/foundation/data/media/` with full enterprise surface — typed
|
||||
errors via `@suaveplan/error`, OTel counters + histograms,
|
||||
Zod-validated constraints, pluggable `SignatureRegistry`, async
|
||||
metadata extractor hook, custom predicate support, async + assert
|
||||
validation variants, filename + bytes MIME resolution, canonical MIME
|
||||
inference from signature label.
|
||||
- **Add**: error-code namespace `MEDIA.*` with eight stable codes
|
||||
(`EMPTY_INPUT`, `INVALID_CONSTRAINTS`, `VALIDATION_FAILED`,
|
||||
`UNSUPPORTED_TYPE`, `PREDICATE_FAILED`, `DUPLICATE_SIGNATURE`,
|
||||
`METADATA_EXTRACTOR_FAILED`, `INVALID_SIGNATURE`).
|
||||
- **Add**: OTel instruments — counters `media_detect_total`,
|
||||
`media_metadata_total`, `media_validate_total`, `media_error_total`
|
||||
and histograms `media_op_latency_ms`, `media_input_size_bytes`.
|
||||
- **Add**: subpath exports per feature module
|
||||
(`./detection/detection`, `./metadata/metadata`,
|
||||
`./validation/validation`, `./errors/errors`,
|
||||
`./observability/observability`, `./signatures/signatures`,
|
||||
`./types`).
|
||||
- **Add**: `media` to the `data` plugin in
|
||||
`scripts/folder-reorg/assignment.yml`.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
`bunx turbo ls --filter=...@suaveplan/media` — empty (media is a leaf
|
||||
migration; no current genesis package depends on it).
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
| ------------------------------------------------------------------- | -------- | -------- | -------------------------------------------------------------------------------- |
|
||||
| `detectMediaType(bytes)` | yes | yes | Now routes through pluggable `SignatureRegistry`. |
|
||||
| `detectMediaTypeFromName(filename)` | yes | yes | Same MIME-bucket semantics; emits counter + latency. |
|
||||
| `getMetadata(bytes, filename?)` | yes | yes | Adds `signatureLabel`; MIME refines to filename when present. |
|
||||
| `validateMedia(bytes, constraints, filename?)` | yes | yes | Now Zod-validated; reports MIME / minSize / minWidth / minHeight / maxDuration. |
|
||||
| `MediaType` / `MediaMetadata` / `MediaConstraints` / `MediaResult` | yes | yes | Preserved; `MediaConstraints` extended with new fields. |
|
||||
| Pluggable signatures | no | yes | New `SignatureRegistry` class + `defaultSignatureRegistry`. |
|
||||
| Async metadata extractor | no | yes | New `getMetadataAsync` + `MediaMetadataExtractor` hook. |
|
||||
| Async validation predicate | no | yes | New `validateMediaAsync` runs `constraints.predicate`. |
|
||||
| Throwing assert variant | no | yes | New `assertValidMedia`. |
|
||||
| Typed errors | no | yes | `MediaError` + `MEDIA_CODES` + per-code metadata. |
|
||||
| OTel instrumentation | no | yes | Four counters + two histograms. |
|
||||
357
openspec/archive/2026-05-25-enterprise-media/specs/media/spec.md
Normal file
357
openspec/archive/2026-05-25-enterprise-media/specs/media/spec.md
Normal file
@@ -0,0 +1,357 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: media (media)
|
||||
|
||||
**Package**: `@suaveplan/media`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Stable error code registry
|
||||
|
||||
The package SHALL expose a `MEDIA_CODES` constant created via
|
||||
`defineCodes("MEDIA", [...] as const)` containing exactly eight stable codes:
|
||||
`EMPTY_INPUT`, `INVALID_CONSTRAINTS`, `VALIDATION_FAILED`, `UNSUPPORTED_TYPE`,
|
||||
`PREDICATE_FAILED`, `DUPLICATE_SIGNATURE`, `METADATA_EXTRACTOR_FAILED`,
|
||||
`INVALID_SIGNATURE`. Each code MUST have a metadata record (`httpStatus`,
|
||||
`retryable`, `severity`, `description`) accessible via `MEDIA_ERROR_METADATA`.
|
||||
|
||||
#### Scenario: registry shape is frozen
|
||||
|
||||
- **WHEN** a consumer reads `MEDIA_CODES`
|
||||
- **THEN** the returned object MUST contain all eight expected keys and MUST
|
||||
be `Object.isFrozen`.
|
||||
|
||||
#### Scenario: every code is namespaced
|
||||
|
||||
- **WHEN** a consumer iterates `Object.values(MEDIA_CODES)`
|
||||
- **THEN** every value MUST start with the `MEDIA.` namespace prefix.
|
||||
|
||||
**Verified by:** `src/errors/errors.test.ts::"contains all eight stable codes"` AND `src/errors/errors.test.ts::"registry is frozen"` AND `src/errors/errors.test.ts::"codes carry MEDIA namespace prefix"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MediaError extends SuaveplanError
|
||||
|
||||
The package SHALL export `MediaError extends SuaveplanError<MediaErrorCode>`.
|
||||
Constructing `new MediaError(MEDIA_CODES.EMPTY_INPUT, "msg")` MUST yield an
|
||||
instance whose `.code === "MEDIA.EMPTY_INPUT"`, whose `.message === "msg"`,
|
||||
and which satisfies both `instanceof MediaError` and `instanceof Error`. The
|
||||
class MUST expose a `.metadata` getter returning the matching record from
|
||||
`MEDIA_ERROR_METADATA`.
|
||||
|
||||
#### Scenario: round-trips through cause chain
|
||||
|
||||
- **WHEN** a `MediaError` is constructed with `cause: nativeErr`
|
||||
- **THEN** `error.cause` MUST return the original native error and
|
||||
`error.code` MUST preserve the namespaced code.
|
||||
|
||||
#### Scenario: metadata getter resolves code
|
||||
|
||||
- **WHEN** a consumer reads `new MediaError(MEDIA_CODES.UNSUPPORTED_TYPE,
|
||||
"x").metadata`
|
||||
- **THEN** the returned record MUST report `httpStatus: 415` and
|
||||
`severity: "warning"`.
|
||||
|
||||
**Verified by:** `src/errors/errors.test.ts::"constructs with code, message, instanceof Error"` AND `src/errors/errors.test.ts::"preserves cause through chain"` AND `src/errors/errors.test.ts::"exposes metadata via getter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: asMediaError lift helper
|
||||
|
||||
The package SHALL expose `asMediaError(code, cause, fallback)` that wraps an
|
||||
unknown thrown value into a `MediaError`. When `cause instanceof Error` and
|
||||
its message is non-empty, the wrapped error's `.message` MUST equal the
|
||||
cause's message; otherwise it MUST equal the supplied `fallback`.
|
||||
`error.cause` MUST be an `Error` instance regardless of the cause's original
|
||||
shape.
|
||||
|
||||
#### Scenario: preserves Error message and cause
|
||||
|
||||
- **WHEN** `asMediaError(MEDIA_CODES.PREDICATE_FAILED, new Error("root"),
|
||||
"fb")` is invoked
|
||||
- **THEN** `.message` MUST be `"root"` and `.cause` MUST be the original
|
||||
Error.
|
||||
|
||||
#### Scenario: falls back to default for non-Error cause
|
||||
|
||||
- **WHEN** `asMediaError(MEDIA_CODES.PREDICATE_FAILED, "scalar", "fb")` is
|
||||
invoked
|
||||
- **THEN** `.message` MUST be `"fb"` and `.cause` MUST be a synthesised
|
||||
Error whose message is `"scalar"`.
|
||||
|
||||
**Verified by:** `src/errors/errors.test.ts::"preserves Error cause when wrapping"` AND `src/errors/errors.test.ts::"falls back to default message when cause has none"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Pluggable signature registry
|
||||
|
||||
The package SHALL expose a `SignatureRegistry` class with `register`,
|
||||
`unregister`, `list`, `match`, and `detect` methods. `BUILTIN_SIGNATURES`
|
||||
MUST be a frozen array covering at minimum PNG, JPEG, GIF, BMP, WEBP, RIFF,
|
||||
MP4/MOV, WebM/MKV, MP3 (ID3 + sync), FLAC, and OGG. A process-wide
|
||||
`defaultSignatureRegistry` MUST be exposed, seeded with `BUILTIN_SIGNATURES`.
|
||||
|
||||
#### Scenario: built-in registry detects PNG
|
||||
|
||||
- **WHEN** `defaultSignatureRegistry.detect(new Uint8Array([0x89, 0x50,
|
||||
0x4e, 0x47, 0x00]))` is invoked
|
||||
- **THEN** the result MUST be `"image"`.
|
||||
|
||||
#### Scenario: signature offset is honoured
|
||||
|
||||
- **WHEN** the registry is asked to detect MP4 bytes whose `ftyp` marker
|
||||
lives at offset 4
|
||||
- **THEN** detection MUST return `"video"`.
|
||||
|
||||
#### Scenario: register rejects duplicate label
|
||||
|
||||
- **WHEN** a caller registers a second signature whose `label` matches an
|
||||
existing entry
|
||||
- **THEN** a `MediaError` with code `MEDIA.DUPLICATE_SIGNATURE` MUST be
|
||||
thrown.
|
||||
|
||||
#### Scenario: register rejects empty bytes
|
||||
|
||||
- **WHEN** a caller registers a signature with `bytes: []`
|
||||
- **THEN** a `MediaError` with code `MEDIA.INVALID_SIGNATURE` MUST be
|
||||
thrown.
|
||||
|
||||
#### Scenario: register rejects non-byte values
|
||||
|
||||
- **WHEN** a caller registers a signature whose `bytes` array contains a
|
||||
value outside `[0, 0xff]` or a non-integer
|
||||
- **THEN** a `MediaError` with code `MEDIA.INVALID_SIGNATURE` MUST be
|
||||
thrown.
|
||||
|
||||
#### Scenario: unregister removes by label
|
||||
|
||||
- **WHEN** a caller invokes `unregister("PNG")`
|
||||
- **THEN** the method MUST return `true` and subsequent `detect` of PNG
|
||||
bytes MUST return `"unknown"`.
|
||||
|
||||
**Verified by:** `src/signatures/signatures.test.ts::"detects PNG via default registry"` AND `src/signatures/signatures.test.ts::"honours non-zero offset (MP4 ftyp)"` AND `src/signatures/signatures.test.ts::"rejects duplicate label"` AND `src/signatures/signatures.test.ts::"register rejects empty byte array"` AND `src/signatures/signatures.test.ts::"register rejects non-byte values"` AND `src/signatures/signatures.test.ts::"unregister removes signature and returns true"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Byte and filename detection
|
||||
|
||||
The package SHALL expose `detectMediaType(input, opts?)`,
|
||||
`detectSignature(input, opts?)`, and `detectMediaTypeFromName(filename,
|
||||
opts?)`. The bytes-based helpers MUST delegate to
|
||||
`opts.registry ?? defaultSignatureRegistry`. The filename helper MUST call
|
||||
`getMimeType` from `@suaveplan/mime` and bucket the result by top-level
|
||||
type prefix (`image/`, `video/`, `audio/`).
|
||||
|
||||
#### Scenario: PNG bytes route to image
|
||||
|
||||
- **WHEN** `detectMediaType` is invoked with PNG magic bytes
|
||||
- **THEN** the result MUST be `"image"`.
|
||||
|
||||
#### Scenario: empty input is detected as unknown
|
||||
|
||||
- **WHEN** `detectMediaType(new Uint8Array([]))` is invoked
|
||||
- **THEN** the result MUST be `"unknown"`.
|
||||
|
||||
#### Scenario: filename .mp4 routes to video
|
||||
|
||||
- **WHEN** `detectMediaTypeFromName("clip.mp4")` is invoked
|
||||
- **THEN** the result MUST be `"video"`.
|
||||
|
||||
**Verified by:** `src/detection/detection.test.ts::"detects PNG from magic bytes"` AND `src/detection/detection.test.ts::"returns unknown for empty input"` AND `src/detection/detection.test.ts::"detects video from .mp4 filename"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Canonical MIME inference
|
||||
|
||||
The package SHALL expose `inferMimeFromSignature(sig)` and
|
||||
`defaultMimeForType(type)`. `inferMimeFromSignature` MUST return the
|
||||
canonical MIME for each built-in label (PNG → `image/png`, JPEG →
|
||||
`image/jpeg`, MP4/MOV → `video/mp4`, WebM/MKV → `video/webm`, FLAC →
|
||||
`audio/flac`, OGG → `audio/ogg`, MP3-ID3 / MP3-sync → `audio/mpeg`). For
|
||||
unknown labels it MUST fall back to `defaultMimeForType(sig.type)`. For a
|
||||
`null` signature it MUST return `"application/octet-stream"`.
|
||||
|
||||
#### Scenario: PNG signature maps to image/png
|
||||
|
||||
- **WHEN** `inferMimeFromSignature` is called with a PNG signature record
|
||||
- **THEN** the result MUST be `"image/png"`.
|
||||
|
||||
#### Scenario: null signature falls back to octet-stream
|
||||
|
||||
- **WHEN** `inferMimeFromSignature(null)` is invoked
|
||||
- **THEN** the result MUST be `"application/octet-stream"`.
|
||||
|
||||
**Verified by:** `src/detection/detection.test.ts::"returns canonical MIME for PNG signature"` AND `src/detection/detection.test.ts::"returns octet-stream for null signature"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Metadata extraction with extractor hook
|
||||
|
||||
The package SHALL expose `getMetadata(input, opts?)` and
|
||||
`getMetadataAsync(input, opts?)`. Both MUST return a `MediaMetadata` bundle
|
||||
populated with `type`, `mimeType`, `size`, and (when a signature matched)
|
||||
`signatureLabel`. When `opts.filename` is supplied, the MIME MUST be
|
||||
resolved via `getMimeType(filename)`. The async variant MUST accept a
|
||||
`MediaMetadataExtractor` and merge its return value over the base bundle.
|
||||
|
||||
#### Scenario: getMetadata reports type, mime, size, label
|
||||
|
||||
- **WHEN** `getMetadata(pngBytes, { filename: "x.png" })` is invoked
|
||||
- **THEN** the returned bundle MUST satisfy `type === "image"`,
|
||||
`mimeType === "image/png"`, `size === pngBytes.byteLength`,
|
||||
`signatureLabel === "PNG"`.
|
||||
|
||||
#### Scenario: extractor merges async fields
|
||||
|
||||
- **WHEN** `getMetadataAsync` is invoked with an extractor that returns
|
||||
`{ width: 100, height: 50 }`
|
||||
- **THEN** the returned bundle MUST satisfy `width === 100` and
|
||||
`height === 50`.
|
||||
|
||||
#### Scenario: extractor exception is wrapped
|
||||
|
||||
- **WHEN** the supplied extractor throws
|
||||
- **THEN** `getMetadataAsync` MUST reject with a `MediaError` whose code
|
||||
is `MEDIA.METADATA_EXTRACTOR_FAILED`, preserving the original error as
|
||||
`.cause`.
|
||||
|
||||
**Verified by:** `src/metadata/metadata.test.ts::"returns type, mime, size, label for PNG bytes"` AND `src/metadata/metadata.test.ts::"merges extractor output"` AND `src/metadata/metadata.test.ts::"wraps extractor error in MediaError"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod-validated constraints
|
||||
|
||||
`MediaConstraintsSchema` SHALL validate every constraint field. Malformed
|
||||
constraints supplied to `validateMedia` or `validateMediaAsync` MUST cause
|
||||
the call to throw `MediaError(MEDIA.INVALID_CONSTRAINTS)` unless the caller
|
||||
opts out via `skipConstraintValidation: true`.
|
||||
|
||||
#### Scenario: negative maxSize is rejected
|
||||
|
||||
- **WHEN** `validateMedia(input, { maxSize: -1 })` is invoked without
|
||||
`skipConstraintValidation`
|
||||
- **THEN** a `MediaError` with code `MEDIA.INVALID_CONSTRAINTS` MUST be
|
||||
thrown.
|
||||
|
||||
#### Scenario: skipConstraintValidation bypasses Zod
|
||||
|
||||
- **WHEN** `validateMedia(input, badConstraints, { skipConstraintValidation:
|
||||
true })` is invoked
|
||||
- **THEN** the call MUST NOT throw `MEDIA.INVALID_CONSTRAINTS`.
|
||||
|
||||
**Verified by:** `src/validation/validation.test.ts::"throws INVALID_CONSTRAINTS for malformed input"` AND `src/validation/validation.test.ts::"skips Zod check when skipConstraintValidation: true"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Synchronous validation
|
||||
|
||||
`validateMedia(input, constraints, opts?)` SHALL run the empty-input guard,
|
||||
build the metadata bundle, and apply every constraint check
|
||||
(`minSize`, `maxSize`, `allowedTypes`, `allowedMimeTypes`, `maxWidth`,
|
||||
`minWidth`, `maxHeight`, `minHeight`, `maxDuration`). It MUST return a
|
||||
`MediaResult` whose `valid` flag is `errors.length === 0`. Empty input MUST
|
||||
throw `MediaError(MEDIA.EMPTY_INPUT)`.
|
||||
|
||||
#### Scenario: compliant input returns valid result
|
||||
|
||||
- **WHEN** `validateMedia(pngBytes, { maxSize: 1024, allowedTypes:
|
||||
["image"] })` is invoked
|
||||
- **THEN** the result MUST satisfy `valid === true` and `errors.length ===
|
||||
0`.
|
||||
|
||||
#### Scenario: empty input throws MEDIA.EMPTY_INPUT
|
||||
|
||||
- **WHEN** `validateMedia(new Uint8Array([]), {})` is invoked
|
||||
- **THEN** a `MediaError` with code `MEDIA.EMPTY_INPUT` MUST be thrown.
|
||||
|
||||
#### Scenario: failures are reported per rule
|
||||
|
||||
- **WHEN** `validateMedia(pngBytes, { allowedTypes: ["video"],
|
||||
allowedMimeTypes: ["image/jpeg"] })` is invoked
|
||||
- **THEN** `errors` MUST contain entries for both the unsupported type and
|
||||
unsupported MIME.
|
||||
|
||||
**Verified by:** `src/validation/validation.test.ts::"passes when all constraints met"` AND `src/validation/validation.test.ts::"rejects empty input with MediaError"` AND `src/validation/validation.test.ts::"fails when type not in allowedTypes"` AND `src/validation/validation.test.ts::"fails when MIME not in allowedMimeTypes"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Asynchronous validation with predicate
|
||||
|
||||
`validateMediaAsync` SHALL behave identically to `validateMedia` but MUST
|
||||
also run `constraints.predicate(metadata, input)` when present. A `string`
|
||||
return value MUST be appended to the `errors` array. A `null` /
|
||||
`undefined` return MUST pass. A thrown exception MUST surface as
|
||||
`MediaError(MEDIA.PREDICATE_FAILED)` with the original error chained.
|
||||
|
||||
#### Scenario: predicate returning string contributes error
|
||||
|
||||
- **WHEN** `validateMediaAsync(input, { predicate: () => "rule failed" })`
|
||||
is invoked
|
||||
- **THEN** the result MUST satisfy `valid === false` and `errors` MUST
|
||||
include `"rule failed"`.
|
||||
|
||||
#### Scenario: predicate exception wraps in MEDIA.PREDICATE_FAILED
|
||||
|
||||
- **WHEN** the supplied predicate throws
|
||||
- **THEN** the call MUST reject with `MediaError(MEDIA.PREDICATE_FAILED)`,
|
||||
preserving the original error as `.cause`.
|
||||
|
||||
**Verified by:** `src/validation/validation.test.ts::"runs predicate and reports its error string"` AND `src/validation/validation.test.ts::"predicate throwing yields PREDICATE_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: assertValidMedia throwing variant
|
||||
|
||||
`assertValidMedia(input, constraints, opts?)` SHALL invoke `validateMedia`
|
||||
and return the resulting `MediaMetadata` when `result.valid === true`. When
|
||||
any error is reported it MUST throw
|
||||
`MediaError(MEDIA.VALIDATION_FAILED)` whose message contains the joined
|
||||
errors.
|
||||
|
||||
#### Scenario: returns metadata on success
|
||||
|
||||
- **WHEN** `assertValidMedia(pngBytes, { allowedTypes: ["image"] })` is
|
||||
invoked
|
||||
- **THEN** the returned bundle's `type` MUST be `"image"`.
|
||||
|
||||
#### Scenario: throws on failure
|
||||
|
||||
- **WHEN** `assertValidMedia(pngBytes, { allowedTypes: ["video"] })` is
|
||||
invoked
|
||||
- **THEN** a `MediaError` with code `MEDIA.VALIDATION_FAILED` MUST be
|
||||
thrown.
|
||||
|
||||
**Verified by:** `src/validation/validation.test.ts::"returns metadata on success"` AND `src/validation/validation.test.ts::"throws VALIDATION_FAILED on failure"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Observability bundle factory
|
||||
|
||||
The package SHALL expose `createMediaObservability(opts?)` returning a
|
||||
`MediaObservability` bundle with `meter`, `tracer`, four counters
|
||||
(`detectTotal`, `metadataTotal`, `validateTotal`, `errorTotal`), and two
|
||||
histograms (`opLatencyMs`, `inputSizeBytes`). When `opts.observability` is
|
||||
supplied the function MUST short-circuit and return the same instance.
|
||||
|
||||
#### Scenario: factory produces complete bundle
|
||||
|
||||
- **WHEN** `createMediaObservability()` is invoked with no options
|
||||
- **THEN** the returned bundle MUST include `meter`, `tracer`, every
|
||||
counter, and every histogram.
|
||||
|
||||
#### Scenario: pre-built bundle short-circuits
|
||||
|
||||
- **WHEN** `createMediaObservability({ observability: existing })` is
|
||||
invoked
|
||||
- **THEN** the returned bundle MUST be the same reference as `existing`.
|
||||
|
||||
#### Scenario: injected meter is preserved
|
||||
|
||||
- **WHEN** `createMediaObservability({ meter })` is invoked with a custom
|
||||
meter
|
||||
- **THEN** the returned bundle's `.meter` MUST be the injected instance.
|
||||
|
||||
**Verified by:** `src/observability/observability.test.ts::"returns a fully-populated bundle with global providers by default"` AND `src/observability/observability.test.ts::"short-circuits when a pre-built observability bundle is given"` AND `src/observability/observability.test.ts::"uses an injected meter when supplied"`
|
||||
28
openspec/archive/2026-05-25-enterprise-media/tasks.md
Normal file
28
openspec/archive/2026-05-25-enterprise-media/tasks.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Tasks: enterprise-media
|
||||
|
||||
- [x] Read upstream package + enterprise audit.
|
||||
- [x] Add `media` to `data` plugin in `scripts/folder-reorg/assignment.yml`.
|
||||
- [x] Scaffold `packages/foundation/data/media/` with `package.json`,
|
||||
`tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, README.
|
||||
- [x] Author types module (`types/media.types.ts` + Zod schemas + `.md`).
|
||||
- [x] Author errors module (`MEDIA_CODES`, `MEDIA_ERROR_METADATA`,
|
||||
`MediaError`, `asMediaError` + tests + `.md`).
|
||||
- [x] Author signatures module (`BUILTIN_SIGNATURES`,
|
||||
`SignatureRegistry`, `defaultSignatureRegistry` + tests + `.md`).
|
||||
- [x] Author observability module (`createMediaObservability` + tests +
|
||||
`.md`).
|
||||
- [x] Author detection module (`detectMediaType`, `detectSignature`,
|
||||
`detectMediaTypeFromName`, `inferMimeFromSignature`,
|
||||
`defaultMimeForType` + tests + `.md`).
|
||||
- [x] Author metadata module (`getMetadata`, `getMetadataAsync` + tests
|
||||
+ `.md`).
|
||||
- [x] Author validation module (`validateMedia`, `validateMediaAsync`,
|
||||
`assertValidMedia` + tests + `.md`).
|
||||
- [x] Author root barrel + barrel test.
|
||||
- [x] Run `bunx turbo lint typecheck test build --filter=@suaveplan/media`.
|
||||
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/media`.
|
||||
- [x] Run `bun scripts/folder-reorg/generate-manifest.ts`.
|
||||
- [x] Fold spec delta into `openspec/specs/media/spec.md`.
|
||||
- [x] Archive change to `openspec/archive/2026-05-25-enterprise-media/`.
|
||||
- [x] Flip `@suaveplan/media` row in `STATUS.md` to `✅`.
|
||||
- [x] Commit on `feat/migrate-media` branch.
|
||||
Reference in New Issue
Block a user