From 53fc8dc65b1cf4954e893b1f82a7a14d328e99c4 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:52:49 +0200 Subject: [PATCH] fix(pipeline): re-exports keep a shot's approval, and carry no client-facing text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A shot is routinely re-exported for delivery after it has already been client-approved — most often an unscaled/un-punched-in pass. Every pipeline-created Version defaulted to approvalStatus PENDING_REVIEW, which is exactly what the dashboard counts, the shot page badge and the playlist read as "awaiting review" — so a re-export made an approved shot look like new unreviewed work to the whole team, with nothing actually different about its approval. approvalStatus is now inherited from the task's previous latest version rather than left at the create default. A task with no prior version (its first export) still gets PENDING_REVIEW, which is correct. Separately, every pipeline version was stamped with notes: "Pipeline render vXXX (internal only)" — and notes is the artist's message to reviewers, rendered verbatim in the client portal and the review player. Nothing enforces that a pipeline version stays unshared once created (see the gap noted in RenderPipeline2.md), so that internal-sounding string could reach a client if the shot were re-shared after a re-export. notes is left unset; what a version is and where it came from belongs on the Export and its timeline, which is where a human would actually go looking. RenderPipeline2.md §9 documents both, plus the share-flow gap that let notes reach a client in the first place: shareWithClient promotes whichever Version is isLatest with no awareness that a pipeline version is meant to stay internal permanently. Not fixed here — closing it needs a way to identify a pipeline-origin version at the share call sites, and Export.versionId is currently a one-way scalar with no inverse relation on Version. Co-Authored-By: Claude Opus 5 --- RenderPipeline2.md | 4 ++++ lib/render-pipeline/preview.ts | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/RenderPipeline2.md b/RenderPipeline2.md index 5d18ffa..70e87de 100644 --- a/RenderPipeline2.md +++ b/RenderPipeline2.md @@ -1132,6 +1132,10 @@ Cost note: the AE preview pass roughly doubles machine time per export. This mat Finalize (E13) then creates the `Version` row server-side (Section 6.4) in pipeline mode — visible internally, never client-shared, no task/shot status changes. +**Re-exports inherit approval, and carry no artist-facing text.** A shot is routinely re-exported after client approval — most often for an unscaled/un-punched-in delivery pass — and that must not read as new work awaiting review. The new `Version.approvalStatus` is copied from the task's previous latest version rather than left at its `PENDING_REVIEW` default; an export that starts a task's history has nothing to inherit and gets `PENDING_REVIEW` as before. `Version.notes` is left unset rather than stamped with an internal description: `notes` is the artist's message to reviewers and is rendered verbatim in the client portal and the review player, so pipeline provenance belongs on the Export and its `ExportEvent` timeline — where a human would go looking for it — not in a field a client can read. + +**Known gap:** `shareWithClient` (and the per-task share action) promote whichever `Version` is `isLatest` for a task to `isClientVisible: true`, with no awareness that a pipeline-created version is meant to stay internal permanently (this section, "never client-shared"). Re-sharing a shot after a pipeline re-export will currently expose the internal version if it is the task's latest. Closing this needs a way to identify a pipeline-origin version at the share call sites — `Export.versionId` is presently a one-way scalar with no inverse relation on `Version` — and is not yet implemented. + ### 9.3a Where a preview sits in the queue The queue is ordered `priority ASC, createdAt ASC`, and a preview job is only created when its render *finishes* — so at equal priority it sorts behind every render queued alongside it. Ten shots queued together would run as ten renders and then ten previews, with nothing reviewable until the end. diff --git a/lib/render-pipeline/preview.ts b/lib/render-pipeline/preview.ts index ded5eed..471764c 100644 --- a/lib/render-pipeline/preview.ts +++ b/lib/render-pipeline/preview.ts @@ -319,6 +319,7 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi let versionId: string | null = null; let versionReplaced = false; + let inheritedApproval: string | null = null; if (artifacts.previewMovKey) { const mediaFields = { fileUrl: `/api/files/${artifacts.previewMovKey}`, @@ -350,6 +351,20 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi // Version numbering stays monotonic for the task (existing review flows // rely on the newest version having the highest number) while matching // the export version whenever it is ahead. + // A re-export does not change the picture — it re-renders EXRs for + // delivery from a shot that has usually already been approved. A fresh + // Version defaults to PENDING_REVIEW, which is what the dashboard, the + // shot page and the client portal all read, so an approved shot would + // silently reappear as awaiting review. The approval carries over from + // the version this one supersedes. + const previousLatest = exportRow.taskId + ? await tx.version.findFirst({ + where: { taskId: exportRow.taskId }, + orderBy: { versionNumber: "desc" }, + select: { approvalStatus: true, versionNumber: true }, + }) + : null; + const taskMax = exportRow.taskId ? await tx.version.aggregate({ where: { taskId: exportRow.taskId }, @@ -365,12 +380,17 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi shotId: exportRow.shotId, taskId: exportRow.taskId, artistId: exportRow.submittedById, - notes: `Pipeline render ${exportRow.versionString} (internal only)`, + // `notes` is the artist's message to reviewers and is shown in the + // client portal, so the pipeline stays out of it. What this version + // is and where it came from lives on the Export and its timeline, + // which is where someone would go looking anyway. + approvalStatus: previousLatest?.approvalStatus ?? "PENDING_REVIEW", isLatest: true, isClientVisible: false, // never shared to the client portal (§10.0) }, }); versionId = version.id; + inheritedApproval = previousLatest?.approvalStatus ?? null; } if (exportRow.taskId) { @@ -416,7 +436,7 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi (versionId ? versionReplaced ? " — review version media replaced" - : " — review version created" + : ` — review version created (${inheritedApproval ?? "PENDING_REVIEW"})` : " — no preview media reported") + (taskRenamed ? ` — task renamed to ${taskTitle}` : ""), },