@codeyam/codeyam-cli 0.1.0-staging.ad88eeb → 0.1.0-staging.b147f46

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 (437) hide show
  1. package/analyzer-template/.build-info.json +8 -8
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/package.json +4 -4
  4. package/analyzer-template/packages/ai/package.json +1 -1
  5. package/analyzer-template/packages/ai/src/lib/astScopes/methodSemantics.ts +135 -0
  6. package/analyzer-template/packages/ai/src/lib/astScopes/nodeToSource.ts +19 -0
  7. package/analyzer-template/packages/ai/src/lib/astScopes/paths.ts +11 -4
  8. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +36 -9
  9. package/analyzer-template/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.ts +10 -3
  10. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +16 -6
  11. package/analyzer-template/packages/analyze/index.ts +4 -1
  12. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +28 -2
  13. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +5 -36
  14. package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +10 -6
  15. package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +9 -12
  16. package/analyzer-template/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.ts +21 -0
  17. package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +82 -10
  18. package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
  19. package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
  20. package/analyzer-template/packages/analyze/src/lib/files/analyzeNextRoute.ts +8 -3
  21. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +239 -58
  22. package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +1684 -1462
  23. package/analyzer-template/packages/aws/package.json +2 -2
  24. package/analyzer-template/packages/database/package.json +2 -2
  25. package/analyzer-template/packages/database/src/lib/kysely/tables/editorScenariosTable.ts +82 -0
  26. package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +19 -15
  27. package/analyzer-template/packages/database/src/lib/loadEntities.ts +0 -6
  28. package/analyzer-template/packages/database/src/lib/loadEntity.ts +19 -8
  29. package/analyzer-template/packages/database/src/lib/updateCommitMetadata.ts +0 -65
  30. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts +5 -0
  31. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts.map +1 -1
  32. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js +84 -0
  33. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
  34. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.d.ts.map +1 -1
  35. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js +1 -1
  36. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js.map +1 -1
  37. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.d.ts.map +1 -1
  38. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js +0 -6
  39. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js.map +1 -1
  40. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts +4 -1
  41. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
  42. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +5 -5
  43. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
  44. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.d.ts.map +1 -1
  45. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js +0 -25
  46. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js.map +1 -1
  47. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
  48. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
  49. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
  50. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  51. package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
  52. package/analyzer-template/project/analyzeFileEntities.ts +26 -0
  53. package/background/src/lib/virtualized/project/analyzeFileEntities.js +22 -0
  54. package/background/src/lib/virtualized/project/analyzeFileEntities.js.map +1 -1
  55. package/codeyam-cli/src/cli.js +24 -0
  56. package/codeyam-cli/src/cli.js.map +1 -1
  57. package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js +47 -0
  58. package/codeyam-cli/src/commands/__tests__/editor.analyzeImportsArgs.test.js.map +1 -0
  59. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js +71 -0
  60. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js.map +1 -0
  61. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js +51 -0
  62. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js.map +1 -0
  63. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +56 -0
  64. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js.map +1 -0
  65. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +101 -47
  66. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
  67. package/codeyam-cli/src/commands/editor.js +3495 -553
  68. package/codeyam-cli/src/commands/editor.js.map +1 -1
  69. package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js +23 -0
  70. package/codeyam-cli/src/commands/editorAnalyzeImportsArgs.js.map +1 -0
  71. package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
  72. package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
  73. package/codeyam-cli/src/commands/init.js +69 -34
  74. package/codeyam-cli/src/commands/init.js.map +1 -1
  75. package/codeyam-cli/src/commands/telemetry.js +37 -0
  76. package/codeyam-cli/src/commands/telemetry.js.map +1 -0
  77. package/codeyam-cli/src/data/techStacks.js +2 -7
  78. package/codeyam-cli/src/data/techStacks.js.map +1 -1
  79. package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js +173 -0
  80. package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js.map +1 -0
  81. package/codeyam-cli/src/utils/__tests__/backgroundServer.test.js +46 -0
  82. package/codeyam-cli/src/utils/__tests__/backgroundServer.test.js.map +1 -0
  83. package/codeyam-cli/src/utils/__tests__/editorApi.test.js +18 -8
  84. package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -1
  85. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +3380 -1
  86. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  87. package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js +76 -0
  88. package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js.map +1 -0
  89. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js +137 -0
  90. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js.map +1 -0
  91. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js +100 -0
  92. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js.map +1 -0
  93. package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js +27 -2
  94. package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js.map +1 -1
  95. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +76 -3
  96. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -1
  97. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +381 -0
  98. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -0
  99. package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js +67 -0
  100. package/codeyam-cli/src/utils/__tests__/editorGuardMiddleware.test.js.map +1 -0
  101. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js +202 -1
  102. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js.map +1 -1
  103. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js +435 -0
  104. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js.map +1 -0
  105. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +96 -1
  106. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
  107. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +47 -1
  108. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -1
  109. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +70 -0
  110. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
  111. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +1548 -1
  112. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  113. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +117 -1
  114. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -1
  115. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js +143 -0
  116. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js.map +1 -0
  117. package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js +66 -0
  118. package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js.map +1 -0
  119. package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js +53 -0
  120. package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js.map +1 -0
  121. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +395 -11
  122. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  123. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
  124. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
  125. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +16 -1
  126. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -1
  127. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
  128. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
  129. package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js +30 -2
  130. package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js.map +1 -1
  131. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
  132. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
  133. package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js +118 -0
  134. package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js.map +1 -0
  135. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +284 -0
  136. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -0
  137. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +649 -223
  138. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
  139. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
  140. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
  141. package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js +1 -0
  142. package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js.map +1 -1
  143. package/codeyam-cli/src/utils/__tests__/telemetry.test.js +159 -0
  144. package/codeyam-cli/src/utils/__tests__/telemetry.test.js.map +1 -0
  145. package/codeyam-cli/src/utils/__tests__/testRunner.test.js +217 -0
  146. package/codeyam-cli/src/utils/__tests__/testRunner.test.js.map +1 -0
  147. package/codeyam-cli/src/utils/analysisRunner.js +39 -8
  148. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  149. package/codeyam-cli/src/utils/analyzer.js +19 -0
  150. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  151. package/codeyam-cli/src/utils/analyzerFinalization.js +100 -0
  152. package/codeyam-cli/src/utils/analyzerFinalization.js.map +1 -0
  153. package/codeyam-cli/src/utils/backgroundServer.js +93 -17
  154. package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
  155. package/codeyam-cli/src/utils/database.js +37 -2
  156. package/codeyam-cli/src/utils/database.js.map +1 -1
  157. package/codeyam-cli/src/utils/editorApi.js +11 -5
  158. package/codeyam-cli/src/utils/editorApi.js.map +1 -1
  159. package/codeyam-cli/src/utils/editorAudit.js +673 -5
  160. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  161. package/codeyam-cli/src/utils/editorBroadcastViewport.js +26 -0
  162. package/codeyam-cli/src/utils/editorBroadcastViewport.js.map +1 -0
  163. package/codeyam-cli/src/utils/editorDeleteScenario.js +67 -0
  164. package/codeyam-cli/src/utils/editorDeleteScenario.js.map +1 -0
  165. package/codeyam-cli/src/utils/editorDevServer.js +5 -1
  166. package/codeyam-cli/src/utils/editorDevServer.js.map +1 -1
  167. package/codeyam-cli/src/utils/editorEntityChangeStatus.js +13 -7
  168. package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -1
  169. package/codeyam-cli/src/utils/editorEntityHelpers.js +144 -0
  170. package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -0
  171. package/codeyam-cli/src/utils/editorGuard.js +36 -0
  172. package/codeyam-cli/src/utils/editorGuard.js.map +1 -0
  173. package/codeyam-cli/src/utils/editorLoaderHelpers.js +72 -1
  174. package/codeyam-cli/src/utils/editorLoaderHelpers.js.map +1 -1
  175. package/codeyam-cli/src/utils/editorMigration.js +224 -0
  176. package/codeyam-cli/src/utils/editorMigration.js.map +1 -0
  177. package/codeyam-cli/src/utils/editorPreview.js +33 -0
  178. package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
  179. package/codeyam-cli/src/utils/editorRecapture.js +109 -0
  180. package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
  181. package/codeyam-cli/src/utils/editorScenarioSwitch.js +24 -2
  182. package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
  183. package/codeyam-cli/src/utils/editorScenarios.js +580 -0
  184. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  185. package/codeyam-cli/src/utils/editorSeedAdapter.js +295 -6
  186. package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -1
  187. package/codeyam-cli/src/utils/editorShouldRevalidate.js +21 -0
  188. package/codeyam-cli/src/utils/editorShouldRevalidate.js.map +1 -0
  189. package/codeyam-cli/src/utils/entityChangeStatus.js +53 -6
  190. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
  191. package/codeyam-cli/src/utils/entityChangeStatus.server.js +57 -3
  192. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
  193. package/codeyam-cli/src/utils/fileWatcher.js +38 -0
  194. package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
  195. package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
  196. package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
  197. package/codeyam-cli/src/utils/install-skills.js +14 -0
  198. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  199. package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
  200. package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
  201. package/codeyam-cli/src/utils/parseRegisterArg.js.map +1 -1
  202. package/codeyam-cli/src/utils/progress.js +2 -2
  203. package/codeyam-cli/src/utils/progress.js.map +1 -1
  204. package/codeyam-cli/src/utils/queue/job.js +26 -5
  205. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  206. package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
  207. package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
  208. package/codeyam-cli/src/utils/routePatternMatching.js +129 -0
  209. package/codeyam-cli/src/utils/routePatternMatching.js.map +1 -0
  210. package/codeyam-cli/src/utils/scenarioCoverage.js +77 -0
  211. package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -0
  212. package/codeyam-cli/src/utils/scenariosManifest.js +269 -74
  213. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
  214. package/codeyam-cli/src/utils/screenshotHash.js +26 -0
  215. package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
  216. package/codeyam-cli/src/utils/serverState.js +30 -0
  217. package/codeyam-cli/src/utils/serverState.js.map +1 -1
  218. package/codeyam-cli/src/utils/setupClaudeCodeSettings.js +1 -0
  219. package/codeyam-cli/src/utils/setupClaudeCodeSettings.js.map +1 -1
  220. package/codeyam-cli/src/utils/simulationGateMiddleware.js +17 -1
  221. package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
  222. package/codeyam-cli/src/utils/slugUtils.js +25 -0
  223. package/codeyam-cli/src/utils/slugUtils.js.map +1 -0
  224. package/codeyam-cli/src/utils/syncMocksMiddleware.js +2 -2
  225. package/codeyam-cli/src/utils/syncMocksMiddleware.js.map +1 -1
  226. package/codeyam-cli/src/utils/telemetry.js +106 -0
  227. package/codeyam-cli/src/utils/telemetry.js.map +1 -0
  228. package/codeyam-cli/src/utils/telemetryMiddleware.js +22 -0
  229. package/codeyam-cli/src/utils/telemetryMiddleware.js.map +1 -0
  230. package/codeyam-cli/src/utils/testRunner.js +199 -1
  231. package/codeyam-cli/src/utils/testRunner.js.map +1 -1
  232. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +35 -0
  233. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -0
  234. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +107 -0
  235. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -0
  236. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +469 -0
  237. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  238. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +283 -0
  239. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -0
  240. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +135 -0
  241. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -0
  242. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +86 -0
  243. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -0
  244. package/codeyam-cli/src/webserver/app/lib/git.js +3 -2
  245. package/codeyam-cli/src/webserver/app/lib/git.js.map +1 -1
  246. package/codeyam-cli/src/webserver/app/types/editor.js +8 -0
  247. package/codeyam-cli/src/webserver/app/types/editor.js.map +1 -0
  248. package/codeyam-cli/src/webserver/backgroundServer.js +60 -61
  249. package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
  250. package/codeyam-cli/src/webserver/build/client/assets/CopyButton-CLe80MMu.js +1 -0
  251. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-BcgbViKV.js → EntityItem-Crt_KN_U.js} +3 -3
  252. package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-CQgyEGV-.js +1 -0
  253. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-CQIG2qda.js → EntityTypeIcon-CD7lGABo.js} +1 -1
  254. package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-CgTNOhnu.js +1 -0
  255. package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-CKeQT5Ty.js +25 -0
  256. package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-D3s1MFkb.js +3 -0
  257. package/codeyam-cli/src/webserver/build/client/assets/{LoadingDots-BU_OAEMP.js → LoadingDots-By5zI316.js} +1 -1
  258. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-ceAyBX-H.js → LogViewer-CM5zg40N.js} +3 -3
  259. package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-CQENLSrF.js +36 -0
  260. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-BzHcG7SE.js → ReportIssueModal-C2PLkej3.js} +2 -2
  261. package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-DanvyBPb.js +1 -0
  262. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-0DY_NKil.js → ScenarioViewer-DUMfcNVK.js} +3 -3
  263. package/codeyam-cli/src/webserver/build/client/assets/Spinner-D0LgAaSa.js +34 -0
  264. package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-CK7-NaPZ.js +1 -0
  265. package/codeyam-cli/src/webserver/build/client/assets/ViewportInspectBar-BA_Ry-rs.js +1 -0
  266. package/codeyam-cli/src/webserver/build/client/assets/{_index-DLxKhri3.js → _index-BAWd-Xjf.js} +2 -2
  267. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-BcY3q6nt.js → activity.(_tab)-BOARiB-g.js} +3 -3
  268. package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-Duc5hnl7.js → addon-web-links-CHx25PAe.js} +1 -1
  269. package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-Bni3iiUj.js → agent-transcripts-Bg3e7q4S.js} +3 -3
  270. package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
  271. package/codeyam-cli/src/webserver/build/client/assets/api.editor-rename-scenario-l0sNRNKZ.js +1 -0
  272. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
  273. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-seed-state-l0sNRNKZ.js +1 -0
  274. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-coverage-l0sNRNKZ.js +1 -0
  275. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-prompt-l0sNRNKZ.js +1 -0
  276. package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
  277. package/codeyam-cli/src/webserver/build/client/assets/api.editor-session-l0sNRNKZ.js +1 -0
  278. package/codeyam-cli/src/webserver/build/client/assets/{book-open-BYOypzCa.js → book-open-CL-lMgHh.js} +1 -1
  279. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-C_Pmso5S.js → chevron-down-GmAjGS9-.js} +1 -1
  280. package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BAdwhyCx.js +43 -0
  281. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-BVMi9VA5.js → circle-check-DFcQkN5j.js} +1 -1
  282. package/codeyam-cli/src/webserver/build/client/assets/{copy-n2FB0_Sw.js → copy-C6iF61Xs.js} +1 -1
  283. package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-CC6AbExI.js → createLucideIcon-4ImjHTVC.js} +1 -1
  284. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-Coe5NhbS.js +1 -0
  285. package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
  286. package/codeyam-cli/src/webserver/build/client/assets/dev.empty-C8y4mmyv.js +1 -0
  287. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +1 -0
  288. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DMv5ESGo.js +96 -0
  289. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-CluPkvXJ.js +41 -0
  290. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-BF4oLwaE.js → entity._sha._-ByHz6rAQ.js} +13 -12
  291. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-CmLO432x.js +6 -0
  292. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-Bz9sCUF_.js +6 -0
  293. package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-DQM8E7L4.js +6 -0
  294. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-BMvVHNXU.js → entity._sha_.edit._scenarioId-CAoXLsQr.js} +2 -2
  295. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-DTvKq3TY.js → entry.client-SuW9syRS.js} +6 -6
  296. package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-Daa96Fr1.js +1 -0
  297. package/codeyam-cli/src/webserver/build/client/assets/files-D-xGrg29.js +1 -0
  298. package/codeyam-cli/src/webserver/build/client/assets/git-Bq_fbXP5.js +1 -0
  299. package/codeyam-cli/src/webserver/build/client/assets/globals-oyPmV37k.css +1 -0
  300. package/codeyam-cli/src/webserver/build/client/assets/{index-BcvgDzbZ.js → index-Bp1l4hSv.js} +1 -1
  301. package/codeyam-cli/src/webserver/build/client/assets/{index-10oVnAAH.js → index-CWV9XZiG.js} +1 -1
  302. package/codeyam-cli/src/webserver/build/client/assets/{index-yHOVb4rc.js → index-DE3jI_dv.js} +1 -1
  303. package/codeyam-cli/src/webserver/build/client/assets/jsx-runtime-D_zvdyIk.js +9 -0
  304. package/codeyam-cli/src/webserver/build/client/assets/labs-B_IX45ih.js +1 -0
  305. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-DaAZ_H2w.js → loader-circle-De-7qQ2u.js} +1 -1
  306. package/codeyam-cli/src/webserver/build/client/assets/manifest-1a45e154.js +1 -0
  307. package/codeyam-cli/src/webserver/build/client/assets/memory-Cx2xEx7s.js +101 -0
  308. package/codeyam-cli/src/webserver/build/client/assets/{pause-f5-1lKBt.js → pause-CFxEKL1u.js} +1 -1
  309. package/codeyam-cli/src/webserver/build/client/assets/root-D2_tktnk.js +80 -0
  310. package/codeyam-cli/src/webserver/build/client/assets/{search-Di64LWVb.js → search-BdBb5aqc.js} +1 -1
  311. package/codeyam-cli/src/webserver/build/client/assets/settings-DdE-Untf.js +1 -0
  312. package/codeyam-cli/src/webserver/build/client/assets/simulations-DSCdE99u.js +1 -0
  313. package/codeyam-cli/src/webserver/build/client/assets/{terminal-Br7MOqts.js → terminal-CrplD4b1.js} +1 -1
  314. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-BLdiCuG-.js → triangle-alert-DqJ0j69l.js} +1 -1
  315. package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-DhXHbEjP.js +1 -0
  316. package/codeyam-cli/src/webserver/build/client/assets/{useLastLogLine-C14nCb1q.js → useLastLogLine-BNd5hYuW.js} +1 -1
  317. package/codeyam-cli/src/webserver/build/client/assets/useReportContext-Cy5Qg_UR.js +1 -0
  318. package/codeyam-cli/src/webserver/build/client/assets/useToast-5HR2j9ZE.js +1 -0
  319. package/codeyam-cli/src/webserver/build/client/sound-test.html +98 -0
  320. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-By5slFjw.js +16 -0
  321. package/codeyam-cli/src/webserver/build/server/assets/index-DXaOwBnm.js +1 -0
  322. package/codeyam-cli/src/webserver/build/server/assets/init-CLG1LjQM.js +10 -0
  323. package/codeyam-cli/src/webserver/build/server/assets/progress-CHTtrxFG.js +1 -0
  324. package/codeyam-cli/src/webserver/build/server/assets/server-build-NZmUqQv6.js +688 -0
  325. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  326. package/codeyam-cli/src/webserver/build-info.json +5 -5
  327. package/codeyam-cli/src/webserver/editorProxy.js +436 -13
  328. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  329. package/codeyam-cli/src/webserver/idleDetector.js +121 -0
  330. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -0
  331. package/codeyam-cli/src/webserver/mockStateEvents.js +28 -0
  332. package/codeyam-cli/src/webserver/mockStateEvents.js.map +1 -0
  333. package/codeyam-cli/src/webserver/public/sound-test.html +98 -0
  334. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +53 -0
  335. package/codeyam-cli/src/webserver/server.js +125 -4
  336. package/codeyam-cli/src/webserver/server.js.map +1 -1
  337. package/codeyam-cli/src/webserver/terminalServer.js +288 -43
  338. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  339. package/codeyam-cli/templates/__tests__/editor-step-hook.prompt-capture.test.ts +118 -0
  340. package/codeyam-cli/templates/chrome-extension-react/README.md +46 -0
  341. package/codeyam-cli/templates/chrome-extension-react/package.json +1 -0
  342. package/codeyam-cli/templates/chrome-extension-react/vite.config.ts +6 -0
  343. package/codeyam-cli/templates/codeyam-editor-claude.md +86 -5
  344. package/codeyam-cli/templates/codeyam-editor-reference.md +216 -0
  345. package/codeyam-cli/templates/editor-step-hook.py +193 -56
  346. package/codeyam-cli/templates/expo-react-native/README.md +41 -0
  347. package/codeyam-cli/templates/expo-react-native/package.json +1 -0
  348. package/codeyam-cli/templates/nextjs-prisma-sqlite/DATABASE.md +14 -0
  349. package/codeyam-cli/templates/nextjs-prisma-sqlite/README.md +53 -0
  350. package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +2 -1
  351. package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +83 -40
  352. package/codeyam-cli/templates/nextjs-prisma-supabase/README.md +52 -0
  353. package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +2 -1
  354. package/codeyam-cli/templates/seed-adapters/supabase.ts +282 -0
  355. package/codeyam-cli/templates/skills/codeyam-dev-mode/SKILL.md +1 -1
  356. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +94 -10
  357. package/package.json +2 -1
  358. package/packages/ai/src/lib/astScopes/methodSemantics.js +99 -0
  359. package/packages/ai/src/lib/astScopes/methodSemantics.js.map +1 -1
  360. package/packages/ai/src/lib/astScopes/nodeToSource.js +16 -0
  361. package/packages/ai/src/lib/astScopes/nodeToSource.js.map +1 -1
  362. package/packages/ai/src/lib/astScopes/paths.js +12 -3
  363. package/packages/ai/src/lib/astScopes/paths.js.map +1 -1
  364. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +27 -10
  365. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  366. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
  367. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
  368. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
  369. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
  370. package/packages/analyze/index.js +1 -1
  371. package/packages/analyze/index.js.map +1 -1
  372. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
  373. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
  374. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
  375. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  376. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
  377. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
  378. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
  379. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
  380. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
  381. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
  382. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
  383. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
  384. package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
  385. package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
  386. package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
  387. package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
  388. package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
  389. package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
  390. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +120 -28
  391. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  392. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +1368 -1193
  393. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  394. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js +84 -0
  395. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
  396. package/packages/database/src/lib/loadAnalysis.js +1 -1
  397. package/packages/database/src/lib/loadAnalysis.js.map +1 -1
  398. package/packages/database/src/lib/loadEntities.js +0 -6
  399. package/packages/database/src/lib/loadEntities.js.map +1 -1
  400. package/packages/database/src/lib/loadEntity.js +5 -5
  401. package/packages/database/src/lib/loadEntity.js.map +1 -1
  402. package/packages/database/src/lib/updateCommitMetadata.js +0 -25
  403. package/packages/database/src/lib/updateCommitMetadata.js.map +1 -1
  404. package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
  405. package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  406. package/codeyam-cli/src/webserver/build/client/assets/CopyButton-BPXZwM4t.js +0 -1
  407. package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-g3saevPb.js +0 -1
  408. package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-Bu6c6aDe.js +0 -1
  409. package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-DYFW3lDD.js +0 -25
  410. package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-DLeucoVX.js +0 -3
  411. package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-BED4B6sP.js +0 -1
  412. package/codeyam-cli/src/webserver/build/client/assets/Spinner-Bb5uFQ5V.js +0 -34
  413. package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-C8OKAR5x.js +0 -1
  414. package/codeyam-cli/src/webserver/build/client/assets/ViewportInspectBar-oAf2Kqsf.js +0 -1
  415. package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-C4pqxYJB.js +0 -51
  416. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
  417. package/codeyam-cli/src/webserver/build/client/assets/dev.empty-Csi0_PMl.js +0 -1
  418. package/codeyam-cli/src/webserver/build/client/assets/editor-BuT_Huj0.js +0 -10
  419. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-B7ztwLut.js +0 -41
  420. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-D5rYBT5x.js +0 -6
  421. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-CF164ouH.js +0 -6
  422. package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-p9hhkjJM.js +0 -6
  423. package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-cPo8LiG3.js +0 -1
  424. package/codeyam-cli/src/webserver/build/client/assets/files-BZrlFE1F.js +0 -1
  425. package/codeyam-cli/src/webserver/build/client/assets/git-DdZcvjGh.js +0 -1
  426. package/codeyam-cli/src/webserver/build/client/assets/globals-BkWJ_UNc.css +0 -1
  427. package/codeyam-cli/src/webserver/build/client/assets/labs-Zk7ryIM1.js +0 -1
  428. package/codeyam-cli/src/webserver/build/client/assets/manifest-b0f1372e.js +0 -1
  429. package/codeyam-cli/src/webserver/build/client/assets/memory-Bl2rpw8u.js +0 -96
  430. package/codeyam-cli/src/webserver/build/client/assets/root-B_X8HS1x.js +0 -67
  431. package/codeyam-cli/src/webserver/build/client/assets/settings-0OrEMU6J.js +0 -1
  432. package/codeyam-cli/src/webserver/build/client/assets/simulations-DWT-CvLy.js +0 -1
  433. package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-CrAK28Bc.js +0 -1
  434. package/codeyam-cli/src/webserver/build/client/assets/useReportContext-O-jkvSPx.js +0 -1
  435. package/codeyam-cli/src/webserver/build/client/assets/useToast-9FIWuYfK.js +0 -1
  436. package/codeyam-cli/src/webserver/build/server/assets/index-CbF6h3dj.js +0 -1
  437. package/codeyam-cli/src/webserver/build/server/assets/server-build-DRFwTJqO.js +0 -367
@@ -1,4 +1,6 @@
1
- import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, } from "../editorAudit.js";
1
+ import Database from 'better-sqlite3';
2
+ import { Kysely, SqliteDialect } from 'kysely';
3
+ import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isOnlyPreExistingIncomplete, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, aggregateClientErrorsByComponent, determineTargetedAnalysisPaths, shouldAutoRecapture, } from "../editorAudit.js";
2
4
  describe('editorAudit', () => {
3
5
  describe('isComponent', () => {
4
6
  it('should return true for JSX.Element return type', () => {
@@ -288,6 +290,29 @@ describe('editorAudit', () => {
288
290
  expect(result.functions[0].testsPassing).toBe(false);
289
291
  expect(result.functions[0].testsVisibleInUi).toBe(true);
290
292
  });
293
+ it('should distinguish runner errors from test failures and include error message', () => {
294
+ const result = computeAudit({
295
+ components: [],
296
+ functions: [
297
+ {
298
+ name: 'getTimeAgo',
299
+ filePath: 'src/lib/format.ts',
300
+ testFile: 'src/lib/format.test.ts',
301
+ },
302
+ ],
303
+ scenarioCounts: {},
304
+ testFileExistence: { 'src/lib/format.test.ts': true },
305
+ testResults: {
306
+ 'src/lib/format.test.ts': {
307
+ passing: false,
308
+ hasEntityNameDescribe: false,
309
+ errorMessage: 'Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"',
310
+ },
311
+ },
312
+ });
313
+ expect(result.functions[0].status).toBe('runner_error');
314
+ expect(result.functions[0].errorMessage).toBe('Error: Cannot find module "@/lib/format" from "src/lib/format.test.ts"');
315
+ });
291
316
  it('should mark function as name_mismatch when tests pass but no describe matches', () => {
292
317
  const result = computeAudit({
293
318
  components: [],
@@ -631,5 +656,3359 @@ describe('editorAudit', () => {
631
656
  expect(filtered.map((e) => e.name)).toContain('DrinkCard');
632
657
  });
633
658
  });
659
+ // ── resolveAuditSessionScope ──────────────────────────────────────
660
+ describe('resolveAuditSessionScope', () => {
661
+ it('should scope to session when entityChangeStatus has entries and featureStartedAt exists', () => {
662
+ const result = resolveAuditSessionScope({
663
+ featureStartedAt: '2026-03-12T14:01:31.291Z',
664
+ entityChangeStatus: {
665
+ ArticleCard: { status: 'new' },
666
+ LibraryHeader: { status: 'new' },
667
+ },
668
+ });
669
+ expect(result.featureStartedAt).toBe('2026-03-12T14:01:31.291Z');
670
+ expect(result.entityChangeStatus).toEqual({
671
+ ArticleCard: { status: 'new' },
672
+ LibraryHeader: { status: 'new' },
673
+ });
674
+ });
675
+ it('should NOT scope scenario counts to session when entityChangeStatus is empty', () => {
676
+ // This is the testapp bug: entityChangeStatus computation returned empty,
677
+ // so the glossary filter falls back to "audit all components", but the
678
+ // scenario count query still filters by featureStartedAt — causing
679
+ // Feature 1 components to appear as "no scenarios" even though they have them.
680
+ const result = resolveAuditSessionScope({
681
+ featureStartedAt: '2026-03-12T14:01:31.291Z',
682
+ entityChangeStatus: undefined,
683
+ });
684
+ // When we can't scope the glossary, we shouldn't scope scenario counts either
685
+ expect(result.featureStartedAt).toBeNull();
686
+ });
687
+ it('should NOT scope scenario counts to session when entityChangeStatus is an empty object', () => {
688
+ const result = resolveAuditSessionScope({
689
+ featureStartedAt: '2026-03-12T14:01:31.291Z',
690
+ entityChangeStatus: {},
691
+ });
692
+ expect(result.featureStartedAt).toBeNull();
693
+ });
694
+ it('should return null featureStartedAt when none was provided', () => {
695
+ const result = resolveAuditSessionScope({
696
+ featureStartedAt: null,
697
+ entityChangeStatus: { Header: { status: 'new' } },
698
+ });
699
+ expect(result.featureStartedAt).toBeNull();
700
+ });
701
+ it('should pass through entityChangeStatus unchanged when it has entries', () => {
702
+ const ecs = {
703
+ ArticleCard: { status: 'new' },
704
+ Header: { status: 'edited' },
705
+ };
706
+ const result = resolveAuditSessionScope({
707
+ featureStartedAt: '2026-03-12T14:01:31.291Z',
708
+ entityChangeStatus: ecs,
709
+ });
710
+ expect(result.entityChangeStatus).toBe(ecs);
711
+ });
712
+ describe('multi-feature audit scenario (testapp reproduction)', () => {
713
+ // Reproduces the exact testapp bug:
714
+ // - Feature 1 built 8 components with scenarios at 13:28-13:30
715
+ // - Feature 2 starts at 14:01, builds 4 new components with scenarios at 14:14-14:16
716
+ // - entityChangeStatus computation returns empty (fails/returns {})
717
+ // - Glossary includes ALL 12 components
718
+ // - Scenario counts only include Feature 2 scenarios (after 14:01)
719
+ // - Result: Feature 1's 8 components reported as "no scenarios"
720
+ const allComponents = [
721
+ // Feature 1 components
722
+ {
723
+ name: 'Header',
724
+ filePath: 'src/components/Header.tsx',
725
+ returnType: 'JSX.Element',
726
+ },
727
+ {
728
+ name: 'Logo',
729
+ filePath: 'src/components/Logo.tsx',
730
+ returnType: 'JSX.Element',
731
+ },
732
+ {
733
+ name: 'TabBar',
734
+ filePath: 'src/components/TabBar.tsx',
735
+ returnType: 'JSX.Element',
736
+ },
737
+ {
738
+ name: 'ArticlePreview',
739
+ filePath: 'src/components/ArticlePreview.tsx',
740
+ returnType: 'JSX.Element',
741
+ },
742
+ {
743
+ name: 'SaveButton',
744
+ filePath: 'src/components/SaveButton.tsx',
745
+ returnType: 'JSX.Element',
746
+ },
747
+ {
748
+ name: 'StatusBanner',
749
+ filePath: 'src/components/StatusBanner.tsx',
750
+ returnType: 'JSX.Element',
751
+ },
752
+ {
753
+ name: 'EmptyState',
754
+ filePath: 'src/components/EmptyState.tsx',
755
+ returnType: 'JSX.Element',
756
+ },
757
+ {
758
+ name: 'ArticleRow',
759
+ filePath: 'src/components/ArticleRow.tsx',
760
+ returnType: 'JSX.Element',
761
+ },
762
+ // Feature 2 components
763
+ {
764
+ name: 'ArticleCard',
765
+ filePath: 'src/components/ArticleCard.tsx',
766
+ returnType: 'JSX.Element',
767
+ },
768
+ {
769
+ name: 'LibraryHeader',
770
+ filePath: 'src/components/LibraryHeader.tsx',
771
+ returnType: 'JSX.Element',
772
+ },
773
+ {
774
+ name: 'ArticleCardGrid',
775
+ filePath: 'src/components/ArticleCardGrid.tsx',
776
+ returnType: 'JSX.Element',
777
+ },
778
+ {
779
+ name: 'OpenLibraryButton',
780
+ filePath: 'src/components/OpenLibraryButton.tsx',
781
+ returnType: 'JSX.Element',
782
+ },
783
+ ];
784
+ // All scenarios that exist in the DB (both features)
785
+ const allScenarioCounts = {
786
+ Header: 2,
787
+ Logo: 1,
788
+ TabBar: 3,
789
+ ArticlePreview: 4,
790
+ SaveButton: 2,
791
+ StatusBanner: 2,
792
+ EmptyState: 2,
793
+ ArticleRow: 3,
794
+ ArticleCard: 4,
795
+ LibraryHeader: 3,
796
+ ArticleCardGrid: 2,
797
+ OpenLibraryButton: 1,
798
+ };
799
+ // Only Feature 2 scenarios (created after featureStartedAt)
800
+ const sessionScopedCounts = {
801
+ ArticleCard: 4,
802
+ LibraryHeader: 3,
803
+ ArticleCardGrid: 2,
804
+ OpenLibraryButton: 1,
805
+ };
806
+ it('should pass audit when entityChangeStatus is empty and all scenarios are counted', () => {
807
+ // With the fix: resolveAuditSessionScope nullifies featureStartedAt
808
+ // when entityChangeStatus is empty, so ALL scenarios are counted
809
+ const scope = resolveAuditSessionScope({
810
+ featureStartedAt: '2026-03-12T14:01:31.291Z',
811
+ entityChangeStatus: undefined,
812
+ });
813
+ // Glossary filter: returns all (no change status to filter by)
814
+ const filtered = filterGlossaryByChangeStatus(allComponents, scope.entityChangeStatus);
815
+ expect(filtered).toHaveLength(12);
816
+ // Since featureStartedAt is now null, the route would query ALL scenarios
817
+ // (not session-scoped), giving us allScenarioCounts
818
+ const countsToUse = scope.featureStartedAt
819
+ ? sessionScopedCounts
820
+ : allScenarioCounts;
821
+ const { components } = classifyGlossaryEntries(filtered);
822
+ const result = computeAudit({
823
+ components,
824
+ functions: [],
825
+ scenarioCounts: countsToUse,
826
+ testFileExistence: {},
827
+ });
828
+ // Every component should have scenarios
829
+ expect(result.summary.componentsMissing).toBe(0);
830
+ expect(result.summary.componentsOk).toBe(12);
831
+ expect(result.summary.allPassing).toBe(true);
832
+ });
833
+ it('demonstrates the bug: session-scoped counts with unscoped glossary fails Feature 1', () => {
834
+ // WITHOUT the fix: entityChangeStatus is empty so all 12 components are audited,
835
+ // but scenario counts are session-scoped so Feature 1 components get 0
836
+ const filtered = filterGlossaryByChangeStatus(allComponents, undefined);
837
+ expect(filtered).toHaveLength(12);
838
+ const { components } = classifyGlossaryEntries(filtered);
839
+ const result = computeAudit({
840
+ components,
841
+ functions: [],
842
+ scenarioCounts: sessionScopedCounts, // BUG: only Feature 2 scenarios
843
+ testFileExistence: {},
844
+ });
845
+ // This is the broken behavior — 8 components incorrectly flagged
846
+ expect(result.summary.componentsMissing).toBe(8);
847
+ expect(result.summary.allPassing).toBe(false);
848
+ });
849
+ it('should correctly scope audit when entityChangeStatus is available', () => {
850
+ // When entityChangeStatus works, only Feature 2 components are audited
851
+ // and only Feature 2 scenarios are counted — both filters agree
852
+ const ecs = {
853
+ ArticleCard: { status: 'new' },
854
+ LibraryHeader: { status: 'new' },
855
+ ArticleCardGrid: { status: 'new' },
856
+ OpenLibraryButton: { status: 'new' },
857
+ };
858
+ const scope = resolveAuditSessionScope({
859
+ featureStartedAt: '2026-03-12T14:01:31.291Z',
860
+ entityChangeStatus: ecs,
861
+ });
862
+ // featureStartedAt preserved — session scoping is valid
863
+ expect(scope.featureStartedAt).toBe('2026-03-12T14:01:31.291Z');
864
+ const filtered = filterGlossaryByChangeStatus(allComponents, scope.entityChangeStatus);
865
+ expect(filtered).toHaveLength(4); // Only Feature 2 components
866
+ const { components } = classifyGlossaryEntries(filtered);
867
+ const result = computeAudit({
868
+ components,
869
+ functions: [],
870
+ scenarioCounts: sessionScopedCounts,
871
+ testFileExistence: {},
872
+ });
873
+ expect(result.summary.componentsMissing).toBe(0);
874
+ expect(result.summary.componentsOk).toBe(4);
875
+ expect(result.summary.allPassing).toBe(true);
876
+ });
877
+ });
878
+ });
879
+ // ── queryScenarioCounts ─────────────────────────────────────────────
880
+ describe('queryScenarioCounts', () => {
881
+ let db;
882
+ let rawDb;
883
+ const projectId = 'test-project-id';
884
+ beforeEach(async () => {
885
+ rawDb = new Database(':memory:');
886
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
887
+ await db.schema
888
+ .createTable('editor_scenarios')
889
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
890
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
891
+ .addColumn('name', 'varchar', (col) => col.notNull())
892
+ .addColumn('description', 'text')
893
+ .addColumn('component_name', 'varchar')
894
+ .addColumn('component_path', 'varchar')
895
+ .addColumn('url', 'varchar')
896
+ .addColumn('type', 'varchar')
897
+ .addColumn('screenshot_path', 'varchar')
898
+ .addColumn('viewport_width', 'integer')
899
+ .addColumn('viewport_height', 'integer')
900
+ .addColumn('dimension', 'varchar')
901
+ .addColumn('created_at', 'datetime')
902
+ .addColumn('updated_at', 'datetime')
903
+ .execute();
904
+ });
905
+ afterEach(async () => {
906
+ await db.destroy();
907
+ });
908
+ it('should count all scenarios when featureStartedAt is null', async () => {
909
+ await db
910
+ .insertInto('editor_scenarios')
911
+ .values({
912
+ id: 'sc-1',
913
+ project_id: projectId,
914
+ name: 'ArticleRow - Default',
915
+ component_name: 'ArticleRow',
916
+ viewport_width: 1280,
917
+ viewport_height: 720,
918
+ created_at: '2026-03-12 13:00:00',
919
+ })
920
+ .execute();
921
+ const counts = await queryScenarioCounts(db, projectId, null);
922
+ expect(counts).toEqual({ ArticleRow: 1 });
923
+ });
924
+ it('should count scenarios created after featureStartedAt', async () => {
925
+ await db
926
+ .insertInto('editor_scenarios')
927
+ .values({
928
+ id: 'sc-1',
929
+ project_id: projectId,
930
+ name: 'ArticleRow - Default',
931
+ component_name: 'ArticleRow',
932
+ viewport_width: 1280,
933
+ viewport_height: 720,
934
+ created_at: '2026-03-12 14:30:00',
935
+ })
936
+ .execute();
937
+ const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
938
+ expect(counts).toEqual({ ArticleRow: 1 });
939
+ });
940
+ it('should exclude scenarios created before featureStartedAt', async () => {
941
+ await db
942
+ .insertInto('editor_scenarios')
943
+ .values({
944
+ id: 'sc-1',
945
+ project_id: projectId,
946
+ name: 'ArticleRow - Default',
947
+ component_name: 'ArticleRow',
948
+ viewport_width: 1280,
949
+ viewport_height: 720,
950
+ created_at: '2026-03-12 13:00:00',
951
+ })
952
+ .execute();
953
+ const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
954
+ expect(counts).toEqual({});
955
+ });
956
+ it('should count re-registered scenarios whose updated_at is after featureStartedAt', async () => {
957
+ // This is the bug: a scenario from Feature 1 (created_at before featureStartedAt)
958
+ // is re-registered in Feature 2 (updated_at after featureStartedAt).
959
+ // The audit should count it because it was actively re-registered in this session.
960
+ await db
961
+ .insertInto('editor_scenarios')
962
+ .values({
963
+ id: 'sc-1',
964
+ project_id: projectId,
965
+ name: 'ArticleRow - Default',
966
+ component_name: 'ArticleRow',
967
+ viewport_width: 400,
968
+ viewport_height: 600,
969
+ created_at: '2026-03-12 13:28:00', // Feature 1
970
+ updated_at: '2026-03-12 14:32:00', // Re-registered in Feature 2
971
+ })
972
+ .execute();
973
+ const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
974
+ // Should count because updated_at is after featureStartedAt
975
+ expect(counts).toEqual({ ArticleRow: 1 });
976
+ });
977
+ it('should handle mixed scenarios: some new, some re-registered', async () => {
978
+ // ArticleCard: new in Feature 2 (created_at after featureStartedAt)
979
+ await db
980
+ .insertInto('editor_scenarios')
981
+ .values({
982
+ id: 'sc-1',
983
+ project_id: projectId,
984
+ name: 'ArticleCard - Default',
985
+ component_name: 'ArticleCard',
986
+ viewport_width: 1280,
987
+ viewport_height: 720,
988
+ created_at: '2026-03-12 14:14:00',
989
+ })
990
+ .execute();
991
+ // ArticleRow: from Feature 1, re-registered in Feature 2
992
+ await db
993
+ .insertInto('editor_scenarios')
994
+ .values({
995
+ id: 'sc-2',
996
+ project_id: projectId,
997
+ name: 'ArticleRow - Default',
998
+ component_name: 'ArticleRow',
999
+ viewport_width: 400,
1000
+ viewport_height: 600,
1001
+ created_at: '2026-03-12 13:28:00', // Feature 1
1002
+ updated_at: '2026-03-12 14:32:00', // Re-registered in Feature 2
1003
+ })
1004
+ .execute();
1005
+ const counts = await queryScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1006
+ expect(counts).toEqual({ ArticleCard: 1, ArticleRow: 1 });
1007
+ });
1008
+ });
1009
+ // ── queryPageScenarioCounts ──────────────────────────────────────────
1010
+ describe('queryPageScenarioCounts', () => {
1011
+ let db;
1012
+ let rawDb;
1013
+ const projectId = 'test-project-id';
1014
+ beforeEach(async () => {
1015
+ rawDb = new Database(':memory:');
1016
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1017
+ await db.schema
1018
+ .createTable('editor_scenarios')
1019
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1020
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1021
+ .addColumn('name', 'varchar', (col) => col.notNull())
1022
+ .addColumn('description', 'text')
1023
+ .addColumn('component_name', 'varchar')
1024
+ .addColumn('component_path', 'varchar')
1025
+ .addColumn('page_file_path', 'varchar')
1026
+ .addColumn('url', 'varchar')
1027
+ .addColumn('type', 'varchar')
1028
+ .addColumn('screenshot_path', 'varchar')
1029
+ .addColumn('viewport_width', 'integer')
1030
+ .addColumn('viewport_height', 'integer')
1031
+ .addColumn('dimension', 'varchar')
1032
+ .addColumn('created_at', 'datetime')
1033
+ .addColumn('updated_at', 'datetime')
1034
+ .execute();
1035
+ });
1036
+ afterEach(async () => {
1037
+ await db.destroy();
1038
+ });
1039
+ it('should count app-level scenarios by page_file_path', async () => {
1040
+ // App-level scenario: has page_file_path but no component_name
1041
+ await db
1042
+ .insertInto('editor_scenarios')
1043
+ .values({
1044
+ id: 'sc-1',
1045
+ project_id: projectId,
1046
+ name: 'Library - Default',
1047
+ component_name: null,
1048
+ page_file_path: 'app/library/page.tsx',
1049
+ created_at: '2026-03-12 13:00:00',
1050
+ })
1051
+ .execute();
1052
+ await db
1053
+ .insertInto('editor_scenarios')
1054
+ .values({
1055
+ id: 'sc-2',
1056
+ project_id: projectId,
1057
+ name: 'Library - Empty',
1058
+ component_name: null,
1059
+ page_file_path: 'app/library/page.tsx',
1060
+ created_at: '2026-03-12 13:05:00',
1061
+ })
1062
+ .execute();
1063
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1064
+ expect(counts).toEqual({ 'app/library/page.tsx': 2 });
1065
+ });
1066
+ it('should not count component scenarios (those have component_name)', async () => {
1067
+ // Component scenario — should NOT appear in page counts
1068
+ await db
1069
+ .insertInto('editor_scenarios')
1070
+ .values({
1071
+ id: 'sc-1',
1072
+ project_id: projectId,
1073
+ name: 'ArticleRow - Default',
1074
+ component_name: 'ArticleRow',
1075
+ page_file_path: null,
1076
+ created_at: '2026-03-12 13:00:00',
1077
+ })
1078
+ .execute();
1079
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1080
+ expect(counts).toEqual({});
1081
+ });
1082
+ it('should respect featureStartedAt filter', async () => {
1083
+ await db
1084
+ .insertInto('editor_scenarios')
1085
+ .values({
1086
+ id: 'sc-1',
1087
+ project_id: projectId,
1088
+ name: 'Library - Old',
1089
+ component_name: null,
1090
+ page_file_path: 'app/library/page.tsx',
1091
+ created_at: '2026-03-12 13:00:00',
1092
+ })
1093
+ .execute();
1094
+ await db
1095
+ .insertInto('editor_scenarios')
1096
+ .values({
1097
+ id: 'sc-2',
1098
+ project_id: projectId,
1099
+ name: 'Library - New',
1100
+ component_name: null,
1101
+ page_file_path: 'app/library/page.tsx',
1102
+ created_at: '2026-03-12 15:00:00',
1103
+ })
1104
+ .execute();
1105
+ const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1106
+ expect(counts).toEqual({ 'app/library/page.tsx': 1 });
1107
+ });
1108
+ it('should count re-registered page scenarios via updated_at', async () => {
1109
+ await db
1110
+ .insertInto('editor_scenarios')
1111
+ .values({
1112
+ id: 'sc-1',
1113
+ project_id: projectId,
1114
+ name: 'Library - Default',
1115
+ component_name: null,
1116
+ page_file_path: 'app/library/page.tsx',
1117
+ created_at: '2026-03-12 13:00:00',
1118
+ updated_at: '2026-03-12 15:00:00',
1119
+ })
1120
+ .execute();
1121
+ const counts = await queryPageScenarioCounts(db, projectId, '2026-03-12T14:01:31.291Z');
1122
+ expect(counts).toEqual({ 'app/library/page.tsx': 1 });
1123
+ });
1124
+ it('should group counts by page_file_path across multiple pages', async () => {
1125
+ await db
1126
+ .insertInto('editor_scenarios')
1127
+ .values([
1128
+ {
1129
+ id: 'sc-1',
1130
+ project_id: projectId,
1131
+ name: 'Library - Default',
1132
+ component_name: null,
1133
+ page_file_path: 'app/library/page.tsx',
1134
+ created_at: '2026-03-12 13:00:00',
1135
+ },
1136
+ {
1137
+ id: 'sc-2',
1138
+ project_id: projectId,
1139
+ name: 'Library - Rich',
1140
+ component_name: null,
1141
+ page_file_path: 'app/library/page.tsx',
1142
+ created_at: '2026-03-12 13:00:00',
1143
+ },
1144
+ {
1145
+ id: 'sc-3',
1146
+ project_id: projectId,
1147
+ name: 'Collections - Default',
1148
+ component_name: null,
1149
+ page_file_path: 'app/library/collections/page.tsx',
1150
+ created_at: '2026-03-12 13:00:00',
1151
+ },
1152
+ ])
1153
+ .execute();
1154
+ const counts = await queryPageScenarioCounts(db, projectId, null);
1155
+ expect(counts).toEqual({
1156
+ 'app/library/page.tsx': 2,
1157
+ 'app/library/collections/page.tsx': 1,
1158
+ });
1159
+ });
1160
+ });
1161
+ // ── Audit + entity completeness integration ─────────────────────────
1162
+ describe('audit should catch incomplete entities (bug reproduction)', () => {
1163
+ let db;
1164
+ let rawDb;
1165
+ const projectId = 'test-project-id';
1166
+ beforeEach(async () => {
1167
+ rawDb = new Database(':memory:');
1168
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1169
+ await db.schema
1170
+ .createTable('editor_scenarios')
1171
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1172
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1173
+ .addColumn('name', 'varchar', (col) => col.notNull())
1174
+ .addColumn('component_name', 'varchar')
1175
+ .addColumn('component_path', 'varchar')
1176
+ .addColumn('entity_sha', 'varchar')
1177
+ .addColumn('display_name', 'varchar')
1178
+ .addColumn('page_file_path', 'varchar')
1179
+ .addColumn('url', 'varchar')
1180
+ .addColumn('created_at', 'datetime')
1181
+ .addColumn('updated_at', 'datetime')
1182
+ .execute();
1183
+ await db.schema
1184
+ .createTable('analyses')
1185
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1186
+ .addColumn('entity_sha', 'varchar')
1187
+ .addColumn('entity_name', 'varchar')
1188
+ .addColumn('project_id', 'varchar')
1189
+ .execute();
1190
+ await db.schema
1191
+ .createTable('entities')
1192
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1193
+ .addColumn('name', 'varchar')
1194
+ .addColumn('entity_type', 'varchar')
1195
+ .addColumn('file_path', 'varchar')
1196
+ .execute();
1197
+ });
1198
+ afterEach(async () => {
1199
+ await db.destroy();
1200
+ });
1201
+ it('demonstrates the bug: computeAudit passes but entities are incomplete', async () => {
1202
+ // Setup: Two components in glossary, both have scenarios registered.
1203
+ // CollectionChips has scenarios but NO analysis records — it's "incomplete."
1204
+ // The glossary-based audit (computeAudit) doesn't know about entity analyses
1205
+ // so it says allPassing: true. This is the bug.
1206
+ // Entities in DB
1207
+ await db
1208
+ .insertInto('entities')
1209
+ .values({
1210
+ sha: 'sha-header',
1211
+ name: 'Header',
1212
+ entity_type: 'visual',
1213
+ file_path: 'src/components/Header.tsx',
1214
+ })
1215
+ .execute();
1216
+ await db
1217
+ .insertInto('entities')
1218
+ .values({
1219
+ sha: 'sha-chips',
1220
+ name: 'CollectionChips',
1221
+ entity_type: 'visual',
1222
+ file_path: 'src/components/CollectionChips.tsx',
1223
+ })
1224
+ .execute();
1225
+ // Header has an analysis — it's complete
1226
+ await db
1227
+ .insertInto('analyses')
1228
+ .values({
1229
+ id: 'a-1',
1230
+ entity_sha: 'sha-header',
1231
+ entity_name: 'Header',
1232
+ project_id: projectId,
1233
+ })
1234
+ .execute();
1235
+ // CollectionChips has NO analysis — it's incomplete
1236
+ // Both have scenarios
1237
+ await db
1238
+ .insertInto('editor_scenarios')
1239
+ .values({
1240
+ id: 'sc-1',
1241
+ project_id: projectId,
1242
+ name: 'Header - Default',
1243
+ component_name: 'Header',
1244
+ entity_sha: 'sha-header',
1245
+ created_at: '2026-03-16 23:00:00',
1246
+ })
1247
+ .execute();
1248
+ await db
1249
+ .insertInto('editor_scenarios')
1250
+ .values({
1251
+ id: 'sc-2',
1252
+ project_id: projectId,
1253
+ name: 'CollectionChips - Default',
1254
+ component_name: 'CollectionChips',
1255
+ entity_sha: 'sha-chips',
1256
+ created_at: '2026-03-16 23:19:00',
1257
+ })
1258
+ .execute();
1259
+ await db
1260
+ .insertInto('editor_scenarios')
1261
+ .values({
1262
+ id: 'sc-3',
1263
+ project_id: projectId,
1264
+ name: 'CollectionChips - Many',
1265
+ component_name: 'CollectionChips',
1266
+ entity_sha: 'sha-chips',
1267
+ created_at: '2026-03-16 23:19:05',
1268
+ })
1269
+ .execute();
1270
+ // The glossary says both are components with scenarios
1271
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1272
+ expect(scenarioCounts).toEqual({ Header: 1, CollectionChips: 2 });
1273
+ // computeAudit only checks glossary coverage — it passes!
1274
+ const auditResult = computeAudit({
1275
+ components: [
1276
+ {
1277
+ name: 'Header',
1278
+ filePath: 'src/components/Header.tsx',
1279
+ returnType: 'JSX.Element',
1280
+ },
1281
+ {
1282
+ name: 'CollectionChips',
1283
+ filePath: 'src/components/CollectionChips.tsx',
1284
+ returnType: 'JSX.Element',
1285
+ },
1286
+ ],
1287
+ functions: [],
1288
+ scenarioCounts,
1289
+ testFileExistence: {},
1290
+ });
1291
+ // BUG: computeAudit alone says everything is fine
1292
+ expect(auditResult.summary.allPassing).toBe(true);
1293
+ expect(auditResult.summary.componentsOk).toBe(2);
1294
+ // But queryIncompleteEntities catches the real issue
1295
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1296
+ expect(incomplete).toHaveLength(1);
1297
+ expect(incomplete[0].name).toBe('CollectionChips');
1298
+ expect(incomplete[0].scenarioCount).toBe(2);
1299
+ });
1300
+ it('audit should fail when combining computeAudit with incomplete entity check', async () => {
1301
+ // Same setup as above — this test shows the FIX working:
1302
+ // after computeAudit, we also check queryIncompleteEntities,
1303
+ // and if any are found, allPassing becomes false.
1304
+ await db
1305
+ .insertInto('entities')
1306
+ .values({
1307
+ sha: 'sha-header',
1308
+ name: 'Header',
1309
+ entity_type: 'visual',
1310
+ file_path: 'src/components/Header.tsx',
1311
+ })
1312
+ .execute();
1313
+ await db
1314
+ .insertInto('entities')
1315
+ .values({
1316
+ sha: 'sha-chips',
1317
+ name: 'CollectionChips',
1318
+ entity_type: 'visual',
1319
+ file_path: 'src/components/CollectionChips.tsx',
1320
+ })
1321
+ .execute();
1322
+ await db
1323
+ .insertInto('analyses')
1324
+ .values({
1325
+ id: 'a-1',
1326
+ entity_sha: 'sha-header',
1327
+ entity_name: 'Header',
1328
+ project_id: projectId,
1329
+ })
1330
+ .execute();
1331
+ await db
1332
+ .insertInto('editor_scenarios')
1333
+ .values({
1334
+ id: 'sc-1',
1335
+ project_id: projectId,
1336
+ name: 'Header - Default',
1337
+ component_name: 'Header',
1338
+ entity_sha: 'sha-header',
1339
+ created_at: '2026-03-16 23:00:00',
1340
+ })
1341
+ .execute();
1342
+ await db
1343
+ .insertInto('editor_scenarios')
1344
+ .values({
1345
+ id: 'sc-2',
1346
+ project_id: projectId,
1347
+ name: 'CollectionChips - Default',
1348
+ component_name: 'CollectionChips',
1349
+ entity_sha: 'sha-chips',
1350
+ created_at: '2026-03-16 23:19:00',
1351
+ })
1352
+ .execute();
1353
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1354
+ const auditResult = computeAudit({
1355
+ components: [
1356
+ {
1357
+ name: 'Header',
1358
+ filePath: 'src/components/Header.tsx',
1359
+ returnType: 'JSX.Element',
1360
+ },
1361
+ {
1362
+ name: 'CollectionChips',
1363
+ filePath: 'src/components/CollectionChips.tsx',
1364
+ returnType: 'JSX.Element',
1365
+ },
1366
+ ],
1367
+ functions: [],
1368
+ scenarioCounts,
1369
+ testFileExistence: {},
1370
+ });
1371
+ // Apply the same post-processing the audit endpoint does
1372
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1373
+ if (incomplete.length > 0) {
1374
+ auditResult.summary.allPassing = false;
1375
+ auditResult.summary.incompleteEntities = incomplete.length;
1376
+ }
1377
+ // NOW the audit correctly fails
1378
+ expect(auditResult.summary.allPassing).toBe(false);
1379
+ expect(auditResult.summary.incompleteEntities).toBe(1);
1380
+ });
1381
+ it('audit should pass when all entities have analyses', async () => {
1382
+ // Both entities have analyses — everything is complete
1383
+ await db
1384
+ .insertInto('entities')
1385
+ .values({
1386
+ sha: 'sha-header',
1387
+ name: 'Header',
1388
+ entity_type: 'visual',
1389
+ file_path: 'src/components/Header.tsx',
1390
+ })
1391
+ .execute();
1392
+ await db
1393
+ .insertInto('entities')
1394
+ .values({
1395
+ sha: 'sha-chips',
1396
+ name: 'CollectionChips',
1397
+ entity_type: 'visual',
1398
+ file_path: 'src/components/CollectionChips.tsx',
1399
+ })
1400
+ .execute();
1401
+ await db
1402
+ .insertInto('analyses')
1403
+ .values({
1404
+ id: 'a-1',
1405
+ entity_sha: 'sha-header',
1406
+ entity_name: 'Header',
1407
+ project_id: projectId,
1408
+ })
1409
+ .execute();
1410
+ await db
1411
+ .insertInto('analyses')
1412
+ .values({
1413
+ id: 'a-2',
1414
+ entity_sha: 'sha-chips',
1415
+ entity_name: 'CollectionChips',
1416
+ project_id: projectId,
1417
+ })
1418
+ .execute();
1419
+ await db
1420
+ .insertInto('editor_scenarios')
1421
+ .values({
1422
+ id: 'sc-1',
1423
+ project_id: projectId,
1424
+ name: 'Header - Default',
1425
+ component_name: 'Header',
1426
+ entity_sha: 'sha-header',
1427
+ created_at: '2026-03-16 23:00:00',
1428
+ })
1429
+ .execute();
1430
+ await db
1431
+ .insertInto('editor_scenarios')
1432
+ .values({
1433
+ id: 'sc-2',
1434
+ project_id: projectId,
1435
+ name: 'CollectionChips - Default',
1436
+ component_name: 'CollectionChips',
1437
+ entity_sha: 'sha-chips',
1438
+ created_at: '2026-03-16 23:19:00',
1439
+ })
1440
+ .execute();
1441
+ const scenarioCounts = await queryScenarioCounts(db, projectId, null);
1442
+ const auditResult = computeAudit({
1443
+ components: [
1444
+ {
1445
+ name: 'Header',
1446
+ filePath: 'src/components/Header.tsx',
1447
+ returnType: 'JSX.Element',
1448
+ },
1449
+ {
1450
+ name: 'CollectionChips',
1451
+ filePath: 'src/components/CollectionChips.tsx',
1452
+ returnType: 'JSX.Element',
1453
+ },
1454
+ ],
1455
+ functions: [],
1456
+ scenarioCounts,
1457
+ testFileExistence: {},
1458
+ });
1459
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1460
+ if (incomplete.length > 0) {
1461
+ auditResult.summary.allPassing = false;
1462
+ auditResult.summary.incompleteEntities = incomplete.length;
1463
+ }
1464
+ // Everything complete — audit passes
1465
+ expect(auditResult.summary.allPassing).toBe(true);
1466
+ expect(auditResult.summary.incompleteEntities).toBeUndefined();
1467
+ });
1468
+ });
1469
+ // ── filterToIncompleteFilePaths ──────────────────────────────────────
1470
+ describe('filterToIncompleteFilePaths', () => {
1471
+ // analyze-imports processes ALL file paths (~120 files) even when only
1472
+ // a few need analysis. This function filters to files that have no
1473
+ // entity with an analysis record.
1474
+ let db;
1475
+ let rawDb;
1476
+ const projectId = 'test-project-id';
1477
+ beforeEach(async () => {
1478
+ rawDb = new Database(':memory:');
1479
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1480
+ await db.schema
1481
+ .createTable('analyses')
1482
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1483
+ .addColumn('entity_sha', 'varchar')
1484
+ .addColumn('entity_name', 'varchar')
1485
+ .addColumn('project_id', 'varchar')
1486
+ .execute();
1487
+ await db.schema
1488
+ .createTable('entities')
1489
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1490
+ .addColumn('name', 'varchar')
1491
+ .addColumn('entity_type', 'varchar')
1492
+ .addColumn('file_path', 'varchar')
1493
+ .execute();
1494
+ });
1495
+ afterEach(async () => {
1496
+ await db.destroy();
1497
+ });
1498
+ it('should exclude files that have an entity with an analysis', async () => {
1499
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1500
+ // Entity with analysis — skip
1501
+ await db
1502
+ .insertInto('entities')
1503
+ .values({
1504
+ sha: 'sha-header',
1505
+ name: 'Header',
1506
+ entity_type: 'visual',
1507
+ file_path: 'app/components/Header.tsx',
1508
+ })
1509
+ .execute();
1510
+ await db
1511
+ .insertInto('analyses')
1512
+ .values({
1513
+ id: 'a-1',
1514
+ entity_sha: 'sha-header',
1515
+ entity_name: 'Header',
1516
+ project_id: projectId,
1517
+ })
1518
+ .execute();
1519
+ // Entity without analysis — needs analysis
1520
+ await db
1521
+ .insertInto('entities')
1522
+ .values({
1523
+ sha: 'sha-rule',
1524
+ name: 'RuleBuilder',
1525
+ entity_type: 'visual',
1526
+ file_path: 'app/components/RuleBuilder.tsx',
1527
+ })
1528
+ .execute();
1529
+ const allFilePaths = [
1530
+ 'app/components/Header.tsx',
1531
+ 'app/components/RuleBuilder.tsx',
1532
+ 'app/components/Footer.tsx', // no entity — needs analysis
1533
+ ];
1534
+ const result = await filterToIncompleteFilePaths(db, projectId, allFilePaths);
1535
+ expect(result).toContain('app/components/RuleBuilder.tsx');
1536
+ expect(result).toContain('app/components/Footer.tsx');
1537
+ expect(result).not.toContain('app/components/Header.tsx');
1538
+ });
1539
+ it('should return all file paths when no entities exist yet', async () => {
1540
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1541
+ const filePaths = ['app/Foo.tsx', 'app/Bar.tsx'];
1542
+ const result = await filterToIncompleteFilePaths(db, projectId, filePaths);
1543
+ expect(result).toEqual(filePaths);
1544
+ });
1545
+ it('should return empty when all files have analyzed entities', async () => {
1546
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1547
+ await db
1548
+ .insertInto('entities')
1549
+ .values({
1550
+ sha: 'sha-a',
1551
+ name: 'CompA',
1552
+ entity_type: 'visual',
1553
+ file_path: 'app/CompA.tsx',
1554
+ })
1555
+ .execute();
1556
+ await db
1557
+ .insertInto('analyses')
1558
+ .values({
1559
+ id: 'a-1',
1560
+ entity_sha: 'sha-a',
1561
+ entity_name: 'CompA',
1562
+ project_id: projectId,
1563
+ })
1564
+ .execute();
1565
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1566
+ 'app/CompA.tsx',
1567
+ ]);
1568
+ expect(result).toEqual([]);
1569
+ });
1570
+ it('should skip files with analysis even without scenarios', async () => {
1571
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1572
+ // Entity with analysis but NO scenarios — still complete
1573
+ await db
1574
+ .insertInto('entities')
1575
+ .values({
1576
+ sha: 'sha-util',
1577
+ name: 'utils',
1578
+ entity_type: 'library',
1579
+ file_path: 'app/lib/utils.ts',
1580
+ })
1581
+ .execute();
1582
+ await db
1583
+ .insertInto('analyses')
1584
+ .values({
1585
+ id: 'a-1',
1586
+ entity_sha: 'sha-util',
1587
+ entity_name: 'utils',
1588
+ project_id: projectId,
1589
+ })
1590
+ .execute();
1591
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1592
+ 'app/lib/utils.ts',
1593
+ ]);
1594
+ expect(result).toEqual([]);
1595
+ });
1596
+ });
1597
+ // ── phantom entity SHAs ─────────────────────────────────────────────
1598
+ describe('queryIncompleteEntities with phantom entity SHAs', () => {
1599
+ // Root cause of the Margo/reader step-blocking bug:
1600
+ // Scenarios registered without component_path got a phantom entity_sha
1601
+ // computed from component_name alone. These SHAs have NO entity record
1602
+ // in the entities table and NO analyses. syncScenarioEntityShas can't
1603
+ // fix them (it skips scenarios without component_path). So they remain
1604
+ // "incomplete" forever, blocking step progression.
1605
+ //
1606
+ // Fix: queryIncompleteEntities should not report scenarios whose
1607
+ // entity_sha has no entity record — these are orphaned data, not
1608
+ // fixable by running analyze-imports.
1609
+ let db;
1610
+ let rawDb;
1611
+ const projectId = 'test-project-id';
1612
+ beforeEach(async () => {
1613
+ rawDb = new Database(':memory:');
1614
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1615
+ await db.schema
1616
+ .createTable('editor_scenarios')
1617
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1618
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1619
+ .addColumn('name', 'varchar', (col) => col.notNull())
1620
+ .addColumn('component_name', 'varchar')
1621
+ .addColumn('component_path', 'varchar')
1622
+ .addColumn('entity_sha', 'varchar')
1623
+ .addColumn('display_name', 'varchar')
1624
+ .addColumn('page_file_path', 'varchar')
1625
+ .addColumn('url', 'varchar')
1626
+ .addColumn('created_at', 'datetime')
1627
+ .addColumn('updated_at', 'datetime')
1628
+ .execute();
1629
+ await db.schema
1630
+ .createTable('analyses')
1631
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1632
+ .addColumn('entity_sha', 'varchar')
1633
+ .addColumn('entity_name', 'varchar')
1634
+ .addColumn('project_id', 'varchar')
1635
+ .execute();
1636
+ await db.schema
1637
+ .createTable('entities')
1638
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1639
+ .addColumn('name', 'varchar')
1640
+ .addColumn('entity_type', 'varchar')
1641
+ .addColumn('file_path', 'varchar')
1642
+ .execute();
1643
+ });
1644
+ afterEach(async () => {
1645
+ await db.destroy();
1646
+ });
1647
+ it('should not report scenarios with phantom entity SHAs (no entity record exists)', async () => {
1648
+ // Real entity — has an entity record, analyses, and scenarios. Complete.
1649
+ await db
1650
+ .insertInto('entities')
1651
+ .values({
1652
+ sha: 'sha-real',
1653
+ name: 'RuleBuilder',
1654
+ entity_type: 'visual',
1655
+ file_path: 'app/components/RuleBuilder.tsx',
1656
+ })
1657
+ .execute();
1658
+ await db
1659
+ .insertInto('analyses')
1660
+ .values({
1661
+ id: 'a-1',
1662
+ entity_sha: 'sha-real',
1663
+ entity_name: 'RuleBuilder',
1664
+ project_id: projectId,
1665
+ })
1666
+ .execute();
1667
+ await db
1668
+ .insertInto('editor_scenarios')
1669
+ .values({
1670
+ id: 'sc-good',
1671
+ project_id: projectId,
1672
+ name: 'RuleBuilder - Empty',
1673
+ component_name: 'RuleBuilder',
1674
+ component_path: 'app/components/RuleBuilder.tsx',
1675
+ entity_sha: 'sha-real',
1676
+ created_at: '2026-03-16 23:00:00',
1677
+ })
1678
+ .execute();
1679
+ // Phantom entity — scenario points to a SHA that doesn't exist
1680
+ // in the entities table (registered without component_path).
1681
+ // No entity record, no analyses, unfixable by analyze-imports.
1682
+ await db
1683
+ .insertInto('editor_scenarios')
1684
+ .values({
1685
+ id: 'sc-phantom',
1686
+ project_id: projectId,
1687
+ name: 'Empty',
1688
+ component_name: 'RuleBuilder',
1689
+ component_path: null,
1690
+ entity_sha: 'sha-phantom-no-entity-record',
1691
+ created_at: '2026-03-16 22:00:00',
1692
+ })
1693
+ .execute();
1694
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1695
+ // Should NOT report phantom SHAs as incomplete — they can't be fixed
1696
+ // by running analyze-imports (no entity record exists to resolve).
1697
+ expect(incomplete).toHaveLength(0);
1698
+ });
1699
+ it('should still report real incomplete entities (entity exists but no analysis)', async () => {
1700
+ // Real entity without analysis — this IS a legitimate incomplete entity
1701
+ await db
1702
+ .insertInto('entities')
1703
+ .values({
1704
+ sha: 'sha-noanalysis',
1705
+ name: 'Footer',
1706
+ entity_type: 'visual',
1707
+ file_path: 'app/components/Footer.tsx',
1708
+ })
1709
+ .execute();
1710
+ await db
1711
+ .insertInto('editor_scenarios')
1712
+ .values({
1713
+ id: 'sc-1',
1714
+ project_id: projectId,
1715
+ name: 'Footer - Default',
1716
+ component_name: 'Footer',
1717
+ component_path: 'app/components/Footer.tsx',
1718
+ entity_sha: 'sha-noanalysis',
1719
+ created_at: '2026-03-16 23:00:00',
1720
+ })
1721
+ .execute();
1722
+ // Phantom scenario (shouldn't affect results)
1723
+ await db
1724
+ .insertInto('editor_scenarios')
1725
+ .values({
1726
+ id: 'sc-phantom',
1727
+ project_id: projectId,
1728
+ name: 'Footer - Alt',
1729
+ component_name: 'Footer',
1730
+ component_path: null,
1731
+ entity_sha: 'sha-phantom-does-not-exist',
1732
+ created_at: '2026-03-16 22:00:00',
1733
+ })
1734
+ .execute();
1735
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1736
+ // Should report Footer (real entity, no analysis) but NOT the phantom
1737
+ expect(incomplete).toHaveLength(1);
1738
+ expect(incomplete[0].name).toBe('Footer');
1739
+ expect(incomplete[0].entitySha).toBe('sha-noanalysis');
1740
+ });
1741
+ });
1742
+ // ── queryMiscategorizedScenarios ─────────────────────────────────────
1743
+ describe('queryMiscategorizedScenarios', () => {
1744
+ let db;
1745
+ let rawDb;
1746
+ const projectId = 'test-project-id';
1747
+ beforeEach(async () => {
1748
+ rawDb = new Database(':memory:');
1749
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1750
+ await db.schema
1751
+ .createTable('editor_scenarios')
1752
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1753
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1754
+ .addColumn('name', 'varchar', (col) => col.notNull())
1755
+ .addColumn('component_name', 'varchar')
1756
+ .addColumn('component_path', 'varchar')
1757
+ .addColumn('entity_sha', 'varchar')
1758
+ .addColumn('display_name', 'varchar')
1759
+ .addColumn('page_file_path', 'varchar')
1760
+ .addColumn('url', 'varchar')
1761
+ .addColumn('created_at', 'datetime')
1762
+ .addColumn('updated_at', 'datetime')
1763
+ .execute();
1764
+ });
1765
+ afterEach(async () => {
1766
+ await db.destroy();
1767
+ });
1768
+ it('should return empty when all component scenarios use isolation routes', async () => {
1769
+ await db
1770
+ .insertInto('editor_scenarios')
1771
+ .values({
1772
+ id: 'sc-1',
1773
+ project_id: projectId,
1774
+ name: 'LibraryCard - Default',
1775
+ component_name: 'LibraryCard',
1776
+ url: '/isolated-components/LibraryCard?s=Default',
1777
+ created_at: '2026-03-17 12:00:00',
1778
+ })
1779
+ .execute();
1780
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1781
+ expect(result).toEqual([]);
1782
+ });
1783
+ it('should flag component scenarios that use non-isolation URLs', async () => {
1784
+ // This is the bug: "Full Library Page" registered as component_name=LibraryPage
1785
+ // but url=/library — it's pointing at the real page, not an isolation route
1786
+ await db
1787
+ .insertInto('editor_scenarios')
1788
+ .values({
1789
+ id: 'sc-1',
1790
+ project_id: projectId,
1791
+ name: 'Full Library Page',
1792
+ component_name: 'LibraryPage',
1793
+ url: '/library',
1794
+ created_at: '2026-03-17 12:41:40',
1795
+ })
1796
+ .execute();
1797
+ await db
1798
+ .insertInto('editor_scenarios')
1799
+ .values({
1800
+ id: 'sc-2',
1801
+ project_id: projectId,
1802
+ name: 'Empty Library Page',
1803
+ component_name: 'LibraryPage',
1804
+ url: '/library',
1805
+ created_at: '2026-03-17 12:41:51',
1806
+ })
1807
+ .execute();
1808
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1809
+ expect(result).toEqual([
1810
+ {
1811
+ componentName: 'LibraryPage',
1812
+ scenarioNames: ['Full Library Page', 'Empty Library Page'],
1813
+ url: '/library',
1814
+ },
1815
+ ]);
1816
+ });
1817
+ it('should not flag page-level scenarios (no component_name)', async () => {
1818
+ // App-level scenarios have no component_name — they're fine with real URLs
1819
+ await db
1820
+ .insertInto('editor_scenarios')
1821
+ .values({
1822
+ id: 'sc-1',
1823
+ project_id: projectId,
1824
+ name: 'Library with Articles',
1825
+ url: '/',
1826
+ created_at: '2026-03-17 12:25:14',
1827
+ })
1828
+ .execute();
1829
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1830
+ expect(result).toEqual([]);
1831
+ });
1832
+ it('should group miscategorized scenarios by component and URL', async () => {
1833
+ // Two different components both misusing real URLs
1834
+ await db
1835
+ .insertInto('editor_scenarios')
1836
+ .values({
1837
+ id: 'sc-1',
1838
+ project_id: projectId,
1839
+ name: 'Full Library Page',
1840
+ component_name: 'LibraryPage',
1841
+ url: '/library',
1842
+ created_at: '2026-03-17 12:41:40',
1843
+ })
1844
+ .execute();
1845
+ await db
1846
+ .insertInto('editor_scenarios')
1847
+ .values({
1848
+ id: 'sc-2',
1849
+ project_id: projectId,
1850
+ name: 'Dashboard - Full',
1851
+ component_name: 'Dashboard',
1852
+ url: '/dashboard',
1853
+ created_at: '2026-03-17 12:50:00',
1854
+ })
1855
+ .execute();
1856
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1857
+ expect(result).toHaveLength(2);
1858
+ expect(result.map((r) => r.componentName).sort()).toEqual([
1859
+ 'Dashboard',
1860
+ 'LibraryPage',
1861
+ ]);
1862
+ });
1863
+ it('should scope to session when featureStartedAt is provided', async () => {
1864
+ // Old miscategorized scenario — before session
1865
+ await db
1866
+ .insertInto('editor_scenarios')
1867
+ .values({
1868
+ id: 'sc-old',
1869
+ project_id: projectId,
1870
+ name: 'Old Page',
1871
+ component_name: 'OldComponent',
1872
+ url: '/old',
1873
+ created_at: '2026-03-16 10:00:00',
1874
+ })
1875
+ .execute();
1876
+ // New miscategorized scenario — in session
1877
+ await db
1878
+ .insertInto('editor_scenarios')
1879
+ .values({
1880
+ id: 'sc-new',
1881
+ project_id: projectId,
1882
+ name: 'Full Library Page',
1883
+ component_name: 'LibraryPage',
1884
+ url: '/library',
1885
+ created_at: '2026-03-17 12:41:40',
1886
+ })
1887
+ .execute();
1888
+ const result = await queryMiscategorizedScenarios(db, projectId, '2026-03-17T11:58:55.562Z');
1889
+ expect(result).toHaveLength(1);
1890
+ expect(result[0].componentName).toBe('LibraryPage');
1891
+ });
1892
+ it('should not flag component scenarios with null URL', async () => {
1893
+ await db
1894
+ .insertInto('editor_scenarios')
1895
+ .values({
1896
+ id: 'sc-1',
1897
+ project_id: projectId,
1898
+ name: 'NoUrl - Default',
1899
+ component_name: 'NoUrl',
1900
+ created_at: '2026-03-17 12:00:00',
1901
+ })
1902
+ .execute();
1903
+ const result = await queryMiscategorizedScenarios(db, projectId, null);
1904
+ expect(result).toEqual([]);
1905
+ });
1906
+ });
1907
+ // ── isOnlyIncompleteEntities ─────────────────────────────────────────
1908
+ describe('isOnlyIncompleteEntities', () => {
1909
+ it('should return true when incompleteEntities is the only failure', () => {
1910
+ expect(isOnlyIncompleteEntities({
1911
+ componentsMissing: 0,
1912
+ componentsWithErrors: 0,
1913
+ functionsFailing: 0,
1914
+ functionsNameMismatch: 0,
1915
+ functionsMissing: 0,
1916
+ missingFromGlossary: 0,
1917
+ incompleteEntities: 3,
1918
+ allPassing: false,
1919
+ })).toBe(true);
1920
+ });
1921
+ it('should return false when there are also missing components', () => {
1922
+ expect(isOnlyIncompleteEntities({
1923
+ componentsMissing: 1,
1924
+ componentsWithErrors: 0,
1925
+ functionsFailing: 0,
1926
+ functionsNameMismatch: 0,
1927
+ functionsMissing: 0,
1928
+ missingFromGlossary: 0,
1929
+ incompleteEntities: 2,
1930
+ allPassing: false,
1931
+ })).toBe(false);
1932
+ });
1933
+ it('should return false when there are also failing tests', () => {
1934
+ expect(isOnlyIncompleteEntities({
1935
+ componentsMissing: 0,
1936
+ componentsWithErrors: 0,
1937
+ functionsFailing: 1,
1938
+ functionsNameMismatch: 0,
1939
+ functionsMissing: 0,
1940
+ missingFromGlossary: 0,
1941
+ incompleteEntities: 2,
1942
+ allPassing: false,
1943
+ })).toBe(false);
1944
+ });
1945
+ it('should return false when there are also missing glossary entries', () => {
1946
+ expect(isOnlyIncompleteEntities({
1947
+ componentsMissing: 0,
1948
+ componentsWithErrors: 0,
1949
+ functionsFailing: 0,
1950
+ functionsNameMismatch: 0,
1951
+ functionsMissing: 0,
1952
+ missingFromGlossary: 1,
1953
+ incompleteEntities: 2,
1954
+ allPassing: false,
1955
+ })).toBe(false);
1956
+ });
1957
+ it('should return true even when incompleteEntities is 0 (no failures at all)', () => {
1958
+ // Edge case: all zeros means nothing is failing
1959
+ expect(isOnlyIncompleteEntities({
1960
+ componentsMissing: 0,
1961
+ componentsWithErrors: 0,
1962
+ functionsFailing: 0,
1963
+ functionsNameMismatch: 0,
1964
+ functionsMissing: 0,
1965
+ missingFromGlossary: 0,
1966
+ incompleteEntities: 0,
1967
+ allPassing: true,
1968
+ })).toBe(true);
1969
+ });
1970
+ it('should return false when there are also miscategorized scenarios', () => {
1971
+ expect(isOnlyIncompleteEntities({
1972
+ componentsMissing: 0,
1973
+ componentsWithErrors: 0,
1974
+ functionsFailing: 0,
1975
+ functionsNameMismatch: 0,
1976
+ functionsMissing: 0,
1977
+ missingFromGlossary: 0,
1978
+ miscategorizedScenarios: 1,
1979
+ incompleteEntities: 2,
1980
+ allPassing: false,
1981
+ })).toBe(false);
1982
+ });
1983
+ it('should handle missing fields gracefully', () => {
1984
+ // Summary from older API version might not have all fields
1985
+ expect(isOnlyIncompleteEntities({
1986
+ incompleteEntities: 2,
1987
+ allPassing: false,
1988
+ })).toBe(true);
1989
+ });
1990
+ it('should return false when there are also runner errors', () => {
1991
+ // functionsRunnerError means the test runner crashed — a real failure
1992
+ // that cannot be fixed by entity SHA backfill or analyze-imports.
1993
+ // If this returns true, checkAuditGate would attempt a useless backfill
1994
+ // instead of reporting the runner error, and isOnlyPreExistingIncomplete
1995
+ // could let the gate pass entirely.
1996
+ expect(isOnlyIncompleteEntities({
1997
+ componentsMissing: 0,
1998
+ componentsWithErrors: 0,
1999
+ functionsFailing: 0,
2000
+ functionsRunnerError: 2,
2001
+ functionsNameMismatch: 0,
2002
+ functionsMissing: 0,
2003
+ missingFromGlossary: 0,
2004
+ incompleteEntities: 1,
2005
+ allPassing: false,
2006
+ })).toBe(false);
2007
+ });
2008
+ });
2009
+ // ── isAutoRemediable ─────────────────────────────────────────────────
2010
+ describe('isAutoRemediable', () => {
2011
+ // isAutoRemediable always returns false — the audit never triggers
2012
+ // full analyze-imports inline. It takes minutes on large projects.
2013
+ // Only lightweight backfill is acceptable during audit.
2014
+ it('should return false even on first attempt with only incomplete entities', () => {
2015
+ const result = isAutoRemediable({
2016
+ componentsMissing: 0,
2017
+ componentsWithErrors: 0,
2018
+ functionsFailing: 0,
2019
+ functionsNameMismatch: 0,
2020
+ functionsMissing: 0,
2021
+ missingFromGlossary: 0,
2022
+ miscategorizedScenarios: 0,
2023
+ incompleteEntities: 3,
2024
+ allPassing: false,
2025
+ }, false);
2026
+ expect(result).toBe(false);
2027
+ });
2028
+ it('should return false on second attempt', () => {
2029
+ const result = isAutoRemediable({
2030
+ componentsMissing: 0,
2031
+ componentsWithErrors: 0,
2032
+ functionsFailing: 0,
2033
+ functionsNameMismatch: 0,
2034
+ functionsMissing: 0,
2035
+ missingFromGlossary: 0,
2036
+ miscategorizedScenarios: 0,
2037
+ incompleteEntities: 3,
2038
+ allPassing: false,
2039
+ }, true);
2040
+ expect(result).toBe(false);
2041
+ });
2042
+ it('should return false when there are other failures besides incomplete entities', () => {
2043
+ const result = isAutoRemediable({
2044
+ componentsMissing: 1,
2045
+ incompleteEntities: 3,
2046
+ allPassing: false,
2047
+ }, false);
2048
+ expect(result).toBe(false);
2049
+ });
2050
+ it('should return false when there are no incomplete entities', () => {
2051
+ const result = isAutoRemediable({
2052
+ componentsMissing: 1,
2053
+ allPassing: false,
2054
+ }, false);
2055
+ expect(result).toBe(false);
2056
+ });
2057
+ });
2058
+ // ── queryIncompleteEntities ─────────────────────────────────────────
2059
+ describe('queryIncompleteEntities', () => {
2060
+ let db;
2061
+ let rawDb;
2062
+ const projectId = 'test-project-id';
2063
+ beforeEach(async () => {
2064
+ rawDb = new Database(':memory:');
2065
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
2066
+ await db.schema
2067
+ .createTable('editor_scenarios')
2068
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
2069
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
2070
+ .addColumn('name', 'varchar', (col) => col.notNull())
2071
+ .addColumn('component_name', 'varchar')
2072
+ .addColumn('component_path', 'varchar')
2073
+ .addColumn('entity_sha', 'varchar')
2074
+ .addColumn('display_name', 'varchar')
2075
+ .addColumn('page_file_path', 'varchar')
2076
+ .addColumn('url', 'varchar')
2077
+ .addColumn('created_at', 'datetime')
2078
+ .addColumn('updated_at', 'datetime')
2079
+ .execute();
2080
+ await db.schema
2081
+ .createTable('analyses')
2082
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
2083
+ .addColumn('entity_sha', 'varchar')
2084
+ .addColumn('entity_name', 'varchar')
2085
+ .addColumn('project_id', 'varchar')
2086
+ .execute();
2087
+ await db.schema
2088
+ .createTable('entities')
2089
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
2090
+ .addColumn('name', 'varchar')
2091
+ .addColumn('entity_type', 'varchar')
2092
+ .addColumn('file_path', 'varchar')
2093
+ .execute();
2094
+ });
2095
+ afterEach(async () => {
2096
+ await db.destroy();
2097
+ });
2098
+ it('should return empty when all scenario entity SHAs have analyses', async () => {
2099
+ // Entity with analysis
2100
+ await db
2101
+ .insertInto('entities')
2102
+ .values({
2103
+ sha: 'sha-header',
2104
+ name: 'Header',
2105
+ entity_type: 'visual',
2106
+ file_path: 'src/Header.tsx',
2107
+ })
2108
+ .execute();
2109
+ await db
2110
+ .insertInto('analyses')
2111
+ .values({
2112
+ id: 'a-1',
2113
+ entity_sha: 'sha-header',
2114
+ entity_name: 'Header',
2115
+ project_id: projectId,
2116
+ })
2117
+ .execute();
2118
+ await db
2119
+ .insertInto('editor_scenarios')
2120
+ .values({
2121
+ id: 'sc-1',
2122
+ project_id: projectId,
2123
+ name: 'Header - Default',
2124
+ component_name: 'Header',
2125
+ entity_sha: 'sha-header',
2126
+ created_at: '2026-03-16 23:00:00',
2127
+ })
2128
+ .execute();
2129
+ const result = await queryIncompleteEntities(db, projectId, null);
2130
+ expect(result).toEqual([]);
2131
+ });
2132
+ it('should return entities with scenarios but no analyses', async () => {
2133
+ // Entity WITHOUT analysis
2134
+ await db
2135
+ .insertInto('entities')
2136
+ .values({
2137
+ sha: 'sha-chips',
2138
+ name: 'CollectionChips',
2139
+ entity_type: 'visual',
2140
+ file_path: 'src/components/CollectionChips.tsx',
2141
+ })
2142
+ .execute();
2143
+ // Scenario referencing that entity
2144
+ await db
2145
+ .insertInto('editor_scenarios')
2146
+ .values({
2147
+ id: 'sc-1',
2148
+ project_id: projectId,
2149
+ name: 'CollectionChips - Default',
2150
+ component_name: 'CollectionChips',
2151
+ entity_sha: 'sha-chips',
2152
+ created_at: '2026-03-16 23:00:00',
2153
+ })
2154
+ .execute();
2155
+ await db
2156
+ .insertInto('editor_scenarios')
2157
+ .values({
2158
+ id: 'sc-2',
2159
+ project_id: projectId,
2160
+ name: 'CollectionChips - Many',
2161
+ component_name: 'CollectionChips',
2162
+ entity_sha: 'sha-chips',
2163
+ created_at: '2026-03-16 23:01:00',
2164
+ })
2165
+ .execute();
2166
+ const result = await queryIncompleteEntities(db, projectId, null);
2167
+ expect(result).toEqual([
2168
+ {
2169
+ entitySha: 'sha-chips',
2170
+ name: 'CollectionChips',
2171
+ scenarioCount: 2,
2172
+ preExisting: false,
2173
+ },
2174
+ ]);
2175
+ });
2176
+ it('should only return entities without analyses, not those with analyses', async () => {
2177
+ // Entity WITH analysis (Header)
2178
+ await db
2179
+ .insertInto('entities')
2180
+ .values({
2181
+ sha: 'sha-header',
2182
+ name: 'Header',
2183
+ entity_type: 'visual',
2184
+ file_path: 'src/Header.tsx',
2185
+ })
2186
+ .execute();
2187
+ await db
2188
+ .insertInto('analyses')
2189
+ .values({
2190
+ id: 'a-1',
2191
+ entity_sha: 'sha-header',
2192
+ entity_name: 'Header',
2193
+ project_id: projectId,
2194
+ })
2195
+ .execute();
2196
+ await db
2197
+ .insertInto('editor_scenarios')
2198
+ .values({
2199
+ id: 'sc-1',
2200
+ project_id: projectId,
2201
+ name: 'Header - Default',
2202
+ component_name: 'Header',
2203
+ entity_sha: 'sha-header',
2204
+ created_at: '2026-03-16 23:00:00',
2205
+ })
2206
+ .execute();
2207
+ // Entity WITHOUT analysis (CollectionPicker)
2208
+ await db
2209
+ .insertInto('entities')
2210
+ .values({
2211
+ sha: 'sha-picker',
2212
+ name: 'CollectionPicker',
2213
+ entity_type: 'visual',
2214
+ file_path: 'src/components/CollectionPicker.tsx',
2215
+ })
2216
+ .execute();
2217
+ await db
2218
+ .insertInto('editor_scenarios')
2219
+ .values({
2220
+ id: 'sc-2',
2221
+ project_id: projectId,
2222
+ name: 'CollectionPicker - Default',
2223
+ component_name: 'CollectionPicker',
2224
+ entity_sha: 'sha-picker',
2225
+ created_at: '2026-03-16 23:00:00',
2226
+ })
2227
+ .execute();
2228
+ const result = await queryIncompleteEntities(db, projectId, null);
2229
+ expect(result).toEqual([
2230
+ {
2231
+ entitySha: 'sha-picker',
2232
+ name: 'CollectionPicker',
2233
+ scenarioCount: 1,
2234
+ preExisting: false,
2235
+ },
2236
+ ]);
2237
+ });
2238
+ it('should return both pre-session and in-session entities with preExisting flags', async () => {
2239
+ // Entity without analysis, scenario created BEFORE session
2240
+ await db
2241
+ .insertInto('entities')
2242
+ .values({
2243
+ sha: 'sha-old',
2244
+ name: 'OldComponent',
2245
+ entity_type: 'visual',
2246
+ file_path: 'src/OldComponent.tsx',
2247
+ })
2248
+ .execute();
2249
+ await db
2250
+ .insertInto('editor_scenarios')
2251
+ .values({
2252
+ id: 'sc-old',
2253
+ project_id: projectId,
2254
+ name: 'OldComponent - Default',
2255
+ component_name: 'OldComponent',
2256
+ entity_sha: 'sha-old',
2257
+ created_at: '2026-03-16 20:00:00',
2258
+ })
2259
+ .execute();
2260
+ // Entity without analysis, scenario created DURING session
2261
+ await db
2262
+ .insertInto('entities')
2263
+ .values({
2264
+ sha: 'sha-new',
2265
+ name: 'NewComponent',
2266
+ entity_type: 'visual',
2267
+ file_path: 'src/NewComponent.tsx',
2268
+ })
2269
+ .execute();
2270
+ await db
2271
+ .insertInto('editor_scenarios')
2272
+ .values({
2273
+ id: 'sc-new',
2274
+ project_id: projectId,
2275
+ name: 'NewComponent - Default',
2276
+ component_name: 'NewComponent',
2277
+ entity_sha: 'sha-new',
2278
+ created_at: '2026-03-16 23:10:00',
2279
+ })
2280
+ .execute();
2281
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2282
+ // Both should be returned — OldComponent is preExisting, NewComponent is not
2283
+ expect(result).toEqual(expect.arrayContaining([
2284
+ {
2285
+ entitySha: 'sha-old',
2286
+ name: 'OldComponent',
2287
+ scenarioCount: 1,
2288
+ preExisting: true,
2289
+ },
2290
+ {
2291
+ entitySha: 'sha-new',
2292
+ name: 'NewComponent',
2293
+ scenarioCount: 1,
2294
+ preExisting: false,
2295
+ },
2296
+ ]));
2297
+ expect(result).toHaveLength(2);
2298
+ });
2299
+ it('should flag preExisting: false when scenario was updated in session even if created before', async () => {
2300
+ await db
2301
+ .insertInto('entities')
2302
+ .values({
2303
+ sha: 'sha-updated',
2304
+ name: 'UpdatedComponent',
2305
+ entity_type: 'visual',
2306
+ file_path: 'src/Updated.tsx',
2307
+ })
2308
+ .execute();
2309
+ await db
2310
+ .insertInto('editor_scenarios')
2311
+ .values({
2312
+ id: 'sc-updated',
2313
+ project_id: projectId,
2314
+ name: 'UpdatedComponent - Default',
2315
+ component_name: 'UpdatedComponent',
2316
+ entity_sha: 'sha-updated',
2317
+ created_at: '2026-03-16 20:00:00',
2318
+ updated_at: '2026-03-16 23:20:00', // Updated in session
2319
+ })
2320
+ .execute();
2321
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2322
+ expect(result).toEqual([
2323
+ {
2324
+ entitySha: 'sha-updated',
2325
+ name: 'UpdatedComponent',
2326
+ scenarioCount: 1,
2327
+ preExisting: false,
2328
+ },
2329
+ ]);
2330
+ });
2331
+ it('should skip scenarios with null entity_sha', async () => {
2332
+ await db
2333
+ .insertInto('editor_scenarios')
2334
+ .values({
2335
+ id: 'sc-null',
2336
+ project_id: projectId,
2337
+ name: 'Orphan Scenario',
2338
+ component_name: 'Orphan',
2339
+ created_at: '2026-03-16 23:00:00',
2340
+ })
2341
+ .execute();
2342
+ const result = await queryIncompleteEntities(db, projectId, null);
2343
+ expect(result).toEqual([]);
2344
+ });
2345
+ it('should return empty when there are no scenarios', async () => {
2346
+ const result = await queryIncompleteEntities(db, projectId, null);
2347
+ expect(result).toEqual([]);
2348
+ });
2349
+ it('should not flag entities when a sibling version (same name+filePath) has analyses', async () => {
2350
+ // Old entity version WITH analysis
2351
+ await db
2352
+ .insertInto('entities')
2353
+ .values({
2354
+ sha: 'sha-btn-v1',
2355
+ name: 'OpenLibraryButton',
2356
+ entity_type: 'visual',
2357
+ file_path: 'src/components/OpenLibraryButton.tsx',
2358
+ })
2359
+ .execute();
2360
+ await db
2361
+ .insertInto('analyses')
2362
+ .values({
2363
+ id: 'a-btn-v1',
2364
+ entity_sha: 'sha-btn-v1',
2365
+ entity_name: 'OpenLibraryButton',
2366
+ project_id: projectId,
2367
+ })
2368
+ .execute();
2369
+ // New entity version WITHOUT analysis (created by file watcher)
2370
+ await db
2371
+ .insertInto('entities')
2372
+ .values({
2373
+ sha: 'sha-btn-v2',
2374
+ name: 'OpenLibraryButton',
2375
+ entity_type: 'visual',
2376
+ file_path: 'src/components/OpenLibraryButton.tsx',
2377
+ })
2378
+ .execute();
2379
+ // Scenario points to the NEW version (backfilled after file watcher)
2380
+ await db
2381
+ .insertInto('editor_scenarios')
2382
+ .values({
2383
+ id: 'sc-btn',
2384
+ project_id: projectId,
2385
+ name: 'OpenLibraryButton - Default',
2386
+ component_name: 'OpenLibraryButton',
2387
+ entity_sha: 'sha-btn-v2',
2388
+ created_at: '2026-03-16 23:00:00',
2389
+ })
2390
+ .execute();
2391
+ // Should NOT flag as incomplete — sibling version has analyses
2392
+ const result = await queryIncompleteEntities(db, projectId, null);
2393
+ expect(result).toEqual([]);
2394
+ });
2395
+ it('should flag entity when sibling has analyses but different filePath (extracted component)', async () => {
2396
+ // Old entity version WITH analysis at ORIGINAL file path
2397
+ await db
2398
+ .insertInto('entities')
2399
+ .values({
2400
+ sha: 'sha-card-v1',
2401
+ name: 'TaskCard',
2402
+ entity_type: 'visual',
2403
+ file_path: 'app/page.tsx',
2404
+ })
2405
+ .execute();
2406
+ await db
2407
+ .insertInto('analyses')
2408
+ .values({
2409
+ id: 'a-card-v1',
2410
+ entity_sha: 'sha-card-v1',
2411
+ entity_name: 'TaskCard',
2412
+ project_id: projectId,
2413
+ })
2414
+ .execute();
2415
+ // New entity version WITHOUT analysis at EXTRACTED file path
2416
+ await db
2417
+ .insertInto('entities')
2418
+ .values({
2419
+ sha: 'sha-card-v2',
2420
+ name: 'TaskCard',
2421
+ entity_type: 'visual',
2422
+ file_path: 'app/components/TaskCard.tsx',
2423
+ })
2424
+ .execute();
2425
+ // Scenario points to the new version (synced by syncScenarioEntityShas)
2426
+ await db
2427
+ .insertInto('editor_scenarios')
2428
+ .values({
2429
+ id: 'sc-card',
2430
+ project_id: projectId,
2431
+ name: 'TaskCard - Default',
2432
+ component_name: 'TaskCard',
2433
+ entity_sha: 'sha-card-v2',
2434
+ created_at: '2026-03-16 23:00:00',
2435
+ })
2436
+ .execute();
2437
+ // SHOULD flag as incomplete — sibling has analyses but at a different filePath,
2438
+ // so getAllEntities() won't inherit (it matches by name+filePath)
2439
+ const result = await queryIncompleteEntities(db, projectId, null);
2440
+ expect(result).toEqual([
2441
+ {
2442
+ entitySha: 'sha-card-v2',
2443
+ name: 'TaskCard',
2444
+ scenarioCount: 1,
2445
+ preExisting: false,
2446
+ },
2447
+ ]);
2448
+ });
2449
+ it('should still flag entities when no sibling version has analyses', async () => {
2450
+ // Only one version, no analyses
2451
+ await db
2452
+ .insertInto('entities')
2453
+ .values({
2454
+ sha: 'sha-icon',
2455
+ name: 'ExternalLinkIcon',
2456
+ entity_type: 'visual',
2457
+ file_path: 'src/components/ExternalLinkIcon.tsx',
2458
+ })
2459
+ .execute();
2460
+ await db
2461
+ .insertInto('editor_scenarios')
2462
+ .values({
2463
+ id: 'sc-icon',
2464
+ project_id: projectId,
2465
+ name: 'ExternalLinkIcon - Default',
2466
+ component_name: 'ExternalLinkIcon',
2467
+ entity_sha: 'sha-icon',
2468
+ created_at: '2026-03-16 23:00:00',
2469
+ })
2470
+ .execute();
2471
+ // Should flag as incomplete — no version has analyses
2472
+ const result = await queryIncompleteEntities(db, projectId, null);
2473
+ expect(result).toEqual([
2474
+ {
2475
+ entitySha: 'sha-icon',
2476
+ name: 'ExternalLinkIcon',
2477
+ scenarioCount: 1,
2478
+ preExisting: false,
2479
+ },
2480
+ ]);
2481
+ });
2482
+ it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
2483
+ // Scenario has entity_sha but entity record doesn't exist.
2484
+ // These are "phantom SHAs" created when scenarios were registered
2485
+ // without component_path — they can never be fixed by analyze-imports
2486
+ // and should not block audit progression.
2487
+ await db
2488
+ .insertInto('editor_scenarios')
2489
+ .values({
2490
+ id: 'sc-1',
2491
+ project_id: projectId,
2492
+ name: 'Ghost - Default',
2493
+ component_name: 'GhostComponent',
2494
+ entity_sha: 'sha-ghost',
2495
+ created_at: '2026-03-16 23:00:00',
2496
+ })
2497
+ .execute();
2498
+ const result = await queryIncompleteEntities(db, projectId, null);
2499
+ // Phantom SHAs are excluded — not reportable as incomplete
2500
+ expect(result).toEqual([]);
2501
+ });
2502
+ it('should detect incomplete entity whose scenario predates the session', async () => {
2503
+ // Entity with no analyses, scenario created BEFORE session
2504
+ await db
2505
+ .insertInto('entities')
2506
+ .values({
2507
+ sha: 'sha-preexisting',
2508
+ name: 'PreExistingComponent',
2509
+ entity_type: 'visual',
2510
+ file_path: 'src/PreExistingComponent.tsx',
2511
+ })
2512
+ .execute();
2513
+ await db
2514
+ .insertInto('editor_scenarios')
2515
+ .values({
2516
+ id: 'sc-preexisting',
2517
+ project_id: projectId,
2518
+ name: 'PreExistingComponent - Default',
2519
+ component_name: 'PreExistingComponent',
2520
+ entity_sha: 'sha-preexisting',
2521
+ created_at: '2026-03-16 20:00:00',
2522
+ updated_at: '2026-03-16 20:00:00',
2523
+ })
2524
+ .execute();
2525
+ // Session started well after scenario was created/updated
2526
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2527
+ // Should still be detected — the old time filter would have excluded it
2528
+ expect(result).toEqual([
2529
+ {
2530
+ entitySha: 'sha-preexisting',
2531
+ name: 'PreExistingComponent',
2532
+ scenarioCount: 1,
2533
+ preExisting: true,
2534
+ },
2535
+ ]);
2536
+ });
2537
+ it('should flag preExisting: true when all scenarios predate the session', async () => {
2538
+ await db
2539
+ .insertInto('entities')
2540
+ .values({
2541
+ sha: 'sha-old-entity',
2542
+ name: 'OldEntity',
2543
+ entity_type: 'visual',
2544
+ file_path: 'src/OldEntity.tsx',
2545
+ })
2546
+ .execute();
2547
+ // Two scenarios, both before session
2548
+ await db
2549
+ .insertInto('editor_scenarios')
2550
+ .values({
2551
+ id: 'sc-old-1',
2552
+ project_id: projectId,
2553
+ name: 'OldEntity - Default',
2554
+ component_name: 'OldEntity',
2555
+ entity_sha: 'sha-old-entity',
2556
+ created_at: '2026-03-16 19:00:00',
2557
+ updated_at: '2026-03-16 19:00:00',
2558
+ })
2559
+ .execute();
2560
+ await db
2561
+ .insertInto('editor_scenarios')
2562
+ .values({
2563
+ id: 'sc-old-2',
2564
+ project_id: projectId,
2565
+ name: 'OldEntity - Hover',
2566
+ component_name: 'OldEntity',
2567
+ entity_sha: 'sha-old-entity',
2568
+ created_at: '2026-03-16 19:30:00',
2569
+ updated_at: '2026-03-16 19:30:00',
2570
+ })
2571
+ .execute();
2572
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2573
+ expect(result).toEqual([
2574
+ {
2575
+ entitySha: 'sha-old-entity',
2576
+ name: 'OldEntity',
2577
+ scenarioCount: 2,
2578
+ preExisting: true,
2579
+ },
2580
+ ]);
2581
+ });
2582
+ it('should flag preExisting: false when scenario is from the current session', async () => {
2583
+ await db
2584
+ .insertInto('entities')
2585
+ .values({
2586
+ sha: 'sha-session-entity',
2587
+ name: 'SessionEntity',
2588
+ entity_type: 'visual',
2589
+ file_path: 'src/SessionEntity.tsx',
2590
+ })
2591
+ .execute();
2592
+ await db
2593
+ .insertInto('editor_scenarios')
2594
+ .values({
2595
+ id: 'sc-session',
2596
+ project_id: projectId,
2597
+ name: 'SessionEntity - Default',
2598
+ component_name: 'SessionEntity',
2599
+ entity_sha: 'sha-session-entity',
2600
+ created_at: '2026-03-16 23:30:00',
2601
+ updated_at: '2026-03-16 23:30:00',
2602
+ })
2603
+ .execute();
2604
+ const result = await queryIncompleteEntities(db, projectId, '2026-03-16T23:07:12.698Z');
2605
+ expect(result).toEqual([
2606
+ {
2607
+ entitySha: 'sha-session-entity',
2608
+ name: 'SessionEntity',
2609
+ scenarioCount: 1,
2610
+ preExisting: false,
2611
+ },
2612
+ ]);
2613
+ });
2614
+ });
2615
+ // ── identifyScenariosNeedingRecapture ──────────────────────────────
2616
+ describe('identifyScenariosNeedingRecapture', () => {
2617
+ // Reproduces the Margo bug: Feature 1 built app-level popup scenarios,
2618
+ // Feature 2 edited LibraryView (used by App), but app-level scenarios
2619
+ // were never flagged for recapture because the audit only checked
2620
+ // component scenario existence — not whether app-level scenarios are stale.
2621
+ //
2622
+ // Each scenario's entityName is resolved by the caller via
2623
+ // entity_sha → entities.name (the default export for app-level scenarios).
2624
+ it('should flag app-level scenario when its entity is impacted by transitive dependency change', () => {
2625
+ // LibraryView was edited → App is impacted (imports LibraryView)
2626
+ // App-level scenario "Library - Rich Library" has entity_sha pointing to App
2627
+ // It was NOT recaptured during Feature 2 → should be flagged
2628
+ const entityChangeStatus = {
2629
+ LibraryView: { status: 'edited' },
2630
+ App: {
2631
+ status: 'impacted',
2632
+ impactedBy: [
2633
+ {
2634
+ name: 'LibraryView',
2635
+ filePath: 'src/components/LibraryView.tsx',
2636
+ changeType: 'edited',
2637
+ },
2638
+ ],
2639
+ },
2640
+ };
2641
+ const result = identifyScenariosNeedingRecapture({
2642
+ scenarios: [
2643
+ {
2644
+ name: 'Library - Rich Library',
2645
+ entityName: 'App', // resolved from entity_sha → entities.name
2646
+ updatedInSession: false,
2647
+ },
2648
+ ],
2649
+ entityChangeStatus,
2650
+ });
2651
+ expect(result).toHaveLength(1);
2652
+ expect(result[0].scenarioName).toBe('Library - Rich Library');
2653
+ expect(result[0].entityName).toBe('App');
2654
+ expect(result[0].status.status).toBe('impacted');
2655
+ });
2656
+ it('should flag component scenario when its entity is directly edited and not recaptured', () => {
2657
+ const entityChangeStatus = {
2658
+ LibraryView: { status: 'edited' },
2659
+ };
2660
+ const result = identifyScenariosNeedingRecapture({
2661
+ scenarios: [
2662
+ {
2663
+ name: 'LibraryView - Empty',
2664
+ entityName: 'LibraryView',
2665
+ updatedInSession: false,
2666
+ },
2667
+ ],
2668
+ entityChangeStatus,
2669
+ });
2670
+ expect(result).toHaveLength(1);
2671
+ expect(result[0].scenarioName).toBe('LibraryView - Empty');
2672
+ expect(result[0].entityName).toBe('LibraryView');
2673
+ expect(result[0].status.status).toBe('edited');
2674
+ });
2675
+ it('should NOT flag scenario that was already recaptured in the current session', () => {
2676
+ const entityChangeStatus = {
2677
+ App: {
2678
+ status: 'impacted',
2679
+ impactedBy: [
2680
+ {
2681
+ name: 'LibraryView',
2682
+ filePath: 'src/components/LibraryView.tsx',
2683
+ changeType: 'edited',
2684
+ },
2685
+ ],
2686
+ },
2687
+ };
2688
+ const result = identifyScenariosNeedingRecapture({
2689
+ scenarios: [
2690
+ {
2691
+ name: 'App - Default',
2692
+ entityName: 'App',
2693
+ updatedInSession: true, // re-registered during Feature 2
2694
+ },
2695
+ ],
2696
+ entityChangeStatus,
2697
+ });
2698
+ expect(result).toHaveLength(0);
2699
+ });
2700
+ it('should NOT flag scenario whose entity has no change status', () => {
2701
+ const entityChangeStatus = {
2702
+ LibraryView: { status: 'edited' },
2703
+ };
2704
+ const result = identifyScenariosNeedingRecapture({
2705
+ scenarios: [
2706
+ {
2707
+ name: 'WelcomeScreen - Default',
2708
+ entityName: 'WelcomeScreen',
2709
+ updatedInSession: false,
2710
+ },
2711
+ ],
2712
+ entityChangeStatus,
2713
+ });
2714
+ expect(result).toHaveLength(0);
2715
+ });
2716
+ it('should return empty array when entityChangeStatus is undefined', () => {
2717
+ const result = identifyScenariosNeedingRecapture({
2718
+ scenarios: [
2719
+ {
2720
+ name: 'Library - Rich Library',
2721
+ entityName: 'App',
2722
+ updatedInSession: false,
2723
+ },
2724
+ ],
2725
+ entityChangeStatus: undefined,
2726
+ });
2727
+ expect(result).toHaveLength(0);
2728
+ });
2729
+ it('should return empty array when entityChangeStatus is empty', () => {
2730
+ const result = identifyScenariosNeedingRecapture({
2731
+ scenarios: [
2732
+ {
2733
+ name: 'Library - Rich Library',
2734
+ entityName: 'App',
2735
+ updatedInSession: false,
2736
+ },
2737
+ ],
2738
+ entityChangeStatus: {},
2739
+ });
2740
+ expect(result).toHaveLength(0);
2741
+ });
2742
+ it('should flag multiple app-level scenarios sharing the same impacted entity', () => {
2743
+ const entityChangeStatus = {
2744
+ LibraryView: { status: 'edited' },
2745
+ App: {
2746
+ status: 'impacted',
2747
+ impactedBy: [
2748
+ {
2749
+ name: 'LibraryView',
2750
+ filePath: 'src/components/LibraryView.tsx',
2751
+ changeType: 'edited',
2752
+ },
2753
+ ],
2754
+ },
2755
+ };
2756
+ const result = identifyScenariosNeedingRecapture({
2757
+ scenarios: [
2758
+ {
2759
+ name: 'Library - Empty',
2760
+ entityName: 'App',
2761
+ updatedInSession: false,
2762
+ },
2763
+ {
2764
+ name: 'Library - Rich Library',
2765
+ entityName: 'App',
2766
+ updatedInSession: false,
2767
+ },
2768
+ {
2769
+ name: 'First Article Saved',
2770
+ entityName: 'App',
2771
+ updatedInSession: false,
2772
+ },
2773
+ ],
2774
+ entityChangeStatus,
2775
+ });
2776
+ expect(result).toHaveLength(3);
2777
+ expect(result.map((r) => r.scenarioName).sort()).toEqual([
2778
+ 'First Article Saved',
2779
+ 'Library - Empty',
2780
+ 'Library - Rich Library',
2781
+ ]);
2782
+ expect(result.every((r) => r.entityName === 'App')).toBe(true);
2783
+ });
2784
+ it('should skip scenarios with null entityName (no entity_sha set)', () => {
2785
+ const entityChangeStatus = {
2786
+ App: { status: 'edited' },
2787
+ };
2788
+ const result = identifyScenariosNeedingRecapture({
2789
+ scenarios: [
2790
+ {
2791
+ name: 'Mystery Scenario',
2792
+ entityName: null, // no entity_sha → no entity name
2793
+ updatedInSession: false,
2794
+ },
2795
+ ],
2796
+ entityChangeStatus,
2797
+ });
2798
+ expect(result).toHaveLength(0);
2799
+ });
2800
+ it('should NOT flag scenarios for new entities (they need creation, not recapture)', () => {
2801
+ // "new" entities are being seen for the first time. Their scenarios need
2802
+ // initial creation, not recapture of old screenshots. Flagging them as
2803
+ // "needs_recapture" sends the wrong remediation signal to Claude.
2804
+ const entityChangeStatus = {
2805
+ NewComponent: { status: 'new' },
2806
+ };
2807
+ const result = identifyScenariosNeedingRecapture({
2808
+ scenarios: [
2809
+ {
2810
+ name: 'NewComponent - Default',
2811
+ entityName: 'NewComponent',
2812
+ updatedInSession: false,
2813
+ },
2814
+ ],
2815
+ entityChangeStatus,
2816
+ });
2817
+ expect(result).toHaveLength(0);
2818
+ });
2819
+ });
2820
+ // ── detectDuplicateNames ──────────────────────────────────────────
2821
+ describe('detectDuplicateNames', () => {
2822
+ it('should return empty map when no duplicates exist', () => {
2823
+ const entries = [
2824
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2825
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2826
+ { name: 'Sidebar', filePath: 'app/components/Sidebar.tsx' },
2827
+ ];
2828
+ const result = detectDuplicateNames(entries);
2829
+ expect(result.size).toBe(0);
2830
+ });
2831
+ it('should group entries that share a name', () => {
2832
+ const entries = [
2833
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2834
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2835
+ { name: 'Page', filePath: 'app/isolated-components/Baz/page.tsx' },
2836
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2837
+ ];
2838
+ const result = detectDuplicateNames(entries);
2839
+ expect(result.size).toBe(1);
2840
+ expect(result.has('Page')).toBe(true);
2841
+ const pageGroup = result.get('Page');
2842
+ expect(pageGroup).toHaveLength(3);
2843
+ expect(pageGroup.map((e) => e.filePath)).toEqual([
2844
+ 'app/isolated-components/Foo/page.tsx',
2845
+ 'app/isolated-components/Bar/page.tsx',
2846
+ 'app/isolated-components/Baz/page.tsx',
2847
+ ]);
2848
+ });
2849
+ it('should exclude single-occurrence names', () => {
2850
+ const entries = [
2851
+ { name: 'Page', filePath: 'app/isolated-components/Foo/page.tsx' },
2852
+ { name: 'Page', filePath: 'app/isolated-components/Bar/page.tsx' },
2853
+ { name: 'Header', filePath: 'app/components/Header.tsx' },
2854
+ { name: 'Footer', filePath: 'app/components/Footer.tsx' },
2855
+ ];
2856
+ const result = detectDuplicateNames(entries);
2857
+ expect(result.size).toBe(1);
2858
+ expect(result.has('Header')).toBe(false);
2859
+ expect(result.has('Footer')).toBe(false);
2860
+ });
2861
+ it('should handle multiple duplicate groups', () => {
2862
+ const entries = [
2863
+ { name: 'Page', filePath: 'app/isolated-components/A/page.tsx' },
2864
+ { name: 'Page', filePath: 'app/isolated-components/B/page.tsx' },
2865
+ { name: 'Layout', filePath: 'app/isolated-components/A/layout.tsx' },
2866
+ { name: 'Layout', filePath: 'app/isolated-components/B/layout.tsx' },
2867
+ { name: 'Unique', filePath: 'app/components/Unique.tsx' },
2868
+ ];
2869
+ const result = detectDuplicateNames(entries);
2870
+ expect(result.size).toBe(2);
2871
+ expect(result.has('Page')).toBe(true);
2872
+ expect(result.has('Layout')).toBe(true);
2873
+ expect(result.get('Page')).toHaveLength(2);
2874
+ expect(result.get('Layout')).toHaveLength(2);
2875
+ });
2876
+ it('should return empty map for empty input', () => {
2877
+ const result = detectDuplicateNames([]);
2878
+ expect(result.size).toBe(0);
2879
+ });
2880
+ });
2881
+ // ── computeAudit: impacted components with stale scenarios ──────────
2882
+ describe('computeAudit — impacted components with stale scenarios', () => {
2883
+ it('should mark impacted component as needs_recapture when it has total scenarios but none in session', () => {
2884
+ // Library page has 3 scenarios from Feature 1 (totalScenarioCounts),
2885
+ // 0 in the current session (scenarioCounts), and is "impacted" in entityChangeStatus.
2886
+ // It should NOT be marked "missing" — it needs recapture, not new scenarios.
2887
+ const result = computeAudit({
2888
+ components: [
2889
+ {
2890
+ name: 'Library',
2891
+ filePath: 'app/library/page.tsx',
2892
+ returnType: 'JSX.Element',
2893
+ },
2894
+ {
2895
+ name: 'ArticleTable',
2896
+ filePath: 'app/components/ArticleTable.tsx',
2897
+ returnType: 'JSX.Element',
2898
+ },
2899
+ ],
2900
+ functions: [],
2901
+ scenarioCounts: { ArticleTable: 2 },
2902
+ testFileExistence: {},
2903
+ totalScenarioCounts: { Library: 3 },
2904
+ entityChangeStatus: {
2905
+ Library: { status: 'impacted' },
2906
+ ArticleTable: { status: 'edited' },
2907
+ },
2908
+ });
2909
+ // Library: impacted + has total scenarios but 0 in session → needs_recapture
2910
+ expect(result.components[0].status).toBe('needs_recapture');
2911
+ expect(result.components[0].scenarioCount).toBe(3);
2912
+ // ArticleTable: edited + has session scenarios → ok
2913
+ expect(result.components[1].status).toBe('ok');
2914
+ // needs_recapture should NOT count as missing
2915
+ expect(result.summary.componentsMissing).toBe(0);
2916
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
2917
+ // should NOT fail the audit (scenariosNeedingRecapture handles it)
2918
+ expect(result.summary.allPassing).toBe(true);
2919
+ });
2920
+ it('should still mark component as missing when impacted but has zero total scenarios', () => {
2921
+ // New page added to glossary but never had scenarios — truly missing
2922
+ const result = computeAudit({
2923
+ components: [
2924
+ {
2925
+ name: 'NewPage',
2926
+ filePath: 'app/new/page.tsx',
2927
+ returnType: 'JSX.Element',
2928
+ },
2929
+ ],
2930
+ functions: [],
2931
+ scenarioCounts: {},
2932
+ testFileExistence: {},
2933
+ totalScenarioCounts: {},
2934
+ entityChangeStatus: {
2935
+ NewPage: { status: 'impacted' },
2936
+ },
2937
+ });
2938
+ expect(result.components[0].status).toBe('missing');
2939
+ expect(result.summary.componentsMissing).toBe(1);
2940
+ });
2941
+ it('should use needs_recapture for edited entities with existing scenarios from prior sessions', () => {
2942
+ // Edited entities that already have scenarios from prior sessions
2943
+ // need recapture, not re-registration. The code changed, but the
2944
+ // scenarios exist — they just need fresh screenshots.
2945
+ const result = computeAudit({
2946
+ components: [
2947
+ {
2948
+ name: 'EditedComp',
2949
+ filePath: 'app/components/Edited.tsx',
2950
+ returnType: 'JSX.Element',
2951
+ },
2952
+ ],
2953
+ functions: [],
2954
+ scenarioCounts: {},
2955
+ testFileExistence: {},
2956
+ totalScenarioCounts: { EditedComp: 2 },
2957
+ entityChangeStatus: {
2958
+ EditedComp: { status: 'edited' },
2959
+ },
2960
+ });
2961
+ expect(result.components[0].status).toBe('needs_recapture');
2962
+ expect(result.summary.componentsMissing).toBe(0);
2963
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
2964
+ });
2965
+ it('should still mark as missing when new entity has zero total scenarios', () => {
2966
+ // Truly new component with no scenarios ever — needs scenarios created
2967
+ const result = computeAudit({
2968
+ components: [
2969
+ {
2970
+ name: 'BrandNew',
2971
+ filePath: 'app/components/BrandNew.tsx',
2972
+ returnType: 'JSX.Element',
2973
+ },
2974
+ ],
2975
+ functions: [],
2976
+ scenarioCounts: {},
2977
+ testFileExistence: {},
2978
+ totalScenarioCounts: {},
2979
+ entityChangeStatus: {
2980
+ BrandNew: { status: 'new' },
2981
+ },
2982
+ });
2983
+ expect(result.components[0].status).toBe('missing');
2984
+ expect(result.summary.componentsMissing).toBe(1);
2985
+ });
2986
+ it('should not count needs_recapture components as componentsOk', () => {
2987
+ // A needs_recapture component is not "ok" — it needs action. Counting it
2988
+ // in componentsOk is misleading: if totalComponents=2, componentsOk=2,
2989
+ // and componentsNeedingRecapture=1, the numbers don't add up (2+1 > 2).
2990
+ const result = computeAudit({
2991
+ components: [
2992
+ { name: 'Library', filePath: 'app/library/page.tsx' },
2993
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
2994
+ ],
2995
+ functions: [],
2996
+ scenarioCounts: { DrinkCard: 2 },
2997
+ testFileExistence: {},
2998
+ totalScenarioCounts: { Library: 3 },
2999
+ entityChangeStatus: { Library: { status: 'impacted' } },
3000
+ });
3001
+ expect(result.components[0].status).toBe('needs_recapture');
3002
+ expect(result.components[1].status).toBe('ok');
3003
+ // needs_recapture is not "ok" — should be counted separately
3004
+ expect(result.summary.componentsOk).toBe(1);
3005
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
3006
+ expect(result.summary.totalComponents).toBe(2);
3007
+ });
3008
+ });
3009
+ // ── queryUnassociatedScenarios ──────────────────────────────────────
3010
+ describe('queryUnassociatedScenarios', () => {
3011
+ let db;
3012
+ let rawDb;
3013
+ const projectId = 'test-project-id';
3014
+ beforeEach(async () => {
3015
+ rawDb = new Database(':memory:');
3016
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3017
+ await db.schema
3018
+ .createTable('editor_scenarios')
3019
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
3020
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
3021
+ .addColumn('name', 'varchar', (col) => col.notNull())
3022
+ .addColumn('component_name', 'varchar')
3023
+ .addColumn('component_path', 'varchar')
3024
+ .addColumn('entity_sha', 'varchar')
3025
+ .addColumn('display_name', 'varchar')
3026
+ .addColumn('page_file_path', 'varchar')
3027
+ .addColumn('url', 'varchar')
3028
+ .addColumn('type', 'varchar')
3029
+ .addColumn('created_at', 'datetime')
3030
+ .addColumn('updated_at', 'datetime')
3031
+ .execute();
3032
+ });
3033
+ afterEach(async () => {
3034
+ await db.destroy();
3035
+ });
3036
+ it('should return empty when all scenarios have entity_sha', async () => {
3037
+ await db
3038
+ .insertInto('editor_scenarios')
3039
+ .values({
3040
+ id: 'sc-1',
3041
+ project_id: projectId,
3042
+ name: 'Header - Default',
3043
+ component_name: 'Header',
3044
+ component_path: 'src/components/Header.tsx',
3045
+ entity_sha: 'sha-header',
3046
+ created_at: '2026-03-16 23:00:00',
3047
+ updated_at: '2026-03-16 23:00:00',
3048
+ })
3049
+ .execute();
3050
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3051
+ expect(result).toEqual([]);
3052
+ });
3053
+ it('should find component scenarios with NULL entity_sha', async () => {
3054
+ // This reproduces the Margo testapp bug: subagent registered scenarios
3055
+ // but entity records didn't exist yet, so entity_sha was never set
3056
+ await db
3057
+ .insertInto('editor_scenarios')
3058
+ .values({
3059
+ id: 'sc-1',
3060
+ project_id: projectId,
3061
+ name: 'SearchBar - Default',
3062
+ component_name: 'SearchBar',
3063
+ component_path: 'src/components/SearchBar.tsx',
3064
+ entity_sha: null,
3065
+ created_at: '2026-03-20 18:45:00',
3066
+ updated_at: '2026-03-20 18:45:00',
3067
+ })
3068
+ .execute();
3069
+ await db
3070
+ .insertInto('editor_scenarios')
3071
+ .values({
3072
+ id: 'sc-2',
3073
+ project_id: projectId,
3074
+ name: 'SearchBar - With Results',
3075
+ component_name: 'SearchBar',
3076
+ component_path: 'src/components/SearchBar.tsx',
3077
+ entity_sha: null,
3078
+ created_at: '2026-03-20 18:45:05',
3079
+ updated_at: '2026-03-20 18:45:05',
3080
+ })
3081
+ .execute();
3082
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3083
+ expect(result).toHaveLength(1);
3084
+ expect(result[0].name).toBe('SearchBar');
3085
+ expect(result[0].filePath).toBe('src/components/SearchBar.tsx');
3086
+ expect(result[0].scenarioCount).toBe(2);
3087
+ expect(result[0].scenarioNames).toEqual(expect.arrayContaining([
3088
+ 'SearchBar - Default',
3089
+ 'SearchBar - With Results',
3090
+ ]));
3091
+ });
3092
+ it('should find page scenarios with NULL entity_sha', async () => {
3093
+ await db
3094
+ .insertInto('editor_scenarios')
3095
+ .values({
3096
+ id: 'sc-1',
3097
+ project_id: projectId,
3098
+ name: 'Full Page — Rich Library',
3099
+ component_name: null,
3100
+ component_path: null,
3101
+ page_file_path: 'src/library/LibraryApp.tsx',
3102
+ entity_sha: null,
3103
+ created_at: '2026-03-20 18:50:00',
3104
+ updated_at: '2026-03-20 18:50:00',
3105
+ })
3106
+ .execute();
3107
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3108
+ expect(result).toHaveLength(1);
3109
+ expect(result[0].name).toBe('LibraryApp');
3110
+ expect(result[0].filePath).toBe('src/library/LibraryApp.tsx');
3111
+ expect(result[0].scenarioCount).toBe(1);
3112
+ });
3113
+ it('should ignore scenarios without any file path (orphans without component_path or page_file_path)', async () => {
3114
+ // Scenarios with no file path at all can't be associated — they're not
3115
+ // actionable, so don't report them as unassociated
3116
+ await db
3117
+ .insertInto('editor_scenarios')
3118
+ .values({
3119
+ id: 'sc-1',
3120
+ project_id: projectId,
3121
+ name: 'Some Orphan',
3122
+ component_name: null,
3123
+ component_path: null,
3124
+ page_file_path: null,
3125
+ entity_sha: null,
3126
+ created_at: '2026-03-20 18:50:00',
3127
+ updated_at: '2026-03-20 18:50:00',
3128
+ })
3129
+ .execute();
3130
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3131
+ expect(result).toEqual([]);
3132
+ });
3133
+ it('should group multiple components separately', async () => {
3134
+ // Two different components both missing entity_sha
3135
+ await db
3136
+ .insertInto('editor_scenarios')
3137
+ .values({
3138
+ id: 'sc-1',
3139
+ project_id: projectId,
3140
+ name: 'FullPageHeader - Default',
3141
+ component_name: 'FullPageHeader',
3142
+ component_path: 'src/components/FullPageHeader.tsx',
3143
+ entity_sha: null,
3144
+ created_at: '2026-03-20 18:45:00',
3145
+ updated_at: '2026-03-20 18:45:00',
3146
+ })
3147
+ .execute();
3148
+ await db
3149
+ .insertInto('editor_scenarios')
3150
+ .values({
3151
+ id: 'sc-2',
3152
+ project_id: projectId,
3153
+ name: 'SaveConfirmation - Visible',
3154
+ component_name: 'SaveConfirmation',
3155
+ component_path: 'src/components/SaveConfirmation.tsx',
3156
+ entity_sha: null,
3157
+ created_at: '2026-03-20 19:00:00',
3158
+ updated_at: '2026-03-20 19:00:00',
3159
+ })
3160
+ .execute();
3161
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3162
+ expect(result).toHaveLength(2);
3163
+ const names = result.map((r) => r.name).sort();
3164
+ expect(names).toEqual(['FullPageHeader', 'SaveConfirmation']);
3165
+ });
3166
+ it('should only include scenarios from the specified project', async () => {
3167
+ await db
3168
+ .insertInto('editor_scenarios')
3169
+ .values({
3170
+ id: 'sc-1',
3171
+ project_id: 'other-project',
3172
+ name: 'SearchBar - Default',
3173
+ component_name: 'SearchBar',
3174
+ component_path: 'src/components/SearchBar.tsx',
3175
+ entity_sha: null,
3176
+ created_at: '2026-03-20 18:45:00',
3177
+ updated_at: '2026-03-20 18:45:00',
3178
+ })
3179
+ .execute();
3180
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3181
+ expect(result).toEqual([]);
3182
+ });
3183
+ it('should scope to feature session when featureStartedAt is provided', async () => {
3184
+ // Pre-existing unassociated scenario (before session)
3185
+ await db
3186
+ .insertInto('editor_scenarios')
3187
+ .values({
3188
+ id: 'sc-old',
3189
+ project_id: projectId,
3190
+ name: 'OldComponent - Default',
3191
+ component_name: 'OldComponent',
3192
+ component_path: 'src/components/OldComponent.tsx',
3193
+ entity_sha: null,
3194
+ created_at: '2026-03-19 10:00:00',
3195
+ updated_at: '2026-03-19 10:00:00',
3196
+ })
3197
+ .execute();
3198
+ // New unassociated scenario (during session)
3199
+ await db
3200
+ .insertInto('editor_scenarios')
3201
+ .values({
3202
+ id: 'sc-new',
3203
+ project_id: projectId,
3204
+ name: 'NewComponent - Default',
3205
+ component_name: 'NewComponent',
3206
+ component_path: 'src/components/NewComponent.tsx',
3207
+ entity_sha: null,
3208
+ created_at: '2026-03-20 18:45:00',
3209
+ updated_at: '2026-03-20 18:45:00',
3210
+ })
3211
+ .execute();
3212
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3213
+ // Should only find the session-scoped one
3214
+ expect(result).toHaveLength(1);
3215
+ expect(result[0].name).toBe('NewComponent');
3216
+ });
3217
+ it('should include re-registered scenarios (updated_at in session) even if created before', async () => {
3218
+ await db
3219
+ .insertInto('editor_scenarios')
3220
+ .values({
3221
+ id: 'sc-1',
3222
+ project_id: projectId,
3223
+ name: 'SearchBar - Default',
3224
+ component_name: 'SearchBar',
3225
+ component_path: 'src/components/SearchBar.tsx',
3226
+ entity_sha: null,
3227
+ created_at: '2026-03-19 10:00:00',
3228
+ updated_at: '2026-03-20 18:45:00', // re-registered during session
3229
+ })
3230
+ .execute();
3231
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3232
+ expect(result).toHaveLength(1);
3233
+ expect(result[0].name).toBe('SearchBar');
3234
+ });
3235
+ it('should not include scenarios with entity_sha set (even if stale)', async () => {
3236
+ // This scenario has an entity_sha — even if it's stale, that's a
3237
+ // different problem (handled by queryIncompleteEntities)
3238
+ await db
3239
+ .insertInto('editor_scenarios')
3240
+ .values({
3241
+ id: 'sc-1',
3242
+ project_id: projectId,
3243
+ name: 'Header - Default',
3244
+ component_name: 'Header',
3245
+ component_path: 'src/components/Header.tsx',
3246
+ entity_sha: 'sha-old-version',
3247
+ created_at: '2026-03-20 18:45:00',
3248
+ updated_at: '2026-03-20 18:45:00',
3249
+ })
3250
+ .execute();
3251
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3252
+ expect(result).toEqual([]);
3253
+ });
3254
+ });
3255
+ // ── isAutoRemediable with unassociatedScenarios ────────────────────
3256
+ describe('isAutoRemediable always returns false (no inline full analysis)', () => {
3257
+ // Full analyze-imports takes minutes on large projects. The audit should
3258
+ // never trigger it — only the lightweight backfill path is acceptable.
3259
+ it('should return false for unassociatedScenarios only', () => {
3260
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3261
+ });
3262
+ it('should return false for incompleteEntities + unassociatedScenarios', () => {
3263
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3264
+ });
3265
+ it('should return false even with no other failures', () => {
3266
+ expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
3267
+ });
3268
+ it('should return false when already attempted', () => {
3269
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
3270
+ });
3271
+ });
3272
+ describe('suggestedTestFile for functions without testFile', () => {
3273
+ it('should suggest conventional .test.ts path when testFile is undefined', () => {
3274
+ const result = computeAudit({
3275
+ components: [],
3276
+ functions: [
3277
+ { name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
3278
+ ],
3279
+ scenarioCounts: {},
3280
+ testFileExistence: {},
3281
+ });
3282
+ expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
3283
+ expect(result.functions[0].status).toBe('missing');
3284
+ });
3285
+ it('should suggest .test.ts for .ts files', () => {
3286
+ const result = computeAudit({
3287
+ components: [],
3288
+ functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
3289
+ scenarioCounts: {},
3290
+ testFileExistence: {},
3291
+ });
3292
+ expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
3293
+ });
3294
+ it('should not set suggestedTestFile when testFile is already specified', () => {
3295
+ const result = computeAudit({
3296
+ components: [],
3297
+ functions: [
3298
+ {
3299
+ name: 'calculatePrice',
3300
+ filePath: 'app/lib/pricing.ts',
3301
+ testFile: 'app/lib/pricing.test.ts',
3302
+ },
3303
+ ],
3304
+ scenarioCounts: {},
3305
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
3306
+ });
3307
+ expect(result.functions[0].suggestedTestFile).toBeUndefined();
3308
+ });
3309
+ });
3310
+ describe('hint for function audit entries', () => {
3311
+ it('should include a hint for name_mismatch functions explaining the fix', () => {
3312
+ // Claude sees "name mismatch" with no guidance on what it means or how
3313
+ // to fix it. The hint should explain that a top-level describe block
3314
+ // matching the function name is required for the CodeYam UI.
3315
+ const result = computeAudit({
3316
+ components: [],
3317
+ functions: [
3318
+ {
3319
+ name: 'useDrinks',
3320
+ filePath: 'app/hooks/useDrinks.ts',
3321
+ testFile: 'app/hooks/useDrinks.test.ts',
3322
+ },
3323
+ ],
3324
+ scenarioCounts: {},
3325
+ testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
3326
+ testResults: {
3327
+ 'app/hooks/useDrinks.test.ts': {
3328
+ passing: true,
3329
+ hasEntityNameDescribe: false,
3330
+ },
3331
+ },
3332
+ });
3333
+ expect(result.functions[0].status).toBe('name_mismatch');
3334
+ expect(result.functions[0].hint).toBeDefined();
3335
+ expect(result.functions[0].hint).toContain('describe');
3336
+ expect(result.functions[0].hint).toContain('useDrinks');
3337
+ });
3338
+ it('should include a hint for runner_error functions showing the error', () => {
3339
+ // When the test runner crashes, Claude needs to see WHY it crashed
3340
+ // to fix the underlying issue. Without this, Claude loops re-running audit.
3341
+ const result = computeAudit({
3342
+ components: [],
3343
+ functions: [
3344
+ {
3345
+ name: 'getTimeAgo',
3346
+ filePath: 'src/lib/format.ts',
3347
+ testFile: 'src/lib/format.test.ts',
3348
+ },
3349
+ ],
3350
+ scenarioCounts: {},
3351
+ testFileExistence: { 'src/lib/format.test.ts': true },
3352
+ testResults: {
3353
+ 'src/lib/format.test.ts': {
3354
+ passing: false,
3355
+ hasEntityNameDescribe: false,
3356
+ errorMessage: 'Cannot find module "@/lib/format"',
3357
+ },
3358
+ },
3359
+ });
3360
+ expect(result.functions[0].status).toBe('runner_error');
3361
+ expect(result.functions[0].hint).toBeDefined();
3362
+ expect(result.functions[0].hint).toContain('Cannot find module');
3363
+ });
3364
+ });
3365
+ describe('hint for missing components', () => {
3366
+ it('should hint that layout files need app-level scenarios', () => {
3367
+ const result = computeAudit({
3368
+ components: [
3369
+ { name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
3370
+ ],
3371
+ functions: [],
3372
+ scenarioCounts: {},
3373
+ testFileExistence: {},
3374
+ });
3375
+ expect(result.components[0].hint).toContain('layout');
3376
+ expect(result.components[0].hint).toContain('pageFilePath');
3377
+ });
3378
+ it('should hint that page files need app-level scenarios', () => {
3379
+ const result = computeAudit({
3380
+ components: [
3381
+ { name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
3382
+ ],
3383
+ functions: [],
3384
+ scenarioCounts: {},
3385
+ testFileExistence: {},
3386
+ });
3387
+ expect(result.components[0].hint).toContain('page');
3388
+ expect(result.components[0].hint).toContain('pageFilePath');
3389
+ });
3390
+ it('should hint that regular components need isolation routes', () => {
3391
+ const result = computeAudit({
3392
+ components: [
3393
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3394
+ ],
3395
+ functions: [],
3396
+ scenarioCounts: {},
3397
+ testFileExistence: {},
3398
+ });
3399
+ expect(result.components[0].hint).toContain('isolated-components');
3400
+ });
3401
+ it('should not set hint when component has scenarios', () => {
3402
+ const result = computeAudit({
3403
+ components: [
3404
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3405
+ ],
3406
+ functions: [],
3407
+ scenarioCounts: { DrinkCard: 2 },
3408
+ testFileExistence: {},
3409
+ });
3410
+ expect(result.components[0].hint).toBeUndefined();
3411
+ });
3412
+ it('should provide a hint for needs_recapture components', () => {
3413
+ // Components with needs_recapture status need guidance on what to do.
3414
+ // Without a hint, Claude has no instructions for fixing the issue.
3415
+ const result = computeAudit({
3416
+ components: [{ name: 'Library', filePath: 'app/library/page.tsx' }],
3417
+ functions: [],
3418
+ scenarioCounts: {},
3419
+ testFileExistence: {},
3420
+ totalScenarioCounts: { Library: 3 },
3421
+ entityChangeStatus: { Library: { status: 'impacted' } },
3422
+ });
3423
+ expect(result.components[0].status).toBe('needs_recapture');
3424
+ expect(result.components[0].hint).toBeDefined();
3425
+ expect(result.components[0].hint).toContain('recapture');
3426
+ });
3427
+ });
3428
+ describe('formatIncompleteEntityGuidance', () => {
3429
+ it('should include the entity name and scenario count', () => {
3430
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3431
+ const result = formatIncompleteEntityGuidance({
3432
+ entitySha: 'abc123',
3433
+ name: 'RuleBuilder',
3434
+ scenarioCount: 5,
3435
+ preExisting: false,
3436
+ });
3437
+ expect(result).toContain('RuleBuilder');
3438
+ expect(result).toContain('5');
3439
+ });
3440
+ it('should tell Claude the exact fix command', () => {
3441
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3442
+ const result = formatIncompleteEntityGuidance({
3443
+ entitySha: 'abc123',
3444
+ name: 'RuleBuilder',
3445
+ scenarioCount: 5,
3446
+ preExisting: false,
3447
+ });
3448
+ expect(result).toContain('codeyam editor analyze-imports');
3449
+ });
3450
+ it('should flag pre-existing issues as non-blocking', () => {
3451
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3452
+ const result = formatIncompleteEntityGuidance({
3453
+ entitySha: 'abc123',
3454
+ name: 'RuleBuilder',
3455
+ scenarioCount: 5,
3456
+ preExisting: true,
3457
+ });
3458
+ expect(result).toContain('pre-existing');
3459
+ });
3460
+ it('should explain what incomplete means', () => {
3461
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3462
+ const result = formatIncompleteEntityGuidance({
3463
+ entitySha: 'abc123',
3464
+ name: 'RuleBuilder',
3465
+ scenarioCount: 5,
3466
+ preExisting: false,
3467
+ });
3468
+ // Should explain the root cause, not just the symptom
3469
+ expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
3470
+ });
3471
+ });
3472
+ describe('formatManualAnalysisGuidance', () => {
3473
+ it('should include entity name, scenario count, and error message', () => {
3474
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3475
+ const result = formatManualAnalysisGuidance({
3476
+ name: 'Header',
3477
+ filePath: 'src/components/Header.tsx',
3478
+ scenarioCount: 3,
3479
+ error: 'TypeScript parsing error: unexpected token',
3480
+ });
3481
+ expect(result).toContain('Header');
3482
+ expect(result).toContain('3 scenario(s)');
3483
+ expect(result).toContain('TypeScript parsing error');
3484
+ });
3485
+ it('should include MANUAL ANALYSIS REQUIRED header', () => {
3486
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3487
+ const result = formatManualAnalysisGuidance({
3488
+ name: 'Header',
3489
+ filePath: 'src/components/Header.tsx',
3490
+ scenarioCount: 2,
3491
+ error: 'Parse error',
3492
+ });
3493
+ expect(result).toContain('MANUAL ANALYSIS REQUIRED');
3494
+ });
3495
+ it('should tell Claude to read the source file', () => {
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('Read src/components/Header.tsx');
3504
+ });
3505
+ it('should include the manual-entity command', () => {
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('codeyam editor manual-entity');
3514
+ });
3515
+ it('should tell Claude to check glossary for imports', () => {
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('glossary');
3524
+ });
3525
+ });
3526
+ describe('getIncompleteEntityFilePaths', () => {
3527
+ // The audit should auto-fix incomplete entities by running analysis on
3528
+ // just their specific file paths, not all 117+ files. This function
3529
+ // resolves entity SHAs to file paths for targeted analysis.
3530
+ let db;
3531
+ let rawDb;
3532
+ beforeEach(async () => {
3533
+ rawDb = new Database(':memory:');
3534
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3535
+ await db.schema
3536
+ .createTable('entities')
3537
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
3538
+ .addColumn('name', 'varchar')
3539
+ .addColumn('file_path', 'varchar')
3540
+ .addColumn('project_id', 'varchar')
3541
+ .addColumn('entity_type', 'varchar')
3542
+ .execute();
3543
+ });
3544
+ afterEach(() => {
3545
+ rawDb.close();
3546
+ });
3547
+ it('should resolve entity SHAs to file paths from the entities table', async () => {
3548
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3549
+ await db
3550
+ .insertInto('entities')
3551
+ .values([
3552
+ {
3553
+ sha: 'sha-rule',
3554
+ name: 'RuleBuilder',
3555
+ file_path: 'app/components/RuleBuilder.tsx',
3556
+ project_id: 'p1',
3557
+ entity_type: 'component',
3558
+ },
3559
+ {
3560
+ sha: 'sha-row',
3561
+ name: 'ArticleTableRow',
3562
+ file_path: 'app/components/ArticleTableRow.tsx',
3563
+ project_id: 'p1',
3564
+ entity_type: 'component',
3565
+ },
3566
+ ])
3567
+ .execute();
3568
+ const result = await getIncompleteEntityFilePaths(db, [
3569
+ {
3570
+ entitySha: 'sha-rule',
3571
+ name: 'RuleBuilder',
3572
+ scenarioCount: 5,
3573
+ preExisting: false,
3574
+ },
3575
+ {
3576
+ entitySha: 'sha-row',
3577
+ name: 'ArticleTableRow',
3578
+ scenarioCount: 2,
3579
+ preExisting: false,
3580
+ },
3581
+ ]);
3582
+ expect(result).toContain('app/components/RuleBuilder.tsx');
3583
+ expect(result).toContain('app/components/ArticleTableRow.tsx');
3584
+ expect(result).toHaveLength(2);
3585
+ });
3586
+ it('should skip entities whose SHA is not in the entities table', async () => {
3587
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3588
+ const result = await getIncompleteEntityFilePaths(db, [
3589
+ {
3590
+ entitySha: 'nonexistent-sha',
3591
+ name: 'Ghost',
3592
+ scenarioCount: 1,
3593
+ preExisting: false,
3594
+ },
3595
+ ]);
3596
+ expect(result).toHaveLength(0);
3597
+ });
3598
+ it('should deduplicate file paths', async () => {
3599
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3600
+ await db
3601
+ .insertInto('entities')
3602
+ .values([
3603
+ {
3604
+ sha: 'sha-v1',
3605
+ name: 'Foo',
3606
+ file_path: 'app/Foo.tsx',
3607
+ project_id: 'p1',
3608
+ entity_type: 'component',
3609
+ },
3610
+ {
3611
+ sha: 'sha-v2',
3612
+ name: 'Foo',
3613
+ file_path: 'app/Foo.tsx',
3614
+ project_id: 'p1',
3615
+ entity_type: 'component',
3616
+ },
3617
+ ])
3618
+ .execute();
3619
+ const result = await getIncompleteEntityFilePaths(db, [
3620
+ {
3621
+ entitySha: 'sha-v1',
3622
+ name: 'Foo',
3623
+ scenarioCount: 3,
3624
+ preExisting: false,
3625
+ },
3626
+ {
3627
+ entitySha: 'sha-v2',
3628
+ name: 'Foo',
3629
+ scenarioCount: 1,
3630
+ preExisting: false,
3631
+ },
3632
+ ]);
3633
+ expect(result).toEqual(['app/Foo.tsx']);
3634
+ });
3635
+ });
3636
+ describe('isAutoRemediable never triggers full analysis', () => {
3637
+ // The audit must NEVER run handleAnalyzeImports inline — it takes minutes
3638
+ // for large projects. Auto-remediation should only do the lightweight
3639
+ // entity SHA backfill. isAutoRemediable is now always false; the callers
3640
+ // use needsBackfillOnly for the fast path instead.
3641
+ it('should always return false regardless of summary state', () => {
3642
+ expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
3643
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3644
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3645
+ });
3646
+ });
3647
+ // ── isOnlyPreExistingIncomplete ─────────────────────────────────────
3648
+ describe('isOnlyPreExistingIncomplete', () => {
3649
+ it('should return true when all incomplete entities are pre-existing and no other failures', () => {
3650
+ expect(isOnlyPreExistingIncomplete({
3651
+ incompleteEntities: 2,
3652
+ preExistingIncompleteEntities: 2,
3653
+ componentsMissing: 0,
3654
+ componentsWithErrors: 0,
3655
+ functionsFailing: 0,
3656
+ functionsNameMismatch: 0,
3657
+ functionsMissing: 0,
3658
+ missingFromGlossary: 0,
3659
+ miscategorizedScenarios: 0,
3660
+ scenariosNeedingRecapture: 0,
3661
+ }, [
3662
+ {
3663
+ entitySha: 'a',
3664
+ name: 'RuleBuilder',
3665
+ scenarioCount: 5,
3666
+ preExisting: true,
3667
+ },
3668
+ {
3669
+ entitySha: 'b',
3670
+ name: 'ArticleTableRow',
3671
+ scenarioCount: 2,
3672
+ preExisting: true,
3673
+ },
3674
+ ])).toBe(true);
3675
+ });
3676
+ it('should return false when there are runner errors even if all incompletes are pre-existing', () => {
3677
+ // Safety net: runner errors (crashed test runner) must NEVER bypass the gate.
3678
+ // Without this test, a regression removing functionsRunnerError from
3679
+ // isOnlyIncompleteEntities would silently let broken test infrastructure through.
3680
+ expect(isOnlyPreExistingIncomplete({
3681
+ incompleteEntities: 1,
3682
+ preExistingIncompleteEntities: 1,
3683
+ componentsMissing: 0,
3684
+ componentsWithErrors: 0,
3685
+ functionsFailing: 0,
3686
+ functionsRunnerError: 1,
3687
+ functionsNameMismatch: 0,
3688
+ functionsMissing: 0,
3689
+ missingFromGlossary: 0,
3690
+ }, [
3691
+ {
3692
+ entitySha: 'a',
3693
+ name: 'OldComponent',
3694
+ scenarioCount: 1,
3695
+ preExisting: true,
3696
+ },
3697
+ ])).toBe(false);
3698
+ });
3699
+ it('should return false when some incomplete entities are NOT pre-existing', () => {
3700
+ expect(isOnlyPreExistingIncomplete({
3701
+ incompleteEntities: 2,
3702
+ preExistingIncompleteEntities: 1,
3703
+ componentsMissing: 0,
3704
+ componentsWithErrors: 0,
3705
+ functionsFailing: 0,
3706
+ functionsNameMismatch: 0,
3707
+ functionsMissing: 0,
3708
+ missingFromGlossary: 0,
3709
+ miscategorizedScenarios: 0,
3710
+ scenariosNeedingRecapture: 0,
3711
+ }, [
3712
+ {
3713
+ entitySha: 'a',
3714
+ name: 'RuleBuilder',
3715
+ scenarioCount: 5,
3716
+ preExisting: true,
3717
+ },
3718
+ {
3719
+ entitySha: 'b',
3720
+ name: 'NewComponent',
3721
+ scenarioCount: 1,
3722
+ preExisting: false,
3723
+ },
3724
+ ])).toBe(false);
3725
+ });
3726
+ it('should return false when there are other failures besides incomplete entities', () => {
3727
+ expect(isOnlyPreExistingIncomplete({
3728
+ incompleteEntities: 2,
3729
+ preExistingIncompleteEntities: 2,
3730
+ componentsMissing: 1,
3731
+ }, [
3732
+ {
3733
+ entitySha: 'a',
3734
+ name: 'RuleBuilder',
3735
+ scenarioCount: 5,
3736
+ preExisting: true,
3737
+ },
3738
+ {
3739
+ entitySha: 'b',
3740
+ name: 'ArticleTableRow',
3741
+ scenarioCount: 2,
3742
+ preExisting: true,
3743
+ },
3744
+ ])).toBe(false);
3745
+ });
3746
+ it('should return false when incomplete entities array is empty', () => {
3747
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
3748
+ });
3749
+ it('should return false when incomplete entities array is missing', () => {
3750
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
3751
+ });
3752
+ });
3753
+ describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
3754
+ it('should return true when only unassociatedScenarios present', () => {
3755
+ expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
3756
+ });
3757
+ it('should return false when unassociatedScenarios present with other failures', () => {
3758
+ expect(isOnlyIncompleteEntities({
3759
+ unassociatedScenarios: 5,
3760
+ functionsMissing: 1,
3761
+ })).toBe(false);
3762
+ });
3763
+ });
3764
+ // ── aggregateClientErrorsByComponent ─────────────────────────────────
3765
+ describe('aggregateClientErrorsByComponent', () => {
3766
+ it('should attribute errors to component using componentName from metadata', () => {
3767
+ const result = aggregateClientErrorsByComponent({
3768
+ 'scenario-1': {
3769
+ scenarioName: 'DrinkCard - Default',
3770
+ errors: ['TypeError: Cannot read property "price"'],
3771
+ },
3772
+ }, [
3773
+ {
3774
+ name: 'DrinkCard - Default',
3775
+ componentName: 'DrinkCard',
3776
+ },
3777
+ ]);
3778
+ expect(result['DrinkCard']).toEqual([
3779
+ 'TypeError: Cannot read property "price"',
3780
+ ]);
3781
+ });
3782
+ it('should attribute app-level scenario errors using pageFilePath', () => {
3783
+ // App-level scenarios have componentName=null. The old approach
3784
+ // parsed the scenario name "Full Page with Library" and got
3785
+ // "Full Page with Library" as the component — which matches nothing.
3786
+ // With metadata, we derive the entity name from pageFilePath.
3787
+ const result = aggregateClientErrorsByComponent({
3788
+ 'scenario-1': {
3789
+ scenarioName: 'Full Page with Library',
3790
+ errors: ['TypeError: Cannot read property "title"'],
3791
+ },
3792
+ }, [
3793
+ {
3794
+ name: 'Full Page with Library',
3795
+ componentName: null,
3796
+ pageFilePath: 'app/library/page.tsx',
3797
+ },
3798
+ ]);
3799
+ // Should be keyed by the canonical entity name from scenarioEntityName()
3800
+ expect(result['Library']).toEqual([
3801
+ 'TypeError: Cannot read property "title"',
3802
+ ]);
3803
+ });
3804
+ it('should fall back to scenario name parsing when no metadata match exists', () => {
3805
+ // If the scenario is not in the metadata list (e.g., metadata not yet loaded),
3806
+ // fall back to the "ComponentName - Variant" convention
3807
+ const result = aggregateClientErrorsByComponent({
3808
+ 'scenario-1': {
3809
+ scenarioName: 'Header - Dark Mode',
3810
+ errors: ['ReferenceError: theme is not defined'],
3811
+ },
3812
+ }, []);
3813
+ expect(result['Header']).toEqual([
3814
+ 'ReferenceError: theme is not defined',
3815
+ ]);
3816
+ });
3817
+ it('should handle non-route file paths via scenarioEntityName', () => {
3818
+ // Non-route files (src/...) are treated as app entry points by
3819
+ // scenarioEntityName → buildRoutePattern returns '/' → 'Home'.
3820
+ // When a url is available, it provides a better entity name.
3821
+ const result = aggregateClientErrorsByComponent({
3822
+ 'scenario-1': {
3823
+ scenarioName: 'LibraryApp - Rich',
3824
+ errors: ['Error: fetch failed'],
3825
+ },
3826
+ }, [
3827
+ {
3828
+ name: 'LibraryApp - Rich',
3829
+ componentName: null,
3830
+ pageFilePath: 'src/library/LibraryApp.tsx',
3831
+ url: '/library',
3832
+ },
3833
+ ]);
3834
+ // scenarioEntityName uses url fallback when pageFilePath is a non-route file
3835
+ // pageFilePath 'src/...' → buildRoutePattern → '/' → routeDisplayName → 'Home'
3836
+ // But componentName takes priority, and with url '/library' as final fallback
3837
+ // scenarioEntityName({ pageFilePath: 'src/library/LibraryApp.tsx' }) → 'Home'
3838
+ // because pageFilePath is checked before url
3839
+ expect(result['Home']).toEqual(['Error: fetch failed']);
3840
+ });
3841
+ it('should skip scenarios with no errors', () => {
3842
+ const result = aggregateClientErrorsByComponent({
3843
+ 'scenario-1': {
3844
+ scenarioName: 'DrinkCard - Default',
3845
+ errors: [],
3846
+ },
3847
+ }, [
3848
+ {
3849
+ name: 'DrinkCard - Default',
3850
+ componentName: 'DrinkCard',
3851
+ },
3852
+ ]);
3853
+ expect(result).toEqual({});
3854
+ });
3855
+ it('should aggregate errors from multiple scenarios for same component', () => {
3856
+ const result = aggregateClientErrorsByComponent({
3857
+ 'scenario-1': {
3858
+ scenarioName: 'DrinkCard - Default',
3859
+ errors: ['Error A'],
3860
+ },
3861
+ 'scenario-2': {
3862
+ scenarioName: 'DrinkCard - Hover',
3863
+ errors: ['Error B', 'Error C'],
3864
+ },
3865
+ }, [
3866
+ { name: 'DrinkCard - Default', componentName: 'DrinkCard' },
3867
+ { name: 'DrinkCard - Hover', componentName: 'DrinkCard' },
3868
+ ]);
3869
+ expect(result['DrinkCard']).toEqual(['Error A', 'Error B', 'Error C']);
3870
+ });
3871
+ it('should use capitalized display name for page file paths, not lowercase directory', () => {
3872
+ // aggregateClientErrorsByComponent must produce keys matching scenarioEntityName().
3873
+ // scenarioEntityName({ pageFilePath: 'app/library/page.tsx' }) returns 'Library',
3874
+ // so the key must be 'Library' — not 'library' (the raw directory name).
3875
+ // computeAudit checks clientErrors[glossaryEntryName], so a lowercase key
3876
+ // will never match, silently dropping all client errors for page-level scenarios.
3877
+ const result = aggregateClientErrorsByComponent({
3878
+ 'sc-1': {
3879
+ scenarioName: 'Library - Default',
3880
+ errors: ['TypeError: fetch failed'],
3881
+ },
3882
+ }, [
3883
+ {
3884
+ name: 'Library - Default',
3885
+ componentName: null,
3886
+ pageFilePath: 'app/library/page.tsx',
3887
+ },
3888
+ ]);
3889
+ expect(result).toHaveProperty('Library');
3890
+ expect(result['Library']).toEqual(['TypeError: fetch failed']);
3891
+ });
3892
+ it('should use "Home" for root page app/page.tsx, not "app"', () => {
3893
+ // Root page: scenarioEntityName returns 'Home', not 'app'.
3894
+ // Custom path parsing incorrectly pops the parent dir ('app').
3895
+ const result = aggregateClientErrorsByComponent({
3896
+ 'sc-1': {
3897
+ scenarioName: 'Home - Default',
3898
+ errors: ['ReferenceError: window is not defined'],
3899
+ },
3900
+ }, [
3901
+ {
3902
+ name: 'Home - Default',
3903
+ componentName: null,
3904
+ pageFilePath: 'app/page.tsx',
3905
+ },
3906
+ ]);
3907
+ expect(result).toHaveProperty('Home');
3908
+ expect(result['Home']).toEqual(['ReferenceError: window is not defined']);
3909
+ });
3910
+ });
3911
+ // ── determineTargetedAnalysisPaths ──────────────────────────────────
3912
+ describe('determineTargetedAnalysisPaths', () => {
3913
+ it('should return unique file paths from unassociated scenarios', () => {
3914
+ const result = determineTargetedAnalysisPaths({
3915
+ unassociatedScenarios: [
3916
+ { filePath: 'app/components/CreateFromFiltersButton.tsx' },
3917
+ { filePath: 'app/components/StaleDismissedBanner.tsx' },
3918
+ ],
3919
+ incompleteEntities: [],
3920
+ });
3921
+ expect(result).toEqual([
3922
+ 'app/components/CreateFromFiltersButton.tsx',
3923
+ 'app/components/StaleDismissedBanner.tsx',
3924
+ ]);
3925
+ });
3926
+ it('should include file paths from incomplete entities', () => {
3927
+ const result = determineTargetedAnalysisPaths({
3928
+ unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
3929
+ incompleteEntities: [{ filePath: 'app/components/Dashboard.tsx' }],
3930
+ });
3931
+ expect(result).toEqual([
3932
+ 'app/components/TaskCard.tsx',
3933
+ 'app/components/Dashboard.tsx',
3934
+ ]);
3935
+ });
3936
+ it('should deduplicate file paths across both sources', () => {
3937
+ const result = determineTargetedAnalysisPaths({
3938
+ unassociatedScenarios: [{ filePath: 'app/components/TaskCard.tsx' }],
3939
+ incompleteEntities: [{ filePath: 'app/components/TaskCard.tsx' }],
3940
+ });
3941
+ expect(result).toEqual(['app/components/TaskCard.tsx']);
3942
+ });
3943
+ it('should return empty array when no issues', () => {
3944
+ const result = determineTargetedAnalysisPaths({
3945
+ unassociatedScenarios: [],
3946
+ incompleteEntities: [],
3947
+ });
3948
+ expect(result).toEqual([]);
3949
+ });
3950
+ it('should filter out entries with empty file paths', () => {
3951
+ const result = determineTargetedAnalysisPaths({
3952
+ unassociatedScenarios: [{ filePath: '' }],
3953
+ incompleteEntities: [],
3954
+ });
3955
+ expect(result).toEqual([]);
3956
+ });
3957
+ it('should handle incomplete entities with undefined filePath', () => {
3958
+ const result = determineTargetedAnalysisPaths({
3959
+ unassociatedScenarios: [{ filePath: 'app/components/Good.tsx' }],
3960
+ incompleteEntities: [
3961
+ {
3962
+ /* no filePath at all */
3963
+ },
3964
+ { filePath: undefined },
3965
+ { filePath: 'app/components/AlsoGood.tsx' },
3966
+ ],
3967
+ });
3968
+ expect(result).toEqual([
3969
+ 'app/components/Good.tsx',
3970
+ 'app/components/AlsoGood.tsx',
3971
+ ]);
3972
+ });
3973
+ });
3974
+ // ── shouldAutoRecapture ─────────────────────────────────────────────
3975
+ describe('shouldAutoRecapture', () => {
3976
+ it('should return true when fix flag is set and stale scenarios exist', () => {
3977
+ expect(shouldAutoRecapture({
3978
+ fix: true,
3979
+ scenariosNeedingRecapture: [
3980
+ {
3981
+ scenarioName: 'Default',
3982
+ entityName: 'TaskCard',
3983
+ status: { status: 'edited' },
3984
+ },
3985
+ ],
3986
+ })).toBe(true);
3987
+ });
3988
+ it('should return false when fix flag is not set', () => {
3989
+ expect(shouldAutoRecapture({
3990
+ fix: false,
3991
+ scenariosNeedingRecapture: [
3992
+ {
3993
+ scenarioName: 'Default',
3994
+ entityName: 'TaskCard',
3995
+ status: { status: 'edited' },
3996
+ },
3997
+ ],
3998
+ })).toBe(false);
3999
+ });
4000
+ it('should return false when fix is set but no stale scenarios', () => {
4001
+ expect(shouldAutoRecapture({
4002
+ fix: true,
4003
+ scenariosNeedingRecapture: [],
4004
+ })).toBe(false);
4005
+ });
4006
+ it('should return false when both are falsy', () => {
4007
+ expect(shouldAutoRecapture({
4008
+ fix: false,
4009
+ scenariosNeedingRecapture: [],
4010
+ })).toBe(false);
4011
+ });
4012
+ });
634
4013
  });
635
4014
  //# sourceMappingURL=editorAudit.test.js.map