@codeyam/codeyam-cli 0.1.0-staging.8df382d → 0.1.0-staging.9574237

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 (656) 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 +10 -10
  4. package/analyzer-template/packages/ai/package.json +2 -2
  5. package/analyzer-template/packages/ai/src/lib/astScopes/astScopeAnalyzer.ts +34 -3
  6. package/analyzer-template/packages/ai/src/lib/completionCall.ts +14 -2
  7. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +27 -0
  8. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/coercePrimitivesToArraysBySchema.ts +62 -0
  9. package/analyzer-template/packages/ai/src/lib/generateEntityScenarioData.ts +78 -2
  10. package/analyzer-template/packages/ai/src/lib/generateExecutionFlows.ts +0 -33
  11. package/analyzer-template/packages/analyze/src/lib/ProjectAnalyzer.ts +19 -7
  12. package/analyzer-template/packages/analyze/src/lib/asts/index.ts +7 -2
  13. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +9 -1
  14. package/analyzer-template/packages/analyze/src/lib/files/analyze/dependencyResolver.ts +0 -6
  15. package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +12 -0
  16. package/analyzer-template/packages/analyze/src/lib/files/scenarios/TransformationTracer.ts +65 -28
  17. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +83 -0
  18. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.ts +0 -98
  19. package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +23 -4
  20. package/analyzer-template/packages/aws/package.json +2 -2
  21. package/analyzer-template/packages/database/index.ts +1 -0
  22. package/analyzer-template/packages/database/package.json +3 -3
  23. package/analyzer-template/packages/database/src/lib/kysely/db.ts +8 -0
  24. package/analyzer-template/packages/database/src/lib/kysely/tables/editorScenariosTable.ts +164 -0
  25. package/analyzer-template/packages/database/src/lib/loadCommits.ts +31 -20
  26. package/analyzer-template/packages/database/src/lib/loadEntities.ts +0 -6
  27. package/analyzer-template/packages/database/src/lib/loadReadyToBeCapturedAnalyses.ts +0 -5
  28. package/analyzer-template/packages/database/src/lib/updateCommitMetadata.ts +94 -143
  29. package/analyzer-template/packages/database/src/lib/updateFreshAnalysisStatus.ts +58 -42
  30. package/analyzer-template/packages/database/src/lib/updateFreshAnalysisStatusWithScenarios.ts +81 -65
  31. package/analyzer-template/packages/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.ts +29 -1
  32. package/analyzer-template/packages/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.ts +33 -5
  33. package/analyzer-template/packages/github/dist/database/index.d.ts +1 -0
  34. package/analyzer-template/packages/github/dist/database/index.d.ts.map +1 -1
  35. package/analyzer-template/packages/github/dist/database/index.js +1 -0
  36. package/analyzer-template/packages/github/dist/database/index.js.map +1 -1
  37. package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.d.ts +2 -0
  38. package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.d.ts.map +1 -1
  39. package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.js +5 -0
  40. package/analyzer-template/packages/github/dist/database/src/lib/kysely/db.js.map +1 -1
  41. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts +29 -0
  42. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.d.ts.map +1 -0
  43. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js +149 -0
  44. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -0
  45. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/scenariosTable.d.ts +5 -0
  46. package/analyzer-template/packages/github/dist/database/src/lib/kysely/tables/scenariosTable.d.ts.map +1 -1
  47. package/analyzer-template/packages/github/dist/database/src/lib/loadCommits.d.ts.map +1 -1
  48. package/analyzer-template/packages/github/dist/database/src/lib/loadCommits.js +23 -13
  49. package/analyzer-template/packages/github/dist/database/src/lib/loadCommits.js.map +1 -1
  50. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.d.ts.map +1 -1
  51. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js +0 -6
  52. package/analyzer-template/packages/github/dist/database/src/lib/loadEntities.js.map +1 -1
  53. package/analyzer-template/packages/github/dist/database/src/lib/loadReadyToBeCapturedAnalyses.d.ts.map +1 -1
  54. package/analyzer-template/packages/github/dist/database/src/lib/loadReadyToBeCapturedAnalyses.js +1 -4
  55. package/analyzer-template/packages/github/dist/database/src/lib/loadReadyToBeCapturedAnalyses.js.map +1 -1
  56. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.d.ts.map +1 -1
  57. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js +76 -90
  58. package/analyzer-template/packages/github/dist/database/src/lib/updateCommitMetadata.js.map +1 -1
  59. package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatus.d.ts.map +1 -1
  60. package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatus.js +41 -30
  61. package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatus.js.map +1 -1
  62. package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatusWithScenarios.d.ts.map +1 -1
  63. package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatusWithScenarios.js +68 -57
  64. package/analyzer-template/packages/github/dist/database/src/lib/updateFreshAnalysisStatusWithScenarios.js.map +1 -1
  65. package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.d.ts.map +1 -1
  66. package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js +29 -1
  67. package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js.map +1 -1
  68. package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.d.ts.map +1 -1
  69. package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js +33 -5
  70. package/analyzer-template/packages/github/dist/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js.map +1 -1
  71. package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.d.ts +2 -0
  72. package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.d.ts.map +1 -1
  73. package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.js +2 -0
  74. package/analyzer-template/packages/github/dist/types/src/enums/ProjectFramework.js.map +1 -1
  75. package/analyzer-template/packages/github/dist/types/src/types/ProjectMetadata.d.ts +1 -0
  76. package/analyzer-template/packages/github/dist/types/src/types/ProjectMetadata.d.ts.map +1 -1
  77. package/analyzer-template/packages/github/dist/types/src/types/Scenario.d.ts +10 -0
  78. package/analyzer-template/packages/github/dist/types/src/types/Scenario.d.ts.map +1 -1
  79. package/analyzer-template/packages/github/package.json +1 -1
  80. package/analyzer-template/packages/types/src/enums/ProjectFramework.ts +2 -0
  81. package/analyzer-template/packages/types/src/types/ProjectMetadata.ts +1 -0
  82. package/analyzer-template/packages/types/src/types/Scenario.ts +10 -0
  83. package/analyzer-template/packages/ui-components/package.json +1 -1
  84. package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.d.ts +2 -0
  85. package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.d.ts.map +1 -1
  86. package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.js +2 -0
  87. package/analyzer-template/packages/utils/dist/types/src/enums/ProjectFramework.js.map +1 -1
  88. package/analyzer-template/packages/utils/dist/types/src/types/ProjectMetadata.d.ts +1 -0
  89. package/analyzer-template/packages/utils/dist/types/src/types/ProjectMetadata.d.ts.map +1 -1
  90. package/analyzer-template/packages/utils/dist/types/src/types/Scenario.d.ts +10 -0
  91. package/analyzer-template/packages/utils/dist/types/src/types/Scenario.d.ts.map +1 -1
  92. package/analyzer-template/playwright/captureFromUrl.ts +89 -82
  93. package/analyzer-template/project/constructMockCode.ts +136 -43
  94. package/analyzer-template/project/reconcileMockDataKeys.ts +19 -14
  95. package/analyzer-template/project/start.ts +3 -0
  96. package/analyzer-template/project/startScenarioCapture.ts +9 -0
  97. package/analyzer-template/project/writeClientLogRoute.ts +125 -0
  98. package/analyzer-template/project/writeMockDataTsx.ts +17 -0
  99. package/analyzer-template/project/writeScenarioComponents.ts +36 -7
  100. package/analyzer-template/tsconfig.json +13 -1
  101. package/background/src/lib/virtualized/project/constructMockCode.js +115 -34
  102. package/background/src/lib/virtualized/project/constructMockCode.js.map +1 -1
  103. package/background/src/lib/virtualized/project/reconcileMockDataKeys.js +17 -11
  104. package/background/src/lib/virtualized/project/reconcileMockDataKeys.js.map +1 -1
  105. package/background/src/lib/virtualized/project/start.js +2 -0
  106. package/background/src/lib/virtualized/project/start.js.map +1 -1
  107. package/background/src/lib/virtualized/project/startScenarioCapture.js +5 -0
  108. package/background/src/lib/virtualized/project/startScenarioCapture.js.map +1 -1
  109. package/background/src/lib/virtualized/project/writeClientLogRoute.js +110 -0
  110. package/background/src/lib/virtualized/project/writeClientLogRoute.js.map +1 -0
  111. package/background/src/lib/virtualized/project/writeMockDataTsx.js +12 -0
  112. package/background/src/lib/virtualized/project/writeMockDataTsx.js.map +1 -1
  113. package/background/src/lib/virtualized/project/writeScenarioComponents.js +29 -7
  114. package/background/src/lib/virtualized/project/writeScenarioComponents.js.map +1 -1
  115. package/codeyam-cli/scripts/apply-setup.js +208 -11
  116. package/codeyam-cli/scripts/apply-setup.js.map +1 -1
  117. package/codeyam-cli/src/__tests__/memory-scripts/filter-session.test.js +196 -0
  118. package/codeyam-cli/src/__tests__/memory-scripts/filter-session.test.js.map +1 -0
  119. package/codeyam-cli/src/__tests__/memory-scripts/read-json-field.test.js +114 -0
  120. package/codeyam-cli/src/__tests__/memory-scripts/read-json-field.test.js.map +1 -0
  121. package/codeyam-cli/src/__tests__/memory-scripts/ripgrep-fallback.test.js +149 -0
  122. package/codeyam-cli/src/__tests__/memory-scripts/ripgrep-fallback.test.js.map +1 -0
  123. package/codeyam-cli/src/cli.js +2 -0
  124. package/codeyam-cli/src/cli.js.map +1 -1
  125. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js +56 -0
  126. package/codeyam-cli/src/commands/__tests__/editor.stepDispatch.test.js.map +1 -0
  127. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +101 -47
  128. package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
  129. package/codeyam-cli/src/commands/analyze.js +17 -7
  130. package/codeyam-cli/src/commands/analyze.js.map +1 -1
  131. package/codeyam-cli/src/commands/default.js +14 -2
  132. package/codeyam-cli/src/commands/default.js.map +1 -1
  133. package/codeyam-cli/src/commands/editor.js +4283 -0
  134. package/codeyam-cli/src/commands/editor.js.map +1 -0
  135. package/codeyam-cli/src/commands/init.js +108 -45
  136. package/codeyam-cli/src/commands/init.js.map +1 -1
  137. package/codeyam-cli/src/commands/memory.js +26 -2
  138. package/codeyam-cli/src/commands/memory.js.map +1 -1
  139. package/codeyam-cli/src/data/techStacks.js +77 -0
  140. package/codeyam-cli/src/data/techStacks.js.map +1 -0
  141. package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js +173 -0
  142. package/codeyam-cli/src/utils/__tests__/analyzerFinalization.test.js.map +1 -0
  143. package/codeyam-cli/src/utils/__tests__/backgroundServer.test.js +46 -0
  144. package/codeyam-cli/src/utils/__tests__/backgroundServer.test.js.map +1 -0
  145. package/codeyam-cli/src/utils/__tests__/devServerState.test.js +134 -0
  146. package/codeyam-cli/src/utils/__tests__/devServerState.test.js.map +1 -0
  147. package/codeyam-cli/src/utils/__tests__/editorApi.test.js +137 -0
  148. package/codeyam-cli/src/utils/__tests__/editorApi.test.js.map +1 -0
  149. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +987 -0
  150. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -0
  151. package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js +76 -0
  152. package/codeyam-cli/src/utils/__tests__/editorBroadcastViewport.test.js.map +1 -0
  153. package/codeyam-cli/src/utils/__tests__/editorCapture.test.js +93 -0
  154. package/codeyam-cli/src/utils/__tests__/editorCapture.test.js.map +1 -0
  155. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js +100 -0
  156. package/codeyam-cli/src/utils/__tests__/editorDeleteScenario.test.js.map +1 -0
  157. package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js +304 -0
  158. package/codeyam-cli/src/utils/__tests__/editorDevServer.test.js.map +1 -0
  159. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js +124 -0
  160. package/codeyam-cli/src/utils/__tests__/editorEntityChangeStatus.test.js.map +1 -0
  161. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js +222 -0
  162. package/codeyam-cli/src/utils/__tests__/editorEntityHelpers.test.js.map +1 -0
  163. package/codeyam-cli/src/utils/__tests__/editorImageVerifier.test.js +294 -0
  164. package/codeyam-cli/src/utils/__tests__/editorImageVerifier.test.js.map +1 -0
  165. package/codeyam-cli/src/utils/__tests__/editorJournal.test.js +542 -0
  166. package/codeyam-cli/src/utils/__tests__/editorJournal.test.js.map +1 -0
  167. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js +594 -0
  168. package/codeyam-cli/src/utils/__tests__/editorLoaderHelpers.test.js.map +1 -0
  169. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js +435 -0
  170. package/codeyam-cli/src/utils/__tests__/editorMigration.test.js.map +1 -0
  171. package/codeyam-cli/src/utils/__tests__/editorMockState.test.js +270 -0
  172. package/codeyam-cli/src/utils/__tests__/editorMockState.test.js.map +1 -0
  173. package/codeyam-cli/src/utils/__tests__/editorPreloadHelpers.test.js +217 -0
  174. package/codeyam-cli/src/utils/__tests__/editorPreloadHelpers.test.js.map +1 -0
  175. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +353 -0
  176. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -0
  177. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +153 -0
  178. package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -0
  179. package/codeyam-cli/src/utils/__tests__/editorScenarioLookup.test.js +139 -0
  180. package/codeyam-cli/src/utils/__tests__/editorScenarioLookup.test.js.map +1 -0
  181. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js +221 -0
  182. package/codeyam-cli/src/utils/__tests__/editorScenarioSwitch.test.js.map +1 -0
  183. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +1431 -0
  184. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -0
  185. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js +280 -0
  186. package/codeyam-cli/src/utils/__tests__/editorSeedAdapter.test.js.map +1 -0
  187. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js +143 -0
  188. package/codeyam-cli/src/utils/__tests__/editorSeedAdapterPrismaValidation.test.js.map +1 -0
  189. package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js +66 -0
  190. package/codeyam-cli/src/utils/__tests__/editorSessionFilter.test.js.map +1 -0
  191. package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js +53 -0
  192. package/codeyam-cli/src/utils/__tests__/editorShouldRevalidate.test.js.map +1 -0
  193. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +1829 -0
  194. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -0
  195. package/codeyam-cli/src/utils/__tests__/git.editor.test.js +134 -0
  196. package/codeyam-cli/src/utils/__tests__/git.editor.test.js.map +1 -0
  197. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js +107 -0
  198. package/codeyam-cli/src/utils/__tests__/journalCaptureStabilization.test.js.map +1 -0
  199. package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js +129 -0
  200. package/codeyam-cli/src/utils/__tests__/parseRegisterArg.test.js.map +1 -0
  201. package/codeyam-cli/src/utils/__tests__/pathIgnoring.test.js +9 -0
  202. package/codeyam-cli/src/utils/__tests__/pathIgnoring.test.js.map +1 -1
  203. package/codeyam-cli/src/utils/__tests__/project.test.js +65 -0
  204. package/codeyam-cli/src/utils/__tests__/project.test.js.map +1 -0
  205. package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js +118 -0
  206. package/codeyam-cli/src/utils/__tests__/routePatternMatching.test.js.map +1 -0
  207. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js +227 -0
  208. package/codeyam-cli/src/utils/__tests__/scenarioCoverage.test.js.map +1 -0
  209. package/codeyam-cli/src/utils/__tests__/scenarioMarkers.test.js +121 -0
  210. package/codeyam-cli/src/utils/__tests__/scenarioMarkers.test.js.map +1 -0
  211. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js +493 -0
  212. package/codeyam-cli/src/utils/__tests__/scenariosManifest.test.js.map +1 -0
  213. package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js +51 -4
  214. package/codeyam-cli/src/utils/__tests__/setupClaudeCodeSettings.test.js.map +1 -1
  215. package/codeyam-cli/src/utils/__tests__/templateConsistency.test.js +51 -0
  216. package/codeyam-cli/src/utils/__tests__/templateConsistency.test.js.map +1 -0
  217. package/codeyam-cli/src/utils/__tests__/webappDetection.test.js +142 -0
  218. package/codeyam-cli/src/utils/__tests__/webappDetection.test.js.map +1 -0
  219. package/codeyam-cli/src/utils/analysisRunner.js +3 -1
  220. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  221. package/codeyam-cli/src/utils/analyzer.js +9 -0
  222. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  223. package/codeyam-cli/src/utils/analyzerFinalization.js +100 -0
  224. package/codeyam-cli/src/utils/analyzerFinalization.js.map +1 -0
  225. package/codeyam-cli/src/utils/backgroundServer.js +104 -12
  226. package/codeyam-cli/src/utils/backgroundServer.js.map +1 -1
  227. package/codeyam-cli/src/utils/buildFlags.js +4 -0
  228. package/codeyam-cli/src/utils/buildFlags.js.map +1 -0
  229. package/codeyam-cli/src/utils/database.js +37 -2
  230. package/codeyam-cli/src/utils/database.js.map +1 -1
  231. package/codeyam-cli/src/utils/devModeEvents.js +40 -0
  232. package/codeyam-cli/src/utils/devModeEvents.js.map +1 -0
  233. package/codeyam-cli/src/utils/devServerState.js +71 -0
  234. package/codeyam-cli/src/utils/devServerState.js.map +1 -0
  235. package/codeyam-cli/src/utils/editorApi.js +79 -0
  236. package/codeyam-cli/src/utils/editorApi.js.map +1 -0
  237. package/codeyam-cli/src/utils/editorAudit.js +210 -0
  238. package/codeyam-cli/src/utils/editorAudit.js.map +1 -0
  239. package/codeyam-cli/src/utils/editorBroadcastViewport.js +26 -0
  240. package/codeyam-cli/src/utils/editorBroadcastViewport.js.map +1 -0
  241. package/codeyam-cli/src/utils/editorCapture.js +102 -0
  242. package/codeyam-cli/src/utils/editorCapture.js.map +1 -0
  243. package/codeyam-cli/src/utils/editorDeleteScenario.js +67 -0
  244. package/codeyam-cli/src/utils/editorDeleteScenario.js.map +1 -0
  245. package/codeyam-cli/src/utils/editorDevServer.js +197 -0
  246. package/codeyam-cli/src/utils/editorDevServer.js.map +1 -0
  247. package/codeyam-cli/src/utils/editorEntityChangeStatus.js +44 -0
  248. package/codeyam-cli/src/utils/editorEntityChangeStatus.js.map +1 -0
  249. package/codeyam-cli/src/utils/editorEntityHelpers.js +129 -0
  250. package/codeyam-cli/src/utils/editorEntityHelpers.js.map +1 -0
  251. package/codeyam-cli/src/utils/editorImageVerifier.js +155 -0
  252. package/codeyam-cli/src/utils/editorImageVerifier.js.map +1 -0
  253. package/codeyam-cli/src/utils/editorJournal.js +225 -0
  254. package/codeyam-cli/src/utils/editorJournal.js.map +1 -0
  255. package/codeyam-cli/src/utils/editorLoaderHelpers.js +152 -0
  256. package/codeyam-cli/src/utils/editorLoaderHelpers.js.map +1 -0
  257. package/codeyam-cli/src/utils/editorMigration.js +224 -0
  258. package/codeyam-cli/src/utils/editorMigration.js.map +1 -0
  259. package/codeyam-cli/src/utils/editorMockState.js +248 -0
  260. package/codeyam-cli/src/utils/editorMockState.js.map +1 -0
  261. package/codeyam-cli/src/utils/editorPreloadHelpers.js +135 -0
  262. package/codeyam-cli/src/utils/editorPreloadHelpers.js.map +1 -0
  263. package/codeyam-cli/src/utils/editorPreview.js +137 -0
  264. package/codeyam-cli/src/utils/editorPreview.js.map +1 -0
  265. package/codeyam-cli/src/utils/editorScenarioSwitch.js +112 -0
  266. package/codeyam-cli/src/utils/editorScenarioSwitch.js.map +1 -0
  267. package/codeyam-cli/src/utils/editorScenarios.js +523 -0
  268. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -0
  269. package/codeyam-cli/src/utils/editorSeedAdapter.js +422 -0
  270. package/codeyam-cli/src/utils/editorSeedAdapter.js.map +1 -0
  271. package/codeyam-cli/src/utils/editorShouldRevalidate.js +21 -0
  272. package/codeyam-cli/src/utils/editorShouldRevalidate.js.map +1 -0
  273. package/codeyam-cli/src/utils/entityChangeStatus.js +360 -0
  274. package/codeyam-cli/src/utils/entityChangeStatus.js.map +1 -0
  275. package/codeyam-cli/src/utils/entityChangeStatus.server.js +196 -0
  276. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -0
  277. package/codeyam-cli/src/utils/fileMetadata.js +5 -0
  278. package/codeyam-cli/src/utils/fileMetadata.js.map +1 -1
  279. package/codeyam-cli/src/utils/fileWatcher.js +25 -9
  280. package/codeyam-cli/src/utils/fileWatcher.js.map +1 -1
  281. package/codeyam-cli/src/utils/git.js +103 -0
  282. package/codeyam-cli/src/utils/git.js.map +1 -1
  283. package/codeyam-cli/src/utils/install-skills.js +64 -13
  284. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  285. package/codeyam-cli/src/utils/interactiveSyncWatcher.js +126 -0
  286. package/codeyam-cli/src/utils/interactiveSyncWatcher.js.map +1 -0
  287. package/codeyam-cli/src/utils/parseRegisterArg.js +31 -0
  288. package/codeyam-cli/src/utils/parseRegisterArg.js.map +1 -0
  289. package/codeyam-cli/src/utils/pathIgnoring.js +19 -7
  290. package/codeyam-cli/src/utils/pathIgnoring.js.map +1 -1
  291. package/codeyam-cli/src/utils/progress.js +2 -2
  292. package/codeyam-cli/src/utils/progress.js.map +1 -1
  293. package/codeyam-cli/src/utils/project.js +15 -5
  294. package/codeyam-cli/src/utils/project.js.map +1 -1
  295. package/codeyam-cli/src/utils/queue/__tests__/heartbeat.test.js +11 -11
  296. package/codeyam-cli/src/utils/queue/__tests__/heartbeat.test.js.map +1 -1
  297. package/codeyam-cli/src/utils/queue/__tests__/manager.test.js +22 -0
  298. package/codeyam-cli/src/utils/queue/__tests__/manager.test.js.map +1 -1
  299. package/codeyam-cli/src/utils/queue/heartbeat.js +13 -5
  300. package/codeyam-cli/src/utils/queue/heartbeat.js.map +1 -1
  301. package/codeyam-cli/src/utils/queue/job.js +70 -1
  302. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  303. package/codeyam-cli/src/utils/queue/manager.js +7 -6
  304. package/codeyam-cli/src/utils/queue/manager.js.map +1 -1
  305. package/codeyam-cli/src/utils/routePatternMatching.js +129 -0
  306. package/codeyam-cli/src/utils/routePatternMatching.js.map +1 -0
  307. package/codeyam-cli/src/utils/scenarioCoverage.js +74 -0
  308. package/codeyam-cli/src/utils/scenarioCoverage.js.map +1 -0
  309. package/codeyam-cli/src/utils/scenarioMarkers.js +134 -0
  310. package/codeyam-cli/src/utils/scenarioMarkers.js.map +1 -0
  311. package/codeyam-cli/src/utils/scenariosManifest.js +249 -0
  312. package/codeyam-cli/src/utils/scenariosManifest.js.map +1 -0
  313. package/codeyam-cli/src/utils/serverState.js +57 -2
  314. package/codeyam-cli/src/utils/serverState.js.map +1 -1
  315. package/codeyam-cli/src/utils/setupClaudeCodeSettings.js +83 -11
  316. package/codeyam-cli/src/utils/setupClaudeCodeSettings.js.map +1 -1
  317. package/codeyam-cli/src/utils/simulationGateMiddleware.js +8 -1
  318. package/codeyam-cli/src/utils/simulationGateMiddleware.js.map +1 -1
  319. package/codeyam-cli/src/utils/slugUtils.js +25 -0
  320. package/codeyam-cli/src/utils/slugUtils.js.map +1 -0
  321. package/codeyam-cli/src/utils/syncMocksMiddleware.js +2 -2
  322. package/codeyam-cli/src/utils/syncMocksMiddleware.js.map +1 -1
  323. package/codeyam-cli/src/utils/testRunner.js +158 -0
  324. package/codeyam-cli/src/utils/testRunner.js.map +1 -0
  325. package/codeyam-cli/src/utils/transcriptPruning.js +67 -0
  326. package/codeyam-cli/src/utils/transcriptPruning.js.map +1 -0
  327. package/codeyam-cli/src/utils/webappDetection.js +35 -2
  328. package/codeyam-cli/src/utils/webappDetection.js.map +1 -1
  329. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +35 -0
  330. package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -0
  331. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +40 -0
  332. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -0
  333. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +567 -0
  334. package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -0
  335. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +146 -0
  336. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -0
  337. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +65 -0
  338. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -0
  339. package/codeyam-cli/src/webserver/app/lib/database.js +41 -27
  340. package/codeyam-cli/src/webserver/app/lib/database.js.map +1 -1
  341. package/codeyam-cli/src/webserver/app/lib/dbNotifier.js.map +1 -1
  342. package/codeyam-cli/src/webserver/app/lib/git.js +397 -0
  343. package/codeyam-cli/src/webserver/app/lib/git.js.map +1 -0
  344. package/codeyam-cli/src/webserver/app/types/editor.js +8 -0
  345. package/codeyam-cli/src/webserver/app/types/editor.js.map +1 -0
  346. package/codeyam-cli/src/webserver/backgroundServer.js +125 -21
  347. package/codeyam-cli/src/webserver/backgroundServer.js.map +1 -1
  348. package/codeyam-cli/src/webserver/build/client/assets/CopyButton-CzTDWkF2.js +1 -0
  349. package/codeyam-cli/src/webserver/build/client/assets/{EntityItem-DlMph_Hm.js → EntityItem-BFbq6iFk.js} +5 -5
  350. package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-CQgyEGV-.js +1 -0
  351. package/codeyam-cli/src/webserver/build/client/assets/{EntityTypeIcon-DN9eiJAO.js → EntityTypeIcon-B6OMi58N.js} +9 -9
  352. package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-DuYodzo1.js +1 -0
  353. package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-CXo9EeCl.js +25 -0
  354. package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-DYCNb2It.js +3 -0
  355. package/codeyam-cli/src/webserver/build/client/assets/{LoadingDots-CSP6DZrh.js → LoadingDots-By5zI316.js} +1 -1
  356. package/codeyam-cli/src/webserver/build/client/assets/{LogViewer-CMK8Q7yk.js → LogViewer-CZgY3sxX.js} +3 -3
  357. package/codeyam-cli/src/webserver/build/client/assets/{ReportIssueModal-TCV_HBjy.js → ReportIssueModal-CnYYwRDw.js} +4 -4
  358. package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-CDoF7ZpU.js +1 -0
  359. package/codeyam-cli/src/webserver/build/client/assets/{ScenarioViewer-CU_TDYd8.js → ScenarioViewer-DrnfvaLL.js} +3 -3
  360. package/codeyam-cli/src/webserver/build/client/assets/Spinner-Df3UCi8k.js +34 -0
  361. package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-CK7-NaPZ.js +1 -0
  362. package/codeyam-cli/src/webserver/build/client/assets/ViewportInspectBar-DRKR9T0U.js +1 -0
  363. package/codeyam-cli/src/webserver/build/client/assets/{_index-B8z7mjR-.js → _index-ClR-g3tY.js} +4 -4
  364. package/codeyam-cli/src/webserver/build/client/assets/{activity.(_tab)-DZu78RI1.js → activity.(_tab)-DTH6ydEA.js} +8 -8
  365. package/codeyam-cli/src/webserver/build/client/assets/addon-canvas-DpzMmAy5.js +1 -0
  366. package/codeyam-cli/src/webserver/build/client/assets/addon-fit-YJmn1quW.js +12 -0
  367. package/codeyam-cli/src/webserver/build/client/assets/addon-web-links-74hnHF59.js +1 -0
  368. package/codeyam-cli/src/webserver/build/client/assets/addon-webgl-DI8QOUvO.js +58 -0
  369. package/codeyam-cli/src/webserver/build/client/assets/agent-transcripts-B8CYhCO9.js +22 -0
  370. package/codeyam-cli/src/webserver/build/client/assets/api.dev-mode-events-l0sNRNKZ.js +1 -0
  371. package/codeyam-cli/src/webserver/build/client/assets/api.editor-audit-l0sNRNKZ.js +1 -0
  372. package/codeyam-cli/src/webserver/build/client/assets/api.editor-capture-scenario-l0sNRNKZ.js +1 -0
  373. package/codeyam-cli/src/webserver/build/client/assets/api.editor-client-errors-l0sNRNKZ.js +1 -0
  374. package/codeyam-cli/src/webserver/build/client/assets/api.editor-commit-l0sNRNKZ.js +1 -0
  375. package/codeyam-cli/src/webserver/build/client/assets/api.editor-dev-server-l0sNRNKZ.js +1 -0
  376. package/codeyam-cli/src/webserver/build/client/assets/api.editor-entity-status-l0sNRNKZ.js +1 -0
  377. package/codeyam-cli/src/webserver/build/client/assets/api.editor-file-diff-l0sNRNKZ.js +1 -0
  378. package/codeyam-cli/src/webserver/build/client/assets/api.editor-file-l0sNRNKZ.js +1 -0
  379. package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-entry-l0sNRNKZ.js +1 -0
  380. package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-image._-l0sNRNKZ.js +1 -0
  381. package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-l0sNRNKZ.js +1 -0
  382. package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-screenshot-l0sNRNKZ.js +1 -0
  383. package/codeyam-cli/src/webserver/build/client/assets/api.editor-journal-update-l0sNRNKZ.js +1 -0
  384. package/codeyam-cli/src/webserver/build/client/assets/api.editor-load-commit-l0sNRNKZ.js +1 -0
  385. package/codeyam-cli/src/webserver/build/client/assets/api.editor-project-info-l0sNRNKZ.js +1 -0
  386. package/codeyam-cli/src/webserver/build/client/assets/api.editor-refresh-l0sNRNKZ.js +1 -0
  387. package/codeyam-cli/src/webserver/build/client/assets/api.editor-register-scenario-l0sNRNKZ.js +1 -0
  388. package/codeyam-cli/src/webserver/build/client/assets/api.editor-rename-scenario-l0sNRNKZ.js +1 -0
  389. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-seed-state-l0sNRNKZ.js +1 -0
  390. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-coverage-l0sNRNKZ.js +1 -0
  391. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-data-l0sNRNKZ.js +1 -0
  392. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-image._-l0sNRNKZ.js +1 -0
  393. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenario-prompt-l0sNRNKZ.js +1 -0
  394. package/codeyam-cli/src/webserver/build/client/assets/api.editor-scenarios-l0sNRNKZ.js +1 -0
  395. package/codeyam-cli/src/webserver/build/client/assets/api.editor-session-l0sNRNKZ.js +1 -0
  396. package/codeyam-cli/src/webserver/build/client/assets/api.editor-switch-scenario-l0sNRNKZ.js +1 -0
  397. package/codeyam-cli/src/webserver/build/client/assets/api.editor-test-results-l0sNRNKZ.js +1 -0
  398. package/codeyam-cli/src/webserver/build/client/assets/{book-open-Bp5FLkd4.js → book-open-CLaoh4ac.js} +2 -2
  399. package/codeyam-cli/src/webserver/build/client/assets/{chevron-down-DQJA9f4o.js → chevron-down-BZ2DZxbW.js} +2 -2
  400. package/codeyam-cli/src/webserver/build/client/assets/{chunk-JZWAC4HX-7VptmeIr.js → chunk-JZWAC4HX-BBXArFPl.js} +13 -21
  401. package/codeyam-cli/src/webserver/build/client/assets/{circle-check-B6C4LY9o.js → circle-check-CT4unAk-.js} +2 -2
  402. package/codeyam-cli/src/webserver/build/client/assets/{copy-6nzYCu0G.js → copy-zK0B6Nu-.js} +3 -3
  403. package/codeyam-cli/src/webserver/build/client/assets/createLucideIcon-DJB0YQJL.js +41 -0
  404. package/codeyam-cli/src/webserver/build/client/assets/dev.empty-CkXFP_i-.js +1 -0
  405. package/codeyam-cli/src/webserver/build/client/assets/editor._tab-DPw7NZHc.js +1 -0
  406. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Dmg9cGK3.js +58 -0
  407. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-DBa7T2FK.js +41 -0
  408. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._--zvFJ4OH.js → entity._sha._-BqAN7hyG.js} +11 -11
  409. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.dev-BOi8kpwd.js +6 -0
  410. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-Dg1NhIms.js +6 -0
  411. package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-CJX6kkkV.js +6 -0
  412. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha_.edit._scenarioId-C7ysA4Jq.js → entity._sha_.edit._scenarioId-BhVjZhKg.js} +2 -2
  413. package/codeyam-cli/src/webserver/build/client/assets/{entry.client-CU6EUArK.js → entry.client-_gzKltPN.js} +6 -6
  414. package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-Daa96Fr1.js +1 -0
  415. package/codeyam-cli/src/webserver/build/client/assets/files-CV_17tZS.js +1 -0
  416. package/codeyam-cli/src/webserver/build/client/assets/git-D-YXmMbR.js +1 -0
  417. package/codeyam-cli/src/webserver/build/client/assets/globals-Bqg9V6XV.css +1 -0
  418. package/codeyam-cli/src/webserver/build/client/assets/index-Blo6EK8G.js +15 -0
  419. package/codeyam-cli/src/webserver/build/client/assets/{index-7-1FmlHo.js → index-BsX0F-9C.js} +1 -1
  420. package/codeyam-cli/src/webserver/build/client/assets/{index-DuYcwYp_.js → index-CCrgCshv.js} +1 -1
  421. package/codeyam-cli/src/webserver/build/client/assets/jsx-runtime-D_zvdyIk.js +9 -0
  422. package/codeyam-cli/src/webserver/build/client/assets/labs-Byazq8Pv.js +1 -0
  423. package/codeyam-cli/src/webserver/build/client/assets/{loader-circle-BnDcD54R.js → loader-circle-DVQ0oHR7.js} +2 -2
  424. package/codeyam-cli/src/webserver/build/client/assets/manifest-422a3551.js +1 -0
  425. package/codeyam-cli/src/webserver/build/client/assets/memory-b-VmA2Vj.js +101 -0
  426. package/codeyam-cli/src/webserver/build/client/assets/{pause-DhQX2g22.js → pause-DGcndCAa.js} +3 -3
  427. package/codeyam-cli/src/webserver/build/client/assets/root-ue8uWVRS.js +67 -0
  428. package/codeyam-cli/src/webserver/build/client/assets/{search-DborVoKD.js → search-C0Uw0bcK.js} +2 -2
  429. package/codeyam-cli/src/webserver/build/client/assets/settings-OoNgHIfW.js +1 -0
  430. package/codeyam-cli/src/webserver/build/client/assets/simulations-Bcemfu8a.js +1 -0
  431. package/codeyam-cli/src/webserver/build/client/assets/{terminal-Bs4NC-VZ.js → terminal-BgMmG7R9.js} +3 -3
  432. package/codeyam-cli/src/webserver/build/client/assets/{triangle-alert-DTf3Jojp.js → triangle-alert-Cs87hJYK.js} +2 -2
  433. package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-BR3Rs7JY.js +1 -0
  434. package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-BxxP_XF9.js +2 -0
  435. package/codeyam-cli/src/webserver/build/client/assets/useReportContext-BermyNU5.js +1 -0
  436. package/codeyam-cli/src/webserver/build/client/assets/useToast-a_QN_W9_.js +1 -0
  437. package/codeyam-cli/src/webserver/build/client/assets/xterm-BqvuqXEL.js +27 -0
  438. package/codeyam-cli/src/webserver/build/client/sound-test.html +98 -0
  439. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-DXQyOV0G.js +13 -0
  440. package/codeyam-cli/src/webserver/build/server/assets/index-CHSrVJtC.js +1 -0
  441. package/codeyam-cli/src/webserver/build/server/assets/init-DL8vWZ6m.js +10 -0
  442. package/codeyam-cli/src/webserver/build/server/assets/progress-CHTtrxFG.js +1 -0
  443. package/codeyam-cli/src/webserver/build/server/assets/server-build-BUKVjBSZ.js +501 -0
  444. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  445. package/codeyam-cli/src/webserver/build-info.json +5 -5
  446. package/codeyam-cli/src/webserver/devServer.js +39 -5
  447. package/codeyam-cli/src/webserver/devServer.js.map +1 -1
  448. package/codeyam-cli/src/webserver/editorProxy.js +901 -0
  449. package/codeyam-cli/src/webserver/editorProxy.js.map +1 -0
  450. package/codeyam-cli/src/webserver/idleDetector.js +73 -0
  451. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -0
  452. package/codeyam-cli/src/webserver/mockStateEvents.js +28 -0
  453. package/codeyam-cli/src/webserver/mockStateEvents.js.map +1 -0
  454. package/codeyam-cli/src/webserver/public/sound-test.html +98 -0
  455. package/codeyam-cli/src/webserver/scripts/codeyam-preload.mjs +414 -0
  456. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +230 -0
  457. package/codeyam-cli/src/webserver/server.js +309 -1
  458. package/codeyam-cli/src/webserver/server.js.map +1 -1
  459. package/codeyam-cli/src/webserver/terminalServer.js +831 -0
  460. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -0
  461. package/codeyam-cli/templates/chrome-extension-react/EXTENSION_SETUP.md +75 -0
  462. package/codeyam-cli/templates/chrome-extension-react/README.md +46 -0
  463. package/codeyam-cli/templates/chrome-extension-react/gitignore +15 -0
  464. package/codeyam-cli/templates/chrome-extension-react/index.html +12 -0
  465. package/codeyam-cli/templates/chrome-extension-react/package.json +27 -0
  466. package/codeyam-cli/templates/chrome-extension-react/popup.html +12 -0
  467. package/codeyam-cli/templates/chrome-extension-react/public/manifest.json +15 -0
  468. package/codeyam-cli/templates/chrome-extension-react/src/background/service-worker.ts +7 -0
  469. package/codeyam-cli/templates/chrome-extension-react/src/globals.css +6 -0
  470. package/codeyam-cli/templates/chrome-extension-react/src/lib/storage.ts +37 -0
  471. package/codeyam-cli/templates/chrome-extension-react/src/popup/App.tsx +12 -0
  472. package/codeyam-cli/templates/chrome-extension-react/src/popup/main.tsx +10 -0
  473. package/codeyam-cli/templates/chrome-extension-react/tsconfig.json +24 -0
  474. package/codeyam-cli/templates/chrome-extension-react/vite.config.ts +41 -0
  475. package/codeyam-cli/templates/codeyam-editor-claude.md +147 -0
  476. package/codeyam-cli/templates/editor-step-hook.py +321 -0
  477. package/codeyam-cli/templates/expo-react-native/MOBILE_SETUP.md +89 -0
  478. package/codeyam-cli/templates/expo-react-native/README.md +41 -0
  479. package/codeyam-cli/templates/expo-react-native/app/(tabs)/_layout.tsx +33 -0
  480. package/codeyam-cli/templates/expo-react-native/app/(tabs)/index.tsx +12 -0
  481. package/codeyam-cli/templates/expo-react-native/app/(tabs)/settings.tsx +12 -0
  482. package/codeyam-cli/templates/expo-react-native/app/_layout.tsx +12 -0
  483. package/codeyam-cli/templates/expo-react-native/app.json +18 -0
  484. package/codeyam-cli/templates/expo-react-native/babel.config.js +9 -0
  485. package/codeyam-cli/templates/expo-react-native/gitignore +12 -0
  486. package/codeyam-cli/templates/expo-react-native/global.css +3 -0
  487. package/codeyam-cli/templates/expo-react-native/lib/storage.ts +32 -0
  488. package/codeyam-cli/templates/expo-react-native/metro.config.js +6 -0
  489. package/codeyam-cli/templates/expo-react-native/nativewind-env.d.ts +1 -0
  490. package/codeyam-cli/templates/expo-react-native/package.json +38 -0
  491. package/codeyam-cli/templates/expo-react-native/tailwind.config.js +10 -0
  492. package/codeyam-cli/templates/expo-react-native/tsconfig.json +10 -0
  493. package/codeyam-cli/templates/isolation-route/next-app.tsx.template +80 -0
  494. package/codeyam-cli/templates/isolation-route/next-pages.tsx.template +79 -0
  495. package/codeyam-cli/templates/isolation-route/vite-react.tsx.template +78 -0
  496. package/codeyam-cli/templates/msw/browser-setup.ts.template +47 -0
  497. package/codeyam-cli/templates/msw/handler-router.ts.template +47 -0
  498. package/codeyam-cli/templates/msw/server-setup.ts.template +52 -0
  499. package/codeyam-cli/templates/nextjs-prisma-sqlite/AUTH_PATTERNS.md +308 -0
  500. package/codeyam-cli/templates/nextjs-prisma-sqlite/AUTH_UPGRADE.md +304 -0
  501. package/codeyam-cli/templates/nextjs-prisma-sqlite/DATABASE.md +126 -0
  502. package/codeyam-cli/templates/nextjs-prisma-sqlite/FEATURE_PATTERNS.md +37 -0
  503. package/codeyam-cli/templates/nextjs-prisma-sqlite/README.md +53 -0
  504. package/codeyam-cli/templates/nextjs-prisma-sqlite/app/api/todos/route.ts +17 -0
  505. package/codeyam-cli/templates/nextjs-prisma-sqlite/app/codeyam-isolate/layout.tsx +12 -0
  506. package/codeyam-cli/templates/nextjs-prisma-sqlite/app/globals.css +26 -0
  507. package/codeyam-cli/templates/nextjs-prisma-sqlite/app/layout.tsx +34 -0
  508. package/codeyam-cli/templates/nextjs-prisma-sqlite/app/lib/prisma.ts +24 -0
  509. package/codeyam-cli/templates/nextjs-prisma-sqlite/app/page.tsx +10 -0
  510. package/codeyam-cli/templates/nextjs-prisma-sqlite/env +4 -0
  511. package/codeyam-cli/templates/nextjs-prisma-sqlite/eslint.config.mjs +11 -0
  512. package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +64 -0
  513. package/codeyam-cli/templates/nextjs-prisma-sqlite/next.config.ts +14 -0
  514. package/codeyam-cli/templates/nextjs-prisma-sqlite/package.json +39 -0
  515. package/codeyam-cli/templates/nextjs-prisma-sqlite/postcss.config.mjs +7 -0
  516. package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma/schema.prisma +27 -0
  517. package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma/seed.ts +40 -0
  518. package/codeyam-cli/templates/nextjs-prisma-sqlite/prisma.config.ts +12 -0
  519. package/codeyam-cli/templates/nextjs-prisma-sqlite/seed-adapter.ts +127 -0
  520. package/codeyam-cli/templates/nextjs-prisma-sqlite/tsconfig.json +34 -0
  521. package/codeyam-cli/templates/nextjs-prisma-sqlite/vitest.config.ts +13 -0
  522. package/codeyam-cli/templates/nextjs-prisma-supabase/README.md +52 -0
  523. package/codeyam-cli/templates/nextjs-prisma-supabase/SUPABASE_SETUP.md +104 -0
  524. package/codeyam-cli/templates/nextjs-prisma-supabase/app/api/todos/route.ts +17 -0
  525. package/codeyam-cli/templates/nextjs-prisma-supabase/app/globals.css +26 -0
  526. package/codeyam-cli/templates/nextjs-prisma-supabase/app/layout.tsx +34 -0
  527. package/codeyam-cli/templates/nextjs-prisma-supabase/app/lib/prisma.ts +20 -0
  528. package/codeyam-cli/templates/nextjs-prisma-supabase/app/lib/supabase.ts +12 -0
  529. package/codeyam-cli/templates/nextjs-prisma-supabase/app/page.tsx +10 -0
  530. package/codeyam-cli/templates/nextjs-prisma-supabase/env +9 -0
  531. package/codeyam-cli/templates/nextjs-prisma-supabase/eslint.config.mjs +11 -0
  532. package/codeyam-cli/templates/nextjs-prisma-supabase/gitignore +40 -0
  533. package/codeyam-cli/templates/nextjs-prisma-supabase/next.config.ts +11 -0
  534. package/codeyam-cli/templates/nextjs-prisma-supabase/package.json +37 -0
  535. package/codeyam-cli/templates/nextjs-prisma-supabase/postcss.config.mjs +7 -0
  536. package/codeyam-cli/templates/nextjs-prisma-supabase/prisma/schema.prisma +27 -0
  537. package/codeyam-cli/templates/nextjs-prisma-supabase/prisma/seed.ts +39 -0
  538. package/codeyam-cli/templates/nextjs-prisma-supabase/prisma.config.ts +12 -0
  539. package/codeyam-cli/templates/nextjs-prisma-supabase/tsconfig.json +34 -0
  540. package/codeyam-cli/templates/prompts/conversation-guidance.txt +12 -0
  541. package/codeyam-cli/templates/prompts/conversation-prompt.txt +3 -3
  542. package/codeyam-cli/templates/prompts/interruption-prompt.txt +3 -3
  543. package/codeyam-cli/templates/prompts/stale-rules-prompt.txt +3 -3
  544. package/codeyam-cli/templates/rule-notification-hook.py +44 -17
  545. package/codeyam-cli/templates/rule-reflection-hook.py +24 -4
  546. package/codeyam-cli/templates/rules-instructions.md +4 -3
  547. package/codeyam-cli/templates/seed-adapters/supabase.ts +282 -0
  548. package/codeyam-cli/templates/skills/codeyam-dev-mode/SKILL.md +237 -0
  549. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +211 -0
  550. package/codeyam-cli/templates/{codeyam-memory.md → skills/codeyam-memory/SKILL.md} +215 -0
  551. package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/deprecated-prompt.md +100 -0
  552. package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/detect-deprecated-patterns.mjs +139 -0
  553. package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/find-exports.mjs +52 -0
  554. package/codeyam-cli/templates/skills/codeyam-memory/scripts/holistic-analysis/misleading-api-prompt.md +117 -0
  555. package/codeyam-cli/templates/skills/codeyam-memory/scripts/lib/read-json-field.mjs +61 -0
  556. package/codeyam-cli/templates/skills/codeyam-memory/scripts/lib/ripgrep-fallback.mjs +155 -0
  557. package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/analyze-prompt.md +46 -0
  558. package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/cleanup.mjs +13 -0
  559. package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/filter-session.mjs +95 -0
  560. package/codeyam-cli/templates/skills/codeyam-memory/scripts/session-mining/preprocess.mjs +160 -0
  561. package/package.json +17 -10
  562. package/packages/ai/src/lib/astScopes/astScopeAnalyzer.js +22 -4
  563. package/packages/ai/src/lib/astScopes/astScopeAnalyzer.js.map +1 -1
  564. package/packages/ai/src/lib/completionCall.js +10 -2
  565. package/packages/ai/src/lib/completionCall.js.map +1 -1
  566. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +21 -0
  567. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  568. package/packages/ai/src/lib/dataStructure/helpers/coercePrimitivesToArraysBySchema.js +54 -0
  569. package/packages/ai/src/lib/dataStructure/helpers/coercePrimitivesToArraysBySchema.js.map +1 -0
  570. package/packages/ai/src/lib/dataStructure/helpers/stripNullableMarkers.js +34 -0
  571. package/packages/ai/src/lib/dataStructure/helpers/stripNullableMarkers.js.map +1 -0
  572. package/packages/ai/src/lib/generateEntityScenarioData.js +57 -2
  573. package/packages/ai/src/lib/generateEntityScenarioData.js.map +1 -1
  574. package/packages/ai/src/lib/generateExecutionFlows.js +0 -11
  575. package/packages/ai/src/lib/generateExecutionFlows.js.map +1 -1
  576. package/packages/analyze/src/lib/ProjectAnalyzer.js +13 -4
  577. package/packages/analyze/src/lib/ProjectAnalyzer.js.map +1 -1
  578. package/packages/analyze/src/lib/asts/index.js +4 -2
  579. package/packages/analyze/src/lib/asts/index.js.map +1 -1
  580. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +8 -1
  581. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  582. package/packages/analyze/src/lib/files/analyze/dependencyResolver.js +0 -5
  583. package/packages/analyze/src/lib/files/analyze/dependencyResolver.js.map +1 -1
  584. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +9 -0
  585. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
  586. package/packages/analyze/src/lib/files/scenarios/TransformationTracer.js +54 -27
  587. package/packages/analyze/src/lib/files/scenarios/TransformationTracer.js.map +1 -1
  588. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +65 -0
  589. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  590. package/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.js +0 -40
  591. package/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.js.map +1 -1
  592. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +18 -4
  593. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  594. package/packages/database/index.js +1 -0
  595. package/packages/database/index.js.map +1 -1
  596. package/packages/database/src/lib/kysely/db.js +5 -0
  597. package/packages/database/src/lib/kysely/db.js.map +1 -1
  598. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js +149 -0
  599. package/packages/database/src/lib/kysely/tables/editorScenariosTable.js.map +1 -0
  600. package/packages/database/src/lib/loadCommits.js +23 -13
  601. package/packages/database/src/lib/loadCommits.js.map +1 -1
  602. package/packages/database/src/lib/loadEntities.js +0 -6
  603. package/packages/database/src/lib/loadEntities.js.map +1 -1
  604. package/packages/database/src/lib/loadReadyToBeCapturedAnalyses.js +1 -4
  605. package/packages/database/src/lib/loadReadyToBeCapturedAnalyses.js.map +1 -1
  606. package/packages/database/src/lib/updateCommitMetadata.js +76 -90
  607. package/packages/database/src/lib/updateCommitMetadata.js.map +1 -1
  608. package/packages/database/src/lib/updateFreshAnalysisStatus.js +41 -30
  609. package/packages/database/src/lib/updateFreshAnalysisStatus.js.map +1 -1
  610. package/packages/database/src/lib/updateFreshAnalysisStatusWithScenarios.js +68 -57
  611. package/packages/database/src/lib/updateFreshAnalysisStatusWithScenarios.js.map +1 -1
  612. package/packages/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js +29 -1
  613. package/packages/generate/src/lib/componentScenarioPage/generateScenarioClientWrapper.js.map +1 -1
  614. package/packages/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js +33 -5
  615. package/packages/generate/src/lib/componentScenarioPage/getIFrameMessageListenerCode.js.map +1 -1
  616. package/packages/types/src/enums/ProjectFramework.js +2 -0
  617. package/packages/types/src/enums/ProjectFramework.js.map +1 -1
  618. package/scripts/npm-post-install.cjs +34 -0
  619. package/codeyam-cli/src/webserver/app/routes/api.agent-transcripts.js +0 -486
  620. package/codeyam-cli/src/webserver/app/routes/api.agent-transcripts.js.map +0 -1
  621. package/codeyam-cli/src/webserver/build/client/assets/CopyButton-CtmbP4Gl.js +0 -1
  622. package/codeyam-cli/src/webserver/build/client/assets/EntityTypeBadge-B-0PjGOU.js +0 -1
  623. package/codeyam-cli/src/webserver/build/client/assets/InlineSpinner-C1rIyZdV.js +0 -34
  624. package/codeyam-cli/src/webserver/build/client/assets/InteractivePreview-rE_fI2h2.js +0 -25
  625. package/codeyam-cli/src/webserver/build/client/assets/LibraryFunctionPreview-CnatsCw2.js +0 -3
  626. package/codeyam-cli/src/webserver/build/client/assets/SafeScreenshot-CG2uh31y.js +0 -1
  627. package/codeyam-cli/src/webserver/build/client/assets/TruncatedFilePath-D7IoaWUW.js +0 -1
  628. package/codeyam-cli/src/webserver/build/client/assets/agent-transcripts-DxCa1oBt.js +0 -23
  629. package/codeyam-cli/src/webserver/build/client/assets/createLucideIcon-D-QUFOwe.js +0 -21
  630. package/codeyam-cli/src/webserver/build/client/assets/dev.empty-DmzSmblj.js +0 -1
  631. package/codeyam-cli/src/webserver/build/client/assets/entity._sha.scenarios._scenarioId.fullscreen-DVTcUnur.js +0 -6
  632. package/codeyam-cli/src/webserver/build/client/assets/entity._sha_.create-scenario-BVgNO76F.js +0 -6
  633. package/codeyam-cli/src/webserver/build/client/assets/fileTableUtils-EWpfFU4X.js +0 -1
  634. package/codeyam-cli/src/webserver/build/client/assets/files-CrxAoWIL.js +0 -1
  635. package/codeyam-cli/src/webserver/build/client/assets/git-BldHtKeW.js +0 -15
  636. package/codeyam-cli/src/webserver/build/client/assets/globals-B4MPiL7S.css +0 -1
  637. package/codeyam-cli/src/webserver/build/client/assets/labs-CPPVOSWB.js +0 -1
  638. package/codeyam-cli/src/webserver/build/client/assets/manifest-c1fc3656.js +0 -1
  639. package/codeyam-cli/src/webserver/build/client/assets/memory-CfpYxpNu.js +0 -93
  640. package/codeyam-cli/src/webserver/build/client/assets/root-CAAbm4U5.js +0 -62
  641. package/codeyam-cli/src/webserver/build/client/assets/settings-BpLDWmGh.js +0 -1
  642. package/codeyam-cli/src/webserver/build/client/assets/simulations-BtrtCYJg.js +0 -1
  643. package/codeyam-cli/src/webserver/build/client/assets/useCustomSizes-D_bDZyDU.js +0 -1
  644. package/codeyam-cli/src/webserver/build/client/assets/useLastLogLine-DZp6rrQD.js +0 -2
  645. package/codeyam-cli/src/webserver/build/client/assets/useReportContext-BsQb6rFd.js +0 -1
  646. package/codeyam-cli/src/webserver/build/client/assets/useToast-BOur3mUv.js +0 -1
  647. package/codeyam-cli/src/webserver/build/server/assets/index-B8A_aaGG.js +0 -1
  648. package/codeyam-cli/src/webserver/build/server/assets/server-build-69rRZnZo.js +0 -286
  649. package/scripts/finalize-analyzer.cjs +0 -13
  650. /package/codeyam-cli/templates/{codeyam-diagnose.md → commands/codeyam-diagnose.md} +0 -0
  651. /package/codeyam-cli/templates/{codeyam-debug.md → skills/codeyam-debug/SKILL.md} +0 -0
  652. /package/codeyam-cli/templates/{codeyam-new-rule.md → skills/codeyam-new-rule/SKILL.md} +0 -0
  653. /package/codeyam-cli/templates/{codeyam-setup.md → skills/codeyam-setup/SKILL.md} +0 -0
  654. /package/codeyam-cli/templates/{codeyam-sim.md → skills/codeyam-sim/SKILL.md} +0 -0
  655. /package/codeyam-cli/templates/{codeyam-test.md → skills/codeyam-test/SKILL.md} +0 -0
  656. /package/codeyam-cli/templates/{codeyam-verify.md → skills/codeyam-verify/SKILL.md} +0 -0
@@ -0,0 +1,1431 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
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, } from "../editorScenarios.js";
7
+ describe('editorScenarios', () => {
8
+ describe('deduplicateByName', () => {
9
+ it('should keep only the last item for each key', () => {
10
+ const items = [
11
+ { name: 'Default', id: '1', value: 'old' },
12
+ { name: 'Dark Mode', id: '2', value: 'old' },
13
+ { name: 'Default', id: '3', value: 'new' },
14
+ ];
15
+ const result = deduplicateByName(items, (item) => item.name);
16
+ expect(result).toHaveLength(2);
17
+ expect(result.find((r) => r.name === 'Default')?.id).toBe('3');
18
+ expect(result.find((r) => r.name === 'Dark Mode')?.id).toBe('2');
19
+ });
20
+ it('should return empty array for empty input', () => {
21
+ expect(deduplicateByName([], (x) => x.name)).toEqual([]);
22
+ });
23
+ it('should not lose screenshots when re-registered scenario has null screenshot (filter before dedup)', () => {
24
+ // Simulates the journal screenshot lookup flow:
25
+ // Scenarios ordered by created_at ASC from DB, some re-registered with null screenshot
26
+ const scenarios = [
27
+ {
28
+ name: 'Full Catalog',
29
+ screenshot_path: 'screenshots/aaa.png',
30
+ id: 'aaa',
31
+ },
32
+ {
33
+ name: 'API Error',
34
+ screenshot_path: 'screenshots/bbb.png',
35
+ id: 'bbb',
36
+ },
37
+ { name: 'API Error', screenshot_path: null, id: 'ccc' }, // re-registered, capture in progress
38
+ ];
39
+ // BUG: dedup-then-filter loses "API Error" entirely because dedup picks
40
+ // the latest row (null screenshot), then filter removes it.
41
+ const buggyDeduped = deduplicateByName(scenarios, (s) => s.name);
42
+ const buggyResult = buggyDeduped.filter((s) => s.screenshot_path);
43
+ // This would only contain "Full Catalog" — "API Error" is gone!
44
+ expect(buggyResult).toHaveLength(1); // demonstrates the bug
45
+ // FIX: filter-then-dedup preserves the latest row that HAS a screenshot.
46
+ const filtered = scenarios.filter((s) => s.screenshot_path);
47
+ const fixedResult = deduplicateByName(filtered, (s) => s.name);
48
+ expect(fixedResult).toHaveLength(2);
49
+ expect(fixedResult.find((s) => s.name === 'API Error')?.id).toBe('bbb');
50
+ expect(fixedResult.find((s) => s.name === 'Full Catalog')?.id).toBe('aaa');
51
+ });
52
+ it('should preserve order of first appearance', () => {
53
+ const items = [
54
+ { name: 'A', v: 1 },
55
+ { name: 'B', v: 2 },
56
+ { name: 'A', v: 3 },
57
+ ];
58
+ const result = deduplicateByName(items, (i) => i.name);
59
+ // Map preserves insertion order — A first, then B
60
+ expect(result.map((r) => r.name)).toEqual(['A', 'B']);
61
+ });
62
+ });
63
+ describe('generateScenarioSlug', () => {
64
+ it('should replace non-alphanumeric chars with underscores', () => {
65
+ expect(generateScenarioSlug('Dark Mode')).toBe('Dark_Mode');
66
+ });
67
+ it('should collapse consecutive special characters', () => {
68
+ expect(generateScenarioSlug('Hello -- World!!')).toBe('Hello_World_');
69
+ });
70
+ it('should preserve underscores and alphanumeric characters', () => {
71
+ expect(generateScenarioSlug('Already_Valid123')).toBe('Already_Valid123');
72
+ });
73
+ it('should handle empty string', () => {
74
+ expect(generateScenarioSlug('')).toBe('');
75
+ });
76
+ });
77
+ describe('convertIsoToSqliteTimestamp', () => {
78
+ it('should convert ISO 8601 to SQLite format', () => {
79
+ expect(convertIsoToSqliteTimestamp('2026-02-28T19:00:00.000Z')).toBe('2026-02-28 19:00:00');
80
+ });
81
+ it('should handle timestamps without milliseconds', () => {
82
+ expect(convertIsoToSqliteTimestamp('2026-02-28T19:00:00Z')).toBe('2026-02-28 19:00:00Z');
83
+ });
84
+ it('should replace T with space', () => {
85
+ expect(convertIsoToSqliteTimestamp('2026-01-15T08:30:45.123Z')).toBe('2026-01-15 08:30:45');
86
+ });
87
+ });
88
+ describe('determineCaptureUrl', () => {
89
+ it('should combine path with proxy URL when path-based and proxy available', () => {
90
+ expect(determineCaptureUrl('/isolated-components/DrinkCard?s=Default', 'http://localhost:3112', 'http://localhost:3000')).toBe('http://localhost:3112/isolated-components/DrinkCard?s=Default');
91
+ });
92
+ it('should work with path field normalized to url (path alias)', () => {
93
+ // When Claude writes "path": "/drinks/1" instead of "url": "/drinks/1",
94
+ // the register endpoint normalizes path → url before calling determineCaptureUrl.
95
+ // This test verifies that determineCaptureUrl handles the resulting value correctly.
96
+ const normalizedUrl = '/drinks/1'; // after normalization: body.url = body.path
97
+ expect(determineCaptureUrl(normalizedUrl, 'http://localhost:3112', 'http://localhost:3000')).toBe('http://localhost:3112/drinks/1');
98
+ });
99
+ it('should use full URL directly when not path-based', () => {
100
+ expect(determineCaptureUrl('http://external.com/page', 'http://localhost:3112', 'http://localhost:3000')).toBe('http://external.com/page');
101
+ });
102
+ it('should fall back to proxy root when no URL provided', () => {
103
+ expect(determineCaptureUrl(null, 'http://localhost:3112', 'http://localhost:3000')).toBe('http://localhost:3112');
104
+ });
105
+ it('should fall back to dev server when no URL and no proxy', () => {
106
+ expect(determineCaptureUrl(null, null, 'http://localhost:3000')).toBe('http://localhost:3000');
107
+ });
108
+ it('should return null when nothing available', () => {
109
+ expect(determineCaptureUrl(null, null, null)).toBeNull();
110
+ });
111
+ it('should fall back to dev server for path when proxy unavailable', () => {
112
+ // Path-based but proxy is null — can't route through proxy
113
+ expect(determineCaptureUrl('/some-path', null, 'http://localhost:3000')).toBe('http://localhost:3000');
114
+ });
115
+ });
116
+ describe('clearEditorState', () => {
117
+ let tmpDir;
118
+ let codeyamDir;
119
+ beforeEach(() => {
120
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'editor-state-'));
121
+ codeyamDir = path.join(tmpDir, '.codeyam');
122
+ fs.mkdirSync(codeyamDir, { recursive: true });
123
+ });
124
+ afterEach(() => {
125
+ fs.rmSync(tmpDir, { recursive: true, force: true });
126
+ });
127
+ it('should delete the editor-step.json state file', () => {
128
+ fs.writeFileSync(path.join(codeyamDir, 'editor-step.json'), JSON.stringify({ step: 5, label: 'Extract' }));
129
+ clearEditorState(tmpDir);
130
+ expect(fs.existsSync(path.join(codeyamDir, 'editor-step.json'))).toBe(false);
131
+ });
132
+ it('should preserve editor-user-prompt.txt (captured by hook before step 1)', () => {
133
+ // The hook captures the user's feature request BEFORE step 1 calls clearEditorState.
134
+ // If clearEditorState deletes the prompt file, the next unrelated user message
135
+ // (e.g. "ok" approving a database reset) gets captured as the feature prompt.
136
+ fs.writeFileSync(path.join(codeyamDir, 'editor-step.json'), JSON.stringify({ step: 13, label: 'Present' }));
137
+ fs.writeFileSync(path.join(codeyamDir, 'editor-user-prompt.txt'), 'Could we add a page for each individual drink?');
138
+ clearEditorState(tmpDir);
139
+ expect(fs.readFileSync(path.join(codeyamDir, 'editor-user-prompt.txt'), 'utf8')).toBe('Could we add a page for each individual drink?');
140
+ });
141
+ it('should not throw if state file does not exist', () => {
142
+ expect(() => clearEditorState(tmpDir)).not.toThrow();
143
+ });
144
+ });
145
+ describe('clearEditorUserPrompt', () => {
146
+ let tmpDir;
147
+ let codeyamDir;
148
+ beforeEach(() => {
149
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'editor-prompt-'));
150
+ codeyamDir = path.join(tmpDir, '.codeyam');
151
+ fs.mkdirSync(codeyamDir, { recursive: true });
152
+ });
153
+ afterEach(() => {
154
+ fs.rmSync(tmpDir, { recursive: true, force: true });
155
+ });
156
+ it('should delete the prompt file', () => {
157
+ fs.writeFileSync(path.join(codeyamDir, 'editor-user-prompt.txt'), 'old feature prompt');
158
+ clearEditorUserPrompt(tmpDir);
159
+ expect(fs.existsSync(path.join(codeyamDir, 'editor-user-prompt.txt'))).toBe(false);
160
+ });
161
+ it('should not throw if file does not exist', () => {
162
+ expect(() => clearEditorUserPrompt(tmpDir)).not.toThrow();
163
+ });
164
+ });
165
+ describe('resolvePreviewNavPath', () => {
166
+ it('should use explicit path when provided', () => {
167
+ expect(resolvePreviewNavPath('/explicit', '/scenario-url')).toBe('/explicit');
168
+ });
169
+ it('should fall back to scenario URL when no explicit path', () => {
170
+ expect(resolvePreviewNavPath(null, '/drinks/1')).toBe('/drinks/1');
171
+ });
172
+ it('should return undefined when neither path nor URL provided', () => {
173
+ expect(resolvePreviewNavPath(null, null)).toBeUndefined();
174
+ });
175
+ it('should return undefined for empty strings', () => {
176
+ expect(resolvePreviewNavPath('', '')).toBeUndefined();
177
+ });
178
+ it('should prefer explicit path over scenario URL', () => {
179
+ expect(resolvePreviewNavPath('/', '/drinks/1')).toBe('/');
180
+ });
181
+ });
182
+ describe('register endpoint path→url normalization (logic verification)', () => {
183
+ // These tests verify the normalization logic used in api.editor-register-scenario.ts:
184
+ // body.url = body.url || body.path || undefined;
185
+ // The route can't be unit-tested directly (ESM import.meta), so we verify
186
+ // the normalization + downstream logic here.
187
+ function normalizeUrl(body) {
188
+ // Mirror the exact line from api.editor-register-scenario.ts line 152
189
+ return body.url || body.path || undefined;
190
+ }
191
+ it('should use url when provided', () => {
192
+ expect(normalizeUrl({ url: '/drinks/1' })).toBe('/drinks/1');
193
+ });
194
+ it('should fall back to path when url not provided', () => {
195
+ expect(normalizeUrl({ path: '/drinks/1' })).toBe('/drinks/1');
196
+ });
197
+ it('should prefer url over path', () => {
198
+ expect(normalizeUrl({ url: '/explicit', path: '/fallback' })).toBe('/explicit');
199
+ });
200
+ it('should return undefined when neither provided', () => {
201
+ expect(normalizeUrl({})).toBeUndefined();
202
+ });
203
+ it('should work end-to-end: path → normalize → determineCaptureUrl', () => {
204
+ // Full pipeline: Claude writes "path": "/drinks/1"
205
+ // → register normalizes to url: "/drinks/1"
206
+ // → determineCaptureUrl combines with proxy
207
+ const body = { path: '/drinks/1' };
208
+ const normalizedUrl = normalizeUrl(body);
209
+ const captureUrl = determineCaptureUrl(normalizedUrl || null, 'http://localhost:4100', 'http://localhost:3000');
210
+ expect(captureUrl).toBe('http://localhost:4100/drinks/1');
211
+ });
212
+ it('should work end-to-end: no url → normalize → proxy root', () => {
213
+ // Claude omits both url and path
214
+ // → normalize returns undefined
215
+ // → determineCaptureUrl falls back to proxy root
216
+ const body = {};
217
+ const normalizedUrl = normalizeUrl(body);
218
+ const captureUrl = determineCaptureUrl(normalizedUrl || null, 'http://localhost:4100', 'http://localhost:3000');
219
+ expect(captureUrl).toBe('http://localhost:4100');
220
+ });
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
+ });
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 skip scenarios that already have entity_sha', async () => {
1242
+ rawDb
1243
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha, display_name)
1244
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1245
+ .run('sc-3', projectId, 'Header - Dark', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'existing-sha', 'Header');
1246
+ const result = await backfillEntityShaOnScenarios(db, [
1247
+ {
1248
+ sha: 'new-sha',
1249
+ name: 'Header',
1250
+ filePath: 'src/components/Header.tsx',
1251
+ },
1252
+ ]);
1253
+ expect(result.updated).toBe(0);
1254
+ // Should not have changed the existing sha
1255
+ const row = rawDb
1256
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1257
+ .get('sc-3');
1258
+ expect(row.entity_sha).toBe('existing-sha');
1259
+ });
1260
+ it('should skip scenarios with no matching entity', async () => {
1261
+ rawDb
1262
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1264
+ .run('sc-4', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
1265
+ // Entities list doesn't include Footer.tsx
1266
+ const result = await backfillEntityShaOnScenarios(db, [
1267
+ {
1268
+ sha: 'entity-sha-1',
1269
+ name: 'Header',
1270
+ filePath: 'src/components/Header.tsx',
1271
+ },
1272
+ ]);
1273
+ expect(result.updated).toBe(0);
1274
+ const row = rawDb
1275
+ .prepare('SELECT entity_sha FROM editor_scenarios WHERE id = ?')
1276
+ .get('sc-4');
1277
+ expect(row.entity_sha).toBeNull();
1278
+ });
1279
+ });
1280
+ describe('countScenariosNeedingEntityBackfill', () => {
1281
+ let db;
1282
+ let rawDb;
1283
+ const projectId = 'test-project-id';
1284
+ beforeEach(async () => {
1285
+ rawDb = new Database(':memory:');
1286
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1287
+ await db.schema
1288
+ .createTable('editor_scenarios')
1289
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1290
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1291
+ .addColumn('name', 'varchar', (col) => col.notNull())
1292
+ .addColumn('description', 'text')
1293
+ .addColumn('component_name', 'varchar')
1294
+ .addColumn('component_path', 'varchar')
1295
+ .addColumn('url', 'varchar')
1296
+ .addColumn('type', 'varchar')
1297
+ .addColumn('screenshot_path', 'varchar')
1298
+ .addColumn('viewport_width', 'integer')
1299
+ .addColumn('viewport_height', 'integer')
1300
+ .addColumn('dimension', 'varchar')
1301
+ .addColumn('dimensions', 'text')
1302
+ .addColumn('screenshot_paths', 'text')
1303
+ .addColumn('page_file_path', 'varchar')
1304
+ .addColumn('entity_sha', 'varchar')
1305
+ .addColumn('display_name', 'varchar')
1306
+ .addColumn('created_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1307
+ .addColumn('updated_at', 'datetime', (col) => col.defaultTo(new Date().toISOString()))
1308
+ .execute();
1309
+ });
1310
+ afterEach(async () => {
1311
+ await db.destroy();
1312
+ });
1313
+ it('should NOT count scenarios that already have entity_sha', async () => {
1314
+ rawDb
1315
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height, entity_sha)
1316
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1317
+ .run('sc-1', projectId, 'Header - Default', 'Header', 'src/components/Header.tsx', '/isolated-components/Header', 'component', 1280, 720, 'existing-sha');
1318
+ const count = await countScenariosNeedingEntityBackfill(db);
1319
+ expect(count).toBe(0);
1320
+ });
1321
+ it('should count scenarios with null entity_sha and page_file_path', async () => {
1322
+ rawDb
1323
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, page_file_path, url, type, viewport_width, viewport_height)
1324
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1325
+ .run('sc-2', projectId, 'Home - Default', 'app/page.tsx', '/', 'application', 1280, 720);
1326
+ const count = await countScenariosNeedingEntityBackfill(db);
1327
+ expect(count).toBe(1);
1328
+ });
1329
+ it('should count scenarios with null entity_sha and component_path', async () => {
1330
+ rawDb
1331
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, component_name, component_path, url, type, viewport_width, viewport_height)
1332
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1333
+ .run('sc-3', projectId, 'Footer - Default', 'Footer', 'src/components/Footer.tsx', '/isolated-components/Footer', 'component', 1280, 720);
1334
+ const count = await countScenariosNeedingEntityBackfill(db);
1335
+ expect(count).toBe(1);
1336
+ });
1337
+ it('should NOT count scenarios with null entity_sha and no file paths', async () => {
1338
+ rawDb
1339
+ .prepare(`INSERT INTO editor_scenarios (id, project_id, name, url, type, viewport_width, viewport_height)
1340
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
1341
+ .run('sc-4', projectId, 'No File Path', '/', 'application', 1280, 720);
1342
+ const count = await countScenariosNeedingEntityBackfill(db);
1343
+ expect(count).toBe(0);
1344
+ });
1345
+ });
1346
+ describe('validateEntityLinkageForAppScenario', () => {
1347
+ let tmpDir;
1348
+ let glossaryPath;
1349
+ beforeEach(() => {
1350
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entity-linkage-'));
1351
+ const codeyamDir = path.join(tmpDir, '.codeyam');
1352
+ fs.mkdirSync(codeyamDir, { recursive: true });
1353
+ glossaryPath = path.join(codeyamDir, 'glossary.json');
1354
+ });
1355
+ afterEach(() => {
1356
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1357
+ });
1358
+ it('should return error when file is not in glossary', () => {
1359
+ // Write a glossary that does NOT include the target file
1360
+ fs.writeFileSync(glossaryPath, JSON.stringify([
1361
+ { name: 'Header', filePath: 'src/components/Header.tsx' },
1362
+ ]));
1363
+ const result = validateEntityLinkageForAppScenario({
1364
+ lookupFilePath: 'src/App.tsx',
1365
+ scenarioType: 'application',
1366
+ projectRoot: tmpDir,
1367
+ });
1368
+ expect(result.valid).toBe(false);
1369
+ expect(result.error).toContain('No glossary entry found');
1370
+ expect(result.error).toContain('src/App.tsx');
1371
+ });
1372
+ it('should return needsAnalysis when file IS in glossary but no entity exists', () => {
1373
+ // Write a glossary that includes the target file
1374
+ fs.writeFileSync(glossaryPath, JSON.stringify([
1375
+ { name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
1376
+ ]));
1377
+ const result = validateEntityLinkageForAppScenario({
1378
+ lookupFilePath: 'src/App.tsx',
1379
+ scenarioType: 'application',
1380
+ projectRoot: tmpDir,
1381
+ });
1382
+ expect(result.valid).toBe(true);
1383
+ expect(result.needsAnalysis).toBe(true);
1384
+ });
1385
+ it('should skip validation for component-type scenarios', () => {
1386
+ // No glossary file exists at all
1387
+ const result = validateEntityLinkageForAppScenario({
1388
+ lookupFilePath: 'src/components/Header.tsx',
1389
+ scenarioType: 'component',
1390
+ projectRoot: tmpDir,
1391
+ });
1392
+ expect(result.valid).toBe(true);
1393
+ expect(result.needsAnalysis).toBeUndefined();
1394
+ });
1395
+ it('should skip validation when lookupFilePath is null', () => {
1396
+ const result = validateEntityLinkageForAppScenario({
1397
+ lookupFilePath: null,
1398
+ scenarioType: 'application',
1399
+ projectRoot: tmpDir,
1400
+ });
1401
+ expect(result.valid).toBe(true);
1402
+ expect(result.needsAnalysis).toBeUndefined();
1403
+ });
1404
+ it('should handle glossary wrapped in object format', () => {
1405
+ // LLMs sometimes write glossary as {"components": [...]}
1406
+ fs.writeFileSync(glossaryPath, JSON.stringify({
1407
+ components: [
1408
+ { name: 'App', filePath: 'src/App.tsx', description: 'Main app' },
1409
+ ],
1410
+ }));
1411
+ const result = validateEntityLinkageForAppScenario({
1412
+ lookupFilePath: 'src/App.tsx',
1413
+ scenarioType: 'user',
1414
+ projectRoot: tmpDir,
1415
+ });
1416
+ expect(result.valid).toBe(true);
1417
+ expect(result.needsAnalysis).toBe(true);
1418
+ });
1419
+ it('should return error when glossary file does not exist', () => {
1420
+ // Don't create a glossary file
1421
+ const result = validateEntityLinkageForAppScenario({
1422
+ lookupFilePath: 'src/App.tsx',
1423
+ scenarioType: 'application',
1424
+ projectRoot: tmpDir,
1425
+ });
1426
+ expect(result.valid).toBe(false);
1427
+ expect(result.error).toContain('No glossary entry found');
1428
+ });
1429
+ });
1430
+ });
1431
+ //# sourceMappingURL=editorScenarios.test.js.map