From 855e8c8336e1c18523cbb31cb29f4ce96d7518a7 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 19 May 2026 08:40:48 -0400 Subject: [PATCH] chore: gate release video publish candidates --- .../publication-evidence-2026-05-19.md | 16 +- .../2.0.0-rc.1/video-suite-production.md | 23 ++ scripts/release-video-suite.js | 265 +++++++++++++++--- tests/scripts/release-video-suite.test.js | 12 + 4 files changed, 276 insertions(+), 40 deletions(-) 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 d9d19e46..9eff4949 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 @@ -8,9 +8,9 @@ social announcement. | Field | Evidence | | --- | --- | -| Upstream main | `c07276a347f8dac4945d2ad294124a708c19b108` | +| Upstream main | `f3cd00625222fceedca00164b828db8803fe52d6` | | Git remote | `https://github.com/affaan-m/ECC.git` | -| Evidence scope | Current `main` after PR #1990 harness-audit GitHub integration scoring, PR #1991 canonical ECC identity gate, PR #1992 release video-suite gate, PR #1993 growth outreach pack, and PR #1994 May 19 publication evidence refresh | +| Evidence scope | Current `main` after PR #1990 harness-audit GitHub integration scoring, PR #1991 canonical ECC identity gate, PR #1992 release video-suite gate, PR #1993 growth outreach pack, PR #1994 May 19 publication evidence refresh, PR #1995 operator dashboard refresh, and PR #1996 primary render self-eval gate | | Local status caveat | `git status --short --branch` was clean after pulling `origin/main`; generated evidence files are committed after the source snapshot they describe | The release operator must repeat all publish-facing checks from the exact final @@ -43,6 +43,8 @@ Tracked repositories in the platform audit were: | PR #1992 | Merged the release video-suite gate, production manifest, validator, package file surface, preview-pack smoke wiring, release-surface tests, and compact CI JSON output | | PR #1993 | Merged the partner, sponsor, consulting, conference, podcast, GitHub Discussion, and video CTA pack for the hypergrowth outbound lane | | PR #1994 | Merged the May 19 publication-evidence refresh, platform-audit evidence gate, preview-pack smoke evidence gate, and URL/readiness/roadmap references | +| PR #1995 | Merged the May 19 operator dashboard refresh with the `$1,728/mo` MRR baseline, `$10,000/mo` target, and release/video/outbound top actions | +| PR #1996 | Merged the primary launch render self-eval gate for duration, size, resolution, video stream, and audio stream checks | ## Release And Growth Evidence @@ -51,9 +53,9 @@ 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 self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, 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; 12/12 publish-candidate outputs 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 | +| PR #1996 CI | GitHub Actions run `26096847138` | Completed successfully for `f3cd00625222fceedca00164b828db8803fe52d6`; all reported checks passed, including lint, validation, security scan, coverage, GitGuardian, CodeRabbit, Cubic, and the macOS/Ubuntu/Windows test matrix | | Public-path sanitization | `node scripts/ci/validate-no-personal-paths.js` through local suite and CI | Passed | | Markdown and whitespace | `markdownlint` focused release docs plus `git diff --check` before PR #1993 | Passed | @@ -63,7 +65,7 @@ Tracked repositories in the platform audit were: | --- | --- | | Canonical repo identity | Public URLs and release docs now use `https://github.com/affaan-m/ECC` where public links are needed | | Release claim | Release notes and launch collateral frame ECC as the harness-native operator system for agentic work, not a Claude-only config pack | -| Video proof | `video-suite-production.md` gates the local rough render, timeline, captions, source inventory, self-eval, and no-private-path publication rules | +| Video proof | `video-suite-production.md` gates the local rough render, timeline, captions, source inventory, publish-candidate clip set, self-eval, and no-private-path publication rules | | Growth proof | `partner-sponsor-talks-pack.md` provides approval-gated copy for sponsors, partners, consulting, talks, podcasts, GitHub Discussion, and video CTAs | | Business baseline | Hypergrowth command center and partner pack use `$1,728/mo` current MRR, `$10,000/mo` target MRR, and `$8,272/mo` gap | | Operator dashboard | `operator-readiness-dashboard-2026-05-19.md` pulls the growth baseline into the same queue, publication, video, outbound, AgentShield, ECC Tools, Linear, and supply-chain control surface | @@ -92,8 +94,8 @@ Tracked repositories in the platform audit were: The tracked public PR queue, issue queue, discussion queue, canonical ECC identity, release video suite, preview pack, and growth outreach packet are current on May 19, 2026 for `main` through -`c07276a347f8dac4945d2ad294124a708c19b108`, with the May 19 dashboard -refresh staged for the next merge. +`f3cd00625222fceedca00164b828db8803fe52d6`, with the publish-candidate +video gate staged for the next merge. This improves publication readiness but does not replace the approval-gated release, package, plugin, billing, Discord, and announcement steps in 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 9ac68b0c..ee2dc24e 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 @@ -106,6 +106,27 @@ Required local rough v1 artifacts: - `segments/primary-launch-v1/08-oss-paid-model.mp4` - `segments/primary-launch-v1/09-close-shipping-system.mp4` +## Publish-Candidate Outputs + +The release validator also expects the current publish-candidate set under +`renders/publish-candidates/`. These are still local review files, not public +uploads or committed media. + +| Output | Target | +| --- | --- | +| `ecc-2-primary-launch.mp4` | 90-150s, 1920x1080, audio | +| `ecc-2-primary-launch.captions.srt` | primary captions | +| `ecc-2-install-proof-wide.mp4` | 25-35s, 1920x1080, audio | +| `ecc-2-install-proof-vertical.mp4` | 25-35s, 1080x1920, audio | +| `ecc-2-what-is-ecc-wide.mp4` | 45-60s, 1920x1080, audio | +| `ecc-2-what-is-ecc-vertical.mp4` | 45-60s, 1080x1920, audio | +| `ecc-2-security-proof-wide.mp4` | 45-60s, 1920x1080, audio | +| `ecc-2-security-proof-vertical.mp4` | 45-60s, 1080x1920, audio | +| `ecc-2-money-proof-wide.mp4` | 30-45s, 1920x1080, audio | +| `ecc-2-money-proof-vertical.mp4` | 30-45s, 1080x1920, audio | +| `ecc-2-social-proof-wide.mp4` | 30-45s, 1920x1080, audio | +| `ecc-2-social-proof-vertical.mp4` | 30-45s, 1080x1920, audio | + ## video-use compatible workflow Use the same production shape as Video Use while keeping the ECC-specific media @@ -155,6 +176,8 @@ 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; +- validator self-eval passes for the publish-candidate set: primary MP4 plus + captions and five short clips in both wide and vertical formats; - 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 0a5f3c7f..d4abb8d3 100644 --- a/scripts/release-video-suite.js +++ b/scripts/release-video-suite.js @@ -183,6 +183,135 @@ const REQUIRED_SUITE_ARTIFACTS = [ }, ]; +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]', @@ -471,22 +600,83 @@ function inspectSourceAssets(sourceRoot, skipProbe) { }); } -function inspectSuiteArtifacts(suiteRoot, skipProbe) { - return REQUIRED_SUITE_ARTIFACTS.map(artifact => { - if (!suiteRoot) { +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(suiteRoot, artifact.relativePath); + const filePath = path.join(rootDir, artifact.relativePath); if (!fs.existsSync(filePath)) { return { ...artifact, status: 'missing', configured: true, + validationFailures: [], }; } @@ -496,44 +686,26 @@ function inspectSuiteArtifacts(suiteRoot, skipProbe) { durationSeconds: null, probe: 'not-media', }; - - let durationStatus = 'pass'; - if ( - artifact.kind === 'video' - && Number.isFinite(artifact.minDurationSeconds) - && Number.isFinite(media.durationSeconds) - && media.durationSeconds < artifact.minDurationSeconds - ) { - durationStatus = 'fail'; - } - - if ( - artifact.kind === 'video' - && Number.isFinite(artifact.maxDurationSeconds) - && Number.isFinite(media.durationSeconds) - && media.durationSeconds > artifact.maxDurationSeconds - ) { - durationStatus = 'fail'; - } - - if ( - artifact.kind === 'video' - && Number.isFinite(artifact.minDurationSeconds) - && !skipProbe - && media.durationSeconds === null - ) { - durationStatus = 'fail'; - } + const validationFailures = validateVideoArtifact(artifact, media, skipProbe); return { ...artifact, - status: durationStatus === 'pass' ? 'present' : 'invalid', + 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'); @@ -617,8 +789,10 @@ function buildReport(options = {}) { ]); 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 = [ @@ -682,6 +856,23 @@ function buildReport(options = {}) { 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'); @@ -713,6 +904,7 @@ function buildReport(options = {}) { checks, sourceAssets, suiteArtifacts, + publishCandidates, top_actions: topActions, }; } @@ -748,6 +940,7 @@ function summarizeReport(report) { })), sourceAssetSummary: summarizeItems(report.sourceAssets), suiteArtifactSummary: summarizeItems(report.suiteArtifacts), + publishCandidateSummary: summarizeItems(report.publishCandidates), primaryRender: primaryRender ? { status: primaryRender.status, durationSeconds: primaryRender.durationSeconds, @@ -780,6 +973,11 @@ function renderText(report) { ); } + 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:'); @@ -822,6 +1020,7 @@ if (require.main === module) { } module.exports = { + REQUIRED_PUBLISH_CANDIDATES, REQUIRED_SOURCE_ASSETS, REQUIRED_SUITE_ARTIFACTS, buildReport, diff --git a/tests/scripts/release-video-suite.test.js b/tests/scripts/release-video-suite.test.js index a8a1169a..a39f2f8c 100644 --- a/tests/scripts/release-video-suite.test.js +++ b/tests/scripts/release-video-suite.test.js @@ -10,6 +10,7 @@ const { execFileSync, spawnSync } = require('child_process'); const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'release-video-suite.js'); const { + REQUIRED_PUBLISH_CANDIDATES, REQUIRED_SOURCE_ASSETS, REQUIRED_SUITE_ARTIFACTS, buildReport, @@ -75,6 +76,10 @@ function seedMedia(sourceRoot, suiteRoot) { for (const artifact of REQUIRED_SUITE_ARTIFACTS) { writeFile(suiteRoot, artifact.relativePath, `artifact ${artifact.id}`); } + + for (const candidate of REQUIRED_PUBLISH_CANDIDATES) { + writeFile(suiteRoot, candidate.relativePath, `candidate ${candidate.id}`); + } } function run(args = [], options = {}) { @@ -175,8 +180,13 @@ function runTests() { ))); assert.strictEqual(report.sourceAssets.length, REQUIRED_SOURCE_ASSETS.length); assert.strictEqual(report.suiteArtifacts.length, REQUIRED_SUITE_ARTIFACTS.length); + assert.strictEqual(report.publishCandidates.length, REQUIRED_PUBLISH_CANDIDATES.length); assert.ok(renderText(report).includes('Ready: yes')); assert.strictEqual(summarizeReport(report).sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length); + assert.strictEqual( + summarizeReport(report).publishCandidateSummary.present, + REQUIRED_PUBLISH_CANDIDATES.length + ); } finally { cleanup(rootDir); cleanup(sourceRoot); @@ -202,6 +212,7 @@ function runTests() { 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')); + assert.ok(report.checks.some(check => check.id === 'video-publish-candidates-present' && check.status === 'fail')); } finally { cleanup(rootDir); } @@ -269,6 +280,7 @@ function runTests() { assert.strictEqual(parsed.suiteRootConfigured, true); assert.strictEqual(parsed.sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length); assert.strictEqual(parsed.suiteArtifactSummary.present, REQUIRED_SUITE_ARTIFACTS.length); + assert.strictEqual(parsed.publishCandidateSummary.present, REQUIRED_PUBLISH_CANDIDATES.length); } finally { cleanup(rootDir); cleanup(sourceRoot);