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