import type { CinemaSha256 } from './cinema-types.js'; export interface CinemaArtifactDependencyNode { artifactId: string; artifactType: 'identity-portrait' | 'character-sheet' | 'reference-pack' | 'shot-manifest' | 'provider-payload'; contentHash: CinemaSha256; characterVersionId: string; dependencies: Array<{ artifactId: string; contentHash: CinemaSha256 }>; } export interface CinemaInvalidationRecord { artifactId: string; artifactType: CinemaArtifactDependencyNode['artifactType']; reason: string; invalidatedByArtifactId: string; } export function planCinemaIdentityReplacement(input: { priorIdentityArtifactId: string; priorIdentityHash: CinemaSha256; nextIdentityArtifactId: string; nextIdentityHash: CinemaSha256; priorCharacterVersionId: string; nextCharacterVersionId: string; graph: CinemaArtifactDependencyNode[]; }): { nextCharacterVersionId: string; invalidations: CinemaInvalidationRecord[] } { if (input.priorIdentityArtifactId === input.nextIdentityArtifactId) throw new Error('Identity replacement requires a new immutable portrait artifact ID'); if (input.priorIdentityHash === input.nextIdentityHash) throw new Error('Identity replacement requires changed portrait bytes'); if (input.priorCharacterVersionId === input.nextCharacterVersionId) throw new Error('Identity replacement requires a new character version'); const invalidated = new Map(); let frontier = new Set([input.priorIdentityArtifactId]); while (frontier.size > 0) { const next = new Set(); for (const node of input.graph) { if (invalidated.has(node.artifactId)) continue; const dependency = node.dependencies.find((entry) => frontier.has(entry.artifactId)); if (!dependency) continue; if (dependency.artifactId === input.priorIdentityArtifactId && dependency.contentHash !== input.priorIdentityHash) continue; invalidated.set(node.artifactId, { artifactId: node.artifactId, artifactType: node.artifactType, reason: `Dependency ${dependency.artifactId}@${dependency.contentHash} was superseded by ${input.nextIdentityArtifactId}@${input.nextIdentityHash}`, invalidatedByArtifactId: dependency.artifactId, }); next.add(node.artifactId); } frontier = next; } return { nextCharacterVersionId: input.nextCharacterVersionId, invalidations: [...invalidated.values()] }; }