@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,341 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, relative, sep } from "node:path";
3
+ import { globSync } from "tinyglobby";
4
+ import { SyntaxKind, type JsxAttribute, type SourceFile } from "ts-morph";
5
+ import { parse as parseYaml } from "yaml";
6
+ import { matchedFiles } from "../sourceCheck";
7
+ import type { Rule, SourceCheck, SourceFinding } from "../types";
8
+
9
+ /**
10
+ * Mined from docs/superpowers/specs/2026-07-31-defect-radar-design.md: "the
11
+ * hero and every element the plan's `names:` map declares carry `data-radar`
12
+ * attributes; the source-level vitest gate in packages/video-craft checks
13
+ * scene source against the declared element ids, so the gate verifies the
14
+ * binding, not merely that some tag exists." (line 334). The runtime
15
+ * counterpart is the `tag-coverage` radar check, which verifies the same
16
+ * binding against SAMPLED frames; this rule is the free, no-render
17
+ * precondition - a typo'd or forgotten `data-radar` attribute is a build-time
18
+ * fact, not something that needs a render to discover.
19
+ *
20
+ * SCOPE: PER PROJECT, NOT PER SCENE FILE (final-review I4).
21
+ *
22
+ * The first implementation asked "does THIS file carry a `data-radar` for
23
+ * every element the plan binds to the section THIS file implements", matching
24
+ * file to section by `exportedNames.has(section.component)`. Both halves were
25
+ * wrong against the first real adopter:
26
+ *
27
+ * 1. The name match never fired. `qse-qguard-video/plan.qguard.yaml` declares
28
+ * `component: S1LoginChecksOnce` while the module exports `S1Once` (and
29
+ * five more in the same shape), so the rule returned zero findings on the
30
+ * pilot and read as clean. `video lint` now emits a WARNING for exactly
31
+ * that mismatch - it is also the root cause of the fabricated `where.file`
32
+ * path on every QGuard finding (`sceneFileOf` returns `component`
33
+ * verbatim) - but it is a plan-hygiene defect, not this rule's subject.
34
+ * 2. Even with a correct file match, per-file scope would produce FALSE
35
+ * POSITIVES. A section's declared element is routinely tagged in a child
36
+ * component, not in the scene module: QGuard's `login-scanner` lives in
37
+ * `src/artifacts/Stations.tsx`, its `branch-*` ids in `Theft.tsx`. Which
38
+ * module renders inside a scene at runtime is not decidable from JSX text,
39
+ * and guessing it would make a Gate-tier rule fire on correct code.
40
+ *
41
+ * So the honest source-level claim is the project-wide one: every id the plan
42
+ * declares is tagged SOMEWHERE in that project's source. Binding a tag to the
43
+ * right scene, at the right frame, is what the runtime `tag-coverage` check
44
+ * settles against sampled geometry - and it cannot be settled here.
45
+ *
46
+ * WHAT IS AND IS NOT PINNED, since project-wide is the weaker claim and a
47
+ * future reader should not over-read the evidence:
48
+ *
49
+ * - The CLEAN side on the pilot IS continuously checked. `corpus.test.ts`
50
+ * runs every source rule over every matched file, qse-qguard-video
51
+ * included, and fails on any finding that is not a justified
52
+ * KNOWN_EXCEPTIONS entry. If QGuard ever declares an id nothing tags, the
53
+ * suite goes red on the real project, not just on a fixture.
54
+ *
55
+ * What keeps that pin from being hollow: this rule carries NO
56
+ * KNOWN_EXCEPTIONS rows at all, so nothing on the pilot is excused. An
57
+ * exception added here would quietly convert "the pilot is clean" into
58
+ * "the pilot's failures are listed elsewhere" - which is the same
59
+ * green-because-nobody-looked state the include-glob drift gate
60
+ * (`includeCoverage.test.ts`) exists to prevent. Add one only with a
61
+ * reason that survives being read back a year later.
62
+ * - The RED side is pinned on FIXTURES only (`../fixtures/data-radar-*`: a
63
+ * missing tag, a sibling-file tag, a template family, a forwarded prop,
64
+ * an uninterpretable expression). The equivalent demonstration on the
65
+ * pilot - typo one declared id, get exactly one finding - was run by hand
66
+ * at the final-review fix wave and is deliberately not committed: the
67
+ * only way to assert it would be to keep a broken copy of a client plan
68
+ * in the tree.
69
+ */
70
+
71
+ type PlanSection = {
72
+ id?: string;
73
+ component?: string;
74
+ names?: { element?: string | null }[];
75
+ };
76
+
77
+ type PlanLike = { sections?: PlanSection[] };
78
+
79
+ const toPosix = (p: string): string => p.split(sep).join("/");
80
+
81
+ /**
82
+ * Every project layout, enumerated explicitly - the silent-default trap
83
+ * `deterministic-scene-source` was already bitten by once (act854-video's
84
+ * scenes live under qpa-video/03_scenes/act854, and
85
+ * klaviyo-segments-video/radar-fixture-video were never added to any rule's
86
+ * include list). Ten entries: `ls -d *-video spec-videos` at implementation
87
+ * time (2026-08-02) plus datapeak-usecases, which that glob does not match but
88
+ * which other source-check rules already cover.
89
+ */
90
+ export const INCLUDE = [
91
+ "qpa-video/03_scenes/**/*.{js,jsx,ts,tsx}",
92
+ "revola-video/src/scenes/**/*.{js,jsx,ts,tsx}", "workday-dple-video/src/scenes/**/*.{js,jsx,ts,tsx}",
93
+ "datapeak-video/**/*.{js,jsx,ts,tsx}",
94
+ "datapeak-usecases/**/*.{js,jsx,ts,tsx}",
95
+ "qse-video/**/*.{js,jsx,ts,tsx}",
96
+ "qse-investor-video/**/*.{js,jsx,ts,tsx}",
97
+ "qse-qguard-video/src/**/*.{ts,tsx}",
98
+ "spec-videos/**/*.{js,jsx,ts,tsx}",
99
+ "klaviyo-segments-video/**/*.{js,jsx,ts,tsx}",
100
+ "radar-fixture-video/src/**/*.{js,jsx,ts,tsx}",
101
+ "rcp-fund7-video/src/**/*.{ts,tsx}",
102
+ "expanse-video/src/**/*.{ts,tsx}",
103
+ "reverence-video/src/**/*.{ts,tsx}",
104
+ ];
105
+
106
+ /**
107
+ * Walks up from `startDir` toward `root`, returning the first directory
108
+ * (inclusive of `startDir`) that contains at least one `plan*.yaml`. A
109
+ * project's plan lives at its own root (`qse-qguard-video/plan.qguard.yaml`),
110
+ * not necessarily next to the scene file that implements one of its
111
+ * sections, so this looks upward rather than assuming a fixed depth.
112
+ */
113
+ export function findPlanDir(startDir: string, root: string): string | null {
114
+ let dir = startDir;
115
+ for (;;) {
116
+ if (globSync("plan*.yaml", { cwd: dir }).length > 0) return dir;
117
+ if (dir === root || !dir.startsWith(root)) return null;
118
+ const parent = dirname(dir);
119
+ if (parent === dir) return null;
120
+ dir = parent;
121
+ }
122
+ }
123
+
124
+ /** Every `names:`-declared, non-null element id in `planDir`'s plans, with the
125
+ * section that declared it. Null bindings (`element: null`, an honest "no
126
+ * single subject") are excluded, same as the runtime `tag-coverage` check. */
127
+ export function declaredElements(planDir: string, root: string): { sectionId: string; elementId: string; planFile: string }[] {
128
+ const out: { sectionId: string; elementId: string; planFile: string }[] = [];
129
+ for (const planFile of globSync("plan*.yaml", { cwd: planDir, absolute: true })) {
130
+ let plan: PlanLike;
131
+ try {
132
+ plan = parseYaml(readFileSync(planFile, "utf8")) as PlanLike;
133
+ } catch {
134
+ continue; // A malformed plan is `lint`'s problem, not this check's.
135
+ }
136
+ for (const section of plan.sections ?? []) {
137
+ for (const binding of section.names ?? []) {
138
+ if (typeof binding.element !== "string") continue;
139
+ out.push({
140
+ sectionId: section.id ?? "(unnamed section)",
141
+ elementId: binding.element,
142
+ planFile: toPosix(relative(root, planFile)),
143
+ });
144
+ }
145
+ }
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /** What a project's source says about `data-radar`. */
151
+ type TagIndex = {
152
+ /** Statically known tag values. */
153
+ literals: Set<string>;
154
+ /** Templates with substitutions, e.g. `` `branch-${kind}` `` -> /^branch-.+$/.
155
+ * A declared id matching one is accepted: the source demonstrably tags a
156
+ * family the id belongs to, and refusing it would be a false positive. */
157
+ patterns: RegExp[];
158
+ /** `data-radar={expr}` sites this analysis could not turn into any value.
159
+ * A project with one cannot be checked honestly, so it is reported as a
160
+ * finding of its own rather than silently reading as clean. */
161
+ opaque: { file: string; line: number; text: string }[];
162
+ };
163
+
164
+ const isTagAttrName = (name: string): boolean => name === "data-radar";
165
+
166
+ /** The prop-passthrough convention: a component that forwards a tag takes it
167
+ * as a prop named `dataRadar` (`data-radar={dataRadar}`), and the real ids
168
+ * are the string literals its CALL SITES pass. Resolving those is what keeps
169
+ * a forwarding component from making the whole project unverifiable. */
170
+ const attrLiteralsByName = (files: SourceFile[]): Map<string, Set<string>> => {
171
+ const byName = new Map<string, Set<string>>();
172
+ for (const file of files) {
173
+ for (const attr of file.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
174
+ const value = literalOf(attr.getInitializer());
175
+ if (value === null) continue;
176
+ const name = attr.getNameNode().getText();
177
+ const set = byName.get(name) ?? new Set<string>();
178
+ set.add(value);
179
+ byName.set(name, set);
180
+ }
181
+ }
182
+ return byName;
183
+ };
184
+
185
+ /** The string value of a JSX attribute initializer, when it has one:
186
+ * `x="s"`, `x={"s"}`, and `` x={`s`} `` (no substitutions) all count. */
187
+ function literalOf(init: ReturnType<JsxAttribute["getInitializer"]>): string | null {
188
+ if (!init) return null;
189
+ if (init.isKind(SyntaxKind.StringLiteral)) return init.getLiteralValue();
190
+ if (init.isKind(SyntaxKind.JsxExpression)) {
191
+ const expr = init.getExpression();
192
+ if (!expr) return null;
193
+ if (expr.isKind(SyntaxKind.StringLiteral)) return expr.getLiteralValue();
194
+ if (expr.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)) return expr.getLiteralValue();
195
+ }
196
+ return null;
197
+ }
198
+
199
+ /** A template with substitutions as a pattern: every `${...}` becomes `.+`,
200
+ * every literal chunk is escaped. `` `branch-${kind}` `` -> /^branch-.+$/. */
201
+ function templatePattern(text: string): RegExp | null {
202
+ const body = text.replace(/^`|`$/g, "");
203
+ if (!body.includes("${")) return null;
204
+ const escaped = body
205
+ .split(/\$\{[^}]*\}/g)
206
+ .map((chunk) => chunk.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
207
+ .join(".+");
208
+ return new RegExp(`^${escaped}$`);
209
+ }
210
+
211
+ function buildTagIndex(files: SourceFile[], root: string): TagIndex {
212
+ const literals = new Set<string>();
213
+ const patterns: RegExp[] = [];
214
+ const opaque: { file: string; line: number; text: string }[] = [];
215
+ const byAttrName = attrLiteralsByName(files);
216
+
217
+ for (const file of files) {
218
+ const rel = toPosix(relative(root, file.getFilePath()));
219
+ for (const attr of file.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
220
+ if (!isTagAttrName(attr.getNameNode().getText())) continue;
221
+ const init = attr.getInitializer();
222
+ const literal = literalOf(init);
223
+ if (literal !== null) { literals.add(literal); continue; }
224
+ const expr = init?.isKind(SyntaxKind.JsxExpression) ? init.getExpression() : undefined;
225
+ if (expr?.isKind(SyntaxKind.TemplateExpression)) {
226
+ const pattern = templatePattern(expr.getText());
227
+ if (pattern) { patterns.push(pattern); continue; }
228
+ }
229
+ if (expr?.isKind(SyntaxKind.Identifier)) {
230
+ // A forwarded tag: take the literals its call sites pass for the
231
+ // prop of the same name. Empty means nothing in the project ever
232
+ // passes it a literal, which is genuinely unverifiable here.
233
+ const forwarded = byAttrName.get(expr.getText());
234
+ if (forwarded?.size) { for (const v of forwarded) literals.add(v); continue; }
235
+ }
236
+ opaque.push({ file: rel, line: attr.getStartLineNumber(), text: attr.getText().slice(0, 60) });
237
+ }
238
+ }
239
+ return { literals, patterns, opaque };
240
+ }
241
+
242
+ const IGNORES = ["**/node_modules/**", "**/.worktrees/**", "**/04_exports/**", "**/*.test.{ts,tsx,jsx}", "**/build/**"];
243
+
244
+ /** The project's own source files that this rule's `include` globs reach. */
245
+ const includedUnder = (planDir: string, check: SourceCheck, root: string): string[] =>
246
+ matchedFiles(check, root).filter((f) => f.startsWith(planDir + sep));
247
+
248
+ /**
249
+ * Every source file of the project, because a declared id is routinely tagged
250
+ * in a child component rather than in the scene module.
251
+ *
252
+ * Falls back to a plain walk of `planDir` when the rule's own `include` globs
253
+ * reach nothing there - the `checkFile()` unit-test path, whose fixture
254
+ * projects live under packages/video-craft/src/fixtures and are deliberately
255
+ * outside the corpus globs. Without the fallback a fixture would be checked
256
+ * against its own single file and report every id its siblings tag.
257
+ */
258
+ export function projectSourceFiles(planDir: string, check: SourceCheck, root: string): string[] {
259
+ const included = includedUnder(planDir, check, root);
260
+ if (included.length) return included;
261
+ return globSync("**/*.{js,jsx,ts,tsx}", { cwd: planDir, absolute: true, ignore: IGNORES }).sort();
262
+ }
263
+
264
+ /**
265
+ * The one file per project that reports its project-wide findings.
266
+ *
267
+ * `runSourceCheck` calls `run` once per matched file, in sorted order, so a
268
+ * project-wide finding emitted from every file would appear once per file.
269
+ * The first matched file under the project is a stable, deterministic
270
+ * reporter. A file outside the include set entirely (the unit-test path
271
+ * above) reports for itself, so a fixture is never silently unchecked.
272
+ */
273
+ export function isReporterFile(planDir: string, absPath: string, check: SourceCheck, root: string): boolean {
274
+ const included = includedUnder(planDir, check, root);
275
+ return included.length === 0 || included[0] === absPath;
276
+ }
277
+
278
+ export const dataRadarTagsDeclared: Rule = {
279
+ id: "data-radar-tags-declared",
280
+ claim: "Every element id a plan's `names:` binding declares carries a matching `data-radar` attribute somewhere in that project's source.",
281
+ why: "vo-element-sync joins a declared binding to speech-map timings and geometry samples; a declared element-id with no matching data-radar tag anywhere in the project is a certain finding, and catching it at authoring time (no render, no sampler) is strictly cheaper than catching it at radar:scene time.",
282
+ learnedFrom: "docs/superpowers/specs/2026-07-31-defect-radar-design.md",
283
+ tier: "gate",
284
+ verify: "`vitest run packages/video-craft` (source-level, no render) - `video radar:scene --project <dir> --comp <id> --check tag-coverage` re-verifies the same binding against sampled frames at runtime",
285
+ owner: "tag-coverage",
286
+ scope: { kind: "pipeline" },
287
+ enforcement: {
288
+ kind: "source",
289
+ check: {
290
+ include: INCLUDE,
291
+ run(file, relPath, root): SourceFinding[] {
292
+ const absPath = join(root, relPath);
293
+ const planDir = findPlanDir(dirname(absPath), root);
294
+ if (!planDir || !existsSync(planDir)) return [];
295
+ if (dataRadarTagsDeclared.enforcement.kind !== "source") return [];
296
+ const selfCheck = dataRadarTagsDeclared.enforcement.check;
297
+ if (!isReporterFile(planDir, absPath, selfCheck, root)) return [];
298
+
299
+ const declared = declaredElements(planDir, root);
300
+ if (declared.length === 0) return [];
301
+
302
+ // Parsed in the SAME ts-morph Project the runner already built
303
+ // (re-adding an open file is a no-op), so the whole-project scan
304
+ // costs one parse per file per pass, not one per file per file.
305
+ const project = file.getProject();
306
+ const index = buildTagIndex(
307
+ projectSourceFiles(planDir, selfCheck, root).map((f) => project.addSourceFileAtPath(f)),
308
+ root,
309
+ );
310
+
311
+ const findings: SourceFinding[] = [];
312
+ for (const site of index.opaque) {
313
+ findings.push({
314
+ file: site.file,
315
+ line: site.line,
316
+ match: site.text,
317
+ message: `data-radar is bound to an expression this gate cannot evaluate (${site.text}), so no declared `
318
+ + `element id in ${toPosix(relative(root, planDir))} can be verified from source. Use a string `
319
+ + `literal, a template whose literal prefix is stable, or forward it through a prop named `
320
+ + `\`dataRadar\` that call sites pass a literal to.`,
321
+ });
322
+ }
323
+ if (findings.length) return findings;
324
+
325
+ for (const { sectionId, elementId, planFile } of declared) {
326
+ if (index.literals.has(elementId)) continue;
327
+ if (index.patterns.some((p) => p.test(elementId))) continue;
328
+ findings.push({
329
+ file: planFile,
330
+ line: 1,
331
+ match: elementId,
332
+ message: `plan section "${sectionId}" declares names: element "${elementId}", but no data-radar="${elementId}" `
333
+ + `attribute exists anywhere in ${toPosix(relative(root, planDir))}'s source. Tag the element the cue `
334
+ + `names, or make the binding an explained \`element: null\`.`,
335
+ });
336
+ }
337
+ return findings;
338
+ },
339
+ },
340
+ },
341
+ };
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "ZONE MAP now declared at the
5
+ * top of S3Line.tsx (same device as S1's): copy 96-290 · BREATH 290-420
6
+ * (nothing may enter) · stage 420-840".
7
+ */
8
+ export const declareAVerticalZoneMap: Rule = {
9
+ id: "declare-a-vertical-zone-map",
10
+ claim: "Declare a vertical zone map at the top of each scene file - copy band, breath band (nothing may enter), stage band - and make camera keys and world layout enforce that animation never rises into the copy band.",
11
+ why: "Without declared bands, text and animation collide (a tag overlapping the headline, fog slicing the clock) and each collision gets patched ad hoc.",
12
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
13
+ tier: "craft",
14
+ scope: { kind: "pipeline" },
15
+ enforcement: {
16
+ kind: "prose",
17
+ anchor: { file: "revola-video/src/scenes/craft/S3Line.tsx" },
18
+ },
19
+ };
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "LightBridge + BRIDGE_* + the
5
+ * CraftStage overlay prop: deleted, not left dead. S2's overlay usage
6
+ * reverted."
7
+ */
8
+ export const deleteRejectedRigsCompletely: Rule = {
9
+ id: "delete-rejected-rigs-completely",
10
+ claim: "When a transition or effect is rejected in review, delete the rig, its tokens, and its plumbing (props, overlay slots) entirely; revert call sites rather than leaving the machinery dead.",
11
+ why: "The rejected light bridge left LightBridge, BRIDGE_* tokens, and the CraftStage overlay prop as attractive dead plumbing; deleting it and reverting S2's usage is what makes the rejection stick.",
12
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
13
+ tier: "process",
14
+ scope: { kind: "pipeline" },
15
+ enforcement: {
16
+ kind: "prose",
17
+ unanchorable: "The artifact of following this rule is the absence of code; there is no symbol to point at.",
18
+ },
19
+ };
@@ -0,0 +1,23 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md, "S2 pass 3 - the seam is now
5
+ * DERIVED, not measured". Pass 2 hard-coded ROPE_Y = 224, read off a render.
6
+ * S1 was then recut three times (D_S1 400 -> 396 -> 348, the strike moved from
7
+ * world to HUD space, STRIKE_AT 352 -> 348 -> 300) and every one of those
8
+ * silently invalidated the number.
9
+ */
10
+ export const deriveSeamsDontMeasure: Rule = {
11
+ id: "derive-seams-dont-measure",
12
+ claim: "Derive a cross-scene seam from the neighbouring scene's exported geometry, never from a number measured off a render.",
13
+ why: "A measured constant is a bug with a delay fuse: it stays correct until the neighbouring scene is recut, then silently misplaces the seam with nothing failing. Deriving it means a recut re-derives the seam for free.",
14
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
15
+ tier: "gate",
16
+ verify: "`video radar:film --project <dir> --video <mp4> --comp <id> --check seam-fidelity`",
17
+ owner: "seam-fidelity",
18
+ scope: { kind: "pipeline" },
19
+ enforcement: {
20
+ kind: "prose",
21
+ anchor: { file: "revola-video/src/scenes/craft/S1Graveyard.tsx", symbol: "STRIKE_PTS" },
22
+ },
23
+ };
@@ -0,0 +1,58 @@
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 { deterministicSceneSource } from "./deterministicSceneSource";
6
+
7
+ const check = () => {
8
+ if (deterministicSceneSource.enforcement.kind !== "source") throw new Error("expected a source rule");
9
+ return deterministicSceneSource.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("deterministicSceneSource", () => {
18
+ test("fires once per nondeterministic call: Math.random(), Date.now(), argless new Date() with and without parens", () => {
19
+ const findings = run("nondeterministic-scene.tsx");
20
+ expect(findings).toHaveLength(4);
21
+ expect(findings.map((f) => f.match)).toEqual(["Math.random()", "Date.now()", "new Date()", "new Date"]);
22
+ for (const f of findings) {
23
+ expect(f.message).toMatch(/nondeterministic/);
24
+ expect(f.match.length).toBeLessThanOrEqual(60);
25
+ }
26
+ });
27
+
28
+ test("does not fire on seeded randomness, dated Date construction, or string mentions", () => {
29
+ expect(run("deterministic-scene.tsx")).toEqual([]);
30
+ });
31
+
32
+ test("deliberate limitation: aliased/destructured/prefixed/element-access spellings are not flagged", () => {
33
+ // If the check learns symbol resolution, flip these expectations.
34
+ expect(run("nondeterministic-variants.tsx")).toEqual([]);
35
+ });
36
+
37
+ test("covers every scene tree and the kit", () => {
38
+ expect(check().include).toEqual([
39
+ "qpa-video/03_scenes/**/*.{js,jsx,ts,tsx}",
40
+ "reverence-video/src/scenes/**/*.{js,jsx,ts,tsx}",
41
+ "workday-dple-video/src/scenes/**/*.{js,jsx,ts,tsx}",
42
+ "revola-video/src/scenes/**/*.{js,jsx,ts,tsx}",
43
+ "datapeak-video/**/*.{js,jsx,ts,tsx}",
44
+ "datapeak-usecases/**/*.{js,jsx,ts,tsx}",
45
+ "qse-video/**/*.{js,jsx,ts,tsx}",
46
+ "qse-investor-video/**/*.{js,jsx,ts,tsx}",
47
+ "qse-qguard-video/src/**/*.{ts,tsx}",
48
+ "rcp-fund7-video/src/**/*.{ts,tsx}",
49
+ "expanse-video/src/**/*.{ts,tsx}",
50
+ "reverence-video/src/**/*.{ts,tsx}",
51
+ "spec-videos/**/*.{js,jsx,ts,tsx}",
52
+ "klaviyo-segments-video/src/**/*.{js,jsx,ts,tsx}",
53
+ "radar-fixture-video/src/**/*.{js,jsx,ts,tsx}",
54
+ "packages/video-kit/src/**/*.{ts,tsx}",
55
+ ]);
56
+ expect(check().exclude).toBeUndefined();
57
+ });
58
+ });
@@ -0,0 +1,82 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+ import type { Rule, SourceFinding } from "../types";
3
+
4
+ /**
5
+ * Mined from claude-sessions-digest/revola-usecase-book-meeting.md: the
6
+ * build sessions ran an explicit grep gate for Math.random / Date.now /
7
+ * new Date() after edits, because a nondeterministic scene renders
8
+ * differently on every Remotion render pass (flicker between frames, and
9
+ * un-reproducible output across renders). Remotion's own docs state the
10
+ * constraint; this makes it machine-checked over our scene and kit source.
11
+ *
12
+ * `new Date(...)` WITH arguments is fine - only the argless form reads the
13
+ * wall clock. String mentions never match: the check walks call/new
14
+ * expressions, not text.
15
+ *
16
+ * Deliberate limitation: only the canonical spellings are flagged (callee
17
+ * text compared literally). Aliased receivers (`const M = Math; M.random()`),
18
+ * destructuring, `globalThis.`/`window.` prefixes, and element access
19
+ * (`Math["random"]()`) evade it. Those are evasion shapes, not accidental
20
+ * usage; resolving them would need symbol analysis for no observed payoff.
21
+ * The boundary is pinned by deterministicSceneSource.test.ts.
22
+ */
23
+ export const deterministicSceneSource: Rule = {
24
+ id: "deterministic-scene-source",
25
+ claim: "Scene and kit source must be render-deterministic: no Math.random(), no Date.now(), no argless new Date(); use seeded randomness and explicit dates.",
26
+ why: "A nondeterministic value is re-evaluated on every render pass, so elements jitter between frames and two renders of the same composition differ; the sessions caught these only via an ad-hoc grep gate after the fact.",
27
+ learnedFrom: "claude-sessions-digest/revola-usecase-book-meeting.md",
28
+ upstream: "remotion:@remotion/eslint-plugin/deterministic-randomness",
29
+ tier: "gate",
30
+ verify: "render the same frame range twice and diff every frame; all must report YMAX=0 (automation owed: remotion-videos-5oi)",
31
+ scope: { kind: "pipeline" },
32
+ enforcement: {
33
+ kind: "source",
34
+ check: {
35
+ // Determinism is a crash-class invariant, not a per-project doctrine,
36
+ // so the blast radius matches interpolate-literal-keys-monotonic: every
37
+ // scene tree plus the kit. The corpus runs clean across all of them.
38
+ include: [
39
+ "qpa-video/03_scenes/**/*.{js,jsx,ts,tsx}",
40
+ "reverence-video/src/scenes/**/*.{js,jsx,ts,tsx}", "workday-dple-video/src/scenes/**/*.{js,jsx,ts,tsx}",
41
+ "revola-video/src/scenes/**/*.{js,jsx,ts,tsx}",
42
+ "datapeak-video/**/*.{js,jsx,ts,tsx}",
43
+ "datapeak-usecases/**/*.{js,jsx,ts,tsx}",
44
+ "qse-video/**/*.{js,jsx,ts,tsx}",
45
+ "qse-investor-video/**/*.{js,jsx,ts,tsx}",
46
+ "qse-qguard-video/src/**/*.{ts,tsx}",
47
+ "rcp-fund7-video/src/**/*.{ts,tsx}",
48
+ "expanse-video/src/**/*.{ts,tsx}",
49
+ "reverence-video/src/**/*.{ts,tsx}",
50
+ "spec-videos/**/*.{js,jsx,ts,tsx}",
51
+ // Added Task 23: klaviyo-segments-video and radar-fixture-video were
52
+ // silently excluded (this list said "every scene tree" but named
53
+ // neither) - the exact silent-default trap this rule's own claim
54
+ // exists to catch, applied to the checker itself.
55
+ "klaviyo-segments-video/src/**/*.{js,jsx,ts,tsx}",
56
+ "radar-fixture-video/src/**/*.{js,jsx,ts,tsx}",
57
+ "packages/video-kit/src/**/*.{ts,tsx}",
58
+ ],
59
+ run(file, relPath): SourceFinding[] {
60
+ const findings: SourceFinding[] = [];
61
+ const report = (node: Node, what: string) => {
62
+ findings.push({
63
+ file: relPath,
64
+ line: node.getStartLineNumber(),
65
+ match: node.getText().slice(0, 60),
66
+ message: `nondeterministic ${what} in scene/kit source; seed it or pass an explicit value`,
67
+ });
68
+ };
69
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
70
+ const callee = call.getExpression().getText();
71
+ if (callee === "Math.random" || callee === "Date.now") report(call, `${callee}()`);
72
+ }
73
+ for (const nw of file.getDescendantsOfKind(SyntaxKind.NewExpression)) {
74
+ if (nw.getExpression().getText() === "Date" && nw.getArguments().length === 0) {
75
+ report(nw, "new Date()");
76
+ }
77
+ }
78
+ return findings;
79
+ },
80
+ },
81
+ },
82
+ };
@@ -0,0 +1,21 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/style/CREATIVE_EDGE.md directive 16 ("use asmr-typing.wav
5
+ * anywhere that shows typing - I liked it a lot", "I want the sound effects
6
+ * to give an ASMR style ... very satisfying sounds", "use a more upbeat bg,
7
+ * maybe the one used in the Linear reel"). approved-sfx-palette-only owns
8
+ * the palette and mix hierarchy; this rule owns diegetic sync and the bed.
9
+ */
10
+ export const diegeticSoundForOnscreenActions: Rule = {
11
+ id: "diegetic-sound-for-onscreen-actions",
12
+ claim: "On-screen actions are heard: typing on screen plays asmr-typing.wav in sync, clicks land a soft UI click on the click frame, seams never cross in dead silence, and the music bed is an upbeat tech-reveal track (the Linear-reel EDM track is the reference), subtle and ducked under payoffs.",
13
+ why: "'Use asmr-typing.wav anywhere that shows typing' canonized the diegetic-sync expectation, and 'use a more upbeat bg, maybe the one used in the Linear reel' pinned the bed genre; the palette rule alone let silent typing and a wrong-genre bed through.",
14
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
15
+ tier: "craft",
16
+ scope: { kind: "pipeline" },
17
+ enforcement: {
18
+ kind: "prose",
19
+ unanchorable: "An audio-to-visual sync judgment against the render; no single code location expresses it.",
20
+ },
21
+ };
@@ -0,0 +1,21 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/revola-usecase-book-meeting.md ("just
5
+ * fix the transition, one should leave from the left and the other should
6
+ * come from the right, or up/down"; "overall make all transitions super
7
+ * smooth, zoom in, leave left appear right, any cinematic move") and the
8
+ * win-booth taste summary ("distinct transition per beat").
9
+ */
10
+ export const distinctTransitionPerBeat: Rule = {
11
+ id: "distinct-transition-per-beat",
12
+ claim: "Adjacent beats use distinct, directionally coherent transitions (exit left pairs with enter right, exit up with enter down); never the same transition twice in a row, never an unmotivated hard swap.",
13
+ why: "Back-to-back identical transitions read as template output, and directionally incoherent exits/entries were flagged as 'half left, half middle' confusion; direction logic is what makes cuts feel authored.",
14
+ learnedFrom: "claude-sessions-digest/revola-usecase-book-meeting.md",
15
+ tier: "craft",
16
+ scope: { kind: "pipeline" },
17
+ enforcement: {
18
+ kind: "prose",
19
+ unanchorable: "The rule is a sequencing judgment across scene boundaries; no single code location expresses it.",
20
+ },
21
+ };
@@ -0,0 +1,22 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/style/CREATIVE_EDGE.md directive 21: the standing operator
5
+ * instruction issued per build - download anything you want, or make custom
6
+ * SVGs - plus "the map could look more sleek and more like a map (can use
7
+ * any external package)" and "replace the gradient boxes with animations,
8
+ * icons, real stuff". The builds that looked best treated asset acquisition
9
+ * as expected work, not as an exception to ask about.
10
+ */
11
+ export const downloadOrDrawRichAssets: Rule = {
12
+ id: "download-or-draw-rich-assets",
13
+ claim: "When a beat needs an element, download the real asset (brand SVG, icon set, map or geo data, texture, 3D model, font) at authoring time and commit it, or hand-author a detailed custom SVG; a styled-div rectangle, basic shape, or emoji standing in for art is a defect, and render-time fetching is banned so renders stay deterministic.",
14
+ why: "The gap between prompted builds that looked professional and pipeline builds that looked flat was largely asset quality: agents defaulted to CSS primitives unless told downloading was allowed, while the standing instruction 'download anything you want, or make custom SVGs' produced rich marks every time it was issued.",
15
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
16
+ tier: "craft",
17
+ scope: { kind: "pipeline" },
18
+ enforcement: {
19
+ kind: "prose",
20
+ unanchorable: "An asset-quality judgment per visible element; no single code location expresses it.",
21
+ },
22
+ };
@@ -0,0 +1,25 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/act854-qpa-video.md: "the voice comes
5
+ * before what's shown and it doesn't really match"; "the governance, risk
6
+ * and compliance finished before the compliance card loads .. so that part
7
+ * needs to go faster"; "the NACSA logo at the beginning should come 3-4 s
8
+ * in, not at the same time as the opening". The visual-lag family of
9
+ * defects, distinct from audio-is-the-clock (which fixes the clock) - this
10
+ * fixes what must be on screen while the clock runs.
11
+ */
12
+ export const elementOnscreenForItsVoSentence: Rule = {
13
+ id: "element-onscreen-for-its-vo-sentence",
14
+ claim: "The element a voiceover sentence names is fully on screen, build-in complete, for the whole duration of that sentence; secondary marks (partner logos) enter when mentioned, not at frame zero.",
15
+ why: "Visuals lagging or leading their VO line was the dominant ACT854 feedback category, re-broken by every new voiceover take; the sentence-to-element contract is what re-timing must preserve.",
16
+ learnedFrom: "claude-sessions-digest/act854-qpa-video.md",
17
+ tier: "gate",
18
+ verify: "`video radar:scene --project <dir> --comp <id> --check vo-element-sync --check edge-touch`",
19
+ owner: "vo-element-sync",
20
+ scope: { kind: "pipeline" },
21
+ enforcement: {
22
+ kind: "prose",
23
+ anchor: { file: "packages/video-cli/src/measure.ts" },
24
+ },
25
+ };
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from HOW_I_BUILD_VIDEOS.md: "Keep BUILD_NOTES.md as the source of
5
+ * truth ... An automated pipeline should emit the equivalent artifact, or
6
+ * nobody can pick the project up later."
7
+ */
8
+ export const emitBuildNotes: Rule = {
9
+ id: "emit-build-notes",
10
+ claim: "Maintain BUILD_NOTES.md (or the automated equivalent) per video: current wired WAV, frame layout, and every feedback decision, newest round on top.",
11
+ why: "Without it nobody, human or agent, can pick the project up later; the notes are the only record of why timings and content are what they are.",
12
+ learnedFrom: "HOW_I_BUILD_VIDEOS.md",
13
+ tier: "process",
14
+ scope: { kind: "pipeline" },
15
+ enforcement: {
16
+ kind: "prose",
17
+ unanchorable: "A documentation convention; its exemplars are docs, not live code.",
18
+ },
19
+ };