@revola-ai/video-craft 0.2.1

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 (340) hide show
  1. package/README.md +56 -0
  2. package/dist/anchors.js +69 -0
  3. package/dist/corpus.js +91 -0
  4. package/dist/projectDoc.js +83 -0
  5. package/dist/projectDocSync.js +39 -0
  6. package/dist/projectMeta.js +37 -0
  7. package/dist/provenance.js +41 -0
  8. package/dist/radar-check-ids.json +34 -0
  9. package/dist/registry.js +307 -0
  10. package/dist/rules/animatedBarChartTightBox.js +15 -0
  11. package/dist/rules/animatedBarChartWhenNotToUse.js +15 -0
  12. package/dist/rules/animatedBarChartWhenToUse.js +15 -0
  13. package/dist/rules/animatedLineChartWhenNotToUse.js +15 -0
  14. package/dist/rules/animatedLineChartWhenToUse.js +15 -0
  15. package/dist/rules/approvedSfxPaletteOnly.js +15 -0
  16. package/dist/rules/atmosphereGradientsFadeAtBothEnds.js +15 -0
  17. package/dist/rules/audioIsTheClock.js +16 -0
  18. package/dist/rules/beatsDeriveFromTheirOwnSequence.js +15 -0
  19. package/dist/rules/bestSceneSetsTheBar.js +15 -0
  20. package/dist/rules/bumpBeforeRefreeze.js +15 -0
  21. package/dist/rules/cameraIsTheNarrator.js +15 -0
  22. package/dist/rules/checkPinBeforeShip.js +16 -0
  23. package/dist/rules/coldViewerClarity.js +15 -0
  24. package/dist/rules/commitOrCutEffects.js +29 -0
  25. package/dist/rules/convertFramesAcrossClockBoundary.js +15 -0
  26. package/dist/rules/crossCutElementsLiveInTheHudLayer.js +15 -0
  27. package/dist/rules/cursorClicksRealControls.js +15 -0
  28. package/dist/rules/cutOnRealAudioPauses.js +15 -0
  29. package/dist/rules/darkButNeverDim.js +15 -0
  30. package/dist/rules/dataRadarTagsDeclared.js +193 -0
  31. package/dist/rules/declareAVerticalZoneMap.js +15 -0
  32. package/dist/rules/deleteRejectedRigsCompletely.js +15 -0
  33. package/dist/rules/deriveSeamsDontMeasure.js +17 -0
  34. package/dist/rules/deterministicSceneSource.js +65 -0
  35. package/dist/rules/diegeticSoundForOnscreenActions.js +15 -0
  36. package/dist/rules/distinctTransitionPerBeat.js +15 -0
  37. package/dist/rules/downloadOrDrawRichAssets.js +15 -0
  38. package/dist/rules/elementOnscreenForItsVoSentence.js +17 -0
  39. package/dist/rules/emitBuildNotes.js +15 -0
  40. package/dist/rules/energyFromTimingNotPartCount.js +15 -0
  41. package/dist/rules/evidenceBeforeClaimingFixed.js +15 -0
  42. package/dist/rules/exactPeerLockstep.js +15 -0
  43. package/dist/rules/firstPassIsADraft.js +15 -0
  44. package/dist/rules/fixCrowdingByRebandingNotNudging.js +15 -0
  45. package/dist/rules/fixesPreserveApprovedElements.js +15 -0
  46. package/dist/rules/floatingNotBoxed.js +15 -0
  47. package/dist/rules/focusBeatsRecedeTheContext.js +15 -0
  48. package/dist/rules/fontVarsBrandRemocn.js +15 -0
  49. package/dist/rules/geometricSeamsNotCrossfades.js +23 -0
  50. package/dist/rules/geometrySidecarExportsTyped.js +36 -0
  51. package/dist/rules/glowIsAFocalAccent.js +22 -0
  52. package/dist/rules/heroFillsTheFrame.js +17 -0
  53. package/dist/rules/htmlMasksForAnimatedCounters.js +15 -0
  54. package/dist/rules/impactStackOnPayoffs.js +15 -0
  55. package/dist/rules/interactiveTargetsAreDeclaredData.js +15 -0
  56. package/dist/rules/interpolateLiteralKeysMonotonic.js +79 -0
  57. package/dist/rules/latenessIsStructuralOverlapTheBeats.js +15 -0
  58. package/dist/rules/lessTextMoreVisualizations.js +15 -0
  59. package/dist/rules/liveLinkDuplicateReactAlias.js +15 -0
  60. package/dist/rules/make2dElements3d.js +15 -0
  61. package/dist/rules/masterMixLoudnessTarget.js +17 -0
  62. package/dist/rules/measureAlignmentDontEyeball.js +15 -0
  63. package/dist/rules/motionCarriesWeightAndMeaning.js +15 -0
  64. package/dist/rules/motionDensityFloor.js +15 -0
  65. package/dist/rules/neverEditTierBVendored.js +15 -0
  66. package/dist/rules/neverRestateDerivedFrameNumbersInComments.js +15 -0
  67. package/dist/rules/noAiGradientLook.js +15 -0
  68. package/dist/rules/noColoredFlashToHideACut.js +22 -0
  69. package/dist/rules/noDoubleFrameShift.js +126 -0
  70. package/dist/rules/noDuplicateMotionPrimitives.js +15 -0
  71. package/dist/rules/noLinearMotion.js +22 -0
  72. package/dist/rules/noNestedSvgFilters.js +93 -0
  73. package/dist/rules/noRepeatedAssetAcrossShots.js +15 -0
  74. package/dist/rules/noShorthandWithUndefinedLonghand.js +93 -0
  75. package/dist/rules/noStackedFadesAtACut.js +24 -0
  76. package/dist/rules/noStaticHolds.js +17 -0
  77. package/dist/rules/occlusionVisibleElements.js +17 -0
  78. package/dist/rules/oneContinuousVoTrack.js +15 -0
  79. package/dist/rules/onePunchPerMomentClass.js +15 -0
  80. package/dist/rules/oneRenderPerRound.js +15 -0
  81. package/dist/rules/openStaticAfterAHardCut.js +22 -0
  82. package/dist/rules/openingsAndClosingsCarryMore.js +15 -0
  83. package/dist/rules/persistentDeviceFrames.js +15 -0
  84. package/dist/rules/pixelDiffAnEmptyBandFirst.js +15 -0
  85. package/dist/rules/planIsPublishedNotGating.js +15 -0
  86. package/dist/rules/poweredByRevolaWatermark.js +15 -0
  87. package/dist/rules/predrawContentPresentAtTheCut.js +17 -0
  88. package/dist/rules/premiumSurfacesNotFlatBorders.js +15 -0
  89. package/dist/rules/radarDispositionsAreHumanOnly.js +17 -0
  90. package/dist/rules/realAssetsNotPlaceholders.js +15 -0
  91. package/dist/rules/rebuildUiFromReconNotScreenshots.js +15 -0
  92. package/dist/rules/reconStrandsRunInParallel.js +15 -0
  93. package/dist/rules/registryBeforeHandrolling.js +15 -0
  94. package/dist/rules/remapRateEndpointFormula.js +15 -0
  95. package/dist/rules/remocnContentTailwindChrome.js +15 -0
  96. package/dist/rules/retimeOnlyViaPlaybackOrDesignTokens.js +15 -0
  97. package/dist/rules/reviewTheRenderByFrameSweep.js +15 -0
  98. package/dist/rules/rollingNumberBoundedSequence.js +17 -0
  99. package/dist/rules/rollingNumberSequenceLayoutNone.js +16 -0
  100. package/dist/rules/rollingNumberTightBoxAlignment.js +15 -0
  101. package/dist/rules/rollingNumberWhenNotToUse.js +17 -0
  102. package/dist/rules/rollingNumberWhenToUse.js +17 -0
  103. package/dist/rules/sameFamilySameProject.js +15 -0
  104. package/dist/rules/sampleBrandFromProduct.js +15 -0
  105. package/dist/rules/sampleVelocityFromPreviousFramePosition.js +15 -0
  106. package/dist/rules/sceneRootTagDeclared.js +41 -0
  107. package/dist/rules/scrubDemoDataToGenericRoles.js +15 -0
  108. package/dist/rules/selfReviewThePlanForGaps.js +15 -0
  109. package/dist/rules/sourceArchivedBeforeBrief.js +17 -0
  110. package/dist/rules/statisticsSelfExplanatory.js +15 -0
  111. package/dist/rules/stopsAsRelativeSpacing.js +15 -0
  112. package/dist/rules/theWholeCanvasWorks.js +15 -0
  113. package/dist/rules/themePerSection.js +22 -0
  114. package/dist/rules/thesisDeclaredInBrief.js +17 -0
  115. package/dist/rules/tierARequiresPropUnreachableEdit.js +15 -0
  116. package/dist/rules/tinyBodyCopyAsRuledLines.js +15 -0
  117. package/dist/rules/titlesPinnedOutsideCameraPush.js +15 -0
  118. package/dist/rules/tokensNotRawHexes.js +60 -0
  119. package/dist/rules/typeTokenDefaultedParamsAsString.js +15 -0
  120. package/dist/rules/varyKitChoreographyAcrossSections.js +15 -0
  121. package/dist/rules/varyTheVisualizationGrammar.js +15 -0
  122. package/dist/rules/verifyCheapRenderDear.js +15 -0
  123. package/dist/rules/voAddressesTheViewer.js +15 -0
  124. package/dist/rules/voIsAPerformance.js +15 -0
  125. package/dist/rules/voScriptLocksSpokenWords.js +15 -0
  126. package/dist/rules/wrappersPreserveChildGeometry.js +15 -0
  127. package/dist/sourceCheck.js +47 -0
  128. package/dist/sourceCitations.js +68 -0
  129. package/dist/stageProjection.js +69 -0
  130. package/dist/stages.js +163 -0
  131. package/dist/testSupport.js +6 -0
  132. package/dist/types.js +0 -0
  133. package/dist/variants.js +8 -0
  134. package/dist/waiverProjection.js +20 -0
  135. package/package.json +32 -0
  136. package/src/anchors.test.ts +119 -0
  137. package/src/anchors.ts +110 -0
  138. package/src/corpus.test.ts +96 -0
  139. package/src/corpus.ts +149 -0
  140. package/src/corpusProject.test.ts +103 -0
  141. package/src/creativeEdge.test.ts +58 -0
  142. package/src/distBuild.test.ts +86 -0
  143. package/src/docStatus.test.ts +81 -0
  144. package/src/drift.test.ts +124 -0
  145. package/src/fixtures/data-radar-dynamic/Branches.tsx +14 -0
  146. package/src/fixtures/data-radar-dynamic/plan.fixture.yaml +16 -0
  147. package/src/fixtures/data-radar-forwarded/Scene.tsx +19 -0
  148. package/src/fixtures/data-radar-forwarded/plan.fixture.yaml +13 -0
  149. package/src/fixtures/data-radar-opaque/Scene.tsx +15 -0
  150. package/src/fixtures/data-radar-opaque/plan.fixture.yaml +10 -0
  151. package/src/fixtures/data-radar-tags/SceneClean.tsx +10 -0
  152. package/src/fixtures/data-radar-tags/SceneNullBinding.tsx +12 -0
  153. package/src/fixtures/data-radar-tags/SceneWithGap.tsx +11 -0
  154. package/src/fixtures/data-radar-tags/plan.fixture.yaml +29 -0
  155. package/src/fixtures/deterministic-scene.tsx +15 -0
  156. package/src/fixtures/double-shift/aliased.tsx +13 -0
  157. package/src/fixtures/double-shift/assigned.tsx +13 -0
  158. package/src/fixtures/double-shift/clean.tsx +19 -0
  159. package/src/fixtures/double-shift/destructured.tsx +12 -0
  160. package/src/fixtures/double-shift/elementAccess.tsx +12 -0
  161. package/src/fixtures/double-shift/layout.ts +24 -0
  162. package/src/fixtures/double-shift/mechanism-prefix.tsx.txt +458 -0
  163. package/src/fixtures/double-shift/wrapped.tsx +15 -0
  164. package/src/fixtures/geometry-sidecar-missing-export.ts +10 -0
  165. package/src/fixtures/geometry-sidecar-missing-type.ts +7 -0
  166. package/src/fixtures/geometry-sidecar-valid.ts +6 -0
  167. package/src/fixtures/interpolate-ascending.tsx +6 -0
  168. package/src/fixtures/interpolate-descending.tsx +8 -0
  169. package/src/fixtures/interpolate-identifier-keys.tsx +9 -0
  170. package/src/fixtures/nested-filters-broken.tsx +11 -0
  171. package/src/fixtures/nested-filters-fixed.tsx +8 -0
  172. package/src/fixtures/nested-filters-html-wrapper.tsx +14 -0
  173. package/src/fixtures/nested-filters-html.tsx +7 -0
  174. package/src/fixtures/nested-filters-siblings.tsx +12 -0
  175. package/src/fixtures/nondeterministic-scene.tsx +14 -0
  176. package/src/fixtures/nondeterministic-variants.tsx +16 -0
  177. package/src/fixtures/raw-hex-scene.tsx +10 -0
  178. package/src/fixtures/scene-root-declared/SceneHook.tsx +9 -0
  179. package/src/fixtures/scene-root-declared/plan.fixture.yaml +10 -0
  180. package/src/fixtures/scene-root-missing/SceneHook.tsx +11 -0
  181. package/src/fixtures/scene-root-missing/plan.fixture.yaml +10 -0
  182. package/src/fixtures/scene-root-no-names/SceneHook.tsx +6 -0
  183. package/src/fixtures/scene-root-no-names/plan.fixture.yaml +6 -0
  184. package/src/fixtures/scene-root-shell/SceneHook.tsx +8 -0
  185. package/src/fixtures/scene-root-shell/Shell.tsx +9 -0
  186. package/src/fixtures/scene-root-shell/plan.fixture.yaml +10 -0
  187. package/src/fixtures/shorthand-undefined-clean.tsx +23 -0
  188. package/src/fixtures/shorthand-undefined-longhand.tsx +17 -0
  189. package/src/fixtures/token-scene.tsx +9 -0
  190. package/src/includeCoverage.test.ts +176 -0
  191. package/src/learnedFrom.test.ts +32 -0
  192. package/src/projectDoc.test.ts +230 -0
  193. package/src/projectDoc.ts +152 -0
  194. package/src/projectDocSync.test.ts +152 -0
  195. package/src/projectDocSync.ts +79 -0
  196. package/src/projectMeta.test.ts +75 -0
  197. package/src/projectMeta.ts +53 -0
  198. package/src/provenance.ts +85 -0
  199. package/src/radar-check-ids.json +34 -0
  200. package/src/registry.test.ts +207 -0
  201. package/src/registry.ts +352 -0
  202. package/src/rules/animatedBarChartTightBox.ts +14 -0
  203. package/src/rules/animatedBarChartWhenNotToUse.ts +14 -0
  204. package/src/rules/animatedBarChartWhenToUse.ts +14 -0
  205. package/src/rules/animatedLineChartWhenNotToUse.ts +14 -0
  206. package/src/rules/animatedLineChartWhenToUse.ts +14 -0
  207. package/src/rules/approvedSfxPaletteOnly.ts +23 -0
  208. package/src/rules/atmosphereGradientsFadeAtBothEnds.ts +21 -0
  209. package/src/rules/audioIsTheClock.ts +22 -0
  210. package/src/rules/beatsDeriveFromTheirOwnSequence.ts +21 -0
  211. package/src/rules/bestSceneSetsTheBar.ts +21 -0
  212. package/src/rules/bumpBeforeRefreeze.ts +20 -0
  213. package/src/rules/cameraIsTheNarrator.ts +21 -0
  214. package/src/rules/checkPinBeforeShip.ts +22 -0
  215. package/src/rules/coldViewerClarity.ts +21 -0
  216. package/src/rules/commitOrCutEffects.ts +36 -0
  217. package/src/rules/convertFramesAcrossClockBoundary.ts +19 -0
  218. package/src/rules/crossCutElementsLiveInTheHudLayer.ts +21 -0
  219. package/src/rules/cursorClicksRealControls.ts +22 -0
  220. package/src/rules/cutOnRealAudioPauses.ts +20 -0
  221. package/src/rules/darkButNeverDim.ts +21 -0
  222. package/src/rules/dataRadarTagsDeclared.test.ts +60 -0
  223. package/src/rules/dataRadarTagsDeclared.ts +341 -0
  224. package/src/rules/declareAVerticalZoneMap.ts +19 -0
  225. package/src/rules/deleteRejectedRigsCompletely.ts +19 -0
  226. package/src/rules/deriveSeamsDontMeasure.ts +23 -0
  227. package/src/rules/deterministicSceneSource.test.ts +58 -0
  228. package/src/rules/deterministicSceneSource.ts +82 -0
  229. package/src/rules/diegeticSoundForOnscreenActions.ts +21 -0
  230. package/src/rules/distinctTransitionPerBeat.ts +21 -0
  231. package/src/rules/downloadOrDrawRichAssets.ts +22 -0
  232. package/src/rules/elementOnscreenForItsVoSentence.ts +25 -0
  233. package/src/rules/emitBuildNotes.ts +19 -0
  234. package/src/rules/energyFromTimingNotPartCount.ts +20 -0
  235. package/src/rules/evidenceBeforeClaimingFixed.ts +23 -0
  236. package/src/rules/exactPeerLockstep.ts +19 -0
  237. package/src/rules/firstPassIsADraft.ts +20 -0
  238. package/src/rules/fixCrowdingByRebandingNotNudging.ts +20 -0
  239. package/src/rules/fixesPreserveApprovedElements.ts +24 -0
  240. package/src/rules/floatingNotBoxed.ts +21 -0
  241. package/src/rules/focusBeatsRecedeTheContext.ts +20 -0
  242. package/src/rules/fontVarsBrandRemocn.ts +20 -0
  243. package/src/rules/geometricSeamsNotCrossfades.ts +28 -0
  244. package/src/rules/geometrySidecarExportsTyped.test.ts +37 -0
  245. package/src/rules/geometrySidecarExportsTyped.ts +59 -0
  246. package/src/rules/glowIsAFocalAccent.ts +28 -0
  247. package/src/rules/heroFillsTheFrame.ts +24 -0
  248. package/src/rules/htmlMasksForAnimatedCounters.ts +21 -0
  249. package/src/rules/impactStackOnPayoffs.ts +26 -0
  250. package/src/rules/interactiveTargetsAreDeclaredData.ts +21 -0
  251. package/src/rules/interpolateLiteralKeysMonotonic.test.ts +49 -0
  252. package/src/rules/interpolateLiteralKeysMonotonic.ts +98 -0
  253. package/src/rules/latenessIsStructuralOverlapTheBeats.ts +20 -0
  254. package/src/rules/lessTextMoreVisualizations.ts +22 -0
  255. package/src/rules/liveLinkDuplicateReactAlias.ts +22 -0
  256. package/src/rules/make2dElements3d.ts +21 -0
  257. package/src/rules/masterMixLoudnessTarget.ts +22 -0
  258. package/src/rules/measureAlignmentDontEyeball.ts +20 -0
  259. package/src/rules/motionCarriesWeightAndMeaning.ts +21 -0
  260. package/src/rules/motionDensityFloor.ts +20 -0
  261. package/src/rules/neverEditTierBVendored.ts +20 -0
  262. package/src/rules/neverRestateDerivedFrameNumbersInComments.ts +20 -0
  263. package/src/rules/noAiGradientLook.ts +22 -0
  264. package/src/rules/noColoredFlashToHideACut.ts +29 -0
  265. package/src/rules/noDoubleFrameShift.test.ts +95 -0
  266. package/src/rules/noDoubleFrameShift.ts +193 -0
  267. package/src/rules/noDuplicateMotionPrimitives.ts +19 -0
  268. package/src/rules/noLinearMotion.ts +29 -0
  269. package/src/rules/noNestedSvgFilters.test.ts +47 -0
  270. package/src/rules/noNestedSvgFilters.ts +138 -0
  271. package/src/rules/noRepeatedAssetAcrossShots.ts +19 -0
  272. package/src/rules/noShorthandWithUndefinedLonghand.test.ts +35 -0
  273. package/src/rules/noShorthandWithUndefinedLonghand.ts +140 -0
  274. package/src/rules/noStackedFadesAtACut.ts +30 -0
  275. package/src/rules/noStaticHolds.ts +24 -0
  276. package/src/rules/occlusionVisibleElements.ts +26 -0
  277. package/src/rules/oneContinuousVoTrack.ts +20 -0
  278. package/src/rules/onePunchPerMomentClass.ts +20 -0
  279. package/src/rules/oneRenderPerRound.ts +46 -0
  280. package/src/rules/openStaticAfterAHardCut.ts +29 -0
  281. package/src/rules/openingsAndClosingsCarryMore.ts +21 -0
  282. package/src/rules/persistentDeviceFrames.ts +20 -0
  283. package/src/rules/pixelDiffAnEmptyBandFirst.ts +20 -0
  284. package/src/rules/planIsPublishedNotGating.ts +32 -0
  285. package/src/rules/poweredByRevolaWatermark.ts +19 -0
  286. package/src/rules/predrawContentPresentAtTheCut.ts +21 -0
  287. package/src/rules/premiumSurfacesNotFlatBorders.ts +20 -0
  288. package/src/rules/radarDispositionsAreHumanOnly.ts +28 -0
  289. package/src/rules/realAssetsNotPlaceholders.ts +21 -0
  290. package/src/rules/rebuildUiFromReconNotScreenshots.ts +21 -0
  291. package/src/rules/reconStrandsRunInParallel.ts +63 -0
  292. package/src/rules/registryBeforeHandrolling.ts +20 -0
  293. package/src/rules/remapRateEndpointFormula.ts +19 -0
  294. package/src/rules/remocnContentTailwindChrome.ts +19 -0
  295. package/src/rules/retimeOnlyViaPlaybackOrDesignTokens.ts +18 -0
  296. package/src/rules/reviewTheRenderByFrameSweep.ts +23 -0
  297. package/src/rules/rollingNumberBoundedSequence.ts +21 -0
  298. package/src/rules/rollingNumberSequenceLayoutNone.ts +22 -0
  299. package/src/rules/rollingNumberTightBoxAlignment.ts +19 -0
  300. package/src/rules/rollingNumberWhenNotToUse.ts +24 -0
  301. package/src/rules/rollingNumberWhenToUse.ts +23 -0
  302. package/src/rules/sameFamilySameProject.ts +20 -0
  303. package/src/rules/sampleBrandFromProduct.ts +19 -0
  304. package/src/rules/sampleVelocityFromPreviousFramePosition.ts +21 -0
  305. package/src/rules/sceneRootTagDeclared.test.ts +43 -0
  306. package/src/rules/sceneRootTagDeclared.ts +78 -0
  307. package/src/rules/scrubDemoDataToGenericRoles.ts +19 -0
  308. package/src/rules/selfReviewThePlanForGaps.ts +47 -0
  309. package/src/rules/sourceArchivedBeforeBrief.ts +24 -0
  310. package/src/rules/statisticsSelfExplanatory.ts +18 -0
  311. package/src/rules/stopsAsRelativeSpacing.ts +21 -0
  312. package/src/rules/theWholeCanvasWorks.ts +21 -0
  313. package/src/rules/themePerSection.ts +28 -0
  314. package/src/rules/thesisDeclaredInBrief.ts +25 -0
  315. package/src/rules/tierARequiresPropUnreachableEdit.ts +20 -0
  316. package/src/rules/tinyBodyCopyAsRuledLines.ts +19 -0
  317. package/src/rules/titlesPinnedOutsideCameraPush.ts +22 -0
  318. package/src/rules/tokensNotRawHexes.test.ts +51 -0
  319. package/src/rules/tokensNotRawHexes.ts +89 -0
  320. package/src/rules/typeTokenDefaultedParamsAsString.ts +21 -0
  321. package/src/rules/varyKitChoreographyAcrossSections.ts +21 -0
  322. package/src/rules/varyTheVisualizationGrammar.ts +22 -0
  323. package/src/rules/verifyCheapRenderDear.ts +18 -0
  324. package/src/rules/voAddressesTheViewer.ts +18 -0
  325. package/src/rules/voIsAPerformance.ts +21 -0
  326. package/src/rules/voScriptLocksSpokenWords.ts +21 -0
  327. package/src/rules/wrappersPreserveChildGeometry.ts +22 -0
  328. package/src/sourceCheck.test.ts +50 -0
  329. package/src/sourceCheck.ts +55 -0
  330. package/src/sourceCitations.test.ts +747 -0
  331. package/src/sourceCitations.ts +217 -0
  332. package/src/stageProjection.ts +112 -0
  333. package/src/stages.test.ts +68 -0
  334. package/src/stages.ts +207 -0
  335. package/src/testSupport.ts +16 -0
  336. package/src/types.ts +181 -0
  337. package/src/variants.test.ts +24 -0
  338. package/src/variants.ts +13 -0
  339. package/src/waiverProjection.test.ts +59 -0
  340. package/src/waiverProjection.ts +45 -0
@@ -0,0 +1,22 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/style/CREATIVE_EDGE.md directive 4 ("less text more
5
+ * visualizations", "the voiceover should be what drives most of the textual
6
+ * information so the video should be primarily visuals and animations",
7
+ * "remove unnecessary text") - balanced by the one counter-round where a
8
+ * scene needed orientation copy added back ("add some more text in scene 6
9
+ * and enhance animations so users are not lost").
10
+ */
11
+ export const lessTextMoreVisualizations: Rule = {
12
+ id: "less-text-more-visualizations",
13
+ claim: "The voiceover carries the information and the screen carries evidence: cut every text element that restates the VO or a neighboring label, replace word-lists with the actual marks and drawn subjects, and keep only text with a job - a name, a number with its denominator, the story spine for sound-off viewing, or one line of orientation copy where a viewer would otherwise be lost.",
14
+ why: "'Less text more visualizations' and 'too boring, no animations, text heavy and bad' recurred across videos - text-on-screen scenes got the PowerPoint verdict - while the one under-explained scene needed orientation copy added back, so comprehension, not word count, is the target.",
15
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
16
+ tier: "craft",
17
+ scope: { kind: "pipeline" },
18
+ enforcement: {
19
+ kind: "prose",
20
+ unanchorable: "A per-element editorial judgment against the VO script; no single code location expresses it.",
21
+ },
22
+ };
@@ -0,0 +1,22 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from HOW_I_BUILD_VIDEOS.md: "If the live-link render fails with a
5
+ * duplicate React/Remotion instance error, add the webpack alias override
6
+ * from the link-kit workflow." Note: the doc's "see remotion.config.ts
7
+ * mitigation" pointer does not currently resolve to live code in either
8
+ * project config (recorded follow-up in the design doc); linkKit is where
9
+ * the live-link workflow lives.
10
+ */
11
+ export const liveLinkDuplicateReactAlias: Rule = {
12
+ id: "live-link-duplicate-react-alias",
13
+ claim: "If a live-linked kit render fails with a duplicate React or Remotion instance error, fix it with the webpack alias override in the project's Remotion config, not by abandoning the link.",
14
+ why: "The symlinked kit can resolve a second React or Remotion copy from the monorepo root; without the known fix, the failure looks like a kit bug and costs a debugging session.",
15
+ learnedFrom: "HOW_I_BUILD_VIDEOS.md",
16
+ tier: "process",
17
+ scope: { kind: "pipeline" },
18
+ enforcement: {
19
+ kind: "prose",
20
+ anchor: { file: "packages/video-cli/src/kitShell.ts", symbol: "linkKit" },
21
+ },
22
+ };
@@ -0,0 +1,21 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/style/CREATIVE_EDGE.md directive 3 ("make 2d elements 3d",
5
+ * "much more cinematic, with amazing transitions - something zooming inside
6
+ * another one, a 3D effect, something rotating and changing", "More 3D
7
+ * elements too"). The cabinet-projection buildings and orbit camera moves
8
+ * were the approved implementations.
9
+ */
10
+ export const make2dElements3d: Rule = {
11
+ id: "make-2d-elements-3d",
12
+ claim: "Give flat hero elements depth: perspective or cabinet projection, parallax planes, rotation in space, zoom-through or orbit camera moves, ground-contact plus long-throw shadows; a flat cutout sitting on a flat ground is an amplify target, and external 3D packages are welcome when they serve the look.",
13
+ why: "'Make 2d elements 3d' and 'more 3D elements too' were standing amplify notes across videos; flat-on-flat heroes read as clipart while projected depth is what earned 'cinematic' in review.",
14
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
15
+ tier: "craft",
16
+ scope: { kind: "pipeline" },
17
+ enforcement: {
18
+ kind: "prose",
19
+ unanchorable: "A depth-treatment judgment per hero element; no single code location expresses it.",
20
+ },
21
+ };
@@ -0,0 +1,22 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/portfolio-reels.md: an external
5
+ * frame-by-frame audit flagged the mix at -20.3 LUFS as "objectively too
6
+ * quiet"; the fix normalized to -14 LUFS integrated with a -1 dBTP limiter
7
+ * (the streaming-platform standard), verified with ffmpeg loudnorm.
8
+ */
9
+ export const masterMixLoudnessTarget: Rule = {
10
+ id: "master-mix-loudness-target",
11
+ claim: "Master the final mix to -14 LUFS integrated with a -1 dBTP true-peak limit, and verify with ffmpeg loudnorm before delivery.",
12
+ why: "A -20 LUFS master shipped and was caught only by an external audit; the target is a fixed number, so missing it is pure process failure.",
13
+ learnedFrom: "claude-sessions-digest/portfolio-reels.md",
14
+ tier: "gate",
15
+ verify: "`video radar:film --project <dir> --video <mp4> --check loudness`",
16
+ owner: "loudness",
17
+ scope: { kind: "pipeline" },
18
+ enforcement: {
19
+ kind: "prose",
20
+ unanchorable: "The check runs on the rendered file at delivery time; no repo code location expresses it yet.",
21
+ },
22
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from the product-room build: 'the knowledge blocks aren't centered'
5
+ * was diagnosed by loading the PNG and comparing the card's left edge (x=203)
6
+ * to its first text pixel (x=205), which proved padding was being wiped.
7
+ * Eyeballing had missed it across three rounds.
8
+ */
9
+ export const measureAlignmentDontEyeball: Rule = {
10
+ id: "measure-alignment-dont-eyeball",
11
+ claim: "Answer an alignment or centering complaint with measured pixels - panel edge versus first content pixel, sibling baselines against each other - never by eye.",
12
+ why: "Eyeballing a downscaled frame hid a 2px content inset where 20px was intended and a 6px spread across three sibling CTAs; both were unarguable once measured, and the measurement also proved the fix.",
13
+ learnedFrom: "claude-sessions-digest/product-room-video.md",
14
+ tier: "process",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ unanchorable: "A review-time discipline over rendered PNGs; there is no source symbol that can assert it.",
19
+ },
20
+ };
@@ -0,0 +1,21 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/style/CREATIVE_EDGE.md directive 11 ("the building can
5
+ * fall from the sky and hit the ground, with impact and dust", "something
6
+ * should jump out of the cards and fade away, like giving the message that
7
+ * it falls", versus "the 4 things do not have to revolve - just make them
8
+ * bigger").
9
+ */
10
+ export const motionCarriesWeightAndMeaning: Rule = {
11
+ id: "motion-carries-weight-and-meaning",
12
+ claim: "Motion is physical and means something: objects obey weight - they fall, land with squash and dust, dock flush with a contact pulse, slam with a punch on settle - and the VO's verbs become literal choreography; ornamental motion with no narrative meaning (revolving for its own sake, dashed rings) gets cut.",
13
+ why: "Weighted, meaningful motion was requested by name ('fall from the sky and hit the ground, with impact and dust') and meaningless ornament was cut by name ('the 4 things do not have to revolve'); motion that neither enacts the script nor obeys physics read as filler.",
14
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
15
+ tier: "craft",
16
+ scope: { kind: "pipeline" },
17
+ enforcement: {
18
+ kind: "prose",
19
+ unanchorable: "A meaning-per-motion judgment against the VO script; no single code location expresses it.",
20
+ },
21
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from the product-room build: round one added continuous drift to
5
+ * every layer and was still rejected as static. The craft digest quantifies
6
+ * the target - ~15 beat starts in 348 design frames, 2-3 concurrent through
7
+ * the middle - and pairs it with lateness-is-structural-overlap-the-beats.
8
+ */
9
+ export const motionDensityFloor: Rule = {
10
+ id: "motion-density-floor",
11
+ claim: "Hold a visible state change roughly every half second, with two or three beats overlapping through the middle of a section; continuous background drift alone does not clear the bar.",
12
+ why: "A first pass that satisfied the no-static-holds rule with idle drift still came back as 'not much animation, just static screens'; the reference films run about one beat start every 0.6s with beats deliberately overlapping, and that density is what reads as authored.",
13
+ learnedFrom: "claude-sessions-digest/product-room-video.md",
14
+ tier: "craft",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ unanchorable: "A pacing property of a whole section, measurable only across its beat schedule.",
19
+ },
20
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from HOW_I_BUILD_VIDEOS.md: "They are used unmodified - theming is
5
+ * props + CSS vars, never edits (edits move you into the port lane and
6
+ * forfeit updates)." The hash-vs-upstream check needs network access, so it
7
+ * is not a vitest-gate source check; the rule text is what stops the edit.
8
+ */
9
+ export const neverEditTierBVendored: Rule = {
10
+ id: "never-edit-tier-b-vendored",
11
+ claim: "Never edit a Tier B vendored remocn source file; theme it only via props and CSS vars.",
12
+ why: "An edit silently moves the component into the port lane and forfeits update-safety: shadcn add can no longer pull upstream fixes without clobbering or conflicting.",
13
+ learnedFrom: "HOW_I_BUILD_VIDEOS.md",
14
+ tier: "process",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ anchor: { file: "revola-video/src/components/remocn/backdrop.tsx" },
19
+ },
20
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "S_S3's comment said 624
5
+ * while the computed value was 588 (stale from when D_S1 was 384) - cost a
6
+ * wasted render batch aimed at the wrong seam." The derived S_S2 = S_S1 +
7
+ * P_S1 chain in tokensCraft.ts is what comments must not restate.
8
+ */
9
+ export const neverRestateDerivedFrameNumbersInComments: Rule = {
10
+ id: "never-restate-derived-frame-numbers-in-comments",
11
+ claim: "Do not hand-copy derived frame values into comments; a comment stating a computed seam or start frame goes stale the moment an upstream duration moves.",
12
+ why: "S_S3's comment said 624 while the computed value was 588, stale from an earlier D_S1, which aimed an entire render batch at the wrong seam.",
13
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
14
+ tier: "craft",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ anchor: { file: "revola-video/src/tokensCraft.ts" },
19
+ },
20
+ };
@@ -0,0 +1,22 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/portfolio-reels.md ("Is it too
5
+ * blue-purple, like AI?"; "Don't use purple-blue; use the original colors")
6
+ * and the use-case digests ("the blue-purple gives AI vibes"; "the gradient
7
+ * colors are ai generated in scene 4"). The single most repeated feedback
8
+ * category across every video: generic blue-purple gradient washes, blob
9
+ * shapes, and heavy blur/glow backdrops read as AI-template output.
10
+ */
11
+ export const noAiGradientLook: Rule = {
12
+ id: "no-ai-gradient-look",
13
+ claim: "Never style with generic blue-purple gradient washes, blob shapes, or heavy blur/glow backdrops; backgrounds are a brand-appropriate base plus at most a subtle single-hue aura.",
14
+ why: "Viewers read the blue-purple gradient wash as AI-template output before they can name why; it was rejected in every single video and purple hexes kept creeping back into new code, costing repeated grep-and-purge rounds.",
15
+ learnedFrom: "claude-sessions-digest/portfolio-reels.md",
16
+ tier: "craft",
17
+ scope: { kind: "pipeline" },
18
+ enforcement: {
19
+ kind: "prose",
20
+ anchor: { file: "packages/video-kit/src/react/theme.tsx" },
21
+ },
22
+ };
@@ -0,0 +1,29 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "Violet light bridge. Fixed
5
+ * the hole ... but read as a purple wash on the frame. ... even tightened to
6
+ * ~14f it was rejected. The colour is the problem, not the timing." The rig
7
+ * was deleted, so the anchor is CraftStage's file - the place such an overlay
8
+ * would be reintroduced.
9
+ */
10
+ export const noColoredFlashToHideACut: Rule = {
11
+ id: "no-colored-flash-to-hide-a-cut",
12
+ claim: "Do not hide a cut behind a colored light flash or bloom bridge; the color reads as a wash on the frame regardless of how short you make it.",
13
+ why: "The violet light bridge made the cut genuinely invisible yet was rejected twice, at 40f and again tightened to ~14f, because the problem was the color, not the timing.",
14
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
15
+ tier: "craft",
16
+ exceptions: [
17
+ {
18
+ project: "qse-qguard-video",
19
+ decision:
20
+ "client chose ACT854's transition verbatim for the QGuard restyle, accent flash at the blade's apex included, over the sanctioned white LightSweep",
21
+ recordedIn: "qse-qguard-video/docs/motion-brand-spec.md",
22
+ },
23
+ ],
24
+ scope: { kind: "pipeline" },
25
+ enforcement: {
26
+ kind: "prose",
27
+ anchor: { file: "revola-video/src/scenes/craft/craftKit.tsx" },
28
+ },
29
+ };
@@ -0,0 +1,95 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Project, ts } from "ts-morph";
4
+ import { describe, expect, test } from "vitest";
5
+ import { toolchainRepoRoot as repoRoot } from "../testSupport";
6
+ import { checkFile } from "../sourceCheck";
7
+ import { noDoubleFrameShift } from "./noDoubleFrameShift";
8
+
9
+ const check = () => {
10
+ if (noDoubleFrameShift.enforcement.kind !== "source") throw new Error("expected a source rule");
11
+ return noDoubleFrameShift.enforcement.check;
12
+ };
13
+
14
+ const run = (name: string) => {
15
+ const rel = `packages/video-craft/src/fixtures/double-shift/${name}`;
16
+ return checkFile(check(), join(repoRoot, rel), rel, repoRoot);
17
+ };
18
+
19
+ /** The verbatim pre-fix scene is frozen as .tsx.txt (its real imports do not
20
+ * resolve inside video-craft, so it must stay outside the typecheck globs);
21
+ * parse it through a virtual .tsx path so the check sees real TSX. */
22
+ const runFrozen = (name: string) => {
23
+ const rel = `packages/video-craft/src/fixtures/double-shift/${name}`;
24
+ const text = readFileSync(join(repoRoot, rel), "utf8");
25
+ const project = new Project({
26
+ skipAddingFilesFromTsConfig: true,
27
+ compilerOptions: { allowJs: true, jsx: ts.JsxEmit.ReactJSX },
28
+ });
29
+ return check().run(project.createSourceFile("frozen-fixture.tsx", text), rel, repoRoot);
30
+ };
31
+
32
+ describe("noDoubleFrameShift", () => {
33
+ test("fires on build 4's pre-fix mechanism.tsx verbatim (`const lf = frame - win.from` inside a Sequence)", () => {
34
+ const findings = runFrozen("mechanism-prefix.tsx.txt");
35
+ expect(findings).toHaveLength(1);
36
+ expect(findings[0].match).toBe("win.from");
37
+ expect(findings[0].line).toBe(155);
38
+ expect(findings[0].message).toMatch(/Sequence already localizes/);
39
+ expect(findings[0].message).toMatch(/cue\(\)/);
40
+ });
41
+
42
+ test("fires on the aliased variant (window rebound through a second binding)", () => {
43
+ const findings = run("aliased.tsx");
44
+ expect(findings).toHaveLength(1);
45
+ expect(findings[0].match).toBe("w.from");
46
+ });
47
+
48
+ test("fires on the destructured variant (no `.from` access in the text)", () => {
49
+ const findings = run("destructured.tsx");
50
+ expect(findings).toHaveLength(1);
51
+ expect(findings[0].match).toBe("from: sectionStart");
52
+ });
53
+
54
+ test("fires on the wrapped variants (helpers over windowFor and SECTION_WINDOWS)", () => {
55
+ const findings = run("wrapped.tsx");
56
+ expect(findings.map((f) => f.match)).toEqual([
57
+ "windowFor(\"mechanism\").from",
58
+ "SECTION_WINDOWS[0].from",
59
+ ]);
60
+ });
61
+
62
+ test("fires on the element-access variant (`win[\"from\"]`)", () => {
63
+ const findings = run("elementAccess.tsx");
64
+ expect(findings).toHaveLength(1);
65
+ expect(findings[0].match).toBe("win[\"from\"]");
66
+ });
67
+
68
+ test("fires when the window flows through a plain assignment, not a declaration initializer", () => {
69
+ const findings = run("assigned.tsx");
70
+ expect(findings).toHaveLength(1);
71
+ expect(findings[0].match).toBe("w.from");
72
+ });
73
+
74
+ test("does not fire on cue()/cueEnd(), durationInFrames, non-window `.from`, or Array.from", () => {
75
+ expect(run("clean.tsx")).toEqual([]);
76
+ });
77
+
78
+ test("does not fire on a file with no layout-module import at all", () => {
79
+ // Any existing fixture without a layout import works as the null case.
80
+ const rel = "packages/video-craft/src/fixtures/deterministic-scene.tsx";
81
+ expect(checkFile(check(), join(repoRoot, rel), rel, repoRoot)).toEqual([]);
82
+ });
83
+
84
+ test("scopes to scene subtrees and exempts geometry sidecars and the qguard film-level Chrome layer", () => {
85
+ expect(check().include).toEqual([
86
+ "*/src/scenes/**/*.{js,jsx,ts,tsx}",
87
+ "*/03_scenes/**/*.{js,jsx,ts,tsx}",
88
+ ]);
89
+ expect(check().exclude).toEqual([
90
+ "*/src/scenes/scene.*.geometry.ts",
91
+ "qse-qguard-video/src/scenes/Chrome.tsx",
92
+ "reverence-video/src/scenes/Mix.tsx",
93
+ ]);
94
+ });
95
+ });
@@ -0,0 +1,193 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+ import type { Rule, SourceFinding } from "../types";
3
+
4
+ /**
5
+ * Mined from build 4 (docs/superpowers/specs/2026-08-13-build-speed-phase-spec.md,
6
+ * WS5): 2 of 3 generated scenes computed `const lf = frame - win.from` INSIDE
7
+ * their section's Sequence. A Sequence already localizes useCurrentFrame(),
8
+ * so subtracting the window's composition-space `from` shifts the clock twice -
9
+ * harmless for section 0 (from === 0), catastrophic after it: mechanism's
10
+ * whole rig rendered 264 frames in the past, and nothing failed. Layout's
11
+ * cue()/cueEnd() are the sanctioned in-scene clock; a scene module has no
12
+ * business reading a section window's `from` at all.
13
+ *
14
+ * The check is AST-level, not a regex, because the evasion shapes are the
15
+ * natural way a generator writes the bug: an aliased window
16
+ * (`const w = win; frame - w.from`), a destructured `from`
17
+ * (`const { from } = windowFor(id)`), and helper wrappers whose call sites
18
+ * never mention `from`. It activates only in files that import `windowFor`
19
+ * or `SECTION_WINDOWS` (aliases included) from a module whose specifier
20
+ * basename is `layout`, then taints every binding transitively assigned a
21
+ * window and reports each `.from` read (property access or destructure) on a
22
+ * tainted expression. `.from` on anything untainted (a token object,
23
+ * `Array.from`) never matches, and `durationInFrames` stays legal - a
24
+ * window's length is Sequence-local by definition.
25
+ *
26
+ * Scoping carries the other half of the semantics: film-level seam math is
27
+ * legitimate (Film.tsx computing SFX anchors from a window's `from` reads the
28
+ * composition timeline), so the globs cover only scene subtrees, and the three
29
+ * known scene-directory residents that run on the composition clock - geometry
30
+ * sidecars, qse-qguard's Chrome layer (rendered outside any Sequence in
31
+ * QGuardFilm.tsx), and reverence-video's Mix layer (same, in ReverenceFilm.tsx)
32
+ * - are excluded by name. The tell for this class is always where the
33
+ * component is MOUNTED, never where the file lives.
34
+ */
35
+ export const noDoubleFrameShift: Rule = {
36
+ id: "no-double-frame-shift",
37
+ claim: "Never rebase a scene's clock by its section window's `from`: a Sequence already localizes frames, so a scene module must not read `windowFor(...).from` (aliased, destructured, or wrapped included); layout's cue()/cueEnd() are the sanctioned clock.",
38
+ why: "Build 4's mechanism scene computed `frame - win.from` inside its Sequence, shifting the clock twice; the whole rig rendered 264 frames in the past and nothing failed - the film shipped a round late because only human eyes caught it.",
39
+ learnedFrom: "docs/superpowers/specs/2026-08-13-build-speed-phase-spec.md",
40
+ tier: "gate",
41
+ verify: "`vitest run packages/video-craft` - machine-checked over every scene subtree",
42
+ scope: { kind: "pipeline" },
43
+ enforcement: {
44
+ kind: "source",
45
+ check: {
46
+ // Scene subtrees only, every project (`*` head = wildcard coverage):
47
+ // the rule keys on a layout-module import, so projects without the
48
+ // windowFor pattern are inert by construction. Film-level modules live
49
+ // outside scenes/ and stay exempt; the two composition-clock residents
50
+ // OF scenes/ are excluded by name (see the header comment).
51
+ include: [
52
+ "*/src/scenes/**/*.{js,jsx,ts,tsx}",
53
+ "*/03_scenes/**/*.{js,jsx,ts,tsx}",
54
+ ],
55
+ exclude: [
56
+ "*/src/scenes/scene.*.geometry.ts",
57
+ "qse-qguard-video/src/scenes/Chrome.tsx",
58
+ // The film's SFX mixer, not a scene: ReverenceFilm.tsx mounts <Mix /> as
59
+ // a sibling of the SECTION_WINDOWS.map() block, outside every section
60
+ // Sequence, so its `windowFor(id).from + local` IS the composition
61
+ // timeline - the same film-level seam math this rule's header calls
62
+ // legitimate, and the same shape as rcp-fund7's Film.tsx SFX anchors.
63
+ "reverence-video/src/scenes/Mix.tsx",
64
+ ],
65
+ run(file, relPath): SourceFinding[] {
66
+ // 1. Local names bound to windowFor / SECTION_WINDOWS from a module
67
+ // whose specifier basename is "layout" (aliased imports included).
68
+ const windowForNames = new Set<string>();
69
+ const windowsNames = new Set<string>();
70
+ for (const imp of file.getImportDeclarations()) {
71
+ const specifier = imp.getModuleSpecifierValue();
72
+ if (specifier.split("/").pop() !== "layout") continue;
73
+ for (const named of imp.getNamedImports()) {
74
+ const source = named.getName();
75
+ const local = named.getAliasNode()?.getText() ?? source;
76
+ if (source === "windowFor") windowForNames.add(local);
77
+ if (source === "SECTION_WINDOWS") windowsNames.add(local);
78
+ }
79
+ }
80
+ if (windowForNames.size === 0 && windowsNames.size === 0) return [];
81
+
82
+ // 2. Taint every binding transitively assigned a window. Fixpoint,
83
+ // because aliases can chain (`const w = win`) in either source order.
84
+ const windowVars = new Set<string>();
85
+ const unwrap = (node: Node): Node => {
86
+ let n = node;
87
+ while (
88
+ Node.isParenthesizedExpression(n) ||
89
+ Node.isAsExpression(n) ||
90
+ Node.isNonNullExpression(n) ||
91
+ Node.isSatisfiesExpression(n)
92
+ ) {
93
+ n = n.getExpression();
94
+ }
95
+ return n;
96
+ };
97
+ /** The leftmost identifier of an access/call chain, e.g.
98
+ * `SECTION_WINDOWS[0]`, `SECTION_WINDOWS.find(...)`. */
99
+ const rootIdentifier = (node: Node): string | undefined => {
100
+ let n = unwrap(node);
101
+ while (
102
+ Node.isPropertyAccessExpression(n) ||
103
+ Node.isElementAccessExpression(n) ||
104
+ Node.isCallExpression(n)
105
+ ) {
106
+ n = unwrap(n.getExpression());
107
+ }
108
+ return Node.isIdentifier(n) ? n.getText() : undefined;
109
+ };
110
+ const isWindowExpr = (node: Node): boolean => {
111
+ const n = unwrap(node);
112
+ if (Node.isIdentifier(n)) return windowVars.has(n.getText());
113
+ if (Node.isCallExpression(n)) {
114
+ const callee = unwrap(n.getExpression());
115
+ if (Node.isIdentifier(callee) && windowForNames.has(callee.getText())) return true;
116
+ }
117
+ // Anything rooted at SECTION_WINDOWS (an index, a .find(...)) is a
118
+ // window as far as a `.from` read is concerned.
119
+ const root = rootIdentifier(n);
120
+ return root !== undefined && windowsNames.has(root);
121
+ };
122
+
123
+ // Known limitation: no inter-procedural taint. A window flowing
124
+ // through a FUNCTION PARAMETER (`const helper = (w) => frame - w.from`)
125
+ // is not tracked - the parameter binding is never tainted, so `.from`
126
+ // reads inside the helper's body escape this rule.
127
+ //
128
+ // The AST is never mutated, so descendant queries are hoisted out of
129
+ // the fixpoint loop and the cached arrays are iterated instead.
130
+ const varDecls = file.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
131
+ const binaryExprs = file.getDescendantsOfKind(SyntaxKind.BinaryExpression);
132
+ for (let grew = true; grew; ) {
133
+ grew = false;
134
+ const taint = (name: Node) => {
135
+ if (Node.isIdentifier(name) && !windowVars.has(name.getText())) {
136
+ windowVars.add(name.getText());
137
+ grew = true;
138
+ }
139
+ };
140
+ for (const decl of varDecls) {
141
+ const init = decl.getInitializer();
142
+ if (!init || !isWindowExpr(init)) continue;
143
+ taint(decl.getNameNode());
144
+ }
145
+ // Assignment taint: `let w; w = windowFor(id)` - a plain `=` whose
146
+ // right-hand side is a window taints the assigned identifier just
147
+ // like a declaration initializer would.
148
+ for (const bin of binaryExprs) {
149
+ if (bin.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) continue;
150
+ if (!isWindowExpr(bin.getRight())) continue;
151
+ taint(unwrap(bin.getLeft()));
152
+ }
153
+ }
154
+
155
+ // 3. Report every `.from` read on a tainted expression: property
156
+ // accesses and destructures alike.
157
+ const findings: SourceFinding[] = [];
158
+ const report = (node: Node) => {
159
+ findings.push({
160
+ file: relPath,
161
+ line: node.getStartLineNumber(),
162
+ match: node.getText().replace(/\s+/g, " ").slice(0, 60),
163
+ message:
164
+ "a Sequence already localizes the clock: reading a section window's `from` inside a scene " +
165
+ "module double-shifts every frame it feeds (build 4's mechanism rendered 264 frames in the " +
166
+ "past); use layout's cue()/cueEnd() for in-scene beats",
167
+ });
168
+ };
169
+ for (const access of file.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
170
+ if (access.getName() !== "from") continue;
171
+ if (isWindowExpr(access.getExpression())) report(access);
172
+ }
173
+ // Element access spelling: `win["from"]` reads the same field a
174
+ // property access does; only literal "from" keys match.
175
+ for (const access of file.getDescendantsOfKind(SyntaxKind.ElementAccessExpression)) {
176
+ const arg = unwrap(access.getArgumentExpression() ?? access);
177
+ if (!Node.isStringLiteral(arg) || arg.getLiteralText() !== "from") continue;
178
+ if (isWindowExpr(access.getExpression())) report(access);
179
+ }
180
+ for (const decl of varDecls) {
181
+ const name = decl.getNameNode();
182
+ const init = decl.getInitializer();
183
+ if (!Node.isObjectBindingPattern(name) || !init || !isWindowExpr(init)) continue;
184
+ for (const element of name.getElements()) {
185
+ const property = element.getPropertyNameNode()?.getText() ?? element.getName();
186
+ if (property === "from") report(element);
187
+ }
188
+ }
189
+ return findings;
190
+ },
191
+ },
192
+ },
193
+ };
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from HOW_I_BUILD_VIDEOS.md: "Note revola-video already has a
5
+ * hand-built WhipPan in src/transitions/; do not create a third." The anchor
6
+ * is the existing primitive the doc warns against duplicating.
7
+ */
8
+ export const noDuplicateMotionPrimitives: Rule = {
9
+ id: "no-duplicate-motion-primitives",
10
+ claim: "Before adding a registry component or porting one, check for an existing hand-built equivalent in the project; never create a second implementation of the same primitive.",
11
+ why: "Two implementations of the same transition drift apart and double every future styling and timing fix - the _lib fork problem at component granularity.",
12
+ learnedFrom: "HOW_I_BUILD_VIDEOS.md",
13
+ tier: "process",
14
+ scope: { kind: "pipeline" },
15
+ enforcement: {
16
+ kind: "prose",
17
+ anchor: { file: "revola-video/src/transitions/WhipPan.tsx" },
18
+ },
19
+ };
@@ -0,0 +1,29 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from HOW_I_BUILD_VIDEOS.md: "everything animates via spring() or
5
+ * eased interpolate() - nothing linear". Demoted from an AST check because
6
+ * flagging interpolate() without an easing option would hit legitimate
7
+ * non-visible uses (progress plumbing, spring-driven inputs); this is
8
+ * doctrine, not mechanics.
9
+ */
10
+ export const noLinearMotion: Rule = {
11
+ id: "no-linear-motion",
12
+ claim: "Animate everything with spring() or eased interpolate(); never linear interpolation for visible motion.",
13
+ why: "Linear motion reads as mechanical and cheap - the doctrine violation viewers perceive even when they cannot name it.",
14
+ learnedFrom: "HOW_I_BUILD_VIDEOS.md",
15
+ tier: "craft",
16
+ exceptions: [
17
+ {
18
+ project: "qse-qguard-video",
19
+ decision:
20
+ "ACT854's un-eased win()/camPush() envelopes are ported verbatim at the client's direction; eased variants ship alongside behind a one-line flag",
21
+ recordedIn: "qse-qguard-video/docs/motion-brand-spec.md",
22
+ },
23
+ ],
24
+ scope: { kind: "pipeline" },
25
+ enforcement: {
26
+ kind: "prose",
27
+ anchor: { file: "packages/video-kit/src/react/motion.ts" },
28
+ },
29
+ };
@@ -0,0 +1,47 @@
1
+ import { join } from "node:path";
2
+ import { describe, expect, test } from "vitest";
3
+ import { toolchainRepoRoot as repoRoot } from "../testSupport";
4
+ import { checkFile } from "../sourceCheck";
5
+ import { noNestedSvgFilters } from "./noNestedSvgFilters";
6
+
7
+ const check = () => {
8
+ if (noNestedSvgFilters.enforcement.kind !== "source") throw new Error("expected a source rule");
9
+ return noNestedSvgFilters.enforcement.check;
10
+ };
11
+
12
+ const run = (name: string) => {
13
+ const rel = `packages/video-craft/src/fixtures/${name}`;
14
+ return checkFile(check(), join(repoRoot, rel), rel, repoRoot);
15
+ };
16
+
17
+ describe("noNestedSvgFilters", () => {
18
+ test("fires on a filtered element inside a filtered ancestor, once, on the inner element", () => {
19
+ const findings = run("nested-filters-broken.tsx");
20
+ expect(findings).toHaveLength(1);
21
+ expect(findings[0].message).toMatch(/nested CSS filter/);
22
+ expect(findings[0].line).toBe(8);
23
+ expect(findings[0].match).toContain('<path d="M0 0 L10 30"');
24
+ expect(findings[0].match.length).toBeLessThanOrEqual(60);
25
+ });
26
+
27
+ test("does not fire when only the outer layer is filtered", () => {
28
+ expect(run("nested-filters-fixed.tsx")).toEqual([]);
29
+ });
30
+
31
+ test("does not fire on filtered siblings", () => {
32
+ expect(run("nested-filters-siblings.tsx")).toEqual([]);
33
+ });
34
+
35
+ test("does not fire on nested filters outside an <svg>", () => {
36
+ expect(run("nested-filters-html.tsx")).toEqual([]);
37
+ });
38
+
39
+ test("does not fire when the filtered ancestor is an HTML wrapper around the <svg>", () => {
40
+ expect(run("nested-filters-html-wrapper.tsx")).toEqual([]);
41
+ });
42
+
43
+ test("covers both project layouts", () => {
44
+ expect(check().include).toContain("qpa-video/03_scenes/**/*.jsx");
45
+ expect(check().include).toContain("revola-video/src/scenes/**/*.tsx");
46
+ });
47
+ });