@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,217 @@
1
+ /**
2
+ * Citations carried in SOURCE COMMENTS, extracted so they can be checked.
3
+ *
4
+ * This codebase cites heavily and on purpose: a comment names the bd issue that
5
+ * motivated the code, or the design doc that settled it, so a reader can get to
6
+ * the reasoning instead of re-deriving it. A manual sweep before this guard
7
+ * existed found no typos in any of them - the discipline is real, and this only
8
+ * has to keep it that way.
9
+ *
10
+ * On the numbers in this file: none of them counts the CURRENT corpus. A census
11
+ * ("this repo has N citations") is a derived number that goes stale on the next
12
+ * commit, and CLAUDE.md's Comments section tells everyone else not to write one
13
+ * - an earlier version of this file shipped three that were wrong. What remains
14
+ * is the other thing that section asks for: a record of what a past defect
15
+ * cost, which is fixed once it has happened and is the number that stops the
16
+ * next person undoing the fix.
17
+ *
18
+ * Nothing checked any of it. `drift.test.ts`, `anchors.test.ts` and
19
+ * `learnedFrom.test.ts` already guard the equivalent references in RULE prose;
20
+ * source comments were the uncovered case, and the 2026-08-25 extraction left
21
+ * ten doc citations dangling before anyone noticed.
22
+ *
23
+ * Pure: the callers read the files.
24
+ */
25
+
26
+ import { ts } from "ts-morph";
27
+
28
+ /**
29
+ * The parser to read a file with, from its name.
30
+ *
31
+ * This is not a detail. TSX cannot parse the generic arrow `<T>(x) => x`, which
32
+ * is ordinary TypeScript, and the parser does not throw on it - it recovers by
33
+ * reading `<T>` as an unterminated JSX element and loses most, though not all,
34
+ * of the comments after that point. Parsing everything as TSX cost four real
35
+ * files in this repo between 12% and 49% of their comment text, and hid a live
36
+ * `remotion-videos-b4pq` citation from the guard entirely.
37
+ *
38
+ * That is the same failure the hand-rolled scanner had (a stray opener eating
39
+ * the rest of the file), reintroduced one layer down, in the fix for it.
40
+ *
41
+ * NOTE ON THE PARSER VERSION. `ts` here is whatever TypeScript ts-morph bundles,
42
+ * which is NOT the version the repo compiles with - today 5.6.2 against a 5.9.3
43
+ * tsc. Since a parse diagnostic now throws, syntax newer than the bundled
44
+ * parser hard-fails this guard even though `npm run typecheck` accepts it. If
45
+ * that ever bites, bump ts-morph rather than softening the throw: silently
46
+ * reading fewer comments is the failure this module exists to prevent.
47
+ */
48
+ function scriptKindFor(fileName: string): ts.ScriptKind {
49
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
50
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
51
+ if (/\.(?:m|c)?js$/.test(fileName)) return ts.ScriptKind.JS;
52
+ return ts.ScriptKind.TS;
53
+ }
54
+
55
+ /**
56
+ * Just the comment text of a JS/TS source file.
57
+ *
58
+ * PARSED, not scanned. Two hand-rolled heuristics were tried first and both
59
+ * were wrong in the same direction - a comment opener inside a literal:
60
+ *
61
+ * - naive: `joinShell.test.ts:1838` writes a gitignore glob whose `/*`
62
+ * opened a block comment that never closed and swallowed the 600 lines
63
+ * after it;
64
+ * - "opener must follow whitespace or a delimiter": `costShell.ts:115`
65
+ * interpolates a `/*.json` glob where the `/` follows a `}` - legal by that
66
+ * rule - and reopened the same hole.
67
+ *
68
+ * Both put string literals back in citation scope and produced false failures.
69
+ * A third patch would have been a third hole, so this uses the real TypeScript
70
+ * parser, which ts-morph already brings in as a dependency of this package.
71
+ *
72
+ * `fileName` is REQUIRED because two things depend on it: the parser choice
73
+ * (see scriptKindFor - defaulting it cost a live citation) and the error below,
74
+ * which is raised at the caller's module scope where the path is the only clue
75
+ * a reader gets.
76
+ *
77
+ * THROWS on a parse error rather than returning what it managed to read. A file
78
+ * the parser cannot read is a file this module cannot vouch for, and returning
79
+ * partial comments is precisely the "reports clean while covering less than it
80
+ * claims" failure the whole module exists to prevent.
81
+ */
82
+ export function commentsOnly(text: string, fileName: string): string {
83
+ const source = ts.createSourceFile(
84
+ fileName, text, ts.ScriptTarget.Latest, /* setParentNodes */ false, scriptKindFor(fileName));
85
+ const diagnostics =
86
+ (source as unknown as { parseDiagnostics?: readonly ts.DiagnosticWithLocation[] })
87
+ .parseDiagnostics ?? [];
88
+ const first = diagnostics[0];
89
+ if (first) {
90
+ const { line } = source.getLineAndCharacterOfPosition(first.start);
91
+ throw new Error(
92
+ `${fileName}:${line + 1} cannot be parsed as ${ts.ScriptKind[scriptKindFor(fileName)]}: ` +
93
+ `${ts.flattenDiagnosticMessageText(first.messageText, " ")}. The citation guard reads ` +
94
+ "comments by parsing, so an unparseable file would be scanned as if it had none.");
95
+ }
96
+
97
+ const ranges: ts.CommentRange[] = [];
98
+ const jsxText: ts.TextRange[] = [];
99
+ const seen = new Set<number>();
100
+ const take = (pos: number): void => {
101
+ for (const range of [
102
+ ...(ts.getLeadingCommentRanges(text, pos) ?? []),
103
+ ...(ts.getTrailingCommentRanges(text, pos) ?? []),
104
+ ]) {
105
+ if (seen.has(range.pos)) continue;
106
+ seen.add(range.pos);
107
+ ranges.push(range);
108
+ }
109
+ };
110
+ // getChildren, NOT forEachChild. forEachChild yields only syntactic children
111
+ // and never the punctuation tokens, so a comment in the leading trivia of a
112
+ // closing `}` / `]` / `)` was invisible - the last comment in a function
113
+ // body, an object literal, or a switch case, and every `{/* ... */}` in JSX,
114
+ // which is the only comment form JSX children have. 229 comments across the
115
+ // scanned tree were unreachable that way.
116
+ const walk = (node: ts.Node): void => {
117
+ if (node.kind === ts.SyntaxKind.JsxText) jsxText.push({ pos: node.pos, end: node.end });
118
+ take(node.pos);
119
+ for (const child of node.getChildren(source)) walk(child);
120
+ };
121
+ walk(source);
122
+
123
+ // JSX TEXT IS DATA, NOT CODE - the same rule as a string literal, and the
124
+ // third appearance of the same bug. `<div>/* x */</div>` renders those
125
+ // characters; they are not a comment. But the JSX children SyntaxList begins
126
+ // at the first character of the text run, so getLeading/TrailingCommentRanges
127
+ // reads an opener there and hands back "a comment". An unterminated `/*` in
128
+ // rendered text then swallows the file to EOF and puts every string literal
129
+ // below back in citation scope - exactly what the gitignore glob did to
130
+ // joinShell.test.ts before any of this was parsed rather than scanned.
131
+ //
132
+ // Dropping ranges that START inside a JsxText run is enough: a real
133
+ // `{/* ... */}` lives in a JsxExpression, never in JsxText, so it survives.
134
+ return ranges
135
+ .filter((r) => !jsxText.some((j) => r.pos >= j.pos && r.pos < j.end))
136
+ .map((r) => text.slice(r.pos, r.end))
137
+ .join("\n");
138
+ }
139
+
140
+ /** Punctuation a citation collects from the prose it sits in. */
141
+ const TRAILING = /[.,;:!?)\]}`'"]+$/;
142
+
143
+ /** Extensions a doc citation may end in. A path ending in none of these, and
144
+ * not in a `/`, is a fragment - a wrapped comment line, or a path built at
145
+ * runtime - not a resolvable file. */
146
+ const DOC_EXTENSIONS = [".md", ".json", ".yaml", ".yml", ".txt"];
147
+
148
+ /**
149
+ * A repo-root doc citation.
150
+ *
151
+ * The LEFT BOUNDARY is the load-bearing part. Without it `docs/` matches
152
+ * anywhere inside a longer path, which goes wrong in two directions at once:
153
+ *
154
+ * - `https://remotion.dev/docs/lambda/bucket-security` yields
155
+ * `docs/lambda/bucket-security` - a URL read as a local file. This is the
156
+ * shape that actually occurs in this repo's comments;
157
+ * - `qse-qguard-video/docs/motion-brand-spec.md` yields
158
+ * `docs/motion-brand-spec.md`, a path that does not exist at the repo root.
159
+ * This form appears inside the scanned trees in string literals - rule
160
+ * `recordedIn:` fields and their test assertions - so it reaches the
161
+ * extractor whenever a comment quotes one, and the anchor is what stops
162
+ * that becoming a false failure.
163
+ *
164
+ * An earlier version required a subdirectory (`docs/<seg>/<rest>`), which
165
+ * masked both cases by accident - and silently dropped every single-segment
166
+ * citation the repo has, `docs/accounting.md` among them. Anchoring left is
167
+ * what lets the pattern accept those safely.
168
+ */
169
+ const DOC_PATTERN = /(?<![\w/.-])docs\/[A-Za-z0-9._/*-]+/g;
170
+
171
+ /**
172
+ * Doc paths cited in `text`, de-duplicated, in first-seen order.
173
+ *
174
+ * Accepts a trailing-slash directory (`docs/superpowers/`), which the prose
175
+ * convention uses and which resolves like any other path.
176
+ *
177
+ * Rejects two shapes that are NOT broken citations:
178
+ * - a path split across two comment lines, which ends mid-token. This one
179
+ * fires on real comments here;
180
+ * - a runtime glob (`docs/headless/phase-*.md`) - never a single file. This
181
+ * one is defensive today: the only such path in the tree is written
182
+ * project-relative, so the left anchor rejects it before the glob branch
183
+ * is reached. Keep it anyway - a repo-root glob is one comment away.
184
+ * Flagging either would train readers to ignore the check, which is worse than
185
+ * not having it. Both rejections are silent, which can hide a typo in the
186
+ * wrapped half of a long path (video-toolchain-2gw).
187
+ */
188
+ export function extractDocCitations(text: string): string[] {
189
+ const out: string[] = [];
190
+ for (const raw of text.match(DOC_PATTERN) ?? []) {
191
+ const path = raw.replace(TRAILING, "");
192
+ if (path.includes("*")) continue;
193
+ const resolvable = path.endsWith("/") || DOC_EXTENSIONS.some((e) => path.endsWith(e));
194
+ if (!resolvable) continue;
195
+ if (!out.includes(path)) out.push(path);
196
+ }
197
+ return out;
198
+ }
199
+
200
+ /** The two bd id families this repo's comments cite. `remotion-videos-` ids are
201
+ * kept on purpose after the extraction - source cites them by name. */
202
+ const ISSUE_PATTERN = /\b(?:remotion-videos|video-toolchain)-[a-z0-9]{3,4}\b/g;
203
+
204
+ /**
205
+ * bd issue ids cited in `text`, de-duplicated and sorted.
206
+ *
207
+ * The trailing `\b` is what stops `remotion-videos-abcdefg` (not an id) from
208
+ * matching its own first four characters. The bare repo name is excluded by the
209
+ * required `-[a-z0-9]{3,4}` suffix instead - there is nothing after it to bound
210
+ * - so loosening that suffix rule would start matching prose.
211
+ *
212
+ * A child id (`...-5ky3.1`) resolves to its parent, which is the record that
213
+ * has to exist.
214
+ */
215
+ export function extractIssueCitations(text: string): string[] {
216
+ return [...new Set(text.match(ISSUE_PATTERN) ?? [])].sort();
217
+ }
@@ -0,0 +1,112 @@
1
+ import { ALL_STAGES } from "./stages";
2
+ import type { Rule, Stage } from "./types";
3
+
4
+ /** Waiver lines, duplicated in shape from projectDoc's exceptionLines: a
5
+ * stage sheet inside a waived project must carry the waiver too, or a
6
+ * dispatched agent "fixes" the client's decision back out. */
7
+ const exceptionLines = (r: Rule): string[] =>
8
+ (r.exceptions ?? []).map(
9
+ (ex) => ` - waived in \`${ex.project}\`: ${ex.decision} (recorded: \`${ex.recordedIn}\`)`,
10
+ );
11
+
12
+ /**
13
+ * One stage's rule sheet: the markdown a dispatched agent for that stage
14
+ * reads instead of the full registry. Same tier ORDER as the CLAUDE.md
15
+ * projection - gates first, with their verify lines - but NOT the same
16
+ * shape, and deliberately so: a sheet carries every in-scope rule's full
17
+ * claim, where CLAUDE.md compacts unwaived craft and process rules to an id
18
+ * index (video-toolchain-cg6). CLAUDE.md can compact because it is a
19
+ * navigation surface for a session that can go read HOW_I_BUILD_VIDEOS.md;
20
+ * a sheet is inlined into a dispatched worker's prompt, which cannot. Do
21
+ * not "restore symmetry" by compacting sheets - that removes the fallback
22
+ * the CLAUDE.md compaction depends on.
23
+ */
24
+ export function renderStageSheet(
25
+ rules: Rule[],
26
+ stages: Record<string, Stage[]>,
27
+ stage: Stage,
28
+ ): string {
29
+ const mine = rules
30
+ .filter((r) => r.scope.kind === "pipeline" && (stages[r.id] ?? []).includes(stage))
31
+ .sort((a, b) => a.id.localeCompare(b.id));
32
+
33
+ const of = (tier: Rule["tier"]) => mine.filter((r) => r.tier === tier);
34
+ const section = (heading: string, rs: Rule[], withVerify: boolean) =>
35
+ rs.length === 0
36
+ ? []
37
+ : [
38
+ heading,
39
+ ``,
40
+ ...rs.map((r) =>
41
+ [
42
+ `- ${r.claim} (\`${r.id}\`)`,
43
+ ...(withVerify && r.verify ? [` - verify: ${r.verify}`] : []),
44
+ ...exceptionLines(r),
45
+ ].join("\n"),
46
+ ),
47
+ ``,
48
+ ];
49
+
50
+ return [
51
+ `<!-- Generated from packages/video-craft/src/rules/ + stages.ts. Do not edit. -->`,
52
+ ``,
53
+ `# Rules for the ${stage} stage`,
54
+ ``,
55
+ `These are the registry rules scoped to ${stage}-stage agents. Scoping narrows attention, never authority: every rule in the registry still applies to you. This sheet carries the full claim of each rule in scope; HOW_I_BUILD_VIDEOS.md carries every rule's claim, why and provenance, and is where to read one you meet only as an id (search for it in a \`Rule id:\` line). CLAUDE.md lists the out-of-scope rules by id rather than in full, so do not treat it as the place to go for a claim.`,
56
+ ``,
57
+ ...section(`## Gates - settled by running the check, not by reading the diff`, of("gate"), true),
58
+ ...section(`## Craft rules`, of("craft"), false),
59
+ ...section(`## Process rules`, of("process"), false),
60
+ ]
61
+ .join("\n")
62
+ .trimEnd();
63
+ }
64
+
65
+ /** Extracts the text between `<!-- edge:begin NAME -->` / `<!-- edge:end NAME -->`
66
+ * in docs/style/CREATIVE_EDGE.md. Throws on a missing marker: a renamed or
67
+ * deleted marker must fail generation, not silently ship an empty digest. */
68
+ export function extractEdgeSection(doc: string, name: string): string {
69
+ const begin = `<!-- edge:begin ${name} -->`;
70
+ const end = `<!-- edge:end ${name} -->`;
71
+ const from = doc.indexOf(begin);
72
+ if (from === -1) throw new Error(`marker "edge:begin ${name}" not found in CREATIVE_EDGE.md`);
73
+ // A duplicated marker (paste error, the marker syntax quoted in prose)
74
+ // would silently select the wrong span; only absence is louder than that.
75
+ if (doc.indexOf(begin, from + begin.length) !== -1) {
76
+ throw new Error(`marker "edge:begin ${name}" appears more than once in CREATIVE_EDGE.md`);
77
+ }
78
+ const to = doc.indexOf(end, from);
79
+ if (to === -1) throw new Error(`marker "edge:end ${name}" not found in CREATIVE_EDGE.md`);
80
+ if (doc.indexOf(end, to + end.length) !== -1) {
81
+ throw new Error(`marker "edge:end ${name}" appears more than once in CREATIVE_EDGE.md`);
82
+ }
83
+ return doc.slice(from + begin.length, to).trim();
84
+ }
85
+
86
+ /**
87
+ * The generated artifact crossing the craft -> cli boundary (same mechanism
88
+ * and direction as waivers.generated.json): per-stage sheets plus the
89
+ * CREATIVE_EDGE digest the dispatch shell inlines for scene/amplify/review
90
+ * nodes. Stable key order so regeneration is byte-deterministic.
91
+ */
92
+ export function renderRuleSheetsJson(
93
+ rules: Rule[],
94
+ stages: Record<string, Stage[]>,
95
+ creativeEdgeDoc: string,
96
+ ): string {
97
+ const sheets: Record<string, string> = {};
98
+ for (const stage of ALL_STAGES) {
99
+ sheets[stage] = renderStageSheet(rules, stages, stage);
100
+ }
101
+ return `${JSON.stringify(
102
+ {
103
+ stages: sheets,
104
+ creativeEdge: {
105
+ bar: extractEdgeSection(creativeEdgeDoc, "bar"),
106
+ checklist: extractEdgeSection(creativeEdgeDoc, "checklist"),
107
+ },
108
+ },
109
+ null,
110
+ 2,
111
+ )}\n`;
112
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { RULES } from "./registry";
3
+ import { ALL_STAGES, STAGES, validateStages } from "./stages";
4
+ import type { Rule, Stage } from "./types";
5
+
6
+ const rule = (id: string, tier: Rule["tier"]): Rule => ({
7
+ id,
8
+ claim: "c",
9
+ why: "w",
10
+ learnedFrom: "docs/style/CREATIVE_EDGE.md",
11
+ tier,
12
+ ...(tier === "gate" ? { verify: "v" } : {}),
13
+ scope: { kind: "pipeline" },
14
+ enforcement: { kind: "prose", unanchorable: "u" },
15
+ });
16
+
17
+ describe("validateStages", () => {
18
+ test("flags a pipeline rule with no stage entry", () => {
19
+ const errors = validateStages([rule("lonely-rule", "craft")], {});
20
+ expect(errors.some((e) => e.includes("lonely-rule"))).toBe(true);
21
+ });
22
+
23
+ test("flags an empty stage list", () => {
24
+ const errors = validateStages([rule("empty-rule", "craft")], { "empty-rule": [] });
25
+ expect(errors.some((e) => e.includes("empty-rule"))).toBe(true);
26
+ });
27
+
28
+ test("flags a stage entry for an id no pipeline rule carries", () => {
29
+ const errors = validateStages([], { "ghost-rule": ["scene"] });
30
+ expect(errors.some((e) => e.includes("ghost-rule"))).toBe(true);
31
+ });
32
+
33
+ test("flags a gate that carries neither review nor ship", () => {
34
+ const errors = validateStages([rule("author-only-gate", "gate")], {
35
+ "author-only-gate": ["scene"],
36
+ });
37
+ expect(errors.some((e) => e.includes("author-only-gate"))).toBe(true);
38
+ });
39
+
40
+ test("accepts a gate verified at review", () => {
41
+ expect(validateStages([rule("ok-gate", "gate")], { "ok-gate": ["scene", "review"] })).toEqual(
42
+ [],
43
+ );
44
+ });
45
+
46
+ test("accepts a gate verified at ship", () => {
47
+ expect(validateStages([rule("ship-gate", "gate")], { "ship-gate": ["ship"] })).toEqual([]);
48
+ });
49
+
50
+ test("flags an unknown stage name in a dynamically built table", () => {
51
+ const stages = { "typo-rule": ["scene "] } as unknown as Record<string, Stage[]>;
52
+ const errors = validateStages([rule("typo-rule", "craft")], stages);
53
+ expect(errors.some((e) => e.includes('unknown stage "scene "'))).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe("the real stage table", () => {
58
+ test("the registry and STAGES validate together", () => {
59
+ expect(validateStages(RULES, STAGES)).toEqual([]);
60
+ });
61
+
62
+ test("every stage has at least one rule", () => {
63
+ for (const stage of ALL_STAGES) {
64
+ const count = Object.values(STAGES).filter((s) => s.includes(stage)).length;
65
+ expect(count, `stage "${stage}" has no rules`).toBeGreaterThan(0);
66
+ }
67
+ });
68
+ });
package/src/stages.ts ADDED
@@ -0,0 +1,207 @@
1
+ import type { Rule, Stage } from "./types";
2
+
3
+ /**
4
+ * Stage assignment for every pipeline-scoped rule: which dispatched agents
5
+ * must read it. This is THE audit surface for scoping - review it as a
6
+ * table, not as 100 diffs. Component-scoped rules ride kit sidecars and
7
+ * carry no entry here.
8
+ *
9
+ * Conventions, enforced by validateStages:
10
+ * - every pipeline rule has an entry with at least one stage (a rule tagged
11
+ * out of every view is a rule nobody reads);
12
+ * - every gate carries "review" or "ship" (someone downstream must be able
13
+ * to verify it, not only the author who can violate it).
14
+ *
15
+ * When in doubt, tag broadly: the cost of an extra stage is a few lines of
16
+ * prompt; the cost of a missing one is the documented failure mode (the
17
+ * scene-root tag convention absent from the contract template cost a fix
18
+ * round on both build-2 first passes).
19
+ *
20
+ * Stage meanings map to graph node families (see nodeStage in
21
+ * video-cli/src/graph/ruleSheets.ts): recon = clarify + recon:*;
22
+ * plan = plan:draft/self-review/revise; scene = scene:* + transitions;
23
+ * amplify = amplify:*; review = coherence + review:consume; vo = vo:lock +
24
+ * vo recording; audio = sfx:place + music/mix; ship = operator-side release
25
+ * steps (freeze, pin, render, notes).
26
+ */
27
+ export const STAGES: Record<string, Stage[]> = {
28
+ // gates
29
+ "audio-is-the-clock": ["vo", "review", "ship"],
30
+ "check-pin-before-ship": ["ship"],
31
+ "data-radar-tags-declared": ["scene", "amplify", "review"],
32
+ "derive-seams-dont-measure": ["scene", "amplify", "review"],
33
+ "deterministic-scene-source": ["scene", "amplify", "review"],
34
+ "element-onscreen-for-its-vo-sentence": ["plan", "scene", "amplify", "review"],
35
+ "geometric-seams-not-crossfades": ["plan", "scene", "review"],
36
+ "geometry-sidecar-exports-typed": ["scene", "amplify", "review"],
37
+ "hero-fills-the-frame": ["scene", "amplify", "review"],
38
+ "interpolate-literal-keys-monotonic": ["scene", "amplify", "review"],
39
+ "master-mix-loudness-target": ["audio", "ship"],
40
+ "no-double-frame-shift": ["scene", "amplify", "review"],
41
+ "no-nested-svg-filters": ["scene", "amplify", "review"],
42
+ "no-shorthand-with-undefined-longhand": ["scene", "amplify", "review"],
43
+ "no-stacked-fades-at-a-cut": ["scene", "review"],
44
+ "no-static-holds": ["scene", "amplify", "review"],
45
+ "occlusion-visible-elements": ["scene", "amplify", "review"],
46
+ "predraw-content-present-at-the-cut": ["scene", "review"],
47
+ "radar-dispositions-are-human-only": ["review", "ship"],
48
+ "scene-root-tag-declared": ["scene", "amplify", "review"],
49
+ "source-archived-before-brief": ["recon", "review"],
50
+ "thesis-declared-in-brief": ["plan", "review"],
51
+ "tokens-not-raw-hexes": ["scene", "amplify", "review"],
52
+
53
+ // craft
54
+ "approved-sfx-palette-only": ["audio", "review"],
55
+ "atmosphere-gradients-fade-at-both-ends": ["scene", "amplify"],
56
+ "beats-derive-from-their-own-sequence": ["scene", "amplify"],
57
+ "camera-is-the-narrator": ["plan", "scene", "amplify", "review"],
58
+ "cold-viewer-clarity": ["plan", "scene", "review"],
59
+ "commit-or-cut-effects": ["scene", "amplify", "review"],
60
+ "convert-frames-across-clock-boundary": ["scene", "amplify"],
61
+ "cross-cut-elements-live-in-the-hud-layer": ["scene", "amplify"],
62
+ "cursor-clicks-real-controls": ["scene", "review"],
63
+ "cut-on-real-audio-pauses": ["plan", "vo"],
64
+ "dark-but-never-dim": ["scene", "amplify", "review"],
65
+ "declare-a-vertical-zone-map": ["scene", "amplify"],
66
+ "diegetic-sound-for-onscreen-actions": ["scene", "audio", "review"],
67
+ "distinct-transition-per-beat": ["scene", "amplify", "review"],
68
+ "download-or-draw-rich-assets": ["recon", "scene", "amplify", "review"],
69
+ "energy-from-timing-not-part-count": ["plan", "scene", "amplify", "review"],
70
+ "fix-crowding-by-rebanding-not-nudging": ["scene", "amplify"],
71
+ "floating-not-boxed": ["scene", "amplify", "review"],
72
+ "focus-beats-recede-the-context": ["scene", "amplify"],
73
+ "font-vars-brand-remocn": ["scene"],
74
+ "glow-is-a-focal-accent": ["scene", "amplify", "review"],
75
+ "html-masks-for-animated-counters": ["scene", "amplify"],
76
+ "impact-stack-on-payoffs": ["scene", "amplify"],
77
+ "interactive-targets-are-declared-data": ["plan", "scene"],
78
+ "lateness-is-structural-overlap-the-beats": ["plan", "scene", "amplify"],
79
+ "less-text-more-visualizations": ["plan", "scene", "amplify", "review"],
80
+ "make-2d-elements-3d": ["scene", "amplify", "review"],
81
+ "motion-carries-weight-and-meaning": ["scene", "amplify", "review"],
82
+ "motion-density-floor": ["scene", "amplify", "review"],
83
+ "never-restate-derived-frame-numbers-in-comments": ["scene", "amplify"],
84
+ "no-ai-gradient-look": ["scene", "amplify", "review"],
85
+ "no-colored-flash-to-hide-a-cut": ["scene", "review"],
86
+ "no-linear-motion": ["scene", "amplify", "review"],
87
+ "no-repeated-asset-across-shots": ["plan", "scene", "review"],
88
+ "one-continuous-vo-track": ["vo", "scene"],
89
+ "one-punch-per-moment-class": ["scene", "amplify", "review"],
90
+ "open-static-after-a-hard-cut": ["scene", "review"],
91
+ "openings-and-closings-carry-more": ["plan", "scene", "amplify", "review"],
92
+ "persistent-device-frames": ["plan", "scene"],
93
+ "premium-surfaces-not-flat-borders": ["scene", "amplify"],
94
+ "real-assets-not-placeholders": ["recon", "scene", "review"],
95
+ "rebuild-ui-from-recon-not-screenshots": ["recon", "scene"],
96
+ "remap-rate-endpoint-formula": ["scene"],
97
+ "remocn-content-tailwind-chrome": ["scene", "amplify"],
98
+ "retime-only-via-playback-or-design-tokens": ["scene", "vo"],
99
+ "sample-brand-from-product": ["recon", "scene"],
100
+ "sample-velocity-from-previous-frame-position": ["scene", "amplify"],
101
+ "scrub-demo-data-to-generic-roles": ["recon", "scene", "review"],
102
+ "statistics-self-explanatory": ["plan", "vo", "scene", "review"],
103
+ "stops-as-relative-spacing": ["scene"],
104
+ "the-whole-canvas-works": ["scene", "amplify", "review"],
105
+ "theme-per-section": ["plan", "scene"],
106
+ "tiny-body-copy-as-ruled-lines": ["scene"],
107
+ "titles-pinned-outside-camera-push": ["scene", "review"],
108
+ "type-token-defaulted-params-as-string": ["scene"],
109
+ "vary-kit-choreography-across-sections": ["plan", "scene", "amplify", "review"],
110
+ "vary-the-visualization-grammar": ["plan", "scene", "amplify", "review"],
111
+ "vo-addresses-the-viewer": ["plan", "vo"],
112
+ "vo-is-a-performance": ["vo", "review"],
113
+ "wrappers-preserve-child-geometry": ["scene", "amplify"],
114
+
115
+ // process
116
+ "best-scene-sets-the-bar": ["amplify", "review"],
117
+ "bump-before-refreeze": ["ship"],
118
+ "delete-rejected-rigs-completely": ["scene", "amplify"],
119
+ "emit-build-notes": ["review", "ship"],
120
+ // egw3: NOT in scene/amplify. Dispatched scene/fix/amplify agents are
121
+ // author-once - their task bodies command "typecheck only, no render, no
122
+ // stills" - so shipping a render-evidence rule in the same prompt made the
123
+ // two contradict; whichever the worker obeyed, it violated the other. The
124
+ // review/fix-round driver renders, so evidence rules live there.
125
+ "evidence-before-claiming-fixed": ["review"],
126
+ "exact-peer-lockstep": ["ship"],
127
+ "first-pass-is-a-draft": ["plan", "amplify"],
128
+ "fixes-preserve-approved-elements": ["scene", "amplify", "review"],
129
+ "live-link-duplicate-react-alias": ["scene"],
130
+ "measure-alignment-dont-eyeball": ["scene", "amplify", "review"],
131
+ "never-edit-tier-b-vendored": ["scene", "amplify"],
132
+ "no-duplicate-motion-primitives": ["scene"],
133
+ "one-render-per-round": ["review", "ship"],
134
+ "pixel-diff-an-empty-band-first": ["review"],
135
+ "plan-is-published-not-gating": ["plan"],
136
+ "powered-by-revola-watermark": ["plan", "scene", "review"],
137
+ "recon-strands-run-in-parallel": ["recon"],
138
+ "registry-before-handrolling": ["scene"],
139
+ "review-the-render-by-frame-sweep": ["review"],
140
+ "same-family-same-project": ["plan"],
141
+ "self-review-the-plan-for-gaps": ["plan"],
142
+ "tier-a-requires-prop-unreachable-edit": ["scene", "ship"],
143
+ // egw3: NOT in scene/amplify, same contradiction as
144
+ // evidence-before-claiming-fixed above - the rule's "run the full render
145
+ // only on demand" framing presumes a driver that CAN render.
146
+ "verify-cheap-render-dear": ["review", "ship"],
147
+ "vo-script-locks-spoken-words": ["plan", "vo"],
148
+ };
149
+
150
+ /**
151
+ * The scoping invariants. Run by generate.ts (fail fast, never project a
152
+ * mis-scoped table) and by stages.test.ts. Errors, not throws, so callers
153
+ * can report all problems at once.
154
+ */
155
+ export function validateStages(rules: Rule[], stages: Record<string, Stage[]>): string[] {
156
+ const errors: string[] = [];
157
+ const pipelineIds = new Set(rules.filter((r) => r.scope.kind === "pipeline").map((r) => r.id));
158
+
159
+ for (const rule of rules) {
160
+ if (rule.scope.kind !== "pipeline") continue;
161
+ const assigned = stages[rule.id];
162
+ if (!assigned || assigned.length === 0) {
163
+ errors.push(`pipeline rule "${rule.id}" has no stage assignment`);
164
+ continue;
165
+ }
166
+ // TS catches typos in the literal table, but a dynamically built or
167
+ // cast table would not be checked; a rule tagged with an unknown stage
168
+ // drops out of every generated sheet.
169
+ for (const s of assigned) {
170
+ if (!(ALL_STAGES as readonly string[]).includes(s)) {
171
+ errors.push(`rule "${rule.id}" carries unknown stage "${s}"`);
172
+ }
173
+ }
174
+ // A gate blocks the ship, so it must sit in a view someone downstream of
175
+ // the author reads: review (per-round verification) or ship (release
176
+ // checks). An author-only gate is a claim nobody re-checks.
177
+ if (rule.tier === "gate" && !assigned.includes("review") && !assigned.includes("ship")) {
178
+ errors.push(`gate "${rule.id}" carries neither "review" nor "ship"`);
179
+ }
180
+ }
181
+
182
+ for (const id of Object.keys(stages)) {
183
+ if (!pipelineIds.has(id)) {
184
+ errors.push(`stage entry "${id}" matches no pipeline rule`);
185
+ }
186
+ }
187
+
188
+ return errors;
189
+ }
190
+
191
+ export const ALL_STAGES = [
192
+ "recon",
193
+ "plan",
194
+ "scene",
195
+ "amplify",
196
+ "review",
197
+ "vo",
198
+ "audio",
199
+ "ship",
200
+ ] as const satisfies readonly Stage[];
201
+
202
+ // Compile-time exhaustiveness: a Stage variant added to the union in types.ts
203
+ // but missing from ALL_STAGES would otherwise silently get no generated
204
+ // sheet, no drift test, and no rules (every consumer iterates ALL_STAGES).
205
+ type MissingStage = Exclude<Stage, (typeof ALL_STAGES)[number]>;
206
+ const _allStagesExhaustive: [MissingStage] extends [never] ? true : MissingStage = true;
207
+ void _allStagesExhaustive;
@@ -0,0 +1,16 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ /**
5
+ * TEST SUPPORT ONLY. The absolute root of THIS repository's checkout, for the
6
+ * package's own tests (and only them) to pass as the explicit scan root.
7
+ *
8
+ * Library code must never import this: computing a root by walking up from
9
+ * the module's own location is exactly what breaks when the package is
10
+ * installed inside a consumer repo (the walk lands in node_modules, or in the
11
+ * consumer's tree, depending on layout). Every library entry point takes the
12
+ * root as an explicit parameter instead; scripts compute theirs at the script
13
+ * boundary; the CLI derives its from the operator's arguments.
14
+ */
15
+ // this file -> packages/video-craft/src -> packages/video-craft -> packages -> repo root
16
+ export const toolchainRepoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");