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