@funnelsgrove/cli 0.1.14 → 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 +428 -53
  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 -55
  282. package/template_docs/docs/analytics.md +0 -65
  283. package/template_docs/docs/editing-flow.md +0 -125
  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,55 +0,0 @@
1
- # A/B Experiments
2
-
3
- Experiments are config, not code: declared in `src/config/experiments.ts`, exported into the manifest, resolved by the shared runtime. Never write variant conditionals inside step components. If the variant step already exists, setting up the experiment is a one-file edit that takes under a minute.
4
-
5
- ## Recipe: Step or Paywall Test
6
-
7
- Open `src/config/experiments.ts` and add one entry inside `defineFunnelExperiments([...])`:
8
-
9
- ```ts
10
- export const experiments = defineFunnelExperiments([
11
- {
12
- id: 'paywall-ab', // durable, readable, never reused
13
- name: 'Paywall copy test',
14
- type: 'paywall', // 'step' for quiz steps, 'paywall' for paywall tests
15
- status: 'running', // 'paused' | 'stopped' removes it from the manifest
16
- launchDate: '2026-06-12T00:00:00.000Z',
17
- control: { stepId: 'paywall', label: 'paywall-ab-control', trafficPercent: 50 },
18
- variant: { stepId: 'paywall-b', label: 'paywall-ab-variant-b', trafficPercent: 50 },
19
- },
20
- ] as const);
21
- ```
22
-
23
- That's it — `toManifestExperiments(experiments)` in the same file feeds the manifest, traffic percents are validated to sum to 100, and non-`running` entries are filtered out automatically. (Older funnels may declare a plain array and map it manually — follow the local file's existing shape there.) Preconditions (each is its own task if missing):
24
-
25
- 1. Both `control.stepId` and `variant.stepId` exist in the manifest with normal `edgesByStepId` exits ([editing-step.md](editing-step.md) to create a variant step — usually a copy of the control step with one deliberate change).
26
- 2. Both steps route to the same next step, unless the experiment is explicitly about the flow.
27
-
28
- ## How the Runtime Behaves
29
-
30
- 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.
31
-
32
- 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.
33
-
34
- 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.
35
-
36
- ## Verify (part of the < 1 min setup)
37
-
38
- In local preview or builder preview, force each side with the editor override:
39
-
40
- ```text
41
- /paywall?editor=true&experimentVariant=control
42
- /paywall?editor=true&experimentVariant=variant_b
43
- ```
44
-
45
- 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.
46
-
47
- ## Rules
48
-
49
- - One deliberate change per experiment. Don't bundle unrelated edits into a variant.
50
- - `control` stays stable; never edit the control step as part of launching a variant.
51
- - Add stable `control.label` and `variant.label` values in the `<experiment-id>-control` / `<experiment-id>-variant-b` format so builder labels and analytics views stay readable.
52
- - Variant keys and experiment ids are durable — they flow into analytics. Never recycle an id for a different hypothesis.
53
- - Keep every variant step's outgoing edge in `edgesByStepId`, or assigned visitors strand.
54
- - 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)).
55
- - 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,125 +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`. 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.
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
- 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.
105
-
106
- 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.
107
-
108
- Keep the control variant stable and do not remove a running variant until analytics have been reviewed.
109
-
110
- ## Checklist
111
-
112
- - Add or update the manifest step.
113
- - Register the component in `src/runtime/step-registry.ts`.
114
- - Update `edgesByStepId`, `entryPoints`, `branches`, step `tags`, and `assetIds` if needed.
115
- - Label every branch-owned path with `branches[].name`, answer-derived `label`, useful `tags`, and owned `stepIds`.
116
- - Keep builder metadata inline/literal so groups, labels, and `unused` badges are visible in builder.
117
- - Label every running experiment variant with stable tags/labels like `<experiment-id>-control` and `<experiment-id>-variant-b`.
118
- - Confirm intentionally inactive/unreachable steps will show as `unused` in builder.
119
- - Use a meaningful public `path` for every new step. Internal ids can be
120
- sequential, but URLs should describe the screen.
121
- - Keep image preloading manifest-driven: step images belong in
122
- `funnelManifest.assets` and step `assetIds`; the shell should warm only likely
123
- next-step images at low priority, not the entire funnel.
124
- - Make sure skipped steps do not own required answers.
125
- - 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.
@@ -1,58 +0,0 @@
1
- # Editor and Content
2
-
3
- Editable step content is a runtime contract. The step view should read from typed content, not from scattered hardcoded strings, when the value is copy, image, legal text, pricing label, FAQ, testimonial, or builder-editable UI.
4
-
5
- ## Content Files
6
-
7
- Each step content file exports a typed `LocalizedStepContent<T>`:
8
-
9
- ```ts
10
- export const stepContent = {
11
- defaultLocale: 'en',
12
- locales: {
13
- en: {
14
- headline: ['People getting', 'free money', 'know one secret'],
15
- artwork: { src: '/figma/example.png', alt: '' },
16
- },
17
- },
18
- } as const satisfies LocalizedStepContent<StepLocaleContent>;
19
- ```
20
-
21
- The runtime validates that a default locale exists and resolves locale from funnel attributes (`browserLanguage`, `language`, `locale`) or browser locale. In preview, builder content patches are merged before the step renders.
22
-
23
- Use runtime item types where possible:
24
-
25
- - `StepImage` for images.
26
- - `ChoiceItem`, `InfoItem`, `QuoteItem`, `QaItem`, `LinkItem`, `ReasonItem`, `StoreLinkItem`, `PlanPresentationItem` for lists.
27
- - `CountryPricingProfile` for country-specific plan ordering/defaults.
28
-
29
- ## Editor Files
30
-
31
- Each editor file exports `readonly StepEditorSection[]`. A field's `path` must match the content object path:
32
-
33
- ```ts
34
- {
35
- id: 'hero.image',
36
- label: 'Hero image',
37
- kind: 'image',
38
- path: 'hero.image',
39
- }
40
- ```
41
-
42
- Supported field kinds are `text`, `textLines`, `textarea`, `image`, `boolean`, `select`, `list`, and `pricingProfile`. List fields must use a preset such as `choiceItems`, `infoItems`, `quoteItems`, `qaItems`, or `linkItems`.
43
-
44
- Only expose fields the builder should edit. Do not add spare content fields for possible future use.
45
-
46
- ## Variables
47
-
48
- For builder variables, set `supportsVariables: true` on text-capable fields only. Editor variables map `user.*` tokens to answer paths, then runtime interpolation replaces `{{ user.token }}` style values in supported fields. Universal variables also support simple `{user_id}` replacement at render time.
49
-
50
- Keep variable use obvious. If a step depends on a value from an earlier step, make sure that earlier step writes a stable answer key.
51
-
52
- ## Agent Rules
53
-
54
- - Keep copy, images, FAQ, testimonials, legal strings, and checkout labels in content files.
55
- - Keep layout decisions in the step file.
56
- - Keep editor sections aligned with the content shape.
57
- - Keep `src/steps/step-content.registry.ts` in sync when adding or renaming content/editor files.
58
- - Use checked-in public assets (`/paywall/...`, `/figma/...`, etc.); do not leave temporary localhost asset URLs.