import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { isClipQcReport, type ClipQcReport } from './clip-qc.js'; import type { CinemaConsoleModel } from './cinema-console-types.js'; import { readCinemaContinuityLedger, readCinemaTimedAudioContract } from './cinema-production-contract-store.js'; import { listCinemaCandidateReviews } from './cinema-candidate-review.js'; import { readCinemaDeliveryManifest } from './cinema-delivery.js'; import { readCinemaEvidenceLedger } from './cinema-evidence-store.js'; import { readCinemaPlanningPackage } from './cinema-planning-store.js'; import { createCinemaPreflightReport } from './cinema-preflight.js'; import { buildCinemaProjectConsole } from './cinema-project-console.js'; import { readCinemaProductionQueue } from './cinema-production-queue.js'; import { readCinemaReviewLedger } from './cinema-review-store.js'; import { readCinemaCharacterSheet, readCinemaGenerationLedger, readCinemaReferencePack, readCinemaShotManifest } from './cinema-store.js'; import type { CinemaCharacterSheetArtifact, CinemaReferencePackArtifact, CinemaShotManifestArtifact } from './cinema-types.js'; import { resolveProjectWorkspace } from './workspace.js'; async function readOptionalClipQc(root: string, projectSlug: string): Promise { const path = join(resolveProjectWorkspace(projectSlug, root).artifactsDir, 'clip-qc.json'); try { const parsed: unknown = JSON.parse(await readFile(path, 'utf8')); if (!isClipQcReport(parsed)) throw new Error('artifact shape does not match ClipQcReport'); if (parsed.projectSlug !== projectSlug) throw new Error(`artifact belongs to ${parsed.projectSlug}`); return parsed; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw new Error(`Clip QC report is not readable valid JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`); } } /** Load the canonical persisted project once for static exports and the live console. */ export async function loadCinemaProjectConsole( root: string, projectSlug: string, mode: CinemaConsoleModel['mode'] = 'static-read-only', ): Promise { const planning = await readCinemaPlanningPackage(root, projectSlug); if (!planning) throw new Error(`No Cinema planning package exists for project ${projectSlug}`); const evidenceLedger = await readCinemaEvidenceLedger(root, projectSlug); const characterAssets = planning.assetEvidence.assets.filter((asset) => asset.kind === 'hero' || asset.kind === 'supporting'); const characterSheets = (await Promise.all(characterAssets.map((asset) => readCinemaCharacterSheet(root, projectSlug, asset.versionId)))) .filter((sheet): sheet is CinemaCharacterSheetArtifact => sheet !== null); const shotManifests = (await Promise.all(planning.receipt.plannedShotIds.map((shotId) => readCinemaShotManifest(root, projectSlug, shotId)))) .filter((manifest): manifest is CinemaShotManifestArtifact => manifest !== null); const referencePacks = (await Promise.all(shotManifests.map((manifest) => readCinemaReferencePack(root, projectSlug, manifest.referencePackId)))) .filter((pack): pack is CinemaReferencePackArtifact => pack !== null); const complete = shotManifests.length === planning.receipt.plannedShotIds.length; const continuityLedger = complete ? await readCinemaContinuityLedger(root, projectSlug, planning.geographyMap, shotManifests) : null; const timedAudioContract = complete ? await readCinemaTimedAudioContract(root, projectSlug, shotManifests) : null; const preflight = createCinemaPreflightReport({ planning, evidenceLedger, characterSheets, generatedAt: evidenceLedger?.generatedAt ?? planning.receipt.generatedAt, }); return buildCinemaProjectConsole({ root, planning, evidenceLedger, characterSheets, preflight, referencePacks, shotManifests, continuityLedger, timedAudioContract, queue: await readCinemaProductionQueue(root, projectSlug), generationLedger: await readCinemaGenerationLedger(root, projectSlug), reviewLedger: await readCinemaReviewLedger(root, projectSlug), candidateReviews: await listCinemaCandidateReviews(root, projectSlug), deliveryManifest: await readCinemaDeliveryManifest(root, projectSlug), clipQcReport: await readOptionalClipQc(root, projectSlug), mode, }); }