import { VclawError } from './errors.js'; import type { FilmmakingPromptsArtifact } from './filmmaking-prompts.js'; import { lintFilmmakingPrompts } from './prompt-lint.js'; import type { VideoExecutionTask } from './types.js'; import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { anchorManifestAssetPath } from './asset-spec.js'; /** Validate the resolved request, after reference merging and prompt additions. */ export function validateFinalFilmmakingRequests( artifact: FilmmakingPromptsArtifact | null, tasks: VideoExecutionTask[], projectDir: string, ): void { const policy = artifact; if (policy?.validationProfile !== 'cinematic-v1') return; const failures: Array<{ sceneIndex: number; code: string; message: string }> = []; const finalPackets = tasks.flatMap((task) => { const packet = policy.seedancePackets.find((item) => item.sceneIndex === task.sceneIndex); if (!packet) { failures.push({ sceneIndex: task.sceneIndex, code: 'packet-missing', message: 'Regenerate cinematic prompt packets for this scene.' }); return []; } for (const reference of packet.references) { if (reference.status !== 'ready' || !reference.path) { failures.push({ sceneIndex: task.sceneIndex, code: 'reference-pending', message: `${reference.slot} has no reviewed, ready reference.` }); // The task carries the path as the render anchored it (a relative packet // path becomes the project file); the packet here is the artifact as // written, so it is anchored the same way before the two are compared. } else if (!task.referencePaths.includes(reference.path) && !task.referencePaths.includes(anchorManifestAssetPath(projectDir, reference.path))) { failures.push({ sceneIndex: task.sceneIndex, code: 'reference-dropped', message: `${reference.slot} was removed from the final request; rebuild the packet with the actual reference set.` }); } else if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(reference.path) && !existsSync(resolve(projectDir, anchorManifestAssetPath(projectDir, reference.path)))) { failures.push({ sceneIndex: task.sceneIndex, code: 'reference-file-missing', message: `${reference.slot} points to a missing local file.` }); } } if (!task.prompt.trim()) failures.push({ sceneIndex: task.sceneIndex, code: 'empty-prompt', message: 'The final prompt is empty.' }); return [{ ...packet, promptText: task.prompt, durationSeconds: task.durationSeconds ?? packet.durationSeconds }]; }); const lint = lintFilmmakingPrompts({ ...policy, seedancePackets: finalPackets, storyboardGridPrompt: null }, { register: policy.promptRegister ?? 'prose' }); for (const packet of lint.packets) { for (const issue of packet.issues.filter((item) => item.severity === 'error')) { failures.push({ sceneIndex: packet.sceneIndex, code: issue.code, message: issue.message }); } } if (failures.length) throw new VclawError('invalid_flag_value', 'Cinematic request validation failed before submission.', { failures }); }