@faceless-photolib/schemas
Zod schemas as the single source of truth for faceless-photolib types — branded IDs, the Result/Resource vocabulary, the Layer discriminated union, color spaces, transforms, and masks.
Zod schemas as the single source of truth for faceless-photolib — a headless, color-managed, GPU-accelerated image-editing engine.
Every cross-package type — branded IDs, the Result / Resource vocabulary, the Layer
discriminated union, color-space identifiers, transforms, and masks — is defined once here as a
Zod schema with its inferred TypeScript type. There are no optional or nullable fields; unions are
named and discriminated, so invalid states fail safeParse rather than slipping through.
Install
pnpm add @faceless-photolib/schemasUsage
import { match } from "ts-pattern";
import {
LayerIdSchema,
LayerSchema,
ok,
rejected,
type Result,
resource,
ready,
type Layer,
} from "@faceless-photolib/schemas";
// Branded IDs: a LayerId is not assignable from a bare string.
const id = LayerIdSchema.parse("layer-1");
// Validate untrusted input against the Layer discriminated union.
const parsed = LayerSchema.safeParse(input);
if (!parsed.success) {
// parsed.error.issues describes exactly what was wrong.
}
// Construct canonical results instead of throwing or returning null.
function lockedEdit(layer: Layer): Result<Layer> {
if (layer.locks.composite) return rejected("locked", "composite lock is active");
return ok(layer);
}
// Pattern-match the result; the union forces every variant to be handled.
const message = match(lockedEdit(layer))
.with({ kind: "ok" }, () => "applied")
.with({ kind: "rejected" }, (r) => `rejected: ${r.reason} — ${r.detail}`)
.otherwise(() => "other outcome");
// Resource<T> models async/heavy work with explicit idle/loading/ready/error states.
const renderSchema = resource(LayerSchema);
const state = ready(layer); // { status: "ready", value: layer }API
| Export | Description |
|---|---|
LayerIdSchema, DocumentIdSchema, AssetIdSchema, MaskIdSchema, ContentHashSchema | Branded string-ID schemas (with matching inferred types); structurally incompatible with each other. |
LayerSchema / Layer | Discriminated union (on type) of image, adjustment, lut3d, colorTransform, fill, text, and recursive group layers. |
result(value) / type Result<T> | Result-vocabulary factory and TS union (ok, not-found, invalid-request, validation-error, rejected, conflict, backend-unavailable). |
ok, notFound, invalidRequest, validationError, rejected, conflict, backendUnavailable | Variant constructors for Result<T>. |
issuesFromZod(error) | Maps a ZodError's issues into the canonical Issue[] shape. |
resource(value) / type Resource<T> | Async-lifecycle factory and TS union with idle, loading, ready, error. |
idle, loading, ready, errored | Variant constructors for Resource<T>. |
ColorSpaceSchema, ColorSpaceIdSchema, PrimariesSchema, TransferFnSchema, ChromaticitySchema | Color-management schemas (ACEScg-centric color spaces, primaries, transfer functions). |
BlendModeSchema, BlendSpaceSchema | Blend-mode and blend-space enums. |
TransformSchema, Mat3Schema, DecomposedTransformSchema, ResampleQualitySchema, IDENTITY_MAT3 | Geometry: affine transforms, 3×3 matrices, resample quality. |
MaskSchema / Mask, NO_MASK | Layer mask schema plus the canonical empty-mask constant. |
DocumentSchema, CanvasSizeSchema, DocumentSettingsSchema, CURRENT_SCHEMA_VERSION | Document model and current schema version. |
StoredDocumentSchema, DocV1Schema | On-disk file-format schemas. |
RenderResultSchema, ViewportSchema, SurfaceSchema | Render-output, viewport, and surface schemas. |
Each schema is paired with its z.infer TypeScript type (e.g. LayerSchema / Layer). engine-api
re-exports the Result / Resource vocabulary so leaf packages can construct outcomes without
depending on it.
License
MIT
API reference
113 public exports · 58 documented · generated from source.
collectDocV1AssetHashesfunctioncollectDocV1AssetHashes(doc: { [x: string]: unknown; schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly LayerV1[]; }): readonly (string & $brand<...>)[]Collect the distinct bare asset hashes a v1 document's image layers reference (groups walked recursively), in first-appearance order. The engine resolves these into `ContentRef`s before calling the migration.
computeHistogramsfunctioncomputeHistograms(result: { width: number; height: number; colorSpace: string & $brand<"ColorSpaceId">; pixels: Float32Array<ArrayBuffer>; }): { r: Uint32Array<ArrayBuffer>; g: Uint32Array<...>; b: Uint32Array<...>; luma: Uint32Array<...>; peak: number; }Per-channel 256-bin histograms (R, G, B, luma) from straight f32 RGBA pixels (stride 4). This is the CPU golden reference the GPU compute histogram pass is conformance-checked against (gpu-histogram spec, "CPU-reference conformance"), and it backs the Histogram panel and features-page demo (via the injected CPU test backend's pixel-bearing `RenderResult`). `peak` is the max R/G/B bin count only (see {@link peakOfChannels}).
conflictfunctionconflict<T = never>(what: string, detail: string): Result<T>erroredfunctionerrored<T = never>(message: string): Resource<T>idlefunctionidle<T = never>(): Resource<T>imageSourceKeyfunctionimageSourceKey(source: { t: "content"; ref: { algo: "b3"; digest: string & $brand<"HexDigest">; mediaType: string & $brand<"MediaType">; role: "master" | "preview" | "cache" | "document" | "history"; bytes: number; }; } | { ...; }): stringThe stable string key a source lowers to for render-graph pass identity and synchronous `SourceResolver` lookups: a content source keys by its versioned `v1:b3:<hex>` ref string, an external source by its URI. Two sources with the same key are the same pixels.
invalidRequestfunctioninvalidRequest<T = never>(issues: readonly { path: readonly (string | number)[]; message: string; code: string; }[]): Result<T>issuesFromZodfunctionissuesFromZod(error: ZodError<unknown>): readonly { path: readonly (string | number)[]; message: string; code: string; }[]Convert a `ZodError`'s issues into the canonical `Issue[]` shape.
loadingfunctionloading<T = never>(): Resource<T>migrateDocV1toV2functionmigrateDocV1toV2(doc: { [x: string]: unknown; schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly LayerV1[]; }, assetRefs: V1AssetRefIndex): Result<...>The first REAL step of the migration chain: `DocV1 → DocV2` (native-file-format D7). Pure and deterministic — a function of the parsed v1 document and the caller-supplied asset-ref index only; migrating the same inputs twice yields byte-identical v2 documents (and identical records). Unknown top-level forward-compatible fields are preserved; the original v1 state is untouched (the record's `sourceRef` is its canonical content ref, so the pre-migration bytes remain retrievable wherever they were stored). Failure modes (all `validation-error`, naming the offending fields; no partially-migrated document is ever returned): - an image asset hash the index cannot map, - a produced payload that fails `DocV2Schema` (the "broken step" contract of the file-format spec), - a document that cannot be canonicalized for the provenance record.
notFoundfunctionnotFound<T = never>(what: string): Result<T>okfunctionok<T>(value: T): Result<T>peakOfChannelsfunctionpeakOfChannels(r: Uint32Array<ArrayBufferLike>, g: Uint32Array<ArrayBufferLike>, b: Uint32Array<ArrayBufferLike>): numberThe max R/G/B bin count across all 256 bins — the value both histogram UIs (`HistogramPanel.tsx`, features-page demo) scale their bar heights against, shared here so neither duplicates the reduction. Luma is deliberately excluded because it is every channel's weighted sum and typically taller than any single channel; folding it in would shrink the R/G/B bars.
readyfunctionready<T>(value: T): Resource<T>rejectedfunctionrejected<T = never>(reason: "not-implemented" | "locked" | "degenerate" | "unsupported" | "out-of-range" | "conflict", detail: string): Result<T>resourcefunctionresource<T extends z.ZodTypeAny>(value: T): ZodDiscriminatedUnion<[ZodObject<{ status: ZodLiteral<"idle">; }, $strip>, ZodObject<{ status: ZodLiteral<"loading">; }, $strip>, ZodObject<...>, ZodObject<...>], "status">`Resource<T>` — the lifecycle of asynchronous or heavy work (a render, an export). Held with `@observable.ref` in UI samples; every async UI action renders all four states distinctly (project.md §4, engine-api spec). Note the controller `idle` *lifecycle* state (interaction-controllers spec) is a different concept from this async `idle` — controllers are synchronous and have no `loading` phase.
resultfunctionresult<T extends z.ZodTypeAny>(value: T): ZodDiscriminatedUnion<[ZodObject<{ kind: ZodLiteral<"ok">; value: T; }, $strip>, ZodObject<{ kind: ZodLiteral<"not-found">; what: ZodString; }, $strip>, ... 4 more ..., ZodObject<...>], "kind">Generic result factory. `result(z.object({...}))` yields a Zod schema for the success payload plus every canonical failure variant; `z.infer` of it is the `Result<T>` TS type. Discriminated on `kind`, so callers `match(result).with({ kind: "ok" }, …).exhaustive()`.
upgradeStoredDocumentfunctionupgradeStoredDocument(stored: { [x: string]: unknown; schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly LayerV1[]; } | { ...; }, ctx: { ...; }): Result<...>Apply the migration chain to a parsed stored document until it reaches the current version (file-format spec: deterministic linear chain). A current- version document passes through untouched with no records; an older document is upgraded step by step, each step contributing its provenance record. Any failing step surfaces its `validation-error` unchanged — never a partially migrated document.
validationErrorfunctionvalidationError<T = never>(fields: readonly { path: readonly (string | number)[]; message: string; code: string; }[]): Result<T>MigratedDocV2interfaceinterface MigratedDocV2A successful migration step's output: the upgraded doc + its provenance record.
UpgradedDocumentinterfaceinterface UpgradedDocumentThe outcome of upgrading a stored document to the current version.
AdjustmentParamstypetype AdjustmentParamsAssetIdtypetype AssetIdBlendModetypetype BlendModeBlendSpacetypetype BlendSpaceCanvasSizetypetype CanvasSizeChromaticitytypetype ChromaticityColorSpacetypetype ColorSpaceColorSpaceIdtypetype ColorSpaceIdContentHashtypetype ContentHashDecomposedTransformtypetype DecomposedTransformDocumenttypetype DocumentDocumentIdtypetype DocumentIdDocumentSettingstypetype DocumentSettingsDocV1typetype DocV1DocV2typetype DocV2FillContenttypetype FillContentGroupIsolationtypetype GroupIsolationGroupLayertypetype GroupLayerA group layer, expressed explicitly because of the recursive `children` field.
GroupLayerV1typetype GroupLayerV1A v1 group layer, expressed explicitly because of the recursive `children`.
HistogramStatetypetype HistogramStateImageSourcetypetype ImageSourceIssuetypetype IssueLayertypetype Layer`Layer` — a discriminated union on `type` with a shared base (document-model spec; D4). `group` is recursive (children are themselves `Layer`s) so groups nest to arbitrary depth, hence the `z.lazy` for the child stack. An unknown `type`, missing discriminant, or mismatched variant-field is unrepresentable: it fails `safeParse`.
LayerIdtypetype LayerIdLayerLockstypetype LayerLocksLayerTypetypetype LayerTypeLayerV1typetype LayerV1The v1 layer union (recursive through `group.children`).
Masktypetype MaskMaskIdtypetype MaskIdMat3typetype Mat3MigrationRecordtypetype MigrationRecordMigrationSteptypetype MigrationStepOutputTransformVersiontypetype OutputTransformVersionPrimariestypetype PrimariesRejectReasontypetype RejectReasonRenderResulttypetype RenderResultResampleQualitytypetype ResampleQualityResourcetypetype Resource`Resource<T>` as a plain TS discriminated union.
Resulttypetype Result`Result<T>` as a plain TS discriminated union, decoupled from any specific payload schema so signatures across the workspace can name `Result<Foo>` uniformly.
StoredDocumenttypetype StoredDocumentSurfacetypetype SurfaceTransferFntypetype TransferFnTransformtypetype TransformV1AssetRefIndextypetype V1AssetRefIndexThe data a v1 → v2 migration needs beyond the document itself: for every v1 bare asset hash, the full typed `ContentRef` the rewritten `{ t: "content" }` source will carry. Plain data (hash → ref) so the migration stays a pure, deterministic function of its inputs — the CALLER (engine-api) performs the I/O of resolving bytes and minting refs, then hands the index in.
Viewporttypetype ViewportAdjustmentParamsSchemaconstAdjustmentParamsSchema: ZodObject<{ effect: ZodString; params: ZodRecord<ZodString, ZodUnknown>; }, $strip>Adjustment payloads (interpreted by the `adjustments` package).
AssetIdSchemaconstAssetIdSchema: $ZodBranded<ZodString, "AssetId", "out">BlendModeSchemaconstconst BlendModeSchemaThe complete set of 27 blend modes (blend-compositing spec; D5). Order mirrors the spec's enumeration: 12 separable, then the Photoshop-only modes, then the 4 non-separable HSL modes. A value outside this set fails `safeParse` and returns `invalid-request` — never silently substituted with `normal`. Names are the camelCase identifiers used throughout the engine; the spec's display names map 1:1 (e.g. "Linear Dodge (Add)" → `linearDodge`).
BlendSpaceSchemaconstBlendSpaceSchema: ZodEnum<{ "linear-light": "linear-light"; perceptual: "perceptual"; }>Per-document toggle selecting the space blends are computed in (blend-compositing spec; D5): physically-correct linear-light vs Photoshop-parity perceptual.
CanvasSizeSchemaconstCanvasSizeSchema: ZodObject<{ width: ZodNumber; height: ZodNumber; }, $strip>Pixel dimensions of the document canvas.
ChromaticitySchemaconstChromaticitySchema: ZodObject<{ x: ZodNumber; y: ZodNumber; }, $strip>A CIE xy chromaticity coordinate.
ColorSpaceIdSchemaconstColorSpaceIdSchema: $ZodBranded<ZodString, "ColorSpaceId", "out">A color-space id, branded so an unknown id cannot masquerade as a resolved one.
ColorSpaceSchemaconstconst ColorSpaceSchemaA registered color space. `toReference`/`fromReference` are 3x3 row-major matrices.
ContentHashSchemaconstContentHashSchema: $ZodBranded<ZodString, "ContentHash", "out">A content-addressed asset hash (CAS). Assets are referenced by hash, never inlined.
CURRENT_SCHEMA_VERSIONconstCURRENT_SCHEMA_VERSION: 2The current persisted document schema version literal (v2: content-ref image sources).
DecomposedTransformSchemaconstconst DecomposedTransformSchemaDecomposed transform components (W3C "unmatrix"). Transforms are *stored* decomposed so interactive handles stay editable and recomposed to a `Mat3` at render (transform-geometry spec; D8). This is a required named field of every `Transform`, never optional.
DOC_MIGRATION_CHAINconstDOC_MIGRATION_CHAIN: readonly { id: string; from: number; to: number; }[]The declared chain: each shipped version `vN` has exactly one `up()` step to `vN+1`. {@link upgradeStoredDocument} applies the steps in order.
DOC_MIGRATION_V1_TO_V2constDOC_MIGRATION_V1_TO_V2: "doc-migration/v1-to-v2@1"The stable identifier of the v1 → v2 migration. Recorded in every {@link MigrationRecord} so provenance survives as data — a package that carries `(sourceRef, migrationId) → resultRef` can always retrieve the pre-migration bytes and name the transform that produced the current state.
DocumentIdSchemaconstDocumentIdSchema: $ZodBranded<ZodString, "DocumentId", "out">DocumentSchemaconstconst DocumentSchemaThe in-memory `Document` model. Layers are an ordered stack — index 0 is the bottom-most, the last index is the top-most (document-model spec). Every edit produces a new `Document` value (immer); source pixels are never mutated.
DocumentSettingsSchemaconstconst DocumentSettingsSchemaDocument-level rendering settings (color-management + blend specs). `connectionSpace` is the working/reference space (ACEScg in v1); the blend space toggle and output-transform version are explicit, never inferred.
DocV1Schemaconstconst DocV1SchemaThe persisted v1 document (file-format spec; D10). `.passthrough()` preserves unknown forward-compatible fields so a load → no-op → save round-trip is byte-stable (the engine MUST NOT drop content it did not intentionally change).
DocV2Schemaconstconst DocV2Schemav2 (current): image layers reference assets through the typed `{ t: "content", ref } | { t: "external", uri }` source union — never a bare hash string (native-file-format D7).
FillContentSchemaconstconst FillContentSchemaFill content as a named union — solid, linear gradient, or radial gradient.
GroupIsolationSchemaconstGroupIsolationSchema: ZodEnum<{ isolated: "isolated"; "pass-through": "pass-through"; }>Group isolation mode — an explicit field; the engine never infers it.
HISTOGRAM_BINSconstHISTOGRAM_BINS: 256Per-channel bin count: straight f32 values quantize to 256 8-bit levels.
HistogramStateSchemaconstconst HistogramStateSchemaIDENTITY_MAT3constIDENTITY_MAT3: [number, number, number, number, number, number, number, number, number]The 3x3 identity, row-major.
ImageSourceSchemaconstconst ImageSourceSchemaImage-layer asset sources (native-file-format D7). A source is a discriminated union — never a bare hash string: - `{ t: "content", ref }` — the asset lives in a content-addressed store and is referenced by the typed, versioned `ContentRef` (`v1:b3:<hex>` string form with `algo`/`digest`/`mediaType`/`role`/`bytes` metadata). - `{ t: "external", uri }` — the asset is NOT managed by the content store; the URI is an opaque resolver key (a URL, a host-registered upload id, …). A persisted document carrying a bare unversioned hash where a source is expected fails `safeParse` with an issue path naming the field — the engine surfaces it as `validation-error` and constructs no document.
IssueSchemaconstconst IssueSchemaOne structured validation finding (mirrors a Zod issue without the union sprawl).
LayerIdSchemaconstLayerIdSchema: $ZodBranded<ZodString, "LayerId", "out">Branded string IDs. Branding makes a `LayerId` structurally incompatible with a `DocumentId` even though both are strings — you cannot pass one where the other is expected (project.md §4: brand IDs).
LayerLocksSchemaconstLayerLocksSchema: ZodObject<{ transparency: ZodBoolean; composite: ZodBoolean; position: ZodBoolean; }, $strip>Layer locks (document-model spec; D4). Each is an explicit boolean — an operation that violates an active lock returns `rejected` naming the lock, never silently applies or silently ignores.
LayerSchemaconstLayerSchema: ZodType<Layer, unknown, $ZodTypeInternals<Layer, unknown>>MaskIdSchemaconstMaskIdSchema: $ZodBranded<ZodString, "MaskId", "out">MaskSchemaconstconst MaskSchemaA layer mask as a discriminated union. The `none` variant means "no mask" — never `null`/`undefined` (document-model spec: "absent mask is a named variant, not null"). v1 supports raster masks; the raster bytes are referenced by content hash through the asset store.
Mat3SchemaconstMat3Schema: ZodTuple<[ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber], null>`Mat3` — a 3x3 row-major matrix as a 9-tuple. Affine = last row [0,0,1]; perspective = full homography. Coordinate convention is fixed: top-left origin, Y-down (transform-geometry spec; D8). No backend may flip it.
MigrationRecordSchemaconstconst MigrationRecordSchemaMigration linkage recorded as data (native-file-format D7): the canonical (JCS + BLAKE3) ref of the source document, the migration id, and the canonical ref of the result document. Deterministic: the same source doc and the same asset index always produce the same record.
MigrationStepSchemaconstMigrationStepSchema: ZodObject<{ id: ZodString; from: ZodNumber; to: ZodNumber; }, $strip>One step of the linear migration chain, as data (id + version endpoints).
NO_MASKconstconst NO_MASKThe "no mask" sentinel value, reused everywhere a default mask is needed.
OutputTransformVersionSchemaconstOutputTransformVersionSchema: ZodEnum<{ "aces-1.x": "aces-1.x"; "aces-2.0": "aces-2.0"; }>Selects the output-transform generation; ACES 1.x is the default (color-management spec).
PrimariesSchemaconstconst PrimariesSchemaRGB primaries + white point as CIE xy coordinates.
RejectReasonSchemaconstconst RejectReasonSchemaBusiness-rejection reasons. `not-implemented` is always paired with a beacon.
RenderResultSchemaconstconst RenderResultSchema`RenderResult` — pure pixel data produced by a render (the payload of a `Resource<RenderResult>`). Lives in `schemas` because it carries no effect-IR references, so every consumer (render-graph, backends, engine-api, color-conformance) can name `Resource<RenderResult>` without extra coupling. Pixels are f32 RGBA in the named color space; integer quantization only ever happens at the export boundary, never here (color-management spec).
ResampleQualitySchemaconstResampleQualitySchema: ZodEnum<{ bilinear: "bilinear"; "mipmap-trilinear": "mipmap-trilinear"; bicubic: "bicubic"; lanczos: "lanczos"; }>Resampling quality for a transformed layer (transform-geometry spec; D8).
StoredDocumentSchemaconstconst StoredDocumentSchemaThe versioned union. Add `DocV3Schema`, … here as new versions ship.
SurfaceSchemaconstSurfaceSchema: ZodEnum<{ web: "web"; expo: "expo"; server: "server"; mcp: "mcp"; "node-sdk": "node-sdk"; }>The transports/runtimes the one engine drives identically (project.md §6). `mcp` is a future surface but kept in the union, never dropped.
TransferFnSchemaconstconst TransferFnSchemaTransfer function as a discriminated union. `linear` is the connection-space EOTF; named curves carry their parameters explicitly. Unknown curves are a `not-implemented` rejection at resolve time, never a silent identity.
TransformSchemaconstconst TransformSchemaA transform is a discriminated union on `kind`. Both variants carry the recomposed `matrix` and the canonical `decomposed` components (a named field, not an optional) so storage keeps handles editable while render reads the matrix directly.
ViewportSchemaconstViewportSchema: ZodObject<{ x: ZodNumber; y: ZodNumber; width: ZodNumber; height: ZodNumber; scale: ZodNumber; }, $strip>A rectangular region of the canvas to render (top-left origin, Y-down).
@faceless-photolib/engine-api
The main public entry point for the faceless-photolib engine: open a document, edit its layer stack with named commands, and render/export through a pluggable backend (CPU reference today, GPU later).
@faceless-photolib/diagnostics
Runtime diagnostics for faceless-photolib — warnNotImplemented / warnDegraded / warnUnexpected beacons backed by a dedup registry, used instead of // TODO comments.