@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,7 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { deduplicateByName, generateScenarioSlug, convertIsoToSqliteTimestamp, determineCaptureUrl, resolvePreviewNavPath, clearEditorState, clearEditorUserPrompt, } from "../editorScenarios.js";
4
+ import Database from 'better-sqlite3';
5
+ import { Kysely, SqliteDialect } from 'kysely';
6
+ import { deduplicateByName, generateScenarioSlug, convertIsoToSqliteTimestamp, determineCaptureUrl, resolvePreviewNavPath, clearEditorState, clearEditorUserPrompt, readDefaultScreenSize, readScreenSizes, resolveScenarioViewport, resolveViewportWithProjectDefault, upsertEditorScenario, cleanupScenarioFiles, readPreservedConfigProperties, validateStepTransition, slugifyDimension, isRowInFeatureSession, backfillEntityShaOnScenarios, countScenariosNeedingEntityBackfill, validateEntityLinkageForAppScenario, validateScenarioCategorization, resolveSeedFrom, } from "../editorScenarios.js";
5
7
  describe('editorScenarios', () => {
6
8
  describe('deduplicateByName', () => {
7
9
  it('should keep only the last item for each key', () => {
@@ -217,5 +219,1550 @@ describe('editorScenarios', () => {
217
219
  expect(captureUrl).toBe('http://localhost:4100');
218
220
  });
219
221
  });
222
+ describe('readDefaultScreenSize', () => {
223
+ let tmpDir;
224
+ beforeEach(() => {
225
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'screen-size-test-'));
226
+ fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
227
+ });
228
+ afterEach(() => {
229
+ fs.rmSync(tmpDir, { recursive: true, force: true });
230
+ });
231
+ it('should return width and height from config', () => {
232
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
233
+ defaultScreenSize: { name: 'Mobile', width: 375, height: 667 },
234
+ }));
235
+ const result = readDefaultScreenSize(tmpDir);
236
+ expect(result).toEqual({ width: 375, height: 667 });
237
+ });
238
+ it('should return null when no defaultScreenSize in config', () => {
239
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'Test' }));
240
+ const result = readDefaultScreenSize(tmpDir);
241
+ expect(result).toBeNull();
242
+ });
243
+ it('should return null when config file does not exist', () => {
244
+ const result = readDefaultScreenSize(tmpDir);
245
+ expect(result).toBeNull();
246
+ });
247
+ });
248
+ describe('resolveScenarioViewport', () => {
249
+ it('should use explicit viewport when provided', () => {
250
+ const result = resolveScenarioViewport({
251
+ bodyWidth: 800,
252
+ bodyHeight: 600,
253
+ projectDefault: { width: 375, height: 667 },
254
+ });
255
+ expect(result).toEqual({ width: 800, height: 600 });
256
+ });
257
+ it('should use project default when no explicit viewport', () => {
258
+ const result = resolveScenarioViewport({
259
+ projectDefault: { width: 375, height: 667 },
260
+ });
261
+ expect(result).toEqual({ width: 375, height: 667 });
262
+ });
263
+ it('should fall back to 1280x720 when neither is provided', () => {
264
+ const result = resolveScenarioViewport({});
265
+ expect(result).toEqual({ width: 1280, height: 720 });
266
+ });
267
+ it('should use project default for missing dimensions', () => {
268
+ const result = resolveScenarioViewport({
269
+ bodyWidth: 800,
270
+ projectDefault: { width: 375, height: 667 },
271
+ });
272
+ // bodyWidth provided, bodyHeight not — should use project default height
273
+ expect(result).toEqual({ width: 800, height: 667 });
274
+ });
275
+ });
276
+ describe('resolveViewportWithProjectDefault', () => {
277
+ let tmpDir;
278
+ beforeEach(() => {
279
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'viewport-project-default-'));
280
+ fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
281
+ });
282
+ afterEach(() => {
283
+ fs.rmSync(tmpDir, { recursive: true, force: true });
284
+ });
285
+ it('should use custom default from config.json when no explicit viewport provided', () => {
286
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
287
+ defaultScreenSize: { name: 'iPhone 15', width: 393, height: 852 },
288
+ }));
289
+ const result = resolveViewportWithProjectDefault({
290
+ codeyamRoot: tmpDir,
291
+ });
292
+ expect(result).toEqual({ width: 393, height: 852 });
293
+ });
294
+ it('should prefer explicit viewport over project default', () => {
295
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
296
+ defaultScreenSize: { name: 'iPhone 15', width: 393, height: 852 },
297
+ }));
298
+ const result = resolveViewportWithProjectDefault({
299
+ bodyWidth: 1024,
300
+ bodyHeight: 768,
301
+ codeyamRoot: tmpDir,
302
+ });
303
+ expect(result).toEqual({ width: 1024, height: 768 });
304
+ });
305
+ it('should fall back to 1280x720 when no config and no explicit viewport', () => {
306
+ const result = resolveViewportWithProjectDefault({
307
+ codeyamRoot: tmpDir,
308
+ });
309
+ expect(result).toEqual({ width: 1280, height: 720 });
310
+ });
311
+ it('should fall back to 1280x720 when config has no defaultScreenSize', () => {
312
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'Test' }));
313
+ const result = resolveViewportWithProjectDefault({
314
+ codeyamRoot: tmpDir,
315
+ });
316
+ expect(result).toEqual({ width: 1280, height: 720 });
317
+ });
318
+ it('should resolve dimension name to screenSizes entry', () => {
319
+ // Claude registers "Home - Mobile" with dimension: "Mobile"
320
+ // → resolveViewportWithProjectDefault looks up "Mobile" in screenSizes
321
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
322
+ screenSizes: {
323
+ Desktop: { width: 1440, height: 900 },
324
+ Mobile: { width: 375, height: 667 },
325
+ },
326
+ }));
327
+ const result = resolveViewportWithProjectDefault({
328
+ dimension: 'Mobile',
329
+ codeyamRoot: tmpDir,
330
+ });
331
+ expect(result).toEqual({ width: 375, height: 667 });
332
+ });
333
+ it('should prefer explicit viewport over dimension name', () => {
334
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
335
+ screenSizes: {
336
+ Mobile: { width: 375, height: 667 },
337
+ },
338
+ }));
339
+ const result = resolveViewportWithProjectDefault({
340
+ bodyWidth: 800,
341
+ bodyHeight: 600,
342
+ dimension: 'Mobile',
343
+ codeyamRoot: tmpDir,
344
+ });
345
+ expect(result).toEqual({ width: 800, height: 600 });
346
+ });
347
+ it('should prefer dimension name over project defaultScreenSize', () => {
348
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
349
+ defaultScreenSize: { name: 'Desktop', width: 1440, height: 900 },
350
+ screenSizes: {
351
+ Tablet: { width: 768, height: 1024 },
352
+ },
353
+ }));
354
+ const result = resolveViewportWithProjectDefault({
355
+ dimension: 'Tablet',
356
+ codeyamRoot: tmpDir,
357
+ });
358
+ expect(result).toEqual({ width: 768, height: 1024 });
359
+ });
360
+ it('should fall back to defaultScreenSize when dimension name not found in screenSizes', () => {
361
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
362
+ defaultScreenSize: { name: 'Desktop', width: 1440, height: 900 },
363
+ screenSizes: {
364
+ Mobile: { width: 375, height: 667 },
365
+ },
366
+ }));
367
+ const result = resolveViewportWithProjectDefault({
368
+ dimension: 'NonExistent',
369
+ codeyamRoot: tmpDir,
370
+ });
371
+ expect(result).toEqual({ width: 1440, height: 900 });
372
+ });
373
+ it('should use inherited viewport from existing scenario instead of chrome-extension project default', () => {
374
+ // BUG: When re-registering a scenario after a code change, the register
375
+ // endpoint creates a new DB record. If Claude doesn't pass viewport info,
376
+ // the resolution falls to defaultScreenSize (chrome-extension size for
377
+ // extension projects). But the original scenario had desktop viewport.
378
+ //
379
+ // FIX: The register endpoint looks up the existing scenario by name and
380
+ // passes its viewport_width/viewport_height as bodyWidth/bodyHeight,
381
+ // so the inherited desktop size takes precedence over the project default.
382
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
383
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
384
+ }));
385
+ // Without inherited values → falls to chrome-extension default
386
+ const withoutInherited = resolveViewportWithProjectDefault({
387
+ codeyamRoot: tmpDir,
388
+ });
389
+ expect(withoutInherited).toEqual({ width: 400, height: 600 });
390
+ // With inherited values from existing scenario → uses desktop size
391
+ const withInherited = resolveViewportWithProjectDefault({
392
+ bodyWidth: 1280,
393
+ bodyHeight: 720,
394
+ codeyamRoot: tmpDir,
395
+ });
396
+ expect(withInherited).toEqual({ width: 1280, height: 720 });
397
+ });
398
+ it('should use inherited dimension from existing scenario instead of project default', () => {
399
+ // Same bug but for dimension-based scenarios: the existing scenario had
400
+ // dimension "Desktop" stored, and the re-registration should inherit it.
401
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
402
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
403
+ screenSizes: {
404
+ Desktop: { width: 1440, height: 900 },
405
+ Popup: { width: 400, height: 600 },
406
+ },
407
+ }));
408
+ // Without inherited dimension → falls to project default (Popup)
409
+ const withoutInherited = resolveViewportWithProjectDefault({
410
+ codeyamRoot: tmpDir,
411
+ });
412
+ expect(withoutInherited).toEqual({ width: 400, height: 600 });
413
+ // With inherited dimension from existing scenario → resolves to Desktop
414
+ const withInherited = resolveViewportWithProjectDefault({
415
+ dimension: 'Desktop',
416
+ codeyamRoot: tmpDir,
417
+ });
418
+ expect(withInherited).toEqual({ width: 1440, height: 900 });
419
+ });
420
+ });
421
+ describe('readScreenSizes', () => {
422
+ let tmpDir;
423
+ beforeEach(() => {
424
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'screen-sizes-test-'));
425
+ fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
426
+ });
427
+ afterEach(() => {
428
+ fs.rmSync(tmpDir, { recursive: true, force: true });
429
+ });
430
+ it('should return screenSizes map from config', () => {
431
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
432
+ screenSizes: {
433
+ Desktop: { width: 1440, height: 900 },
434
+ Mobile: { width: 375, height: 667 },
435
+ Tablet: { width: 768, height: 1024 },
436
+ },
437
+ }));
438
+ const result = readScreenSizes(tmpDir);
439
+ expect(result).toEqual({
440
+ Desktop: { width: 1440, height: 900 },
441
+ Mobile: { width: 375, height: 667 },
442
+ Tablet: { width: 768, height: 1024 },
443
+ });
444
+ });
445
+ it('should return empty object when no screenSizes in config', () => {
446
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ projectTitle: 'Test' }));
447
+ const result = readScreenSizes(tmpDir);
448
+ expect(result).toEqual({});
449
+ });
450
+ it('should return empty object when config file does not exist', () => {
451
+ const result = readScreenSizes(tmpDir);
452
+ expect(result).toEqual({});
453
+ });
454
+ it('should return empty object when screenSizes is not an object', () => {
455
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ screenSizes: 'invalid' }));
456
+ const result = readScreenSizes(tmpDir);
457
+ expect(result).toEqual({});
458
+ });
459
+ });
460
+ describe('upsertEditorScenario', () => {
461
+ let db;
462
+ let rawDb;
463
+ const projectId = 'test-project-id';
464
+ beforeEach(async () => {
465
+ rawDb = new Database(':memory:');
466
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
467
+ // Create the editor_scenarios table matching the real schema
468
+ await db.schema
469
+ .createTable('editor_scenarios')
470
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
471
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
472
+ .addColumn('name', 'varchar', (col) => col.notNull())
473
+ .addColumn('description', 'text')
474
+ .addColumn('component_name', 'varchar')
475
+ .addColumn('component_path', 'varchar')
476
+ .addColumn('url', 'varchar')
477
+ .addColumn('type', 'varchar')
478
+ .addColumn('screenshot_path', 'varchar')
479
+ .addColumn('viewport_width', 'integer')
480
+ .addColumn('viewport_height', 'integer')
481
+ .addColumn('dimension', 'varchar')
482
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
483
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
484
+ .execute();
485
+ });
486
+ afterEach(async () => {
487
+ await db.destroy();
488
+ });
489
+ it('should insert a new scenario when none exists with same name', async () => {
490
+ const result = await upsertEditorScenario(db, {
491
+ projectId,
492
+ name: 'ArticleRow - Default',
493
+ description: null,
494
+ componentName: 'ArticleRow',
495
+ componentPath: 'src/components/ArticleRow.tsx',
496
+ url: '/isolated-components/ArticleRow?s=Default',
497
+ type: null,
498
+ viewportWidth: 1280,
499
+ viewportHeight: 720,
500
+ });
501
+ expect(result.isNew).toBe(true);
502
+ expect(result.cleanedUpIds).toEqual([]);
503
+ // Verify exactly 1 row in DB
504
+ const rows = await db
505
+ .selectFrom('editor_scenarios')
506
+ .selectAll()
507
+ .execute();
508
+ expect(rows).toHaveLength(1);
509
+ expect(rows[0].id).toBe(result.scenarioId);
510
+ expect(rows[0].name).toBe('ArticleRow - Default');
511
+ });
512
+ it('should update existing scenario instead of creating a duplicate', async () => {
513
+ // First registration
514
+ const first = await upsertEditorScenario(db, {
515
+ projectId,
516
+ name: 'ArticleRow - Default',
517
+ description: null,
518
+ componentName: 'ArticleRow',
519
+ componentPath: 'src/components/ArticleRow.tsx',
520
+ url: '/isolated-components/ArticleRow?s=Default',
521
+ type: null,
522
+ viewportWidth: 1280,
523
+ viewportHeight: 720,
524
+ });
525
+ // Second registration with same name
526
+ const second = await upsertEditorScenario(db, {
527
+ projectId,
528
+ name: 'ArticleRow - Default',
529
+ description: 'updated description',
530
+ componentName: 'ArticleRow',
531
+ componentPath: 'src/components/ArticleRow.tsx',
532
+ url: '/isolated-components/ArticleRow?s=Default',
533
+ type: null,
534
+ viewportWidth: 800,
535
+ viewportHeight: 600,
536
+ });
537
+ // Should reuse the same ID
538
+ expect(second.scenarioId).toBe(first.scenarioId);
539
+ expect(second.isNew).toBe(false);
540
+ // Should have exactly 1 row, not 2
541
+ const rows = await db
542
+ .selectFrom('editor_scenarios')
543
+ .selectAll()
544
+ .execute();
545
+ expect(rows).toHaveLength(1);
546
+ expect(rows[0].description).toBe('updated description');
547
+ expect(rows[0].viewport_width).toBe(800);
548
+ });
549
+ it('should clean up duplicate rows from past always-insert behavior', async () => {
550
+ // Simulate old behavior: 3 rows with the same name (accumulated duplicates)
551
+ for (let i = 0; i < 3; i++) {
552
+ await db
553
+ .insertInto('editor_scenarios')
554
+ .values({
555
+ id: `old-id-${i}`,
556
+ project_id: projectId,
557
+ name: 'ArticleRow - Default',
558
+ url: '/isolated-components/ArticleRow?s=Default',
559
+ viewport_width: 1280,
560
+ viewport_height: 720,
561
+ created_at: new Date(Date.now() + i * 1000).toISOString(),
562
+ })
563
+ .execute();
564
+ }
565
+ // Now upsert — should consolidate to 1 row
566
+ const result = await upsertEditorScenario(db, {
567
+ projectId,
568
+ name: 'ArticleRow - Default',
569
+ description: null,
570
+ componentName: 'ArticleRow',
571
+ componentPath: 'src/components/ArticleRow.tsx',
572
+ url: '/isolated-components/ArticleRow?s=Default',
573
+ type: null,
574
+ viewportWidth: 1280,
575
+ viewportHeight: 720,
576
+ });
577
+ expect(result.isNew).toBe(false);
578
+ // Should return the IDs of cleaned-up duplicates
579
+ expect(result.cleanedUpIds).toHaveLength(2);
580
+ // Should have exactly 1 row remaining
581
+ const rows = await db
582
+ .selectFrom('editor_scenarios')
583
+ .selectAll()
584
+ .execute();
585
+ expect(rows).toHaveLength(1);
586
+ expect(rows[0].id).toBe(result.scenarioId);
587
+ });
588
+ it('should not conflict with scenarios of different names', async () => {
589
+ await upsertEditorScenario(db, {
590
+ projectId,
591
+ name: 'ArticleRow - Default',
592
+ description: null,
593
+ componentName: 'ArticleRow',
594
+ componentPath: 'src/components/ArticleRow.tsx',
595
+ url: '/isolated-components/ArticleRow?s=Default',
596
+ type: null,
597
+ viewportWidth: 1280,
598
+ viewportHeight: 720,
599
+ });
600
+ await upsertEditorScenario(db, {
601
+ projectId,
602
+ name: 'ArticleCard - Default',
603
+ description: null,
604
+ componentName: 'ArticleCard',
605
+ componentPath: 'src/components/ArticleCard.tsx',
606
+ url: '/isolated-components/ArticleCard?s=Default',
607
+ type: null,
608
+ viewportWidth: 1280,
609
+ viewportHeight: 720,
610
+ });
611
+ const rows = await db
612
+ .selectFrom('editor_scenarios')
613
+ .selectAll()
614
+ .execute();
615
+ expect(rows).toHaveLength(2);
616
+ });
617
+ it('should scope upsert to the same project — different projects can have same scenario name', async () => {
618
+ const result1 = await upsertEditorScenario(db, {
619
+ projectId: 'project-a',
620
+ name: 'Home - Default',
621
+ description: null,
622
+ componentName: null,
623
+ componentPath: null,
624
+ url: '/',
625
+ type: 'application',
626
+ viewportWidth: 1280,
627
+ viewportHeight: 720,
628
+ });
629
+ const result2 = await upsertEditorScenario(db, {
630
+ projectId: 'project-b',
631
+ name: 'Home - Default',
632
+ description: null,
633
+ componentName: null,
634
+ componentPath: null,
635
+ url: '/',
636
+ type: 'application',
637
+ viewportWidth: 1280,
638
+ viewportHeight: 720,
639
+ });
640
+ expect(result1.scenarioId).not.toBe(result2.scenarioId);
641
+ expect(result1.isNew).toBe(true);
642
+ expect(result2.isNew).toBe(true);
643
+ const rows = await db
644
+ .selectFrom('editor_scenarios')
645
+ .selectAll()
646
+ .execute();
647
+ expect(rows).toHaveLength(2);
648
+ });
649
+ });
650
+ describe('cleanupScenarioFiles', () => {
651
+ let tmpDir;
652
+ let scenariosDir;
653
+ let screenshotsDir;
654
+ beforeEach(() => {
655
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scenario-cleanup-'));
656
+ scenariosDir = path.join(tmpDir, '.codeyam', 'editor-scenarios');
657
+ screenshotsDir = path.join(scenariosDir, 'screenshots');
658
+ fs.mkdirSync(screenshotsDir, { recursive: true });
659
+ });
660
+ afterEach(() => {
661
+ fs.rmSync(tmpDir, { recursive: true, force: true });
662
+ });
663
+ it('should delete .json, .seed.json, and screenshot files for given IDs', () => {
664
+ // Create files for two scenario IDs
665
+ fs.writeFileSync(path.join(scenariosDir, 'old-id-1.json'), '{"mock":"data"}');
666
+ fs.writeFileSync(path.join(scenariosDir, 'old-id-1.seed.json'), '{"seed":"data"}');
667
+ fs.writeFileSync(path.join(screenshotsDir, 'old-id-1.png'), 'fake-png-data');
668
+ fs.writeFileSync(path.join(scenariosDir, 'old-id-2.json'), '{"mock":"data2"}');
669
+ cleanupScenarioFiles(tmpDir, ['old-id-1', 'old-id-2']);
670
+ expect(fs.existsSync(path.join(scenariosDir, 'old-id-1.json'))).toBe(false);
671
+ expect(fs.existsSync(path.join(scenariosDir, 'old-id-1.seed.json'))).toBe(false);
672
+ expect(fs.existsSync(path.join(screenshotsDir, 'old-id-1.png'))).toBe(false);
673
+ expect(fs.existsSync(path.join(scenariosDir, 'old-id-2.json'))).toBe(false);
674
+ });
675
+ it('should not throw when files do not exist', () => {
676
+ expect(() => cleanupScenarioFiles(tmpDir, ['nonexistent-id'])).not.toThrow();
677
+ });
678
+ it('should not delete files for unrelated scenario IDs', () => {
679
+ fs.writeFileSync(path.join(scenariosDir, 'keep-me.json'), '{"keep":"this"}');
680
+ cleanupScenarioFiles(tmpDir, ['delete-me']);
681
+ expect(fs.existsSync(path.join(scenariosDir, 'keep-me.json'))).toBe(true);
682
+ });
683
+ });
684
+ describe('readPreservedConfigProperties', () => {
685
+ let tmpDir;
686
+ beforeEach(() => {
687
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-preserve-'));
688
+ fs.mkdirSync(path.join(tmpDir, '.codeyam'), { recursive: true });
689
+ });
690
+ afterEach(() => {
691
+ fs.rmSync(tmpDir, { recursive: true, force: true });
692
+ });
693
+ it('should preserve defaultScreenSize from existing config', () => {
694
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
695
+ fs.writeFileSync(configPath, JSON.stringify({
696
+ projectSlug: 'my-project',
697
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
698
+ webapps: [],
699
+ }));
700
+ const preserved = readPreservedConfigProperties(configPath);
701
+ expect(preserved.defaultScreenSize).toEqual({
702
+ name: 'Popup',
703
+ width: 400,
704
+ height: 600,
705
+ });
706
+ });
707
+ it('should preserve screenSizes from existing config', () => {
708
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
709
+ fs.writeFileSync(configPath, JSON.stringify({
710
+ projectSlug: 'my-project',
711
+ screenSizes: {
712
+ Desktop: { width: 1440, height: 900 },
713
+ Mobile: { width: 375, height: 667 },
714
+ },
715
+ }));
716
+ const preserved = readPreservedConfigProperties(configPath);
717
+ expect(preserved.screenSizes).toEqual({
718
+ Desktop: { width: 1440, height: 900 },
719
+ Mobile: { width: 375, height: 667 },
720
+ });
721
+ });
722
+ it('should preserve projectTitle and projectDescription', () => {
723
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
724
+ fs.writeFileSync(configPath, JSON.stringify({
725
+ projectSlug: 'my-project',
726
+ projectTitle: 'My Chrome Extension',
727
+ projectDescription: 'A cool extension',
728
+ }));
729
+ const preserved = readPreservedConfigProperties(configPath);
730
+ expect(preserved.projectTitle).toBe('My Chrome Extension');
731
+ expect(preserved.projectDescription).toBe('A cool extension');
732
+ });
733
+ it('should preserve all user-configured properties at once', () => {
734
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
735
+ fs.writeFileSync(configPath, JSON.stringify({
736
+ projectSlug: 'my-project',
737
+ packageManager: 'npm',
738
+ webapps: [],
739
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
740
+ screenSizes: { Desktop: { width: 1440, height: 900 } },
741
+ projectTitle: 'My Chrome Extension',
742
+ projectDescription: 'A cool extension',
743
+ }));
744
+ const preserved = readPreservedConfigProperties(configPath);
745
+ expect(preserved).toEqual({
746
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
747
+ screenSizes: { Desktop: { width: 1440, height: 900 } },
748
+ projectTitle: 'My Chrome Extension',
749
+ projectDescription: 'A cool extension',
750
+ });
751
+ });
752
+ it('should NOT preserve auto-detected properties like projectSlug and webapps', () => {
753
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
754
+ fs.writeFileSync(configPath, JSON.stringify({
755
+ projectSlug: 'my-project',
756
+ packageManager: 'npm',
757
+ webapps: [{ path: '.', framework: 'next' }],
758
+ createdAt: '2026-01-01',
759
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
760
+ }));
761
+ const preserved = readPreservedConfigProperties(configPath);
762
+ // Only user-configured properties should survive
763
+ expect(preserved).toEqual({
764
+ defaultScreenSize: { name: 'Popup', width: 400, height: 600 },
765
+ });
766
+ expect(preserved).not.toHaveProperty('projectSlug');
767
+ expect(preserved).not.toHaveProperty('packageManager');
768
+ expect(preserved).not.toHaveProperty('webapps');
769
+ expect(preserved).not.toHaveProperty('createdAt');
770
+ });
771
+ it('should return empty object when config does not exist', () => {
772
+ const configPath = path.join(tmpDir, '.codeyam', 'nonexistent.json');
773
+ const preserved = readPreservedConfigProperties(configPath);
774
+ expect(preserved).toEqual({});
775
+ });
776
+ it('should return empty object when config has no preservable properties', () => {
777
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
778
+ fs.writeFileSync(configPath, JSON.stringify({
779
+ projectSlug: 'my-project',
780
+ webapps: [],
781
+ }));
782
+ const preserved = readPreservedConfigProperties(configPath);
783
+ expect(preserved).toEqual({});
784
+ });
785
+ it('should return empty object when config is invalid JSON', () => {
786
+ const configPath = path.join(tmpDir, '.codeyam', 'config.json');
787
+ fs.writeFileSync(configPath, 'not valid json');
788
+ const preserved = readPreservedConfigProperties(configPath);
789
+ expect(preserved).toEqual({});
790
+ });
791
+ });
792
+ describe('slugifyDimension', () => {
793
+ it('should lowercase and replace spaces with hyphens', () => {
794
+ expect(slugifyDimension('Extension Popup')).toBe('extension-popup');
795
+ });
796
+ it('should strip non-alphanumeric characters', () => {
797
+ expect(slugifyDimension('Desktop (Large)')).toBe('desktop-large');
798
+ });
799
+ it('should collapse multiple hyphens', () => {
800
+ expect(slugifyDimension('My -- Screen')).toBe('my-screen');
801
+ });
802
+ it('should handle simple names', () => {
803
+ expect(slugifyDimension('Desktop')).toBe('desktop');
804
+ expect(slugifyDimension('Mobile')).toBe('mobile');
805
+ });
806
+ it('should handle empty string', () => {
807
+ expect(slugifyDimension('')).toBe('');
808
+ });
809
+ it('should trim leading/trailing hyphens', () => {
810
+ expect(slugifyDimension(' Mobile ')).toBe('mobile');
811
+ });
812
+ });
813
+ describe('upsertEditorScenario with dimensions', () => {
814
+ let db;
815
+ let rawDb;
816
+ const projectId = 'test-project-id';
817
+ beforeEach(async () => {
818
+ rawDb = new Database(':memory:');
819
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
820
+ await db.schema
821
+ .createTable('editor_scenarios')
822
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
823
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
824
+ .addColumn('name', 'varchar', (col) => col.notNull())
825
+ .addColumn('description', 'text')
826
+ .addColumn('component_name', 'varchar')
827
+ .addColumn('component_path', 'varchar')
828
+ .addColumn('url', 'varchar')
829
+ .addColumn('type', 'varchar')
830
+ .addColumn('screenshot_path', 'varchar')
831
+ .addColumn('viewport_width', 'integer')
832
+ .addColumn('viewport_height', 'integer')
833
+ .addColumn('dimension', 'varchar')
834
+ .addColumn('dimensions', 'text')
835
+ .addColumn('screenshot_paths', 'text')
836
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
837
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
838
+ .execute();
839
+ });
840
+ afterEach(async () => {
841
+ await db.destroy();
842
+ });
843
+ it('should store dimensions as JSON array', async () => {
844
+ const result = await upsertEditorScenario(db, {
845
+ projectId,
846
+ name: 'Home Page',
847
+ description: null,
848
+ componentName: null,
849
+ componentPath: null,
850
+ url: '/',
851
+ type: 'application',
852
+ viewportWidth: 1440,
853
+ viewportHeight: 900,
854
+ dimensions: ['Desktop', 'Mobile'],
855
+ screenshotPaths: null,
856
+ });
857
+ const row = await db
858
+ .selectFrom('editor_scenarios')
859
+ .selectAll()
860
+ .where('id', '=', result.scenarioId)
861
+ .executeTakeFirst();
862
+ expect(row).toBeDefined();
863
+ expect(JSON.parse(row.dimensions)).toEqual([
864
+ 'Desktop',
865
+ 'Mobile',
866
+ ]);
867
+ });
868
+ it('should store screenshot_paths as JSON object', async () => {
869
+ const screenshotPaths = {
870
+ Desktop: 'screenshots/abc--desktop.png',
871
+ Mobile: 'screenshots/abc--mobile.png',
872
+ };
873
+ const result = await upsertEditorScenario(db, {
874
+ projectId,
875
+ name: 'Home Page',
876
+ description: null,
877
+ componentName: null,
878
+ componentPath: null,
879
+ url: '/',
880
+ type: 'application',
881
+ viewportWidth: 1440,
882
+ viewportHeight: 900,
883
+ dimensions: ['Desktop', 'Mobile'],
884
+ screenshotPaths,
885
+ });
886
+ const row = await db
887
+ .selectFrom('editor_scenarios')
888
+ .selectAll()
889
+ .where('id', '=', result.scenarioId)
890
+ .executeTakeFirst();
891
+ expect(JSON.parse(row.screenshot_paths)).toEqual(screenshotPaths);
892
+ });
893
+ it('should update dimensions and screenshot_paths on re-registration', async () => {
894
+ const first = await upsertEditorScenario(db, {
895
+ projectId,
896
+ name: 'Home Page',
897
+ description: null,
898
+ componentName: null,
899
+ componentPath: null,
900
+ url: '/',
901
+ type: 'application',
902
+ viewportWidth: 1440,
903
+ viewportHeight: 900,
904
+ dimensions: ['Desktop'],
905
+ screenshotPaths: { Desktop: 'screenshots/old--desktop.png' },
906
+ });
907
+ const second = await upsertEditorScenario(db, {
908
+ projectId,
909
+ name: 'Home Page',
910
+ description: null,
911
+ componentName: null,
912
+ componentPath: null,
913
+ url: '/',
914
+ type: 'application',
915
+ viewportWidth: 1440,
916
+ viewportHeight: 900,
917
+ dimensions: ['Desktop', 'Mobile'],
918
+ screenshotPaths: {
919
+ Desktop: 'screenshots/new--desktop.png',
920
+ Mobile: 'screenshots/new--mobile.png',
921
+ },
922
+ });
923
+ expect(second.scenarioId).toBe(first.scenarioId);
924
+ const row = await db
925
+ .selectFrom('editor_scenarios')
926
+ .selectAll()
927
+ .where('id', '=', second.scenarioId)
928
+ .executeTakeFirst();
929
+ expect(JSON.parse(row.dimensions)).toEqual([
930
+ 'Desktop',
931
+ 'Mobile',
932
+ ]);
933
+ expect(JSON.parse(row.screenshot_paths)).toEqual({
934
+ Desktop: 'screenshots/new--desktop.png',
935
+ Mobile: 'screenshots/new--mobile.png',
936
+ });
937
+ });
938
+ it('should handle null dimensions gracefully (backward compat)', async () => {
939
+ const result = await upsertEditorScenario(db, {
940
+ projectId,
941
+ name: 'Legacy Scenario',
942
+ description: null,
943
+ componentName: null,
944
+ componentPath: null,
945
+ url: '/',
946
+ type: null,
947
+ viewportWidth: 1280,
948
+ viewportHeight: 720,
949
+ });
950
+ const row = await db
951
+ .selectFrom('editor_scenarios')
952
+ .selectAll()
953
+ .where('id', '=', result.scenarioId)
954
+ .executeTakeFirst();
955
+ expect(row.dimensions).toBeNull();
956
+ expect(row.screenshot_paths).toBeNull();
957
+ });
958
+ });
959
+ describe('validateStepTransition', () => {
960
+ it('should allow step 1 with no current state', () => {
961
+ expect(validateStepTransition(1, null)).toBeNull();
962
+ });
963
+ it('should allow step 1 even when on a later step (restart)', () => {
964
+ expect(validateStepTransition(1, 5)).toBeNull();
965
+ });
966
+ it('should allow advancing to the next step', () => {
967
+ expect(validateStepTransition(2, 1)).toBeNull();
968
+ expect(validateStepTransition(3, 2)).toBeNull();
969
+ expect(validateStepTransition(13, 12)).toBeNull();
970
+ });
971
+ it('should allow re-running the current step', () => {
972
+ expect(validateStepTransition(3, 3)).toBeNull();
973
+ expect(validateStepTransition(7, 7)).toBeNull();
974
+ });
975
+ it('should allow going back to a previous step', () => {
976
+ expect(validateStepTransition(3, 5)).toBeNull();
977
+ });
978
+ it('should reject skipping steps forward', () => {
979
+ const err = validateStepTransition(5, 2);
980
+ expect(err).not.toBeNull();
981
+ expect(err).toContain('step 2');
982
+ expect(err).toContain('step 5');
983
+ });
984
+ it('should reject jumping to step 13 from step 3', () => {
985
+ const err = validateStepTransition(13, 3);
986
+ expect(err).not.toBeNull();
987
+ expect(err).toContain('step 4');
988
+ });
989
+ it('should reject step 2 with no current state', () => {
990
+ const err = validateStepTransition(2, null);
991
+ expect(err).not.toBeNull();
992
+ expect(err).toContain('step 1');
993
+ });
994
+ it('should reject step 13 with no current state', () => {
995
+ const err = validateStepTransition(13, null);
996
+ expect(err).not.toBeNull();
997
+ });
998
+ it('should allow advancing from step 13 to step 14', () => {
999
+ expect(validateStepTransition(14, 13)).toBeNull();
1000
+ });
1001
+ it('should allow advancing from step 14 to step 15', () => {
1002
+ expect(validateStepTransition(15, 14)).toBeNull();
1003
+ });
1004
+ it('should allow advancing from step 15 to step 16', () => {
1005
+ expect(validateStepTransition(16, 15)).toBeNull();
1006
+ });
1007
+ it('should reject skipping from step 13 to step 15', () => {
1008
+ const err = validateStepTransition(15, 13);
1009
+ expect(err).not.toBeNull();
1010
+ expect(err).toContain('step 14');
1011
+ });
1012
+ it('should reject rapid-fire advance when step started < 3s ago', () => {
1013
+ const recentStart = new Date(Date.now() - 1000).toISOString(); // 1s ago
1014
+ const err = validateStepTransition(4, 3, recentStart);
1015
+ expect(err).not.toBeNull();
1016
+ expect(err).toContain('only 1s ago');
1017
+ expect(err).toContain('checklist');
1018
+ });
1019
+ it('should allow advance when step started > 3s ago', () => {
1020
+ const oldStart = new Date(Date.now() - 5000).toISOString(); // 5s ago
1021
+ expect(validateStepTransition(4, 3, oldStart)).toBeNull();
1022
+ });
1023
+ it('should not enforce time guard on step 1 → 2 transition', () => {
1024
+ const recentStart = new Date(Date.now() - 2000).toISOString();
1025
+ // currentStep=1, so guard only applies for currentStep >= 2
1026
+ expect(validateStepTransition(2, 1, recentStart)).toBeNull();
1027
+ });
1028
+ it('should not enforce time guard when re-running the same step', () => {
1029
+ const recentStart = new Date(Date.now() - 2000).toISOString();
1030
+ expect(validateStepTransition(5, 5, recentStart)).toBeNull();
1031
+ });
1032
+ });
1033
+ describe('isRowInFeatureSession', () => {
1034
+ const featureStart = '2026-03-12 14:01:31';
1035
+ it('should include row created after feature start', () => {
1036
+ expect(isRowInFeatureSession({ created_at: '2026-03-12 14:30:00' }, featureStart)).toBe(true);
1037
+ });
1038
+ it('should exclude row created before feature start with no updated_at', () => {
1039
+ expect(isRowInFeatureSession({ created_at: '2026-03-12 13:00:00' }, featureStart)).toBe(false);
1040
+ });
1041
+ it('should include re-registered row with old created_at but recent updated_at', () => {
1042
+ // This is the bug: application-level scenarios from a previous feature
1043
+ // are re-registered, which only updates updated_at. The session filter
1044
+ // must check updated_at too, otherwise these scenarios vanish from results.
1045
+ expect(isRowInFeatureSession({
1046
+ created_at: '2026-03-12 13:28:00',
1047
+ updated_at: '2026-03-12 14:32:00',
1048
+ }, featureStart)).toBe(true);
1049
+ });
1050
+ it('should exclude row with both timestamps before feature start', () => {
1051
+ expect(isRowInFeatureSession({
1052
+ created_at: '2026-03-12 13:00:00',
1053
+ updated_at: '2026-03-12 13:30:00',
1054
+ }, featureStart)).toBe(false);
1055
+ });
1056
+ it('should handle null created_at with recent updated_at', () => {
1057
+ expect(isRowInFeatureSession({ created_at: null, updated_at: '2026-03-12 14:32:00' }, featureStart)).toBe(true);
1058
+ });
1059
+ it('should handle null updated_at with recent created_at', () => {
1060
+ expect(isRowInFeatureSession({ created_at: '2026-03-12 14:30:00', updated_at: null }, featureStart)).toBe(true);
1061
+ });
1062
+ });
1063
+ describe('upsertEditorScenario with entitySha and displayName', () => {
1064
+ let db;
1065
+ let rawDb;
1066
+ const projectId = 'test-project-id';
1067
+ beforeEach(async () => {
1068
+ rawDb = new Database(':memory:');
1069
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1070
+ await db.schema
1071
+ .createTable('editor_scenarios')
1072
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1073
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1074
+ .addColumn('name', 'varchar', (col) => col.notNull())
1075
+ .addColumn('description', 'text')
1076
+ .addColumn('component_name', 'varchar')
1077
+ .addColumn('component_path', 'varchar')
1078
+ .addColumn('url', 'varchar')
1079
+ .addColumn('type', 'varchar')
1080
+ .addColumn('screenshot_path', 'varchar')
1081
+ .addColumn('viewport_width', 'integer')
1082
+ .addColumn('viewport_height', 'integer')
1083
+ .addColumn('dimension', 'varchar')
1084
+ .addColumn('dimensions', 'text')
1085
+ .addColumn('screenshot_paths', 'text')
1086
+ .addColumn('page_file_path', 'varchar')
1087
+ .addColumn('entity_sha', 'varchar')
1088
+ .addColumn('display_name', 'varchar')
1089
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1090
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1091
+ .execute();
1092
+ });
1093
+ afterEach(async () => {
1094
+ await db.destroy();
1095
+ });
1096
+ it('should store entitySha and displayName on insert', async () => {
1097
+ const result = await upsertEditorScenario(db, {
1098
+ projectId,
1099
+ name: 'Home - Default',
1100
+ description: null,
1101
+ componentName: null,
1102
+ componentPath: null,
1103
+ url: '/',
1104
+ type: 'application',
1105
+ viewportWidth: 400,
1106
+ viewportHeight: 600,
1107
+ entitySha: 'abc123',
1108
+ displayName: 'Home',
1109
+ });
1110
+ expect(result.isNew).toBe(true);
1111
+ const row = rawDb
1112
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1113
+ .get(result.scenarioId);
1114
+ expect(row.entity_sha).toBe('abc123');
1115
+ expect(row.display_name).toBe('Home');
1116
+ });
1117
+ it('should update entitySha and displayName on upsert', async () => {
1118
+ // Insert first
1119
+ await upsertEditorScenario(db, {
1120
+ projectId,
1121
+ name: 'Home - Default',
1122
+ description: null,
1123
+ componentName: null,
1124
+ componentPath: null,
1125
+ url: '/',
1126
+ type: 'application',
1127
+ viewportWidth: 400,
1128
+ viewportHeight: 600,
1129
+ entitySha: 'old-sha',
1130
+ displayName: 'OldName',
1131
+ });
1132
+ // Upsert with new values
1133
+ const result = await upsertEditorScenario(db, {
1134
+ projectId,
1135
+ name: 'Home - Default',
1136
+ description: null,
1137
+ componentName: null,
1138
+ componentPath: null,
1139
+ url: '/',
1140
+ type: 'application',
1141
+ viewportWidth: 400,
1142
+ viewportHeight: 600,
1143
+ entitySha: 'new-sha',
1144
+ displayName: 'Home',
1145
+ });
1146
+ expect(result.isNew).toBe(false);
1147
+ const row = rawDb
1148
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1149
+ .get(result.scenarioId);
1150
+ expect(row.entity_sha).toBe('new-sha');
1151
+ expect(row.display_name).toBe('Home');
1152
+ });
1153
+ it('should not overwrite entitySha when not provided', async () => {
1154
+ // Insert with SHA
1155
+ const first = await upsertEditorScenario(db, {
1156
+ projectId,
1157
+ name: 'Home - Default',
1158
+ description: null,
1159
+ componentName: null,
1160
+ componentPath: null,
1161
+ url: '/',
1162
+ type: 'application',
1163
+ viewportWidth: 400,
1164
+ viewportHeight: 600,
1165
+ entitySha: 'keep-this',
1166
+ displayName: 'Home',
1167
+ });
1168
+ // Upsert without entitySha/displayName
1169
+ await upsertEditorScenario(db, {
1170
+ projectId,
1171
+ name: 'Home - Default',
1172
+ description: 'updated',
1173
+ componentName: null,
1174
+ componentPath: null,
1175
+ url: '/',
1176
+ type: 'application',
1177
+ viewportWidth: 400,
1178
+ viewportHeight: 600,
1179
+ });
1180
+ const row = rawDb
1181
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1182
+ .get(first.scenarioId);
1183
+ expect(row.entity_sha).toBe('keep-this');
1184
+ expect(row.display_name).toBe('Home');
1185
+ });
1186
+ });
1187
+ describe('backfillEntityShaOnScenarios', () => {
1188
+ let db;
1189
+ let rawDb;
1190
+ const projectId = 'test-project-id';
1191
+ beforeEach(async () => {
1192
+ rawDb = new Database(':memory:');
1193
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1194
+ await db.schema
1195
+ .createTable('editor_scenarios')
1196
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1197
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1198
+ .addColumn('name', 'varchar', (col) => col.notNull())
1199
+ .addColumn('description', 'text')
1200
+ .addColumn('component_name', 'varchar')
1201
+ .addColumn('component_path', 'varchar')
1202
+ .addColumn('url', 'varchar')
1203
+ .addColumn('type', 'varchar')
1204
+ .addColumn('screenshot_path', 'varchar')
1205
+ .addColumn('viewport_width', 'integer')
1206
+ .addColumn('viewport_height', 'integer')
1207
+ .addColumn('dimension', 'varchar')
1208
+ .addColumn('dimensions', 'text')
1209
+ .addColumn('screenshot_paths', 'text')
1210
+ .addColumn('page_file_path', 'varchar')
1211
+ .addColumn('entity_sha', 'varchar')
1212
+ .addColumn('display_name', 'varchar')
1213
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1214
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1215
+ .execute();
1216
+ });
1217
+ afterEach(async () => {
1218
+ await db.destroy();
1219
+ });
1220
+ it('should backfill component scenario by component_path match', async () => {
1221
+ // Insert a scenario with null entity_sha but with component_path
1222
+ rawDb
1223
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1224
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1225
+ .run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720);
1226
+ const result = await backfillEntityShaOnScenarios(db, [
1227
+ {
1228
+ sha: 'entity-sha-1',
1229
+ name: 'Header',
1230
+ filePath: 'src/components/Header.tsx',
1231
+ },
1232
+ ]);
1233
+ expect(result.updated).toBe(1);
1234
+ const row = rawDb
1235
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1236
+ .get('sc-1');
1237
+ expect(row.entity_sha).toBe('entity-sha-1');
1238
+ expect(row.display_name).toBe('Header');
1239
+ });
1240
+ it('should backfill page scenario by page_file_path match and derive display_name from route', async () => {
1241
+ // Insert a scenario with null entity_sha but with page_file_path
1242
+ rawDb
1243
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1244
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1245
+ .run('sc-2', projectId, 'Feedback - Default', 'app/feedback/page.tsx', '/feedback', 'application', 1280, 720);
1246
+ const result = await backfillEntityShaOnScenarios(db, [
1247
+ {
1248
+ sha: 'entity-sha-2',
1249
+ name: 'Feedback',
1250
+ filePath: 'app/feedback/page.tsx',
1251
+ },
1252
+ ]);
1253
+ expect(result.updated).toBe(1);
1254
+ const row = rawDb
1255
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1256
+ .get('sc-2');
1257
+ expect(row.entity_sha).toBe('entity-sha-2');
1258
+ // display_name derived from page_file_path via routeDisplayName(buildRoutePattern())
1259
+ expect(row.display_name).toBe('Feedback');
1260
+ });
1261
+ it('should update scenarios with stale entity_sha to latest', async () => {
1262
+ // Margo bug: entity code changes create new versions with new SHAs,
1263
+ // but scenarios still point to the old SHA. Since journals preserve
1264
+ // screenshots and everything is in git, we always want scenarios
1265
+ // pointing to the latest entity version.
1266
+ rawDb
1267
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1268
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1269
+ .run('sc-3', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'old-sha', 'Header');
1270
+ const result = await backfillEntityShaOnScenarios(db, [
1271
+ {
1272
+ sha: 'latest-sha',
1273
+ name: 'Header',
1274
+ filePath: 'src/components/Header.tsx',
1275
+ },
1276
+ ]);
1277
+ expect(result.updated).toBe(1);
1278
+ const row = rawDb
1279
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1280
+ .get('sc-3');
1281
+ expect(row.entity_sha).toBe('latest-sha');
1282
+ });
1283
+ it('should not update scenarios already pointing to the latest SHA', async () => {
1284
+ rawDb
1285
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1286
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1287
+ .run('sc-3b', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'current-sha', 'Header');
1288
+ const result = await backfillEntityShaOnScenarios(db, [
1289
+ {
1290
+ sha: 'current-sha',
1291
+ name: 'Header',
1292
+ filePath: 'src/components/Header.tsx',
1293
+ },
1294
+ ]);
1295
+ expect(result.updated).toBe(0);
1296
+ });
1297
+ it('should skip scenarios with no matching entity', async () => {
1298
+ rawDb
1299
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1300
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1301
+ .run('sc-4', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
1302
+ // Entities list doesn't include Footer.tsx
1303
+ const result = await backfillEntityShaOnScenarios(db, [
1304
+ {
1305
+ sha: 'entity-sha-1',
1306
+ name: 'Header',
1307
+ filePath: 'src/components/Header.tsx',
1308
+ },
1309
+ ]);
1310
+ expect(result.updated).toBe(0);
1311
+ const row = rawDb
1312
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1313
+ .get('sc-4');
1314
+ expect(row.entity_sha).toBeNull();
1315
+ });
1316
+ it('should match component scenarios by name+path, not just path (multi-entity files)', async () => {
1317
+ // A single file can export multiple components (e.g., FullPageLibrary.tsx
1318
+ // exports FullPageLibrary, FullPageEmptyState, FullPageArticleCard).
1319
+ // Each component scenario should get the SHA for its specific entity,
1320
+ // not whichever entity happens to be last in the map.
1321
+ rawDb
1322
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1323
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1324
+ .run('sc-fplib', projectId, 'FullPageLibrary - Default', 'FullPageLibrary', 'src/library/FullPageLibrary.tsx', '/isolated-components/FullPageLibrary', 'component', 1280, 720, 'old-fplib-sha');
1325
+ rawDb
1326
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1327
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1328
+ .run('sc-fpempty', projectId, 'FullPageEmptyState - Default', 'FullPageEmptyState', 'src/library/FullPageLibrary.tsx', '/isolated-components/FullPageEmptyState', 'component', 1280, 720, 'old-fpempty-sha');
1329
+ const result = await backfillEntityShaOnScenarios(db, [
1330
+ {
1331
+ sha: 'new-fplib-sha',
1332
+ name: 'FullPageLibrary',
1333
+ filePath: 'src/library/FullPageLibrary.tsx',
1334
+ },
1335
+ {
1336
+ sha: 'new-fpempty-sha',
1337
+ name: 'FullPageEmptyState',
1338
+ filePath: 'src/library/FullPageLibrary.tsx',
1339
+ },
1340
+ ]);
1341
+ expect(result.updated).toBe(2);
1342
+ const fplib = rawDb
1343
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1344
+ .get('sc-fplib');
1345
+ expect(fplib.entity_sha).toBe('new-fplib-sha');
1346
+ const fpempty = rawDb
1347
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1348
+ .get('sc-fpempty');
1349
+ expect(fpempty.entity_sha).toBe('new-fpempty-sha');
1350
+ });
1351
+ it('should prefer default export entity for page scenarios when file has multiple entities', async () => {
1352
+ // Bug: src/popup/App.tsx has multiple entities (App visual, getInitialView library,
1353
+ // View type). Page scenarios should link to the default export (App), not whichever
1354
+ // entity happens to come first in the array.
1355
+ rawDb
1356
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1357
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1358
+ .run('sc-page-1', projectId, 'Save View - First Time User', 'src/popup/App.tsx', '/', 'application', 400, 600);
1359
+ // Pass multiple entities for the same file — default export should win
1360
+ // regardless of order. Here the default export (App) appears first but
1361
+ // gets overwritten by later non-default entities in the naive set() impl.
1362
+ const result = await backfillEntityShaOnScenarios(db, [
1363
+ {
1364
+ sha: 'app-sha',
1365
+ name: 'App',
1366
+ filePath: 'src/popup/App.tsx',
1367
+ isDefaultExport: true,
1368
+ },
1369
+ {
1370
+ sha: 'get-initial-view-sha',
1371
+ name: 'getInitialView',
1372
+ filePath: 'src/popup/App.tsx',
1373
+ isDefaultExport: false,
1374
+ },
1375
+ {
1376
+ sha: 'view-type-sha',
1377
+ name: 'View',
1378
+ filePath: 'src/popup/App.tsx',
1379
+ isDefaultExport: false,
1380
+ },
1381
+ ]);
1382
+ expect(result.updated).toBe(1);
1383
+ const row = rawDb
1384
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1385
+ .get('sc-page-1');
1386
+ expect(row.entity_sha).toBe('app-sha');
1387
+ });
1388
+ it('should update page scenario to latest entity version when multiple versions exist', async () => {
1389
+ // Real-world bug: App.tsx entity gets versioned (code changes create new SHAs).
1390
+ // The scenario points to the oldest version's SHA. When sync receives all 3
1391
+ // versions of the same default-export entity, it should update the scenario
1392
+ // to the LATEST version, not keep it stuck on the oldest.
1393
+ rawDb
1394
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1395
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1396
+ .run('sc-home', projectId, 'Welcome Screen — First Install', 'src/popup/App.tsx', '/', 'application', 400, 600, 'version-1-sha', // stale — points to oldest version
1397
+ 'Home');
1398
+ // Pass all 3 versions — all default exports for the same file.
1399
+ // The sync should pick the LAST one (latest) since callers pass
1400
+ // entities in creation order.
1401
+ const result = await backfillEntityShaOnScenarios(db, [
1402
+ {
1403
+ sha: 'version-1-sha',
1404
+ name: 'App',
1405
+ filePath: 'src/popup/App.tsx',
1406
+ isDefaultExport: true,
1407
+ },
1408
+ {
1409
+ sha: 'version-2-sha',
1410
+ name: 'App',
1411
+ filePath: 'src/popup/App.tsx',
1412
+ isDefaultExport: true,
1413
+ },
1414
+ {
1415
+ sha: 'version-3-sha',
1416
+ name: 'App',
1417
+ filePath: 'src/popup/App.tsx',
1418
+ isDefaultExport: true,
1419
+ },
1420
+ ]);
1421
+ expect(result.updated).toBe(1);
1422
+ const row = rawDb
1423
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1424
+ .get('sc-home');
1425
+ expect(row.entity_sha).toBe('version-3-sha');
1426
+ });
1427
+ });
1428
+ describe('countScenariosNeedingEntityBackfill', () => {
1429
+ let db;
1430
+ let rawDb;
1431
+ const projectId = 'test-project-id';
1432
+ beforeEach(async () => {
1433
+ rawDb = new Database(':memory:');
1434
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1435
+ await db.schema
1436
+ .createTable('editor_scenarios')
1437
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1438
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1439
+ .addColumn('name', 'varchar', (col) => col.notNull())
1440
+ .addColumn('description', 'text')
1441
+ .addColumn('component_name', 'varchar')
1442
+ .addColumn('component_path', 'varchar')
1443
+ .addColumn('url', 'varchar')
1444
+ .addColumn('type', 'varchar')
1445
+ .addColumn('screenshot_path', 'varchar')
1446
+ .addColumn('viewport_width', 'integer')
1447
+ .addColumn('viewport_height', 'integer')
1448
+ .addColumn('dimension', 'varchar')
1449
+ .addColumn('dimensions', 'text')
1450
+ .addColumn('screenshot_paths', 'text')
1451
+ .addColumn('page_file_path', 'varchar')
1452
+ .addColumn('entity_sha', 'varchar')
1453
+ .addColumn('display_name', 'varchar')
1454
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1455
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1456
+ .execute();
1457
+ });
1458
+ afterEach(async () => {
1459
+ await db.destroy();
1460
+ });
1461
+ it('should NOT count scenarios that already have entity_sha', async () => {
1462
+ rawDb
1463
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1464
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1465
+ .run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'existing-sha');
1466
+ const count = await countScenariosNeedingEntityBackfill(db);
1467
+ expect(count).toBe(0);
1468
+ });
1469
+ it('should count scenarios with null entity_sha and page_file_path', async () => {
1470
+ rawDb
1471
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1472
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1473
+ .run('sc-2', projectId, 'Home - Default', 'app/page.tsx', '/', 'application', 1280, 720);
1474
+ const count = await countScenariosNeedingEntityBackfill(db);
1475
+ expect(count).toBe(1);
1476
+ });
1477
+ it('should count scenarios with null entity_sha and component_path', async () => {
1478
+ rawDb
1479
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1480
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1481
+ .run('sc-3', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
1482
+ const count = await countScenariosNeedingEntityBackfill(db);
1483
+ expect(count).toBe(1);
1484
+ });
1485
+ it('should NOT count scenarios with null entity_sha and no file paths', async () => {
1486
+ rawDb
1487
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, url, type, viewport_width, viewport_height)
1488
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
1489
+ .run('sc-4', projectId, 'No File Path', '/', 'application', 1280, 720);
1490
+ const count = await countScenariosNeedingEntityBackfill(db);
1491
+ expect(count).toBe(0);
1492
+ });
1493
+ });
1494
+ describe('validateEntityLinkageForAppScenario', () => {
1495
+ let tmpDir;
1496
+ let glossaryPath;
1497
+ beforeEach(() => {
1498
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entity-linkage-'));
1499
+ const codeyamDir = path.join(tmpDir, '.codeyam');
1500
+ fs.mkdirSync(codeyamDir, { recursive: true });
1501
+ glossaryPath = path.join(codeyamDir, 'glossary.json');
1502
+ });
1503
+ afterEach(() => {
1504
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1505
+ });
1506
+ it('should return error when file is not in glossary', () => {
1507
+ // Write a glossary that does NOT include the target file
1508
+ fs.writeFileSync(glossaryPath, JSON.stringify([
1509
+ { name: 'Header', filePath: 'src/components/Header.tsx' },
1510
+ ]));
1511
+ const result = validateEntityLinkageForAppScenario({
1512
+ lookupFilePath: 'src/App.tsx',
1513
+ scenarioType: 'application',
1514
+ projectRoot: tmpDir,
1515
+ });
1516
+ expect(result.valid).toBe(false);
1517
+ expect(result.error).toContain('No glossary entry found');
1518
+ expect(result.error).toContain('src/App.tsx');
1519
+ });
1520
+ it('should return needsAnalysis when file IS in glossary but no entity exists', () => {
1521
+ // Write a glossary that includes the target file
1522
+ fs.writeFileSync(glossaryPath, JSON.stringify([
1523
+ { name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
1524
+ ]));
1525
+ const result = validateEntityLinkageForAppScenario({
1526
+ lookupFilePath: 'src/App.tsx',
1527
+ scenarioType: 'application',
1528
+ projectRoot: tmpDir,
1529
+ });
1530
+ expect(result.valid).toBe(true);
1531
+ expect(result.needsAnalysis).toBe(true);
1532
+ });
1533
+ it('should skip validation for component-type scenarios', () => {
1534
+ // No glossary file exists at all
1535
+ const result = validateEntityLinkageForAppScenario({
1536
+ lookupFilePath: 'src/components/Header.tsx',
1537
+ scenarioType: 'component',
1538
+ projectRoot: tmpDir,
1539
+ });
1540
+ expect(result.valid).toBe(true);
1541
+ expect(result.needsAnalysis).toBeUndefined();
1542
+ });
1543
+ it('should skip validation when lookupFilePath is null', () => {
1544
+ const result = validateEntityLinkageForAppScenario({
1545
+ lookupFilePath: null,
1546
+ scenarioType: 'application',
1547
+ projectRoot: tmpDir,
1548
+ });
1549
+ expect(result.valid).toBe(true);
1550
+ expect(result.needsAnalysis).toBeUndefined();
1551
+ });
1552
+ it('should handle glossary wrapped in object format', () => {
1553
+ // LLMs sometimes write glossary as {"components": [...]}
1554
+ fs.writeFileSync(glossaryPath, JSON.stringify({
1555
+ components: [
1556
+ { name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
1557
+ ],
1558
+ }));
1559
+ const result = validateEntityLinkageForAppScenario({
1560
+ lookupFilePath: 'src/App.tsx',
1561
+ scenarioType: 'user',
1562
+ projectRoot: tmpDir,
1563
+ });
1564
+ expect(result.valid).toBe(true);
1565
+ expect(result.needsAnalysis).toBe(true);
1566
+ });
1567
+ it('should return error when glossary file does not exist', () => {
1568
+ // Don't create a glossary file
1569
+ const result = validateEntityLinkageForAppScenario({
1570
+ lookupFilePath: 'src/App.tsx',
1571
+ scenarioType: 'application',
1572
+ projectRoot: tmpDir,
1573
+ });
1574
+ expect(result.valid).toBe(false);
1575
+ expect(result.error).toContain('No glossary entry found');
1576
+ });
1577
+ });
1578
+ describe('validateScenarioCategorization', () => {
1579
+ it('should reject componentName with non-isolation URL', () => {
1580
+ const result = validateScenarioCategorization({
1581
+ componentName: 'Library',
1582
+ url: '/library',
1583
+ });
1584
+ expect(result.valid).toBe(false);
1585
+ expect(result.error).toBeDefined();
1586
+ expect(result.error).toContain('componentName');
1587
+ });
1588
+ it('should accept componentName with /isolated-components/ URL', () => {
1589
+ const result = validateScenarioCategorization({
1590
+ componentName: 'Library',
1591
+ url: '/isolated-components/Library?s=Default',
1592
+ });
1593
+ expect(result.valid).toBe(true);
1594
+ });
1595
+ it('should accept componentName with /codeyam-isolate/ URL', () => {
1596
+ const result = validateScenarioCategorization({
1597
+ componentName: 'Library',
1598
+ url: '/codeyam-isolate/Library?s=Default',
1599
+ });
1600
+ expect(result.valid).toBe(true);
1601
+ });
1602
+ it('should accept application scenario without componentName', () => {
1603
+ const result = validateScenarioCategorization({
1604
+ url: '/library',
1605
+ type: 'application',
1606
+ });
1607
+ expect(result.valid).toBe(true);
1608
+ });
1609
+ it('should accept component with no URL', () => {
1610
+ const result = validateScenarioCategorization({
1611
+ componentName: 'Library',
1612
+ url: null,
1613
+ });
1614
+ expect(result.valid).toBe(true);
1615
+ });
1616
+ it('should suggest both fixes in error message', () => {
1617
+ const result = validateScenarioCategorization({
1618
+ componentName: 'Library',
1619
+ url: '/library',
1620
+ });
1621
+ expect(result.valid).toBe(false);
1622
+ // Should suggest changing URL to isolation route
1623
+ expect(result.error).toContain('/isolated-components/');
1624
+ // Should suggest removing componentName
1625
+ expect(result.error).toContain('remove');
1626
+ });
1627
+ });
1628
+ describe('resolveSeedFrom', () => {
1629
+ let tmpDir;
1630
+ let db;
1631
+ beforeEach(() => {
1632
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'seedfrom-'));
1633
+ fs.mkdirSync(path.join(tmpDir, '.codeyam', 'editor-scenarios'), {
1634
+ recursive: true,
1635
+ });
1636
+ const raw = new Database(':memory:');
1637
+ raw.exec(`
1638
+ CREATE TABLE editor_scenarios (
1639
+ id TEXT PRIMARY KEY,
1640
+ project_id TEXT,
1641
+ name TEXT,
1642
+ description TEXT,
1643
+ component_name TEXT,
1644
+ component_path TEXT,
1645
+ url TEXT,
1646
+ type TEXT,
1647
+ screenshot_path TEXT,
1648
+ viewport_width INTEGER,
1649
+ viewport_height INTEGER,
1650
+ dimensions TEXT,
1651
+ screenshot_paths TEXT,
1652
+ page_file_path TEXT,
1653
+ entity_sha TEXT,
1654
+ display_name TEXT,
1655
+ created_at TEXT DEFAULT (datetime('now')),
1656
+ updated_at TEXT DEFAULT (datetime('now'))
1657
+ )
1658
+ `);
1659
+ db = new Kysely({ dialect: new SqliteDialect({ database: raw }) });
1660
+ });
1661
+ afterEach(() => {
1662
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1663
+ });
1664
+ it('should resolve seed data by scenario ID', async () => {
1665
+ const scenarioId = 'aaaa-bbbb-cccc-dddd';
1666
+ const seedData = { article: [{ id: '1', title: 'Test' }] };
1667
+ // Write the seed file
1668
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), JSON.stringify(seedData));
1669
+ const result = await resolveSeedFrom(tmpDir, db, scenarioId);
1670
+ expect(result).toEqual(seedData);
1671
+ });
1672
+ it('should resolve seed data by scenario name', async () => {
1673
+ const scenarioId = 'eeee-ffff-1111-2222';
1674
+ const seedData = { user: [{ id: '1', name: 'Alice' }] };
1675
+ // Write the seed file
1676
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), JSON.stringify(seedData));
1677
+ // Insert scenario into DB so name lookup works
1678
+ await db
1679
+ .insertInto('editor_scenarios')
1680
+ .values({
1681
+ id: scenarioId,
1682
+ project_id: 'proj-1',
1683
+ name: 'Library - Rich Data',
1684
+ viewport_width: 1280,
1685
+ viewport_height: 720,
1686
+ })
1687
+ .execute();
1688
+ const result = await resolveSeedFrom(tmpDir, db, 'Library - Rich Data');
1689
+ expect(result).toEqual(seedData);
1690
+ });
1691
+ it('should fall back to seed from scenario data file when no .seed.json exists', async () => {
1692
+ const scenarioId = 'cccc-dddd-eeee-ffff';
1693
+ const seedData = { tag: [{ id: '1', name: 'AI' }] };
1694
+ // Write the scenario data file (not the .seed.json)
1695
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), JSON.stringify({ seed: seedData, _metadata: { name: 'Test' } }));
1696
+ const result = await resolveSeedFrom(tmpDir, db, scenarioId);
1697
+ expect(result).toEqual(seedData);
1698
+ });
1699
+ it('should return null when scenario not found', async () => {
1700
+ const result = await resolveSeedFrom(tmpDir, db, 'nonexistent');
1701
+ expect(result).toBeNull();
1702
+ });
1703
+ it('should return null when scenario exists but has no seed data', async () => {
1704
+ const scenarioId = 'no-seed-1111-2222';
1705
+ // Scenario data file with no seed
1706
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), JSON.stringify({ _metadata: { name: 'No Seed' } }));
1707
+ const result = await resolveSeedFrom(tmpDir, db, scenarioId);
1708
+ expect(result).toBeNull();
1709
+ });
1710
+ it('should return null when seed.json contains invalid JSON', async () => {
1711
+ const scenarioId = 'bad-json-1111';
1712
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), '{ this is not valid json }}}');
1713
+ const result = await resolveSeedFrom(tmpDir, db, scenarioId);
1714
+ expect(result).toBeNull();
1715
+ });
1716
+ it('should return null when scenario data file contains invalid JSON', async () => {
1717
+ const scenarioId = 'bad-data-2222';
1718
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), 'not json at all');
1719
+ const result = await resolveSeedFrom(tmpDir, db, scenarioId);
1720
+ expect(result).toBeNull();
1721
+ });
1722
+ it('should pick the most recent scenario when multiple share the same name', async () => {
1723
+ const oldId = 'old-scenario-1111';
1724
+ const newId = 'new-scenario-2222';
1725
+ const oldSeed = { article: [{ id: '1', title: 'Old' }] };
1726
+ const newSeed = { article: [{ id: '2', title: 'New' }] };
1727
+ // Write seed files for both
1728
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${oldId}.seed.json`), JSON.stringify(oldSeed));
1729
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${newId}.seed.json`), JSON.stringify(newSeed));
1730
+ // Insert both with the same name, different created_at
1731
+ await db
1732
+ .insertInto('editor_scenarios')
1733
+ .values({
1734
+ id: oldId,
1735
+ project_id: 'proj-1',
1736
+ name: 'Shared Name',
1737
+ viewport_width: 1280,
1738
+ viewport_height: 720,
1739
+ created_at: '2026-01-01 00:00:00',
1740
+ })
1741
+ .execute();
1742
+ await db
1743
+ .insertInto('editor_scenarios')
1744
+ .values({
1745
+ id: newId,
1746
+ project_id: 'proj-1',
1747
+ name: 'Shared Name',
1748
+ viewport_width: 1280,
1749
+ viewport_height: 720,
1750
+ created_at: '2026-03-01 00:00:00',
1751
+ })
1752
+ .execute();
1753
+ const result = await resolveSeedFrom(tmpDir, db, 'Shared Name');
1754
+ expect(result).toEqual(newSeed);
1755
+ });
1756
+ it('should prefer .seed.json over inline seed in data file', async () => {
1757
+ const scenarioId = 'prefer-seed-file';
1758
+ const seedFileData = { article: [{ id: '1', title: 'From seed file' }] };
1759
+ const inlineData = { article: [{ id: '2', title: 'From inline' }] };
1760
+ // Write both files
1761
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.seed.json`), JSON.stringify(seedFileData));
1762
+ fs.writeFileSync(path.join(tmpDir, '.codeyam', 'editor-scenarios', `${scenarioId}.json`), JSON.stringify({ seed: inlineData }));
1763
+ const result = await resolveSeedFrom(tmpDir, db, scenarioId);
1764
+ expect(result).toEqual(seedFileData);
1765
+ });
1766
+ });
220
1767
  });
221
1768
  //# sourceMappingURL=editorScenarios.test.js.map