From 369e09715324ceeb499d6e5dd32fa8e55c09bdac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 01:57:39 +0000 Subject: [PATCH 1/2] feat(producer): write public render provenance sidecar next to outputs Every successful render now writes .hf-render.json after the artifact commits: tool versions (producer/node/ffmpeg), entry sha256, compiled-composition hash, @font-face families, variables hash (never raw values), format/fps/resolution/duration, encoder facts, output sha256+size, stage timings, worker count, HDR flag, and warning codes. - Default ON; --no-provenance (or --provenance false) disables, --provenance relocates. Batch rows each write their own sidecar; --docker forwards the disable flag into the container. - JSON Schema published at packages/core/schemas/hf-render-sidecar.json. - Docs at docs/reference/render-provenance.mdx. - Sidecar write failures log a warning and never un-complete a render. --- docs/docs.json | 1 + docs/reference/render-provenance.mdx | 117 +++++ .../src/commands/render.provenance.test.ts | 138 ++++++ packages/cli/src/commands/render.ts | 16 + packages/cli/src/commands/render/execute.ts | 88 ++-- packages/cli/src/commands/render/plan.ts | 58 +++ packages/cli/src/utils/dockerRunArgs.ts | 8 + packages/core/schemas/hf-render-sidecar.json | 203 +++++++++ packages/producer/src/index.ts | 10 + packages/producer/src/renderRequest.ts | 13 + .../services/render/provenanceSidecar.test.ts | 309 ++++++++++++++ .../src/services/render/provenanceSidecar.ts | 400 ++++++++++++++++++ .../src/services/renderOrchestrator.ts | 44 ++ 13 files changed, 1356 insertions(+), 49 deletions(-) create mode 100644 docs/reference/render-provenance.mdx create mode 100644 packages/cli/src/commands/render.provenance.test.ts create mode 100644 packages/core/schemas/hf-render-sidecar.json create mode 100644 packages/producer/src/services/render/provenanceSidecar.test.ts create mode 100644 packages/producer/src/services/render/provenanceSidecar.ts diff --git a/docs/docs.json b/docs/docs.json index f2744d4f09..1d7baa2313 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -832,6 +832,7 @@ "group": "Rendering paths", "pages": [ "guides/rendering", + "reference/render-provenance", "deploy/overview", "deploy/cloud", "guides/deploy" diff --git a/docs/reference/render-provenance.mdx b/docs/reference/render-provenance.mdx new file mode 100644 index 0000000000..29a080d3e7 --- /dev/null +++ b/docs/reference/render-provenance.mdx @@ -0,0 +1,117 @@ +--- +title: "Render provenance sidecar" +sidebarTitle: "Render provenance" +description: "The portable JSON receipt written next to every rendered output." +--- + +Every successful `hyperframes render` writes a small JSON receipt next to the +output file: `out.mp4` gets `out.mp4.hf-render.json`. The sidecar records what +produced the file — tool versions, input hashes, fonts, format, encoder, stage +timings, and warning codes — so agents, CI pipelines, and support tooling can +answer "what rendered this, from what, and how" without re-running anything. + +```bash +npx hyperframes render --output out.mp4 +# → out.mp4 +# → out.mp4.hf-render.json +``` + +```json +{ + "$schema": "https://hyperframes.heygen.com/schema/hf-render-sidecar.json", + "schemaVersion": 1, + "kind": "hf-render-sidecar", + "createdAt": "2026-09-08T01:10:29.640Z", + "versions": { + "producer": "0.8.31", + "node": "v22.22.2", + "ffmpeg": "ffmpeg version 6.1.1-3ubuntu5" + }, + "render": { + "jobId": "render-1757294992655-h2y0iqk3d", + "outcome": "completed", + "warningCodes": [], + "totalElapsedMs": 36980, + "stages": { "compileMs": 189, "captureFrameMs": 33212, "encodeMs": 941 }, + "workers": 1, + "quality": "standard" + }, + "input": { + "entryFile": "index.html", + "entrySha256": "9917f7d1…", + "compositionHash": "d59fa8f6a2f95627", + "fonts": ["Inter", "Space Grotesk"], + "variables": { "count": 2, "sha256": "0b26e313…" } + }, + "output": { + "file": "out.mp4", + "format": "mp4", + "fps": { "num": 30, "den": 1 }, + "width": 1920, + "height": 1080, + "durationSeconds": 4, + "totalFrames": 120, + "sizeBytes": 693352, + "sha256": "6cbb43c1…", + "hdr": false, + "encoder": { "codec": "h264", "preset": "medium", "pixelFormat": "yuv420p" } + }, + "host": { "platform": "linux", "arch": "x64" } +} +``` + +## Controlling the sidecar + +The sidecar is on by default for every render, including `--docker` and +`--batch` renders (each batch row writes its own +`.hf-render.json`). + +```bash +npx hyperframes render --output out.mp4 --no-provenance # disable +npx hyperframes render --output out.mp4 --provenance receipts/out.json # relocate +``` + +Two combinations are rejected up front: a custom `--provenance ` with +`--batch` (one fixed path cannot serve N row outputs) and with `--docker` +(an arbitrary host path is not visible from the render container). Disabling +works everywhere. + +Programmatic callers set the same tri-state on the render request: +`provenance` omitted (default path), `false` (disabled), or a custom path +string. + +## What the fields mean + +| Field | Meaning | +| --- | --- | +| `versions` | `@hyperframes/producer` package version, Node.js version, and the first line of `ffmpeg -version`. | +| `render.outcome` | `completed` or `completed_with_warnings` — sidecars only exist for renders whose artifact committed. | +| `render.warningCodes` | Sorted capture-readiness warning codes (empty on a clean render). | +| `render.stages` | Per-stage wall-clock timings in milliseconds (`compileMs`, `captureMs`, `encodeMs`, …). | +| `input.entrySha256` | sha256 of the entry HTML source bytes. | +| `input.compositionHash` | Content hash of the compiled composition — the same value render telemetry reports. | +| `input.fonts` | `@font-face` family names baked into the compiled composition. | +| `input.variables` | Count and sha256 of the render-time variable overrides — the hash proves *which* parametrization produced the output without disclosing values. `null` when no variables were passed. | +| `output.sha256` / `output.sizeBytes` | Digest and size of the committed artifact. Omitted for `png-sequence` directory outputs. | +| `output.encoder` | Codec, preset, and pixel format used by the encode stage. `null` for `png-sequence` and `gif`. | + +The full contract is published as a JSON Schema at +[`hf-render-sidecar.json`](https://hyperframes.heygen.com/schema/hf-render-sidecar.json) +(source: `packages/core/schemas/hf-render-sidecar.json`). + +## What the sidecar deliberately omits + +Variable **values** (they routinely carry user text and tokens — only a hash +is recorded), environment variables, absolute host paths, usernames, and +machine names. Once a receipt travels with a shared file, metadata leaks are +hard to walk back. + +## Sidecar vs. embedded container tags + +HyperFrames also stamps MP4/MOV/WebM containers with two unsigned metadata +tags (`hyperframes_renderer`, `hyperframes_version`). The two are +complementary: the embedded tags survive file moves but hold only the +renderer name and version; the sidecar carries the full receipt but is a +separate file. Both are unauthenticated hints — any tool can write either — +so use them for diagnostics and CI bookkeeping, never as an authenticity or +attribution boundary. diff --git a/packages/cli/src/commands/render.provenance.test.ts b/packages/cli/src/commands/render.provenance.test.ts new file mode 100644 index 0000000000..596aad6a50 --- /dev/null +++ b/packages/cli/src/commands/render.provenance.test.ts @@ -0,0 +1,138 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { CliUsageError } from "../utils/commandResult.js"; +import { buildDockerRunArgs } from "../utils/dockerRunArgs.js"; +import { createRenderPlan, parseProvenanceArg } from "./render/plan.js"; + +describe("parseProvenanceArg", () => { + it("defaults to on (undefined) when the flag is absent or bare", () => { + expect(parseProvenanceArg(undefined)).toBeUndefined(); + expect(parseProvenanceArg(true)).toBeUndefined(); + expect(parseProvenanceArg(" ")).toBeUndefined(); + }); + + it("disables on --no-provenance and on disable-alias values", () => { + expect(parseProvenanceArg(false)).toBe(false); + expect(parseProvenanceArg("false")).toBe(false); + expect(parseProvenanceArg("OFF")).toBe(false); + expect(parseProvenanceArg("0")).toBe(false); + expect(parseProvenanceArg("none")).toBe(false); + }); + + it("resolves a custom sidecar path", () => { + expect(parseProvenanceArg("receipts/out.json")).toBe(resolve("receipts/out.json")); + }); +}); + +/** Minimal renderable project fixture — one composition root, no clips. */ +function makeProvenanceProjectDir(): string { + const dir = mkdtempSync(join(tmpdir(), "hf-render-provenance-")); + const root = + '
'; + writeFileSync(join(dir, "index.html"), root); + return dir; +} + +describe("createRenderPlan provenance", () => { + let projectDir: string; + + beforeEach(() => { + projectDir = makeProvenanceProjectDir(); + vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + rmSync(projectDir, { recursive: true, force: true }); + }); + + it("leaves provenance on by default", () => { + const plan = createRenderPlan({ dir: projectDir, output: "out.mp4" }); + expect(plan.provenance).toBeUndefined(); + }); + + it("threads --no-provenance through the plan", () => { + const plan = createRenderPlan({ dir: projectDir, output: "out.mp4", provenance: false }); + expect(plan.provenance).toBe(false); + }); + + it("threads a custom sidecar path through the plan", () => { + const plan = createRenderPlan({ + dir: projectDir, + output: "out.mp4", + provenance: "receipts/out.json", + }); + expect(plan.provenance).toBe(resolve("receipts/out.json")); + }); + + it("rejects a custom sidecar path with --batch (rows write their own sidecars)", () => { + expect(() => + createRenderPlan({ + dir: projectDir, + batch: "rows.json", + provenance: "receipts/out.json", + }), + ).toThrow(CliUsageError); + }); + + it("allows disabling provenance batch-wide", () => { + const plan = createRenderPlan({ dir: projectDir, batch: "rows.json", provenance: false }); + expect(plan.provenance).toBe(false); + }); + + it("rejects a custom sidecar path with --docker (path is not container-visible)", () => { + expect(() => + createRenderPlan({ + dir: projectDir, + output: "out.mp4", + docker: true, + provenance: "receipts/out.json", + }), + ).toThrow(CliUsageError); + }); + + it("allows disabling provenance with --docker", () => { + const plan = createRenderPlan({ + dir: projectDir, + output: "out.mp4", + docker: true, + provenance: "false", + }); + expect(plan.provenance).toBe(false); + }); +}); + +describe("buildDockerRunArgs provenance forwarding", () => { + const base = { + imageTag: "hyperframes-renderer:test", + projectDir: "/host/project", + outputDir: "/host/renders", + outputFilename: "out.mp4", + platform: "linux/amd64", + options: { + fps: { num: 30, den: 1 }, + quality: "standard" as const, + format: "mp4" as const, + gpu: false, + browserGpu: false, + hdrMode: "auto" as const, + quiet: true, + }, + }; + + it("forwards --no-provenance into the container CLI", () => { + const args = buildDockerRunArgs({ + ...base, + options: { ...base.options, provenance: false as const }, + }); + expect(args).toContain("--no-provenance"); + }); + + it("does not forward anything for the default-on setting", () => { + const args = buildDockerRunArgs(base); + expect(args).not.toContain("--no-provenance"); + expect(args).not.toContain("--provenance"); + }); +}); diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index f53af70355..b0033cb71a 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -338,6 +338,15 @@ export default defineCommand({ // guard below leaves PRODUCER_EXPERIMENTAL_FAST_CAPTURE untouched and the // env fallback survives (matches the --low-memory-mode idiom). }, + provenance: { + type: "string", + description: + "Render provenance sidecar path (default: .hf-render.json). " + + "The sidecar is a portable JSON receipt written next to the output " + + "after a successful render: tool versions, input hashes, variables " + + "hash, fonts, format/fps/resolution, encoder, stage timings, and " + + "warning codes. Pass false (or use --no-provenance) to disable.", + }, "frames-cache-dir": { type: "string", description: @@ -426,6 +435,11 @@ export interface RenderOptions { protocolTimeout?: number; /** Player-ready timeout override (ms). */ playerReadyTimeout?: number; + /** + * Provenance sidecar setting: `undefined` = default sidecar next to the + * output, `false` = disabled, string = custom sidecar path. + */ + provenance?: string | false; /** Throw render failures to the caller instead of printing and exiting. */ throwOnError?: boolean; /** Skip the interactive feedback prompt after a successful render. */ @@ -728,6 +742,7 @@ async function renderDocker( pageNavigationTimeoutMs: options.pageNavigationTimeoutMs, protocolTimeoutMs: options.protocolTimeout, playerReadyTimeoutMs: options.playerReadyTimeout, + provenance: options.provenance, }, }); @@ -894,6 +909,7 @@ export async function renderLocal( entryFile: options.entryFile, outputResolution: options.outputResolution, outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic, + provenance: options.provenance, debug: options.debug, strictness: options.bestEffort === false ? "strict" : "best-effort", }, diff --git a/packages/cli/src/commands/render/execute.ts b/packages/cli/src/commands/render/execute.ts index 83fa15942c..bf81f76364 100644 --- a/packages/cli/src/commands/render/execute.ts +++ b/packages/cli/src/commands/render/execute.ts @@ -56,6 +56,43 @@ function renderLintShouldAbort( ); } +/** + * The plan-derived render options shared by the single-render and batch + * paths. Call-site-specific lifecycle fields (quiet override, variables, + * exitAfterComplete, throwOnError, skipFeedback, breaker management) are + * spread over this base by each caller. + */ +function planRenderOptions(plan: RenderPlan, browserPath: string | undefined): RenderOptions { + return { + fps: plan.fps, + quality: plan.quality, + authoringSkill: plan.authoringSkill, + catalogUsage: plan.catalogUsage, + format: plan.format, + gifLoop: plan.gifLoop, + workers: plan.workers, + gpu: plan.useGpu, + browserGpuMode: plan.browserGpuMode, + hdrMode: plan.hdrMode, + crf: plan.crf, + vp9CpuUsed: plan.vp9CpuUsed, + videoBitrate: plan.videoBitrate, + videoFrameFormat: plan.videoFrameFormat, + quiet: plan.quiet, + browserPath, + debug: plan.debug, + bestEffort: plan.bestEffort, + entryFile: plan.entryFile, + outputResolution: plan.outputResolution, + outputResolutionAspectAgnostic: plan.outputResolutionAspectAgnostic, + outputResolutionRaw: plan.outputResolutionRaw, + pageNavigationTimeoutMs: plan.pageNavigationTimeoutMs, + protocolTimeout: plan.protocolTimeout, + playerReadyTimeout: plan.playerReadyTimeout, + provenance: plan.provenance, + }; +} + /** Execute a validated plan. Output and process lifecycle stay outside parsing. */ export async function executeRenderPlan( plan: RenderPlan, @@ -98,32 +135,8 @@ export async function executeRenderPlan( } const options: RenderOptions = { - fps: plan.fps, - quality: plan.quality, - authoringSkill: plan.authoringSkill, - catalogUsage: plan.catalogUsage, - format: plan.format, - gifLoop: plan.gifLoop, - workers: plan.workers, - gpu: plan.useGpu, - browserGpuMode: plan.browserGpuMode, - hdrMode: plan.hdrMode, - crf: plan.crf, - vp9CpuUsed: plan.vp9CpuUsed, - videoBitrate: plan.videoBitrate, - videoFrameFormat: plan.videoFrameFormat, - quiet: plan.quiet, - browserPath, - debug: plan.debug, - bestEffort: plan.bestEffort, + ...planRenderOptions(plan, browserPath), variables, - entryFile: plan.entryFile, - outputResolution: plan.outputResolution, - outputResolutionAspectAgnostic: plan.outputResolutionAspectAgnostic, - outputResolutionRaw: plan.outputResolutionRaw, - pageNavigationTimeoutMs: plan.pageNavigationTimeoutMs, - protocolTimeout: plan.protocolTimeout, - playerReadyTimeout: plan.playerReadyTimeout, exitAfterComplete: true, manageDeParallelRouterBreaker: true, }; @@ -252,31 +265,8 @@ async function executeBatchRender( ): Promise { const batchQuiet = plan.quiet || plan.batchJson; const renderOptionsBase: RenderOptions = { - fps: plan.fps, - quality: plan.quality, - authoringSkill: plan.authoringSkill, - catalogUsage: plan.catalogUsage, - format: plan.format, - gifLoop: plan.gifLoop, - workers: plan.workers, - gpu: plan.useGpu, - browserGpuMode: plan.browserGpuMode, - hdrMode: plan.hdrMode, - crf: plan.crf, - vp9CpuUsed: plan.vp9CpuUsed, - videoBitrate: plan.videoBitrate, - videoFrameFormat: plan.videoFrameFormat, + ...planRenderOptions(plan, browserPath), quiet: batchQuiet, - browserPath, - entryFile: plan.entryFile, - outputResolution: plan.outputResolution, - outputResolutionAspectAgnostic: plan.outputResolutionAspectAgnostic, - outputResolutionRaw: plan.outputResolutionRaw, - pageNavigationTimeoutMs: plan.pageNavigationTimeoutMs, - protocolTimeout: plan.protocolTimeout, - playerReadyTimeout: plan.playerReadyTimeout, - debug: plan.debug, - bestEffort: plan.bestEffort, exitAfterComplete: false, throwOnError: true, skipFeedback: true, diff --git a/packages/cli/src/commands/render/plan.ts b/packages/cli/src/commands/render/plan.ts index 600a267760..60b6594b20 100644 --- a/packages/cli/src/commands/render/plan.ts +++ b/packages/cli/src/commands/render/plan.ts @@ -90,6 +90,12 @@ export interface RenderCommandArgs { "low-memory-mode"?: boolean; "experimental-fast-capture"?: boolean; "frames-cache-dir"?: string; + /** + * String when `--provenance ` was passed; boolean when the parser + * negated it (`--no-provenance` → false) or it was passed bare + * (`--provenance` → true). + */ + provenance?: string | boolean; } export interface RenderPlan { @@ -137,6 +143,11 @@ export interface RenderPlan { variablesArg?: string; variablesFileArg?: string; strictVariables: boolean; + /** + * Provenance sidecar setting: `undefined` = default sidecar next to the + * output, `false` = disabled, string = resolved custom sidecar path. + */ + provenance?: string | false; environment: Readonly>; } @@ -174,6 +185,29 @@ function positiveInteger(raw: string, title: string, message: string, min = 1): return parsed; } +/** + * Aliases that disable the provenance sidecar when passed as the flag VALUE + * (`--provenance false`). `--no-provenance` arrives as boolean `false` from + * the arg parser's standard negation and is handled separately. + */ +const PROVENANCE_DISABLE_ALIASES = new Set(["false", "off", "0", "none"]); + +/** + * Normalize the raw `--provenance` flag into the plan's tri-state setting: + * `undefined` = default on (sidecar next to the output), `false` = disabled, + * string = resolved custom sidecar path. A bare `--provenance` (boolean + * `true`) and an empty value both mean "default on" — the flag exists to + * relocate or disable the sidecar, not to enable an already-on default. + */ +export function parseProvenanceArg(raw: string | boolean | undefined): string | false | undefined { + if (raw === undefined || raw === true) return undefined; + if (raw === false) return false; + const trimmed = raw.trim(); + if (trimmed === "") return undefined; + if (PROVENANCE_DISABLE_ALIASES.has(trimmed.toLowerCase())) return false; + return resolve(trimmed); +} + /** Parse and validate command input into an immutable execution plan. */ // fallow-ignore-next-line complexity export function createRenderPlan(args: RenderCommandArgs, now = new Date()): RenderPlan { @@ -409,6 +443,29 @@ export function createRenderPlan(args: RenderCommandArgs, now = new Date()): Ren failUsage(); } + const provenance = parseProvenanceArg(args.provenance); + if (typeof provenance === "string" && batchPath) { + // One fixed sidecar path cannot serve N row outputs; rows always write + // `.hf-render.json`. Disabling still applies batch-wide. + errorBox( + "Invalid provenance", + "--provenance with a custom path cannot be combined with --batch. " + + "Each batch row writes its own .hf-render.json; use --no-provenance to disable.", + ); + failUsage(); + } + if (typeof provenance === "string" && useDocker) { + // The containerized CLI writes the sidecar inside the mounted output + // directory; an arbitrary host path is not visible from the container. + errorBox( + "Invalid provenance", + "--provenance with a custom path is not supported with --docker. " + + "The sidecar is written next to the output inside the mounted output directory; " + + "use the default location or --no-provenance.", + ); + failUsage(); + } + const quiet = args.quiet ?? false; const batchJson = args.json ?? false; return Object.freeze({ @@ -455,6 +512,7 @@ export function createRenderPlan(args: RenderCommandArgs, now = new Date()): Ren variablesArg: args.variables, variablesFileArg: args["variables-file"], strictVariables: args["strict-variables"] ?? false, + provenance, environment: Object.freeze(environment), }); } diff --git a/packages/cli/src/utils/dockerRunArgs.ts b/packages/cli/src/utils/dockerRunArgs.ts index beb4d9cc94..940956c2fe 100644 --- a/packages/cli/src/utils/dockerRunArgs.ts +++ b/packages/cli/src/utils/dockerRunArgs.ts @@ -70,6 +70,13 @@ export interface DockerRenderOptions { protocolTimeoutMs?: number; /** Player readiness timeout in milliseconds. */ playerReadyTimeoutMs?: number; + /** + * Provenance sidecar setting. Only `false` is forwarded (as + * `--no-provenance`): the default sidecar already lands next to the output + * inside the mounted output directory, and custom host paths are rejected + * by the render plan before Docker is invoked. + */ + provenance?: string | false; } /** @@ -164,5 +171,6 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] { ...(options.playerReadyTimeoutMs != null ? ["--player-ready-timeout", String(options.playerReadyTimeoutMs)] : []), + ...(options.provenance === false ? ["--no-provenance"] : []), ]; } diff --git a/packages/core/schemas/hf-render-sidecar.json b/packages/core/schemas/hf-render-sidecar.json new file mode 100644 index 0000000000..b49f418b57 --- /dev/null +++ b/packages/core/schemas/hf-render-sidecar.json @@ -0,0 +1,203 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hyperframes.heygen.com/schema/hf-render-sidecar.json", + "title": "HyperFrames Render Provenance Sidecar", + "description": "Portable render receipt written next to a committed render artifact as .hf-render.json. An unauthenticated hint for agents, CI, and support tooling — never an authenticity or attribution boundary.", + "type": "object", + "required": [ + "schemaVersion", + "kind", + "createdAt", + "versions", + "render", + "input", + "output", + "host" + ], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "format": "uri" + }, + "schemaVersion": { + "const": 1, + "description": "Sidecar schema version. Bumped when the framing changes incompatibly." + }, + "kind": { + "const": "hf-render-sidecar" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "ISO-8601 timestamp of sidecar creation (artifact commit time)." + }, + "versions": { + "type": "object", + "required": ["producer", "node"], + "additionalProperties": false, + "properties": { + "producer": { + "type": "string", + "description": "@hyperframes/producer package version." + }, + "node": { + "type": "string", + "description": "Node.js version the render ran on (process.version)." + }, + "ffmpeg": { + "type": "string", + "description": "First line of `ffmpeg -version`. Absent when the probe failed." + } + } + }, + "render": { + "type": "object", + "required": ["jobId", "outcome", "warningCodes", "totalElapsedMs", "stages", "quality"], + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string", + "description": "Producer render job id." + }, + "outcome": { + "type": "string", + "enum": ["completed", "completed_with_warnings"], + "description": "Sidecars are only written after a successful artifact commit." + }, + "warningCodes": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted, de-duplicated capture-readiness warning codes." + }, + "totalElapsedMs": { + "type": "number", + "minimum": 0, + "description": "Wall-clock render pipeline duration." + }, + "stages": { + "type": "object", + "additionalProperties": { "type": "number" }, + "description": "Per-stage wall-clock timings in milliseconds (compileMs, videoExtractMs, captureMs, encodeMs, assembleMs, …)." + }, + "workers": { + "type": "integer", + "minimum": 1, + "description": "Parallel capture worker count actually used." + }, + "quality": { + "type": "string", + "enum": ["draft", "standard", "high"] + } + } + }, + "input": { + "type": "object", + "required": ["entryFile", "fonts", "variables"], + "additionalProperties": false, + "properties": { + "entryFile": { + "type": "string", + "description": "Project-relative entry HTML path." + }, + "entrySha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "sha256 of the entry HTML source bytes. Absent when unreadable." + }, + "compositionHash": { + "type": "string", + "description": "Content hash of the compiled composition (matches render telemetry)." + }, + "fonts": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted @font-face family names baked into the compiled composition." + }, + "variables": { + "description": "Render-time variable overrides, hashed (sha256 of canonical JSON) — never the raw values. Null when no variables were passed.", + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "required": ["count", "sha256"], + "additionalProperties": false, + "properties": { + "count": { "type": "integer", "minimum": 1 }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + } + } + ] + } + } + }, + "output": { + "type": "object", + "required": ["file", "format", "fps", "hdr", "encoder"], + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "Output file (or png-sequence directory) basename. No host paths." + }, + "format": { + "type": "string", + "enum": ["mp4", "webm", "mov", "gif", "png-sequence"] + }, + "fps": { + "type": "object", + "required": ["num", "den"], + "additionalProperties": false, + "properties": { + "num": { "type": "integer", "minimum": 1 }, + "den": { "type": "integer", "minimum": 1 } + }, + "description": "Frame rate as an exact rational (NTSC 29.97 is 30000/1001)." + }, + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 }, + "durationSeconds": { "type": "number", "minimum": 0 }, + "totalFrames": { "type": "integer", "minimum": 0 }, + "sizeBytes": { + "type": "integer", + "minimum": 0, + "description": "On-disk size of the committed artifact. File outputs only." + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "sha256 of the committed artifact bytes. File outputs only." + }, + "hdr": { + "type": "boolean", + "description": "True when the artifact was encoded as HDR." + }, + "encoder": { + "description": "Video encoder facts. Null for png-sequence and gif outputs.", + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "required": ["codec", "preset", "pixelFormat"], + "additionalProperties": false, + "properties": { + "codec": { "type": "string" }, + "preset": { "type": "string" }, + "pixelFormat": { "type": "string" } + } + } + ] + } + } + }, + "host": { + "type": "object", + "required": ["platform", "arch"], + "additionalProperties": false, + "properties": { + "platform": { "type": "string" }, + "arch": { "type": "string" } + } + } + } +} diff --git a/packages/producer/src/index.ts b/packages/producer/src/index.ts index 4a91156f14..a837b599f1 100644 --- a/packages/producer/src/index.ts +++ b/packages/producer/src/index.ts @@ -44,6 +44,16 @@ export { type RenderObservationEvent, type RenderObservationStatus, } from "./services/render/observability.js"; +// ── Render provenance sidecar ─────────────────────────────────────────────── +// Public receipt written next to committed render artifacts (default on). +export { + RENDER_SIDECAR_SCHEMA_URL, + RENDER_SIDECAR_SCHEMA_VERSION, + RENDER_SIDECAR_SUFFIX, + resolveProvenanceSidecarPath, + type ProvenanceSetting, + type RenderProvenanceSidecar, +} from "./services/render/provenanceSidecar.js"; // ── HTML asset localization ───────────────────────────────────────────────── // Rewrite remote /