/** Explicit performance canon; absent traits never produce invented behaviour. */ export interface CharacterPerformance { voice?: string; movement?: string; stillness?: string; } export interface FilmmakingCharacterContext extends CharacterPerformance { slot?: string; description: string; } export function performanceTraits(value: unknown): CharacterPerformance { if (!value || typeof value !== 'object') return {}; const record = value as Record; return Object.fromEntries(['voice', 'movement', 'stillness'].flatMap(key => typeof record[key] === 'string' && record[key].trim() ? [[key, record[key].trim()]] : [])); } export function performanceDirection(name: string, traits: CharacterPerformance): string { const parts = [ traits.movement ? `Movement: ${traits.movement}` : '', traits.stillness ? `Stillness: ${traits.stillness}` : '', traits.voice ? `Voice (only when speaking is authorised by the audio contract): ${traits.voice}` : '', ].filter(Boolean); return parts.length ? `${name}: ${parts.join(' ')}` : ''; } export function mergePerformanceCanon(...sources: unknown[]): Map { const result = new Map(); for (const source of sources) { if (!Array.isArray(source)) continue; for (const entry of source) { if (!entry || typeof entry.name !== 'string') continue; const key = entry.name.trim().toLowerCase(); result.set(key, { ...result.get(key), ...performanceTraits(entry) }); } } return result; } /** Contextual behaviour replaces habitual movement without changing vocal identity. */ export function adaptPerformanceContext( canon: Map, actors: Array<{ character: string; behaviour: string }> = [], ): Map { const result = new Map(canon); for (const actor of actors) { const prior = result.get(actor.character); if (prior) result.set(actor.character, { ...prior, movement: actor.behaviour, stillness: undefined }); } return result; }