Repository Public
1import { sql } from "../lib/db";2import { getPolicy } from "../lib/budget";3import { enqueueStage } from "../lib/queue";4import { log } from "../lib/log";5import { PAID_MEDIA, type ProjectRow, type Stage, getProject } from "./stages";6import { publishProject, recordApproval } from "./publish";78/** Which stage follows a given project status. */9export const NEXT: Record<string, Stage | undefined> = {10 draft: "concept", pitched: "script", scripted: "storyboard", storyboarded: "keyframes", keyframes: "voice", voiced: "video", filmed: "assemble", assembled: "qa",11};12/** Status a project must have reached before a stage may run. */13export const REQUIRES: Record<Stage, string[]> = {14 concept: ["draft", "pitched", "scripted", "storyboarded"],15 script: ["pitched", "scripted", "storyboarded", "keyframes", "voiced"],16 storyboard: ["scripted", "storyboarded", "keyframes", "voiced"],17 keyframes: ["storyboarded", "keyframes", "voiced", "filmed", "assembled", "qa_failed", "qa_passed"],18 voice: ["keyframes", "voiced", "filmed", "assembled", "qa_failed", "qa_passed"],19 video: ["voiced", "filmed", "assembled", "qa_failed", "qa_passed"],20 assemble: ["filmed", "assembled", "qa_failed", "qa_passed"],21 qa: ["assembled", "qa_failed", "qa_passed"],22};2324/** Pure policy, unit-tested: what runs automatically after a stage, per autonomy mode. */25export function autoNext(mode: ProjectRow["mode"], status: string): Stage | null {26 const next = NEXT[status];27 if (!next || mode === "manual") return null;28 if (mode === "assisted" && PAID_MEDIA.includes(next)) return null; // human starts the expensive stage29 return next;30}3132export async function afterStage(projectId: string) {33 const project = await getProject(projectId);34 const policy = await getPolicy();35 if (policy.kill_switch) return;36 const next = autoNext(project.mode, project.status);37 if (next) { await enqueueStage(projectId, next); return; }38 if (project.status === "qa_passed" && project.mode === "autonomous" && policy.auto_publish) {39 await recordApproval({ projectId, userId: null, decision: "auto", note: "Automatic approval: QA passed and auto-publish is enabled in the policy" });40 await publishProject(projectId, null);41 log("info", "publish.auto", { projectId });42 }43}4445export async function startStage(projectId: string, stage: Stage) {46 const project = await getProject(projectId);47 if (!REQUIRES[stage].includes(project.status)) throw new Error(`Stage "${stage}" cannot run while the project is "${project.status}"`);48 return enqueueStage(projectId, stage);49}5051/** Recover projects whose lock outlived its run (worker killed between stages). */52export async function releaseStaleLocks() {53 await sql`UPDATE stage_runs SET status = 'failed', error = 'Worker stopped while this stage was running', finished_at = now()54 WHERE status = 'running' AND started_at < now() - interval '45 minutes'`;55 await sql`UPDATE projects p SET running_stage = NULL WHERE running_stage IS NOT NULL56 AND NOT EXISTS (SELECT 1 FROM stage_runs r WHERE r.project_id = p.id AND r.status IN ('queued','running'))`;57}58