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,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
- });