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 `
MEDIA OFFLINE${esc(label)}${esc(asset.path ?? 'no path recorded')}

Contract data remains reviewable. Generate or attach media to enable playback.

`; } return kind === 'video' ? `` : `${esc(label)}`; } 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 `
    ${String(shot.ordinal).padStart(2, '0')}
    ${esc(shot.shotId)} · ${shot.durationSeconds}s · ${shot.referenceCount} refs

    ${esc(shot.purpose)}

    ${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
      ${continuity}
    `; } function candidateRow(candidate: CinemaConsoleCandidate, selected: boolean): string { return ``; } 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}
    MEDIA OFFLINE${esc(candidate?.attemptId ?? `Candidate ${slot}`)}

    Attach candidate media to enable synchronized playback.

    ${esc(candidate?.attemptId ?? `Candidate ${slot}`)} full-clip QC filmstrip
    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

    Changed variable

    A retry must name one controlled change and retain its parent attempt.

    Authority

    Review authority is verifier-scoped; generation_batch remains a separate evidence-bearing approval.

    '; } const targets = model.candidates.filter((candidate) => candidate.queueTaskId).map((candidate) => ``).join(''); const lanes: CinemaCandidateReviewLane[] = ['technical', 'identity-continuity', 'performance', 'editorial']; const laneRows = lanes.map((lane) => `
    ${esc(lane)}
    `).join(''); return `
    Authenticated live session. Review and promotion are provider-free. This surface has no generation, provider-submit or spend-approval endpoint.

    Four-lane candidate review

    ${laneRows}

    Promote passed candidate

    Director or producer role required. Review authority never authorizes generation spend.

    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

    ${recognition}
    ConditionRisk proofReviewerEvidenceStatus

    Rights

      ${rights}

    Voice and release

      ${voices}
    `; } 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 recordedStatic 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 attachedAttach 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}sspend ${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)}

    Ordered gates

      ${gateItems}

    Blockers

      ${blockers}

    Assumptions

      ${assumptions}
    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' ? '
    ' : ''}
    ${queueRows}
    TaskStateRoute + gateDependenciesLaneLeaseProvider progressFailure / retryCost
    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')}
    ${comparisonDiff(baselineCandidate)}
    Canonical fieldCandidate ACandidate B
    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

      ${reviewHistory}
    08 / rough cut

    Edit room

    Shot order, timing, act/arc/beat/setup/payoff coverage, missing coverage and sound layers remain visible before picture lock.

      ${editRail}
    ${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

      ${storyCoverage}
    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)}
    `; }