mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-20 07:43:07 +08:00
chore: gate ECC release video suite (#1992)
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
173
docs/releases/2.0.0-rc.1/video-suite-production.md
Normal file
173
docs/releases/2.0.0-rc.1/video-suite-production.md
Normal 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`.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
747
scripts/release-video-suite.js
Normal file
747
scripts/release-video-suite.js
Normal 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,
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
303
tests/scripts/release-video-suite.test.js
Normal file
303
tests/scripts/release-video-suite.test.js
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user