feat(validation): manual override for a failed run
Deploy / deploy (push) Successful in 6m13s

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:
twotalesanimation
2026-08-14 11:19:45 +02:00
parent af810258e6
commit e66e4868ea
6 changed files with 236 additions and 3 deletions
+98 -1
View File
@@ -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;