{"version":3,"file":"contracts.mjs","names":[],"sources":["../../../src/batteries/media/contracts.ts"],"sourcesContent":["/**\n * Generic engine contracts for the media pipeline battery — the seams every implementation\n * (bundled or BYO) plugs into.\n *\n * @module @nhtio/adk/batteries/media/contracts\n *\n * @remarks\n * Engines are seams, not policies. An engine is a self-declaring capability provider: it\n * states exactly which transforms it supports — {@link ConvertCapability} edges (input MIME\n * patterns to output format tokens), {@link MutateCapability} groups (same-format content\n * transforms), and {@link EditCapability} groups (structural document operations) — and the\n * pipeline dispatches against those declarations. There are only three capability shapes\n * because there are only three things a media engine ever does: change the format, change the\n * content, or restructure the document. OCR is a convert (image to text). Transcription is a\n * convert (PCM to text). Decoding audio is a convert (container to PCM). Generating a blank\n * file is a convert (from {@link EMPTY_MIME} — the source format happens to be nothing).\n * Resizing is a mutate. Inserting a worksheet row is an edit. A new capability is a new edge\n * in the data, never a new contract.\n *\n * Engines are supplied to `createMediaPipeline` as a flat ordered array and resolved eagerly\n * at construction (declarations drive verb narrowing, so they must be known up front).\n * Bundled engines stay cheap to resolve: their heavy peer dependencies load lazily inside\n * the capability methods, on first actual use.\n *\n * All contracts are duck-typed: validation guards check structure, not class identity, so a\n * consumer can implement an interface from scratch or adapt an existing client. Contracts are\n * enforced at runtime — construction validates every engine and every declared capability and\n * throws `E_INVALID_MEDIA_PIPELINE_CONFIG` naming the offending index when a value fails.\n *\n * Two further contracts exist so that even \"run a binary\" and \"give a binary a file\" are\n * movable seams rather than Node assumptions:\n *\n * - {@link BinaryExecutor} — how an invocation runs. The bundled `execa_executor` wraps execa;\n *   a browser/remote/sandbox executor satisfies the same contract.\n * - {@link ScratchWorkspace} — bytes ⇄ executor-visible paths. A sibling of `ByteStore`, NOT a\n *   `ByteStore`: byte stores promise in-process readers, while binaries are foreign processes\n *   that need real paths. The bundled `fs_workspace` uses `node:fs/promises` via an async\n *   resolver; any implementation whose paths the chosen executor can see is valid — that\n *   compatibility is the consumer's composition decision.\n */\n\nimport { isObject } from '@nhtio/adk/guards'\n\n// ── shared helper shapes ─────────────────────────────────────────────────────\n\n/**\n * A value-or-resolver: the canonical way to supply an engine. Resolvers may be sync or async\n * (dynamic import) and may resolve to the value directly or a `{ default: value }` module\n * namespace. Engine resolvers run eagerly at pipeline construction — the engine module itself\n * is cheap; heavy peer dependencies load lazily inside capability methods.\n */\nexport type EngineResolver<T = MediaEngine> =\n  | T\n  | (() => T | { default: T } | Promise<T | { default: T }>)\n\n/** Common result shape for engines that transform bytes to bytes. */\nexport interface EngineBytesResult {\n  /** The output bytes. */\n  bytes: Uint8Array\n  /** The output MIME type. */\n  mimeType: string\n}\n\n// ── process execution + scratch filesystem ──────────────────────────────────\n\n/** A single binary invocation handed to a {@link BinaryExecutor}. */\nexport interface BinaryInvocation {\n  /** The command to run (an absolute path or a name the executor can resolve). */\n  cmd: string\n  /** Arguments, exec-style (no shell interpolation). */\n  args: string[]\n  /** Wall-clock timeout in milliseconds. */\n  timeoutMs?: number\n  /** Abort signal to cancel the invocation. */\n  signal?: AbortSignal\n}\n\n/** The settled result of a {@link BinaryExecutor.exec} call. */\nexport interface BinaryExecResult {\n  /** The process exit code (or -1 when the process failed to start). */\n  exitCode: number\n  /** Captured standard output. */\n  stdout: string\n  /** Captured standard error. */\n  stderr: string\n  /** `true` when the invocation failed (non-zero exit, spawn failure, timeout, abort). */\n  failed: boolean\n}\n\n/**\n * Runs a binary invocation to completion. How and where it runs — local child process, remote\n * runner, sandbox, container, a browser-side WASI shim — is the implementation's business.\n */\nexport interface BinaryExecutor {\n  /**\n   * Run one invocation to completion and report the result. Implementations must not throw on\n   * non-zero exits — report via `failed`/`exitCode` so callers map failures to readable errors.\n   *\n   * @param invocation - The command, args, and limits to run.\n   * @returns The settled result.\n   */\n  exec(invocation: BinaryInvocation): Promise<BinaryExecResult>\n}\n\n/**\n * Bytes ⇄ executor-visible paths. The seam that lets binary-backed engines exchange files\n * with the process (or remote runner) that executes them.\n */\nexport interface ScratchWorkspace {\n  /**\n   * Write `bytes` into the workspace under `filename` and return the absolute path the\n   * paired executor can open.\n   *\n   * @param bytes - The content to materialize.\n   * @param filename - The basename to use (extension matters to format-sniffing binaries).\n   * @returns The absolute path.\n   */\n  materialize(bytes: Uint8Array, filename: string): Promise<string>\n  /**\n   * Read a file the executor produced inside the workspace.\n   *\n   * @param path - The absolute path to read.\n   * @returns The file bytes.\n   */\n  read(path: string): Promise<Uint8Array>\n  /** The workspace root directory, for `--outdir`-style binary arguments. */\n  dir(): string\n  /** List the files currently in the workspace root (basenames). */\n  list(): Promise<string[]>\n  /** Remove the workspace and everything in it. Engines call this in `finally`. */\n  dispose(): Promise<void>\n}\n\n/**\n * A factory for per-execution scratch workspaces. Engines mint one workspace per invocation\n * so concurrent executions never share a directory.\n */\nexport type ScratchWorkspaceFactory = () => ScratchWorkspace | Promise<ScratchWorkspace>\n\n// ── the format vocabulary ────────────────────────────────────────────────────\n\n/**\n * An input-matching pattern: an exact MIME type (`application/pdf`), a family wildcard\n * (`image/*`), or a virtual MIME such as {@link PCM_MIME}.\n */\nexport type MimePattern = string\n\n/**\n * The virtual MIME type for decoded mono PCM audio — the intermediate between an audio\n * container and a transcription. Bytes are little-endian Float32 samples in `[-1, 1]`;\n * a {@link ConvertOutput} carrying PCM must set `meta.sampleRate` (Hz).\n */\nexport const PCM_MIME = 'audio/x-adk-pcm'\n\n/**\n * The virtual source MIME type for media generation — the single seam through which new media\n * comes into existence. An engine that can mint a blank/seed file declares\n * `converts: [{ from: [EMPTY_MIME], to: [...] }]` and receives a {@link ConvertRequest} with\n * zero bytes; the format token in `to` names what gets created.\n *\n * @remarks\n * Generating an .xlsx, a blank canvas, or a second of silence IS media generation — it is\n * *deterministic* generation (same inputs, same bytes). *Model-based semantic* generation\n * (diffusion, TTS) is the same edge with different machinery: a BYO engine declares\n * `from: [EMPTY_MIME]` and consumes a prompt from `request.options`. Both kinds ride one\n * declaration shape, which is exactly why this is a MIME and not a special API. `EMPTY_MIME`\n * can never become a conversion intermediate: no engine declares `to: 'empty'`, so the\n * pathfinder only ever sees it as a source.\n */\nexport const EMPTY_MIME = 'application/x-adk-empty'\n\n/**\n * Pack PCM samples into transport bytes for a {@link ConvertOutput}.\n *\n * @remarks\n * A `Float32Array` view over arbitrary `Uint8Array` bytes requires 4-byte alignment, which\n * sliced buffers do not guarantee — this helper (and {@link bytesToPcm}) copy-normalize so\n * neither side has to think about alignment.\n *\n * @param pcm - Mono PCM samples in `[-1, 1]`.\n * @returns The samples as little-endian Float32 bytes.\n */\nexport const pcmToBytes = (pcm: Float32Array): Uint8Array => {\n  const copy = new Float32Array(pcm)\n  return new Uint8Array(copy.buffer, 0, copy.byteLength)\n}\n\n/**\n * Read PCM samples back out of transport bytes.\n *\n * @param bytes - Little-endian Float32 bytes (as produced by {@link pcmToBytes}).\n * @returns The mono PCM samples.\n */\nexport const bytesToPcm = (bytes: Uint8Array): Float32Array => {\n  const aligned = new Uint8Array(bytes.length)\n  aligned.set(bytes)\n  return new Float32Array(aligned.buffer, 0, Math.floor(bytes.length / 4))\n}\n\n// ── convert options (typed, augmentable) ─────────────────────────────────────\n\n/** Options understood by OCR-flavored converts (`image/*` → `txt`/`hocr`/`json`). */\nexport interface OcrConvertOptions {\n  /** Recognition language hints (e.g. `['eng','deu']`). */\n  languages?: readonly string[]\n}\n\n/** Options understood by transcription-flavored converts ({@link PCM_MIME} → `txt`/`srt`/`vtt`/`json`). */\nexport interface AsrConvertOptions {\n  /** Source-language hint (BCP-47-ish). */\n  lang?: string\n  /** Translate the transcription to English. */\n  translate?: boolean\n}\n\n/** Options understood by embedded-image extraction converts (`application/pdf` → `images`). */\nexport interface ImagesConvertOptions {\n  /**\n   * Preferred output encoding token (`jpg`, `png`, …). An extractor that can emit it natively\n   * should; outputs in other encodings are re-encoded downstream by the requesting step.\n   */\n  format?: string\n}\n\n/**\n * The options bag carried by a {@link ConvertRequest} — one typed, augmentable interface\n * merging every documented convention.\n *\n * @remarks\n * Consumers add their own keys via declaration merging against this module:\n *\n * ```ts\n * declare module '@nhtio/adk/batteries/media/contracts' {\n *   interface ConvertOptions {\n *     watermark?: { text: string }\n *   }\n * }\n * ```\n *\n * Typo'd keys become excess-property compile errors at literal call sites; the runtime stays\n * open — engines must ignore keys they don't understand (multi-hop conversion forwards one\n * bag to every hop). The namespace is flat and globally merged, so BYO keys should be named\n * to avoid collisions (prefix by engine where ambiguous).\n */\nexport interface ConvertOptions\n  extends OcrConvertOptions, AsrConvertOptions, ImagesConvertOptions {}\n\n// ── the three capabilities ───────────────────────────────────────────────────\n\n/** A format-changing request handed to a {@link ConvertCapability}. */\nexport interface ConvertRequest {\n  /** The input content bytes. */\n  bytes: Uint8Array\n  /** The input MIME type. */\n  mimeType: string\n  /** The input filename (extension informs format sniffing). */\n  filename: string\n  /** The target format token (`pdf`, `docx`, `txt`, `pcm`, `images`, …). */\n  to: string\n  /** Capability-specific options — see {@link ConvertOptions}. */\n  options?: ConvertOptions\n  /** Abort signal threaded from the pipeline execution. */\n  signal?: AbortSignal\n}\n\n/** One output of a convert — most converts yield exactly one; `images` yields many. */\nexport interface ConvertOutput {\n  /** The output bytes. */\n  bytes: Uint8Array\n  /** The output MIME type (honest — native encoding, no silent re-encode). */\n  mimeType: string\n  /** Output metadata (e.g. `{ sampleRate: 44100 }` on {@link PCM_MIME} outputs). */\n  meta?: Record<string, unknown>\n}\n\n/** The settled result of a convert. */\nexport interface ConvertResult {\n  /** The outputs, in source order. */\n  outputs: readonly ConvertOutput[]\n}\n\n/**\n * One uniform block of an engine's conversion matrix: every format token in `to` is\n * producible from every input matching `from`.\n *\n * @remarks\n * Declarations are plain data — the registry reads them without calling engine code. An\n * input-dependent matrix (LibreOffice: docx→pdf yes, docx→xlsx no, ods→xlsx yes) is expressed\n * as several capability groups, each a uniform from×to block.\n */\nexport interface ConvertCapability {\n  /** Input patterns this block accepts. */\n  from: readonly MimePattern[]\n  /** Format tokens producible from every `from` member. */\n  to: readonly string[]\n  /**\n   * Perform the conversion.\n   *\n   * @param request - The input bytes, target token, and options.\n   * @returns The conversion outputs.\n   */\n  convert(request: ConvertRequest): Promise<ConvertResult>\n}\n\n/**\n * A same-format content transform handed to a {@link MutateCapability} — the fused image\n * request: adjacent `image.*` steps fold into ONE request so a resize→rotate→format chain\n * costs a single decode/encode.\n */\nexport interface MutateRequest {\n  /** The input bytes. */\n  bytes: Uint8Array\n  /** The input MIME type. */\n  mimeType: string\n  /** Resize, when requested. */\n  resize?: {\n    width?: number\n    height?: number\n    fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'\n  }\n  /** Clockwise rotation in degrees. */\n  rotate?: 90 | 180 | 270\n  /** Flip axes. */\n  flip?: { horizontal?: boolean; vertical?: boolean }\n  /** Remove EXIF/ICC metadata. */\n  stripMetadata?: boolean\n  /** Vector annotation primitives, rendered over the image. */\n  annotate?: readonly ImageAnnotation[]\n  /** Re-encode target, when requested (rides the same fused call). */\n  format?: { to: string; quality?: number }\n  /** Abort signal threaded from the pipeline execution. */\n  signal?: AbortSignal\n}\n\n/** A same-format content-transform capability group. */\n/** A primitive rendered by the image annotation operation. */\nexport type ImageAnnotation =\n  | {\n      type: 'rect'\n      x: number\n      y: number\n      width: number\n      height: number\n      color?: string\n      fill?: string\n      strokeWidth?: number\n    }\n  | {\n      type: 'line' | 'arrow'\n      x1: number\n      y1: number\n      x2: number\n      y2: number\n      color?: string\n      strokeWidth?: number\n    }\n  | {\n      type: 'ellipse'\n      cx: number\n      cy: number\n      rx: number\n      ry: number\n      color?: string\n      fill?: string\n      strokeWidth?: number\n    }\n  | {\n      type: 'text'\n      x: number\n      y: number\n      text: string\n      color?: string\n      size?: number\n      font?: string\n    }\n\n/** A same-format content-transform capability group. */\nexport interface MutateCapability {\n  /** Input patterns this block mutates. */\n  over: readonly MimePattern[]\n  /** Content operations supported (`resize`, `rotate`, `flip`, `strip_metadata`, `annotate`). */\n  ops: readonly string[]\n  /** Format tokens reachable via `request.format` in the same fused call. */\n  encodes: readonly string[]\n  /**\n   * Apply the fused transform.\n   *\n   * @param request - The folded operations and input bytes.\n   * @returns The transformed bytes.\n   */\n  mutate(request: MutateRequest): Promise<EngineBytesResult>\n}\n\n/**\n * A structural document operation handed to an {@link EditCapability}: one named op with its\n * verb-table args, applied to in-memory bytes. Unlike the fused {@link MutateRequest} (which\n * folds adjacent image transforms into one decode/encode), edits run one op per request — a\n * worksheet splice is not a pixel pass and gains nothing from fusion.\n */\nexport interface EditRequest {\n  /** The input bytes. */\n  bytes: Uint8Array\n  /** The input MIME type. */\n  mimeType: string\n  /** The operation name, namespaced as in the verb table (`sheet.update_cells`, …). */\n  op: string\n  /** The op's arguments, in the verb table's declared shapes. */\n  args: Record<string, unknown>\n  /** Abort signal threaded from the pipeline execution. */\n  signal?: AbortSignal\n}\n\n/** Counts an edit reports back for result summaries. */\nexport interface EditSummary {\n  /** Items added (rows, columns, sheets…). */\n  added?: number\n  /** Items removed. */\n  removed?: number\n  /** Items modified. */\n  modified?: number\n  /** Non-fatal notes surfaced to the model. */\n  warnings?: string[]\n}\n\n/** The settled result of an edit: the restructured bytes plus optional change counts. */\nexport interface EditResult extends EngineBytesResult {\n  /** Change counts for result summaries. */\n  summary?: EditSummary\n}\n\n/**\n * A structural document-editing capability group: every op in `ops` is applicable to every\n * input matching `over`.\n *\n * @remarks\n * Two engines may declare the same ops over the same patterns with different fidelity — e.g.\n * an ExcelJS-backed editor preserves styling while a SheetJS CE-backed one strips it. The\n * registry does not rank fidelity; supply order (or selection middleware) decides, which makes\n * the trade-off the consumer's visible composition decision.\n */\nexport interface EditCapability {\n  /** Input patterns this block edits. */\n  over: readonly MimePattern[]\n  /** Operation names supported (`sheet.update_cells`, `sheet.add_rows`, …). */\n  ops: readonly string[]\n  /**\n   * Apply one structural operation.\n   *\n   * @param request - The op, its args, and the input bytes.\n   * @returns The restructured bytes plus optional change counts.\n   */\n  edit(request: EditRequest): Promise<EditResult>\n}\n\n/**\n * A self-declaring media engine: an id for error messages plus the capabilities it provides.\n * At least one capability entry is required.\n */\nexport interface MediaEngine {\n  /** Stable identifier used in config and dispatch error messages (`jimp`, `soffice`, …). */\n  readonly id: string\n  /** Format-changing capability groups. */\n  readonly converts?: readonly ConvertCapability[]\n  /** Same-format content-transform capability groups. */\n  readonly mutates?: readonly MutateCapability[]\n  /** Structural document-editing capability groups. */\n  readonly edits?: readonly EditCapability[]\n}\n\n// ── duck-typed guards ────────────────────────────────────────────────────────\n\nconst hasFns = (value: unknown, names: readonly string[]): boolean =>\n  isObject(value) && names.every((n) => typeof (value as Record<string, unknown>)[n] === 'function')\n\nconst isStringArray = (value: unknown): boolean =>\n  Array.isArray(value) && value.every((v) => typeof v === 'string')\n\n/** `true` when `value` structurally implements {@link BinaryExecutor}. */\nexport const implementsBinaryExecutor = (value: unknown): value is BinaryExecutor =>\n  hasFns(value, ['exec'])\n\n/** `true` when `value` structurally implements {@link ScratchWorkspace}. */\nexport const implementsScratchWorkspace = (value: unknown): value is ScratchWorkspace =>\n  hasFns(value, ['materialize', 'read', 'dir', 'list', 'dispose'])\n\n/** `true` when `value` structurally implements {@link ConvertCapability}. */\nexport const implementsConvertCapability = (value: unknown): value is ConvertCapability =>\n  hasFns(value, ['convert']) &&\n  isStringArray((value as ConvertCapability).from) &&\n  isStringArray((value as ConvertCapability).to)\n\n/** `true` when `value` structurally implements {@link MutateCapability}. */\nexport const implementsMutateCapability = (value: unknown): value is MutateCapability =>\n  hasFns(value, ['mutate']) &&\n  isStringArray((value as MutateCapability).over) &&\n  isStringArray((value as MutateCapability).ops) &&\n  isStringArray((value as MutateCapability).encodes)\n\n/** `true` when `value` structurally implements {@link EditCapability}. */\nexport const implementsEditCapability = (value: unknown): value is EditCapability =>\n  hasFns(value, ['edit']) &&\n  isStringArray((value as EditCapability).over) &&\n  isStringArray((value as EditCapability).ops)\n\n/**\n * `true` when `value` structurally implements {@link MediaEngine}: a string id plus at least\n * one well-formed capability entry. Every declared entry must pass its capability guard.\n */\nexport const implementsMediaEngine = (value: unknown): value is MediaEngine => {\n  if (!isObject(value)) return false\n  const engine = value as unknown as MediaEngine\n  if (typeof engine.id !== 'string' || engine.id.length === 0) return false\n  const converts = engine.converts\n  const mutates = engine.mutates\n  const edits = engine.edits\n  if (converts !== undefined) {\n    if (!Array.isArray(converts) || !converts.every(implementsConvertCapability)) return false\n  }\n  if (mutates !== undefined) {\n    if (!Array.isArray(mutates) || !mutates.every(implementsMutateCapability)) return false\n  }\n  if (edits !== undefined) {\n    if (!Array.isArray(edits) || !edits.every(implementsEditCapability)) return false\n  }\n  const convertCount = Array.isArray(converts) ? converts.length : 0\n  const mutateCount = Array.isArray(mutates) ? mutates.length : 0\n  const editCount = Array.isArray(edits) ? edits.length : 0\n  return convertCount + mutateCount + editCount > 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwJA,IAAa,WAAW;;;;;;;;;;;;;;;;AAiBxB,IAAa,aAAa;;;;;;;;;;;;AAa1B,IAAa,cAAc,QAAkC;CAC3D,MAAM,OAAO,IAAI,aAAa,GAAG;CACjC,OAAO,IAAI,WAAW,KAAK,QAAQ,GAAG,KAAK,UAAU;AACvD;;;;;;;AAQA,IAAa,cAAc,UAAoC;CAC7D,MAAM,UAAU,IAAI,WAAW,MAAM,MAAM;CAC3C,QAAQ,IAAI,KAAK;CACjB,OAAO,IAAI,aAAa,QAAQ,QAAQ,GAAG,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC;AACzE;AAkRA,IAAM,UAAU,OAAgB,UAC9B,SAAS,KAAK,KAAK,MAAM,OAAO,MAAM,OAAQ,MAAkC,OAAO,UAAU;AAEnG,IAAM,iBAAiB,UACrB,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,QAAQ;;AAGlE,IAAa,4BAA4B,UACvC,OAAO,OAAO,CAAC,MAAM,CAAC;;AAGxB,IAAa,8BAA8B,UACzC,OAAO,OAAO;CAAC;CAAe;CAAQ;CAAO;CAAQ;AAAS,CAAC;;AAGjE,IAAa,+BAA+B,UAC1C,OAAO,OAAO,CAAC,SAAS,CAAC,KACzB,cAAe,MAA4B,IAAI,KAC/C,cAAe,MAA4B,EAAE;;AAG/C,IAAa,8BAA8B,UACzC,OAAO,OAAO,CAAC,QAAQ,CAAC,KACxB,cAAe,MAA2B,IAAI,KAC9C,cAAe,MAA2B,GAAG,KAC7C,cAAe,MAA2B,OAAO;;AAGnD,IAAa,4BAA4B,UACvC,OAAO,OAAO,CAAC,MAAM,CAAC,KACtB,cAAe,MAAyB,IAAI,KAC5C,cAAe,MAAyB,GAAG;;;;;AAM7C,IAAa,yBAAyB,UAAyC;CAC7E,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAC7B,MAAM,SAAS;CACf,IAAI,OAAO,OAAO,OAAO,YAAY,OAAO,GAAG,WAAW,GAAG,OAAO;CACpE,MAAM,WAAW,OAAO;CACxB,MAAM,UAAU,OAAO;CACvB,MAAM,QAAQ,OAAO;CACrB,IAAI,aAAa,KAAA;MACX,CAAC,MAAM,QAAQ,QAAQ,KAAK,CAAC,SAAS,MAAM,2BAA2B,GAAG,OAAO;CAAA;CAEvF,IAAI,YAAY,KAAA;MACV,CAAC,MAAM,QAAQ,OAAO,KAAK,CAAC,QAAQ,MAAM,0BAA0B,GAAG,OAAO;CAAA;CAEpF,IAAI,UAAU,KAAA;MACR,CAAC,MAAM,QAAQ,KAAK,KAAK,CAAC,MAAM,MAAM,wBAAwB,GAAG,OAAO;CAAA;CAE9E,MAAM,eAAe,MAAM,QAAQ,QAAQ,IAAI,SAAS,SAAS;CACjE,MAAM,cAAc,MAAM,QAAQ,OAAO,IAAI,QAAQ,SAAS;CAC9D,MAAM,YAAY,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS;CACxD,OAAO,eAAe,cAAc,YAAY;AAClD"}