@revola-ai/video-craft 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/README.md +56 -0
  2. package/dist/anchors.js +69 -0
  3. package/dist/corpus.js +91 -0
  4. package/dist/projectDoc.js +83 -0
  5. package/dist/projectDocSync.js +39 -0
  6. package/dist/projectMeta.js +37 -0
  7. package/dist/provenance.js +41 -0
  8. package/dist/radar-check-ids.json +34 -0
  9. package/dist/registry.js +307 -0
  10. package/dist/rules/animatedBarChartTightBox.js +15 -0
  11. package/dist/rules/animatedBarChartWhenNotToUse.js +15 -0
  12. package/dist/rules/animatedBarChartWhenToUse.js +15 -0
  13. package/dist/rules/animatedLineChartWhenNotToUse.js +15 -0
  14. package/dist/rules/animatedLineChartWhenToUse.js +15 -0
  15. package/dist/rules/approvedSfxPaletteOnly.js +15 -0
  16. package/dist/rules/atmosphereGradientsFadeAtBothEnds.js +15 -0
  17. package/dist/rules/audioIsTheClock.js +16 -0
  18. package/dist/rules/beatsDeriveFromTheirOwnSequence.js +15 -0
  19. package/dist/rules/bestSceneSetsTheBar.js +15 -0
  20. package/dist/rules/bumpBeforeRefreeze.js +15 -0
  21. package/dist/rules/cameraIsTheNarrator.js +15 -0
  22. package/dist/rules/checkPinBeforeShip.js +16 -0
  23. package/dist/rules/coldViewerClarity.js +15 -0
  24. package/dist/rules/commitOrCutEffects.js +29 -0
  25. package/dist/rules/convertFramesAcrossClockBoundary.js +15 -0
  26. package/dist/rules/crossCutElementsLiveInTheHudLayer.js +15 -0
  27. package/dist/rules/cursorClicksRealControls.js +15 -0
  28. package/dist/rules/cutOnRealAudioPauses.js +15 -0
  29. package/dist/rules/darkButNeverDim.js +15 -0
  30. package/dist/rules/dataRadarTagsDeclared.js +193 -0
  31. package/dist/rules/declareAVerticalZoneMap.js +15 -0
  32. package/dist/rules/deleteRejectedRigsCompletely.js +15 -0
  33. package/dist/rules/deriveSeamsDontMeasure.js +17 -0
  34. package/dist/rules/deterministicSceneSource.js +65 -0
  35. package/dist/rules/diegeticSoundForOnscreenActions.js +15 -0
  36. package/dist/rules/distinctTransitionPerBeat.js +15 -0
  37. package/dist/rules/downloadOrDrawRichAssets.js +15 -0
  38. package/dist/rules/elementOnscreenForItsVoSentence.js +17 -0
  39. package/dist/rules/emitBuildNotes.js +15 -0
  40. package/dist/rules/energyFromTimingNotPartCount.js +15 -0
  41. package/dist/rules/evidenceBeforeClaimingFixed.js +15 -0
  42. package/dist/rules/exactPeerLockstep.js +15 -0
  43. package/dist/rules/firstPassIsADraft.js +15 -0
  44. package/dist/rules/fixCrowdingByRebandingNotNudging.js +15 -0
  45. package/dist/rules/fixesPreserveApprovedElements.js +15 -0
  46. package/dist/rules/floatingNotBoxed.js +15 -0
  47. package/dist/rules/focusBeatsRecedeTheContext.js +15 -0
  48. package/dist/rules/fontVarsBrandRemocn.js +15 -0
  49. package/dist/rules/geometricSeamsNotCrossfades.js +23 -0
  50. package/dist/rules/geometrySidecarExportsTyped.js +36 -0
  51. package/dist/rules/glowIsAFocalAccent.js +22 -0
  52. package/dist/rules/heroFillsTheFrame.js +17 -0
  53. package/dist/rules/htmlMasksForAnimatedCounters.js +15 -0
  54. package/dist/rules/impactStackOnPayoffs.js +15 -0
  55. package/dist/rules/interactiveTargetsAreDeclaredData.js +15 -0
  56. package/dist/rules/interpolateLiteralKeysMonotonic.js +79 -0
  57. package/dist/rules/latenessIsStructuralOverlapTheBeats.js +15 -0
  58. package/dist/rules/lessTextMoreVisualizations.js +15 -0
  59. package/dist/rules/liveLinkDuplicateReactAlias.js +15 -0
  60. package/dist/rules/make2dElements3d.js +15 -0
  61. package/dist/rules/masterMixLoudnessTarget.js +17 -0
  62. package/dist/rules/measureAlignmentDontEyeball.js +15 -0
  63. package/dist/rules/motionCarriesWeightAndMeaning.js +15 -0
  64. package/dist/rules/motionDensityFloor.js +15 -0
  65. package/dist/rules/neverEditTierBVendored.js +15 -0
  66. package/dist/rules/neverRestateDerivedFrameNumbersInComments.js +15 -0
  67. package/dist/rules/noAiGradientLook.js +15 -0
  68. package/dist/rules/noColoredFlashToHideACut.js +22 -0
  69. package/dist/rules/noDoubleFrameShift.js +126 -0
  70. package/dist/rules/noDuplicateMotionPrimitives.js +15 -0
  71. package/dist/rules/noLinearMotion.js +22 -0
  72. package/dist/rules/noNestedSvgFilters.js +93 -0
  73. package/dist/rules/noRepeatedAssetAcrossShots.js +15 -0
  74. package/dist/rules/noShorthandWithUndefinedLonghand.js +93 -0
  75. package/dist/rules/noStackedFadesAtACut.js +24 -0
  76. package/dist/rules/noStaticHolds.js +17 -0
  77. package/dist/rules/occlusionVisibleElements.js +17 -0
  78. package/dist/rules/oneContinuousVoTrack.js +15 -0
  79. package/dist/rules/onePunchPerMomentClass.js +15 -0
  80. package/dist/rules/oneRenderPerRound.js +15 -0
  81. package/dist/rules/openStaticAfterAHardCut.js +22 -0
  82. package/dist/rules/openingsAndClosingsCarryMore.js +15 -0
  83. package/dist/rules/persistentDeviceFrames.js +15 -0
  84. package/dist/rules/pixelDiffAnEmptyBandFirst.js +15 -0
  85. package/dist/rules/planIsPublishedNotGating.js +15 -0
  86. package/dist/rules/poweredByRevolaWatermark.js +15 -0
  87. package/dist/rules/predrawContentPresentAtTheCut.js +17 -0
  88. package/dist/rules/premiumSurfacesNotFlatBorders.js +15 -0
  89. package/dist/rules/radarDispositionsAreHumanOnly.js +17 -0
  90. package/dist/rules/realAssetsNotPlaceholders.js +15 -0
  91. package/dist/rules/rebuildUiFromReconNotScreenshots.js +15 -0
  92. package/dist/rules/reconStrandsRunInParallel.js +15 -0
  93. package/dist/rules/registryBeforeHandrolling.js +15 -0
  94. package/dist/rules/remapRateEndpointFormula.js +15 -0
  95. package/dist/rules/remocnContentTailwindChrome.js +15 -0
  96. package/dist/rules/retimeOnlyViaPlaybackOrDesignTokens.js +15 -0
  97. package/dist/rules/reviewTheRenderByFrameSweep.js +15 -0
  98. package/dist/rules/rollingNumberBoundedSequence.js +17 -0
  99. package/dist/rules/rollingNumberSequenceLayoutNone.js +16 -0
  100. package/dist/rules/rollingNumberTightBoxAlignment.js +15 -0
  101. package/dist/rules/rollingNumberWhenNotToUse.js +17 -0
  102. package/dist/rules/rollingNumberWhenToUse.js +17 -0
  103. package/dist/rules/sameFamilySameProject.js +15 -0
  104. package/dist/rules/sampleBrandFromProduct.js +15 -0
  105. package/dist/rules/sampleVelocityFromPreviousFramePosition.js +15 -0
  106. package/dist/rules/sceneRootTagDeclared.js +41 -0
  107. package/dist/rules/scrubDemoDataToGenericRoles.js +15 -0
  108. package/dist/rules/selfReviewThePlanForGaps.js +15 -0
  109. package/dist/rules/sourceArchivedBeforeBrief.js +17 -0
  110. package/dist/rules/statisticsSelfExplanatory.js +15 -0
  111. package/dist/rules/stopsAsRelativeSpacing.js +15 -0
  112. package/dist/rules/theWholeCanvasWorks.js +15 -0
  113. package/dist/rules/themePerSection.js +22 -0
  114. package/dist/rules/thesisDeclaredInBrief.js +17 -0
  115. package/dist/rules/tierARequiresPropUnreachableEdit.js +15 -0
  116. package/dist/rules/tinyBodyCopyAsRuledLines.js +15 -0
  117. package/dist/rules/titlesPinnedOutsideCameraPush.js +15 -0
  118. package/dist/rules/tokensNotRawHexes.js +60 -0
  119. package/dist/rules/typeTokenDefaultedParamsAsString.js +15 -0
  120. package/dist/rules/varyKitChoreographyAcrossSections.js +15 -0
  121. package/dist/rules/varyTheVisualizationGrammar.js +15 -0
  122. package/dist/rules/verifyCheapRenderDear.js +15 -0
  123. package/dist/rules/voAddressesTheViewer.js +15 -0
  124. package/dist/rules/voIsAPerformance.js +15 -0
  125. package/dist/rules/voScriptLocksSpokenWords.js +15 -0
  126. package/dist/rules/wrappersPreserveChildGeometry.js +15 -0
  127. package/dist/sourceCheck.js +47 -0
  128. package/dist/sourceCitations.js +68 -0
  129. package/dist/stageProjection.js +69 -0
  130. package/dist/stages.js +163 -0
  131. package/dist/testSupport.js +6 -0
  132. package/dist/types.js +0 -0
  133. package/dist/variants.js +8 -0
  134. package/dist/waiverProjection.js +20 -0
  135. package/package.json +32 -0
  136. package/src/anchors.test.ts +119 -0
  137. package/src/anchors.ts +110 -0
  138. package/src/corpus.test.ts +96 -0
  139. package/src/corpus.ts +149 -0
  140. package/src/corpusProject.test.ts +103 -0
  141. package/src/creativeEdge.test.ts +58 -0
  142. package/src/distBuild.test.ts +86 -0
  143. package/src/docStatus.test.ts +81 -0
  144. package/src/drift.test.ts +124 -0
  145. package/src/fixtures/data-radar-dynamic/Branches.tsx +14 -0
  146. package/src/fixtures/data-radar-dynamic/plan.fixture.yaml +16 -0
  147. package/src/fixtures/data-radar-forwarded/Scene.tsx +19 -0
  148. package/src/fixtures/data-radar-forwarded/plan.fixture.yaml +13 -0
  149. package/src/fixtures/data-radar-opaque/Scene.tsx +15 -0
  150. package/src/fixtures/data-radar-opaque/plan.fixture.yaml +10 -0
  151. package/src/fixtures/data-radar-tags/SceneClean.tsx +10 -0
  152. package/src/fixtures/data-radar-tags/SceneNullBinding.tsx +12 -0
  153. package/src/fixtures/data-radar-tags/SceneWithGap.tsx +11 -0
  154. package/src/fixtures/data-radar-tags/plan.fixture.yaml +29 -0
  155. package/src/fixtures/deterministic-scene.tsx +15 -0
  156. package/src/fixtures/double-shift/aliased.tsx +13 -0
  157. package/src/fixtures/double-shift/assigned.tsx +13 -0
  158. package/src/fixtures/double-shift/clean.tsx +19 -0
  159. package/src/fixtures/double-shift/destructured.tsx +12 -0
  160. package/src/fixtures/double-shift/elementAccess.tsx +12 -0
  161. package/src/fixtures/double-shift/layout.ts +24 -0
  162. package/src/fixtures/double-shift/mechanism-prefix.tsx.txt +458 -0
  163. package/src/fixtures/double-shift/wrapped.tsx +15 -0
  164. package/src/fixtures/geometry-sidecar-missing-export.ts +10 -0
  165. package/src/fixtures/geometry-sidecar-missing-type.ts +7 -0
  166. package/src/fixtures/geometry-sidecar-valid.ts +6 -0
  167. package/src/fixtures/interpolate-ascending.tsx +6 -0
  168. package/src/fixtures/interpolate-descending.tsx +8 -0
  169. package/src/fixtures/interpolate-identifier-keys.tsx +9 -0
  170. package/src/fixtures/nested-filters-broken.tsx +11 -0
  171. package/src/fixtures/nested-filters-fixed.tsx +8 -0
  172. package/src/fixtures/nested-filters-html-wrapper.tsx +14 -0
  173. package/src/fixtures/nested-filters-html.tsx +7 -0
  174. package/src/fixtures/nested-filters-siblings.tsx +12 -0
  175. package/src/fixtures/nondeterministic-scene.tsx +14 -0
  176. package/src/fixtures/nondeterministic-variants.tsx +16 -0
  177. package/src/fixtures/raw-hex-scene.tsx +10 -0
  178. package/src/fixtures/scene-root-declared/SceneHook.tsx +9 -0
  179. package/src/fixtures/scene-root-declared/plan.fixture.yaml +10 -0
  180. package/src/fixtures/scene-root-missing/SceneHook.tsx +11 -0
  181. package/src/fixtures/scene-root-missing/plan.fixture.yaml +10 -0
  182. package/src/fixtures/scene-root-no-names/SceneHook.tsx +6 -0
  183. package/src/fixtures/scene-root-no-names/plan.fixture.yaml +6 -0
  184. package/src/fixtures/scene-root-shell/SceneHook.tsx +8 -0
  185. package/src/fixtures/scene-root-shell/Shell.tsx +9 -0
  186. package/src/fixtures/scene-root-shell/plan.fixture.yaml +10 -0
  187. package/src/fixtures/shorthand-undefined-clean.tsx +23 -0
  188. package/src/fixtures/shorthand-undefined-longhand.tsx +17 -0
  189. package/src/fixtures/token-scene.tsx +9 -0
  190. package/src/includeCoverage.test.ts +176 -0
  191. package/src/learnedFrom.test.ts +32 -0
  192. package/src/projectDoc.test.ts +230 -0
  193. package/src/projectDoc.ts +152 -0
  194. package/src/projectDocSync.test.ts +152 -0
  195. package/src/projectDocSync.ts +79 -0
  196. package/src/projectMeta.test.ts +75 -0
  197. package/src/projectMeta.ts +53 -0
  198. package/src/provenance.ts +85 -0
  199. package/src/radar-check-ids.json +34 -0
  200. package/src/registry.test.ts +207 -0
  201. package/src/registry.ts +352 -0
  202. package/src/rules/animatedBarChartTightBox.ts +14 -0
  203. package/src/rules/animatedBarChartWhenNotToUse.ts +14 -0
  204. package/src/rules/animatedBarChartWhenToUse.ts +14 -0
  205. package/src/rules/animatedLineChartWhenNotToUse.ts +14 -0
  206. package/src/rules/animatedLineChartWhenToUse.ts +14 -0
  207. package/src/rules/approvedSfxPaletteOnly.ts +23 -0
  208. package/src/rules/atmosphereGradientsFadeAtBothEnds.ts +21 -0
  209. package/src/rules/audioIsTheClock.ts +22 -0
  210. package/src/rules/beatsDeriveFromTheirOwnSequence.ts +21 -0
  211. package/src/rules/bestSceneSetsTheBar.ts +21 -0
  212. package/src/rules/bumpBeforeRefreeze.ts +20 -0
  213. package/src/rules/cameraIsTheNarrator.ts +21 -0
  214. package/src/rules/checkPinBeforeShip.ts +22 -0
  215. package/src/rules/coldViewerClarity.ts +21 -0
  216. package/src/rules/commitOrCutEffects.ts +36 -0
  217. package/src/rules/convertFramesAcrossClockBoundary.ts +19 -0
  218. package/src/rules/crossCutElementsLiveInTheHudLayer.ts +21 -0
  219. package/src/rules/cursorClicksRealControls.ts +22 -0
  220. package/src/rules/cutOnRealAudioPauses.ts +20 -0
  221. package/src/rules/darkButNeverDim.ts +21 -0
  222. package/src/rules/dataRadarTagsDeclared.test.ts +60 -0
  223. package/src/rules/dataRadarTagsDeclared.ts +341 -0
  224. package/src/rules/declareAVerticalZoneMap.ts +19 -0
  225. package/src/rules/deleteRejectedRigsCompletely.ts +19 -0
  226. package/src/rules/deriveSeamsDontMeasure.ts +23 -0
  227. package/src/rules/deterministicSceneSource.test.ts +58 -0
  228. package/src/rules/deterministicSceneSource.ts +82 -0
  229. package/src/rules/diegeticSoundForOnscreenActions.ts +21 -0
  230. package/src/rules/distinctTransitionPerBeat.ts +21 -0
  231. package/src/rules/downloadOrDrawRichAssets.ts +22 -0
  232. package/src/rules/elementOnscreenForItsVoSentence.ts +25 -0
  233. package/src/rules/emitBuildNotes.ts +19 -0
  234. package/src/rules/energyFromTimingNotPartCount.ts +20 -0
  235. package/src/rules/evidenceBeforeClaimingFixed.ts +23 -0
  236. package/src/rules/exactPeerLockstep.ts +19 -0
  237. package/src/rules/firstPassIsADraft.ts +20 -0
  238. package/src/rules/fixCrowdingByRebandingNotNudging.ts +20 -0
  239. package/src/rules/fixesPreserveApprovedElements.ts +24 -0
  240. package/src/rules/floatingNotBoxed.ts +21 -0
  241. package/src/rules/focusBeatsRecedeTheContext.ts +20 -0
  242. package/src/rules/fontVarsBrandRemocn.ts +20 -0
  243. package/src/rules/geometricSeamsNotCrossfades.ts +28 -0
  244. package/src/rules/geometrySidecarExportsTyped.test.ts +37 -0
  245. package/src/rules/geometrySidecarExportsTyped.ts +59 -0
  246. package/src/rules/glowIsAFocalAccent.ts +28 -0
  247. package/src/rules/heroFillsTheFrame.ts +24 -0
  248. package/src/rules/htmlMasksForAnimatedCounters.ts +21 -0
  249. package/src/rules/impactStackOnPayoffs.ts +26 -0
  250. package/src/rules/interactiveTargetsAreDeclaredData.ts +21 -0
  251. package/src/rules/interpolateLiteralKeysMonotonic.test.ts +49 -0
  252. package/src/rules/interpolateLiteralKeysMonotonic.ts +98 -0
  253. package/src/rules/latenessIsStructuralOverlapTheBeats.ts +20 -0
  254. package/src/rules/lessTextMoreVisualizations.ts +22 -0
  255. package/src/rules/liveLinkDuplicateReactAlias.ts +22 -0
  256. package/src/rules/make2dElements3d.ts +21 -0
  257. package/src/rules/masterMixLoudnessTarget.ts +22 -0
  258. package/src/rules/measureAlignmentDontEyeball.ts +20 -0
  259. package/src/rules/motionCarriesWeightAndMeaning.ts +21 -0
  260. package/src/rules/motionDensityFloor.ts +20 -0
  261. package/src/rules/neverEditTierBVendored.ts +20 -0
  262. package/src/rules/neverRestateDerivedFrameNumbersInComments.ts +20 -0
  263. package/src/rules/noAiGradientLook.ts +22 -0
  264. package/src/rules/noColoredFlashToHideACut.ts +29 -0
  265. package/src/rules/noDoubleFrameShift.test.ts +95 -0
  266. package/src/rules/noDoubleFrameShift.ts +193 -0
  267. package/src/rules/noDuplicateMotionPrimitives.ts +19 -0
  268. package/src/rules/noLinearMotion.ts +29 -0
  269. package/src/rules/noNestedSvgFilters.test.ts +47 -0
  270. package/src/rules/noNestedSvgFilters.ts +138 -0
  271. package/src/rules/noRepeatedAssetAcrossShots.ts +19 -0
  272. package/src/rules/noShorthandWithUndefinedLonghand.test.ts +35 -0
  273. package/src/rules/noShorthandWithUndefinedLonghand.ts +140 -0
  274. package/src/rules/noStackedFadesAtACut.ts +30 -0
  275. package/src/rules/noStaticHolds.ts +24 -0
  276. package/src/rules/occlusionVisibleElements.ts +26 -0
  277. package/src/rules/oneContinuousVoTrack.ts +20 -0
  278. package/src/rules/onePunchPerMomentClass.ts +20 -0
  279. package/src/rules/oneRenderPerRound.ts +46 -0
  280. package/src/rules/openStaticAfterAHardCut.ts +29 -0
  281. package/src/rules/openingsAndClosingsCarryMore.ts +21 -0
  282. package/src/rules/persistentDeviceFrames.ts +20 -0
  283. package/src/rules/pixelDiffAnEmptyBandFirst.ts +20 -0
  284. package/src/rules/planIsPublishedNotGating.ts +32 -0
  285. package/src/rules/poweredByRevolaWatermark.ts +19 -0
  286. package/src/rules/predrawContentPresentAtTheCut.ts +21 -0
  287. package/src/rules/premiumSurfacesNotFlatBorders.ts +20 -0
  288. package/src/rules/radarDispositionsAreHumanOnly.ts +28 -0
  289. package/src/rules/realAssetsNotPlaceholders.ts +21 -0
  290. package/src/rules/rebuildUiFromReconNotScreenshots.ts +21 -0
  291. package/src/rules/reconStrandsRunInParallel.ts +63 -0
  292. package/src/rules/registryBeforeHandrolling.ts +20 -0
  293. package/src/rules/remapRateEndpointFormula.ts +19 -0
  294. package/src/rules/remocnContentTailwindChrome.ts +19 -0
  295. package/src/rules/retimeOnlyViaPlaybackOrDesignTokens.ts +18 -0
  296. package/src/rules/reviewTheRenderByFrameSweep.ts +23 -0
  297. package/src/rules/rollingNumberBoundedSequence.ts +21 -0
  298. package/src/rules/rollingNumberSequenceLayoutNone.ts +22 -0
  299. package/src/rules/rollingNumberTightBoxAlignment.ts +19 -0
  300. package/src/rules/rollingNumberWhenNotToUse.ts +24 -0
  301. package/src/rules/rollingNumberWhenToUse.ts +23 -0
  302. package/src/rules/sameFamilySameProject.ts +20 -0
  303. package/src/rules/sampleBrandFromProduct.ts +19 -0
  304. package/src/rules/sampleVelocityFromPreviousFramePosition.ts +21 -0
  305. package/src/rules/sceneRootTagDeclared.test.ts +43 -0
  306. package/src/rules/sceneRootTagDeclared.ts +78 -0
  307. package/src/rules/scrubDemoDataToGenericRoles.ts +19 -0
  308. package/src/rules/selfReviewThePlanForGaps.ts +47 -0
  309. package/src/rules/sourceArchivedBeforeBrief.ts +24 -0
  310. package/src/rules/statisticsSelfExplanatory.ts +18 -0
  311. package/src/rules/stopsAsRelativeSpacing.ts +21 -0
  312. package/src/rules/theWholeCanvasWorks.ts +21 -0
  313. package/src/rules/themePerSection.ts +28 -0
  314. package/src/rules/thesisDeclaredInBrief.ts +25 -0
  315. package/src/rules/tierARequiresPropUnreachableEdit.ts +20 -0
  316. package/src/rules/tinyBodyCopyAsRuledLines.ts +19 -0
  317. package/src/rules/titlesPinnedOutsideCameraPush.ts +22 -0
  318. package/src/rules/tokensNotRawHexes.test.ts +51 -0
  319. package/src/rules/tokensNotRawHexes.ts +89 -0
  320. package/src/rules/typeTokenDefaultedParamsAsString.ts +21 -0
  321. package/src/rules/varyKitChoreographyAcrossSections.ts +21 -0
  322. package/src/rules/varyTheVisualizationGrammar.ts +22 -0
  323. package/src/rules/verifyCheapRenderDear.ts +18 -0
  324. package/src/rules/voAddressesTheViewer.ts +18 -0
  325. package/src/rules/voIsAPerformance.ts +21 -0
  326. package/src/rules/voScriptLocksSpokenWords.ts +21 -0
  327. package/src/rules/wrappersPreserveChildGeometry.ts +22 -0
  328. package/src/sourceCheck.test.ts +50 -0
  329. package/src/sourceCheck.ts +55 -0
  330. package/src/sourceCitations.test.ts +747 -0
  331. package/src/sourceCitations.ts +217 -0
  332. package/src/stageProjection.ts +112 -0
  333. package/src/stages.test.ts +68 -0
  334. package/src/stages.ts +207 -0
  335. package/src/testSupport.ts +16 -0
  336. package/src/types.ts +181 -0
  337. package/src/variants.test.ts +24 -0
  338. package/src/variants.ts +13 -0
  339. package/src/waiverProjection.test.ts +59 -0
  340. package/src/waiverProjection.ts +45 -0
@@ -0,0 +1,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
+ });