@funnelsgrove/cli 0.1.15 → 0.1.18

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 (293) hide show
  1. package/contracts/step-contract-v2.json +733 -0
  2. package/dist/analyticsOutput.d.ts +30 -15
  3. package/dist/analyticsOutput.js +45 -28
  4. package/dist/apiClient.d.ts +85 -0
  5. package/dist/apiClient.js +225 -8
  6. package/dist/cli.d.ts +149 -1
  7. package/dist/cli.js +1232 -71
  8. package/dist/diagnosticOutput.d.ts +42 -0
  9. package/dist/diagnosticOutput.js +83 -0
  10. package/dist/docsOutput.d.ts +19 -0
  11. package/dist/docsOutput.js +73 -0
  12. package/dist/docsTransaction.d.ts +81 -0
  13. package/dist/docsTransaction.js +1755 -0
  14. package/dist/funnelValidation.d.ts +14 -0
  15. package/dist/funnelValidation.js +297 -0
  16. package/dist/localSync.d.ts +79 -0
  17. package/dist/localSync.js +400 -29
  18. package/dist/projectValidator.d.ts +27 -0
  19. package/dist/projectValidator.js +208 -0
  20. package/dist/reskin.js +27 -1
  21. package/dist/stepContractMigration.d.ts +71 -0
  22. package/dist/stepContractMigration.js +1323 -0
  23. package/dist/templateDocs.js +8 -25
  24. package/docs-release-history.json +66 -0
  25. package/legacy-docs-catalog.json +1012 -0
  26. package/package.json +13 -4
  27. package/template_docs/.funnelsgrove-docs.json +144 -0
  28. package/template_docs/AGENTS.md +30 -58
  29. package/template_docs/CLAUDE.md +0 -2
  30. package/template_docs/docs/funnelsgrove/START-HERE.md +73 -0
  31. package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +59 -0
  32. package/template_docs/docs/funnelsgrove/contracts/content-answers.md +44 -0
  33. package/template_docs/docs/funnelsgrove/contracts/flow-routing.md +41 -0
  34. package/template_docs/docs/funnelsgrove/contracts/payments.md +42 -0
  35. package/template_docs/docs/funnelsgrove/contracts/step-metadata.md +57 -0
  36. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
  37. package/template_docs/docs/funnelsgrove/qa/analytics.md +43 -0
  38. package/template_docs/docs/funnelsgrove/qa/local.md +42 -0
  39. package/template_docs/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
  40. package/template_docs/docs/funnelsgrove/qa/publish.md +32 -0
  41. package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +34 -0
  42. package/template_docs/docs/funnelsgrove/recipes/add-step.md +33 -0
  43. package/template_docs/docs/funnelsgrove/recipes/edit-step.md +31 -0
  44. package/template_docs/docs/funnelsgrove/steps/INDEX.md +26 -0
  45. package/template_docs/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
  46. package/template_docs/docs/funnelsgrove/steps/checkout.md +120 -0
  47. package/template_docs/docs/funnelsgrove/steps/form_input.md +115 -0
  48. package/template_docs/docs/funnelsgrove/steps/intro_hero.md +110 -0
  49. package/template_docs/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
  50. package/template_docs/docs/funnelsgrove/steps/paywall_offer.md +115 -0
  51. package/template_docs/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
  52. package/template_docs/docs/funnelsgrove/steps/purchase_completed.md +117 -0
  53. package/template_docs/docs/funnelsgrove/steps/single_step_choice.md +122 -0
  54. package/template_docs/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
  55. package/template_docs/docs/funnelsgrove/steps/social_proof.md +110 -0
  56. package/template_docs/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
  57. package/template_docs/docs/funnelsgrove/steps/subscription_management.md +112 -0
  58. package/template_docs/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
  59. package/template_docs/docs/funnelsgrove/steps/upsell_offer.md +112 -0
  60. package/template_docs/docs/funnelsgrove/steps/value_prop_story.md +110 -0
  61. package/template_docs/funnel-docs.config.json +43 -0
  62. package/template_scaffold/.env.example +41 -0
  63. package/template_scaffold/.funnelsgrove-contract-tools.json +19 -0
  64. package/template_scaffold/.funnelsgrove-docs.json +144 -0
  65. package/template_scaffold/.funnelsgrove-scaffold.json +1078 -0
  66. package/template_scaffold/AGENTS.md +42 -0
  67. package/template_scaffold/CLAUDE.md +1 -0
  68. package/template_scaffold/PLAN.md +81 -0
  69. package/template_scaffold/PRODUCT_SENSE.md +28 -0
  70. package/template_scaffold/context/build-context.md +34 -0
  71. package/template_scaffold/docs/AGENT_FUNNEL_IMPLEMENTATION_SPEC.md +5 -0
  72. package/template_scaffold/docs/ARCHITECTURE.md +5 -0
  73. package/template_scaffold/docs/FLOW_CONFIG_AND_ROUTING.md +5 -0
  74. package/template_scaffold/docs/PAYWALL_AND_PAYMENTS.md +5 -0
  75. package/template_scaffold/docs/README.md +5 -0
  76. package/template_scaffold/docs/funnelsgrove/START-HERE.md +73 -0
  77. package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +59 -0
  78. package/template_scaffold/docs/funnelsgrove/contracts/content-answers.md +44 -0
  79. package/template_scaffold/docs/funnelsgrove/contracts/flow-routing.md +41 -0
  80. package/template_scaffold/docs/funnelsgrove/contracts/payments.md +42 -0
  81. package/template_scaffold/docs/funnelsgrove/contracts/step-metadata.md +57 -0
  82. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
  83. package/template_scaffold/docs/funnelsgrove/qa/analytics.md +43 -0
  84. package/template_scaffold/docs/funnelsgrove/qa/local.md +42 -0
  85. package/template_scaffold/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
  86. package/template_scaffold/docs/funnelsgrove/qa/publish.md +32 -0
  87. package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +34 -0
  88. package/template_scaffold/docs/funnelsgrove/recipes/add-step.md +33 -0
  89. package/template_scaffold/docs/funnelsgrove/recipes/edit-step.md +31 -0
  90. package/template_scaffold/docs/funnelsgrove/steps/INDEX.md +26 -0
  91. package/template_scaffold/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
  92. package/template_scaffold/docs/funnelsgrove/steps/checkout.md +120 -0
  93. package/template_scaffold/docs/funnelsgrove/steps/form_input.md +115 -0
  94. package/template_scaffold/docs/funnelsgrove/steps/intro_hero.md +110 -0
  95. package/template_scaffold/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
  96. package/template_scaffold/docs/funnelsgrove/steps/paywall_offer.md +115 -0
  97. package/template_scaffold/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
  98. package/template_scaffold/docs/funnelsgrove/steps/purchase_completed.md +117 -0
  99. package/template_scaffold/docs/funnelsgrove/steps/single_step_choice.md +122 -0
  100. package/template_scaffold/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
  101. package/template_scaffold/docs/funnelsgrove/steps/social_proof.md +110 -0
  102. package/template_scaffold/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
  103. package/template_scaffold/docs/funnelsgrove/steps/subscription_management.md +112 -0
  104. package/template_scaffold/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
  105. package/template_scaffold/docs/funnelsgrove/steps/upsell_offer.md +112 -0
  106. package/template_scaffold/docs/funnelsgrove/steps/value_prop_story.md +110 -0
  107. package/template_scaffold/docs/references/funnel-sdk-user-answers.md +5 -0
  108. package/template_scaffold/eslint.config.mjs +16 -0
  109. package/template_scaffold/funnel-agent-docs.test.ts +699 -0
  110. package/template_scaffold/funnel-docs.config.json +43 -0
  111. package/template_scaffold/funnel.config.json +5 -0
  112. package/template_scaffold/gitignore.template +5 -0
  113. package/template_scaffold/next-env.d.ts +6 -0
  114. package/template_scaffold/next.config.ts +53 -0
  115. package/template_scaffold/package-lock.json +7737 -0
  116. package/template_scaffold/package.json +37 -0
  117. package/template_scaffold/package.test.ts +23 -0
  118. package/template_scaffold/public/claimbee/bee.png +0 -0
  119. package/template_scaffold/public/claimbee/device-iphone.png +0 -0
  120. package/template_scaffold/public/claimbee/device-macbook.png +0 -0
  121. package/template_scaffold/public/claimbee/device-none.png +0 -0
  122. package/template_scaffold/public/claimbee/device-watch.png +0 -0
  123. package/template_scaffold/public/claimbee/glow.svg +3 -0
  124. package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
  125. package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
  126. package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
  127. package/template_scaffold/public/claimbee/testimonial-star.svg +3 -0
  128. package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
  129. package/template_scaffold/public/paywall/claim-history.png +0 -0
  130. package/template_scaffold/public/paywall/clear-progress.png +0 -0
  131. package/template_scaffold/public/paywall/hero.png +0 -0
  132. package/template_scaffold/public/paywall/money-alerts.png +0 -0
  133. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  134. package/template_scaffold/public/paywall/paywall-image.png +0 -0
  135. package/template_scaffold/public/paywall/r1.png +0 -0
  136. package/template_scaffold/public/paywall/r2.png +0 -0
  137. package/template_scaffold/public/paywall/r3.png +0 -0
  138. package/template_scaffold/public/paywall/r4.png +0 -0
  139. package/template_scaffold/public/paywall/r5.png +0 -0
  140. package/template_scaffold/public/paywall/r6.png +0 -0
  141. package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
  142. package/template_scaffold/public/please/bee.png +0 -0
  143. package/template_scaffold/public/please/device-iphone.png +0 -0
  144. package/template_scaffold/public/please/device-macbook.png +0 -0
  145. package/template_scaffold/public/please/device-none.png +0 -0
  146. package/template_scaffold/public/please/device-watch.png +0 -0
  147. package/template_scaffold/public/please/glow.svg +3 -0
  148. package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
  149. package/template_scaffold/public/please/steps-sprite-default.png +0 -0
  150. package/template_scaffold/public/please/testimonial-avatar.png +0 -0
  151. package/template_scaffold/public/please/testimonial-star.svg +3 -0
  152. package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
  153. package/template_scaffold/src/app/[stepId]/page.test.tsx +59 -0
  154. package/template_scaffold/src/app/[stepId]/page.tsx +62 -0
  155. package/template_scaffold/src/app/globals.css +49 -0
  156. package/template_scaffold/src/app/layout.test.ts +26 -0
  157. package/template_scaffold/src/app/layout.tsx +61 -0
  158. package/template_scaffold/src/app/page.tsx +8 -0
  159. package/template_scaffold/src/app/sdk/page.tsx +673 -0
  160. package/template_scaffold/src/app/subscription-dashboard/page.tsx +274 -0
  161. package/template_scaffold/src/assets/claimbee/bee.png +0 -0
  162. package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
  163. package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
  164. package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
  165. package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
  166. package/template_scaffold/src/assets/claimbee/glow.svg +3 -0
  167. package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
  168. package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
  169. package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
  170. package/template_scaffold/src/assets/claimbee/testimonial-star.svg +3 -0
  171. package/template_scaffold/src/components/FunnelEditorPanel.tsx +194 -0
  172. package/template_scaffold/src/components/FunnelFlow.test.tsx +17 -0
  173. package/template_scaffold/src/components/FunnelFlow.tsx +114 -0
  174. package/template_scaffold/src/components/FunnelStepImage.test.ts +15 -0
  175. package/template_scaffold/src/components/FunnelStepImage.tsx +28 -0
  176. package/template_scaffold/src/components/step/FunnelStepHeader.tsx +35 -0
  177. package/template_scaffold/src/config/billing.plans.test.ts +143 -0
  178. package/template_scaffold/src/config/billing.plans.ts +69 -0
  179. package/template_scaffold/src/config/billing.test.plans.ts +32 -0
  180. package/template_scaffold/src/config/experiments.generated.ts +4 -0
  181. package/template_scaffold/src/config/experiments.test.ts +12 -0
  182. package/template_scaffold/src/config/experiments.ts +9 -0
  183. package/template_scaffold/src/config/funnel.config.ts +12 -0
  184. package/template_scaffold/src/config/funnel.manifest.test.ts +166 -0
  185. package/template_scaffold/src/config/funnel.manifest.ts +139 -0
  186. package/template_scaffold/src/config/offer-sets.generated.ts +4 -0
  187. package/template_scaffold/src/contract/funnel-project.validation.test.ts +80 -0
  188. package/template_scaffold/src/contract/funnel-project.validation.ts +44 -0
  189. package/template_scaffold/src/contract/funnel-validator.vite.config.ts +11 -0
  190. package/template_scaffold/src/contract/golden/cancellation_offer.fixture.ts +29 -0
  191. package/template_scaffold/src/contract/golden/checkout.fixture.ts +37 -0
  192. package/template_scaffold/src/contract/golden/form_input.fixture.ts +28 -0
  193. package/template_scaffold/src/contract/golden/golden-fixtures.test.ts +190 -0
  194. package/template_scaffold/src/contract/golden/intro_hero.fixture.ts +27 -0
  195. package/template_scaffold/src/contract/golden/multi_select_choice.fixture.ts +37 -0
  196. package/template_scaffold/src/contract/golden/paywall_offer.fixture.ts +30 -0
  197. package/template_scaffold/src/contract/golden/progress_interstitial.fixture.ts +28 -0
  198. package/template_scaffold/src/contract/golden/purchase_completed.fixture.ts +33 -0
  199. package/template_scaffold/src/contract/golden/single_step_choice.fixture.ts +36 -0
  200. package/template_scaffold/src/contract/golden/single_step_choice_emoji.fixture.ts +36 -0
  201. package/template_scaffold/src/contract/golden/social_proof.fixture.ts +27 -0
  202. package/template_scaffold/src/contract/golden/subscription_handoff.fixture.ts +30 -0
  203. package/template_scaffold/src/contract/golden/subscription_management.fixture.ts +29 -0
  204. package/template_scaffold/src/contract/golden/summary_confirmation.fixture.ts +27 -0
  205. package/template_scaffold/src/contract/golden/upsell_offer.fixture.ts +29 -0
  206. package/template_scaffold/src/contract/golden/value_prop_story.fixture.ts +27 -0
  207. package/template_scaffold/src/contract/validate-funnel.cli.test.ts +180 -0
  208. package/template_scaffold/src/contract/validate-funnel.cli.ts +199 -0
  209. package/template_scaffold/src/runtime/checkout-runtime-config.test.ts +60 -0
  210. package/template_scaffold/src/runtime/checkout-runtime-config.ts +26 -0
  211. package/template_scaffold/src/runtime/funnel-runtime.test.ts +25 -0
  212. package/template_scaffold/src/runtime/funnel-runtime.ts +118 -0
  213. package/template_scaffold/src/runtime/step-content-context.test.ts +38 -0
  214. package/template_scaffold/src/runtime/step-content-context.ts +65 -0
  215. package/template_scaffold/src/runtime/step-registry.ts +66 -0
  216. package/template_scaffold/src/runtime/step-variable-values.ts +31 -0
  217. package/template_scaffold/src/runtime/use-funnel-flow-controller.integration.test.ts +203 -0
  218. package/template_scaffold/src/runtime/use-funnel-flow-controller.test.ts +76 -0
  219. package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +53 -0
  220. package/template_scaffold/src/steps/content/email-capture.content.ts +29 -0
  221. package/template_scaffold/src/steps/content/manage-subscription.content.ts +62 -0
  222. package/template_scaffold/src/steps/content/paywall.content.ts +191 -0
  223. package/template_scaffold/src/steps/content/step-01.content.ts +23 -0
  224. package/template_scaffold/src/steps/content/step-02.content.ts +77 -0
  225. package/template_scaffold/src/steps/content/step-03.content.ts +40 -0
  226. package/template_scaffold/src/steps/content/step-04.content.ts +49 -0
  227. package/template_scaffold/src/steps/content/step-32-paywall.content.ts +352 -0
  228. package/template_scaffold/src/steps/content/subscription-started.content.ts +68 -0
  229. package/template_scaffold/src/steps/editor/email-capture.editor.ts +58 -0
  230. package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +179 -0
  231. package/template_scaffold/src/steps/editor/paywall.editor.ts +232 -0
  232. package/template_scaffold/src/steps/editor/step-01.editor.ts +22 -0
  233. package/template_scaffold/src/steps/editor/step-02.editor.ts +41 -0
  234. package/template_scaffold/src/steps/editor/step-03.editor.ts +58 -0
  235. package/template_scaffold/src/steps/editor/step-04.editor.ts +98 -0
  236. package/template_scaffold/src/steps/editor/step-32-paywall.editor.ts +478 -0
  237. package/template_scaffold/src/steps/editor/subscription-started.editor.ts +131 -0
  238. package/template_scaffold/src/steps/index.ts +25 -0
  239. package/template_scaffold/src/steps/paywall-checkout-options.test.ts +104 -0
  240. package/template_scaffold/src/steps/paywall-checkout-options.ts +130 -0
  241. package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +12 -0
  242. package/template_scaffold/src/steps/pricing/paywall.pricing.ts +12 -0
  243. package/template_scaffold/src/steps/pricing/shared-plan-order.ts +17 -0
  244. package/template_scaffold/src/steps/pricing/upsell-form.pricing.ts +16 -0
  245. package/template_scaffold/src/steps/shared-header-contract.test.ts +27 -0
  246. package/template_scaffold/src/steps/step-01.test.ts +38 -0
  247. package/template_scaffold/src/steps/step-01.tsx +108 -0
  248. package/template_scaffold/src/steps/step-02.test.ts +43 -0
  249. package/template_scaffold/src/steps/step-02.tsx +140 -0
  250. package/template_scaffold/src/steps/step-03.test.ts +15 -0
  251. package/template_scaffold/src/steps/step-03.tsx +262 -0
  252. package/template_scaffold/src/steps/step-04.test.ts +32 -0
  253. package/template_scaffold/src/steps/step-04.tsx +673 -0
  254. package/template_scaffold/src/steps/step-31-email-capture.test.ts +22 -0
  255. package/template_scaffold/src/steps/step-31-email-capture.tsx +195 -0
  256. package/template_scaffold/src/steps/step-32-paywall.test.ts +770 -0
  257. package/template_scaffold/src/steps/step-32-paywall.tsx +2253 -0
  258. package/template_scaffold/src/steps/step-32b-paywall-b.tsx +35 -0
  259. package/template_scaffold/src/steps/step-33-subscription-started.test.ts +26 -0
  260. package/template_scaffold/src/steps/step-33-subscription-started.tsx +86 -0
  261. package/template_scaffold/src/steps/step-34-upsell-form.test.ts +27 -0
  262. package/template_scaffold/src/steps/step-35-manage-subscription.tsx +46 -0
  263. package/template_scaffold/src/steps/step-content.registry.ts +45 -0
  264. package/template_scaffold/src/steps/step-image-migration.test.ts +20 -0
  265. package/template_scaffold/src/steps/styles/index.css +2 -0
  266. package/template_scaffold/src/steps/styles/shared/final-flow-shared.css +55 -0
  267. package/template_scaffold/src/steps/styles/shared/funnel-base.css +543 -0
  268. package/template_scaffold/src/steps/styles/shared/info-shared.css +139 -0
  269. package/template_scaffold/src/steps/styles/shared/progress-shared.css +117 -0
  270. package/template_scaffold/src/steps/styles/shared/question-shared.css +76 -0
  271. package/template_scaffold/src/steps/styles/shared/responsive-container.css +176 -0
  272. package/template_scaffold/src/steps/types.ts +7 -0
  273. package/template_scaffold/src/steps/use-claimbee-step-content.ts +15 -0
  274. package/template_scaffold/src/theme/theme.ts +81 -0
  275. package/template_scaffold/tsconfig.json +38 -0
  276. package/template_scaffold/vitest.config.ts +32 -0
  277. package/template_validation/.funnelsgrove-contract-tools.json +19 -0
  278. package/template_validation/src/contract/funnel-project.validation.ts +44 -0
  279. package/template_validation/src/contract/funnel-validator.vite.config.ts +11 -0
  280. package/template_validation/src/contract/validate-funnel.cli.ts +199 -0
  281. package/template_docs/docs/ab-experiments.md +0 -111
  282. package/template_docs/docs/analytics.md +0 -65
  283. package/template_docs/docs/editing-flow.md +0 -136
  284. package/template_docs/docs/editing-step.md +0 -125
  285. package/template_docs/docs/editor-and-content.md +0 -58
  286. package/template_docs/docs/funnel-runtime-architecture.md +0 -125
  287. package/template_docs/docs/meta-pixel-conversions-api.md +0 -50
  288. package/template_docs/docs/payment-plans-and-discounts.md +0 -106
  289. package/template_docs/docs/publishing-and-versioning.md +0 -117
  290. package/template_docs/docs/qa-checklist.md +0 -80
  291. package/template_docs/docs/sdk-api-endpoints.md +0 -78
  292. package/template_docs/docs/step-ui-guidelines.md +0 -110
  293. package/template_docs/docs/theme.md +0 -35
@@ -0,0 +1,199 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import {
4
+ CURRENT_STEP_CONTRACT_VERSION,
5
+ type FunnelContractDiagnostic,
6
+ } from '@funnelsgrove/runtime';
7
+
8
+ export const VALIDATOR_CONTRACT_VERSION = CURRENT_STEP_CONTRACT_VERSION;
9
+
10
+ export type FunnelValidationProtocol = {
11
+ schemaVersion: 1;
12
+ contractVersion: number;
13
+ valid: boolean;
14
+ diagnostics: readonly FunnelContractDiagnostic[];
15
+ };
16
+
17
+ type LoadedValidation = {
18
+ contractVersion: number;
19
+ diagnostics: readonly FunnelContractDiagnostic[];
20
+ };
21
+
22
+ type TextWriter = (text: string) => void;
23
+
24
+ export type RunValidateFunnelCliOptions = {
25
+ args?: readonly string[];
26
+ loadValidation?: () => Promise<LoadedValidation>;
27
+ writeStdout?: TextWriter;
28
+ writeStderr?: TextWriter;
29
+ };
30
+
31
+ export type FunnelValidationExitCode = 0 | 1 | 2;
32
+
33
+ const internalDiagnostic = (): FunnelContractDiagnostic => ({
34
+ code: 'FG-CLIENT-001',
35
+ severity: 'error',
36
+ file: 'src/contract/validate-funnel.cli.ts',
37
+ stepId: null,
38
+ expected: 'loadable funnel project validation module',
39
+ received: 'internal validator failure',
40
+ reason: 'The funnel project validator could not load or run.',
41
+ guide: 'docs/funnelsgrove/qa/local.md',
42
+ repair: 'Fix project import errors, then rerun fgrove validate.',
43
+ });
44
+
45
+ const loadProjectValidation = async (): Promise<LoadedValidation> => {
46
+ const projectValidation = await import('./funnel-project.validation');
47
+ return {
48
+ contractVersion: projectValidation.FUNNEL_PROJECT_CONTRACT_VERSION,
49
+ diagnostics: projectValidation.validateFunnelProject(),
50
+ };
51
+ };
52
+
53
+ const describeUnknownError = (error: unknown): string => {
54
+ try {
55
+ if (error instanceof Error && error.message.trim()) {
56
+ return error.message;
57
+ }
58
+ return String(error);
59
+ } catch {
60
+ return 'unknown validator failure';
61
+ }
62
+ };
63
+
64
+ const renderValue = (value: unknown): string => {
65
+ try {
66
+ return JSON.stringify(value) ?? String(value);
67
+ } catch {
68
+ return '[unserializable]';
69
+ }
70
+ };
71
+
72
+ const renderDiagnostic = (diagnostic: FunnelContractDiagnostic): string => {
73
+ const location = diagnostic.file
74
+ ? `${diagnostic.file}${diagnostic.stepId ? ` (${diagnostic.stepId})` : ''}`
75
+ : diagnostic.stepId ?? 'project';
76
+ return [
77
+ `[${diagnostic.code}] ${diagnostic.reason}`,
78
+ ` Location: ${location}`,
79
+ ` Expected: ${renderValue(diagnostic.expected)}`,
80
+ ` Received: ${renderValue(diagnostic.received)}`,
81
+ ` Repair: ${diagnostic.repair}`,
82
+ ` Guide: ${diagnostic.guide}`,
83
+ ].join('\n');
84
+ };
85
+
86
+ const invokeWithoutStdoutNoise = async <T>(
87
+ operation: () => Promise<T>,
88
+ writeStderr: TextWriter,
89
+ ): Promise<T> => {
90
+ const originalWrite = process.stdout.write;
91
+ let captured = '';
92
+ process.stdout.write = ((chunk: string | Uint8Array, ...args: unknown[]) => {
93
+ const encoding = typeof args[0] === 'string' ? args[0] as BufferEncoding : undefined;
94
+ captured += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString(encoding);
95
+ const callback = args.find((arg) => typeof arg === 'function');
96
+ if (typeof callback === 'function') {
97
+ callback();
98
+ }
99
+ return true;
100
+ }) as typeof process.stdout.write;
101
+
102
+ try {
103
+ return await operation();
104
+ } finally {
105
+ process.stdout.write = originalWrite;
106
+ if (captured) {
107
+ writeStderr(captured);
108
+ if (!captured.endsWith('\n')) {
109
+ writeStderr('\n');
110
+ }
111
+ }
112
+ }
113
+ };
114
+
115
+ const isLoadedValidation = (value: LoadedValidation): boolean => {
116
+ return value.contractVersion === VALIDATOR_CONTRACT_VERSION
117
+ && Array.isArray(value.diagnostics);
118
+ };
119
+
120
+ const emitProtocol = (
121
+ protocol: FunnelValidationProtocol,
122
+ writeStdout: TextWriter,
123
+ ): void => {
124
+ writeStdout(`${JSON.stringify(protocol)}\n`);
125
+ };
126
+
127
+ const protocolFor = (
128
+ diagnostics: readonly FunnelContractDiagnostic[],
129
+ ): FunnelValidationProtocol => ({
130
+ schemaVersion: 1,
131
+ contractVersion: VALIDATOR_CONTRACT_VERSION,
132
+ valid: diagnostics.length === 0,
133
+ diagnostics,
134
+ });
135
+
136
+ const usageError = (
137
+ json: boolean,
138
+ writeStdout: TextWriter,
139
+ writeStderr: TextWriter,
140
+ ): FunnelValidationExitCode => {
141
+ const diagnostic = internalDiagnostic();
142
+ writeStderr('Usage: validate-funnel.cli.ts [--json]\n');
143
+ if (json) {
144
+ emitProtocol(protocolFor([diagnostic]), writeStdout);
145
+ }
146
+ return 2;
147
+ };
148
+
149
+ export const runValidateFunnelCli = async (
150
+ options: RunValidateFunnelCliOptions = {},
151
+ ): Promise<FunnelValidationExitCode> => {
152
+ const args = options.args ?? process.argv.slice(2);
153
+ const writeStdout = options.writeStdout ?? ((text) => {
154
+ process.stdout.write(text);
155
+ });
156
+ const writeStderr = options.writeStderr ?? ((text) => {
157
+ process.stderr.write(text);
158
+ });
159
+ const json = args.includes('--json');
160
+ if (args.some((arg) => arg !== '--json') || args.filter((arg) => arg === '--json').length > 1) {
161
+ return usageError(json, writeStdout, writeStderr);
162
+ }
163
+
164
+ try {
165
+ const loaded = await invokeWithoutStdoutNoise(
166
+ options.loadValidation ?? loadProjectValidation,
167
+ writeStderr,
168
+ );
169
+ if (!isLoadedValidation(loaded)) {
170
+ throw new Error('The project validator returned an invalid protocol result.');
171
+ }
172
+
173
+ const protocol = protocolFor(loaded.diagnostics);
174
+ if (json) {
175
+ emitProtocol(protocol, writeStdout);
176
+ }
177
+ for (const diagnostic of loaded.diagnostics) {
178
+ writeStderr(`${renderDiagnostic(diagnostic)}\n`);
179
+ }
180
+ return protocol.valid ? 0 : 1;
181
+ } catch (error) {
182
+ const diagnostic = internalDiagnostic();
183
+ writeStderr(`Funnel validation failed internally: ${describeUnknownError(error)}\n`);
184
+ if (json) {
185
+ emitProtocol(protocolFor([diagnostic]), writeStdout);
186
+ }
187
+ return 2;
188
+ }
189
+ };
190
+
191
+ const isCliEntrypoint = (argv: readonly string[], modulePath: string): boolean => {
192
+ const resolvedModulePath = path.resolve(modulePath);
193
+ return path.basename(argv[1] ?? '') === 'vite-node'
194
+ || argv.slice(1).some((argument) => path.resolve(argument) === resolvedModulePath);
195
+ };
196
+
197
+ if (isCliEntrypoint(process.argv, fileURLToPath(import.meta.url))) {
198
+ process.exitCode = await runValidateFunnelCli();
199
+ }
@@ -1,111 +0,0 @@
1
- # A/B Experiments
2
-
3
- Experiments are config, not component code. Prefer creating or updating the
4
- experiment through the FunnelsGrove UI/API, which owns the database row,
5
- PostHog flag, and generated `src/config/experiments.generated.ts` file. Keep
6
- `src/config/experiments.ts` as the generated compatibility wrapper when
7
- possible. Make sure every control or variant route target is a real manifest
8
- step with normal outgoing edges. Never write variant conditionals inside step
9
- components. If the variant step already exists, setting up the experiment is
10
- mostly config plus publish sync.
11
-
12
- ## Recipe: Step or Paywall Test
13
-
14
- Open `src/config/funnel.manifest.ts` and make sure every control and variant
15
- screen exists in `steps` with readable tags:
16
-
17
- ```ts
18
- steps: [
19
- { id: 'paywall', path: '/offer', tags: ['paywall-ab-control'], /* ... */ },
20
- { id: 'paywall-b', path: '/offer-b', tags: ['paywall-ab-variant-b'], /* ... */ },
21
- ]
22
- ```
23
-
24
- Add normal outgoing edges for both routes:
25
-
26
- ```ts
27
- edgesByStepId: {
28
- paywall: [{ toStepId: 'checkout' }],
29
- 'paywall-b': [{ toStepId: 'checkout' }],
30
- }
31
- ```
32
-
33
- Then create the experiment in the FunnelsGrove UI/API and pull the generated
34
- config. The synced source should export it from `src/config/experiments.generated.ts`
35
- and keep `src/config/experiments.ts` as a wrapper:
36
-
37
- ```ts
38
- import {
39
- defineFunnelExperiments,
40
- toManifestExperiments,
41
- } from '@funnelsgrove/runtime';
42
- import { generatedExperiments } from './experiments.generated';
43
-
44
- export const experiments = defineFunnelExperiments(generatedExperiments);
45
-
46
- export const manifestExperiments = toManifestExperiments(experiments);
47
- ```
48
-
49
- If a code-authored experiment is necessary, add one explicit object inside the
50
- local experiment array or `defineFunnelExperiments([...])` call:
51
-
52
- ```ts
53
- export const experiments = defineFunnelExperiments([
54
- {
55
- id: 'paywall-ab', // durable, readable, never reused
56
- name: 'Paywall copy test',
57
- type: 'paywall', // 'step' for quiz steps, 'paywall' for paywall tests
58
- status: 'running', // non-running entries stay out of active routing
59
- launchDate: '2026-06-12T00:00:00.000Z',
60
- stepId: 'paywall',
61
- variants: [
62
- { variantKey: 'control', stepId: 'paywall', label: 'paywall-ab-control', trafficPercent: 50 },
63
- { variantKey: 'variant_b', stepId: 'paywall-b', label: 'paywall-ab-variant-b', trafficPercent: 50 },
64
- ],
65
- },
66
- ] as const);
67
- ```
68
-
69
- Preconditions:
70
-
71
- 1. Every control or variant `stepId` exists in the manifest `steps` array.
72
- 2. Both steps route to the same next step, unless the experiment is explicitly about the flow.
73
- 3. `steps`, `edgesByStepId`, experiment `stepId`, and experiment `label` values use explicit string literals that analytics can read.
74
-
75
- The manifest may keep `experiments: manifestExperiments` and import the
76
- runnable experiment shape from `src/config/experiments.ts`. That is fine as
77
- long as the separate file exports a generated wrapper or a source-readable
78
- `experiments` array. Do not hide step ids or labels behind computed variables.
79
- For route experiments where the source step differs from the rendered control
80
- step, set `sourceStepId` or `stepId` to the source step and set each variant's
81
- `stepId` to its rendered route.
82
-
83
- ## How the Runtime Behaves
84
-
85
- When a visitor opens the source step, the runtime suspends rendering until the assignment from `@funnelsgrove/analytics` is ready, then routes to the assigned `stepId` and syncs the URL. After assignment, navigation continues through the assigned step's normal edges — the experiment is not re-evaluated mid-flow. Assignments are sticky per visitor.
86
-
87
- Feature-flag evaluation is scoped by the public project and funnel ids (`NEXT_PUBLIC_PROJECT_ID` and `NEXT_PUBLIC_FUNNEL_ID`). The shared runtime bootstrap fills that scope from public env when callers omit it; if a local funnel owns a custom controller, keep those ids wired into PostHog flag bootstrap or verify the runtime fallback is still in place.
88
-
89
- The paywall runtime is experiment-ready out of the box: paywall variants can differ in copy, layout, plan presentation, or pricing source, and checkout/discount state stays scoped per funnel.
90
-
91
- ## Verify (part of the < 1 min setup)
92
-
93
- In local preview or builder preview, force each side with the editor override:
94
-
95
- ```text
96
- /paywall?editor=true&experimentVariant=control
97
- /paywall?editor=true&experimentVariant=variant_b
98
- ```
99
-
100
- Check: opening the source URL promotes to the assigned route, each variant renders, Continue advances to the step after the variant. Then run a normal (non-editor) open to confirm the suspension → assignment → route flow works.
101
-
102
- ## Rules
103
-
104
- - One deliberate change per experiment. Don't bundle unrelated edits into a variant.
105
- - `control` stays stable; never edit the control step as part of launching a variant.
106
- - Add stable variant `label` values in the `<experiment-id>-control` / `<experiment-id>-variant-b` format so builder labels and analytics views stay readable.
107
- - Add matching `steps[].tags` to every control and variant step.
108
- - Variant keys and experiment ids are durable — they flow into analytics. Never recycle an id for a different hypothesis.
109
- - Keep every variant step's outgoing edge in `edgesByStepId`, or assigned visitors strand.
110
- - If a paywall variant changes plans or pricing, follow [payment-plans-and-discounts.md](payment-plans-and-discounts.md) for the plan/discount sync rules and QA both variants' checkout ([qa-checklist.md](qa-checklist.md)).
111
- - Don't remove a running variant until the user confirms the analysis is done; route all traffic to the winner by setting `trafficPercent` rather than deleting history.
@@ -1,65 +0,0 @@
1
- # Analytics
2
-
3
- Analytics should describe meaningful user behavior. `@funnelsgrove/analytics` is the tracking boundary for funnel code: it encapsulates the analytics integration for event delivery and experiment metadata.
4
-
5
- ## Standard Events
6
-
7
- `FunnelFlow` tracks:
8
-
9
- - `step_start` when a step becomes active.
10
- - `step_end` when the user leaves a step, including the answer diff selected during that step.
11
-
12
- `@funnelsgrove/analytics` maps canonical runtime event types to provider event names:
13
-
14
- - `step_start` -> `step_started`.
15
- - `step_end` -> `step_completed`.
16
- - `funnel_start` -> `funnel_started`.
17
- - Successful `POST /sdk/public/users/:user_id/claim_subscription` calls emit `registration_completed` from the server.
18
-
19
- Experiment assignments are attached as feature flag properties when available. Preview runtime skips normal analytics delivery.
20
-
21
- ## Standard Checkout Events
22
-
23
- Use named helpers from `@funnelsgrove/analytics` for checkout lifecycle events:
24
-
25
- ```ts
26
- publicAnalyticsSdk.trackCheckoutStarted({
27
- stepId: stepPaywallId,
28
- stepName: stepPaywall.name || stepPaywall.title,
29
- stepType: stepPaywall.type,
30
- metadata: {
31
- planId,
32
- providerPlanId,
33
- couponId,
34
- amountCents,
35
- environment,
36
- },
37
- });
38
- ```
39
-
40
- Use `trackPaymentInfoSubmitted(...)` for payment details submitted, `trackCheckoutCompleted(...)` after a confirmed checkout, and `trackPaymentCheckoutSucceeded(...)` / `trackPaymentCheckoutReturned(...)` on redirect return screens.
41
-
42
- Flush before redirects or payment handoff when losing the page would drop the event:
43
-
44
- ```ts
45
- await publicAnalyticsSdk.flush().catch(() => 0);
46
- ```
47
-
48
- ## Experiment Tracking
49
-
50
- Flow and experiment assignment are configured in `src/config/funnel.manifest.ts`, while tracking uses the analytics package. When an experiment is active, the runtime passes assignment metadata into `publicAnalyticsSdk.trackStepStarted(...)`, `trackStepCompleted(...)`, and custom events through feature flag properties.
51
-
52
- Agents should only document and verify the analytics package surface: stable event names, step ids, selected answers, checkout metadata, and experiment metadata. Treat the underlying provider as an implementation detail.
53
-
54
- Provider-specific behavior is documented separately. Read [Meta Pixel and Conversions API](meta-pixel-conversions-api.md) before changing Meta event mapping, Pixel behavior, server-side conversions, or attribution matching.
55
-
56
- ## Agent Rules
57
-
58
- - Keep event names stable.
59
- - Count Completed Registration only from successful subscription claim requests after app login/signup.
60
- - Use `metadata`, `payload`, `context`, and `selected` for small analysis fields only.
61
- - Do not send personal data, secrets, full payment objects, SDK keys, or raw Stripe responses.
62
- - Use `@funnelsgrove/analytics` for custom tracking; do not call the provider SDK directly.
63
- - When changing flow, verify step start/end still fires on the new path.
64
- - When changing offers, verify checkout events include plan id, provider plan id, coupon id, amount, and mode.
65
- - Use `stepType: 'paywall_offer'` on paywall checkout events so analytics rollups do not depend on step names.
@@ -1,136 +0,0 @@
1
- # Editing Flow
2
-
3
- Flow is product logic. The source of truth is `src/config/funnel.manifest.ts`; runtime helpers in `@funnelsgrove/runtime` resolve paths, edges, entry points, experiments, and browser history.
4
-
5
- ## Manifest Contract
6
-
7
- The manifest defines:
8
-
9
- - `viewport`: the designed shell size. New funnels also declare default QA
10
- `breakpoints`: small 375x667, medium 393x852, large 402x874, and
11
- desktop-small 1280x800.
12
- - `assets`: image metadata used by runtime/build tooling. Declare every
13
- funnel-critical raster image here with stable `src`, `width`, and `height`
14
- so publish can reduce image size during the build and generate AVIF/WebP
15
- variants. Funnel shells may use this data to warm likely next-step images,
16
- but first-viewport images should still use the framework's normal
17
- priority/preload mechanism.
18
- - `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, optional `tags`, and optional `assetIds`. This includes experiment variant screens and intentionally inactive screens that should appear as `unused` in builder. New `path` values must be meaningful public route slugs, not `/step-1` style URLs. Sequential ids are acceptable when the funnel uses them internally.
19
- - `edgesByStepId`: graph edges between steps.
20
- - `branches`: builder metadata for conditional paths that own one or more steps before reconverging.
21
- - `experiments`: optional variant routing. This may be imported from `src/config/experiments.ts` when that file keeps source-readable experiment definitions.
22
-
23
- Keep `steps[].id`, `path`, and answer keys stable unless the request is a migration.
24
- When adding a step, prefer a semantic path like `/motivation`, `/fitness-goal`,
25
- or `/email-capture`; do not expose internal ordering through `/step-1` or
26
- `/step-07` routes.
27
-
28
- ## Routing Rules
29
-
30
- Sequential routes use:
31
-
32
- ```ts
33
- edgesByStepId: {
34
- claim: [{ toStepId: 'profile' }],
35
- }
36
- ```
37
-
38
- Conditional routes use `conditionId` values written by `goChoice(...)`. For yes/no choices the runtime expects ids like:
39
-
40
- ```ts
41
- eligibility: [
42
- { toStepId: 'active-google-claim', conditionId: 'eligibility:yes' },
43
- { toStepId: 'subscriptions', conditionId: 'eligibility:no' },
44
- ]
45
- ```
46
-
47
- The flow controller resolves the next step from configured edges first, then falls back to the sequential order. It also keeps the URL in sync through `getPathForStep(...)` and browser history.
48
-
49
- ## Branch Labels
50
-
51
- A branch starts when one step has different next steps depending on an answer. If a direction owns one or more branch-only steps before all directions share a common next step again, add a `branches` entry:
52
-
53
- ```ts
54
- branches: [
55
- {
56
- id: 'device-android',
57
- name: 'Device type',
58
- sourceStepId: 'eligibility',
59
- conditionId: 'eligibility:yes',
60
- label: 'yes-branch',
61
- tags: ['android-branch', 'yes-branch'],
62
- stepIds: ['active-google-claim', 'subscriptions'],
63
- },
64
- ]
65
- ```
66
-
67
- `name` is the readable branch group shown in builder. `label` should come from the answer that opened the path, such as `yes-branch`, `no-branch`, or `family-branch`. `stepIds` includes only the steps owned by that direction; stop before the common next step. Directions that jump directly to the common next step do not need a branch entry.
68
-
69
- Keep branch and edge metadata builder-readable, following the ClaimBee pattern:
70
- `branches` should be an inline array on the manifest, and `edgesByStepId`
71
- should use literal string keys and `toStepId` values. Avoid hiding builder
72
- metadata behind constants such as `branches: flowBranches`, computed keys such
73
- as `[emailCaptureManifestStep.id]`, or targets such as
74
- `toStepId: emailCaptureManifestStep.id`; the builder reads these fields from
75
- source text without executing the module.
76
-
77
- Use `steps[].tags` for stable builder labels that belong to one step, including current experiment labels such as `paywall-test-control`. Keep stopped A/B variants and other intentional inactive screens tagged too; builder derives `unused` automatically when those steps are not reachable from the default entry point or any active experiment path.
78
-
79
- ## Step Navigation
80
-
81
- Use the funnel context:
82
-
83
- - `goNext()` for normal manifest progression.
84
- - `goChoice('yes' | 'no')` when the current branch is represented by conditional edges.
85
- - `goToStep(stepId)` for explicit jumps such as paywall success or manage-subscription return.
86
-
87
- Do not hardcode route strings in step logic when `getPathForStep(...)` or context navigation is available.
88
-
89
- ## Experiments
90
-
91
- Experiments attach to a step and route to variant steps:
92
-
93
- ```ts
94
- {
95
- experimentId: 'paywall-ab',
96
- stepId: 'paywall',
97
- variants: [
98
- { variantKey: 'control', routeToStepId: 'paywall' },
99
- { variantKey: 'variant_b', routeToStepId: 'paywall-b' },
100
- ],
101
- }
102
- ```
103
-
104
- Keep every control and variant route target as a real step in
105
- `funnel.manifest.ts`. The runnable experiment entry may live in
106
- `src/config/experiments.ts` and be imported into the manifest as
107
- `experiments: manifestExperiments`; if so, keep the experiment `stepId`, labels,
108
- and traffic as explicit string/number literals so analytics can parse them.
109
- Do not hide step ids or labels behind computed variables.
110
-
111
- The runtime resolves assignments through the shared analytics/runtime integration outside preview and uses editor overrides inside preview/editor mode. Opening the source step waits for the assignment, then opens the assigned route and keeps the URL in sync. Continue then advances from the assigned route's normal graph edge, with the already-applied experiment ignored for that continuation.
112
-
113
- Keep experiment redirects out of step components. Add normal `edgesByStepId` entries for the source/control step and every variant step, then let `goNext()` or the shell Continue button use the shared runtime.
114
-
115
- Keep the control variant stable and do not remove a running variant until analytics have been reviewed.
116
-
117
- ## Checklist
118
-
119
- - Add or update the manifest step.
120
- - Register the component in `src/runtime/step-registry.ts`.
121
- - Update `edgesByStepId`, `entryPoints`, `branches`, step `tags`, and `assetIds` if needed.
122
- - Add every experiment control and variant route target to `steps`.
123
- - Add normal outgoing `edgesByStepId` entries for every experiment route target.
124
- - Add each running experiment to `src/config/experiments.ts` or the manifest's inline `experiments` array, matching the local funnel pattern.
125
- - Keep experiment config synchronized with manifest steps, tags, and edges.
126
- - Label every branch-owned path with `branches[].name`, answer-derived `label`, useful `tags`, and owned `stepIds`.
127
- - Keep builder metadata inline/literal so groups, labels, and `unused` badges are visible in builder.
128
- - Label every running experiment variant with stable tags/labels like `<experiment-id>-control` and `<experiment-id>-variant-b`.
129
- - Confirm intentionally inactive/unreachable steps will show as `unused` in builder.
130
- - Use a meaningful public `path` for every new step. Internal ids can be
131
- sequential, but URLs should describe the screen.
132
- - Keep image preloading manifest-driven: step images belong in
133
- `funnelManifest.assets` and step `assetIds`; the shell should warm only likely
134
- next-step images at low priority, not the entire funnel.
135
- - Make sure skipped steps do not own required answers.
136
- - Verify the step before, the edited step, and the step after.
@@ -1,125 +0,0 @@
1
- # Adding or Editing a Step
2
-
3
- A step is one screen, one decision. Target: a new step compiles and renders in local preview in under 3 minutes. Exactly six files are involved — no others.
4
-
5
- | # | File | What goes there |
6
- | --- | --- | --- |
7
- | 1 | `src/steps/step-NN-<name>.tsx` | View, step meta, local state, one-off CSS |
8
- | 2 | `src/steps/content/step-NN-<name>.content.ts` | All copy and image refs (localized) |
9
- | 3 | `src/steps/editor/step-NN-<name>.editor.ts` | Which content fields the builder may edit |
10
- | 4 | `src/runtime/step-registry.ts` | `componentKey` → component + meta |
11
- | 5 | `src/steps/step-content.registry.ts` | step id → content/editor file paths |
12
- | 6 | `src/config/funnel.manifest.ts` | Step entry + `edgesByStepId` routing |
13
-
14
- Before writing UI, read [step-ui-guidelines.md](step-ui-guidelines.md) — it defines the layout shell, the sticky action bar, and the viewport rules every step must pass.
15
-
16
- ## Naming and URLs
17
-
18
- Files may use ordered names like `step-07-motivation.tsx`, and step ids may stay
19
- sequential if the existing funnel uses sequential ids. Public route paths must
20
- be meaningful product slugs. Use `/motivation`, `/fitness-goal`, or
21
- `/email-capture`; do not create routes like `/step-1`, `/step-07`, or
22
- `/question-3`.
23
-
24
- ## Recipe: New Step
25
-
26
- Copy the closest existing step as the starting point (`step-02` for selections, `step-03` for interstitials). Then:
27
-
28
- **1. View** — `src/steps/step-07-motivation.tsx`:
29
-
30
- ```tsx
31
- export const stepMotivationId = 'motivation';
32
-
33
- export const stepMotivation: FunnelStepMeta = {
34
- id: stepMotivationId,
35
- name: 'motivation',
36
- type: 'single_step_choice',
37
- title: 'Motivation',
38
- description: 'Asks the user what motivates them.',
39
- actionBar: { buttonText: 'Continue' }, // omit for auto-advance; { hidden: true } if the step owns its CTA
40
- };
41
-
42
- export function StepMotivation() {
43
- const { attributes, setAnswer, goNext } = useFunnel();
44
- const content = usePreviewStepLocalizedContent(
45
- stepMotivationId,
46
- stepMotivationContent,
47
- getStepContentLocale(attributes),
48
- );
49
- // render options; on select: setAnswer('motivation', id) then goNext() for auto-advance
50
- return (
51
- <>
52
- <section className='motivation-step'>...</section>
53
- <style>{stepStyles}</style>
54
- </>
55
- );
56
- }
57
-
58
- const stepStyles = `...one-off CSS only...`;
59
- ```
60
-
61
- **2. Content** — `src/steps/content/step-07-motivation.content.ts`: export a `LocalizedStepContent` object holding every user-visible string and image. The view must not contain literal copy.
62
-
63
- **3. Editor** — `src/steps/editor/step-07-motivation.editor.ts`: export `StepEditorSection[]` exposing only the fields a non-developer should edit (see an existing editor file for the field kinds).
64
-
65
- **4. Register the component** — `src/runtime/step-registry.ts`:
66
-
67
- ```ts
68
- stepMotivation: { component: StepMotivation, meta: stepMotivation },
69
- ```
70
-
71
- **5. Register content/editor** — `src/steps/step-content.registry.ts`:
72
-
73
- ```ts
74
- motivation: {
75
- contentPath: 'src/steps/content/step-07-motivation.content.ts',
76
- editorPath: 'src/steps/editor/step-07-motivation.editor.ts',
77
- },
78
- ```
79
-
80
- **6. Manifest + routing** — `src/config/funnel.manifest.ts`:
81
-
82
- ```ts
83
- // steps[]
84
- {
85
- id: 'motivation',
86
- path: '/motivation', // meaningful public URL, not '/step-07'
87
- filePath: 'src/steps/step-07-motivation.tsx',
88
- componentKey: 'stepMotivation',
89
- type: 'single_step_choice',
90
- title: 'Motivation',
91
- },
92
- // edgesByStepId — wire it in AND give it an exit
93
- 'step-2': [{ toStepId: 'motivation' }],
94
- motivation: [{ toStepId: 'step-3' }],
95
- ```
96
-
97
- **Verify:** `npm run dev`, open `/motivation`, confirm it renders at small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800, Continue (or auto-advance) lands on the next step, and `npm run test:run && npm run lint` pass.
98
-
99
- ## Editing an Existing Step
100
-
101
- Find the step in `src/config/funnel.manifest.ts` by `id` — `filePath` points at the view, `componentKey` at the registry entry. Copy changes go in the `content/` file, never inline in the view. Behavior changes go in the view file. Re-run the verify line above for the edited step.
102
-
103
- ## Navigation Inside a Step
104
-
105
- Use `useFunnel()` only:
106
-
107
- - `setAnswer(key, value)` / `setAttribute(key, value)` — store data another step needs.
108
- - `goNext()` — normal manifest progression.
109
- - `goChoice('yes' | 'no')` — when the manifest has conditional edges for this step.
110
- - `goToStep(stepId)` — explicit jumps (paywall success, manage-subscription return).
111
-
112
- Never navigate with raw `window.location`, and never encode experiment routing in the step ([ab-experiments.md](ab-experiments.md) handles that in config).
113
-
114
- ## CSS
115
-
116
- One-off styling lives in a `const stepStyles` string at the bottom of the step file. Shared styling under `src/steps/styles/shared/` only when several steps already use the same pattern. Do not move paywall-specific styling into shared files.
117
-
118
- ## Rules
119
-
120
- - Keep `id`, `path`, and answer keys stable — renames break routes, persisted answers, analytics, and publish history.
121
- - For new steps, use a meaningful `path`. Sequential ids and filenames are fine
122
- when they match the project, but public URLs should not be `/step-1` style
123
- routes.
124
- - Every step needs an outgoing edge (except terminal steps like subscription handoff).
125
- - Add a focused test when changing routing, state, checkout, parsing, or non-trivial UI logic.