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 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,10 @@ export const EXPORT_TRANSITIONS: Record<ExportStatus, ExportStatus[]> = {
|
||||
// QUEUED/RENDER_FAILED: the worker died *during* validation, so the render is
|
||||
// retried — nothing was learned about the EXRs either way (reaper).
|
||||
VALIDATING: ["VALIDATION_FAILED", "GENERATING_PREVIEW", "READY_FOR_QC", "QUEUED", "RENDER_FAILED", "CANCELLED"],
|
||||
VALIDATION_FAILED: ["QUEUED", "SUPERSEDED"],
|
||||
// GENERATING_PREVIEW / READY_FOR_QC: a human overruled the checks (§8.2).
|
||||
// The override resumes where a pass would have handed off — it never skips
|
||||
// QC, so the frames are still looked at before anything ships.
|
||||
VALIDATION_FAILED: ["QUEUED", "SUPERSEDED", "GENERATING_PREVIEW", "READY_FOR_QC"],
|
||||
GENERATING_PREVIEW: ["PREVIEW_FAILED", "READY_FOR_QC"],
|
||||
PREVIEW_FAILED: ["GENERATING_PREVIEW", "SUPERSEDED"],
|
||||
// GENERATING_PREVIEW: a slate/burn-in mistake is caught *after* a successful
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { PipelineError } from "./errors";
|
||||
import { applyTransition } from "./transitions";
|
||||
import { getConfigBoolean, getConfigValue, getPreviewConfig } from "./config";
|
||||
import { getConfigBoolean, getConfigNumber, getConfigValue, getPreviewConfig } from "./config";
|
||||
import { buildPreviewManifest, createPreviewJob } from "./preview";
|
||||
|
||||
/**
|
||||
@@ -214,6 +214,103 @@ export async function isValidationEnabled(): Promise<boolean> {
|
||||
return getConfigBoolean("validation.enabled");
|
||||
}
|
||||
|
||||
export const overrideValidationSchema = z.object({
|
||||
/**
|
||||
* Required, and deliberately so: an override is a human overruling a check
|
||||
* that exists to stop bad frames shipping, and "why" is the only part of
|
||||
* that decision the next person cannot reconstruct.
|
||||
*/
|
||||
reason: z.string().trim().min(3, "A reason is required").max(2000),
|
||||
});
|
||||
|
||||
/**
|
||||
* Manual override of a failed validation run.
|
||||
*
|
||||
* Some checks fail on renders that are genuinely fine — a plate the engine
|
||||
* cannot reach, an expectation that does not apply to this shot. Rather than
|
||||
* leaving those exports stranded, an override resumes the pipeline *at the
|
||||
* point validation would have handed off*: the preview is built and the export
|
||||
* lands in READY_FOR_QC. It never skips QC, so a human still looks at the
|
||||
* frames before anything can be delivered.
|
||||
*
|
||||
* The failed ValidationResult rows are left exactly as they are — history is
|
||||
* not rewritten, and the QC queue keeps surfacing them as warnings so whoever
|
||||
* QCs the shot knows what was waved through.
|
||||
*/
|
||||
export async function overrideValidation(
|
||||
exportId: string,
|
||||
input: { reason: string; userId: string; userName?: string | null }
|
||||
) {
|
||||
const previewEnabled = (await getPreviewConfig()).enabled;
|
||||
const maxAttempts = await getConfigNumber("render.maxAttempts");
|
||||
|
||||
return db.$transaction(async (tx) => {
|
||||
const exportRow = await tx.export.findUnique({ where: { id: exportId } });
|
||||
if (!exportRow) throw new PipelineError(404, "Export not found");
|
||||
if (exportRow.status !== "VALIDATION_FAILED") {
|
||||
throw new PipelineError(
|
||||
409,
|
||||
`Export is ${exportRow.status} — only a VALIDATION_FAILED export can be overridden`
|
||||
);
|
||||
}
|
||||
|
||||
const failed = await tx.validationResult.findMany({
|
||||
where: { exportId, status: "FAIL" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { checkName: true, renderJobId: true },
|
||||
});
|
||||
// Only the most recent run's failures are what is being waved through.
|
||||
const latestRun = failed[0]?.renderJobId;
|
||||
const waved = [...new Set(failed.filter((f) => f.renderJobId === latestRun).map((f) => f.checkName))];
|
||||
|
||||
const who = input.userName ?? "unknown user";
|
||||
const note =
|
||||
`Validation overridden by ${who}: ${input.reason.trim()}` +
|
||||
(waved.length ? ` — waved: ${waved.join(", ")}` : "");
|
||||
|
||||
if (!previewEnabled) {
|
||||
const updated = await applyTransition(tx, exportRow, "READY_FOR_QC", {
|
||||
type: "USER",
|
||||
id: input.userId,
|
||||
note: `${note} (preview stage disabled)`,
|
||||
});
|
||||
return {
|
||||
export: { id: updated.id, status: updated.status },
|
||||
overridden: waved,
|
||||
previewJob: null,
|
||||
};
|
||||
}
|
||||
|
||||
const manifest = await buildPreviewManifest(exportRow);
|
||||
const last = await tx.renderJob.findFirst({
|
||||
where: { exportId, type: "PREVIEW_ONLY" },
|
||||
orderBy: { attempt: "desc" },
|
||||
});
|
||||
const previewJob = await tx.renderJob.create({
|
||||
data: {
|
||||
type: "PREVIEW_ONLY",
|
||||
exportId,
|
||||
attempt: (last?.attempt ?? 0) + 1,
|
||||
maxAttempts,
|
||||
priority: last?.priority ?? 50,
|
||||
manifest: manifest as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
const updated = await applyTransition(tx, exportRow, "GENERATING_PREVIEW", {
|
||||
type: "USER",
|
||||
id: input.userId,
|
||||
note: `${note} — queued preview build`,
|
||||
});
|
||||
|
||||
return {
|
||||
export: { id: updated.id, status: updated.status },
|
||||
overridden: waved,
|
||||
previewJob: { id: previewJob.id, attempt: previewJob.attempt },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export interface ValidationBrowserFilter {
|
||||
projectId?: string;
|
||||
episode?: string;
|
||||
|
||||
Reference in New Issue
Block a user