From e66e4868ea5b549024b2c45fa84bcdc9bba01a76 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:19:45 +0200 Subject: [PATCH] feat(validation): manual override for a failed run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checks fail on renders that are genuinely fine — a plate the engine cannot reach, an expectation that does not apply to this shot — and a VALIDATION_FAILED export was a dead end, blocking delivery on a shot a human can see is correct. Override validation on the Export detail page resumes the pipeline where a pass would have handed off: preview, then READY_FOR_QC. It never skips QC and never delivers directly, so the frames are still looked at — the override waives the automated check, not the human one. The failed ValidationResult rows are left untouched rather than rewritten, so the QC queue keeps listing them as warnings and whoever QCs the shot sees what was waved through. The reason is required and recorded with the checks it covered on the export's timeline, and the validation panel keeps an "overridden" badge long after the event scrolls away. Restricted to admin/producer/supervisor: waving a failed check through is a supervisor's call. Co-Authored-By: Claude Opus 5 --- DELIVERY_WORKFLOW.md | 6 ++ RenderPipeline2.md | 1 + .../exports/[exportId]/ExportDetailClient.tsx | 85 +++++++++++++++- .../[exportId]/override-validation/route.ts | 43 ++++++++ lib/render-pipeline/transitions.ts | 5 +- lib/render-pipeline/validation.ts | 99 ++++++++++++++++++- 6 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 app/api/render/exports/[exportId]/override-validation/route.ts diff --git a/DELIVERY_WORKFLOW.md b/DELIVERY_WORKFLOW.md index d304490..e6e675d 100644 --- a/DELIVERY_WORKFLOW.md +++ b/DELIVERY_WORKFLOW.md @@ -27,6 +27,12 @@ An Export reaches `READY_FOR_DELIVERY` only by passing QC — from the AE panel or the web. That is the single gate; there is no way to package an export that has not been QC'd. +A render that failed automated validation can still get there: **Override +validation** on the Export detail page (admin/producer/supervisor, reason +required) resumes the pipeline at the preview stage, so the shot still goes +through QC like any other. The failed checks stay on record and keep appearing +as warnings in the QC queue, so whoever QCs it knows what was waved through. + Two rules worth knowing: - **Newest version per shot only.** If a shot has two exports sitting ready, the diff --git a/RenderPipeline2.md b/RenderPipeline2.md index 0e5aa04..0ce817d 100644 --- a/RenderPipeline2.md +++ b/RenderPipeline2.md @@ -1085,6 +1085,7 @@ The offset term matters — a shot may render a sub-range of a long plate (obser - Any FAIL → whole run FAIL → Export = VALIDATION_FAILED, pipeline stops, exact reasons in `ValidationResult` rows (E12 example above shows a missing-frame report). - WARN never stops the pipeline but is shown in QC and delivery UIs. +- **Manual override.** A check can fail on a render that is genuinely fine — an unreachable plate, an expectation that does not apply to this shot — so a VALIDATION_FAILED export is not a dead end. An admin/producer/supervisor can override it from the Export detail page with a **required reason**, and the pipeline resumes exactly where a pass would have handed off: preview, then READY_FOR_QC. **An override never skips QC and never delivers directly** — a human still looks at the frames. The failed `ValidationResult` rows are left untouched, so the QC queue keeps listing them as warnings, and the reason plus the checks waved through are recorded on the Export's timeline. - Checksum: xxHash64 per file, sequence digest = xxHash64 of sorted per-file digests; stored on `Export.checksum` and reused by the delivery builder for copy verification (MD5 available for the client-facing manifest per 18.1-Q13). --- diff --git a/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx b/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx index 8b67811..23cccb7 100644 --- a/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx +++ b/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx @@ -1,8 +1,9 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, RotateCcw, XCircle } from "lucide-react"; +import { ArrowLeft, RotateCcw, XCircle, ShieldAlert } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; @@ -105,6 +106,9 @@ const CHECK_STYLES: Record = { export function ExportDetailClient({ exportId }: { exportId: string }) { const { toast } = useToast(); const queryClient = useQueryClient(); + const [overrideOpen, setOverrideOpen] = useState(false); + const [overrideReason, setOverrideReason] = useState(""); + const [overriding, setOverriding] = useState(false); const { data, isLoading, error } = useQuery({ queryKey: ["pipeline-export", exportId], @@ -125,6 +129,31 @@ export function ExportDetailClient({ exportId }: { exportId: string }) { queryClient.invalidateQueries({ queryKey: ["pipeline-export", exportId] }); } + async function overrideValidation() { + setOverriding(true); + try { + const res = await fetch(`/api/render/exports/${exportId}/override-validation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason: overrideReason }), + }); + const body = await res.json().catch(() => ({})); + if (!res.ok) { + toast({ title: "Override failed", description: body.error ?? res.statusText, variant: "destructive" }); + return; + } + toast({ + title: "Validation overridden", + description: `${(body.overridden ?? []).join(", ") || "No failed checks recorded"} — continuing to preview and QC`, + }); + setOverrideOpen(false); + setOverrideReason(""); + queryClient.invalidateQueries({ queryKey: ["pipeline-export", exportId] }); + } finally { + setOverriding(false); + } + } + if (isLoading) return
Loading…
; if (error || !data) { return ( @@ -178,6 +207,11 @@ export function ExportDetailClient({ exportId }: { exportId: string }) { Retry )} + {e.status === "VALIDATION_FAILED" && !overrideOpen && ( + + )} {ACTIVE_STATUSES.includes(e.status) && (