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