import type { CinemaProductionQueueArtifact } from './cinema-queue-types.js'; import type { CinemaWalkingSkeletonBundle } from './cinema-types.js'; import type { CinemaConsoleModel, CinemaConsoleQcFinding } from './cinema-console-types.js'; import type { CinemaReviewLedgerArtifact } from './cinema-review-types.js'; import type { CinemaStoryBlueprint } from './cinema-story-types.js'; import type { CinemaCoverageArtifact } from './cinema-continuity-types.js'; import type { CinemaScopeReportArtifact } from './cinema-scope-report.js'; export function buildCinemaConsoleProjection(input: { bundle: CinemaWalkingSkeletonBundle; queue: CinemaProductionQueueArtifact; generatedAt?: string; mode?: CinemaConsoleModel['mode']; title?: string; candidateMedia?: Record; reviewLedger?: CinemaReviewLedgerArtifact; storyBlueprint?: CinemaStoryBlueprint; coverage?: CinemaCoverageArtifact; scopeReport?: CinemaScopeReportArtifact; }): CinemaConsoleModel { const { bundle, queue } = input; if (bundle.ledger.projectSlug !== queue.projectSlug) throw new Error('Cinema console bundle and queue belong to different projects'); if (input.reviewLedger && input.reviewLedger.projectSlug !== queue.projectSlug) throw new Error('Cinema console review ledger belongs to a different project'); if (input.storyBlueprint && input.storyBlueprint.storyBible.projectSlug !== queue.projectSlug) throw new Error('Cinema console story blueprint belongs to a different project'); if (input.coverage && input.coverage.projectSlug !== queue.projectSlug) throw new Error('Cinema console coverage belongs to a different project'); if (input.scopeReport && input.scopeReport.projectSlug !== queue.projectSlug) throw new Error('Cinema console scope report belongs to a different project'); const packs = new Map(bundle.referencePacks.map((pack) => [pack.packId, pack])); const supersededPromotions = new Set(bundle.ledger.promotions.map((promotion) => promotion.supersedesPromotionId).filter((id): id is string => id !== null)); const promotions = new Map(bundle.ledger.promotions.filter((promotion) => !supersededPromotions.has(promotion.promotionId)).map((promotion) => [promotion.attemptId, promotion])); const outcomes = new Map((bundle.ledger.outcomes ?? []).map((outcome) => [outcome.attemptId, outcome])); const assets = bundle.characterSheets.flatMap((sheet) => [ { assetId: sheet.identityAnchor.assetId, label: `${sheet.characterName} · identity portrait`, role: 'identity_face', path: sheet.identityAnchor.path, contentHash: sheet.identityAnchor.contentHash, faceCount: 1, faceFragmentCount: 0, maskArtifactId: null, lockStatus: 'locked' as const, mediaExists: false, }, ...sheet.bodyPlates.map((plate) => ({ assetId: plate.assetId, label: `${sheet.characterName} · ${plate.view} body plate`, role: `body_${plate.view}_headless`, path: plate.path, contentHash: plate.contentHash, faceCount: 0, faceFragmentCount: plate.faceFragmentCount, maskArtifactId: plate.maskArtifactId, lockStatus: 'headless' as const, mediaExists: false, })), { assetId: sheet.compositeSheet.assetId, label: `${sheet.characterName} · approved single-face sheet`, role: 'character_sheet', path: sheet.compositeSheet.path, contentHash: sheet.compositeSheet.contentHash, faceCount: sheet.compositeSheet.identityBearingFaceCount, faceFragmentCount: 0, maskArtifactId: null, lockStatus: 'locked' as const, mediaExists: false, }, ]); const shots = [...bundle.shotManifests].sort((left, right) => left.ordinal - right.ordinal).map((shot) => { const pack = packs.get(shot.referencePackId); return { shotId: shot.shotId, ordinal: shot.ordinal, purpose: shot.story.scenePurpose, expectedChange: shot.story.expectedChange, durationSeconds: shot.durationSeconds, camera: `${shot.camera.shotSize} · ${shot.camera.position} · ${shot.camera.movement} · ${shot.camera.stability}`, action: shot.actionBeats.map((beat) => `${beat.startSeconds.toFixed(1)}–${beat.endSeconds.toFixed(1)}s ${beat.description}`).join(' '), prompt: shot.compiledPrompt.text, promptHash: shot.compiledPrompt.contentHash, referenceCount: pack?.references.length ?? 0, continuity: pack?.references.filter((reference) => reference.state).map((reference) => `${reference.subjectId}: ${reference.state?.group}=${reference.state?.value}`) ?? [], videoPath: null, posterPath: pack?.references.find((reference) => reference.role === 'environment')?.path ?? null, mediaExists: false, }; }); const shotById = new Map(bundle.shotManifests.map((shot) => [shot.shotId, shot])); const candidates = bundle.ledger.attempts.map((attempt) => { const promotion = promotions.get(attempt.attemptId); const outcome = outcomes.get(attempt.attemptId); const shot = shotById.get(attempt.shotId); if (!shot) throw new Error(`Cinema console attempt ${attempt.attemptId} references missing shot ${attempt.shotId}`); const pack = packs.get(shot.referencePackId); if (!pack) throw new Error(`Cinema console shot ${shot.shotId} references missing pack ${shot.referencePackId}`); const candidateMedia = input.candidateMedia?.[attempt.attemptId]; return { attemptId: attempt.attemptId, queueTaskId: outcome?.queueTaskId ?? null, shotId: attempt.shotId, routeId: attempt.routeId, status: outcome?.status ?? attempt.status, parentAttemptId: attempt.parentAttemptId, changedVariables: [...attempt.changedVariables], evidenceIds: [...new Set([...attempt.evidenceIds, ...(outcome?.evidenceIds ?? []), ...(outcome ? [outcome.contentHash] : [])])], promoted: Boolean(promotion), editorialSlot: promotion?.editorialSlot ?? null, actualCost: outcome?.actualCost.amount ?? attempt.actualCost.amount, currency: outcome?.actualCost.currency ?? attempt.actualCost.currency, prompt: shot.compiledPrompt.text, promptHash: shot.compiledPrompt.contentHash, providerParameters: structuredClone(shot.providerParameters.values), providerParametersHash: shot.providerParameters.contentHash, referenceAssetIds: pack.references.map((reference) => reference.assetId), videoPath: candidateMedia?.videoPath ?? null, mediaExists: candidateMedia?.mediaExists ?? false, filmstripPath: candidateMedia?.filmstripPath ?? null, filmstripExists: candidateMedia?.filmstripExists ?? false, }; }); const qc: CinemaConsoleQcFinding[] = [ ...bundle.characterSheets.map((sheet) => ({ id: `qc-${sheet.characterVersionId}-identity`, shotId: 'asset-room', lane: 'identity' as const, severity: sheet.qc.status === 'pass' ? 'pass' as const : 'blocker' as const, timeSeconds: null, summary: `${sheet.characterName}: one locked portrait, two zero-face body plates, deterministic composite`, evidenceIds: [...sheet.qc.humanReview.evidenceIds], })), ...shots.map((shot) => ({ id: `qc-${shot.shotId}-editorial`, shotId: shot.shotId, lane: 'editorial' as const, severity: 'pass' as const, timeSeconds: 0, summary: `${shot.purpose}; expected change: ${shot.expectedChange}`, evidenceIds: [`evidence-${shot.shotId}-story-purpose`], })), ...shots.flatMap((shot): CinemaConsoleQcFinding[] => [ { id: `qc-${shot.shotId}-technical`, shotId: shot.shotId, lane: 'technical', severity: 'info', timeSeconds: null, summary: 'Not evaluated: no provider media is attached to this zero-spend proof.', evidenceIds: [], }, { id: `qc-${shot.shotId}-continuity`, shotId: shot.shotId, lane: 'continuity', severity: 'pass', timeSeconds: 0, summary: `Reference-pack continuity contract carries ${shot.continuity.length} explicit state item(s).`, evidenceIds: [shot.promptHash], }, { id: `qc-${shot.shotId}-performance`, shotId: shot.shotId, lane: 'performance', severity: 'info', timeSeconds: null, summary: 'Not evaluated: performance review begins only after a clip candidate exists.', evidenceIds: [], }, ]), ]; const estimatedCost = queue.tasks.reduce((sum, task) => sum + task.estimatedCost.amount, 0); const actualCost = queue.tasks.reduce((sum, task) => sum + task.actualCost.amount, 0); const allSucceeded = queue.tasks.length > 0 && queue.tasks.every((task) => task.status === 'succeeded'); const assignedCoverage = new Map(); for (const assignment of input.coverage?.shotAssignments ?? []) for (const requirementId of assignment.requirementIds) { assignedCoverage.set(requirementId, [...(assignedCoverage.get(requirementId) ?? []), assignment.shotId]); } const storyCoverage = input.storyBlueprint?.storyBible.beats.map((beat) => { const requirementIds = input.coverage?.requirements.filter((requirement) => requirement.beatId === beat.beatId).map((requirement) => requirement.requirementId) ?? []; const shotIds = [...new Set(requirementIds.flatMap((requirementId) => assignedCoverage.get(requirementId) ?? []))]; return { beatId: beat.beatId, act: beat.act, purpose: beat.purpose, change: beat.change, setupIds: [...beat.setupIds], payoffIds: [...beat.payoffIds], sceneIds: input.storyBlueprint?.storyBible.scenes.filter((scene) => scene.beatIds.includes(beat.beatId)).map((scene) => scene.sceneId) ?? [], shotIds, covered: requirementIds.length > 0 && requirementIds.every((requirementId) => (assignedCoverage.get(requirementId)?.length ?? 0) > 0), }; }) ?? []; const missingCoverage = input.coverage?.requirements.filter((requirement) => !assignedCoverage.has(requirement.requirementId)).map((requirement) => requirement.requirementId) ?? []; return { schemaVersion: 1, projectSlug: queue.projectSlug, title: input.title ?? 'Mira / Service Tunnel — three-shot proof', generatedAt: input.generatedAt ?? queue.generatedAt, mode: input.mode ?? 'static-read-only', scope: 'three-shot-proof', summary: { readiness: allSucceeded ? 'awaiting-approval' : 'blocked', blockers: allSucceeded ? ['Paid or live generation requires a separate exact quote approval.'] : ['Queue work remains incomplete.'], assumptions: ['Original synthetic fixture', 'No provider media exists', 'All costs shown are zero-spend dry-run evidence'], estimatedCost, actualCost, currency: queue.tasks[0]?.estimatedCost.currency ?? 'USD', }, gates: [ { id: 'asset_lock', label: 'Asset lock', status: 'locked', evidenceIds: bundle.characterSheets.flatMap((sheet) => sheet.qc.humanReview.evidenceIds) }, { id: 'shotlist_lock', label: 'Shot list lock', status: 'locked', evidenceIds: shots.map((shot) => shot.promptHash) }, { id: 'generation_batch', label: 'Generation batch', status: 'external-approval', evidenceIds: [] }, { id: 'picture_lock', label: 'Picture lock', status: 'blocked', evidenceIds: [] }, ], queue: queue.tasks.map((task) => ({ taskId: task.taskId, kind: task.kind, status: task.status, routeId: task.routeId, dependencies: [...task.dependencies], shotId: task.shotId ?? null, providerJobId: task.execution?.providerJobId ?? null, providerStatus: task.execution?.providerStatus ?? null, reconciliationRequired: task.execution?.reconciliation.required ?? false, reconciliationAttempts: task.execution?.reconciliation.attempts ?? 0, laneId: task.lane?.laneId ?? null, lanePosition: task.lane?.position ?? null, laneEtaSeconds: task.lane?.etaSeconds ?? null, leaseWorkerId: task.lease?.workerId ?? null, leaseExpiresAt: task.lease?.expiresAt ?? null, failure: task.failure ? { ...task.failure } : null, authorizationRequirement: task.authorizationRequirement, spendAuthorizationId: task.spendAuthorizationId, estimatedCost: task.estimatedCost.amount, actualCost: task.actualCost.amount, currency: task.estimatedCost.currency, })), assets, shots, candidates, qc, reviews: input.reviewLedger?.decisions.map((decision) => ({ decisionId: decision.decisionId, attemptId: decision.attemptId, shotId: decision.shotId, kind: decision.kind, reviewerId: decision.reviewerId, decidedAt: decision.decidedAt, reason: decision.reason, timeSeconds: decision.timeSeconds, changedVariable: decision.changedVariable, evidenceIds: [...decision.evidenceIds], contentHash: decision.contentHash, })) ?? [], evidence: null, qualification: input.scopeReport ? { currentScope: input.scopeReport.currentScope, proposedScope: input.scopeReport.proposedScope, verdict: input.scopeReport.verdict, passedChecks: [...input.scopeReport.passedChecks], blockers: [...input.scopeReport.blockers], evidenceIds: [...input.scopeReport.evidenceIds], contentHash: input.scopeReport.contentHash, } : null, edit: { shotOrder: shots.map((shot) => shot.shotId), totalDurationSeconds: shots.reduce((sum, shot) => sum + shot.durationSeconds, 0), missingCoverage, audioLayers: ['ambience', 'SFX where required', 'intentional no dialogue', 'intentional no music'], roughCutPath: null, storyCoverage, }, delivery: { status: 'not-started', masterPath: null, captionsPath: null, archiveHash: null, notes: ['Static review package only', 'No video master exists', 'Provider execution remains approval-gated'], }, }; }