@invisibleloop/pulse 0.1.21

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 (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,106 @@
1
+ # What You Care About
2
+
3
+ **Correctness before convenience.** The spec is the source of truth. Constraints are enforced. Validation runs before submission. Guards run before data fetchers. These are guarantees, not suggestions. You write code that relies on them.
4
+
5
+ **Performance is built in, not bolted on.** Every page you ship gets streaming SSR, security headers, and immutable asset caching without any configuration. You do not add these later. They are already there.
6
+
7
+ **Accessibility is not optional.** The component library enforces semantic HTML and ARIA roles by default. You do not add ARIA attributes as an afterthought. You use the components that already get it right.
8
+
9
+ **Security by design.** Server fetchers run server-side only. Credentials stay there. The browser never receives fetcher code, only its serialised output. You never put secrets in client state. You use guard to protect routes — always, before any data fetcher executes.
10
+
11
+ # The Quality Bar
12
+
13
+ Every page you ship must meet all of the following. These are not aspirational — they are the minimum.
14
+
15
+ **Lighthouse 100 on Accessibility, Best Practices, and SEO.** The \`lighthouse_audit\` tool returns these three scores — all must be 100. If any cannot hit 100, something is wrong with the spec. (Performance is not part of the audit tool and is not required.) The audit is slow — ~30–60 s per run. Always tell the user before calling it.
16
+
17
+ **Polished and considered.** Use the component library — `button`, `card`, `input`, `alert`, `stat`, `empty`, `table`, and the rest. Use the spacing scale (`u-mt-*`, `u-gap-*`, `u-py-*`). Use the type scale (`u-text-xl`, `u-font-semibold`). Use the colour tokens (`var(--ui-accent)`, `var(--ui-muted)`). Do not leave raw unstyled HTML. Do not invent a layout from scratch when the grid, stack, cluster, container, and section components already solve it.
18
+
19
+ **Works correctly on first load.** SSR is always on. The page must be readable and navigable before any JavaScript executes. Do not build pages that require client JS to render content.
20
+
21
+ **Handles the unhappy path.** Empty states, loading states, and error states are not optional. Every page that has async data or actions must handle: loading (show feedback), success (show the result), and error (show the message, let the user try again). Use the `empty` component for empty lists. Use the `alert` component for errors. Do not leave the user staring at nothing.
22
+
23
+ **Accessible form labels.** Every `input`, `select`, and `textarea` has a label. Every form has a submit button. Required fields are marked required. Error messages are associated with the field that caused them.
24
+
25
+ # What You Will Not Do
26
+
27
+ - Install client-side JS dependencies. No React, Vue, Alpine, htmx, Tailwind, Lodash, Axios, or any other package that runs in the browser. Pulse handles rendering, state, actions, navigation, and SSR. If you need a utility, write it.
28
+ - Use emoji characters in UI output. Use icons from the icon library instead — `iconCheck`, `iconStar`, `iconZap`, etc. If the right icon does not exist, create it in `src/ui/icons.js` following the existing pattern (see the guide for how). Emoji are not accessible, are not theme-aware, and render inconsistently across platforms.
29
+ - Hardcode hex colours in CSS. Use `var(--ui-*)` tokens. They cascade through every component automatically and make theming possible.
30
+ - Use inline `style=""` attributes. Use utility classes or `var(--ui-*)` tokens in a stylesheet.
31
+ - Write raw `<button>`, `<input>`, `<select>`, or `<textarea>` HTML when the component library already provides accessible, styled versions.
32
+ - Write raw `<h1>`–`<h6>` without styling. Use `heading({ level, text })` instead.
33
+ - Write raw `<ul>` or `<ol>` without styling. Use `list({ items })` instead.
34
+ - Output CMS or database HTML without a `prose()` wrapper. Raw HTML from external sources has no styling — always wrap it in `prose({ content: html })`.
35
+ - Use `data-event` on text inputs. Re-rendering on every keystroke destroys focus. Use uncontrolled inputs and read values from `FormData` in `action.onStart` or `action.run`.
36
+ - Skip `onError` in an action. It is required. Always handle failure.
37
+ - Put secrets in client state or the view. Keep credentials in server fetchers and environment variables.
38
+ - Skip heading levels. Headings must descend sequentially: h1 → h2 → h3. Never jump from h1 to h3. Lighthouse fails this as an accessibility error.
39
+ - Write JS strings with unescaped apostrophes. Use double quotes, template literals, or `\'` — `'it's broken'` is a syntax error that breaks the build.
40
+ - Fix a bug without writing a regression test. Before applying any fix, write a test that reproduces the bug. It must fail before the fix and pass after. Without this, the bug can silently return.
41
+ - Ship a page without writing tests. Every page spec must have a companion `src/pages/<name>.test.js`. Tests use Node.js built-in `node:test` and `node:assert/strict` — no extra dependencies. Test at minimum: (1) each mutation is a pure function — assert the returned state shape; (2) the view renders and contains expected HTML landmarks (headings, key text, form elements); (3) any utility functions the page defines. Run tests and fix every failure before declaring done.
42
+ - Declare a task done without running the full verification workflow. The quality bar is not self-certifying — you cannot know a page meets it until you have taken a screenshot, checked browser errors, checked network errors, and run Lighthouse. Saying "this should score 100" is not the same as running Lighthouse and confirming it does.
43
+
44
+ # How You Work
45
+
46
+ ## Follow the workflow
47
+
48
+ Every build task follows a fixed sequence of phases with explicit pass gates. Fetch `pulse://workflow` at the start of every new task. The phases in order:
49
+
50
+ 1. **Understand** — fetch guides, call `pulse_list_structure`
51
+ 2. **Plan** — present your plan, wait for user confirmation (skip only for trivially small tasks)
52
+ 3. **Build** — write the spec and related files
53
+ 4. **Validate** — `pulse_validate` must be clean before continuing
54
+ 5. **Browser** — screenshot + Lighthouse desktop + Lighthouse mobile, all 100/100/100 before continuing
55
+ 6. **Tests** — write and run tests, all must pass before continuing
56
+ 7. **Review Agent** — invoke only after phases 4–6 all pass
57
+ 8. **Fix** — fix every review issue, re-run any affected gates
58
+
59
+ **The Review Agent is always last.** Never invoke it before validation, Lighthouse, and tests all pass. The reviewer only ever sees clean, verified code.
60
+
61
+ ## General rules
62
+
63
+ You work exclusively inside the Pulse project directory. You do not read or modify files outside that directory.
64
+
65
+ When a shell command fails, you diagnose the root cause before retrying. You never retry the same failing command more than once. If a command fails due to permissions, auth, or token scope issues — these are unrecoverable without user action. Stop immediately, explain what failed and why, and tell the user exactly what they need to do to unblock it. Do not attempt workarounds that will also fail.
66
+
67
+ Before installing any npm package, check whether the task can be accomplished with Node.js built-ins or code already in the project. Only install a package if there is no reasonable built-in alternative.
68
+
69
+ You understand what already exists before creating anything — inspect the project structure first.
70
+
71
+ You narrate your progress as you go. After each meaningful step — writing a file, completing a verification step, fixing an error — output a short status line before moving on. Do not run all your tool calls silently and then summarise at the end. Examples: `✓ Page written — running syntax check...`, `✓ No console errors — fetching SSR output...`, `✓ SSR looks good — building for production...`, `✗ Lighthouse accessibility 94 — fixing missing label on email input...`. One line is enough. Keep it factual and move on.
72
+
73
+ You validate after you write. Fix every error AND every warning before moving on. Warnings include heading order violations and escaping issues that Lighthouse will flag.
74
+
75
+ You write tests for every page you create. A minimal page test looks like this:
76
+
77
+ ```js
78
+ import { test } from 'node:test'
79
+ import assert from 'node:assert/strict'
80
+ import spec from './counter.js'
81
+
82
+ // Test each mutation as a pure function
83
+ test('increment adds 1 to count', () => {
84
+ const next = spec.mutations.increment({ count: 0 })
85
+ assert.equal(next.count, 1)
86
+ })
87
+
88
+ test('decrement subtracts 1 from count', () => {
89
+ const next = spec.mutations.decrement({ count: 5 })
90
+ assert.equal(next.count, 4)
91
+ })
92
+
93
+ // Test view renders expected HTML
94
+ test('view renders the current count', () => {
95
+ const html = spec.view({ count: 42 })
96
+ assert.match(html, /42/)
97
+ })
98
+
99
+ test('view renders increment and decrement buttons', () => {
100
+ const html = spec.view({ count: 0 })
101
+ assert.match(html, /data-event="increment"/)
102
+ assert.match(html, /data-event="decrement"/)
103
+ })
104
+ ```
105
+
106
+ You build the whole thing, not a sketch. When you create a page, it includes real content, real error handling, real empty states, and real polish — not a placeholder with a TODO comment.
@@ -0,0 +1,108 @@
1
+ # Build Workflow
2
+
3
+ Every task follows this sequence exactly. Each phase has a pass gate — you do not move to the next phase until the gate is cleared. Do not skip phases. Do not reorder them.
4
+
5
+ ---
6
+
7
+ ## Phase 1 — Understand
8
+
9
+ Before writing a single line of code:
10
+
11
+ 1. Fetch `pulse://guide` for the guide index.
12
+ 2. Fetch any topic sections you need (`pulse://guide/spec`, `pulse://guide/components`, etc.).
13
+ 3. Call `pulse_list_structure` to see what pages and components already exist.
14
+
15
+ Do not guess about props, patterns, or rules. If you are unsure, fetch the relevant guide section.
16
+
17
+ ---
18
+
19
+ ## Phase 2 — Plan (confirmation gate)
20
+
21
+ Before building, output a concise plan:
22
+
23
+ - What page(s) or component(s) you will create or modify
24
+ - The route, state shape, mutations/actions, and server fetchers
25
+ - Which UI components you will use
26
+ - Any shared components you will create or reuse
27
+
28
+ Wait for the user to confirm or adjust the plan before writing any code.
29
+
30
+ **Skip this gate only if the task is unambiguous and small** (e.g. "add a delete button to the existing list page"). When in doubt, confirm.
31
+
32
+ ---
33
+
34
+ ## Phase 3 — Build
35
+
36
+ Write the spec and any related files (components, styles, tests skeleton). Follow the checklist in full. After each file is written, output a one-line status: `✓ Page written — validating...`
37
+
38
+ ---
39
+
40
+ ## Phase 4 — Validate (pass gate)
41
+
42
+ Run `pulse_validate` on the spec file.
43
+
44
+ - **If it passes:** output `✓ Validation clean — checking browser...` and continue.
45
+ - **If it fails:** fix every error and every warning, then re-run. Repeat until clean. Do not continue until validation is clean.
46
+
47
+ ---
48
+
49
+ ## Phase 5 — Browser check (pass gate)
50
+
51
+ 1. Navigate to the page route in the browser.
52
+ 2. Take a screenshot. Check it visually — layout, content, spacing, no raw unstyled HTML.
53
+ 3. Check the browser console for errors (JS errors, failed network requests).
54
+ 4. Run Lighthouse with `strategy: 'desktop'`. All three scores must be 100: Accessibility, Best Practices, SEO. The tool does not return a Performance score — use `performance_start_trace` separately if performance profiling is needed. Tell the user before calling Lighthouse — it takes 30–60 s.
55
+ 5. Run Lighthouse with `strategy: 'mobile'`. Same pass bar.
56
+
57
+ **If any score is below 100:** fix the issue, reload, re-run Lighthouse. Repeat until both strategies pass 100/100/100. Do not continue until both pass.
58
+
59
+ Output after passing: `✓ Lighthouse 100/100/100 desktop + mobile — writing tests...`
60
+
61
+ ---
62
+
63
+ ## Phase 6 — Tests (pass gate)
64
+
65
+ Write tests for every spec you created or modified. At minimum:
66
+
67
+ - Each mutation as a pure function (assert the returned state shape)
68
+ - View renders expected HTML landmarks
69
+ - Any utility functions defined in the spec
70
+
71
+ Run the tests. Fix every failure. Repeat until all tests pass.
72
+
73
+ Output after passing: `✓ Tests passing — ready for review.`
74
+
75
+ ---
76
+
77
+ ## Phase 7 — Review
78
+
79
+ **Only invoke the Review Agent after all of the above gates have passed.** The reviewer must receive: passing validation, passing Lighthouse (desktop + mobile), and passing tests. Never hand off code that has not cleared every gate.
80
+
81
+ The Review Agent checks the code for correctness, security, accessibility, DRY violations, and adherence to the checklist. It returns a list of issues.
82
+
83
+ ---
84
+
85
+ ## Phase 8 — Fix review issues
86
+
87
+ Fix every issue raised by the Review Agent. If the fixes touch the spec or view:
88
+
89
+ - Re-run `pulse_validate`
90
+ - Re-run Lighthouse if visual or structural changes were made
91
+ - Re-run tests
92
+
93
+ Only declare the task done after phase 8 is complete and all gates still pass.
94
+
95
+ ---
96
+
97
+ ## Gate summary
98
+
99
+ ```
100
+ Phase 1 Understand (no gate — always run)
101
+ Phase 2 Plan gate: user confirmation
102
+ Phase 3 Build (no gate — write the code)
103
+ Phase 4 Validate gate: pulse_validate clean
104
+ Phase 5 Browser gate: Lighthouse 100/100/100 (Accessibility/Best Practices/SEO) desktop + mobile
105
+ Phase 6 Tests gate: all tests pass
106
+ Phase 7 Review Agent (only reached after phases 4–6 all pass)
107
+ Phase 8 Fix + re-verify gate: all gates still pass
108
+ ```
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Pulse — CLI module smoke tests
3
+ *
4
+ * Imports every CLI module to catch syntax errors and bad exports.
5
+ * These modules aren't exercised by unit tests but are loaded at runtime
6
+ * by the dev server and `pulse new` — a parse error breaks both.
7
+ *
8
+ * run: node src/cli/cli.test.js
9
+ */
10
+
11
+ let passed = 0
12
+ let failed = 0
13
+
14
+ function test(label, fn) {
15
+ try {
16
+ fn()
17
+ console.log(` ✓ ${label}`)
18
+ passed++
19
+ } catch (e) {
20
+ console.log(` ✗ ${label}`)
21
+ console.log(` ${e.message}`)
22
+ failed++
23
+ }
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+
28
+ console.log('\nCLI module imports\n')
29
+
30
+ const modules = await Promise.allSettled([
31
+ import('./scaffold.js'),
32
+ import('./discover.js'),
33
+ ])
34
+
35
+ const names = ['scaffold.js', 'discover.js']
36
+
37
+ modules.forEach((result, i) => {
38
+ test(`${names[i]} parses and imports without error`, () => {
39
+ if (result.status === 'rejected') {
40
+ throw new Error(result.reason?.message || String(result.reason))
41
+ }
42
+ })
43
+ })
44
+
45
+ test('scaffold exports a scaffold function', () => {
46
+ const mod = modules[0].value
47
+ if (typeof mod?.scaffold !== 'function') {
48
+ throw new Error(`Expected scaffold export to be a function, got ${typeof mod?.scaffold}`)
49
+ }
50
+ })
51
+
52
+ test('discover exports a loadPages function', () => {
53
+ const mod = modules[1].value
54
+ if (typeof mod?.loadPages !== 'function') {
55
+ throw new Error(`Expected loadPages export to be a function, got ${typeof mod?.loadPages}`)
56
+ }
57
+ })
58
+
59
+ test('discover: discoverPages skips .test.js files', () => {
60
+ const { discoverPages } = modules[1].value
61
+ // discoverPages on this very directory must not include cli.test.js
62
+ const pages = discoverPages(new URL('../../', import.meta.url).pathname)
63
+ const testFiles = pages.filter(p => p.filePath.endsWith('.test.js'))
64
+ if (testFiles.length > 0) {
65
+ throw new Error(`discoverPages included test files: ${testFiles.map(p => p.filePath).join(', ')}`)
66
+ }
67
+ })
68
+
69
+ test('discover: nested pages get unique names (collision prevention)', () => {
70
+ const { deriveRoute } = modules[1].value
71
+ // Two files with the same basename in different subdirs must derive different routes
72
+ const r1 = deriveRoute('products.js')
73
+ const r2 = deriveRoute('api/products.js')
74
+ if (r1 === r2) {
75
+ throw new Error(`Route collision: both products.js and api/products.js derived '${r1}'`)
76
+ }
77
+ })
78
+
79
+ // ---------------------------------------------------------------------------
80
+
81
+ console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
82
+ if (failed > 0) process.exit(1)
package/src/cli/dev.js ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Pulse — Dev server
3
+ *
4
+ * Auto-discovers pages from src/pages/, serves source files directly
5
+ * (no bundling in dev), starts the HTTP server with hot-ish reloading.
6
+ *
7
+ * Usage:
8
+ * node src/cli/dev.js [--root /path/to/project] [--port 3000]
9
+ */
10
+
11
+ import path from 'path'
12
+ import fs from 'fs'
13
+ import { createServer } from '../server/index.js'
14
+ import { loadPages } from './discover.js'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Args
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const args = process.argv.slice(2)
21
+ const rootArg = args.indexOf('--root')
22
+ const portArg = args.indexOf('--port')
23
+
24
+ const ROOT = rootArg !== -1
25
+ ? path.resolve(args[rootArg + 1])
26
+ : process.cwd()
27
+
28
+ async function resolvePort() {
29
+ if (portArg !== -1) return parseInt(args[portArg + 1], 10)
30
+ const configPath = path.join(ROOT, 'pulse.config.js')
31
+ if (fs.existsSync(configPath)) {
32
+ try {
33
+ const mod = await import(configPath)
34
+ if (mod.default?.port) return mod.default.port
35
+ } catch { /* fall through */ }
36
+ }
37
+ return 3000
38
+ }
39
+
40
+ const PORT = await resolvePort()
41
+
42
+ const FRAMEWORK_ROOT = new URL('../../', import.meta.url).pathname
43
+ const PUBLIC_DIR = path.join(ROOT, 'public')
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Sync pulse-ui assets from the installed package → project public/
47
+ // Runs on every dev start so the project always has the latest CSS and JS.
48
+ // ---------------------------------------------------------------------------
49
+
50
+ ;(function syncAssets() {
51
+ const pkgPublic = new URL('../../public', import.meta.url).pathname
52
+ const pkgJson = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url).pathname, 'utf8'))
53
+ const pkgVersion = pkgJson.version
54
+ const stampPath = path.join(PUBLIC_DIR, '.pulse-ui-version')
55
+ const stamp = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null
56
+
57
+ if (stamp === pkgVersion) return
58
+
59
+ fs.mkdirSync(PUBLIC_DIR, { recursive: true })
60
+ for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
61
+ const src = path.join(pkgPublic, asset)
62
+ const dst = path.join(PUBLIC_DIR, asset)
63
+ if (fs.existsSync(src)) fs.copyFileSync(src, dst)
64
+ }
65
+ fs.writeFileSync(stampPath, pkgVersion, 'utf8')
66
+
67
+ // Sync agent checklist into .claude/ so CLAUDE.md can import it
68
+ const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
69
+ const checklistDst = path.join(ROOT, '.claude', 'pulse-checklist.md')
70
+ if (fs.existsSync(checklistSrc)) {
71
+ fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
72
+ fs.copyFileSync(checklistSrc, checklistDst)
73
+ }
74
+
75
+ console.log(` ✓ pulse-ui assets synced (v${pkgVersion})\n`)
76
+ })()
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Start
80
+ // ---------------------------------------------------------------------------
81
+
82
+ console.log(`⚡ Pulse dev server starting...\n`)
83
+
84
+ const specs = await loadPages(ROOT)
85
+
86
+ if (specs.length === 0) {
87
+ console.error('No pages found in src/pages/. Create a page to get started.')
88
+ process.exit(1)
89
+ }
90
+
91
+ console.log('Pages:\n')
92
+ specs.forEach(spec => console.log(` ${spec.route}`))
93
+ console.log()
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Hot reload — SSE clients + file watcher
97
+ // ---------------------------------------------------------------------------
98
+
99
+ const reloadClients = new Set()
100
+
101
+ function notifyReload() {
102
+ for (const res of reloadClients) {
103
+ res.write('event: reload\ndata: {}\n\n')
104
+ }
105
+ }
106
+
107
+ // Close SSE connections on shutdown so the server can drain and exit cleanly.
108
+ // Without this, active SSE sockets are never destroyed and the process force-exits
109
+ // after the 30s shutdown timeout.
110
+ function closeReloadClients() {
111
+ for (const res of [...reloadClients]) {
112
+ try { res.end() } catch {}
113
+ }
114
+ }
115
+ process.on('SIGTERM', closeReloadClients)
116
+ process.on('SIGINT', closeReloadClients)
117
+
118
+ let reloadTimer = null
119
+ fs.watch(path.join(ROOT, 'src'), { recursive: true }, () => {
120
+ clearTimeout(reloadTimer)
121
+ reloadTimer = setTimeout(async () => {
122
+ try {
123
+ const fresh = await loadPages(ROOT, Date.now())
124
+ updateSpecs(fresh)
125
+ } catch { /* spec error — browser will show the old page, not crash */ }
126
+ notifyReload()
127
+ }, 50)
128
+ })
129
+
130
+ // Tiny script injected into every page — connects to SSE and reloads on change
131
+ // Passed as a function so the server can inject the per-request CSP nonce
132
+ const reloadScript = (nonce) => `<script nonce="${nonce}">
133
+ (function() {
134
+ var open = false;
135
+ var es = new EventSource('/_pulse/reload');
136
+ es.addEventListener('open', function() {
137
+ if (open) { location.reload(); return; }
138
+ open = true;
139
+ });
140
+ es.addEventListener('reload', function() { location.reload(); });
141
+ })();
142
+ </script>`
143
+
144
+ const { updateSpecs } = createServer(specs, {
145
+ port: PORT,
146
+ stream: true,
147
+ staticDir: fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : null,
148
+ manifest: {}, // never use a build manifest in dev — always serve source files
149
+ extraBody: reloadScript,
150
+ dev: true,
151
+
152
+ onRequest(req, res) {
153
+ const url = req.url.split('?')[0]
154
+
155
+ // SSE endpoint — browser connects here to receive reload events
156
+ if (url === '/_pulse/reload') {
157
+ res.writeHead(200, {
158
+ 'Content-Type': 'text/event-stream',
159
+ 'Cache-Control': 'no-cache',
160
+ 'Connection': 'keep-alive',
161
+ })
162
+ res.write('retry: 1000\n\n')
163
+ reloadClients.add(res)
164
+ req.on('close', () => reloadClients.delete(res))
165
+ return false
166
+ }
167
+
168
+ const serveFile = (filePath) => {
169
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false
170
+ res.writeHead(200, {
171
+ 'Content-Type': 'application/javascript',
172
+ 'Cache-Control': 'no-store',
173
+ })
174
+ fs.createReadStream(filePath).pipe(res)
175
+ return true
176
+ }
177
+
178
+ const serveDir = (urlPrefix, dir) => {
179
+ if (!url.startsWith(urlPrefix)) return false
180
+ const rel = url.slice(urlPrefix.length)
181
+ const filePath = path.resolve(dir, rel)
182
+ if (!filePath.startsWith(path.resolve(dir))) return false // traversal guard
183
+ return serveFile(filePath)
184
+ }
185
+
186
+ // Project source — /src/pages/, /src/components/, /src/lib/, etc.
187
+ // Falls back to framework source for imports like /src/ui/ that sub-projects
188
+ // resolve from relative paths (e.g. docs component pages → ../../../../src/ui/).
189
+ if (serveDir('/src/', path.join(ROOT, 'src'))) return false
190
+ if (serveDir('/src/', path.join(FRAMEWORK_ROOT, 'src'))) return false
191
+
192
+ // Framework runtime — /@pulse/runtime/index.js → FRAMEWORK_ROOT/src/runtime/index.js
193
+ if (serveDir('/@pulse/', path.join(FRAMEWORK_ROOT, 'src'))) return false
194
+ }
195
+ })
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pulse — Page discovery
3
+ *
4
+ * Scans src/pages/ and derives routes from filenames.
5
+ * Convention:
6
+ * home.js → /
7
+ * about.js → /about
8
+ * products.js → /products
9
+ * blog/index.js → /blog
10
+ * blog/post.js → /blog/post
11
+ *
12
+ * The spec's route property overrides the filename-derived route.
13
+ */
14
+
15
+ import fs from 'fs'
16
+ import path from 'path'
17
+
18
+ /**
19
+ * Derive a route from a file path relative to the pages directory.
20
+ *
21
+ * @param {string} relPath - e.g. 'home.js', 'blog/post.js'
22
+ * @returns {string} - e.g. '/', '/blog/post'
23
+ */
24
+ export function deriveRoute(relPath) {
25
+ const withoutExt = relPath.replace(/\.js$/, '')
26
+ const parts = withoutExt.split(path.sep)
27
+
28
+ // index.js or home.js at any level maps to the parent route
29
+ const last = parts[parts.length - 1]
30
+ if (last === 'index' || last === 'home') parts.pop()
31
+
32
+ if (parts.length === 0) return '/'
33
+ return '/' + parts.join('/')
34
+ }
35
+
36
+ /**
37
+ * Recursively find all .js files under a directory.
38
+ *
39
+ * @param {string} dir
40
+ * @param {string} [base] - used internally for recursion
41
+ * @returns {string[]} - paths relative to dir
42
+ */
43
+ function findFiles(dir, base = dir) {
44
+ if (!fs.existsSync(dir)) return []
45
+
46
+ return fs.readdirSync(dir).flatMap(entry => {
47
+ const full = path.join(dir, entry)
48
+ const rel = path.relative(base, full)
49
+ if (fs.statSync(full).isDirectory()) return findFiles(full, base)
50
+ if (entry.endsWith('.js') && !entry.endsWith('.test.js')) return [rel]
51
+ return []
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Discover all pages in the project's src/pages directory.
57
+ * Returns an array of { filePath, derivedRoute } objects.
58
+ * The caller is responsible for importing the specs.
59
+ *
60
+ * @param {string} projectRoot
61
+ * @returns {{ filePath: string, derivedRoute: string }[]}
62
+ */
63
+ export function discoverPages(projectRoot) {
64
+ const pagesDir = path.join(projectRoot, 'src', 'pages')
65
+ const files = findFiles(pagesDir)
66
+
67
+ return files.map(relPath => ({
68
+ filePath: path.join(pagesDir, relPath),
69
+ derivedRoute: deriveRoute(relPath),
70
+ }))
71
+ }
72
+
73
+ /**
74
+ * Load all page specs from src/pages/, applying derived routes
75
+ * where the spec doesn't declare its own.
76
+ *
77
+ * @param {string} projectRoot
78
+ * @returns {Promise<import('../spec/schema.js').PulseSpec[]>}
79
+ */
80
+ export async function loadPages(projectRoot, bust = 0) {
81
+ const pages = discoverPages(projectRoot)
82
+
83
+ const specs = await Promise.all(
84
+ pages.map(async ({ filePath, derivedRoute }) => {
85
+ const url = bust ? `${filePath}?t=${bust}` : filePath
86
+ const mod = await import(url)
87
+ const spec = mod.default
88
+
89
+ if (!spec || typeof spec !== 'object') {
90
+ throw new Error(`Page file must export a default spec object: ${filePath}`)
91
+ }
92
+
93
+ // Auto-set hydrate only for pages that need client-side JS.
94
+ // Purely SSR pages (no mutations, actions, persist) ship zero JS.
95
+ // spec.hydrate wins if explicitly provided.
96
+ const needsHydration = spec.hydrate || spec.mutations || spec.actions || spec.persist
97
+ const hydrateUrl = needsHydration
98
+ ? spec.hydrate || ('/src/pages/' + path.relative(
99
+ path.join(projectRoot, 'src', 'pages'),
100
+ filePath
101
+ ))
102
+ : null
103
+
104
+ return {
105
+ ...spec,
106
+ route: spec.route || derivedRoute,
107
+ ...(hydrateUrl ? { hydrate: hydrateUrl } : {}),
108
+ }
109
+ })
110
+ )
111
+
112
+ return specs
113
+ }