import { createHash } from 'node:crypto'; import { createCinemaAssetEvidence, createCinemaRightsRegistry, createCinemaVisualSoundBible, createCinemaVoiceRegistry, validateCinemaAssetEvidence, validateCinemaRightsRegistry, validateCinemaVisualSoundBible, validateCinemaVoiceRegistry, type CinemaAssetEvidenceArtifact, type CinemaRightsRegistryArtifact, type CinemaVisualSoundBibleArtifact, type CinemaVoiceRegistryArtifact, type CinemaRecognitionCategory, } from './cinema-assurance.js'; import { createCinemaContinuityLedger, createCinemaCoverage, createCinemaGeographyMap, validateCinemaContinuity, validateCinemaCoverage } from './cinema-continuity.js'; import type { CinemaCoverageArtifact, CinemaGeographyMapArtifact } from './cinema-continuity-types.js'; import { stableCinemaJson } from './cinema-shot-compiler.js'; import { compileCinemaStoryBlueprint } from './cinema-story-compiler.js'; import type { CinemaCompactIntake, CinemaStoryBlueprint } from './cinema-story-types.js'; import { validateCinemaStoryBlueprint } from './cinema-story-validation.js'; import type { CinemaSha256 } from './cinema-types.js'; export interface CinemaPlanningBlocker { code: 'character-sheet-pending' | 'asset-recognition-pending' | 'rights-pending' | 'voice-release-pending' | 'dialogue-authorship-pending' | 'shot-manifests-uncompiled'; stage: 'asset-design' | 'compliance' | 'voice' | 'sound' | 'shot-compile'; message: string; } export interface CinemaPlanningReceiptArtifact { schemaVersion: 1; projectSlug: string; planningId: string; generatedAt: string; intakeHash: CinemaSha256; artifactHashes: { creativeContract: CinemaSha256; storyBible: CinemaSha256; visualSoundBible: CinemaSha256; assetEvidence: CinemaSha256; geographyMap: CinemaSha256; coverage: CinemaSha256; rightsRegistry: CinemaSha256; voiceRegistry: CinemaSha256; }; plannedShotIds: string[]; executable: false; blockers: CinemaPlanningBlocker[]; contentHash: CinemaSha256; } export interface CinemaPlanningPackage { story: CinemaStoryBlueprint; visualSoundBible: CinemaVisualSoundBibleArtifact; assetEvidence: CinemaAssetEvidenceArtifact; geographyMap: CinemaGeographyMapArtifact; coverage: CinemaCoverageArtifact; rightsRegistry: CinemaRightsRegistryArtifact; voiceRegistry: CinemaVoiceRegistryArtifact; receipt: CinemaPlanningReceiptArtifact; } export interface CinemaPlanningValidation { ok: boolean; issues: Array<{ code: string; path: string; message: string }>; } const RECOGNITION_CATEGORIES: CinemaRecognitionCategory[] = [ 'canonical', 'profile', 'profile', 'lighting', 'lighting', 'scale', 'action', 'ensemble', 'material', 'interaction', ]; function hash(value: unknown): CinemaSha256 { return `sha256:${createHash('sha256').update(stableCinemaJson(value)).digest('hex')}`; } export function cinemaIntakeHash(intake: CinemaCompactIntake): CinemaSha256 { return hash({ schemaVersion: 1, ...structuredClone(intake) }); } function withHash(value: T): T & { contentHash: CinemaSha256 } { return { ...structuredClone(value), contentHash: hash(value) }; } export function compileCinemaPlanningPackage( intake: CinemaCompactIntake, generatedAt = new Date().toISOString(), ): CinemaPlanningPackage { const story = compileCinemaStoryBlueprint(intake, {}, generatedAt); const hero = story.storyBible.characters[0]; const projectSlug = intake.projectSlug; const visualSoundBible = createCinemaVisualSoundBible({ projectSlug, bibleId: `${projectSlug}-visual-sound-v1`, generatedAt, visualLawVersion: 'v1', visual: { palette: [`${story.creativeContract.tone} neutral base`, `${story.creativeContract.genre} restrained accent`], contrast: 'Controlled contrast with protected faces and readable shadow detail.', lighting: { sources: ['motivated practical key', 'environmental ambient fill'], direction: 'One declared scene direction preserved through coverage.', quality: 'Naturalistic, shaped and consistent across reverse angles.', }, opticsFamilies: ['28mm environment', '50mm action', '85mm reaction'], cameraGrammar: 'Establish geography, preserve the axis, then earn closer coverage at the choice.', motionCharacter: 'Measured observation before the choice; intentional stillness at consequence.', texture: 'Grounded physical detail without synthetic polish or unmotivated spectacle.', aspectRatio: story.creativeContract.format, cadenceFps: 24, }, sound: { ambience: 'Continuous location tone with explicit perspective and no unexplained discontinuity.', dialoguePerspective: story.creativeContract.dialoguePosture === 'none' ? 'No dialogue; performance and sound effects carry story.' : 'Close, intelligible and spatially consistent with picture.', musicStrategy: 'Withhold score until its narrative purpose is declared; silence remains an authored option.', forbiddenDrift: ['no automatic trailer music', 'no unexplained ambience changes', 'no dialogue without a locked released voice'], }, }); const assetEvidence = createCinemaAssetEvidence({ projectSlug, evidencePlanId: `${projectSlug}-asset-evidence-v1`, generatedAt, assets: [{ assetId: hero.characterId, versionId: `${hero.characterId}-v1`, kind: 'hero', requiredConditionCount: 10, lockStatus: 'pending', conditions: RECOGNITION_CATEGORIES.map((category, index) => ({ conditionId: `${hero.characterId}-recognition-${String(index + 1).padStart(2, '0')}`, category, description: `${hero.name} remains recognisable under the planned ${category} risk condition ${index + 1}.`, status: 'pending', reviewerId: null, reviewedAt: null, evidenceIds: [], })), }], }); const geographyMap = createCinemaGeographyMap({ projectSlug, sceneMapId: `${projectSlug}-primary-map-v1`, generatedAt, landmarks: [ { landmarkId: 'entry', x: 0, y: 0, z: 0 }, { landmarkId: 'decision-point', x: 5, y: 0, z: 0 }, { landmarkId: 'consequence-point', x: 10, y: 0, z: 0 }, ], axis: { fromLandmarkId: 'entry', toLandmarkId: 'consequence-point' }, cameraSides: [{ sideId: 'primary-side', screenDirection: 'left-to-right', lightDirection: 'camera-left to camera-right', allowedLandmarkIds: ['entry', 'decision-point', 'consequence-point'] }], movementPaths: [ { pathId: 'entry-to-choice', fromLandmarkId: 'entry', toLandmarkId: 'decision-point', direction: 'left-to-right' }, { pathId: 'choice-to-consequence', fromLandmarkId: 'decision-point', toLandmarkId: 'consequence-point', direction: 'left-to-right' }, ], }); const coverage = createCinemaCoverage({ projectSlug, coverageId: `${projectSlug}-coverage-v1`, generatedAt, requirements: [ { requirementId: 'coverage-setup-master', beatId: 'beat-setup', kind: 'master', purpose: 'Orient audience, hero, objective and geography.' }, { requirementId: 'coverage-setup-detail', beatId: 'beat-setup', kind: 'insert', purpose: 'Make the closing opportunity physically legible.' }, { requirementId: 'coverage-choice-action', beatId: 'beat-choice', kind: 'action', purpose: 'Show the irreversible action without spatial ambiguity.' }, { requirementId: 'coverage-choice-reaction', beatId: 'beat-choice', kind: 'reaction', purpose: 'Show the internal cost of the choice.' }, { requirementId: 'coverage-consequence-master', beatId: 'beat-consequence', kind: 'master', purpose: 'Pay off the setup and reveal the changed trajectory.' }, ], shotAssignments: [ { shotId: 'shot-001', requirementIds: ['coverage-setup-master'], editorialPurpose: 'Opening geography and objective.' }, { shotId: 'shot-002', requirementIds: ['coverage-setup-detail'], editorialPurpose: 'Concrete setup detail.' }, { shotId: 'shot-003', requirementIds: ['coverage-choice-action'], editorialPurpose: 'Irreversible choice in action.' }, { shotId: 'shot-004', requirementIds: ['coverage-choice-reaction'], editorialPurpose: 'Performance reaction and cost.' }, { shotId: 'shot-005', requirementIds: ['coverage-consequence-master'], editorialPurpose: 'Result, payoff and new trajectory.' }, ], reversePairs: [], }); const rightsRegistry = createCinemaRightsRegistry({ projectSlug, registryId: `${projectSlug}-rights-v1`, generatedAt, records: [{ assetId: hero.characterId, sourceType: 'generated', owner: null, permittedScope: null, status: 'pending', evidenceIds: [] }], }); const speaking = story.creativeContract.dialoguePosture !== 'none'; const voiceRegistry = createCinemaVoiceRegistry({ projectSlug, registryId: `${projectSlug}-voices-v1`, generatedAt, characters: [{ characterId: hero.characterId, speaking, voiceId: null, auditionEvidenceIds: [], releaseStatus: speaking ? 'pending' : 'not-required', releaseEvidenceIds: [], lockStatus: speaking ? 'pending' : 'not-required', }], }); const blockers: CinemaPlanningBlocker[] = [ { code: 'character-sheet-pending', stage: 'asset-design', message: `Create and review the single-face sheet for ${hero.name}; no sheet or face evidence was inferred.` }, { code: 'asset-recognition-pending', stage: 'asset-design', message: 'Review all ten hero recognition conditions with distinct evidence before lock.' }, { code: 'rights-pending', stage: 'compliance', message: 'Record ownership, permitted scope and supporting evidence before execution.' }, ...(speaking ? [{ code: 'voice-release-pending' as const, stage: 'voice' as const, message: 'Audition and release evidence are required before any speaking voice can lock.' }] : []), ...(story.creativeContract.dialoguePosture === 'dialogue-led' ? [{ code: 'dialogue-authorship-pending' as const, stage: 'sound' as const, message: 'Authored dialogue text and timing are required; Cinema will not fabricate a spoken line.' }] : []), { code: 'shot-manifests-uncompiled', stage: 'shot-compile', message: 'Shot manifests are intentionally withheld until assets, rights and voice gates resolve.' }, ]; const receiptInput = { schemaVersion: 1 as const, projectSlug, planningId: `${projectSlug}-planning-v1`, generatedAt, intakeHash: cinemaIntakeHash(intake), artifactHashes: { creativeContract: story.creativeContract.contentHash, storyBible: story.storyBible.contentHash, visualSoundBible: visualSoundBible.contentHash, assetEvidence: assetEvidence.contentHash, geographyMap: geographyMap.contentHash, coverage: coverage.contentHash, rightsRegistry: rightsRegistry.contentHash, voiceRegistry: voiceRegistry.contentHash, }, plannedShotIds: coverage.shotAssignments.map((shot) => shot.shotId), executable: false as const, blockers, }; const receipt = withHash(receiptInput); return { story, visualSoundBible, assetEvidence, geographyMap, coverage, rightsRegistry, voiceRegistry, receipt }; } export function validateCinemaPlanningPackage(value: CinemaPlanningPackage): CinemaPlanningValidation { const issues: CinemaPlanningValidation['issues'] = []; const merge = (prefix: string, validation: CinemaPlanningValidation): void => { issues.push(...validation.issues.map((issue) => ({ ...issue, path: `${prefix}.${issue.path}` }))); }; merge('story', validateCinemaStoryBlueprint(value.story)); merge('visualSoundBible', validateCinemaVisualSoundBible(value.visualSoundBible)); merge('assetEvidence', validateCinemaAssetEvidence(value.assetEvidence)); merge('rightsRegistry', validateCinemaRightsRegistry(value.rightsRegistry)); merge('voiceRegistry', validateCinemaVoiceRegistry(value.voiceRegistry)); merge('coverage', validateCinemaCoverage(value.coverage)); const geographyProbe = createCinemaContinuityLedger({ projectSlug: value.receipt.projectSlug, ledgerId: `${value.receipt.projectSlug}-planning-geography-probe`, generatedAt: value.receipt.generatedAt, records: [], }); merge('geographyMap', validateCinemaContinuity(geographyProbe, value.geographyMap)); const projectSlugs = [value.story.creativeContract.projectSlug, value.story.storyBible.projectSlug, value.visualSoundBible.projectSlug, value.assetEvidence.projectSlug, value.geographyMap.projectSlug, value.coverage.projectSlug, value.rightsRegistry.projectSlug, value.voiceRegistry.projectSlug]; if (projectSlugs.some((slug) => slug !== value.receipt.projectSlug)) issues.push({ code: 'project-mismatch', path: 'receipt.projectSlug', message: 'all planning artifacts must belong to the receipt project' }); for (const [key, expected] of Object.entries(value.receipt.artifactHashes)) { const actual = ({ creativeContract: value.story.creativeContract.contentHash, storyBible: value.story.storyBible.contentHash, visualSoundBible: value.visualSoundBible.contentHash, assetEvidence: value.assetEvidence.contentHash, geographyMap: value.geographyMap.contentHash, coverage: value.coverage.contentHash, rightsRegistry: value.rightsRegistry.contentHash, voiceRegistry: value.voiceRegistry.contentHash, } as Record)[key]; if (actual !== expected) issues.push({ code: 'receipt-link', path: `receipt.artifactHashes.${key}`, message: 'does not match the stored artifact' }); } if (value.receipt.executable !== false || value.receipt.blockers.length === 0) issues.push({ code: 'fabricated-executable-state', path: 'receipt.executable', message: 'initial planning package must remain non-executable with explicit blockers' }); const { contentHash, ...receiptInput } = value.receipt; if (contentHash !== hash(receiptInput)) issues.push({ code: 'content-hash', path: 'receipt.contentHash', message: 'does not match canonical receipt bytes' }); return { ok: issues.length === 0, issues }; }