@codeyam/codeyam-cli 0.1.0-staging.a77070e → 0.1.0-staging.aa28063

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 (349) hide show
  1. package/analyzer-template/.build-info.json +8 -8
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/package.json +4 -4
  4. package/analyzer-template/packages/ai/src/lib/astScopes/methodSemantics.ts +135 -0
  5. package/analyzer-template/packages/ai/src/lib/astScopes/nodeToSource.ts +19 -0
  6. package/analyzer-template/packages/ai/src/lib/astScopes/paths.ts +11 -4
  7. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +36 -9
  8. package/analyzer-template/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.ts +10 -3
  9. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +16 -6
  10. package/analyzer-template/packages/analyze/index.ts +4 -1
  11. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +28 -2
  12. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +5 -36
  13. package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +10 -6
  14. package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +9 -12
  15. package/analyzer-template/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.ts +21 -0
  16. package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +82 -10
  17. package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
  18. package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
  19. package/analyzer-template/packages/analyze/src/lib/files/analyzeNextRoute.ts +8 -3
  20. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +239 -58
  21. package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +1684 -1462
  22. package/analyzer-template/packages/aws/package.json +6 -6
  23. package/analyzer-template/packages/database/package.json +2 -2
  24. package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +25 -15
  25. package/analyzer-template/packages/database/src/lib/loadEntity.ts +19 -8
  26. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.d.ts.map +1 -1
  27. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js +7 -1
  28. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js.map +1 -1
  29. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts +4 -1
  30. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
  31. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +5 -5
  32. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
  33. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
  34. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
  35. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
  36. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  37. package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
  38. package/analyzer-template/project/analyzeFileEntities.ts +26 -0
  39. package/analyzer-template/project/runMultiScenarioServer.ts +26 -3
  40. package/background/src/lib/virtualized/project/analyzeFileEntities.js +22 -0
  41. package/background/src/lib/virtualized/project/analyzeFileEntities.js.map +1 -1
  42. package/background/src/lib/virtualized/project/runMultiScenarioServer.js +23 -3
  43. package/background/src/lib/virtualized/project/runMultiScenarioServer.js.map +1 -1
  44. package/codeyam-cli/src/cli.js +15 -0
  45. package/codeyam-cli/src/cli.js.map +1 -1
  46. package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js +47 -0
  47. package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js.map +1 -0
  48. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js +71 -0
  49. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js.map +1 -0
  50. package/codeyam-cli/src/commands/__tests__/editor.designSystem.test.js +30 -0
  51. package/codeyam-cli/src/commands/__tests__/editor.designSystem.test.js.map +1 -0
  52. package/codeyam-cli/src/commands/__tests__/editor.statePersistence.test.js +55 -0
  53. package/codeyam-cli/src/commands/__tests__/editor.statePersistence.test.js.map +1 -0
  54. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +9 -9
  55. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +39 -3
  56. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
  57. package/codeyam-cli/src/commands/editor.js +2080 -459
  58. package/codeyam-cli/src/commands/editor.js.map +1 -1
  59. package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js +23 -0
  60. package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js.map +1 -0
  61. package/codeyam-cli/src/commands/init.js +20 -0
  62. package/codeyam-cli/src/commands/init.js.map +1 -1
  63. package/codeyam-cli/src/data/designSystems.js +27 -0
  64. package/codeyam-cli/src/data/designSystems.js.map +1 -0
  65. package/codeyam-cli/src/data/techStacks.js +1 -1
  66. package/codeyam-cli/src/utils/__tests__/editorApi.test.js +44 -0
  67. package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -1
  68. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +2426 -786
  69. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  70. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +66 -0
  71. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -1
  72. package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js +67 -0
  73. package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js.map +1 -0
  74. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +11 -3
  75. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
  76. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +98 -1
  77. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -1
  78. package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js +1108 -0
  79. package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js.map +1 -0
  80. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +120 -0
  81. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
  82. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +160 -1
  83. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  84. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +134 -1
  85. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -1
  86. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +246 -1
  87. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  88. package/codeyam-cli/src/utils/__tests__/envFile.test.js +125 -0
  89. package/codeyam-cli/src/utils/__tests__/envFile.test.js.map +1 -0
  90. package/codeyam-cli/src/utils/__tests__/handoffContext.test.js +500 -0
  91. package/codeyam-cli/src/utils/__tests__/handoffContext.test.js.map +1 -0
  92. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +16 -1
  93. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -1
  94. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
  95. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
  96. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
  97. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
  98. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
  99. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
  100. package/codeyam-cli/src/utils/__tests__/testRunner.test.js +216 -0
  101. package/codeyam-cli/src/utils/__tests__/testRunner.test.js.map +1 -0
  102. package/codeyam-cli/src/utils/__tests__/webappDetection.test.js +6 -0
  103. package/codeyam-cli/src/utils/__tests__/webappDetection.test.js.map +1 -1
  104. package/codeyam-cli/src/utils/analysisRunner.js +36 -7
  105. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  106. package/codeyam-cli/src/utils/analyzer.js +11 -1
  107. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  108. package/codeyam-cli/src/utils/designSystemShowcase.js +810 -0
  109. package/codeyam-cli/src/utils/designSystemShowcase.js.map +1 -0
  110. package/codeyam-cli/src/utils/editorApi.js +16 -0
  111. package/codeyam-cli/src/utils/editorApi.js.map +1 -1
  112. package/codeyam-cli/src/utils/editorAudit.js +385 -32
  113. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  114. package/codeyam-cli/src/utils/editorGuard.js +36 -0
  115. package/codeyam-cli/src/utils/editorGuard.js.map +1 -0
  116. package/codeyam-cli/src/utils/editorPreview.js +5 -3
  117. package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
  118. package/codeyam-cli/src/utils/editorRecapture.js +109 -0
  119. package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
  120. package/codeyam-cli/src/utils/editorRoadmap.js +574 -0
  121. package/codeyam-cli/src/utils/editorRoadmap.js.map +1 -0
  122. package/codeyam-cli/src/utils/editorScenarioSwitch.js +27 -12
  123. package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
  124. package/codeyam-cli/src/utils/editorScenarios.js +108 -5
  125. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  126. package/codeyam-cli/src/utils/editorSeedAdapter.js +69 -16
  127. package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -1
  128. package/codeyam-cli/src/utils/entityChangeStatus.js +30 -2
  129. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
  130. package/codeyam-cli/src/utils/entityChangeStatus.server.js +31 -0
  131. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
  132. package/codeyam-cli/src/utils/envFile.js +90 -0
  133. package/codeyam-cli/src/utils/envFile.js.map +1 -0
  134. package/codeyam-cli/src/utils/handoffContext.js +257 -0
  135. package/codeyam-cli/src/utils/handoffContext.js.map +1 -0
  136. package/codeyam-cli/src/utils/install-skills.js +36 -6
  137. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  138. package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
  139. package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
  140. package/codeyam-cli/src/utils/queue/__tests__/job.interactiveStart.test.js +159 -0
  141. package/codeyam-cli/src/utils/queue/__tests__/job.interactiveStart.test.js.map +1 -0
  142. package/codeyam-cli/src/utils/queue/job.js +35 -6
  143. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  144. package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
  145. package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
  146. package/codeyam-cli/src/utils/scenariosManifest.js +30 -2
  147. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
  148. package/codeyam-cli/src/utils/screenshotHash.js +26 -0
  149. package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
  150. package/codeyam-cli/src/utils/simulationGateMiddleware.js +9 -0
  151. package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
  152. package/codeyam-cli/src/utils/techStackConfig.js +38 -0
  153. package/codeyam-cli/src/utils/techStackConfig.js.map +1 -0
  154. package/codeyam-cli/src/utils/techStackConfig.test.js +85 -0
  155. package/codeyam-cli/src/utils/techStackConfig.test.js.map +1 -0
  156. package/codeyam-cli/src/utils/testResultCache.js +53 -0
  157. package/codeyam-cli/src/utils/testResultCache.js.map +1 -0
  158. package/codeyam-cli/src/utils/testResultCache.server.js +81 -0
  159. package/codeyam-cli/src/utils/testResultCache.server.js.map +1 -0
  160. package/codeyam-cli/src/utils/testResultCache.server.test.js +187 -0
  161. package/codeyam-cli/src/utils/testResultCache.server.test.js.map +1 -0
  162. package/codeyam-cli/src/utils/testResultCache.test.js +230 -0
  163. package/codeyam-cli/src/utils/testResultCache.test.js.map +1 -0
  164. package/codeyam-cli/src/utils/testRunner.js +193 -1
  165. package/codeyam-cli/src/utils/testRunner.js.map +1 -1
  166. package/codeyam-cli/src/utils/webappDetection.js +4 -2
  167. package/codeyam-cli/src/utils/webappDetection.js.map +1 -1
  168. package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js +99 -0
  169. package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js.map +1 -0
  170. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +95 -1
  171. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -1
  172. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +28 -1
  173. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -1
  174. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +145 -11
  175. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  176. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +105 -7
  177. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
  178. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +135 -0
  179. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -0
  180. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +15 -0
  181. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
  182. package/codeyam-cli/src/webserver/app/lib/database.js.map +1 -1
  183. package/codeyam-cli/src/webserver/app/routes/api.interactive-switch-scenario.js +34 -0
  184. package/codeyam-cli/src/webserver/app/routes/api.interactive-switch-scenario.js.map +1 -0
  185. package/codeyam-cli/src/webserver/build/client/assets/{CopyButton-CLe80MMu.js → CopyButton-DTBZZfSk.js} +1 -1
  186. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-Crt_KN_U.js → EntityItem-BxclONWq.js} +1 -1
  187. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-CD7lGABo.js → EntityTypeIcon-BsnEOJZ_.js} +1 -1
  188. package/codeyam-cli/src/webserver/build/client/assets/{InlineSpinner-CgTNOhnu.js → InlineSpinner-ByaELMbv.js} +1 -1
  189. package/codeyam-cli/src/webserver/build/client/assets/{InteractivePreview-CKeQT5Ty.js → InteractivePreview-6WjVfhxX.js} +2 -2
  190. package/codeyam-cli/src/webserver/build/client/assets/{LibraryFunctionPreview-D3s1MFkb.js → LibraryFunctionPreview-ChX-Hp7W.js} +1 -1
  191. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CM5zg40N.js → LogViewer-C-9zQdXg.js} +1 -1
  192. package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-Bs2_Oua4.js +36 -0
  193. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-C2PLkej3.js → ReportIssueModal-DQsceHVv.js} +1 -1
  194. package/codeyam-cli/src/webserver/build/client/assets/{SafeScreenshot-DanvyBPb.js → SafeScreenshot-DThcm_9M.js} +1 -1
  195. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-DUMfcNVK.js → ScenarioViewer-Cl4oOA3A.js} +1 -1
  196. package/codeyam-cli/src/webserver/build/client/assets/Spinner-CIil5-gb.js +34 -0
  197. package/codeyam-cli/src/webserver/build/client/assets/{ViewportInspectBar-BA_Ry-rs.js → ViewportInspectBar-BqkA9zyZ.js} +1 -1
  198. package/codeyam-cli/src/webserver/build/client/assets/{_index-BAWd-Xjf.js → _index-DnOgyseQ.js} +1 -1
  199. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-BOARiB-g.js → activity.(_tab)-DqM9hbNE.js} +1 -1
  200. package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-CHx25PAe.js → addon-web-links-C58dYPwR.js} +1 -1
  201. package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-Bg3e7q4S.js → agent-transcripts-B8NCeOrm.js} +1 -1
  202. package/codeyam-cli/src/webserver/build/client/assets/api.editor-database-verify-l0sNRNKZ.js +1 -0
  203. package/codeyam-cli/src/webserver/build/client/assets/api.editor-github-verify-l0sNRNKZ.js +1 -0
  204. package/codeyam-cli/src/webserver/build/client/assets/api.editor-handoff-l0sNRNKZ.js +1 -0
  205. package/codeyam-cli/src/webserver/build/client/assets/api.editor-hosting-verify-l0sNRNKZ.js +1 -0
  206. package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
  207. package/codeyam-cli/src/webserver/build/client/assets/api.editor-roadmap-l0sNRNKZ.js +1 -0
  208. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
  209. package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
  210. package/codeyam-cli/src/webserver/build/client/assets/api.editor-verify-routes-l0sNRNKZ.js +1 -0
  211. package/codeyam-cli/src/webserver/build/client/assets/api.interactive-switch-scenario-l0sNRNKZ.js +1 -0
  212. package/codeyam-cli/src/webserver/build/client/assets/{book-open-CL-lMgHh.js → book-open-BFSIqZgO.js} +1 -1
  213. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-GmAjGS9-.js → chevron-down-B9fDzFVh.js} +1 -1
  214. package/codeyam-cli/src/webserver/build/client/assets/chunk-UVKPFVEO-Bmq2apuh.js +43 -0
  215. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-DFcQkN5j.js → circle-check-DLPObLUx.js} +1 -1
  216. package/codeyam-cli/src/webserver/build/client/assets/{copy-C6iF61Xs.js → copy-DXEmO0TD.js} +1 -1
  217. package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-4ImjHTVC.js → createLucideIcon-BwyFiRot.js} +1 -1
  218. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-Coe5NhbS.js +1 -0
  219. package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
  220. package/codeyam-cli/src/webserver/build/client/assets/{dev.empty-C8y4mmyv.js → dev.empty-iRhRIFlp.js} +1 -1
  221. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-BZPBzV73.js +1 -0
  222. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DOXe0Qx7.js +161 -0
  223. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-C6fEYHrh.js +41 -0
  224. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-Blfy9UlN.js → entity._sha._-pc-vc6wO.js} +13 -12
  225. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js → entity._sha.scenarios._scenarioId.dev-C8AyYgYT.js} +1 -1
  226. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js → entity._sha.scenarios._scenarioId.fullscreen-DziaVQX1.js} +1 -1
  227. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.create-scenario-DQM8E7L4.js → entity._sha_.create-scenario-BTcpgIpC.js} +1 -1
  228. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-CAoXLsQr.js → entity._sha_.edit._scenarioId-D_O_ajfZ.js} +1 -1
  229. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-SuW9syRS.js → entry.client-j1Vi0bco.js} +6 -6
  230. package/codeyam-cli/src/webserver/build/client/assets/{files-D-xGrg29.js → files-kuny2Q_s.js} +1 -1
  231. package/codeyam-cli/src/webserver/build/client/assets/{git-Bq_fbXP5.js → git-DgCZPMie.js} +1 -1
  232. package/codeyam-cli/src/webserver/build/client/assets/globals-L-aUIeux.css +1 -0
  233. package/codeyam-cli/src/webserver/build/client/assets/{index-Bp1l4hSv.js → index-BliGSSpl.js} +1 -1
  234. package/codeyam-cli/src/webserver/build/client/assets/{index-DE3jI_dv.js → index-SqjQKTdH.js} +1 -1
  235. package/codeyam-cli/src/webserver/build/client/assets/{index-CWV9XZiG.js → index-vyrZD2g4.js} +1 -1
  236. package/codeyam-cli/src/webserver/build/client/assets/{labs-B_IX45ih.js → labs-c3yLxSEp.js} +1 -1
  237. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-De-7qQ2u.js → loader-circle-D-q28GLF.js} +1 -1
  238. package/codeyam-cli/src/webserver/build/client/assets/manifest-30c44d84.js +1 -0
  239. package/codeyam-cli/src/webserver/build/client/assets/{memory-Cx2xEx7s.js → memory-CEWIUC4t.js} +1 -1
  240. package/codeyam-cli/src/webserver/build/client/assets/{pause-CFxEKL1u.js → pause-BP6fitdh.js} +1 -1
  241. package/codeyam-cli/src/webserver/build/client/assets/{root-DB3O9_9j.js → root-CLedrjXQ.js} +26 -13
  242. package/codeyam-cli/src/webserver/build/client/assets/{search-BdBb5aqc.js → search-BooqacKS.js} +1 -1
  243. package/codeyam-cli/src/webserver/build/client/assets/{settings-DdE-Untf.js → settings-BM0nbryO.js} +1 -1
  244. package/codeyam-cli/src/webserver/build/client/assets/{simulations-DSCdE99u.js → simulations-ovy6FjRY.js} +1 -1
  245. package/codeyam-cli/src/webserver/build/client/assets/{terminal-CrplD4b1.js → terminal-DHemCJIs.js} +1 -1
  246. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-DqJ0j69l.js → triangle-alert-D87ekDl8.js} +1 -1
  247. package/codeyam-cli/src/webserver/build/client/assets/{useCustomSizes-DhXHbEjP.js → useCustomSizes-Dk0Tciqg.js} +1 -1
  248. package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-C8QvIe05.js +2 -0
  249. package/codeyam-cli/src/webserver/build/client/assets/{useReportContext-Cy5Qg_UR.js → useReportContext-jkCytuYz.js} +1 -1
  250. package/codeyam-cli/src/webserver/build/client/assets/{useToast-5HR2j9ZE.js → useToast-BgqkixU9.js} +1 -1
  251. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-CuR5TvUx.js +16 -0
  252. package/codeyam-cli/src/webserver/build/server/assets/{index-DxB0pOSt.js → index-D4MWAsqb.js} +1 -1
  253. package/codeyam-cli/src/webserver/build/server/assets/init-JObA4lXD.js +14 -0
  254. package/codeyam-cli/src/webserver/build/server/assets/server-build-i8OXK4oL.js +765 -0
  255. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  256. package/codeyam-cli/src/webserver/build-info.json +5 -5
  257. package/codeyam-cli/src/webserver/editorProxy.js +132 -7
  258. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  259. package/codeyam-cli/src/webserver/idleDetector.js +27 -3
  260. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
  261. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +17 -0
  262. package/codeyam-cli/src/webserver/server.js +119 -14
  263. package/codeyam-cli/src/webserver/server.js.map +1 -1
  264. package/codeyam-cli/src/webserver/terminalServer.js +235 -37
  265. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  266. package/codeyam-cli/templates/__tests__/editor-step-hook.prompt-capture.test.ts +118 -0
  267. package/codeyam-cli/templates/codeyam-editor-claude.md +2 -0
  268. package/codeyam-cli/templates/codeyam-editor-codex.md +61 -0
  269. package/codeyam-cli/templates/codeyam-editor-gemini.md +59 -0
  270. package/codeyam-cli/templates/codeyam-editor-reference.md +9 -7
  271. package/codeyam-cli/templates/design-systems/clean-dashboard-design-system.md +255 -0
  272. package/codeyam-cli/templates/design-systems/editorial-design-system.md +267 -0
  273. package/codeyam-cli/templates/design-systems/mono-brutalist-design-system.md +256 -0
  274. package/codeyam-cli/templates/design-systems/neo-brutalist-design-system.md +294 -0
  275. package/codeyam-cli/templates/editor-step-hook.py +93 -46
  276. package/codeyam-cli/templates/expo-react-native/MOBILE_SETUP.md +204 -5
  277. package/codeyam-cli/templates/expo-react-native/__tests__/.gitkeep +0 -0
  278. package/codeyam-cli/templates/expo-react-native/app/_layout.tsx +6 -3
  279. package/codeyam-cli/templates/expo-react-native/app/index.tsx +36 -0
  280. package/codeyam-cli/templates/expo-react-native/app.json +11 -0
  281. package/codeyam-cli/templates/expo-react-native/babel.config.js +1 -0
  282. package/codeyam-cli/templates/expo-react-native/gitignore +2 -0
  283. package/codeyam-cli/templates/expo-react-native/global.css +7 -0
  284. package/codeyam-cli/templates/expo-react-native/lib/theme.ts +73 -0
  285. package/codeyam-cli/templates/expo-react-native/package.json +32 -16
  286. package/codeyam-cli/templates/expo-react-native/patches/expo-modules-autolinking+3.0.24.patch +29 -0
  287. package/codeyam-cli/templates/isolation-route/expo-router.tsx.template +54 -0
  288. package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +1 -0
  289. package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +47 -34
  290. package/codeyam-cli/templates/seed-adapters/supabase.ts +271 -78
  291. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +34 -1
  292. package/package.json +1 -1
  293. package/packages/ai/src/lib/astScopes/methodSemantics.js +99 -0
  294. package/packages/ai/src/lib/astScopes/methodSemantics.js.map +1 -1
  295. package/packages/ai/src/lib/astScopes/nodeToSource.js +16 -0
  296. package/packages/ai/src/lib/astScopes/nodeToSource.js.map +1 -1
  297. package/packages/ai/src/lib/astScopes/paths.js +12 -3
  298. package/packages/ai/src/lib/astScopes/paths.js.map +1 -1
  299. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +27 -10
  300. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  301. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
  302. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
  303. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
  304. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
  305. package/packages/analyze/index.js +1 -1
  306. package/packages/analyze/index.js.map +1 -1
  307. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
  308. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
  309. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
  310. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  311. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
  312. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
  313. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
  314. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
  315. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
  316. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
  317. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
  318. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
  319. package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
  320. package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
  321. package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
  322. package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
  323. package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
  324. package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
  325. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +120 -28
  326. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  327. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +1368 -1193
  328. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  329. package/packages/database/src/lib/loadAnalysis.js +7 -1
  330. package/packages/database/src/lib/loadAnalysis.js.map +1 -1
  331. package/packages/database/src/lib/loadEntity.js +5 -5
  332. package/packages/database/src/lib/loadEntity.js.map +1 -1
  333. package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
  334. package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  335. package/codeyam-cli/src/webserver/build/client/assets/Spinner-D0LgAaSa.js +0 -34
  336. package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BAdwhyCx.js +0 -43
  337. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
  338. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +0 -1
  339. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Bnx7yUP0.js +0 -58
  340. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +0 -41
  341. package/codeyam-cli/src/webserver/build/client/assets/globals-fAqOD9ex.css +0 -1
  342. package/codeyam-cli/src/webserver/build/client/assets/manifest-3157d6b8.js +0 -1
  343. package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-BNd5hYuW.js +0 -2
  344. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-BMmkgAkg.js +0 -13
  345. package/codeyam-cli/src/webserver/build/server/assets/init-DLYLaqqP.js +0 -10
  346. package/codeyam-cli/src/webserver/build/server/assets/server-build-CcyitQLQ.js +0 -551
  347. package/codeyam-cli/templates/expo-react-native/app/(tabs)/_layout.tsx +0 -33
  348. package/codeyam-cli/templates/expo-react-native/app/(tabs)/index.tsx +0 -12
  349. package/codeyam-cli/templates/expo-react-native/app/(tabs)/settings.tsx +0 -12
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { Kysely, SqliteDialect } from 'kysely';
3
- import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } from "../editorAudit.js";
3
+ import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isOnlyPreExistingIncomplete, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, aggregateClientErrorsByComponent, determineTargetedAnalysisPaths, shouldAutoRecapture, } from "../editorAudit.js";
4
4
  describe('editorAudit', () => {
5
5
  describe('isComponent', () => {
6
6
  it('should return true for JSX.Element return type', () => {
@@ -29,6 +29,16 @@ describe('editorAudit', () => {
29
29
  it('should return false when returnType is empty string', () => {
30
30
  expect(isComponent({ returnType: '' })).toBe(false);
31
31
  });
32
+ it('should return true for page files (index.tsx) even without returnType', () => {
33
+ expect(isComponent({ filePath: 'app/(tabs)/index.tsx' })).toBe(true);
34
+ });
35
+ it('should return true for page.tsx files even without returnType', () => {
36
+ expect(isComponent({ filePath: 'app/dashboard/page.tsx' })).toBe(true);
37
+ });
38
+ it('should return false for non-page files without returnType', () => {
39
+ expect(isComponent({ filePath: 'app/hooks/useCounter.ts' })).toBe(false);
40
+ expect(isComponent({ filePath: 'lib/storage.ts' })).toBe(false);
41
+ });
32
42
  });
33
43
  describe('classifyGlossaryEntries', () => {
34
44
  it('should classify entries with JSX return types as components', () => {
@@ -553,6 +563,142 @@ describe('editorAudit', () => {
553
563
  expect(result.summary.componentsOk).toBe(1);
554
564
  expect(result.summary.componentsWithErrors).toBe(1);
555
565
  });
566
+ // ── Test case count integration ─────────────────────────────────
567
+ it('should include testCaseCount when testCaseCounts is provided', () => {
568
+ const result = computeAudit({
569
+ components: [],
570
+ functions: [
571
+ {
572
+ name: 'calculatePrice',
573
+ filePath: 'app/lib/pricing.ts',
574
+ testFile: 'app/lib/pricing.test.ts',
575
+ },
576
+ ],
577
+ scenarioCounts: {},
578
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
579
+ testResults: {
580
+ 'app/lib/pricing.test.ts': {
581
+ passing: true,
582
+ hasEntityNameDescribe: true,
583
+ },
584
+ },
585
+ testCaseCounts: { calculatePrice: 5 },
586
+ });
587
+ expect(result.functions[0].testCaseCount).toBe(5);
588
+ });
589
+ it('should not include testCaseCount when testCaseCounts is not provided', () => {
590
+ const result = computeAudit({
591
+ components: [],
592
+ functions: [
593
+ {
594
+ name: 'calculatePrice',
595
+ filePath: 'app/lib/pricing.ts',
596
+ testFile: 'app/lib/pricing.test.ts',
597
+ },
598
+ ],
599
+ scenarioCounts: {},
600
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
601
+ testResults: {
602
+ 'app/lib/pricing.test.ts': {
603
+ passing: true,
604
+ hasEntityNameDescribe: true,
605
+ },
606
+ },
607
+ });
608
+ expect(result.functions[0].testCaseCount).toBeUndefined();
609
+ });
610
+ it('should count functionsThinCoverage for ok functions with fewer than 3 test cases', () => {
611
+ const result = computeAudit({
612
+ components: [],
613
+ functions: [
614
+ {
615
+ name: 'fnWellTested',
616
+ filePath: 'a.ts',
617
+ testFile: 'a.test.ts',
618
+ },
619
+ {
620
+ name: 'fnThinCoverage',
621
+ filePath: 'b.ts',
622
+ testFile: 'b.test.ts',
623
+ },
624
+ {
625
+ name: 'fnAlsoThin',
626
+ filePath: 'c.ts',
627
+ testFile: 'c.test.ts',
628
+ },
629
+ ],
630
+ scenarioCounts: {},
631
+ testFileExistence: {
632
+ 'a.test.ts': true,
633
+ 'b.test.ts': true,
634
+ 'c.test.ts': true,
635
+ },
636
+ testResults: {
637
+ 'a.test.ts': { passing: true, hasEntityNameDescribe: true },
638
+ 'b.test.ts': { passing: true, hasEntityNameDescribe: true },
639
+ 'c.test.ts': { passing: true, hasEntityNameDescribe: true },
640
+ },
641
+ testCaseCounts: { fnWellTested: 5, fnThinCoverage: 2, fnAlsoThin: 1 },
642
+ });
643
+ expect(result.summary.functionsThinCoverage).toBe(2);
644
+ });
645
+ it('should not count failing functions in functionsThinCoverage', () => {
646
+ const result = computeAudit({
647
+ components: [],
648
+ functions: [
649
+ {
650
+ name: 'fnFailing',
651
+ filePath: 'a.ts',
652
+ testFile: 'a.test.ts',
653
+ },
654
+ ],
655
+ scenarioCounts: {},
656
+ testFileExistence: { 'a.test.ts': true },
657
+ testResults: {
658
+ 'a.test.ts': { passing: false, hasEntityNameDescribe: true },
659
+ },
660
+ testCaseCounts: { fnFailing: 1 },
661
+ });
662
+ expect(result.summary.functionsThinCoverage).toBe(0);
663
+ });
664
+ it('should not block allPassing due to thin test coverage', () => {
665
+ const result = computeAudit({
666
+ components: [],
667
+ functions: [
668
+ {
669
+ name: 'fnThin',
670
+ filePath: 'a.ts',
671
+ testFile: 'a.test.ts',
672
+ },
673
+ ],
674
+ scenarioCounts: {},
675
+ testFileExistence: { 'a.test.ts': true },
676
+ testResults: {
677
+ 'a.test.ts': { passing: true, hasEntityNameDescribe: true },
678
+ },
679
+ testCaseCounts: { fnThin: 1 },
680
+ });
681
+ expect(result.summary.allPassing).toBe(true);
682
+ expect(result.summary.functionsThinCoverage).toBe(1);
683
+ });
684
+ it('should default functionsThinCoverage to 0 when no testCaseCounts provided', () => {
685
+ const result = computeAudit({
686
+ components: [],
687
+ functions: [
688
+ {
689
+ name: 'fn',
690
+ filePath: 'a.ts',
691
+ testFile: 'a.test.ts',
692
+ },
693
+ ],
694
+ scenarioCounts: {},
695
+ testFileExistence: { 'a.test.ts': true },
696
+ testResults: {
697
+ 'a.test.ts': { passing: true, hasEntityNameDescribe: true },
698
+ },
699
+ });
700
+ expect(result.summary.functionsThinCoverage).toBe(0);
701
+ });
556
702
  });
557
703
  // ── filterGlossaryByChangeStatus ──────────────────────────────────
558
704
  describe('filterGlossaryByChangeStatus', () => {
@@ -1466,8 +1612,11 @@ describe('editorAudit', () => {
1466
1612
  expect(auditResult.summary.incompleteEntities).toBeUndefined();
1467
1613
  });
1468
1614
  });
1469
- // ── queryMiscategorizedScenarios ─────────────────────────────────────
1470
- describe('queryMiscategorizedScenarios', () => {
1615
+ // ── filterToIncompleteFilePaths ──────────────────────────────────────
1616
+ describe('filterToIncompleteFilePaths', () => {
1617
+ // analyze-imports processes ALL file paths (~120 files) even when only
1618
+ // a few need analysis. This function filters to files that have no
1619
+ // entity with an analysis record.
1471
1620
  let db;
1472
1621
  let rawDb;
1473
1622
  const projectId = 'test-project-id';
@@ -1475,296 +1624,134 @@ describe('editorAudit', () => {
1475
1624
  rawDb = new Database(':memory:');
1476
1625
  db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1477
1626
  await db.schema
1478
- .createTable('editor_scenarios')
1627
+ .createTable('analyses')
1479
1628
  .addColumn('id', 'varchar', (col) => col.primaryKey())
1480
- .addColumn('project_id', 'varchar', (col) => col.notNull())
1481
- .addColumn('name', 'varchar', (col) => col.notNull())
1482
- .addColumn('component_name', 'varchar')
1483
- .addColumn('component_path', 'varchar')
1484
1629
  .addColumn('entity_sha', 'varchar')
1485
- .addColumn('display_name', 'varchar')
1486
- .addColumn('page_file_path', 'varchar')
1487
- .addColumn('url', 'varchar')
1488
- .addColumn('created_at', 'datetime')
1489
- .addColumn('updated_at', 'datetime')
1630
+ .addColumn('entity_name', 'varchar')
1631
+ .addColumn('project_id', 'varchar')
1632
+ .execute();
1633
+ await db.schema
1634
+ .createTable('entities')
1635
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1636
+ .addColumn('name', 'varchar')
1637
+ .addColumn('entity_type', 'varchar')
1638
+ .addColumn('file_path', 'varchar')
1490
1639
  .execute();
1491
1640
  });
1492
1641
  afterEach(async () => {
1493
1642
  await db.destroy();
1494
1643
  });
1495
- it('should return empty when all component scenarios use isolation routes', async () => {
1644
+ it('should exclude files that have an entity with an analysis', async () => {
1645
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1646
+ // Entity with analysis — skip
1496
1647
  await db
1497
- .insertInto('editor_scenarios')
1648
+ .insertInto('entities')
1498
1649
  .values({
1499
- id: 'sc-1',
1500
- project_id: projectId,
1501
- name: 'LibraryCard - Default',
1502
- component_name: 'LibraryCard',
1503
- url: '/isolated-components/LibraryCard?s=Default',
1504
- created_at: '2026-03-17 12:00:00',
1650
+ sha: 'sha-header',
1651
+ name: 'Header',
1652
+ entity_type: 'visual',
1653
+ file_path: 'app/components/Header.tsx',
1505
1654
  })
1506
1655
  .execute();
1507
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1508
- expect(result).toEqual([]);
1509
- });
1510
- it('should flag component scenarios that use non-isolation URLs', async () => {
1511
- // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1512
- // but url=/library — it's pointing at the real page, not an isolation route
1513
1656
  await db
1514
- .insertInto('editor_scenarios')
1657
+ .insertInto('analyses')
1515
1658
  .values({
1516
- id: 'sc-1',
1659
+ id: 'a-1',
1660
+ entity_sha: 'sha-header',
1661
+ entity_name: 'Header',
1517
1662
  project_id: projectId,
1518
- name: 'Full Library Page',
1519
- component_name: 'LibraryPage',
1520
- url: '/library',
1521
- created_at: '2026-03-17 12:41:40',
1522
1663
  })
1523
1664
  .execute();
1665
+ // Entity without analysis — needs analysis
1524
1666
  await db
1525
- .insertInto('editor_scenarios')
1667
+ .insertInto('entities')
1526
1668
  .values({
1527
- id: 'sc-2',
1528
- project_id: projectId,
1529
- name: 'Empty Library Page',
1530
- component_name: 'LibraryPage',
1531
- url: '/library',
1532
- created_at: '2026-03-17 12:41:51',
1669
+ sha: 'sha-rule',
1670
+ name: 'RuleBuilder',
1671
+ entity_type: 'visual',
1672
+ file_path: 'app/components/RuleBuilder.tsx',
1533
1673
  })
1534
1674
  .execute();
1535
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1536
- expect(result).toEqual([
1537
- {
1538
- componentName: 'LibraryPage',
1539
- scenarioNames: ['Full Library Page', 'Empty Library Page'],
1540
- url: '/library',
1541
- },
1542
- ]);
1675
+ const allFilePaths = [
1676
+ 'app/components/Header.tsx',
1677
+ 'app/components/RuleBuilder.tsx',
1678
+ 'app/components/Footer.tsx', // no entity — needs analysis
1679
+ ];
1680
+ const result = await filterToIncompleteFilePaths(db, projectId, allFilePaths);
1681
+ expect(result).toContain('app/components/RuleBuilder.tsx');
1682
+ expect(result).toContain('app/components/Footer.tsx');
1683
+ expect(result).not.toContain('app/components/Header.tsx');
1543
1684
  });
1544
- it('should not flag page-level scenarios (no component_name)', async () => {
1545
- // App-level scenarios have no component_name — they're fine with real URLs
1546
- await db
1547
- .insertInto('editor_scenarios')
1548
- .values({
1549
- id: 'sc-1',
1550
- project_id: projectId,
1551
- name: 'Library with Articles',
1552
- url: '/',
1553
- created_at: '2026-03-17 12:25:14',
1554
- })
1555
- .execute();
1556
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1557
- expect(result).toEqual([]);
1685
+ it('should return all file paths when no entities exist yet', async () => {
1686
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1687
+ const filePaths = ['app/Foo.tsx', 'app/Bar.tsx'];
1688
+ const result = await filterToIncompleteFilePaths(db, projectId, filePaths);
1689
+ expect(result).toEqual(filePaths);
1558
1690
  });
1559
- it('should group miscategorized scenarios by component and URL', async () => {
1560
- // Two different components both misusing real URLs
1691
+ it('should return empty when all files have analyzed entities', async () => {
1692
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1561
1693
  await db
1562
- .insertInto('editor_scenarios')
1694
+ .insertInto('entities')
1563
1695
  .values({
1564
- id: 'sc-1',
1565
- project_id: projectId,
1566
- name: 'Full Library Page',
1567
- component_name: 'LibraryPage',
1568
- url: '/library',
1569
- created_at: '2026-03-17 12:41:40',
1696
+ sha: 'sha-a',
1697
+ name: 'CompA',
1698
+ entity_type: 'visual',
1699
+ file_path: 'app/CompA.tsx',
1570
1700
  })
1571
1701
  .execute();
1572
1702
  await db
1573
- .insertInto('editor_scenarios')
1703
+ .insertInto('analyses')
1574
1704
  .values({
1575
- id: 'sc-2',
1705
+ id: 'a-1',
1706
+ entity_sha: 'sha-a',
1707
+ entity_name: 'CompA',
1576
1708
  project_id: projectId,
1577
- name: 'Dashboard - Full',
1578
- component_name: 'Dashboard',
1579
- url: '/dashboard',
1580
- created_at: '2026-03-17 12:50:00',
1581
1709
  })
1582
1710
  .execute();
1583
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1584
- expect(result).toHaveLength(2);
1585
- expect(result.map((r) => r.componentName).sort()).toEqual([
1586
- 'Dashboard',
1587
- 'LibraryPage',
1711
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1712
+ 'app/CompA.tsx',
1588
1713
  ]);
1714
+ expect(result).toEqual([]);
1589
1715
  });
1590
- it('should scope to session when featureStartedAt is provided', async () => {
1591
- // Old miscategorized scenario before session
1592
- await db
1593
- .insertInto('editor_scenarios')
1594
- .values({
1595
- id: 'sc-old',
1596
- project_id: projectId,
1597
- name: 'Old Page',
1598
- component_name: 'OldComponent',
1599
- url: '/old',
1600
- created_at: '2026-03-16 10:00:00',
1601
- })
1602
- .execute();
1603
- // New miscategorized scenario — in session
1716
+ it('should skip files with analysis even without scenarios', async () => {
1717
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1718
+ // Entity with analysis but NO scenarios — still complete
1604
1719
  await db
1605
- .insertInto('editor_scenarios')
1720
+ .insertInto('entities')
1606
1721
  .values({
1607
- id: 'sc-new',
1608
- project_id: projectId,
1609
- name: 'Full Library Page',
1610
- component_name: 'LibraryPage',
1611
- url: '/library',
1612
- created_at: '2026-03-17 12:41:40',
1722
+ sha: 'sha-util',
1723
+ name: 'utils',
1724
+ entity_type: 'library',
1725
+ file_path: 'app/lib/utils.ts',
1613
1726
  })
1614
1727
  .execute();
1615
- const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
1616
- expect(result).toHaveLength(1);
1617
- expect(result[0].componentName).toBe('LibraryPage');
1618
- });
1619
- it('should not flag component scenarios with null URL', async () => {
1620
1728
  await db
1621
- .insertInto('editor_scenarios')
1729
+ .insertInto('analyses')
1622
1730
  .values({
1623
- id: 'sc-1',
1731
+ id: 'a-1',
1732
+ entity_sha: 'sha-util',
1733
+ entity_name: 'utils',
1624
1734
  project_id: projectId,
1625
- name: 'NoUrl - Default',
1626
- component_name: 'NoUrl',
1627
- created_at: '2026-03-17 12:00:00',
1628
1735
  })
1629
1736
  .execute();
1630
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1737
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1738
+ 'app/lib/utils.ts',
1739
+ ]);
1631
1740
  expect(result).toEqual([]);
1632
1741
  });
1633
1742
  });
1634
- // ── isOnlyIncompleteEntities ─────────────────────────────────────────
1635
- describe('isOnlyIncompleteEntities', () => {
1636
- it('should return true when incompleteEntities is the only failure', () => {
1637
- expect(isOnlyIncompleteEntities({
1638
- componentsMissing: 0,
1639
- componentsWithErrors: 0,
1640
- functionsFailing: 0,
1641
- functionsNameMismatch: 0,
1642
- functionsMissing: 0,
1643
- missingFromGlossary: 0,
1644
- incompleteEntities: 3,
1645
- allPassing: false,
1646
- })).toBe(true);
1647
- });
1648
- it('should return false when there are also missing components', () => {
1649
- expect(isOnlyIncompleteEntities({
1650
- componentsMissing: 1,
1651
- componentsWithErrors: 0,
1652
- functionsFailing: 0,
1653
- functionsNameMismatch: 0,
1654
- functionsMissing: 0,
1655
- missingFromGlossary: 0,
1656
- incompleteEntities: 2,
1657
- allPassing: false,
1658
- })).toBe(false);
1659
- });
1660
- it('should return false when there are also failing tests', () => {
1661
- expect(isOnlyIncompleteEntities({
1662
- componentsMissing: 0,
1663
- componentsWithErrors: 0,
1664
- functionsFailing: 1,
1665
- functionsNameMismatch: 0,
1666
- functionsMissing: 0,
1667
- missingFromGlossary: 0,
1668
- incompleteEntities: 2,
1669
- allPassing: false,
1670
- })).toBe(false);
1671
- });
1672
- it('should return false when there are also missing glossary entries', () => {
1673
- expect(isOnlyIncompleteEntities({
1674
- componentsMissing: 0,
1675
- componentsWithErrors: 0,
1676
- functionsFailing: 0,
1677
- functionsNameMismatch: 0,
1678
- functionsMissing: 0,
1679
- missingFromGlossary: 1,
1680
- incompleteEntities: 2,
1681
- allPassing: false,
1682
- })).toBe(false);
1683
- });
1684
- it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
1685
- // Edge case: all zeros means nothing is failing
1686
- expect(isOnlyIncompleteEntities({
1687
- componentsMissing: 0,
1688
- componentsWithErrors: 0,
1689
- functionsFailing: 0,
1690
- functionsNameMismatch: 0,
1691
- functionsMissing: 0,
1692
- missingFromGlossary: 0,
1693
- incompleteEntities: 0,
1694
- allPassing: true,
1695
- })).toBe(true);
1696
- });
1697
- it('should return false when there are also miscategorized scenarios', () => {
1698
- expect(isOnlyIncompleteEntities({
1699
- componentsMissing: 0,
1700
- componentsWithErrors: 0,
1701
- functionsFailing: 0,
1702
- functionsNameMismatch: 0,
1703
- functionsMissing: 0,
1704
- missingFromGlossary: 0,
1705
- miscategorizedScenarios: 1,
1706
- incompleteEntities: 2,
1707
- allPassing: false,
1708
- })).toBe(false);
1709
- });
1710
- it('should handle missing fields gracefully', () => {
1711
- // Summary from older API version might not have all fields
1712
- expect(isOnlyIncompleteEntities({
1713
- incompleteEntities: 2,
1714
- allPassing: false,
1715
- })).toBe(true);
1716
- });
1717
- });
1718
- // ── isAutoRemediable ─────────────────────────────────────────────────
1719
- describe('isAutoRemediable', () => {
1720
- it('should return true on first attempt when only incomplete entities', () => {
1721
- const result = isAutoRemediable({
1722
- componentsMissing: 0,
1723
- componentsWithErrors: 0,
1724
- functionsFailing: 0,
1725
- functionsNameMismatch: 0,
1726
- functionsMissing: 0,
1727
- missingFromGlossary: 0,
1728
- miscategorizedScenarios: 0,
1729
- incompleteEntities: 3,
1730
- allPassing: false,
1731
- }, false);
1732
- expect(result).toBe(true);
1733
- });
1734
- it('should return false on second attempt (already tried once)', () => {
1735
- // This is the key fix: if we already tried analyze-imports and
1736
- // entities are STILL incomplete, don't try again — report the failure
1737
- const result = isAutoRemediable({
1738
- componentsMissing: 0,
1739
- componentsWithErrors: 0,
1740
- functionsFailing: 0,
1741
- functionsNameMismatch: 0,
1742
- functionsMissing: 0,
1743
- missingFromGlossary: 0,
1744
- miscategorizedScenarios: 0,
1745
- incompleteEntities: 3,
1746
- allPassing: false,
1747
- }, true);
1748
- expect(result).toBe(false);
1749
- });
1750
- it('should return false when there are other failures besides incomplete entities', () => {
1751
- const result = isAutoRemediable({
1752
- componentsMissing: 1,
1753
- incompleteEntities: 3,
1754
- allPassing: false,
1755
- }, false);
1756
- expect(result).toBe(false);
1757
- });
1758
- it('should return false when there are no incomplete entities', () => {
1759
- const result = isAutoRemediable({
1760
- componentsMissing: 1,
1761
- allPassing: false,
1762
- }, false);
1763
- expect(result).toBe(false);
1764
- });
1765
- });
1766
- // ── queryIncompleteEntities ─────────────────────────────────────────
1767
- describe('queryIncompleteEntities', () => {
1743
+ // ── phantom entity SHAs ─────────────────────────────────────────────
1744
+ describe('queryIncompleteEntities with phantom entity SHAs', () => {
1745
+ // Root cause of the Margo/reader step-blocking bug:
1746
+ // Scenarios registered without component_path got a phantom entity_sha
1747
+ // computed from component_name alone. These SHAs have NO entity record
1748
+ // in the entities table and NO analyses. syncScenarioEntityShas can't
1749
+ // fix them (it skips scenarios without component_path). So they remain
1750
+ // "incomplete" forever, blocking step progression.
1751
+ //
1752
+ // Fix: queryIncompleteEntities should not report scenarios whose
1753
+ // entity_sha has no entity record — these are orphaned data, not
1754
+ // fixable by running analyze-imports.
1768
1755
  let db;
1769
1756
  let rawDb;
1770
1757
  const projectId = 'test-project-id';
@@ -1803,123 +1790,154 @@ describe('editorAudit', () => {
1803
1790
  afterEach(async () => {
1804
1791
  await db.destroy();
1805
1792
  });
1806
- it('should return empty when all scenario entity SHAs have analyses', async () => {
1807
- // Entity with analysis
1793
+ it('should not report scenarios with phantom entity SHAs (no entity record exists)', async () => {
1794
+ // Real entity — has an entity record, analyses, and scenarios. Complete.
1808
1795
  await db
1809
1796
  .insertInto('entities')
1810
1797
  .values({
1811
- sha: 'sha-header',
1812
- name: 'Header',
1798
+ sha: 'sha-real',
1799
+ name: 'RuleBuilder',
1813
1800
  entity_type: 'visual',
1814
- file_path: 'src/Header.tsx',
1801
+ file_path: 'app/components/RuleBuilder.tsx',
1815
1802
  })
1816
1803
  .execute();
1817
1804
  await db
1818
1805
  .insertInto('analyses')
1819
1806
  .values({
1820
1807
  id: 'a-1',
1821
- entity_sha: 'sha-header',
1822
- entity_name: 'Header',
1808
+ entity_sha: 'sha-real',
1809
+ entity_name: 'RuleBuilder',
1823
1810
  project_id: projectId,
1824
1811
  })
1825
1812
  .execute();
1826
1813
  await db
1827
1814
  .insertInto('editor_scenarios')
1828
1815
  .values({
1829
- id: 'sc-1',
1816
+ id: 'sc-good',
1830
1817
  project_id: projectId,
1831
- name: 'Header - Default',
1832
- component_name: 'Header',
1833
- entity_sha: 'sha-header',
1818
+ name: 'RuleBuilder - Empty',
1819
+ component_name: 'RuleBuilder',
1820
+ component_path: 'app/components/RuleBuilder.tsx',
1821
+ entity_sha: 'sha-real',
1834
1822
  created_at: '2026-03-16 23:00:00',
1835
1823
  })
1836
1824
  .execute();
1837
- const result = await queryIncompleteEntities(db, projectId, null);
1838
- expect(result).toEqual([]);
1825
+ // Phantom entity scenario points to a SHA that doesn't exist
1826
+ // in the entities table (registered without component_path).
1827
+ // No entity record, no analyses, unfixable by analyze-imports.
1828
+ await db
1829
+ .insertInto('editor_scenarios')
1830
+ .values({
1831
+ id: 'sc-phantom',
1832
+ project_id: projectId,
1833
+ name: 'Empty',
1834
+ component_name: 'RuleBuilder',
1835
+ component_path: null,
1836
+ entity_sha: 'sha-phantom-no-entity-record',
1837
+ created_at: '2026-03-16 22:00:00',
1838
+ })
1839
+ .execute();
1840
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1841
+ // Should NOT report phantom SHAs as incomplete — they can't be fixed
1842
+ // by running analyze-imports (no entity record exists to resolve).
1843
+ expect(incomplete).toHaveLength(0);
1839
1844
  });
1840
- it('should return entities with scenarios but no analyses', async () => {
1841
- // Entity WITHOUT analysis
1845
+ it('should still report real incomplete entities (entity exists but no analysis)', async () => {
1846
+ // Real entity without analysis — this IS a legitimate incomplete entity
1842
1847
  await db
1843
1848
  .insertInto('entities')
1844
1849
  .values({
1845
- sha: 'sha-chips',
1846
- name: 'CollectionChips',
1850
+ sha: 'sha-noanalysis',
1851
+ name: 'Footer',
1847
1852
  entity_type: 'visual',
1848
- file_path: 'src/components/CollectionChips.tsx',
1853
+ file_path: 'app/components/Footer.tsx',
1849
1854
  })
1850
1855
  .execute();
1851
- // Scenario referencing that entity
1852
1856
  await db
1853
1857
  .insertInto('editor_scenarios')
1854
1858
  .values({
1855
1859
  id: 'sc-1',
1856
1860
  project_id: projectId,
1857
- name: 'CollectionChips - Default',
1858
- component_name: 'CollectionChips',
1859
- entity_sha: 'sha-chips',
1861
+ name: 'Footer - Default',
1862
+ component_name: 'Footer',
1863
+ component_path: 'app/components/Footer.tsx',
1864
+ entity_sha: 'sha-noanalysis',
1860
1865
  created_at: '2026-03-16 23:00:00',
1861
1866
  })
1862
1867
  .execute();
1868
+ // Phantom scenario (shouldn't affect results)
1863
1869
  await db
1864
1870
  .insertInto('editor_scenarios')
1865
1871
  .values({
1866
- id: 'sc-2',
1872
+ id: 'sc-phantom',
1867
1873
  project_id: projectId,
1868
- name: 'CollectionChips - Many',
1869
- component_name: 'CollectionChips',
1870
- entity_sha: 'sha-chips',
1871
- created_at: '2026-03-16 23:01:00',
1874
+ name: 'Footer - Alt',
1875
+ component_name: 'Footer',
1876
+ component_path: null,
1877
+ entity_sha: 'sha-phantom-does-not-exist',
1878
+ created_at: '2026-03-16 22:00:00',
1872
1879
  })
1873
1880
  .execute();
1874
- const result = await queryIncompleteEntities(db, projectId, null);
1875
- expect(result).toEqual([
1876
- {
1877
- entitySha: 'sha-chips',
1878
- name: 'CollectionChips',
1879
- scenarioCount: 2,
1880
- preExisting: false,
1881
- },
1882
- ]);
1881
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1882
+ // Should report Footer (real entity, no analysis) but NOT the phantom
1883
+ expect(incomplete).toHaveLength(1);
1884
+ expect(incomplete[0].name).toBe('Footer');
1885
+ expect(incomplete[0].entitySha).toBe('sha-noanalysis');
1883
1886
  });
1884
- it('should only return entities without analyses, not those with analyses', async () => {
1885
- // Entity WITH analysis (Header)
1886
- await db
1887
- .insertInto('entities')
1888
- .values({
1889
- sha: 'sha-header',
1890
- name: 'Header',
1891
- entity_type: 'visual',
1892
- file_path: 'src/Header.tsx',
1893
- })
1887
+ });
1888
+ // ── queryMiscategorizedScenarios ─────────────────────────────────────
1889
+ describe('queryMiscategorizedScenarios', () => {
1890
+ let db;
1891
+ let rawDb;
1892
+ const projectId = 'test-project-id';
1893
+ beforeEach(async () => {
1894
+ rawDb = new Database(':memory:');
1895
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1896
+ await db.schema
1897
+ .createTable('editor_scenarios')
1898
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1899
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1900
+ .addColumn('name', 'varchar', (col) => col.notNull())
1901
+ .addColumn('component_name', 'varchar')
1902
+ .addColumn('component_path', 'varchar')
1903
+ .addColumn('entity_sha', 'varchar')
1904
+ .addColumn('display_name', 'varchar')
1905
+ .addColumn('page_file_path', 'varchar')
1906
+ .addColumn('url', 'varchar')
1907
+ .addColumn('created_at', 'datetime')
1908
+ .addColumn('updated_at', 'datetime')
1894
1909
  .execute();
1910
+ });
1911
+ afterEach(async () => {
1912
+ await db.destroy();
1913
+ });
1914
+ it('should return empty when all component scenarios use isolation routes', async () => {
1895
1915
  await db
1896
- .insertInto('analyses')
1916
+ .insertInto('editor_scenarios')
1897
1917
  .values({
1898
- id: 'a-1',
1899
- entity_sha: 'sha-header',
1900
- entity_name: 'Header',
1918
+ id: 'sc-1',
1901
1919
  project_id: projectId,
1920
+ name: 'LibraryCard - Default',
1921
+ component_name: 'LibraryCard',
1922
+ url: '/isolated-components/LibraryCard?s=Default',
1923
+ created_at: '2026-03-17 12:00:00',
1902
1924
  })
1903
1925
  .execute();
1926
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1927
+ expect(result).toEqual([]);
1928
+ });
1929
+ it('should flag component scenarios that use non-isolation URLs', async () => {
1930
+ // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1931
+ // but url=/library — it's pointing at the real page, not an isolation route
1904
1932
  await db
1905
1933
  .insertInto('editor_scenarios')
1906
1934
  .values({
1907
1935
  id: 'sc-1',
1908
1936
  project_id: projectId,
1909
- name: 'Header - Default',
1910
- component_name: 'Header',
1911
- entity_sha: 'sha-header',
1912
- created_at: '2026-03-16 23:00:00',
1913
- })
1914
- .execute();
1915
- // Entity WITHOUT analysis (CollectionPicker)
1916
- await db
1917
- .insertInto('entities')
1918
- .values({
1919
- sha: 'sha-picker',
1920
- name: 'CollectionPicker',
1921
- entity_type: 'visual',
1922
- file_path: 'src/components/CollectionPicker.tsx',
1937
+ name: 'Full Library Page',
1938
+ component_name: 'LibraryPage',
1939
+ url: '/library',
1940
+ created_at: '2026-03-17 12:41:40',
1923
1941
  })
1924
1942
  .execute();
1925
1943
  await db
@@ -1927,338 +1945,809 @@ describe('editorAudit', () => {
1927
1945
  .values({
1928
1946
  id: 'sc-2',
1929
1947
  project_id: projectId,
1930
- name: 'CollectionPicker - Default',
1931
- component_name: 'CollectionPicker',
1932
- entity_sha: 'sha-picker',
1933
- created_at: '2026-03-16 23:00:00',
1948
+ name: 'Empty Library Page',
1949
+ component_name: 'LibraryPage',
1950
+ url: '/library',
1951
+ created_at: '2026-03-17 12:41:51',
1934
1952
  })
1935
1953
  .execute();
1936
- const result = await queryIncompleteEntities(db, projectId, null);
1954
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1937
1955
  expect(result).toEqual([
1938
1956
  {
1939
- entitySha: 'sha-picker',
1940
- name: 'CollectionPicker',
1941
- scenarioCount: 1,
1942
- preExisting: false,
1957
+ componentName: 'LibraryPage',
1958
+ scenarioNames: ['Full Library Page', 'Empty Library Page'],
1959
+ url: '/library',
1943
1960
  },
1944
1961
  ]);
1945
1962
  });
1946
- it('should return both pre-session and in-session entities with preExisting flags', async () => {
1947
- // Entity without analysis, scenario created BEFORE session
1948
- await db
1949
- .insertInto('entities')
1950
- .values({
1951
- sha: 'sha-old',
1952
- name: 'OldComponent',
1953
- entity_type: 'visual',
1954
- file_path: 'src/OldComponent.tsx',
1955
- })
1956
- .execute();
1957
- await db
1958
- .insertInto('editor_scenarios')
1959
- .values({
1960
- id: 'sc-old',
1961
- project_id: projectId,
1962
- name: 'OldComponent - Default',
1963
- component_name: 'OldComponent',
1964
- entity_sha: 'sha-old',
1965
- created_at: '2026-03-16 20:00:00',
1966
- })
1967
- .execute();
1968
- // Entity without analysis, scenario created DURING session
1969
- await db
1970
- .insertInto('entities')
1971
- .values({
1972
- sha: 'sha-new',
1973
- name: 'NewComponent',
1974
- entity_type: 'visual',
1975
- file_path: 'src/NewComponent.tsx',
1976
- })
1977
- .execute();
1963
+ it('should not flag page-level scenarios (no component_name)', async () => {
1964
+ // App-level scenarios have no component_name they're fine with real URLs
1978
1965
  await db
1979
1966
  .insertInto('editor_scenarios')
1980
1967
  .values({
1981
- id: 'sc-new',
1968
+ id: 'sc-1',
1982
1969
  project_id: projectId,
1983
- name: 'NewComponent - Default',
1984
- component_name: 'NewComponent',
1985
- entity_sha: 'sha-new',
1986
- created_at: '2026-03-16 23:10:00',
1970
+ name: 'Library with Articles',
1971
+ url: '/',
1972
+ created_at: '2026-03-17 12:25:14',
1987
1973
  })
1988
1974
  .execute();
1989
- const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
1990
- // Both should be returned — OldComponent is preExisting, NewComponent is not
1991
- expect(result).toEqual(expect.arrayContaining([
1992
- {
1993
- entitySha: 'sha-old',
1994
- name: 'OldComponent',
1995
- scenarioCount: 1,
1996
- preExisting: true,
1997
- },
1998
- {
1999
- entitySha: 'sha-new',
2000
- name: 'NewComponent',
2001
- scenarioCount: 1,
2002
- preExisting: false,
2003
- },
2004
- ]));
2005
- expect(result).toHaveLength(2);
1975
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1976
+ expect(result).toEqual([]);
2006
1977
  });
2007
- it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
2008
- await db
2009
- .insertInto('entities')
2010
- .values({
2011
- sha: 'sha-updated',
2012
- name: 'UpdatedComponent',
2013
- entity_type: 'visual',
2014
- file_path: 'src/Updated.tsx',
2015
- })
2016
- .execute();
1978
+ it('should group miscategorized scenarios by component and URL', async () => {
1979
+ // Two different components both misusing real URLs
2017
1980
  await db
2018
1981
  .insertInto('editor_scenarios')
2019
1982
  .values({
2020
- id: 'sc-updated',
1983
+ id: 'sc-1',
2021
1984
  project_id: projectId,
2022
- name: 'UpdatedComponent - Default',
2023
- component_name: 'UpdatedComponent',
2024
- entity_sha: 'sha-updated',
2025
- created_at: '2026-03-16 20:00:00',
2026
- updated_at: '2026-03-16 23:20:00', // Updated in session
1985
+ name: 'Full Library Page',
1986
+ component_name: 'LibraryPage',
1987
+ url: '/library',
1988
+ created_at: '2026-03-17 12:41:40',
2027
1989
  })
2028
1990
  .execute();
2029
- const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2030
- expect(result).toEqual([
2031
- {
2032
- entitySha: 'sha-updated',
2033
- name: 'UpdatedComponent',
2034
- scenarioCount: 1,
2035
- preExisting: false,
2036
- },
2037
- ]);
2038
- });
2039
- it('should skip scenarios with null entity_sha', async () => {
2040
1991
  await db
2041
1992
  .insertInto('editor_scenarios')
2042
1993
  .values({
2043
- id: 'sc-null',
1994
+ id: 'sc-2',
2044
1995
  project_id: projectId,
2045
- name: 'Orphan Scenario',
2046
- component_name: 'Orphan',
2047
- created_at: '2026-03-16 23:00:00',
1996
+ name: 'Dashboard - Full',
1997
+ component_name: 'Dashboard',
1998
+ url: '/dashboard',
1999
+ created_at: '2026-03-17 12:50:00',
2048
2000
  })
2049
2001
  .execute();
2050
- const result = await queryIncompleteEntities(db, projectId, null);
2051
- expect(result).toEqual([]);
2052
- });
2053
- it('should return empty when there are no scenarios', async () => {
2054
- const result = await queryIncompleteEntities(db, projectId, null);
2055
- expect(result).toEqual([]);
2002
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
2003
+ expect(result).toHaveLength(2);
2004
+ expect(result.map((r) => r.componentName).sort()).toEqual([
2005
+ 'Dashboard',
2006
+ 'LibraryPage',
2007
+ ]);
2056
2008
  });
2057
- it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
2058
- // Old entity version WITH analysis
2059
- await db
2060
- .insertInto('entities')
2061
- .values({
2062
- sha: 'sha-btn-v1',
2063
- name: 'OpenLibraryButton',
2064
- entity_type: 'visual',
2065
- file_path: 'src/components/OpenLibraryButton.tsx',
2066
- })
2067
- .execute();
2068
- await db
2069
- .insertInto('analyses')
2070
- .values({
2071
- id: 'a-btn-v1',
2072
- entity_sha: 'sha-btn-v1',
2073
- entity_name: 'OpenLibraryButton',
2074
- project_id: projectId,
2075
- })
2076
- .execute();
2077
- // New entity version WITHOUT analysis (created by file watcher)
2078
- await db
2079
- .insertInto('entities')
2080
- .values({
2081
- sha: 'sha-btn-v2',
2082
- name: 'OpenLibraryButton',
2083
- entity_type: 'visual',
2084
- file_path: 'src/components/OpenLibraryButton.tsx',
2085
- })
2086
- .execute();
2087
- // Scenario points to the NEW version (backfilled after file watcher)
2009
+ it('should scope to session when featureStartedAt is provided', async () => {
2010
+ // Old miscategorized scenario before session
2088
2011
  await db
2089
2012
  .insertInto('editor_scenarios')
2090
2013
  .values({
2091
- id: 'sc-btn',
2014
+ id: 'sc-old',
2092
2015
  project_id: projectId,
2093
- name: 'OpenLibraryButton - Default',
2094
- component_name: 'OpenLibraryButton',
2095
- entity_sha: 'sha-btn-v2',
2096
- created_at: '2026-03-16 23:00:00',
2097
- })
2098
- .execute();
2099
- // Should NOT flag as incomplete — sibling version has analyses
2100
- const result = await queryIncompleteEntities(db, projectId, null);
2101
- expect(result).toEqual([]);
2102
- });
2103
- it('should still flag entities when no sibling version has analyses', async () => {
2104
- // Only one version, no analyses
2105
- await db
2106
- .insertInto('entities')
2107
- .values({
2108
- sha: 'sha-icon',
2109
- name: 'ExternalLinkIcon',
2110
- entity_type: 'visual',
2111
- file_path: 'src/components/ExternalLinkIcon.tsx',
2016
+ name: 'Old Page',
2017
+ component_name: 'OldComponent',
2018
+ url: '/old',
2019
+ created_at: '2026-03-16 10:00:00',
2112
2020
  })
2113
2021
  .execute();
2022
+ // New miscategorized scenario — in session
2114
2023
  await db
2115
2024
  .insertInto('editor_scenarios')
2116
2025
  .values({
2117
- id: 'sc-icon',
2026
+ id: 'sc-new',
2118
2027
  project_id: projectId,
2119
- name: 'ExternalLinkIcon - Default',
2120
- component_name: 'ExternalLinkIcon',
2121
- entity_sha: 'sha-icon',
2122
- created_at: '2026-03-16 23:00:00',
2028
+ name: 'Full Library Page',
2029
+ component_name: 'LibraryPage',
2030
+ url: '/library',
2031
+ created_at: '2026-03-17 12:41:40',
2123
2032
  })
2124
2033
  .execute();
2125
- // Should flag as incomplete no version has analyses
2126
- const result = await queryIncompleteEntities(db, projectId, null);
2127
- expect(result).toEqual([
2128
- {
2129
- entitySha: 'sha-icon',
2130
- name: 'ExternalLinkIcon',
2131
- scenarioCount: 1,
2132
- preExisting: false,
2133
- },
2134
- ]);
2034
+ const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
2035
+ expect(result).toHaveLength(1);
2036
+ expect(result[0].componentName).toBe('LibraryPage');
2135
2037
  });
2136
- it('should use entity name from entities table, falling back to component_name', async () => {
2137
- // Scenario has entity_sha but entity record doesn't exist
2038
+ it('should not flag component scenarios with null URL', async () => {
2138
2039
  await db
2139
2040
  .insertInto('editor_scenarios')
2140
2041
  .values({
2141
2042
  id: 'sc-1',
2142
2043
  project_id: projectId,
2143
- name: 'Ghost - Default',
2144
- component_name: 'GhostComponent',
2145
- entity_sha: 'sha-ghost',
2146
- created_at: '2026-03-16 23:00:00',
2147
- })
2148
- .execute();
2149
- const result = await queryIncompleteEntities(db, projectId, null);
2150
- expect(result).toEqual([
2151
- {
2152
- entitySha: 'sha-ghost',
2153
- name: 'GhostComponent',
2154
- scenarioCount: 1,
2155
- preExisting: false,
2156
- },
2157
- ]);
2158
- });
2159
- it('should detect incomplete entity whose scenario predates the session', async () => {
2160
- // Entity with no analyses, scenario created BEFORE session
2161
- await db
2162
- .insertInto('entities')
2163
- .values({
2164
- sha: 'sha-preexisting',
2165
- name: 'PreExistingComponent',
2166
- entity_type: 'visual',
2167
- file_path: 'src/PreExistingComponent.tsx',
2168
- })
2169
- .execute();
2170
- await db
2171
- .insertInto('editor_scenarios')
2172
- .values({
2173
- id: 'sc-preexisting',
2174
- project_id: projectId,
2175
- name: 'PreExistingComponent - Default',
2176
- component_name: 'PreExistingComponent',
2177
- entity_sha: 'sha-preexisting',
2178
- created_at: '2026-03-16 20:00:00',
2179
- updated_at: '2026-03-16 20:00:00',
2044
+ name: 'NoUrl - Default',
2045
+ component_name: 'NoUrl',
2046
+ created_at: '2026-03-17 12:00:00',
2180
2047
  })
2181
2048
  .execute();
2182
- // Session started well after scenario was created/updated
2183
- const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2184
- // Should still be detected — the old time filter would have excluded it
2185
- expect(result).toEqual([
2186
- {
2187
- entitySha: 'sha-preexisting',
2188
- name: 'PreExistingComponent',
2189
- scenarioCount: 1,
2190
- preExisting: true,
2191
- },
2192
- ]);
2049
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
2050
+ expect(result).toEqual([]);
2193
2051
  });
2194
- it('should flag preExisting: true when all scenarios predate the session', async () => {
2052
+ });
2053
+ // ── isOnlyIncompleteEntities ─────────────────────────────────────────
2054
+ describe('isOnlyIncompleteEntities', () => {
2055
+ it('should return true when incompleteEntities is the only failure', () => {
2056
+ expect(isOnlyIncompleteEntities({
2057
+ componentsMissing: 0,
2058
+ componentsWithErrors: 0,
2059
+ functionsFailing: 0,
2060
+ functionsNameMismatch: 0,
2061
+ functionsMissing: 0,
2062
+ missingFromGlossary: 0,
2063
+ incompleteEntities: 3,
2064
+ allPassing: false,
2065
+ })).toBe(true);
2066
+ });
2067
+ it('should return false when there are also missing components', () => {
2068
+ expect(isOnlyIncompleteEntities({
2069
+ componentsMissing: 1,
2070
+ componentsWithErrors: 0,
2071
+ functionsFailing: 0,
2072
+ functionsNameMismatch: 0,
2073
+ functionsMissing: 0,
2074
+ missingFromGlossary: 0,
2075
+ incompleteEntities: 2,
2076
+ allPassing: false,
2077
+ })).toBe(false);
2078
+ });
2079
+ it('should return false when there are also failing tests', () => {
2080
+ expect(isOnlyIncompleteEntities({
2081
+ componentsMissing: 0,
2082
+ componentsWithErrors: 0,
2083
+ functionsFailing: 1,
2084
+ functionsNameMismatch: 0,
2085
+ functionsMissing: 0,
2086
+ missingFromGlossary: 0,
2087
+ incompleteEntities: 2,
2088
+ allPassing: false,
2089
+ })).toBe(false);
2090
+ });
2091
+ it('should return false when there are also missing glossary entries', () => {
2092
+ expect(isOnlyIncompleteEntities({
2093
+ componentsMissing: 0,
2094
+ componentsWithErrors: 0,
2095
+ functionsFailing: 0,
2096
+ functionsNameMismatch: 0,
2097
+ functionsMissing: 0,
2098
+ missingFromGlossary: 1,
2099
+ incompleteEntities: 2,
2100
+ allPassing: false,
2101
+ })).toBe(false);
2102
+ });
2103
+ it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
2104
+ // Edge case: all zeros means nothing is failing
2105
+ expect(isOnlyIncompleteEntities({
2106
+ componentsMissing: 0,
2107
+ componentsWithErrors: 0,
2108
+ functionsFailing: 0,
2109
+ functionsNameMismatch: 0,
2110
+ functionsMissing: 0,
2111
+ missingFromGlossary: 0,
2112
+ incompleteEntities: 0,
2113
+ allPassing: true,
2114
+ })).toBe(true);
2115
+ });
2116
+ it('should return false when there are also miscategorized scenarios', () => {
2117
+ expect(isOnlyIncompleteEntities({
2118
+ componentsMissing: 0,
2119
+ componentsWithErrors: 0,
2120
+ functionsFailing: 0,
2121
+ functionsNameMismatch: 0,
2122
+ functionsMissing: 0,
2123
+ missingFromGlossary: 0,
2124
+ miscategorizedScenarios: 1,
2125
+ incompleteEntities: 2,
2126
+ allPassing: false,
2127
+ })).toBe(false);
2128
+ });
2129
+ it('should handle missing fields gracefully', () => {
2130
+ // Summary from older API version might not have all fields
2131
+ expect(isOnlyIncompleteEntities({
2132
+ incompleteEntities: 2,
2133
+ allPassing: false,
2134
+ })).toBe(true);
2135
+ });
2136
+ it('should return false when there are also runner errors', () => {
2137
+ // functionsRunnerError means the test runner crashed — a real failure
2138
+ // that cannot be fixed by entity SHA backfill or analyze-imports.
2139
+ // If this returns true, checkAuditGate would attempt a useless backfill
2140
+ // instead of reporting the runner error, and isOnlyPreExistingIncomplete
2141
+ // could let the gate pass entirely.
2142
+ expect(isOnlyIncompleteEntities({
2143
+ componentsMissing: 0,
2144
+ componentsWithErrors: 0,
2145
+ functionsFailing: 0,
2146
+ functionsRunnerError: 2,
2147
+ functionsNameMismatch: 0,
2148
+ functionsMissing: 0,
2149
+ missingFromGlossary: 0,
2150
+ incompleteEntities: 1,
2151
+ allPassing: false,
2152
+ })).toBe(false);
2153
+ });
2154
+ });
2155
+ // ── isAutoRemediable ─────────────────────────────────────────────────
2156
+ describe('isAutoRemediable', () => {
2157
+ // isAutoRemediable always returns false — the audit never triggers
2158
+ // full analyze-imports inline. It takes minutes on large projects.
2159
+ // Only lightweight backfill is acceptable during audit.
2160
+ it('should return false even on first attempt with only incomplete entities', () => {
2161
+ const result = isAutoRemediable({
2162
+ componentsMissing: 0,
2163
+ componentsWithErrors: 0,
2164
+ functionsFailing: 0,
2165
+ functionsNameMismatch: 0,
2166
+ functionsMissing: 0,
2167
+ missingFromGlossary: 0,
2168
+ miscategorizedScenarios: 0,
2169
+ incompleteEntities: 3,
2170
+ allPassing: false,
2171
+ }, false);
2172
+ expect(result).toBe(false);
2173
+ });
2174
+ it('should return false on second attempt', () => {
2175
+ const result = isAutoRemediable({
2176
+ componentsMissing: 0,
2177
+ componentsWithErrors: 0,
2178
+ functionsFailing: 0,
2179
+ functionsNameMismatch: 0,
2180
+ functionsMissing: 0,
2181
+ missingFromGlossary: 0,
2182
+ miscategorizedScenarios: 0,
2183
+ incompleteEntities: 3,
2184
+ allPassing: false,
2185
+ }, true);
2186
+ expect(result).toBe(false);
2187
+ });
2188
+ it('should return false when there are other failures besides incomplete entities', () => {
2189
+ const result = isAutoRemediable({
2190
+ componentsMissing: 1,
2191
+ incompleteEntities: 3,
2192
+ allPassing: false,
2193
+ }, false);
2194
+ expect(result).toBe(false);
2195
+ });
2196
+ it('should return false when there are no incomplete entities', () => {
2197
+ const result = isAutoRemediable({
2198
+ componentsMissing: 1,
2199
+ allPassing: false,
2200
+ }, false);
2201
+ expect(result).toBe(false);
2202
+ });
2203
+ });
2204
+ // ── queryIncompleteEntities ─────────────────────────────────────────
2205
+ describe('queryIncompleteEntities', () => {
2206
+ let db;
2207
+ let rawDb;
2208
+ const projectId = 'test-project-id';
2209
+ beforeEach(async () => {
2210
+ rawDb = new Database(':memory:');
2211
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
2212
+ await db.schema
2213
+ .createTable('editor_scenarios')
2214
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
2215
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
2216
+ .addColumn('name', 'varchar', (col) => col.notNull())
2217
+ .addColumn('component_name', 'varchar')
2218
+ .addColumn('component_path', 'varchar')
2219
+ .addColumn('entity_sha', 'varchar')
2220
+ .addColumn('display_name', 'varchar')
2221
+ .addColumn('page_file_path', 'varchar')
2222
+ .addColumn('url', 'varchar')
2223
+ .addColumn('created_at', 'datetime')
2224
+ .addColumn('updated_at', 'datetime')
2225
+ .execute();
2226
+ await db.schema
2227
+ .createTable('analyses')
2228
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
2229
+ .addColumn('entity_sha', 'varchar')
2230
+ .addColumn('entity_name', 'varchar')
2231
+ .addColumn('project_id', 'varchar')
2232
+ .execute();
2233
+ await db.schema
2234
+ .createTable('entities')
2235
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
2236
+ .addColumn('name', 'varchar')
2237
+ .addColumn('entity_type', 'varchar')
2238
+ .addColumn('file_path', 'varchar')
2239
+ .execute();
2240
+ });
2241
+ afterEach(async () => {
2242
+ await db.destroy();
2243
+ });
2244
+ it('should return empty when all scenario entity SHAs have analyses', async () => {
2245
+ // Entity with analysis
2195
2246
  await db
2196
2247
  .insertInto('entities')
2197
2248
  .values({
2198
- sha: 'sha-old-entity',
2199
- name: 'OldEntity',
2249
+ sha: 'sha-header',
2250
+ name: 'Header',
2200
2251
  entity_type: 'visual',
2201
- file_path: 'src/OldEntity.tsx',
2252
+ file_path: 'src/Header.tsx',
2202
2253
  })
2203
2254
  .execute();
2204
- // Two scenarios, both before session
2205
2255
  await db
2206
- .insertInto('editor_scenarios')
2256
+ .insertInto('analyses')
2207
2257
  .values({
2208
- id: 'sc-old-1',
2258
+ id: 'a-1',
2259
+ entity_sha: 'sha-header',
2260
+ entity_name: 'Header',
2209
2261
  project_id: projectId,
2210
- name: 'OldEntity - Default',
2211
- component_name: 'OldEntity',
2212
- entity_sha: 'sha-old-entity',
2213
- created_at: '2026-03-16 19:00:00',
2214
- updated_at: '2026-03-16 19:00:00',
2215
2262
  })
2216
2263
  .execute();
2217
2264
  await db
2218
2265
  .insertInto('editor_scenarios')
2219
2266
  .values({
2220
- id: 'sc-old-2',
2267
+ id: 'sc-1',
2221
2268
  project_id: projectId,
2222
- name: 'OldEntity - Hover',
2223
- component_name: 'OldEntity',
2224
- entity_sha: 'sha-old-entity',
2225
- created_at: '2026-03-16 19:30:00',
2226
- updated_at: '2026-03-16 19:30:00',
2269
+ name: 'Header - Default',
2270
+ component_name: 'Header',
2271
+ entity_sha: 'sha-header',
2272
+ created_at: '2026-03-16 23:00:00',
2227
2273
  })
2228
2274
  .execute();
2229
- const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2230
- expect(result).toEqual([
2231
- {
2232
- entitySha: 'sha-old-entity',
2233
- name: 'OldEntity',
2234
- scenarioCount: 2,
2235
- preExisting: true,
2236
- },
2237
- ]);
2275
+ const result = await queryIncompleteEntities(db, projectId, null);
2276
+ expect(result).toEqual([]);
2238
2277
  });
2239
- it('should flag preExisting: false when scenario is from the current session', async () => {
2278
+ it('should return entities with scenarios but no analyses', async () => {
2279
+ // Entity WITHOUT analysis
2240
2280
  await db
2241
2281
  .insertInto('entities')
2242
2282
  .values({
2243
- sha: 'sha-session-entity',
2244
- name: 'SessionEntity',
2283
+ sha: 'sha-chips',
2284
+ name: 'CollectionChips',
2245
2285
  entity_type: 'visual',
2246
- file_path: 'src/SessionEntity.tsx',
2286
+ file_path: 'src/components/CollectionChips.tsx',
2247
2287
  })
2248
2288
  .execute();
2289
+ // Scenario referencing that entity
2249
2290
  await db
2250
2291
  .insertInto('editor_scenarios')
2251
2292
  .values({
2252
- id: 'sc-session',
2293
+ id: 'sc-1',
2253
2294
  project_id: projectId,
2254
- name: 'SessionEntity - Default',
2255
- component_name: 'SessionEntity',
2256
- entity_sha: 'sha-session-entity',
2257
- created_at: '2026-03-16 23:30:00',
2258
- updated_at: '2026-03-16 23:30:00',
2295
+ name: 'CollectionChips - Default',
2296
+ component_name: 'CollectionChips',
2297
+ entity_sha: 'sha-chips',
2298
+ created_at: '2026-03-16 23:00:00',
2259
2299
  })
2260
2300
  .execute();
2261
- const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2301
+ await db
2302
+ .insertInto('editor_scenarios')
2303
+ .values({
2304
+ id: 'sc-2',
2305
+ project_id: projectId,
2306
+ name: 'CollectionChips - Many',
2307
+ component_name: 'CollectionChips',
2308
+ entity_sha: 'sha-chips',
2309
+ created_at: '2026-03-16 23:01:00',
2310
+ })
2311
+ .execute();
2312
+ const result = await queryIncompleteEntities(db, projectId, null);
2313
+ expect(result).toEqual([
2314
+ {
2315
+ entitySha: 'sha-chips',
2316
+ name: 'CollectionChips',
2317
+ scenarioCount: 2,
2318
+ preExisting: false,
2319
+ },
2320
+ ]);
2321
+ });
2322
+ it('should only return entities without analyses, not those with analyses', async () => {
2323
+ // Entity WITH analysis (Header)
2324
+ await db
2325
+ .insertInto('entities')
2326
+ .values({
2327
+ sha: 'sha-header',
2328
+ name: 'Header',
2329
+ entity_type: 'visual',
2330
+ file_path: 'src/Header.tsx',
2331
+ })
2332
+ .execute();
2333
+ await db
2334
+ .insertInto('analyses')
2335
+ .values({
2336
+ id: 'a-1',
2337
+ entity_sha: 'sha-header',
2338
+ entity_name: 'Header',
2339
+ project_id: projectId,
2340
+ })
2341
+ .execute();
2342
+ await db
2343
+ .insertInto('editor_scenarios')
2344
+ .values({
2345
+ id: 'sc-1',
2346
+ project_id: projectId,
2347
+ name: 'Header - Default',
2348
+ component_name: 'Header',
2349
+ entity_sha: 'sha-header',
2350
+ created_at: '2026-03-16 23:00:00',
2351
+ })
2352
+ .execute();
2353
+ // Entity WITHOUT analysis (CollectionPicker)
2354
+ await db
2355
+ .insertInto('entities')
2356
+ .values({
2357
+ sha: 'sha-picker',
2358
+ name: 'CollectionPicker',
2359
+ entity_type: 'visual',
2360
+ file_path: 'src/components/CollectionPicker.tsx',
2361
+ })
2362
+ .execute();
2363
+ await db
2364
+ .insertInto('editor_scenarios')
2365
+ .values({
2366
+ id: 'sc-2',
2367
+ project_id: projectId,
2368
+ name: 'CollectionPicker - Default',
2369
+ component_name: 'CollectionPicker',
2370
+ entity_sha: 'sha-picker',
2371
+ created_at: '2026-03-16 23:00:00',
2372
+ })
2373
+ .execute();
2374
+ const result = await queryIncompleteEntities(db, projectId, null);
2375
+ expect(result).toEqual([
2376
+ {
2377
+ entitySha: 'sha-picker',
2378
+ name: 'CollectionPicker',
2379
+ scenarioCount: 1,
2380
+ preExisting: false,
2381
+ },
2382
+ ]);
2383
+ });
2384
+ it('should return both pre-session and in-session entities with preExisting flags', async () => {
2385
+ // Entity without analysis, scenario created BEFORE session
2386
+ await db
2387
+ .insertInto('entities')
2388
+ .values({
2389
+ sha: 'sha-old',
2390
+ name: 'OldComponent',
2391
+ entity_type: 'visual',
2392
+ file_path: 'src/OldComponent.tsx',
2393
+ })
2394
+ .execute();
2395
+ await db
2396
+ .insertInto('editor_scenarios')
2397
+ .values({
2398
+ id: 'sc-old',
2399
+ project_id: projectId,
2400
+ name: 'OldComponent - Default',
2401
+ component_name: 'OldComponent',
2402
+ entity_sha: 'sha-old',
2403
+ created_at: '2026-03-16 20:00:00',
2404
+ })
2405
+ .execute();
2406
+ // Entity without analysis, scenario created DURING session
2407
+ await db
2408
+ .insertInto('entities')
2409
+ .values({
2410
+ sha: 'sha-new',
2411
+ name: 'NewComponent',
2412
+ entity_type: 'visual',
2413
+ file_path: 'src/NewComponent.tsx',
2414
+ })
2415
+ .execute();
2416
+ await db
2417
+ .insertInto('editor_scenarios')
2418
+ .values({
2419
+ id: 'sc-new',
2420
+ project_id: projectId,
2421
+ name: 'NewComponent - Default',
2422
+ component_name: 'NewComponent',
2423
+ entity_sha: 'sha-new',
2424
+ created_at: '2026-03-16 23:10:00',
2425
+ })
2426
+ .execute();
2427
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2428
+ // Both should be returned — OldComponent is preExisting, NewComponent is not
2429
+ expect(result).toEqual(expect.arrayContaining([
2430
+ {
2431
+ entitySha: 'sha-old',
2432
+ name: 'OldComponent',
2433
+ scenarioCount: 1,
2434
+ preExisting: true,
2435
+ },
2436
+ {
2437
+ entitySha: 'sha-new',
2438
+ name: 'NewComponent',
2439
+ scenarioCount: 1,
2440
+ preExisting: false,
2441
+ },
2442
+ ]));
2443
+ expect(result).toHaveLength(2);
2444
+ });
2445
+ it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
2446
+ await db
2447
+ .insertInto('entities')
2448
+ .values({
2449
+ sha: 'sha-updated',
2450
+ name: 'UpdatedComponent',
2451
+ entity_type: 'visual',
2452
+ file_path: 'src/Updated.tsx',
2453
+ })
2454
+ .execute();
2455
+ await db
2456
+ .insertInto('editor_scenarios')
2457
+ .values({
2458
+ id: 'sc-updated',
2459
+ project_id: projectId,
2460
+ name: 'UpdatedComponent - Default',
2461
+ component_name: 'UpdatedComponent',
2462
+ entity_sha: 'sha-updated',
2463
+ created_at: '2026-03-16 20:00:00',
2464
+ updated_at: '2026-03-16 23:20:00', // Updated in session
2465
+ })
2466
+ .execute();
2467
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2468
+ expect(result).toEqual([
2469
+ {
2470
+ entitySha: 'sha-updated',
2471
+ name: 'UpdatedComponent',
2472
+ scenarioCount: 1,
2473
+ preExisting: false,
2474
+ },
2475
+ ]);
2476
+ });
2477
+ it('should skip scenarios with null entity_sha', async () => {
2478
+ await db
2479
+ .insertInto('editor_scenarios')
2480
+ .values({
2481
+ id: 'sc-null',
2482
+ project_id: projectId,
2483
+ name: 'Orphan Scenario',
2484
+ component_name: 'Orphan',
2485
+ created_at: '2026-03-16 23:00:00',
2486
+ })
2487
+ .execute();
2488
+ const result = await queryIncompleteEntities(db, projectId, null);
2489
+ expect(result).toEqual([]);
2490
+ });
2491
+ it('should return empty when there are no scenarios', async () => {
2492
+ const result = await queryIncompleteEntities(db, projectId, null);
2493
+ expect(result).toEqual([]);
2494
+ });
2495
+ it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
2496
+ // Old entity version WITH analysis
2497
+ await db
2498
+ .insertInto('entities')
2499
+ .values({
2500
+ sha: 'sha-btn-v1',
2501
+ name: 'OpenLibraryButton',
2502
+ entity_type: 'visual',
2503
+ file_path: 'src/components/OpenLibraryButton.tsx',
2504
+ })
2505
+ .execute();
2506
+ await db
2507
+ .insertInto('analyses')
2508
+ .values({
2509
+ id: 'a-btn-v1',
2510
+ entity_sha: 'sha-btn-v1',
2511
+ entity_name: 'OpenLibraryButton',
2512
+ project_id: projectId,
2513
+ })
2514
+ .execute();
2515
+ // New entity version WITHOUT analysis (created by file watcher)
2516
+ await db
2517
+ .insertInto('entities')
2518
+ .values({
2519
+ sha: 'sha-btn-v2',
2520
+ name: 'OpenLibraryButton',
2521
+ entity_type: 'visual',
2522
+ file_path: 'src/components/OpenLibraryButton.tsx',
2523
+ })
2524
+ .execute();
2525
+ // Scenario points to the NEW version (backfilled after file watcher)
2526
+ await db
2527
+ .insertInto('editor_scenarios')
2528
+ .values({
2529
+ id: 'sc-btn',
2530
+ project_id: projectId,
2531
+ name: 'OpenLibraryButton - Default',
2532
+ component_name: 'OpenLibraryButton',
2533
+ entity_sha: 'sha-btn-v2',
2534
+ created_at: '2026-03-16 23:00:00',
2535
+ })
2536
+ .execute();
2537
+ // Should NOT flag as incomplete — sibling version has analyses
2538
+ const result = await queryIncompleteEntities(db, projectId, null);
2539
+ expect(result).toEqual([]);
2540
+ });
2541
+ it('should flag entity when sibling has analyses but different filePath (extracted component)', async () => {
2542
+ // Old entity version WITH analysis at ORIGINAL file path
2543
+ await db
2544
+ .insertInto('entities')
2545
+ .values({
2546
+ sha: 'sha-card-v1',
2547
+ name: 'TaskCard',
2548
+ entity_type: 'visual',
2549
+ file_path: 'app/page.tsx',
2550
+ })
2551
+ .execute();
2552
+ await db
2553
+ .insertInto('analyses')
2554
+ .values({
2555
+ id: 'a-card-v1',
2556
+ entity_sha: 'sha-card-v1',
2557
+ entity_name: 'TaskCard',
2558
+ project_id: projectId,
2559
+ })
2560
+ .execute();
2561
+ // New entity version WITHOUT analysis at EXTRACTED file path
2562
+ await db
2563
+ .insertInto('entities')
2564
+ .values({
2565
+ sha: 'sha-card-v2',
2566
+ name: 'TaskCard',
2567
+ entity_type: 'visual',
2568
+ file_path: 'app/components/TaskCard.tsx',
2569
+ })
2570
+ .execute();
2571
+ // Scenario points to the new version (synced by syncScenarioEntityShas)
2572
+ await db
2573
+ .insertInto('editor_scenarios')
2574
+ .values({
2575
+ id: 'sc-card',
2576
+ project_id: projectId,
2577
+ name: 'TaskCard - Default',
2578
+ component_name: 'TaskCard',
2579
+ entity_sha: 'sha-card-v2',
2580
+ created_at: '2026-03-16 23:00:00',
2581
+ })
2582
+ .execute();
2583
+ // SHOULD flag as incomplete — sibling has analyses but at a different filePath,
2584
+ // so getAllEntities() won't inherit (it matches by name+filePath)
2585
+ const result = await queryIncompleteEntities(db, projectId, null);
2586
+ expect(result).toEqual([
2587
+ {
2588
+ entitySha: 'sha-card-v2',
2589
+ name: 'TaskCard',
2590
+ scenarioCount: 1,
2591
+ preExisting: false,
2592
+ },
2593
+ ]);
2594
+ });
2595
+ it('should still flag entities when no sibling version has analyses', async () => {
2596
+ // Only one version, no analyses
2597
+ await db
2598
+ .insertInto('entities')
2599
+ .values({
2600
+ sha: 'sha-icon',
2601
+ name: 'ExternalLinkIcon',
2602
+ entity_type: 'visual',
2603
+ file_path: 'src/components/ExternalLinkIcon.tsx',
2604
+ })
2605
+ .execute();
2606
+ await db
2607
+ .insertInto('editor_scenarios')
2608
+ .values({
2609
+ id: 'sc-icon',
2610
+ project_id: projectId,
2611
+ name: 'ExternalLinkIcon - Default',
2612
+ component_name: 'ExternalLinkIcon',
2613
+ entity_sha: 'sha-icon',
2614
+ created_at: '2026-03-16 23:00:00',
2615
+ })
2616
+ .execute();
2617
+ // Should flag as incomplete — no version has analyses
2618
+ const result = await queryIncompleteEntities(db, projectId, null);
2619
+ expect(result).toEqual([
2620
+ {
2621
+ entitySha: 'sha-icon',
2622
+ name: 'ExternalLinkIcon',
2623
+ scenarioCount: 1,
2624
+ preExisting: false,
2625
+ },
2626
+ ]);
2627
+ });
2628
+ it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
2629
+ // Scenario has entity_sha but entity record doesn't exist.
2630
+ // These are "phantom SHAs" created when scenarios were registered
2631
+ // without component_path — they can never be fixed by analyze-imports
2632
+ // and should not block audit progression.
2633
+ await db
2634
+ .insertInto('editor_scenarios')
2635
+ .values({
2636
+ id: 'sc-1',
2637
+ project_id: projectId,
2638
+ name: 'Ghost - Default',
2639
+ component_name: 'GhostComponent',
2640
+ entity_sha: 'sha-ghost',
2641
+ created_at: '2026-03-16 23:00:00',
2642
+ })
2643
+ .execute();
2644
+ const result = await queryIncompleteEntities(db, projectId, null);
2645
+ // Phantom SHAs are excluded — not reportable as incomplete
2646
+ expect(result).toEqual([]);
2647
+ });
2648
+ it('should detect incomplete entity whose scenario predates the session', async () => {
2649
+ // Entity with no analyses, scenario created BEFORE session
2650
+ await db
2651
+ .insertInto('entities')
2652
+ .values({
2653
+ sha: 'sha-preexisting',
2654
+ name: 'PreExistingComponent',
2655
+ entity_type: 'visual',
2656
+ file_path: 'src/PreExistingComponent.tsx',
2657
+ })
2658
+ .execute();
2659
+ await db
2660
+ .insertInto('editor_scenarios')
2661
+ .values({
2662
+ id: 'sc-preexisting',
2663
+ project_id: projectId,
2664
+ name: 'PreExistingComponent - Default',
2665
+ component_name: 'PreExistingComponent',
2666
+ entity_sha: 'sha-preexisting',
2667
+ created_at: '2026-03-16 20:00:00',
2668
+ updated_at: '2026-03-16 20:00:00',
2669
+ })
2670
+ .execute();
2671
+ // Session started well after scenario was created/updated
2672
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2673
+ // Should still be detected — the old time filter would have excluded it
2674
+ expect(result).toEqual([
2675
+ {
2676
+ entitySha: 'sha-preexisting',
2677
+ name: 'PreExistingComponent',
2678
+ scenarioCount: 1,
2679
+ preExisting: true,
2680
+ },
2681
+ ]);
2682
+ });
2683
+ it('should flag preExisting: true when all scenarios predate the session', async () => {
2684
+ await db
2685
+ .insertInto('entities')
2686
+ .values({
2687
+ sha: 'sha-old-entity',
2688
+ name: 'OldEntity',
2689
+ entity_type: 'visual',
2690
+ file_path: 'src/OldEntity.tsx',
2691
+ })
2692
+ .execute();
2693
+ // Two scenarios, both before session
2694
+ await db
2695
+ .insertInto('editor_scenarios')
2696
+ .values({
2697
+ id: 'sc-old-1',
2698
+ project_id: projectId,
2699
+ name: 'OldEntity - Default',
2700
+ component_name: 'OldEntity',
2701
+ entity_sha: 'sha-old-entity',
2702
+ created_at: '2026-03-16 19:00:00',
2703
+ updated_at: '2026-03-16 19:00:00',
2704
+ })
2705
+ .execute();
2706
+ await db
2707
+ .insertInto('editor_scenarios')
2708
+ .values({
2709
+ id: 'sc-old-2',
2710
+ project_id: projectId,
2711
+ name: 'OldEntity - Hover',
2712
+ component_name: 'OldEntity',
2713
+ entity_sha: 'sha-old-entity',
2714
+ created_at: '2026-03-16 19:30:00',
2715
+ updated_at: '2026-03-16 19:30:00',
2716
+ })
2717
+ .execute();
2718
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2719
+ expect(result).toEqual([
2720
+ {
2721
+ entitySha: 'sha-old-entity',
2722
+ name: 'OldEntity',
2723
+ scenarioCount: 2,
2724
+ preExisting: true,
2725
+ },
2726
+ ]);
2727
+ });
2728
+ it('should flag preExisting: false when scenario is from the current session', async () => {
2729
+ await db
2730
+ .insertInto('entities')
2731
+ .values({
2732
+ sha: 'sha-session-entity',
2733
+ name: 'SessionEntity',
2734
+ entity_type: 'visual',
2735
+ file_path: 'src/SessionEntity.tsx',
2736
+ })
2737
+ .execute();
2738
+ await db
2739
+ .insertInto('editor_scenarios')
2740
+ .values({
2741
+ id: 'sc-session',
2742
+ project_id: projectId,
2743
+ name: 'SessionEntity - Default',
2744
+ component_name: 'SessionEntity',
2745
+ entity_sha: 'sha-session-entity',
2746
+ created_at: '2026-03-16 23:30:00',
2747
+ updated_at: '2026-03-16 23:30:00',
2748
+ })
2749
+ .execute();
2750
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2262
2751
  expect(result).toEqual([
2263
2752
  {
2264
2753
  entitySha: 'sha-session-entity',
@@ -2268,252 +2757,1403 @@ describe('editorAudit', () => {
2268
2757
  },
2269
2758
  ]);
2270
2759
  });
2271
- });
2272
- // ── identifyScenariosNeedingRecapture ──────────────────────────────
2273
- describe('identifyScenariosNeedingRecapture', () => {
2274
- // Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
2275
- // Feature 2 edited LibraryView (used by App), but app-level scenarios
2276
- // were never flagged for recapture because the audit only checked
2277
- // component scenario existence — not whether app-level scenarios are stale.
2278
- //
2279
- // Each scenario's entityName is resolved by the caller via
2280
- // entity_sha → entities.name (the default export for app-level scenarios).
2281
- it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
2282
- // LibraryView was edited → App is impacted (imports LibraryView)
2283
- // App-level scenario "Library - Rich Library" has entity_sha pointing to App
2284
- // It was NOT recaptured during Feature 2 → should be flagged
2285
- const entityChangeStatus = {
2286
- LibraryView: { status: 'edited' },
2287
- App: {
2288
- status: 'impacted',
2289
- impactedBy: [
2290
- {
2291
- name: 'LibraryView',
2292
- filePath: 'src/components/LibraryView.tsx',
2293
- changeType: 'edited',
2294
- },
2295
- ],
2760
+ });
2761
+ // ── identifyScenariosNeedingRecapture ──────────────────────────────
2762
+ describe('identifyScenariosNeedingRecapture', () => {
2763
+ // Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
2764
+ // Feature 2 edited LibraryView (used by App), but app-level scenarios
2765
+ // were never flagged for recapture because the audit only checked
2766
+ // component scenario existence — not whether app-level scenarios are stale.
2767
+ //
2768
+ // Each scenario's entityName is resolved by the caller via
2769
+ // entity_sha → entities.name (the default export for app-level scenarios).
2770
+ it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
2771
+ // LibraryView was edited → App is impacted (imports LibraryView)
2772
+ // App-level scenario "Library - Rich Library" has entity_sha pointing to App
2773
+ // It was NOT recaptured during Feature 2 → should be flagged
2774
+ const entityChangeStatus = {
2775
+ LibraryView: { status: 'edited' },
2776
+ App: {
2777
+ status: 'impacted',
2778
+ impactedBy: [
2779
+ {
2780
+ name: 'LibraryView',
2781
+ filePath: 'src/components/LibraryView.tsx',
2782
+ changeType: 'edited',
2783
+ },
2784
+ ],
2785
+ },
2786
+ };
2787
+ const result = identifyScenariosNeedingRecapture({
2788
+ scenarios: [
2789
+ {
2790
+ name: 'Library - Rich Library',
2791
+ entityName: 'App', // resolved from entity_sha → entities.name
2792
+ updatedInSession: false,
2793
+ },
2794
+ ],
2795
+ entityChangeStatus,
2796
+ });
2797
+ expect(result).toHaveLength(1);
2798
+ expect(result[0].scenarioName).toBe('Library - Rich Library');
2799
+ expect(result[0].entityName).toBe('App');
2800
+ expect(result[0].status.status).toBe('impacted');
2801
+ });
2802
+ it('should flag component scenario when its entity is directly edited and not recaptured', () => {
2803
+ const entityChangeStatus = {
2804
+ LibraryView: { status: 'edited' },
2805
+ };
2806
+ const result = identifyScenariosNeedingRecapture({
2807
+ scenarios: [
2808
+ {
2809
+ name: 'LibraryView - Empty',
2810
+ entityName: 'LibraryView',
2811
+ updatedInSession: false,
2812
+ },
2813
+ ],
2814
+ entityChangeStatus,
2815
+ });
2816
+ expect(result).toHaveLength(1);
2817
+ expect(result[0].scenarioName).toBe('LibraryView - Empty');
2818
+ expect(result[0].entityName).toBe('LibraryView');
2819
+ expect(result[0].status.status).toBe('edited');
2820
+ });
2821
+ it('should NOT flag scenario that was already recaptured in the current session', () => {
2822
+ const entityChangeStatus = {
2823
+ App: {
2824
+ status: 'impacted',
2825
+ impactedBy: [
2826
+ {
2827
+ name: 'LibraryView',
2828
+ filePath: 'src/components/LibraryView.tsx',
2829
+ changeType: 'edited',
2830
+ },
2831
+ ],
2832
+ },
2833
+ };
2834
+ const result = identifyScenariosNeedingRecapture({
2835
+ scenarios: [
2836
+ {
2837
+ name: 'App - Default',
2838
+ entityName: 'App',
2839
+ updatedInSession: true, // re-registered during Feature 2
2840
+ },
2841
+ ],
2842
+ entityChangeStatus,
2843
+ });
2844
+ expect(result).toHaveLength(0);
2845
+ });
2846
+ it('should NOT flag scenario whose entity has no change status', () => {
2847
+ const entityChangeStatus = {
2848
+ LibraryView: { status: 'edited' },
2849
+ };
2850
+ const result = identifyScenariosNeedingRecapture({
2851
+ scenarios: [
2852
+ {
2853
+ name: 'WelcomeScreen - Default',
2854
+ entityName: 'WelcomeScreen',
2855
+ updatedInSession: false,
2856
+ },
2857
+ ],
2858
+ entityChangeStatus,
2859
+ });
2860
+ expect(result).toHaveLength(0);
2861
+ });
2862
+ it('should return empty array when entityChangeStatus is undefined', () => {
2863
+ const result = identifyScenariosNeedingRecapture({
2864
+ scenarios: [
2865
+ {
2866
+ name: 'Library - Rich Library',
2867
+ entityName: 'App',
2868
+ updatedInSession: false,
2869
+ },
2870
+ ],
2871
+ entityChangeStatus: undefined,
2872
+ });
2873
+ expect(result).toHaveLength(0);
2874
+ });
2875
+ it('should return empty array when entityChangeStatus is empty', () => {
2876
+ const result = identifyScenariosNeedingRecapture({
2877
+ scenarios: [
2878
+ {
2879
+ name: 'Library - Rich Library',
2880
+ entityName: 'App',
2881
+ updatedInSession: false,
2882
+ },
2883
+ ],
2884
+ entityChangeStatus: {},
2885
+ });
2886
+ expect(result).toHaveLength(0);
2887
+ });
2888
+ it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
2889
+ const entityChangeStatus = {
2890
+ LibraryView: { status: 'edited' },
2891
+ App: {
2892
+ status: 'impacted',
2893
+ impactedBy: [
2894
+ {
2895
+ name: 'LibraryView',
2896
+ filePath: 'src/components/LibraryView.tsx',
2897
+ changeType: 'edited',
2898
+ },
2899
+ ],
2900
+ },
2901
+ };
2902
+ const result = identifyScenariosNeedingRecapture({
2903
+ scenarios: [
2904
+ {
2905
+ name: 'Library - Empty',
2906
+ entityName: 'App',
2907
+ updatedInSession: false,
2908
+ },
2909
+ {
2910
+ name: 'Library - Rich Library',
2911
+ entityName: 'App',
2912
+ updatedInSession: false,
2913
+ },
2914
+ {
2915
+ name: 'First Article Saved',
2916
+ entityName: 'App',
2917
+ updatedInSession: false,
2918
+ },
2919
+ ],
2920
+ entityChangeStatus,
2921
+ });
2922
+ expect(result).toHaveLength(3);
2923
+ expect(result.map((r) => r.scenarioName).sort()).toEqual([
2924
+ 'First Article Saved',
2925
+ 'Library - Empty',
2926
+ 'Library - Rich Library',
2927
+ ]);
2928
+ expect(result.every((r) => r.entityName === 'App')).toBe(true);
2929
+ });
2930
+ it('should skip scenarios with null entityName (no entity_sha set)', () => {
2931
+ const entityChangeStatus = {
2932
+ App: { status: 'edited' },
2933
+ };
2934
+ const result = identifyScenariosNeedingRecapture({
2935
+ scenarios: [
2936
+ {
2937
+ name: 'Mystery Scenario',
2938
+ entityName: null, // no entity_sha → no entity name
2939
+ updatedInSession: false,
2940
+ },
2941
+ ],
2942
+ entityChangeStatus,
2943
+ });
2944
+ expect(result).toHaveLength(0);
2945
+ });
2946
+ it('should NOT flag scenarios for new entities (they need creation, not recapture)', () => {
2947
+ // "new" entities are being seen for the first time. Their scenarios need
2948
+ // initial creation, not recapture of old screenshots. Flagging them as
2949
+ // "needs_recapture" sends the wrong remediation signal to Claude.
2950
+ const entityChangeStatus = {
2951
+ NewComponent: { status: 'new' },
2952
+ };
2953
+ const result = identifyScenariosNeedingRecapture({
2954
+ scenarios: [
2955
+ {
2956
+ name: 'NewComponent - Default',
2957
+ entityName: 'NewComponent',
2958
+ updatedInSession: false,
2959
+ },
2960
+ ],
2961
+ entityChangeStatus,
2962
+ });
2963
+ expect(result).toHaveLength(0);
2964
+ });
2965
+ });
2966
+ // ── detectDuplicateNames ──────────────────────────────────────────
2967
+ describe('detectDuplicateNames', () => {
2968
+ it('should return empty map when no duplicates exist', () => {
2969
+ const entries = [
2970
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2971
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2972
+ { name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
2973
+ ];
2974
+ const result = detectDuplicateNames(entries);
2975
+ expect(result.size).toBe(0);
2976
+ });
2977
+ it('should group entries that share a name', () => {
2978
+ const entries = [
2979
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2980
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2981
+ { name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
2982
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2983
+ ];
2984
+ const result = detectDuplicateNames(entries);
2985
+ expect(result.size).toBe(1);
2986
+ expect(result.has('Page')).toBe(true);
2987
+ const pageGroup = result.get('Page');
2988
+ expect(pageGroup).toHaveLength(3);
2989
+ expect(pageGroup.map((e) => e.filePath)).toEqual([
2990
+ 'app/isolated-components/Foo/page.tsx',
2991
+ 'app/isolated-components/Bar/page.tsx',
2992
+ 'app/isolated-components/Baz/page.tsx',
2993
+ ]);
2994
+ });
2995
+ it('should exclude single-occurrence names', () => {
2996
+ const entries = [
2997
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2998
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2999
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
3000
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
3001
+ ];
3002
+ const result = detectDuplicateNames(entries);
3003
+ expect(result.size).toBe(1);
3004
+ expect(result.has('Header')).toBe(false);
3005
+ expect(result.has('Footer')).toBe(false);
3006
+ });
3007
+ it('should handle multiple duplicate groups', () => {
3008
+ const entries = [
3009
+ { name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
3010
+ { name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
3011
+ { name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
3012
+ { name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
3013
+ { name: 'Unique', filePath: 'app/components/Unique.tsx' },
3014
+ ];
3015
+ const result = detectDuplicateNames(entries);
3016
+ expect(result.size).toBe(2);
3017
+ expect(result.has('Page')).toBe(true);
3018
+ expect(result.has('Layout')).toBe(true);
3019
+ expect(result.get('Page')).toHaveLength(2);
3020
+ expect(result.get('Layout')).toHaveLength(2);
3021
+ });
3022
+ it('should return empty map for empty input', () => {
3023
+ const result = detectDuplicateNames([]);
3024
+ expect(result.size).toBe(0);
3025
+ });
3026
+ });
3027
+ // ── computeAudit: impacted components with stale scenarios ──────────
3028
+ describe('computeAudit — impacted components with stale scenarios', () => {
3029
+ it('should mark impacted component as needs_recapture when it has total scenarios but none in session', () => {
3030
+ // Library page has 3 scenarios from Feature 1 (totalScenarioCounts),
3031
+ // 0 in the current session (scenarioCounts), and is "impacted" in entityChangeStatus.
3032
+ // It should NOT be marked "missing" — it needs recapture, not new scenarios.
3033
+ const result = computeAudit({
3034
+ components: [
3035
+ {
3036
+ name: 'Library',
3037
+ filePath: 'app/library/page.tsx',
3038
+ returnType: 'JSX.Element',
3039
+ },
3040
+ {
3041
+ name: 'ArticleTable',
3042
+ filePath: 'app/components/ArticleTable.tsx',
3043
+ returnType: 'JSX.Element',
3044
+ },
3045
+ ],
3046
+ functions: [],
3047
+ scenarioCounts: { ArticleTable: 2 },
3048
+ testFileExistence: {},
3049
+ totalScenarioCounts: { Library: 3 },
3050
+ entityChangeStatus: {
3051
+ Library: { status: 'impacted' },
3052
+ ArticleTable: { status: 'edited' },
3053
+ },
3054
+ });
3055
+ // Library: impacted + has total scenarios but 0 in session → needs_recapture
3056
+ expect(result.components[0].status).toBe('needs_recapture');
3057
+ expect(result.components[0].scenarioCount).toBe(3);
3058
+ // ArticleTable: edited + has session scenarios → ok
3059
+ expect(result.components[1].status).toBe('ok');
3060
+ // needs_recapture should NOT count as missing
3061
+ expect(result.summary.componentsMissing).toBe(0);
3062
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
3063
+ // should NOT fail the audit (scenariosNeedingRecapture handles it)
3064
+ expect(result.summary.allPassing).toBe(true);
3065
+ });
3066
+ it('should still mark component as missing when impacted but has zero total scenarios', () => {
3067
+ // New page added to glossary but never had scenarios — truly missing
3068
+ const result = computeAudit({
3069
+ components: [
3070
+ {
3071
+ name: 'NewPage',
3072
+ filePath: 'app/new/page.tsx',
3073
+ returnType: 'JSX.Element',
3074
+ },
3075
+ ],
3076
+ functions: [],
3077
+ scenarioCounts: {},
3078
+ testFileExistence: {},
3079
+ totalScenarioCounts: {},
3080
+ entityChangeStatus: {
3081
+ NewPage: { status: 'impacted' },
3082
+ },
3083
+ });
3084
+ expect(result.components[0].status).toBe('missing');
3085
+ expect(result.summary.componentsMissing).toBe(1);
3086
+ });
3087
+ it('should use needs_recapture for edited entities with existing scenarios from prior sessions', () => {
3088
+ // Edited entities that already have scenarios from prior sessions
3089
+ // need recapture, not re-registration. The code changed, but the
3090
+ // scenarios exist — they just need fresh screenshots.
3091
+ const result = computeAudit({
3092
+ components: [
3093
+ {
3094
+ name: 'EditedComp',
3095
+ filePath: 'app/components/Edited.tsx',
3096
+ returnType: 'JSX.Element',
3097
+ },
3098
+ ],
3099
+ functions: [],
3100
+ scenarioCounts: {},
3101
+ testFileExistence: {},
3102
+ totalScenarioCounts: { EditedComp: 2 },
3103
+ entityChangeStatus: {
3104
+ EditedComp: { status: 'edited' },
3105
+ },
3106
+ });
3107
+ expect(result.components[0].status).toBe('needs_recapture');
3108
+ expect(result.summary.componentsMissing).toBe(0);
3109
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
3110
+ });
3111
+ it('should still mark as missing when new entity has zero total scenarios', () => {
3112
+ // Truly new component with no scenarios ever — needs scenarios created
3113
+ const result = computeAudit({
3114
+ components: [
3115
+ {
3116
+ name: 'BrandNew',
3117
+ filePath: 'app/components/BrandNew.tsx',
3118
+ returnType: 'JSX.Element',
3119
+ },
3120
+ ],
3121
+ functions: [],
3122
+ scenarioCounts: {},
3123
+ testFileExistence: {},
3124
+ totalScenarioCounts: {},
3125
+ entityChangeStatus: {
3126
+ BrandNew: { status: 'new' },
3127
+ },
3128
+ });
3129
+ expect(result.components[0].status).toBe('missing');
3130
+ expect(result.summary.componentsMissing).toBe(1);
3131
+ });
3132
+ it('should not count needs_recapture components as componentsOk', () => {
3133
+ // A needs_recapture component is not "ok" — it needs action. Counting it
3134
+ // in componentsOk is misleading: if totalComponents=2, componentsOk=2,
3135
+ // and componentsNeedingRecapture=1, the numbers don't add up (2+1 > 2).
3136
+ const result = computeAudit({
3137
+ components: [
3138
+ { name: 'Library', filePath: 'app/library/page.tsx' },
3139
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3140
+ ],
3141
+ functions: [],
3142
+ scenarioCounts: { DrinkCard: 2 },
3143
+ testFileExistence: {},
3144
+ totalScenarioCounts: { Library: 3 },
3145
+ entityChangeStatus: { Library: { status: 'impacted' } },
3146
+ });
3147
+ expect(result.components[0].status).toBe('needs_recapture');
3148
+ expect(result.components[1].status).toBe('ok');
3149
+ // needs_recapture is not "ok" — should be counted separately
3150
+ expect(result.summary.componentsOk).toBe(1);
3151
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
3152
+ expect(result.summary.totalComponents).toBe(2);
3153
+ });
3154
+ });
3155
+ // ── queryUnassociatedScenarios ──────────────────────────────────────
3156
+ describe('queryUnassociatedScenarios', () => {
3157
+ let db;
3158
+ let rawDb;
3159
+ const projectId = 'test-project-id';
3160
+ beforeEach(async () => {
3161
+ rawDb = new Database(':memory:');
3162
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3163
+ await db.schema
3164
+ .createTable('editor_scenarios')
3165
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
3166
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
3167
+ .addColumn('name', 'varchar', (col) => col.notNull())
3168
+ .addColumn('component_name', 'varchar')
3169
+ .addColumn('component_path', 'varchar')
3170
+ .addColumn('entity_sha', 'varchar')
3171
+ .addColumn('display_name', 'varchar')
3172
+ .addColumn('page_file_path', 'varchar')
3173
+ .addColumn('url', 'varchar')
3174
+ .addColumn('type', 'varchar')
3175
+ .addColumn('created_at', 'datetime')
3176
+ .addColumn('updated_at', 'datetime')
3177
+ .execute();
3178
+ });
3179
+ afterEach(async () => {
3180
+ await db.destroy();
3181
+ });
3182
+ it('should return empty when all scenarios have entity_sha', async () => {
3183
+ await db
3184
+ .insertInto('editor_scenarios')
3185
+ .values({
3186
+ id: 'sc-1',
3187
+ project_id: projectId,
3188
+ name: 'Header - Default',
3189
+ component_name: 'Header',
3190
+ component_path: 'src/components/Header.tsx',
3191
+ entity_sha: 'sha-header',
3192
+ created_at: '2026-03-16 23:00:00',
3193
+ updated_at: '2026-03-16 23:00:00',
3194
+ })
3195
+ .execute();
3196
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3197
+ expect(result).toEqual([]);
3198
+ });
3199
+ it('should find component scenarios with NULL entity_sha', async () => {
3200
+ // This reproduces the Margo testapp bug: subagent registered scenarios
3201
+ // but entity records didn't exist yet, so entity_sha was never set
3202
+ await db
3203
+ .insertInto('editor_scenarios')
3204
+ .values({
3205
+ id: 'sc-1',
3206
+ project_id: projectId,
3207
+ name: 'SearchBar - Default',
3208
+ component_name: 'SearchBar',
3209
+ component_path: 'src/components/SearchBar.tsx',
3210
+ entity_sha: null,
3211
+ created_at: '2026-03-20 18:45:00',
3212
+ updated_at: '2026-03-20 18:45:00',
3213
+ })
3214
+ .execute();
3215
+ await db
3216
+ .insertInto('editor_scenarios')
3217
+ .values({
3218
+ id: 'sc-2',
3219
+ project_id: projectId,
3220
+ name: 'SearchBar - With Results',
3221
+ component_name: 'SearchBar',
3222
+ component_path: 'src/components/SearchBar.tsx',
3223
+ entity_sha: null,
3224
+ created_at: '2026-03-20 18:45:05',
3225
+ updated_at: '2026-03-20 18:45:05',
3226
+ })
3227
+ .execute();
3228
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3229
+ expect(result).toHaveLength(1);
3230
+ expect(result[0].name).toBe('SearchBar');
3231
+ expect(result[0].filePath).toBe('src/components/SearchBar.tsx');
3232
+ expect(result[0].scenarioCount).toBe(2);
3233
+ expect(result[0].scenarioNames).toEqual(expect.arrayContaining([
3234
+ 'SearchBar - Default',
3235
+ 'SearchBar - With Results',
3236
+ ]));
3237
+ });
3238
+ it('should find page scenarios with NULL entity_sha', async () => {
3239
+ await db
3240
+ .insertInto('editor_scenarios')
3241
+ .values({
3242
+ id: 'sc-1',
3243
+ project_id: projectId,
3244
+ name: 'Full Page — Rich Library',
3245
+ component_name: null,
3246
+ component_path: null,
3247
+ page_file_path: 'src/library/LibraryApp.tsx',
3248
+ entity_sha: null,
3249
+ created_at: '2026-03-20 18:50:00',
3250
+ updated_at: '2026-03-20 18:50:00',
3251
+ })
3252
+ .execute();
3253
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3254
+ expect(result).toHaveLength(1);
3255
+ expect(result[0].name).toBe('LibraryApp');
3256
+ expect(result[0].filePath).toBe('src/library/LibraryApp.tsx');
3257
+ expect(result[0].scenarioCount).toBe(1);
3258
+ });
3259
+ it('should ignore scenarios without any file path (orphans without component_path or page_file_path)', async () => {
3260
+ // Scenarios with no file path at all can't be associated — they're not
3261
+ // actionable, so don't report them as unassociated
3262
+ await db
3263
+ .insertInto('editor_scenarios')
3264
+ .values({
3265
+ id: 'sc-1',
3266
+ project_id: projectId,
3267
+ name: 'Some Orphan',
3268
+ component_name: null,
3269
+ component_path: null,
3270
+ page_file_path: null,
3271
+ entity_sha: null,
3272
+ created_at: '2026-03-20 18:50:00',
3273
+ updated_at: '2026-03-20 18:50:00',
3274
+ })
3275
+ .execute();
3276
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3277
+ expect(result).toEqual([]);
3278
+ });
3279
+ it('should group multiple components separately', async () => {
3280
+ // Two different components both missing entity_sha
3281
+ await db
3282
+ .insertInto('editor_scenarios')
3283
+ .values({
3284
+ id: 'sc-1',
3285
+ project_id: projectId,
3286
+ name: 'FullPageHeader - Default',
3287
+ component_name: 'FullPageHeader',
3288
+ component_path: 'src/components/FullPageHeader.tsx',
3289
+ entity_sha: null,
3290
+ created_at: '2026-03-20 18:45:00',
3291
+ updated_at: '2026-03-20 18:45:00',
3292
+ })
3293
+ .execute();
3294
+ await db
3295
+ .insertInto('editor_scenarios')
3296
+ .values({
3297
+ id: 'sc-2',
3298
+ project_id: projectId,
3299
+ name: 'SaveConfirmation - Visible',
3300
+ component_name: 'SaveConfirmation',
3301
+ component_path: 'src/components/SaveConfirmation.tsx',
3302
+ entity_sha: null,
3303
+ created_at: '2026-03-20 19:00:00',
3304
+ updated_at: '2026-03-20 19:00:00',
3305
+ })
3306
+ .execute();
3307
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3308
+ expect(result).toHaveLength(2);
3309
+ const names = result.map((r) => r.name).sort();
3310
+ expect(names).toEqual(['FullPageHeader', 'SaveConfirmation']);
3311
+ });
3312
+ it('should only include scenarios from the specified project', async () => {
3313
+ await db
3314
+ .insertInto('editor_scenarios')
3315
+ .values({
3316
+ id: 'sc-1',
3317
+ project_id: 'other-project',
3318
+ name: 'SearchBar - Default',
3319
+ component_name: 'SearchBar',
3320
+ component_path: 'src/components/SearchBar.tsx',
3321
+ entity_sha: null,
3322
+ created_at: '2026-03-20 18:45:00',
3323
+ updated_at: '2026-03-20 18:45:00',
3324
+ })
3325
+ .execute();
3326
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3327
+ expect(result).toEqual([]);
3328
+ });
3329
+ it('should scope to feature session when featureStartedAt is provided', async () => {
3330
+ // Pre-existing unassociated scenario (before session)
3331
+ await db
3332
+ .insertInto('editor_scenarios')
3333
+ .values({
3334
+ id: 'sc-old',
3335
+ project_id: projectId,
3336
+ name: 'OldComponent - Default',
3337
+ component_name: 'OldComponent',
3338
+ component_path: 'src/components/OldComponent.tsx',
3339
+ entity_sha: null,
3340
+ created_at: '2026-03-19 10:00:00',
3341
+ updated_at: '2026-03-19 10:00:00',
3342
+ })
3343
+ .execute();
3344
+ // New unassociated scenario (during session)
3345
+ await db
3346
+ .insertInto('editor_scenarios')
3347
+ .values({
3348
+ id: 'sc-new',
3349
+ project_id: projectId,
3350
+ name: 'NewComponent - Default',
3351
+ component_name: 'NewComponent',
3352
+ component_path: 'src/components/NewComponent.tsx',
3353
+ entity_sha: null,
3354
+ created_at: '2026-03-20 18:45:00',
3355
+ updated_at: '2026-03-20 18:45:00',
3356
+ })
3357
+ .execute();
3358
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3359
+ // Should only find the session-scoped one
3360
+ expect(result).toHaveLength(1);
3361
+ expect(result[0].name).toBe('NewComponent');
3362
+ });
3363
+ it('should include re-registered scenarios (updated_at in session) even if created before', async () => {
3364
+ await db
3365
+ .insertInto('editor_scenarios')
3366
+ .values({
3367
+ id: 'sc-1',
3368
+ project_id: projectId,
3369
+ name: 'SearchBar - Default',
3370
+ component_name: 'SearchBar',
3371
+ component_path: 'src/components/SearchBar.tsx',
3372
+ entity_sha: null,
3373
+ created_at: '2026-03-19 10:00:00',
3374
+ updated_at: '2026-03-20 18:45:00', // re-registered during session
3375
+ })
3376
+ .execute();
3377
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3378
+ expect(result).toHaveLength(1);
3379
+ expect(result[0].name).toBe('SearchBar');
3380
+ });
3381
+ it('should not include scenarios with entity_sha set (even if stale)', async () => {
3382
+ // This scenario has an entity_sha — even if it's stale, that's a
3383
+ // different problem (handled by queryIncompleteEntities)
3384
+ await db
3385
+ .insertInto('editor_scenarios')
3386
+ .values({
3387
+ id: 'sc-1',
3388
+ project_id: projectId,
3389
+ name: 'Header - Default',
3390
+ component_name: 'Header',
3391
+ component_path: 'src/components/Header.tsx',
3392
+ entity_sha: 'sha-old-version',
3393
+ created_at: '2026-03-20 18:45:00',
3394
+ updated_at: '2026-03-20 18:45:00',
3395
+ })
3396
+ .execute();
3397
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3398
+ expect(result).toEqual([]);
3399
+ });
3400
+ });
3401
+ // ── isAutoRemediable with unassociatedScenarios ────────────────────
3402
+ describe('isAutoRemediable always returns false (no inline full analysis)', () => {
3403
+ // Full analyze-imports takes minutes on large projects. The audit should
3404
+ // never trigger it — only the lightweight backfill path is acceptable.
3405
+ it('should return false for unassociatedScenarios only', () => {
3406
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3407
+ });
3408
+ it('should return false for incompleteEntities + unassociatedScenarios', () => {
3409
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3410
+ });
3411
+ it('should return false even with no other failures', () => {
3412
+ expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
3413
+ });
3414
+ it('should return false when already attempted', () => {
3415
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
3416
+ });
3417
+ });
3418
+ describe('suggestedTestFile for functions without testFile', () => {
3419
+ it('should suggest conventional .test.ts path when testFile is undefined', () => {
3420
+ const result = computeAudit({
3421
+ components: [],
3422
+ functions: [
3423
+ { name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
3424
+ ],
3425
+ scenarioCounts: {},
3426
+ testFileExistence: {},
3427
+ });
3428
+ expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
3429
+ expect(result.functions[0].status).toBe('missing');
3430
+ });
3431
+ it('should suggest .test.ts for .ts files', () => {
3432
+ const result = computeAudit({
3433
+ components: [],
3434
+ functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
3435
+ scenarioCounts: {},
3436
+ testFileExistence: {},
3437
+ });
3438
+ expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
3439
+ });
3440
+ it('should not set suggestedTestFile when testFile is already specified', () => {
3441
+ const result = computeAudit({
3442
+ components: [],
3443
+ functions: [
3444
+ {
3445
+ name: 'calculatePrice',
3446
+ filePath: 'app/lib/pricing.ts',
3447
+ testFile: 'app/lib/pricing.test.ts',
3448
+ },
3449
+ ],
3450
+ scenarioCounts: {},
3451
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
3452
+ });
3453
+ expect(result.functions[0].suggestedTestFile).toBeUndefined();
3454
+ });
3455
+ });
3456
+ describe('hint for function audit entries', () => {
3457
+ it('should include a hint for name_mismatch functions explaining the fix', () => {
3458
+ // Claude sees "name mismatch" with no guidance on what it means or how
3459
+ // to fix it. The hint should explain that a top-level describe block
3460
+ // matching the function name is required for the CodeYam UI.
3461
+ const result = computeAudit({
3462
+ components: [],
3463
+ functions: [
3464
+ {
3465
+ name: 'useDrinks',
3466
+ filePath: 'app/hooks/useDrinks.ts',
3467
+ testFile: 'app/hooks/useDrinks.test.ts',
3468
+ },
3469
+ ],
3470
+ scenarioCounts: {},
3471
+ testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
3472
+ testResults: {
3473
+ 'app/hooks/useDrinks.test.ts': {
3474
+ passing: true,
3475
+ hasEntityNameDescribe: false,
3476
+ },
3477
+ },
3478
+ });
3479
+ expect(result.functions[0].status).toBe('name_mismatch');
3480
+ expect(result.functions[0].hint).toBeDefined();
3481
+ expect(result.functions[0].hint).toContain('describe');
3482
+ expect(result.functions[0].hint).toContain('useDrinks');
3483
+ });
3484
+ it('should include a hint for runner_error functions showing the error', () => {
3485
+ // When the test runner crashes, Claude needs to see WHY it crashed
3486
+ // to fix the underlying issue. Without this, Claude loops re-running audit.
3487
+ const result = computeAudit({
3488
+ components: [],
3489
+ functions: [
3490
+ {
3491
+ name: 'getTimeAgo',
3492
+ filePath: 'src/lib/format.ts',
3493
+ testFile: 'src/lib/format.test.ts',
3494
+ },
3495
+ ],
3496
+ scenarioCounts: {},
3497
+ testFileExistence: { 'src/lib/format.test.ts': true },
3498
+ testResults: {
3499
+ 'src/lib/format.test.ts': {
3500
+ passing: false,
3501
+ hasEntityNameDescribe: false,
3502
+ errorMessage: 'Cannot find module "@/lib/format"',
3503
+ },
3504
+ },
3505
+ });
3506
+ expect(result.functions[0].status).toBe('runner_error');
3507
+ expect(result.functions[0].hint).toBeDefined();
3508
+ expect(result.functions[0].hint).toContain('Cannot find module');
3509
+ });
3510
+ });
3511
+ describe('hint for missing components', () => {
3512
+ it('should hint that layout files need app-level scenarios', () => {
3513
+ const result = computeAudit({
3514
+ components: [
3515
+ { name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
3516
+ ],
3517
+ functions: [],
3518
+ scenarioCounts: {},
3519
+ testFileExistence: {},
3520
+ });
3521
+ expect(result.components[0].hint).toContain('layout');
3522
+ expect(result.components[0].hint).toContain('pageFilePath');
3523
+ });
3524
+ it('should hint that page files need app-level scenarios', () => {
3525
+ const result = computeAudit({
3526
+ components: [
3527
+ { name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
3528
+ ],
3529
+ functions: [],
3530
+ scenarioCounts: {},
3531
+ testFileExistence: {},
3532
+ });
3533
+ expect(result.components[0].hint).toContain('page');
3534
+ expect(result.components[0].hint).toContain('pageFilePath');
3535
+ });
3536
+ it('should hint that regular components need isolation routes', () => {
3537
+ const result = computeAudit({
3538
+ components: [
3539
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3540
+ ],
3541
+ functions: [],
3542
+ scenarioCounts: {},
3543
+ testFileExistence: {},
3544
+ });
3545
+ expect(result.components[0].hint).toContain('isolated-components');
3546
+ });
3547
+ it('should not set hint when component has scenarios', () => {
3548
+ const result = computeAudit({
3549
+ components: [
3550
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3551
+ ],
3552
+ functions: [],
3553
+ scenarioCounts: { DrinkCard: 2 },
3554
+ testFileExistence: {},
3555
+ });
3556
+ expect(result.components[0].hint).toBeUndefined();
3557
+ });
3558
+ it('should provide a hint for needs_recapture components', () => {
3559
+ // Components with needs_recapture status need guidance on what to do.
3560
+ // Without a hint, Claude has no instructions for fixing the issue.
3561
+ const result = computeAudit({
3562
+ components: [{ name: 'Library', filePath: 'app/library/page.tsx' }],
3563
+ functions: [],
3564
+ scenarioCounts: {},
3565
+ testFileExistence: {},
3566
+ totalScenarioCounts: { Library: 3 },
3567
+ entityChangeStatus: { Library: { status: 'impacted' } },
3568
+ });
3569
+ expect(result.components[0].status).toBe('needs_recapture');
3570
+ expect(result.components[0].hint).toBeDefined();
3571
+ expect(result.components[0].hint).toContain('recapture');
3572
+ });
3573
+ });
3574
+ describe('formatIncompleteEntityGuidance', () => {
3575
+ it('should include the entity name and scenario count', () => {
3576
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3577
+ const result = formatIncompleteEntityGuidance({
3578
+ entitySha: 'abc123',
3579
+ name: 'RuleBuilder',
3580
+ scenarioCount: 5,
3581
+ preExisting: false,
3582
+ });
3583
+ expect(result).toContain('RuleBuilder');
3584
+ expect(result).toContain('5');
3585
+ });
3586
+ it('should tell Claude the exact fix command', () => {
3587
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3588
+ const result = formatIncompleteEntityGuidance({
3589
+ entitySha: 'abc123',
3590
+ name: 'RuleBuilder',
3591
+ scenarioCount: 5,
3592
+ preExisting: false,
3593
+ });
3594
+ expect(result).toContain('codeyam editor analyze-imports');
3595
+ });
3596
+ it('should flag pre-existing issues as non-blocking', () => {
3597
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3598
+ const result = formatIncompleteEntityGuidance({
3599
+ entitySha: 'abc123',
3600
+ name: 'RuleBuilder',
3601
+ scenarioCount: 5,
3602
+ preExisting: true,
3603
+ });
3604
+ expect(result).toContain('pre-existing');
3605
+ });
3606
+ it('should explain what incomplete means', () => {
3607
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3608
+ const result = formatIncompleteEntityGuidance({
3609
+ entitySha: 'abc123',
3610
+ name: 'RuleBuilder',
3611
+ scenarioCount: 5,
3612
+ preExisting: false,
3613
+ });
3614
+ // Should explain the root cause, not just the symptom
3615
+ expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
3616
+ });
3617
+ });
3618
+ describe('formatManualAnalysisGuidance', () => {
3619
+ it('should include entity name, scenario count, and error message', () => {
3620
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3621
+ const result = formatManualAnalysisGuidance({
3622
+ name: 'Header',
3623
+ filePath: 'src/components/Header.tsx',
3624
+ scenarioCount: 3,
3625
+ error: 'TypeScript parsing error: unexpected token',
3626
+ });
3627
+ expect(result).toContain('Header');
3628
+ expect(result).toContain('3 scenario(s)');
3629
+ expect(result).toContain('TypeScript parsing error');
3630
+ });
3631
+ it('should include MANUAL ANALYSIS REQUIRED header', () => {
3632
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3633
+ const result = formatManualAnalysisGuidance({
3634
+ name: 'Header',
3635
+ filePath: 'src/components/Header.tsx',
3636
+ scenarioCount: 2,
3637
+ error: 'Parse error',
3638
+ });
3639
+ expect(result).toContain('MANUAL ANALYSIS REQUIRED');
3640
+ });
3641
+ it('should tell Claude to read the source file', () => {
3642
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3643
+ const result = formatManualAnalysisGuidance({
3644
+ name: 'Header',
3645
+ filePath: 'src/components/Header.tsx',
3646
+ scenarioCount: 2,
3647
+ error: 'Parse error',
3648
+ });
3649
+ expect(result).toContain('Read src/components/Header.tsx');
3650
+ });
3651
+ it('should include the manual-entity command', () => {
3652
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3653
+ const result = formatManualAnalysisGuidance({
3654
+ name: 'Header',
3655
+ filePath: 'src/components/Header.tsx',
3656
+ scenarioCount: 2,
3657
+ error: 'Parse error',
3658
+ });
3659
+ expect(result).toContain('codeyam editor manual-entity');
3660
+ });
3661
+ it('should tell Claude to check glossary for imports', () => {
3662
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3663
+ const result = formatManualAnalysisGuidance({
3664
+ name: 'Header',
3665
+ filePath: 'src/components/Header.tsx',
3666
+ scenarioCount: 2,
3667
+ error: 'Parse error',
3668
+ });
3669
+ expect(result).toContain('glossary');
3670
+ });
3671
+ });
3672
+ describe('getIncompleteEntityFilePaths', () => {
3673
+ // The audit should auto-fix incomplete entities by running analysis on
3674
+ // just their specific file paths, not all 117+ files. This function
3675
+ // resolves entity SHAs to file paths for targeted analysis.
3676
+ let db;
3677
+ let rawDb;
3678
+ beforeEach(async () => {
3679
+ rawDb = new Database(':memory:');
3680
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3681
+ await db.schema
3682
+ .createTable('entities')
3683
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
3684
+ .addColumn('name', 'varchar')
3685
+ .addColumn('file_path', 'varchar')
3686
+ .addColumn('project_id', 'varchar')
3687
+ .addColumn('entity_type', 'varchar')
3688
+ .execute();
3689
+ });
3690
+ afterEach(() => {
3691
+ rawDb.close();
3692
+ });
3693
+ it('should resolve entity SHAs to file paths from the entities table', async () => {
3694
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3695
+ await db
3696
+ .insertInto('entities')
3697
+ .values([
3698
+ {
3699
+ sha: 'sha-rule',
3700
+ name: 'RuleBuilder',
3701
+ file_path: 'app/components/RuleBuilder.tsx',
3702
+ project_id: 'p1',
3703
+ entity_type: 'component',
3704
+ },
3705
+ {
3706
+ sha: 'sha-row',
3707
+ name: 'ArticleTableRow',
3708
+ file_path: 'app/components/ArticleTableRow.tsx',
3709
+ project_id: 'p1',
3710
+ entity_type: 'component',
3711
+ },
3712
+ ])
3713
+ .execute();
3714
+ const result = await getIncompleteEntityFilePaths(db, [
3715
+ {
3716
+ entitySha: 'sha-rule',
3717
+ name: 'RuleBuilder',
3718
+ scenarioCount: 5,
3719
+ preExisting: false,
3720
+ },
3721
+ {
3722
+ entitySha: 'sha-row',
3723
+ name: 'ArticleTableRow',
3724
+ scenarioCount: 2,
3725
+ preExisting: false,
3726
+ },
3727
+ ]);
3728
+ expect(result).toContain('app/components/RuleBuilder.tsx');
3729
+ expect(result).toContain('app/components/ArticleTableRow.tsx');
3730
+ expect(result).toHaveLength(2);
3731
+ });
3732
+ it('should skip entities whose SHA is not in the entities table', async () => {
3733
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3734
+ const result = await getIncompleteEntityFilePaths(db, [
3735
+ {
3736
+ entitySha: 'nonexistent-sha',
3737
+ name: 'Ghost',
3738
+ scenarioCount: 1,
3739
+ preExisting: false,
3740
+ },
3741
+ ]);
3742
+ expect(result).toHaveLength(0);
3743
+ });
3744
+ it('should deduplicate file paths', async () => {
3745
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3746
+ await db
3747
+ .insertInto('entities')
3748
+ .values([
3749
+ {
3750
+ sha: 'sha-v1',
3751
+ name: 'Foo',
3752
+ file_path: 'app/Foo.tsx',
3753
+ project_id: 'p1',
3754
+ entity_type: 'component',
3755
+ },
3756
+ {
3757
+ sha: 'sha-v2',
3758
+ name: 'Foo',
3759
+ file_path: 'app/Foo.tsx',
3760
+ project_id: 'p1',
3761
+ entity_type: 'component',
3762
+ },
3763
+ ])
3764
+ .execute();
3765
+ const result = await getIncompleteEntityFilePaths(db, [
3766
+ {
3767
+ entitySha: 'sha-v1',
3768
+ name: 'Foo',
3769
+ scenarioCount: 3,
3770
+ preExisting: false,
3771
+ },
3772
+ {
3773
+ entitySha: 'sha-v2',
3774
+ name: 'Foo',
3775
+ scenarioCount: 1,
3776
+ preExisting: false,
3777
+ },
3778
+ ]);
3779
+ expect(result).toEqual(['app/Foo.tsx']);
3780
+ });
3781
+ });
3782
+ describe('isAutoRemediable never triggers full analysis', () => {
3783
+ // The audit must NEVER run handleAnalyzeImports inline — it takes minutes
3784
+ // for large projects. Auto-remediation should only do the lightweight
3785
+ // entity SHA backfill. isAutoRemediable is now always false; the callers
3786
+ // use needsBackfillOnly for the fast path instead.
3787
+ it('should always return false regardless of summary state', () => {
3788
+ expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
3789
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3790
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3791
+ });
3792
+ });
3793
+ // ── isOnlyPreExistingIncomplete ─────────────────────────────────────
3794
+ describe('isOnlyPreExistingIncomplete', () => {
3795
+ it('should return true when all incomplete entities are pre-existing and no other failures', () => {
3796
+ expect(isOnlyPreExistingIncomplete({
3797
+ incompleteEntities: 2,
3798
+ preExistingIncompleteEntities: 2,
3799
+ componentsMissing: 0,
3800
+ componentsWithErrors: 0,
3801
+ functionsFailing: 0,
3802
+ functionsNameMismatch: 0,
3803
+ functionsMissing: 0,
3804
+ missingFromGlossary: 0,
3805
+ miscategorizedScenarios: 0,
3806
+ scenariosNeedingRecapture: 0,
3807
+ }, [
3808
+ {
3809
+ entitySha: 'a',
3810
+ name: 'RuleBuilder',
3811
+ scenarioCount: 5,
3812
+ preExisting: true,
3813
+ },
3814
+ {
3815
+ entitySha: 'b',
3816
+ name: 'ArticleTableRow',
3817
+ scenarioCount: 2,
3818
+ preExisting: true,
3819
+ },
3820
+ ])).toBe(true);
3821
+ });
3822
+ it('should return false when there are runner errors even if all incompletes are pre-existing', () => {
3823
+ // Safety net: runner errors (crashed test runner) must NEVER bypass the gate.
3824
+ // Without this test, a regression removing functionsRunnerError from
3825
+ // isOnlyIncompleteEntities would silently let broken test infrastructure through.
3826
+ expect(isOnlyPreExistingIncomplete({
3827
+ incompleteEntities: 1,
3828
+ preExistingIncompleteEntities: 1,
3829
+ componentsMissing: 0,
3830
+ componentsWithErrors: 0,
3831
+ functionsFailing: 0,
3832
+ functionsRunnerError: 1,
3833
+ functionsNameMismatch: 0,
3834
+ functionsMissing: 0,
3835
+ missingFromGlossary: 0,
3836
+ }, [
3837
+ {
3838
+ entitySha: 'a',
3839
+ name: 'OldComponent',
3840
+ scenarioCount: 1,
3841
+ preExisting: true,
3842
+ },
3843
+ ])).toBe(false);
3844
+ });
3845
+ it('should return false when some incomplete entities are NOT pre-existing', () => {
3846
+ expect(isOnlyPreExistingIncomplete({
3847
+ incompleteEntities: 2,
3848
+ preExistingIncompleteEntities: 1,
3849
+ componentsMissing: 0,
3850
+ componentsWithErrors: 0,
3851
+ functionsFailing: 0,
3852
+ functionsNameMismatch: 0,
3853
+ functionsMissing: 0,
3854
+ missingFromGlossary: 0,
3855
+ miscategorizedScenarios: 0,
3856
+ scenariosNeedingRecapture: 0,
3857
+ }, [
3858
+ {
3859
+ entitySha: 'a',
3860
+ name: 'RuleBuilder',
3861
+ scenarioCount: 5,
3862
+ preExisting: true,
3863
+ },
3864
+ {
3865
+ entitySha: 'b',
3866
+ name: 'NewComponent',
3867
+ scenarioCount: 1,
3868
+ preExisting: false,
3869
+ },
3870
+ ])).toBe(false);
3871
+ });
3872
+ it('should return false when there are other failures besides incomplete entities', () => {
3873
+ expect(isOnlyPreExistingIncomplete({
3874
+ incompleteEntities: 2,
3875
+ preExistingIncompleteEntities: 2,
3876
+ componentsMissing: 1,
3877
+ }, [
3878
+ {
3879
+ entitySha: 'a',
3880
+ name: 'RuleBuilder',
3881
+ scenarioCount: 5,
3882
+ preExisting: true,
3883
+ },
3884
+ {
3885
+ entitySha: 'b',
3886
+ name: 'ArticleTableRow',
3887
+ scenarioCount: 2,
3888
+ preExisting: true,
3889
+ },
3890
+ ])).toBe(false);
3891
+ });
3892
+ it('should return false when incomplete entities array is empty', () => {
3893
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
3894
+ });
3895
+ it('should return false when incomplete entities array is missing', () => {
3896
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
3897
+ });
3898
+ });
3899
+ describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
3900
+ it('should return true when only unassociatedScenarios present', () => {
3901
+ expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
3902
+ });
3903
+ it('should return false when unassociatedScenarios present with other failures', () => {
3904
+ expect(isOnlyIncompleteEntities({
3905
+ unassociatedScenarios: 5,
3906
+ functionsMissing: 1,
3907
+ })).toBe(false);
3908
+ });
3909
+ });
3910
+ // ── aggregateClientErrorsByComponent ─────────────────────────────────
3911
+ describe('aggregateClientErrorsByComponent', () => {
3912
+ it('should attribute errors to component using componentName from metadata', () => {
3913
+ const result = aggregateClientErrorsByComponent({
3914
+ 'scenario-1': {
3915
+ scenarioName: 'DrinkCard - Default',
3916
+ errors: ['TypeError: Cannot read property "price"'],
3917
+ },
3918
+ }, [
3919
+ {
3920
+ name: 'DrinkCard - Default',
3921
+ componentName: 'DrinkCard',
3922
+ },
3923
+ ]);
3924
+ expect(result['DrinkCard']).toEqual([
3925
+ 'TypeError: Cannot read property "price"',
3926
+ ]);
3927
+ });
3928
+ it('should attribute app-level scenario errors using pageFilePath', () => {
3929
+ // App-level scenarios have componentName=null. The old approach
3930
+ // parsed the scenario name "Full Page with Library" and got
3931
+ // "Full Page with Library" as the component — which matches nothing.
3932
+ // With metadata, we derive the entity name from pageFilePath.
3933
+ const result = aggregateClientErrorsByComponent({
3934
+ 'scenario-1': {
3935
+ scenarioName: 'Full Page with Library',
3936
+ errors: ['TypeError: Cannot read property "title"'],
3937
+ },
3938
+ }, [
3939
+ {
3940
+ name: 'Full Page with Library',
3941
+ componentName: null,
3942
+ pageFilePath: 'app/library/page.tsx',
3943
+ },
3944
+ ]);
3945
+ // Should be keyed by the canonical entity name from scenarioEntityName()
3946
+ expect(result['Library']).toEqual([
3947
+ 'TypeError: Cannot read property "title"',
3948
+ ]);
3949
+ });
3950
+ it('should fall back to scenario name parsing when no metadata match exists', () => {
3951
+ // If the scenario is not in the metadata list (e.g., metadata not yet loaded),
3952
+ // fall back to the "ComponentName - Variant" convention
3953
+ const result = aggregateClientErrorsByComponent({
3954
+ 'scenario-1': {
3955
+ scenarioName: 'Header - Dark Mode',
3956
+ errors: ['ReferenceError: theme is not defined'],
3957
+ },
3958
+ }, []);
3959
+ expect(result['Header']).toEqual([
3960
+ 'ReferenceError: theme is not defined',
3961
+ ]);
3962
+ });
3963
+ it('should handle non-route file paths via scenarioEntityName', () => {
3964
+ // Non-route files (src/...) are treated as app entry points by
3965
+ // scenarioEntityName → buildRoutePattern returns '/' → 'Home'.
3966
+ // When a url is available, it provides a better entity name.
3967
+ const result = aggregateClientErrorsByComponent({
3968
+ 'scenario-1': {
3969
+ scenarioName: 'LibraryApp - Rich',
3970
+ errors: ['Error: fetch failed'],
3971
+ },
3972
+ }, [
3973
+ {
3974
+ name: 'LibraryApp - Rich',
3975
+ componentName: null,
3976
+ pageFilePath: 'src/library/LibraryApp.tsx',
3977
+ url: '/library',
3978
+ },
3979
+ ]);
3980
+ // scenarioEntityName uses url fallback when pageFilePath is a non-route file
3981
+ // pageFilePath 'src/...' → buildRoutePattern → '/' → routeDisplayName → 'Home'
3982
+ // But componentName takes priority, and with url '/library' as final fallback
3983
+ // scenarioEntityName({ pageFilePath: 'src/library/LibraryApp.tsx' }) → 'Home'
3984
+ // because pageFilePath is checked before url
3985
+ expect(result['Home']).toEqual(['Error: fetch failed']);
3986
+ });
3987
+ it('should skip scenarios with no errors', () => {
3988
+ const result = aggregateClientErrorsByComponent({
3989
+ 'scenario-1': {
3990
+ scenarioName: 'DrinkCard - Default',
3991
+ errors: [],
2296
3992
  },
2297
- };
2298
- const result = identifyScenariosNeedingRecapture({
2299
- scenarios: [
2300
- {
2301
- name: 'Library - Rich Library',
2302
- entityName: 'App', // resolved from entity_sha → entities.name
2303
- updatedInSession: false,
2304
- },
3993
+ }, [
3994
+ {
3995
+ name: 'DrinkCard - Default',
3996
+ componentName: 'DrinkCard',
3997
+ },
3998
+ ]);
3999
+ expect(result).toEqual({});
4000
+ });
4001
+ it('should aggregate errors from multiple scenarios for same component', () => {
4002
+ const result = aggregateClientErrorsByComponent({
4003
+ 'scenario-1': {
4004
+ scenarioName: 'DrinkCard - Default',
4005
+ errors: ['Error A'],
4006
+ },
4007
+ 'scenario-2': {
4008
+ scenarioName: 'DrinkCard - Hover',
4009
+ errors: ['Error B', 'Error C'],
4010
+ },
4011
+ }, [
4012
+ { name: 'DrinkCard - Default', componentName: 'DrinkCard' },
4013
+ { name: 'DrinkCard - Hover', componentName: 'DrinkCard' },
4014
+ ]);
4015
+ expect(result['DrinkCard']).toEqual(['Error A', 'Error B', 'Error C']);
4016
+ });
4017
+ it('should use capitalized display name for page file paths, not lowercase directory', () => {
4018
+ // aggregateClientErrorsByComponent must produce keys matching scenarioEntityName().
4019
+ // scenarioEntityName({ pageFilePath: 'app/library/page.tsx' }) returns 'Library',
4020
+ // so the key must be 'Library' — not 'library' (the raw directory name).
4021
+ // computeAudit checks clientErrors[glossaryEntryName], so a lowercase key
4022
+ // will never match, silently dropping all client errors for page-level scenarios.
4023
+ const result = aggregateClientErrorsByComponent({
4024
+ 'sc-1': {
4025
+ scenarioName: 'Library - Default',
4026
+ errors: ['TypeError: fetch failed'],
4027
+ },
4028
+ }, [
4029
+ {
4030
+ name: 'Library - Default',
4031
+ componentName: null,
4032
+ pageFilePath: 'app/library/page.tsx',
4033
+ },
4034
+ ]);
4035
+ expect(result).toHaveProperty('Library');
4036
+ expect(result['Library']).toEqual(['TypeError: fetch failed']);
4037
+ });
4038
+ it('should use "Home" for root page app/page.tsx, not "app"', () => {
4039
+ // Root page: scenarioEntityName returns 'Home', not 'app'.
4040
+ // Custom path parsing incorrectly pops the parent dir ('app').
4041
+ const result = aggregateClientErrorsByComponent({
4042
+ 'sc-1': {
4043
+ scenarioName: 'Home - Default',
4044
+ errors: ['ReferenceError: window is not defined'],
4045
+ },
4046
+ }, [
4047
+ {
4048
+ name: 'Home - Default',
4049
+ componentName: null,
4050
+ pageFilePath: 'app/page.tsx',
4051
+ },
4052
+ ]);
4053
+ expect(result).toHaveProperty('Home');
4054
+ expect(result['Home']).toEqual(['ReferenceError: window is not defined']);
4055
+ });
4056
+ });
4057
+ // ── determineTargetedAnalysisPaths ──────────────────────────────────
4058
+ describe('determineTargetedAnalysisPaths', () => {
4059
+ it('should return unique file paths from unassociated scenarios', () => {
4060
+ const result = determineTargetedAnalysisPaths({
4061
+ unassociatedScenarios: [
4062
+ { filePath: 'app/components/CreateFromFiltersButton.tsx' },
4063
+ { filePath: 'app/components/StaleDismissedBanner.tsx' },
2305
4064
  ],
2306
- entityChangeStatus,
4065
+ incompleteEntities: [],
2307
4066
  });
2308
- expect(result).toHaveLength(1);
2309
- expect(result[0].scenarioName).toBe('Library - Rich Library');
2310
- expect(result[0].entityName).toBe('App');
2311
- expect(result[0].status.status).toBe('impacted');
4067
+ expect(result).toEqual([
4068
+ 'app/components/CreateFromFiltersButton.tsx',
4069
+ 'app/components/StaleDismissedBanner.tsx',
4070
+ ]);
2312
4071
  });
2313
- it('should flag component scenario when its entity is directly edited and not recaptured', () => {
2314
- const entityChangeStatus = {
2315
- LibraryView: { status: 'edited' },
2316
- };
2317
- const result = identifyScenariosNeedingRecapture({
2318
- scenarios: [
2319
- {
2320
- name: 'LibraryView - Empty',
2321
- entityName: 'LibraryView',
2322
- updatedInSession: false,
2323
- },
2324
- ],
2325
- entityChangeStatus,
4072
+ it('should include file paths from incomplete entities', () => {
4073
+ const result = determineTargetedAnalysisPaths({
4074
+ unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
4075
+ incompleteEntities: [{ filePath: 'app/components/Dashboard.tsx' }],
2326
4076
  });
2327
- expect(result).toHaveLength(1);
2328
- expect(result[0].scenarioName).toBe('LibraryView - Empty');
2329
- expect(result[0].entityName).toBe('LibraryView');
2330
- expect(result[0].status.status).toBe('edited');
4077
+ expect(result).toEqual([
4078
+ 'app/components/TaskCard.tsx',
4079
+ 'app/components/Dashboard.tsx',
4080
+ ]);
2331
4081
  });
2332
- it('should NOT flag scenario that was already recaptured in the current session', () => {
2333
- const entityChangeStatus = {
2334
- App: {
2335
- status: 'impacted',
2336
- impactedBy: [
2337
- {
2338
- name: 'LibraryView',
2339
- filePath: 'src/components/LibraryView.tsx',
2340
- changeType: 'edited',
2341
- },
2342
- ],
2343
- },
2344
- };
2345
- const result = identifyScenariosNeedingRecapture({
2346
- scenarios: [
2347
- {
2348
- name: 'App - Default',
2349
- entityName: 'App',
2350
- updatedInSession: true, // re-registered during Feature 2
2351
- },
2352
- ],
2353
- entityChangeStatus,
4082
+ it('should deduplicate file paths across both sources', () => {
4083
+ const result = determineTargetedAnalysisPaths({
4084
+ unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
4085
+ incompleteEntities: [{ filePath: 'app/components/TaskCard.tsx' }],
2354
4086
  });
2355
- expect(result).toHaveLength(0);
4087
+ expect(result).toEqual(['app/components/TaskCard.tsx']);
2356
4088
  });
2357
- it('should NOT flag scenario whose entity has no change status', () => {
2358
- const entityChangeStatus = {
2359
- LibraryView: { status: 'edited' },
2360
- };
2361
- const result = identifyScenariosNeedingRecapture({
2362
- scenarios: [
2363
- {
2364
- name: 'WelcomeScreen - Default',
2365
- entityName: 'WelcomeScreen',
2366
- updatedInSession: false,
2367
- },
2368
- ],
2369
- entityChangeStatus,
4089
+ it('should return empty array when no issues', () => {
4090
+ const result = determineTargetedAnalysisPaths({
4091
+ unassociatedScenarios: [],
4092
+ incompleteEntities: [],
2370
4093
  });
2371
- expect(result).toHaveLength(0);
4094
+ expect(result).toEqual([]);
2372
4095
  });
2373
- it('should return empty array when entityChangeStatus is undefined', () => {
2374
- const result = identifyScenariosNeedingRecapture({
2375
- scenarios: [
2376
- {
2377
- name: 'Library - Rich Library',
2378
- entityName: 'App',
2379
- updatedInSession: false,
2380
- },
2381
- ],
2382
- entityChangeStatus: undefined,
4096
+ it('should filter out entries with empty file paths', () => {
4097
+ const result = determineTargetedAnalysisPaths({
4098
+ unassociatedScenarios: [{ filePath: '' }],
4099
+ incompleteEntities: [],
2383
4100
  });
2384
- expect(result).toHaveLength(0);
4101
+ expect(result).toEqual([]);
2385
4102
  });
2386
- it('should return empty array when entityChangeStatus is empty', () => {
2387
- const result = identifyScenariosNeedingRecapture({
2388
- scenarios: [
4103
+ it('should handle incomplete entities with undefined filePath', () => {
4104
+ const result = determineTargetedAnalysisPaths({
4105
+ unassociatedScenarios: [{ filePath: 'app/components/Good.tsx' }],
4106
+ incompleteEntities: [
2389
4107
  {
2390
- name: 'Library - Rich Library',
2391
- entityName: 'App',
2392
- updatedInSession: false,
4108
+ /* no filePath at all */
2393
4109
  },
4110
+ { filePath: undefined },
4111
+ { filePath: 'app/components/AlsoGood.tsx' },
2394
4112
  ],
2395
- entityChangeStatus: {},
2396
4113
  });
2397
- expect(result).toHaveLength(0);
4114
+ expect(result).toEqual([
4115
+ 'app/components/Good.tsx',
4116
+ 'app/components/AlsoGood.tsx',
4117
+ ]);
2398
4118
  });
2399
- it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
2400
- const entityChangeStatus = {
2401
- LibraryView: { status: 'edited' },
2402
- App: {
2403
- status: 'impacted',
2404
- impactedBy: [
2405
- {
2406
- name: 'LibraryView',
2407
- filePath: 'src/components/LibraryView.tsx',
2408
- changeType: 'edited',
2409
- },
2410
- ],
2411
- },
2412
- };
2413
- const result = identifyScenariosNeedingRecapture({
2414
- scenarios: [
2415
- {
2416
- name: 'Library - Empty',
2417
- entityName: 'App',
2418
- updatedInSession: false,
2419
- },
2420
- {
2421
- name: 'Library - Rich Library',
2422
- entityName: 'App',
2423
- updatedInSession: false,
2424
- },
4119
+ });
4120
+ // ── shouldAutoRecapture ─────────────────────────────────────────────
4121
+ describe('shouldAutoRecapture', () => {
4122
+ it('should return true when fix flag is set and stale scenarios exist', () => {
4123
+ expect(shouldAutoRecapture({
4124
+ fix: true,
4125
+ scenariosNeedingRecapture: [
2425
4126
  {
2426
- name: 'First Article Saved',
2427
- entityName: 'App',
2428
- updatedInSession: false,
4127
+ scenarioName: 'Default',
4128
+ entityName: 'TaskCard',
4129
+ status: { status: 'edited' },
2429
4130
  },
2430
4131
  ],
2431
- entityChangeStatus,
2432
- });
2433
- expect(result).toHaveLength(3);
2434
- expect(result.map((r) => r.scenarioName).sort()).toEqual([
2435
- 'First Article Saved',
2436
- 'Library - Empty',
2437
- 'Library - Rich Library',
2438
- ]);
2439
- expect(result.every((r) => r.entityName === 'App')).toBe(true);
4132
+ })).toBe(true);
2440
4133
  });
2441
- it('should skip scenarios with null entityName (no entity_sha set)', () => {
2442
- const entityChangeStatus = {
2443
- App: { status: 'edited' },
2444
- };
2445
- const result = identifyScenariosNeedingRecapture({
2446
- scenarios: [
4134
+ it('should return false when fix flag is not set', () => {
4135
+ expect(shouldAutoRecapture({
4136
+ fix: false,
4137
+ scenariosNeedingRecapture: [
2447
4138
  {
2448
- name: 'Mystery Scenario',
2449
- entityName: null, // no entity_sha → no entity name
2450
- updatedInSession: false,
4139
+ scenarioName: 'Default',
4140
+ entityName: 'TaskCard',
4141
+ status: { status: 'edited' },
2451
4142
  },
2452
4143
  ],
2453
- entityChangeStatus,
2454
- });
2455
- expect(result).toHaveLength(0);
2456
- });
2457
- });
2458
- // ── detectDuplicateNames ──────────────────────────────────────────
2459
- describe('detectDuplicateNames', () => {
2460
- it('should return empty map when no duplicates exist', () => {
2461
- const entries = [
2462
- { name: 'Header', filePath: 'app/components/Header.tsx' },
2463
- { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2464
- { name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
2465
- ];
2466
- const result = detectDuplicateNames(entries);
2467
- expect(result.size).toBe(0);
2468
- });
2469
- it('should group entries that share a name', () => {
2470
- const entries = [
2471
- { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2472
- { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2473
- { name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
2474
- { name: 'Header', filePath: 'app/components/Header.tsx' },
2475
- ];
2476
- const result = detectDuplicateNames(entries);
2477
- expect(result.size).toBe(1);
2478
- expect(result.has('Page')).toBe(true);
2479
- const pageGroup = result.get('Page');
2480
- expect(pageGroup).toHaveLength(3);
2481
- expect(pageGroup.map((e) => e.filePath)).toEqual([
2482
- 'app/isolated-components/Foo/page.tsx',
2483
- 'app/isolated-components/Bar/page.tsx',
2484
- 'app/isolated-components/Baz/page.tsx',
2485
- ]);
2486
- });
2487
- it('should exclude single-occurrence names', () => {
2488
- const entries = [
2489
- { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2490
- { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2491
- { name: 'Header', filePath: 'app/components/Header.tsx' },
2492
- { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2493
- ];
2494
- const result = detectDuplicateNames(entries);
2495
- expect(result.size).toBe(1);
2496
- expect(result.has('Header')).toBe(false);
2497
- expect(result.has('Footer')).toBe(false);
4144
+ })).toBe(false);
2498
4145
  });
2499
- it('should handle multiple duplicate groups', () => {
2500
- const entries = [
2501
- { name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
2502
- { name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
2503
- { name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
2504
- { name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
2505
- { name: 'Unique', filePath: 'app/components/Unique.tsx' },
2506
- ];
2507
- const result = detectDuplicateNames(entries);
2508
- expect(result.size).toBe(2);
2509
- expect(result.has('Page')).toBe(true);
2510
- expect(result.has('Layout')).toBe(true);
2511
- expect(result.get('Page')).toHaveLength(2);
2512
- expect(result.get('Layout')).toHaveLength(2);
4146
+ it('should return false when fix is set but no stale scenarios', () => {
4147
+ expect(shouldAutoRecapture({
4148
+ fix: true,
4149
+ scenariosNeedingRecapture: [],
4150
+ })).toBe(false);
2513
4151
  });
2514
- it('should return empty map for empty input', () => {
2515
- const result = detectDuplicateNames([]);
2516
- expect(result.size).toBe(0);
4152
+ it('should return false when both are falsy', () => {
4153
+ expect(shouldAutoRecapture({
4154
+ fix: false,
4155
+ scenariosNeedingRecapture: [],
4156
+ })).toBe(false);
2517
4157
  });
2518
4158
  });
2519
4159
  });