diff --git a/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md b/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md index f0b81fcf..d9d19e46 100644 --- a/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md +++ b/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-19.md @@ -51,7 +51,7 @@ Tracked repositories in the platform audit were: | Release-surface tests | `node tests/docs/ecc2-release-surface.test.js` | 25 passed, 0 failed | | Preview-pack smoke | `npm run preview-pack:smoke -- --format json` | Ready true; digest `bc2bf157616e`; 30 required artifacts; 5 passed, 0 failed | | Operator dashboard | `npm run operator:dashboard -- --markdown --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Generated May 19 dashboard with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, and top actions for plugin publication, notifications, release video, outbound approval, AgentShield, and ECC Tools billing | -| Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; primary rough render is 144.759 seconds and 106.78 MB | +| Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; primary rough render self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, and 106.78 MB | | Full local suite | `node tests/run-all.js` | 2544 passed, 0 failed | | PR #1993 CI | GitHub Actions run `26093792219` | Completed successfully for `d9ac22c697d9a8a8771512ab01e6df857c16776d`; all reported checks passed, including lint, validation, security scan, coverage, GitGuardian, and the macOS/Ubuntu/Windows test matrix | | Public-path sanitization | `node scripts/ci/validate-no-personal-paths.js` through local suite and CI | Passed | diff --git a/docs/releases/2.0.0-rc.1/video-suite-production.md b/docs/releases/2.0.0-rc.1/video-suite-production.md index 60a86ebb..9ac68b0c 100644 --- a/docs/releases/2.0.0-rc.1/video-suite-production.md +++ b/docs/releases/2.0.0-rc.1/video-suite-production.md @@ -153,6 +153,8 @@ npm run release:video-suite -- --format json Then manually check the final render for: +- validator self-eval passes for the primary render: 90-150 seconds, at least + 1280x720, video stream present, audio stream present, and non-empty output; - no blank frames or accidental desktop exposure; - no stale repo name, pivot, rename, or Claude-only framing in captions; - no captions that rewrite speech into a false claim; diff --git a/scripts/release-video-suite.js b/scripts/release-video-suite.js index c19b9ca6..0a5f3c7f 100644 --- a/scripts/release-video-suite.js +++ b/scripts/release-video-suite.js @@ -371,8 +371,12 @@ function probeMedia(filePath, skipProbe) { 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) { @@ -383,7 +387,7 @@ function probeMedia(filePath, skipProbe) { '-v', 'error', '-show_entries', - 'format=duration', + 'format=duration:stream=codec_type,width,height', '-of', 'json', filePath, @@ -407,9 +411,19 @@ function probeMedia(filePath, skipProbe) { const duration = Number(parsed && parsed.format && parsed.format.duration); if (Number.isFinite(duration)) { result.durationSeconds = Number(duration.toFixed(3)); - result.probe = 'ok'; } + 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; } @@ -520,6 +534,69 @@ function inspectSuiteArtifacts(suiteRoot, 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) : ''; @@ -542,6 +619,7 @@ function buildReport(options = {}) { const suiteArtifacts = inspectSuiteArtifacts(suiteRoot, skipProbe); const missingSourceAssets = sourceAssets.filter(asset => asset.status !== 'present'); const missingSuiteArtifacts = suiteArtifacts.filter(artifact => artifact.status !== 'present'); + const primaryRenderSelfEval = evaluatePrimaryRender(suiteArtifacts, skipProbe); const checks = [ makeCheck( @@ -598,6 +676,12 @@ function buildReport(options = {}) { missing: missingSuiteArtifacts.map(artifact => artifact.relativePath), } ), + makeCheck( + 'video-primary-render-self-eval', + primaryRenderSelfEval.status, + primaryRenderSelfEval.summary, + primaryRenderSelfEval.fix + ), ]; const failed = checks.filter(check => check.status !== 'pass'); diff --git a/tests/scripts/release-video-suite.test.js b/tests/scripts/release-video-suite.test.js index 4cab6be2..a8a1169a 100644 --- a/tests/scripts/release-video-suite.test.js +++ b/tests/scripts/release-video-suite.test.js @@ -169,6 +169,10 @@ function runTests() { assert.strictEqual(report.ready, true); assert.strictEqual(report.mediaPathsRedacted, true); assert.ok(report.checks.every(check => check.status === 'pass')); + assert.ok(report.checks.some(check => ( + check.id === 'video-primary-render-self-eval' + && check.summary.includes('skipped by --skip-probe') + ))); assert.strictEqual(report.sourceAssets.length, REQUIRED_SOURCE_ASSETS.length); assert.strictEqual(report.suiteArtifacts.length, REQUIRED_SUITE_ARTIFACTS.length); assert.ok(renderText(report).includes('Ready: yes')); @@ -197,6 +201,7 @@ function runTests() { assert.ok(report.top_actions.some(action => action.includes('ECC_VIDEO_RELEASE_SUITE_ROOT'))); assert.ok(report.checks.some(check => check.id === 'video-source-assets-present' && check.status === 'fail')); assert.ok(report.checks.some(check => check.id === 'video-release-artifacts-present' && check.status === 'fail')); + assert.ok(report.checks.some(check => check.id === 'video-primary-render-self-eval' && check.status === 'fail')); } finally { cleanup(rootDir); }