@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.
- package/README.md +56 -0
- package/dist/anchors.js +69 -0
- package/dist/corpus.js +91 -0
- package/dist/projectDoc.js +83 -0
- package/dist/projectDocSync.js +39 -0
- package/dist/projectMeta.js +37 -0
- package/dist/provenance.js +41 -0
- package/dist/radar-check-ids.json +34 -0
- package/dist/registry.js +307 -0
- package/dist/rules/animatedBarChartTightBox.js +15 -0
- package/dist/rules/animatedBarChartWhenNotToUse.js +15 -0
- package/dist/rules/animatedBarChartWhenToUse.js +15 -0
- package/dist/rules/animatedLineChartWhenNotToUse.js +15 -0
- package/dist/rules/animatedLineChartWhenToUse.js +15 -0
- package/dist/rules/approvedSfxPaletteOnly.js +15 -0
- package/dist/rules/atmosphereGradientsFadeAtBothEnds.js +15 -0
- package/dist/rules/audioIsTheClock.js +16 -0
- package/dist/rules/beatsDeriveFromTheirOwnSequence.js +15 -0
- package/dist/rules/bestSceneSetsTheBar.js +15 -0
- package/dist/rules/bumpBeforeRefreeze.js +15 -0
- package/dist/rules/cameraIsTheNarrator.js +15 -0
- package/dist/rules/checkPinBeforeShip.js +16 -0
- package/dist/rules/coldViewerClarity.js +15 -0
- package/dist/rules/commitOrCutEffects.js +29 -0
- package/dist/rules/convertFramesAcrossClockBoundary.js +15 -0
- package/dist/rules/crossCutElementsLiveInTheHudLayer.js +15 -0
- package/dist/rules/cursorClicksRealControls.js +15 -0
- package/dist/rules/cutOnRealAudioPauses.js +15 -0
- package/dist/rules/darkButNeverDim.js +15 -0
- package/dist/rules/dataRadarTagsDeclared.js +193 -0
- package/dist/rules/declareAVerticalZoneMap.js +15 -0
- package/dist/rules/deleteRejectedRigsCompletely.js +15 -0
- package/dist/rules/deriveSeamsDontMeasure.js +17 -0
- package/dist/rules/deterministicSceneSource.js +65 -0
- package/dist/rules/diegeticSoundForOnscreenActions.js +15 -0
- package/dist/rules/distinctTransitionPerBeat.js +15 -0
- package/dist/rules/downloadOrDrawRichAssets.js +15 -0
- package/dist/rules/elementOnscreenForItsVoSentence.js +17 -0
- package/dist/rules/emitBuildNotes.js +15 -0
- package/dist/rules/energyFromTimingNotPartCount.js +15 -0
- package/dist/rules/evidenceBeforeClaimingFixed.js +15 -0
- package/dist/rules/exactPeerLockstep.js +15 -0
- package/dist/rules/firstPassIsADraft.js +15 -0
- package/dist/rules/fixCrowdingByRebandingNotNudging.js +15 -0
- package/dist/rules/fixesPreserveApprovedElements.js +15 -0
- package/dist/rules/floatingNotBoxed.js +15 -0
- package/dist/rules/focusBeatsRecedeTheContext.js +15 -0
- package/dist/rules/fontVarsBrandRemocn.js +15 -0
- package/dist/rules/geometricSeamsNotCrossfades.js +23 -0
- package/dist/rules/geometrySidecarExportsTyped.js +36 -0
- package/dist/rules/glowIsAFocalAccent.js +22 -0
- package/dist/rules/heroFillsTheFrame.js +17 -0
- package/dist/rules/htmlMasksForAnimatedCounters.js +15 -0
- package/dist/rules/impactStackOnPayoffs.js +15 -0
- package/dist/rules/interactiveTargetsAreDeclaredData.js +15 -0
- package/dist/rules/interpolateLiteralKeysMonotonic.js +79 -0
- package/dist/rules/latenessIsStructuralOverlapTheBeats.js +15 -0
- package/dist/rules/lessTextMoreVisualizations.js +15 -0
- package/dist/rules/liveLinkDuplicateReactAlias.js +15 -0
- package/dist/rules/make2dElements3d.js +15 -0
- package/dist/rules/masterMixLoudnessTarget.js +17 -0
- package/dist/rules/measureAlignmentDontEyeball.js +15 -0
- package/dist/rules/motionCarriesWeightAndMeaning.js +15 -0
- package/dist/rules/motionDensityFloor.js +15 -0
- package/dist/rules/neverEditTierBVendored.js +15 -0
- package/dist/rules/neverRestateDerivedFrameNumbersInComments.js +15 -0
- package/dist/rules/noAiGradientLook.js +15 -0
- package/dist/rules/noColoredFlashToHideACut.js +22 -0
- package/dist/rules/noDoubleFrameShift.js +126 -0
- package/dist/rules/noDuplicateMotionPrimitives.js +15 -0
- package/dist/rules/noLinearMotion.js +22 -0
- package/dist/rules/noNestedSvgFilters.js +93 -0
- package/dist/rules/noRepeatedAssetAcrossShots.js +15 -0
- package/dist/rules/noShorthandWithUndefinedLonghand.js +93 -0
- package/dist/rules/noStackedFadesAtACut.js +24 -0
- package/dist/rules/noStaticHolds.js +17 -0
- package/dist/rules/occlusionVisibleElements.js +17 -0
- package/dist/rules/oneContinuousVoTrack.js +15 -0
- package/dist/rules/onePunchPerMomentClass.js +15 -0
- package/dist/rules/oneRenderPerRound.js +15 -0
- package/dist/rules/openStaticAfterAHardCut.js +22 -0
- package/dist/rules/openingsAndClosingsCarryMore.js +15 -0
- package/dist/rules/persistentDeviceFrames.js +15 -0
- package/dist/rules/pixelDiffAnEmptyBandFirst.js +15 -0
- package/dist/rules/planIsPublishedNotGating.js +15 -0
- package/dist/rules/poweredByRevolaWatermark.js +15 -0
- package/dist/rules/predrawContentPresentAtTheCut.js +17 -0
- package/dist/rules/premiumSurfacesNotFlatBorders.js +15 -0
- package/dist/rules/radarDispositionsAreHumanOnly.js +17 -0
- package/dist/rules/realAssetsNotPlaceholders.js +15 -0
- package/dist/rules/rebuildUiFromReconNotScreenshots.js +15 -0
- package/dist/rules/reconStrandsRunInParallel.js +15 -0
- package/dist/rules/registryBeforeHandrolling.js +15 -0
- package/dist/rules/remapRateEndpointFormula.js +15 -0
- package/dist/rules/remocnContentTailwindChrome.js +15 -0
- package/dist/rules/retimeOnlyViaPlaybackOrDesignTokens.js +15 -0
- package/dist/rules/reviewTheRenderByFrameSweep.js +15 -0
- package/dist/rules/rollingNumberBoundedSequence.js +17 -0
- package/dist/rules/rollingNumberSequenceLayoutNone.js +16 -0
- package/dist/rules/rollingNumberTightBoxAlignment.js +15 -0
- package/dist/rules/rollingNumberWhenNotToUse.js +17 -0
- package/dist/rules/rollingNumberWhenToUse.js +17 -0
- package/dist/rules/sameFamilySameProject.js +15 -0
- package/dist/rules/sampleBrandFromProduct.js +15 -0
- package/dist/rules/sampleVelocityFromPreviousFramePosition.js +15 -0
- package/dist/rules/sceneRootTagDeclared.js +41 -0
- package/dist/rules/scrubDemoDataToGenericRoles.js +15 -0
- package/dist/rules/selfReviewThePlanForGaps.js +15 -0
- package/dist/rules/sourceArchivedBeforeBrief.js +17 -0
- package/dist/rules/statisticsSelfExplanatory.js +15 -0
- package/dist/rules/stopsAsRelativeSpacing.js +15 -0
- package/dist/rules/theWholeCanvasWorks.js +15 -0
- package/dist/rules/themePerSection.js +22 -0
- package/dist/rules/thesisDeclaredInBrief.js +17 -0
- package/dist/rules/tierARequiresPropUnreachableEdit.js +15 -0
- package/dist/rules/tinyBodyCopyAsRuledLines.js +15 -0
- package/dist/rules/titlesPinnedOutsideCameraPush.js +15 -0
- package/dist/rules/tokensNotRawHexes.js +60 -0
- package/dist/rules/typeTokenDefaultedParamsAsString.js +15 -0
- package/dist/rules/varyKitChoreographyAcrossSections.js +15 -0
- package/dist/rules/varyTheVisualizationGrammar.js +15 -0
- package/dist/rules/verifyCheapRenderDear.js +15 -0
- package/dist/rules/voAddressesTheViewer.js +15 -0
- package/dist/rules/voIsAPerformance.js +15 -0
- package/dist/rules/voScriptLocksSpokenWords.js +15 -0
- package/dist/rules/wrappersPreserveChildGeometry.js +15 -0
- package/dist/sourceCheck.js +47 -0
- package/dist/sourceCitations.js +68 -0
- package/dist/stageProjection.js +69 -0
- package/dist/stages.js +163 -0
- package/dist/testSupport.js +6 -0
- package/dist/types.js +0 -0
- package/dist/variants.js +8 -0
- package/dist/waiverProjection.js +20 -0
- package/package.json +32 -0
- package/src/anchors.test.ts +119 -0
- package/src/anchors.ts +110 -0
- package/src/corpus.test.ts +96 -0
- package/src/corpus.ts +149 -0
- package/src/corpusProject.test.ts +103 -0
- package/src/creativeEdge.test.ts +58 -0
- package/src/distBuild.test.ts +86 -0
- package/src/docStatus.test.ts +81 -0
- package/src/drift.test.ts +124 -0
- package/src/fixtures/data-radar-dynamic/Branches.tsx +14 -0
- package/src/fixtures/data-radar-dynamic/plan.fixture.yaml +16 -0
- package/src/fixtures/data-radar-forwarded/Scene.tsx +19 -0
- package/src/fixtures/data-radar-forwarded/plan.fixture.yaml +13 -0
- package/src/fixtures/data-radar-opaque/Scene.tsx +15 -0
- package/src/fixtures/data-radar-opaque/plan.fixture.yaml +10 -0
- package/src/fixtures/data-radar-tags/SceneClean.tsx +10 -0
- package/src/fixtures/data-radar-tags/SceneNullBinding.tsx +12 -0
- package/src/fixtures/data-radar-tags/SceneWithGap.tsx +11 -0
- package/src/fixtures/data-radar-tags/plan.fixture.yaml +29 -0
- package/src/fixtures/deterministic-scene.tsx +15 -0
- package/src/fixtures/double-shift/aliased.tsx +13 -0
- package/src/fixtures/double-shift/assigned.tsx +13 -0
- package/src/fixtures/double-shift/clean.tsx +19 -0
- package/src/fixtures/double-shift/destructured.tsx +12 -0
- package/src/fixtures/double-shift/elementAccess.tsx +12 -0
- package/src/fixtures/double-shift/layout.ts +24 -0
- package/src/fixtures/double-shift/mechanism-prefix.tsx.txt +458 -0
- package/src/fixtures/double-shift/wrapped.tsx +15 -0
- package/src/fixtures/geometry-sidecar-missing-export.ts +10 -0
- package/src/fixtures/geometry-sidecar-missing-type.ts +7 -0
- package/src/fixtures/geometry-sidecar-valid.ts +6 -0
- package/src/fixtures/interpolate-ascending.tsx +6 -0
- package/src/fixtures/interpolate-descending.tsx +8 -0
- package/src/fixtures/interpolate-identifier-keys.tsx +9 -0
- package/src/fixtures/nested-filters-broken.tsx +11 -0
- package/src/fixtures/nested-filters-fixed.tsx +8 -0
- package/src/fixtures/nested-filters-html-wrapper.tsx +14 -0
- package/src/fixtures/nested-filters-html.tsx +7 -0
- package/src/fixtures/nested-filters-siblings.tsx +12 -0
- package/src/fixtures/nondeterministic-scene.tsx +14 -0
- package/src/fixtures/nondeterministic-variants.tsx +16 -0
- package/src/fixtures/raw-hex-scene.tsx +10 -0
- package/src/fixtures/scene-root-declared/SceneHook.tsx +9 -0
- package/src/fixtures/scene-root-declared/plan.fixture.yaml +10 -0
- package/src/fixtures/scene-root-missing/SceneHook.tsx +11 -0
- package/src/fixtures/scene-root-missing/plan.fixture.yaml +10 -0
- package/src/fixtures/scene-root-no-names/SceneHook.tsx +6 -0
- package/src/fixtures/scene-root-no-names/plan.fixture.yaml +6 -0
- package/src/fixtures/scene-root-shell/SceneHook.tsx +8 -0
- package/src/fixtures/scene-root-shell/Shell.tsx +9 -0
- package/src/fixtures/scene-root-shell/plan.fixture.yaml +10 -0
- package/src/fixtures/shorthand-undefined-clean.tsx +23 -0
- package/src/fixtures/shorthand-undefined-longhand.tsx +17 -0
- package/src/fixtures/token-scene.tsx +9 -0
- package/src/includeCoverage.test.ts +176 -0
- package/src/learnedFrom.test.ts +32 -0
- package/src/projectDoc.test.ts +230 -0
- package/src/projectDoc.ts +152 -0
- package/src/projectDocSync.test.ts +152 -0
- package/src/projectDocSync.ts +79 -0
- package/src/projectMeta.test.ts +75 -0
- package/src/projectMeta.ts +53 -0
- package/src/provenance.ts +85 -0
- package/src/radar-check-ids.json +34 -0
- package/src/registry.test.ts +207 -0
- package/src/registry.ts +352 -0
- package/src/rules/animatedBarChartTightBox.ts +14 -0
- package/src/rules/animatedBarChartWhenNotToUse.ts +14 -0
- package/src/rules/animatedBarChartWhenToUse.ts +14 -0
- package/src/rules/animatedLineChartWhenNotToUse.ts +14 -0
- package/src/rules/animatedLineChartWhenToUse.ts +14 -0
- package/src/rules/approvedSfxPaletteOnly.ts +23 -0
- package/src/rules/atmosphereGradientsFadeAtBothEnds.ts +21 -0
- package/src/rules/audioIsTheClock.ts +22 -0
- package/src/rules/beatsDeriveFromTheirOwnSequence.ts +21 -0
- package/src/rules/bestSceneSetsTheBar.ts +21 -0
- package/src/rules/bumpBeforeRefreeze.ts +20 -0
- package/src/rules/cameraIsTheNarrator.ts +21 -0
- package/src/rules/checkPinBeforeShip.ts +22 -0
- package/src/rules/coldViewerClarity.ts +21 -0
- package/src/rules/commitOrCutEffects.ts +36 -0
- package/src/rules/convertFramesAcrossClockBoundary.ts +19 -0
- package/src/rules/crossCutElementsLiveInTheHudLayer.ts +21 -0
- package/src/rules/cursorClicksRealControls.ts +22 -0
- package/src/rules/cutOnRealAudioPauses.ts +20 -0
- package/src/rules/darkButNeverDim.ts +21 -0
- package/src/rules/dataRadarTagsDeclared.test.ts +60 -0
- package/src/rules/dataRadarTagsDeclared.ts +341 -0
- package/src/rules/declareAVerticalZoneMap.ts +19 -0
- package/src/rules/deleteRejectedRigsCompletely.ts +19 -0
- package/src/rules/deriveSeamsDontMeasure.ts +23 -0
- package/src/rules/deterministicSceneSource.test.ts +58 -0
- package/src/rules/deterministicSceneSource.ts +82 -0
- package/src/rules/diegeticSoundForOnscreenActions.ts +21 -0
- package/src/rules/distinctTransitionPerBeat.ts +21 -0
- package/src/rules/downloadOrDrawRichAssets.ts +22 -0
- package/src/rules/elementOnscreenForItsVoSentence.ts +25 -0
- package/src/rules/emitBuildNotes.ts +19 -0
- package/src/rules/energyFromTimingNotPartCount.ts +20 -0
- package/src/rules/evidenceBeforeClaimingFixed.ts +23 -0
- package/src/rules/exactPeerLockstep.ts +19 -0
- package/src/rules/firstPassIsADraft.ts +20 -0
- package/src/rules/fixCrowdingByRebandingNotNudging.ts +20 -0
- package/src/rules/fixesPreserveApprovedElements.ts +24 -0
- package/src/rules/floatingNotBoxed.ts +21 -0
- package/src/rules/focusBeatsRecedeTheContext.ts +20 -0
- package/src/rules/fontVarsBrandRemocn.ts +20 -0
- package/src/rules/geometricSeamsNotCrossfades.ts +28 -0
- package/src/rules/geometrySidecarExportsTyped.test.ts +37 -0
- package/src/rules/geometrySidecarExportsTyped.ts +59 -0
- package/src/rules/glowIsAFocalAccent.ts +28 -0
- package/src/rules/heroFillsTheFrame.ts +24 -0
- package/src/rules/htmlMasksForAnimatedCounters.ts +21 -0
- package/src/rules/impactStackOnPayoffs.ts +26 -0
- package/src/rules/interactiveTargetsAreDeclaredData.ts +21 -0
- package/src/rules/interpolateLiteralKeysMonotonic.test.ts +49 -0
- package/src/rules/interpolateLiteralKeysMonotonic.ts +98 -0
- package/src/rules/latenessIsStructuralOverlapTheBeats.ts +20 -0
- package/src/rules/lessTextMoreVisualizations.ts +22 -0
- package/src/rules/liveLinkDuplicateReactAlias.ts +22 -0
- package/src/rules/make2dElements3d.ts +21 -0
- package/src/rules/masterMixLoudnessTarget.ts +22 -0
- package/src/rules/measureAlignmentDontEyeball.ts +20 -0
- package/src/rules/motionCarriesWeightAndMeaning.ts +21 -0
- package/src/rules/motionDensityFloor.ts +20 -0
- package/src/rules/neverEditTierBVendored.ts +20 -0
- package/src/rules/neverRestateDerivedFrameNumbersInComments.ts +20 -0
- package/src/rules/noAiGradientLook.ts +22 -0
- package/src/rules/noColoredFlashToHideACut.ts +29 -0
- package/src/rules/noDoubleFrameShift.test.ts +95 -0
- package/src/rules/noDoubleFrameShift.ts +193 -0
- package/src/rules/noDuplicateMotionPrimitives.ts +19 -0
- package/src/rules/noLinearMotion.ts +29 -0
- package/src/rules/noNestedSvgFilters.test.ts +47 -0
- package/src/rules/noNestedSvgFilters.ts +138 -0
- package/src/rules/noRepeatedAssetAcrossShots.ts +19 -0
- package/src/rules/noShorthandWithUndefinedLonghand.test.ts +35 -0
- package/src/rules/noShorthandWithUndefinedLonghand.ts +140 -0
- package/src/rules/noStackedFadesAtACut.ts +30 -0
- package/src/rules/noStaticHolds.ts +24 -0
- package/src/rules/occlusionVisibleElements.ts +26 -0
- package/src/rules/oneContinuousVoTrack.ts +20 -0
- package/src/rules/onePunchPerMomentClass.ts +20 -0
- package/src/rules/oneRenderPerRound.ts +46 -0
- package/src/rules/openStaticAfterAHardCut.ts +29 -0
- package/src/rules/openingsAndClosingsCarryMore.ts +21 -0
- package/src/rules/persistentDeviceFrames.ts +20 -0
- package/src/rules/pixelDiffAnEmptyBandFirst.ts +20 -0
- package/src/rules/planIsPublishedNotGating.ts +32 -0
- package/src/rules/poweredByRevolaWatermark.ts +19 -0
- package/src/rules/predrawContentPresentAtTheCut.ts +21 -0
- package/src/rules/premiumSurfacesNotFlatBorders.ts +20 -0
- package/src/rules/radarDispositionsAreHumanOnly.ts +28 -0
- package/src/rules/realAssetsNotPlaceholders.ts +21 -0
- package/src/rules/rebuildUiFromReconNotScreenshots.ts +21 -0
- package/src/rules/reconStrandsRunInParallel.ts +63 -0
- package/src/rules/registryBeforeHandrolling.ts +20 -0
- package/src/rules/remapRateEndpointFormula.ts +19 -0
- package/src/rules/remocnContentTailwindChrome.ts +19 -0
- package/src/rules/retimeOnlyViaPlaybackOrDesignTokens.ts +18 -0
- package/src/rules/reviewTheRenderByFrameSweep.ts +23 -0
- package/src/rules/rollingNumberBoundedSequence.ts +21 -0
- package/src/rules/rollingNumberSequenceLayoutNone.ts +22 -0
- package/src/rules/rollingNumberTightBoxAlignment.ts +19 -0
- package/src/rules/rollingNumberWhenNotToUse.ts +24 -0
- package/src/rules/rollingNumberWhenToUse.ts +23 -0
- package/src/rules/sameFamilySameProject.ts +20 -0
- package/src/rules/sampleBrandFromProduct.ts +19 -0
- package/src/rules/sampleVelocityFromPreviousFramePosition.ts +21 -0
- package/src/rules/sceneRootTagDeclared.test.ts +43 -0
- package/src/rules/sceneRootTagDeclared.ts +78 -0
- package/src/rules/scrubDemoDataToGenericRoles.ts +19 -0
- package/src/rules/selfReviewThePlanForGaps.ts +47 -0
- package/src/rules/sourceArchivedBeforeBrief.ts +24 -0
- package/src/rules/statisticsSelfExplanatory.ts +18 -0
- package/src/rules/stopsAsRelativeSpacing.ts +21 -0
- package/src/rules/theWholeCanvasWorks.ts +21 -0
- package/src/rules/themePerSection.ts +28 -0
- package/src/rules/thesisDeclaredInBrief.ts +25 -0
- package/src/rules/tierARequiresPropUnreachableEdit.ts +20 -0
- package/src/rules/tinyBodyCopyAsRuledLines.ts +19 -0
- package/src/rules/titlesPinnedOutsideCameraPush.ts +22 -0
- package/src/rules/tokensNotRawHexes.test.ts +51 -0
- package/src/rules/tokensNotRawHexes.ts +89 -0
- package/src/rules/typeTokenDefaultedParamsAsString.ts +21 -0
- package/src/rules/varyKitChoreographyAcrossSections.ts +21 -0
- package/src/rules/varyTheVisualizationGrammar.ts +22 -0
- package/src/rules/verifyCheapRenderDear.ts +18 -0
- package/src/rules/voAddressesTheViewer.ts +18 -0
- package/src/rules/voIsAPerformance.ts +21 -0
- package/src/rules/voScriptLocksSpokenWords.ts +21 -0
- package/src/rules/wrappersPreserveChildGeometry.ts +22 -0
- package/src/sourceCheck.test.ts +50 -0
- package/src/sourceCheck.ts +55 -0
- package/src/sourceCitations.test.ts +747 -0
- package/src/sourceCitations.ts +217 -0
- package/src/stageProjection.ts +112 -0
- package/src/stages.test.ts +68 -0
- package/src/stages.ts +207 -0
- package/src/testSupport.ts +16 -0
- package/src/types.ts +181 -0
- package/src/variants.test.ts +24 -0
- package/src/variants.ts +13 -0
- package/src/waiverProjection.test.ts +59 -0
- package/src/waiverProjection.ts +45 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const heroFillsTheFrame = {
|
|
2
|
+
id: "hero-fills-the-frame",
|
|
3
|
+
claim: "The hero element of a shot fills roughly two thirds of the frame or more, legible at 1080p; a hero occupying a third of the screen is a defect, and when enlarging, overshoot rather than nudge.",
|
|
4
|
+
why: "Undersized heroes were flagged in five separate rounds across three videos, and every timid enlargement needed a second round; 'play big' is the single most repeated scale note in the corpus.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/revola-usecase-win-booth.md",
|
|
6
|
+
tier: "gate",
|
|
7
|
+
verify: "`video radar:scene --project <dir> --comp <id> --check hero-size`",
|
|
8
|
+
owner: "hero-size",
|
|
9
|
+
scope: { kind: "pipeline" },
|
|
10
|
+
enforcement: {
|
|
11
|
+
kind: "prose",
|
|
12
|
+
unanchorable: "The rule is a composition judgment per shot; no single code location expresses it."
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
heroFillsTheFrame
|
|
17
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const htmlMasksForAnimatedCounters = {
|
|
2
|
+
id: "html-masks-for-animated-counters",
|
|
3
|
+
claim: "Build animated sliding-mask counters (odometers, digit rolls) in HTML/CSS rather than SVG clipPath; animated clipPath geometry is unreliable in headless renders.",
|
|
4
|
+
why: "The S3 odometer built on SVG clipPath rendered inconsistently in headless Chrome and had to be rebuilt in HTML; static clipPath (e.g. InkFill) remains fine.",
|
|
5
|
+
learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "revola-video/src/scenes/craft/S3Line.tsx" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
htmlMasksForAnimatedCounters
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const impactStackOnPayoffs = {
|
|
2
|
+
id: "impact-stack-on-payoffs",
|
|
3
|
+
claim: "Land each payoff with several simultaneous signals on the same frame - ring pulse, scale punch, light sweep, one approved foley hit - and vary which signals each class of payoff uses.",
|
|
4
|
+
why: "A single fade or count-up reads as a value changing; the stacked signal reads as a moment landing, and varying the stack per payoff class is what keeps four payoffs in one film from reading as the same template beat.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/product-room-video.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "A choreography discipline composed per beat from several primitives; it has no single owning symbol."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
impactStackOnPayoffs
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const interactiveTargetsAreDeclaredData = {
|
|
2
|
+
id: "interactive-targets-are-declared-data",
|
|
3
|
+
claim: "A control the cursor clicks and the cursor path that clicks it must read the same declared coordinate; if a target is measured off a render instead, record the measurement and re-verify it after any layout change.",
|
|
4
|
+
why: "Cursor targets positioned by eye drifted out from under their controls as the layout changed - one click landed 122px right and 155px below its button while the button lit up on its own - and nothing in the build could detect it.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/product-room-video.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "revola-video/src/components/Cursor.tsx" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
interactiveTargetsAreDeclaredData
|
|
15
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
|
+
function literalValue(element) {
|
|
3
|
+
if (Node.isNumericLiteral(element)) return element.getLiteralValue();
|
|
4
|
+
if (Node.isPrefixUnaryExpression(element) && element.getOperatorToken() === SyntaxKind.MinusToken) {
|
|
5
|
+
const operand = element.getOperand();
|
|
6
|
+
if (Node.isNumericLiteral(operand)) return -operand.getLiteralValue();
|
|
7
|
+
}
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
const interpolateLiteralKeysMonotonic = {
|
|
11
|
+
id: "interpolate-literal-keys-monotonic",
|
|
12
|
+
claim: "A literal input-range array passed to interpolate() must be strictly ascending.",
|
|
13
|
+
why: "interpolate() throws at runtime on a non-monotonic range; a literal non-ascending array is a latent crash that renders fine until that frame is reached. The HERO_AT(134)/TILT_END(136) retime would have thrown.",
|
|
14
|
+
learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
|
|
15
|
+
tier: "gate",
|
|
16
|
+
verify: "`vitest run packages/video-craft` - machine-checked over scene and kit source",
|
|
17
|
+
scope: { kind: "pipeline" },
|
|
18
|
+
enforcement: {
|
|
19
|
+
kind: "source",
|
|
20
|
+
check: {
|
|
21
|
+
// Unlike the JSX-only checks, this one also scans plain .ts/.js:
|
|
22
|
+
// interpolate() lives in non-JSX helpers feeding scenes
|
|
23
|
+
// (spec-videos/src/lib/transitions.ts, packages/video-kit/src/react/
|
|
24
|
+
// motion.ts, qse-investor-video/03_scenes/_lib/motion.js), and a
|
|
25
|
+
// non-monotonic literal range crashes wherever it is written.
|
|
26
|
+
include: [
|
|
27
|
+
"qpa-video/03_scenes/**/*.{js,jsx,ts,tsx}",
|
|
28
|
+
"reverence-video/src/scenes/**/*.{js,jsx,ts,tsx}",
|
|
29
|
+
"workday-dple-video/src/scenes/**/*.{js,jsx,ts,tsx}",
|
|
30
|
+
"revola-video/src/scenes/**/*.{js,jsx,ts,tsx}",
|
|
31
|
+
"datapeak-video/**/*.{js,jsx,ts,tsx}",
|
|
32
|
+
"datapeak-usecases/**/*.{js,jsx,ts,tsx}",
|
|
33
|
+
"qse-video/**/*.{js,jsx,ts,tsx}",
|
|
34
|
+
"qse-investor-video/**/*.{js,jsx,ts,tsx}",
|
|
35
|
+
"qse-qguard-video/src/**/*.{ts,tsx}",
|
|
36
|
+
"rcp-fund7-video/src/**/*.{ts,tsx}",
|
|
37
|
+
"expanse-video/src/**/*.{ts,tsx}",
|
|
38
|
+
"reverence-video/src/**/*.{ts,tsx}",
|
|
39
|
+
"spec-videos/**/*.{js,jsx,ts,tsx}",
|
|
40
|
+
// Added Task 23: klaviyo-segments-video and radar-fixture-video were
|
|
41
|
+
// silently excluded - the same gap deterministic-scene-source had.
|
|
42
|
+
"klaviyo-segments-video/src/**/*.{js,jsx,ts,tsx}",
|
|
43
|
+
"radar-fixture-video/src/**/*.{js,jsx,ts,tsx}",
|
|
44
|
+
"packages/video-kit/src/**/*.{ts,tsx}"
|
|
45
|
+
],
|
|
46
|
+
run: (file, relPath) => {
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
49
|
+
if (call.getExpression().getText() !== "interpolate") continue;
|
|
50
|
+
const range = call.getArguments()[1];
|
|
51
|
+
if (!range || !Node.isArrayLiteralExpression(range)) continue;
|
|
52
|
+
const values = [];
|
|
53
|
+
let allLiteral = true;
|
|
54
|
+
for (const element of range.getElements()) {
|
|
55
|
+
const value = literalValue(element);
|
|
56
|
+
if (value === void 0) {
|
|
57
|
+
allLiteral = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
values.push(value);
|
|
61
|
+
}
|
|
62
|
+
if (!allLiteral) continue;
|
|
63
|
+
const ascending = values.every((v, i) => i === 0 || values[i - 1] < v);
|
|
64
|
+
if (ascending) continue;
|
|
65
|
+
findings.push({
|
|
66
|
+
file: relPath,
|
|
67
|
+
line: call.getStartLineNumber(),
|
|
68
|
+
match: range.getText().replace(/\s+/g, " ").slice(0, 60),
|
|
69
|
+
message: `interpolate() input range [${values.join(", ")}] is not strictly ascending; interpolate() throws at runtime when this frame range is evaluated.`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export {
|
|
78
|
+
interpolateLiteralKeysMonotonic
|
|
79
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const latenessIsStructuralOverlapTheBeats = {
|
|
2
|
+
id: "lateness-is-structural-overlap-the-beats",
|
|
3
|
+
claim: "When a key element arrives late, check whether the delay is structural (strictly sequential prerequisite beats) before shaving durations; the fix is to overlap beats, not to trim them.",
|
|
4
|
+
why: "Two shave passes (420f to 396f, deck 232 to 196) failed to fix a late deck because the hook, tilt, and field pop all had to finish first; overlapping them fixed it in one pass.",
|
|
5
|
+
learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "revola-video/src/scenes/craft/S1Graveyard.tsx" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
latenessIsStructuralOverlapTheBeats
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const lessTextMoreVisualizations = {
|
|
2
|
+
id: "less-text-more-visualizations",
|
|
3
|
+
claim: "The voiceover carries the information and the screen carries evidence: cut every text element that restates the VO or a neighboring label, replace word-lists with the actual marks and drawn subjects, and keep only text with a job - a name, a number with its denominator, the story spine for sound-off viewing, or one line of orientation copy where a viewer would otherwise be lost.",
|
|
4
|
+
why: "'Less text more visualizations' and 'too boring, no animations, text heavy and bad' recurred across videos - text-on-screen scenes got the PowerPoint verdict - while the one under-explained scene needed orientation copy added back, so comprehension, not word count, is the target.",
|
|
5
|
+
learnedFrom: "docs/style/CREATIVE_EDGE.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "A per-element editorial judgment against the VO script; no single code location expresses it."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
lessTextMoreVisualizations
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const liveLinkDuplicateReactAlias = {
|
|
2
|
+
id: "live-link-duplicate-react-alias",
|
|
3
|
+
claim: "If a live-linked kit render fails with a duplicate React or Remotion instance error, fix it with the webpack alias override in the project's Remotion config, not by abandoning the link.",
|
|
4
|
+
why: "The symlinked kit can resolve a second React or Remotion copy from the monorepo root; without the known fix, the failure looks like a kit bug and costs a debugging session.",
|
|
5
|
+
learnedFrom: "HOW_I_BUILD_VIDEOS.md",
|
|
6
|
+
tier: "process",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "packages/video-cli/src/kitShell.ts", symbol: "linkKit" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
liveLinkDuplicateReactAlias
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const make2dElements3d = {
|
|
2
|
+
id: "make-2d-elements-3d",
|
|
3
|
+
claim: "Give flat hero elements depth: perspective or cabinet projection, parallax planes, rotation in space, zoom-through or orbit camera moves, ground-contact plus long-throw shadows; a flat cutout sitting on a flat ground is an amplify target, and external 3D packages are welcome when they serve the look.",
|
|
4
|
+
why: "'Make 2d elements 3d' and 'more 3D elements too' were standing amplify notes across videos; flat-on-flat heroes read as clipart while projected depth is what earned 'cinematic' in review.",
|
|
5
|
+
learnedFrom: "docs/style/CREATIVE_EDGE.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "A depth-treatment judgment per hero element; no single code location expresses it."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
make2dElements3d
|
|
15
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const masterMixLoudnessTarget = {
|
|
2
|
+
id: "master-mix-loudness-target",
|
|
3
|
+
claim: "Master the final mix to -14 LUFS integrated with a -1 dBTP true-peak limit, and verify with ffmpeg loudnorm before delivery.",
|
|
4
|
+
why: "A -20 LUFS master shipped and was caught only by an external audit; the target is a fixed number, so missing it is pure process failure.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/portfolio-reels.md",
|
|
6
|
+
tier: "gate",
|
|
7
|
+
verify: "`video radar:film --project <dir> --video <mp4> --check loudness`",
|
|
8
|
+
owner: "loudness",
|
|
9
|
+
scope: { kind: "pipeline" },
|
|
10
|
+
enforcement: {
|
|
11
|
+
kind: "prose",
|
|
12
|
+
unanchorable: "The check runs on the rendered file at delivery time; no repo code location expresses it yet."
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
masterMixLoudnessTarget
|
|
17
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const measureAlignmentDontEyeball = {
|
|
2
|
+
id: "measure-alignment-dont-eyeball",
|
|
3
|
+
claim: "Answer an alignment or centering complaint with measured pixels - panel edge versus first content pixel, sibling baselines against each other - never by eye.",
|
|
4
|
+
why: "Eyeballing a downscaled frame hid a 2px content inset where 20px was intended and a 6px spread across three sibling CTAs; both were unarguable once measured, and the measurement also proved the fix.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/product-room-video.md",
|
|
6
|
+
tier: "process",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "A review-time discipline over rendered PNGs; there is no source symbol that can assert it."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
measureAlignmentDontEyeball
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const motionCarriesWeightAndMeaning = {
|
|
2
|
+
id: "motion-carries-weight-and-meaning",
|
|
3
|
+
claim: "Motion is physical and means something: objects obey weight - they fall, land with squash and dust, dock flush with a contact pulse, slam with a punch on settle - and the VO's verbs become literal choreography; ornamental motion with no narrative meaning (revolving for its own sake, dashed rings) gets cut.",
|
|
4
|
+
why: "Weighted, meaningful motion was requested by name ('fall from the sky and hit the ground, with impact and dust') and meaningless ornament was cut by name ('the 4 things do not have to revolve'); motion that neither enacts the script nor obeys physics read as filler.",
|
|
5
|
+
learnedFrom: "docs/style/CREATIVE_EDGE.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "A meaning-per-motion judgment against the VO script; no single code location expresses it."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
motionCarriesWeightAndMeaning
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const motionDensityFloor = {
|
|
2
|
+
id: "motion-density-floor",
|
|
3
|
+
claim: "Hold a visible state change roughly every half second, with two or three beats overlapping through the middle of a section; continuous background drift alone does not clear the bar.",
|
|
4
|
+
why: "A first pass that satisfied the no-static-holds rule with idle drift still came back as 'not much animation, just static screens'; the reference films run about one beat start every 0.6s with beats deliberately overlapping, and that density is what reads as authored.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/product-room-video.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "A pacing property of a whole section, measurable only across its beat schedule."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
motionDensityFloor
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const neverEditTierBVendored = {
|
|
2
|
+
id: "never-edit-tier-b-vendored",
|
|
3
|
+
claim: "Never edit a Tier B vendored remocn source file; theme it only via props and CSS vars.",
|
|
4
|
+
why: "An edit silently moves the component into the port lane and forfeits update-safety: shadcn add can no longer pull upstream fixes without clobbering or conflicting.",
|
|
5
|
+
learnedFrom: "HOW_I_BUILD_VIDEOS.md",
|
|
6
|
+
tier: "process",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "revola-video/src/components/remocn/backdrop.tsx" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
neverEditTierBVendored
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const neverRestateDerivedFrameNumbersInComments = {
|
|
2
|
+
id: "never-restate-derived-frame-numbers-in-comments",
|
|
3
|
+
claim: "Do not hand-copy derived frame values into comments; a comment stating a computed seam or start frame goes stale the moment an upstream duration moves.",
|
|
4
|
+
why: "S_S3's comment said 624 while the computed value was 588, stale from an earlier D_S1, which aimed an entire render batch at the wrong seam.",
|
|
5
|
+
learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "revola-video/src/tokensCraft.ts" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
neverRestateDerivedFrameNumbersInComments
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const noAiGradientLook = {
|
|
2
|
+
id: "no-ai-gradient-look",
|
|
3
|
+
claim: "Never style with generic blue-purple gradient washes, blob shapes, or heavy blur/glow backdrops; backgrounds are a brand-appropriate base plus at most a subtle single-hue aura.",
|
|
4
|
+
why: "Viewers read the blue-purple gradient wash as AI-template output before they can name why; it was rejected in every single video and purple hexes kept creeping back into new code, costing repeated grep-and-purge rounds.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/portfolio-reels.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "packages/video-kit/src/react/theme.tsx" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
noAiGradientLook
|
|
15
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const noColoredFlashToHideACut = {
|
|
2
|
+
id: "no-colored-flash-to-hide-a-cut",
|
|
3
|
+
claim: "Do not hide a cut behind a colored light flash or bloom bridge; the color reads as a wash on the frame regardless of how short you make it.",
|
|
4
|
+
why: "The violet light bridge made the cut genuinely invisible yet was rejected twice, at 40f and again tightened to ~14f, because the problem was the color, not the timing.",
|
|
5
|
+
learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
exceptions: [
|
|
8
|
+
{
|
|
9
|
+
project: "qse-qguard-video",
|
|
10
|
+
decision: "client chose ACT854's transition verbatim for the QGuard restyle, accent flash at the blade's apex included, over the sanctioned white LightSweep",
|
|
11
|
+
recordedIn: "qse-qguard-video/docs/motion-brand-spec.md"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
scope: { kind: "pipeline" },
|
|
15
|
+
enforcement: {
|
|
16
|
+
kind: "prose",
|
|
17
|
+
anchor: { file: "revola-video/src/scenes/craft/craftKit.tsx" }
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
noColoredFlashToHideACut
|
|
22
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
|
+
const noDoubleFrameShift = {
|
|
3
|
+
id: "no-double-frame-shift",
|
|
4
|
+
claim: "Never rebase a scene's clock by its section window's `from`: a Sequence already localizes frames, so a scene module must not read `windowFor(...).from` (aliased, destructured, or wrapped included); layout's cue()/cueEnd() are the sanctioned clock.",
|
|
5
|
+
why: "Build 4's mechanism scene computed `frame - win.from` inside its Sequence, shifting the clock twice; the whole rig rendered 264 frames in the past and nothing failed - the film shipped a round late because only human eyes caught it.",
|
|
6
|
+
learnedFrom: "docs/superpowers/specs/2026-08-13-build-speed-phase-spec.md",
|
|
7
|
+
tier: "gate",
|
|
8
|
+
verify: "`vitest run packages/video-craft` - machine-checked over every scene subtree",
|
|
9
|
+
scope: { kind: "pipeline" },
|
|
10
|
+
enforcement: {
|
|
11
|
+
kind: "source",
|
|
12
|
+
check: {
|
|
13
|
+
// Scene subtrees only, every project (`*` head = wildcard coverage):
|
|
14
|
+
// the rule keys on a layout-module import, so projects without the
|
|
15
|
+
// windowFor pattern are inert by construction. Film-level modules live
|
|
16
|
+
// outside scenes/ and stay exempt; the two composition-clock residents
|
|
17
|
+
// OF scenes/ are excluded by name (see the header comment).
|
|
18
|
+
include: [
|
|
19
|
+
"*/src/scenes/**/*.{js,jsx,ts,tsx}",
|
|
20
|
+
"*/03_scenes/**/*.{js,jsx,ts,tsx}"
|
|
21
|
+
],
|
|
22
|
+
exclude: [
|
|
23
|
+
"*/src/scenes/scene.*.geometry.ts",
|
|
24
|
+
"qse-qguard-video/src/scenes/Chrome.tsx",
|
|
25
|
+
// The film's SFX mixer, not a scene: ReverenceFilm.tsx mounts <Mix /> as
|
|
26
|
+
// a sibling of the SECTION_WINDOWS.map() block, outside every section
|
|
27
|
+
// Sequence, so its `windowFor(id).from + local` IS the composition
|
|
28
|
+
// timeline - the same film-level seam math this rule's header calls
|
|
29
|
+
// legitimate, and the same shape as rcp-fund7's Film.tsx SFX anchors.
|
|
30
|
+
"reverence-video/src/scenes/Mix.tsx"
|
|
31
|
+
],
|
|
32
|
+
run(file, relPath) {
|
|
33
|
+
const windowForNames = /* @__PURE__ */ new Set();
|
|
34
|
+
const windowsNames = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const imp of file.getImportDeclarations()) {
|
|
36
|
+
const specifier = imp.getModuleSpecifierValue();
|
|
37
|
+
if (specifier.split("/").pop() !== "layout") continue;
|
|
38
|
+
for (const named of imp.getNamedImports()) {
|
|
39
|
+
const source = named.getName();
|
|
40
|
+
const local = named.getAliasNode()?.getText() ?? source;
|
|
41
|
+
if (source === "windowFor") windowForNames.add(local);
|
|
42
|
+
if (source === "SECTION_WINDOWS") windowsNames.add(local);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (windowForNames.size === 0 && windowsNames.size === 0) return [];
|
|
46
|
+
const windowVars = /* @__PURE__ */ new Set();
|
|
47
|
+
const unwrap = (node) => {
|
|
48
|
+
let n = node;
|
|
49
|
+
while (Node.isParenthesizedExpression(n) || Node.isAsExpression(n) || Node.isNonNullExpression(n) || Node.isSatisfiesExpression(n)) {
|
|
50
|
+
n = n.getExpression();
|
|
51
|
+
}
|
|
52
|
+
return n;
|
|
53
|
+
};
|
|
54
|
+
const rootIdentifier = (node) => {
|
|
55
|
+
let n = unwrap(node);
|
|
56
|
+
while (Node.isPropertyAccessExpression(n) || Node.isElementAccessExpression(n) || Node.isCallExpression(n)) {
|
|
57
|
+
n = unwrap(n.getExpression());
|
|
58
|
+
}
|
|
59
|
+
return Node.isIdentifier(n) ? n.getText() : void 0;
|
|
60
|
+
};
|
|
61
|
+
const isWindowExpr = (node) => {
|
|
62
|
+
const n = unwrap(node);
|
|
63
|
+
if (Node.isIdentifier(n)) return windowVars.has(n.getText());
|
|
64
|
+
if (Node.isCallExpression(n)) {
|
|
65
|
+
const callee = unwrap(n.getExpression());
|
|
66
|
+
if (Node.isIdentifier(callee) && windowForNames.has(callee.getText())) return true;
|
|
67
|
+
}
|
|
68
|
+
const root = rootIdentifier(n);
|
|
69
|
+
return root !== void 0 && windowsNames.has(root);
|
|
70
|
+
};
|
|
71
|
+
const varDecls = file.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
|
|
72
|
+
const binaryExprs = file.getDescendantsOfKind(SyntaxKind.BinaryExpression);
|
|
73
|
+
for (let grew = true; grew; ) {
|
|
74
|
+
grew = false;
|
|
75
|
+
const taint = (name) => {
|
|
76
|
+
if (Node.isIdentifier(name) && !windowVars.has(name.getText())) {
|
|
77
|
+
windowVars.add(name.getText());
|
|
78
|
+
grew = true;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (const decl of varDecls) {
|
|
82
|
+
const init = decl.getInitializer();
|
|
83
|
+
if (!init || !isWindowExpr(init)) continue;
|
|
84
|
+
taint(decl.getNameNode());
|
|
85
|
+
}
|
|
86
|
+
for (const bin of binaryExprs) {
|
|
87
|
+
if (bin.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) continue;
|
|
88
|
+
if (!isWindowExpr(bin.getRight())) continue;
|
|
89
|
+
taint(unwrap(bin.getLeft()));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const findings = [];
|
|
93
|
+
const report = (node) => {
|
|
94
|
+
findings.push({
|
|
95
|
+
file: relPath,
|
|
96
|
+
line: node.getStartLineNumber(),
|
|
97
|
+
match: node.getText().replace(/\s+/g, " ").slice(0, 60),
|
|
98
|
+
message: "a Sequence already localizes the clock: reading a section window's `from` inside a scene module double-shifts every frame it feeds (build 4's mechanism rendered 264 frames in the past); use layout's cue()/cueEnd() for in-scene beats"
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
for (const access of file.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
|
102
|
+
if (access.getName() !== "from") continue;
|
|
103
|
+
if (isWindowExpr(access.getExpression())) report(access);
|
|
104
|
+
}
|
|
105
|
+
for (const access of file.getDescendantsOfKind(SyntaxKind.ElementAccessExpression)) {
|
|
106
|
+
const arg = unwrap(access.getArgumentExpression() ?? access);
|
|
107
|
+
if (!Node.isStringLiteral(arg) || arg.getLiteralText() !== "from") continue;
|
|
108
|
+
if (isWindowExpr(access.getExpression())) report(access);
|
|
109
|
+
}
|
|
110
|
+
for (const decl of varDecls) {
|
|
111
|
+
const name = decl.getNameNode();
|
|
112
|
+
const init = decl.getInitializer();
|
|
113
|
+
if (!Node.isObjectBindingPattern(name) || !init || !isWindowExpr(init)) continue;
|
|
114
|
+
for (const element of name.getElements()) {
|
|
115
|
+
const property = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
116
|
+
if (property === "from") report(element);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
export {
|
|
125
|
+
noDoubleFrameShift
|
|
126
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const noDuplicateMotionPrimitives = {
|
|
2
|
+
id: "no-duplicate-motion-primitives",
|
|
3
|
+
claim: "Before adding a registry component or porting one, check for an existing hand-built equivalent in the project; never create a second implementation of the same primitive.",
|
|
4
|
+
why: "Two implementations of the same transition drift apart and double every future styling and timing fix - the _lib fork problem at component granularity.",
|
|
5
|
+
learnedFrom: "HOW_I_BUILD_VIDEOS.md",
|
|
6
|
+
tier: "process",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
anchor: { file: "revola-video/src/transitions/WhipPan.tsx" }
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
noDuplicateMotionPrimitives
|
|
15
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const noLinearMotion = {
|
|
2
|
+
id: "no-linear-motion",
|
|
3
|
+
claim: "Animate everything with spring() or eased interpolate(); never linear interpolation for visible motion.",
|
|
4
|
+
why: "Linear motion reads as mechanical and cheap - the doctrine violation viewers perceive even when they cannot name it.",
|
|
5
|
+
learnedFrom: "HOW_I_BUILD_VIDEOS.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
exceptions: [
|
|
8
|
+
{
|
|
9
|
+
project: "qse-qguard-video",
|
|
10
|
+
decision: "ACT854's un-eased win()/camPush() envelopes are ported verbatim at the client's direction; eased variants ship alongside behind a one-line flag",
|
|
11
|
+
recordedIn: "qse-qguard-video/docs/motion-brand-spec.md"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
scope: { kind: "pipeline" },
|
|
15
|
+
enforcement: {
|
|
16
|
+
kind: "prose",
|
|
17
|
+
anchor: { file: "packages/video-kit/src/react/motion.ts" }
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
noLinearMotion
|
|
22
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
|
+
function hasStyleFilter(element) {
|
|
3
|
+
const attributes = Node.isJsxOpeningElement(element) || Node.isJsxSelfClosingElement(element) ? element.getAttributes() : [];
|
|
4
|
+
for (const attribute of attributes) {
|
|
5
|
+
if (!Node.isJsxAttribute(attribute)) continue;
|
|
6
|
+
if (attribute.getNameNode().getText() !== "style") continue;
|
|
7
|
+
const initializer = attribute.getInitializer();
|
|
8
|
+
if (!initializer || !Node.isJsxExpression(initializer)) continue;
|
|
9
|
+
const expression = initializer.getExpression();
|
|
10
|
+
if (!expression) continue;
|
|
11
|
+
const literals = Node.isObjectLiteralExpression(expression) ? [expression] : expression.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
|
|
12
|
+
for (const literal of literals) {
|
|
13
|
+
for (const property of literal.getProperties()) {
|
|
14
|
+
if (Node.isShorthandPropertyAssignment(property) && property.getName() === "filter") return true;
|
|
15
|
+
if (!Node.isPropertyAssignment(property)) continue;
|
|
16
|
+
if (property.getName() !== "filter") continue;
|
|
17
|
+
const value = property.getInitializer()?.getText() ?? "";
|
|
18
|
+
if (value !== `"none"` && value !== `'none'`) return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const ELEMENT_KINDS = [SyntaxKind.JsxOpeningElement, SyntaxKind.JsxSelfClosingElement];
|
|
25
|
+
const noNestedSvgFilters = {
|
|
26
|
+
id: "no-nested-svg-filters",
|
|
27
|
+
claim: "Never nest CSS filters in SVG. Put one flat filter on one layer.",
|
|
28
|
+
why: "Chrome silently drops the children of a CSS-filtered group when those children also carry CSS filters. Nothing errors and nothing warns: the elements simply do not render, which reads as a logic bug and costs a debugging session to localise.",
|
|
29
|
+
learnedFrom: "revola-video/BUILD_NOTES_CRAFT.md",
|
|
30
|
+
tier: "gate",
|
|
31
|
+
verify: "`vitest run packages/video-craft` - machine-checked over scene and kit source",
|
|
32
|
+
scope: { kind: "pipeline" },
|
|
33
|
+
enforcement: {
|
|
34
|
+
kind: "source",
|
|
35
|
+
check: {
|
|
36
|
+
include: [
|
|
37
|
+
"qpa-video/03_scenes/**/*.jsx",
|
|
38
|
+
"reverence-video/src/scenes/**/*.tsx",
|
|
39
|
+
"workday-dple-video/src/scenes/**/*.tsx",
|
|
40
|
+
"revola-video/src/scenes/**/*.tsx",
|
|
41
|
+
"datapeak-video/**/*.{jsx,tsx}",
|
|
42
|
+
"datapeak-usecases/**/*.{jsx,tsx}",
|
|
43
|
+
"qse-video/**/*.{jsx,tsx}",
|
|
44
|
+
"qse-investor-video/**/*.{jsx,tsx}",
|
|
45
|
+
"qse-qguard-video/src/**/*.tsx",
|
|
46
|
+
"rcp-fund7-video/src/**/*.tsx",
|
|
47
|
+
"expanse-video/src/**/*.tsx",
|
|
48
|
+
"reverence-video/src/**/*.tsx",
|
|
49
|
+
"spec-videos/**/*.{jsx,tsx}",
|
|
50
|
+
// Added Task 23: klaviyo-segments-video and radar-fixture-video were
|
|
51
|
+
// silently excluded - the same gap deterministic-scene-source had.
|
|
52
|
+
"klaviyo-segments-video/src/**/*.tsx",
|
|
53
|
+
"radar-fixture-video/src/**/*.tsx",
|
|
54
|
+
"packages/video-kit/src/**/*.tsx"
|
|
55
|
+
],
|
|
56
|
+
run: (file, relPath) => {
|
|
57
|
+
const findings = [];
|
|
58
|
+
for (const kind of ELEMENT_KINDS) {
|
|
59
|
+
for (const element of file.getDescendantsOfKind(kind)) {
|
|
60
|
+
if (!hasStyleFilter(element)) continue;
|
|
61
|
+
const own = Node.isJsxOpeningElement(element) ? element.getParent() : element;
|
|
62
|
+
let insideSvg = false;
|
|
63
|
+
let filteredAncestor = false;
|
|
64
|
+
let ancestor = own.getParent();
|
|
65
|
+
while (ancestor) {
|
|
66
|
+
if (Node.isJsxElement(ancestor)) {
|
|
67
|
+
const opening = ancestor.getOpeningElement();
|
|
68
|
+
if (opening.getTagNameNode().getText() === "svg") {
|
|
69
|
+
insideSvg = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (hasStyleFilter(opening)) filteredAncestor = true;
|
|
73
|
+
}
|
|
74
|
+
ancestor = ancestor.getParent();
|
|
75
|
+
}
|
|
76
|
+
if (insideSvg && filteredAncestor) {
|
|
77
|
+
findings.push({
|
|
78
|
+
file: relPath,
|
|
79
|
+
line: element.getStartLineNumber(),
|
|
80
|
+
match: element.getText().replace(/\s+/g, " ").slice(0, 60),
|
|
81
|
+
message: `nested CSS filter in SVG: this element sets style.filter inside an ancestor that also does. Chrome drops the inner content silently. Flatten to one filter on one layer.`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export {
|
|
92
|
+
noNestedSvgFilters
|
|
93
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const noRepeatedAssetAcrossShots = {
|
|
2
|
+
id: "no-repeated-asset-across-shots",
|
|
3
|
+
claim: "Never show the same asset (a product video, screenshot, or illustration) in two shots of one video; each shot earns its screen time with new material.",
|
|
4
|
+
why: "A repeated asset reads as filler and was flagged the moment it appeared; the fix was always replacing the repeat with new content, never keeping it.",
|
|
5
|
+
learnedFrom: "claude-sessions-digest/revola-usecase-book-meeting.md",
|
|
6
|
+
tier: "craft",
|
|
7
|
+
scope: { kind: "pipeline" },
|
|
8
|
+
enforcement: {
|
|
9
|
+
kind: "prose",
|
|
10
|
+
unanchorable: "The rule is a cross-shot content judgment; no single code location expresses it."
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
noRepeatedAssetAcrossShots
|
|
15
|
+
};
|