import type {
CinemaConsoleAsset,
CinemaConsoleCandidate,
CinemaConsoleModel,
CinemaConsoleQcFinding,
CinemaConsoleQueueRow,
CinemaConsoleReviewDecision,
CinemaConsoleShot,
} from './cinema-console-types.js';
import type { CinemaCandidateReviewLane } from './cinema-candidate-review.js';
function esc(value: unknown): string {
return String(value ?? '')
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
.replaceAll('"', '"').replaceAll("'", ''');
}
function json(value: unknown): string {
return JSON.stringify(value).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026');
}
function badge(value: string): string {
const normalized = value.toLowerCase();
const tone = ['succeeded', 'pass', 'locked', 'ready', 'approved', 'dry-run-complete', 'covered'].includes(normalized)
? 'good' : ['failed', 'dead-letter', 'blocker', 'rejected', 'missing'].includes(normalized)
? 'bad' : ['blocked', 'warning', 'reconciling', 'external-approval', 'awaiting-approval'].includes(normalized) ? 'warn' : 'neutral';
return `${esc(value.replaceAll('-', ' '))} `;
}
function queueRow(row: CinemaConsoleQueueRow): string {
const dependencies = row.dependencies.length ? row.dependencies.map((id) => `${esc(id)}`).join(' ') : 'none ';
const lane = row.laneId ? `${esc(row.laneId)}position ${row.lanePosition ?? '—'} · ETA ${row.laneEtaSeconds ?? '—'}s ` : 'none ';
const execution = row.providerJobId ? `${esc(row.providerJobId)}${esc(row.providerStatus ?? 'pending')} ` : `${esc(row.providerStatus ?? '—')} `;
const lease = row.leaseWorkerId ? `${esc(row.leaseWorkerId)}until ${esc(row.leaseExpiresAt)} ` : 'none ';
const failure = row.failure ? `${esc(row.failure.code)} ${esc(row.failure.message)} · retryable ${row.failure.retryable} ` : '— ';
return `
${esc(row.taskId)} ${esc(row.kind)} ${badge(row.status)} ${esc(row.routeId)}${esc(row.authorizationRequirement)}${row.spendAuthorizationId ? ` · ${esc(row.spendAuthorizationId)}` : ''} ${row.reconciliationRequired ? `reconcile first · ${row.reconciliationAttempts} attempt(s) ` : ''}${dependencies} ${lane} ${lease} ${execution} ${failure} ${esc(row.currency)} ${row.estimatedCost.toFixed(2)}actual ${row.actualCost.toFixed(2)} `;
}
function media(asset: { path: string | null; mediaExists: boolean }, label: string, kind: 'image' | 'video'): string {
if (!asset.path || !asset.mediaExists) {
return ``;
}
return kind === 'video'
? `Video playback is unavailable.
`
: ` `;
}
function assetCard(asset: CinemaConsoleAsset): string {
const mask = asset.maskArtifactId
? `mask ${esc(asset.maskArtifactId)}
`
: 'mask not required
';
return `${media(asset, asset.label, 'image')}
${esc(asset.role)}
${esc(asset.label)} faces ${asset.faceCount} fragments ${asset.faceFragmentCount} ${badge(asset.lockStatus)}
${mask}
${esc(asset.contentHash)} `;
}
function shotCard(shot: CinemaConsoleShot): string {
const continuity = shot.continuity.length ? shot.continuity.map((item) => `${esc(item)} `).join('') : 'no conflicting visible state ';
return `${media({ path: shot.videoPath, mediaExists: shot.mediaExists }, `${shot.shotId} clip`, 'video')}
Expected change ${esc(shot.expectedChange)} Camera ${esc(shot.camera)} Action ${esc(shot.action)} Compiled provider prompt ${esc(shot.prompt)} ${esc(shot.promptHash)}Continuity carry `;
}
function candidateRow(candidate: CinemaConsoleCandidate, selected: boolean): string {
return `${esc(candidate.attemptId)} ${esc(candidate.shotId)} · ${esc(candidate.routeId)} ${badge(candidate.promoted ? 'promoted' : candidate.status)}changed: ${esc(candidate.changedVariables.join(', ') || 'baseline')} · parent: ${esc(candidate.parentAttemptId ?? 'none')} · evidence: ${candidate.evidenceIds.length} · ${esc(candidate.currency)} ${candidate.actualCost.toFixed(2)} `;
}
function comparisonPanel(candidate: CinemaConsoleCandidate | undefined, slot: 'A' | 'B'): string {
const hasMedia = Boolean(candidate?.videoPath && candidate.mediaExists);
const hasFilmstrip = Boolean(candidate?.filmstripPath && candidate.filmstripExists);
return `Candidate ${slot}
Video playback is unavailable.
Full-clip QC filmstrip · advisory human review Attempt ${esc(candidate?.attemptId ?? 'unassigned')} Shot ${esc(candidate?.shotId ?? '—')} Route ${esc(candidate?.routeId ?? '—')} Exact compiled prompt ${esc(candidate?.prompt ?? '')} Provider parameters ${esc(candidate ? JSON.stringify(candidate.providerParameters, null, 2) : '')} `;
}
function comparisonDiff(candidate: CinemaConsoleCandidate | undefined): string {
const fields: Array<[string, string]> = [
['routeId', candidate?.routeId ?? '—'],
['promptHash', candidate?.promptHash ?? '—'],
['providerParametersHash', candidate?.providerParametersHash ?? '—'],
['referenceAssetIds', candidate?.referenceAssetIds.join(', ') ?? '—'],
['changedVariables', candidate?.changedVariables.join(', ') || 'baseline'],
];
return fields.map(([field, value]) => `${esc(field)} ${esc(value)} ${esc(value)} `).join('');
}
function qcFinding(finding: CinemaConsoleQcFinding): string {
return `${finding.timeSeconds === null ? '—' : `${finding.timeSeconds.toFixed(1)}s`} ${esc(finding.lane)} ${esc(finding.summary)} ${esc(finding.shotId)} · ${finding.evidenceIds.length} evidence item(s) ${badge(finding.severity)} `;
}
function reviewDecision(decision: CinemaConsoleReviewDecision): string {
const changed = decision.changedVariable ? `controlled change: ${esc(decision.changedVariable)} ` : '';
return `${decision.timeSeconds === null ? '—' : `${decision.timeSeconds.toFixed(1)}s`} ${esc(decision.reviewerId)} ${esc(decision.reason)} ${esc(decision.shotId)} · ${esc(decision.attemptId)} · ${decision.evidenceIds.length} evidence ${changed}${esc(decision.contentHash)} ${badge(decision.kind)} `;
}
function reviewActions(model: CinemaConsoleModel): string {
if (model.mode !== 'live') {
return 'Read-only export. Open the authenticated live console to pass, reject or request a controlled redesign. Generation approval remains a separate exact-quote authority.
Editorial note Pass Reject Request redesign
Changed variable A retry must name one controlled change and retain its parent attempt.
Select in live console Authority Review authority is verifier-scoped; generation_batch remains a separate evidence-bearing approval.
';
}
const targets = model.candidates.filter((candidate) => candidate.queueTaskId).map((candidate) => `${esc(candidate.shotId)} · ${esc(candidate.attemptId)} `).join('');
const lanes: CinemaCandidateReviewLane[] = ['technical', 'identity-continuity', 'performance', 'editorial'];
const laneRows = lanes.map((lane) => `${esc(lane)} pass fail `).join('');
return `Authenticated live session. Review and promotion are provider-free. This surface has no generation, provider-submit or spend-approval endpoint.
Ready. `;
}
function liveConsoleScript(model: CinemaConsoleModel): string {
if (model.mode !== 'live') return '';
return ``;
}
function evidenceBoard(model: CinemaConsoleModel): string {
const evidence = model.evidence;
if (!evidence) return '';
const sheets = evidence.characterSheets.map((sheet) => `sheet ${esc(sheet.characterVersionId)} ${esc(sheet.reviewerId ?? 'No authority decision')} ${sheet.evidenceIds.length} evidence item(s) ${esc(sheet.sheetHash ?? 'not stored')} ${badge(sheet.status)} `).join('');
const recognition = evidence.recognition.map((condition) => `${esc(condition.conditionId)} ${esc(condition.category)} ${esc(condition.description)} ${esc(condition.reviewerId ?? '—')} ${condition.evidenceIds.length} ${badge(condition.status)} `).join('');
const rights = evidence.rights.map((record) => `rights ${esc(record.assetId)} ${esc(record.owner ?? 'Owner pending')} ${esc(record.permittedScope ?? 'Permitted scope pending')} · ${record.evidenceIds.length} evidence ${badge(record.status)} `).join('');
const voices = evidence.voices.map((voice) => `voice ${esc(voice.characterId)} ${esc(voice.voiceId ?? (voice.speaking ? 'Voice pending' : 'No voice required'))} lock ${esc(voice.lockStatus)} · release ${esc(voice.releaseStatus)} · ${voice.auditionEvidenceIds.length + voice.releaseEvidenceIds.length} evidence ${badge(voice.lockStatus)} `).join('');
const contracts = evidence.productionContracts;
return `Authority + production contracts
Evidence board Every lock is tied to persisted evidence; pending means pending. Contract hashes shown here are the exact versions bound into the shot manifests.
Character sheet decisions ${sheets || 'No character sheet stored '} Production contract bindings ${badge(contracts.boundToEveryManifest ? 'locked' : 'blocked')} bound to every manifest
Continuity · ${contracts.continuityRecords} records
${esc(contracts.continuityHash ?? 'not compiled')}Timed audio · ${contracts.timedAudioShots} shots
${esc(contracts.timedAudioHash ?? 'not compiled')}Planning
${esc(evidence.planningReceiptHash)}Evidence ledger
${esc(evidence.evidenceLedgerHash ?? 'not created')}Preflight
${esc(evidence.preflightHash)}Ten-condition recognition board Condition Risk proof Reviewer Evidence Status ${recognition}
`;
}
export function renderCinemaConsoleHtml(model: CinemaConsoleModel): string {
const completeTasks = model.queue.filter((task) => task.status === 'succeeded').length;
const gateItems = model.gates.map((gate) => `${esc(gate.label)} ${badge(gate.status)}${gate.evidenceIds.length} evidence `).join('');
const blockers = model.summary.blockers.map((item) => `${esc(item)} `).join('');
const assumptions = model.summary.assumptions.map((item) => `${esc(item)} `).join('');
const reviewHistory = model.reviews.length ? model.reviews.map(reviewDecision).join('') : '— No review decisions recorded Static evidence remains unchanged. ';
const queueRows = model.queue.map(queueRow).join('');
const candidates = model.candidates.map((candidate, index) => candidateRow(candidate, index === 0)).join('');
const baselineCandidate = model.candidates[0];
const editRail = model.edit.shotOrder.map((shotId, index) => {
const shot = model.shots.find((item) => item.shotId === shotId);
return `${String(index + 1).padStart(2, '0')} ${esc(shotId)} ${shot?.durationSeconds ?? 0}s · ${esc(shot?.purpose ?? '')} `;
}).join('');
const storyCoverage = model.edit.storyCoverage.length ? model.edit.storyCoverage.map((beat) => `${esc(beat.act)} ${esc(beat.beatId)} ${esc(beat.purpose)} change: ${esc(beat.change)} scenes: ${esc(beat.sceneIds.join(', ') || 'none')} · shots: ${esc(beat.shotIds.join(', ') || 'none')} setups: ${esc(beat.setupIds.join(', ') || 'none')} · payoffs: ${esc(beat.payoffIds.join(', ') || 'none')} ${badge(beat.covered ? 'covered' : 'missing')} `).join('') : '— Story coverage not attached Attach a Cinema story blueprint and coverage artifact. ';
const qualification = model.qualification ? `Scope qualification
${esc(model.qualification.currentScope)} → ${esc(model.qualification.proposedScope ?? 'terminal')} ${badge(model.qualification.verdict)}
${esc(model.qualification.blockers.join(' · ') || `${model.qualification.passedChecks.length} checks passed`)}
${esc(model.qualification.contentHash)} ` : 'Scope qualification
Not evaluated ';
return `
${esc(model.title)} · VideoClaw Cinema
${esc(model.scope)} / ${esc(model.projectSlug)}
${esc(model.title)} readiness ${esc(model.summary.readiness)} queue ${completeTasks}/${model.queue.length} runtime ${model.edit.totalDurationSeconds}s spend ${esc(model.summary.currency)} ${model.summary.actualCost.toFixed(2)} ${badge(model.mode)}
01 / production state
Overview The decision surface: scope, ordered gates, explicit blockers and cost evidence in one place.
Queue complete ${completeTasks}/${model.queue.length}
Approved assets ${model.assets.filter((item) => item.lockStatus !== 'pending').length}
Actual spend ${model.summary.currency} ${model.summary.actualCost.toFixed(2)}
02 / durable scheduler
Production queue Dependencies, route and approval gate, lane position, leases, provider progress, retries/dead letters and estimated-versus-actual economics.
${model.mode === 'live' ? 'Refresh canonical queue state
' : ''}Task State Route + gate Dependencies Lane Lease Provider progress Failure / retry Cost ${queueRows}
03 / identity system
Asset room Exactly one locked identity portrait. Garment-safe front and back plates remain headless, independently hashed and reviewable.
${model.assets.map(assetCard).join('')}
${evidenceBoard(model)}
04 / story + continuity
Storyboard & shot contracts Editorial purpose stays beside camera, timed action, active reference budget, continuity carry and the exact compiled prompt.
${model.shots.map(shotCard).join('')}
05 / attempt lineage
Candidate viewer Candidate A remains the baseline; choosing a row assigns Candidate B. Playback, seeking and playback rate stay synchronized while exact prompt, parameter, reference and changed-variable differences remain visible.
${candidates}
${comparisonPanel(baselineCandidate, 'A')}${comparisonPanel(baselineCandidate, 'B')}
Canonical field Candidate A Candidate B ${comparisonDiff(baselineCandidate)}
06 / time-addressed evidence
QC timeline Technical, identity, continuity, performance and editorial lanes stay independent. A visual pass cannot hide a story failure.
${model.qc.map(qcFinding).join('')}
07 / authority-aware decisions
Review actions Live commands use authenticated, role-scoped canonical APIs. Static exports expose the resulting audit history but cannot submit commands or authorize spend.
${reviewActions(model)}Immutable decision history
Shot order, timing, act/arc/beat/setup/payoff coverage, missing coverage and sound layers remain visible before picture lock.
${media({ path: model.edit.roughCutPath, mediaExists: Boolean(model.edit.roughCutPath) }, 'Rough cut', 'video')}
Audio: ${esc(model.edit.audioLayers.join(' · '))}
Missing coverage: ${esc(model.edit.missingCoverage.join(', ') || 'none in attached coverage')}
Story coverage through rough cut
09 / archive + handoff
Delivery room Masters, captions, archive hashes and measured scope progression remain blocked until picture, sound and colour are locked and final verification passes.
Master
${esc(model.delivery.masterPath ?? 'Not rendered')} ${badge(model.delivery.status)}
Captions
${esc(model.delivery.captionsPath ?? 'Not authored')} Archive hash
${esc(model.delivery.archiveHash ?? 'pending')}${qualification}
${liveConsoleScript(model)}
`;
}