@amg555/frameforge-mcp 2.3.2 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.mcp.json +2 -4
  2. package/CHANGELOG.md +1 -1
  3. package/CLAUDE.md +104 -26
  4. package/README.md +16 -12
  5. package/SECURITY.md +1 -3
  6. package/package.json +7 -3
  7. package/plugins/video-pipeline/.claude-plugin/plugin.json +19 -0
  8. package/plugins/video-pipeline/skills/html-renderer/SKILL.md +292 -0
  9. package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +308 -0
  10. package/plugins/video-pipeline/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +257 -0
  11. package/plugins/video-pipeline/skills/video-pipeline/assets/project/STORYBOARD.md +77 -0
  12. package/plugins/video-pipeline/skills/video-pipeline/assets/project/index.html +80 -0
  13. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +43 -0
  14. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +56 -0
  15. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +42 -0
  16. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +140 -0
  17. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +870 -0
  18. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +138 -0
  19. package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +167 -0
  20. package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +163 -0
  21. package/scripts/db-migrate.js +112 -0
  22. package/scripts/deep-validate.js +326 -0
  23. package/scripts/generate-initial-assets.js +342 -0
  24. package/scripts/html-renderer/new-project.mjs +408 -0
  25. package/scripts/html-renderer/transcribe.py +294 -0
  26. package/src/asset-library/README.md +2 -2
  27. package/src/asset-library/asset-library.js +1 -1
  28. package/src/cli/make.js +52 -71
  29. package/src/cli/setup-keys.js +15 -17
  30. package/src/fact-checker/review-gate.js +5 -1
  31. package/src/harness/bin/harness.mjs +324 -0
  32. package/src/harness/lib/browser.mjs +137 -0
  33. package/src/harness/lib/capture.mjs +161 -0
  34. package/src/harness/lib/encode.mjs +89 -0
  35. package/src/harness/lib/jobs.mjs +135 -0
  36. package/src/harness/lib/pixels.mjs +137 -0
  37. package/src/harness/lib/sheets.mjs +164 -0
  38. package/src/harness/lib/verify-purity.mjs +119 -0
  39. package/src/harness/test/fixtures/broken-scene.html +27 -0
  40. package/src/harness/test/fixtures/reference-scene.html +23 -0
  41. package/src/harness/test/fixtures/sample-script.json +18 -0
  42. package/src/mcp-server/server.js +285 -144
  43. package/src/pipeline/telemetry.js +1 -0
  44. package/src/utils/log-redaction.js +5 -3
  45. package/src/video-renderer/renderer.js +12 -4
  46. package/src/worker/server-render-free.js +2 -3
  47. package/src/workflows/assets.js +20 -43
  48. package/src/workflows/ingest.js +5 -5
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+ // Deep validation — imports, spec compliance, module correctness.
3
+ // Run with: node scripts/deep-validate.js
4
+ const fs = require('fs');
5
+ let failures = 0;
6
+ function check(label, value, expected = true) {
7
+ const ok = value === expected;
8
+ if (!ok) { console.error(` FAIL: ${label} — got ${JSON.stringify(value)}, expected ${JSON.stringify(expected)}`); failures++; }
9
+ else console.log(` OK : ${label}`);
10
+ }
11
+
12
+ // ── MCP server spec compliance ─────────────────────────────────────────────
13
+ const code = fs.readFileSync('./src/mcp-server/server.js','utf8');
14
+ console.log('\n=== MCP server spec compliance ===');
15
+ check('initialize handler', code.includes("method === 'initialize'"));
16
+ check('jsonrpc 2.0 responses', (code.match(/jsonrpc.*2\.0/g)||[]).length >= 8);
17
+ check('CORS headers', code.includes('Access-Control-Allow-Origin'));
18
+ check('visual_description', code.includes('visual_description'));
19
+ check('batch handling', code.includes('Array.isArray(body)'));
20
+ check('ping handler', code.includes("method === 'ping'"));
21
+ check('notifications/init', code.includes("notifications/initialized"));
22
+ check('body size limit', code.includes('MAX_BODY_BYTES'));
23
+ check('SERVER_INFO version 2.3.3', code.includes("version: '2.3.3'"));
24
+ check('18 tools defined (incl get_job_status)', code.includes('get_job_status'));
25
+ check('export_to_editor tool defined', code.includes('export_to_editor'));
26
+ check('voxforge TTS provider', code.includes('voxforge'));
27
+
28
+ // ── No execSync with shell strings in renderer or extractor ───────────────
29
+ console.log('\n=== Shell injection check ===');
30
+ const renderer = fs.readFileSync('./src/video-renderer/renderer.js','utf8');
31
+ const extractor = fs.readFileSync('./src/shorts-extractor/extractor.js','utf8');
32
+ const mixer = fs.readFileSync('./src/audio-mixer/mixer.js','utf8');
33
+ check('renderer uses spawnSync not execSync', !renderer.includes('execSync'));
34
+ check('extractor uses spawnSync not execSync', !extractor.includes('execSync'));
35
+ check('mixer uses spawnSync not execSync', !mixer.includes('execSync'));
36
+
37
+ // ── Animation System ───────────────────────────────────────────────────────
38
+ console.log('\n=== Animation System ===');
39
+ const { buildCameraFilter, selectCameraHint, MAX_ZOOM, FPS } = require('../src/animation/animation-system');
40
+ check('buildCameraFilter slow_zoom_in contains zoompan', buildCameraFilter('slow_zoom_in').includes('zoompan'));
41
+ check('buildCameraFilter slow_zoom_out contains zoompan', buildCameraFilter('slow_zoom_out').includes('zoompan'));
42
+ check('buildCameraFilter pan_left returns zoompan', buildCameraFilter('pan_left').includes('zoompan'));
43
+ check('buildCameraFilter pan_right returns zoompan', buildCameraFilter('pan_right').includes('zoompan'));
44
+ check('buildCameraFilter tilt_up returns zoompan', buildCameraFilter('tilt_up').includes('zoompan'));
45
+ check('buildCameraFilter static returns scale', buildCameraFilter('static').includes('scale'));
46
+ check('buildCameraFilter ken_burns returns zoompan', buildCameraFilter('ken_burns').includes('zoompan'));
47
+ check('buildCameraFilter auto does not throw', typeof buildCameraFilter('auto') === 'string');
48
+ check('MAX_ZOOM is 1.12', MAX_ZOOM, 1.12);
49
+ check('FPS is 24', FPS, 24);
50
+ const hint = selectCameraHint(0);
51
+ check('selectCameraHint returns string', typeof hint === 'string');
52
+ check('camera filter does not exceed MAX_ZOOM', !buildCameraFilter('slow_zoom_in').includes('1.13'));
53
+
54
+ // ── Artifact Manifest ──────────────────────────────────────────────────────
55
+ console.log('\n=== Artifact Manifest ===');
56
+ const { ArtifactManifest } = require('../src/pipeline/artifact-manifest');
57
+ const os = require('os');
58
+ const path = require('path');
59
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manifest-test-'));
60
+ const mf = new ArtifactManifest({ productionId: 'test_prod_001', artifactsDir: tmpDir });
61
+ const data = mf.create({ narrationText: 'hello world test narration', scenes: [{narration:'s1',caption:'c1'},{narration:'s2',caption:'c2'}] });
62
+ check('manifest created', data.production_id === 'test_prod_001');
63
+ check('schema_version 1.0.0', data.schema_version === '1.0.0');
64
+ check('two scenes', data.scenes.length, 2);
65
+ check('narration_hash is sha256', data.narration_hash.length, 64);
66
+ check('manifest.json written to disk', fs.existsSync(path.join(tmpDir, 'test_prod_001', 'manifest.json')));
67
+ check('isSceneComplete false initially', !mf.isSceneComplete(0));
68
+ check('isMasterComplete false initially',!mf.isMasterComplete());
69
+ const mf2 = new ArtifactManifest({ productionId: 'test_prod_001', artifactsDir: tmpDir });
70
+ const { resumed } = mf2.loadOrCreate({ narrationText: 'hello world test narration' });
71
+ check('loadOrCreate same narration resumes', resumed, true);
72
+ const { resumed: notResumed } = mf2.loadOrCreate({ narrationText: 'completely different text' });
73
+ check('loadOrCreate different narration does not resume', notResumed, false);
74
+ fs.rmSync(tmpDir, { recursive: true, force: true });
75
+
76
+ // ── Vertical Reformatting (ShortsExtractor) ───────────────────────────────
77
+ console.log('\n=== ShortsExtractor focal point math ===');
78
+ const { ShortsExtractor } = require('../src/shorts-extractor/extractor');
79
+ const sx = new ShortsExtractor();
80
+ check('focalX=0.5 centers crop (608px wide in 1920)', sx._focalToFFmpegX(0.5), 656);
81
+ check('focalX=0.0 clamps to 0', sx._focalToFFmpegX(0.0), 0);
82
+ check('focalX=1.0 clamps to 1312', sx._focalToFFmpegX(1.0), 1312);
83
+ check('focalX=0.3 is even pixel', sx._focalToFFmpegX(0.3) % 2 === 0);
84
+ check('focalX=0.7 is even pixel', sx._focalToFFmpegX(0.7) % 2 === 0);
85
+ check('focalToFFmpegY is always 0',sx._focalToFFmpegY(), 0);
86
+ check('defaultFocalY with no scenes = 0.35', sx._defaultFocalY([]), 0.35);
87
+ check('defaultFocalY uses focal_point.y from scenes', sx._defaultFocalY([{focal_point:{y:0.25}}]), 0.25);
88
+ const fp = sx._focalPointForSegment({ coveredScenes: [] }, []);
89
+ check('focalPointForSegment default x=0.5', fp.x, 0.5);
90
+ check('focalPointForSegment default y=0.35 (safe-zone bias)', fp.y, 0.35);
91
+ const fp2 = sx._focalPointForSegment({ coveredScenes: [{ focal_point: { x: 0.3, y: 0.2 } }] }, []);
92
+ check('focalPointForSegment uses scene focal_point.x', fp2.x, 0.3);
93
+ check('focalPointForSegment uses scene focal_point.y', fp2.y, 0.2);
94
+
95
+ // ── Artifact Retention ────────────────────────────────────────────────────
96
+ console.log('\n=== Artifact Retention ===');
97
+ const { classifyRetention } = require('../src/pipeline/artifact-retention');
98
+ check('final_video is permanent', classifyRetention('final_video_prod123.mp4'), 'permanent');
99
+ check('short is permanent', classifyRetention('short_clip.mp4'), 'permanent');
100
+ check('manifest.json is permanent', classifyRetention('manifest.json'), 'permanent');
101
+ check('scene render is 30_day', classifyRetention('scene_3_out.mp4'), '30_day');
102
+ check('concat txt is 30_day', classifyRetention('concat_main.txt'), '30_day');
103
+ check('raw_concat is 30_day', classifyRetention('raw_concat_123.mp4'), '30_day');
104
+ check('_raw.mp3 is temp', classifyRetention('voice_raw.mp3'), 'temp');
105
+
106
+ // ── Fact-check Validation Set + Review Gate ───────────────────────────────
107
+ console.log('\n=== Fact-check Validation Set + Review Gate ===');
108
+ const { VALIDATION_SET } = require('../src/fact-checker/validation-set');
109
+ const { FactCheckerReviewGate } = require('../src/fact-checker/review-gate');
110
+ check('validation set has ≥100 claims', VALIDATION_SET.length >= 100);
111
+ check('every entry has claim_text', VALIDATION_SET.every(c => typeof c.claim_text === 'string'));
112
+ check('every entry has gold_label', VALIDATION_SET.every(c => ['SUPPORTED','UNSUPPORTED','DISPUTED','UNVERIFIABLE'].includes(c.gold_label)));
113
+ check('every entry has risk_level', VALIDATION_SET.every(c => ['TRIVIAL','NUMERICAL','CONTROVERSIAL'].includes(c.risk_level)));
114
+ check('every entry has expected_decision', VALIDATION_SET.every(c => ['AUTO_APPROVE','HUMAN_REVIEW','REJECT'].includes(c.expected_decision)));
115
+ const mustReject = VALIDATION_SET.filter(c => c.expected_decision === 'REJECT');
116
+ check('≥3 dangerous claims marked REJECT', mustReject.length >= 3);
117
+ const vaccineAutism = VALIDATION_SET.find(c => c.claim_text.includes('Vaccines cause autism'));
118
+ check('vaccines-autism is REJECT', vaccineAutism?.expected_decision === 'REJECT');
119
+ const earthFlat = VALIDATION_SET.find(c => c.claim_text.includes('Earth is flat'));
120
+ check('flat-earth is REJECT', earthFlat?.expected_decision === 'REJECT');
121
+ const gate = new FactCheckerReviewGate({});
122
+ const mockReport = {
123
+ results: [
124
+ { rawClaim: 'safe trivial fact', claim_risk: 'TRIVIAL', evidence_strength: 'STRONG', status: 'VERIFIED', sources: [] },
125
+ { rawClaim: 'disputed stat', claim_risk: 'NUMERICAL', evidence_strength: 'WEAK', status: 'DISPUTED', sources: [] },
126
+ { rawClaim: 'dangerous claim', claim_risk: 'CONTROVERSIAL', evidence_strength: 'INSUFFICIENT', status: 'UNSUPPORTED', sources: [] },
127
+ ],
128
+ };
129
+ const { autoApproved: aa, needsReview: nr, rejected: rj } = gate.classifyDecisions(mockReport);
130
+ check('trivial auto-approved', aa.length, 1);
131
+ check('disputed numerical needs review', nr.length, 1);
132
+ check('dangerous controversial rejected', rj.length, 1);
133
+ const msg = gate.formatReviewRequest(mockReport.results.slice(1, 2), 'prod_test');
134
+ check('review request contains production id', msg.includes('prod_test'));
135
+ check('review request contains approve command', msg.includes('/approve'));
136
+ check('review request contains reject command', msg.includes('/reject'));
137
+
138
+ // ── TTS provider — constructor overrides (v2.0.1) ─────────────────────────
139
+ console.log('\n=== TTS Provider ===');
140
+ const { TTSProvider } = require('../src/pipeline/providers');
141
+ const tts = new TTSProvider();
142
+ check('selectNarratorVoice uses explicit override', tts.selectNarratorVoice('my-voice') === 'my-voice');
143
+ const autoVoice = tts.selectNarratorVoice();
144
+ check('selectNarratorVoice returns string', typeof autoVoice === 'string');
145
+ check('TTSProvider accepts opts.platform', new TTSProvider({ platform: 'elevenlabs' }).name === 'elevenlabs');
146
+ check('TTSProvider accepts opts.rate override', new TTSProvider({ rate: '-10%' })._overrides.rate === '-10%');
147
+ check('no artificial bass boost in edge-tts filter', !fs.readFileSync('./src/pipeline/providers.js','utf8').includes('bass=g=3'));
148
+
149
+ // ── Renderer ← AnimationSystem integration ───────────────────────────────
150
+ console.log('\n=== Renderer ← AnimationSystem integration ===');
151
+ check('renderer imports animation-system', renderer.includes("require('../animation/animation-system')"));
152
+ check('renderer imports AudioMixer', renderer.includes("require('../audio-mixer/mixer')"));
153
+ check('renderer calls buildCameraFilter', renderer.includes('buildCameraFilter'));
154
+ check('renderer calls selectCameraHint', renderer.includes('selectCameraHint'));
155
+ check('renderer calls mixer.mixVideo', renderer.includes('mixer.mixVideo'));
156
+
157
+ // ── Workflow modules import OK ────────────────────────────────────────────
158
+ console.log('\n=== Workflow modules ===');
159
+ const {runResearch} = require('../src/workflows/research.js');
160
+ const {runScript, normaliseScene} = require('../src/workflows/script.js');
161
+ const {runAssets} = require('../src/workflows/assets.js');
162
+ const {runRender} = require('../src/workflows/render.js');
163
+ const {runShorts} = require('../src/workflows/shorts.js');
164
+ check('all workflow modules import OK', true);
165
+
166
+ // ── normaliseScene — schema v2.0.0 ───────────────────────────────────────
167
+ console.log('\n=== normaliseScene (schema v2.0.0) ===');
168
+ const legacyScene = { n:1, narration:'Test narration for scene one.', caption:'test cap', visual_action:'historic figure reacting', camera:'slow_zoom_in' };
169
+ const n1 = normaliseScene(legacyScene, 0);
170
+ check('legacy: schema_version set', typeof n1.schema_version === 'string');
171
+ check('legacy: camera_hint present', typeof n1.camera_hint === 'string');
172
+ check('legacy: focal_point present', typeof n1.focal_point === 'object');
173
+
174
+ const newScene = { scene_id:2, schema_version:'1.0.0', narration:'Test narration scene two.', caption:'TEST', visual_description:'modern office scene', character:{character_id:'narrator',pose:'sitting',emotion:'happy',position:{x:0.3,y:0.7,scale:1.2}}, focal_point:{x:0.3,y:0.3,weight:0.9}, overlay:{type:'none'}, sound_effects:[], music_mood:'calm', camera_hint:'zoom_out', metadata:{importance:'high',hook_potential:0.8,standalone:true} };
175
+ const n2 = normaliseScene(newScene, 1);
176
+ check('v1.0.0: schema_version preserved', typeof n2.schema_version === 'string');
177
+ check('v1.0.0: character_id preserved', n2.character?.character_id === 'narrator');
178
+ check('v1.0.0: camera_hint preserved', n2.camera_hint, 'zoom_out');
179
+ check('v1.0.0: hook_potential', n2.metadata?.hook_potential, 0.8);
180
+ check('v1.0.0: standalone', n2.metadata?.standalone, true);
181
+
182
+ // ── publishing.js — file + webhook delivery ──────────────────────────────
183
+ console.log('\n=== publishing.js file+webhook delivery ===');
184
+ const pubjs = fs.readFileSync('./src/workflows/publishing.js', 'utf8');
185
+ check('publishing.js file channel present', pubjs.includes("channels.includes('file')"));
186
+ check('publishing.js webhook channel present', pubjs.includes("channels.includes('webhook')"));
187
+ check('publishing.js has PUBLISH_WEBHOOK_URL', pubjs.includes('PUBLISH_WEBHOOK_URL'));
188
+ check('publishing.js no Telegram curl', !pubjs.includes('telegramShorts') && !pubjs.includes('TELEGRAM_BOT_TOKEN'));
189
+ check('publishing.js no YouTube OAuth2', !pubjs.includes('_refreshAccessToken') && !pubjs.includes('YOUTUBE_CLIENT_ID'));
190
+ check('publishing.js no stale youtubeTitle fallback', !pubjs.includes('youtubeTitle') && !pubjs.includes('youtubeDescription'));
191
+
192
+ // ── packaging.js — canonical title/description/tags field names ──────────
193
+ console.log('\n=== packaging.js canonical field names ===');
194
+ const pkgjs = fs.readFileSync('./src/workflows/packaging.js', 'utf8');
195
+ check('packaging.js uses title not youtubeTitle', !pkgjs.includes("youtubeTitle,") && pkgjs.includes("title,"));
196
+ check('packaging.js metadata.title canonical', pkgjs.includes('metadata: {') && pkgjs.includes('title,'));
197
+ check('packaging.js writes description.md not youtube', pkgjs.includes("'description.md'") && !pkgjs.includes("'youtube_description.md'"));
198
+
199
+ // ── server-render-free.js — no stale Telegram default ────────────────────
200
+ console.log('\n=== server-render-free.js ===');
201
+ const renderFreeJs = fs.readFileSync('./src/worker/server-render-free.js', 'utf8');
202
+ check('render-free: no stale Telegram channel default', !renderFreeJs.includes("channels = ['telegram']"));
203
+ check('render-free: default channel is file', renderFreeJs.includes("channels = ['file']"));
204
+
205
+ // ── server.js — queue_full_pipeline + tool completeness ──────────────────
206
+ console.log('\n=== server.js tool completeness ===');
207
+ const serverjs = fs.readFileSync('./src/mcp-server/server.js', 'utf8');
208
+ check('server.js exposes 14 tools (header)', serverjs.includes('14 video production tools'));
209
+ check('server.js publish_video calls runPublishing', serverjs.includes("require('../workflows/publishing')"));
210
+ check('server.js has queue_full_pipeline tool', serverjs.includes('queue_full_pipeline'));
211
+ check('server.js has get_job_status tool', serverjs.includes('get_job_status'));
212
+ check('server.js has create_video_from_script', serverjs.includes('create_video_from_script'));
213
+ check('server.js has multi_voice_tts', serverjs.includes('multi_voice_tts'));
214
+ check('server.js body size limit present', serverjs.includes('MAX_BODY_BYTES'));
215
+ check('server.js no stale YouTube stub', !serverjs.includes("'YouTube upload requires manual setup'"));
216
+ check('publish_video channels default is file not telegram', serverjs.includes("channels = ['file']") && !serverjs.includes("channels = ['telegram']"));
217
+ check('publish_video metadata has no youtubeTitle alias', !serverjs.includes('youtubeTitle: title'));
218
+ check('get_job_status forwards INTERNAL_API_TOKEN', serverjs.includes('internalToken') && serverjs.includes('get_job_status'));
219
+ check('queue_full_pipeline forwards INTERNAL_API_TOKEN', serverjs.includes('authHeaders') && serverjs.includes('queue_full_pipeline'));
220
+ check('MCP HTTP server enforces MCP_AUTH_TOKEN', serverjs.includes('_checkMCPAuth') && serverjs.includes('crypto.timingSafeEqual'));
221
+ check('server startup does not log provider env config', !serverjs.includes("LLM=${process.env.PROVIDER_LLM"));
222
+
223
+ // ── state-machine.js — write mutex ───────────────────────────────────────
224
+ console.log('\n=== state-machine.js write mutex ===');
225
+ const smjs = fs.readFileSync('./src/pipeline/state-machine.js', 'utf8');
226
+ check('state-machine has _writeLocks Map', smjs.includes('_writeLocks = new Map()'));
227
+ check('state-machine _saveProduction uses mutex', smjs.includes('_writeLocks.get(productionId)'));
228
+
229
+ // ── asset-library.js ──────────────────────────────────────────────────────
230
+ console.log('\n=== asset-library.js ===');
231
+ const assetLib = fs.readFileSync('./src/asset-library/asset-library.js', 'utf8');
232
+ check('asset-library has _downloadAsset', assetLib.includes('_downloadAsset'));
233
+ check('asset-library has _semanticSearch', assetLib.includes('_semanticSearch'));
234
+ check('asset-library has composeCharacter', assetLib.includes('composeCharacter'));
235
+
236
+ // ── fact-checker ──────────────────────────────────────────────────────────
237
+ console.log('\n=== fact-checker ===');
238
+ const checker = fs.readFileSync('./src/fact-checker/checker.js', 'utf8');
239
+ check('fact-checker has _querySources', checker.includes('_querySources'));
240
+
241
+ // ── log-redaction patterns ────────────────────────────────────────────────
242
+ console.log('\n=== log-redaction patterns ===');
243
+ const redaction = fs.readFileSync('./src/utils/log-redaction.js', 'utf8');
244
+ check('log-redaction has ElevenLabs sk_ pattern', redaction.includes('sk_') || redaction.includes('elevenlabs'));
245
+ check('log-redaction has Tavily tvly- pattern', redaction.includes('tvly') || redaction.includes('tavily'));
246
+
247
+ // ── html-renderer scaffolder ──────────────────────────────────────────────
248
+ console.log('\n=== html-renderer scaffolder ===');
249
+ check('new-project.mjs exists', fs.existsSync('./scripts/html-renderer/new-project.mjs'));
250
+
251
+ // ── html-renderer engine assets ──────────────────────────────────────────
252
+ console.log('\n=== html-renderer engine assets ===');
253
+ const engineAssetBase = './plugins/video-pipeline/skills/video-pipeline/assets/project';
254
+ check('reel/engine.js exists', fs.existsSync(`${engineAssetBase}/reel/engine.js`));
255
+ check('reel/engine.css exists', fs.existsSync(`${engineAssetBase}/reel/engine.css`));
256
+ check('scripts/render.mjs exists', fs.existsSync(`${engineAssetBase}/scripts/render.mjs`));
257
+ check('scripts/mix_audio.py exists', fs.existsSync(`${engineAssetBase}/scripts/mix_audio.py`));
258
+ check('scripts/verify.py exists', fs.existsSync(`${engineAssetBase}/scripts/verify.py`));
259
+ check('scripts/transcribe.py exists', fs.existsSync(`${engineAssetBase}/scripts/transcribe.py`));
260
+ check('scripts/capture.mjs exists', fs.existsSync(`${engineAssetBase}/scripts/capture.mjs`));
261
+ check('scripts/stock.py exists', fs.existsSync(`${engineAssetBase}/scripts/stock.py`));
262
+ const engineJs = fs.readFileSync(`${engineAssetBase}/reel/engine.js`, 'utf8');
263
+ check('engine.js exports seek()', engineJs.includes('export function seek'));
264
+ check('engine.js exports spring()', engineJs.includes('export function spring'));
265
+ check('engine.js exports layer()', engineJs.includes('export function layer'));
266
+ check('engine.js exports wallCard()', engineJs.includes('export function wallCard'));
267
+ check('engine.js exports reel()', engineJs.includes('export function reel'));
268
+ check('engine.js REEL state exported',engineJs.includes('export const REEL'));
269
+ check('engine.js exports boilSeed()', engineJs.includes('export function boilSeed'));
270
+ check('engine.js exports kf()', engineJs.includes('export function kf'));
271
+ check('engine.js exports onTwos()', engineJs.includes('export function onTwos'));
272
+ check('engine.js exports seg()', engineJs.includes('export function seg'));
273
+ check('engine.js exports pulse()', engineJs.includes('export function pulse'));
274
+ check('engine.js exports shots()', engineJs.includes('export function shots'));
275
+
276
+ // ── Code-Driven Video Harness ─────────────────────────────────────────────
277
+ console.log('\n=== Code-Driven Video Harness ===');
278
+ check('harness/bin/harness.mjs exists', fs.existsSync('./src/harness/bin/harness.mjs'));
279
+ check('harness/lib/browser.mjs exists', fs.existsSync('./src/harness/lib/browser.mjs'));
280
+ check('harness/lib/capture.mjs exists', fs.existsSync('./src/harness/lib/capture.mjs'));
281
+ check('harness/lib/jobs.mjs exists', fs.existsSync('./src/harness/lib/jobs.mjs'));
282
+ check('harness/lib/sheets.mjs exists', fs.existsSync('./src/harness/lib/sheets.mjs'));
283
+ check('harness/lib/verify-purity.mjs exists', fs.existsSync('./src/harness/lib/verify-purity.mjs'));
284
+ check('harness/lib/pixels.mjs exists', fs.existsSync('./src/harness/lib/pixels.mjs'));
285
+ check('harness/lib/encode.mjs exists', fs.existsSync('./src/harness/lib/encode.mjs'));
286
+ check('asset harness.mjs exists', fs.existsSync(`${engineAssetBase}/scripts/harness.mjs`));
287
+ check('asset verify-purity.mjs exists', fs.existsSync(`${engineAssetBase}/scripts/verify-purity.mjs`));
288
+
289
+ // ── Cloudflare Worker rate limiting ───────────────────────────────────────
290
+ console.log('\n=== Cloudflare Worker rate limiting ===');
291
+ const cfWorker = fs.readFileSync('./cloudflare-mcp/src/index.js', 'utf8');
292
+ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
293
+ check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
294
+ check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
295
+ check('cf-worker returns 429 on limit', cfWorker.includes('429'));
296
+ check('cf-worker version 2.3.3', cfWorker.includes("'2.3.3'"));
297
+ check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
298
+
299
+ // ── plugins/skills present ────────────────────────────────────────────────
300
+ console.log('\n=== Plugin skills ===');
301
+ check('video-pipeline SKILL.md exists', fs.existsSync('./plugins/video-pipeline/skills/video-pipeline/SKILL.md'));
302
+ check('html-renderer SKILL.md exists', fs.existsSync('./plugins/video-pipeline/skills/html-renderer/SKILL.md'));
303
+
304
+ // ── Security: no .kiro references in committed source ────────────────────
305
+ console.log('\n=== No stale .kiro references in source ===');
306
+ const srcFiles = [
307
+ 'src/mcp-server/server.js',
308
+ 'src/worker/server-render-free.js',
309
+ 'src/worker/server.js',
310
+ 'cloudflare-mcp/src/index.js',
311
+ 'docs/env.template',
312
+ 'README.md',
313
+ ];
314
+ for (const f of srcFiles) {
315
+ const content = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
316
+ check(`${f} has no .kiro reference`, !content.includes('.kiro/'));
317
+ }
318
+
319
+ // ── Final result ──────────────────────────────────────────────────────────
320
+ console.log(`\n${'─'.repeat(48)}`);
321
+ if (failures > 0) {
322
+ console.error(`\n${failures} check(s) FAILED`);
323
+ process.exit(1);
324
+ } else {
325
+ console.log('\nAll checks passed.');
326
+ }