chore: gate ECC release video suite (#1992)

This commit is contained in:
Affaan Mustafa
2026-05-19 07:13:52 -04:00
committed by GitHub
parent 8141f6904f
commit e209afc8c1
10 changed files with 1295 additions and 2 deletions

View File

@@ -37,6 +37,10 @@
- publish the LinkedIn draft from `linkedin-post.md` - publish the LinkedIn draft from `linkedin-post.md`
- use `article-outline.md` for the longer writeup - use `article-outline.md` for the longer writeup
- record one 30-60 second proof-of-work clip - record one 30-60 second proof-of-work clip
- validate the release video suite with `npm run release:video-suite -- --format json`
after setting `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT`
- keep `video-suite-production.md` aligned with the actual primary launch
render, timeline, captions, and self-eval gate
## Demo Asset Suggestions ## Demo Asset Suggestions

View File

@@ -28,6 +28,7 @@ surfaces, or posting announcements.
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-17.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 18 generated dashboard | | `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-17.md` | Previous prompt-to-artifact operator dashboard | Superseded by the May 18 generated dashboard |
| `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md` | Current prompt-to-artifact operator dashboard | Shows PR/issue/discussion/platform/supply-chain gates current and publication, plugin, billing, AgentShield, ECC Tools, legacy, and Linear productization gaps still open | | `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-18.md` | Current prompt-to-artifact operator dashboard | Shows PR/issue/discussion/platform/supply-chain gates current and publication, plugin, billing, AgentShield, ECC Tools, legacy, and Linear productization gaps still open |
| `docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md` | Live URL and approval-gated URL ledger for release copy | Must be regenerated from the final release commit before public announcements | | `docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md` | Live URL and approval-gated URL ledger for release copy | Must be regenerated from the final release commit before public announcements |
| `docs/releases/2.0.0-rc.1/video-suite-production.md` | Release video production manifest | Gates local media inventory, rough primary render, captions, timeline, self-eval, and no-private-path publication rules |
| `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` | Naming, slug, and publication-path decision record | Keeps `ECC`, npm `ecc-universal`, and plugin slug `ecc` for rc.1 | | `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` | Naming, slug, and publication-path decision record | Keeps `ECC`, npm `ecc-universal`, and plugin slug `ecc` for rc.1 |
| `docs/releases/2.0.0-rc.1/release-name-plugin-publication-checklist-2026-05-18.md` | Release name, package, Claude plugin, Codex plugin, and publication-order checklist | Freezes rc.1 identity and requires final commit evidence before release, npm, plugin, billing, or announcement actions | | `docs/releases/2.0.0-rc.1/release-name-plugin-publication-checklist-2026-05-18.md` | Release name, package, Claude plugin, Codex plugin, and publication-order checklist | Freezes rc.1 identity and requires final commit evidence before release, npm, plugin, billing, or announcement actions |
| `docs/releases/2.0.0-rc.1/x-thread.md` | X launch draft | Must replace placeholders with live URLs after release/package/plugin publication | | `docs/releases/2.0.0-rc.1/x-thread.md` | X launch draft | Must replace placeholders with live URLs after release/package/plugin publication |
@@ -75,6 +76,7 @@ Run these from the exact release commit before publication:
git status --short --branch git status --short --branch
node scripts/platform-audit.js --json node scripts/platform-audit.js --json
npm run preview-pack:smoke npm run preview-pack:smoke
npm run release:video-suite -- --format json
npm run harness:adapters -- --check npm run harness:adapters -- --check
npm run harness:audit -- --format json npm run harness:audit -- --format json
npm run observability:ready npm run observability:ready

View File

@@ -0,0 +1,173 @@
# ECC 2.0 Video Suite Production Manifest
Snapshot date: 2026-05-19.
This is the production contract for the ECC 2.0 release video suite. It keeps
the public release story, local source inventory, render outputs, and self-eval
gate in one place without committing raw footage, private transcript exports, or
absolute local paths.
## Claim
ECC 2.0 is the harness-native operator system for agentic work.
The videos should prove that claim directly:
- one reusable layer across Claude Code, Codex, OpenCode, Cursor, Gemini, Zed,
GitHub Copilot, and terminal workflows;
- reusable skills, rules, hooks, agents, MCP conventions, release gates, and
operator workflows;
- `ecc2/` as the alpha control-plane/TUI direction, not the whole product;
- AgentShield and supply-chain gates as the enterprise trust layer;
- OSS stays free, with GitHub Sponsors, ECC Tools Pro, and consulting as the
funding surface.
Do not frame the launch as a rename, pivot, config pack, or Claude-only package.
## Private Inputs
Do not commit raw footage, transcript JSON, or timeline exports.
Operators should point the validator at local media using environment variables:
```bash
ECC_VIDEO_SOURCE_ROOT=/path/to/ecc_2_raws \
ECC_VIDEO_RELEASE_SUITE_ROOT=/path/to/ecc_2_release_suite \
npm run release:video-suite -- --format json
```
`ECC_VIDEO_SOURCE_ROOT` should contain proof images and may contain an `_edited/`
subdirectory with edited source clips. `ECC_VIDEO_RELEASE_SUITE_ROOT` should
contain `edl/`, `segments/`, `renders/`, `timelines/`, and `transcripts/`.
## Source Inventory
These basenames are the required local inputs for the release suite validator.
| Asset | Lane | Proof |
| --- | --- | --- |
| `longform-full-wide.mp4` | Primary launch video | operator system, control-plane direction, closing proof |
| `sf-longform-full.mp4` | Primary launch video | structured context opener |
| `sf-thread-2-whatisecc.mp4` | What is ECC | category clarity and GitHub App explanation |
| `sf-thread-4-security.mp4` | Security proof | AgentShield, hooks, MCP, permission risk |
| `thread-2-ghapp-money.mp4` | Money/proof clip | OSS plus paid hosting and services |
| `architecture-2-wide.mp4` | B-roll | harness-native architecture |
| `terminal-scan-2-wide.mp4` | Install proof | terminal workflow and install confidence |
| `new_site_raw.mp4` | B-roll | site and product surface |
| `coverage-montage-wide.mp4` | Coverage/social proof | distribution and social proof |
| `metrics-ticker-2-wide.mp4` | Money/proof clip | traction and funnel proof |
| `growth-timeline-2-wide.mp4` | Coverage/social proof | release momentum timeline |
| `gh_app_1.png` | Money/proof clip | hosted GitHub App surface |
| `star_history.png` | Coverage/social proof | OSS adoption chart |
| `x_analytics.png` | Coverage/social proof | social distribution proof |
| `100k.png` | Coverage/social proof | reach milestone proof |
## Deliverables
| Deliverable | Length | Aspect | Output |
| --- | ---: | --- | --- |
| Primary launch video | 90-150s | 16:9 | `ecc-2-primary-launch.mp4` |
| Install proof clip | 25-35s | 16:9 and 9:16 | `ecc-2-install-proof-*` |
| What is ECC clip | 45-60s | 16:9 and 9:16 | `ecc-2-what-is-ecc-*` |
| Security proof clip | 45-60s | 16:9 and 9:16 | `ecc-2-security-proof-*` |
| Money/proof clip | 30-45s | 16:9 and 9:16 | `ecc-2-money-proof-*` |
| Coverage/social proof clip | 30-45s | 16:9 and 9:16 | `ecc-2-social-proof-*` |
## Primary Launch Video
The rough v1 primary launch assembly is the current spine. It should stay
speech-led, with product proof covering jump cuts and older wording.
| Order | Source | In | Out | Use |
| --- | --- | ---: | ---: | --- |
| 01 | `sf-longform-full.mp4` | 161.12 | 177.68 | Cleaner opener: ECC as structured context with skills, commands, agents, hooks, and project setup. |
| 02 | `thread-2-ghapp-money.mp4` | 21.84 | 30.40 | Direct product thesis: agentic harness optimization. |
| 03 | `thread-2-ghapp-money.mp4` | 41.00 | 59.72 | Not another harness; ECC is the layer and tooling on top of harnesses. |
| 04 | `longform-full-wide.mp4` | 254.60 | 271.20 | Agentic IDE, observability, tracing, and multi-agent control-plane direction. |
| 05 | `sf-thread-2-whatisecc.mp4` | 40.08 | 60.60 | GitHub App analyzes repos and injects project-specific skills, prompts, and hooks. |
| 06 | `sf-thread-4-security.mp4` | 17.60 | 32.72 | Security risk setup: hooks, MCP servers, permissions. |
| 07 | `sf-thread-4-security.mp4` | 37.28 | 51.32 | AgentShield proof: rules, categories, grades, secrets, injection, exfiltration. |
| 08 | `thread-2-ghapp-money.mp4` | 59.72 | 75.96 | OSS-first business model plus managed GitHub App surface. |
| 09 | `longform-full-wide.mp4` | 507.34 | 525.62 | Close on workflows, tested shipping, and secure daily agent work. |
Required local rough v1 artifacts:
- `edl/primary-launch.edl.md`
- `timelines/primary-launch-v1.timeline.json`
- `renders/ecc-2-primary-launch-rough-v1.mp4`
- `renders/ecc-2-primary-launch-rough-v1.captions.srt`
- `segments/primary-launch-v1/01-structured-context.mp4`
- `segments/primary-launch-v1/02-agentic-harness-optimization.mp4`
- `segments/primary-launch-v1/03-not-another-harness.mp4`
- `segments/primary-launch-v1/04-agentic-ide-surface.mp4`
- `segments/primary-launch-v1/05-github-app-proof.mp4`
- `segments/primary-launch-v1/06-security-risk.mp4`
- `segments/primary-launch-v1/07-agentshield-proof.mp4`
- `segments/primary-launch-v1/08-oss-paid-model.mp4`
- `segments/primary-launch-v1/09-close-shipping-system.mp4`
## video-use compatible workflow
Use the same production shape as Video Use while keeping the ECC-specific media
stack intact:
1. Treat transcript and timeline data as the editing surface.
2. Inspect filmstrip or frame samples only at ambiguous cut points.
3. Keep an edit decision list before rendering.
4. Cut deterministically with FFmpeg.
5. Add proof overlays with Remotion or Manim where product claims need visual
evidence.
6. Export the MP4 plus editable timeline and caption state.
7. Run self-eval before any upload or social post.
Do not dump frames into the repo. Frame samples used for self-eval belong in the
local release suite workspace.
## Browser Capture Plan
Use Browser or equivalent desktop capture only for proof footage that must be
current on release day:
| Surface | Capture |
| --- | --- |
| GitHub repo | README hero, install block, sponsor links, release notes |
| Codex plugin | repo marketplace install path and local plugin README |
| OpenCode package | package install and plugin banner |
| ECC Tools Pro | billing/product page only after live readback confirms claims |
| AgentShield | CLI output, policy category view, supply-chain gate |
| `ecc2/` | alpha control-plane/TUI surface with alpha framing |
If a surface is not live, use a local browser capture and label it as local or
release-candidate proof. Do not claim marketplace, billing, or official
directory availability before evidence exists.
## Self-Eval Gate
Run the validator:
```bash
ECC_VIDEO_SOURCE_ROOT=/path/to/ecc_2_raws \
ECC_VIDEO_RELEASE_SUITE_ROOT=/path/to/ecc_2_release_suite \
npm run release:video-suite -- --format json
```
Then manually check the final render for:
- 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;
- no stale URLs, old install commands, or pre-rename repository links;
- no internal MRR numbers unless the post explicitly needs them;
- audio continuity across every cut;
- first 10 seconds clearly say what ECC is;
- final CTA routes to repo, sponsor, Pro, or consulting without clutter.
## Do Not Publish If
- `npm run release:video-suite` is not ready for the local source roots.
- The primary launch render is outside the 90-150 second target.
- Captions mention the old repository name.
- Product proof relies on private screens, secrets, customer data, or raw local
paths.
- The release URL, npm, plugin, billing, or marketplace claims outrun the
evidence in `publication-readiness.md`.

View File

@@ -50,7 +50,7 @@ MRR growth should come from four lanes at once:
| Package and plugin publication | `ecc-universal@2.0.0-rc.1` dry-runs clean, npm `next` is approved, Claude plugin tag dry-runs, Codex repo marketplace smoke passes, OpenCode build passes | Refresh publication evidence from final commit | | Package and plugin publication | `ecc-universal@2.0.0-rc.1` dry-runs clean, npm `next` is approved, Claude plugin tag dry-runs, Codex repo marketplace smoke passes, OpenCode build passes | Refresh publication evidence from final commit |
| Product proof | Quickstart, cross-harness architecture, demo prompts, `ecc2/` alpha boundary, AgentShield safety proof, and hosted ECC Tools links are consistent | Keep proof surfaces concrete | | Product proof | Quickstart, cross-harness architecture, demo prompts, `ecc2/` alpha boundary, AgentShield safety proof, and hosted ECC Tools links are consistent | Keep proof surfaces concrete |
| Revenue proof | Sponsor tiers, Pro pricing, consulting CTA, partner CTA, and billing-readback language are current | Do not announce billing claims before live readback | | Revenue proof | Sponsor tiers, Pro pricing, consulting CTA, partner CTA, and billing-readback language are current | Do not announce billing claims before live readback |
| Content proof | Launch video, short-form clips, screenshots, release notes, GitHub Discussion, X, LinkedIn, and longform post are aligned | Produce video suite from existing raw material | | Content proof | Launch video, short-form clips, screenshots, release notes, GitHub Discussion, X, LinkedIn, and longform post are aligned | Validate `video-suite-production.md` and the local render suite |
| Community proof | Discord invite, rules, channels, onboarding, and sponsor/community routing are ready | Needs invite/token decision before public links | | Community proof | Discord invite, rules, channels, onboarding, and sponsor/community routing are ready | Needs invite/token decision before public links |
## Video Suite ## Video Suite
@@ -124,7 +124,8 @@ Avoid:
1. Land the public repo identity fixes. 1. Land the public repo identity fixes.
2. Refresh package, plugin, workflow, release, and launch-copy URLs. 2. Refresh package, plugin, workflow, release, and launch-copy URLs.
3. Record final publication evidence from the exact release commit. 3. Record final publication evidence from the exact release commit.
4. Produce the video suite manifest and transcripts from existing raw material. 4. Produce the video suite manifest and transcripts from existing raw material;
gate it with `npm run release:video-suite -- --format json`.
5. Browser-capture the README, ECC Tools app, install flow, and relevant proof 5. Browser-capture the README, ECC Tools app, install flow, and relevant proof
surfaces for b-roll. surfaces for b-roll.
6. Render the primary launch video plus five short clips. 6. Render the primary launch video plus five short clips.

View File

@@ -89,6 +89,7 @@
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
"scripts/preview-pack-smoke.js", "scripts/preview-pack-smoke.js",
"scripts/release-video-suite.js",
"scripts/hooks/", "scripts/hooks/",
"scripts/install-apply.js", "scripts/install-apply.js",
"scripts/install-plan.js", "scripts/install-plan.js",
@@ -311,6 +312,7 @@
"observability:ready": "node scripts/observability-readiness.js", "observability:ready": "node scripts/observability-readiness.js",
"operator:dashboard": "node scripts/operator-readiness-dashboard.js", "operator:dashboard": "node scripts/operator-readiness-dashboard.js",
"preview-pack:smoke": "node scripts/preview-pack-smoke.js", "preview-pack:smoke": "node scripts/preview-pack-smoke.js",
"release:video-suite": "node scripts/release-video-suite.js",
"platform:audit": "node scripts/platform-audit.js", "platform:audit": "node scripts/platform-audit.js",
"discussion:audit": "node scripts/discussion-audit.js", "discussion:audit": "node scripts/discussion-audit.js",
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js", "security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",

View File

@@ -29,6 +29,7 @@ const REQUIRED_ARTIFACTS = [
`${RELEASE_DIR}/operator-readiness-dashboard-2026-05-17.md`, `${RELEASE_DIR}/operator-readiness-dashboard-2026-05-17.md`,
`${RELEASE_DIR}/operator-readiness-dashboard-2026-05-18.md`, `${RELEASE_DIR}/operator-readiness-dashboard-2026-05-18.md`,
`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`, `${RELEASE_DIR}/release-url-ledger-2026-05-19.md`,
`${RELEASE_DIR}/video-suite-production.md`,
`${RELEASE_DIR}/naming-and-publication-matrix.md`, `${RELEASE_DIR}/naming-and-publication-matrix.md`,
`${RELEASE_DIR}/release-name-plugin-publication-checklist-2026-05-18.md`, `${RELEASE_DIR}/release-name-plugin-publication-checklist-2026-05-18.md`,
`${RELEASE_DIR}/x-thread.md`, `${RELEASE_DIR}/x-thread.md`,
@@ -42,6 +43,7 @@ const REQUIRED_VERIFICATION_COMMANDS = [
'git status --short --branch', 'git status --short --branch',
'node scripts/platform-audit.js --json', 'node scripts/platform-audit.js --json',
'npm run preview-pack:smoke', 'npm run preview-pack:smoke',
'npm run release:video-suite -- --format json',
'npm run harness:adapters -- --check', 'npm run harness:adapters -- --check',
'npm run harness:audit -- --format json', 'npm run harness:audit -- --format json',
'npm run observability:ready', 'npm run observability:ready',

View File

@@ -0,0 +1,747 @@
#!/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',
},
];
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),
durationSeconds: null,
probe: skipProbe ? 'skipped' : 'unavailable',
};
if (skipProbe) {
return result;
}
const probe = spawnSync('ffprobe', [
'-v',
'error',
'-show_entries',
'format=duration',
'-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));
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 inspectSuiteArtifacts(suiteRoot, skipProbe) {
return REQUIRED_SUITE_ARTIFACTS.map(artifact => {
if (!suiteRoot) {
return {
...artifact,
status: 'missing',
configured: false,
};
}
const filePath = path.join(suiteRoot, artifact.relativePath);
if (!fs.existsSync(filePath)) {
return {
...artifact,
status: 'missing',
configured: true,
};
}
const media = artifact.kind === 'video' ? probeMedia(filePath, skipProbe) : {
sizeBytes: fs.statSync(filePath).size,
sizeMb: formatBytes(fs.statSync(filePath).size),
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';
}
return {
...artifact,
status: durationStatus === 'pass' ? 'present' : 'invalid',
configured: true,
...media,
};
});
}
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 missingSourceAssets = sourceAssets.filter(asset => asset.status !== 'present');
const missingSuiteArtifacts = suiteArtifacts.filter(artifact => artifact.status !== 'present');
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),
}
),
];
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,
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),
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.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_SOURCE_ASSETS,
REQUIRED_SUITE_ARTIFACTS,
buildReport,
parseArgs,
renderText,
summarizeReport,
};

View File

@@ -52,6 +52,7 @@ const expectedReleaseFiles = [
'quickstart.md', 'quickstart.md',
'preview-pack-manifest.md', 'preview-pack-manifest.md',
'publication-readiness.md', 'publication-readiness.md',
'video-suite-production.md',
'release-name-plugin-publication-checklist-2026-05-18.md', 'release-name-plugin-publication-checklist-2026-05-18.md',
]; ];
@@ -176,6 +177,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
'docs/releases/2.0.0-rc.1/publication-readiness.md', 'docs/releases/2.0.0-rc.1/publication-readiness.md',
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md', 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md', 'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md',
'docs/releases/2.0.0-rc.1/video-suite-production.md',
'docs/releases/2.0.0-rc.1/release-name-plugin-publication-checklist-2026-05-18.md', 'docs/releases/2.0.0-rc.1/release-name-plugin-publication-checklist-2026-05-18.md',
]) { ]) {
assert.ok(manifest.includes(artifact), `preview pack manifest missing ${artifact}`); assert.ok(manifest.includes(artifact), `preview pack manifest missing ${artifact}`);
@@ -194,6 +196,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
assert.ok(manifest.includes('no raw workspace exports')); assert.ok(manifest.includes('no raw workspace exports'));
assert.ok(manifest.includes('Final Verification Commands')); assert.ok(manifest.includes('Final Verification Commands'));
assert.ok(manifest.includes('npm run preview-pack:smoke')); assert.ok(manifest.includes('npm run preview-pack:smoke'));
assert.ok(manifest.includes('npm run release:video-suite -- --format json'));
assert.ok(manifest.includes('Reference-Inspired Adapter Direction')); assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
}); });
@@ -231,6 +234,60 @@ test('launch checklist records the ecc2 alpha version policy', () => {
assert.ok(!launchChecklist.includes('confirm whether `ecc2/Cargo.toml` moves')); assert.ok(!launchChecklist.includes('confirm whether `ecc2/Cargo.toml` moves'));
}); });
test('release video suite manifest gates the content launch lane', () => {
const videoManifest = read('docs/releases/2.0.0-rc.1/video-suite-production.md');
const launchChecklist = read('docs/releases/2.0.0-rc.1/launch-checklist.md');
const hypergrowth = read('docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md');
const packageJson = JSON.parse(read('package.json'));
for (const marker of [
'ECC 2.0 Video Suite Production Manifest',
'ECC_VIDEO_SOURCE_ROOT',
'ECC_VIDEO_RELEASE_SUITE_ROOT',
'video-use compatible workflow',
'Self-Eval Gate',
'Do Not Publish If',
'renders/ecc-2-primary-launch-rough-v1.mp4',
'timelines/primary-launch-v1.timeline.json',
'Primary launch video',
]) {
assert.ok(videoManifest.includes(marker), `video suite manifest missing ${marker}`);
}
for (const asset of [
'longform-full-wide.mp4',
'sf-thread-2-whatisecc.mp4',
'thread-2-ghapp-money.mp4',
'coverage-montage-wide.mp4',
'star_history.png',
'x_analytics.png',
]) {
assert.ok(videoManifest.includes(asset), `video suite manifest missing asset ${asset}`);
}
assert.ok(launchChecklist.includes('npm run release:video-suite -- --format json'));
assert.ok(hypergrowth.includes('Validate `video-suite-production.md`'));
assert.strictEqual(packageJson.scripts['release:video-suite'], 'node scripts/release-video-suite.js');
assert.ok(packageJson.files.includes('scripts/release-video-suite.js'));
});
test('release video suite public docs do not expose private media paths', () => {
const releaseVideoDocs = [
'docs/releases/2.0.0-rc.1/video-suite-production.md',
'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md',
];
const offenders = [];
for (const relativePath of releaseVideoDocs) {
const source = read(relativePath);
if (/\/Users\/[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/.test(source)) {
offenders.push(relativePath);
}
}
assert.deepStrictEqual(offenders, []);
});
test('publication readiness checklist gates public release actions on evidence', () => { test('publication readiness checklist gates public release actions on evidence', () => {
const source = read('docs/releases/2.0.0-rc.1/publication-readiness.md'); const source = read('docs/releases/2.0.0-rc.1/publication-readiness.md');
const may15Evidence = read('docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md'); const may15Evidence = read('docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md');

View File

@@ -60,6 +60,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
"scripts/preview-pack-smoke.js", "scripts/preview-pack-smoke.js",
"scripts/release-video-suite.js",
"scripts/skill-create-output.js", "scripts/skill-create-output.js",
"scripts/repair.js", "scripts/repair.js",
"scripts/harness-adapter-compliance.js", "scripts/harness-adapter-compliance.js",
@@ -131,6 +132,7 @@ function main() {
"scripts/discussion-audit.js", "scripts/discussion-audit.js",
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/preview-pack-smoke.js", "scripts/preview-pack-smoke.js",
"scripts/release-video-suite.js",
"scripts/work-items.js", "scripts/work-items.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
".gemini/GEMINI.md", ".gemini/GEMINI.md",

View File

@@ -0,0 +1,303 @@
/**
* Tests for scripts/release-video-suite.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'release-video-suite.js');
const {
REQUIRED_SOURCE_ASSETS,
REQUIRED_SUITE_ARTIFACTS,
buildReport,
parseArgs,
renderText,
summarizeReport,
} = require(SCRIPT);
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content = 'fixture') {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function seedRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
name: 'ecc-universal',
files: ['scripts/release-video-suite.js'],
scripts: {
'release:video-suite': 'node scripts/release-video-suite.js',
},
}, null, 2),
'docs/releases/2.0.0-rc.1/video-suite-production.md': [
'# ECC 2.0 Video Suite Production Manifest',
'ECC_VIDEO_SOURCE_ROOT',
'ECC_VIDEO_RELEASE_SUITE_ROOT',
'Primary launch video',
'video-use compatible workflow',
'Self-Eval Gate',
'Do Not Publish If',
'Do not commit raw footage, transcript JSON, or timeline exports',
].join('\n'),
'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md': [
'Keep raw absolute paths out of public docs',
'Validate `video-suite-production.md`',
].join('\n'),
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'video-suite-production.md',
'docs/releases/2.0.0-rc.1/launch-checklist.md': 'release video suite',
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function seedMedia(sourceRoot, suiteRoot) {
for (const asset of REQUIRED_SOURCE_ASSETS) {
writeFile(sourceRoot, asset.file, `source ${asset.id}`);
}
for (const artifact of REQUIRED_SUITE_ARTIFACTS) {
writeFile(suiteRoot, artifact.relativePath, `artifact ${artifact.id}`);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing release-video-suite.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts release video flags and rejects invalid values', () => {
const rootDir = createTempDir('release-video-args-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
const parsed = parseArgs([
'node',
'script',
'--json',
`--root=${rootDir}`,
'--source-root',
sourceRoot,
`--suite-root=${suiteRoot}`,
'--skip-probe',
'--summary',
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.strictEqual(parsed.sourceRoot, path.resolve(sourceRoot));
assert.strictEqual(parsed.suiteRoot, path.resolve(suiteRoot));
assert.strictEqual(parsed.skipProbe, true);
assert.strictEqual(parsed.summary, true);
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--source-root']), /--source-root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('buildReport passes with a sanitized manifest and complete local media fixture', () => {
const rootDir = createTempDir('release-video-report-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
seedRepo(rootDir);
seedMedia(sourceRoot, suiteRoot);
const report = buildReport({
root: rootDir,
sourceRoot,
suiteRoot,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.schema_version, 'ecc.release-video-suite.v1');
assert.strictEqual(report.ready, true);
assert.strictEqual(report.mediaPathsRedacted, true);
assert.ok(report.checks.every(check => check.status === 'pass'));
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'));
assert.strictEqual(summarizeReport(report).sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('missing local roots keep the release video gate blocked', () => {
const rootDir = createTempDir('release-video-missing-roots-');
try {
seedRepo(rootDir);
const report = buildReport({
root: rootDir,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.ready, false);
assert.ok(report.top_actions.some(action => action.includes('ECC_VIDEO_SOURCE_ROOT')));
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'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('private media paths in public docs fail sanitization', () => {
const rootDir = createTempDir('release-video-private-path-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
seedRepo(rootDir, {
'docs/releases/2.0.0-rc.1/video-suite-production.md': [
'# ECC 2.0 Video Suite Production Manifest',
'ECC_VIDEO_SOURCE_ROOT',
'ECC_VIDEO_RELEASE_SUITE_ROOT',
'Primary launch video',
'video-use compatible workflow',
'Self-Eval Gate',
'Do Not Publish If',
'Do not commit raw footage, transcript JSON, or timeline exports',
'/Users/affoon/private-media',
].join('\n'),
});
seedMedia(sourceRoot, suiteRoot);
const report = buildReport({
root: rootDir,
sourceRoot,
suiteRoot,
skipProbe: true,
generatedAt: '2026-05-19T00:00:00.000Z',
});
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'video-suite-public-sanitization' && check.status === 'fail'));
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('CLI emits JSON and exits successfully for complete fixture', () => {
const rootDir = createTempDir('release-video-cli-');
const sourceRoot = createTempDir('release-video-source-');
const suiteRoot = createTempDir('release-video-suite-');
try {
seedRepo(rootDir);
seedMedia(sourceRoot, suiteRoot);
const output = run([
'--format=json',
`--root=${rootDir}`,
`--source-root=${sourceRoot}`,
`--suite-root=${suiteRoot}`,
'--skip-probe',
'--summary',
], { cwd: rootDir });
const parsed = JSON.parse(output);
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.sourceRootConfigured, true);
assert.strictEqual(parsed.suiteRootConfigured, true);
assert.strictEqual(parsed.sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length);
assert.strictEqual(parsed.suiteArtifactSummary.present, REQUIRED_SUITE_ARTIFACTS.length);
} finally {
cleanup(rootDir);
cleanup(sourceRoot);
cleanup(suiteRoot);
}
})) passed++; else failed++;
if (test('CLI exits nonzero when media roots are missing', () => {
const rootDir = createTempDir('release-video-cli-blocked-');
try {
seedRepo(rootDir);
const result = runProcess([
'--format=json',
`--root=${rootDir}`,
'--skip-probe',
'--summary',
], { cwd: rootDir });
assert.strictEqual(result.status, 1);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.ready, false);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
if (require.main === module) {
runTests();
}