@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,747 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { isUpstreamIssue } from "./provenance";
|
|
6
|
+
import { commentsOnly, extractDocCitations, extractIssueCitations } from "./sourceCitations";
|
|
7
|
+
import { toolchainRepoRoot as repoRoot } from "./testSupport";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The extractors, unit-tested against the shapes citations actually take in
|
|
11
|
+
* this codebase's comments. The trailing-punctuation cases are not
|
|
12
|
+
* hypothetical: a prune script written during the 2026-08-26 review used a
|
|
13
|
+
* greedy character class that swallowed the sentence-ending period, so
|
|
14
|
+
* `docs/.../foo.md.` failed an existsSync and two live design docs landed in
|
|
15
|
+
* a delete set. Nothing caught it but a manual read of the output.
|
|
16
|
+
*/
|
|
17
|
+
describe("extractDocCitations", () => {
|
|
18
|
+
test("finds a bare path", () => {
|
|
19
|
+
expect(extractDocCitations("see docs/superpowers/specs/a.md for why"))
|
|
20
|
+
.toEqual(["docs/superpowers/specs/a.md"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("strips the sentence-ending period, not the extension", () => {
|
|
24
|
+
expect(extractDocCitations("Design: docs/superpowers/specs/a.md."))
|
|
25
|
+
.toEqual(["docs/superpowers/specs/a.md"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("strips the punctuation a citation is wrapped in", () => {
|
|
29
|
+
const cases: [string, string][] = [
|
|
30
|
+
["(docs/plans/b.md)", "docs/plans/b.md"],
|
|
31
|
+
["`docs/plans/b.md`", "docs/plans/b.md"],
|
|
32
|
+
['"docs/plans/b.md",', "docs/plans/b.md"],
|
|
33
|
+
["docs/plans/b.md;", "docs/plans/b.md"],
|
|
34
|
+
["docs/plans/b.md:", "docs/plans/b.md"],
|
|
35
|
+
];
|
|
36
|
+
for (const [input, want] of cases) {
|
|
37
|
+
expect(extractDocCitations(input), `input ${input}`).toEqual([want]);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("finds a single-segment path at the repo root", () => {
|
|
42
|
+
// An earlier DOC_PATTERN required a subdirectory, so every root-level
|
|
43
|
+
// citation - `docs/accounting.md` among them - went unchecked.
|
|
44
|
+
expect(extractDocCitations("Accounting correlation (docs/accounting.md): present only when"))
|
|
45
|
+
.toEqual(["docs/accounting.md"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("does NOT extract a docs/ path out of a longer path or a URL", () => {
|
|
49
|
+
// The URL shape is the one that occurs in this repo's comments
|
|
50
|
+
// (remotion.dev/docs/..., gemini-api/docs/...): without the left anchor its
|
|
51
|
+
// tail is read as a local file and fails. The project-relative shape occurs
|
|
52
|
+
// INSIDE the scanned trees too, in rule `recordedIn:` fields and their test
|
|
53
|
+
// assertions - string literals today, but one quoted line away from a
|
|
54
|
+
// comment, and it resolves nowhere at this repo's root.
|
|
55
|
+
expect(extractDocCitations("recorded: qse-qguard-video/docs/motion-brand-spec.md")).toEqual([]);
|
|
56
|
+
expect(extractDocCitations("see https://remotion.dev/docs/lambda/bucket-security")).toEqual([]);
|
|
57
|
+
expect(extractDocCitations("https://github.com/acme/repo/blob/main/docs/setup/install.md")).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("does NOT invent a citation from a directory glob or a wrapped line", () => {
|
|
61
|
+
// Real shapes in this repo: a `phase-*.md` glob built at runtime, and a
|
|
62
|
+
// path broken across two comment lines. Neither is a resolvable file, and
|
|
63
|
+
// flagging them would train readers to ignore the check.
|
|
64
|
+
expect(extractDocCitations("reads docs/headless/phase-*.md")).toEqual([]);
|
|
65
|
+
expect(extractDocCitations("docs/superpowers/specs/2026-08-08-build2-\n// round2-report.md")).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("accepts a trailing-slash directory citation", () => {
|
|
69
|
+
// The prose convention says "the design doc under docs/superpowers/", so
|
|
70
|
+
// the directory form is a real citation and is resolved like any other.
|
|
71
|
+
expect(extractDocCitations("the design doc under docs/superpowers/"))
|
|
72
|
+
.toEqual(["docs/superpowers/"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("deduplicates and returns stable order", () => {
|
|
76
|
+
expect(extractDocCitations("docs/plans/x.md and again docs/plans/x.md"))
|
|
77
|
+
.toEqual(["docs/plans/x.md"]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("extractIssueCitations", () => {
|
|
82
|
+
test("finds both id families", () => {
|
|
83
|
+
expect(extractIssueCitations("bd remotion-videos-8r8 and video-toolchain-n0n"))
|
|
84
|
+
.toEqual(["remotion-videos-8r8", "video-toolchain-n0n"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("finds an id in prose punctuation", () => {
|
|
88
|
+
expect(extractIssueCitations("(remotion-videos-corc, remotion-videos-b4pq)."))
|
|
89
|
+
.toEqual(["remotion-videos-b4pq", "remotion-videos-corc"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("does not match a longer word that merely starts with an id", () => {
|
|
93
|
+
// This is the case the trailing \b earns its keep on: without it the regex
|
|
94
|
+
// would happily take the first four suffix characters and invent an id.
|
|
95
|
+
expect(extractIssueCitations("remotion-videos-abcdefg")).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("does not match the bare repo name", () => {
|
|
99
|
+
// Note this one passes on the {3,4} suffix requirement, NOT on \b - there
|
|
100
|
+
// is no suffix at all to bound. Kept because the bare name is everywhere in
|
|
101
|
+
// this repo's prose and a future loosening of the suffix rule must not
|
|
102
|
+
// start matching it.
|
|
103
|
+
expect(extractIssueCitations("extracted from remotion-videos on 2026-08-24")).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("resolves a child id to the parent that has the record", () => {
|
|
107
|
+
expect(extractIssueCitations("see remotion-videos-5ky3.1")).toEqual(["remotion-videos-5ky3"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("commentsOnly", () => {
|
|
112
|
+
test("keeps line and block comments, drops code", () => {
|
|
113
|
+
const src = [
|
|
114
|
+
'const a = "docs/not-a-citation.md";',
|
|
115
|
+
"// see docs/real/one.md",
|
|
116
|
+
"/* and docs/real/two.md */",
|
|
117
|
+
].join("\n");
|
|
118
|
+
const got = commentsOnly(src, "a.ts");
|
|
119
|
+
expect(got).toContain("docs/real/one.md");
|
|
120
|
+
expect(got).toContain("docs/real/two.md");
|
|
121
|
+
expect(got).not.toContain("not-a-citation");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Each literal-shape test below carries a REAL citation in a real comment and
|
|
125
|
+
// asserts it survives, alongside the literal that must not. Without that
|
|
126
|
+
// positive half a bare `not.toContain` passes just as happily on the empty
|
|
127
|
+
// string, so a commentsOnly that extracted NOTHING would have looked green.
|
|
128
|
+
// Four of these had exactly that gap when the 2026-08-26 mutation pass ran.
|
|
129
|
+
|
|
130
|
+
test("a /* inside a TEMPLATE literal does NOT open a block comment", () => {
|
|
131
|
+
// costShell.ts:115 interpolates a `/*.json` glob into an error message. The
|
|
132
|
+
// "opener must follow a delimiter" heuristic allowed it, because the slash
|
|
133
|
+
// there follows a closing brace - which reopened the whole false-open class
|
|
134
|
+
// one patch after it was supposedly closed.
|
|
135
|
+
const src = [
|
|
136
|
+
"// docs/real/keep-me.md",
|
|
137
|
+
"const msg = `${projectDir} has BOTH ${legacy} and ${dir}/*.json; counted twice.`;",
|
|
138
|
+
'const p = "docs/not-a-citation.md";',
|
|
139
|
+
].join("\n");
|
|
140
|
+
const got = commentsOnly(src, "a.ts");
|
|
141
|
+
expect(got).toContain("docs/real/keep-me.md");
|
|
142
|
+
expect(got).not.toContain("docs/not-a-citation.md");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a // inside a string does NOT open a line comment", () => {
|
|
146
|
+
// joinShell.test.ts:146 writes a source file whose CONTENT carries a `//`.
|
|
147
|
+
const src = [
|
|
148
|
+
"// docs/real/keep-me.md",
|
|
149
|
+
'writeFileSync(p, "export default () => null; // gap v0\\n");',
|
|
150
|
+
].join("\n");
|
|
151
|
+
const got = commentsOnly(src, "a.ts");
|
|
152
|
+
expect(got).toContain("docs/real/keep-me.md");
|
|
153
|
+
expect(got).not.toContain("gap v0");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a /* inside a string does NOT open a block comment", () => {
|
|
157
|
+
// The real shape, from joinShell.test.ts:1838: a gitignore glob written as
|
|
158
|
+
// a string literal. The naive scanner treated everything after it as
|
|
159
|
+
// comment text - 600 lines of one file, which put every string literal
|
|
160
|
+
// below back in scope and produced a false dangling-citation failure.
|
|
161
|
+
const src = [
|
|
162
|
+
"// docs/real/keep-me.md",
|
|
163
|
+
'writeFileSync(p, "**/.video/*\\n.worktrees/\\n");',
|
|
164
|
+
'expect(stray("docs/vendor-notes.txt")).toBeNull();',
|
|
165
|
+
].join("\n");
|
|
166
|
+
const got = commentsOnly(src, "a.ts");
|
|
167
|
+
expect(got).toContain("docs/real/keep-me.md");
|
|
168
|
+
expect(got).not.toContain("docs/vendor-notes.txt");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a // inside a URL string does NOT open a line comment", () => {
|
|
172
|
+
const src = '// docs/real/keep-me.md\nconst u = "https://host/docs/x.md";';
|
|
173
|
+
const got = commentsOnly(src, "a.ts");
|
|
174
|
+
expect(got).toContain("docs/real/keep-me.md");
|
|
175
|
+
expect(got).not.toContain("docs/x.md");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a real block comment still closes on the same line", () => {
|
|
179
|
+
const src = "/* docs/a/b.md */ const x = \"docs/c/d.md\";";
|
|
180
|
+
const got = commentsOnly(src, "a.ts");
|
|
181
|
+
expect(got).toContain("docs/a/b.md");
|
|
182
|
+
expect(got).not.toContain("docs/c/d.md");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a generic arrow in a .ts file does not swallow the comments after it", () => {
|
|
186
|
+
// `<T>(x) => x` is ordinary TypeScript and a JSX error in TSX. The parser
|
|
187
|
+
// does not throw on it - it error-recovers, reads the `<T>` as an
|
|
188
|
+
// unterminated JSX element, and stops yielding comment ranges from there to
|
|
189
|
+
// EOF. Parsing every file as TSX cost worktreeShell.ts half its comment
|
|
190
|
+
// text and hid a live remotion-videos-b4pq citation from the guard, which
|
|
191
|
+
// is the same swallow-everything-after failure the hand-rolled scanner had.
|
|
192
|
+
const src = "const id = <T>(x: T): T => x;\n// docs/after-generic.md\n";
|
|
193
|
+
expect(commentsOnly(src, "a.ts")).toContain("docs/after-generic.md");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a .tsx file is still parsed as TSX", () => {
|
|
197
|
+
const src = 'const El = () => <div className="x" />;\n// docs/after-jsx.md\n';
|
|
198
|
+
expect(commentsOnly(src, "a.tsx")).toContain("docs/after-jsx.md");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("refuses a file the parser cannot read, rather than silently seeing less", () => {
|
|
202
|
+
// The failure this whole module exists to prevent is a guard reporting
|
|
203
|
+
// clean while reading less than it claims. A parse error means exactly
|
|
204
|
+
// that, so it is raised - naming the file, because the throw happens at
|
|
205
|
+
// module scope in the caller where the path is the only clue available.
|
|
206
|
+
expect(() => commentsOnly("const a = (;", "broken.ts")).toThrow(/broken\.ts/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("sees a comment that is the last thing inside a block", () => {
|
|
210
|
+
// forEachChild never visits closing tokens, so a comment in the leading
|
|
211
|
+
// trivia of a `}` / `]` / `)` was invisible - 229 of them across the tree.
|
|
212
|
+
const src = "function f() {\n go();\n // docs/tail-of-block.md\n}\n";
|
|
213
|
+
expect(commentsOnly(src, "a.ts")).toContain("docs/tail-of-block.md");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("sees a JSX expression-container comment", () => {
|
|
217
|
+
// {/* ... */} is the ONLY comment form available inside JSX children, and
|
|
218
|
+
// the kit and fixture trees are .tsx - so this was a blind spot over real
|
|
219
|
+
// source, not a hypothetical one.
|
|
220
|
+
const src = "const E = () => <div>{/* docs/jsx-child.md */}</div>;\n";
|
|
221
|
+
expect(commentsOnly(src, "a.tsx")).toContain("docs/jsx-child.md");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("JSX TEXT is data: a bare // or /* in rendered text is not a comment", () => {
|
|
225
|
+
// The same bug a third time. `<div>/* x */</div>` RENDERS those characters
|
|
226
|
+
// - they are text, like a string literal - but the JSX children SyntaxList
|
|
227
|
+
// starts at the first character of the run, so the comment-range lookup
|
|
228
|
+
// read an opener there and called it a comment.
|
|
229
|
+
const line = commentsOnly("const E = () => <div>// docs/jsx-text.md</div>;\n", "a.tsx");
|
|
230
|
+
expect(line).not.toContain("docs/jsx-text.md");
|
|
231
|
+
const block = commentsOnly("const E = () => <div>/* docs/jsx-text.md */</div>;\n", "b.tsx");
|
|
232
|
+
expect(block).not.toContain("docs/jsx-text.md");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("an unterminated /* in JSX text does not swallow the rest of the file", () => {
|
|
236
|
+
// The unbounded form, and the reason this is not merely cosmetic: the
|
|
237
|
+
// stray opener ran to EOF and dragged a string literal back into citation
|
|
238
|
+
// scope, which is precisely what the gitignore glob did to joinShell before
|
|
239
|
+
// any of this was parsed rather than scanned.
|
|
240
|
+
const src = [
|
|
241
|
+
"export const Card = () => (",
|
|
242
|
+
" <pre>",
|
|
243
|
+
" /* build with: npm run build",
|
|
244
|
+
" </pre>",
|
|
245
|
+
");",
|
|
246
|
+
"",
|
|
247
|
+
"// docs/real/keep-me.md",
|
|
248
|
+
'const cfg = { doc: "docs/this-is-a-string-literal.md" };',
|
|
249
|
+
].join("\n");
|
|
250
|
+
const got = commentsOnly(src, "c.tsx");
|
|
251
|
+
expect(got).toContain("docs/real/keep-me.md");
|
|
252
|
+
expect(got).not.toContain("docs/this-is-a-string-literal.md");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// The repo-wide guard. Everything above tests the extractors; this walks the
|
|
258
|
+
// real source and holds its citations to account.
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
// Everything this repo authors. `scripts/` and `infra/` were left out of the
|
|
262
|
+
// first version and that is where the only real dangling citation in the tree
|
|
263
|
+
// turned out to live - a citation checker blind to the file it would have
|
|
264
|
+
// caught is a poor advertisement for itself. Fixture trees inside these roots
|
|
265
|
+
// are scanned too: a typo'd bd id in a fixture comment is the same defect as
|
|
266
|
+
// one in source.
|
|
267
|
+
//
|
|
268
|
+
// The repo-root video projects are NOT here, and the reason is the opposite of
|
|
269
|
+
// what an earlier version of this comment claimed. They cite the BARE form
|
|
270
|
+
// `docs/motion-brand-spec.md`, which DOC_PATTERN matches and which does not
|
|
271
|
+
// exist at this repo's root, so adding them would produce a dozen dangling
|
|
272
|
+
// failures for citations that are correct relative to their own project. Doing
|
|
273
|
+
// it properly means resolving a project's citations against that project's
|
|
274
|
+
// root, which is a different guard from this one.
|
|
275
|
+
const SOURCE_DIRS = ["packages", "scripts", "infra"];
|
|
276
|
+
const SKIP = new Set(["node_modules", "dist", ".cache", ".turbo"]);
|
|
277
|
+
/** Authored source, whatever the extension - `commentsOnly` picks the parser
|
|
278
|
+
* from the file name, so .js/.mjs/.cjs/.jsx are read as themselves. */
|
|
279
|
+
const SOURCE_EXT = /\.(?:m|c)?[jt]sx?$/;
|
|
280
|
+
|
|
281
|
+
// Root-level config source is scanned too. vitest.config.ts carries a real bd
|
|
282
|
+
// citation in a comment and sat outside every tree above, which the coverage
|
|
283
|
+
// test below now makes impossible to reintroduce silently.
|
|
284
|
+
|
|
285
|
+
// NOT recorded as an exception anywhere: `scripts/session-preflight.sh`
|
|
286
|
+
// hard-fails unless `docs/security/act854-auth-json-remediation.md` exists, and
|
|
287
|
+
// that file has never existed in this repo or upstream - so that gate has never
|
|
288
|
+
// been passable. SOURCE_EXT never opens a .sh file, so this guard cannot see it
|
|
289
|
+
// at all, and writing an exception for it would claim coverage the code does
|
|
290
|
+
// not have. Tracked as video-toolchain-o3r, which needs a human answer about
|
|
291
|
+
// token rotation before it can be closed.
|
|
292
|
+
|
|
293
|
+
/** The citation module's own two files, and NOTHING ELSE - which is why this is
|
|
294
|
+
* derived from one base name rather than written as a list.
|
|
295
|
+
*
|
|
296
|
+
* Both hold SYNTHETIC citations (`docs/plans/b.md`, `docs/.../foo.md`) whose
|
|
297
|
+
* whole job is to exercise the parser, and the module's doc comments name
|
|
298
|
+
* example paths the same way - illustrations, not claims. They are fixtures
|
|
299
|
+
* that happen to live outside a `fixtures/` directory, and scanning them would
|
|
300
|
+
* make the guard fail on its own test data. Every OTHER file IS scanned: a
|
|
301
|
+
* typo'd bd id in a test comment is the same defect as one in source.
|
|
302
|
+
*
|
|
303
|
+
* A LIST HERE WAS A HOLE. SKIP_FILES is spliced into both sides of the
|
|
304
|
+
* coverage equality below, so any entry naming a real tracked file was
|
|
305
|
+
* invisible to it by construction: exempting 653 of 667 files kept the suite
|
|
306
|
+
* green, and a planted dangling citation was silenced just by adding its
|
|
307
|
+
* file's path here. That is the same unchecked-exemption defect this module
|
|
308
|
+
* exists to catch, so the exemption is no longer a list that can be appended
|
|
309
|
+
* to - adding a third file now means changing the shape of this code, and the
|
|
310
|
+
* coverage test pins the count regardless. */
|
|
311
|
+
const SELF = "packages/video-craft/src/sourceCitations";
|
|
312
|
+
const SKIP_FILES = new Set([`${SELF}.ts`, `${SELF}.test.ts`]);
|
|
313
|
+
|
|
314
|
+
/** Authored source under SOURCE_DIRS plus the repo-root config files, minus
|
|
315
|
+
* build output and the two files above. A symlink to a DIRECTORY is refused:
|
|
316
|
+
* `entry.isDirectory()` is false for one, so a symlinked subtree would
|
|
317
|
+
* silently leave the walk - the same invisible under-coverage this whole file
|
|
318
|
+
* exists to stop. A symlinked FILE is followed like any other file. */
|
|
319
|
+
const sourceFiles = (): string[] => {
|
|
320
|
+
const out: string[] = [];
|
|
321
|
+
const walk = (dir: string): void => {
|
|
322
|
+
for (const entry of readdirSync(join(repoRoot, dir), { withFileTypes: true })) {
|
|
323
|
+
if (SKIP.has(entry.name)) continue;
|
|
324
|
+
const rel = `${dir}/${entry.name}`;
|
|
325
|
+
if (entry.isSymbolicLink() && statSync(join(repoRoot, rel)).isDirectory()) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`${rel} is a symlinked directory; this walk does not descend into one, so its ` +
|
|
328
|
+
"source would leave the scan silently. Teach the walk to follow it (watching for " +
|
|
329
|
+
"cycles) before landing one in a scanned tree.");
|
|
330
|
+
}
|
|
331
|
+
if (entry.isDirectory()) walk(rel);
|
|
332
|
+
else if (SOURCE_EXT.test(entry.name) && !SKIP_FILES.has(rel)) out.push(rel);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
for (const d of SOURCE_DIRS) walk(d);
|
|
336
|
+
for (const entry of readdirSync(repoRoot, { withFileTypes: true })) {
|
|
337
|
+
if (!entry.isDirectory() && SOURCE_EXT.test(entry.name) && !SKIP_FILES.has(entry.name)) {
|
|
338
|
+
out.push(entry.name);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Comment text only: a path in a string literal is data, not a claim. The files
|
|
345
|
+
// are read and parsed once here, at module scope, and the citation tests below
|
|
346
|
+
// work from this array rather than re-reading them. The file name is passed
|
|
347
|
+
// through so `commentsOnly` can pick the right parser for it.
|
|
348
|
+
const scanned = sourceFiles().map((rel) => ({
|
|
349
|
+
rel, text: commentsOnly(readFileSync(join(repoRoot, rel), "utf8"), rel),
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
type Scanned = { rel: string; text: string };
|
|
353
|
+
|
|
354
|
+
/** Ids in a committed bd export. A missing export is a broken checkout, not a
|
|
355
|
+
* reason to pass silently - the convention depends on it. A malformed line
|
|
356
|
+
* names the file and the line, because a bare JSON.parse throw at module scope
|
|
357
|
+
* points at this function and gives the reader nothing to act on. Nothing is
|
|
358
|
+
* skipped but blank lines: a `#` line in a JSONL export is corruption, and an
|
|
359
|
+
* entry with no `id` would otherwise put `undefined` in the set. */
|
|
360
|
+
const idsFrom = (rel: string): Set<string> => {
|
|
361
|
+
const out = new Set<string>();
|
|
362
|
+
const lines = readFileSync(join(repoRoot, rel), "utf8").split("\n");
|
|
363
|
+
lines.forEach((line, i) => {
|
|
364
|
+
if (!line.trim()) return;
|
|
365
|
+
let id: unknown;
|
|
366
|
+
try {
|
|
367
|
+
({ id } = JSON.parse(line) as { id: unknown });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
throw new Error(`${rel}:${i + 1} is not valid JSON: ${(err as Error).message}`);
|
|
370
|
+
}
|
|
371
|
+
if (typeof id !== "string" || !id) throw new Error(`${rel}:${i + 1} has no string id`);
|
|
372
|
+
out.add(id);
|
|
373
|
+
});
|
|
374
|
+
return out;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** Read once, not per test - several of the tests below need them. */
|
|
378
|
+
const KNOWN_ISSUE_IDS = idsFrom(".beads/issues.jsonl");
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Every issue id in the upstream remotion-videos backlog, open and closed,
|
|
382
|
+
* snapshotted into this repo. See the header of the file itself for why.
|
|
383
|
+
*
|
|
384
|
+
* The short version: the migration took only the OPEN issues, so a comment
|
|
385
|
+
* citing one that was closed upstream has no local record and is still a
|
|
386
|
+
* perfectly good citation. Before this snapshot those were covered by a
|
|
387
|
+
* hand-maintained allowlist that lived in this file, alongside the citations it
|
|
388
|
+
* described - so the two tests over it only proved the two halves agreed with
|
|
389
|
+
* each other. A typo in a comment plus the same typo in the list passed green,
|
|
390
|
+
* and the failure message steered the reader straight into doing that, because
|
|
391
|
+
* "add it to the list" is the same keystroke for a real citation and a typo.
|
|
392
|
+
*
|
|
393
|
+
* What CI can and cannot see is stated on the tests themselves, below.
|
|
394
|
+
*/
|
|
395
|
+
const UPSTREAM_SNAPSHOT = ".beads/upstream-issue-ids.txt";
|
|
396
|
+
const upstreamSnapshotIds = (): Set<string> => {
|
|
397
|
+
const out = new Set<string>();
|
|
398
|
+
readFileSync(join(repoRoot, UPSTREAM_SNAPSHOT), "utf8").split("\n").forEach((line, i) => {
|
|
399
|
+
const id = line.trim();
|
|
400
|
+
if (!id || id.startsWith("#")) return;
|
|
401
|
+
if (/\s/.test(id)) throw new Error(`${UPSTREAM_SNAPSHOT}:${i + 1} is not a bare id: ${id}`);
|
|
402
|
+
out.add(id);
|
|
403
|
+
});
|
|
404
|
+
return out;
|
|
405
|
+
};
|
|
406
|
+
const UPSTREAM_ISSUE_IDS = upstreamSnapshotIds();
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Case-EXACT existence, resolved one path segment at a time.
|
|
410
|
+
*
|
|
411
|
+
* `existsSync` answers on the filesystem's terms: APFS is case-insensitive, so
|
|
412
|
+
* a comment citing `docs/Accounting.md` resolves on the author's Mac and fails
|
|
413
|
+
* on a Linux CI runner. readdirSync returns the true on-disk casing, so
|
|
414
|
+
* comparing against it checks what Linux will check. Unit-tested below against
|
|
415
|
+
* real repo paths - a mutation pass found that reverting this to existsSync was
|
|
416
|
+
* otherwise undetectable, since the repo-wide test only ever exercises it in
|
|
417
|
+
* the direction that passes.
|
|
418
|
+
*/
|
|
419
|
+
const existsCaseExact = (cited: string): boolean => {
|
|
420
|
+
let dir = repoRoot;
|
|
421
|
+
for (const segment of cited.split("/").filter(Boolean)) {
|
|
422
|
+
let entries: string[];
|
|
423
|
+
try {
|
|
424
|
+
entries = readdirSync(dir);
|
|
425
|
+
} catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
if (!entries.includes(segment)) return false;
|
|
429
|
+
dir = join(dir, segment);
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/** Files in one scanned tree or package, by path prefix. */
|
|
435
|
+
const under = (prefix: string): Scanned[] => scanned.filter((f) => f.rel.startsWith(prefix));
|
|
436
|
+
|
|
437
|
+
const countDocs = (files: Scanned[]): number =>
|
|
438
|
+
files.flatMap((f) => extractDocCitations(f.text)).length;
|
|
439
|
+
const countIds = (files: Scanned[]): number =>
|
|
440
|
+
files.flatMap((f) => extractIssueCitations(f.text)).length;
|
|
441
|
+
|
|
442
|
+
// The three checks, as functions over an arbitrary file list rather than
|
|
443
|
+
// straight-line code over `scanned`. Each is run twice below: once against the
|
|
444
|
+
// real tree expecting nothing, and once against a synthetic file KNOWN to be
|
|
445
|
+
// bad. Without that second call, deleting the body of a check leaves an empty
|
|
446
|
+
// `missing` array and the test passes - a mutation pass proved both id checks
|
|
447
|
+
// could be removed outright with the suite still green.
|
|
448
|
+
const danglingDocs = (files: Scanned[]): string[] => {
|
|
449
|
+
const out: string[] = [];
|
|
450
|
+
for (const { rel, text } of files) {
|
|
451
|
+
for (const cited of extractDocCitations(text)) {
|
|
452
|
+
if (!existsCaseExact(cited)) out.push(`${rel} -> ${cited}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const unresolvedLocalIds = (files: Scanned[]): string[] => {
|
|
459
|
+
const out: string[] = [];
|
|
460
|
+
for (const { rel, text } of files) {
|
|
461
|
+
for (const id of extractIssueCitations(text)) {
|
|
462
|
+
if (!isUpstreamIssue(id) && !KNOWN_ISSUE_IDS.has(id)) out.push(`${rel} -> ${id}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const unresolvedUpstreamIds = (files: Scanned[]): string[] => {
|
|
469
|
+
const out: string[] = [];
|
|
470
|
+
for (const { rel, text } of files) {
|
|
471
|
+
for (const id of extractIssueCitations(text)) {
|
|
472
|
+
if (!isUpstreamIssue(id)) continue;
|
|
473
|
+
if (!KNOWN_ISSUE_IDS.has(id) && !UPSTREAM_ISSUE_IDS.has(id)) {
|
|
474
|
+
out.push(
|
|
475
|
+
`${rel} -> ${id} (not in .beads/issues.jsonl, not in ${UPSTREAM_SNAPSHOT}; ` +
|
|
476
|
+
"if this id is real, refresh the snapshot from a remotion-videos checkout - " +
|
|
477
|
+
"see that file's header - rather than adding the id by hand)");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/** A file that is definitely not in the tree, for the positive controls. */
|
|
485
|
+
const SYNTHETIC_BAD: Scanned[] = [{
|
|
486
|
+
rel: "synthetic-control.ts",
|
|
487
|
+
text: "// docs/definitely-not-a-real-doc.md and remotion-videos-zzz9 and video-toolchain-zzz9",
|
|
488
|
+
}];
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The source files git knows about, which is where the coverage floor comes
|
|
492
|
+
* from. Deliberately NOT filtered by SKIP: build output and node_modules are
|
|
493
|
+
* gitignored, so git never lists them, and taking the expected set from a
|
|
494
|
+
* source the walk does not control is the whole point.
|
|
495
|
+
*
|
|
496
|
+
* A floor derived from the same exclusion list as the walk is not a floor. Two
|
|
497
|
+
* rounds of this file learned that the hard way - first the package list came
|
|
498
|
+
* from SKIP, then its replacement was itself unasserted, so returning a single
|
|
499
|
+
* package dropped 615 of 668 files with every test still green.
|
|
500
|
+
*/
|
|
501
|
+
const trackedSourceFiles = (): string[] =>
|
|
502
|
+
execFileSync("git", ["ls-files", "-z"], { cwd: repoRoot, encoding: "utf8", maxBuffer: 64 << 20 })
|
|
503
|
+
.split("\0")
|
|
504
|
+
.filter((p) => p && SOURCE_EXT.test(p))
|
|
505
|
+
.filter((p) => SOURCE_DIRS.some((d) => p.startsWith(`${d}/`)) || !p.includes("/"))
|
|
506
|
+
.sort();
|
|
507
|
+
|
|
508
|
+
/** Workspace packages, from the workspace definition. */
|
|
509
|
+
const workspacePackages = (): string[] => {
|
|
510
|
+
const globs = (JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as
|
|
511
|
+
{ workspaces?: string[] }).workspaces ?? [];
|
|
512
|
+
const out: string[] = [];
|
|
513
|
+
for (const glob of globs) {
|
|
514
|
+
const [root, star] = glob.split("/");
|
|
515
|
+
if (!root || star !== "*") throw new Error(`unsupported workspace glob: ${glob}`);
|
|
516
|
+
for (const entry of readdirSync(join(repoRoot, root), { withFileTypes: true })) {
|
|
517
|
+
if (entry.isDirectory() && existsSync(join(repoRoot, root, entry.name, "package.json"))) {
|
|
518
|
+
out.push(`${root}/${entry.name}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/** Packages that legitimately carry neither kind of citation. NAMED, because an
|
|
526
|
+
* exemption should be a claim someone made on purpose - and checked, below, so
|
|
527
|
+
* it cannot quietly grow to cover everything. */
|
|
528
|
+
const NO_CITATIONS = new Set(["packages/design-systems"]);
|
|
529
|
+
|
|
530
|
+
describe("the scan covers what it claims to", () => {
|
|
531
|
+
test("the git file list has not collapsed", () => {
|
|
532
|
+
// trackedSourceFiles() is the expected side of the equality below, so a
|
|
533
|
+
// collapsed or truncated list would quietly shrink the floor. Hard
|
|
534
|
+
// thresholds, deliberately far below any real value, not a census.
|
|
535
|
+
//
|
|
536
|
+
// WHAT THIS CANNOT PROVE: that the list came from git. A mutation returning
|
|
537
|
+
// the walked set instead passes every assertion here, because the invented
|
|
538
|
+
// list is the same files. The obvious cross-check - `git ls-tree -r HEAD`
|
|
539
|
+
// against `git ls-files` - was rejected on purpose: ls-tree reads the
|
|
540
|
+
// commit and ls-files reads the index, so it would fail for anyone holding
|
|
541
|
+
// an uncommitted source file, and a floor that fires during ordinary work
|
|
542
|
+
// gets deleted. Rewriting this helper to launder the walk is self-sabotage
|
|
543
|
+
// of the same order as deleting the assertion, which no test defends
|
|
544
|
+
// against; it is recorded here rather than papered over.
|
|
545
|
+
const tracked = trackedSourceFiles();
|
|
546
|
+
expect(tracked.length).toBeGreaterThan(600);
|
|
547
|
+
expect(tracked).toContain("vitest.config.ts");
|
|
548
|
+
expect(tracked).toContain("packages/video-cli/src/cli.ts");
|
|
549
|
+
expect(tracked).toContain(`${SELF}.ts`);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("every tracked source file is scanned, and nothing else is", () => {
|
|
553
|
+
// The floor that makes the rest meaningful, and the one that cannot be
|
|
554
|
+
// widened by editing what it checks: git's file list does not care what
|
|
555
|
+
// SKIP, SOURCE_DIRS or workspacePackages() say. Adding a directory name to
|
|
556
|
+
// SKIP, dropping a tree, or returning fewer packages all show up here as a
|
|
557
|
+
// concrete list of files that stopped being scanned.
|
|
558
|
+
//
|
|
559
|
+
// SKIP_FILES is the exception, because it appears on both sides - so it is
|
|
560
|
+
// pinned to exactly the citation module's own two files, which is the only
|
|
561
|
+
// exemption this guard is entitled to.
|
|
562
|
+
expect([...SKIP_FILES].sort()).toEqual([`${SELF}.test.ts`, `${SELF}.ts`].sort());
|
|
563
|
+
for (const skipped of SKIP_FILES) {
|
|
564
|
+
expect(trackedSourceFiles(), `${skipped} is exempted but not a tracked file`)
|
|
565
|
+
.toContain(skipped);
|
|
566
|
+
}
|
|
567
|
+
const walked = [...scanned.map((f) => f.rel), ...SKIP_FILES].sort();
|
|
568
|
+
expect(walked).toEqual(trackedSourceFiles());
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("the corpus the citation checks run over has not collapsed", () => {
|
|
572
|
+
// The checks below take a file list, so a truncated corpus would make them
|
|
573
|
+
// pass over nothing. Hard floors with headroom, deliberately not a census.
|
|
574
|
+
//
|
|
575
|
+
// What this does NOT defend: a call site edited to pass a slice. Nothing
|
|
576
|
+
// can - that is indistinguishable from deleting the assertion, and no test
|
|
577
|
+
// defends against its own removal.
|
|
578
|
+
expect(scanned.length).toBeGreaterThan(600);
|
|
579
|
+
expect(countDocs(scanned)).toBeGreaterThan(40);
|
|
580
|
+
expect(countIds(scanned)).toBeGreaterThan(150);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("the workspace package list is the one git sees on disk", () => {
|
|
584
|
+
// workspacePackages() no longer shares an input with the walk, but nothing
|
|
585
|
+
// asserted it was COMPLETE: `return out.slice(0, 1)` was green, and it kept
|
|
586
|
+
// design-systems - the one package the citation floor exempts - so every
|
|
587
|
+
// per-package check collapsed to "one package has at least one file".
|
|
588
|
+
const fromGit = [...new Set(
|
|
589
|
+
trackedSourceFiles()
|
|
590
|
+
.filter((p) => p.startsWith("packages/"))
|
|
591
|
+
.map((p) => p.split("/").slice(0, 2).join("/")))].sort();
|
|
592
|
+
expect(workspacePackages().sort()).toEqual(fromGit);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("every scanned tree and every workspace package is actually reached", () => {
|
|
596
|
+
for (const dir of SOURCE_DIRS) {
|
|
597
|
+
expect(under(`${dir}/`).length, `no files scanned under ${dir}/`).toBeGreaterThan(0);
|
|
598
|
+
}
|
|
599
|
+
for (const pkg of workspacePackages()) {
|
|
600
|
+
expect(under(`${pkg}/`).length, `no files scanned in ${pkg}`).toBeGreaterThan(0);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("each kind of citation is floored separately, per package", () => {
|
|
605
|
+
// Summing doc and id citations reproduced inside each package the very
|
|
606
|
+
// residual the per-package split was meant to remove: id citations alone
|
|
607
|
+
// clear a combined floor everywhere, so extractDocCitations could return
|
|
608
|
+
// [] for the entire repo and this test would still pass. Floor them apart.
|
|
609
|
+
for (const pkg of workspacePackages()) {
|
|
610
|
+
if (NO_CITATIONS.has(pkg)) continue;
|
|
611
|
+
const files = under(`${pkg}/`);
|
|
612
|
+
expect(countDocs(files), `${pkg} yielded no docs/ citations`).toBeGreaterThan(0);
|
|
613
|
+
expect(countIds(files), `${pkg} yielded no bd id citations`).toBeGreaterThan(0);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("the citation exemption covers only packages that really cite nothing", () => {
|
|
618
|
+
// Otherwise the exemption is the escape hatch: adding every package to it
|
|
619
|
+
// turns the floor above into a no-op, silently.
|
|
620
|
+
for (const pkg of NO_CITATIONS) {
|
|
621
|
+
const files = under(`${pkg}/`);
|
|
622
|
+
expect(countDocs(files) + countIds(files),
|
|
623
|
+
`${pkg} is exempted but does cite things - drop it from NO_CITATIONS`).toBe(0);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("the trees outside packages/ carry citations of their own", () => {
|
|
628
|
+
// NAMED HERE, deliberately not derived from SOURCE_DIRS. The first version
|
|
629
|
+
// of this test iterated SOURCE_DIRS.filter(d => d !== "packages"), so
|
|
630
|
+
// deleting a tree from SOURCE_DIRS also deleted its own floor: the mutation
|
|
631
|
+
// "scripts/ dropped from the walk" passed green. That is the circularity
|
|
632
|
+
// the workspacePackages() comment above describes, written into the fix
|
|
633
|
+
// for it, and only a mutation pass over this file surfaced it.
|
|
634
|
+
// Floored APART, like the packages above. A summed floor here was blind to
|
|
635
|
+
// narrowing SOURCE_EXT to drop .mjs: that deletes 100% of infra/'s docs
|
|
636
|
+
// coverage (both docs/accounting.md citations are in .mjs) and scripts/'s
|
|
637
|
+
// only docs citation, while each tree keeps one .ts id citation and clears
|
|
638
|
+
// a combined count. Same residual as the packages floor had, one tree over.
|
|
639
|
+
for (const dir of ["scripts", "infra"]) {
|
|
640
|
+
expect(SOURCE_DIRS, `${dir}/ is no longer walked at all`).toContain(dir);
|
|
641
|
+
const files = under(`${dir}/`);
|
|
642
|
+
expect(countDocs(files), `${dir}/ yielded no docs/ citations`).toBeGreaterThan(0);
|
|
643
|
+
expect(countIds(files), `${dir}/ yielded no bd id citations`).toBeGreaterThan(0);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe("existsCaseExact", () => {
|
|
649
|
+
test("resolves a real path, and rejects the same path in the wrong case", () => {
|
|
650
|
+
// The whole point: existsSync says true to both of these on APFS, and the
|
|
651
|
+
// second one is a red CI run on Linux.
|
|
652
|
+
expect(existsCaseExact("docs/accounting.md")).toBe(true);
|
|
653
|
+
expect(existsCaseExact("docs/Accounting.md")).toBe(false);
|
|
654
|
+
expect(existsCaseExact("DOCS/accounting.md")).toBe(false);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("checks INTERMEDIATE segments, not just the file name", () => {
|
|
658
|
+
expect(existsCaseExact("docs/superpowers/")).toBe(true);
|
|
659
|
+
expect(existsCaseExact("docs/Superpowers/")).toBe(false);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("rejects a path that is not there at all", () => {
|
|
663
|
+
expect(existsCaseExact("docs/definitely-not-a-real-doc.md")).toBe(false);
|
|
664
|
+
expect(existsCaseExact("docs/accounting.md/nested")).toBe(false);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
describe("source comments cite things that exist", () => {
|
|
669
|
+
test("every docs/ path cited in a source comment resolves on disk", () => {
|
|
670
|
+
// The 2026-08-25 extraction left ten of these dangling. They were fixed by
|
|
671
|
+
// hand and nothing was left behind to stop it recurring; this is it.
|
|
672
|
+
expect(danglingDocs(scanned)).toEqual([]);
|
|
673
|
+
expect(danglingDocs(SYNTHETIC_BAD), "the check itself stopped working").toHaveLength(1);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test("every video-toolchain- issue cited in source exists in this repo's DB", () => {
|
|
677
|
+
// This repo's own ids have no upstream excuse.
|
|
678
|
+
//
|
|
679
|
+
// NO LONGER VACUOUS, and the transition is worth recording. This check
|
|
680
|
+
// spent its whole life with nothing real to check - every id in the corpus
|
|
681
|
+
// was a remotion-videos- one, and the synthetic control below was the only
|
|
682
|
+
// thing exercising it. video-toolchain-h6e was the first change to cite a
|
|
683
|
+
// local id from scanned comments, and this test caught it immediately:
|
|
684
|
+
// `bd create` writes the live DB, `.beads/issues.jsonl` is a committed
|
|
685
|
+
// EXPORT of it, and the id resolves only once someone runs `bd export`. So
|
|
686
|
+
// the failure mode it now guards, in the words it shows up in, is
|
|
687
|
+
// "<file> -> video-toolchain-<id> (not in .beads/issues.jsonl ...)" on a
|
|
688
|
+
// freshly filed issue, and the fix is the export, not the comment.
|
|
689
|
+
expect(unresolvedLocalIds(scanned)).toEqual([]);
|
|
690
|
+
expect(unresolvedLocalIds(SYNTHETIC_BAD), "the check itself stopped working").toHaveLength(1);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("every remotion-videos- issue cited in source exists, here or in the snapshot", () => {
|
|
694
|
+
// Runs in CI with no upstream checkout, because the upstream ids are
|
|
695
|
+
// snapshotted in the repo. This replaced a tautology: the old version
|
|
696
|
+
// asserted the id matched /^remotion-videos-[a-z0-9]{3,4}$/, which
|
|
697
|
+
// extractIssueCitations guarantees by construction, so it could not fail -
|
|
698
|
+
// over exactly the ids that no other tier verified.
|
|
699
|
+
//
|
|
700
|
+
// What this does NOT prove: that the snapshot itself is honest. In CI the
|
|
701
|
+
// evidence for those ids is a text file in this same repo, so a typo added
|
|
702
|
+
// to a comment AND to the snapshot passes here. The drift test below is
|
|
703
|
+
// what catches that, and it needs the sibling checkout.
|
|
704
|
+
expect(unresolvedUpstreamIds(scanned)).toEqual([]);
|
|
705
|
+
expect(unresolvedUpstreamIds(SYNTHETIC_BAD), "the check itself stopped working")
|
|
706
|
+
.toHaveLength(1);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("the snapshot is a well-formed export of bare ids", () => {
|
|
710
|
+
// Shape only - it catches a truncated or corrupted file, which would
|
|
711
|
+
// otherwise surface as a pile of "citation does not exist" failures
|
|
712
|
+
// pointing at comments that are perfectly fine. It does NOT detect a
|
|
713
|
+
// plausible hand-added id; only the drift test does, so this test is named
|
|
714
|
+
// for what it checks rather than for what one might hope it checks.
|
|
715
|
+
expect(UPSTREAM_ISSUE_IDS.size).toBeGreaterThan(300);
|
|
716
|
+
const malformed = [...UPSTREAM_ISSUE_IDS]
|
|
717
|
+
// `.1` suffixes are real: bd child issues appear in the export under
|
|
718
|
+
// their parent's id, and extractIssueCitations resolves a cited child to
|
|
719
|
+
// that parent, so both forms have to survive.
|
|
720
|
+
.filter((id) => !/^remotion-videos-[a-z0-9]+(?:\.\d+)?$/.test(id));
|
|
721
|
+
expect(malformed).toEqual([]);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Drift tier. The snapshot is what CI checks against; this is what proves the
|
|
725
|
+
// snapshot still matches reality, and it needs the other repo on disk. It
|
|
726
|
+
// SKIPS rather than passes when absent, so the weaker coverage is visible in
|
|
727
|
+
// the run instead of silent. Nothing in .github/workflows sets
|
|
728
|
+
// REMOTION_VIDEOS_ROOT, so in CI this never runs - which is exactly why the
|
|
729
|
+
// test above says out loud what it cannot prove. Gated the same way as the
|
|
730
|
+
// upstream tier in packages/video-cli/src/radar/corpus.test.ts.
|
|
731
|
+
const upstreamDb = process.env.REMOTION_VIDEOS_ROOT
|
|
732
|
+
? join(process.env.REMOTION_VIDEOS_ROOT, ".beads/issues.jsonl") : "";
|
|
733
|
+
const haveUpstream = Boolean(upstreamDb && existsSync(upstreamDb));
|
|
734
|
+
|
|
735
|
+
test.skipIf(!haveUpstream)("the upstream id snapshot has not drifted", () => {
|
|
736
|
+
const live = new Set(
|
|
737
|
+
readFileSync(upstreamDb, "utf8").split("\n").filter((l) => l.trim())
|
|
738
|
+
.map((l) => (JSON.parse(l) as { id: string }).id));
|
|
739
|
+
// An id in the snapshot but NOT upstream means the snapshot was hand-edited
|
|
740
|
+
// or the id was deleted, and either way the CI check above is trusting a
|
|
741
|
+
// fiction. The other direction - upstream gaining issues - is normal and
|
|
742
|
+
// does not invalidate anything already snapshotted, so it is not checked
|
|
743
|
+
// here and the snapshot may legitimately be stale in that sense.
|
|
744
|
+
const fabricated = [...UPSTREAM_ISSUE_IDS].filter((id) => !live.has(id));
|
|
745
|
+
expect(fabricated).toEqual([]);
|
|
746
|
+
});
|
|
747
|
+
});
|