import type { ProducerLogger } from "../../logger.js";
export type RenderObservationStatus = "start" | "end" | "error" | "checkpoint";
export type RenderObservationValue = string | number | boolean | null;
export type RenderObservationData = Record<string, RenderObservationValue>;
export interface RenderObservationEvent {
    renderJobId?: string;
    phase: string;
    status: RenderObservationStatus;
    elapsedMs: number;
    durationMs?: number;
    message?: string;
    data?: RenderObservationData;
}
export interface BrowserDiagnosticSummary {
    total: number;
    /** Generic browser error lines after page/request/navigation/console-specific diagnostics are classified. */
    errors: number;
    pageErrors: number;
    requestFailed: number;
    httpErrors: number;
    navigationStarts: number;
    navigationFailures: number;
    consoleErrors: number;
    consoleWarnings: number;
}
export interface RenderCaptureObservability {
    forceScreenshot: boolean;
    captureMode: "screenshot" | "beginframe";
    captureBeyondViewport?: boolean;
    workerCount?: number;
    useStreamingEncode?: boolean;
    useLayeredComposite?: boolean;
    usePageSideCompositing?: boolean;
    hasHdrContent?: boolean;
    browserGpuMode?: string;
    /**
     * drawElement per-render SELF-VERIFICATION tripped (blank/PSNR) → whole
     * render re-ran via screenshot. NARROWED semantics since the pinned-fallback
     * retry was widened (review): OOM- and generic-capture-error-triggered
     * fallbacks report FALSE here, with `deFallbackReason` ∈ {oom,
     * capture_error}. The "any fallback fired" signal is `deFallbackReason`
     * being set, NOT this flag — dashboards keyed on `de_self_verify_fallback =
     * true` as any-fallback must migrate to `de_fallback_reason IS NOT NULL`.
     */
    deSelfVerifyFallback?: boolean;
    /**
     * Why the capture-stage retry (self-verify OR the pinned-worker-count
     * fallback) fired: "blank"/"psnr" for a real self-verify trip,
     * "oom"/"capture_error" for the widened generic-failure retry. Set
     * whenever a fallback is attempted, independent of whether that retry
     * itself later succeeds — so a render that fails AFTER a fallback attempt
     * (perfSummary never built) is still distinguishable in failure-path
     * telemetry from one that never attempted any fallback.
     */
    deFallbackReason?: string;
    /** The failing PSNR (dB) when `deFallbackReason === "psnr"`; undefined for blank/oom/capture_error (no score exists). */
    deFallbackFailedDb?: number;
    /** Frame index the verification failure was detected at; set for both "psnr" and "blank" fallback reasons. */
    deFallbackFrameIndex?: number;
    /** The HF_DE_VERIFY_MIN_DB threshold the failing dB breached; only set alongside deFallbackFailedDb (psnr reason). */
    deFallbackThresholdDb?: number;
    /** Auto-parallel inversion outcome: "inverted" (fired, held) | "reverted" (fired, self-verify retry rolled back). */
    deWorkerInversion?: "inverted" | "reverted";
    /** Worker count the resolver would have used absent the inversion; undefined if it never fired. */
    dePreInversionWorkers?: number;
    /**
     * Element count for the short-comp band gate (`resolveCompositionElementCount`):
     * the LIVE DOM size from the already-running probe session when one is
     * initialized, falling back to a static scan of the compiled HTML
     * (`countElementTags`) otherwise. Live is authoritative — a static scan
     * cannot see elements a composition's own script creates at runtime.
     *
     * Emitted on every render, not just inverted ones — this is the variable the
     * short-comp inversion band is gated on, and the fleet distribution of it is
     * unknown. Without it there is no way to tell whether the 2500 ceiling opens
     * the band for most short comps or almost none, and no way to re-derive the
     * threshold from real content instead of synthetic sweeps.
     */
    compositionElementCount?: number;
    /**
     * Provenance of `compositionElementCount`: "live" (measured from the probe
     * session's real DOM — sees runtime-generated elements) or "static" (source
     * markup scan, which does not). The probe is CONDITIONAL, so this is not a
     * detail: only a `live` count may open the short band, and the fleet rate of
     * "static" sizes the population a future conditional-probe-launch would
     * unlock for the band.
     */
    compositionElementCountSource?: "live" | "static";
    /**
     * Short-comp band decision, emitted only when the band is DECISIVE — every
     * other inversion-eligibility condition passed and only the floor (250 vs
     * 900) differed. "applied": the element count cleared the ceiling too, so
     * with routing enabled (HF_DE_SHORT_BAND_ROUTE) this render inverts; in the
     * baseline release the same value is the COUNTERFACTUAL "would have
     * inverted". "skipped_elements": the element ceiling was the only blocker.
     * Unset: the band could not have affected this render (ineligible for some
     * other reason, or already inverting at 900+). The selector is computed
     * identically before and after the routing flip, and the skipped/oversize
     * renders form the concurrent control for the difference-in-differences
     * read — that is the entire point of the field.
     */
    deShortBand?: "applied" | "skipped_elements" | "unmeasured";
    /** DE parallel-router outcome: "routed" (fired, held) | "reverted" (fired, self-verify retry rolled back). */
    deParallelRouter?: "routed" | "reverted";
    /**
     * Low-cardinality GPU bucket (`<backend>/<vendor>`) from the DE probe
     * session. Lives on capture observability (not just perfSummary) so a hard
     * failure — crash / OOM / timeout — still reports which GPU backend it hit:
     * that is precisely the cohort the win32 D3D11 rollout must attribute.
     */
    deGpuRenderer?: string;
    /** Worker count the resolver would have used absent the router; undefined if it never fired. */
    dePreRouterWorkers?: number;
    /**
     * Non-DE parallel-streaming router outcome (HF_CAPTURE_PARALLEL_STREAM):
     * "screenshot" | "beginframe" — the render passed every gate AND the kill
     * switch was on, so it was routed through the interleaved streaming encoder
     * (the value is the capture mode that streamed); "eligible_off" — the render
     * passed every gate EXCEPT the kill switch (passive cohort-sizing signal for
     * the default-off soak: how many renders WOULD route if enabled). Absent =
     * ineligible regardless of the switch.
     */
    captureParallelStream?: "screenshot" | "beginframe" | "eligible_off";
    protocolTimeoutMs?: number;
    pageNavigationTimeoutMs?: number;
    playerReadyTimeoutMs?: number;
    /**
     * Render-reliability counters (see PostHog dashboard 1783183). Emitted so the
     * capture-hardening in #1842 is measurable from a metric, not just logs:
     * how often the bounded transient-tab-death retry fired on a render that
     * ultimately succeeded, and whether the failure was classified as an
     * out-of-memory exhaustion (`Set maximum size exceeded` and friends).
     */
    transientRetries?: number;
    memoryExhaustionDetected?: boolean;
}
export interface RenderExtractionObservability {
    videoCount: number;
    extractedVideoCount: number;
    totalFramesExtracted: number;
    maxFramesPerVideo: number;
    avgFramesPerExtractedVideo?: number;
    vfrProbeMs?: number;
    vfrPreflightMs?: number;
    vfrPreflightCount?: number;
    cacheHits?: number;
    cacheMisses?: number;
    /** Per-source transient download/metadata/FFmpeg retries performed during extraction. */
    transientRetries?: number;
    /**
     * Per-clip captured-vs-expected-frame gauges. Emitted by the parity gate
     * at extract finalization (see `videoFrameCoverage.ts`). Undefined when
     * the render has no source videos to cover.
     *
     * • `minVideoFrameCoverageRatio` — worst clip's `captured / expected`
     *   ratio (0 when a clip was never extracted; a strong "later-injected
     *   clip silently dropped" signal per field ts=1784139267).
     * • `coverageShortfallClipCount` — clips whose ratio fell below the
     *   configured threshold (`HF_VIDEO_COVERAGE_THRESHOLD`, default 0.95).
     *   Non-zero only ever accompanies a `VideoFrameCoverageError` throw.
     */
    minVideoFrameCoverageRatio?: number;
    coverageShortfallClipCount?: number;
    /**
     * Count of authored `[data-start]` clip windows in the compiled HTML —
     * a coarse proxy for the ts=1784144554 field signal shape (147-clip
     * composition, 130 word-level caption divs authored-clip-count-scaled
     * failure). Static scan; dynamic script-inserted timed clips land in
     * the probe-stage's `hasRuntimeInsertedMedia` path (PR #2474).
     */
    authoredTimedClipCount?: number;
}
export interface RenderInitObservability {
    initDurationMs?: number;
    tweenCount?: number;
    /**
     * Live DOM element count at end of capture-session init; undefined when
     * the measurement failed (never 0). Observational: measured after routing
     * has already been decided, so it cannot gate — it exists because the
     * routing gate's own count is only available on the ~17% of renders that
     * get a probe session, leaving the fleet element-count distribution (and
     * any large-runtime-DOM tail) unreadable for the rest.
     *
     * Not interchangeable with `RenderCaptureObservability.compositionElementCount`:
     * that one is measured pre-routing from the probe session (or a static
     * scan) and is what the band gates on. This one is measured post-routing
     * from the capture session and covers renders the gate cannot see. Query
     * the former for router behaviour, this for distribution/tail analysis.
     */
    elementCount?: number;
}
export interface RenderObservabilitySummary {
    renderJobId?: string;
    compositionHash?: string;
    events: RenderObservationEvent[];
    eventCount: number;
    lastEvent?: RenderObservationEvent;
    failedPhase?: string;
    browserDiagnostics: BrowserDiagnosticSummary;
    capture: RenderCaptureObservability;
    extraction?: RenderExtractionObservability;
    init?: RenderInitObservability;
}
export declare function sanitizeObservationMessage(value: string): string;
export declare function computeCompositionObservabilityHash(compiledHtml: string): string;
export declare function summarizeBrowserDiagnostics(lines: string[]): BrowserDiagnosticSummary;
export declare class RenderObservabilityRecorder {
    private readonly input;
    private readonly events;
    private eventCount;
    private failedPhase;
    constructor(input: {
        pipelineStartMs: number;
        log: ProducerLogger;
        renderJobId?: string;
    });
    checkpoint(phase: string, message: string, data?: RenderObservationData): RenderObservationEvent;
    stageStart(phase: string, data?: RenderObservationData): number;
    stageEnd(phase: string, startedAtMs: number, data?: RenderObservationData): void;
    stageError(phase: string, startedAtMs: number, error: unknown, data?: RenderObservationData): void;
    summary(input: {
        lastBrowserConsole: string[];
        capture: RenderCaptureObservability;
        /** Structured init telemetry from per-worker perf summaries — the only success-path channel parallel workers have (their console buffers propagate on failure only). */
        initFallback?: RenderInitObservability;
        extraction?: RenderExtractionObservability;
        compositionHash?: string;
    }): RenderObservabilitySummary;
    hasFailure(): boolean;
    /** A phase failure that was subsequently recovered (e.g. the drawElement
     * self-verify fallback re-rendering via screenshot) should not brand the
     * whole render as failed in the summary. */
    clearFailure(phase: string): void;
    private record;
}
/**
 * Options for `observeRenderStage` heartbeat behavior.
 *
 * The default heartbeat message is "stage still running", chosen for the
 * capture stages where a live frame count in the observation data already
 * communicates progress. Stages that run BEFORE any frame count is
 * meaningful — the ~64s browser calibration path in particular — inherit
 * that default and confusingly report "stage still running / framesCompleted:
 * 0" to downstream consumers. Field signal ts=1784019503 captured exactly
 * that read-as-broken shape on a healthy 64s calibration.
 *
 * `heartbeatMessage` lets the calibration call sites override the message
 * to "browser calibrating" so operator-facing logs and downstream metrics
 * can distinguish healthy pre-capture waits from actual zero-frame stalls
 * mid-capture. The `data` payload also flows through (callers pass a
 * `stagePhase: "calibrating" | "capturing"` field) so structured consumers
 * don't have to string-match on the message.
 */
export interface ObserveRenderStageOptions {
    /**
     * Message to attach to each heartbeat checkpoint for this stage.
     * Defaults to "stage still running".
     */
    heartbeatMessage?: string;
}
export declare function observeRenderStage<T>(recorder: RenderObservabilityRecorder, phase: string, data: RenderObservationData | undefined, fn: () => Promise<T>, options?: ObserveRenderStageOptions): Promise<T>;
//# sourceMappingURL=observability.d.ts.map