create-ailk 0.2.0 → 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 (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. 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
- });