@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,152 @@
1
+ import type { Rule } from "./types";
2
+
3
+ /**
4
+ * One line per waiver, appended to a rule's claim in both projections. The
5
+ * claims are what future sessions read, so a waiver that lives only in the rule
6
+ * file is invisible exactly where it matters: a session inside the waived
7
+ * project would read the unqualified claim and "fix" the client's decision
8
+ * back out.
9
+ */
10
+ const exceptionLines = (r: Rule, indent: string): string[] =>
11
+ (r.exceptions ?? []).map(
12
+ (ex) => `${indent}- waived in \`${ex.project}\`: ${ex.decision} (recorded: \`${ex.recordedIn}\`)`,
13
+ );
14
+
15
+ /**
16
+ * Replaces the text between `<!-- craft:begin NAME -->` and
17
+ * `<!-- craft:end NAME -->`. Throws on a missing marker rather than appending,
18
+ * because silently appending would duplicate the block on every run and the
19
+ * drift test would then never converge.
20
+ */
21
+ export function spliceBlock(doc: string, marker: string, body: string): string {
22
+ const begin = `<!-- craft:begin ${marker} -->`;
23
+ const end = `<!-- craft:end ${marker} -->`;
24
+
25
+ const from = doc.indexOf(begin);
26
+ if (from === -1) throw new Error(`marker "craft:begin ${marker}" not found`);
27
+
28
+ const to = doc.indexOf(end, from);
29
+ if (to === -1) throw new Error(`marker "craft:end ${marker}" not found`);
30
+
31
+ return doc.slice(0, from + begin.length) + "\n" + body + "\n" + doc.slice(to);
32
+ }
33
+
34
+ /**
35
+ * Compact projection for the repo-root CLAUDE.md. CLAUDE.md is injected into
36
+ * every Claude Code session's context (and re-injected after compaction);
37
+ * HOW_I_BUILD_VIDEOS.md is read only when a session touches pipeline
38
+ * behaviour. Measured 2026-08-27: all 107 pipeline claims appeared VERBATIM in
39
+ * both, so a pipeline session paid ~6k tokens to read the same sentences
40
+ * twice (video-toolchain-cg6).
41
+ *
42
+ * Dropping the craft rules outright would have been the wrong trade - a
43
+ * session that never opens HOW_I_BUILD would not know they exist. So the split
44
+ * is by what a session must be able to act on WITHOUT another read:
45
+ *
46
+ * - gates keep their full claim and `verify` line: these cost rebuild rounds,
47
+ * and each is settled by running something the line names.
48
+ * - waived rules keep their full claim at ANY tier. `exceptionLines` above
49
+ * records why: a waiver invisible in the projection is exactly how a
50
+ * session inside the waived project "fixes" the client's decision back out.
51
+ * A bare id cannot carry that, so waived rules are never compacted.
52
+ * - every other rule is named by id. The ids are written as claims
53
+ * (`no-static-holds`, `tokens-not-raw-hexes`), so an agent can never not
54
+ * know a rule exists; it fetches the full claim when the id looks relevant.
55
+ */
56
+ export function renderClaudeMdBlock(
57
+ rules: Rule[],
58
+ // Compaction is only sound where HOW_I_BUILD_VIDEOS.md exists to look an id
59
+ // up in. `syncProjectedDocs` passes false for a consumer that carries no
60
+ // workflow doc - its contract explicitly allows that - because an id index
61
+ // pointing at an absent document is strictly worse than the full block it
62
+ // replaced. Defaults to true: the toolchain repo always has HOW.
63
+ { compact = true }: { compact?: boolean } = {},
64
+ ): string {
65
+ const pipeline = rules
66
+ .filter((r) => r.scope.kind === "pipeline")
67
+ .sort((a, b) => a.id.localeCompare(b.id));
68
+
69
+ const of = (tier: Rule["tier"]) => pipeline.filter((r) => r.tier === tier);
70
+ const isWaived = (r: Rule) => (r.exceptions ?? []).length > 0;
71
+
72
+ // Headings are emitted only for tiers that have members. An empty "Gates"
73
+ // heading reads as "there are no gates", which is a different and much more
74
+ // reassuring claim than "this projection has nothing to put here".
75
+ const section = (heading: string[], body: string[]) =>
76
+ body.length === 0 ? [] : [...heading, ``, ...body, ``];
77
+
78
+ const full = (r: Rule) => [`- ${r.claim} (\`${r.id}\`)`, ...exceptionLines(r, " ")].join("\n");
79
+
80
+ /** Waived rules in full, then one line naming every other id in the tier. */
81
+ const compacted = (rs: Rule[], label: string): string[] => {
82
+ if (rs.length === 0) return [];
83
+ if (!compact) return rs.map(full);
84
+ const waived = rs.filter(isWaived);
85
+ const plain = rs.filter((r) => !isWaived(r));
86
+ return [
87
+ ...waived.map(full),
88
+ ...(waived.length && plain.length ? [``] : []),
89
+ ...(plain.length
90
+ ? [
91
+ // Wording is load-bearing and was wrong in review twice over. "further"
92
+ // dangles in a tier with no waived rules above it (process has none).
93
+ // And the lookup recipe must key on what the reader HOLDS - an id -
94
+ // not on the heading text: renderPipelineBlock emits `### <claim>`, so
95
+ // the id appears only in the `Rule id:` line two lines below. Naming
96
+ // the wrong token would send every lookup into a 139 KB scan, which is
97
+ // the one thing this compaction cannot afford to get wrong.
98
+ `${plain.length} more ${label} rule${plain.length === 1 ? "" : "s"} `
99
+ + `bind${plain.length === 1 ? "s" : ""} this work, listed by id below. `
100
+ + `To read one, search HOW_I_BUILD_VIDEOS.md for its id in a \`Rule id:\` line - the headings there are the `
101
+ + `claims themselves, so the id is not a heading. That is the only doc carrying each rule's why and `
102
+ + `provenance; the per-stage sheets under \`docs/rules/stages/\` carry the claims, scoped by stage. `
103
+ + `Every id here is a rule you are accountable to, not a suggestion.`,
104
+ ``,
105
+ plain.map((r) => `\`${r.id}\``).join(", "),
106
+ ]
107
+ : []),
108
+ ];
109
+ };
110
+
111
+ return [
112
+ `<!-- This block is generated from packages/video-craft/src/rules/. Do not edit it here. -->`,
113
+ ``,
114
+ ...section(
115
+ [
116
+ `### Gates - if you remember nothing else`,
117
+ ``,
118
+ `Each line below has cost a rebuild round. Every one is settled by running something, not by reading the diff: run the check.`,
119
+ ],
120
+ of("gate").map((r) =>
121
+ [`- ${r.claim} (\`${r.id}\`)`, ` - verify: ${r.verify}`, ...exceptionLines(r, " ")].join("\n"),
122
+ ),
123
+ ),
124
+ ...section([`### Craft rules`], compacted(of("craft"), "craft")),
125
+ ...section([`### Process rules`], compacted(of("process"), "process")),
126
+ ].join("\n").trimEnd();
127
+ }
128
+
129
+ export function renderPipelineBlock(rules: Rule[]): string {
130
+ const pipeline = rules
131
+ .filter((r) => r.scope.kind === "pipeline")
132
+ .sort((a, b) => a.id.localeCompare(b.id));
133
+
134
+ return [
135
+ `<!-- This block is generated from packages/video-craft/src/rules/. Do not edit it here. -->`,
136
+ ``,
137
+ ...pipeline.flatMap((r) => [
138
+ `### ${r.claim}`,
139
+ ``,
140
+ r.why,
141
+ ``,
142
+ // The tier is restated here rather than only in the CLAUDE.md projection
143
+ // so the reference doc and the injected block cannot disagree about how
144
+ // much a rule weighs - a reader who arrives via a rule id from a build
145
+ // note lands here, not there.
146
+ `Learned from \`${r.learnedFrom}\`. Rule id: \`${r.id}\`. Tier: ${r.tier}.`,
147
+ ...(r.verify ? [``, `Verify: ${r.verify}`] : []),
148
+ ...((r.exceptions ?? []).length > 0 ? [``, ...exceptionLines(r, "")] : []),
149
+ ``,
150
+ ]),
151
+ ].join("\n").trimEnd();
152
+ }
@@ -0,0 +1,152 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test } from "vitest";
5
+ import { syncProjectedDocs } from "./projectDocSync";
6
+
7
+ const CLAUDE_WITH_MARKERS = [
8
+ "# Consumer repo",
9
+ "",
10
+ "<!-- craft:begin claude-md -->",
11
+ "OLD-BLOCK-SENTINEL",
12
+ "<!-- craft:end claude-md -->",
13
+ "",
14
+ "tail text stays",
15
+ "",
16
+ ].join("\n");
17
+
18
+ const HOW_WITH_MARKERS = [
19
+ "# How I build videos",
20
+ "",
21
+ "<!-- craft:begin pipeline -->",
22
+ "OLD-BLOCK-SENTINEL",
23
+ "<!-- craft:end pipeline -->",
24
+ "",
25
+ ].join("\n");
26
+
27
+ let dirs: string[] = [];
28
+ const targetRoot = (): string => {
29
+ const d = mkdtempSync(join(tmpdir(), "doc-sync-"));
30
+ dirs.push(d);
31
+ return d;
32
+ };
33
+ afterEach(() => {
34
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
35
+ dirs = [];
36
+ });
37
+
38
+ describe("syncProjectedDocs", () => {
39
+ test("splices both docs when both carry markers", () => {
40
+ const root = targetRoot();
41
+ writeFileSync(join(root, "CLAUDE.md"), CLAUDE_WITH_MARKERS);
42
+ writeFileSync(join(root, "HOW_I_BUILD_VIDEOS.md"), HOW_WITH_MARKERS);
43
+
44
+ const result = syncProjectedDocs(root);
45
+
46
+ expect(result.updated).toEqual(["CLAUDE.md", "HOW_I_BUILD_VIDEOS.md"]);
47
+ expect(result.skipped).toEqual([]);
48
+ const claude = readFileSync(join(root, "CLAUDE.md"), "utf8");
49
+ expect(claude).not.toContain("OLD-BLOCK-SENTINEL");
50
+ // A real rule claim landed inside the markers, and text outside survived.
51
+ expect(claude).toContain("audio-is-the-clock");
52
+ expect(claude).toContain("tail text stays");
53
+ const how = readFileSync(join(root, "HOW_I_BUILD_VIDEOS.md"), "utf8");
54
+ expect(how).not.toContain("OLD-BLOCK-SENTINEL");
55
+ expect(how).toContain("<!-- craft:begin pipeline -->");
56
+ });
57
+
58
+ test("skips a target with no HOW_I_BUILD_VIDEOS.md at all, with a reason", () => {
59
+ const root = targetRoot();
60
+ writeFileSync(join(root, "CLAUDE.md"), CLAUDE_WITH_MARKERS);
61
+
62
+ const result = syncProjectedDocs(root);
63
+
64
+ expect(result.updated).toEqual(["CLAUDE.md"]);
65
+ expect(result.skipped).toEqual([
66
+ { file: "HOW_I_BUILD_VIDEOS.md", reason: "not present in target" },
67
+ ]);
68
+ });
69
+
70
+ // CLAUDE.md compacts unwaived craft and process rules to an id index
71
+ // (video-toolchain-cg6), which only works because HOW_I_BUILD_VIDEOS.md is
72
+ // there to look an id up in. A consumer may legitimately carry no workflow
73
+ // doc - this function's own contract allows it - and compacting into that
74
+ // target would leave ~78 ids pointing at nothing. So the projection adapts
75
+ // to whether its fallback exists rather than emitting a dangling pointer.
76
+ test("writes CLAUDE.md UNCOMPACTED when the target has no HOW_I_BUILD_VIDEOS.md to look ids up in", () => {
77
+ const root = targetRoot();
78
+ writeFileSync(join(root, "CLAUDE.md"), CLAUDE_WITH_MARKERS);
79
+
80
+ syncProjectedDocs(root);
81
+
82
+ const claude = readFileSync(join(root, "CLAUDE.md"), "utf8");
83
+ expect(claude).not.toMatch(/rules? bind|binds this work, listed by id/);
84
+ // A known craft rule renders as a full claim line, not a bare id.
85
+ expect(claude).toContain("- Never hard-code a hex color in a scene file");
86
+ expect(claude).toContain("- Never style with generic blue-purple gradient washes");
87
+ });
88
+
89
+ test("writes CLAUDE.md compacted when HOW_I_BUILD_VIDEOS.md IS present", () => {
90
+ const root = targetRoot();
91
+ writeFileSync(join(root, "CLAUDE.md"), CLAUDE_WITH_MARKERS);
92
+ writeFileSync(join(root, "HOW_I_BUILD_VIDEOS.md"), HOW_WITH_MARKERS);
93
+
94
+ syncProjectedDocs(root);
95
+
96
+ const claude = readFileSync(join(root, "CLAUDE.md"), "utf8");
97
+ expect(claude).toMatch(/binds? this work, listed by id/);
98
+ expect(claude).toContain("`no-ai-gradient-look`");
99
+ expect(claude).not.toContain("- Never style with generic blue-purple gradient washes");
100
+ });
101
+
102
+ test("throws when CLAUDE.md is missing entirely", () => {
103
+ const root = targetRoot();
104
+ expect(() => syncProjectedDocs(root)).toThrow(/CLAUDE\.md/);
105
+ });
106
+
107
+ test("throws the spliceBlock error when CLAUDE.md exists without markers - never appends", () => {
108
+ const root = targetRoot();
109
+ writeFileSync(join(root, "CLAUDE.md"), "# no markers here\n");
110
+ expect(() => syncProjectedDocs(root)).toThrow('marker "craft:begin claude-md" not found');
111
+ // The file is untouched: no append, no partial write.
112
+ expect(readFileSync(join(root, "CLAUDE.md"), "utf8")).toBe("# no markers here\n");
113
+ });
114
+
115
+ test("throws the spliceBlock error when HOW_I_BUILD_VIDEOS.md exists without markers", () => {
116
+ const root = targetRoot();
117
+ writeFileSync(join(root, "CLAUDE.md"), CLAUDE_WITH_MARKERS);
118
+ writeFileSync(join(root, "HOW_I_BUILD_VIDEOS.md"), "# no markers\n");
119
+ expect(() => syncProjectedDocs(root)).toThrow('marker "craft:begin pipeline" not found');
120
+ });
121
+
122
+ // rulesSyncShell.ts re-splices CLAUDE.md after materializing a packaged
123
+ // HOW_I_BUILD_VIDEOS.md so one rules:sync run leaves the compact block a
124
+ // second run would (Task 5 review, bd video-toolchain-zaaz). That re-splice
125
+ // must never touch the manual: it is the packaged release artifact, and
126
+ // rendering renderPipelineBlock(RULES) against it here would diverge from
127
+ // whatever shipped.
128
+ test("claudeMdOnly splices CLAUDE.md and leaves an existing HOW_I_BUILD_VIDEOS.md byte-identical", () => {
129
+ const root = targetRoot();
130
+ writeFileSync(join(root, "CLAUDE.md"), CLAUDE_WITH_MARKERS);
131
+ const packagedHow = [
132
+ "# HOW (packaged)",
133
+ "",
134
+ "<!-- craft:begin pipeline -->",
135
+ "RELEASE-PIPELINE-BLOCK",
136
+ "<!-- craft:end pipeline -->",
137
+ "",
138
+ ].join("\n");
139
+ writeFileSync(join(root, "HOW_I_BUILD_VIDEOS.md"), packagedHow);
140
+
141
+ const result = syncProjectedDocs(root, { claudeMdOnly: true });
142
+
143
+ expect(result.updated).toEqual(["CLAUDE.md"]);
144
+ expect(result.skipped).toEqual([]);
145
+ const claude = readFileSync(join(root, "CLAUDE.md"), "utf8");
146
+ expect(claude).not.toContain("OLD-BLOCK-SENTINEL");
147
+ // Compact, because the manual is present.
148
+ expect(claude).toMatch(/binds? this work, listed by id/);
149
+ // The manual itself is untouched: no re-render of the pipeline block.
150
+ expect(readFileSync(join(root, "HOW_I_BUILD_VIDEOS.md"), "utf8")).toBe(packagedHow);
151
+ });
152
+ });
@@ -0,0 +1,79 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { renderClaudeMdBlock, renderPipelineBlock, spliceBlock } from "./projectDoc";
4
+ import { RULES } from "./registry";
5
+
6
+ export type DocSyncResult = {
7
+ /** Root-relative names of the files whose generated blocks were rewritten. */
8
+ updated: string[];
9
+ /** Files legitimately absent from the target, with why they were skipped. */
10
+ skipped: { file: string; reason: string }[];
11
+ };
12
+
13
+ /**
14
+ * Splices the generated consumer-doc projections (the CLAUDE.md craft block
15
+ * and the HOW_I_BUILD_VIDEOS.md pipeline block) into `targetRoot`'s files.
16
+ * This is the consumer half of the rules generator: package-internal
17
+ * projections (kit meta files, stage sheets, ruleSheets.generated.json) run
18
+ * at toolchain release time via scripts/generate.ts, while this function runs
19
+ * inside any consumer repo via `video rules:sync`.
20
+ *
21
+ * Splice-only, never append: a target file must already carry the craft
22
+ * markers, and a missing marker throws spliceBlock's error rather than
23
+ * duplicating the block on every run. A target with no HOW_I_BUILD_VIDEOS.md
24
+ * at all is skipped with a reason - consumers may not carry the workflow doc -
25
+ * but CLAUDE.md is where the rules become binding for sessions in the target
26
+ * repo, so its absence (like a missing marker in it) is an error.
27
+ *
28
+ * `opts.claudeMdOnly` renders and splices CLAUDE.md exactly as above but
29
+ * never touches HOW_I_BUILD_VIDEOS.md. rulesSyncShell.ts needs this for the
30
+ * re-splice it runs after materializing a missing manual: CLAUDE.md's compact
31
+ * block must be rendered against the now-present HOW, but the manual itself
32
+ * is the packaged release artifact (cpSync'd verbatim, pipeline block
33
+ * included) and re-splicing it here would render the pipeline block from the
34
+ * live RULES instead, which could diverge from what shipped (Task 5 review,
35
+ * bd video-toolchain-zaaz).
36
+ */
37
+ export function syncProjectedDocs(targetRoot: string, opts?: { claudeMdOnly?: boolean }): DocSyncResult {
38
+ const updated: string[] = [];
39
+ const skipped: { file: string; reason: string }[] = [];
40
+
41
+ const claudePath = join(targetRoot, "CLAUDE.md");
42
+ if (!existsSync(claudePath)) {
43
+ throw new Error(
44
+ `CLAUDE.md does not exist in ${targetRoot} - rules:sync splices into existing files and never creates them. `
45
+ + `Create CLAUDE.md with the craft markers (<!-- craft:begin claude-md --> / <!-- craft:end claude-md -->) first.`,
46
+ );
47
+ }
48
+ // Resolved BEFORE CLAUDE.md is written, because it decides how CLAUDE.md is
49
+ // rendered: the compacted block names unwaived craft and process rules by id
50
+ // only, which is sound exactly when HOW_I_BUILD_VIDEOS.md is there to look an
51
+ // id up in. A target without it gets the full claims instead - an id index
52
+ // pointing at an absent document is worse than the block it replaced.
53
+ const howPath = join(targetRoot, "HOW_I_BUILD_VIDEOS.md");
54
+ const hasHow = existsSync(howPath);
55
+
56
+ writeFileSync(
57
+ claudePath,
58
+ spliceBlock(readFileSync(claudePath, "utf8"), "claude-md", renderClaudeMdBlock(RULES, { compact: hasHow })),
59
+ "utf8",
60
+ );
61
+ updated.push("CLAUDE.md");
62
+
63
+ if (opts?.claudeMdOnly) {
64
+ return { updated, skipped };
65
+ }
66
+
67
+ if (!hasHow) {
68
+ skipped.push({ file: "HOW_I_BUILD_VIDEOS.md", reason: "not present in target" });
69
+ } else {
70
+ writeFileSync(
71
+ howPath,
72
+ spliceBlock(readFileSync(howPath, "utf8"), "pipeline", renderPipelineBlock(RULES)),
73
+ "utf8",
74
+ );
75
+ updated.push("HOW_I_BUILD_VIDEOS.md");
76
+ }
77
+
78
+ return { updated, skipped };
79
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { componentSlugs, renderComponentMeta } from "./projectMeta";
3
+ import { RULES } from "./registry";
4
+ import type { Rule } from "./types";
5
+
6
+ const rule = (id: string, field: "whenToUse" | "whenNotToUse" | "gotcha", claim: string): Rule => ({
7
+ id,
8
+ claim,
9
+ why: "because",
10
+ learnedFrom: "HOW_I_BUILD_VIDEOS.md",
11
+ tier: "craft",
12
+ scope: { kind: "component", slug: "demo", field },
13
+ enforcement: { kind: "prose", unanchorable: "test fixture" },
14
+ });
15
+
16
+ describe("componentSlugs", () => {
17
+ test("lists each component once, sorted", () => {
18
+ const rules = [rule("b", "gotcha", "B."), rule("a", "whenToUse", "A.")];
19
+ expect(componentSlugs(rules)).toEqual(["demo"]);
20
+ });
21
+
22
+ test("ignores pipeline rules", () => {
23
+ expect(componentSlugs(RULES.filter((r) => r.scope.kind === "pipeline"))).toEqual([]);
24
+ });
25
+ });
26
+
27
+ describe("renderComponentMeta", () => {
28
+ const text = () => renderComponentMeta(
29
+ [
30
+ rule("use", "whenToUse", "Use it for counters."),
31
+ rule("avoid", "whenNotToUse", "Not for text."),
32
+ rule("g1", "gotcha", "Wrap it."),
33
+ rule("g2", "gotcha", "Use speed."),
34
+ ],
35
+ "demo",
36
+ );
37
+
38
+ test("marks the file as generated and names its source", () => {
39
+ expect(text()).toContain("GENERATED FILE - DO NOT EDIT");
40
+ expect(text()).toContain("packages/video-craft");
41
+ });
42
+
43
+ test("emits the ComponentMeta fields WS1 expects", () => {
44
+ const out = text();
45
+ expect(out).toContain('whenToUse: "Use it for counters."');
46
+ expect(out).toContain('whenNotToUse: "Not for text."');
47
+ expect(out).toContain('gotchas: [');
48
+ expect(out).toContain('"Wrap it."');
49
+ expect(out).toContain('"Use speed."');
50
+ expect(out).toContain("variants: []");
51
+ });
52
+
53
+ test("projects COMPONENT_VARIANTS for a declared Tier A slug", () => {
54
+ const out = renderComponentMeta(
55
+ [rule("use", "whenToUse", "Use it for counters.")],
56
+ "rolling-number",
57
+ );
58
+ expect(out).toContain('variants: ["odometer", "slam"] as string[]');
59
+ });
60
+
61
+ test("emits empty strings when a component has no whenToUse or whenNotToUse", () => {
62
+ const out = renderComponentMeta([rule("g1", "gotcha", "Wrap it.")], "demo");
63
+ expect(out).toContain('whenToUse: ""');
64
+ expect(out).toContain('whenNotToUse: ""');
65
+ });
66
+
67
+ test("imports nothing, so it cannot couple the kit to this package", () => {
68
+ expect(text()).not.toContain("import ");
69
+ });
70
+
71
+ test("orders gotchas by rule id so regeneration is stable", () => {
72
+ const out = text();
73
+ expect(out.indexOf('"Wrap it."')).toBeLessThan(out.indexOf('"Use speed."'));
74
+ });
75
+ });
@@ -0,0 +1,53 @@
1
+ import type { Rule } from "./types";
2
+ import { COMPONENT_VARIANTS } from "./variants";
3
+
4
+ const forComponent = (rules: Rule[], slug: string, field: "whenToUse" | "whenNotToUse" | "gotcha") =>
5
+ rules
6
+ .filter((r) => r.scope.kind === "component" && r.scope.slug === slug && r.scope.field === field)
7
+ // Sorted by id, not by registry order, so regeneration is stable under
8
+ // registry reordering and the drift test does not fire spuriously.
9
+ .sort((a, b) => a.id.localeCompare(b.id))
10
+ .map((r) => r.claim);
11
+
12
+ export function componentSlugs(rules: Rule[]): string[] {
13
+ const slugs = new Set<string>();
14
+ for (const rule of rules) {
15
+ if (rule.scope.kind === "component") slugs.add(rule.scope.slug);
16
+ }
17
+ return [...slugs].sort();
18
+ }
19
+
20
+ /** A TS string literal with quotes and backslashes escaped. */
21
+ const lit = (s: string) => JSON.stringify(s);
22
+
23
+ /**
24
+ * Renders a complete `.meta.ts` file. The output imports nothing: WS1's
25
+ * ComponentMeta type does not exist yet, and the kit must not depend on this
26
+ * package. WS1's registry imports this object and TypeScript checks it
27
+ * structurally.
28
+ */
29
+ export function renderComponentMeta(rules: Rule[], slug: string): string {
30
+ const whenToUse = forComponent(rules, slug, "whenToUse")[0] ?? "";
31
+ const whenNotToUse = forComponent(rules, slug, "whenNotToUse")[0] ?? "";
32
+ const gotchas = forComponent(rules, slug, "gotcha");
33
+
34
+ const name = slug.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + "Meta";
35
+ const variants = COMPONENT_VARIANTS[slug] ?? [];
36
+
37
+ return [
38
+ `// GENERATED FILE - DO NOT EDIT.`,
39
+ `// Source of truth: packages/video-craft/src/rules/. Regenerate with:`,
40
+ `// npx tsx packages/video-craft/scripts/generate.ts`,
41
+ `// Edits here are reverted by the drift test in packages/video-craft/src/drift.test.ts.`,
42
+ ``,
43
+ `export const ${name} = {`,
44
+ ` whenToUse: ${lit(whenToUse)},`,
45
+ ` whenNotToUse: ${lit(whenNotToUse)},`,
46
+ ` gotchas: [`,
47
+ ...gotchas.map((g) => ` ${lit(g)},`),
48
+ ` ],`,
49
+ ` variants: [${variants.map((v) => lit(v)).join(", ")}] as string[],`,
50
+ `};`,
51
+ ``,
52
+ ].join("\n");
53
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * This repo was extracted from remotion-videos: the toolchain moved here, the
3
+ * video projects and the pre-split history stayed behind. Rules whose
4
+ * provenance (learnedFrom, prose anchors, waiver records) points into one of
5
+ * the roots below is citing the upstream repo, where those files still live
6
+ * with their full history. Such paths are validated as citations - format
7
+ * only - instead of being resolved on this repo's disk.
8
+ *
9
+ * qse-qguard-video/ and radar-fixture-video/ moved WITH the toolchain and are
10
+ * deliberately absent from this list: paths into them must still resolve
11
+ * locally, and a missing one is still a failure.
12
+ *
13
+ * docs/superpowers/ and docs/plans/ were on this list until the design record
14
+ * followed the code across. They are absent for the same reason: the specs and
15
+ * plans that rules cite as learnedFrom now live here, so a citation into them
16
+ * is resolved on disk instead of merely format-checked. Adding a root back
17
+ * silently downgrades every citation under it from "the file exists" to "the
18
+ * string is shaped like a path" - only do it for a tree that genuinely stayed
19
+ * upstream.
20
+ */
21
+ export const UPSTREAM_PROVENANCE_ROOTS = [
22
+ // Video projects that stayed in remotion-videos.
23
+ "revola-video",
24
+ "expanse-video",
25
+ "qse-video",
26
+ "act854-video",
27
+ "datapeak-video",
28
+ "klaviyo-segments-video",
29
+ "qpa-video",
30
+ "reverence-video",
31
+ "enjamb-teaser",
32
+ "workday-harness-video",
33
+ "rcp-fund7-video",
34
+ // Pre-split working documents that stayed with the video projects.
35
+ "claude-sessions-digest",
36
+ ] as const;
37
+
38
+ /**
39
+ * The bd id prefix of the upstream repo. Source comments cite issues by id, and
40
+ * the migration deliberately brought only the OPEN issues across - so a comment
41
+ * naming an issue that was closed upstream has no record here, and that is not
42
+ * a broken citation.
43
+ *
44
+ * Those citations are not broken: they point at a real, readable record in
45
+ * remotion-videos. They simply cannot be resolved against this repo's DB.
46
+ *
47
+ * They ARE resolved, though - against `.beads/upstream-issue-ids.txt`, a
48
+ * committed snapshot of the upstream backlog, so the check runs in CI without
49
+ * the other repo on disk. An earlier version validated them on FORM instead,
50
+ * with a regex that extractIssueCitations already guaranteed by construction:
51
+ * a tautology that could not fail, standing over exactly the ids nothing else
52
+ * verified. Do not reintroduce a shape check here and call it verification.
53
+ *
54
+ * A `video-toolchain-` id has no such excuse: it is this repo's own work and
55
+ * must resolve locally.
56
+ */
57
+ export const UPSTREAM_ISSUE_PREFIX = "remotion-videos-";
58
+
59
+ /** True when the id names an issue that may live only in the upstream repo. */
60
+ export function isUpstreamIssue(id: string): boolean {
61
+ return id.startsWith(UPSTREAM_ISSUE_PREFIX);
62
+ }
63
+
64
+ /** True when the path cites the upstream remotion-videos repo. */
65
+ export function isUpstreamProvenance(path: string): boolean {
66
+ return UPSTREAM_PROVENANCE_ROOTS.some(
67
+ (root) => path === root || path.startsWith(`${root}/`),
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Format check for an upstream citation. The file cannot be resolved here, so
73
+ * the only guard left is that the citation is a well-formed repo-relative
74
+ * path: a typo that breaks the shape still fails, but existence is upstream's
75
+ * problem. Returns the reason the citation is malformed, or undefined.
76
+ */
77
+ export function upstreamCitationError(path: string): string | undefined {
78
+ if (path.includes("\\")) return "contains a backslash";
79
+ const segments = path.split("/");
80
+ if (segments.some((s) => s === "")) return "has an empty path segment";
81
+ if (segments.some((s) => s === "." || s === "..")) {
82
+ return "contains a relative segment";
83
+ }
84
+ return undefined;
85
+ }
@@ -0,0 +1,34 @@
1
+ [
2
+ "alignment-group",
3
+ "attachment-offset",
4
+ "band-breach",
5
+ "blank-region-hold",
6
+ "brand-provenance",
7
+ "brief-structure",
8
+ "case-system",
9
+ "contract-satisfiable",
10
+ "copy-tracks-the-read",
11
+ "crowding-legibility",
12
+ "dead-frame-at-cut",
13
+ "edge-touch",
14
+ "fade-stack-at-cut",
15
+ "frame-count",
16
+ "group-member-count",
17
+ "hero-regression",
18
+ "hero-size",
19
+ "loudness",
20
+ "occlusion",
21
+ "placeholder-content",
22
+ "rebanding-conflict",
23
+ "scene-serves-thesis",
24
+ "seam-continuity",
25
+ "seam-fidelity",
26
+ "sibling-overlap",
27
+ "snap",
28
+ "static-hold",
29
+ "tag-coverage",
30
+ "text-fit",
31
+ "thesis-fidelity",
32
+ "viz-declared",
33
+ "vo-element-sync"
34
+ ]