/**
 * Render Orchestrator Service
 *
 * `executeRenderJob` is the in-process entry point that composes the
 * pipeline's six stages. Each stage lives in its own module under
 * `./render/stages/` so the pure-function primitives can be reused by
 * the distributed render path without dragging the orchestrator's
 * cleanup and observability scaffolding with them.
 *
 *   Stage 1  compile         → services/render/stages/compileStage.ts
 *   Stage 1b probe           → services/render/stages/probeStage.ts
 *            (browser-driven duration discovery + media reconciliation;
 *            grouped with Stage 1 in the perf summary)
 *   Stage 2  extract videos  → services/render/stages/extractVideosStage.ts
 *   Stage 3  audio           → services/render/stages/audioStage.ts
 *   Stage 4  capture         → services/render/stages/captureStage.ts
 *                              services/render/stages/captureStreamingStage.ts
 *                              services/render/stages/captureHdrStage.ts
 *   Stage 5  encode          → services/render/stages/encodeStage.ts
 *   Stage 6  assemble        → services/render/stages/assembleStage.ts
 *
 * Resources spawned by stages (file server, capture sessions, streaming
 * encoders, raw HDR frame files) are tracked in the orchestrator's
 * `try/finally` so a stage throwing mid-pipeline doesn't leak Chrome
 * processes or ffmpeg subprocesses.
 *
 * Heavy observability: every stage records timing into `perfStages`,
 * errors carry full context, and failures produce a diagnostic summary
 * (browser console tail, memory peaks, capture attempts, HDR
 * diagnostics).
 */
import { type CanvasResolution, type Fps, type FpsInput } from "@hyperframes/core";
import { type EngineConfig, type ExtractionPhaseBreakdown, type VideoFrameFormat, type CaptureOptions, type CaptureVideoMetadataHint, type CaptureSession, type BeforeCaptureHook, type ParallelProgress, type WorkerTask, type CapturePerfSummary, type CaptureWarning, type SubTimelineWaitOutcome, type WorkerSizing } from "@hyperframes/engine";
import { type ProducerLogger } from "../logger.js";
import { type RenderOutputFormat } from "./render/renderFormat.js";
import { type RenderCaptureObservability, type RenderObservabilitySummary } from "./render/observability.js";
import { type HdrPerfSummary } from "./render/hdrPerf.js";
export type RenderStatus = "queued" | "preprocessing" | "rendering" | "encoding" | "assembling" | "complete" | "failed" | "cancelled";
export type RenderOutcome = "completed" | "completed_with_warnings" | "failed" | "cancelled";
export type RenderStrictness = "strict" | "best-effort";
export interface RenderWarning extends CaptureWarning {
    stage: "capture-readiness";
}
export interface RenderConfig {
    /**
     * Frame rate as an exact rational. Integer fps is `{ num: 30, den: 1 }`;
     * NTSC is `{ num: 30000, den: 1001 }`. This shape lets the orchestrator
     * pass the exact rational through to FFmpeg's `-r` / `-framerate` flags
     * without a decimal round-trip — see `fpsToFfmpegArg` in @hyperframes/core.
     *
     * Use `fpsToNumber(config.fps)` at any site that needs a `number` for
     * arithmetic (frame-index → time, telemetry, frame-interval ms). Decimal
     * precision at our scales is more than sufficient.
     */
    fps: Fps;
    quality: "draft" | "standard" | "high";
    /**
     * Output container format. Defaults to `"mp4"`; existing renders are
     * unaffected unless this field is set explicitly.
     *
     * - `"mp4"`: H.264 by default, or H.265 + HDR10 when HDR auto-detect
     *   engages or `hdrMode: "force-hdr"` is set. Opaque. The
     *   default streaming/social deliverable. Faststart is applied so the
     *   `moov` atom sits at the file start and the file plays from a
     *   partial download.
     * - `"webm"`: VP9 + `yuva420p` pixel format → **true alpha channel**, no
     *   chroma key. Plays in Chrome, Edge, and Firefox; Safari support for
     *   alpha-WebM is incomplete. Use this when the output should drop
     *   straight into a `<video>` over a colored background on the web.
     *   Audio is muxed as Opus.
     * - `"mov"`: ProRes 4444 + `yuva444p10le` → **true alpha channel +
     *   10-bit color**. Sized for editor ingest (Premiere, Final Cut Pro,
     *   DaVinci Resolve), not direct web playback. Audio is muxed as AAC.
     * - `"gif"`: animated GIF encoded from captured RGBA frames with a two-pass
     *   FFmpeg palette (`palettegen` + `paletteuse`). Use for PRs, READMEs,
     *   and docs where inline autoplay matters more than file size. No audio
     *   stream; transparency is binary because GIF has no partial alpha.
     * - `"png-sequence"`: a directory of zero-padded RGBA PNGs
     *   (`frame_000001.png` …). Lossless alpha, largest on disk, no muxed
     *   audio (an `audio.m4a` sidecar is written alongside the PNGs when
     *   the composition has audio elements). Use for After Effects / Nuke
     *   / Fusion ingest, or when frames need post-processing before
     *   encoding. `outputPath` is treated as a directory; it is created if
     *   it doesn't exist.
     *
     * Alpha output (`"webm"`, `"mov"`, `"png-sequence"`, `"gif"`) automatically
     * forces screenshot capture (Chrome's BeginFrame compositor does not
     * preserve alpha on Linux headless-shell) and disables HDR — HDR +
     * alpha is not a supported combination, a warning is logged and HDR
     * falls back to SDR. The transparent-background CSS is injected by
     * the engine's `initTransparentBackground` helper, so authors should
     * not paint a fullscreen `body` / `#root` background in their
     * compositions when targeting alpha output.
     */
    format?: RenderOutputFormat;
    /** GIF Netscape loop count. 0 means infinite looping. Only used with `format: "gif"`. */
    gifLoop?: number;
    workers?: number;
    useGpu?: boolean;
    debug?: boolean;
    /** Strict rejects correctness warnings; best-effort returns a qualified outcome. */
    strictness?: RenderStrictness;
    /** Entry HTML file relative to projectDir. Defaults to "index.html". */
    entryFile?: string;
    /** Full producer config. When provided, env vars are not read. */
    producerConfig?: EngineConfig;
    /** Custom logger. Defaults to console-based defaultLogger. */
    logger?: ProducerLogger;
    /** Override CRF for the video encoder. Mutually exclusive with `videoBitrate`. */
    crf?: number;
    /** Target video bitrate (e.g. "10M"). Mutually exclusive with `crf`. */
    videoBitrate?: string;
    /**
     * Source-video frame extraction format. Defaults to `"auto"`, which preserves
     * the historical behavior: alpha/alpha-capable sources extract as PNG, all
     * other videos extract as JPG. Set to `"png"` for lossless source-frame
     * extraction on UI recordings, screen captures, or other color-sensitive
     * videos.
     */
    videoFrameFormat?: VideoFrameFormat;
    /** HDR rendering mode.
     * - `auto` (default): probe sources; enable HDR if any HDR content is found.
     * - `force-hdr`: enable HDR even on SDR-only compositions (falls back to HLG transfer).
     * - `force-sdr`: skip probing entirely; always render SDR.
     */
    hdrMode?: "auto" | "force-hdr" | "force-sdr";
    /**
     * Render-time variable overrides for the composition. Injected as
     * `window.__hfVariables` before any page script runs and consumed by the
     * runtime helper `getVariables()`, which merges them over the declared
     * defaults from `<html data-composition-variables="...">`.
     *
     * Populated by the CLI from `--variables '<json>'` /
     * `--variables-file <path>`. Must be a JSON-serializable plain object.
     */
    variables?: Record<string, unknown>;
    /**
     * Override the output resolution via Chrome `deviceScaleFactor` (DPR).
     * The composition's authored dimensions are unchanged. See
     * {@link resolveDeviceScaleFactor} for the integer-scale, aspect, and
     * HDR constraints.
     */
    outputResolution?: CanvasResolution;
    /**
     * True when `outputResolution` was normalized from an aspect-agnostic alias
     * (`1080p`, `hd`, `4k`, `uhd`) rather than a preset that names its own
     * orientation (`landscape`, `portrait`, `1080p-portrait`, …). Set by the
     * CLI + server layers via `isAspectAgnosticResolutionAlias(rawInput)` at
     * flag/body parse time.
     *
     * When true, the compile stage adapts the preset to the composition's
     * orientation before calling `resolveDeviceScaleFactor` — a portrait
     * 1080×1920 composition with `--resolution 1080p` (normalized to
     * `landscape`) is re-mapped to `portrait`, honoring the user's intent
     * ("render at 1080p") without forcing them to know the aspect-suffixed
     * alias (`1080p-portrait`). Explicit orientation presets stay strict.
     */
    outputResolutionAspectAgnostic?: boolean;
}
export interface RenderPerfSummary {
    renderId: string;
    totalElapsedMs: number;
    fps: number;
    quality: string;
    workers: number;
    /**
     * Provenance of the auto worker-sizing decision (undefined when the
     * htmlInCanvas / low-memory pins short-circuited sizing). `boundBy` names
     * the binding constraint; the heap fields are the advisory budget being
     * validated by fleet telemetry before enforcement — see
     * `computeWorkerSizing` in @hyperframes/engine.
     */
    workerSizing?: WorkerSizing;
    chunkedEncode: boolean;
    chunkSizeFrames: number | null;
    compositionDurationSeconds: number;
    totalFrames: number;
    resolution: {
        width: number;
        height: number;
    };
    videoCount: number;
    audioCount: number;
    stages: Record<string, number>;
    /** Per-phase breakdown of the Phase 2 video extraction (resolve, HDR probe, HDR preflight, VFR probe/preflight, per-video extract). Undefined when the composition has no videos. */
    videoExtractBreakdown?: ExtractionPhaseBreakdown;
    /** Bytes on disk in the render's workDir at assembly time (sampled before cleanup). Lets callers correlate peak temp usage with render duration. */
    tmpPeakBytes?: number;
    /**
     * Average wall-clock capture time per output frame.
     *
     * Uses `stages.captureFrameMs` when present so fixed Stage 4 setup costs
     * (file server creation, calibration, readiness/session init, strategy
     * resolution) do not get amortized into a per-frame metric. Older summaries
     * without the split fall back to `stages.captureMs`.
     */
    captureAvgMs?: number;
    /**
     * Median per-frame capture time from the engine's per-frame samples —
     * warmup-robust (first frames pay font/image decode) and free of stage
     * setup amortization, unlike `captureAvgMs`. From the session that
     * captured the most frames when parallel workers report separately.
     */
    captureP50Ms?: number;
    /** Worst sub-composition timeline wait outcome across sessions. */
    subTimelineWait?: SubTimelineWaitOutcome;
    capturePeakMs?: number;
    captureCalibration?: {
        sampledFrames: number[];
        p95Ms?: number;
        multiplier: number;
        reasons: string[];
    };
    captureAttempts?: CaptureAttemptSummary[];
    observability?: RenderObservabilitySummary;
    /**
     * Peak resident set size (RSS) observed during the render, in MiB.
     *
     * Sampled every 250ms by a process-wide poller; surfaces gross memory
     * regressions (e.g. unbounded image-cache growth) that wall-clock numbers
     * miss. Optional because callers can serialize older `RenderPerfSummary`
     * shapes back into this type.
     */
    peakRssMb?: number;
    /**
     * Peak V8 heap used observed during the render, in MiB.
     *
     * Useful as a finer-grained complement to {@link peakRssMb} — RSS includes
     * native ffmpeg/Chrome allocations, while heapUsed isolates JS-object growth
     * inside the orchestrator. Optional for the same back-compat reason.
     */
    peakHeapUsedMb?: number;
    hdrDiagnostics?: HdrDiagnostics;
    hdrPerf?: HdrPerfSummary;
    /**
     * Static-frame dedup outcome for this render (opt-out HF_STATIC_DEDUP=false),
     * aggregated across the sequential session or all parallel workers. `enabled`
     * is the adoption signal; `armed` means it passed every gate + verification;
     * `skipReason` says why it didn't arm; `reusedFrames`/`predictedFrames` measure
     * effectiveness (reuse % = reusedFrames / totalFrames). Undefined when no
     * capture session ran (e.g. layered-HDR-only paths).
     */
    staticDedup?: {
        enabled: boolean;
        armed: boolean;
        predictedFrames: number;
        reusedFrames: number;
        skipReason?: string;
    };
    /**
     * BeginFrame no-damage reuse outcome for this render (Linux/Docker),
     * aggregated across the sequential session or all parallel workers: frames
     * Chrome reported unchanged (`hasDamage=false` → previous buffer reused via
     * the engine's lastFrameCache) vs frames freshly encoded. The BF counterpart
     * of `staticDedup` (predictive dedup never arms under beginframe); the
     * static-frame fraction is noDamageFrames / (noDamageFrames + hasDamageFrames).
     * Undefined when no session captured in beginframe mode.
     *
     * Like every metric aggregated from `dedupPerfs` (staticDedup, drawElement,
     * subTimelineWait), a partial-capture RETRY replaces the counters with the
     * final attempt's set (see the reset in executeDiskCaptureWithAdaptiveRetry)
     * — after a missing-range retry the counts cover only the recaptured ranges,
     * not the whole render, so noDamage + hasDamage may be < totalFrames.
     */
    beginFrameReuse?: {
        noDamageFrames: number;
        hasDamageFrames: number;
    };
    /**
     * drawElement fast-capture outcome for this render (default-on release
     * visibility). Undefined when no capture session ran.
     */
    drawElement?: {
        /** Final capture mode: "drawelement" | "screenshot" | "beginframe" (|-joined if workers diverge). */
        mode: string;
        /** Compile-time gate that disabled default DE: 3d | mix_blend_mode | shader_transitions. */
        compileGate?: string;
        /** Producer clamp that disabled default DE: parallel | disk_path. */
        clampReason?: string;
        /** Auto-parallel inversion outcome: "inverted" (fired, held), "reverted" (fired, self-verify retry rolled back), "none". */
        workerInversion?: string;
        /** Worker count the auto-resolution chose BEFORE the inversion pinned it to 1 — the parallel counterfactual for speedup math. Only set when the inversion fired. */
        preInversionWorkers?: number;
        /** Rough compiled-composition element count — the variable the short-comp inversion band is gated on. Always set. */
        compositionElementCount?: number;
        /** Rough compiled-composition element-count provenance: "live" (probe DOM) | "static" (source scan, not trusted to open the band). */
        compositionElementCountSource?: "live" | "static";
        /** Short-comp band attribution: "applied" | "skipped_elements" | "unmeasured"; unset when the frame count made the band irrelevant. */
        shortBand?: "applied" | "skipped_elements" | "unmeasured";
        /** DE parallel-router outcome: "routed" (fired, held), "reverted" (fired, self-verify retry rolled back), "none". Mutually exclusive with workerInversion. */
        parallelRouter?: string;
        /** Worker count the auto-resolution chose BEFORE the router pinned it to 3 — the single-worker-inversion counterfactual. Only set when the router fired. */
        preRouterWorkers?: number;
        /** Engine init-time gate: swiftshader | css_effect:* | at_risk_timeline | 3d_init_failed | supersampling | render_mode_hint. */
        gateReason?: string;
        /** Low-cardinality GPU bucket from DE session init (`<backend>/<vendor>`, e.g. `d3d11/nvidia`); |-joined across parallel sessions (bounded: one bucket per distinct backend on the host). */
        gpuRenderer?: string;
        /** Worker-encode drain (the verified path) was active. */
        workerEncode: boolean;
        /** Self-verification ground-truth samples armed at init. */
        verifyArmed: number;
        /** Samples actually compared at drain time. */
        verifyChecked: number;
        /** Minimum PSNR across checked samples (dB; margin above the 32dB threshold). */
        verifyMinDb?: number;
        /** Init cost of capturing ground truth (ms). */
        verifyInitMs: number;
        /**
         * SELF-VERIFICATION tripped (blank/PSNR) and the render re-ran via
         * screenshot. Narrowed since the pinned-fallback retry was widened
         * (review): OOM/generic-capture-error fallbacks report FALSE here —
         * `fallbackReason` being set is the "any fallback fired" signal.
         */
        selfVerifyFallback: boolean;
        /** What tripped the fallback retry: psnr | blank | oom | capture_error. */
        fallbackReason?: string;
        /** The failing PSNR (dB) when `fallbackReason === "psnr"`; undefined for blank/oom/capture_error (no score exists). */
        fallbackFailedDb?: number;
        /** Frame index the verification failure was detected at; set for both "psnr" and "blank" fallback reasons. */
        fallbackFrameIndex?: number;
        /** The HF_DE_VERIFY_MIN_DB threshold the failing dB breached; only set alongside fallbackFailedDb (psnr reason). */
        fallbackThresholdDb?: number;
        /** Blank-guard counters. */
        blankSuspects: number;
        blankDeterministicAccepts: number;
        blankRecaptures: number;
        /** Clip-cut boundary frames captured via per-frame screenshot. */
        boundaryFrames: number;
        /** Per-frame "No cached paint record" screenshot fallbacks. */
        ncprFallbacks: number;
    };
}
export interface HdrDiagnostics {
    videoExtractionFailures: number;
    imageDecodeFailures: number;
}
export interface FrameRange {
    startFrame: number;
    endFrame: number;
}
export interface CaptureAttemptSummary {
    attempt: number;
    workers: number;
    frameCount: number;
    /**
     * `"transient-retry"` is a same-worker-count retry after a transient browser
     * death (Target closed / tab crash); `"retry"` is the worker-halving retry
     * after a recoverable timeout. Distinguished so transient-retry burn is
     * countable for telemetry (dashboard 1783183).
     */
    reason: "initial" | "retry" | "transient-retry";
}
export interface RenderJob {
    id: string;
    config: RenderConfig;
    status: RenderStatus;
    progress: number;
    currentStage: string;
    createdAt: Date;
    startedAt?: Date;
    completedAt?: Date;
    outcome?: RenderOutcome;
    warnings: RenderWarning[];
    error?: string;
    outputPath?: string;
    duration?: number;
    totalFrames?: number;
    framesRendered?: number;
    perfSummary?: RenderPerfSummary;
    failedStage?: string;
    errorDetails?: {
        message: string;
        stack?: string;
        elapsedMs: number;
        freeMemoryMB: number;
        browserConsoleTail?: string[];
        perfStages?: Record<string, number>;
        hdrDiagnostics?: HdrDiagnostics;
        observability?: RenderObservabilitySummary;
        /** Worst sub-composition timeline wait outcome across sessions captured before the failure. */
        subTimelineWait?: SubTimelineWaitOutcome;
    };
}
export type ProgressCallback = (job: RenderJob, message: string) => void | Promise<void>;
export declare class RenderQualityError extends Error {
    readonly warnings: readonly RenderWarning[];
    constructor(warnings: readonly RenderWarning[]);
}
export declare function applyRenderWarningPolicy(job: RenderJob, captureWarnings: readonly CaptureWarning[], log?: ProducerLogger): void;
export declare class RenderCancelledError extends Error {
    reason: "user_cancelled" | "timeout" | "aborted";
    constructor(message?: string, reason?: "user_cancelled" | "timeout" | "aborted");
}
export declare function createRenderFileLogger(logPath: string, base?: ProducerLogger): ProducerLogger;
export declare function collectVideoReadinessSkipIds(nativeHdrVideoIds: ReadonlySet<string>, extractedVideos: readonly ExtractedVideoReadinessInput[]): string[];
interface ExtractedVideoReadinessInput {
    videoId: string;
    metadata: {
        width: number;
        height: number;
    };
}
export declare function collectVideoMetadataHints(extractedVideos: readonly ExtractedVideoReadinessInput[]): CaptureVideoMetadataHint[];
export declare function findMissingFrameRanges(totalFrames: number, framesDir: string, frameExt: "jpg" | "png"): FrameRange[];
export declare function buildMissingFrameRetryBatches(ranges: FrameRange[], maxWorkers: number, workDir: string, attempt: number, rangeStart?: number): WorkerTask[][];
/**
 * The capture mode this render will REPORT, pre-capture.
 *
 * BeginFrame is Linux-only. Both real entry points enforce that —
 * `frameCapture`'s preMode (`headlessShell && isLinux && !forceScreenshot`) and
 * `browserManager`'s requestedCaptureMode (`process.platform === "linux"`) —
 * but the observability field derived the mode from `forceScreenshot` alone,
 * with no platform test. Every non-Linux render that did not force screenshot
 * therefore reported `beginframe` for a capture that was really screenshot:
 * 30,625 Windows renders over 14 days, a fifth of the fast-capture dashboard's
 * capture-mode data.
 *
 * `config.ts` documents this same failure for "darwin + software" and adds a
 * `forceScreenshot` clamp as defence-in-depth — but that clamp only fires on
 * software GPU, so Windows-on-hardware slipped straight past it (41,102 of the
 * mislabelled renders).
 *
 * NECESSARY, NOT SUFFICIENT — read this before trusting the value on Linux.
 * The platform test is the only condition modelled here. Linux BeginFrame
 * additionally requires a headless-shell binary, no supersampling, no
 * transparent drawElement route (`frameCapture.ts` preMode) and the
 * `--enable-begin-frame-control` flag (`browserManager.ts`). Any of those can
 * make the ACTUAL mode screenshot while this still reports `beginframe`, so a
 * Linux `beginframe` reading is an upper bound, not a fact. The authoritative
 * value is the session's own `launchCaptureMode` — the same field the runtime
 * video gate already falls back to. Deriving this field from the resolved
 * session instead of from config is the real fix and is deliberately NOT in
 * this change: it closes the Windows mislabel, which is platform-only and
 * needs no session plumbing.
 *
 * Pure; exported for tests.
 */
export declare function resolveObservedCaptureMode(forceScreenshot: boolean, platform?: NodeJS.Platform): "screenshot" | "beginframe";
/**
 * Build the observability patcher, re-deriving `captureMode` on every patch.
 *
 * Extracted and exported because the previous inline closure was where the
 * Windows mislabel actually lived. Seeding `captureMode` correctly at
 * construction is NOT sufficient: this updater is invoked at 23 sites through
 * the pipeline, and one of them —
 * `updateCaptureObservability({ forceScreenshot: captureForceScreenshot })`
 * straight after compile — runs unconditionally on every render. The old body
 * re-derived from `forceScreenshot` alone, so the seeded value was overwritten
 * with `beginframe` again before capture began, and both the success and error
 * telemetry emits read the reverted object. A helper-only test cannot catch
 * that: it never round-trips through this closure. Hence the export.
 */
export declare function createCaptureObservabilityUpdater(observability: RenderCaptureObservability, platform?: NodeJS.Platform): (patch: Partial<RenderCaptureObservability>) => void;
export declare function getNextRetryWorkerCount(currentWorkers: number): number;
export declare function resolveRenderWorkDirPrefix(outputPath: string, jobId: string, platform?: NodeJS.Platform, systemTempDir?: string): string;
/**
 * Bounded number of retries for transient browser deaths (a `Target closed` /
 * `Page crashed` — the tab died, not the composition). Distinct from the
 * worker-count-halving retry: a transient death is often a one-off (contended
 * host, OOM-killed tab, flaky CDP session) that clears on a fresh session, so
 * we retry ONCE at the SAME worker count before falling through to the
 * halving/structural-failure logic. Capped at 1 so a deterministically-dying
 * tab can't loop.
 */
export declare const MAX_TRANSIENT_CAPTURE_RETRIES = 1;
/**
 * A retry only pays off if the attempt that just finished captured at least one
 * frame toward its target. When it captured nothing (frames still missing >=
 * frames it set out to capture), the composition is structurally broken — a
 * never-ready page, zero duration, or unparseable HTML — not a flaky worker.
 * Re-running it at lower parallelism just burns another full readiness/protocol
 * timeout per worker, turning a render that can never succeed into a long hang.
 * A partially-captured attempt still retries, so genuine flaky-worker gaps are
 * unaffected.
 */
export declare function captureAttemptMadeProgress(attemptTargetFrameCount: number, remainingFrameCount: number): boolean;
export declare function resetCaptureAttemptProgress(job: {
    framesRendered?: number;
}): void;
export declare function isRecoverableParallelCaptureError(error: unknown): boolean;
/**
 * Turn a cryptic memory-exhaustion failure (V8 `Set maximum size exceeded`,
 * heap-limit abort, oversized allocation) into an actionable message. These
 * come from oversized compositions — very high resolution, very long duration,
 * or a huge frame count — not composition-logic bugs, and a retry re-hits the
 * same ceiling. The guidance points at the levers that actually reduce memory
 * pressure. Returns the original message unchanged for non-OOM errors.
 */
export declare function describeMemoryExhaustion(error: unknown, ctx: {
    width?: number;
    height?: number;
    totalFrames?: number;
}): string | null;
export declare function executeDiskCaptureWithAdaptiveRetry(options: {
    serverUrl: string;
    workDir: string;
    framesDir: string;
    totalFrames: number;
    initialWorkerCount: number;
    allowRetry: boolean;
    frameExt: "jpg" | "png";
    captureOptions: CaptureOptions;
    createBeforeCaptureHook: () => BeforeCaptureHook | null;
    abortSignal?: AbortSignal;
    onProgress?: (progress: ParallelProgress) => void;
    cfg: EngineConfig;
    log: ProducerLogger;
    /**
     * Forwarded to each `WorkerTask`'s `outputFrameOffset` and to the
     * `buildMissingFrameRetryBatches` translation. Default 0 (in-process
     * contract: `[0, totalFrames)`). See `WorkerTask.outputFrameOffset`.
     */
    frameRangeStart?: number;
    /** Mutated in place — replaced each attempt so only the final attempt's worker perf survives (see retry reset below). */
    dedupPerfs: CapturePerfSummary[];
}): Promise<CaptureAttemptSummary[]>;
export type RenderConfigInput = Omit<RenderConfig, "fps"> & {
    fps: FpsInput;
};
export declare function createRenderJob(config: RenderConfigInput): RenderJob;
export declare function shouldUseStreamingEncode(cfg: Pick<EngineConfig, "enableStreamingEncode" | "streamingEncodeMaxDurationSeconds"> & Partial<Pick<EngineConfig, "lowMemoryMode">>, outputFormat: NonNullable<RenderConfig["format"]>, workerCount: number, durationSeconds: number, forceParallelStream?: boolean): boolean;
/**
 * Integer tuning knob from the environment. Matches the convention the
 * surrounding DE thresholds already use: unset OR set-but-empty falls back to
 * the default (a blank var is not a kill switch), and so does anything
 * non-numeric — a typo must never silently disable a routing guard.
 */
export declare function envInt(name: string, fallback: number): number;
/**
 * Rough element count for compiled composition HTML.
 *
 * Deliberately a string scan and not a `parseHTML` + `querySelectorAll` (the
 * `countAuthoredTimedClips` approach): this runs on EVERY render before the
 * routing decision, and a full linkedom parse of the exact documents that
 * matter here — the 20k-40k node ones — is the most expensive case. Precision
 * is not needed. It feeds a threshold whose measured crossover is ~3.9k and
 * whose default sits at 2500, so tag counting is comfortably inside the
 * margin — PROVIDED the count is not unboundedly low for some real content
 * shape. Three sources are counted, each catching a case the others miss:
 *
 *   1. Closing tags (`</div>`) — the base count for ordinary HTML.
 *   2. Named HTML void elements (`<img>`, `<br>`, …), bare or self-closed —
 *      voids matter because they skew EXPENSIVE to paint (images), and
 *      counting only closers would read an image gallery as a tiny comp and
 *      open the band on exactly the content most likely to lose it.
 *   3. Any self-closing tag (`<circle/>`, `<path d="…"/>`) — SVG's own
 *      elements are neither closing-tag-shaped nor in the void list, so
 *      without this a self-closing-SVG-heavy composition (`<circle/>` x 40k)
 *      counted as ZERO — an unbounded undercount, not a rounding error, and
 *      exactly the shape of comp the 1.8x regression case is made of
 *      (review finding: the ceiling cannot compensate for an error with no
 *      bound).
 *
 * Opening (non-self-closing, non-void) tags are deliberately NOT counted:
 * compiled comps embed inline scripts, and `a < b` or `x <breadth` would
 * false-positive on a bare `<letter` scan. All three counted forms require a
 * literal closing marker (`</`, a void name at a word boundary, or `/>`), so
 * ordinary JS comparisons and divisions don't qualify — verified by test.
 *
 * FALLBACK ONLY as of the live-DOM fix below — a string scan of the SOURCE
 * markup cannot see elements a composition's own script creates at runtime
 * (`document.createElement`), which is an unbounded undercount no regex can
 * close: `style-10-prod`'s per-transcript-word caption generator measures 2
 * source tags against thousands of live nodes after init (review finding).
 * `resolveCompositionElementCount` prefers the initialized probe session's
 * live count and uses this only when no such session exists.
 */
export declare function countElementTags(html: string): number;
/**
 * Element count for the short-comp band's gate, WITH PROVENANCE.
 *
 * `live` — measured from the initialized probe session's real DOM. This is
 * the only trustworthy source: it sees elements a composition's own script
 * created after load, which no scan of the source markup can (the
 * caption-word-span pattern above builds thousands of nodes from two source
 * tags).
 *
 * `static` — the `countElementTags` fallback. Emitted for diagnostics, but
 * NOT trusted to open the band: the probe is conditional (see
 * `probeStage.ts`'s `needsBrowser` — only unknown duration, unresolved
 * compositions, or specific media cases launch one), so a known-duration,
 * media-free composition that builds 40k nodes in script gets no probe, and
 * a static count that says "2". Treating that as measured would admit
 * exactly the regression case the ceiling exists to exclude (review finding,
 * R4). The caller fails closed on anything but `live`.
 *
 * These semantics are FROZEN while the short-band baseline is being read —
 * the fleet distribution recorded by the baseline release must be measured
 * by the same resolver that later gates routing, or the baseline is invalid.
 */
export declare function resolveCompositionElementCount(probeSession: Pick<CaptureSession, "isInitialized" | "page"> | null, html: string): Promise<{
    count: number;
    source: "live" | "static";
}>;
/**
 * Max-merge init telemetry across per-worker capture perf summaries — the
 * success-path channel for PARALLEL renders, whose worker console buffers
 * (and so the `[FrameCapture:INIT]` line) only propagate on failure. Max
 * matches summarizeInitObservability's own multi-session semantics: keep the
 * worst observed startup cost for duration. Tween count is per-composition,
 * so workers should agree — max is a defensive read against a worker that
 * initializes before the timeline is fully wired, not an expected disagreement.
 */
export declare function mergeWorkerInitObservability(perfs: ReadonlyArray<{
    initDurationMs?: number;
    initTweenCount?: number;
    initElementCount?: number;
}>): {
    initDurationMs?: number;
    tweenCount?: number;
    elementCount?: number;
} | undefined;
/**
 * The short-comp band's attribution decision, extracted as a pure function so
 * the gating fixes below are independently testable rather than living inline
 * where only a full render pipeline run could exercise them.
 *
 * A value is emitted ONLY when the band is DECISIVE — every other
 * inversion-eligibility condition already passed (both floor evaluations
 * agree on everything except which floor they used) and the band floor alone
 * flipped the answer. `bandEnabled` gates that decisiveness itself:
 * `HF_DE_SHORT_MAX_ELEMENTS=0` is a documented kill switch (symmetric with
 * `HF_DE_SHORT_MIN_FRAMES=0`, which already disables via the predicate's own
 * `minFrames > 0` guard), and without this gate a fired kill switch left
 * every in-band render decisive against a real floor comparison — reporting
 * "skipped_elements" (comp too large) instead of undefined (band disabled)
 * and corrupting the DiD control cohort with kill-switched renders (review
 * finding).
 *
 * Three decisive outcomes, and the distinction between the last two is the
 * point:
 *   "applied"          — measured LIVE and under the ceiling. Only this
 *                        routes (once HF_DE_SHORT_BAND_ROUTE is on) and only
 *                        this joins the treatment cohort.
 *   "skipped_elements" — measured live, over the ceiling. A real oversize
 *                        observation; the DiD control group.
 *   "unmeasured"       — no live DOM count available (no probe session ran;
 *                        see `resolveCompositionElementCount`). FAILS CLOSED:
 *                        never routes, and kept out of BOTH cohorts so a
 *                        static undercount cannot masquerade as a small comp
 *                        (review finding, R4). Emitted rather than dropped
 *                        because its fleet rate sizes the population a
 *                        future conditional-probe-launch would unlock.
 */
export declare function resolveDeShortBand(args: {
    invertAtBaseFloor: boolean;
    invertAtBandFloor: boolean;
    bandEnabled: boolean;
    bandOpen: boolean;
    elementCountSource: "live" | "static";
}): "applied" | "skipped_elements" | "unmeasured" | undefined;
/**
 * DE priority inversion predicate: should an AUTO-resolved multi-worker render
 * drop to single-worker verified drawElement streaming?
 *
 * Benchmarked 2026-07-08: above ~900 frames DE-single beats screenshot-parallel
 * at every worker count (2,380f: 66s vs 109–127s at W2–W5); below it DE's fixed
 * init cost (verify + dedup arming) loses by a small margin. Only fires for the
 * exact benchmarked configuration: default-on DE, mp4, streaming-eligible,
 * no compile gate, no forced screenshot, workers not explicitly requested.
 */
export declare function shouldPreferSingleWorkerDrawElement(args: {
    workerCount: number;
    /** job.config.workers — a number means the user explicitly chose. */
    requestedWorkers: number | "auto" | undefined;
    useDrawElement: boolean;
    deCompileGate: string | undefined;
    forceScreenshot: boolean;
    outputFormat: NonNullable<RenderConfig["format"]>;
    totalFrames: number;
    /** Amortization threshold; <=0 disables the inversion. */
    minFrames: number;
    /** shouldUseStreamingEncode(cfg, format, 1, duration) at the call site. */
    singleWorkerStreamingOk: boolean;
    /**
     * Comp routes to the layered-composite / page-side-compositing paths
     * (HDR content or shader transitions) — those force screenshots and never
     * run drawElement or streaming, so an inversion would only mislabel
     * telemetry and keep the probe session alive through the heaviest stage.
     */
    layeredOrEffectRoute: boolean;
    /** deviceScaleFactor > 1 — the engine's supersampling gate blocks DE. */
    supersampling: boolean;
    /**
     * The probe session already ran the engine's init-time DE gates and DE did
     * NOT engage (not drawelement mode, not a deferred video comp) — inverting
     * would pin a known-screenshot render to one worker.
     */
    probeDeGated: boolean;
    /**
     * PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true is an explicit opt-in that
     * deliberately allows parallel drawElement (bypassing the downstream
     * clamp) — honor it like an explicit --workers request.
     */
    experimentalParallelDeOptIn: boolean;
}): boolean;
/**
 * Plan the self-verify retry for an inverted render: the inversion bet on
 * drawElement and lost, so the re-render returns to the pre-inversion parallel
 * screenshot path (streaming re-resolved for that worker count — multi-worker
 * routes to the disk stage). Returns null when the render was not inverted.
 *
 * On OOM specifically, the retry drops to a single worker regardless of the
 * pre-inversion count — an actual memory remedy (one Chrome page instead of
 * N), not just a different capture mode at the same parallelism the host
 * already choked on. The pre-inversion count can be higher than what the DE
 * path used (calibration's own pick), so reusing it unmodified on an
 * OOM-triggered retry would re-run at equal or greater parallelism than the
 * failure, worsening the odds for this render and anything sharing the host.
 */
export declare function resolveInversionRetryPlan(args: {
    deWorkerInversion: "inverted" | "reverted" | undefined;
    preInversionWorkerCount: number;
    cfg: Pick<EngineConfig, "enableStreamingEncode" | "streamingEncodeMaxDurationSeconds">;
    outputFormat: NonNullable<RenderConfig["format"]>;
    durationSeconds: number;
    isMemoryExhaustion: boolean;
}): {
    workerCount: number;
    useStreamingEncode: boolean;
    deWorkerInversion: "reverted";
} | null;
/**
 * DE parallel-router predicate: should an AUTO-resolved multi-worker render
 * use VERIFIED PARALLEL drawElement streaming (HF_DE_PARALLEL_STREAM) instead
 * of the #2026 single-worker inversion?
 *
 * Benchmarked 2026-07-08 (clean, quiet-machine re-run): par3/single 1.16–1.36x
 * on real-work comps ≥2,000 frames (2,381f GSAP graphics 1.36x, 3,245f rAF
 * high-variance 1.29x, 915f crossover probe 1.27x); the one comp that didn't
 * clear 1.25x (3,600f, 39% static/dedup-heavy) still didn't LOSE to single-
 * worker (1.16x) — dedup already skips the capture work parallelism would
 * split, so there's mechanically less headroom, not a regression. No comp
 * anywhere showed par3 < single. Default ON since 2026-07-27
 * (HF_DE_PARALLEL_ROUTER=false is the kill switch): the default-off soak
 * proved the safety half (zero shipped damage, 100% revert recovery), so the
 * flip trades an accepted ~2.3% revert rate for parallelizing the ≥700f
 * band. This promotes the opt-in mechanism from #2056 into the auto-routing
 * decision.
 * Takes priority over the single-worker inversion when both would fire.
 * Re-calibrated 2026-07-27: a controlled crossover sweep (three content
 * profiles including a genuinely init-expensive 24-sub-composition comp;
 * worker counts and capture modes verified per run) found par3 > single at
 * every size from 350f up in every profile — workers init concurrently, so
 * per-worker init duplication costs CPU, not wall-clock. minFrames therefore
 * dropped below the inversion's threshold (700 vs 900): where both fire,
 * parallel wins over the inversion's single-worker pick (+17–21% at 700f).
 */
/**
 * Is the DE parallel router enabled for this process?
 *
 * Default ON since 2026-07-27; `HF_DE_PARALLEL_ROUTER` is the kill switch.
 * Every conventional spelling of "off" disables it — a naive
 * `!== "false"` would silently ignore `0`, `off`, `no`, `FALSE`, and an
 * exported-but-empty var, i.e. an opt-out that FAILS OPEN and hands the user
 * 3-worker parallel DE anyway (review finding). A set-but-empty value means
 * "unset" here, matching how the sibling HF_DE_* numeric knobs treat it.
 *
 * The CLI's circuit breaker relies on this accepting an explicit "false":
 * once an install trips the breaker it writes that value rather than
 * unsetting the var, because under a default-ON flag unsetting means ON.
 * Pure; exported for tests.
 */
export declare function isDeParallelRouterEnabled(env: Readonly<Record<string, string | undefined>>): boolean;
export declare function shouldPreferParallelDrawElement(args: {
    workerCount: number;
    /** job.config.workers — a number means the user explicitly chose. */
    requestedWorkers: number | "auto" | undefined;
    useDrawElement: boolean;
    deCompileGate: string | undefined;
    forceScreenshot: boolean;
    outputFormat: NonNullable<RenderConfig["format"]>;
    totalFrames: number;
    /** Amortization threshold; <=0 disables the router. */
    minFrames: number;
    layeredOrEffectRoute: boolean;
    supersampling: boolean;
    probeDeGated: boolean;
    experimentalParallelDeOptIn: boolean;
    /** HF_DE_PARALLEL_ROUTER !== "false" — default ON since 2026-07-27; env var is the kill switch. */
    routerEnabled: boolean;
    /**
     * Whether verified parallel DE STREAMING can actually run for this render
     * (`shouldUseStreamingEncode` at the router's worker count with
     * forceParallelStream). The router's entire value is that path; without it
     * firing would pin workerCount to 3 and skip calibration while delivering
     * none of the benefit — e.g. a composition longer than
     * `streamingEncodeMaxDurationSeconds` (240 s default), where the duration
     * cap disables streaming before the router's force flag is consulted.
     */
    parallelStreamingAvailable: boolean;
    /** Machine RAM (os.totalmem, MB). */
    totalMemoryMb: number;
    /** RAM floor for routing; <=0 disables the guard. */
    minMemoryMb: number;
}): boolean;
/**
 * Plan the self-verify retry for a router-routed render: the bet on verified
 * parallel drawElement streaming lost, so the re-render falls back to the
 * pre-router worker count on the ordinary (non-DE) parallel path. Unlike
 * `resolveInversionRetryPlan`, the caller must also clear the router's
 * `deParallelStreamForced` local BEFORE calling this — `shouldUseStreamingEncode`
 * takes it as a direct argument, so a stale `true` would keep resolving to
 * the parallel-streaming shape on the retry instead of the well-tested
 * parallel-disk fallback. Returns null when the render was not router-routed.
 *
 * On OOM specifically, the retry drops to a single worker regardless of the
 * pre-router count — see `resolveInversionRetryPlan`'s doc for why (the
 * pre-router count is calibration's own pick and can exceed the router's
 * pin, e.g. calibration wanting 5 while the router pinned to 3 — reusing it
 * unmodified on an OOM retry would run the fallback at MORE parallelism than
 * what just failed).
 */
export declare function resolveParallelRouterRetryPlan(args: {
    deParallelRouter: "routed" | "reverted" | undefined;
    preRouterWorkerCount: number;
    cfg: Pick<EngineConfig, "enableStreamingEncode" | "streamingEncodeMaxDurationSeconds">;
    outputFormat: NonNullable<RenderConfig["format"]>;
    durationSeconds: number;
    isMemoryExhaustion: boolean;
}): {
    workerCount: number;
    useStreamingEncode: boolean;
    deParallelRouter: "reverted";
} | null;
/**
 * Should a capture-stage error retry via the pinned-worker-count fallback
 * (the same "well-tested parallel-disk / single-worker screenshot" path
 * `resolveInversionRetryPlan`/`resolveParallelRouterRetryPlan` reroute to)
 * instead of failing the render outright?
 *
 * True for the drawElement self-verify failures this retry path was
 * originally built for (blank frame / PSNR breach), AND for any OTHER
 * capture-stage failure (host-contention timeout, worker crash, OOM) while a
 * worker count was PINNED by the inversion or router — those pin regardless
 * of calibration, so a generic capture failure on that pinned count is
 * exactly the scenario the pin itself introduced risk for.
 *
 * Includes OOM (previously excluded — see PR history): every worker's
 * `executeWorkerTask` closes its capture session in a `finally` that awaits
 * `closeCaptureSession` → `releaseBrowser`, which SIGKILLs the Chrome process
 * via `forceReleaseBrowser` if a graceful `page.close()` hangs
 * (`browserManager.ts`). `Promise.all` in `executeParallelCapture` waits for
 * every worker's `finally` before this error is even thrown, so by the time
 * we're deciding whether to retry, the failed attempt's Chrome processes are
 * already gone — there's no lingering memory to retry into. And the
 * fallback itself is structurally lighter than what OOM'd: parallel DE
 * forces `enableBrowserPool: false` (N separate Chrome processes — required,
 * not incidental, to avoid a co-tenant-page compositor-starvation bug), while
 * the parallel-SS fallback uses the default pooled browser (one shared
 * process). Retrying at a possibly-higher worker count is still fewer total
 * Chrome processes than what just failed.
 *
 * Excludes cancellation (review): a user-initiated abort must propagate
 * immediately, not detour through spawning a fresh encoder/capture session
 * before the outer catch's `RenderCancelledError` branch ends the render —
 * that would delay honoring "stop" with a pointless resource spin-up/
 * tear-down cycle.
 */
export declare function shouldRetryViaPinnedFallback(args: {
    isVerifyError: boolean;
    isCancellation: boolean;
    deWorkerInversion: "inverted" | "reverted" | undefined;
    deParallelRouter: "routed" | "reverted" | undefined;
}): boolean;
/**
 * When a self-verify (or pinned-fallback) retry is triggered mid-capture, the
 * caller may still hold a live probe session that the failed stage was passed
 * but did not (or could not) close in its own `finally` before it threw. Left
 * behind, that session's Chrome process orphans until the containing render
 * exits — precisely when we are recovering from GPU/memory pressure and can
 * least afford an unaccounted Chrome. Close it before the caller clears its
 * reference; swallow any close error with a warn so the retry itself is never
 * derailed by a shutdown hiccup.
 */
export declare function closeOrphanedProbeForRetry(probe: CaptureSession, closer: (session: CaptureSession) => Promise<void>, log: Pick<ProducerLogger, "warn">, retryContext: string): Promise<void>;
/**
 * Parallel-streaming router for NON-drawElement capture (screenshot on
 * macOS/Windows/forced-screenshot, BeginFrame on Linux): should this
 * multi-worker render stream captured frame buffers straight into the single
 * ffmpeg stdin encoder (interleaved distribution + ordered reorder-buffer
 * writer — the PR #2056 machinery) instead of the parallel disk path (workers
 * write JPEGs, a separate sequential encode pass reads them back)?
 *
 * Measured motivation (2026-07-10, macOS SS W3): the disk path's encode is a
 * purely additive tail (~27% of wall clock on a 3,600-frame comp). Streaming
 * overlapped it for 1.29x on a uniform-cost comp and was a wash (not a
 * regression) on a 39%-static bimodal comp — the interleaved writer's
 * near-lockstep coupling eats the encode win when frame costs are bimodal.
 * v1 accepts the wash; static-aware routing is a documented follow-up.
 *
 * Unlike the DE router this deliberately does NOT require auto-resolved
 * workers: streaming doesn't change the worker count, so an explicit
 * `--workers 3` should benefit too. It requires !useDrawElement
 * (post-resolveConfig — always true on Linux): DE parallel renders belong to
 * the DE parallel router (HF_DE_PARALLEL_ROUTER) with its self-verify
 * machinery; both DE predicates independently require useDrawElement, making
 * the two routers mutually exclusive by construction.
 */
export declare function shouldStreamParallelCapture(args: {
    /** HF_CAPTURE_PARALLEL_STREAM === "true" — kill switch, default OFF. */
    routerEnabled: boolean;
    workerCount: number;
    /** cfg.useDrawElement AFTER resolveConfig clamps. */
    useDrawElement: boolean;
    outputFormat: NonNullable<RenderConfig["format"]>;
    /** shouldUseStreamingEncode(cfg, format, 1, duration) at the call site —
     * carries the enableStreamingEncode/format/duration-cap gates. */
    streamingOk: boolean;
    /** HDR layered composite or shader transitions — bespoke pipelines
     * (including page-side compositing, which only engages when
     * hasShaderTransitions) that never stream. */
    layeredOrEffectRoute: boolean;
}): boolean;
export declare function resolveCaptureForceScreenshotForPageSideCompositing(args: {
    forceScreenshot: boolean;
    usePageSideCompositing: boolean;
}): boolean;
export declare function shouldDiscardProbeSessionForPageSideCompositing(args: {
    hasProbeSession: boolean;
    usePageSideCompositing: boolean;
}): boolean;
/**
 * Main render pipeline
 */
export declare function extractStandaloneEntryFromIndex(indexHtml: string, entryFile: string, entryHtml?: string): string | null;
/**
 * Render a `RenderJob` end-to-end: compile → probe → extract videos →
 * audio → capture → encode → assemble. The function body is a thin
 * sequencer over the eight stage modules in `./render/stages/`; the
 * orchestrator owns shared resources (work dir, file server, probe
 * session, browser console buffer, perf counters, peak-memory sampler)
 * and the `try/finally` cleanup. Returns once the final output exists at
 * `outputPath`; throws on cancellation, encoder failure, or a stage
 * error (with a diagnostic summary written to `perf-summary.json`).
 */
export declare function executeRenderJob(job: RenderJob, projectDir: string, outputPath: string, progressSink?: ProgressCallback, abortSignal?: AbortSignal): Promise<void>;
export {};
//# sourceMappingURL=renderOrchestrator.d.ts.map