@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,138 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+ import type { Rule, SourceFinding } from "../types";
3
+
4
+ /**
5
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md, "S2 pass 2": a CSS-filtered
6
+ * <g> wrapping children that also carry CSS filters makes Chrome drop those
7
+ * children silently. The first cursor build rendered nothing while its group
8
+ * rendered fine, and it took a debug rect to prove it.
9
+ */
10
+
11
+ /**
12
+ * True when a JSX element's `style` prop sets an active `filter`. Looks through
13
+ * the attribute's own expression (bare object literals, conditionals, nested
14
+ * literals, shorthand), skips `filter: "none"` (that resets a filter), and
15
+ * deliberately does not chase styles imported from helpers or spread from
16
+ * constants - see the fixtures note above.
17
+ */
18
+ function hasStyleFilter(element: Node): boolean {
19
+ const attributes = Node.isJsxOpeningElement(element) || Node.isJsxSelfClosingElement(element)
20
+ ? element.getAttributes()
21
+ : [];
22
+
23
+ for (const attribute of attributes) {
24
+ if (!Node.isJsxAttribute(attribute)) continue;
25
+ if (attribute.getNameNode().getText() !== "style") continue;
26
+
27
+ const initializer = attribute.getInitializer();
28
+ if (!initializer || !Node.isJsxExpression(initializer)) continue;
29
+
30
+ const expression = initializer.getExpression();
31
+ if (!expression) continue;
32
+
33
+ const literals = Node.isObjectLiteralExpression(expression)
34
+ ? [expression]
35
+ : expression.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
36
+
37
+ for (const literal of literals) {
38
+ for (const property of literal.getProperties()) {
39
+ if (Node.isShorthandPropertyAssignment(property) && property.getName() === "filter") return true;
40
+ if (!Node.isPropertyAssignment(property)) continue;
41
+ if (property.getName() !== "filter") continue;
42
+ const value = property.getInitializer()?.getText() ?? "";
43
+ if (value !== `"none"` && value !== `'none'`) return true;
44
+ }
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+
50
+ const ELEMENT_KINDS = [SyntaxKind.JsxOpeningElement, SyntaxKind.JsxSelfClosingElement];
51
+
52
+ export const noNestedSvgFilters: Rule = {
53
+ id: "no-nested-svg-filters",
54
+ claim: "Never nest CSS filters in SVG. Put one flat filter on one layer.",
55
+ why: "Chrome silently drops the children of a CSS-filtered group when those children also carry CSS filters. Nothing errors and nothing warns: the elements simply do not render, which reads as a logic bug and costs a debugging session to localise.",
56
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
57
+ tier: "gate",
58
+ verify: "`vitest run packages/video-craft` - machine-checked over scene and kit source",
59
+ scope: { kind: "pipeline" },
60
+ enforcement: {
61
+ kind: "source",
62
+ check: {
63
+ include: [
64
+ "qpa-video/03_scenes/**/*.jsx",
65
+ "reverence-video/src/scenes/**/*.tsx",
66
+ "workday-dple-video/src/scenes/**/*.tsx",
67
+ "revola-video/src/scenes/**/*.tsx",
68
+ "datapeak-video/**/*.{jsx,tsx}",
69
+ "datapeak-usecases/**/*.{jsx,tsx}",
70
+ "qse-video/**/*.{jsx,tsx}",
71
+ "qse-investor-video/**/*.{jsx,tsx}",
72
+ "qse-qguard-video/src/**/*.tsx",
73
+ "rcp-fund7-video/src/**/*.tsx",
74
+ "expanse-video/src/**/*.tsx",
75
+ "reverence-video/src/**/*.tsx",
76
+ "spec-videos/**/*.{jsx,tsx}",
77
+ // Added Task 23: klaviyo-segments-video and radar-fixture-video were
78
+ // silently excluded - the same gap deterministic-scene-source had.
79
+ "klaviyo-segments-video/src/**/*.tsx",
80
+ "radar-fixture-video/src/**/*.tsx",
81
+ "packages/video-kit/src/**/*.tsx",
82
+ ],
83
+ run: (file, relPath): SourceFinding[] => {
84
+ const findings: SourceFinding[] = [];
85
+
86
+ for (const kind of ELEMENT_KINDS) {
87
+ for (const element of file.getDescendantsOfKind(kind)) {
88
+ if (!hasStyleFilter(element)) continue;
89
+
90
+ // A JsxOpeningElement's direct parent is its OWN JsxElement, and
91
+ // that JsxElement's opening element is this very node - walking
92
+ // from getParent() would match the element against itself. Start
93
+ // strictly above the element's own JSX node.
94
+ const own = Node.isJsxOpeningElement(element) ? element.getParent() : element;
95
+
96
+ // Walk up through the enclosing JSX elements looking for another
97
+ // filtered layer, stopping at the enclosing <svg>: the Chrome
98
+ // failure is a filtered SVG container (a <g>) wrapping filtered
99
+ // children INSIDE the svg subtree. A CSS filter on the <svg>
100
+ // element itself or on an HTML wrapper around it composites the
101
+ // rendered svg at the HTML level and is fine - corpus calibration
102
+ // hit that shape three times in shipped, correctly-rendering
103
+ // scenes (see fixtures/nested-filters-html-wrapper.tsx). Only the
104
+ // opening element carries the attributes, so a JsxElement
105
+ // ancestor is read through its opening tag.
106
+ let insideSvg = false;
107
+ let filteredAncestor = false;
108
+ let ancestor = own.getParent();
109
+ while (ancestor) {
110
+ if (Node.isJsxElement(ancestor)) {
111
+ const opening = ancestor.getOpeningElement();
112
+ if (opening.getTagNameNode().getText() === "svg") {
113
+ insideSvg = true;
114
+ break;
115
+ }
116
+ if (hasStyleFilter(opening)) filteredAncestor = true;
117
+ }
118
+ ancestor = ancestor.getParent();
119
+ }
120
+
121
+ if (insideSvg && filteredAncestor) {
122
+ findings.push({
123
+ file: relPath,
124
+ line: element.getStartLineNumber(),
125
+ match: element.getText().replace(/\s+/g, " ").slice(0, 60),
126
+ message:
127
+ `nested CSS filter in SVG: this element sets style.filter inside an ancestor that also does. ` +
128
+ `Chrome drops the inner content silently. Flatten to one filter on one layer.`,
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ return findings;
135
+ },
136
+ },
137
+ },
138
+ };
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/revola-usecase-book-meeting.md: "In
5
+ * scene 2, the third animation is the video again, but it was shown in the
6
+ * first animation."
7
+ */
8
+ export const noRepeatedAssetAcrossShots: Rule = {
9
+ id: "no-repeated-asset-across-shots",
10
+ claim: "Never show the same asset (a product video, screenshot, or illustration) in two shots of one video; each shot earns its screen time with new material.",
11
+ why: "A repeated asset reads as filler and was flagged the moment it appeared; the fix was always replacing the repeat with new content, never keeping it.",
12
+ learnedFrom: "claude-sessions-digest/revola-usecase-book-meeting.md",
13
+ tier: "craft",
14
+ scope: { kind: "pipeline" },
15
+ enforcement: {
16
+ kind: "prose",
17
+ unanchorable: "The rule is a cross-shot content judgment; no single code location expresses it.",
18
+ },
19
+ };
@@ -0,0 +1,35 @@
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 { noShorthandWithUndefinedLonghand } from "./noShorthandWithUndefinedLonghand";
6
+
7
+ const check = () => {
8
+ if (noShorthandWithUndefinedLonghand.enforcement.kind !== "source") throw new Error("expected a source rule");
9
+ return noShorthandWithUndefinedLonghand.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("noShorthandWithUndefinedLonghand", () => {
18
+ test("fires on an explicit undefined AND on a forwarded longhand beside its shorthand", () => {
19
+ const findings = run("shorthand-undefined-longhand.tsx");
20
+ expect(findings.map((f) => f.match)).toEqual([
21
+ "paddingTop",
22
+ "paddingLeft: undefined",
23
+ "marginRight: undefined",
24
+ "backgroundColor: undefined",
25
+ ]);
26
+ for (const f of findings) {
27
+ expect(f.message).toMatch(/clears the shorthand/);
28
+ expect(f.match.length).toBeLessThanOrEqual(60);
29
+ }
30
+ });
31
+
32
+ test("does not fire on ternaries, lone longhands, defined pairs, or borderRadius beside border", () => {
33
+ expect(run("shorthand-undefined-clean.tsx")).toEqual([]);
34
+ });
35
+ });
@@ -0,0 +1,140 @@
1
+ import { SyntaxKind } from "ts-morph";
2
+ import type { Rule, SourceFinding } from "../types";
3
+
4
+ /**
5
+ * Mined from the product-room build. A shared panel component forwarded style
6
+ * as `{ padding, paddingTop, paddingRight, paddingBottom, paddingLeft }` after
7
+ * destructuring them out of a caller's style object. When a caller passed only
8
+ * the `padding` shorthand, the four longhands were `undefined` - and React
9
+ * applies style keys in order, so writing `style.paddingTop = ""` after
10
+ * `style.padding = "18px 20px"` clears it. Every panel in the film silently
11
+ * lost its padding; the defect was invisible in source review and only showed
12
+ * up as "the blocks aren't centered correctly".
13
+ *
14
+ * Two shapes fire, both only when the same object literal also carries that
15
+ * property's own shorthand: an explicit `paddingLeft: undefined`, and a
16
+ * forwarded identifier (`{ padding, paddingTop }`) whose definedness cannot be
17
+ * known here - the shape that actually caused the bug.
18
+ *
19
+ * A ternary yielding undefined (`filter: blur > 0.5 ? ... : undefined`) is the
20
+ * sanctioned omit-a-value shape, appears throughout the corpus, and is
21
+ * deliberately left alone; so is a literal, which is definitely defined.
22
+ *
23
+ * The identifier case is an over-approximation, stated plainly: ANY bare
24
+ * identifier counts, not only one destructured from a caller's style, because
25
+ * definedness is not knowable here without type information. It returns zero
26
+ * findings across the current corpus. If it ever gets noisy, narrow it to
27
+ * identifiers bound by a destructure rather than loosening the family table.
28
+ *
29
+ * Known boundary: spreading a partially-undefined object (`{...pad}`) has the
30
+ * same effect and is not detectable here without type information. The fix
31
+ * shape the rule pushes toward - build the style object with only the keys you
32
+ * actually have - is immune to both.
33
+ */
34
+
35
+ /**
36
+ * Shorthand -> longhands it resets. Listed explicitly rather than derived by
37
+ * prefix because CSS does not follow the prefix: `border` does NOT reset
38
+ * `borderRadius`, `borderCollapse` or `borderSpacing`, so a prefix rule would
39
+ * fire on legal code.
40
+ */
41
+ const FAMILIES: Record<string, RegExp> = {
42
+ padding: /^padding(Top|Right|Bottom|Left|Block|Inline)/,
43
+ margin: /^margin(Top|Right|Bottom|Left|Block|Inline)/,
44
+ // `background` resets image/position/size/repeat/origin/clip/attachment/color.
45
+ // It does NOT reset background-blend-mode (CSS Backgrounds L3).
46
+ background: /^background(Color|Image|Position|Size|Repeat|Attachment|Clip|Origin)/,
47
+ // Ordered before `border` so the corner spellings can never be claimed by it.
48
+ borderRadius: /^border(Top|Bottom)(Left|Right)Radius$/,
49
+ // The negative lookahead is load-bearing: without it `^border(Top` also
50
+ // matches `borderTopLeftRadius`, and `border` does not reset border-radius.
51
+ border: /^border(Top|Right|Bottom|Left|Width|Style|Color|Block|Inline)(?!.*Radius)/,
52
+ font: /^font(Size|Weight|Family|Style|Variant|Stretch)/,
53
+ flex: /^flex(Grow|Shrink|Basis)$/,
54
+ overflow: /^overflow(X|Y)$/,
55
+ transition: /^transition(Property|Duration|TimingFunction|Delay)$/,
56
+ animation: /^animation(Name|Duration|TimingFunction|Delay|IterationCount|Direction|FillMode|PlayState)$/,
57
+ gap: /^(row|column)Gap$/,
58
+ };
59
+
60
+ export const noShorthandWithUndefinedLonghand: Rule = {
61
+ id: "no-shorthand-with-undefined-longhand",
62
+ claim:
63
+ "Never put a longhand style property explicitly set to `undefined` in the same object as its shorthand; build the style object from only the keys you actually have.",
64
+ why: "React writes style keys in order, so an undefined longhand clears the shorthand that preceded it - one forwarded style object silently removed the padding from every panel in a film, and the source read as correct.",
65
+ learnedFrom: "claude-sessions-digest/product-room-video.md",
66
+ tier: "gate",
67
+ verify: "`vitest run packages/video-craft` - machine-checked over scene and kit source",
68
+ scope: { kind: "pipeline" },
69
+ enforcement: {
70
+ kind: "source",
71
+ check: {
72
+ // Scene trees plus the kit: anywhere a style object is composed or
73
+ // forwarded. Same blast radius as the other geometry-class checks.
74
+ include: [
75
+ "revola-video/src/**/*.{ts,tsx}",
76
+ "workday-dple-video/src/**/*.{ts,tsx}",
77
+ "spec-videos/src/**/*.{ts,tsx}",
78
+ "qpa-video/03_scenes/**/*.{js,jsx,ts,tsx}",
79
+ "qse-qguard-video/src/**/*.{ts,tsx}",
80
+ "rcp-fund7-video/src/**/*.{ts,tsx}",
81
+ "expanse-video/src/**/*.{ts,tsx}",
82
+ "reverence-video/src/**/*.{ts,tsx}",
83
+ // Added Task 23: klaviyo-segments-video and radar-fixture-video were
84
+ // silently excluded - the same gap deterministic-scene-source had.
85
+ "klaviyo-segments-video/src/**/*.{ts,tsx}",
86
+ "radar-fixture-video/src/**/*.{ts,tsx}",
87
+ "packages/video-kit/src/**/*.{ts,tsx}",
88
+ ],
89
+ run(file, relPath): SourceFinding[] {
90
+ const findings: SourceFinding[] = [];
91
+ for (const obj of file.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
92
+ const props = obj.getProperties().filter(
93
+ (p) => p.isKind(SyntaxKind.PropertyAssignment) || p.isKind(SyntaxKind.ShorthandPropertyAssignment),
94
+ );
95
+ const unquote = (n: string) => n.replace(/^["']|["']$/g, "");
96
+ const names = new Set(
97
+ props.map((p) =>
98
+ unquote(
99
+ p.isKind(SyntaxKind.PropertyAssignment)
100
+ ? p.getName()
101
+ : p.asKindOrThrow(SyntaxKind.ShorthandPropertyAssignment).getName(),
102
+ ),
103
+ ),
104
+ );
105
+ for (const prop of props) {
106
+ const shorthandSyntax = prop.isKind(SyntaxKind.ShorthandPropertyAssignment);
107
+ const name = unquote(
108
+ shorthandSyntax
109
+ ? prop.asKindOrThrow(SyntaxKind.ShorthandPropertyAssignment).getName()
110
+ : prop.asKindOrThrow(SyntaxKind.PropertyAssignment).getName(),
111
+ );
112
+ // A value is "possibly undefined" when it is the bare `undefined`
113
+ // identifier, or any forwarded identifier (`{ padding, paddingTop }`
114
+ // out of a destructure - the shape that actually caused the bug).
115
+ // A literal is definitely defined; a ternary is the sanctioned
116
+ // omit-a-value shape and is left alone deliberately.
117
+ let possiblyUndefined = shorthandSyntax;
118
+ if (!shorthandSyntax) {
119
+ const init = prop.asKindOrThrow(SyntaxKind.PropertyAssignment).getInitializer();
120
+ possiblyUndefined = init?.getKind() === SyntaxKind.Identifier;
121
+ }
122
+ if (!possiblyUndefined) continue;
123
+ for (const [shorthand, longhands] of Object.entries(FAMILIES)) {
124
+ if (!longhands.test(name)) continue;
125
+ if (!names.has(shorthand)) continue;
126
+ findings.push({
127
+ file: relPath,
128
+ line: prop.getStartLineNumber(),
129
+ match: prop.getText().slice(0, 60),
130
+ message: `\`${name}\` may be undefined beside the \`${shorthand}\` shorthand in the same object, which clears the shorthand; omit the key instead of passing it undefined`,
131
+ });
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ return findings;
137
+ },
138
+ },
139
+ },
140
+ };
@@ -0,0 +1,30 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "S3 opacity fade-in. S2
5
+ * already fades out; two stacked fades = the film dips through a black hole.
6
+ * ... Never fade in here." Proved by render: f584 violet bloom, f590
7
+ * near-black. The outgoing-fade mechanism is CraftStage's exitOpacity prop.
8
+ */
9
+ export const noStackedFadesAtACut: Rule = {
10
+ id: "no-stacked-fades-at-a-cut",
11
+ claim: "Never stack the outgoing scene's fade-out with an incoming fade-in at a cut; at most one side of a seam may fade.",
12
+ why: "Two stacked fades multiply to near-zero and the film dips through a black hole at the seam; no easing can fix a cut that happens in black.",
13
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
14
+ tier: "gate",
15
+ verify: "`video radar:film --project <dir> --video <mp4> --check fade-stack-at-cut`",
16
+ owner: "fade-stack-at-cut",
17
+ exceptions: [
18
+ {
19
+ project: "qse-qguard-video",
20
+ decision:
21
+ "client chose ACT854's paired 14f exit-fade + 14f enter-fade at every seam for the QGuard restyle; the black-dip risk is accepted and checked with stills at each seam",
22
+ recordedIn: "qse-qguard-video/docs/motion-brand-spec.md",
23
+ },
24
+ ],
25
+ scope: { kind: "pipeline" },
26
+ enforcement: {
27
+ kind: "prose",
28
+ anchor: { file: "revola-video/src/scenes/craft/craftKit.tsx" },
29
+ },
30
+ };
@@ -0,0 +1,24 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/portfolio-reels.md ("The dashboard scene
5
+ * is too static - when the camera zooms in, something should happen"; "Too
6
+ * static - the bg should move too, throughout the video") and
7
+ * revola-usecase-book-meeting.md ("the one scene that says 'a meeting you
8
+ * did nothing to get' has no animation, just text. looks empty"). "Too
9
+ * static" was raised for at least five different scenes.
10
+ */
11
+ export const noStaticHolds: Rule = {
12
+ id: "no-static-holds",
13
+ claim: "No shot holds fully static for more than about two seconds: the background always has subtle continuous motion, and idle hero objects get living motion (drift, rotate, breathe).",
14
+ why: "Every static hold in the corpus was eventually flagged as boring or empty; continuous low-amplitude motion is what separates a motion-graphics video from a slideshow.",
15
+ learnedFrom: "claude-sessions-digest/portfolio-reels.md",
16
+ tier: "gate",
17
+ verify: "`video radar:film --project <dir> --video <mp4> --check static-hold`",
18
+ owner: "static-hold",
19
+ scope: { kind: "pipeline" },
20
+ enforcement: {
21
+ kind: "prose",
22
+ unanchorable: "The rule is a motion-over-time judgment; it is not expressible as a single code location.",
23
+ },
24
+ };
@@ -0,0 +1,26 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/superpowers/specs/2026-07-31-defect-radar-design.md: QGuard
5
+ * r1 printed station labels through every panel at their station; QGuard r3
6
+ * painted S2's lane labels under theft strip 2, where they were never seen.
7
+ * Both defects passed every existing check because nothing tested whether a
8
+ * tagged element was actually VISIBLE, only whether it existed and sat in
9
+ * its declared band. New doctrine - no prior violation site to anchor
10
+ * against, since nothing in the corpus checked this before the radar's
11
+ * `occlusion` check existed.
12
+ */
13
+ export const occlusionVisibleElements: Rule = {
14
+ id: "occlusion-visible-elements",
15
+ claim: "A tagged element stays visibly on top for the whole duration it is meant to be read; another element must never paint over enough of it, for long enough, that it reads as buried rather than merely overlapping.",
16
+ why: "A label that exists, sits in its correct band, and is fully painted over by a later layer looks identical to a correct scene in source and in every geometry check that only reasons about position - the QGuard station labels and S2 lane labels were both invisible in the shipped render while every positional check stayed clean.",
17
+ learnedFrom: "docs/superpowers/specs/2026-07-31-defect-radar-design.md",
18
+ tier: "gate",
19
+ verify: "`video radar:scene --project <dir> --comp <id> --check occlusion`",
20
+ owner: "occlusion",
21
+ scope: { kind: "pipeline" },
22
+ enforcement: {
23
+ kind: "prose",
24
+ unanchorable: "The rule is a paint-order judgment across a shot's whole sampled range; no single code location expresses it.",
25
+ },
26
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md. The old build sliced the read
5
+ * into five chunks (VO_S1...VO_S5) and started each at its scene's first frame;
6
+ * the scenes ran longer than their slices, so every seam padded the read with
7
+ * dead air - 252f (9s) across four seams.
8
+ */
9
+ export const oneContinuousVoTrack: Rule = {
10
+ id: "one-continuous-vo-track",
11
+ claim: "Play the narration as one continuous Audio track from frame 0 and make the film exactly its length; never slice the VO into per-scene chunks that start at scene boundaries.",
12
+ why: "Per-scene VO slices start at each scene's first frame while scenes run longer than their slices, silently inserting dead air at every seam - the sliced build padded the read with 9s across four seams and the film read as padded.",
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/RevolaCraft.tsx" },
19
+ },
20
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/portfolio-reels.md: "THE 'TO DONE IN
5
+ * SECONDS' BEAT ZOOM-IN ON EVERY PRESS IS UNNECESSARY" and "It zooms in and
6
+ * suddenly zooms out". Over-animation was rejected as firmly as static
7
+ * scenes; the counterweight to no-static-holds.
8
+ */
9
+ export const onePunchPerMomentClass: Rule = {
10
+ id: "one-punch-per-moment-class",
11
+ claim: "Use a beat punch (zoom-in, scale pop, flash) at most once per class of moment; never repeat the same punch on every keypress, card, or list item.",
12
+ why: "A punch repeated on every event stops reading as emphasis and starts reading as a bug; repeated beat zooms were rejected twice in caps.",
13
+ learnedFrom: "claude-sessions-digest/portfolio-reels.md",
14
+ tier: "craft",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ unanchorable: "The rule is an emphasis-budget judgment across a scene's events; no single code location expresses it.",
19
+ },
20
+ };
@@ -0,0 +1,46 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * `verify-cheap-render-dear` says the full render runs "on demand, last". This
5
+ * is what makes that operational, because "last" turns out to be ambiguous the
6
+ * moment a render surfaces more than one defect.
7
+ *
8
+ * QGuard round 2 spent three full renders - about 21 minutes of an 85-second
9
+ * film at 1080p - and only one of them was necessary:
10
+ *
11
+ * render 2 swept, found two defects (a retracting rule striking through the
12
+ * wordmark, a request card off frame left)
13
+ * -> fixed one, verified it, rendered again
14
+ * render 3 found the second fix incomplete: it corrected the card's resting
15
+ * position but not its travel, which the single verifying still
16
+ * had not covered
17
+ * -> fixed properly, rendered again
18
+ * render 4 clean
19
+ *
20
+ * Two failures, both cheap to avoid. Fixes were verified one at a time when the
21
+ * render had already listed them all; and each fix was checked at ONE frame when
22
+ * the thing it changed spanned a hundred. A still costs seconds and a render
23
+ * costs minutes, so the still should cover the whole window the fix touches -
24
+ * the beginning, the middle and the end of any motion it alters - and every
25
+ * defect from a render should be closed before the next one starts.
26
+ *
27
+ * For scale, the corpus in `claude-sessions-digest/` ran 9 full renders across
28
+ * 3,208 recorded actions, against 103 stills and 231 typechecks.
29
+ *
30
+ * This does not weaken `review-the-render-by-frame-sweep`: the sweep still
31
+ * gates the round, and it still has to happen on a rendered file. It defines
32
+ * what a round IS - one render, one complete review of it, one batch of fixes -
33
+ * rather than letting a round quietly become one render per fix.
34
+ */
35
+ export const oneRenderPerRound: Rule = {
36
+ id: "one-render-per-round",
37
+ claim: "Close every defect a render surfaced before rendering again, and verify each fix with stills spanning the whole window it changes, not a single frame.",
38
+ why: "Rendering per fix instead of per round cost QGuard round 2 two extra full renders (~14 minutes), one of them because a fix was verified at a single frame while the motion it altered spanned a hundred.",
39
+ learnedFrom: "qse-qguard-video/BUILD_NOTES.md",
40
+ tier: "process",
41
+ scope: { kind: "pipeline" },
42
+ enforcement: {
43
+ kind: "prose",
44
+ anchor: { file: "packages/video-cli/src/review.ts" },
45
+ },
46
+ };
@@ -0,0 +1,29 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "Momentum entry (over-scale +
5
+ * blur decelerating). Theoretically correct ... in practice reads as the
6
+ * camera drifting on the opening frames instead of the scene being *there*."
7
+ * The live example is the duplicated camTarget(0), camTarget(0) opening keys
8
+ * in S3Line's CAM_X.
9
+ */
10
+ export const openStaticAfterAHardCut: Rule = {
11
+ id: "open-static-after-a-hard-cut",
12
+ claim: "After a hard cut, open the incoming scene static and at full opacity: no entry zoom, no entry blur, and the first two camera keys equal so nothing moves until the first deliberate travel.",
13
+ why: "A momentum entry is theoretically correct continuation but on screen reads as the camera drifting; the landing element must be the first thing the eye finds, and it must be still.",
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 0.97-to-1 entry settle at every seam for the QGuard restyle; scenes still hold their first camera key static underneath it",
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/S3Line.tsx" },
28
+ },
29
+ };
@@ -0,0 +1,21 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from docs/style/CREATIVE_EDGE.md directive 2 ("first scene is too
5
+ * boring for an opening", "make it an amazing closing scene", "upscale the
6
+ * final animation to look better and close with a good impact and a good
7
+ * professional layout"). The opening drew its own dedicated feedback rounds
8
+ * in session after session; no other scene position did.
9
+ */
10
+ export const openingsAndClosingsCarryMore: Rule = {
11
+ id: "openings-and-closings-carry-more",
12
+ claim: "The opening and closing scenes out-spectacle the middle: frame one already carries the film's biggest visual statement, and the close lands one deliberate, composed payoff instead of fizzling.",
13
+ why: "'First scene is too boring for an opening' and 'make it an amazing closing scene' recurred across builds as their own feedback rounds; a strong middle never rescued a film whose first seconds read as template.",
14
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
15
+ tier: "craft",
16
+ scope: { kind: "pipeline" },
17
+ enforcement: {
18
+ kind: "prose",
19
+ unanchorable: "A relative-spectacle judgment across scene positions; no single code location expresses it.",
20
+ },
21
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from claude-sessions-digest/revola-intro-video.md and
5
+ * revola-usecase-win-booth.md, where the identical complaint appeared in
6
+ * both: "why does the laptop keep reappearing, i hate the transitions.. it
7
+ * should just change content".
8
+ */
9
+ export const persistentDeviceFrames: Rule = {
10
+ id: "persistent-device-frames",
11
+ claim: "A device frame (laptop, phone, browser chrome) enters the video once; subsequent shots swap only the content inside it, never re-animate the frame's entrance.",
12
+ why: "A frame that re-enters on every shot reads as a reset and was hated on sight in two separate videos; persistence is what makes a sequence feel like one continuous product story.",
13
+ learnedFrom: "claude-sessions-digest/revola-intro-video.md",
14
+ tier: "craft",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ unanchorable: "The rule is a cross-shot continuity judgment; no single code location expresses it.",
19
+ },
20
+ };
@@ -0,0 +1,20 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Mined from revola-video/BUILD_NOTES_CRAFT.md: "Raw per-pixel delta is
5
+ * ~120/255, but it's identical in empty regions of the frame - that's
6
+ * FilmGrain/DustMotes, which re-randomise every frame by design. Not a seam
7
+ * artifact." Those layers render inside CraftStage.
8
+ */
9
+ export const pixelDiffAnEmptyBandFirst: Rule = {
10
+ id: "pixel-diff-an-empty-band-first",
11
+ claim: "When pixel-diffing rendered frames across a seam, measure an empty region of frame first; per-frame randomized layers (film grain, dust motes) produce large raw deltas by design.",
12
+ why: "A raw ~120/255 per-pixel delta at a verified-perfect seam was entirely FilmGrain/DustMotes re-randomizing; without the empty-band control you chase a phantom seam artifact.",
13
+ learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
14
+ tier: "process",
15
+ scope: { kind: "pipeline" },
16
+ enforcement: {
17
+ kind: "prose",
18
+ anchor: { file: "revola-video/src/scenes/craft/craftKit.tsx" },
19
+ },
20
+ };
@@ -0,0 +1,32 @@
1
+ import type { Rule } from "../types";
2
+
3
+ /**
4
+ * Replaces `plan-signoff-before-scenes`, which held all scene work behind a
5
+ * human approval.
6
+ *
7
+ * The reason for the original rule was sound and is preserved here: structural
8
+ * feedback is cheapest against a markdown plan. What was wrong was making it a
9
+ * BARRIER. Measured on the QGuard build, the wait cost 40 minutes before a
10
+ * single scene file existed, against 8.9 minutes in the reference sessions in
11
+ * `claude-sessions-digest/` - and nothing behind the barrier depended on the
12
+ * answers. The tokens, the theme, the layout module and every artifact
13
+ * component were buildable the whole time.
14
+ *
15
+ * It also did not buy correctness where it mattered. The approved QGuard plan
16
+ * specified the S1 seam exactly right ("the second cookie chip is snatched
17
+ * upward out of frame and the camera follows it up into the dark"), and the
18
+ * build silently did something else. Sign-off certified a document; it could
19
+ * not certify the film.
20
+ */
21
+ export const planIsPublishedNotGating: Rule = {
22
+ id: "plan-is-published-not-gating",
23
+ claim: "Publish the scene-flow plan with its open questions and keep building; never hold scene work waiting for sign-off, and when an answer arrives, change the plan and re-derive from it.",
24
+ why: "A plan used as a barrier stops all downstream work for feedback that is usually orthogonal to it - the QGuard build spent 40 minutes before its first scene against 8.9 in the reference sessions - while the things sign-off cannot catch (a plan the build silently does not honour) pass it anyway.",
25
+ learnedFrom: "qse-qguard-video/BUILD_NOTES.md",
26
+ tier: "process",
27
+ scope: { kind: "pipeline" },
28
+ enforcement: {
29
+ kind: "prose",
30
+ anchor: { file: "packages/video-cli/src/planmd.ts" },
31
+ },
32
+ };