create-ailk 0.1.1 → 0.3.0

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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -1,475 +0,0 @@
1
- /**
2
- * @file project-listing-flow.test.ts
3
- * @description Unit tests for the approve-time projection (#5001 AC1/AC2/AC3):
4
- * the template default's `questions` block becomes a `FlowDefinition` with the
5
- * six-category step order and a live `WaitlistExperiment` keyed `{slug}`, the
6
- * row's `experimentId` is written back, the thank-you shows the confirmation
7
- * line and never the tier, and two different listings carry the same scoring
8
- * RULE over their own option values (#5036 D4).
9
- *
10
- * The configs the projection builds are asserted against the REAL
11
- * `flowConfigSchema` / `waitlistExperimentConfigSchema` as they arrive at the
12
- * (mocked) Prisma upsert, so a projection that could not actually persist
13
- * fails here rather than at approve time.
14
- *
15
- * External dependency boundaries mocked: @working-theory/database (auto-mock
16
- * via `../../lib/__mocks__/prisma.ts`), plus the same
17
- * `@working-theory/email` / `@working-theory/license` / `../aeo-score.js`
18
- * boundary mocks `waitlist-experiments.test.ts` uses — this service composes
19
- * `upsertFlowDefinition` from `flow-engine.ts`, whose import chain reaches all
20
- * three.
21
- */
22
- // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock (Jest requires explicit call for local files)
23
- jest.mock("../../lib/prisma.js");
24
-
25
- jest.mock("@working-theory/email", () => ({
26
- sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
27
- sendDeliverableEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-deliverable-test" }),
28
- }));
29
-
30
- jest.mock("@working-theory/license", () => ({
31
- checkLicense: jest.fn().mockResolvedValue({
32
- tier: "free",
33
- plan: null,
34
- features: [],
35
- expiresAt: null,
36
- revoked: false,
37
- }),
38
- }));
39
-
40
- // eslint-disable-next-line no-restricted-syntax -- aeo-score.ts is a network boundary (external crawl), same class as the DB mock above
41
- jest.mock("../aeo-score.js", () => ({
42
- scoreUrl: jest.fn(),
43
- loadExampleScore: jest.fn(),
44
- resolveScoreBudgetMs: () => 6000,
45
- }));
46
-
47
- import { buildListingDiagnosticScoring } from "@working-theory/database/flows/listing-diagnostic";
48
- import {
49
- flowConfigSchema,
50
- projectListingQuestionsSchema,
51
- waitlistExperimentConfigSchema,
52
- type FlowConfig,
53
- type ProjectListingQuestions,
54
- } from "@working-theory/validation";
55
-
56
- import { prisma } from "../../lib/prisma.js";
57
- import {
58
- generateListingFlow,
59
- listingAnswerLabels,
60
- listingWaitlistKey,
61
- type ListingFlowSource,
62
- } from "../project-listing-flow";
63
-
64
- const SITE_ID = "site-abc";
65
-
66
- /**
67
- * The template default (#4998's `projectListingQuestionsSchema`): Q2's
68
- * founder options terminated by the fixed `just-curious` bail-out, Q3/Q4 at
69
- * four founder options each, Q5's three fixed urgency values, Q6's four fixed
70
- * bands, Q7 wording only, no extras.
71
- */
72
- const TEMPLATE_DEFAULT: ProjectListingQuestions = projectListingQuestionsSchema.parse({
73
- q2: {
74
- prompt: "What brings you here?",
75
- options: [
76
- { value: "shipping-now", label: "I'm shipping something now" },
77
- { value: "planning", label: "I'm planning a build" },
78
- { value: "just-curious", label: "Just curious" },
79
- ],
80
- },
81
- q3: {
82
- prompt: "How painful is this problem today?",
83
- options: [
84
- { value: "none", label: "Not a problem" },
85
- { value: "mild", label: "Mildly annoying" },
86
- { value: "real", label: "A real drag" },
87
- { value: "blocking", label: "It's blocking me" },
88
- ],
89
- },
90
- q4: {
91
- prompt: "How close is this to what you need?",
92
- options: [
93
- { value: "off", label: "Not what I need" },
94
- { value: "adjacent", label: "Adjacent" },
95
- { value: "close", label: "Close" },
96
- { value: "exact", label: "Exactly it" },
97
- ],
98
- },
99
- q5: {
100
- prompt: "When would you use this?",
101
- options: [
102
- { value: "urgent", label: "Right away" },
103
- { value: "planning", label: "Next quarter" },
104
- { value: "exploring", label: "Just looking" },
105
- ],
106
- },
107
- q6: {
108
- prompt: "What would you pay?",
109
- options: [
110
- { value: "low", label: "Nothing yet" },
111
- { value: "medium", label: "A little" },
112
- { value: "high", label: "A real budget" },
113
- { value: "critical", label: "Whatever it takes" },
114
- ],
115
- },
116
- q7: { prompt: "Anything else we should know?" },
117
- extras: [],
118
- });
119
-
120
- function listing(overrides: Partial<ListingFlowSource> = {}): ListingFlowSource {
121
- return {
122
- id: "prjl_1",
123
- siteId: SITE_ID,
124
- slug: "orbit-notes",
125
- name: "Orbit Notes",
126
- oneLiner: "Meeting notes that file themselves.",
127
- questions: TEMPLATE_DEFAULT,
128
- ...overrides,
129
- };
130
- }
131
-
132
- /** The `FlowConfig` handed to `flowDefinition.upsert`, as it was persisted. */
133
- function upsertedFlow(): FlowConfig {
134
- const call = (prisma.flowDefinition.upsert as jest.Mock).mock.calls[0]?.[0];
135
- return call.create.config as FlowConfig;
136
- }
137
-
138
- /** The experiment config handed to `waitlistExperiment.upsert`. */
139
- function upsertedExperiment(): Record<string, unknown> {
140
- const call = (prisma.waitlistExperiment.upsert as jest.Mock).mock.calls[0]?.[0];
141
- return call.create.config as Record<string, unknown>;
142
- }
143
-
144
- beforeEach(() => {
145
- jest.clearAllMocks();
146
- process.env.DATABASE_URL = "postgres://test";
147
- (prisma.flowDefinition.upsert as jest.Mock).mockResolvedValue({ id: "flow_1" });
148
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValue({ id: "wlexp_1" });
149
- (prisma.projectListing.update as jest.Mock).mockResolvedValue({ id: "prjl_1" });
150
- });
151
-
152
- describe("generateListingFlow — the flow (#5001 AC1)", () => {
153
- // #5036 D6d: the write-back is the experiment's KEY, and the key IS the
154
- // listing's slug (`buildExperimentConfig`). Storing the id meant a reader
155
- // going from a listing to its `WaitlistScore` rows had to fetch the
156
- // experiment first purely to learn that key; storing the key is one hop and
157
- // the same string on both sides of the join.
158
- it("writes a servable flow, a live experiment, and the row's experimentKey", async () => {
159
- const result = await generateListingFlow(listing());
160
-
161
- expect(result).toEqual({
162
- ok: true,
163
- flowSlug: "orbit-notes",
164
- experimentKey: "orbit-notes",
165
- });
166
- expect(prisma.projectListing.update).toHaveBeenCalledWith({
167
- where: { id: "prjl_1" },
168
- data: { experimentKey: "orbit-notes" },
169
- });
170
- expect(
171
- (prisma.flowDefinition.upsert as jest.Mock).mock.calls[0]?.[0].where,
172
- ).toEqual({ siteId_slug: { siteId: SITE_ID, slug: "orbit-notes" } });
173
- expect(
174
- (prisma.waitlistExperiment.upsert as jest.Mock).mock.calls[0]?.[0].where,
175
- ).toEqual({ siteId_key: { siteId: SITE_ID, key: "orbit-notes" } });
176
- });
177
-
178
- it("declares the six-category step order, then the confirmation", async () => {
179
- await generateListingFlow(listing());
180
-
181
- expect(upsertedFlow().steps.map((s) => s.key)).toEqual([
182
- "contact",
183
- "segment",
184
- "problem",
185
- "fit",
186
- "urgency",
187
- "price",
188
- "open",
189
- "done",
190
- ]);
191
- });
192
-
193
- it("appends an extras step only when the row carries extras", async () => {
194
- const questions = projectListingQuestionsSchema.parse({
195
- ...TEMPLATE_DEFAULT,
196
- extras: [
197
- {
198
- prompt: "Which stack?",
199
- options: [
200
- { value: "a", label: "TypeScript" },
201
- { value: "b", label: "Python" },
202
- ],
203
- },
204
- ],
205
- });
206
-
207
- await generateListingFlow(listing({ questions }));
208
-
209
- const steps = upsertedFlow().steps;
210
- expect(steps.map((s) => s.key)).toEqual([
211
- "contact",
212
- "segment",
213
- "problem",
214
- "fit",
215
- "urgency",
216
- "price",
217
- "open",
218
- "extras",
219
- "done",
220
- ]);
221
- const extras = steps.find((s) => s.key === "extras");
222
- expect(extras?.type).toBe("field");
223
- if (extras?.type !== "field") throw new Error("extras step must be a field step");
224
- expect(extras.fields.map((f) => f.name)).toEqual(["extra_1"]);
225
- expect(extras.fields[0]?.options?.map((o) => o.value)).toEqual(["x1:0of2", "x1:1of2"]);
226
- });
227
-
228
- it("renders a scored question under the founder's OWN values, in order (#5036 D4)", async () => {
229
- await generateListingFlow(listing());
230
-
231
- const problem = upsertedFlow().steps.find((s) => s.key === "problem");
232
- if (problem?.type !== "field") throw new Error("problem step must be a field step");
233
- expect(problem.fields[0]?.options).toEqual([
234
- { value: "none", label: { en: "Not a problem" } },
235
- { value: "mild", label: { en: "Mildly annoying" } },
236
- { value: "real", label: { en: "A real drag" } },
237
- { value: "blocking", label: { en: "It's blocking me" } },
238
- ]);
239
- });
240
-
241
- it("keeps Q2's founder values and pins `just-curious` LAST", async () => {
242
- await generateListingFlow(listing());
243
-
244
- const segment = upsertedFlow().steps.find((s) => s.key === "segment");
245
- if (segment?.type !== "field") throw new Error("segment step must be a field step");
246
- const values = segment.fields[0]?.options?.map((o) => o.value) ?? [];
247
- expect(values).toEqual(["shipping-now", "planning", "just-curious"]);
248
- expect(values[values.length - 1]).toBe("just-curious");
249
- });
250
-
251
- it("makes Q7 an optional textarea", async () => {
252
- await generateListingFlow(listing());
253
-
254
- const open = upsertedFlow().steps.find((s) => s.key === "open");
255
- if (open?.type !== "field") throw new Error("open step must be a field step");
256
- expect(open.fields[0]?.inputType).toBe("textarea");
257
- expect(open.fields[0]?.required).toBeUndefined();
258
- });
259
-
260
- it("binds the flow to its experiment so completion scores", async () => {
261
- await generateListingFlow(listing());
262
-
263
- expect(upsertedFlow().experiment).toEqual({
264
- key: "orbit-notes",
265
- waitlist: "listing_orbit_notes",
266
- });
267
- });
268
-
269
- it("persists a config that validates against flowConfigSchema", async () => {
270
- await generateListingFlow(listing());
271
-
272
- expect(flowConfigSchema.safeParse(upsertedFlow()).success).toBe(true);
273
- });
274
- });
275
-
276
- describe("generateListingFlow — the thank-you (#5001 AC2)", () => {
277
- it("shows the confirmation line and the founder's own line, never the tier", async () => {
278
- await generateListingFlow(listing());
279
-
280
- const flow = upsertedFlow();
281
- const done = flow.steps[flow.steps.length - 1];
282
- expect(done?.type).toBe("message");
283
- expect(done?.copy).toEqual({
284
- title: { en: "You're on the Orbit Notes waitlist" },
285
- body: { en: "Meeting notes that file themselves." },
286
- });
287
-
288
- // No tier vocabulary anywhere on the visitor's path, and no thankYou /
289
- // deliverables block that could route one in.
290
- const serialized = JSON.stringify(flow);
291
- for (const tier of ["now", "soon", "later", "curious"]) {
292
- expect(serialized).not.toContain(`"${tier}"`);
293
- }
294
- expect(flow.thankYou).toBeUndefined();
295
- expect(flow.deliverables).toBeUndefined();
296
- expect(flow.scoring).toBeUndefined();
297
- });
298
-
299
- it("omits the body when the founder's line is blank", async () => {
300
- await generateListingFlow(listing({ oneLiner: " " }));
301
-
302
- const flow = upsertedFlow();
303
- const done = flow.steps[flow.steps.length - 1];
304
- expect(done?.copy).toEqual({ title: { en: "You're on the Orbit Notes waitlist" } });
305
- });
306
- });
307
-
308
- describe("generateListingFlow — the experiment (#5001 AC1/AC3)", () => {
309
- it("is a live, qualifying experiment keyed by the listing slug", async () => {
310
- await generateListingFlow(listing());
311
-
312
- const config = upsertedExperiment();
313
- expect(config.key).toBe("orbit-notes");
314
- expect(config.mode).toBe("qualifying");
315
- expect(config.status).toBe("live");
316
- expect(config.waitlists).toEqual([
317
- { key: "listing_orbit_notes", flowSlug: "orbit-notes" },
318
- ]);
319
- });
320
-
321
- it("carries the register the shared rule builds for THIS listing", async () => {
322
- await generateListingFlow(listing());
323
-
324
- // Deep, not identity: `upsertWaitlistExperiment` persists Zod's parsed
325
- // clone, so the register arrives as a structural copy of what
326
- // `buildListingDiagnosticScoring` returns for this row's questions.
327
- expect(upsertedExperiment().scoring).toEqual(
328
- buildListingDiagnosticScoring(TEMPLATE_DEFAULT),
329
- );
330
- });
331
-
332
- it("declares each scored question's OWN options in the question set (#5036 D4)", async () => {
333
- await generateListingFlow(listing());
334
-
335
- const questionSet = upsertedExperiment().questionSet as {
336
- questions: { id: string; options?: { value: string }[] }[];
337
- };
338
- const q3 = questionSet.questions.find((q) => q.id === "q3");
339
- expect(q3?.options?.map((o) => o.value)).toEqual(["none", "mild", "real", "blocking"]);
340
-
341
- // Which is what makes the weights keys legal at the config level — the
342
- // finite `q{n}:{i}of{k}` value universe the old shared constant forced
343
- // the question set to declare is gone.
344
- const scoring = upsertedExperiment().scoring as {
345
- points: { question: string; weights: Record<string, number> }[];
346
- };
347
- expect(scoring.points.find((p) => p.question === "q3")?.weights).toEqual({
348
- none: 0,
349
- mild: 1,
350
- real: 2,
351
- blocking: 3,
352
- });
353
- });
354
-
355
- it("persists a config that validates against waitlistExperimentConfigSchema", async () => {
356
- await generateListingFlow(listing());
357
-
358
- const parsed = waitlistExperimentConfigSchema.safeParse(upsertedExperiment());
359
- expect(parsed.success).toBe(true);
360
- });
361
-
362
- it("gives two DIFFERENT listings the same scoring RULE (AC3, #5036 D4)", async () => {
363
- await generateListingFlow(listing());
364
- const first = upsertedExperiment().scoring as Record<string, unknown>;
365
-
366
- jest.clearAllMocks();
367
- (prisma.flowDefinition.upsert as jest.Mock).mockResolvedValue({ id: "flow_2" });
368
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValue({ id: "wlexp_2" });
369
- (prisma.projectListing.update as jest.Mock).mockResolvedValue({ id: "prjl_2" });
370
-
371
- // A second listing with DIFFERENT wording AND a different Q3 arity.
372
- const otherQuestions = projectListingQuestionsSchema.parse({
373
- ...TEMPLATE_DEFAULT,
374
- q3: {
375
- prompt: "How badly does this hurt?",
376
- options: [
377
- { value: "x", label: "Not at all" },
378
- { value: "y", label: "Somewhat" },
379
- { value: "z", label: "Badly" },
380
- ],
381
- },
382
- });
383
- await generateListingFlow(
384
- listing({ id: "prjl_2", slug: "ledger-lite", name: "Ledger Lite", questions: otherQuestions }),
385
- );
386
-
387
- const second = upsertedExperiment().scoring as Record<string, unknown>;
388
-
389
- // Same shape, same ladder, same 0-12 range — only the founder-authored
390
- // Q3/Q4 weight KEYS differ, because since #5036 D4 those keys are the
391
- // founder's own option values rather than positional tokens.
392
- expect(second.scores).toEqual(first.scores);
393
- expect(second.tiers).toEqual(first.tiers);
394
- expect(second.default).toEqual(first.default);
395
- const names = (s: Record<string, unknown>) =>
396
- (s.points as { name: string }[]).map((p) => p.name);
397
- expect(names(second)).toEqual(names(first));
398
- const weights = (s: Record<string, unknown>, question: string) =>
399
- (s.points as { question: string; weights: Record<string, number> }[]).find(
400
- (p) => p.question === question,
401
- )?.weights;
402
- expect(weights(second, "q5")).toEqual(weights(first, "q5"));
403
- expect(weights(second, "q6")).toEqual(weights(first, "q6"));
404
- expect(weights(second, "q3")).toEqual({ x: 0, y: 2, z: 3 });
405
- expect(weights(second, "q3")).not.toEqual(weights(first, "q3"));
406
- });
407
- });
408
-
409
- describe("generateListingFlow — idempotency + failure", () => {
410
- it("re-approving upserts the same keys and rewrites the same experimentKey", async () => {
411
- await generateListingFlow(listing());
412
- await generateListingFlow(listing());
413
-
414
- const flowCalls = (prisma.flowDefinition.upsert as jest.Mock).mock.calls;
415
- const experimentCalls = (prisma.waitlistExperiment.upsert as jest.Mock).mock.calls;
416
- expect(flowCalls).toHaveLength(2);
417
- expect(flowCalls[0]?.[0]).toEqual(flowCalls[1]?.[0]);
418
- expect(experimentCalls[0]?.[0]).toEqual(experimentCalls[1]?.[0]);
419
- expect(prisma.projectListing.update).toHaveBeenNthCalledWith(2, {
420
- where: { id: "prjl_1" },
421
- data: { experimentKey: "orbit-notes" },
422
- });
423
- });
424
-
425
- it("refuses a slug the flow grammar rejects, and writes nothing", async () => {
426
- // A listing slug may carry a doubled hyphen (#4998's `^[a-z0-9-]{3,48}$`);
427
- // `flowSlugSchema` may not.
428
- const result = await generateListingFlow(listing({ slug: "orbit--notes" }));
429
-
430
- expect(result.ok).toBe(false);
431
- if (result.ok) throw new Error("expected a rejected slug");
432
- expect(result.error.kind).toBe("invalid_input");
433
- expect(prisma.waitlistExperiment.upsert).not.toHaveBeenCalled();
434
- expect(prisma.projectListing.update).not.toHaveBeenCalled();
435
- });
436
- });
437
-
438
- describe("listingWaitlistKey", () => {
439
- it("prefixes so a digit-leading slug still yields a legal identifier", () => {
440
- expect(listingWaitlistKey("3d-printer")).toBe("listing_3d_printer");
441
- });
442
- });
443
-
444
- describe("listingAnswerLabels — the CSV's inverse", () => {
445
- it("maps every emitted value back to the founder's own wording", () => {
446
- const labels = listingAnswerLabels(TEMPLATE_DEFAULT);
447
-
448
- // Q2-Q6 are all plain value->label now: the flow renders the founder's
449
- // own values, so the inverse is their own option list read backwards.
450
- expect(labels.q3?.blocking).toBe("It's blocking me");
451
- expect(labels.q5?.urgent).toBe("Right away");
452
- expect(labels.q6?.critical).toBe("Whatever it takes");
453
- expect(labels.q2?.["just-curious"]).toBe("Just curious");
454
- });
455
-
456
- it("keeps the positional encoding for the unscored extras", () => {
457
- const questions = projectListingQuestionsSchema.parse({
458
- ...TEMPLATE_DEFAULT,
459
- extras: [
460
- {
461
- prompt: "Which stack?",
462
- options: [
463
- { value: "a", label: "TypeScript" },
464
- { value: "b", label: "Python" },
465
- ],
466
- },
467
- ],
468
- });
469
-
470
- expect(listingAnswerLabels(questions).extra_1).toEqual({
471
- "x1:0of2": "TypeScript",
472
- "x1:1of2": "Python",
473
- });
474
- });
475
- });