mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-20 11:19:58 +08:00
1031 lines
29 KiB
JavaScript
1031 lines
29 KiB
JavaScript
#!/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,
|
|
},
|
|
];
|
|
|
|
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 <text|json> Output format (default: text)',
|
|
' --json Alias for --format json',
|
|
' --root <dir> Repository root to inspect (default: cwd)',
|
|
' --source-root <dir> Directory containing ECC 2 source media, with optional _edited subdir',
|
|
' --suite-root <dir> 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 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');
|
|
}
|
|
|
|
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) : {
|
|
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 and self-evaluable`
|
|
: `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,
|
|
};
|