@codeyam/codeyam-cli 0.1.0-staging.b8b17a5 → 0.1.0-staging.ba3f279

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 (331) hide show
  1. package/analyzer-template/.build-info.json +7 -7
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/package.json +2 -2
  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 +1 -1
  23. package/analyzer-template/packages/database/package.json +1 -1
  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/editor.js +1906 -468
  56. package/codeyam-cli/src/commands/editor.js.map +1 -1
  57. package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js +23 -0
  58. package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js.map +1 -0
  59. package/codeyam-cli/src/commands/init.js +1 -0
  60. package/codeyam-cli/src/commands/init.js.map +1 -1
  61. package/codeyam-cli/src/data/designSystems.js +27 -0
  62. package/codeyam-cli/src/data/designSystems.js.map +1 -0
  63. package/codeyam-cli/src/data/techStacks.js +1 -1
  64. package/codeyam-cli/src/utils/__tests__/editorApi.test.js +44 -0
  65. package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -1
  66. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +2114 -70
  67. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  68. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js +137 -0
  69. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js.map +1 -0
  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__/editorScenarioSwitch.test.js +190 -0
  77. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
  78. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +249 -1
  79. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  80. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +134 -1
  81. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -1
  82. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +266 -2
  83. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  84. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
  85. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
  86. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +16 -1
  87. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -1
  88. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
  89. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
  90. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
  91. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
  92. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +57 -0
  93. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -1
  94. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +180 -1
  95. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
  96. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
  97. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
  98. package/codeyam-cli/src/utils/__tests__/testRunner.test.js +216 -0
  99. package/codeyam-cli/src/utils/__tests__/testRunner.test.js.map +1 -0
  100. package/codeyam-cli/src/utils/__tests__/webappDetection.test.js +6 -0
  101. package/codeyam-cli/src/utils/__tests__/webappDetection.test.js.map +1 -1
  102. package/codeyam-cli/src/utils/analysisRunner.js +36 -7
  103. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  104. package/codeyam-cli/src/utils/analyzer.js +11 -1
  105. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  106. package/codeyam-cli/src/utils/backgroundServer.js +1 -1
  107. package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
  108. package/codeyam-cli/src/utils/editorApi.js +16 -0
  109. package/codeyam-cli/src/utils/editorApi.js.map +1 -1
  110. package/codeyam-cli/src/utils/editorAudit.js +499 -43
  111. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  112. package/codeyam-cli/src/utils/editorGuard.js +36 -0
  113. package/codeyam-cli/src/utils/editorGuard.js.map +1 -0
  114. package/codeyam-cli/src/utils/editorPreview.js +5 -3
  115. package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
  116. package/codeyam-cli/src/utils/editorRecapture.js +109 -0
  117. package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
  118. package/codeyam-cli/src/utils/editorScenarioSwitch.js +39 -2
  119. package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
  120. package/codeyam-cli/src/utils/editorScenarios.js +131 -7
  121. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  122. package/codeyam-cli/src/utils/editorSeedAdapter.js +69 -16
  123. package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -1
  124. package/codeyam-cli/src/utils/entityChangeStatus.js +31 -3
  125. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
  126. package/codeyam-cli/src/utils/entityChangeStatus.server.js +31 -0
  127. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
  128. package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
  129. package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
  130. package/codeyam-cli/src/utils/install-skills.js +5 -0
  131. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  132. package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
  133. package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
  134. package/codeyam-cli/src/utils/queue/__tests__/job.interactiveStart.test.js +159 -0
  135. package/codeyam-cli/src/utils/queue/__tests__/job.interactiveStart.test.js.map +1 -0
  136. package/codeyam-cli/src/utils/queue/job.js +35 -6
  137. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  138. package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
  139. package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
  140. package/codeyam-cli/src/utils/scenarioCoverage.js +4 -1
  141. package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -1
  142. package/codeyam-cli/src/utils/scenariosManifest.js +66 -2
  143. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
  144. package/codeyam-cli/src/utils/screenshotHash.js +26 -0
  145. package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
  146. package/codeyam-cli/src/utils/simulationGateMiddleware.js +9 -0
  147. package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
  148. package/codeyam-cli/src/utils/testResultCache.js +53 -0
  149. package/codeyam-cli/src/utils/testResultCache.js.map +1 -0
  150. package/codeyam-cli/src/utils/testResultCache.server.js +81 -0
  151. package/codeyam-cli/src/utils/testResultCache.server.js.map +1 -0
  152. package/codeyam-cli/src/utils/testResultCache.server.test.js +187 -0
  153. package/codeyam-cli/src/utils/testResultCache.server.test.js.map +1 -0
  154. package/codeyam-cli/src/utils/testResultCache.test.js +230 -0
  155. package/codeyam-cli/src/utils/testResultCache.test.js.map +1 -0
  156. package/codeyam-cli/src/utils/testRunner.js +193 -1
  157. package/codeyam-cli/src/utils/testRunner.js.map +1 -1
  158. package/codeyam-cli/src/utils/webappDetection.js +4 -2
  159. package/codeyam-cli/src/utils/webappDetection.js.map +1 -1
  160. package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js +98 -0
  161. package/codeyam-cli/src/webserver/__tests__/api.interactive-switch-scenario.test.js.map +1 -0
  162. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +68 -1
  163. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -1
  164. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +30 -11
  165. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  166. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +190 -21
  167. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
  168. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +135 -0
  169. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -0
  170. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +22 -1
  171. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
  172. package/codeyam-cli/src/webserver/app/routes/api.interactive-switch-scenario.js +34 -0
  173. package/codeyam-cli/src/webserver/app/routes/api.interactive-switch-scenario.js.map +1 -0
  174. package/codeyam-cli/src/webserver/backgroundServer.js +42 -57
  175. package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
  176. package/codeyam-cli/src/webserver/build/client/assets/{CopyButton-CzTDWkF2.js → CopyButton-CLe80MMu.js} +1 -1
  177. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-BFbq6iFk.js → EntityItem-Crt_KN_U.js} +1 -1
  178. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-B6OMi58N.js → EntityTypeIcon-CD7lGABo.js} +1 -1
  179. package/codeyam-cli/src/webserver/build/client/assets/{InlineSpinner-DuYodzo1.js → InlineSpinner-CgTNOhnu.js} +1 -1
  180. package/codeyam-cli/src/webserver/build/client/assets/{InteractivePreview-CXo9EeCl.js → InteractivePreview-DtYTSPL2.js} +2 -2
  181. package/codeyam-cli/src/webserver/build/client/assets/{LibraryFunctionPreview-DYCNb2It.js → LibraryFunctionPreview-D3s1MFkb.js} +1 -1
  182. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CZgY3sxX.js → LogViewer-CM5zg40N.js} +1 -1
  183. package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-CQENLSrF.js +36 -0
  184. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-CnYYwRDw.js → ReportIssueModal-C2PLkej3.js} +1 -1
  185. package/codeyam-cli/src/webserver/build/client/assets/{SafeScreenshot-CDoF7ZpU.js → SafeScreenshot-DanvyBPb.js} +1 -1
  186. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-DrnfvaLL.js → ScenarioViewer-CefgqbCr.js} +1 -1
  187. package/codeyam-cli/src/webserver/build/client/assets/Spinner-Bc8BG-Lw.js +34 -0
  188. package/codeyam-cli/src/webserver/build/client/assets/{ViewportInspectBar-DRKR9T0U.js → ViewportInspectBar-BA_Ry-rs.js} +1 -1
  189. package/codeyam-cli/src/webserver/build/client/assets/{_index-ClR-g3tY.js → _index-C1YkzTAV.js} +1 -1
  190. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-DTH6ydEA.js → activity.(_tab)-yH46LLUz.js} +1 -1
  191. package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-74hnHF59.js → addon-web-links-CHx25PAe.js} +1 -1
  192. package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-B8CYhCO9.js → agent-transcripts-Bg3e7q4S.js} +1 -1
  193. package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
  194. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
  195. package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
  196. package/codeyam-cli/src/webserver/build/client/assets/api.editor-verify-routes-l0sNRNKZ.js +1 -0
  197. package/codeyam-cli/src/webserver/build/client/assets/api.interactive-switch-scenario-l0sNRNKZ.js +1 -0
  198. package/codeyam-cli/src/webserver/build/client/assets/{book-open-CLaoh4ac.js → book-open-CL-lMgHh.js} +1 -1
  199. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-BZ2DZxbW.js → chevron-down-GmAjGS9-.js} +1 -1
  200. package/codeyam-cli/src/webserver/build/client/assets/{chunk-JZWAC4HX-BBXArFPl.js → chunk-JZWAC4HX-BAdwhyCx.js} +11 -11
  201. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-CT4unAk-.js → circle-check-DFcQkN5j.js} +1 -1
  202. package/codeyam-cli/src/webserver/build/client/assets/{copy-zK0B6Nu-.js → copy-C6iF61Xs.js} +1 -1
  203. package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-DJB0YQJL.js → createLucideIcon-4ImjHTVC.js} +1 -1
  204. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-Coe5NhbS.js +1 -0
  205. package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
  206. package/codeyam-cli/src/webserver/build/client/assets/{dev.empty-CkXFP_i-.js → dev.empty-CRepiabR.js} +1 -1
  207. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +1 -0
  208. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-CRxPi2BB.js +96 -0
  209. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-CluPkvXJ.js +41 -0
  210. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-BqAN7hyG.js → entity._sha._-DYJRGiDI.js} +13 -12
  211. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-D1eikpe1.js → entity._sha.scenarios._scenarioId.dev-wdiwx5-Z.js} +1 -1
  212. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-Dg1NhIms.js → entity._sha.scenarios._scenarioId.fullscreen-BrkN-40Y.js} +1 -1
  213. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.create-scenario-CJX6kkkV.js → entity._sha_.create-scenario-DxfhekTZ.js} +1 -1
  214. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-BhVjZhKg.js → entity._sha_.edit._scenarioId-CRXJWmpB.js} +1 -1
  215. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-_gzKltPN.js → entry.client-SuW9syRS.js} +1 -1
  216. package/codeyam-cli/src/webserver/build/client/assets/{files-CV_17tZS.js → files-D-xGrg29.js} +1 -1
  217. package/codeyam-cli/src/webserver/build/client/assets/{git-D-YXmMbR.js → git-Bq_fbXP5.js} +1 -1
  218. package/codeyam-cli/src/webserver/build/client/assets/globals-BsGHu8WX.css +1 -0
  219. package/codeyam-cli/src/webserver/build/client/assets/{index-CCrgCshv.js → index-Bp1l4hSv.js} +1 -1
  220. package/codeyam-cli/src/webserver/build/client/assets/{index-BsX0F-9C.js → index-CWV9XZiG.js} +1 -1
  221. package/codeyam-cli/src/webserver/build/client/assets/{index-Blo6EK8G.js → index-DE3jI_dv.js} +1 -1
  222. package/codeyam-cli/src/webserver/build/client/assets/{labs-Byazq8Pv.js → labs-B_IX45ih.js} +1 -1
  223. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-DVQ0oHR7.js → loader-circle-De-7qQ2u.js} +1 -1
  224. package/codeyam-cli/src/webserver/build/client/assets/manifest-9032538f.js +1 -0
  225. package/codeyam-cli/src/webserver/build/client/assets/{memory-b-VmA2Vj.js → memory-Cx2xEx7s.js} +1 -1
  226. package/codeyam-cli/src/webserver/build/client/assets/{pause-DGcndCAa.js → pause-CFxEKL1u.js} +1 -1
  227. package/codeyam-cli/src/webserver/build/client/assets/root-dKFRTYcy.js +80 -0
  228. package/codeyam-cli/src/webserver/build/client/assets/{search-C0Uw0bcK.js → search-BdBb5aqc.js} +1 -1
  229. package/codeyam-cli/src/webserver/build/client/assets/{settings-OoNgHIfW.js → settings-DdE-Untf.js} +1 -1
  230. package/codeyam-cli/src/webserver/build/client/assets/{simulations-Bcemfu8a.js → simulations-DSCdE99u.js} +1 -1
  231. package/codeyam-cli/src/webserver/build/client/assets/{terminal-BgMmG7R9.js → terminal-CrplD4b1.js} +1 -1
  232. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-Cs87hJYK.js → triangle-alert-DqJ0j69l.js} +1 -1
  233. package/codeyam-cli/src/webserver/build/client/assets/{useCustomSizes-BR3Rs7JY.js → useCustomSizes-DhXHbEjP.js} +1 -1
  234. package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-D9QZKaLJ.js +2 -0
  235. package/codeyam-cli/src/webserver/build/client/assets/{useReportContext-BermyNU5.js → useReportContext-Cy5Qg_UR.js} +1 -1
  236. package/codeyam-cli/src/webserver/build/client/assets/{useToast-a_QN_W9_.js → useToast-5HR2j9ZE.js} +1 -1
  237. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-OLsM110H.js +16 -0
  238. package/codeyam-cli/src/webserver/build/server/assets/{index-CHymws6l.js → index-WHdB6WTN.js} +1 -1
  239. package/codeyam-cli/src/webserver/build/server/assets/{init-D3HkMDbI.js → init-DbSiZoE6.js} +2 -2
  240. package/codeyam-cli/src/webserver/build/server/assets/server-build-DZbLY6O_.js +690 -0
  241. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  242. package/codeyam-cli/src/webserver/build-info.json +5 -5
  243. package/codeyam-cli/src/webserver/editorProxy.js +55 -3
  244. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  245. package/codeyam-cli/src/webserver/idleDetector.js +65 -8
  246. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
  247. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +53 -0
  248. package/codeyam-cli/src/webserver/server.js +52 -14
  249. package/codeyam-cli/src/webserver/server.js.map +1 -1
  250. package/codeyam-cli/src/webserver/terminalServer.js +153 -32
  251. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  252. package/codeyam-cli/templates/__tests__/editor-step-hook.prompt-capture.test.ts +118 -0
  253. package/codeyam-cli/templates/codeyam-editor-claude.md +2 -0
  254. package/codeyam-cli/templates/codeyam-editor-reference.md +216 -0
  255. package/codeyam-cli/templates/design-systems/clean-dashboard-design-system.md +255 -0
  256. package/codeyam-cli/templates/design-systems/editorial-design-system.md +267 -0
  257. package/codeyam-cli/templates/design-systems/mono-brutalist-design-system.md +256 -0
  258. package/codeyam-cli/templates/design-systems/neo-brutalist-design-system.md +294 -0
  259. package/codeyam-cli/templates/editor-step-hook.py +93 -46
  260. package/codeyam-cli/templates/expo-react-native/MOBILE_SETUP.md +204 -5
  261. package/codeyam-cli/templates/expo-react-native/__tests__/.gitkeep +0 -0
  262. package/codeyam-cli/templates/expo-react-native/app/_layout.tsx +6 -3
  263. package/codeyam-cli/templates/expo-react-native/app/index.tsx +36 -0
  264. package/codeyam-cli/templates/expo-react-native/app.json +11 -0
  265. package/codeyam-cli/templates/expo-react-native/babel.config.js +1 -0
  266. package/codeyam-cli/templates/expo-react-native/gitignore +2 -0
  267. package/codeyam-cli/templates/expo-react-native/global.css +7 -0
  268. package/codeyam-cli/templates/expo-react-native/lib/theme.ts +73 -0
  269. package/codeyam-cli/templates/expo-react-native/package.json +32 -16
  270. package/codeyam-cli/templates/expo-react-native/patches/expo-modules-autolinking+3.0.24.patch +29 -0
  271. package/codeyam-cli/templates/isolation-route/expo-router.tsx.template +54 -0
  272. package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +47 -34
  273. package/codeyam-cli/templates/seed-adapters/supabase.ts +101 -9
  274. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +34 -1
  275. package/package.json +1 -1
  276. package/packages/ai/src/lib/astScopes/methodSemantics.js +99 -0
  277. package/packages/ai/src/lib/astScopes/methodSemantics.js.map +1 -1
  278. package/packages/ai/src/lib/astScopes/nodeToSource.js +16 -0
  279. package/packages/ai/src/lib/astScopes/nodeToSource.js.map +1 -1
  280. package/packages/ai/src/lib/astScopes/paths.js +12 -3
  281. package/packages/ai/src/lib/astScopes/paths.js.map +1 -1
  282. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +27 -10
  283. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  284. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
  285. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
  286. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
  287. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
  288. package/packages/analyze/index.js +1 -1
  289. package/packages/analyze/index.js.map +1 -1
  290. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
  291. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
  292. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
  293. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  294. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
  295. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
  296. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
  297. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
  298. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
  299. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
  300. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
  301. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
  302. package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
  303. package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
  304. package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
  305. package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
  306. package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
  307. package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
  308. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +120 -28
  309. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  310. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +1368 -1193
  311. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  312. package/packages/database/src/lib/loadAnalysis.js +7 -1
  313. package/packages/database/src/lib/loadAnalysis.js.map +1 -1
  314. package/packages/database/src/lib/loadEntity.js +5 -5
  315. package/packages/database/src/lib/loadEntity.js.map +1 -1
  316. package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
  317. package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  318. package/codeyam-cli/src/webserver/build/client/assets/Spinner-Df3UCi8k.js +0 -34
  319. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
  320. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-DPw7NZHc.js +0 -1
  321. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DYqG1D_d.js +0 -58
  322. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-DggyRwOr.js +0 -41
  323. package/codeyam-cli/src/webserver/build/client/assets/globals-DRvOjyO3.css +0 -1
  324. package/codeyam-cli/src/webserver/build/client/assets/manifest-f4212c17.js +0 -1
  325. package/codeyam-cli/src/webserver/build/client/assets/root-F-k2uYj5.js +0 -67
  326. package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-BxxP_XF9.js +0 -2
  327. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-if8kM_1Q.js +0 -13
  328. package/codeyam-cli/src/webserver/build/server/assets/server-build-DTCzJQiH.js +0 -551
  329. package/codeyam-cli/templates/expo-react-native/app/(tabs)/_layout.tsx +0 -33
  330. package/codeyam-cli/templates/expo-react-native/app/(tabs)/index.tsx +0 -12
  331. 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, queryIncompleteEntities, queryMiscategorizedScenarios, isOnlyIncompleteEntities, isAutoRemediable, } 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', () => {
@@ -1006,6 +1016,158 @@ describe('editorAudit', () => {
1006
1016
  expect(counts).toEqual({ ArticleCard: 1, ArticleRow: 1 });
1007
1017
  });
1008
1018
  });
1019
+ // ── queryPageScenarioCounts ──────────────────────────────────────────
1020
+ describe('queryPageScenarioCounts', () => {
1021
+ let db;
1022
+ let rawDb;
1023
+ const projectId = 'test-project-id';
1024
+ beforeEach(async () => {
1025
+ rawDb = new Database(':memory:');
1026
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1027
+ await db.schema
1028
+ .createTable('editor_scenarios')
1029
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1030
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1031
+ .addColumn('name', 'varchar', (col) => col.notNull())
1032
+ .addColumn('description', 'text')
1033
+ .addColumn('component_name', 'varchar')
1034
+ .addColumn('component_path', 'varchar')
1035
+ .addColumn('page_file_path', 'varchar')
1036
+ .addColumn('url', 'varchar')
1037
+ .addColumn('type', 'varchar')
1038
+ .addColumn('screenshot_path', 'varchar')
1039
+ .addColumn('viewport_width', 'integer')
1040
+ .addColumn('viewport_height', 'integer')
1041
+ .addColumn('dimension', 'varchar')
1042
+ .addColumn('created_at', 'datetime')
1043
+ .addColumn('updated_at', 'datetime')
1044
+ .execute();
1045
+ });
1046
+ afterEach(async () => {
1047
+ await db.destroy();
1048
+ });
1049
+ it('should count app-level scenarios by page_file_path', async () => {
1050
+ // App-level scenario: has page_file_path but no component_name
1051
+ await db
1052
+ .insertInto('editor_scenarios')
1053
+ .values({
1054
+ id: 'sc-1',
1055
+ project_id: projectId,
1056
+ name: 'Library - Default',
1057
+ component_name: null,
1058
+ page_file_path: 'app/library/page.tsx',
1059
+ created_at: '2026-03-12 13:00:00',
1060
+ })
1061
+ .execute();
1062
+ await db
1063
+ .insertInto('editor_scenarios')
1064
+ .values({
1065
+ id: 'sc-2',
1066
+ project_id: projectId,
1067
+ name: 'Library - Empty',
1068
+ component_name: null,
1069
+ page_file_path: 'app/library/page.tsx',
1070
+ created_at: '2026-03-12 13:05:00',
1071
+ })
1072
+ .execute();
1073
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1074
+ expect(counts).toEqual({ 'app/library/page.tsx': 2 });
1075
+ });
1076
+ it('should not count component scenarios (those have component_name)', async () => {
1077
+ // Component scenario — should NOT appear in page counts
1078
+ await db
1079
+ .insertInto('editor_scenarios')
1080
+ .values({
1081
+ id: 'sc-1',
1082
+ project_id: projectId,
1083
+ name: 'ArticleRow - Default',
1084
+ component_name: 'ArticleRow',
1085
+ page_file_path: null,
1086
+ created_at: '2026-03-12 13:00:00',
1087
+ })
1088
+ .execute();
1089
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1090
+ expect(counts).toEqual({});
1091
+ });
1092
+ it('should respect featureStartedAt filter', async () => {
1093
+ await db
1094
+ .insertInto('editor_scenarios')
1095
+ .values({
1096
+ id: 'sc-1',
1097
+ project_id: projectId,
1098
+ name: 'Library - Old',
1099
+ component_name: null,
1100
+ page_file_path: 'app/library/page.tsx',
1101
+ created_at: '2026-03-12 13:00:00',
1102
+ })
1103
+ .execute();
1104
+ await db
1105
+ .insertInto('editor_scenarios')
1106
+ .values({
1107
+ id: 'sc-2',
1108
+ project_id: projectId,
1109
+ name: 'Library - New',
1110
+ component_name: null,
1111
+ page_file_path: 'app/library/page.tsx',
1112
+ created_at: '2026-03-12 15:00:00',
1113
+ })
1114
+ .execute();
1115
+ const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1116
+ expect(counts).toEqual({ 'app/library/page.tsx': 1 });
1117
+ });
1118
+ it('should count re-registered page scenarios via updated_at', async () => {
1119
+ await db
1120
+ .insertInto('editor_scenarios')
1121
+ .values({
1122
+ id: 'sc-1',
1123
+ project_id: projectId,
1124
+ name: 'Library - Default',
1125
+ component_name: null,
1126
+ page_file_path: 'app/library/page.tsx',
1127
+ created_at: '2026-03-12 13:00:00',
1128
+ updated_at: '2026-03-12 15:00:00',
1129
+ })
1130
+ .execute();
1131
+ const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1132
+ expect(counts).toEqual({ 'app/library/page.tsx': 1 });
1133
+ });
1134
+ it('should group counts by page_file_path across multiple pages', async () => {
1135
+ await db
1136
+ .insertInto('editor_scenarios')
1137
+ .values([
1138
+ {
1139
+ id: 'sc-1',
1140
+ project_id: projectId,
1141
+ name: 'Library - Default',
1142
+ component_name: null,
1143
+ page_file_path: 'app/library/page.tsx',
1144
+ created_at: '2026-03-12 13:00:00',
1145
+ },
1146
+ {
1147
+ id: 'sc-2',
1148
+ project_id: projectId,
1149
+ name: 'Library - Rich',
1150
+ component_name: null,
1151
+ page_file_path: 'app/library/page.tsx',
1152
+ created_at: '2026-03-12 13:00:00',
1153
+ },
1154
+ {
1155
+ id: 'sc-3',
1156
+ project_id: projectId,
1157
+ name: 'Collections - Default',
1158
+ component_name: null,
1159
+ page_file_path: 'app/library/collections/page.tsx',
1160
+ created_at: '2026-03-12 13:00:00',
1161
+ },
1162
+ ])
1163
+ .execute();
1164
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1165
+ expect(counts).toEqual({
1166
+ 'app/library/page.tsx': 2,
1167
+ 'app/library/collections/page.tsx': 1,
1168
+ });
1169
+ });
1170
+ });
1009
1171
  // ── Audit + entity completeness integration ─────────────────────────
1010
1172
  describe('audit should catch incomplete entities (bug reproduction)', () => {
1011
1173
  let db;
@@ -1314,8 +1476,146 @@ describe('editorAudit', () => {
1314
1476
  expect(auditResult.summary.incompleteEntities).toBeUndefined();
1315
1477
  });
1316
1478
  });
1317
- // ── queryMiscategorizedScenarios ─────────────────────────────────────
1318
- describe('queryMiscategorizedScenarios', () => {
1479
+ // ── filterToIncompleteFilePaths ──────────────────────────────────────
1480
+ describe('filterToIncompleteFilePaths', () => {
1481
+ // analyze-imports processes ALL file paths (~120 files) even when only
1482
+ // a few need analysis. This function filters to files that have no
1483
+ // entity with an analysis record.
1484
+ let db;
1485
+ let rawDb;
1486
+ const projectId = 'test-project-id';
1487
+ beforeEach(async () => {
1488
+ rawDb = new Database(':memory:');
1489
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1490
+ await db.schema
1491
+ .createTable('analyses')
1492
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1493
+ .addColumn('entity_sha', 'varchar')
1494
+ .addColumn('entity_name', 'varchar')
1495
+ .addColumn('project_id', 'varchar')
1496
+ .execute();
1497
+ await db.schema
1498
+ .createTable('entities')
1499
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1500
+ .addColumn('name', 'varchar')
1501
+ .addColumn('entity_type', 'varchar')
1502
+ .addColumn('file_path', 'varchar')
1503
+ .execute();
1504
+ });
1505
+ afterEach(async () => {
1506
+ await db.destroy();
1507
+ });
1508
+ it('should exclude files that have an entity with an analysis', async () => {
1509
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1510
+ // Entity with analysis — skip
1511
+ await db
1512
+ .insertInto('entities')
1513
+ .values({
1514
+ sha: 'sha-header',
1515
+ name: 'Header',
1516
+ entity_type: 'visual',
1517
+ file_path: 'app/components/Header.tsx',
1518
+ })
1519
+ .execute();
1520
+ await db
1521
+ .insertInto('analyses')
1522
+ .values({
1523
+ id: 'a-1',
1524
+ entity_sha: 'sha-header',
1525
+ entity_name: 'Header',
1526
+ project_id: projectId,
1527
+ })
1528
+ .execute();
1529
+ // Entity without analysis — needs analysis
1530
+ await db
1531
+ .insertInto('entities')
1532
+ .values({
1533
+ sha: 'sha-rule',
1534
+ name: 'RuleBuilder',
1535
+ entity_type: 'visual',
1536
+ file_path: 'app/components/RuleBuilder.tsx',
1537
+ })
1538
+ .execute();
1539
+ const allFilePaths = [
1540
+ 'app/components/Header.tsx',
1541
+ 'app/components/RuleBuilder.tsx',
1542
+ 'app/components/Footer.tsx', // no entity — needs analysis
1543
+ ];
1544
+ const result = await filterToIncompleteFilePaths(db, projectId, allFilePaths);
1545
+ expect(result).toContain('app/components/RuleBuilder.tsx');
1546
+ expect(result).toContain('app/components/Footer.tsx');
1547
+ expect(result).not.toContain('app/components/Header.tsx');
1548
+ });
1549
+ it('should return all file paths when no entities exist yet', async () => {
1550
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1551
+ const filePaths = ['app/Foo.tsx', 'app/Bar.tsx'];
1552
+ const result = await filterToIncompleteFilePaths(db, projectId, filePaths);
1553
+ expect(result).toEqual(filePaths);
1554
+ });
1555
+ it('should return empty when all files have analyzed entities', async () => {
1556
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1557
+ await db
1558
+ .insertInto('entities')
1559
+ .values({
1560
+ sha: 'sha-a',
1561
+ name: 'CompA',
1562
+ entity_type: 'visual',
1563
+ file_path: 'app/CompA.tsx',
1564
+ })
1565
+ .execute();
1566
+ await db
1567
+ .insertInto('analyses')
1568
+ .values({
1569
+ id: 'a-1',
1570
+ entity_sha: 'sha-a',
1571
+ entity_name: 'CompA',
1572
+ project_id: projectId,
1573
+ })
1574
+ .execute();
1575
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1576
+ 'app/CompA.tsx',
1577
+ ]);
1578
+ expect(result).toEqual([]);
1579
+ });
1580
+ it('should skip files with analysis even without scenarios', async () => {
1581
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1582
+ // Entity with analysis but NO scenarios — still complete
1583
+ await db
1584
+ .insertInto('entities')
1585
+ .values({
1586
+ sha: 'sha-util',
1587
+ name: 'utils',
1588
+ entity_type: 'library',
1589
+ file_path: 'app/lib/utils.ts',
1590
+ })
1591
+ .execute();
1592
+ await db
1593
+ .insertInto('analyses')
1594
+ .values({
1595
+ id: 'a-1',
1596
+ entity_sha: 'sha-util',
1597
+ entity_name: 'utils',
1598
+ project_id: projectId,
1599
+ })
1600
+ .execute();
1601
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1602
+ 'app/lib/utils.ts',
1603
+ ]);
1604
+ expect(result).toEqual([]);
1605
+ });
1606
+ });
1607
+ // ── phantom entity SHAs ─────────────────────────────────────────────
1608
+ describe('queryIncompleteEntities with phantom entity SHAs', () => {
1609
+ // Root cause of the Margo/reader step-blocking bug:
1610
+ // Scenarios registered without component_path got a phantom entity_sha
1611
+ // computed from component_name alone. These SHAs have NO entity record
1612
+ // in the entities table and NO analyses. syncScenarioEntityShas can't
1613
+ // fix them (it skips scenarios without component_path). So they remain
1614
+ // "incomplete" forever, blocking step progression.
1615
+ //
1616
+ // Fix: queryIncompleteEntities should not report scenarios whose
1617
+ // entity_sha has no entity record — these are orphaned data, not
1618
+ // fixable by running analyze-imports.
1319
1619
  let db;
1320
1620
  let rawDb;
1321
1621
  const projectId = 'test-project-id';
@@ -1336,94 +1636,229 @@ describe('editorAudit', () => {
1336
1636
  .addColumn('created_at', 'datetime')
1337
1637
  .addColumn('updated_at', 'datetime')
1338
1638
  .execute();
1639
+ await db.schema
1640
+ .createTable('analyses')
1641
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1642
+ .addColumn('entity_sha', 'varchar')
1643
+ .addColumn('entity_name', 'varchar')
1644
+ .addColumn('project_id', 'varchar')
1645
+ .execute();
1646
+ await db.schema
1647
+ .createTable('entities')
1648
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1649
+ .addColumn('name', 'varchar')
1650
+ .addColumn('entity_type', 'varchar')
1651
+ .addColumn('file_path', 'varchar')
1652
+ .execute();
1339
1653
  });
1340
1654
  afterEach(async () => {
1341
1655
  await db.destroy();
1342
1656
  });
1343
- it('should return empty when all component scenarios use isolation routes', async () => {
1657
+ it('should not report scenarios with phantom entity SHAs (no entity record exists)', async () => {
1658
+ // Real entity — has an entity record, analyses, and scenarios. Complete.
1344
1659
  await db
1345
- .insertInto('editor_scenarios')
1660
+ .insertInto('entities')
1346
1661
  .values({
1347
- id: 'sc-1',
1348
- project_id: projectId,
1349
- name: 'LibraryCard - Default',
1350
- component_name: 'LibraryCard',
1351
- url: '/isolated-components/LibraryCard?s=Default',
1352
- created_at: '2026-03-17 12:00:00',
1662
+ sha: 'sha-real',
1663
+ name: 'RuleBuilder',
1664
+ entity_type: 'visual',
1665
+ file_path: 'app/components/RuleBuilder.tsx',
1353
1666
  })
1354
1667
  .execute();
1355
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1356
- expect(result).toEqual([]);
1357
- });
1358
- it('should flag component scenarios that use non-isolation URLs', async () => {
1359
- // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1360
- // but url=/library — it's pointing at the real page, not an isolation route
1361
1668
  await db
1362
- .insertInto('editor_scenarios')
1669
+ .insertInto('analyses')
1363
1670
  .values({
1364
- id: 'sc-1',
1671
+ id: 'a-1',
1672
+ entity_sha: 'sha-real',
1673
+ entity_name: 'RuleBuilder',
1365
1674
  project_id: projectId,
1366
- name: 'Full Library Page',
1367
- component_name: 'LibraryPage',
1368
- url: '/library',
1369
- created_at: '2026-03-17 12:41:40',
1370
1675
  })
1371
1676
  .execute();
1372
1677
  await db
1373
1678
  .insertInto('editor_scenarios')
1374
1679
  .values({
1375
- id: 'sc-2',
1680
+ id: 'sc-good',
1376
1681
  project_id: projectId,
1377
- name: 'Empty Library Page',
1378
- component_name: 'LibraryPage',
1379
- url: '/library',
1380
- created_at: '2026-03-17 12:41:51',
1682
+ name: 'RuleBuilder - Empty',
1683
+ component_name: 'RuleBuilder',
1684
+ component_path: 'app/components/RuleBuilder.tsx',
1685
+ entity_sha: 'sha-real',
1686
+ created_at: '2026-03-16 23:00:00',
1381
1687
  })
1382
1688
  .execute();
1383
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1384
- expect(result).toEqual([
1385
- {
1386
- componentName: 'LibraryPage',
1387
- scenarioNames: ['Full Library Page', 'Empty Library Page'],
1388
- url: '/library',
1389
- },
1390
- ]);
1391
- });
1392
- it('should not flag page-level scenarios (no component_name)', async () => {
1393
- // App-level scenarios have no component_name — they're fine with real URLs
1689
+ // Phantom entity scenario points to a SHA that doesn't exist
1690
+ // in the entities table (registered without component_path).
1691
+ // No entity record, no analyses, unfixable by analyze-imports.
1394
1692
  await db
1395
1693
  .insertInto('editor_scenarios')
1396
1694
  .values({
1397
- id: 'sc-1',
1695
+ id: 'sc-phantom',
1398
1696
  project_id: projectId,
1399
- name: 'Library with Articles',
1400
- url: '/',
1401
- created_at: '2026-03-17 12:25:14',
1697
+ name: 'Empty',
1698
+ component_name: 'RuleBuilder',
1699
+ component_path: null,
1700
+ entity_sha: 'sha-phantom-no-entity-record',
1701
+ created_at: '2026-03-16 22:00:00',
1402
1702
  })
1403
1703
  .execute();
1404
- const result = await queryMiscategorizedScenarios(db, projectId, null);
1405
- expect(result).toEqual([]);
1704
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1705
+ // Should NOT report phantom SHAs as incomplete — they can't be fixed
1706
+ // by running analyze-imports (no entity record exists to resolve).
1707
+ expect(incomplete).toHaveLength(0);
1406
1708
  });
1407
- it('should group miscategorized scenarios by component and URL', async () => {
1408
- // Two different components both misusing real URLs
1709
+ it('should still report real incomplete entities (entity exists but no analysis)', async () => {
1710
+ // Real entity without analysis this IS a legitimate incomplete entity
1711
+ await db
1712
+ .insertInto('entities')
1713
+ .values({
1714
+ sha: 'sha-noanalysis',
1715
+ name: 'Footer',
1716
+ entity_type: 'visual',
1717
+ file_path: 'app/components/Footer.tsx',
1718
+ })
1719
+ .execute();
1409
1720
  await db
1410
1721
  .insertInto('editor_scenarios')
1411
1722
  .values({
1412
1723
  id: 'sc-1',
1413
1724
  project_id: projectId,
1414
- name: 'Full Library Page',
1415
- component_name: 'LibraryPage',
1416
- url: '/library',
1417
- created_at: '2026-03-17 12:41:40',
1725
+ name: 'Footer - Default',
1726
+ component_name: 'Footer',
1727
+ component_path: 'app/components/Footer.tsx',
1728
+ entity_sha: 'sha-noanalysis',
1729
+ created_at: '2026-03-16 23:00:00',
1418
1730
  })
1419
1731
  .execute();
1732
+ // Phantom scenario (shouldn't affect results)
1420
1733
  await db
1421
1734
  .insertInto('editor_scenarios')
1422
1735
  .values({
1423
- id: 'sc-2',
1736
+ id: 'sc-phantom',
1424
1737
  project_id: projectId,
1425
- name: 'Dashboard - Full',
1426
- component_name: 'Dashboard',
1738
+ name: 'Footer - Alt',
1739
+ component_name: 'Footer',
1740
+ component_path: null,
1741
+ entity_sha: 'sha-phantom-does-not-exist',
1742
+ created_at: '2026-03-16 22:00:00',
1743
+ })
1744
+ .execute();
1745
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1746
+ // Should report Footer (real entity, no analysis) but NOT the phantom
1747
+ expect(incomplete).toHaveLength(1);
1748
+ expect(incomplete[0].name).toBe('Footer');
1749
+ expect(incomplete[0].entitySha).toBe('sha-noanalysis');
1750
+ });
1751
+ });
1752
+ // ── queryMiscategorizedScenarios ─────────────────────────────────────
1753
+ describe('queryMiscategorizedScenarios', () => {
1754
+ let db;
1755
+ let rawDb;
1756
+ const projectId = 'test-project-id';
1757
+ beforeEach(async () => {
1758
+ rawDb = new Database(':memory:');
1759
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1760
+ await db.schema
1761
+ .createTable('editor_scenarios')
1762
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1763
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1764
+ .addColumn('name', 'varchar', (col) => col.notNull())
1765
+ .addColumn('component_name', 'varchar')
1766
+ .addColumn('component_path', 'varchar')
1767
+ .addColumn('entity_sha', 'varchar')
1768
+ .addColumn('display_name', 'varchar')
1769
+ .addColumn('page_file_path', 'varchar')
1770
+ .addColumn('url', 'varchar')
1771
+ .addColumn('created_at', 'datetime')
1772
+ .addColumn('updated_at', 'datetime')
1773
+ .execute();
1774
+ });
1775
+ afterEach(async () => {
1776
+ await db.destroy();
1777
+ });
1778
+ it('should return empty when all component scenarios use isolation routes', async () => {
1779
+ await db
1780
+ .insertInto('editor_scenarios')
1781
+ .values({
1782
+ id: 'sc-1',
1783
+ project_id: projectId,
1784
+ name: 'LibraryCard - Default',
1785
+ component_name: 'LibraryCard',
1786
+ url: '/isolated-components/LibraryCard?s=Default',
1787
+ created_at: '2026-03-17 12:00:00',
1788
+ })
1789
+ .execute();
1790
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1791
+ expect(result).toEqual([]);
1792
+ });
1793
+ it('should flag component scenarios that use non-isolation URLs', async () => {
1794
+ // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1795
+ // but url=/library — it's pointing at the real page, not an isolation route
1796
+ await db
1797
+ .insertInto('editor_scenarios')
1798
+ .values({
1799
+ id: 'sc-1',
1800
+ project_id: projectId,
1801
+ name: 'Full Library Page',
1802
+ component_name: 'LibraryPage',
1803
+ url: '/library',
1804
+ created_at: '2026-03-17 12:41:40',
1805
+ })
1806
+ .execute();
1807
+ await db
1808
+ .insertInto('editor_scenarios')
1809
+ .values({
1810
+ id: 'sc-2',
1811
+ project_id: projectId,
1812
+ name: 'Empty Library Page',
1813
+ component_name: 'LibraryPage',
1814
+ url: '/library',
1815
+ created_at: '2026-03-17 12:41:51',
1816
+ })
1817
+ .execute();
1818
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1819
+ expect(result).toEqual([
1820
+ {
1821
+ componentName: 'LibraryPage',
1822
+ scenarioNames: ['Full Library Page', 'Empty Library Page'],
1823
+ url: '/library',
1824
+ },
1825
+ ]);
1826
+ });
1827
+ it('should not flag page-level scenarios (no component_name)', async () => {
1828
+ // App-level scenarios have no component_name — they're fine with real URLs
1829
+ await db
1830
+ .insertInto('editor_scenarios')
1831
+ .values({
1832
+ id: 'sc-1',
1833
+ project_id: projectId,
1834
+ name: 'Library with Articles',
1835
+ url: '/',
1836
+ created_at: '2026-03-17 12:25:14',
1837
+ })
1838
+ .execute();
1839
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1840
+ expect(result).toEqual([]);
1841
+ });
1842
+ it('should group miscategorized scenarios by component and URL', async () => {
1843
+ // Two different components both misusing real URLs
1844
+ await db
1845
+ .insertInto('editor_scenarios')
1846
+ .values({
1847
+ id: 'sc-1',
1848
+ project_id: projectId,
1849
+ name: 'Full Library Page',
1850
+ component_name: 'LibraryPage',
1851
+ url: '/library',
1852
+ created_at: '2026-03-17 12:41:40',
1853
+ })
1854
+ .execute();
1855
+ await db
1856
+ .insertInto('editor_scenarios')
1857
+ .values({
1858
+ id: 'sc-2',
1859
+ project_id: projectId,
1860
+ name: 'Dashboard - Full',
1861
+ component_name: 'Dashboard',
1427
1862
  url: '/dashboard',
1428
1863
  created_at: '2026-03-17 12:50:00',
1429
1864
  })
@@ -1562,10 +1997,31 @@ describe('editorAudit', () => {
1562
1997
  allPassing: false,
1563
1998
  })).toBe(true);
1564
1999
  });
2000
+ it('should return false when there are also runner errors', () => {
2001
+ // functionsRunnerError means the test runner crashed — a real failure
2002
+ // that cannot be fixed by entity SHA backfill or analyze-imports.
2003
+ // If this returns true, checkAuditGate would attempt a useless backfill
2004
+ // instead of reporting the runner error, and isOnlyPreExistingIncomplete
2005
+ // could let the gate pass entirely.
2006
+ expect(isOnlyIncompleteEntities({
2007
+ componentsMissing: 0,
2008
+ componentsWithErrors: 0,
2009
+ functionsFailing: 0,
2010
+ functionsRunnerError: 2,
2011
+ functionsNameMismatch: 0,
2012
+ functionsMissing: 0,
2013
+ missingFromGlossary: 0,
2014
+ incompleteEntities: 1,
2015
+ allPassing: false,
2016
+ })).toBe(false);
2017
+ });
1565
2018
  });
1566
2019
  // ── isAutoRemediable ─────────────────────────────────────────────────
1567
2020
  describe('isAutoRemediable', () => {
1568
- it('should return true on first attempt when only incomplete entities', () => {
2021
+ // isAutoRemediable always returns false the audit never triggers
2022
+ // full analyze-imports inline. It takes minutes on large projects.
2023
+ // Only lightweight backfill is acceptable during audit.
2024
+ it('should return false even on first attempt with only incomplete entities', () => {
1569
2025
  const result = isAutoRemediable({
1570
2026
  componentsMissing: 0,
1571
2027
  componentsWithErrors: 0,
@@ -1577,11 +2033,9 @@ describe('editorAudit', () => {
1577
2033
  incompleteEntities: 3,
1578
2034
  allPassing: false,
1579
2035
  }, false);
1580
- expect(result).toBe(true);
2036
+ expect(result).toBe(false);
1581
2037
  });
1582
- it('should return false on second attempt (already tried once)', () => {
1583
- // This is the key fix: if we already tried analyze-imports and
1584
- // entities are STILL incomplete, don't try again — report the failure
2038
+ it('should return false on second attempt', () => {
1585
2039
  const result = isAutoRemediable({
1586
2040
  componentsMissing: 0,
1587
2041
  componentsWithErrors: 0,
@@ -1721,7 +2175,12 @@ describe('editorAudit', () => {
1721
2175
  .execute();
1722
2176
  const result = await queryIncompleteEntities(db, projectId, null);
1723
2177
  expect(result).toEqual([
1724
- { entitySha: 'sha-chips', name: 'CollectionChips', scenarioCount: 2 },
2178
+ {
2179
+ entitySha: 'sha-chips',
2180
+ name: 'CollectionChips',
2181
+ scenarioCount: 2,
2182
+ preExisting: false,
2183
+ },
1725
2184
  ]);
1726
2185
  });
1727
2186
  it('should only return entities without analyses, not those with analyses', async () => {
@@ -1778,10 +2237,15 @@ describe('editorAudit', () => {
1778
2237
  .execute();
1779
2238
  const result = await queryIncompleteEntities(db, projectId, null);
1780
2239
  expect(result).toEqual([
1781
- { entitySha: 'sha-picker', name: 'CollectionPicker', scenarioCount: 1 },
2240
+ {
2241
+ entitySha: 'sha-picker',
2242
+ name: 'CollectionPicker',
2243
+ scenarioCount: 1,
2244
+ preExisting: false,
2245
+ },
1782
2246
  ]);
1783
2247
  });
1784
- it('should scope to session when featureStartedAt is provided', async () => {
2248
+ it('should return both pre-session and in-session entities with preExisting flags', async () => {
1785
2249
  // Entity without analysis, scenario created BEFORE session
1786
2250
  await db
1787
2251
  .insertInto('entities')
@@ -1825,12 +2289,24 @@ describe('editorAudit', () => {
1825
2289
  })
1826
2290
  .execute();
1827
2291
  const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
1828
- // Only NewComponent should be flagged (created in session)
1829
- expect(result).toEqual([
1830
- { entitySha: 'sha-new', name: 'NewComponent', scenarioCount: 1 },
1831
- ]);
2292
+ // Both should be returned OldComponent is preExisting, NewComponent is not
2293
+ expect(result).toEqual(expect.arrayContaining([
2294
+ {
2295
+ entitySha: 'sha-old',
2296
+ name: 'OldComponent',
2297
+ scenarioCount: 1,
2298
+ preExisting: true,
2299
+ },
2300
+ {
2301
+ entitySha: 'sha-new',
2302
+ name: 'NewComponent',
2303
+ scenarioCount: 1,
2304
+ preExisting: false,
2305
+ },
2306
+ ]));
2307
+ expect(result).toHaveLength(2);
1832
2308
  });
1833
- it('should include scenarios updated in session even if created before', async () => {
2309
+ it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
1834
2310
  await db
1835
2311
  .insertInto('entities')
1836
2312
  .values({
@@ -1858,6 +2334,7 @@ describe('editorAudit', () => {
1858
2334
  entitySha: 'sha-updated',
1859
2335
  name: 'UpdatedComponent',
1860
2336
  scenarioCount: 1,
2337
+ preExisting: false,
1861
2338
  },
1862
2339
  ]);
1863
2340
  });
@@ -1925,6 +2402,60 @@ describe('editorAudit', () => {
1925
2402
  const result = await queryIncompleteEntities(db, projectId, null);
1926
2403
  expect(result).toEqual([]);
1927
2404
  });
2405
+ it('should flag entity when sibling has analyses but different filePath (extracted component)', async () => {
2406
+ // Old entity version WITH analysis at ORIGINAL file path
2407
+ await db
2408
+ .insertInto('entities')
2409
+ .values({
2410
+ sha: 'sha-card-v1',
2411
+ name: 'TaskCard',
2412
+ entity_type: 'visual',
2413
+ file_path: 'app/page.tsx',
2414
+ })
2415
+ .execute();
2416
+ await db
2417
+ .insertInto('analyses')
2418
+ .values({
2419
+ id: 'a-card-v1',
2420
+ entity_sha: 'sha-card-v1',
2421
+ entity_name: 'TaskCard',
2422
+ project_id: projectId,
2423
+ })
2424
+ .execute();
2425
+ // New entity version WITHOUT analysis at EXTRACTED file path
2426
+ await db
2427
+ .insertInto('entities')
2428
+ .values({
2429
+ sha: 'sha-card-v2',
2430
+ name: 'TaskCard',
2431
+ entity_type: 'visual',
2432
+ file_path: 'app/components/TaskCard.tsx',
2433
+ })
2434
+ .execute();
2435
+ // Scenario points to the new version (synced by syncScenarioEntityShas)
2436
+ await db
2437
+ .insertInto('editor_scenarios')
2438
+ .values({
2439
+ id: 'sc-card',
2440
+ project_id: projectId,
2441
+ name: 'TaskCard - Default',
2442
+ component_name: 'TaskCard',
2443
+ entity_sha: 'sha-card-v2',
2444
+ created_at: '2026-03-16 23:00:00',
2445
+ })
2446
+ .execute();
2447
+ // SHOULD flag as incomplete — sibling has analyses but at a different filePath,
2448
+ // so getAllEntities() won't inherit (it matches by name+filePath)
2449
+ const result = await queryIncompleteEntities(db, projectId, null);
2450
+ expect(result).toEqual([
2451
+ {
2452
+ entitySha: 'sha-card-v2',
2453
+ name: 'TaskCard',
2454
+ scenarioCount: 1,
2455
+ preExisting: false,
2456
+ },
2457
+ ]);
2458
+ });
1928
2459
  it('should still flag entities when no sibling version has analyses', async () => {
1929
2460
  // Only one version, no analyses
1930
2461
  await db
@@ -1954,11 +2485,15 @@ describe('editorAudit', () => {
1954
2485
  entitySha: 'sha-icon',
1955
2486
  name: 'ExternalLinkIcon',
1956
2487
  scenarioCount: 1,
2488
+ preExisting: false,
1957
2489
  },
1958
2490
  ]);
1959
2491
  });
1960
- it('should use entity name from entities table, falling back to component_name', async () => {
1961
- // Scenario has entity_sha but entity record doesn't exist
2492
+ it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
2493
+ // Scenario has entity_sha but entity record doesn't exist.
2494
+ // These are "phantom SHAs" created when scenarios were registered
2495
+ // without component_path — they can never be fixed by analyze-imports
2496
+ // and should not block audit progression.
1962
2497
  await db
1963
2498
  .insertInto('editor_scenarios')
1964
2499
  .values({
@@ -1971,10 +2506,1519 @@ describe('editorAudit', () => {
1971
2506
  })
1972
2507
  .execute();
1973
2508
  const result = await queryIncompleteEntities(db, projectId, null);
2509
+ // Phantom SHAs are excluded — not reportable as incomplete
2510
+ expect(result).toEqual([]);
2511
+ });
2512
+ it('should detect incomplete entity whose scenario predates the session', async () => {
2513
+ // Entity with no analyses, scenario created BEFORE session
2514
+ await db
2515
+ .insertInto('entities')
2516
+ .values({
2517
+ sha: 'sha-preexisting',
2518
+ name: 'PreExistingComponent',
2519
+ entity_type: 'visual',
2520
+ file_path: 'src/PreExistingComponent.tsx',
2521
+ })
2522
+ .execute();
2523
+ await db
2524
+ .insertInto('editor_scenarios')
2525
+ .values({
2526
+ id: 'sc-preexisting',
2527
+ project_id: projectId,
2528
+ name: 'PreExistingComponent - Default',
2529
+ component_name: 'PreExistingComponent',
2530
+ entity_sha: 'sha-preexisting',
2531
+ created_at: '2026-03-16 20:00:00',
2532
+ updated_at: '2026-03-16 20:00:00',
2533
+ })
2534
+ .execute();
2535
+ // Session started well after scenario was created/updated
2536
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2537
+ // Should still be detected — the old time filter would have excluded it
2538
+ expect(result).toEqual([
2539
+ {
2540
+ entitySha: 'sha-preexisting',
2541
+ name: 'PreExistingComponent',
2542
+ scenarioCount: 1,
2543
+ preExisting: true,
2544
+ },
2545
+ ]);
2546
+ });
2547
+ it('should flag preExisting: true when all scenarios predate the session', async () => {
2548
+ await db
2549
+ .insertInto('entities')
2550
+ .values({
2551
+ sha: 'sha-old-entity',
2552
+ name: 'OldEntity',
2553
+ entity_type: 'visual',
2554
+ file_path: 'src/OldEntity.tsx',
2555
+ })
2556
+ .execute();
2557
+ // Two scenarios, both before session
2558
+ await db
2559
+ .insertInto('editor_scenarios')
2560
+ .values({
2561
+ id: 'sc-old-1',
2562
+ project_id: projectId,
2563
+ name: 'OldEntity - Default',
2564
+ component_name: 'OldEntity',
2565
+ entity_sha: 'sha-old-entity',
2566
+ created_at: '2026-03-16 19:00:00',
2567
+ updated_at: '2026-03-16 19:00:00',
2568
+ })
2569
+ .execute();
2570
+ await db
2571
+ .insertInto('editor_scenarios')
2572
+ .values({
2573
+ id: 'sc-old-2',
2574
+ project_id: projectId,
2575
+ name: 'OldEntity - Hover',
2576
+ component_name: 'OldEntity',
2577
+ entity_sha: 'sha-old-entity',
2578
+ created_at: '2026-03-16 19:30:00',
2579
+ updated_at: '2026-03-16 19:30:00',
2580
+ })
2581
+ .execute();
2582
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2583
+ expect(result).toEqual([
2584
+ {
2585
+ entitySha: 'sha-old-entity',
2586
+ name: 'OldEntity',
2587
+ scenarioCount: 2,
2588
+ preExisting: true,
2589
+ },
2590
+ ]);
2591
+ });
2592
+ it('should flag preExisting: false when scenario is from the current session', async () => {
2593
+ await db
2594
+ .insertInto('entities')
2595
+ .values({
2596
+ sha: 'sha-session-entity',
2597
+ name: 'SessionEntity',
2598
+ entity_type: 'visual',
2599
+ file_path: 'src/SessionEntity.tsx',
2600
+ })
2601
+ .execute();
2602
+ await db
2603
+ .insertInto('editor_scenarios')
2604
+ .values({
2605
+ id: 'sc-session',
2606
+ project_id: projectId,
2607
+ name: 'SessionEntity - Default',
2608
+ component_name: 'SessionEntity',
2609
+ entity_sha: 'sha-session-entity',
2610
+ created_at: '2026-03-16 23:30:00',
2611
+ updated_at: '2026-03-16 23:30:00',
2612
+ })
2613
+ .execute();
2614
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
1974
2615
  expect(result).toEqual([
1975
- { entitySha: 'sha-ghost', name: 'GhostComponent', scenarioCount: 1 },
2616
+ {
2617
+ entitySha: 'sha-session-entity',
2618
+ name: 'SessionEntity',
2619
+ scenarioCount: 1,
2620
+ preExisting: false,
2621
+ },
1976
2622
  ]);
1977
2623
  });
1978
2624
  });
2625
+ // ── identifyScenariosNeedingRecapture ──────────────────────────────
2626
+ describe('identifyScenariosNeedingRecapture', () => {
2627
+ // Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
2628
+ // Feature 2 edited LibraryView (used by App), but app-level scenarios
2629
+ // were never flagged for recapture because the audit only checked
2630
+ // component scenario existence — not whether app-level scenarios are stale.
2631
+ //
2632
+ // Each scenario's entityName is resolved by the caller via
2633
+ // entity_sha → entities.name (the default export for app-level scenarios).
2634
+ it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
2635
+ // LibraryView was edited → App is impacted (imports LibraryView)
2636
+ // App-level scenario "Library - Rich Library" has entity_sha pointing to App
2637
+ // It was NOT recaptured during Feature 2 → should be flagged
2638
+ const entityChangeStatus = {
2639
+ LibraryView: { status: 'edited' },
2640
+ App: {
2641
+ status: 'impacted',
2642
+ impactedBy: [
2643
+ {
2644
+ name: 'LibraryView',
2645
+ filePath: 'src/components/LibraryView.tsx',
2646
+ changeType: 'edited',
2647
+ },
2648
+ ],
2649
+ },
2650
+ };
2651
+ const result = identifyScenariosNeedingRecapture({
2652
+ scenarios: [
2653
+ {
2654
+ name: 'Library - Rich Library',
2655
+ entityName: 'App', // resolved from entity_sha → entities.name
2656
+ updatedInSession: false,
2657
+ },
2658
+ ],
2659
+ entityChangeStatus,
2660
+ });
2661
+ expect(result).toHaveLength(1);
2662
+ expect(result[0].scenarioName).toBe('Library - Rich Library');
2663
+ expect(result[0].entityName).toBe('App');
2664
+ expect(result[0].status.status).toBe('impacted');
2665
+ });
2666
+ it('should flag component scenario when its entity is directly edited and not recaptured', () => {
2667
+ const entityChangeStatus = {
2668
+ LibraryView: { status: 'edited' },
2669
+ };
2670
+ const result = identifyScenariosNeedingRecapture({
2671
+ scenarios: [
2672
+ {
2673
+ name: 'LibraryView - Empty',
2674
+ entityName: 'LibraryView',
2675
+ updatedInSession: false,
2676
+ },
2677
+ ],
2678
+ entityChangeStatus,
2679
+ });
2680
+ expect(result).toHaveLength(1);
2681
+ expect(result[0].scenarioName).toBe('LibraryView - Empty');
2682
+ expect(result[0].entityName).toBe('LibraryView');
2683
+ expect(result[0].status.status).toBe('edited');
2684
+ });
2685
+ it('should NOT flag scenario that was already recaptured in the current session', () => {
2686
+ const entityChangeStatus = {
2687
+ App: {
2688
+ status: 'impacted',
2689
+ impactedBy: [
2690
+ {
2691
+ name: 'LibraryView',
2692
+ filePath: 'src/components/LibraryView.tsx',
2693
+ changeType: 'edited',
2694
+ },
2695
+ ],
2696
+ },
2697
+ };
2698
+ const result = identifyScenariosNeedingRecapture({
2699
+ scenarios: [
2700
+ {
2701
+ name: 'App - Default',
2702
+ entityName: 'App',
2703
+ updatedInSession: true, // re-registered during Feature 2
2704
+ },
2705
+ ],
2706
+ entityChangeStatus,
2707
+ });
2708
+ expect(result).toHaveLength(0);
2709
+ });
2710
+ it('should NOT flag scenario whose entity has no change status', () => {
2711
+ const entityChangeStatus = {
2712
+ LibraryView: { status: 'edited' },
2713
+ };
2714
+ const result = identifyScenariosNeedingRecapture({
2715
+ scenarios: [
2716
+ {
2717
+ name: 'WelcomeScreen - Default',
2718
+ entityName: 'WelcomeScreen',
2719
+ updatedInSession: false,
2720
+ },
2721
+ ],
2722
+ entityChangeStatus,
2723
+ });
2724
+ expect(result).toHaveLength(0);
2725
+ });
2726
+ it('should return empty array when entityChangeStatus is undefined', () => {
2727
+ const result = identifyScenariosNeedingRecapture({
2728
+ scenarios: [
2729
+ {
2730
+ name: 'Library - Rich Library',
2731
+ entityName: 'App',
2732
+ updatedInSession: false,
2733
+ },
2734
+ ],
2735
+ entityChangeStatus: undefined,
2736
+ });
2737
+ expect(result).toHaveLength(0);
2738
+ });
2739
+ it('should return empty array when entityChangeStatus is empty', () => {
2740
+ const result = identifyScenariosNeedingRecapture({
2741
+ scenarios: [
2742
+ {
2743
+ name: 'Library - Rich Library',
2744
+ entityName: 'App',
2745
+ updatedInSession: false,
2746
+ },
2747
+ ],
2748
+ entityChangeStatus: {},
2749
+ });
2750
+ expect(result).toHaveLength(0);
2751
+ });
2752
+ it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
2753
+ const entityChangeStatus = {
2754
+ LibraryView: { status: 'edited' },
2755
+ App: {
2756
+ status: 'impacted',
2757
+ impactedBy: [
2758
+ {
2759
+ name: 'LibraryView',
2760
+ filePath: 'src/components/LibraryView.tsx',
2761
+ changeType: 'edited',
2762
+ },
2763
+ ],
2764
+ },
2765
+ };
2766
+ const result = identifyScenariosNeedingRecapture({
2767
+ scenarios: [
2768
+ {
2769
+ name: 'Library - Empty',
2770
+ entityName: 'App',
2771
+ updatedInSession: false,
2772
+ },
2773
+ {
2774
+ name: 'Library - Rich Library',
2775
+ entityName: 'App',
2776
+ updatedInSession: false,
2777
+ },
2778
+ {
2779
+ name: 'First Article Saved',
2780
+ entityName: 'App',
2781
+ updatedInSession: false,
2782
+ },
2783
+ ],
2784
+ entityChangeStatus,
2785
+ });
2786
+ expect(result).toHaveLength(3);
2787
+ expect(result.map((r) => r.scenarioName).sort()).toEqual([
2788
+ 'First Article Saved',
2789
+ 'Library - Empty',
2790
+ 'Library - Rich Library',
2791
+ ]);
2792
+ expect(result.every((r) => r.entityName === 'App')).toBe(true);
2793
+ });
2794
+ it('should skip scenarios with null entityName (no entity_sha set)', () => {
2795
+ const entityChangeStatus = {
2796
+ App: { status: 'edited' },
2797
+ };
2798
+ const result = identifyScenariosNeedingRecapture({
2799
+ scenarios: [
2800
+ {
2801
+ name: 'Mystery Scenario',
2802
+ entityName: null, // no entity_sha → no entity name
2803
+ updatedInSession: false,
2804
+ },
2805
+ ],
2806
+ entityChangeStatus,
2807
+ });
2808
+ expect(result).toHaveLength(0);
2809
+ });
2810
+ it('should NOT flag scenarios for new entities (they need creation, not recapture)', () => {
2811
+ // "new" entities are being seen for the first time. Their scenarios need
2812
+ // initial creation, not recapture of old screenshots. Flagging them as
2813
+ // "needs_recapture" sends the wrong remediation signal to Claude.
2814
+ const entityChangeStatus = {
2815
+ NewComponent: { status: 'new' },
2816
+ };
2817
+ const result = identifyScenariosNeedingRecapture({
2818
+ scenarios: [
2819
+ {
2820
+ name: 'NewComponent - Default',
2821
+ entityName: 'NewComponent',
2822
+ updatedInSession: false,
2823
+ },
2824
+ ],
2825
+ entityChangeStatus,
2826
+ });
2827
+ expect(result).toHaveLength(0);
2828
+ });
2829
+ });
2830
+ // ── detectDuplicateNames ──────────────────────────────────────────
2831
+ describe('detectDuplicateNames', () => {
2832
+ it('should return empty map when no duplicates exist', () => {
2833
+ const entries = [
2834
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2835
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2836
+ { name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
2837
+ ];
2838
+ const result = detectDuplicateNames(entries);
2839
+ expect(result.size).toBe(0);
2840
+ });
2841
+ it('should group entries that share a name', () => {
2842
+ const entries = [
2843
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2844
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2845
+ { name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
2846
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2847
+ ];
2848
+ const result = detectDuplicateNames(entries);
2849
+ expect(result.size).toBe(1);
2850
+ expect(result.has('Page')).toBe(true);
2851
+ const pageGroup = result.get('Page');
2852
+ expect(pageGroup).toHaveLength(3);
2853
+ expect(pageGroup.map((e) => e.filePath)).toEqual([
2854
+ 'app/isolated-components/Foo/page.tsx',
2855
+ 'app/isolated-components/Bar/page.tsx',
2856
+ 'app/isolated-components/Baz/page.tsx',
2857
+ ]);
2858
+ });
2859
+ it('should exclude single-occurrence names', () => {
2860
+ const entries = [
2861
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2862
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2863
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2864
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2865
+ ];
2866
+ const result = detectDuplicateNames(entries);
2867
+ expect(result.size).toBe(1);
2868
+ expect(result.has('Header')).toBe(false);
2869
+ expect(result.has('Footer')).toBe(false);
2870
+ });
2871
+ it('should handle multiple duplicate groups', () => {
2872
+ const entries = [
2873
+ { name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
2874
+ { name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
2875
+ { name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
2876
+ { name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
2877
+ { name: 'Unique', filePath: 'app/components/Unique.tsx' },
2878
+ ];
2879
+ const result = detectDuplicateNames(entries);
2880
+ expect(result.size).toBe(2);
2881
+ expect(result.has('Page')).toBe(true);
2882
+ expect(result.has('Layout')).toBe(true);
2883
+ expect(result.get('Page')).toHaveLength(2);
2884
+ expect(result.get('Layout')).toHaveLength(2);
2885
+ });
2886
+ it('should return empty map for empty input', () => {
2887
+ const result = detectDuplicateNames([]);
2888
+ expect(result.size).toBe(0);
2889
+ });
2890
+ });
2891
+ // ── computeAudit: impacted components with stale scenarios ──────────
2892
+ describe('computeAudit — impacted components with stale scenarios', () => {
2893
+ it('should mark impacted component as needs_recapture when it has total scenarios but none in session', () => {
2894
+ // Library page has 3 scenarios from Feature 1 (totalScenarioCounts),
2895
+ // 0 in the current session (scenarioCounts), and is "impacted" in entityChangeStatus.
2896
+ // It should NOT be marked "missing" — it needs recapture, not new scenarios.
2897
+ const result = computeAudit({
2898
+ components: [
2899
+ {
2900
+ name: 'Library',
2901
+ filePath: 'app/library/page.tsx',
2902
+ returnType: 'JSX.Element',
2903
+ },
2904
+ {
2905
+ name: 'ArticleTable',
2906
+ filePath: 'app/components/ArticleTable.tsx',
2907
+ returnType: 'JSX.Element',
2908
+ },
2909
+ ],
2910
+ functions: [],
2911
+ scenarioCounts: { ArticleTable: 2 },
2912
+ testFileExistence: {},
2913
+ totalScenarioCounts: { Library: 3 },
2914
+ entityChangeStatus: {
2915
+ Library: { status: 'impacted' },
2916
+ ArticleTable: { status: 'edited' },
2917
+ },
2918
+ });
2919
+ // Library: impacted + has total scenarios but 0 in session → needs_recapture
2920
+ expect(result.components[0].status).toBe('needs_recapture');
2921
+ expect(result.components[0].scenarioCount).toBe(3);
2922
+ // ArticleTable: edited + has session scenarios → ok
2923
+ expect(result.components[1].status).toBe('ok');
2924
+ // needs_recapture should NOT count as missing
2925
+ expect(result.summary.componentsMissing).toBe(0);
2926
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
2927
+ // should NOT fail the audit (scenariosNeedingRecapture handles it)
2928
+ expect(result.summary.allPassing).toBe(true);
2929
+ });
2930
+ it('should still mark component as missing when impacted but has zero total scenarios', () => {
2931
+ // New page added to glossary but never had scenarios — truly missing
2932
+ const result = computeAudit({
2933
+ components: [
2934
+ {
2935
+ name: 'NewPage',
2936
+ filePath: 'app/new/page.tsx',
2937
+ returnType: 'JSX.Element',
2938
+ },
2939
+ ],
2940
+ functions: [],
2941
+ scenarioCounts: {},
2942
+ testFileExistence: {},
2943
+ totalScenarioCounts: {},
2944
+ entityChangeStatus: {
2945
+ NewPage: { status: 'impacted' },
2946
+ },
2947
+ });
2948
+ expect(result.components[0].status).toBe('missing');
2949
+ expect(result.summary.componentsMissing).toBe(1);
2950
+ });
2951
+ it('should use needs_recapture for edited entities with existing scenarios from prior sessions', () => {
2952
+ // Edited entities that already have scenarios from prior sessions
2953
+ // need recapture, not re-registration. The code changed, but the
2954
+ // scenarios exist — they just need fresh screenshots.
2955
+ const result = computeAudit({
2956
+ components: [
2957
+ {
2958
+ name: 'EditedComp',
2959
+ filePath: 'app/components/Edited.tsx',
2960
+ returnType: 'JSX.Element',
2961
+ },
2962
+ ],
2963
+ functions: [],
2964
+ scenarioCounts: {},
2965
+ testFileExistence: {},
2966
+ totalScenarioCounts: { EditedComp: 2 },
2967
+ entityChangeStatus: {
2968
+ EditedComp: { status: 'edited' },
2969
+ },
2970
+ });
2971
+ expect(result.components[0].status).toBe('needs_recapture');
2972
+ expect(result.summary.componentsMissing).toBe(0);
2973
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
2974
+ });
2975
+ it('should still mark as missing when new entity has zero total scenarios', () => {
2976
+ // Truly new component with no scenarios ever — needs scenarios created
2977
+ const result = computeAudit({
2978
+ components: [
2979
+ {
2980
+ name: 'BrandNew',
2981
+ filePath: 'app/components/BrandNew.tsx',
2982
+ returnType: 'JSX.Element',
2983
+ },
2984
+ ],
2985
+ functions: [],
2986
+ scenarioCounts: {},
2987
+ testFileExistence: {},
2988
+ totalScenarioCounts: {},
2989
+ entityChangeStatus: {
2990
+ BrandNew: { status: 'new' },
2991
+ },
2992
+ });
2993
+ expect(result.components[0].status).toBe('missing');
2994
+ expect(result.summary.componentsMissing).toBe(1);
2995
+ });
2996
+ it('should not count needs_recapture components as componentsOk', () => {
2997
+ // A needs_recapture component is not "ok" — it needs action. Counting it
2998
+ // in componentsOk is misleading: if totalComponents=2, componentsOk=2,
2999
+ // and componentsNeedingRecapture=1, the numbers don't add up (2+1 > 2).
3000
+ const result = computeAudit({
3001
+ components: [
3002
+ { name: 'Library', filePath: 'app/library/page.tsx' },
3003
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3004
+ ],
3005
+ functions: [],
3006
+ scenarioCounts: { DrinkCard: 2 },
3007
+ testFileExistence: {},
3008
+ totalScenarioCounts: { Library: 3 },
3009
+ entityChangeStatus: { Library: { status: 'impacted' } },
3010
+ });
3011
+ expect(result.components[0].status).toBe('needs_recapture');
3012
+ expect(result.components[1].status).toBe('ok');
3013
+ // needs_recapture is not "ok" — should be counted separately
3014
+ expect(result.summary.componentsOk).toBe(1);
3015
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
3016
+ expect(result.summary.totalComponents).toBe(2);
3017
+ });
3018
+ });
3019
+ // ── queryUnassociatedScenarios ──────────────────────────────────────
3020
+ describe('queryUnassociatedScenarios', () => {
3021
+ let db;
3022
+ let rawDb;
3023
+ const projectId = 'test-project-id';
3024
+ beforeEach(async () => {
3025
+ rawDb = new Database(':memory:');
3026
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3027
+ await db.schema
3028
+ .createTable('editor_scenarios')
3029
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
3030
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
3031
+ .addColumn('name', 'varchar', (col) => col.notNull())
3032
+ .addColumn('component_name', 'varchar')
3033
+ .addColumn('component_path', 'varchar')
3034
+ .addColumn('entity_sha', 'varchar')
3035
+ .addColumn('display_name', 'varchar')
3036
+ .addColumn('page_file_path', 'varchar')
3037
+ .addColumn('url', 'varchar')
3038
+ .addColumn('type', 'varchar')
3039
+ .addColumn('created_at', 'datetime')
3040
+ .addColumn('updated_at', 'datetime')
3041
+ .execute();
3042
+ });
3043
+ afterEach(async () => {
3044
+ await db.destroy();
3045
+ });
3046
+ it('should return empty when all scenarios have entity_sha', async () => {
3047
+ await db
3048
+ .insertInto('editor_scenarios')
3049
+ .values({
3050
+ id: 'sc-1',
3051
+ project_id: projectId,
3052
+ name: 'Header - Default',
3053
+ component_name: 'Header',
3054
+ component_path: 'src/components/Header.tsx',
3055
+ entity_sha: 'sha-header',
3056
+ created_at: '2026-03-16 23:00:00',
3057
+ updated_at: '2026-03-16 23:00:00',
3058
+ })
3059
+ .execute();
3060
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3061
+ expect(result).toEqual([]);
3062
+ });
3063
+ it('should find component scenarios with NULL entity_sha', async () => {
3064
+ // This reproduces the Margo testapp bug: subagent registered scenarios
3065
+ // but entity records didn't exist yet, so entity_sha was never set
3066
+ await db
3067
+ .insertInto('editor_scenarios')
3068
+ .values({
3069
+ id: 'sc-1',
3070
+ project_id: projectId,
3071
+ name: 'SearchBar - Default',
3072
+ component_name: 'SearchBar',
3073
+ component_path: 'src/components/SearchBar.tsx',
3074
+ entity_sha: null,
3075
+ created_at: '2026-03-20 18:45:00',
3076
+ updated_at: '2026-03-20 18:45:00',
3077
+ })
3078
+ .execute();
3079
+ await db
3080
+ .insertInto('editor_scenarios')
3081
+ .values({
3082
+ id: 'sc-2',
3083
+ project_id: projectId,
3084
+ name: 'SearchBar - With Results',
3085
+ component_name: 'SearchBar',
3086
+ component_path: 'src/components/SearchBar.tsx',
3087
+ entity_sha: null,
3088
+ created_at: '2026-03-20 18:45:05',
3089
+ updated_at: '2026-03-20 18:45:05',
3090
+ })
3091
+ .execute();
3092
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3093
+ expect(result).toHaveLength(1);
3094
+ expect(result[0].name).toBe('SearchBar');
3095
+ expect(result[0].filePath).toBe('src/components/SearchBar.tsx');
3096
+ expect(result[0].scenarioCount).toBe(2);
3097
+ expect(result[0].scenarioNames).toEqual(expect.arrayContaining([
3098
+ 'SearchBar - Default',
3099
+ 'SearchBar - With Results',
3100
+ ]));
3101
+ });
3102
+ it('should find page scenarios with NULL entity_sha', async () => {
3103
+ await db
3104
+ .insertInto('editor_scenarios')
3105
+ .values({
3106
+ id: 'sc-1',
3107
+ project_id: projectId,
3108
+ name: 'Full Page — Rich Library',
3109
+ component_name: null,
3110
+ component_path: null,
3111
+ page_file_path: 'src/library/LibraryApp.tsx',
3112
+ entity_sha: null,
3113
+ created_at: '2026-03-20 18:50:00',
3114
+ updated_at: '2026-03-20 18:50:00',
3115
+ })
3116
+ .execute();
3117
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3118
+ expect(result).toHaveLength(1);
3119
+ expect(result[0].name).toBe('LibraryApp');
3120
+ expect(result[0].filePath).toBe('src/library/LibraryApp.tsx');
3121
+ expect(result[0].scenarioCount).toBe(1);
3122
+ });
3123
+ it('should ignore scenarios without any file path (orphans without component_path or page_file_path)', async () => {
3124
+ // Scenarios with no file path at all can't be associated — they're not
3125
+ // actionable, so don't report them as unassociated
3126
+ await db
3127
+ .insertInto('editor_scenarios')
3128
+ .values({
3129
+ id: 'sc-1',
3130
+ project_id: projectId,
3131
+ name: 'Some Orphan',
3132
+ component_name: null,
3133
+ component_path: null,
3134
+ page_file_path: null,
3135
+ entity_sha: null,
3136
+ created_at: '2026-03-20 18:50:00',
3137
+ updated_at: '2026-03-20 18:50:00',
3138
+ })
3139
+ .execute();
3140
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3141
+ expect(result).toEqual([]);
3142
+ });
3143
+ it('should group multiple components separately', async () => {
3144
+ // Two different components both missing entity_sha
3145
+ await db
3146
+ .insertInto('editor_scenarios')
3147
+ .values({
3148
+ id: 'sc-1',
3149
+ project_id: projectId,
3150
+ name: 'FullPageHeader - Default',
3151
+ component_name: 'FullPageHeader',
3152
+ component_path: 'src/components/FullPageHeader.tsx',
3153
+ entity_sha: null,
3154
+ created_at: '2026-03-20 18:45:00',
3155
+ updated_at: '2026-03-20 18:45:00',
3156
+ })
3157
+ .execute();
3158
+ await db
3159
+ .insertInto('editor_scenarios')
3160
+ .values({
3161
+ id: 'sc-2',
3162
+ project_id: projectId,
3163
+ name: 'SaveConfirmation - Visible',
3164
+ component_name: 'SaveConfirmation',
3165
+ component_path: 'src/components/SaveConfirmation.tsx',
3166
+ entity_sha: null,
3167
+ created_at: '2026-03-20 19:00:00',
3168
+ updated_at: '2026-03-20 19:00:00',
3169
+ })
3170
+ .execute();
3171
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3172
+ expect(result).toHaveLength(2);
3173
+ const names = result.map((r) => r.name).sort();
3174
+ expect(names).toEqual(['FullPageHeader', 'SaveConfirmation']);
3175
+ });
3176
+ it('should only include scenarios from the specified project', async () => {
3177
+ await db
3178
+ .insertInto('editor_scenarios')
3179
+ .values({
3180
+ id: 'sc-1',
3181
+ project_id: 'other-project',
3182
+ name: 'SearchBar - Default',
3183
+ component_name: 'SearchBar',
3184
+ component_path: 'src/components/SearchBar.tsx',
3185
+ entity_sha: null,
3186
+ created_at: '2026-03-20 18:45:00',
3187
+ updated_at: '2026-03-20 18:45:00',
3188
+ })
3189
+ .execute();
3190
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3191
+ expect(result).toEqual([]);
3192
+ });
3193
+ it('should scope to feature session when featureStartedAt is provided', async () => {
3194
+ // Pre-existing unassociated scenario (before session)
3195
+ await db
3196
+ .insertInto('editor_scenarios')
3197
+ .values({
3198
+ id: 'sc-old',
3199
+ project_id: projectId,
3200
+ name: 'OldComponent - Default',
3201
+ component_name: 'OldComponent',
3202
+ component_path: 'src/components/OldComponent.tsx',
3203
+ entity_sha: null,
3204
+ created_at: '2026-03-19 10:00:00',
3205
+ updated_at: '2026-03-19 10:00:00',
3206
+ })
3207
+ .execute();
3208
+ // New unassociated scenario (during session)
3209
+ await db
3210
+ .insertInto('editor_scenarios')
3211
+ .values({
3212
+ id: 'sc-new',
3213
+ project_id: projectId,
3214
+ name: 'NewComponent - Default',
3215
+ component_name: 'NewComponent',
3216
+ component_path: 'src/components/NewComponent.tsx',
3217
+ entity_sha: null,
3218
+ created_at: '2026-03-20 18:45:00',
3219
+ updated_at: '2026-03-20 18:45:00',
3220
+ })
3221
+ .execute();
3222
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3223
+ // Should only find the session-scoped one
3224
+ expect(result).toHaveLength(1);
3225
+ expect(result[0].name).toBe('NewComponent');
3226
+ });
3227
+ it('should include re-registered scenarios (updated_at in session) even if created before', async () => {
3228
+ await db
3229
+ .insertInto('editor_scenarios')
3230
+ .values({
3231
+ id: 'sc-1',
3232
+ project_id: projectId,
3233
+ name: 'SearchBar - Default',
3234
+ component_name: 'SearchBar',
3235
+ component_path: 'src/components/SearchBar.tsx',
3236
+ entity_sha: null,
3237
+ created_at: '2026-03-19 10:00:00',
3238
+ updated_at: '2026-03-20 18:45:00', // re-registered during session
3239
+ })
3240
+ .execute();
3241
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3242
+ expect(result).toHaveLength(1);
3243
+ expect(result[0].name).toBe('SearchBar');
3244
+ });
3245
+ it('should not include scenarios with entity_sha set (even if stale)', async () => {
3246
+ // This scenario has an entity_sha — even if it's stale, that's a
3247
+ // different problem (handled by queryIncompleteEntities)
3248
+ await db
3249
+ .insertInto('editor_scenarios')
3250
+ .values({
3251
+ id: 'sc-1',
3252
+ project_id: projectId,
3253
+ name: 'Header - Default',
3254
+ component_name: 'Header',
3255
+ component_path: 'src/components/Header.tsx',
3256
+ entity_sha: 'sha-old-version',
3257
+ created_at: '2026-03-20 18:45:00',
3258
+ updated_at: '2026-03-20 18:45:00',
3259
+ })
3260
+ .execute();
3261
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3262
+ expect(result).toEqual([]);
3263
+ });
3264
+ });
3265
+ // ── isAutoRemediable with unassociatedScenarios ────────────────────
3266
+ describe('isAutoRemediable always returns false (no inline full analysis)', () => {
3267
+ // Full analyze-imports takes minutes on large projects. The audit should
3268
+ // never trigger it — only the lightweight backfill path is acceptable.
3269
+ it('should return false for unassociatedScenarios only', () => {
3270
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3271
+ });
3272
+ it('should return false for incompleteEntities + unassociatedScenarios', () => {
3273
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3274
+ });
3275
+ it('should return false even with no other failures', () => {
3276
+ expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
3277
+ });
3278
+ it('should return false when already attempted', () => {
3279
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
3280
+ });
3281
+ });
3282
+ describe('suggestedTestFile for functions without testFile', () => {
3283
+ it('should suggest conventional .test.ts path when testFile is undefined', () => {
3284
+ const result = computeAudit({
3285
+ components: [],
3286
+ functions: [
3287
+ { name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
3288
+ ],
3289
+ scenarioCounts: {},
3290
+ testFileExistence: {},
3291
+ });
3292
+ expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
3293
+ expect(result.functions[0].status).toBe('missing');
3294
+ });
3295
+ it('should suggest .test.ts for .ts files', () => {
3296
+ const result = computeAudit({
3297
+ components: [],
3298
+ functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
3299
+ scenarioCounts: {},
3300
+ testFileExistence: {},
3301
+ });
3302
+ expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
3303
+ });
3304
+ it('should not set suggestedTestFile when testFile is already specified', () => {
3305
+ const result = computeAudit({
3306
+ components: [],
3307
+ functions: [
3308
+ {
3309
+ name: 'calculatePrice',
3310
+ filePath: 'app/lib/pricing.ts',
3311
+ testFile: 'app/lib/pricing.test.ts',
3312
+ },
3313
+ ],
3314
+ scenarioCounts: {},
3315
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
3316
+ });
3317
+ expect(result.functions[0].suggestedTestFile).toBeUndefined();
3318
+ });
3319
+ });
3320
+ describe('hint for function audit entries', () => {
3321
+ it('should include a hint for name_mismatch functions explaining the fix', () => {
3322
+ // Claude sees "name mismatch" with no guidance on what it means or how
3323
+ // to fix it. The hint should explain that a top-level describe block
3324
+ // matching the function name is required for the CodeYam UI.
3325
+ const result = computeAudit({
3326
+ components: [],
3327
+ functions: [
3328
+ {
3329
+ name: 'useDrinks',
3330
+ filePath: 'app/hooks/useDrinks.ts',
3331
+ testFile: 'app/hooks/useDrinks.test.ts',
3332
+ },
3333
+ ],
3334
+ scenarioCounts: {},
3335
+ testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
3336
+ testResults: {
3337
+ 'app/hooks/useDrinks.test.ts': {
3338
+ passing: true,
3339
+ hasEntityNameDescribe: false,
3340
+ },
3341
+ },
3342
+ });
3343
+ expect(result.functions[0].status).toBe('name_mismatch');
3344
+ expect(result.functions[0].hint).toBeDefined();
3345
+ expect(result.functions[0].hint).toContain('describe');
3346
+ expect(result.functions[0].hint).toContain('useDrinks');
3347
+ });
3348
+ it('should include a hint for runner_error functions showing the error', () => {
3349
+ // When the test runner crashes, Claude needs to see WHY it crashed
3350
+ // to fix the underlying issue. Without this, Claude loops re-running audit.
3351
+ const result = computeAudit({
3352
+ components: [],
3353
+ functions: [
3354
+ {
3355
+ name: 'getTimeAgo',
3356
+ filePath: 'src/lib/format.ts',
3357
+ testFile: 'src/lib/format.test.ts',
3358
+ },
3359
+ ],
3360
+ scenarioCounts: {},
3361
+ testFileExistence: { 'src/lib/format.test.ts': true },
3362
+ testResults: {
3363
+ 'src/lib/format.test.ts': {
3364
+ passing: false,
3365
+ hasEntityNameDescribe: false,
3366
+ errorMessage: 'Cannot find module "@/lib/format"',
3367
+ },
3368
+ },
3369
+ });
3370
+ expect(result.functions[0].status).toBe('runner_error');
3371
+ expect(result.functions[0].hint).toBeDefined();
3372
+ expect(result.functions[0].hint).toContain('Cannot find module');
3373
+ });
3374
+ });
3375
+ describe('hint for missing components', () => {
3376
+ it('should hint that layout files need app-level scenarios', () => {
3377
+ const result = computeAudit({
3378
+ components: [
3379
+ { name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
3380
+ ],
3381
+ functions: [],
3382
+ scenarioCounts: {},
3383
+ testFileExistence: {},
3384
+ });
3385
+ expect(result.components[0].hint).toContain('layout');
3386
+ expect(result.components[0].hint).toContain('pageFilePath');
3387
+ });
3388
+ it('should hint that page files need app-level scenarios', () => {
3389
+ const result = computeAudit({
3390
+ components: [
3391
+ { name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
3392
+ ],
3393
+ functions: [],
3394
+ scenarioCounts: {},
3395
+ testFileExistence: {},
3396
+ });
3397
+ expect(result.components[0].hint).toContain('page');
3398
+ expect(result.components[0].hint).toContain('pageFilePath');
3399
+ });
3400
+ it('should hint that regular components need isolation routes', () => {
3401
+ const result = computeAudit({
3402
+ components: [
3403
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3404
+ ],
3405
+ functions: [],
3406
+ scenarioCounts: {},
3407
+ testFileExistence: {},
3408
+ });
3409
+ expect(result.components[0].hint).toContain('isolated-components');
3410
+ });
3411
+ it('should not set hint when component has scenarios', () => {
3412
+ const result = computeAudit({
3413
+ components: [
3414
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3415
+ ],
3416
+ functions: [],
3417
+ scenarioCounts: { DrinkCard: 2 },
3418
+ testFileExistence: {},
3419
+ });
3420
+ expect(result.components[0].hint).toBeUndefined();
3421
+ });
3422
+ it('should provide a hint for needs_recapture components', () => {
3423
+ // Components with needs_recapture status need guidance on what to do.
3424
+ // Without a hint, Claude has no instructions for fixing the issue.
3425
+ const result = computeAudit({
3426
+ components: [{ name: 'Library', filePath: 'app/library/page.tsx' }],
3427
+ functions: [],
3428
+ scenarioCounts: {},
3429
+ testFileExistence: {},
3430
+ totalScenarioCounts: { Library: 3 },
3431
+ entityChangeStatus: { Library: { status: 'impacted' } },
3432
+ });
3433
+ expect(result.components[0].status).toBe('needs_recapture');
3434
+ expect(result.components[0].hint).toBeDefined();
3435
+ expect(result.components[0].hint).toContain('recapture');
3436
+ });
3437
+ });
3438
+ describe('formatIncompleteEntityGuidance', () => {
3439
+ it('should include the entity name and scenario count', () => {
3440
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3441
+ const result = formatIncompleteEntityGuidance({
3442
+ entitySha: 'abc123',
3443
+ name: 'RuleBuilder',
3444
+ scenarioCount: 5,
3445
+ preExisting: false,
3446
+ });
3447
+ expect(result).toContain('RuleBuilder');
3448
+ expect(result).toContain('5');
3449
+ });
3450
+ it('should tell Claude the exact fix command', () => {
3451
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3452
+ const result = formatIncompleteEntityGuidance({
3453
+ entitySha: 'abc123',
3454
+ name: 'RuleBuilder',
3455
+ scenarioCount: 5,
3456
+ preExisting: false,
3457
+ });
3458
+ expect(result).toContain('codeyam editor analyze-imports');
3459
+ });
3460
+ it('should flag pre-existing issues as non-blocking', () => {
3461
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3462
+ const result = formatIncompleteEntityGuidance({
3463
+ entitySha: 'abc123',
3464
+ name: 'RuleBuilder',
3465
+ scenarioCount: 5,
3466
+ preExisting: true,
3467
+ });
3468
+ expect(result).toContain('pre-existing');
3469
+ });
3470
+ it('should explain what incomplete means', () => {
3471
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3472
+ const result = formatIncompleteEntityGuidance({
3473
+ entitySha: 'abc123',
3474
+ name: 'RuleBuilder',
3475
+ scenarioCount: 5,
3476
+ preExisting: false,
3477
+ });
3478
+ // Should explain the root cause, not just the symptom
3479
+ expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
3480
+ });
3481
+ });
3482
+ describe('formatManualAnalysisGuidance', () => {
3483
+ it('should include entity name, scenario count, and error message', () => {
3484
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3485
+ const result = formatManualAnalysisGuidance({
3486
+ name: 'Header',
3487
+ filePath: 'src/components/Header.tsx',
3488
+ scenarioCount: 3,
3489
+ error: 'TypeScript parsing error: unexpected token',
3490
+ });
3491
+ expect(result).toContain('Header');
3492
+ expect(result).toContain('3 scenario(s)');
3493
+ expect(result).toContain('TypeScript parsing error');
3494
+ });
3495
+ it('should include MANUAL ANALYSIS REQUIRED header', () => {
3496
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3497
+ const result = formatManualAnalysisGuidance({
3498
+ name: 'Header',
3499
+ filePath: 'src/components/Header.tsx',
3500
+ scenarioCount: 2,
3501
+ error: 'Parse error',
3502
+ });
3503
+ expect(result).toContain('MANUAL ANALYSIS REQUIRED');
3504
+ });
3505
+ it('should tell Claude to read the source file', () => {
3506
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3507
+ const result = formatManualAnalysisGuidance({
3508
+ name: 'Header',
3509
+ filePath: 'src/components/Header.tsx',
3510
+ scenarioCount: 2,
3511
+ error: 'Parse error',
3512
+ });
3513
+ expect(result).toContain('Read src/components/Header.tsx');
3514
+ });
3515
+ it('should include the manual-entity command', () => {
3516
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3517
+ const result = formatManualAnalysisGuidance({
3518
+ name: 'Header',
3519
+ filePath: 'src/components/Header.tsx',
3520
+ scenarioCount: 2,
3521
+ error: 'Parse error',
3522
+ });
3523
+ expect(result).toContain('codeyam editor manual-entity');
3524
+ });
3525
+ it('should tell Claude to check glossary for imports', () => {
3526
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3527
+ const result = formatManualAnalysisGuidance({
3528
+ name: 'Header',
3529
+ filePath: 'src/components/Header.tsx',
3530
+ scenarioCount: 2,
3531
+ error: 'Parse error',
3532
+ });
3533
+ expect(result).toContain('glossary');
3534
+ });
3535
+ });
3536
+ describe('getIncompleteEntityFilePaths', () => {
3537
+ // The audit should auto-fix incomplete entities by running analysis on
3538
+ // just their specific file paths, not all 117+ files. This function
3539
+ // resolves entity SHAs to file paths for targeted analysis.
3540
+ let db;
3541
+ let rawDb;
3542
+ beforeEach(async () => {
3543
+ rawDb = new Database(':memory:');
3544
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3545
+ await db.schema
3546
+ .createTable('entities')
3547
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
3548
+ .addColumn('name', 'varchar')
3549
+ .addColumn('file_path', 'varchar')
3550
+ .addColumn('project_id', 'varchar')
3551
+ .addColumn('entity_type', 'varchar')
3552
+ .execute();
3553
+ });
3554
+ afterEach(() => {
3555
+ rawDb.close();
3556
+ });
3557
+ it('should resolve entity SHAs to file paths from the entities table', async () => {
3558
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3559
+ await db
3560
+ .insertInto('entities')
3561
+ .values([
3562
+ {
3563
+ sha: 'sha-rule',
3564
+ name: 'RuleBuilder',
3565
+ file_path: 'app/components/RuleBuilder.tsx',
3566
+ project_id: 'p1',
3567
+ entity_type: 'component',
3568
+ },
3569
+ {
3570
+ sha: 'sha-row',
3571
+ name: 'ArticleTableRow',
3572
+ file_path: 'app/components/ArticleTableRow.tsx',
3573
+ project_id: 'p1',
3574
+ entity_type: 'component',
3575
+ },
3576
+ ])
3577
+ .execute();
3578
+ const result = await getIncompleteEntityFilePaths(db, [
3579
+ {
3580
+ entitySha: 'sha-rule',
3581
+ name: 'RuleBuilder',
3582
+ scenarioCount: 5,
3583
+ preExisting: false,
3584
+ },
3585
+ {
3586
+ entitySha: 'sha-row',
3587
+ name: 'ArticleTableRow',
3588
+ scenarioCount: 2,
3589
+ preExisting: false,
3590
+ },
3591
+ ]);
3592
+ expect(result).toContain('app/components/RuleBuilder.tsx');
3593
+ expect(result).toContain('app/components/ArticleTableRow.tsx');
3594
+ expect(result).toHaveLength(2);
3595
+ });
3596
+ it('should skip entities whose SHA is not in the entities table', async () => {
3597
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3598
+ const result = await getIncompleteEntityFilePaths(db, [
3599
+ {
3600
+ entitySha: 'nonexistent-sha',
3601
+ name: 'Ghost',
3602
+ scenarioCount: 1,
3603
+ preExisting: false,
3604
+ },
3605
+ ]);
3606
+ expect(result).toHaveLength(0);
3607
+ });
3608
+ it('should deduplicate file paths', async () => {
3609
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3610
+ await db
3611
+ .insertInto('entities')
3612
+ .values([
3613
+ {
3614
+ sha: 'sha-v1',
3615
+ name: 'Foo',
3616
+ file_path: 'app/Foo.tsx',
3617
+ project_id: 'p1',
3618
+ entity_type: 'component',
3619
+ },
3620
+ {
3621
+ sha: 'sha-v2',
3622
+ name: 'Foo',
3623
+ file_path: 'app/Foo.tsx',
3624
+ project_id: 'p1',
3625
+ entity_type: 'component',
3626
+ },
3627
+ ])
3628
+ .execute();
3629
+ const result = await getIncompleteEntityFilePaths(db, [
3630
+ {
3631
+ entitySha: 'sha-v1',
3632
+ name: 'Foo',
3633
+ scenarioCount: 3,
3634
+ preExisting: false,
3635
+ },
3636
+ {
3637
+ entitySha: 'sha-v2',
3638
+ name: 'Foo',
3639
+ scenarioCount: 1,
3640
+ preExisting: false,
3641
+ },
3642
+ ]);
3643
+ expect(result).toEqual(['app/Foo.tsx']);
3644
+ });
3645
+ });
3646
+ describe('isAutoRemediable never triggers full analysis', () => {
3647
+ // The audit must NEVER run handleAnalyzeImports inline — it takes minutes
3648
+ // for large projects. Auto-remediation should only do the lightweight
3649
+ // entity SHA backfill. isAutoRemediable is now always false; the callers
3650
+ // use needsBackfillOnly for the fast path instead.
3651
+ it('should always return false regardless of summary state', () => {
3652
+ expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
3653
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3654
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3655
+ });
3656
+ });
3657
+ // ── isOnlyPreExistingIncomplete ─────────────────────────────────────
3658
+ describe('isOnlyPreExistingIncomplete', () => {
3659
+ it('should return true when all incomplete entities are pre-existing and no other failures', () => {
3660
+ expect(isOnlyPreExistingIncomplete({
3661
+ incompleteEntities: 2,
3662
+ preExistingIncompleteEntities: 2,
3663
+ componentsMissing: 0,
3664
+ componentsWithErrors: 0,
3665
+ functionsFailing: 0,
3666
+ functionsNameMismatch: 0,
3667
+ functionsMissing: 0,
3668
+ missingFromGlossary: 0,
3669
+ miscategorizedScenarios: 0,
3670
+ scenariosNeedingRecapture: 0,
3671
+ }, [
3672
+ {
3673
+ entitySha: 'a',
3674
+ name: 'RuleBuilder',
3675
+ scenarioCount: 5,
3676
+ preExisting: true,
3677
+ },
3678
+ {
3679
+ entitySha: 'b',
3680
+ name: 'ArticleTableRow',
3681
+ scenarioCount: 2,
3682
+ preExisting: true,
3683
+ },
3684
+ ])).toBe(true);
3685
+ });
3686
+ it('should return false when there are runner errors even if all incompletes are pre-existing', () => {
3687
+ // Safety net: runner errors (crashed test runner) must NEVER bypass the gate.
3688
+ // Without this test, a regression removing functionsRunnerError from
3689
+ // isOnlyIncompleteEntities would silently let broken test infrastructure through.
3690
+ expect(isOnlyPreExistingIncomplete({
3691
+ incompleteEntities: 1,
3692
+ preExistingIncompleteEntities: 1,
3693
+ componentsMissing: 0,
3694
+ componentsWithErrors: 0,
3695
+ functionsFailing: 0,
3696
+ functionsRunnerError: 1,
3697
+ functionsNameMismatch: 0,
3698
+ functionsMissing: 0,
3699
+ missingFromGlossary: 0,
3700
+ }, [
3701
+ {
3702
+ entitySha: 'a',
3703
+ name: 'OldComponent',
3704
+ scenarioCount: 1,
3705
+ preExisting: true,
3706
+ },
3707
+ ])).toBe(false);
3708
+ });
3709
+ it('should return false when some incomplete entities are NOT pre-existing', () => {
3710
+ expect(isOnlyPreExistingIncomplete({
3711
+ incompleteEntities: 2,
3712
+ preExistingIncompleteEntities: 1,
3713
+ componentsMissing: 0,
3714
+ componentsWithErrors: 0,
3715
+ functionsFailing: 0,
3716
+ functionsNameMismatch: 0,
3717
+ functionsMissing: 0,
3718
+ missingFromGlossary: 0,
3719
+ miscategorizedScenarios: 0,
3720
+ scenariosNeedingRecapture: 0,
3721
+ }, [
3722
+ {
3723
+ entitySha: 'a',
3724
+ name: 'RuleBuilder',
3725
+ scenarioCount: 5,
3726
+ preExisting: true,
3727
+ },
3728
+ {
3729
+ entitySha: 'b',
3730
+ name: 'NewComponent',
3731
+ scenarioCount: 1,
3732
+ preExisting: false,
3733
+ },
3734
+ ])).toBe(false);
3735
+ });
3736
+ it('should return false when there are other failures besides incomplete entities', () => {
3737
+ expect(isOnlyPreExistingIncomplete({
3738
+ incompleteEntities: 2,
3739
+ preExistingIncompleteEntities: 2,
3740
+ componentsMissing: 1,
3741
+ }, [
3742
+ {
3743
+ entitySha: 'a',
3744
+ name: 'RuleBuilder',
3745
+ scenarioCount: 5,
3746
+ preExisting: true,
3747
+ },
3748
+ {
3749
+ entitySha: 'b',
3750
+ name: 'ArticleTableRow',
3751
+ scenarioCount: 2,
3752
+ preExisting: true,
3753
+ },
3754
+ ])).toBe(false);
3755
+ });
3756
+ it('should return false when incomplete entities array is empty', () => {
3757
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
3758
+ });
3759
+ it('should return false when incomplete entities array is missing', () => {
3760
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
3761
+ });
3762
+ });
3763
+ describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
3764
+ it('should return true when only unassociatedScenarios present', () => {
3765
+ expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
3766
+ });
3767
+ it('should return false when unassociatedScenarios present with other failures', () => {
3768
+ expect(isOnlyIncompleteEntities({
3769
+ unassociatedScenarios: 5,
3770
+ functionsMissing: 1,
3771
+ })).toBe(false);
3772
+ });
3773
+ });
3774
+ // ── aggregateClientErrorsByComponent ─────────────────────────────────
3775
+ describe('aggregateClientErrorsByComponent', () => {
3776
+ it('should attribute errors to component using componentName from metadata', () => {
3777
+ const result = aggregateClientErrorsByComponent({
3778
+ 'scenario-1': {
3779
+ scenarioName: 'DrinkCard - Default',
3780
+ errors: ['TypeError: Cannot read property "price"'],
3781
+ },
3782
+ }, [
3783
+ {
3784
+ name: 'DrinkCard - Default',
3785
+ componentName: 'DrinkCard',
3786
+ },
3787
+ ]);
3788
+ expect(result['DrinkCard']).toEqual([
3789
+ 'TypeError: Cannot read property "price"',
3790
+ ]);
3791
+ });
3792
+ it('should attribute app-level scenario errors using pageFilePath', () => {
3793
+ // App-level scenarios have componentName=null. The old approach
3794
+ // parsed the scenario name "Full Page with Library" and got
3795
+ // "Full Page with Library" as the component — which matches nothing.
3796
+ // With metadata, we derive the entity name from pageFilePath.
3797
+ const result = aggregateClientErrorsByComponent({
3798
+ 'scenario-1': {
3799
+ scenarioName: 'Full Page with Library',
3800
+ errors: ['TypeError: Cannot read property "title"'],
3801
+ },
3802
+ }, [
3803
+ {
3804
+ name: 'Full Page with Library',
3805
+ componentName: null,
3806
+ pageFilePath: 'app/library/page.tsx',
3807
+ },
3808
+ ]);
3809
+ // Should be keyed by the canonical entity name from scenarioEntityName()
3810
+ expect(result['Library']).toEqual([
3811
+ 'TypeError: Cannot read property "title"',
3812
+ ]);
3813
+ });
3814
+ it('should fall back to scenario name parsing when no metadata match exists', () => {
3815
+ // If the scenario is not in the metadata list (e.g., metadata not yet loaded),
3816
+ // fall back to the "ComponentName - Variant" convention
3817
+ const result = aggregateClientErrorsByComponent({
3818
+ 'scenario-1': {
3819
+ scenarioName: 'Header - Dark Mode',
3820
+ errors: ['ReferenceError: theme is not defined'],
3821
+ },
3822
+ }, []);
3823
+ expect(result['Header']).toEqual([
3824
+ 'ReferenceError: theme is not defined',
3825
+ ]);
3826
+ });
3827
+ it('should handle non-route file paths via scenarioEntityName', () => {
3828
+ // Non-route files (src/...) are treated as app entry points by
3829
+ // scenarioEntityName → buildRoutePattern returns '/' → 'Home'.
3830
+ // When a url is available, it provides a better entity name.
3831
+ const result = aggregateClientErrorsByComponent({
3832
+ 'scenario-1': {
3833
+ scenarioName: 'LibraryApp - Rich',
3834
+ errors: ['Error: fetch failed'],
3835
+ },
3836
+ }, [
3837
+ {
3838
+ name: 'LibraryApp - Rich',
3839
+ componentName: null,
3840
+ pageFilePath: 'src/library/LibraryApp.tsx',
3841
+ url: '/library',
3842
+ },
3843
+ ]);
3844
+ // scenarioEntityName uses url fallback when pageFilePath is a non-route file
3845
+ // pageFilePath 'src/...' → buildRoutePattern → '/' → routeDisplayName → 'Home'
3846
+ // But componentName takes priority, and with url '/library' as final fallback
3847
+ // scenarioEntityName({ pageFilePath: 'src/library/LibraryApp.tsx' }) → 'Home'
3848
+ // because pageFilePath is checked before url
3849
+ expect(result['Home']).toEqual(['Error: fetch failed']);
3850
+ });
3851
+ it('should skip scenarios with no errors', () => {
3852
+ const result = aggregateClientErrorsByComponent({
3853
+ 'scenario-1': {
3854
+ scenarioName: 'DrinkCard - Default',
3855
+ errors: [],
3856
+ },
3857
+ }, [
3858
+ {
3859
+ name: 'DrinkCard - Default',
3860
+ componentName: 'DrinkCard',
3861
+ },
3862
+ ]);
3863
+ expect(result).toEqual({});
3864
+ });
3865
+ it('should aggregate errors from multiple scenarios for same component', () => {
3866
+ const result = aggregateClientErrorsByComponent({
3867
+ 'scenario-1': {
3868
+ scenarioName: 'DrinkCard - Default',
3869
+ errors: ['Error A'],
3870
+ },
3871
+ 'scenario-2': {
3872
+ scenarioName: 'DrinkCard - Hover',
3873
+ errors: ['Error B', 'Error C'],
3874
+ },
3875
+ }, [
3876
+ { name: 'DrinkCard - Default', componentName: 'DrinkCard' },
3877
+ { name: 'DrinkCard - Hover', componentName: 'DrinkCard' },
3878
+ ]);
3879
+ expect(result['DrinkCard']).toEqual(['Error A', 'Error B', 'Error C']);
3880
+ });
3881
+ it('should use capitalized display name for page file paths, not lowercase directory', () => {
3882
+ // aggregateClientErrorsByComponent must produce keys matching scenarioEntityName().
3883
+ // scenarioEntityName({ pageFilePath: 'app/library/page.tsx' }) returns 'Library',
3884
+ // so the key must be 'Library' — not 'library' (the raw directory name).
3885
+ // computeAudit checks clientErrors[glossaryEntryName], so a lowercase key
3886
+ // will never match, silently dropping all client errors for page-level scenarios.
3887
+ const result = aggregateClientErrorsByComponent({
3888
+ 'sc-1': {
3889
+ scenarioName: 'Library - Default',
3890
+ errors: ['TypeError: fetch failed'],
3891
+ },
3892
+ }, [
3893
+ {
3894
+ name: 'Library - Default',
3895
+ componentName: null,
3896
+ pageFilePath: 'app/library/page.tsx',
3897
+ },
3898
+ ]);
3899
+ expect(result).toHaveProperty('Library');
3900
+ expect(result['Library']).toEqual(['TypeError: fetch failed']);
3901
+ });
3902
+ it('should use "Home" for root page app/page.tsx, not "app"', () => {
3903
+ // Root page: scenarioEntityName returns 'Home', not 'app'.
3904
+ // Custom path parsing incorrectly pops the parent dir ('app').
3905
+ const result = aggregateClientErrorsByComponent({
3906
+ 'sc-1': {
3907
+ scenarioName: 'Home - Default',
3908
+ errors: ['ReferenceError: window is not defined'],
3909
+ },
3910
+ }, [
3911
+ {
3912
+ name: 'Home - Default',
3913
+ componentName: null,
3914
+ pageFilePath: 'app/page.tsx',
3915
+ },
3916
+ ]);
3917
+ expect(result).toHaveProperty('Home');
3918
+ expect(result['Home']).toEqual(['ReferenceError: window is not defined']);
3919
+ });
3920
+ });
3921
+ // ── determineTargetedAnalysisPaths ──────────────────────────────────
3922
+ describe('determineTargetedAnalysisPaths', () => {
3923
+ it('should return unique file paths from unassociated scenarios', () => {
3924
+ const result = determineTargetedAnalysisPaths({
3925
+ unassociatedScenarios: [
3926
+ { filePath: 'app/components/CreateFromFiltersButton.tsx' },
3927
+ { filePath: 'app/components/StaleDismissedBanner.tsx' },
3928
+ ],
3929
+ incompleteEntities: [],
3930
+ });
3931
+ expect(result).toEqual([
3932
+ 'app/components/CreateFromFiltersButton.tsx',
3933
+ 'app/components/StaleDismissedBanner.tsx',
3934
+ ]);
3935
+ });
3936
+ it('should include file paths from incomplete entities', () => {
3937
+ const result = determineTargetedAnalysisPaths({
3938
+ unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
3939
+ incompleteEntities: [{ filePath: 'app/components/Dashboard.tsx' }],
3940
+ });
3941
+ expect(result).toEqual([
3942
+ 'app/components/TaskCard.tsx',
3943
+ 'app/components/Dashboard.tsx',
3944
+ ]);
3945
+ });
3946
+ it('should deduplicate file paths across both sources', () => {
3947
+ const result = determineTargetedAnalysisPaths({
3948
+ unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
3949
+ incompleteEntities: [{ filePath: 'app/components/TaskCard.tsx' }],
3950
+ });
3951
+ expect(result).toEqual(['app/components/TaskCard.tsx']);
3952
+ });
3953
+ it('should return empty array when no issues', () => {
3954
+ const result = determineTargetedAnalysisPaths({
3955
+ unassociatedScenarios: [],
3956
+ incompleteEntities: [],
3957
+ });
3958
+ expect(result).toEqual([]);
3959
+ });
3960
+ it('should filter out entries with empty file paths', () => {
3961
+ const result = determineTargetedAnalysisPaths({
3962
+ unassociatedScenarios: [{ filePath: '' }],
3963
+ incompleteEntities: [],
3964
+ });
3965
+ expect(result).toEqual([]);
3966
+ });
3967
+ it('should handle incomplete entities with undefined filePath', () => {
3968
+ const result = determineTargetedAnalysisPaths({
3969
+ unassociatedScenarios: [{ filePath: 'app/components/Good.tsx' }],
3970
+ incompleteEntities: [
3971
+ {
3972
+ /* no filePath at all */
3973
+ },
3974
+ { filePath: undefined },
3975
+ { filePath: 'app/components/AlsoGood.tsx' },
3976
+ ],
3977
+ });
3978
+ expect(result).toEqual([
3979
+ 'app/components/Good.tsx',
3980
+ 'app/components/AlsoGood.tsx',
3981
+ ]);
3982
+ });
3983
+ });
3984
+ // ── shouldAutoRecapture ─────────────────────────────────────────────
3985
+ describe('shouldAutoRecapture', () => {
3986
+ it('should return true when fix flag is set and stale scenarios exist', () => {
3987
+ expect(shouldAutoRecapture({
3988
+ fix: true,
3989
+ scenariosNeedingRecapture: [
3990
+ {
3991
+ scenarioName: 'Default',
3992
+ entityName: 'TaskCard',
3993
+ status: { status: 'edited' },
3994
+ },
3995
+ ],
3996
+ })).toBe(true);
3997
+ });
3998
+ it('should return false when fix flag is not set', () => {
3999
+ expect(shouldAutoRecapture({
4000
+ fix: false,
4001
+ scenariosNeedingRecapture: [
4002
+ {
4003
+ scenarioName: 'Default',
4004
+ entityName: 'TaskCard',
4005
+ status: { status: 'edited' },
4006
+ },
4007
+ ],
4008
+ })).toBe(false);
4009
+ });
4010
+ it('should return false when fix is set but no stale scenarios', () => {
4011
+ expect(shouldAutoRecapture({
4012
+ fix: true,
4013
+ scenariosNeedingRecapture: [],
4014
+ })).toBe(false);
4015
+ });
4016
+ it('should return false when both are falsy', () => {
4017
+ expect(shouldAutoRecapture({
4018
+ fix: false,
4019
+ scenariosNeedingRecapture: [],
4020
+ })).toBe(false);
4021
+ });
4022
+ });
1979
4023
  });
1980
4024
  //# sourceMappingURL=editorAudit.test.js.map