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,474 +0,0 @@
1
- /**
2
- * @file waitlist-scoring.test.ts
3
- * @description Unit tests for the waitlist-scoring glue service (#4662 D6).
4
- *
5
- * External dependency boundary mocked: @working-theory/database (auto-mock
6
- * via `../../lib/__mocks__/prisma.ts`, mirroring waitlist-experiments.test.ts).
7
- * `@working-theory/observability`'s logger is mocked to a spyable stub
8
- * (mirrors deliverable-fulfillment.test.ts / abandon-email.test.ts) so the
9
- * PII-safety of the error logs is directly assertable.
10
- *
11
- * `@working-theory/validation`'s `evaluateWaitlistScoring` is a PARTIAL mock
12
- * (every other export runs real, incl. `waitlistExperimentConfigSchema` and
13
- * `answersFromPayload`) — every one of the pure evaluator's own defensive
14
- * throws (unresolvable ref/score/sum term, a ref cycle) is on a register
15
- * shape `waitlistExperimentConfigSchema`'s own superRefine already rejects
16
- * (waitlist-scoring.test.ts, packages/validation, covers those exhaustively
17
- * against a register that bypasses the schema entirely). So the ONLY way to
18
- * observe this glue's `evaluation_failed` outcome against a schema-VALID
19
- * config is to make the (already schema-validated, already unit-tested)
20
- * evaluator itself throw for one call — not to hand-craft an "invalid but
21
- * schema-valid" register, which cannot exist by construction.
22
- */
23
- // 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)
24
- jest.mock("../../lib/prisma.js");
25
-
26
- const mockLoggerError = jest.fn();
27
- jest.mock("@working-theory/observability", () => ({
28
- logger: {
29
- info: jest.fn(),
30
- warn: jest.fn(),
31
- error: (...args: unknown[]) => mockLoggerError(...args),
32
- debug: jest.fn(),
33
- },
34
- }));
35
-
36
- jest.mock("@working-theory/validation", () => {
37
- const actual = jest.requireActual("@working-theory/validation");
38
- return { ...actual, evaluateWaitlistScoring: jest.fn(actual.evaluateWaitlistScoring) };
39
- });
40
-
41
- import { evaluateWaitlistScoring } from "@working-theory/validation";
42
-
43
- import { prisma } from "../../lib/prisma.js";
44
- import { scoreCompletedSignup } from "../waitlist-scoring";
45
-
46
- const en = (s: string) => ({ en: s });
47
- const SITE_ID = "site-abc";
48
- const LEAD_ID = "lead-1";
49
- const EXPERIMENT_KEY = "my-experiment";
50
- const WAITLIST_KEY = "main";
51
-
52
- beforeEach(() => {
53
- jest.clearAllMocks();
54
- });
55
-
56
- function noScoringConfig() {
57
- return {
58
- key: EXPERIMENT_KEY,
59
- mode: "lead-capture" as const,
60
- status: "draft" as const,
61
- waitlists: [{ key: WAITLIST_KEY, flowSlug: EXPERIMENT_KEY }],
62
- questionSet: {
63
- version: 1 as const,
64
- questions: [
65
- {
66
- id: "email",
67
- prompt: en("Your email"),
68
- answerType: "email" as const,
69
- mapsTo: "email" as const,
70
- },
71
- ],
72
- },
73
- tracking: {
74
- version: 1 as const,
75
- utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
76
- waitlists: { [WAITLIST_KEY]: { campaign: "launch", channels: ["organic"] } },
77
- },
78
- stripeConfigRef: null,
79
- };
80
- }
81
-
82
- function validQualifyingConfig() {
83
- return {
84
- key: EXPERIMENT_KEY,
85
- mode: "qualifying" as const,
86
- status: "draft" as const,
87
- waitlists: [{ key: WAITLIST_KEY, flowSlug: EXPERIMENT_KEY }],
88
- questionSet: {
89
- version: 1 as const,
90
- questions: [
91
- {
92
- id: "email",
93
- prompt: en("Your email"),
94
- answerType: "email" as const,
95
- mapsTo: "email" as const,
96
- },
97
- {
98
- id: "budget",
99
- prompt: en("What's your budget?"),
100
- answerType: "single" as const,
101
- options: [
102
- { value: "small", label: en("Small") },
103
- { value: "large", label: en("Large") },
104
- ],
105
- },
106
- ],
107
- },
108
- scoring: {
109
- version: 1 as const,
110
- points: [
111
- {
112
- name: "budget_points",
113
- question: "budget",
114
- kind: "optionWeights" as const,
115
- weights: { small: 1, large: 10 },
116
- },
117
- ],
118
- scores: { total: { sum: ["budget_points"] } },
119
- tiers: [{ key: "hot", when: { score: "total", gte: 5 } }],
120
- default: "hot",
121
- },
122
- tracking: {
123
- version: 1 as const,
124
- utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
125
- waitlists: { [WAITLIST_KEY]: { campaign: "launch", channels: ["organic"] } },
126
- },
127
- stripeConfigRef: null,
128
- };
129
- }
130
-
131
- describe("scoreCompletedSignup (D6)", () => {
132
- it("no experiment row ⇒ skipped: no_experiment, no upsert", async () => {
133
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue(null);
134
-
135
- const result = await scoreCompletedSignup({
136
- siteId: SITE_ID,
137
- leadId: LEAD_ID,
138
- experimentKey: EXPERIMENT_KEY,
139
- waitlistKey: WAITLIST_KEY,
140
- payload: {},
141
- });
142
-
143
- expect(result).toEqual({ ok: true, skipped: "no_experiment" });
144
- expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
145
- });
146
-
147
- it("a config with no scoring block ⇒ skipped: no_scoring, no upsert (a lead-capture experiment)", async () => {
148
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
149
- config: noScoringConfig(),
150
- });
151
-
152
- const result = await scoreCompletedSignup({
153
- siteId: SITE_ID,
154
- leadId: LEAD_ID,
155
- experimentKey: EXPERIMENT_KEY,
156
- waitlistKey: WAITLIST_KEY,
157
- payload: {},
158
- });
159
-
160
- expect(result).toEqual({ ok: true, skipped: "no_scoring" });
161
- expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
162
- });
163
-
164
- it("an invalid stored config (defensive — the row was validated on write) ⇒ invalid_config, no upsert", async () => {
165
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
166
- config: { not: "a config" },
167
- });
168
-
169
- const result = await scoreCompletedSignup({
170
- siteId: SITE_ID,
171
- leadId: LEAD_ID,
172
- experimentKey: EXPERIMENT_KEY,
173
- waitlistKey: WAITLIST_KEY,
174
- payload: {},
175
- });
176
-
177
- expect(result).toEqual({ ok: false, error: { kind: "invalid_config" } });
178
- expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
179
- });
180
-
181
- it("a valid qualifying config ⇒ exactly one upsert on {leadId, experimentKey} carrying the evaluator's output", async () => {
182
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
183
- config: validQualifyingConfig(),
184
- });
185
- (prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
186
-
187
- const result = await scoreCompletedSignup({
188
- siteId: SITE_ID,
189
- leadId: LEAD_ID,
190
- experimentKey: EXPERIMENT_KEY,
191
- waitlistKey: WAITLIST_KEY,
192
- payload: { budget: "large" },
193
- });
194
-
195
- expect(result).toEqual({ ok: true, tier: "hot" });
196
- expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(1);
197
- expect(prisma.waitlistScore.upsert).toHaveBeenCalledWith(
198
- expect.objectContaining({
199
- // The composite key since #5036 D6d — `{ leadId }` alone scoped one
200
- // score per LEAD, which silently erased a founder's evidence the
201
- // moment a visitor joined a second listing's waitlist.
202
- where: { leadId_experimentKey: { leadId: LEAD_ID, experimentKey: EXPERIMENT_KEY } },
203
- create: expect.objectContaining({
204
- siteId: SITE_ID,
205
- leadId: LEAD_ID,
206
- experimentKey: EXPERIMENT_KEY,
207
- waitlistKey: WAITLIST_KEY,
208
- tier: "hot",
209
- score: 10,
210
- categoryScores: { total: 10 },
211
- }),
212
- update: expect.objectContaining({
213
- tier: "hot",
214
- score: 10,
215
- categoryScores: { total: 10 },
216
- }),
217
- }),
218
- );
219
- });
220
-
221
- it("a second call on the same lead re-scores via the same upsert call — update, never a second row", async () => {
222
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
223
- config: validQualifyingConfig(),
224
- });
225
- (prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
226
-
227
- await scoreCompletedSignup({
228
- siteId: SITE_ID,
229
- leadId: LEAD_ID,
230
- experimentKey: EXPERIMENT_KEY,
231
- waitlistKey: WAITLIST_KEY,
232
- payload: { budget: "large" },
233
- });
234
- await scoreCompletedSignup({
235
- siteId: SITE_ID,
236
- leadId: LEAD_ID,
237
- experimentKey: EXPERIMENT_KEY,
238
- waitlistKey: WAITLIST_KEY,
239
- payload: { budget: "small" },
240
- });
241
-
242
- expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(2);
243
- const calls = (prisma.waitlistScore.upsert as jest.Mock).mock.calls;
244
- // Same lead, same experiment ⇒ the SAME composite key both times, so the
245
- // second write lands as an update. Re-scoring overwriting is the intended
246
- // behaviour; #5036 D6d only scoped it per experiment, it did not remove it.
247
- const key = { leadId_experimentKey: { leadId: LEAD_ID, experimentKey: EXPERIMENT_KEY } };
248
- expect(calls[0]![0]).toMatchObject({ where: key });
249
- expect(calls[1]![0]).toMatchObject({ where: key });
250
- });
251
-
252
- // ── #5036 D6d — one lead, two experiments, two surviving rows ───────────
253
-
254
- /**
255
- * The correctness defect D6d straightened, asserted from the outside.
256
- *
257
- * `WaitlistScore` carried `@@unique([leadId])` — "one row per lead". Right
258
- * for a single instrument, wrong for a marketplace of them: a visitor who
259
- * joins two founders' waitlists is one lead with TWO scores, and under
260
- * `where: { leadId }` the second upsert resolved the SAME row as the first
261
- * and overwrote it. One founder's evidence vanished into a CSV that
262
- * reported fewer signups than actually happened, with no error raised
263
- * anywhere.
264
- *
265
- * What makes the two rows survive is that the two writes address DIFFERENT
266
- * keys, so this test asserts exactly that and nothing about Prisma's own
267
- * upsert semantics.
268
- */
269
- it("scores one lead against two experiments — the two upserts carry DIFFERENT keys, so neither can overwrite the other", async () => {
270
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
271
- config: validQualifyingConfig(),
272
- });
273
- (prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
274
-
275
- for (const experimentKey of ["exp-a", "exp-b"]) {
276
- await scoreCompletedSignup({
277
- siteId: SITE_ID,
278
- leadId: LEAD_ID,
279
- experimentKey,
280
- waitlistKey: WAITLIST_KEY,
281
- payload: { budget: "large" },
282
- });
283
- }
284
-
285
- expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(2);
286
- const [first, second] = (prisma.waitlistScore.upsert as jest.Mock).mock.calls.map(
287
- (call) => call[0] as { where: unknown },
288
- );
289
-
290
- expect(first!.where).toEqual({
291
- leadId_experimentKey: { leadId: LEAD_ID, experimentKey: "exp-a" },
292
- });
293
- expect(second!.where).toEqual({
294
- leadId_experimentKey: { leadId: LEAD_ID, experimentKey: "exp-b" },
295
- });
296
- // The load-bearing assertion: two distinct addresses, so the second write
297
- // cannot resolve the first row. Under the old `where: { leadId }` both of
298
- // these were `{ leadId: "lead-1" }` and this comparison would be equal.
299
- expect(first!.where).not.toEqual(second!.where);
300
- });
301
-
302
- it("an evaluator throw ⇒ evaluation_failed, no upsert", async () => {
303
- // The evaluator's own defensive throws (WaitlistEvaluationError, for an
304
- // unresolvable ref/score/sum term or a ref cycle) are exhaustively
305
- // covered against a register that bypasses the schema, in
306
- // packages/validation/src/schemas/__tests__/waitlist-scoring.test.ts — a
307
- // schema-VALID config cannot reach any of them (waitlistExperimentConfigSchema's
308
- // own superRefine already rejects every such shape). So this glue-level
309
- // test forces the (real, unit-tested) evaluator to throw for one call,
310
- // to prove the glue's own catch → `evaluation_failed` → no upsert path.
311
- (evaluateWaitlistScoring as jest.Mock).mockImplementationOnce(() => {
312
- throw new Error("boom");
313
- });
314
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
315
- config: validQualifyingConfig(),
316
- });
317
-
318
- const result = await scoreCompletedSignup({
319
- siteId: SITE_ID,
320
- leadId: LEAD_ID,
321
- experimentKey: EXPERIMENT_KEY,
322
- waitlistKey: WAITLIST_KEY,
323
- payload: { budget: "large" },
324
- });
325
-
326
- expect(result).toEqual({ ok: false, error: { kind: "evaluation_failed" } });
327
- expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
328
- });
329
-
330
- it("a persist (Prisma) failure ⇒ persist_failed", async () => {
331
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
332
- config: validQualifyingConfig(),
333
- });
334
- (prisma.waitlistScore.upsert as jest.Mock).mockRejectedValue(new Error("db down"));
335
-
336
- const result = await scoreCompletedSignup({
337
- siteId: SITE_ID,
338
- leadId: LEAD_ID,
339
- experimentKey: EXPERIMENT_KEY,
340
- waitlistKey: WAITLIST_KEY,
341
- payload: { budget: "large" },
342
- });
343
-
344
- expect(result).toEqual({ ok: false, error: { kind: "persist_failed" } });
345
- });
346
-
347
- it("the log line carries no answer value — PII-safe outcome logs only", async () => {
348
- (evaluateWaitlistScoring as jest.Mock).mockImplementationOnce(() => {
349
- throw new Error("boom");
350
- });
351
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
352
- config: validQualifyingConfig(),
353
- });
354
-
355
- await scoreCompletedSignup({
356
- siteId: SITE_ID,
357
- leadId: LEAD_ID,
358
- experimentKey: EXPERIMENT_KEY,
359
- waitlistKey: WAITLIST_KEY,
360
- payload: { budget: "large" },
361
- });
362
-
363
- expect(mockLoggerError).toHaveBeenCalledWith(
364
- expect.objectContaining({
365
- siteId: SITE_ID,
366
- experimentKey: EXPERIMENT_KEY,
367
- leadId: LEAD_ID,
368
- kind: "evaluation_failed",
369
- }),
370
- expect.any(String),
371
- );
372
- const [logMeta] = mockLoggerError.mock.calls[0]!;
373
- expect(JSON.stringify(logMeta)).not.toContain("large");
374
- expect(JSON.stringify(logMeta)).not.toContain("budget");
375
- });
376
- });
377
-
378
- describe("scoreCompletedSignup — FEAT-074 U4a playbook compile hook (#4715)", () => {
379
- // Exercises the REAL compileAndStorePlaybook (no jest.mock of the internal
380
- // module — CONTRIBUTING.md § Test code quality / #507 forbids mocking an
381
- // internal module; the real boundary is prisma + observability, both
382
- // already mocked above). The tier must be a real `level_N` the VENDORED
383
- // library declares (Level 1) so the real compile succeeds — this mirrors
384
- // validQualifyingConfig()'s own register shape, only its tiers/default
385
- // differ, so it stays a minimal diff from the existing fixture.
386
- const TARGETING_MATURITY_KEY = "targeting-maturity";
387
-
388
- function targetingMaturityConfig() {
389
- const base = validQualifyingConfig();
390
- return {
391
- ...base,
392
- key: TARGETING_MATURITY_KEY,
393
- scoring: {
394
- ...base.scoring,
395
- tiers: [{ key: "level_1", when: { score: "total", gte: 0 } }],
396
- default: "level_1",
397
- },
398
- };
399
- }
400
-
401
- it("experimentKey === targeting-maturity ⇒ a PlaybookInstance row is created from the upserted score's id and tier (AC-3)", async () => {
402
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
403
- config: targetingMaturityConfig(),
404
- });
405
- (prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_playbook_1" });
406
- (prisma.playbookInstance.create as jest.Mock).mockResolvedValue({ id: "pbk_1" });
407
-
408
- const result = await scoreCompletedSignup({
409
- siteId: SITE_ID,
410
- leadId: LEAD_ID,
411
- experimentKey: TARGETING_MATURITY_KEY,
412
- waitlistKey: WAITLIST_KEY,
413
- payload: { budget: "large" },
414
- });
415
-
416
- expect(result).toEqual({ ok: true, tier: "level_1" });
417
- expect(prisma.playbookInstance.create).toHaveBeenCalledTimes(1);
418
- const [call] = (prisma.playbookInstance.create as jest.Mock).mock.calls[0]!;
419
- expect(call.data).toMatchObject({
420
- siteId: SITE_ID,
421
- leadId: LEAD_ID,
422
- experimentKey: TARGETING_MATURITY_KEY,
423
- scoreId: "wlscore_playbook_1",
424
- tier: "level_1",
425
- level: 1,
426
- });
427
- });
428
-
429
- it("any other experimentKey ⇒ no PlaybookInstance row is created", async () => {
430
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
431
- config: validQualifyingConfig(),
432
- });
433
- (prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
434
-
435
- const result = await scoreCompletedSignup({
436
- siteId: SITE_ID,
437
- leadId: LEAD_ID,
438
- experimentKey: EXPERIMENT_KEY,
439
- waitlistKey: WAITLIST_KEY,
440
- payload: { budget: "large" },
441
- });
442
-
443
- expect(result).toEqual({ ok: true, tier: "hot" });
444
- expect(prisma.playbookInstance.create).not.toHaveBeenCalled();
445
- });
446
-
447
- it("a compile/store failure leaves the score intact — scoreCompletedSignup still resolves ok:true, never throws to its caller (AC-4)", async () => {
448
- (prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
449
- config: targetingMaturityConfig(),
450
- });
451
- (prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_playbook_2" });
452
- (prisma.playbookInstance.create as jest.Mock).mockRejectedValueOnce(new Error("db down"));
453
-
454
- const result = await scoreCompletedSignup({
455
- siteId: SITE_ID,
456
- leadId: LEAD_ID,
457
- experimentKey: TARGETING_MATURITY_KEY,
458
- waitlistKey: WAITLIST_KEY,
459
- payload: { budget: "large" },
460
- });
461
-
462
- expect(result).toEqual({ ok: true, tier: "level_1" });
463
- expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(1);
464
- expect(mockLoggerError).toHaveBeenCalledWith(
465
- expect.objectContaining({
466
- siteId: SITE_ID,
467
- experimentKey: TARGETING_MATURITY_KEY,
468
- leadId: LEAD_ID,
469
- kind: "playbook_compile_failed",
470
- }),
471
- expect.any(String),
472
- );
473
- });
474
- });