@codeyam/codeyam-cli 0.1.0-staging.8778565 → 0.1.0-staging.87dd4be

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 (310) 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 +2 -2
  4. package/analyzer-template/packages/ai/package.json +1 -1
  5. package/analyzer-template/packages/aws/package.json +1 -1
  6. package/analyzer-template/packages/database/package.json +1 -1
  7. package/analyzer-template/packages/database/src/lib/kysely/tables/editorScenariosTable.ts +82 -0
  8. package/analyzer-template/packages/database/src/lib/loadEntities.ts +0 -6
  9. package/analyzer-template/packages/database/src/lib/updateCommitMetadata.ts +0 -65
  10. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts +5 -0
  11. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts.map +1 -1
  12. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js +84 -0
  13. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
  14. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.d.ts.map +1 -1
  15. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js +0 -6
  16. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js.map +1 -1
  17. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.d.ts.map +1 -1
  18. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js +0 -25
  19. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js.map +1 -1
  20. package/codeyam-cli/src/cli.js +9 -0
  21. package/codeyam-cli/src/cli.js.map +1 -1
  22. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js +51 -0
  23. package/codeyam-cli/src/commands/__tests__/editor.isolateArgs.test.js.map +1 -0
  24. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +56 -0
  25. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js.map +1 -0
  26. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +101 -47
  27. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
  28. package/codeyam-cli/src/commands/editor.js +2369 -346
  29. package/codeyam-cli/src/commands/editor.js.map +1 -1
  30. package/codeyam-cli/src/commands/editorIsolateArgs.js +25 -0
  31. package/codeyam-cli/src/commands/editorIsolateArgs.js.map +1 -0
  32. package/codeyam-cli/src/commands/init.js +69 -34
  33. package/codeyam-cli/src/commands/init.js.map +1 -1
  34. package/codeyam-cli/src/commands/telemetry.js +37 -0
  35. package/codeyam-cli/src/commands/telemetry.js.map +1 -0
  36. package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js +173 -0
  37. package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js.map +1 -0
  38. package/codeyam-cli/src/utils/__tests__/editorApi.test.js +18 -8
  39. package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -1
  40. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +2046 -1
  41. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  42. package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js +76 -0
  43. package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js.map +1 -0
  44. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js +137 -0
  45. package/codeyam-cli/src/utils/__tests__/editorCaptureScenarioSeeding.test.js.map +1 -0
  46. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js +100 -0
  47. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js.map +1 -0
  48. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +76 -3
  49. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -1
  50. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +381 -0
  51. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -0
  52. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js +202 -1
  53. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js.map +1 -1
  54. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js +435 -0
  55. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js.map +1 -0
  56. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +88 -1
  57. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
  58. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +47 -1
  59. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -1
  60. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +70 -0
  61. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -1
  62. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +1335 -1
  63. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  64. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +67 -0
  65. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -1
  66. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js +143 -0
  67. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js.map +1 -0
  68. package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js +66 -0
  69. package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js.map +1 -0
  70. package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js +53 -0
  71. package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js.map +1 -0
  72. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +363 -11
  73. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  74. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js +177 -0
  75. package/codeyam-cli/src/utils/__tests__/glossaryAdd.test.js.map +1 -0
  76. package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js +30 -2
  77. package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js.map +1 -1
  78. package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js +118 -0
  79. package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js.map +1 -0
  80. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +284 -0
  81. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -0
  82. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +649 -223
  83. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -1
  84. package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js +1 -0
  85. package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js.map +1 -1
  86. package/codeyam-cli/src/utils/__tests__/telemetry.test.js +159 -0
  87. package/codeyam-cli/src/utils/__tests__/telemetry.test.js.map +1 -0
  88. package/codeyam-cli/src/utils/analysisRunner.js +3 -1
  89. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  90. package/codeyam-cli/src/utils/analyzer.js +9 -0
  91. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  92. package/codeyam-cli/src/utils/analyzerFinalization.js +100 -0
  93. package/codeyam-cli/src/utils/analyzerFinalization.js.map +1 -0
  94. package/codeyam-cli/src/utils/backgroundServer.js +3 -9
  95. package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
  96. package/codeyam-cli/src/utils/database.js +37 -2
  97. package/codeyam-cli/src/utils/database.js.map +1 -1
  98. package/codeyam-cli/src/utils/editorApi.js +11 -5
  99. package/codeyam-cli/src/utils/editorApi.js.map +1 -1
  100. package/codeyam-cli/src/utils/editorAudit.js +372 -5
  101. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  102. package/codeyam-cli/src/utils/editorBroadcastViewport.js +26 -0
  103. package/codeyam-cli/src/utils/editorBroadcastViewport.js.map +1 -0
  104. package/codeyam-cli/src/utils/editorDeleteScenario.js +67 -0
  105. package/codeyam-cli/src/utils/editorDeleteScenario.js.map +1 -0
  106. package/codeyam-cli/src/utils/editorEntityChangeStatus.js +13 -7
  107. package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -1
  108. package/codeyam-cli/src/utils/editorEntityHelpers.js +144 -0
  109. package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -0
  110. package/codeyam-cli/src/utils/editorLoaderHelpers.js +72 -1
  111. package/codeyam-cli/src/utils/editorLoaderHelpers.js.map +1 -1
  112. package/codeyam-cli/src/utils/editorMigration.js +224 -0
  113. package/codeyam-cli/src/utils/editorMigration.js.map +1 -0
  114. package/codeyam-cli/src/utils/editorPreview.js +31 -0
  115. package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
  116. package/codeyam-cli/src/utils/editorRecapture.js +109 -0
  117. package/codeyam-cli/src/utils/editorRecapture.js.map +1 -0
  118. package/codeyam-cli/src/utils/editorScenarioSwitch.js +24 -2
  119. package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -1
  120. package/codeyam-cli/src/utils/editorScenarios.js +458 -0
  121. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  122. package/codeyam-cli/src/utils/editorSeedAdapter.js +253 -4
  123. package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -1
  124. package/codeyam-cli/src/utils/editorShouldRevalidate.js +21 -0
  125. package/codeyam-cli/src/utils/editorShouldRevalidate.js.map +1 -0
  126. package/codeyam-cli/src/utils/entityChangeStatus.js +53 -6
  127. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -1
  128. package/codeyam-cli/src/utils/entityChangeStatus.server.js +41 -3
  129. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
  130. package/codeyam-cli/src/utils/fileWatcher.js +38 -0
  131. package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
  132. package/codeyam-cli/src/utils/glossaryAdd.js +74 -0
  133. package/codeyam-cli/src/utils/glossaryAdd.js.map +1 -0
  134. package/codeyam-cli/src/utils/install-skills.js +14 -0
  135. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  136. package/codeyam-cli/src/utils/parseRegisterArg.js.map +1 -1
  137. package/codeyam-cli/src/utils/progress.js +2 -2
  138. package/codeyam-cli/src/utils/progress.js.map +1 -1
  139. package/codeyam-cli/src/utils/routePatternMatching.js +129 -0
  140. package/codeyam-cli/src/utils/routePatternMatching.js.map +1 -0
  141. package/codeyam-cli/src/utils/scenarioCoverage.js +77 -0
  142. package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -0
  143. package/codeyam-cli/src/utils/scenariosManifest.js +269 -74
  144. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -1
  145. package/codeyam-cli/src/utils/setupClaudeCodeSettings.js +1 -0
  146. package/codeyam-cli/src/utils/setupClaudeCodeSettings.js.map +1 -1
  147. package/codeyam-cli/src/utils/simulationGateMiddleware.js +8 -1
  148. package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
  149. package/codeyam-cli/src/utils/slugUtils.js +25 -0
  150. package/codeyam-cli/src/utils/slugUtils.js.map +1 -0
  151. package/codeyam-cli/src/utils/syncMocksMiddleware.js +2 -2
  152. package/codeyam-cli/src/utils/syncMocksMiddleware.js.map +1 -1
  153. package/codeyam-cli/src/utils/telemetry.js +106 -0
  154. package/codeyam-cli/src/utils/telemetry.js.map +1 -0
  155. package/codeyam-cli/src/utils/telemetryMiddleware.js +22 -0
  156. package/codeyam-cli/src/utils/telemetryMiddleware.js.map +1 -0
  157. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +35 -0
  158. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -0
  159. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +80 -0
  160. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -0
  161. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +218 -0
  162. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
  163. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +217 -0
  164. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -0
  165. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +71 -0
  166. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -0
  167. package/codeyam-cli/src/webserver/app/lib/git.js +3 -2
  168. package/codeyam-cli/src/webserver/app/lib/git.js.map +1 -1
  169. package/codeyam-cli/src/webserver/app/types/editor.js +8 -0
  170. package/codeyam-cli/src/webserver/app/types/editor.js.map +1 -0
  171. package/codeyam-cli/src/webserver/backgroundServer.js +60 -61
  172. package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
  173. package/codeyam-cli/src/webserver/build/client/assets/CopyButton-CLe80MMu.js +1 -0
  174. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-BcgbViKV.js → EntityItem-Crt_KN_U.js} +3 -3
  175. package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-CQgyEGV-.js +1 -0
  176. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-CQIG2qda.js → EntityTypeIcon-CD7lGABo.js} +1 -1
  177. package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-CgTNOhnu.js +1 -0
  178. package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-CKeQT5Ty.js +25 -0
  179. package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-D3s1MFkb.js +3 -0
  180. package/codeyam-cli/src/webserver/build/client/assets/{LoadingDots-BU_OAEMP.js → LoadingDots-By5zI316.js} +1 -1
  181. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-ceAyBX-H.js → LogViewer-CM5zg40N.js} +3 -3
  182. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-BzHcG7SE.js → ReportIssueModal-C2PLkej3.js} +2 -2
  183. package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-DanvyBPb.js +1 -0
  184. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-0DY_NKil.js → ScenarioViewer-DUMfcNVK.js} +3 -3
  185. package/codeyam-cli/src/webserver/build/client/assets/Spinner-D0LgAaSa.js +34 -0
  186. package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-CK7-NaPZ.js +1 -0
  187. package/codeyam-cli/src/webserver/build/client/assets/ViewportInspectBar-BA_Ry-rs.js +1 -0
  188. package/codeyam-cli/src/webserver/build/client/assets/{_index-DLxKhri3.js → _index-BAWd-Xjf.js} +2 -2
  189. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-BcY3q6nt.js → activity.(_tab)-BOARiB-g.js} +3 -3
  190. package/codeyam-cli/src/webserver/build/client/assets/{addon-web-links-Duc5hnl7.js → addon-web-links-CHx25PAe.js} +1 -1
  191. package/codeyam-cli/src/webserver/build/client/assets/{agent-transcripts-Bni3iiUj.js → agent-transcripts-Bg3e7q4S.js} +3 -3
  192. package/codeyam-cli/src/webserver/build/client/assets/api.editor-recapture-stale-l0sNRNKZ.js +1 -0
  193. package/codeyam-cli/src/webserver/build/client/assets/api.editor-rename-scenario-l0sNRNKZ.js +1 -0
  194. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-seed-state-l0sNRNKZ.js +1 -0
  195. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-coverage-l0sNRNKZ.js +1 -0
  196. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-prompt-l0sNRNKZ.js +1 -0
  197. package/codeyam-cli/src/webserver/build/client/assets/api.editor-session-l0sNRNKZ.js +1 -0
  198. package/codeyam-cli/src/webserver/build/client/assets/{book-open-BYOypzCa.js → book-open-CL-lMgHh.js} +1 -1
  199. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-C_Pmso5S.js → chevron-down-GmAjGS9-.js} +1 -1
  200. package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-BAdwhyCx.js +43 -0
  201. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-BVMi9VA5.js → circle-check-DFcQkN5j.js} +1 -1
  202. package/codeyam-cli/src/webserver/build/client/assets/{copy-n2FB0_Sw.js → copy-C6iF61Xs.js} +1 -1
  203. package/codeyam-cli/src/webserver/build/client/assets/{createLucideIcon-CC6AbExI.js → createLucideIcon-4ImjHTVC.js} +1 -1
  204. package/codeyam-cli/src/webserver/build/client/assets/dev.empty-C8y4mmyv.js +1 -0
  205. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-Gbk_i5Js.js +1 -0
  206. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Bnx7yUP0.js +58 -0
  207. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +41 -0
  208. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-BF4oLwaE.js → entity._sha._-Blfy9UlN.js} +2 -2
  209. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js +6 -0
  210. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js +6 -0
  211. package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-DQM8E7L4.js +6 -0
  212. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-BMvVHNXU.js → entity._sha_.edit._scenarioId-CAoXLsQr.js} +2 -2
  213. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-DTvKq3TY.js → entry.client-SuW9syRS.js} +6 -6
  214. package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-Daa96Fr1.js +1 -0
  215. package/codeyam-cli/src/webserver/build/client/assets/files-D-xGrg29.js +1 -0
  216. package/codeyam-cli/src/webserver/build/client/assets/git-Bq_fbXP5.js +1 -0
  217. package/codeyam-cli/src/webserver/build/client/assets/globals-fAqOD9ex.css +1 -0
  218. package/codeyam-cli/src/webserver/build/client/assets/{index-BcvgDzbZ.js → index-Bp1l4hSv.js} +1 -1
  219. package/codeyam-cli/src/webserver/build/client/assets/{index-10oVnAAH.js → index-CWV9XZiG.js} +1 -1
  220. package/codeyam-cli/src/webserver/build/client/assets/{index-yHOVb4rc.js → index-DE3jI_dv.js} +1 -1
  221. package/codeyam-cli/src/webserver/build/client/assets/jsx-runtime-D_zvdyIk.js +9 -0
  222. package/codeyam-cli/src/webserver/build/client/assets/labs-B_IX45ih.js +1 -0
  223. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-DaAZ_H2w.js → loader-circle-De-7qQ2u.js} +1 -1
  224. package/codeyam-cli/src/webserver/build/client/assets/manifest-b9d4d267.js +1 -0
  225. package/codeyam-cli/src/webserver/build/client/assets/memory-Cx2xEx7s.js +101 -0
  226. package/codeyam-cli/src/webserver/build/client/assets/{pause-f5-1lKBt.js → pause-CFxEKL1u.js} +1 -1
  227. package/codeyam-cli/src/webserver/build/client/assets/root-DB3O9_9j.js +67 -0
  228. package/codeyam-cli/src/webserver/build/client/assets/{search-Di64LWVb.js → search-BdBb5aqc.js} +1 -1
  229. package/codeyam-cli/src/webserver/build/client/assets/settings-DdE-Untf.js +1 -0
  230. package/codeyam-cli/src/webserver/build/client/assets/simulations-DSCdE99u.js +1 -0
  231. package/codeyam-cli/src/webserver/build/client/assets/{terminal-Br7MOqts.js → terminal-CrplD4b1.js} +1 -1
  232. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-BLdiCuG-.js → triangle-alert-DqJ0j69l.js} +1 -1
  233. package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-DhXHbEjP.js +1 -0
  234. package/codeyam-cli/src/webserver/build/client/assets/{useLastLogLine-C14nCb1q.js → useLastLogLine-BNd5hYuW.js} +1 -1
  235. package/codeyam-cli/src/webserver/build/client/assets/useReportContext-Cy5Qg_UR.js +1 -0
  236. package/codeyam-cli/src/webserver/build/client/assets/useToast-5HR2j9ZE.js +1 -0
  237. package/codeyam-cli/src/webserver/build/client/sound-test.html +98 -0
  238. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-CGwTN3V2.js +13 -0
  239. package/codeyam-cli/src/webserver/build/server/assets/index-D4meMKy3.js +1 -0
  240. package/codeyam-cli/src/webserver/build/server/assets/init-odGJ_c2-.js +10 -0
  241. package/codeyam-cli/src/webserver/build/server/assets/progress-CHTtrxFG.js +1 -0
  242. package/codeyam-cli/src/webserver/build/server/assets/server-build-TmPfF7pT.js +552 -0
  243. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  244. package/codeyam-cli/src/webserver/build-info.json +5 -5
  245. package/codeyam-cli/src/webserver/editorProxy.js +208 -17
  246. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
  247. package/codeyam-cli/src/webserver/idleDetector.js +106 -0
  248. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -0
  249. package/codeyam-cli/src/webserver/mockStateEvents.js +28 -0
  250. package/codeyam-cli/src/webserver/mockStateEvents.js.map +1 -0
  251. package/codeyam-cli/src/webserver/public/sound-test.html +98 -0
  252. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +36 -0
  253. package/codeyam-cli/src/webserver/server.js +87 -4
  254. package/codeyam-cli/src/webserver/server.js.map +1 -1
  255. package/codeyam-cli/src/webserver/terminalServer.js +140 -35
  256. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  257. package/codeyam-cli/templates/chrome-extension-react/README.md +46 -0
  258. package/codeyam-cli/templates/chrome-extension-react/package.json +1 -0
  259. package/codeyam-cli/templates/codeyam-editor-claude.md +84 -5
  260. package/codeyam-cli/templates/codeyam-editor-reference.md +214 -0
  261. package/codeyam-cli/templates/editor-step-hook.py +114 -24
  262. package/codeyam-cli/templates/expo-react-native/README.md +41 -0
  263. package/codeyam-cli/templates/expo-react-native/package.json +1 -0
  264. package/codeyam-cli/templates/nextjs-prisma-sqlite/DATABASE.md +14 -0
  265. package/codeyam-cli/templates/nextjs-prisma-sqlite/README.md +53 -0
  266. package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +2 -1
  267. package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +42 -7
  268. package/codeyam-cli/templates/nextjs-prisma-supabase/README.md +52 -0
  269. package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +2 -1
  270. package/codeyam-cli/templates/seed-adapters/supabase.ts +282 -0
  271. package/codeyam-cli/templates/skills/codeyam-dev-mode/SKILL.md +1 -1
  272. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +76 -10
  273. package/package.json +2 -1
  274. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js +84 -0
  275. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -1
  276. package/packages/database/src/lib/loadEntities.js +0 -6
  277. package/packages/database/src/lib/loadEntities.js.map +1 -1
  278. package/packages/database/src/lib/updateCommitMetadata.js +0 -25
  279. package/packages/database/src/lib/updateCommitMetadata.js.map +1 -1
  280. package/codeyam-cli/src/webserver/build/client/assets/CopyButton-BPXZwM4t.js +0 -1
  281. package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-g3saevPb.js +0 -1
  282. package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-Bu6c6aDe.js +0 -1
  283. package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-DYFW3lDD.js +0 -25
  284. package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-DLeucoVX.js +0 -3
  285. package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-BED4B6sP.js +0 -1
  286. package/codeyam-cli/src/webserver/build/client/assets/Spinner-Bb5uFQ5V.js +0 -34
  287. package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-C8OKAR5x.js +0 -1
  288. package/codeyam-cli/src/webserver/build/client/assets/ViewportInspectBar-oAf2Kqsf.js +0 -1
  289. package/codeyam-cli/src/webserver/build/client/assets/chunk-JZWAC4HX-C4pqxYJB.js +0 -51
  290. package/codeyam-cli/src/webserver/build/client/assets/dev.empty-Csi0_PMl.js +0 -1
  291. package/codeyam-cli/src/webserver/build/client/assets/editor-DgN1LTTt.js +0 -10
  292. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-BLQMSKZa.js +0 -41
  293. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-C7YX6r3H.js +0 -6
  294. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-CF164ouH.js +0 -6
  295. package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-p9hhkjJM.js +0 -6
  296. package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-cPo8LiG3.js +0 -1
  297. package/codeyam-cli/src/webserver/build/client/assets/files-BZrlFE1F.js +0 -1
  298. package/codeyam-cli/src/webserver/build/client/assets/git-DdZcvjGh.js +0 -1
  299. package/codeyam-cli/src/webserver/build/client/assets/globals-BkWJ_UNc.css +0 -1
  300. package/codeyam-cli/src/webserver/build/client/assets/labs-Zk7ryIM1.js +0 -1
  301. package/codeyam-cli/src/webserver/build/client/assets/manifest-c26eb85b.js +0 -1
  302. package/codeyam-cli/src/webserver/build/client/assets/memory-Bl2rpw8u.js +0 -96
  303. package/codeyam-cli/src/webserver/build/client/assets/root-ClvYBUSA.js +0 -67
  304. package/codeyam-cli/src/webserver/build/client/assets/settings-0OrEMU6J.js +0 -1
  305. package/codeyam-cli/src/webserver/build/client/assets/simulations-DWT-CvLy.js +0 -1
  306. package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-CrAK28Bc.js +0 -1
  307. package/codeyam-cli/src/webserver/build/client/assets/useReportContext-O-jkvSPx.js +0 -1
  308. package/codeyam-cli/src/webserver/build/client/assets/useToast-9FIWuYfK.js +0 -1
  309. package/codeyam-cli/src/webserver/build/server/assets/index-DflIr5SD.js +0 -1
  310. package/codeyam-cli/src/webserver/build/server/assets/server-build-OhKy839M.js +0 -416
@@ -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, readDefaultScreenSize, resolveScenarioViewport, } 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, } from "../editorScenarios.js";
5
7
  describe('editorScenarios', () => {
6
8
  describe('deduplicateByName', () => {
7
9
  it('should keep only the last item for each key', () => {
@@ -271,5 +273,1337 @@ describe('editorScenarios', () => {
271
273
  expect(result).toEqual({ width: 800, height: 667 });
272
274
  });
273
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
+ });
1013
+ describe('isRowInFeatureSession', () => {
1014
+ const featureStart = '2026-03-12 14:01:31';
1015
+ it('should include row created after feature start', () => {
1016
+ expect(isRowInFeatureSession({ created_at: '2026-03-12 14:30:00' }, featureStart)).toBe(true);
1017
+ });
1018
+ it('should exclude row created before feature start with no updated_at', () => {
1019
+ expect(isRowInFeatureSession({ created_at: '2026-03-12 13:00:00' }, featureStart)).toBe(false);
1020
+ });
1021
+ it('should include re-registered row with old created_at but recent updated_at', () => {
1022
+ // This is the bug: application-level scenarios from a previous feature
1023
+ // are re-registered, which only updates updated_at. The session filter
1024
+ // must check updated_at too, otherwise these scenarios vanish from results.
1025
+ expect(isRowInFeatureSession({
1026
+ created_at: '2026-03-12 13:28:00',
1027
+ updated_at: '2026-03-12 14:32:00',
1028
+ }, featureStart)).toBe(true);
1029
+ });
1030
+ it('should exclude row with both timestamps before feature start', () => {
1031
+ expect(isRowInFeatureSession({
1032
+ created_at: '2026-03-12 13:00:00',
1033
+ updated_at: '2026-03-12 13:30:00',
1034
+ }, featureStart)).toBe(false);
1035
+ });
1036
+ it('should handle null created_at with recent updated_at', () => {
1037
+ expect(isRowInFeatureSession({ created_at: null, updated_at: '2026-03-12 14:32:00' }, featureStart)).toBe(true);
1038
+ });
1039
+ it('should handle null updated_at with recent created_at', () => {
1040
+ expect(isRowInFeatureSession({ created_at: '2026-03-12 14:30:00', updated_at: null }, featureStart)).toBe(true);
1041
+ });
1042
+ });
1043
+ describe('upsertEditorScenario with entitySha and displayName', () => {
1044
+ let db;
1045
+ let rawDb;
1046
+ const projectId = 'test-project-id';
1047
+ beforeEach(async () => {
1048
+ rawDb = new Database(':memory:');
1049
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1050
+ await db.schema
1051
+ .createTable('editor_scenarios')
1052
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1053
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1054
+ .addColumn('name', 'varchar', (col) => col.notNull())
1055
+ .addColumn('description', 'text')
1056
+ .addColumn('component_name', 'varchar')
1057
+ .addColumn('component_path', 'varchar')
1058
+ .addColumn('url', 'varchar')
1059
+ .addColumn('type', 'varchar')
1060
+ .addColumn('screenshot_path', 'varchar')
1061
+ .addColumn('viewport_width', 'integer')
1062
+ .addColumn('viewport_height', 'integer')
1063
+ .addColumn('dimension', 'varchar')
1064
+ .addColumn('dimensions', 'text')
1065
+ .addColumn('screenshot_paths', 'text')
1066
+ .addColumn('page_file_path', 'varchar')
1067
+ .addColumn('entity_sha', 'varchar')
1068
+ .addColumn('display_name', 'varchar')
1069
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1070
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1071
+ .execute();
1072
+ });
1073
+ afterEach(async () => {
1074
+ await db.destroy();
1075
+ });
1076
+ it('should store entitySha and displayName on insert', async () => {
1077
+ const result = await upsertEditorScenario(db, {
1078
+ projectId,
1079
+ name: 'Home - Default',
1080
+ description: null,
1081
+ componentName: null,
1082
+ componentPath: null,
1083
+ url: '/',
1084
+ type: 'application',
1085
+ viewportWidth: 400,
1086
+ viewportHeight: 600,
1087
+ entitySha: 'abc123',
1088
+ displayName: 'Home',
1089
+ });
1090
+ expect(result.isNew).toBe(true);
1091
+ const row = rawDb
1092
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1093
+ .get(result.scenarioId);
1094
+ expect(row.entity_sha).toBe('abc123');
1095
+ expect(row.display_name).toBe('Home');
1096
+ });
1097
+ it('should update entitySha and displayName on upsert', async () => {
1098
+ // Insert first
1099
+ await upsertEditorScenario(db, {
1100
+ projectId,
1101
+ name: 'Home - Default',
1102
+ description: null,
1103
+ componentName: null,
1104
+ componentPath: null,
1105
+ url: '/',
1106
+ type: 'application',
1107
+ viewportWidth: 400,
1108
+ viewportHeight: 600,
1109
+ entitySha: 'old-sha',
1110
+ displayName: 'OldName',
1111
+ });
1112
+ // Upsert with new values
1113
+ const result = await upsertEditorScenario(db, {
1114
+ projectId,
1115
+ name: 'Home - Default',
1116
+ description: null,
1117
+ componentName: null,
1118
+ componentPath: null,
1119
+ url: '/',
1120
+ type: 'application',
1121
+ viewportWidth: 400,
1122
+ viewportHeight: 600,
1123
+ entitySha: 'new-sha',
1124
+ displayName: 'Home',
1125
+ });
1126
+ expect(result.isNew).toBe(false);
1127
+ const row = rawDb
1128
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1129
+ .get(result.scenarioId);
1130
+ expect(row.entity_sha).toBe('new-sha');
1131
+ expect(row.display_name).toBe('Home');
1132
+ });
1133
+ it('should not overwrite entitySha when not provided', async () => {
1134
+ // Insert with SHA
1135
+ const first = await upsertEditorScenario(db, {
1136
+ projectId,
1137
+ name: 'Home - Default',
1138
+ description: null,
1139
+ componentName: null,
1140
+ componentPath: null,
1141
+ url: '/',
1142
+ type: 'application',
1143
+ viewportWidth: 400,
1144
+ viewportHeight: 600,
1145
+ entitySha: 'keep-this',
1146
+ displayName: 'Home',
1147
+ });
1148
+ // Upsert without entitySha/displayName
1149
+ await upsertEditorScenario(db, {
1150
+ projectId,
1151
+ name: 'Home - Default',
1152
+ description: 'updated',
1153
+ componentName: null,
1154
+ componentPath: null,
1155
+ url: '/',
1156
+ type: 'application',
1157
+ viewportWidth: 400,
1158
+ viewportHeight: 600,
1159
+ });
1160
+ const row = rawDb
1161
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1162
+ .get(first.scenarioId);
1163
+ expect(row.entity_sha).toBe('keep-this');
1164
+ expect(row.display_name).toBe('Home');
1165
+ });
1166
+ });
1167
+ describe('backfillEntityShaOnScenarios', () => {
1168
+ let db;
1169
+ let rawDb;
1170
+ const projectId = 'test-project-id';
1171
+ beforeEach(async () => {
1172
+ rawDb = new Database(':memory:');
1173
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1174
+ await db.schema
1175
+ .createTable('editor_scenarios')
1176
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1177
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1178
+ .addColumn('name', 'varchar', (col) => col.notNull())
1179
+ .addColumn('description', 'text')
1180
+ .addColumn('component_name', 'varchar')
1181
+ .addColumn('component_path', 'varchar')
1182
+ .addColumn('url', 'varchar')
1183
+ .addColumn('type', 'varchar')
1184
+ .addColumn('screenshot_path', 'varchar')
1185
+ .addColumn('viewport_width', 'integer')
1186
+ .addColumn('viewport_height', 'integer')
1187
+ .addColumn('dimension', 'varchar')
1188
+ .addColumn('dimensions', 'text')
1189
+ .addColumn('screenshot_paths', 'text')
1190
+ .addColumn('page_file_path', 'varchar')
1191
+ .addColumn('entity_sha', 'varchar')
1192
+ .addColumn('display_name', 'varchar')
1193
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1194
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1195
+ .execute();
1196
+ });
1197
+ afterEach(async () => {
1198
+ await db.destroy();
1199
+ });
1200
+ it('should backfill component scenario by component_path match', async () => {
1201
+ // Insert a scenario with null entity_sha but with component_path
1202
+ rawDb
1203
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1205
+ .run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720);
1206
+ const result = await backfillEntityShaOnScenarios(db, [
1207
+ {
1208
+ sha: 'entity-sha-1',
1209
+ name: 'Header',
1210
+ filePath: 'src/components/Header.tsx',
1211
+ },
1212
+ ]);
1213
+ expect(result.updated).toBe(1);
1214
+ const row = rawDb
1215
+ .prepare('SELECT entity_sha, display_name FROM editor_scenarios WHERE id = ?')
1216
+ .get('sc-1');
1217
+ expect(row.entity_sha).toBe('entity-sha-1');
1218
+ expect(row.display_name).toBe('Header');
1219
+ });
1220
+ it('should backfill page scenario by page_file_path match and derive display_name from route', async () => {
1221
+ // Insert a scenario with null entity_sha but with page_file_path
1222
+ rawDb
1223
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1224
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1225
+ .run('sc-2', projectId, 'Feedback - Default', 'app/feedback/page.tsx', '/feedback', 'application', 1280, 720);
1226
+ const result = await backfillEntityShaOnScenarios(db, [
1227
+ {
1228
+ sha: 'entity-sha-2',
1229
+ name: 'Feedback',
1230
+ filePath: 'app/feedback/page.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-2');
1237
+ expect(row.entity_sha).toBe('entity-sha-2');
1238
+ // display_name derived from page_file_path via routeDisplayName(buildRoutePattern())
1239
+ expect(row.display_name).toBe('Feedback');
1240
+ });
1241
+ it('should update scenarios with stale entity_sha to latest', async () => {
1242
+ // Margo bug: entity code changes create new versions with new SHAs,
1243
+ // but scenarios still point to the old SHA. Since journals preserve
1244
+ // screenshots and everything is in git, we always want scenarios
1245
+ // pointing to the latest entity version.
1246
+ rawDb
1247
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1248
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1249
+ .run('sc-3', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'old-sha', 'Header');
1250
+ const result = await backfillEntityShaOnScenarios(db, [
1251
+ {
1252
+ sha: 'latest-sha',
1253
+ name: 'Header',
1254
+ filePath: 'src/components/Header.tsx',
1255
+ },
1256
+ ]);
1257
+ expect(result.updated).toBe(1);
1258
+ const row = rawDb
1259
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1260
+ .get('sc-3');
1261
+ expect(row.entity_sha).toBe('latest-sha');
1262
+ });
1263
+ it('should not update scenarios already pointing to the latest SHA', async () => {
1264
+ rawDb
1265
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1266
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1267
+ .run('sc-3b', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'current-sha', 'Header');
1268
+ const result = await backfillEntityShaOnScenarios(db, [
1269
+ {
1270
+ sha: 'current-sha',
1271
+ name: 'Header',
1272
+ filePath: 'src/components/Header.tsx',
1273
+ },
1274
+ ]);
1275
+ expect(result.updated).toBe(0);
1276
+ });
1277
+ it('should skip scenarios with no matching entity', async () => {
1278
+ rawDb
1279
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1280
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1281
+ .run('sc-4', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
1282
+ // Entities list doesn't include Footer.tsx
1283
+ const result = await backfillEntityShaOnScenarios(db, [
1284
+ {
1285
+ sha: 'entity-sha-1',
1286
+ name: 'Header',
1287
+ filePath: 'src/components/Header.tsx',
1288
+ },
1289
+ ]);
1290
+ expect(result.updated).toBe(0);
1291
+ const row = rawDb
1292
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1293
+ .get('sc-4');
1294
+ expect(row.entity_sha).toBeNull();
1295
+ });
1296
+ it('should match component scenarios by name+path, not just path (multi-entity files)', async () => {
1297
+ // A single file can export multiple components (e.g., FullPageLibrary.tsx
1298
+ // exports FullPageLibrary, FullPageEmptyState, FullPageArticleCard).
1299
+ // Each component scenario should get the SHA for its specific entity,
1300
+ // not whichever entity happens to be last in the map.
1301
+ rawDb
1302
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1303
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1304
+ .run('sc-fplib', projectId, 'FullPageLibrary - Default', 'FullPageLibrary', 'src/library/FullPageLibrary.tsx', '/isolated-components/FullPageLibrary', 'component', 1280, 720, 'old-fplib-sha');
1305
+ rawDb
1306
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1307
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1308
+ .run('sc-fpempty', projectId, 'FullPageEmptyState - Default', 'FullPageEmptyState', 'src/library/FullPageLibrary.tsx', '/isolated-components/FullPageEmptyState', 'component', 1280, 720, 'old-fpempty-sha');
1309
+ const result = await backfillEntityShaOnScenarios(db, [
1310
+ {
1311
+ sha: 'new-fplib-sha',
1312
+ name: 'FullPageLibrary',
1313
+ filePath: 'src/library/FullPageLibrary.tsx',
1314
+ },
1315
+ {
1316
+ sha: 'new-fpempty-sha',
1317
+ name: 'FullPageEmptyState',
1318
+ filePath: 'src/library/FullPageLibrary.tsx',
1319
+ },
1320
+ ]);
1321
+ expect(result.updated).toBe(2);
1322
+ const fplib = rawDb
1323
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1324
+ .get('sc-fplib');
1325
+ expect(fplib.entity_sha).toBe('new-fplib-sha');
1326
+ const fpempty = rawDb
1327
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1328
+ .get('sc-fpempty');
1329
+ expect(fpempty.entity_sha).toBe('new-fpempty-sha');
1330
+ });
1331
+ it('should prefer default export entity for page scenarios when file has multiple entities', async () => {
1332
+ // Bug: src/popup/App.tsx has multiple entities (App visual, getInitialView library,
1333
+ // View type). Page scenarios should link to the default export (App), not whichever
1334
+ // entity happens to come first in the array.
1335
+ rawDb
1336
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1337
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1338
+ .run('sc-page-1', projectId, 'Save View - First Time User', 'src/popup/App.tsx', '/', 'application', 400, 600);
1339
+ // Pass multiple entities for the same file — default export should win
1340
+ // regardless of order. Here the default export (App) appears first but
1341
+ // gets overwritten by later non-default entities in the naive set() impl.
1342
+ const result = await backfillEntityShaOnScenarios(db, [
1343
+ {
1344
+ sha: 'app-sha',
1345
+ name: 'App',
1346
+ filePath: 'src/popup/App.tsx',
1347
+ isDefaultExport: true,
1348
+ },
1349
+ {
1350
+ sha: 'get-initial-view-sha',
1351
+ name: 'getInitialView',
1352
+ filePath: 'src/popup/App.tsx',
1353
+ isDefaultExport: false,
1354
+ },
1355
+ {
1356
+ sha: 'view-type-sha',
1357
+ name: 'View',
1358
+ filePath: 'src/popup/App.tsx',
1359
+ isDefaultExport: false,
1360
+ },
1361
+ ]);
1362
+ expect(result.updated).toBe(1);
1363
+ const row = rawDb
1364
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1365
+ .get('sc-page-1');
1366
+ expect(row.entity_sha).toBe('app-sha');
1367
+ });
1368
+ it('should update page scenario to latest entity version when multiple versions exist', async () => {
1369
+ // Real-world bug: App.tsx entity gets versioned (code changes create new SHAs).
1370
+ // The scenario points to the oldest version's SHA. When sync receives all 3
1371
+ // versions of the same default-export entity, it should update the scenario
1372
+ // to the LATEST version, not keep it stuck on the oldest.
1373
+ rawDb
1374
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1375
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1376
+ .run('sc-home', projectId, 'Welcome Screen — First Install', 'src/popup/App.tsx', '/', 'application', 400, 600, 'version-1-sha', // stale — points to oldest version
1377
+ 'Home');
1378
+ // Pass all 3 versions — all default exports for the same file.
1379
+ // The sync should pick the LAST one (latest) since callers pass
1380
+ // entities in creation order.
1381
+ const result = await backfillEntityShaOnScenarios(db, [
1382
+ {
1383
+ sha: 'version-1-sha',
1384
+ name: 'App',
1385
+ filePath: 'src/popup/App.tsx',
1386
+ isDefaultExport: true,
1387
+ },
1388
+ {
1389
+ sha: 'version-2-sha',
1390
+ name: 'App',
1391
+ filePath: 'src/popup/App.tsx',
1392
+ isDefaultExport: true,
1393
+ },
1394
+ {
1395
+ sha: 'version-3-sha',
1396
+ name: 'App',
1397
+ filePath: 'src/popup/App.tsx',
1398
+ isDefaultExport: true,
1399
+ },
1400
+ ]);
1401
+ expect(result.updated).toBe(1);
1402
+ const row = rawDb
1403
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1404
+ .get('sc-home');
1405
+ expect(row.entity_sha).toBe('version-3-sha');
1406
+ });
1407
+ });
1408
+ describe('countScenariosNeedingEntityBackfill', () => {
1409
+ let db;
1410
+ let rawDb;
1411
+ const projectId = 'test-project-id';
1412
+ beforeEach(async () => {
1413
+ rawDb = new Database(':memory:');
1414
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1415
+ await db.schema
1416
+ .createTable('editor_scenarios')
1417
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1418
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1419
+ .addColumn('name', 'varchar', (col) => col.notNull())
1420
+ .addColumn('description', 'text')
1421
+ .addColumn('component_name', 'varchar')
1422
+ .addColumn('component_path', 'varchar')
1423
+ .addColumn('url', 'varchar')
1424
+ .addColumn('type', 'varchar')
1425
+ .addColumn('screenshot_path', 'varchar')
1426
+ .addColumn('viewport_width', 'integer')
1427
+ .addColumn('viewport_height', 'integer')
1428
+ .addColumn('dimension', 'varchar')
1429
+ .addColumn('dimensions', 'text')
1430
+ .addColumn('screenshot_paths', 'text')
1431
+ .addColumn('page_file_path', 'varchar')
1432
+ .addColumn('entity_sha', 'varchar')
1433
+ .addColumn('display_name', 'varchar')
1434
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1435
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1436
+ .execute();
1437
+ });
1438
+ afterEach(async () => {
1439
+ await db.destroy();
1440
+ });
1441
+ it('should NOT count scenarios that already have entity_sha', async () => {
1442
+ rawDb
1443
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1444
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1445
+ .run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'existing-sha');
1446
+ const count = await countScenariosNeedingEntityBackfill(db);
1447
+ expect(count).toBe(0);
1448
+ });
1449
+ it('should count scenarios with null entity_sha and page_file_path', async () => {
1450
+ rawDb
1451
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1452
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1453
+ .run('sc-2', projectId, 'Home - Default', 'app/page.tsx', '/', 'application', 1280, 720);
1454
+ const count = await countScenariosNeedingEntityBackfill(db);
1455
+ expect(count).toBe(1);
1456
+ });
1457
+ it('should count scenarios with null entity_sha and component_path', async () => {
1458
+ rawDb
1459
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1460
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1461
+ .run('sc-3', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
1462
+ const count = await countScenariosNeedingEntityBackfill(db);
1463
+ expect(count).toBe(1);
1464
+ });
1465
+ it('should NOT count scenarios with null entity_sha and no file paths', async () => {
1466
+ rawDb
1467
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, url, type, viewport_width, viewport_height)
1468
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
1469
+ .run('sc-4', projectId, 'No File Path', '/', 'application', 1280, 720);
1470
+ const count = await countScenariosNeedingEntityBackfill(db);
1471
+ expect(count).toBe(0);
1472
+ });
1473
+ });
1474
+ describe('validateEntityLinkageForAppScenario', () => {
1475
+ let tmpDir;
1476
+ let glossaryPath;
1477
+ beforeEach(() => {
1478
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entity-linkage-'));
1479
+ const codeyamDir = path.join(tmpDir, '.codeyam');
1480
+ fs.mkdirSync(codeyamDir, { recursive: true });
1481
+ glossaryPath = path.join(codeyamDir, 'glossary.json');
1482
+ });
1483
+ afterEach(() => {
1484
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1485
+ });
1486
+ it('should return error when file is not in glossary', () => {
1487
+ // Write a glossary that does NOT include the target file
1488
+ fs.writeFileSync(glossaryPath, JSON.stringify([
1489
+ { name: 'Header', filePath: 'src/components/Header.tsx' },
1490
+ ]));
1491
+ const result = validateEntityLinkageForAppScenario({
1492
+ lookupFilePath: 'src/App.tsx',
1493
+ scenarioType: 'application',
1494
+ projectRoot: tmpDir,
1495
+ });
1496
+ expect(result.valid).toBe(false);
1497
+ expect(result.error).toContain('No glossary entry found');
1498
+ expect(result.error).toContain('src/App.tsx');
1499
+ });
1500
+ it('should return needsAnalysis when file IS in glossary but no entity exists', () => {
1501
+ // Write a glossary that includes the target file
1502
+ fs.writeFileSync(glossaryPath, JSON.stringify([
1503
+ { name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
1504
+ ]));
1505
+ const result = validateEntityLinkageForAppScenario({
1506
+ lookupFilePath: 'src/App.tsx',
1507
+ scenarioType: 'application',
1508
+ projectRoot: tmpDir,
1509
+ });
1510
+ expect(result.valid).toBe(true);
1511
+ expect(result.needsAnalysis).toBe(true);
1512
+ });
1513
+ it('should skip validation for component-type scenarios', () => {
1514
+ // No glossary file exists at all
1515
+ const result = validateEntityLinkageForAppScenario({
1516
+ lookupFilePath: 'src/components/Header.tsx',
1517
+ scenarioType: 'component',
1518
+ projectRoot: tmpDir,
1519
+ });
1520
+ expect(result.valid).toBe(true);
1521
+ expect(result.needsAnalysis).toBeUndefined();
1522
+ });
1523
+ it('should skip validation when lookupFilePath is null', () => {
1524
+ const result = validateEntityLinkageForAppScenario({
1525
+ lookupFilePath: null,
1526
+ scenarioType: 'application',
1527
+ projectRoot: tmpDir,
1528
+ });
1529
+ expect(result.valid).toBe(true);
1530
+ expect(result.needsAnalysis).toBeUndefined();
1531
+ });
1532
+ it('should handle glossary wrapped in object format', () => {
1533
+ // LLMs sometimes write glossary as {"components": [...]}
1534
+ fs.writeFileSync(glossaryPath, JSON.stringify({
1535
+ components: [
1536
+ { name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
1537
+ ],
1538
+ }));
1539
+ const result = validateEntityLinkageForAppScenario({
1540
+ lookupFilePath: 'src/App.tsx',
1541
+ scenarioType: 'user',
1542
+ projectRoot: tmpDir,
1543
+ });
1544
+ expect(result.valid).toBe(true);
1545
+ expect(result.needsAnalysis).toBe(true);
1546
+ });
1547
+ it('should return error when glossary file does not exist', () => {
1548
+ // Don't create a glossary file
1549
+ const result = validateEntityLinkageForAppScenario({
1550
+ lookupFilePath: 'src/App.tsx',
1551
+ scenarioType: 'application',
1552
+ projectRoot: tmpDir,
1553
+ });
1554
+ expect(result.valid).toBe(false);
1555
+ expect(result.error).toContain('No glossary entry found');
1556
+ });
1557
+ });
1558
+ describe('validateScenarioCategorization', () => {
1559
+ it('should reject componentName with non-isolation URL', () => {
1560
+ const result = validateScenarioCategorization({
1561
+ componentName: 'Library',
1562
+ url: '/library',
1563
+ });
1564
+ expect(result.valid).toBe(false);
1565
+ expect(result.error).toBeDefined();
1566
+ expect(result.error).toContain('componentName');
1567
+ });
1568
+ it('should accept componentName with /isolated-components/ URL', () => {
1569
+ const result = validateScenarioCategorization({
1570
+ componentName: 'Library',
1571
+ url: '/isolated-components/Library?s=Default',
1572
+ });
1573
+ expect(result.valid).toBe(true);
1574
+ });
1575
+ it('should accept componentName with /codeyam-isolate/ URL', () => {
1576
+ const result = validateScenarioCategorization({
1577
+ componentName: 'Library',
1578
+ url: '/codeyam-isolate/Library?s=Default',
1579
+ });
1580
+ expect(result.valid).toBe(true);
1581
+ });
1582
+ it('should accept application scenario without componentName', () => {
1583
+ const result = validateScenarioCategorization({
1584
+ url: '/library',
1585
+ type: 'application',
1586
+ });
1587
+ expect(result.valid).toBe(true);
1588
+ });
1589
+ it('should accept component with no URL', () => {
1590
+ const result = validateScenarioCategorization({
1591
+ componentName: 'Library',
1592
+ url: null,
1593
+ });
1594
+ expect(result.valid).toBe(true);
1595
+ });
1596
+ it('should suggest both fixes in error message', () => {
1597
+ const result = validateScenarioCategorization({
1598
+ componentName: 'Library',
1599
+ url: '/library',
1600
+ });
1601
+ expect(result.valid).toBe(false);
1602
+ // Should suggest changing URL to isolation route
1603
+ expect(result.error).toContain('/isolated-components/');
1604
+ // Should suggest removing componentName
1605
+ expect(result.error).toContain('remove');
1606
+ });
1607
+ });
274
1608
  });
275
1609
  //# sourceMappingURL=editorScenarios.test.js.map