import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; import { stat } from 'node:fs/promises'; import { resolve } from 'node:path'; import { VclawError } from './errors.js'; interface ReferencePacket { sceneIndex: number; references: Array<{ path?: string }> } export interface FilmReferenceEvidence { sceneIndex: number; path: string; status: 'hashed' | 'missing' | 'remote-unverified'; sha256?: string } /** Only local reference bytes are measured. Remote handles retain explicit unverified status. */ export async function captureFilmReferenceEvidence(projectDir: string, packets: ReferencePacket[]): Promise { const evidence: FilmReferenceEvidence[] = []; for (const packet of packets) for (const ref of packet.references) { if (!ref.path) continue; const base = { sceneIndex: packet.sceneIndex, path: ref.path }; if (/^[a-z][a-z0-9+.-]*:\/\//i.test(ref.path)) { evidence.push({ ...base, status: 'remote-unverified' }); continue; } const path = resolve(projectDir, ref.path); try { if (!(await stat(path)).isFile()) throw new Error(`Reference is not a regular file: ${ref.path}`); const hash = createHash('sha256'); for await (const chunk of createReadStream(path)) hash.update(chunk); evidence.push({ ...base, status: 'hashed', sha256: hash.digest('hex') }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; evidence.push({ ...base, status: 'missing' }); } } return evidence; } export async function assertFilmReferenceEvidence(projectDir: string, packets: ReferencePacket[], previous: FilmReferenceEvidence[] | undefined): Promise { const current = await captureFilmReferenceEvidence(projectDir, packets); if (!previous) throw new VclawError('invalid_flag_value', 'Reference evidence is missing: rebuild filmmaking-prompts before generation.', { code: 'film-reference-evidence-missing' }); const changed = current.filter((entry, index) => JSON.stringify(entry) !== JSON.stringify(previous[index])); const removed = previous.slice(current.length); if (changed.length || removed.length) { const scenes = [...new Set([...changed, ...removed].map(entry => entry.sceneIndex))]; throw new VclawError('invalid_flag_value', `Reference bytes or attachments changed for scenes ${scenes.join(', ')}: inspect references and rebuild filmmaking-prompts.`, { code: 'film-reference-stale', scenes }); } }