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