#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const RELEASE = '2.0.0-rc.1'; const SCHEMA_VERSION = 'ecc.release-video-suite.v1'; const VIDEO_MANIFEST_PATH = `docs/releases/${RELEASE}/video-suite-production.md`; const HYPERGROWTH_DOC_PATH = 'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md'; const REQUIRED_DOC_MARKERS = [ 'ECC 2.0 Video Suite Production Manifest', 'video-use compatible workflow', 'ECC_VIDEO_SOURCE_ROOT', 'ECC_VIDEO_RELEASE_SUITE_ROOT', 'Primary launch video', 'Self-Eval Gate', 'Do Not Publish If', ]; const REQUIRED_SOURCE_ASSETS = [ { id: 'primary-longform-wide', file: 'longform-full-wide.mp4', lane: 'primary-launch', proof: 'operator system, control-plane direction, closing proof', }, { id: 'primary-shortform-full', file: 'sf-longform-full.mp4', lane: 'primary-launch', proof: 'structured context opener', }, { id: 'what-is-ecc-wide', file: 'sf-thread-2-whatisecc.mp4', lane: 'what-is-ecc', proof: 'category clarity and GitHub App explanation', }, { id: 'security-wide', file: 'sf-thread-4-security.mp4', lane: 'security-proof', proof: 'AgentShield, hooks, MCP, permission risk', }, { id: 'money-proof-wide', file: 'thread-2-ghapp-money.mp4', lane: 'money-proof', proof: 'OSS plus paid hosting and services', }, { id: 'architecture-wide', file: 'architecture-2-wide.mp4', lane: 'b-roll', proof: 'harness-native architecture', }, { id: 'terminal-scan-wide', file: 'terminal-scan-2-wide.mp4', lane: 'install-proof', proof: 'terminal workflow and install confidence', }, { id: 'site-raw', file: 'new_site_raw.mp4', lane: 'b-roll', proof: 'site and product surface', }, { id: 'coverage-montage', file: 'coverage-montage-wide.mp4', lane: 'coverage-proof', proof: 'distribution and social proof', }, { id: 'metrics-ticker-wide', file: 'metrics-ticker-2-wide.mp4', lane: 'money-proof', proof: 'traction and funnel proof', }, { id: 'growth-timeline-wide', file: 'growth-timeline-2-wide.mp4', lane: 'coverage-proof', proof: 'release momentum timeline', }, { id: 'github-app-proof-1', file: 'gh_app_1.png', lane: 'money-proof', proof: 'hosted GitHub App surface', }, { id: 'stars', file: 'star_history.png', lane: 'coverage-proof', proof: 'OSS adoption chart', }, { id: 'x-analytics', file: 'x_analytics.png', lane: 'coverage-proof', proof: 'social distribution proof', }, { id: '100k-proof', file: '100k.png', lane: 'coverage-proof', proof: 'reach milestone proof', }, ]; const REQUIRED_SUITE_ARTIFACTS = [ { id: 'primary-edl', relativePath: 'edl/primary-launch.edl.md', kind: 'edl', }, { id: 'primary-timeline-v1', relativePath: 'timelines/primary-launch-v1.timeline.json', kind: 'timeline', }, { id: 'primary-captions-v1', relativePath: 'renders/ecc-2-primary-launch-rough-v1.captions.srt', kind: 'captions', }, { id: 'primary-render-v1', relativePath: 'renders/ecc-2-primary-launch-rough-v1.mp4', kind: 'video', minDurationSeconds: 90, maxDurationSeconds: 150, }, { id: 'segment-structured-context', relativePath: 'segments/primary-launch-v1/01-structured-context.mp4', kind: 'video', }, { id: 'segment-agentic-harness-optimization', relativePath: 'segments/primary-launch-v1/02-agentic-harness-optimization.mp4', kind: 'video', }, { id: 'segment-not-another-harness', relativePath: 'segments/primary-launch-v1/03-not-another-harness.mp4', kind: 'video', }, { id: 'segment-agentic-ide-surface', relativePath: 'segments/primary-launch-v1/04-agentic-ide-surface.mp4', kind: 'video', }, { id: 'segment-github-app-proof', relativePath: 'segments/primary-launch-v1/05-github-app-proof.mp4', kind: 'video', }, { id: 'segment-security-risk', relativePath: 'segments/primary-launch-v1/06-security-risk.mp4', kind: 'video', }, { id: 'segment-agentshield-proof', relativePath: 'segments/primary-launch-v1/07-agentshield-proof.mp4', kind: 'video', }, { id: 'segment-oss-paid-model', relativePath: 'segments/primary-launch-v1/08-oss-paid-model.mp4', kind: 'video', }, { id: 'segment-close-shipping-system', relativePath: 'segments/primary-launch-v1/09-close-shipping-system.mp4', kind: 'video', }, ]; const REQUIRED_PUBLISH_CANDIDATES = [ { id: 'publish-primary-launch', relativePath: 'renders/publish-candidates/ecc-2-primary-launch.mp4', kind: 'video', minDurationSeconds: 90, maxDurationSeconds: 150, minWidth: 1920, minHeight: 1080, minSizeMb: 5, requiresAudio: true, }, { id: 'publish-primary-launch-captions', relativePath: 'renders/publish-candidates/ecc-2-primary-launch.captions.srt', kind: 'captions', }, { id: 'publish-install-proof-wide', relativePath: 'renders/publish-candidates/ecc-2-install-proof-wide.mp4', kind: 'video', minDurationSeconds: 25, maxDurationSeconds: 35, minWidth: 1920, minHeight: 1080, minSizeMb: 1, requiresAudio: true, }, { id: 'publish-install-proof-vertical', relativePath: 'renders/publish-candidates/ecc-2-install-proof-vertical.mp4', kind: 'video', minDurationSeconds: 25, maxDurationSeconds: 35, minWidth: 1080, minHeight: 1920, minSizeMb: 1, requiresAudio: true, }, { id: 'publish-what-is-ecc-wide', relativePath: 'renders/publish-candidates/ecc-2-what-is-ecc-wide.mp4', kind: 'video', minDurationSeconds: 45, maxDurationSeconds: 60, minWidth: 1920, minHeight: 1080, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-what-is-ecc-vertical', relativePath: 'renders/publish-candidates/ecc-2-what-is-ecc-vertical.mp4', kind: 'video', minDurationSeconds: 45, maxDurationSeconds: 60, minWidth: 1080, minHeight: 1920, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-security-proof-wide', relativePath: 'renders/publish-candidates/ecc-2-security-proof-wide.mp4', kind: 'video', minDurationSeconds: 45, maxDurationSeconds: 60, minWidth: 1920, minHeight: 1080, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-security-proof-vertical', relativePath: 'renders/publish-candidates/ecc-2-security-proof-vertical.mp4', kind: 'video', minDurationSeconds: 45, maxDurationSeconds: 60, minWidth: 1080, minHeight: 1920, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-money-proof-wide', relativePath: 'renders/publish-candidates/ecc-2-money-proof-wide.mp4', kind: 'video', minDurationSeconds: 30, maxDurationSeconds: 45, minWidth: 1920, minHeight: 1080, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-money-proof-vertical', relativePath: 'renders/publish-candidates/ecc-2-money-proof-vertical.mp4', kind: 'video', minDurationSeconds: 30, maxDurationSeconds: 45, minWidth: 1080, minHeight: 1920, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-social-proof-wide', relativePath: 'renders/publish-candidates/ecc-2-social-proof-wide.mp4', kind: 'video', minDurationSeconds: 30, maxDurationSeconds: 45, minWidth: 1920, minHeight: 1080, minSizeMb: 2, requiresAudio: true, }, { id: 'publish-social-proof-vertical', relativePath: 'renders/publish-candidates/ecc-2-social-proof-vertical.mp4', kind: 'video', minDurationSeconds: 30, maxDurationSeconds: 45, minWidth: 1080, minHeight: 1920, minSizeMb: 2, requiresAudio: true, }, ].map(candidate => ( candidate.kind === 'video' ? { noBlackFrames: true, ...candidate } : candidate )); function usage() { console.log([ 'Usage: node scripts/release-video-suite.js [options]', '', 'Validates the ECC 2.0 release video production lane without committing raw media paths.', '', 'Options:', ' --format Output format (default: text)', ' --json Alias for --format json', ' --root Repository root to inspect (default: cwd)', ' --source-root Directory containing ECC 2 source media, with optional _edited subdir', ' --suite-root Directory containing render/timeline/transcript outputs', ' --skip-probe Skip ffprobe duration reads for fixture or dry-run checks', ' --summary Emit compact JSON when used with --format json', ' --help, -h Show this help', '', 'Environment:', ' ECC_VIDEO_SOURCE_ROOT', ' ECC_VIDEO_RELEASE_SUITE_ROOT', ].join('\n')); } function readArgValue(args, index, flagName) { const value = args[index + 1]; if (!value || value.startsWith('--')) { throw new Error(`${flagName} requires a value`); } return value; } function parseArgs(argv) { const args = argv.slice(2); const parsed = { format: 'text', help: false, root: path.resolve(process.cwd()), sourceRoot: process.env.ECC_VIDEO_SOURCE_ROOT || '', suiteRoot: process.env.ECC_VIDEO_RELEASE_SUITE_ROOT || '', skipProbe: false, summary: false, }; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === '--help' || arg === '-h') { parsed.help = true; continue; } if (arg === '--json') { parsed.format = 'json'; continue; } if (arg === '--skip-probe') { parsed.skipProbe = true; continue; } if (arg === '--summary') { parsed.summary = true; continue; } if (arg === '--format') { parsed.format = readArgValue(args, index, arg).toLowerCase(); index += 1; continue; } if (arg.startsWith('--format=')) { parsed.format = arg.slice('--format='.length).toLowerCase(); continue; } if (arg === '--root') { parsed.root = path.resolve(readArgValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--root=')) { parsed.root = path.resolve(arg.slice('--root='.length)); continue; } if (arg === '--source-root') { parsed.sourceRoot = path.resolve(readArgValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--source-root=')) { parsed.sourceRoot = path.resolve(arg.slice('--source-root='.length)); continue; } if (arg === '--suite-root') { parsed.suiteRoot = path.resolve(readArgValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--suite-root=')) { parsed.suiteRoot = path.resolve(arg.slice('--suite-root='.length)); continue; } throw new Error(`Unknown argument: ${arg}`); } if (!['text', 'json'].includes(parsed.format)) { throw new Error(`Invalid format: ${parsed.format}. Use text or json.`); } return parsed; } function readText(rootDir, relativePath) { try { return fs.readFileSync(path.join(rootDir, relativePath), 'utf8'); } catch (_error) { return ''; } } function safeParseJson(text) { if (!text.trim()) { return null; } try { return JSON.parse(text); } catch (_error) { return null; } } function lineNumberForIndex(text, index) { return text.slice(0, index).split('\n').length; } function scanForbiddenPaths(rootDir, relativePaths) { const offenders = []; const privatePathPattern = /\/Users\/(?!\.\.\.)[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/g; for (const relativePath of relativePaths) { const text = readText(rootDir, relativePath); if (!text) { continue; } for (const match of text.matchAll(privatePathPattern)) { offenders.push({ path: relativePath, line: lineNumberForIndex(text, match.index), marker: match[0], }); } } return offenders; } function makeCheck(id, status, summary, fix, details = {}) { return { id, status, summary, fix: status === 'pass' ? '' : fix, ...details, }; } function formatBytes(bytes) { if (!Number.isFinite(bytes)) { return null; } return Number((bytes / 1024 / 1024).toFixed(2)); } function probeMedia(filePath, skipProbe) { const stat = fs.statSync(filePath); const result = { sizeBytes: stat.size, sizeMb: formatBytes(stat.size), audioStreams: null, durationSeconds: null, height: null, probe: skipProbe ? 'skipped' : 'unavailable', videoStreams: null, width: null, }; if (skipProbe) { return result; } const probe = spawnSync('ffprobe', [ '-v', 'error', '-show_entries', 'format=duration:stream=codec_type,width,height', '-of', 'json', filePath, ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15000, }); if (probe.error) { result.probe = `error: ${probe.error.message}`; return result; } if (probe.status !== 0) { result.probe = `failed: ${(probe.stderr || '').trim() || `exit ${probe.status}`}`; return result; } const parsed = safeParseJson(probe.stdout); const duration = Number(parsed && parsed.format && parsed.format.duration); if (Number.isFinite(duration)) { result.durationSeconds = Number(duration.toFixed(3)); } const streams = Array.isArray(parsed && parsed.streams) ? parsed.streams : []; const videoStreams = streams.filter(stream => stream.codec_type === 'video'); const audioStreams = streams.filter(stream => stream.codec_type === 'audio'); const firstVideo = videoStreams[0] || {}; result.audioStreams = audioStreams.length; result.videoStreams = videoStreams.length; result.width = Number.isFinite(Number(firstVideo.width)) ? Number(firstVideo.width) : null; result.height = Number.isFinite(Number(firstVideo.height)) ? Number(firstVideo.height) : null; result.probe = 'ok'; return result; } function detectBlackSegments(filePath, skipProbe) { if (skipProbe) { return { blackFrameProbe: 'skipped', blackSegments: null, }; } const result = { blackFrameProbe: 'unavailable', blackSegments: null, }; const probe = spawnSync('ffmpeg', [ '-hide_banner', '-nostats', '-i', filePath, '-vf', 'blackdetect=d=0.5:pix_th=0.10', '-an', '-f', 'null', '-', ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 120000, }); if (probe.error) { result.blackFrameProbe = `error: ${probe.error.message}`; return result; } if (probe.status !== 0) { result.blackFrameProbe = `failed: ${(probe.stderr || '').trim() || `exit ${probe.status}`}`; return result; } const output = `${probe.stdout || ''}\n${probe.stderr || ''}`; result.blackSegments = output .split('\n') .filter(line => line.includes('black_start')) .length; result.blackFrameProbe = 'ok'; return result; } function resolveSourceAssetPath(sourceRoot, fileName) { const candidates = [ path.join(sourceRoot, fileName), path.join(sourceRoot, '_edited', fileName), ]; return candidates.find(candidate => fs.existsSync(candidate)) || candidates[0]; } function inspectSourceAssets(sourceRoot, skipProbe) { return REQUIRED_SOURCE_ASSETS.map(asset => { if (!sourceRoot) { return { ...asset, status: 'missing', configured: false, }; } const filePath = resolveSourceAssetPath(sourceRoot, asset.file); if (!fs.existsSync(filePath)) { return { ...asset, status: 'missing', configured: true, }; } const media = asset.file.endsWith('.mp4') ? probeMedia(filePath, skipProbe) : { sizeBytes: fs.statSync(filePath).size, sizeMb: formatBytes(fs.statSync(filePath).size), durationSeconds: null, probe: 'not-media', }; return { ...asset, status: 'present', configured: true, ...media, }; }); } function validateVideoArtifact(artifact, media, skipProbe) { if (artifact.kind !== 'video' || skipProbe) { return []; } const failures = []; if (media.probe !== 'ok') { failures.push(`ffprobe ${media.probe}`); } if ( Number.isFinite(artifact.minDurationSeconds) && ( !Number.isFinite(media.durationSeconds) || media.durationSeconds < artifact.minDurationSeconds ) ) { failures.push(`duration below ${artifact.minDurationSeconds}s`); } if ( Number.isFinite(artifact.maxDurationSeconds) && ( !Number.isFinite(media.durationSeconds) || media.durationSeconds > artifact.maxDurationSeconds ) ) { failures.push(`duration above ${artifact.maxDurationSeconds}s`); } if ( Number.isFinite(artifact.minSizeMb) && (!Number.isFinite(media.sizeMb) || media.sizeMb < artifact.minSizeMb) ) { failures.push(`size below ${artifact.minSizeMb} MB`); } if ( Number.isFinite(artifact.minWidth) && (!Number.isFinite(media.width) || media.width < artifact.minWidth) ) { failures.push(`width below ${artifact.minWidth}`); } if ( Number.isFinite(artifact.minHeight) && (!Number.isFinite(media.height) || media.height < artifact.minHeight) ) { failures.push(`height below ${artifact.minHeight}`); } if (artifact.requiresAudio && (!Number.isFinite(media.audioStreams) || media.audioStreams < 1)) { failures.push('audio stream missing'); } if (artifact.noBlackFrames) { if (media.blackFrameProbe !== 'ok') { failures.push(`blackdetect ${media.blackFrameProbe}`); } else if (Number.isFinite(media.blackSegments) && media.blackSegments > 0) { failures.push(`${media.blackSegments} black frame segment(s)`); } } return failures; } function inspectArtifactCollection(rootDir, artifacts, skipProbe) { return artifacts.map(artifact => { if (!rootDir) { return { ...artifact, status: 'missing', configured: false, validationFailures: [], }; } const filePath = path.join(rootDir, artifact.relativePath); if (!fs.existsSync(filePath)) { return { ...artifact, status: 'missing', configured: true, validationFailures: [], }; } const media = artifact.kind === 'video' ? { ...probeMedia(filePath, skipProbe), ...(artifact.noBlackFrames ? detectBlackSegments(filePath, skipProbe) : {}), } : { sizeBytes: fs.statSync(filePath).size, sizeMb: formatBytes(fs.statSync(filePath).size), durationSeconds: null, probe: 'not-media', }; const validationFailures = validateVideoArtifact(artifact, media, skipProbe); return { ...artifact, status: validationFailures.length === 0 ? 'present' : 'invalid', configured: true, validationFailures, ...media, }; }); } function inspectSuiteArtifacts(suiteRoot, skipProbe) { return inspectArtifactCollection(suiteRoot, REQUIRED_SUITE_ARTIFACTS, skipProbe); } function inspectPublishCandidates(suiteRoot, skipProbe) { return inspectArtifactCollection(suiteRoot, REQUIRED_PUBLISH_CANDIDATES, skipProbe); } function evaluatePrimaryRender(suiteArtifacts, skipProbe) { const primary = suiteArtifacts.find(artifact => artifact.id === 'primary-render-v1'); if (!primary || primary.status !== 'present') { return { status: 'fail', summary: 'primary launch render is missing or outside the duration target', fix: 'Render the primary launch video within the 90-150 second target before release review.', }; } if (skipProbe) { return { status: 'pass', summary: 'primary launch render exists; stream self-eval skipped by --skip-probe', fix: '', }; } const failures = []; if (primary.probe !== 'ok') { failures.push(`ffprobe ${primary.probe}`); } if (!Number.isFinite(primary.durationSeconds) || primary.durationSeconds < 90 || primary.durationSeconds > 150) { failures.push('duration outside 90-150 seconds'); } if (!Number.isFinite(primary.sizeMb) || primary.sizeMb < 5) { failures.push('render is unexpectedly small'); } if (!Number.isFinite(primary.videoStreams) || primary.videoStreams < 1) { failures.push('no video stream'); } if (!Number.isFinite(primary.audioStreams) || primary.audioStreams < 1) { failures.push('no audio stream'); } if (!Number.isFinite(primary.width) || !Number.isFinite(primary.height) || primary.width < 1280 || primary.height < 720) { failures.push('resolution below 1280x720'); } if (failures.length > 0) { return { status: 'fail', summary: `primary launch render failed self-eval: ${failures.join(', ')}`, fix: 'Regenerate the primary launch render with audio, HD video, valid duration, and non-empty output.', }; } return { status: 'pass', summary: `primary launch render self-eval passed: ${primary.durationSeconds}s, ${primary.width}x${primary.height}, ${primary.audioStreams} audio stream(s), ${primary.sizeMb} MB`, fix: '', }; } function buildReport(options = {}) { const rootDir = path.resolve(options.root || process.cwd()); const sourceRoot = options.sourceRoot ? path.resolve(options.sourceRoot) : ''; const suiteRoot = options.suiteRoot ? path.resolve(options.suiteRoot) : ''; const skipProbe = Boolean(options.skipProbe); const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {}; const packageScripts = packageJson.scripts || {}; const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : []; const manifest = readText(rootDir, VIDEO_MANIFEST_PATH); const hypergrowth = readText(rootDir, HYPERGROWTH_DOC_PATH); const missingDocMarkers = REQUIRED_DOC_MARKERS.filter(marker => !manifest.includes(marker)); const forbiddenPaths = scanForbiddenPaths(rootDir, [ VIDEO_MANIFEST_PATH, HYPERGROWTH_DOC_PATH, `docs/releases/${RELEASE}/preview-pack-manifest.md`, `docs/releases/${RELEASE}/launch-checklist.md`, ]); const sourceAssets = inspectSourceAssets(sourceRoot, skipProbe); const suiteArtifacts = inspectSuiteArtifacts(suiteRoot, skipProbe); const publishCandidates = inspectPublishCandidates(suiteRoot, skipProbe); const missingSourceAssets = sourceAssets.filter(asset => asset.status !== 'present'); const missingSuiteArtifacts = suiteArtifacts.filter(artifact => artifact.status !== 'present'); const missingPublishCandidates = publishCandidates.filter(candidate => candidate.status !== 'present'); const primaryRenderSelfEval = evaluatePrimaryRender(suiteArtifacts, skipProbe); const checks = [ makeCheck( 'video-suite-command-registered', packageScripts['release:video-suite'] === 'node scripts/release-video-suite.js' && packageFiles.includes('scripts/release-video-suite.js') ? 'pass' : 'fail', 'package script and npm package entry for the release video suite validator', 'Add release:video-suite to package scripts and include scripts/release-video-suite.js in package files.' ), makeCheck( 'video-suite-manifest-present', manifest && missingDocMarkers.length === 0 ? 'pass' : 'fail', manifest && missingDocMarkers.length === 0 ? `${VIDEO_MANIFEST_PATH} includes the required production markers` : `missing markers: ${missingDocMarkers.join(', ') || 'manifest file missing'}`, 'Restore the video production manifest and required production markers.' ), makeCheck( 'video-suite-public-sanitization', forbiddenPaths.length === 0 && manifest.includes('Do not commit raw footage, transcript JSON, or timeline exports') && /Keep raw\s+absolute paths out of public docs/.test(hypergrowth) ? 'pass' : 'fail', forbiddenPaths.length === 0 ? 'public launch docs avoid private media paths and keep raw assets local' : `private path markers: ${forbiddenPaths.map(item => `${item.path}:${item.line}`).join(', ')}`, 'Remove private absolute paths from public release docs and keep raw media in the local production workspace.', { forbiddenPaths } ), makeCheck( 'video-source-assets-present', missingSourceAssets.length === 0 ? 'pass' : 'fail', missingSourceAssets.length === 0 ? `${sourceAssets.length} source assets are present` : `missing source assets: ${missingSourceAssets.map(asset => asset.file).join(', ')}`, 'Set ECC_VIDEO_SOURCE_ROOT or pass --source-root to the edited ECC 2 media directory.', { configured: Boolean(sourceRoot), missing: missingSourceAssets.map(asset => asset.file), } ), makeCheck( 'video-release-artifacts-present', missingSuiteArtifacts.length === 0 ? 'pass' : 'fail', missingSuiteArtifacts.length === 0 ? `${suiteArtifacts.length} render, timeline, caption, EDL, and segment artifacts are present` : `missing or invalid suite artifacts: ${missingSuiteArtifacts.map(artifact => artifact.relativePath).join(', ')}`, 'Set ECC_VIDEO_RELEASE_SUITE_ROOT or pass --suite-root to the ECC 2 release suite workspace.', { configured: Boolean(suiteRoot), missing: missingSuiteArtifacts.map(artifact => artifact.relativePath), } ), makeCheck( 'video-primary-render-self-eval', primaryRenderSelfEval.status, primaryRenderSelfEval.summary, primaryRenderSelfEval.fix ), makeCheck( 'video-publish-candidates-present', missingPublishCandidates.length === 0 ? 'pass' : 'fail', missingPublishCandidates.length === 0 ? `${publishCandidates.length} publish-candidate MP4/caption artifacts are present, self-evaluable, and free of detected black-frame segments` : `missing or invalid publish candidates: ${missingPublishCandidates.map(candidate => { const reason = candidate.validationFailures && candidate.validationFailures.length > 0 ? ` (${candidate.validationFailures.join(', ')})` : ''; return `${candidate.relativePath}${reason}`; }).join(', ')}`, 'Render the publish-candidate MP4/caption set under renders/publish-candidates before release review.', { configured: Boolean(suiteRoot), missing: missingPublishCandidates.map(candidate => candidate.relativePath), } ), ]; const failed = checks.filter(check => check.status !== 'pass'); const topActions = []; if (!sourceRoot) { topActions.push('Set ECC_VIDEO_SOURCE_ROOT to the edited ECC 2 media directory.'); } if (!suiteRoot) { topActions.push('Set ECC_VIDEO_RELEASE_SUITE_ROOT to the local release suite workspace.'); } for (const check of failed) { if (check.fix && !topActions.includes(check.fix)) { topActions.push(check.fix); } } return { schema_version: SCHEMA_VERSION, release: RELEASE, generatedAt: options.generatedAt || new Date().toISOString(), root: rootDir, sourceRootConfigured: Boolean(sourceRoot), suiteRootConfigured: Boolean(suiteRoot), mediaPathsRedacted: true, ready: failed.length === 0, checks, sourceAssets, suiteArtifacts, publishCandidates, top_actions: topActions, }; } function summarizeItems(items) { const present = items.filter(item => item.status === 'present'); const missing = items.filter(item => item.status !== 'present'); return { total: items.length, present: present.length, missing: missing.map(item => item.file || item.relativePath), }; } function summarizeReport(report) { const primaryRender = report.suiteArtifacts.find(item => item.id === 'primary-render-v1') || null; return { schema_version: report.schema_version, release: report.release, generatedAt: report.generatedAt, root: report.root, sourceRootConfigured: report.sourceRootConfigured, suiteRootConfigured: report.suiteRootConfigured, mediaPathsRedacted: report.mediaPathsRedacted, ready: report.ready, checks: report.checks.map(check => ({ id: check.id, status: check.status, summary: check.summary, fix: check.fix, })), sourceAssetSummary: summarizeItems(report.sourceAssets), suiteArtifactSummary: summarizeItems(report.suiteArtifacts), publishCandidateSummary: summarizeItems(report.publishCandidates), primaryRender: primaryRender ? { status: primaryRender.status, durationSeconds: primaryRender.durationSeconds, sizeMb: primaryRender.sizeMb, } : null, top_actions: report.top_actions, }; } function renderText(report) { const lines = [ `ECC ${report.release} release video suite`, `Ready: ${report.ready ? 'yes' : 'no'}`, `Source root configured: ${report.sourceRootConfigured ? 'yes' : 'no'}`, `Suite root configured: ${report.suiteRootConfigured ? 'yes' : 'no'}`, '', 'Checks:', ]; for (const check of report.checks) { lines.push(`- ${check.status.toUpperCase()} ${check.id}: ${check.summary}`); } const primaryRender = report.suiteArtifacts.find(item => item.id === 'primary-render-v1'); if (primaryRender && primaryRender.status === 'present') { lines.push(''); lines.push( `Primary rough render: ${primaryRender.relativePath}` + (Number.isFinite(primaryRender.durationSeconds) ? ` (${primaryRender.durationSeconds}s)` : '') ); } if (report.publishCandidates.length > 0) { const present = report.publishCandidates.filter(item => item.status === 'present').length; lines.push(`Publish candidates: ${present}/${report.publishCandidates.length} present`); } if (report.top_actions.length > 0) { lines.push(''); lines.push('Top actions:'); for (const action of report.top_actions) { lines.push(`- ${action}`); } } return `${lines.join('\n')}\n`; } function main() { let options; try { options = parseArgs(process.argv); } catch (error) { console.error(error.message); process.exit(2); } if (options.help) { usage(); return; } const report = buildReport(options); const outputReport = options.summary ? summarizeReport(report) : report; if (options.format === 'json') { console.log(JSON.stringify(outputReport, null, 2)); } else { process.stdout.write(renderText(report)); } process.exit(report.ready ? 0 : 1); } if (require.main === module) { main(); } module.exports = { REQUIRED_PUBLISH_CANDIDATES, REQUIRED_SOURCE_ASSETS, REQUIRED_SUITE_ARTIFACTS, buildReport, parseArgs, renderText, summarizeReport, };